Skip to content

Pre-release security hardening and CI/harness push (9 SDKs)#216

Open
EfeDurmaz16 wants to merge 113 commits into
mainfrom
audit/mega-fix-h1-l5
Open

Pre-release security hardening and CI/harness push (9 SDKs)#216
EfeDurmaz16 wants to merge 113 commits into
mainfrom
audit/mega-fix-h1-l5

Conversation

@EfeDurmaz16

Copy link
Copy Markdown
Collaborator

What this changes

Pre-release security hardening and a CI/harness push across all nine SDKs (typescript, rust, go, python, ruby, php, lua, kotlin, swift).

Security fixes, each ported to every SDK that implements the behavior:

  • Subscription activation now enforces a strict instruction allowlist and pins the sponsored fee payer to account index 0, so a subscriber can no longer append fund-draining instructions to a co-signed activation transaction.
  • x402 exact settlement reserves the payment signature before serving and releases it only on a proven not-landed outcome, closing cross-process double-serve windows on the pull path and under confirmation-timeout.
  • The x402 replay guard is now injectable with a shared put-if-absent store, so multi-replica deployments dedupe across processes instead of per-process.
  • Session top-up binds the raised deposit to the on-chain Channel account in the core method (go and python now match rust), and fails closed off localnet.
  • Batch settlement gates the paid serve on the in-lock committed watermark delta, closing an under-pay race.
  • The WWW-Authenticate challenge serializer escapes quotes and rejects CR/LF, and the x402 exact fee-payer fund-mover rule is unified across the six verifier ports with shared cross-SDK reject vectors.
  • Plus the lower-severity parity and robustness items: channel-status checks, transaction-version rejection, header size caps, voucher reject-tag unification, and idle per-channel lock eviction.

CI and harness hardening:

  • Release-gate legs fail on zero selected tests, the on-chain settlement gate hard-fails when its flag is unset, and python CI installs are frozen, so a rename or desync can no longer ship green with tests silently skipped.
  • Per-file coverage floors (go), frozen installs and least-privilege publish permissions, and a new repo-hygiene guard that blocks audit IDs left in shipped source and dependency pins stranded in package.json's pnpm field (ignored by pnpm v10).

Reproducibility:

  • rust/Cargo.lock is now tracked and pinned, toolchains are pinned, and the dependency security overrides are enforced from pnpm-workspace.yaml where pnpm v10 actually reads them.

Why

These SDKs are going live. The audit surfaced one critical and several high-severity issues, nearly all of them the same shape: a guard hardened in one SDK but never ported to its siblings, with no shared conformance vector to catch the drift. This lands the fixes with cross-SDK parity and closes the CI gaps that let that class of divergence ship unnoticed.

Tests

The full local CI matrix was run and is green:

  • TypeScript: lint, typecheck, 1053 tests pass.
  • Rust: cargo fmt, clippy with -D warnings, tests pass.
  • Go: tests pass, per-file and 91.0% aggregate coverage gate passes.
  • Python: 1331 tests pass, pyright reports 0 errors.
  • Ruby, PHP, Swift: green (Swift line coverage 95.4%).
  • Kotlin and Lua verified structurally; their runners (gradle 9.5.1, luarocks) are CI-only.

Every fix ships with a regression test that fails before the fix and passes after it. The x402 exact verifier gains shared cross-SDK reject vectors that every port must satisfy.

What to review especially

  • The subscription activation allowlist and the fee-payer index-0 pin (rust and typescript).
  • The consume-signature ordering across the x402 exact servers (rust, go, ruby, typescript) and the shared-store injection.
  • The in-core top-up deposit bind (go and python) against the rust reference.
  • The batch-settlement in-lock watermark gate.
  • The CI gate changes under .github/workflows and scripts/.

Note on size: this is the full audit branch, so it is large (99 commits, ~228 files). rust/Cargo.lock accounts for roughly 13k of the added lines. It is best reviewed by area using the list above.

Fable 5 reviewer note

This branch was put through a multi-model adversarial review before it opened: Claude (Fable 5) across four domain slices, plus independent passes from codex (GPT) and opencode (GLM). That review found one blocker and several high and medium issues, all since fixed and re-reviewed as closed. The blocker was that the strict subscription allowlist rejected the TypeScript client's own first-time-subscriber transaction; the client now pre-broadcasts the subscription-authority init as a separate transaction, matching the Rust client. The highs are also closed: the challenge-serialization guard now runs on the real 402 emission path, the false-green CI fix covers every release-gating leg, the repo-hygiene guard checks every package.json and catches both dashed and dashless audit IDs, and the new cross-SDK reject vectors are wired into CI. The batch-settlement per-channel gate is bounded and now has a regression test that actually fails if the gate is removed.

The full local CI matrix was run and is green: TypeScript, Rust (clippy with -D warnings), Go (91 percent coverage gate), Python (pyright clean), Ruby, PHP, and Swift. Kotlin and Lua were verified structurally since their runners are CI-only locally. Two items are worth a reviewer's attention: the harness assert-run-count values were not re-derived by hand and will fail loudly rather than pass silently if wrong, and Cargo.lock is committed and pinned.

verifyTransaction (type=transaction) reserved the consumed-signature marker
after broadcast but never checked it and took no per-signature lock, so the
same signed transaction could be verified concurrently or resubmitted, settling
one on-chain payment as many receipts. This is the pull-path sibling of the
push-mode TOCTOU fixed in #211.

Derive the fee-payer signature from the signed wire transaction before
broadcast (it equals the value sendTransaction returns) and run the
consumed-check, broadcast, reserve, and on-chain verify inside withKeyLock,
sharing the same solana-charge:consumed namespace as the push path. Release the
reservation only when the transaction definitively never landed, so an honest
retry of a still-valid transaction is not bricked.

Adds sequential and concurrent pull-mode replay regression tests.
verifySolTransfer compared String(info.lamports) === expectedAmount, where the
jsonParsed lamports is a JS number. Above 2^53 that number is lossy, so a
correct large SOL transfer could mismatch (the SPL path is safe because the RPC
returns amounts as strings). Parse lamports to an exact bigint with a
safe-integer guard and fail closed on a numeric value beyond the safe range.

Also format the interactive payment-page amount from bigint instead of
Number(amount) / 10 ** dec, which loses precision on large amounts (display
only; settlement already uses the exact string).

Adds a regression test that a system transfer with lamports at 2^53 is rejected.
process_open (push mode) trusted the client-supplied channelId, deposit, payer,
payee, and authorizedSigner, and only confirmed that *some* transaction
signature had succeeded. A client could therefore claim a fabricated deposit up
to max_cap against any confirmed signature and be served metered vouchers up to
that phantom balance, with settlement later failing.

When an rpc_url is configured, validate the payload against the challenge and
the derived channel PDA (reusing payment_channel_open_params), confirm the open
transaction succeeded, then fetch the on-chain Channel account and bind status,
mint, payee, authorized_signer, and payer to it, persisting the on-chain deposit
rather than the client's claim. Mirrors the x402 batch-settlement fetch-and-bind
path. Off localnet with no rpc_url the open now fails closed instead of trusting
the client.

Adds a fail-closed regression test.
replayStore defaulted to undefined, so the mpp adapter passed no store and each
cached charge handler built its own in-memory store. Replay markers were
therefore process-local: a second replica or a restart would accept a replayed
payment, and even within one process distinct handlers did not share the
consumed set.

Resolve the store once into a single shared instance (mirroring
resolveChallengeBindingSecret) and fail closed outside localnet when no store is
provided, unless PAY_KIT_ALLOW_INMEMORY_REPLAY_STORE=1 opts into single-process
scope. The error points operators at a shared, atomic-reserve backend.

Adds a fail-closed regression test and updates off-localnet config tests.
…model

Commit rust/Cargo.lock so CI builds, the coverage gate, and hashFiles cache
keys are reproducible; without it every build re-resolved floating transitive
deps and a fresh transitive release could break any branch. The workspace ships
binaries, so a committed lockfile is appropriate.

Add a Deployment threat model section to SECURITY.md covering the shared,
atomic replay-store requirement and the on-chain session-open binding, which the
keyLock and charge-verify comments reference.
The native-SOL on-chain verify compared info.lamports (a Lua number = double on
the jsonParsed RPC path under LuaJIT/Lua 5.1) via uint.compare. A u64 value at
or above 2^53 is lossy and serializes as scientific notation, raising
"invalid unsigned integer" on legitimate large payments and risking a
truncated-amount match. Add uint.exact to coerce only exact integers (rejecting
NaN/inf/negative/non-integral/out-of-range doubles) and guard both the SOL and
SPL amount comparisons; a rejected value is a non-match, falling through to the
canonical rejection. Adds regression tests that pass lamports as a number.
The MPP adapter fell back to an in-memory replay store with only a warning, so
off-localnet deployments got process-local replay markers that a restart or a
second replica would not see. Mirror the TS resolveReplayStore guard: fail
closed off localnet unless a store is injected or
PAY_KIT_ALLOW_INMEMORY_REPLAY_STORE=1 opts into single-process scope.
Go, Ruby, and Swift were already safe (Go/Ruby stores expose an atomic
put_if_absent checked on the broadcast path; Swift is client-only with lamports
only ever an exact UInt64). Add regression tests so the pull-mode replay guard
and the exact-integer lamports handling cannot silently regress into the
TypeScript failure mode.
…rt, npm env gate)

- Pin the rolling/branch-tracked actions to commit SHAs: rust-toolchain@stable,
  gh-action-pypi-publish@release/v1, install-action@cargo-llvm-cov. These refs
  literally move, so an upstream compromise would run in CI and the publish jobs.
- Add a Dependabot github-actions config to keep the pinned SHAs fresh.
- report.yml: drop contents:write to contents:read. It runs in the privileged
  workflow_run context over fork-PR artifacts and only downloads and comments;
  it never pushes.
- npm-publish: add a protected 'npm' environment gate to both publish jobs
  (mirroring pypi) and document npm OIDC trusted publishing.

Note: the version-tagged actions (actions/*@vn, etc.) remain on tags and are
Dependabot-tracked; a full SHA sweep is a mechanical follow-up. The surfpool
feat/sdk action ref in report.yml could not be resolved (branch renamed) and
still needs a SHA pin.
Mirror the Rust process_open fix: a push-mode session open must not trust the
client-supplied deposit, channelId, payer, or authorizedSigner. When an RPC is
configured, confirm the open signature, read the authoritative on-chain Channel
account, bind mint/payee/authorized_signer, and persist the on-chain deposit
rather than the client's claim. Off localnet without an RPC the open fails
closed. Adds a fetch_and_bind_channel_account helper and regression tests.
Complete the supply-chain hardening: every third-party action across all
workflows and composite actions is now pinned to a full commit SHA with a
version comment, so a retagged or force-moved upstream ref cannot inject code
into CI or the publish jobs. Dependabot (added earlier) keeps the SHAs fresh.

Local composite actions (./.github/actions/*) are unaffected. The surfpool
feat/sdk external action ref in report.yml still needs a SHA once the upstream
branch/ref is confirmed.
Both publish jobs now check npm for the target version before publishing and
fail if it already exists, so a re-dispatch or a forgotten version bump cannot
silently no-op or error mid-publish. Combined with the earlier protected
environment gate and OIDC provenance, this closes the L3 release-workflow gaps.
…sible

M4: add an off-chain golden test asserting the account order, signer/writable
flags, and discriminator for settle, settle_and_finalize, request_close, and
finalize. A codama regen or hand edit that reorders accounts or flips a flag
(the #202 class) now fails without a live program. settle_and_finalize stays
merchant-first by design (merchant must equal the channel payee).

M2: the on-chain settlement test skipped silently when the surfnet was
unreachable, so a broken or absent on-chain gate passed green. It still skips
locally, but fails hard when PAYKIT_ONCHAIN_REQUIRED=1 (which the CI on-chain
job sets), so the gate cannot pass without actually running.
new_session silently defaulted to an in-memory channel store, which is
process-local: a second replica or a restart drops the voucher watermark and
would accept a replayed voucher. Off localnet, fail closed unless a store is
provided or PAY_KIT_ALLOW_INMEMORY_REPLAY_STORE=1 opts into single-process
scope, mirroring the charge/pay-kit replay-store guard. Adds a regression test.
…adapter (H3)

The x402 adapter fell back to an in-memory replay store with only a warning, so
off-localnet deployments got process-local settlement-replay markers (a restart
or a second replica accepts a replayed settlement). Mirror the MPP adapter guard
(defaultReplayStore): fail closed off localnet unless a store is injected or
PAY_KIT_ALLOW_INMEMORY_REPLAY_STORE=1 opts into single-process scope.

Also fixes a regression the earlier MPP replay-store guard introduced but could
not catch without phpunit: Middleware/RequirePaymentTest builds a devnet client
whose adapters are auto-constructed with no store, so it now opts into
single-process scope. x402 adapter tests inject a store; adds x402 H3 tests.
Verified: full php suite 435 tests OK.
verifyChargeTransaction (pre-broadcast, pull) and verifyInstructions (on-chain,
push) are two copies of the same charge policy over different transaction
representations, so they can drift. Add a parity test asserting the same
tampered payment (underpayment, wrong recipient) is rejected by both paths.

A deeper structural dedup of the two verifiers is deferred: they operate on
different data shapes (compiled message vs jsonParsed RPC) and merging them is a
large, risky rewrite of the security-critical verifier better done outside a
pre-release change. The parity test is the anti-drift guard in the meantime.
…loor (M5)

The Rust coverage floor excluded both src/mpp/program/ and src/x402/, so the
instruction builders (the #202-relevant code) and the entire x402 implementation
were invisible to the gate. Now:
- src/mpp/program/ is inside the 90% mpp floor (verified 90.57%), its account
  layout additionally pinned by settle_family_account_layout_golden.
- src/x402/ gets its own ratchet floor at 75% (verified 77.50%) instead of being
  excluded, so x402 regressions are caught. Raising it to 90% needs new tests for
  server/batch_settlement (~37%) and server/upto (~63%), tracked as follow-up.

One instrumented run enforces both floors from per-file data.
Swift shipped tests with code coverage but enforced no floor, so it could
silently regress to near-zero. Add scripts/check_coverage.sh, which parses the
llvm-cov JSON SwiftPM exports (located via swift test --show-codecov-path, robust
to the arch-specific build layout) and gates Sources/ line coverage. Wire it
into swift.yml at an 80% ratchet (current Sources coverage is ~89%). Verified
locally: 89.40% >= 80%.
…sions (M5)

phpunit.xml excluded the security-critical x402 Verifier and Adapter from the
91% coverage gate, so their verification and settlement logic was untested by the
floor. Remove both exclusions and add branch-focused tests:
- VerifierBranchTest (31 cases): mutate one field per test to hit every rejection
  branch (compute-budget guards, transfer program/data/account, fee-payer
  authority, mint/recipient/amount, optional-slot allowlist, memo, missing fields)
  plus Token + Token-2022 happy paths.
- AdapterSettleTest (14 cases): verifyAndSettle end-to-end via FakeRpcGateway
  (v2 + v1 happy paths, payment-id extension gate, replay guard, broadcast
  failure, confirmation timeout, malformed payloads).

Verifier 69->97%, Adapter 67->95%, overall 92.83% (floor 91). Full suite 482 OK.
Cover the genuinely under-tested modules so x402 can be gated at 90 rather than
excluded. Aggregate line coverage: x402 77.5 -> 94.3%, mpp 90.6 -> 93.2%.

- x402 settlement servers (batch_settlement 37->94%, upto 63->93%, exact 89->92%)
  are now testable via a test-only in-process mock Solana JSON-RPC server
  (mock_rpc.rs, served over a std TcpListener, no new dependency, no production
  change): the concrete RpcClient reads config.rpc_url, so pointing it at the
  mock exercises the broadcast/confirm/channel-bind paths and every bind-mismatch
  branch.
- x402 client builders + protocol schemes + error (44/55/81/85/80 -> 94-99%) via
  pure error/edge-path unit tests.
- mpp subscription 63->91%, authenticate 84->98% (subscription has one
  behavior-preserving extraction of the terms-validation block so its mismatch
  branches are unit-testable).

Full lib suite 1095 passing, fmt clean.
…to >=90

mpp/error.rs, mpp/client/subscription.rs, and mpp/protocol/intents/subscription.rs
were 78-88% line; add error/edge-path unit tests to bring each over 90 line and
region. Full lib suite 1119 passing.
…+region

Cover the last core modules via the test-only mock JSON-RPC server:
- mpp/server/session.rs 88->98% line: the H1 on-chain open-bind path and every
  bind-mismatch branch (mint/payee/signer/payer/status/deposit), asserting the
  persisted deposit equals the on-chain value not the client's claim.
- mpp/server/charge.rs 88->94% line: pull-mode broadcast/confirm and push-mode
  on-chain verify (transfer match, allowlist, memo, ATA owner, mint resolution).
- core/settlement/worker.rs 80->95% line: RpcBroadcaster + actor drain/regroup
  and the confirm-loop Failed/transient/pending arms via scripted broadcasters.
- mock_rpc.rs (test-only) gains getTransaction + set_transaction and a corrected
  failed-status fixture.

mpp 96.8% line / 96.7% region, x402 94.4% / 93.8%. Full lib suite 1210 passing.
…covery)

blockhash.rs had no tests. Add the empty-get, set+fresh-get, stale-entry
(seeded with an old Instant so the MAX_AGE false arm runs without a 45s wait),
and poisoned-lock recovery cases, bringing it >=90 line and region.
Cover the SIWX Default impl, chain selection (supported + unsupported), header
create/encode, and every verify error branch (nonce mismatch, issued-at too old,
not-before/expiration bounds, malformed/pre-epoch RFC3339). 91.4->97.8% line,
89.3->95.7% region.
… (M5)

Replace the mpp>=90 / x402>=75-ratchet split with a single strong gate: both mpp
and x402 must hold >=90% on LINE and REGION (llvm-cov's branch-like metric), and
the floor is enforced PER-FILE as well as in aggregate, so a weak file cannot
hide behind inflated easy ones and no branch class can silently regress. x402 is
now fully inside the floor (was excluded). The test-only mock JSON-RPC server is
excluded. Verified against a real run: mpp 96.8/96.7, x402 94.9/94.2, every file
>=90 on both metrics, 1224 lib tests passing.
Add coverage tests for the least-covered Sources files (X402 Exact Types
61->99%, RpcClient 52->96%, Transaction 75->90%, SolanaPayKit 84->98%, plus
SolanaSigner/Ed25519/x402 client guards) via dedicated URLProtocol stubs, each
suite isolated so Swift Testing's parallel suites don't race on shared stub
state. Sources aggregate 88->95%; raise the CI floor from 80 to 90. 231 tests
passing.
The TypeScript reference SDK collected coverage but enforced no floor, and sat at
79% line / 69% branch. Exclude the Codama-generated client and barrel files from
coverage (matching the Rust src/generated exclusion), then add real tests across
the under-covered surface:
- pay-kit adapters (mpp/x402/x402-upto/mpp-session 44-59 -> ~100%), paykit.ts
  (70 -> 98%), errors/express-routes/protocol.
- mpp server Charge.ts (81 -> 98% line / 91% branch), Session.ts (87 -> 94/91),
  session/on-chain.ts (91/71 -> 99/93), session/lifecycle.ts (0 -> 100),
  Subscription.ts (93/75 -> 98/91), challenge-guard.
- mpp client PaymentChannels/SessionFetch/MultiDelegate/Charge/Subscription and
  shared/voucher branch coverage to >=90.

Aggregate: line 97.8, branch 91.9, functions 97.6, statements 96.4. Add a vitest
thresholds block (90 on all four metrics) so the reference SDK is now gated like
the others. 955 tests passing.
…payer to index 0 (C1)

The subscription activation path co-signs the client-supplied transaction
with the server's sponsored fee-payer key. Two gaps let an attacker abuse
that signature:

1. validate_activation_scope scanned for the subscribe/transfer pair and
   silently skipped every other instruction, so a client could append an
   arbitrary instruction (e.g. a System or SPL transfer draining the
   fee payer) and still pass scope validation.
2. co_sign_as_fee_payer located the fee-payer key with position() and
   signed wherever it appeared, so the key could be placed at a non-zero
   index as the authority/source of an attacker-inserted instruction and
   still collect the server signature.

Enforce a strict program allowlist (subscriptions program subscribe/
transfer only, plus compute-budget, idempotent-ATA, and memo aux
programs) and reject anything else instead of skipping. Require the fee
payer to be account index 0 (Solana's fee-payer slot) before signing.

Add regression tests: reject an instruction on an unknown program, an
out-of-range program index, a disallowed subscriptions-program
discriminator, and a fee-payer key at a non-zero index; accept the full
legitimate activation shape.
…enticate verifiers (M3)

The subscription and SIWX-authenticate verifiers compared the client-
supplied challenge id against the recomputed HMAC with String equality,
which short-circuits on the first differing byte and leaks a timing
side channel usable to recover a valid id byte-by-byte. Use the existing
constant_time_eq helper, matching the charge verifier.
…ture mode (H3)

Signature (push) mode presented an already-confirmed on-chain transaction
and accepted it after only checking the transfer amount/mint/recipient.
That let a single confirmed transfer be replayed to satisfy unlimited
requests, any same-priced route, and arbitrarily old transactions, since
there was no consumed-signature store and no freshness check.

Add three guards to the Signature arm of verify_envelope_payload:
- require a route-bound memo (extra.memo) so a push payment is tied to a
  specific challenge, not just an amount+recipient (verify_transaction_details
  then enforces the tx actually carries it);
- enforce freshness against max_age using the tx block_time;
- atomically consume the settlement signature via a pluggable Store
  (default in-process MemoryStore; with_store injects a shared one for
  multi-replica deployments), mirroring the MPP charge consume_signature.

Add Error::StaleTransaction and Error::MissingSignatureBinding. Regression
tests via the mock JSON-RPC cover: missing-memo rejection, fresh+bound
success then replay rejection, stale-transaction rejection, and shared-store
single-use across two instances.
…nd IO-cap plugin autoload

- PHP x402 harness server: inject a process-scoped FileStore into the
  X402 adapter so it no longer fails closed on the non-localnet harness
  network, mirroring the MPP charge path.
- python-x402 / python-x402-upto clients: run through the SDK uv env
  (uv run --project ../python) like python-session so runtime deps such
  as httpx resolve instead of failing under a bare python3.
- IO-cap CI step: disable the anchorpy pytest plugin on the command line
  (-p no:pytest_anchorpy) so it does not autoload and crash importing the
  uninstalled pytest-xprocess; addopts alone did not apply for a target
  outside python/.
@EfeDurmaz16

Copy link
Copy Markdown
Collaborator Author

Thanks — all three review findings are resolved:

  1. P1 async blocking sleep (x402 exact confirm_pull_settlement): replaced std::thread::sleep with tokio::time::sleep().await so the confirmation poll yields to the executor instead of holding the Tokio worker thread for up to 6s. (commit 7447faa)

  2. P1 TOCTOU on the subscription activation replay guard: replaced the non-atomic store.get-then-store.put with the Store trait atomic put_if_absent, so exactly one of two concurrent identical activations claims the marker and issues a receipt while the other is rejected as consumed. Added release-on-failure (a transient RPC error releases the marker so a legitimate retry is not bricked) and two regression tests that fail on the old get-then-put and pass on the fix. (commit 30c0085)

  3. P2 Go/Python channel-fetch duplication: extracted a shared fetchAndValidateChannel / _fetch_and_validate_channel helper in both SDKs; each caller keeps only its divergent tail (authorizedSigner for bind, deposit for top-up). Behavior, error messages, and validation order are byte-identical. (commit 07c2dc2)

@greptile-apps please run a full general re-review of the whole branch (not limited to these areas).

The go-x402 harness client (go-client) still pinned the removed lgalabru
solana-go fork in its go.mod/go.sum, so 'go run .' failed to resolve deps.
go mod tidy moves it to upstream, matching go/ and harness/go-server.
go mod tidy alone kept the explicit replace => lgalabru/solana-go directive,
so the go-x402 client still tried to fetch the unreliable fork in clean CI.
Remove the replace so it resolves upstream gagliardetto/solana-go v1.22.0,
matching go/ and harness/go-server.
@EfeDurmaz16

Copy link
Copy Markdown
Collaborator Author

@greptile-apps review

Rust used solana-x402-exact:consumed:, Go and TypeScript used
x402-exact:consumed:, while Python, Ruby, and PHP used x402-svm-exact:consumed:.
Two differently-languaged replicas sharing one replay store wrote to different
namespaces and would not dedupe each other's replays. Standardize every SDK on
x402-svm-exact:consumed: so a shared, cross-process store protects across all
language servers.
@EfeDurmaz16

Copy link
Copy Markdown
Collaborator Author

Resolved the cross-SDK replay-namespace divergence: unified the x402 exact consumed-signature key prefix to x402-svm-exact:consumed: across every SDK (Rust used solana-x402-exact:, Go and TypeScript used x402-exact:; Python/Ruby/PHP already used x402-svm-exact:). A shared cross-process store now dedupes replays across differently-languaged replicas. Verified: Rust 54, Go, and the TypeScript x402-adapter (31) tests pass. (commit 201e8fd)

@greptile-apps please re-review.

The RELEASE_SAFE_SETTLE_REASONS set hard-codes @x402/svm's errorReason strings;
note the vendored 2.16.0 version so an upgrade prompts re-deriving the set
rather than silently reclassifying a payload between release and keep.
@EfeDurmaz16

Copy link
Copy Markdown
Collaborator Author

@greptile-apps review — all inline findings are resolved: the async sleep (tokio::time::sleep, 7447faa), the subscription replay TOCTOU (atomic put_if_absent + release-on-failure + regression tests, 30c0085), the cross-SDK consumed-key prefix (unified to x402-svm-exact:consumed:, 201e8fd), and the @x402/svm reason-set version pin (ef6e977). Please run a fresh full review of the latest commit.

@EfeDurmaz16

Copy link
Copy Markdown
Collaborator Author

Resolved the remaining finding — the TypeScript subscription activation TOCTOU. settleActivation now claims the consumed key atomically via claimConsumed under withKeyLock in BOTH transaction and signature modes (before broadcast), releases the reservation on every failure path so a transient error does not brick a retry, and keeps it only when a receipt is produced. This mirrors the Rust fix (30c0085) and the charge path's usage. Added two regression tests (concurrent-activation admits exactly one; release-on-failure allows retry), both verified to fail on the old get/put and pass on the fix. Full @solana/mpp suite: 1057 tests pass. (commit ca8396e)

@greptile-apps please run a fresh full review of the latest commit.

@EfeDurmaz16

Copy link
Copy Markdown
Collaborator Author

@lgalabru PR #216 (the pre-release security hardening and CI/harness push across all nine SDKs) is ready for your review and merge: CI is green, Greptile is at 5/5 with every finding resolved, and all 100+ commits are signed and Verified.

@solana-foundation solana-foundation deleted a comment from greptile-apps Bot Jul 4, 2026
@EfeDurmaz16

Copy link
Copy Markdown
Collaborator Author

@greptile-apps please run a fresh full review of the latest commit. better if you can do TREX review!

lgalabru pushed a commit that referenced this pull request Jul 10, 2026
EfeDurmaz16 added a commit that referenced this pull request Jul 13, 2026
python.yml's missing-ATA step is owned and gated by the Python leaf
(#228); this harness leaf asserts only the harness.yml step it modifies.
Both leaves make the step blocking; the step-name reconciliation lands
at #216 integration.
EfeDurmaz16 added a commit that referenced this pull request Jul 14, 2026
…216 equivalence ledger

Those five redelivery PRs landed on main rather than the #219 line, so their
open_pr followUps are corrected from 'Land PR #X' to note the merge and the
revalidation that follows when #219 rebases onto main. Validator + self-test
still pass (113 commits, 237 paths accounted for).
EfeDurmaz16 added a commit that referenced this pull request Jul 14, 2026
…lidator

Composing the rust hardening with the mpp subscription/session work makes the
activation validator strictly stronger: every activation carries exactly one
canonical SetComputeUnitLimit; an existing delegation account is decoded (a
placeholder byte is no longer 'absent'); an idempotent receipt requires the
exact submitted activation signature to be confirmed on-chain; the sponsored
path enforces the full canonical subscribe/transfer layout, the two idempotent
ATA creations, and the live SubscriptionAuthority init id before co-signing;
and a challenge that omits recentBlockhash now fetches a fresh one instead of
erroring (2bcee2d).

Update the fixtures to that contract without weakening any assertion: the
rejection tests still reject for their original reasons (divergent terms,
broadcast failure), the idempotent retry asserts the exact confirmed
signature, the pre-#216 'no history omits the signature' behavior is replaced
by its fail-closed successor (reject + release the claim so a retry can win),
and the fee-sponsored test now builds the full canonical activation with the
production subscribe/transfer builders.
EfeDurmaz16 added a commit that referenced this pull request Jul 14, 2026
Rebuilds rehearsal/pr216-integration purely from the current leaf heads in
the canonical merge order. Conflicts abort loudly: the resolution belongs in
the later leaf of the conflicting pair, never in the rehearsal branch, so a
green rehearsal is green by construction.
EfeDurmaz16 added a commit that referenced this pull request Jul 14, 2026
…osed cascade

Re-run the reconcile against the tree that now carries every redelivery leaf
(the same tree #240 rebuilds from the leaf heads): two harness paths a sibling
leaf superseded move from integrated to open_pr evidence, and the delivery
baseline advances. Validator and self-test pass; the blocking radar scan on
this tree reports zero findings.
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.

1 participant