Repository navigation
Description
Updated 2026-11-04. Widened from "the two inner field renames" to the whole
PaymentPayload, after diffing our schemas against the spec. The original text
is in the edit history; the two renames are items 1 below.
The Canton scheme spec and core v2 define PaymentPayload at two
levels, and we diverge at both. A conforming client's payload does not parse here,
and ours does not parse there.
The envelope
| ours | spec | |
|---|---|---|
x402Version: 2 |
same | ✅ |
scheme |
— | moves inside accepted |
network |
— | moves inside accepted |
payload |
same | ✅ |
| — | accepted Required |
the chosen PaymentRequirements, inline |
| — | resource Optional |
{ url, description, mimeType } |
| — | extensions Optional |
protocol extensions |
accepted is the substantive one: the payer echoes back the accepts[] entry it
chose, so the facilitator verifies against what the client actually saw rather
than re-deriving it.
scheme and network are a relocation, not a rename. Their values were
corrected in #83 (exact, and the registered canton:<name> ids) and that work
is done — the fields simply stop being top-level, because accepted already
carries them. Nothing about the ids is outstanding here.
The inner payload
| ours | spec | |
|---|---|---|
preparedTransaction |
same | ✅ |
preparedTransactionHash |
preparedTxHash |
rename |
partySignature (hex) |
signature (base64) |
rename + re-encode |
hashingSchemeVersion |
same | ✅ |
| — | assetTransferMethod |
missing; MUST be "transfer-factory", required in both extra and payload |
payer |
— | not in the spec |
publicKey |
— | not in the spec |
requirementsHash |
— | not in the spec |
signature is an encoding change, not a rename
The spec: "The payer's ed25519 signature (base64) over preparedTxHash."
Ours is hex. Renaming the field without re-encoding produces a payload that looks
conforming and fails every signature check.
Dropping payer / publicKey moves signature verification to the ledger
This is the item to think hardest about. Today:
// verify/canton-exact.ts
verifySignature(inner.preparedTransactionHash, inner.partySignature, inner.publicKey)and the payer's ::<fingerprint> suffix is checked against a fingerprint derived
from publicKey. The spec has neither field. Rule 8 derives the payer from
the decoded prepared transaction's sender — our decoder already exposes
DecodedTransfer.sender, so that part is a straight swap — and the facilitator
wraps signature into the ledger's partySignatures for that party.
The consequence: with no publicKey on the wire there is no key to verify
against, so the signature is no longer checkable in /verify. It is validated
by the ledger at execute time instead. /verify keeps every other check (shape,
transfer binding, deadline, preapproval) but stops being able to reject a bad
signature up front. That is what the spec prescribes; it should be a deliberate
decision rather than a side effect of deleting a field.
Removing requirementsHash is safe because of transfer binding
requirementsHash is our mechanism for binding a payload to the requirements it
was signed against. The spec's mechanism is different: accepted carries the
requirements inline, and Rules 4–6 bind the decoded transfer to them — amount,
receiver, instrument. We already implement that (findTransferMismatch), so the
binding survives the removal. It would not if transfer binding were dropped too.
Not in scope
Everything inside accepted is #76 (requirements). This issue carries
accepted as a field; its contents are that issue's problem.
Suggested split
| scope | notes | |
|---|---|---|
| 1 | preparedTxHash + signature renames, with the hex→base64 re-encode |
the original scope of this issue |
| 2 | assetTransferMethod in the payload |
additive |
| 3 | drop payer / publicKey; derive the payer from the decoded transfer |
decide the /verify trade-off first |
| 4 | drop requirementsHash |
depends on transfer binding staying |
| 5 | the envelope: accepted, resource, extensions; drop scheme / network |
largest; needs #76 first, since accepted is a PaymentRequirements |
Blocks ChainSafe/canton-x402-facilitator#340. Context: ChainSafe/canton-x402-facilitator#316.
Note: the two renames in item 1 are missing from the conformance analysis in #316 — they were found by diffing our schemas against the spec directly.
Metadata
Metadata
Assignees
Labels
Type
Fields
Priority
Activity
nikhilkumar1612 commentedon Oct 31, 2026
Hi @dhyaniarun1993, I’d be happy to work on this. Since I’ve already been working in the payment payload and verifier code for #56, this looks like a good follow-up. Can you please assign it to me if nobody else is working on it?
dhyaniarun1993 commentedon Nov 3, 2026
@nikhilkumar1612 Thanks so much for reaching out and taking the initiative! This is one of the more time-sensitive issues on our board and also requires some internal discussion, so we'll be handling this one internally for now. Really appreciate your willingness to contribute and your continued interest in the project.
nikhilkumar1612 commentedon Nov 3, 2026
No worries! thanks for the reply 😃. It'd be nice if there is a label dedicated for such issues. Would help me understand which issues are scoped for internal.
dhyaniarun1993 commentedon Nov 3, 2026
I agree that having a dedicated label for internally scoped issues would make things clearer for contributors. We will allocate some time for this. Thank you for the feedback.
chore(changeset): major, for the 1.0 spec-conformance release
[-]feat(core): accept the spec's inner payload field names[/-][+]feat(core)!: speak the spec's PaymentPayload[/+]feat(core)!: preparedTxHash and a base64 signature, as the spec names…
feat(core)!: preparedTxHash and a base64 signature, as the spec names…
feat(core)!: preparedTransaction is gzipped, and read as exactly one …
feat(core)!: the payload carries assetTransferMethod, as the spec req…