test(harness/x402): x402 exact interop harness, intent + matrix#134
Closed
EfeDurmaz16 wants to merge 14 commits into
Closed
test(harness/x402): x402 exact interop harness, intent + matrix#134EfeDurmaz16 wants to merge 14 commits into
EfeDurmaz16 wants to merge 14 commits into
Conversation
This was referenced May 26, 2026
Closed
f4eab24 to
2aa3aa6
Compare
…eferences (solana-foundation#122) Per maintainer guidance in solana-foundation#122, this is a transversal cleanup PR: Part A — remove internal kitchen references - Drop M1/M2/M3 milestone framing from swift/README.md, swift/Examples/README.md - Reword 'M1 baseline / M2-followup' coverage gate comments in python/pyproject.toml and .github/workflows/python.yml as plain coverage gate descriptions - Remove 'M1 closure / L6 audit row' tag from lua/mpp/protocol/core/error_codes.lua Part B — rename tests/interop to harness - git mv tests/interop harness - Update all path references repo-wide (.github/workflows/*, READMEs, .gitignore, docs, composer.json, .php-cs-fixer.dist.php, skill files) - Fix relative paths inside the harness now that depth dropped by one (rust-client/Cargo.toml, php-server, ruby-server, go.mod replace lines, src/implementations.ts, test/compute-budget-caps.test.ts REPO_ROOT) - Update Go module identifiers harness/{go-client,go-server} to match path - Refresh internal comments/docs that still mentioned tests/interop Part C — skill / README polish - Skill references and intent docs now point at harness/* paths Closes solana-foundation#122.
Adds the canonical x402 `exact` intent to the cross-language interop harness, plus TypeScript reference client and server fixtures and matrix wiring that registers the Rust spine adapters already shipped under `rust/crates/x402/src/bin/`. Language adapters can now target the harness contract (X402_INTEROP_* env vars, ready/result JSON shapes) to validate against the Rust spine cell. The TS reference fixture carries a stub credential payload (challenge id + resource) so the harness wiring, negative-code classification, cross-server portability, and idempotent-resubmit flows can run without a full Solana signer. Pair restriction in the matrix gates TS↔TS and Rust↔Rust by default; full TS↔Rust on-chain settlement parity lands with a follow-up SDK port. The legacy MPP charge runner hard-skips the new intent so default `pnpm test` behaviour is unchanged.
Three fixes so the x402-exact matrix actually executes once the language adapter PRs (solana-foundation#124, solana-foundation#126, solana-foundation#127, solana-foundation#128, solana-foundation#129, solana-foundation#130) rebase on top of this branch: 1. Pair filter is data-driven. Previously only ts-x402 self-pair and rust-x402 self-pair were accepted. Now the filter walks the registered adapters and accepts any pair where: both sides are the TS reference (stub-payload), both sides are the Rust spine, the two sides share a base language id (same-language self-pair, e.g. go-x402-client <-> go-x402-server), or one side is the Rust spine (cross-spine pair in either direction). TS reference is locked to its self-pair only because its stub payload would fail real signature verification on any other server. 2. rust-x402 cargo --manifest-path corrected from ../../rust/Cargo.toml to ../rust/Cargo.toml. The path was stale after the tests/interop -> harness rename; the existing rust (charge) entries already used the correct relative path, the x402 entries did not. 3. Pair selector docstring rewritten to spell out the data-driven matrix policy so future language ports don't need to touch the test.
…rver The TS x402 fixture server gated its cross-server portability rejection behind `issued.size > 0`, so a freshly started server (or one that had not yet issued any 402 challenge) would accept any challengeId from another server's credential and settle it. That contradicts canonical Rust behavior, which rejects unknown challengeIds with `challenge_verification_failed` from the very first request. Drop the `issued.size > 0` guard so the membership check fires unconditionally. The happy-path flow (GET /protected -> 402 with challengeId -> POST with challengeId) is unaffected because the served challengeId is added to `issued` on the 402 path before the client returns. Codex r8 solana-foundation#132 P2: cross-server replay to a fresh TS server now returns `challenge_verification_failed` immediately rather than settling.
… key envs The x402-exact-network-mismatch and x402-exact-cross-route-replay scenarios were registered in src/intents/x402-exact.ts but never run by any test (only the happy path was exercised in e2e.test.ts). Add a TS-only negative-scenario suite that drives each one against the TS reference server with hand-crafted credentials and asserts the canonical reject code. The network-mismatch scenario was previously a no-op even if invoked because the scenario.network value flowed to both client and server. The new test sends distinct networks: server advertises offers on network A, credential claims network B, server emits wrong_network. The TS reference fixture parsed X402_INTEROP_CLIENT_SECRET_KEY and X402_INTEROP_FACILITATOR_SECRET_KEY as required envs but never read them (stub credential, no on-chain signing). Drop the requirement so the verifier surface can be exercised without standing up a Surfpool RPC or funded keypair; the live matrix in x402-exact.e2e.test.ts still requires them via the test guard. Real-signing language adapters read their own keypair envs.
…irs + parity locks) Add three-tier x402-exact test architecture on top of solana-foundation#132: 1. Wire compat (no RPC, default `pnpm test`): - `harness/test/x402-exact.compat.test.ts` - Drives every registered x402-exact adapter (gated by COMPAT_INCLUDE_IDS) against canonical fixtures and an attack suite. Catches wire-format drift before the live matrix runs. 2. Parity-lock fixtures (`harness/fixtures/x402-exact/`): - canonical-challenge.json — 402 envelope every client must parse. - canonical-payment-signature.json — credential every server must parse (accept or reject with a known token). - canonical-reject-tokens.json — union of high-level reject tokens and the invalid_exact_svm_payload_* family mirrored from rust/crates/x402/src/protocol/schemes/exact/verify.rs. - attack-scenarios.json — 9 tampered credential scenarios + replay. 3. Live full matrix (`harness/test/x402-exact.live.matrix.test.ts`): - Env-gated (X402_INTEROP_MATRIX=1 + funded keypair). Enumerates every allowedPair from the policy in implementations.ts and runs the happy path. Widens automatically as new adapters register. Also expand `harness/test/x402-exact.e2e.test.ts` with an explicit self-pair group so per-language regressions stand out in vitest output, and update `harness/README.md` with the three-tier documentation and extension recipe.
…lback
Address review findings on the x402-exact matrix:
- Drop blanket `payment_invalid` fallback in attack-scenario assertion;
only wire-only adapters (WIRE_ONLY_ADAPTER_IDS) may emit the generic
token. Full verifiers must emit a specific reject token per scenario.
- Rework extractRejectToken: the Rust spine wraps verifier failures as
`{ error: "payment_invalid", message: "<verifier-token>: ..." }`, so
the most-specific token is in `message`, not `error`. Search every
field for a known taxonomy token (svm-payload tokens before
high-level) and return that; previously the test masked specific
tokens behind the high-level error.
- Replay test now requires the first submission to be accepted; a
server that rejects every credential previously passed trivially.
- Reject-token taxonomy is now strict-checked against the rust spine
source (rust/crates/x402/src/protocol/schemes/exact/verify.rs): the
fixture set must equal the set of `invalid_exact_svm_payload_*`
literals in the spine. Token add/remove/rename in the spine fails
the test with a pointed diff.
- Add canonical-payment-signature-rust.json with the Rust-spine
PaymentProof::Transaction shape (vs the existing TS-wire stub).
- Reframe TS-wire fixture descriptions to honestly document the
PaymentRequiredEnvelope `resource: ResourceInfo` and
`payload: PaymentProof` differences vs the Rust spine.
- Replace `it.fails` skip in the live matrix with a hard `it` failure
so a broken scenario registry fails CI loudly.
- Remove generic `payment_invalid` from per-scenario expectedRejectTokens
in attack-scenarios.json. Wire-only adapters still get the fallback
via WIRE_ONLY_ADAPTER_IDS in the test runner; full verifiers must now
emit a specific token (no silent regression to generic rejection).
- Document each scenario's true scope: wire-binding checks (rejected by
the TS reference's classifyCredential / rust spine's requirement
binding) vs SVM transaction structural checks (live matrix only).
- Tone down canonical-payment-signature-rust.json description: the
placeholder transaction fails bincode-deserialization BEFORE
verify.rs runs, so the fixture asserts envelope parsing + structured
402 emission, not `invalid_exact_svm_payload_*` tokens.
- Add `once("error")` rejection on the in-process fixture server's
listen call so EPERM/EADDRINUSE fails the test cleanly instead of
hanging 60s on the adapter timeout.
- X402_COMPAT_INCLUDE_RUST=1 opts the rust-x402 adapter into the compat suite (off by default to keep `pnpm test` cargo-free). - Replay assertion gated by WIRE_ONLY_ADAPTER_IDS + opt-in X402_COMPAT_REPLAY_TRUST list: adapters whose verifier requires a real signed transaction (rust spine) skip the stub-credential replay test cleanly with a documented skip message; live matrix covers replay against them with a real PaymentProof::Transaction. - README documents both opt-in flags.
…olicy - Default full-verifier behavior: server adapters not in WIRE_ONLY_ADAPTER_IDS that accept the TS-wire stub credential are now flagged as a verifier bypass. Opt-in via X402_COMPAT_STUB_ACCEPT (CSV) for adapters whose verifier accepts the stub by design. - Drop payment_invalid fallback for the replay second-submit assertion: once first=200 the second submission MUST be classified as signature_consumed by every adapter (no generic-rejection regression). - Add explicit canonical-payment-signature-rust.json shape lock: every field rust spine's PaymentSignatureEnvelope requires must be present, payload must deserialize as PaymentProof::Transaction xor PaymentProof::Signature, base64 round-trip stable. Fixture can no longer drift undetected. - Reject-token taxonomy match is now longest-first so suffixed tokens (e.g. ..._compute_price_instruction_too_high) match before their prefix (..._compute_price_instruction). - Extract allowedX402Pair / baseLang / isRustSpine to src/x402-pair-policy.ts so the e2e and live-matrix tests share one source of truth and cannot drift apart silently.
…pat keypair requirement - Add console.warn for live-matrix skip-due-to-missing-env so CI matrix misconfiguration is visible in job logs (per spec the behavior remains skip-not-fail, since the matrix is opt-in by env). - Document that X402_COMPAT_INCLUDE_RUST=1 requires real ed25519 keypairs (rust spine validates via MemorySigner::from_bytes); the README and inline comment make this contract explicit.
The TS-wire canonical credential carries `payload.challengeId/resource`, which the rust spine rejects at PaymentProof deserialization with the generic `payment_invalid` token — defeating the per-scenario specific-token assertions that make the compat suite robust. Rather than ship a half-functional opt-in, drop it: the compat suite is now honestly TS-only, and the live matrix (tier 3) is the canonical place for rust spine coverage. README documents the rationale.
Two Codex r8 P2 findings on the x402 harness matrix:
1) TS x402 fixture server gated its cross-server portability rejection
behind `issued.size > 0`, so a freshly started server (or one that
had not yet issued any 402) would accept any challengeId. Drop the
size guard so the membership check fires unconditionally. The
happy-path flow (GET /protected -> 402 with challengeId -> POST
with challengeId) is unaffected because the served challengeId is
added to `issued` on the 402 path before the client returns.
2) `cross-server-scenarios.test.ts:extractCanonicalCode` searched
`error` before `message`. The Rust x402 interop server wraps
verifier failures as `{ error: "payment_invalid", message:
"<specific-verifier-token>" }`, so the first-match strategy
resolved to the generic `payment_invalid` and silently discarded
the verifier-specific token that the canonical taxonomy needs to
classify. Combine both fields into one string before classifying
so the richer signal survives.
The cross-server portability scenario previously listed a single TS->Rust crossServerPair. Add the TS->TS control pair so the assertion exercises the canonical challenge_verification_failed reject token on the TS reference server itself (two independent server instances issue disjoint challenge id sets), not only on the Rust spine's proof-layer reject path. Document why the reverse Rust->TS direction is gated to the live matrix: the Rust spine adapter does not echo the captured credential to the harness by design, so credential-capture replay flows can only originate from the TS client; the canonical Rust->TS portability is asserted end-to-end via the live matrix where a real signed transaction is exchanged.
e88b862 to
a178362
Compare
Collaborator
Author
|
Closing as superseded. The core deliverable (x402 interop intent, TS reference fixtures, e2e matrix, cross-server scenarios) landed on main through another path during the Ruby PR cycle. The remaining bits (compat-tier RPC-less suite, pair-policy helper, attack-scenarios fixtures, dedicated live-matrix + negative tests) are valuable as a fast-feedback hardening pass but are not blockers - happy to spin them out as a focused follow-up PR after the Lua work. |
4 tasks
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
Adds the x402-exact interop harness in one mergeable unit: scenario contracts and env-var protocol, plus the full client/server cross-pair matrix and negative scenarios. Consolidates the prior split between #132 (intent) and #133 (matrix) for easier review.
Scope
Harness intent + contracts
harness/src/intents/x402-exact.ts, scenario catalogue + canonical-codes alignmentharness/src/contracts.ts,InteropScenarioextension forpaymentVersion,paymentMode,expectedHeaderharness/src/x402-pair-policy.ts,allowedPairhelper (data-driven matrix)X402_INTEROP_TARGET_URL,X402_INTEROP_RPC_URL, etc. (seeharness/README.md)Reference fixture
harness/src/fixtures/typescript/exact-{client,server,shared}.ts, TS stub fixture (challenge framing only, no real settlement)Test matrix (3 tiers)
harness/test/x402-exact.compat.test.ts, 17 compat tests, no RPC, runs everywhereharness/test/x402-exact.e2e.test.ts, full e2e cross-pairs, Surfpool-gatedharness/test/x402-exact.live.matrix.test.ts, live-matrix, keypair + Surfpool gatedharness/test/x402-exact.negative.test.ts, network-mismatch + cross-route-replayharness/test/cross-server-scenarios.test.ts, symmetric portability including ts-x402 to ts-x402Security highlights
issued.size > 0guard that let fresh-server scenarios bypass deduplication.charge_request_mismatch.Test evidence
Supersedes