Skip to content

feat(svm): optimize upto facilitator settlements with transaction v1 - #17

Open
lgalabru wants to merge 17 commits into
feat/svm-transaction-versions-exactfrom
feat/svm-transaction-versions-upto
Open

lgalabru wants to merge 17 commits into
feat/svm-transaction-versions-exactfrom
feat/svm-transaction-versions-upto

Conversation

@lgalabru

@lgalabru lgalabru commented Sep 14, 2026 •

Copy link
Copy Markdown
Collaborator

Stacked on #16 (transaction-version negotiation for exact). This PR extends negotiation to upto and implements the facilitator-owned transaction behavior required by the upto SVM spec. It does not depend on #8.

What changes

  • The upto facilitator advertises transactionVersions: [0]; the client uses that only for its client-built open transaction and fails closed when v0 is unavailable.
  • Open verification remains legacy/v0-only. An open whose message version is outside that set is rejected before any instruction is inspected, and the facilitator reports it as invalidReason / errorReason unsupported_transaction_version, as the spec requires.
  • Facilitator-built claim, cancel, distribute, and reclaim transactions are independently constructed as transaction v1.
  • Every facilitator v1 transaction sets inline computeUnitLimit and loadedAccountsDataSizeLimit; the existing microlamports/CU setting is converted to v1's total lamport priority fee.
  • The settlement loadedAccountsDataSizeLimit defaults to 4 MiB. The runtime counts the program-data account of every invoked program, and mainnet Token-2022's is 1,382,061 bytes (devnet's is 711,053), so a 1 MiB budget would have failed every mainnet Token-2022 claim at simulation. Operators can override it with settleLoadedAccountsDataSizeLimit on the facilitator and rent-cleanup configs (TS and Go).
  • Reclaim cleanup greedily packs actual candidate transactions against all three v1 ceilings: 4,096 serialized bytes, 64 static account keys, and 64 top-level instructions. The current reclaim shape fits 62 distinct channels per transaction; the TS size estimate matches the @solana/kit 8 encoder byte for byte.
  • Raises the SVM SDK floors to @solana/kit >=8 and solana-go v1.23, the first versions used here with transaction-v1 construction support. The TS version-gate tests now use genuine v1 and legacy wire payloads instead of mocking the decoder.

Verification

  • TypeScript: format and lint clean; 704 unit tests pass with coverage; package build passes; workspace, examples, and e2e lockfiles refreshed.
  • Go: gofmt, go vet ./..., golangci-lint run ./... (v2.7.2, 0 issues), and go test ./... pass across the module.
  • Not yet exercised on a cluster: a devnet smoke run of the TS and Go v1 claim and multi-channel reclaim, ideally against a Token-2022 mint, is still recommended before release.

🤖 Generated with Claude Code

@lgalabru
lgalabru force-pushed the feat/svm-transaction-versions-exact branch from 929a210 to 76ebd65 Compare September 14, 2026 18:20
@lgalabru
lgalabru force-pushed the feat/svm-transaction-versions-upto branch from 950872d to ff46809 Compare September 14, 2026 18:20
@lgalabru
lgalabru force-pushed the feat/svm-transaction-versions-exact branch from 76ebd65 to 614d650 Compare September 14, 2026 18:24
@lgalabru
lgalabru force-pushed the feat/svm-transaction-versions-upto branch from ff46809 to f8f864e Compare September 14, 2026 18:24
@lgalabru
lgalabru force-pushed the feat/svm-transaction-versions-exact branch from 614d650 to 7a5dc4f Compare September 14, 2026 18:25
@lgalabru
lgalabru force-pushed the feat/svm-transaction-versions-upto branch 2 times, most recently from a135c3f to f53b855 Compare September 15, 2026 03:18
@lgalabru lgalabru changed the title feat(svm): negotiate transaction versions in upto (TS, Go) feat(svm): optimize upto facilitator settlements with transaction v1 Sep 15, 2026
@lgalabru
lgalabru force-pushed the feat/svm-transaction-versions-exact branch from fa2cdb4 to 68edcb3 Compare October 6, 2026 15:35
@lgalabru
lgalabru force-pushed the feat/svm-transaction-versions-upto branch from 7f88c04 to 73c6fdd Compare October 6, 2026 15:35
@lgalabru
lgalabru force-pushed the feat/svm-transaction-versions-exact branch from 68edcb3 to 631f006 Compare October 6, 2026 15:53
@lgalabru
lgalabru force-pushed the feat/svm-transaction-versions-upto branch 2 times, most recently from de98fa4 to 315f556 Compare October 6, 2026 15:55
@lgalabru
lgalabru force-pushed the feat/svm-transaction-versions-exact branch from 631f006 to c7b2814 Compare October 26, 2026 19:20
@lgalabru
lgalabru force-pushed the feat/svm-transaction-versions-upto branch 3 times, most recently from 0b77470 to 50addd4 Compare October 26, 2026 19:53
lgalabru and others added 8 commits October 26, 2026 13:39
…ndation#3221)

* docs(exact/svm): add multi-payee splits to the exact scheme

Adds an optional `extra.splits` array to the SVM `exact` scheme so a single
client payment can be distributed atomically across the merchant (`payTo`) and
up to eight additional recipients — for platform fees, revenue sharing,
referral commissions, and fee-payer cost recovery.

Design:
- Absolute amounts in base units (not basis points); the primary `payTo`
  receives the remainder `amount - sum(splits[].amount)`, which MUST be > 0.
- Each payment leg matches a distinct on-chain `TransferChecked`; split legs
  match exactly, the primary leg keeps the base overpayment tolerance.
- Fee-payer-funded ATA creation is a bounded, opt-in exception to fee-payer
  isolation: allowed only for split recipients marked `ataCreationRequired`,
  forbidden for the primary recipient and arbitrary owners.
- Backward compatible: absent/empty `splits` behaves as single-payee `exact`.

Adds a self-contained §5 to scheme_exact_svm.md with surgical pointers from
§1–§3, and a network-agnostic "Multi-Payee Splits" note to scheme_exact.md.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* Update specs/schemes/exact/scheme_exact_svm.md

Co-authored-by: phdargen <pdargent@icloud.com>

* docs(exact/svm): address review feedback on multi-payee splits

- Advertise split support in /supported (maxSplits, ataCreationSponsored)
  instead of leaving it as unadvertised local policy; absent means none.
- Allow fee-payer-funded ATA creation for the primary payTo as well as
  split recipients, gated by explicit opt-in on both sides
  (extra.ataCreationRequired / ataCreationSponsored) and client consent.
- Require CreateIdempotent over Create and let the client omit the
  instruction when the ATA already exists.
- Drop the informational per-split memo.
- Simplify the client-side ataCreationRequired rejection rule.

* docs(exact/svm): restore per-split memo as the client consent label

Keeps wire parity with splits[].memo in the MPP Solana charge draft and
gives wallets a label to show the payer for each leg. Still never
emitted on-chain and not verified by the sponsor.

* docs(exact/svm): generalize Path 1 cap and Path 2 isolation for splits

- §3.1/§5.7: replace the fixed 3–7 cap with 3+N+K to 7+N+K for split /
  ATA-creation charges, with positional leg matching, so standard
  wallets stay on Path 1.
- §3.2: Path 2 fee-payer isolation references the bounded
  CreateIdempotent exception.
- §2.2.2: exempt the ATA program for §5.4-conforming CreateIdempotent.
- §5.5: explain why maxSplits / ataCreationSponsored live in
  kinds[].extra (fail-closed) rather than a x402-foundation#2498 capabilities map.

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

* docs(exact/svm): make base rules split-aware and tighten ATA sponsorship

- §1.1 / parent: scope single-transfer outcome to single-payee; per-leg
  remainder rule with splits, parent amount/destination bullets.
- §3.4 / I4: post-settlement matches N+1 legs, per-leg balance deltas.
- splits[].amount: positive integer in u64 range; parent states the
  primary >= / split == asymmetry.
- §2.1.1: enumerate fee-payer positions in CreateIdempotent (funding
  payer, wallet owner) and the rent CPI; drop the once-per-mint bound
  (owner can close the ATA) and require sponsor policy for repeats.
- ataCreationRequired always means fee-payer-funded; drop client-funded
  creation, which contradicted the advertisement rule.
- Hints list covers only recentBlockhash / lastValidBlockHeight.
- splits[].memo is bound payment terms, shown with the address.

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

* docs(exact/svm): align memo consent in parent and I11, retitle I9

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

* docs(exact/svm): extend split layout and size limits to v1 transactions

Main now specifies v1 transactions (no Compute Budget instructions,
4096-byte limit, no ALTs). Scope the v1 1-5 layout to single-payee,
add the 1+N+K..5+N+K split window, use per-version size limits in
§5.2, and require loadedAccountsDataSizeLimit to cover the ATA program.

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

---------

Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Co-authored-by: phdargen <pdargent@icloud.com>
* feat(go): advertise and enforce SVM transaction versions in exact

Exact facilitators (current and legacy x402 v1 wire) publish
extra.transactionVersions ([0]) in /supported, servers copy it into
PaymentRequirements.extra, and clients fail fast with
unsupported_transaction_version when version 0 is not advertised. Verifiers
reject any message version other than legacy or 0 before inspecting
signatures or instructions; legacy stays accepted for backward compatibility
but is deprecated and never advertised. The shared helpers in svm/utils.go
are reused by the upto scheme in a follow-up.

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

* feat(ts): advertise and enforce SVM transaction versions in exact

Exact facilitators (current and legacy x402 v1 wire) publish
extra.transactionVersions ([0]) in /supported, servers copy it into
PaymentRequirements.extra, and clients fail fast with
unsupported_transaction_version when version 0 is not advertised. The exact
static and smart-wallet verifiers reject any message version other than
legacy or 0 before inspecting signatures or instructions; legacy stays
accepted for backward compatibility but is deprecated and never advertised.
The shared constants and helpers are reused by upto in a follow-up.

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

* feat(py): advertise and enforce SVM transaction versions

Facilitators publish extra.transactionVersions ([0]) in /supported, the
server copies it into PaymentRequirements.extra, and clients fail fast with
unsupported_transaction_version when version 0 is not advertised. Verifiers
reject any message version they do not model before inspecting instructions;
legacy messages stay accepted for backward compatibility but are deprecated
and never advertised.

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

* fix(svm): fail closed on transaction version metadata

* fix(svm): negotiate highest mutual transaction version

* fix(svm): fail closed on any non-integer transactionVersions entry

The merged spec (x402-foundation#3473) limits extra.transactionVersions to the integers 0
and 1 and requires a field with any other entry (string, null, "legacy",
unknown integer) to fail closed instead of being skipped.

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

* fix(svm): reject real v1 wire bytes before decoding; build the negotiated version

- TS and Python verifiers read the message version from the raw wire bytes
  before the SDK decoder runs, so a SIMD-0385 v1 payload is reported as
  unsupported_transaction_version instead of a decode error, whatever the
  installed kit or solders does with it.
- Tests feed real 0x81 payloads (Go-style retag and the SIMD-0385 layout)
  through verify and settle and assert nothing is signed or simulated.
- The Python clients compile the version resolve_transaction_version
  returns instead of discarding it.
- Go maps wire versions to solana-go MessageVersion explicitly instead of
  selected+1, which only holds for version 0.

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

* docs(specs): name the SVM v1 config mask bits and scope the Compute Budget rule

SIMD-0385 carries the v1 budget in TransactionConfigMask bits, not in a
TransactionConfig struct: bits 0-1 priority fee (u64 lamports), bit 2
compute-unit limit, bit 3 loaded-accounts data size limit, bit 4 heap size.
Name the bits, state that unset values request the minimum (the cost model
still reserves 32 KiB for a 0 data-size limit, but the budget is 0), and say
that rejecting Compute Budget instructions in v1 is an x402 sponsor rule,
since the runtime treats them as no-ops.

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

---------

Co-authored-by: Claude Fable 5.1 <noreply@anthropic.com>
The upto facilitator publishes extra.transactionVersions ([0]) in
/supported, the client fails fast with unsupported_transaction_version when
version 0 is not advertised, and the payment-channels open verifier and the
upto settlement path reject any message version other than legacy or 0
before inspecting signatures or instructions.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
The upto facilitator publishes extra.transactionVersions ([0]) in
/supported, the server forwards it, the client fails fast with
unsupported_transaction_version when version 0 is not advertised, and
verifyOpenTransaction plus the upto settlement simulation reject any message
version other than legacy or 0 before inspecting the instruction layout.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
lgalabru and others added 9 commits October 26, 2026 17:29
…he version reason

Facilitator-built upto settlements set a 1 MiB loadedAccountsDataSizeLimit,
but the runtime counts the program-data account of every invoked program and
mainnet Token-2022's is 1,382,061 bytes, so every mainnet Token-2022 claim,
cancel or cleanup distribute would fail at the pre-broadcast simulation.
Raise the default to 4 MiB and expose settleLoadedAccountsDataSizeLimit on
the facilitator and rent-cleanup configs in TS and Go.

The spec requires an open whose message version is outside the accepted set
to be rejected with unsupported_transaction_version; both facilitators
reported the generic open-transaction reason with the code buried in the
message. Map the version-gate failure to the spec reason on verify and settle.

Exercise the TS version gates with genuine transaction-v1 and legacy wire
payloads now that the workspace resolves @solana/kit 8, instead of mocking
the compiled-message decoder. Cap the rent-cleanup refetch at what the
remaining transaction budget can carry.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
…the encoded wire

- batch-settlement facilitators (TS, Go) advertise transactionVersions [0];
  servers already forward the facilitator extra, and the batch clients fail
  closed with unsupported_transaction_version before building a deposit,
  top-up, or refund the facilitator cannot accept. Matches the merged spec.
- facilitatorV1TransactionFits measures the fully serialized transaction,
  as Go does, with a test pinning the 4096/4097-byte boundary.
- Go upto GetExtra advertises a copy of AdvertisedTransactionVersions.
- Changeset calls out the @solana/kit >=8.0.0 peer floor.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
62 reclaims sharing one fee and rent payer fill the 64 static-account budget
of a v1 transaction within 4,096 bytes (matches pay-kit's calibrated
MAX_RECLAIMS_PER_TX_V1). The packer still splits any batch that exceeds the
wire, account, or instruction limits; v0 keeps its default of 8 and cap of 16.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The lockfile refresh replayed over a main that had moved on (axios bump),
leaving examples/typescript/pnpm-lock.yaml without an axios@1.18.0 entry.
Regenerate both from main's lockfiles plus this branch's package.json
changes; e2e needs no changes on top of main.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The e2e workspace links typescript/packages/mechanisms/svm, so its lockfile
must record the raised @solana/kit, program-client and @solana-program
specifiers.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@lgalabru
lgalabru force-pushed the feat/svm-transaction-versions-upto branch from a4ab86c to 64fe800 Compare October 26, 2026 21:30
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant