Skip to content

refactor(host_primitives): single home for the Turnkey wire envelope - #446

Merged
pepe-anchor merged 3 commits into
mainfrom
pepefigueira/prs-581-01-turnkey-envelope
Aug 26, 2026
Merged

pepe-anchor merged 3 commits into
mainfrom
pepefigueira/prs-581-01-turnkey-envelope

Conversation

@pepe-anchor

@pepe-anchor pepe-anchor commented Aug 6, 2026

Copy link
Copy Markdown
Contributor

Why

Two host binaries have to serve the same Turnkey JSON envelope to the same client.

  • parser_gateway: REST in front, gRPC to parser_grpc_server behind. Non-TEE local dev and CI, so it emits a mock bootProof.
  • parser_http_server (feat(parser_http_server): HTTP+JSON pivot for TVC public ingress #450): HTTP+JSON inside the enclave, calling parser_app in process, no gRPC hop. This is what gets deployed to the TVC, because Cloudflare in front of app-<uuid>.turnkey.cloud rejects gRPC with 403. It emits a real attested bootProof.

Different transports, different trust levels, one wire contract. The Go visualsign-turnkeyclient and the wallet integrators behind it cannot tell the two apart, and must not be able to.

Two features also land in that envelope during PRS-581: bootProof (#337) and intermediateOutput (#414). If the definition stays inline in parser_gateway, the pivot copies it, and every envelope change from here on gets made twice and kept in sync by hand. The unmerged x402 branch had already grown its own third copy.

So: one home, two importers, and the bootProof difference becomes a parameter instead of a fork.

Downstream, #450, #451 and #452 build on parser_http_server; #449 builds on parser_gateway. Both sides import the envelope from here, which is also what shrinks the PR #304 rebase from "reconcile two envelope definitions" to "add a module".

What

  • host_primitives::turnkey becomes the single home for the Turnkey request/response envelope, as the union of both existing definitions.
  • bootProof is now an injected value rather than a hardcoded mock: error_response(msg, boot_proof). parser_gateway keeps its stable local-dev mock through a local shim; the enclave pivot supplies a real attested one.
  • Construction moved with the types: success_response sits next to error_response, so parse_handler no longer assembles the success envelope field by field in the gateway.
  • parser_gateway imports the shared types and keeps the four MOCK_BOOT_PROOF_* constants and the tests that are about its own mock and top-level wire shape. Three tests that would have duplicated coverage now living in host_primitives::turnkey moved there instead of being maintained in both places.
  • intermediate_output gained serde(default) alongside its existing skip_serializing_if. Without it the omitted key serialized fine but failed to deserialize, so a response could be written and not read back. Nothing in tree deserializes a response today (both binaries only emit one), which is why no test caught it. The client-direction derives are there so the envelope is symmetric for the out-of-tree clients that do read it, and the new round-trip test is what keeps it that way. Happy to drop those derives and the default until something in tree needs them, if you'd rather not carry them.

No wire change. The gateway's JSON output is byte-identical.

Test evidence

cargo test -p host_primitives -p parser_gateway
  host_primitives: 5 passed
  parser_gateway: 11 passed
cargo fmt --all -- --check: clean
make -C src lint: clean

The gateway test count moves from 13 to 11 because three tests relocated to host_primitives::turnkey, where the types they cover now live: the six-key bootProof assertion (boot_proof_wire_shape_is_exactly_six_camel_case_keys), the empty intermediateOutput omission check, and the Solana chain-metadata discriminator test. host_primitives goes 4 to 5 with the new round-trip test. No coverage was dropped.

The two regression tests that matter both still pass with unchanged constant values: mock_boot_proof_matches_production_wire_shape, error_response_carries_mock_boot_proof.

The "no wire change" claim was checked rather than assumed: a throwaway test reconstructed the pre-refactor struct definitions verbatim from base and byte-compared serde_json output against the new host_primitives::turnkey types across success, error, signature-present, and intermediate-output-present cases. All identical.

One trap worth recording: the x402 branch's TurnkeyResponseWrapper had no rename_all = "camelCase", which the gateway's inline struct did have. Adding boot_proof without it would have serialized as boot_proof instead of bootProof and silently broken the wallet contract. The six-key wire-shape test catches it.

Rollback

Revert the commits. No deploy, no migration, no wire change, so a revert restores the previous state exactly.

Linear

PRS-581

Stack position: base of the PRS-581 stack. #337, #414.

🤖 Generated with Claude Code

parser_gateway kept the Turnkey request/response types inline while the
x402 branch had grown its own copy in host_primitives::turnkey. Two
definitions of one wire contract means every envelope change (bootProof
in #337, intermediateOutput in #414) has to be made twice and kept in
sync by hand.

Move them to host_primitives::turnkey as the union of both, and make
bootProof an injected value instead of a hardcoded mock, so the enclave
pivot can supply a real one while the gateway keeps its local-dev mock.

No wire change: the gateway still emits the same mock constants, and the
wire-shape tests move with the types.

Co-Authored-By: Claude <noreply@anthropic.com>

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Centralizes the Turnkey wire envelope in host_primitives while preserving the gateway’s existing JSON contract.

Changes:

  • Adds shared request, response, boot-proof, and error-envelope types.
  • Updates parser_gateway to use shared types with its local mock boot proof.
  • Adds wire-shape and serialization tests.

Reviewed changes

Copilot reviewed 4 out of 5 changed files in this pull request and generated 1 comment.

Show a summary per file
File Description
src/parser/gateway/src/main.rs Replaces local envelope types with shared ones.
src/host_primitives/src/turnkey.rs Defines the shared Turnkey envelope.
src/host_primitives/src/lib.rs Exports the new module.
src/host_primitives/Cargo.toml Adds serialization dependencies.
src/Cargo.lock Records dependency updates.

💡 Add a code-review agent skill or configure MCP servers for context-aware, tailored reviews. Learn more in the docs.

Comment thread src/host_primitives/src/turnkey.rs Outdated
pepe-anchor and others added 2 commits August 25, 2026 15:24
…elper

Moving the types into host_primitives left construction behind: error_response
lived in the shared crate, but parse_handler still assembled the success
envelope field by field in the gateway. That splits one wire contract across two
places again, in the same way the duplicated type definitions did, and it is the
copy the enclave pivot would have had to reproduce a third time.

Add success_response next to error_response and have the gateway call it. Three
gateway tests that now duplicate coverage living in host_primitives::turnkey
move out instead of being maintained in both places: the six-key bootProof
assertion, the empty intermediate_output omission check, and the Solana
chain-metadata discriminator test. The gateway keeps the assertions that are
about its own mock constants and its top-level wire shape.

No wire change. The emitted JSON is byte-identical, verified against the
pre-refactor struct definitions.

Co-Authored-By: Claude <noreply@anthropic.com>
…trip

intermediate_output carries skip_serializing_if, so a response without one omits
the key entirely. That is deliberate and keeps existing consumers byte-identical.
But String has no serde default, so the omitted key made deserialization fail:
the envelope could be written and not read back.

Nothing in tree deserializes a response today, which is why no test caught it.
parser_http_server will, and the shape it will be handed most often is exactly
the one that fails.

Add serde(default) next to the existing skip_serializing_if, plus a round-trip
test over the error envelope, the shape every current caller produces. Serde
defaults apply on the way in only, so serialization output is unchanged.

Co-Authored-By: Claude <noreply@anthropic.com>

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 4 out of 5 changed files in this pull request and generated no new comments.

@pepe-anchor
pepe-anchor marked this pull request as ready for review August 26, 2026 09:04

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Right call: one home, boot_proof as a parameter instead of a fork. Verified locally — 5 + 11 tests pass, clippy and fmt --check clean, and no-wire-change holds.

Good: mock stays local to the gateway; parity was byte-compared rather than asserted; test relocation accounted for (13 -> 11, 4 -> 5); the untagged hazard docstring and its test moved with the type; the x402 rename_all trap is covered by the six-key test.

Notes, all non-blocking:

  1. Take the offer on the client-direction derives. Drop Serialize on request types, Deserialize on response types. Worth noting none of this trips dead_codepub items in a lib crate never do, so clippy passing says nothing about reachability here.

    • Serializing a request emits "signature":null,"abiType":null,... and mixes casing (unsigned_payload + networkId). Not a regression — the read path has always accepted that shape (README.md:57) — but Serialize is now what emits it, and nothing pins the request wire shape the way the six-key test pins the response.
    • No inverse of From<ChainMetadataInput> for ChainMetadata, so a caller holding the proto type can't reach the serialize path.
    • Deserialize is stricter than serialize — a response missing one bootProof field fails on missing field deploymentLabel. The round-trip test only reads back what the same type wrote.

    I measured the trim (compiles, 4 + 11 pass): 266 -> 242 source lines, but expanded 2251 -> 883 and rlib 364KB -> 165KB. Side effect: commit 3 becomes moot, since default on intermediate_output goes inert without Deserialize. Keep the attribute anyway; PR drops to two commits. The comment at turnkey.rs:26-28 is aspirational per CLAUDE.md — belongs in the PR body.

    If you'd rather keep the derives, the three things that would make them real: a request wire-shape test, a TryFrom<ChainMetadata> for ChainMetadataInput, and a call on null-vs-omit.

  2. default on chain_metadata is a no-op — serde already defaults Option<T> to None (checked). Inconsistent with error/signature, which carry only skip_serializing_if. rename_all on ChainMetadataInput is also dead (both variants explicitly renamed), but that's pre-existing.

  3. The enclave picked up serde derives it doesn't use. parser_app links host_primitives, so generated goes from tonic_types to serde, serde_derive, tonic_types. images/parser_app/Containerfile:16-18 reasons about attestation surface and the vsock image has no HTTP surface; #450 enables the feature itself. A non-default turnkey feature covers it. Survives the trim in (1). Minor: health_check's default-features = false on host_primitives is a no-op — no features declared.

  4. The duplication that hurts didn't move. #449:1314 and #450:970 hand-build the success envelope field by field, so success_response has one caller and the two PRs motivating it bypass it. The triplicated part is proto -> wire: Chain::from_str_name, the SignatureScheme as i32 match, the base64 encode, the parsed_transaction/payload unwrapping. success_response_from_proto(...) would collapse all three.

  5. #304 is the third envelope copy, still open on spec/x402-gated-http-api — worth confirming it rebases onto this rather than reconciling definitions.

@pepe-anchor
pepe-anchor merged commit e77273e into main Aug 26, 2026
11 checks passed
@pepe-anchor
pepe-anchor deleted the pepefigueira/prs-581-01-turnkey-envelope branch August 26, 2026 15:46
pepe-anchor added a commit that referenced this pull request Aug 26, 2026
…esponse

The pivot was written against host_primitives::turnkey as of the fork point,
before #446 grew a shared success_response helper. Rebasing onto main left
handle_parse assembling the wrapper field by field, which is the second copy of
the wire contract that #446 existed to remove.

No wire change: success_response fills the same three fields and sets
error: None, so the emitted JSON is byte-identical. The bootProof six-key
assertions in host_primitives, parser_http_server and the http_server
integration test all still pass.

Co-Authored-By: Claude <noreply@anthropic.com>
pepe-anchor added a commit that referenced this pull request Sep 3, 2026
…esponse

The pivot was written against host_primitives::turnkey as of the fork point,
before #446 grew a shared success_response helper. Rebasing onto main left
handle_parse assembling the wrapper field by field, which is the second copy of
the wire contract that #446 existed to remove.

No wire change: success_response fills the same three fields and sets
error: None, so the emitted JSON is byte-identical. The bootProof six-key
assertions in host_primitives, parser_http_server and the http_server
integration test all still pass.

Co-Authored-By: Claude <noreply@anthropic.com>
pepe-anchor added a commit that referenced this pull request Sep 4, 2026
…esponse

The pivot was written against host_primitives::turnkey as of the fork point,
before #446 grew a shared success_response helper. Rebasing onto main left
handle_parse assembling the wrapper field by field, which is the second copy of
the wire contract that #446 existed to remove.

No wire change: success_response fills the same three fields and sets
error: None, so the emitted JSON is byte-identical. The bootProof six-key
assertions in host_primitives, parser_http_server and the http_server
integration test all still pass.

Co-Authored-By: Claude <noreply@anthropic.com>
pepe-anchor added a commit that referenced this pull request Sep 7, 2026
…#449)

## Why

Host side of the x402 work, ported off the unmerged PR #304 (stacked on
a now-dead branch, 132 commits behind main) onto current main so it can
actually be reviewed. Rebased onto #446 (merged), reusing the shared
Turnkey wire types (`success_response` etc.) from
`host_primitives::turnkey` instead of a second copy.

## What

- Splits `parser_gateway`'s single `main.rs` into modules: `lib.rs`,
`state.rs`, `auth.rs`, `attestation.rs`, `x402_config.rs`,
`handlers/{mod,health,parse}.rs`.
- x402 price-tag configuration and middleware builder.
- Optional shared-bearer-token gate on the gateway hop.
- Demo TVC response-attestation verifier: checks the enclave's
ephemeral-key signature against a pinned pubkey, and binds it to the
specific response being forwarded (not just "the enclave signed
something at some point").
- Moves the crate to axum 0.8, already the workspace norm (`parser/cli`
runs it alongside 0.6.20 in `metrics`).

**Deliberately left out** (needs `host_primitives::payment_marker` from
#448, not in this base): `signing.rs`/`gateway_keygen`,
`parse_tvc.rs`/`tvc_probe`, and the chain-to-402-network derivation
living in `parse_tvc.rs`. Net effect: this gateway can verify and settle
x402, but can't yet *sign* a `VerifiedPaymentMarker`, so the trust pair
completes in the follow-up PR.

### Review fixes folded in

- Signature verification recomputes the digest from the forwarded
payload instead of trusting the wire-carried `Signature.message`,
closing a replay path (a captured valid signature could otherwise be
paired with a forged payload).
- The request/response digest-binding check now runs unconditionally,
not only when a pinned attestation verifier is configured.
- x402 config rejects silent mis-pricing at load: non-USDC assets,
zero/sub-atomic prices, a zero facilitator timeout.
- Bearer gate: fixed fail-open on a non-UTF-8 token, a non-ASCII token
booting clean then 401'ing every request, and case-sensitive scheme
matching.
- Facilitator startup probe now resolves `/supported` the way x402-axum
actually does (RFC 3986 relative join, not string concatenation).
- Deduped the "unset vs non-UTF-8 env var" and bounded-file-read idioms,
previously hand-copied 2-3x across `auth.rs` / `attestation.rs` /
`x402_config.rs`.
- IPv6 loopback check now matches `Url::host_str()`'s bracketed form
(`[::1]`), not just `::1`.

## Test evidence

```
make -C src build             -> clean
make -C src test              -> 0 failed across the workspace
cargo test -p parser_gateway  -> 84 passed, 0 failed (11 on main before this PR)
cargo fmt --check             -> clean
make -C src lint              -> clean (clippy --all-targets -D warnings)
```

Wire shape is unchanged. Boot-proof regression tests
(`mock_boot_proof_matches_production_wire_shape`,
`error_response_carries_mock_boot_proof`) survive the module split with
byte-for-byte identical mock values.

## Rollback

Revert the commits. The gateway only runs in non-TEE local dev and CI,
never a real enclave, so a revert can't touch production.

## Linear

PRS-581. Follows #446 (merged). Supersedes the gateway portion of #304.

🤖 Generated with [Claude Code](https://claude.com/claude-code)
pepe-anchor added a commit that referenced this pull request Sep 7, 2026
…esponse

The pivot was written against host_primitives::turnkey as of the fork point,
before #446 grew a shared success_response helper. Rebasing onto main left
handle_parse assembling the wrapper field by field, which is the second copy of
the wire contract that #446 existed to remove.

No wire change: success_response fills the same three fields and sets
error: None, so the emitted JSON is byte-identical. The bootProof six-key
assertions in host_primitives, parser_http_server and the http_server
integration test all still pass.

Co-Authored-By: Claude <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants