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
119 changes: 119 additions & 0 deletions harness/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -123,6 +123,125 @@ Use these environment variables to filter the active matrix:
- `MPP_INTEROP_INTENTS=charge`
- `MPP_INTEROP_SCENARIOS=charge-basic,charge-split-ata,charge-network-mismatch,charge-cross-route-replay`

### x402 exact intent

A second intent, `x402-exact`, exercises the canonical x402 `exact` scheme
against the Rust spine in `rust/crates/x402/src/bin/interop_{client,server}.rs`.
The TypeScript reference adapters live at
`src/fixtures/typescript/exact-{client,server}.ts` and share the same
harness contract as the Rust spine: identical `X402_INTEROP_*` env vars,
identical `PAYMENT-REQUIRED` / `PAYMENT-SIGNATURE` headers, identical
ready / result JSON shapes. The TS reference fixture carries a stub
credential payload (challenge id + resource) and is paired against the
TS reference server in the default matrix; the Rust spine is paired
against itself. As language adapters that carry a real Solana
PaymentProof land, they expand the matrix by registering under
`intents: ["x402-exact"]` in `implementations.ts`.

Env vars consumed by both roles:

- `X402_INTEROP_RPC_URL`, `X402_INTEROP_NETWORK`, `X402_INTEROP_MINT`
- `X402_INTEROP_PAY_TO`, `X402_INTEROP_PRICE`
- `X402_INTEROP_FACILITATOR_SECRET_KEY`

Server-only:

- `X402_INTEROP_EXTRA_OFFERED_MINTS` (CSV of additional mint addresses)

Client-only:

- `X402_INTEROP_TARGET_URL`
- `X402_INTEROP_CLIENT_SECRET_KEY`
- `X402_INTEROP_PREFER_CURRENCIES` (CSV of preferred currencies)

Run the x402 matrix slice:

```bash
X402_INTEROP_MATRIX=1 \
X402_INTEROP_RPC_URL=http://127.0.0.1:8899 \
X402_INTEROP_MINT=... X402_INTEROP_PAY_TO=... \
X402_INTEROP_CLIENT_SECRET_KEY='[...]' \
X402_INTEROP_FACILITATOR_SECRET_KEY='[...]' \
pnpm test x402-exact.e2e.test.ts
```

#### x402-exact test tiers

The x402-exact intent splits its coverage across three tiers:

1. **Wire compat (`test/x402-exact.compat.test.ts`)** — runs in the
default `pnpm test` invocation. No live RPC, no cargo build, no
funded keypair. Drives each registered x402-exact adapter (gated by
`COMPAT_INCLUDE_IDS`) against the canonical fixtures in
`harness/fixtures/x402-exact/`:
- **canonical-challenge.json** — the 402 envelope every client must
parse.
- **canonical-payment-signature.json** — the TS-wire credential every
server must parse (accept or reject with a known token). Wire-only
adapters may emit `payment_invalid` as fallback.
- **canonical-payment-signature-rust.json** — Rust-spine canonical
`PaymentSignatureEnvelope` with a `PaymentProof::Transaction`
payload. Asserts the Rust serde envelope parser accepts a
well-formed envelope and the verifier rejects with a specific
`invalid_exact_svm_payload_*` token (used by the live matrix
against the Rust spine server).
- **canonical-reject-tokens.json** — the union of taxonomy-aligned
reject tokens (high-level + `invalid_exact_svm_payload_*` family,
mirrored from `rust/crates/x402/src/protocol/schemes/exact/verify.rs`).
- **attack-scenarios.json** — tampered credential overrides; each
scenario enumerates the reject tokens a spec-compliant server may
emit. Wire-only adapters may emit `payment_invalid` as fallback.

2. **Self-pair + spine cross-pair (`test/x402-exact.e2e.test.ts`)** —
the canonical cross-language matrix, env-gated behind
`X402_INTEROP_MATRIX=1`. Enumerates every same-language self-pair
plus every adapter ↔ Rust spine cross-pair.

3. **Live full matrix (`test/x402-exact.live.matrix.test.ts`)** —
superset of tier 2: every `allowedPair` from the policy in
`implementations.ts`. Also env-gated. Designed to widen
automatically as new x402-exact adapters register; no test edit
required to pick them up.

To extend with a new language adapter:
- Register `{id, label, role, command, intents: ["x402-exact"], enabled}`
in `harness/src/implementations.ts`.
- Add the adapter id to `COMPAT_INCLUDE_IDS` in
`test/x402-exact.compat.test.ts` once the adapter has a fast startup
cost (no cargo build per test); otherwise leave it out and rely on
the live matrix.
- The live matrix picks up the adapter automatically via the
`allowedPair` policy.

Why the Rust spine is excluded from the compat suite: the spine
deserializes `payload` as `PaymentProof::Transaction | Signature`
(rust/crates/x402/src/protocol/schemes/exact/types.rs), so the TS-wire
canonical credential — which carries `payload.challengeId/resource` —
is rejected at the proof layer with a generic `payment_invalid` before
the per-scenario assertions can run. Rust spine coverage is provided
by the live matrix (tier 3), which builds real signed transactions
and exercises the full structural verifier.

Optional opt-in flag:

- `X402_COMPAT_STUB_ACCEPT=<id,id,...>` — declares that the listed
full-verifier adapters intentionally accept the TS-wire stub
credential. Without this, a full-verifier adapter that returns 200
on the canonical credential is flagged as a verifier bypass.
- `X402_COMPAT_REPLAY_TRUST=<id,id,...>` — declares that the listed
adapters' verifier accepts the canonical stub credential and is
therefore eligible for the replay assertion. Without this, only
adapters in `WIRE_ONLY_ADAPTER_IDS` run the replay test (other
adapters cover replay via the live matrix with a real signed
transaction).

Cross-server portability and idempotent-resubmit scenarios are gated
separately:

```bash
X402_INTEROP_CROSS_SERVER=1 pnpm test cross-server-scenarios.test.ts
```

The current scenario set covers only the `charge` intent. It includes a basic
payment, a split payment that requires the server fee payer to create the split
recipient ATA, a negative network-mismatch payment, and a cross-route replay
Expand Down
119 changes: 119 additions & 0 deletions harness/fixtures/x402-exact/attack-scenarios.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,119 @@
{
"_description": "Attack scenarios for the x402-exact verifier surface. Each scenario provides a malformed PAYMENT-SIGNATURE credential the server MUST reject with one of `expectedRejectTokens`. Wire-only adapters (no SVM transaction decoder, listed in WIRE_ONLY_ADAPTER_IDS in the test runner) get an automatic `payment_invalid` fallback — full verifiers do NOT and must emit a specific token. The harness extractRejectToken searches every response field for a known taxonomy token before falling back, so a server that buries a specific token in `message` is still credited.",
"scenarios": [
{
"name": "missing_accepted_block",
"description": "Credential lacks `accepted` block entirely. Structural reject — every adapter must reject. The TS reference server hits the challenge-verification path first when the `payload.challengeId` is foreign-issued; the rust spine rejects at envelope deserialization. Both are surfaced as canonical tokens.",
"credentialOverride": {},
"deleteFields": ["accepted"],
"expectedRejectTokens": ["challenge_verification_failed"]
},
{
"name": "missing_payload_block",
"description": "Credential lacks `payload`. Structural reject. Rust spine rejects at PaymentSignatureEnvelope deserialization (payload is required); TS reference server's classifyCredential rejects when payload is absent. Both surface a canonical token.",
"credentialOverride": {},
"deleteFields": ["payload"],
"expectedRejectTokens": ["challenge_verification_failed"]
},
{
"name": "tampered_amount",
"description": "Credential `accepted.amount` diverges from offered requirement. The TS reference compares `accepted` to offered requirements and emits charge_request_mismatch. Full SVM verifiers (rust spine) require a real signed transaction here; this scenario therefore targets the wire-binding check, not the on-chain amount check (which lives in the live matrix and emits invalid_exact_svm_payload_amount_mismatch).",
"credentialOverride": {
"accepted": {
"amount": "1"
}
},
"expectedRejectTokens": [
"charge_request_mismatch",
"invalid_exact_svm_payload_amount_mismatch"
]
},
{
"name": "tampered_recipient",
"description": "Credential `accepted.payTo` diverges from offered requirement. Wire-binding check; on-chain recipient parity is asserted in the live matrix.",
"credentialOverride": {
"accepted": {
"payTo": "11111111111111111111111111111112"
}
},
"expectedRejectTokens": [
"charge_request_mismatch",
"invalid_exact_svm_payload_recipient_mismatch"
]
},
{
"name": "tampered_mint",
"description": "Credential `accepted.asset` diverges from offered requirement. Wire-binding check.",
"credentialOverride": {
"accepted": {
"asset": "So11111111111111111111111111111111111111112"
}
},
"expectedRejectTokens": [
"charge_request_mismatch",
"invalid_exact_svm_payload_mint_mismatch"
]
},
{
"name": "wrong_network",
"description": "Credential `accepted.network` diverges from server's offered network. The TS reference returns the canonical wrong_network token; full verifiers may also reject earlier with charge_request_mismatch when the requirement-binding check fails before the network check.",
"credentialOverride": {
"accepted": {
"network": "solana:zzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzz"
}
},
"expectedRejectTokens": [
"wrong_network",
"charge_request_mismatch"
]
},
{
"name": "tokenProgram_mismatch",
"description": "Credential carries an `extra.tokenProgram` that does not match the offered token program (SPL Token-2022 vs legacy SPL confusion class). Wire-only adapters cannot catch this without decoding the transaction blob (wireOnlyMayAccept) — full-verifier adapters MUST reject with the mint_mismatch token (the tokenProgram is bound to the mint at verify time).",
"credentialOverride": {
"accepted": {
"extra": {
"decimals": 6,
"tokenProgram": "TokenzQdBNbLqP5VEhdkAS6EPFLC1PHnBqCXEpPxuEb"
}
}
},
"wireOnlyMayAccept": true,
"expectedRejectTokens": [
"invalid_exact_svm_payload_mint_mismatch",
"charge_request_mismatch"
]
},
{
"name": "missing_challenge_id",
"description": "Payload missing challengeId. The TS reference server's facilitator-fixture rejects with challenge_verification_failed; the rust spine handles the absence via its own facilitator/idempotency layer and emits a sibling canonical token.",
"credentialOverride": {
"payload": {
"resource": "/protected"
}
},
"replaceFields": ["payload"],
"expectedRejectTokens": [
"challenge_verification_failed"
]
},
{
"name": "resource_mismatch",
"description": "Payload claims a different resource than the one requested. Cross-route replay attempt at the credential layer.",
"credentialOverride": {
"payload": {
"challengeId": "canonical-fixture-challenge-0001",
"resource": "/some-other-route"
}
},
"expectedRejectTokens": [
"charge_request_mismatch",
"challenge_route_mismatch"
]
}
],
"replayScenario": {
"_description": "Replay: submit the same canonical-payment-signature.json twice. Server MUST accept the first and reject the second with `signature_consumed`. Wire-only adapters get the payment_invalid fallback via WIRE_ONLY_ADAPTER_IDS.",
"expectedRejectTokens": ["signature_consumed"]
}
}
23 changes: 23 additions & 0 deletions harness/fixtures/x402-exact/canonical-challenge.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
{
"_description": "TS-wire x402 `exact` 402 challenge envelope. Mirrors the shape the TS reference server emits in harness/src/fixtures/typescript/exact-server.ts::encodePaymentRequiredHeader. NOTE on Rust-spine parity: the Rust spine (rust/crates/x402/src/protocol/schemes/exact/types.rs::PaymentRequiredEnvelope) serializes the top-level `resource` as a `ResourceInfo` object instead of a string, and Rust's PaymentRequirements is the canonical superset. Adapters that target ONLY this fixture pass the wire-compat tier; full Rust-spine compatibility is asserted by the live matrix in test/x402-exact.live.matrix.test.ts (which exchanges the actual Rust envelope), not by this fixture. See canonical-payment-signature-rust.json for the Rust-spine PaymentProof shape.",
"x402Version": 2,
"accepts": [
{
"scheme": "exact",
"network": "solana:EtWTRABZaYq6iMfeYKouRu166VU2xqa1",
"resource": "/protected",
"description": "Surfpool-backed protected content",
"mimeType": "application/json",
"payTo": "5xYbHvVQfTUyzCzKx5KjVxyqXqQ4Ujm5SbqQXJ5w8nVA",
"asset": "4zMMC9srt5Ri5X14GAgXhaHii3GnPAEERYPJgZJDncDU",
"maxAmountRequired": "1000",
"maxTimeoutSeconds": 60,
"extra": {
"decimals": 6,
"tokenProgram": "TokenkegQfeZyiNwAJbNbGKPFXCWuBvf9Ss623VQ5DA"
}
}
],
"resource": "/protected",
"error": null
}
16 changes: 16 additions & 0 deletions harness/fixtures/x402-exact/canonical-payment-signature-rust.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
{
"_description": "Rust-spine canonical PAYMENT-SIGNATURE envelope. Wire-mirrors rust/crates/x402/src/protocol/schemes/exact/types.rs::PaymentSignatureEnvelope. The `payload` field deserializes as PaymentProof::Transaction with a base64-encoded serialized signed VersionedTransaction. The transaction blob below is the SHORTEST recognisable placeholder — base64 of the bytes `not-a-real-signed-transaction-but-valid-base64` — so the JSON envelope parses cleanly. Bincode-deserialization of the placeholder bytes fails BEFORE verify_exact_versioned_transaction runs, so the spine rejects with the generic `payment_invalid` + a deserialization message (not an `invalid_exact_svm_payload_*` token). The fixture's purpose is therefore (a) the JSON envelope parser accepts a well-formed PaymentSignatureEnvelope and (b) the spine emits a structured 402 with a deserialization diagnostic — NOT a process crash. Asserting `invalid_exact_svm_payload_*` tokens against the spine requires a real signed transaction, which lives in the live matrix.",
"x402Version": 2,
"scheme": "exact",
"network": "solana:EtWTRABZaYq6iMfeYKouRu166VU2xqa1",
"accepted": {
"scheme": "exact",
"network": "solana:EtWTRABZaYq6iMfeYKouRu166VU2xqa1",
"asset": "4zMMC9srt5Ri5X14GAgXhaHii3GnPAEERYPJgZJDncDU",
"payTo": "5xYbHvVQfTUyzCzKx5KjVxyqXqQ4Ujm5SbqQXJ5w8nVA",
"amount": "1000"
},
"payload": {
"transaction": "bm90LWEtcmVhbC1zaWduZWQtdHJhbnNhY3Rpb24tYnV0LXZhbGlkLWJhc2U2NA=="
}
}
20 changes: 20 additions & 0 deletions harness/fixtures/x402-exact/canonical-payment-signature.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
{
"_description": "TS-wire PAYMENT-SIGNATURE credential. Mirrors the shape the TS reference client emits in harness/src/fixtures/typescript/exact-client.ts. Used by the wire-compat tier (test/x402-exact.compat.test.ts) to assert every registered adapter's parser handles a foreign-issued credential gracefully (accept, or reject with a parseable token). NOTE on Rust-spine parity: the Rust spine deserializes `payload` as PaymentProof — `{ \"transaction\": base64(...) }` or `{ \"signature\": \"...\" }` (rust/crates/x402/src/protocol/schemes/exact/types.rs::PaymentProof). See canonical-payment-signature-rust.json for the Rust-canonical PaymentProof variant the live matrix exchanges. This stub payload uses `challengeId/resource` and is treated by the rust spine as an unknown payload (early reject at deserialization or at verify time, both acceptable for the compat tier).",
"x402Version": 2,
"accepted": {
"scheme": "exact",
"network": "solana:EtWTRABZaYq6iMfeYKouRu166VU2xqa1",
"asset": "4zMMC9srt5Ri5X14GAgXhaHii3GnPAEERYPJgZJDncDU",
"payTo": "5xYbHvVQfTUyzCzKx5KjVxyqXqQ4Ujm5SbqQXJ5w8nVA",
"amount": "1000",
"extra": {
"decimals": 6,
"tokenProgram": "TokenkegQfeZyiNwAJbNbGKPFXCWuBvf9Ss623VQ5DA"
}
},
"payload": {
"challengeId": "canonical-fixture-challenge-0001",
"resource": "/protected"
},
"resource": "/protected"
}
29 changes: 29 additions & 0 deletions harness/fixtures/x402-exact/canonical-reject-tokens.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
{
"_description": "Canonical reject tokens that every x402-exact server adapter MUST be capable of emitting on the appropriate failure class. The high-level (`payment_invalid`, `signature_consumed`, `wrong_network`, etc.) tokens are shared with the MPP charge intent and live in harness/src/canonical-codes.ts. The `invalid_exact_svm_payload_*` family is x402-exact-specific and is enumerated from rust/crates/x402/src/protocol/schemes/exact/verify.rs — adapters that ship a real SVM verifier (Rust spine + any language port that wires a full transaction structural verifier) MUST be able to emit every token in this list for the corresponding attack class. Wire-only TS reference adapters may emit `payment_invalid` instead, since they don't decode the signed transaction blob.",
"highLevelTokens": [
"payment_invalid",
"signature_consumed",
"wrong_network",
"charge_request_mismatch",
"challenge_verification_failed",
"challenge_route_mismatch",
"challenge_expired"
],
"exactSvmPayloadTokens": [
"invalid_exact_svm_payload_amount_mismatch",
"invalid_exact_svm_payload_memo_count",
"invalid_exact_svm_payload_memo_mismatch",
"invalid_exact_svm_payload_mint_mismatch",
"invalid_exact_svm_payload_no_transfer_instruction",
"invalid_exact_svm_payload_recipient_mismatch",
"invalid_exact_svm_payload_transaction_fee_payer_transferring_funds",
"invalid_exact_svm_payload_transaction_instructions_compute_limit_instruction",
"invalid_exact_svm_payload_transaction_instructions_compute_price_instruction",
"invalid_exact_svm_payload_transaction_instructions_compute_price_instruction_too_high",
"invalid_exact_svm_payload_transaction_instructions_length",
"invalid_exact_svm_payload_unknown_fifth_instruction",
"invalid_exact_svm_payload_unknown_fourth_instruction",
"invalid_exact_svm_payload_unknown_optional_instruction",
"invalid_exact_svm_payload_unknown_sixth_instruction"
]
}
21 changes: 15 additions & 6 deletions harness/src/contracts.ts
Original file line number Diff line number Diff line change
@@ -1,11 +1,12 @@
import type { CanonicalErrorCode } from "./canonical-codes";
import { chargeScenarios } from "./intents/charge";
import { x402ExactScenarios } from "./intents/x402-exact";

export type { CanonicalErrorCode };

export type AdapterKind = "client" | "server";

export type InteropIntent = "charge";
export type InteropIntent = "charge" | "x402-exact";

export type InteropScenarioSplit = {
recipientKey: string;
Expand Down Expand Up @@ -136,8 +137,10 @@ export type AdapterMessage = ReadyMessage | ClientRunResult;

export { chargeCanonicalJsonVectors } from "./intents/charge";

export const interopScenarios: readonly InteropScenario[] =
chargeScenarios;
export const interopScenarios: readonly InteropScenario[] = [
...chargeScenarios,
...x402ExactScenarios,
];

export const interopScenario: InteropScenario = {
...(interopScenarios[0] as InteropScenario),
Expand Down Expand Up @@ -191,11 +194,18 @@ function selectScenarioIds(rawSelection: string | undefined): string[] {
return selected;
}

// The legacy MPP charge runner predates the x402-exact intent. To keep
// the existing CI matrix's default behaviour (charge-only) stable while
// still surfacing the new intent through `selectInteropIntents("x402-exact")`,
// the empty-selection default is restricted to "charge". Callers that
// want the full intent set should pass the explicit list.
const DEFAULT_INTENTS: readonly InteropIntent[] = ["charge"];

export function selectInteropIntents(
rawSelection: string | undefined,
): InteropIntent[] {
if (!rawSelection || rawSelection.trim() === "") {
return [...supportedInteropIntents];
return [...DEFAULT_INTENTS];
}

const selected = rawSelection
Expand All @@ -209,8 +219,7 @@ export function selectInteropIntents(
if (unsupported.length > 0) {
throw new Error(
`Unsupported MPP_INTEROP_INTENTS value(s): ${unsupported.join(", ")}. ` +
`Supported intents: ${supportedInteropIntents.join(", ")}. ` +
"Session and subscription scenarios are not implemented in this harness yet.",
`Supported intents: ${supportedInteropIntents.join(", ")}.`,
);
}

Expand Down
Loading
Loading