Skip to content
Open
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
2 changes: 1 addition & 1 deletion .github/workflows/conformance.yml
Original file line number Diff line number Diff line change
Expand Up @@ -277,7 +277,7 @@ jobs:
path: harness/results/
if-no-files-found: ignore

# Real on-chain settlement: forks mainnet (surfpool 1.4) and executes the
# Real on-chain settlement: forks mainnet (surfpool 1.5) and executes the
# deployed payment-channels program, so a green run proves settlement lands.
onchain:
name: onchain
Expand Down
3 changes: 3 additions & 0 deletions .github/workflows/npm-publish.yml
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,9 @@ on:
permissions:
contents: write
id-token: write
# The called CI workflow's path-filter job reads PR metadata; a reusable
# workflow cannot request more than its caller grants (startup_failure).
pull-requests: read

# Prevent concurrent publishes
concurrency:
Expand Down
3 changes: 3 additions & 0 deletions .github/workflows/pypi-publish.yml
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,9 @@ on:
permissions:
contents: write
id-token: write # PyPI Trusted Publishing (OIDC)
# The called CI workflow's path-filter job reads PR metadata; a reusable
# workflow cannot request more than its caller grants (startup_failure).
pull-requests: read

# Prevent concurrent publishes
concurrency:
Expand Down
5 changes: 3 additions & 2 deletions .gitmodules
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
[submodule "typescript/external/x402"]
path = typescript/external/x402
url = https://github.com/x402-foundation/x402.git
branch = main
url = https://github.com/solana-foundation/x402.git
branch = feat/svm-client-transaction-v1
[submodule "external/mpp-specs"]
path = external/mpp-specs
url = https://github.com/tempoxyz/mpp-specs.git
Expand All @@ -16,3 +16,4 @@
path = external/x402-txv1
url = https://github.com/solana-foundation/x402.git
shallow = true
branch = feat/svm-client-transaction-v1
2 changes: 1 addition & 1 deletion external/x402-txv1
Submodule x402-txv1 updated 612 files
220 changes: 220 additions & 0 deletions greptile.json

Large diffs are not rendered by default.

368 changes: 208 additions & 160 deletions harness/README.md

Large diffs are not rendered by default.

260 changes: 115 additions & 145 deletions harness/expectations/FINDINGS.md

Large diffs are not rendered by default.

144 changes: 105 additions & 39 deletions harness/expectations/RULINGS.md
Original file line number Diff line number Diff line change
Expand Up @@ -46,8 +46,10 @@ sentence fixes the shape of a client-paid transaction; it does not oblige a
client to accept the challenge, and Client-Side Verification leaves split
acceptance to the client. A client that will not fund that rent declines the
challenge, which is conformant; it never builds a transaction without the
instruction. Rust's low-level builder includes the creation by default, and
`PayKitClient`, whose spend cap covers only `amount`, declines.
instruction. Rust's builders decline unless the caller opts in
(`PayerFundedAtaCreation::Include`), since the rent sits outside any amount
cap; the conformance testee opts in so the vectors can pin the client-paid
shape.

## MPP core

Expand Down Expand Up @@ -103,27 +105,45 @@ accepts a credential without decoding its transaction does not conform.
16–128 characters of `[A-Za-z0-9_-]`. A server that advertises the extension
as required MUST reject a payload without a valid id.

**S5. Overpayment is tolerated by default.** scheme_exact_svm.md §1.4: “A
matching transfer MAY exceed PaymentRequirements.amount ... it MUST NOT be
less.” The generic scheme_exact.md SVM bullet (“Amount exactness: the
transferred amount MUST equal maxAmountRequired”) is listed under “Examples
include”, uses the v1 field name, and is superseded by the network binding's
normative §1 outcome definition. §2.3 lets a sponsor reject overpayment only
as an explicit stricter policy; default verifiers MUST accept it.

**S6. A sponsor bounds the priority fee as limit times price.**
**S5. Overpayment is tolerated; an exact-amount sponsor is conformant too.**
scheme_exact_svm.md §1.4: “A matching transfer MAY exceed
PaymentRequirements.amount ... it MUST NOT be less.” The generic
scheme_exact.md SVM bullet once required the transferred amount to equal
maxAmountRequired; it now agrees with the network binding (“Amount: the
transfer to `payTo` MUST be at least `amount`”). But §3.1 still says “exact
amount match are enforced on both layouts”, and §2.3 lets a sponsor refuse a
scheme-valid transaction (“A transaction MAY be valid under the `exact` scheme
while being rejected by a sponsor's policy.”). The Rust sponsor accepts
overpayment; `x402-exact-verify-overpayment-accept` pins that and also
admits `reject amount-mismatch` (`expect.alsoConformant`). Upstream: align
the §3.1 wording with §1.4.

**S6. A cap on limit times price is sponsor policy, not spec.**
scheme_exact_svm.md §2.2.1: “Sponsors SHOULD bound their fee exposure by
enforcing maximum compute unit limits and maximum compute unit (priority fee)
price”. Capping each field alone still lets the largest limit pair with the
largest price, so the fee the sponsor pays is bounded as their product. The
pay-kit sponsors cap it at 2,000 lamports; a 200,000 unit limit at 5,000,000
micro-lamports per unit (each within its own cap, a 1,000,000 lamport fee) is
refused as `compute-price-over-cap`. Vector:
`x402-exact-verify-priority-fee-over-cap-reject`. A version-1 header
`priorityFee` is the same product, so the cap applies to it unchanged; the
txv1 per-unit cap (5 lamports per unit) still holds below it, and
`x402-verify-v1-priority-fee-{at,over}-cap-*` pin that boundary at a 300 unit
limit, where it binds before the total does.
price”, with reference defaults of 400,000 units and 5,000,000
micro-lamports per unit, each capped on its own. Capping each field alone
still lets the largest limit pair with the largest price, so the pay-kit
sponsors also bound the fee they pay as the product, at 2,000 lamports; a
200,000 unit limit at 5,000,000 micro-lamports per unit (each within its own
cap, a 1,000,000 lamport fee) is refused as `compute-price-over-cap`. The spec
fixes no such total, so it is a stricter local policy (“Sponsors MAY
introduce stricter limits but MUST NOT relax the §2.1 MUSTs.”): the vectors
pin pay-kit's reject and also admit `accept` (`expect.alsoConformant`).
Vector: `x402-exact-verify-priority-fee-over-cap-reject`. A version-1 header
`priorityFee` is the same product; the txv1 per-unit cap (5 lamports per
unit) is spec and holds below it, and `x402-verify-v1-priority-fee-{at,over}-cap-*`
pin that boundary at a 300 unit limit, where it binds before the total does.
The same policy covers the payment-channel transactions an x402 sponsor
co-signs (upto `open`, batch `open`, `top_up` and refund `request_close`),
whose spec ceilings (400,000 units, 5,000,000 micro-lamports) are absolute
maxima a sponsor MAY tighten (“A sponsor MAY apply a stricter local policy for
compute-unit limit, compute-unit price, and required-signature count”);
without a declared limit pay-kit charges the price on the runtime default of
200,000 units per non-ComputeBudget instruction. Vectors (reject, or accept):
`x402-upto-verify-{priority-fee,implicit-limit-priority-fee,v1-priority-fee-over-sponsor}-*`
and `batch-verify-top-up-{priority-fee,implicit-limit-priority-fee}-over-cap-reject`,
`batch-txv1-top-up-v1-priority-fee-over-sponsor-cap-reject`.

## x402 upto on SVM

Expand Down Expand Up @@ -276,9 +296,12 @@ Associated Token program. Both SDK clients prepend a `CreateIdempotent` of the
subscriber's own ATA so a first-time wallet can activate; it moves no tokens.
Until the draft lists it, vectors accept that one instruction and reject any
other ATA creation (the recipient's, a third party's) as
`unexpected-instruction`: “MUST NOT reference writable accounts that could
redirect funds away from recipient.” Upstream: list the subscriber's
idempotent ATA creation explicitly.
`unexpected-instruction`, on the client list's closing MUST: the activation
must “contain no instructions other than those above plus optional
compute-budget and memo instructions.” Creating the recipient's ATA moves no
funds away from `recipient`, so the writable-account rule is not what refuses
it; it is an instruction the list does not name. Upstream: list the
subscriber's idempotent ATA creation explicitly.

**SUB2. Activation is `type="transaction"` only.** “Solana subscription
activation uses type="transaction".” and “Solana transactions containing
Expand All @@ -296,6 +319,28 @@ cancellation (`delegation.expires_at_ts`) equal to `now` has taken effect:
“a non-zero timestamp only blocks renewal once the current time is at or
after that timestamp.”

**SUB4. The subscription authority is initialized before the activation,
not inside it (spec bug).** The client list says the activation must
“contain an `initialize_subscription_authority` instruction prepended only
when the `(subscriber, mint)` authority does not yet exist on-chain;”. But
`subscribe` commits the subscriber's signature to the authority's `init_id`,
which the program sets when `initialize_subscription_authority` lands (from
that slot), and every later access check must “require its current `init_id`
to equal the delegation header's `init_id`.” A client that signs one
transaction carrying both instructions cannot know the slot it lands in, so
it cannot sign the `init_id` `subscribe` needs: the prescribed shape cannot be
built. The Rust client sends `initialize_subscription_authority` first, in
its own subscriber-signed and subscriber-paid transaction, reads the
resulting `init_id`, then signs the activation; the Rust server refuses an
activation carrying the instruction (`invalid-payload`). The vectors do not
depend on it: every subscription fixture serves an existing authority, so no
conformant activation carries the instruction, and
`subscription-verify-unrelated-authority-init-reject` refuses one for another
mint (`unexpected-instruction`). Upstream: drop the prepended initialization from
the activation list and say the authority MUST exist before the client signs
the activation (or let `subscribe` take the `init_id` from the same
transaction).

## x402 batch-settlement on SVM

Pinned `scheme_batch_settlement_svm.md` (x402 @03b3919b); transaction-version
Expand All @@ -310,9 +355,12 @@ the vector's `batchServerChannel`), so `charged + amount + 1` is rejected
with `cumulative-amount-mismatch`. A stateless facilitator cannot apply this
rule: it sees only onchain `settled`, and the txv1 draft gives it a floor
instead (“the facilitator MUST require `maxClaimableAmount >= Channel.settled +
PaymentRequirements.amount`”). An SDK whose only verify entry point is a
facilitator therefore fails the above-amount vectors; that is a known
failure of the facilitator-only testee, not a reading of the server rule.
PaymentRequirements.amount`”). The vectors whose verdict rests on the store
record alone (above, below and at the charged watermark, a close-pending
mark, a refund at an older accepted cumulative) carry
`batchVerdictFromStore: true`; a testee whose only verify entry point is a
stateless facilitator reports them unsupported, since it cannot express the
resource server they judge.

**B2. A client-mode refund carries the voucher.** “| `voucher` | `BatchVoucher`
| REQUIRED in client mode.” The `request_close` transaction is the retry path
Expand All @@ -331,19 +379,24 @@ requirement) is `invalid-challenge`; wire objects that do not parse are

Pinned drafts: `external/mpp-specs-txv1` (solana-foundation/mpp-specs
feat/solana-transaction-versions `77aa6a1`, charge and session) and
`external/x402-txv1` (x402-foundation/x402 PR 3473, solana-foundation/x402
`3d723dd`, scheme_exact_svm.md §1.5).
`external/x402-txv1` (x402-foundation/x402 PR 3473, merged; solana-foundation/x402
`f7043a1`, scheme_exact_svm.md §1.5).

**T1. `transactionVersions` is an array of the integers 0 and 1, read as a
set.** MPP: “Each entry is the integer `0` or `1`, the Wallet Standard
`supportedTransactionVersions` vocabulary without `"legacy"`.” x402: “Entries
are the integers `0` and `1`”. Neither text allows a string entry, a bare
MUST be the JSON integers `0` and `1` only.” Neither text allows a string entry, a bare
integer or another version, so an MPP charge or session request carrying one
is malformed and the SDK's request type refuses it (`invalid-payload` in the
wire vectors). A repeated entry names the same version: `[1, 1]` advertises
{1}, and a client builds version 1. x402 clients intersect the advertisement
with what they can build, so an unknown entry in an x402 offer is not by
itself a reason to refuse; the vectors pin no x402 verdict for one.
{1}, and a client builds version 1. x402 is explicit about the rest: “A
client or server MUST treat a field containing any non-integer entry (a
string, null, or "legacy") or any integer other than 0 or 1 as invalid and
fail closed with unsupported_transaction_version.” An x402 client therefore
refuses the whole offer (`unsupported-version`) rather than dropping the
entries it does not know and building from the rest
(`x402-build-unknown-entry-reject`, `x402-build-string-entry-reject`,
`x402-build-legacy-only-reject`).

**T2. Present and empty is not absent.** Only an omitted field defaults:
MPP “Defaults to `[0]` when omitted”, x402 “When the field is absent, the
Expand All @@ -366,12 +419,19 @@ advertised version. A lookup table, a size over the version's limit, or a
version-1 header without `computeUnitLimit` or `loadedAccountsDataSizeLimit`
is `invalid-payload`. A ComputeBudget instruction in a version-1 message is
`unexpected-instruction` for MPP charge (“Any unexpected instructions MUST
cause rejection.”) and `invalid-payload` for x402 (the static layout
mismatch). A charge or session server accepts exactly the versions its
challenge's `methodDetails.transactionVersions` lists; an x402 sponsor its
own configured set (`input.x402ServerTransactionVersions`), since “The
sponsor enforces its own accepted set; it does not read
`PaymentRequirements.extra.transactionVersions`.”
cause rejection.”), `invalid-payload` for x402 exact (the static layout
mismatch), `unexpected-instruction` for an upto open (an instruction outside
the open) and `setup-transaction` for a batch top-up. A version-1 header over
the compute caps is `compute-limit-over-cap` / `compute-price-over-cap` for
x402 exact and upto. A charge or session server accepts exactly the versions
its challenge's `methodDetails.transactionVersions` lists; an x402 sponsor
its own configured set (`input.x402ServerTransactionVersions`,
`batchServer.transactionVersions`), since “The sponsor enforces its own
accepted set; it does not read `PaymentRequirements.extra.transactionVersions`.”
That set has no default (“When the field is absent, the accepted set is
`[0]`” is about the advertised field), so every vector whose verdict depends
on it states it; an SDK whose sponsor cannot be narrowed reports those
unsupported.

## Signatures (both protocols)

Expand All @@ -396,6 +456,12 @@ both specs' verification steps.

## Harness consequences

- Where the specs leave a verdict to the implementation (a sponsor's
stricter local policy, two checks in no prescribed order, a rule stated for
another instruction), a vector pins the Rust kit's verdict and lists the
other conformant ones in `expect.alsoConformant`, with the reason
(`alsoConformantReason`). The judge passes either; an accepted build is
still judged on its artifact.
- Vectors encode these rulings. Runners pass chain state through the fixture
RPC (`input.rpcUrl`) and MUST NOT inject defaults, resolve token programs or
apply field precedence themselves.
Expand Down
10 changes: 6 additions & 4 deletions harness/expectations/go.toml
Original file line number Diff line number Diff line change
Expand Up @@ -208,7 +208,11 @@ ids = [
"vector/x402-build-no-intersection-reject",
"vector/x402-build-empty-advertisement-reject",
"vector/x402-build-legacy-only-reject",
"vector/x402-build-unknown-entry-reject",
"vector/x402-build-string-entry-reject",
"vector/x402-verify-v1-accept",
"vector/x402-verify-v1-loaded-data-limit-4mib-accept",
"vector/x402-verify-v1-loaded-data-limit-small-accept",
"vector/x402-verify-v1-not-accepted-reject",
"vector/x402-verify-v1-explicit-v0-sponsor-reject",
"vector/x402-verify-v1-over-1232-accept",
Expand Down Expand Up @@ -265,17 +269,15 @@ tracking = "FINDINGS.md#txv1-go"
ids = [
"cross/x402-build-duplicate-advertisement/rust/go",
"cross/x402-build-v1-advertised/rust/go",
"cross/x402-build-duplicate-advertisement/typescript/go",
"cross/x402-build-v1-advertised/typescript/go",
]
reason = "refuses a valid version-1 x402 build: the verifier cannot decode or admit version 1"
tracking = "FINDINGS.md#txv1-go"

[[unsupported]]
reason = "the runner's session verify path drives only the SDK's voucher verifier (input.sessionVoucherVerify); it has no path to the session server's open checks (input.sessionOpenVerify, input.sessionServerVerify)"
pattern = "vector/session-open-*"
[[known_failing]]
id = "vector/x402-exact-verify-priority-fee-over-cap-reject"
reason = "the x402 exact verifier caps the unit price alone (go/protocols/x402/verify.go:21), so the 200,000 unit limit at 5,000,000 micro-lamports per unit (a 1,000,000 lamport priority fee) is sponsored (RULINGS S6)"
tracking = "FINDINGS.md#x402-priority-fee-product-uncapped"

[[known_failing]]
ids = [
Expand Down
10 changes: 6 additions & 4 deletions harness/expectations/lua.toml
Original file line number Diff line number Diff line change
Expand Up @@ -148,6 +148,8 @@ ids = [
"vector/x402-build-no-intersection-reject",
"vector/x402-build-empty-advertisement-reject",
"vector/x402-build-legacy-only-reject",
"vector/x402-build-unknown-entry-reject",
"vector/x402-build-string-entry-reject",
]

[[known_failing]]
Expand Down Expand Up @@ -197,6 +199,8 @@ tracking = "FINDINGS.md#txv1-lua"
[[known_failing]]
ids = [
"vector/x402-verify-v1-accept",
"vector/x402-verify-v1-loaded-data-limit-4mib-accept",
"vector/x402-verify-v1-loaded-data-limit-small-accept",
"vector/x402-verify-v1-not-accepted-reject",
"vector/x402-verify-v1-explicit-v0-sponsor-reject",
"vector/x402-verify-v1-over-1232-accept",
Expand Down Expand Up @@ -253,10 +257,8 @@ tracking = "FINDINGS.md#txv1-lua"
ids = [
"cross/x402-build-duplicate-advertisement/rust/lua",
"cross/x402-build-v1-advertised/rust/lua",
"cross/x402-build-duplicate-advertisement/typescript/lua",
"cross/x402-build-v1-advertised/typescript/lua",
]
reason = "refuses a valid version-1 x402 build: the verifier cannot decode or admit version 1"
tracking = "FINDINGS.md#txv1-lua"
[[known_failing]]
id = "vector/x402-exact-verify-priority-fee-over-cap-reject"
reason = "the x402 exact verifier caps the unit price alone (lua/pay_kit/protocols/x402/exact/verify.lua:109), so the 200,000 unit limit at 5,000,000 micro-lamports per unit (a 1,000,000 lamport priority fee) is sponsored (RULINGS S6)"
tracking = "FINDINGS.md#x402-priority-fee-product-uncapped"
11 changes: 6 additions & 5 deletions harness/expectations/php.toml
Original file line number Diff line number Diff line change
Expand Up @@ -38,7 +38,6 @@ ids = [
"vector/charge-build-zero-amount-reject",
]


[[unsupported]]
reason = "php is server-only: x402 build-transaction is not supported (no client envelope builder)"
ids = [
Expand Down Expand Up @@ -143,6 +142,8 @@ ids = [
"vector/x402-build-no-intersection-reject",
"vector/x402-build-empty-advertisement-reject",
"vector/x402-build-legacy-only-reject",
"vector/x402-build-unknown-entry-reject",
"vector/x402-build-string-entry-reject",
]

[[known_failing]]
Expand Down Expand Up @@ -189,6 +190,8 @@ tracking = "FINDINGS.md#txv1-php"
[[known_failing]]
ids = [
"vector/x402-verify-v1-accept",
"vector/x402-verify-v1-loaded-data-limit-4mib-accept",
"vector/x402-verify-v1-loaded-data-limit-small-accept",
"vector/x402-verify-v1-not-accepted-reject",
"vector/x402-verify-v1-explicit-v0-sponsor-reject",
"vector/x402-verify-v1-over-1232-accept",
Expand Down Expand Up @@ -245,10 +248,8 @@ tracking = "FINDINGS.md#txv1-php"
ids = [
"cross/x402-build-duplicate-advertisement/rust/php",
"cross/x402-build-v1-advertised/rust/php",
"cross/x402-build-duplicate-advertisement/typescript/php",
"cross/x402-build-v1-advertised/typescript/php",
]
reason = "refuses a valid version-1 x402 build: the verifier cannot decode or admit version 1"
tracking = "FINDINGS.md#txv1-php"
[[known_failing]]
id = "vector/x402-exact-verify-priority-fee-over-cap-reject"
reason = "the x402 exact verifier caps the unit price alone (php/src/Protocols/X402/Exact/Verifier.php:184), so the 200,000 unit limit at 5,000,000 micro-lamports per unit (a 1,000,000 lamport priority fee) is sponsored (RULINGS S6)"
tracking = "FINDINGS.md#x402-priority-fee-product-uncapped"
Loading
Loading