Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions sdk/compat/parser-differential/.gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
rustdriver/target/
rustdriver/Cargo.lock
godriver/pdiff-go
results.json
63 changes: 63 additions & 0 deletions sdk/compat/parser-differential/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,63 @@
<!--
SPDX-FileCopyrightText: © 2026 Phala Network <dstack@phala.network>

SPDX-License-Identifier: Apache-2.0
-->

# Cross-language parser differential harness

Feeds one canned guest-agent response to the Rust, Python, Go and JavaScript
SDKs and tabulates what each does with it. Four implementations of one protocol
is the shape that produces parser differentials, and the only way to find them
is to run the same bytes through all four.

This is **not** part of `sdk/run-tests.sh`. It needs all four toolchains and it
answers a question — "do these agree?" — rather than asserting a fixed
expectation. Run it by hand when the wire format changes or when one SDK's
decoding is touched.

```
./run.sh
```

Findings from the first run are written up in
`.agent/PARSER-INVENTORY-sdk.md`.

## How it works

`server.py` listens on a unix socket and answers every POST with one entry from
`cases.json`, chosen by a preceding `POST /__case/<name>`. The four drivers each
select a case, call the SDK method under test, and print one line:

```
OK|<a summary of what was decoded>
ERR|<exception type>: <message>
PANIC|<...> # Go only; the others cannot
CRASH(rc=N)|<...> # the process died
```

`run.py` walks the cross product and writes `results.json`.

## Adding a case

Append to `cases.json`:

```json
"A99_my_case": {
"method": "GetKey", // GetKey | Info | Attest | v0GetKey | v0Info | v0TlsKey
"status": 200,
"body": "{\"key\": ...}" // verbatim, so malformed JSON and duplicate keys are expressible
}
```

`body` is a **string**, not an object, on purpose: duplicate keys, trailing
commas and truncated documents all have to survive to the wire.

## Why not the simulator

`sdk/simulator` is the real guest agent with one trait swapped
(`dstack/guest-agent/src/backend.rs`), which is exactly what makes it
trustworthy — a test that passes against it cannot be rejected by a real agent
on validation grounds. Giving it a fault-injection mode would put test-only
branches inside the production handler and cost that property. So malformed
responses come from here instead.
297 changes: 297 additions & 0 deletions sdk/compat/parser-differential/cases.json

Large diffs are not rendered by default.

53 changes: 53 additions & 0 deletions sdk/compat/parser-differential/driver.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
// SPDX-FileCopyrightText: © 2026 Phala Network <dstack@phala.network>
//
// SPDX-License-Identifier: Apache-2.0

const net = require('net')
const { DstackClientV1, DstackClientV0 } = require(require('path').join(__dirname, '..', '..', 'js', 'dist', 'index.js'))

const [sock, name, method] = process.argv.slice(2)

function select() {
return new Promise((res, rej) => {
const c = net.connect(sock, () => {
c.write(`POST /__case/${name} HTTP/1.1\r\nHost: x\r\nContent-Length: 0\r\n\r\n`)
})
c.on('data', () => { c.end(); res() })
c.on('error', rej)
})
}

const t = f => { try { return f() } catch (e) { return '<'+e.constructor.name+'>' } }
const hexClip = (u8, n) => Buffer.from(u8).toString('hex').slice(0, n)

;(async () => {
await select()
const c = method.startsWith('v0') ? new DstackClientV0(sock) : new DstackClientV1(sock)
try {
let out
if (method === 'GetKey') {
const r = await c.getKey('d', 'secp256k1')
out = `key=${hexClip(r.key,16)} chain=${r.signature_chain.length}`
} else if (method === 'Info') {
const r = await c.info()
out = `app_id=${hexClip(r.app_id,16)} app_name=${JSON.stringify(r.app_name)} os_image_hash=${hexClip(r.os_image_hash,16)}`
} else if (method === 'v0GetKey') {
const r = await c.getKey('d')
out = `key=${hexClip(r.key,16)} keylen=${r.key.length} chain=${r.signature_chain.length}`
} else if (method === 'v0Info') {
const r = await c.info()
out = `app_id=${String(r.app_id).slice(0,16)} tcb=${typeof r.tcb_info}`
} else if (method === 'v0TlsKey') {
const r = await c.getTlsKey()
out = `as32=${t(()=>hexClip(r.asUint8Array(32),64))} full=${t(()=>String(r.asUint8Array().length))}`
} else if (method === 'Attest') {
const r = await c.attest(Buffer.alloc(32, 1), false)
out = `attestation=${r.attestation.length}B gpu=${r.boottime_gpu_evidence.length}`
}
// prototype pollution probe
const polluted = ({}).polluted !== undefined ? ' [PROTO-POLLUTED]' : ''
console.log(`OK|${out}${polluted}`)
} catch (e) {
console.log(`ERR|${e.constructor.name}: ${String(e.message).replace(/\n/g,' ').slice(0,160)}`)
}
})().catch(e => console.log(`ERR|fatal ${e}`))
40 changes: 40 additions & 0 deletions sdk/compat/parser-differential/driver.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
# SPDX-FileCopyrightText: © 2026 Phala Network <dstack@phala.network>
#
# SPDX-License-Identifier: Apache-2.0

import json, os, socket, sys, warnings
warnings.simplefilter("ignore")
sys.path.insert(0, os.path.join(os.path.dirname(os.path.abspath(__file__)), "..", "..", "python", "src"))
sock, case, method = sys.argv[1], sys.argv[2], sys.argv[3]

def select(case):
s = socket.socket(socket.AF_UNIX, socket.SOCK_STREAM); s.connect(sock)
s.sendall(("POST /__case/%s HTTP/1.1\r\nHost: x\r\nContent-Length: 0\r\n\r\n" % case).encode())
s.recv(4096); s.close()

def t(f):
try:
v = f()
return v.hex() if isinstance(v, (bytes, bytearray)) else str(v)
except BaseException as e:
return "<%s>" % type(e).__name__

select(case)
from dstack_sdk import DstackClientV1, DstackClientV0
c = DstackClientV0(sock) if method.startswith("v0") else DstackClientV1(sock)
try:
if method == "GetKey":
r = c.get_key("d", "secp256k1"); out = "key=%s chain=%d" % (r.key.hex()[:16], len(r.signature_chain))
elif method == "Info":
r = c.info(); out = "app_id=%s app_name=%r os_image_hash=%s" % (r.app_id.hex()[:16], r.app_name, r.os_image_hash.hex()[:16])
elif method == "Attest":
r = c.attest(b"\x01"*32); out = "attestation=%dB gpu=%d" % (len(r.attestation), len(r.boottime_gpu_evidence))
elif method == "v0GetKey":
r = c.get_key("d"); out = "key=%s chain=%d decode=%s" % (r.key[:16], len(r.signature_chain), t(r.decode_key))
elif method == "v0Info":
r = c.info(); out = "app_id=%s tcb=%s" % (r.app_id[:16], type(r.tcb_info).__name__)
elif method == "v0TlsKey":
r = c.get_tls_key(); out = "as32=%s fulllen=%s" % (t(lambda: r.as_uint8array(32)), t(lambda: len(r.as_uint8array())))
print("OK|%s" % out)
except BaseException as e:
print("ERR|%s: %s" % (type(e).__name__, str(e).replace("\n", " ")[:160]))
20 changes: 20 additions & 0 deletions sdk/compat/parser-differential/godriver/go.mod
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
// SPDX-FileCopyrightText: © 2026 Phala Network <dstack@phala.network>
//
// SPDX-License-Identifier: Apache-2.0

module pdiff

go 1.25.0

require github.com/Dstack-TEE/dstack/sdk/go v0.0.0

require (
github.com/ProjectZKM/Ziren/crates/go-runtime/zkvm_runtime v0.0.0-20251001021608-1fe7b43fc4d6 // indirect
github.com/decred/dcrd/dcrec/secp256k1/v4 v4.0.1 // indirect
github.com/ethereum/go-ethereum v1.17.4 // indirect
github.com/holiman/uint256 v1.3.2 // indirect
golang.org/x/crypto v0.52.0 // indirect
golang.org/x/sys v0.45.0 // indirect
)

replace github.com/Dstack-TEE/dstack/sdk/go => ../../../go
14 changes: 14 additions & 0 deletions sdk/compat/parser-differential/godriver/go.sum
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
github.com/ProjectZKM/Ziren/crates/go-runtime/zkvm_runtime v0.0.0-20251001021608-1fe7b43fc4d6 h1:1zYrtlhrZ6/b6SAjLSfKzWtdgqK0U+HtH/VcBWh1BaU=
github.com/ProjectZKM/Ziren/crates/go-runtime/zkvm_runtime v0.0.0-20251001021608-1fe7b43fc4d6/go.mod h1:ioLG6R+5bUSO1oeGSDxOV3FADARuMoytZCSX6MEMQkI=
github.com/decred/dcrd/crypto/blake256 v1.0.0 h1:/8DMNYp9SGi5f0w7uCm6d6M4OU2rGFK09Y2A4Xv7EE0=
github.com/decred/dcrd/crypto/blake256 v1.0.0/go.mod h1:sQl2p6Y26YV+ZOcSTP6thNdn47hh8kt6rqSlvmrXFAc=
github.com/decred/dcrd/dcrec/secp256k1/v4 v4.0.1 h1:YLtO71vCjJRCBcrPMtQ9nqBsqpA1m5sE92cU+pd5Mcc=
github.com/decred/dcrd/dcrec/secp256k1/v4 v4.0.1/go.mod h1:hyedUtir6IdtD/7lIxGeCxkaw7y45JueMRL4DIyJDKs=
github.com/ethereum/go-ethereum v1.17.4 h1:uA4q+qiLp7QImBsjdRbINu8iX6OEVmj4DPc5/E5Fsxc=
github.com/ethereum/go-ethereum v1.17.4/go.mod h1:qMdgwqqRAen+aT8P7KKQKi0Qt6RzG4cfejVAbCpJgqA=
github.com/holiman/uint256 v1.3.2 h1:a9EgMPSC1AAaj1SZL5zIQD3WbwTuHrMGOerLjGmM/TA=
github.com/holiman/uint256 v1.3.2/go.mod h1:EOMSn4q6Nyt9P6efbI3bueV4e1b3dGlUCXeiRV4ng7E=
golang.org/x/crypto v0.52.0 h1:RMs7fP2rXdep0CftQlK8Uf+kibLm7qkCcradZWYz988=
golang.org/x/crypto v0.52.0/go.mod h1:1QgfPxDqh0T2M/elOJtp9RvuR95kVjir0e6/BvEmGbc=
golang.org/x/sys v0.45.0 h1:dO4czNzziLiiXplLQgBCEpCvXQ3dnkn0SdaZSYdQ+FY=
golang.org/x/sys v0.45.0/go.mod h1:4GL1E5IUh+htKOUEOaiffhrAeqysfVGipDYzABqnCmw=
125 changes: 125 additions & 0 deletions sdk/compat/parser-differential/godriver/main.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,125 @@
// SPDX-FileCopyrightText: © 2026 Phala Network <dstack@phala.network>
//
// SPDX-License-Identifier: Apache-2.0

package main

import (
"context"
"encoding/hex"
"fmt"
"net"
"os"
"strings"

"github.com/Dstack-TEE/dstack/sdk/go/dstack"
)

func selectCase(sock, name string) {
c, err := net.Dial("unix", sock)
if err != nil {
panic(err)
}
fmt.Fprintf(c, "POST /__case/%s HTTP/1.1\r\nHost: x\r\nContent-Length: 0\r\n\r\n", name)
buf := make([]byte, 4096)
c.Read(buf)
c.Close()
}

func clip(b []byte, n int) string {
s := hex.EncodeToString(b)
if len(s) > n {
return s[:n]
}
return s
}

func main() {
sock, name, method := os.Args[1], os.Args[2], os.Args[3]
selectCase(sock, name)
defer func() {
if r := recover(); r != nil {
fmt.Printf("PANIC|%v\n", r)
}
}()
ctx := context.Background()
var out string
var err error
if strings.HasPrefix(method, "v0") {
c0 := dstack.NewDstackClientV0(dstack.WithEndpoint(sock))
switch method {
case "v0GetKey":
r, e := c0.GetKey(ctx, "d", "", "")
err = e
if e == nil {
k, de := r.DecodeKey()
out = fmt.Sprintf("key=%.16s chain=%d decode=%s", r.Key, len(r.SignatureChain), tb(k, de))
}
case "v0Info":
r, e := c0.Info(ctx)
err = e
if e == nil {
ti, te := r.DecodeTcbInfo()
if te == nil { out = fmt.Sprintf("app_id=%.16s tcb=ok mrtd=%.8s", r.AppID, ti.Mrtd) } else { out = fmt.Sprintf("app_id=%.16s tcb=<%v>", r.AppID, te) }
}
case "v0TlsKey":
r, e := c0.GetTlsKey(ctx)
err = e
if e == nil {
b32, e1 := r.AsUint8Array(32)
bf, e2 := r.AsUint8Array()
out = fmt.Sprintf("as32=%s fulllen=%s", tb(b32, e1), tb2(len(bf), e2))
}
}
report(out, err)
return
}
c := dstack.NewDstackClientV1(dstack.WithEndpoint(sock))
switch method {
case "GetKey":
r, e := c.GetKey(ctx, "d", "secp256k1")
err = e
if e == nil {
out = fmt.Sprintf("key=%s chain=%d", clip(r.Key, 16), len(r.SignatureChain))
}
case "Info":
r, e := c.Info(ctx)
err = e
if e == nil {
out = fmt.Sprintf("app_id=%s app_name=%q os_image_hash=%s", clip(r.AppID, 16), r.AppName, clip(r.OsImageHash, 16))
}
case "Attest":
r, e := c.Attest(ctx, make([]byte, 32), false)
err = e
if e == nil {
out = fmt.Sprintf("attestation=%dB gpu=%d", len(r.Attestation), len(r.BoottimeGpuEvidence))
}
}
report(out, err)
}

func report(out string, err error) {
if err != nil {
m := strings.ReplaceAll(err.Error(), "\n", " ")
if len(m) > 160 {
m = m[:160]
}
fmt.Printf("ERR|%s\n", m)
return
}
fmt.Printf("OK|%s\n", out)
}

func tb(b []byte, err error) string {
if err != nil {
return "<" + err.Error() + ">"
}
return hex.EncodeToString(b)
}

func tb2(v interface{}, err error) string {
if err != nil {
return "<" + err.Error() + ">"
}
return fmt.Sprintf("%v", v)
}
51 changes: 51 additions & 0 deletions sdk/compat/parser-differential/run.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
#!/usr/bin/env python3
# SPDX-FileCopyrightText: © 2026 Phala Network <dstack@phala.network>
#
# SPDX-License-Identifier: Apache-2.0

import json, os, subprocess, sys, tempfile

HERE = os.path.dirname(os.path.abspath(__file__))
SDK = os.path.abspath(os.path.join(HERE, "..", ".."))
SOCK = os.path.join(tempfile.mkdtemp(prefix="pdiff-"), "agent.sock")
CASES = json.load(open(os.path.join(HERE, "cases.json")))

srv = subprocess.Popen([sys.executable, os.path.join(HERE, "server.py"), SOCK, os.path.join(HERE, "cases.json")],
stdout=subprocess.PIPE, stderr=subprocess.DEVNULL, text=True)
assert srv.stdout.readline().strip() == "READY"

PYTHON = os.environ.get("PDIFF_PYTHON", os.path.join(SDK, "python", ".venv", "bin", "python"))

RUNNERS = {
"rust": lambda c, m: [os.path.join(HERE, "rustdriver/target/debug/pdiff-rust"), SOCK, c, m],
"python": lambda c, m: [PYTHON, os.path.join(HERE, "driver.py"), SOCK, c, m],
"go": lambda c, m: [os.path.join(HERE, "godriver/pdiff-go"), SOCK, c, m],
"js": lambda c, m: ["node", os.path.join(HERE, "driver.js"), SOCK, c, m],
}

results = {}
for name in sorted(CASES):
method = CASES[name]["method"]
row = {}
for lang, mk in RUNNERS.items():
try:
p = subprocess.run(mk(name, method), capture_output=True, text=True, timeout=90)
line = (p.stdout.strip().splitlines() or [""])[-1]
if p.returncode != 0:
tail = (p.stderr.strip().splitlines() or [""])
sig = ""
for l in tail:
if "panic" in l.lower() or "Traceback" in l or "Error" in l:
sig = l.strip(); break
if not sig and tail: sig = tail[-1].strip()
line = f"CRASH(rc={p.returncode})|{sig[:150]}"
except subprocess.TimeoutExpired:
line = "TIMEOUT|"
row[lang] = line or "EMPTY|"
results[name] = row
print(f"--- {name} [{method}]")
for lang in RUNNERS:
print(f" {lang:7s} {row[lang]}")

json.dump(results, open(os.path.join(HERE, "results.json"), "w"), indent=1)
srv.kill()
15 changes: 15 additions & 0 deletions sdk/compat/parser-differential/run.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
#!/bin/bash
# SPDX-FileCopyrightText: © 2026 Phala Network <dstack@phala.network>
#
# SPDX-License-Identifier: Apache-2.0
#
# Build the four drivers and feed every case in `cases.json` to all of them.
set -euo pipefail
HERE="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd -P)"
SDK="$(cd "$HERE/../.." && pwd -P)"

(cd "$HERE/rustdriver" && cargo build)
(cd "$HERE/godriver" && go build -o pdiff-go .)
(cd "$SDK/js" && npm run --silent build)

exec python3 "$HERE/run.py"
Loading
Loading