Skip to content

feat(go): add x402 exact (client+server)#124

Closed
EfeDurmaz16 wants to merge 10 commits into
solana-foundation:mainfrom
EfeDurmaz16:pr/go-x402-port
Closed

feat(go): add x402 exact (client+server)#124
EfeDurmaz16 wants to merge 10 commits into
solana-foundation:mainfrom
EfeDurmaz16:pr/go-x402-port

Conversation

@EfeDurmaz16

@EfeDurmaz16 EfeDurmaz16 commented May 25, 2026

Copy link
Copy Markdown
Collaborator

Summary

Ports the x402 exact scheme (client + server) to Go under go/x402/, following the rust/crates/x402/ canonical spine landed in #121. Introduces the go/paykit umbrella so Go now mirrors the Rust workspace structure (core/mpp/x402/paykit).

Scope

  • Workspace reorg 8f830c1: new go/paykit/ umbrella re-exports go/core + go/mpp; go/x402/ sibling to go/mpp/
  • Client and server adapter binaries at go/x402/cmd/interop-{client,server}/main.go
  • L8 settlement ordering: broadcast (sendTransaction) then confirm (getSignatureStatuses) then put_if_absent("x402-svm-exact:consumed:<base58_signature>"), no claim-first, no release path
  • PAYMENT-RESPONSE canonical shape on successful settlement (36ff6f9)
  • Explicit requirement.Extra["tokenProgram"] to on-chain transfer-program binding mirroring rust/crates/x402/src/protocol/schemes/exact/verify.rs:73-80
  • Fee-payer-in-instruction-accounts sweep with the ATA-create funding-payer carve-out
  • Mint alias resolution at the env-read boundary so X402_INTEROP_MINT accepts symbol or base58

Files changed

  • SDK: go/x402/**, go/core/**, go/paykit/**, go/mpp/** (umbrella + relocations)
  • Tests: go/x402/cmd/interop-server/main_test.go, go/x402/cmd/interop-client/main_test.go
  • Harness: harness/src/implementations.ts registers go-x402-client / go-x402-server with intents: ["x402-exact"]
  • Docs: go/x402/README.md, go/README.md

Security highlights

  • Lighthouse passthrough is program-ID only, no discriminator/account allowlist by design (spine parity)
  • Compute-unit limit unbounded (spine parity), price capped at 5_000_000 microlamports
  • Replay key namespace x402-svm-exact:consumed:<sig> keyed on confirmed signature, never released
  • tokenProgram binding regression suite: mismatch in both directions plus positive control plus missing-extra reject
  • Fee-payer attack suite covers SPL drain, SOL drain, slot-1 placement, plus positive control

Test evidence

Closes / supersedes

None.

Reviewer notes

  • Go and Lua share an INTENTIONAL_DIVERGENCE on ATA-create in slots 3-5 (spine roadmap item, tracked in notes/lighthouse-allowlist-tracking.md)
  • The duplicate-settlement cache is scheme-namespaced and keyed on the confirmed signature, acting as defense-in-depth on top of Solana's native per-signature uniqueness

@EfeDurmaz16 EfeDurmaz16 changed the title feat(go): port x402 exact (client+server) — from x402-sdk #18 feat(go): add x402 exact (client+server) May 25, 2026
EfeDurmaz16 added a commit to EfeDurmaz16/mpp-sdk that referenced this pull request May 25, 2026
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.
Comment thread go/mpp.go
// and module split mirror the Rust reference crate documented in
// skills/pay-sdk-implementation; cross-language behavior is locked via
// the interop harness at tests/interop.
// the interop harness at harness.

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

can we use the structure

————————————————————--
|                    solana-pay-kit                       |
————————————————————--
|      solana-mpp     |     solana-x402.        |
—————————————————————  
|                   solana-pay-core                     |
—————————————————————

for all the languages?

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

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

Done. Reorganized go/ to mirror the Rust workspace layout at rust/crates/{core,mpp,x402,kit}:

 -----------------------------------------------------------
|                  solana-pay-kit  (go/paykit)              |
 -----------------------------------------------------------
|   solana-mpp  (go/mpp)    |   solana-x402  (go/x402)      |
 -----------------------------------------------------------
|                 solana-pay-core  (go/core)                |
 -----------------------------------------------------------
  • go/paykit/ is the umbrella that re-exports core + mpp for downstream callers
  • go/mpp/ holds the MPP charge intent plus its client/ and server/ subpackages
  • go/x402/ is the x402 exact intent half (cmd/ interop binaries today)
  • go/core/ carries shared Solana primitives: programs, mints, wire types, headers, error codes, the Store interface, Error, and challenge expiry helpers

History is preserved through git mv; internal packages import go/core directly rather than cycling through the umbrella. The L8 pre-broadcast verifyTransfersAgainstChallenge ordering fix in go/mpp/server/server.go is kept verbatim. Workflow at .github/workflows/go.yml and go/README.md layout diagram updated to match.

Commit: 0bf6e4b

EfeDurmaz16 added a commit to EfeDurmaz16/mpp-sdk that referenced this pull request May 26, 2026
Two follow-ups from the Codex r5 review of PR solana-foundation#124:

* the rust-x402 client and server entries used '../../rust/Cargo.toml',
  which resolves outside the repo from harness/. The peer mpp Rust
  entries already use '../rust/Cargo.toml'. Align both x402 entries so
  the cross-spine matrix actually finds the Rust workspace.
* the Go x402 client and server entries were gated on
  MPP_INTEROP_CLIENTS/SERVERS, while the other x402 adapters (ts-x402,
  rust-x402) use X402_INTEROP_CLIENTS/SERVERS. Move the Go x402
  adapters onto the X402_INTEROP_* namespace so opt-in is consistent
  across the x402 matrix.
@EfeDurmaz16 EfeDurmaz16 force-pushed the pr/go-x402-port branch 2 times, most recently from afe439c to 4197eea Compare May 26, 2026 13:00
EfeDurmaz16 added a commit to EfeDurmaz16/mpp-sdk that referenced this pull request May 26, 2026
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.
EfeDurmaz16 added a commit to EfeDurmaz16/mpp-sdk that referenced this pull request May 26, 2026
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.
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.
Ports the x402 `exact` scheme (client + server) Go implementation into
`go/x402/`, following the `rust/crates/x402/` reference spine landed in
PR solana-foundation#121. Source: upstream reference port (tip e3bf746).

Constants, instruction allowlist, fee-payer guard, Lighthouse program-ID
passthrough, destination-ATA re-derivation, broadcast-first L8 ordering,
cross-server reject token, and the X402_INTEROP_* env-var contract all
match the Rust spine 1:1. The Go binaries live under
`go/x402/cmd/interop-{client,server}/` as sub-packages of the existing
single mpp-sdk Go module (github.com/solana-foundation/pay-kit/go).

Verification: go test ./x402/... -cover -race
  interop-server 90.9% / interop-client 91.9%
  gofmt -l go/x402/ clean
  go vet ./x402/... clean
  pnpm typecheck clean for changed files
Removes upstream PR identifier strings; keeps the behavioral
reference (commit tip) intact.
The exact verifier read transfer.tokenProgram from the on-chain
transferChecked instruction and used it to derive the expected
destination ATA, but never compared it against
requirement.Extra["tokenProgram"]. A malicious client could
satisfy an SPL Token requirement with a Token-2022 transfer (or
vice versa) because the destination ATA was derived from the
parsed program rather than the required one.

Mirror the Rust spine binding
(rust/crates/x402/src/protocol/schemes/exact/verify.rs:73-80) and
the PHP/Ruby/Lua ports: reject with
invalid_exact_svm_payload_transaction_token_program when the
on-chain program does not match requirement.Extra["tokenProgram"]
(or when the extra is missing/malformed).

Adds TestVerifyExactTransactionEnforcesTokenProgramBinding with
four subtests covering the mismatch (SPL->2022), reverse mismatch
(2022->SPL), positive control, and the missing-required-extra
branch.
Mirror Rust spine mints::USDT_MAINNET so USDT symbol resolution
succeeds on solana mainnet. USDT has no devnet/testnet entry,
matching spine. Adds USDT cases to TestResolveMintAlias.
Replaces the prior claim-first/release-on-failure replay-cache design
with the canonical L8 ordering: sendTransaction → getSignatureStatuses
poll (until confirmed/finalized OR explicit RPC error OR poll-budget
expiry) → put_if_absent(replay_key). Mirrors MPP server/charge.rs:535-556.

Replay key is now `x402-svm-exact:consumed:<base58_signature>` — scheme-
namespaced and keyed by the confirmed on-chain signature rather than
sha256(base64(encodedTransaction)). The signature is the global
uniqueness primitive; collisions surface the canonical duplicate_settlement
error without echoing a fresh PAYMENT-RESPONSE.

There is no release-on-failure path by design: a crash or RPC failure
before put_if_absent simply never inserts the key, and Solana's per-
signature replay protection prevents a re-broadcast from settling twice
within its blockhash window.

Tests cover: L8 RPC ordering (sendTransaction precedes
getSignatureStatuses precedes replay-store insert), pre-broadcast
failure does not consume the key, broadcast RPC failure does not
consume the key, on-chain confirmation failure does not consume the
key, already-consumed signature surfaces canonical duplicate_settlement,
and concurrent duplicate broadcasts collapse to one success +
N-1 duplicate_settlement. awaitSignatureConfirmation covered for
confirmed/finalized/on-chain-failure/RPC-error/timeout/transport/non-2xx.
Codex r7 rates 5/5 and confirms:
- Replay key namespace is exactly x402-svm-exact:consumed:<sig>.
- Settlement order is sendTransaction → awaitSignatureConfirmation
  → putIfAbsent.
- No claim-first path, no release-on-failure path.
- Tests cover ordering, broadcast failure, confirmation failure,
  pre-consumed key, and concurrent duplicate collapse.
EfeDurmaz16 added a commit to EfeDurmaz16/mpp-sdk that referenced this pull request May 26, 2026
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.
EfeDurmaz16 added a commit to EfeDurmaz16/mpp-sdk that referenced this pull request May 26, 2026
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.
@EfeDurmaz16

Copy link
Copy Markdown
Collaborator Author

Closing per maintainer's focus-on-Ruby guidance. The branch stays available on the fork so the work can be cherry-picked once the Ruby pattern is set and we replay the same shape across languages.

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.

2 participants