The operation identifiers remain
LAND_AUCTION/{CREATE_AUCTION,SUBMIT_BID,CLEAR_AUCTION}. The public product rename does not alter this deployed wire contract.1. Process topology
A prebuilt tee-node./cmd/extension binary runs beside the TypeScript runtime, joined so the container dies if either child dies:
/bin/bash, not /bin/sh. Debian’s /bin/sh is dash, which does not implement wait -n, and without it the container will not exit when a child dies.
Neither process is privileged.
2. HTTP surface the extension MUST serve
On$EXTENSION_PORT, bound on all interfaces inside the container.
POST /action
Request body is an Action (§4.1). Response is an ActionResult (§4.3) as JSON with status 200 whenever the action was routed to a handler — including when the handler fails. Handler failure is signalled by ActionResult.status, not by the HTTP status.
The 501 body is a human-readable diagnostic, not a fixed string — the status
code is what is contractual. The Go implementation additionally names the
received and expected identifiers, which is worth imitating.
GET /state
No request body. Returns a StateResponse (§4.4) with status 200.
Routing rules
3. HTTP surface the extension MAY call
tee-node exposes a signing/crypto API onhttp://localhost:$SIGN_PORT. The extension calls it; it is never exposed outside the container.
POST /decrypt
Decrypts a payload that was encrypted to the TEE’s public key. Because tee-node is Go and Go marshals []byte as base64 in JSON, the wire encoding here is base64, not hex — this is the single most common porting mistake.
Request:
base/node.* module so extension authors never hand-roll it.
4. Wire format
Every field below is derived from the Go types that tee-node actually serializes. The Go type is given because it determines the JSON encoding, and getting the encoding wrong is silent — the node accepts the request and the result fails verification later. Encoding rules for the Go types involved:
bytes32 identifiers (
opType, opCommand) are UTF-8 strings right-padded with zero bytes to 32 bytes, then hex-encoded. "LAND_AUCTION" becomes 12 content bytes followed by 20 zero bytes. The empty string is 32 zero bytes, and it is meaningful: see §5.
4.1 Action — request body of POST /action
Source: tee-node/pkg/types/actions.go.
4.2 ActionData — the data field
Note the double encoding on
message: hex-decode it, then parse the resulting bytes as JSON.
4.3 DataFixed — decoded from ActionData.message
Source: go-flare-common/pkg/tee/instruction/instruction.go.
originalMessage is what a handler receives. Its interpretation is extension-specific. Nautilius expects ECIES ciphertext at the instruction boundary; after /decrypt, the TypeScript handler strictly validates the operation-specific JSON.
4.4 ActionResult — response body of POST /action
Source: tee-node/pkg/types/actions.go.
Every field is always present. The Go struct carries noomitemptytags, sodataandadditionalResultStatusmarshal as"0x"rather than being omitted, andlogis always a string. An implementation that drops empty fields produces a different JSON shape from the reference Go image.
data must be byte-exact across implementations. ActionResult.Hash()
computes keccak256(data) and that hash is signed, so serialization details
matter: emit compact JSON with no whitespace, preserving field declaration
order. Go’s encoding/json, Python dicts and TypeScript object literals all do
this naturally; the conformance fixtures compare the resulting hex exactly.
versionis a plain string, not bytes32. The Go declaration isVersion string(tee-node/pkg/types/actions.go:57). Send"0.1.0", not"0x302e312e30000...". This is easy to get wrong becauseStateResponse.stateVersionis bytes32 (§4.5) — the two are genuinely asymmetric. Nautilius pins the official GoActionResult.Hash()behavior in its TypeScript and Solidity golden-vector tests; removed upstream greeting fixtures are not part of its conformance claim.
4.5 StateResponse — response body of GET /state
4.6 status and log
data is only meaningful for status == 1.
5. Handler dispatch
An extension registers handlers against(opType, opCommand) pairs, both compared as bytes32.
Lookup order: exact (opType, opCommand) match first; then (opType, <empty bytes32>) as a wildcard. Registering a handler with an empty opCommand makes it the default for every command under that op-type. No match at all is a 501.
Concurrency: handler invocations are serialized — at most one runs at a time. GET /state is serialized against handlers too, so a state read never observes a half-applied mutation. Implementations may use a mutex (Go, Python) or a promise chain (TypeScript); what matters is the observable guarantee.
6. Container requirements
Environment variables consumed
An extension implementation itself only needs to read
EXTENSION_PORT and SIGN_PORT; the rest are consumed by tee-node. All of them must still be settable on the container.
Ports
Launch policy label — required
User
7. Reproducibility
The container’s code hash is what gets registered on-chain, so build determinism is a security property, not a nicety. Every language image must:- accept a
SOURCE_DATE_EPOCHbuild arg and propagate it, - pin apt to
snapshot.debian.orgkeyed onSOURCE_DATE_EPOCH, - install dependencies from a committed lockfile (
go.sum,package-lock.json, pinnedrequirements.txt), - normalize mtimes as the final build step:
RUN find /app -exec touch -h -d @${SOURCE_DATE_EPOCH} {} +
node_modules trees embed build-host paths and npm-version-dependent layout, so those images target same-machine determinism only. See local reproducibility for the honest per-language caveats.
tee-node version pinning. Non-Go images build the tee-node ./cmd/extension binary from source. The version must match the Go module pin in go/go.mod, or the node and the proxy will disagree on signature formats and surface confusing verification failures. That pin is frequently a Go pseudo-version (v0.0.21-0.20260619120252-31fc839ae6d2), whose last segment is an abbreviated commit SHA. Two obvious approaches both fail on it: git clone --branch <sha> resolves tags and branches only, and git fetch --depth 1 origin <sha> requires a full 40-char SHA plus server-side uploadpack.allowAnySHA1InWant, which GitHub rejects with couldn't find remote ref. Use a blobless partial clone, which fetches all refs cheaply and lets an abbreviated SHA resolve locally:
scripts/lib/versions.sh derives TEE_NODE_REF from go/go.mod, and scripts/check-versions.sh fails the build if the pins drift apart.
8. Verifying the implementation
- Preserve the framework layer (
base/) wire types, dispatch and serialization. - Keep operation identifiers identical to the sender contract:
LAND_AUCTION/{CREATE_AUCTION,SUBMIT_BID,CLEAR_AUCTION}. - Run
cd typescript && npm test && npm run typecheck; no chain, registration or proxy is required for these wire/product tests.