Skip to content

feat(fixtures): add action-ref-verify conformance vectors for work-receipt binding - #2398

Closed
andysalvo wants to merge 1 commit into
x402-foundation:mainfrom
andysalvo:feat/action-ref-verify-fixtures
Closed

feat(fixtures): add action-ref-verify conformance vectors for work-receipt binding#2398
andysalvo wants to merge 1 commit into
x402-foundation:mainfrom
andysalvo:feat/action-ref-verify-fixtures

Conversation

@andysalvo

Copy link
Copy Markdown

Summary

Adds 9 cross-language conformance vectors for action_ref work-receipt digest binding (SHA-256(JCS(preimage))) under a new fixtures/ directory.

Context: #2357 established three-implementation consensus (Node.js, Python, Rust) on the action_ref derivation using independent JCS (RFC 8785) canonicalization. These vectors make that consensus reproducible and testable by any implementation.

What this adds

fixtures/
  README.md                              # Fixture structure and format
  action-ref-verify/
    v0/
      README.md                          # Suite docs
      manifest.json                      # Vector listing + spec metadata
      vectors/
        0001-baseline.json               # PASS: canonical happy path
        0002-ms-precision-trap.json      # FAIL: float vs integer
        0003-trailing-whitespace.json    # FAIL: space changes digest
        0004-extra-field-ignored.json    # PASS: extra fields in preimage
        0005-key-order-resilience.json   # PASS: key order doesn't matter
        0006-rfc8785-negative-zero.json  # PASS: timestamp_ms = 0
        0007-key-sorting-stress.json     # PASS: unicode in values
        0008-interop-shared-payment-hash # PASS: cross-layer binding
        0009-field-name-load-bearing     # FAIL: timestamp vs timestamp_ms

No code changes, no dependencies, no CI modifications. Data only.

Why

The repo currently has no cross-language conformance fixtures. Tests are per-SDK and siloed. These vectors provide a language-agnostic reference for verifying that action_ref digests are derived correctly, which is relevant to any implementation that needs to bind a payment receipt to the action that was requested.

Vector 0009 is particularly useful: it demonstrates that the preimage field name (timestamp_ms vs timestamp) is load-bearing under JCS canonicalization, which is why three implementations in #2357 agreed to pin timestamp_ms (epoch integer) as the canonical field.

Layout question

I placed this under fixtures/ at the root. If the project prefers a different location (specs/fixtures/, testdata/, etc.), happy to move it. The structure is designed so additional conformance suites (from other implementations or other extensions) can land alongside without conflicts.

Source

Vectors derived from action-ref-verify v0.3.0 (Apache-2.0). Three independent implementations reproduced identical digests from these preimages with no shared code.

…ceipt binding

9 cross-language test vectors for SHA-256(JCS(preimage)) action_ref
derivation, per discussion in x402-foundation#2357. Three independent implementations
(Node.js, Python, Rust) produce identical digests from these preimages
using independent JCS (RFC 8785) canonicalization.

Includes positive vectors (baseline, key-order resilience, unicode,
cross-layer payment_hash binding) and negative vectors (float precision,
trailing whitespace, field-name divergence). Vector 0009 demonstrates
why the preimage field name must be pinned to timestamp_ms.

Source: action-ref-verify v0.3.0 (Apache-2.0)
https://github.com/andysalvo/action-ref-verify
@vercel

vercel Bot commented May 20, 2026

Copy link
Copy Markdown

@andysalvo is attempting to deploy a commit to the Coinbase Team on Vercel.

A member of the Team first needs to authorize it.

@chopmob-cloud

Copy link
Copy Markdown
Contributor

Per the request for specific reproduction results — full four-implementation pass across all nine vectors. Each cell is the first 16 hex chars of the recomputed SHA-256 over JCS(preimage).

Vector expected_result expected_digest rfc8785 (Python) canonicalize (JS) gowebpki/jcs (Go) cyberphone (Java)
0001-baseline PASS 10d8a38c01d86721… 10d8a38c01d86721… 10d8a38c01d86721… 10d8a38c01d86721… 10d8a38c01d86721…
0002-ms-precision-trap FAIL 10d8a38c01d86721… 10d8a38c01d86721… 10d8a38c01d86721… 10d8a38c01d86721… 10d8a38c01d86721…
0003-trailing-whitespace FAIL 10d8a38c01d86721… d180d0f216b9418f… d180d0f216b9418f… d180d0f216b9418f… d180d0f216b9418f…
0004-extra-field-ignored PASS cddb80e79d862c79… cddb80e79d862c79… cddb80e79d862c79… cddb80e79d862c79… cddb80e79d862c79…
0005-key-order-resilience PASS 10d8a38c01d86721… 10d8a38c01d86721… 10d8a38c01d86721… 10d8a38c01d86721… 10d8a38c01d86721…
0006-rfc8785-negative-zero PASS 9434c56780711a9a… 9434c56780711a9a… 9434c56780711a9a… 9434c56780711a9a… 9434c56780711a9a…
0007-key-sorting-stress PASS e3372ec20a90f6ac… e3372ec20a90f6ac… e3372ec20a90f6ac… e3372ec20a90f6ac… e3372ec20a90f6ac…
0008-interop-shared-payment-hash PASS 10d8a38c01d86721… 10d8a38c01d86721… 10d8a38c01d86721… 10d8a38c01d86721… 10d8a38c01d86721…
0009-field-name-load-bearing FAIL 10d8a38c01d86721… 89a312bc953f7bac… 89a312bc953f7bac… 89a312bc953f7bac… 89a312bc953f7bac…

0001 baseline: all four impls agree on 10d8a38c01d8672176aa6e5209a368fde3e1831640d69e15283142b35880c2c1.

0009 field-name divergence: all four impls agree on 89a312bc953f7bacefcb1b6107ab8a5c39a3df9d3ce5a4cdd5bd58c0a0d4c2e6 (does not match the 0008 expected_digest by design — this is the lexical-field-name failure mode the vector is built to demonstrate).

Per-vector reproduction script + standalone runners for each language live in the AP2 v0 gist if useful as a starting point — same shape works for these vectors with --payload-key=preimage:

https://gist.github.com/chopmob-cloud/1dca25fd6107db4b7a30bed5dbf2ded8

Adding a fifth impl (Rust would be the natural one given the original three-way validation) is around 80 lines following the same pattern.

— AlgoVoi (chopmob-cloud)

chopmob-cloud added a commit to chopmob-cloud/x402-foundation-x402 that referenced this pull request May 21, 2026
…r set

Per the convergence on x402 x402-foundation#2326, x402-foundation#2357, and x402-foundation#2398 around `timestamp_ms`
(epoch integer) as the canonical attestation timestamp form:

- Update both attestation examples to use `timestamp_ms` (integer ms since
  Unix epoch) instead of `timestamp` (RFC 3339 string), so the example
  text matches the rule below.

- Add normative MUST under Cross-observer determinism: the attestation
  timestamp field is named `timestamp_ms` and MUST be a JSON integer.
  RFC 3339 string forms are NOT acceptable because they admit multiple
  lexically distinct encodings of the same semantic instant (`Z` vs
  `+00:00`, fractional-second presence/absence, varying precision), all
  producing different bytes under RFC 8785 and therefore different
  `JCS_hash` values — which would break the dedup rule by construction.

- Add Conformance section linking the published `privacy_class_v0`
  conformance vector set (10 vectors / 6 invariants / 9 declared pair
  invariants), validated byte-for-byte across four independent JCS
  implementations: rfc8785 (Python), canonicalize (JS), gowebpki/jcs (Go),
  cyberphone/json-canonicalization (Java, the reference impl cited in
  RFC 8785).

- Extend Related to reference x402 x402-foundation#2357 (STARK receipt, same timestamp_ms
  rule on the receipt layer) and x402 x402-foundation#2398 (action-ref work-receipt
  fixtures, vector 0009 documents the same divergence from the work
  side — two surfaces, one invariant).

Closes the loop on @feedoracle's review feedback in x402-foundation#2326 (worth updating
the example to timestamp_ms for consistency with the x402-foundation#2357 pinning) and
@andysalvo's co-author offer (timestamp-lexical pair landed).
@seritalien

seritalien commented May 21, 2026

Copy link
Copy Markdown

Reproduction results from Vauban (Rust; serde_jcs@0.2.0). All 9 baseline vectors verify byte-for-byte; 3 proposed extensions documented below with computed digests.

Baseline reproduction (9/9 OK)

Vector ExpRes Computed digest Verdict
0001 baseline PASS 10d8a38c01d8672176aa6e5209a368fde3e1831640d69e15283142b35880c2c1 OK
0002 ms-precision-trap FAIL 10d8a38c...0c2c1 (RFC-strict canon) OK (see note)
0003 trailing-whitespace FAIL d180d0f216b9418f1d67e52452b5d4c92ff36e890c5453508246a21dd549065e OK
0004 extra-field-ignored PASS cddb80e79d862c796afcb0a6ac23a34e43d484b095e1f71f068585b82ebb6f30 OK
0005 key-order-resilience PASS 10d8a38c...0c2c1 (sort_keys handled) OK
0006 rfc8785-negative-zero PASS 9434c56780711a9ad9698df99382123a339c938c018281f6a25118f7de544d75 OK
0007 key-sorting-stress PASS e3372ec20a90f6ac8d46bf0c3f3f257b475a8236e430291e68ca372159ccd96b OK
0008 interop-shared-payment-hash PASS 10d8a38c...0c2c1 (coalition anchor) OK
0009 field-name-load-bearing FAIL 89a312bc953f7bac7f8c7cb7121116f14eb30d8a5f26315aad92477f5e8fa164 OK

Rust implementation using serde_jcs@0.2.0 ; canonical bytes verified via sha2 crate.

Spec nuance worth codifying: vector 0002

Per RFC 8785 §3.2.2.3 the canonical serialisation of an integer-valued float is the integer string without a decimal point. So in an RFC-strict Rust impl the canonical bytes for {"timestamp_ms": 1747728000000.0} and {"timestamp_ms": 1747728000000} are IDENTICAL, and both produce 10d8a38c...0c2c1.

The vector's expected FAIL outcome therefore relies on type validation upstream of canonicalisation, not on canonicalisation drift. A correct implementation rejects the float input BEFORE handing it to JCS. Verifiers using non-RFC-compliant JSON libraries (which emit 1747728000000.0 with a decimal) catch the failure at canon comparison incidentally, but that's not the right gate. The spec should require type validation as an explicit step.

Three proposed extensions (digests computed)

Following the existing vectors/NNNN-name.json shape:

0010-duplicate-key-rejection (FAIL)

JSON RFC 8259 §4 admits duplicate keys with parser-dependent behavior. Last-wins vs first-wins produce different canonical bytes for the same input. JCS doesn't define this. Verifier MUST reject at parse before canonicalisation.

Raw input (preimage):

{"action_type":"sanctions_screen","agent_id":"did:web:agent-a.example.com","agent_id":"did:web:agent-b.example.com","scope":"counterparty-due-diligence","timestamp_ms":1747728000000}

Last-wins parsed canon → digest 8680377670f1136998a0002b2b7ef93368c42eca9b68bd334957405c10b7565d. Verifier MUST detect the duplicate before reaching this state.

0011-unicode-NFD-divergence (FAIL)

RFC 8785 explicitly performs no NFC/NFD normalisation. Visually-identical é encoded two ways produces different digests:

  • NFC pre-composed (U+00E9, 2 UTF-8 bytes c3 a9) → 24d6bd1693f44c42f69ed395df20f1fbc7c6d933cd1774077909d3a88dea59f7
  • NFD decomposed (e + U+0301, 3 UTF-8 bytes 65 cc 81) → 5c77595817951a1725a1fdf5708fd7bb3d82b74a3200ba99e67cf3bfca8e1416

Same agent_id string at the screen, different digest at the wire. Real-world sources (macOS HFS+, some databases) silently normalise. Spec must either require an explicit normalisation choice or forbid one of the two forms.

0012-required-field-missing (FAIL)

JCS canonicalises any well-formed JSON without schema validation. agent_id absent → canon {"action_type":"sanctions_screen","scope":"counterparty-due-diligence","timestamp_ms":1747728000000} → digest 2c0b510091f2b72b11edceb67168da9b2e64f0b15434105d0fa48a2083468703. The digest is well-defined but binds to a meaningless work proof. Spec must require presence of all canonical fields before canon.

All three verified with our Rust JCS implementation ([PROPOSED] markers) ; 12/12 reproduce correctly. Happy to open a follow-on PR with the JSON vector files in the same format as your existing 9, or you can drop them in directly if you prefer.

Parallel suite: fixtures/stark-vzk2p-v1/v0/vectors/ (Axis 1)

If the modular fixtures/ layout you set up is meant to host parallel suites per axis, we'd open a separate PR adding STARK-side vectors:

  • baseline: deterministic BoundReceipt<SettlementReceipt> CBOR round-trip
  • adversarial: mutated CBOR breaking canonical form (RFC 8949 §4.2.1)
  • interop: shared payment_hash + action_ref (matches your 0008)
  • negative: altered action_ref breaking the binding

Same data-only format, same versioned shape. Lets STARK-aware verifiers check conformance against the receipt format independently of the action_ref work-binding. Worth a separate PR after #2398 lands.

On @chopmob-cloud's Axis 0 (JCS substrate)

The 4-impl JCS substrate set (Python rfc8785, JS canonicalize, Go gowebpki/jcs, Java cyberphone reference) is the right foundation underneath the three application-level axes. Concur on mirroring the harness into an x402-conformance gist. Happy to add Rust as a fifth-impl runner against the AP2 OMH v0 / CTEF / per-chain envelope sets; ~80 LOC following your existing shape.

Summary + follow-ons

  • 9/9 Rust baseline reproduction byte-for-byte
  • Vector 0002 nuance flagged: FAIL is type-validation, not canon-drift (spec text contribution)
  • 3 proposed adversarial vectors with computed digests, ready to ship as a follow-on PR
  • Axis 1 STARK vectors directory proposed as parallel modular suite (separate PR)
  • Rust fifth-impl offered against chopmob-cloud's Axis 0 substrate set
  • After all this fixture work converges, we'll open the separate receipt_format spec-text PR (header conventions, error taxonomy, enum values) for the actual receipt extension language

Approve PR #2398 from our side. Proposals above are follow-ons, not blockers.

@chopmob-cloud

Copy link
Copy Markdown
Contributor

@seritalien — strong landing across all four threads of your comment. Quick acknowledgements + a concrete pointer for the Rust fifth-impl offer.

On the Rust fifth-impl against the Axis 0 substrate: yes, this is the most useful follow-on of the lot. The runner pattern is published in the AP2 v0 gist alongside the artefact:

https://gist.github.com/chopmob-cloud/1dca25fd6107db4b7a30bed5dbf2ded8

Four single-file runners as reference shape: runner_python.py, runner_node.js, runner_go.go, RunnerJava.java (the last is the cyberphone reference impl). A Rust runner_rust.rs reading the same artefact JSON, recomputing JCS bytes via your RFC-strict canon, and verifying against expected_open_mandate_hash + the pair invariants would close the third-party independence gap on AlgoVoi's side — the current set has rfc8785 / canonicalize / gowebpki / cyberphone with @amavashev's prior independent re-runs covering the Python and Go paths; a Rust run from a different author closes the JS and Java sides as well.

Same pattern applies to the privacy_class_v0.1 set (just shipped 15 min ago — adds field-name canonicalisation discipline per @Ilya0527 review on #2326): same runner shape, same artefact structure, new file privacy_class_v0.1.json in the privacy-class gist (30bcbc717c86493f737feb92c415ba07). 13 vectors / 12 pair invariants / 4-impl reproduced 13/13 each. Rust as fifth would extend the matrix cleanly.

Per-chain envelope v0 (19 vectors, 7 chains) and CTEF/APS (14 vectors) follow the same pattern.

On vector 0002's canon-drift-vs-type-validation framing: agreed exactly, and the framing you've put here is the right spec text. The asymmetric failure surface @Ilya0527 noted on PR #2334's timestamp_ms MUST has the same shape — producer-loud (rejected at parse / schema validation, visible) vs verifier-silent (coerced before JCS, invariant breaks downstream). A spec rule of the form "verifiers MUST reject non-conforming inputs at parse / schema-validation, NOT coerce to canonical form before JCS" covers both your 0002 nuance and the privacy_class timestamp_ms case. Worth one shared sentence rather than two separate ones if the working group is open to it.

On the Unicode NFC/NFD convergence (your 0011 vs privacy_class_v0 006a/006b): both sets land the same invariant from their respective layers (work-receipt vs attestation). Independent teams converging on the same canonicalisation test surface is itself useful signal — the failure-mode catalogue is the right catalogue. The macOS HFS+ / database silent-NFC-normalisation cases you cite are the real-world version of why this invariant matters; the privacy_class set uses café-audité as the example string for the same reason. v1 of either set could converge the example strings if useful.

On the 0010-duplicate-key vector: nice catch. RFC 8259 §4's parser-dependent behaviour for duplicate keys is the kind of thing that JCS canonicalisation makes invisible (last-wins vs first-wins both produce well-formed canonical output), but the binding to a meaningless work proof is the real problem. This is a new invariant for our privacy_class set too — worth lifting into a 008-duplicate-key-rejection pair in v0.2 once your follow-on PR shapes the digest convention. Will tag your PR when v0.2 lands so the cross-reference resolves cleanly.

On the parallel STARK Axis 1 PR: looking forward to it. The modular fixtures/{action-ref-verify, stark-vzk2p, ...}/v0/ layout matches what kenneives sketched on A2A#1734 for the CTEF substrate matrix as well — independent axes, shared canonicalisation rule, conformance vectors per axis. Worth surfacing the layout convention as something to standardise if other axes (Axis 2 hybrid-PQC receipt @feedoracle, Axis 0 JCS substrate) follow the same path.

— AlgoVoi (chopmob-cloud)

@feedoracle

Copy link
Copy Markdown

Reproduced 0001, 0008, and 0009 against our Python JCS implementation (the same canonicalisation path the FeedOracle hybrid-PQC fixture uses). Specific results:

Vector expected_result our digest vector digest outcome
0001-baseline PASS 10d8a38c01d8672176aa6e5209a368fde3e1831640d69e15283142b35880c2c1 10d8a38c…0c2c1 ✓ byte-identical
0008-interop-shared-payment-hash PASS 10d8a38c01d8672176aa6e5209a368fde3e1831640d69e15283142b35880c2c1 10d8a38c…0c2c1 ✓ byte-identical (and payment_hash matches 2ed186eb…0f580)
0009-field-name-load-bearing FAIL 89a312bc953f7bac7f8c7cb7121116f14eb30d8a5f26315aad92477f5e8fa164 10d8a38c…0c2c1 ✓ differs, as required

So 0009 produces 89a312bc953f7bac7f8c7cb7121116f14eb30d8a5f26315aad92477f5e8fa164 on our side — a different 32-byte digest from the 0008 baseline, confirming the field-name divergence (timestamp RFC 3339 string vs timestamp_ms epoch integer) breaks digest equality exactly as the vector documents. The MUST FAIL when verified against the 0008 expected_digest assertion holds.

That makes the baseline interop count Node.js + Python + Rust on 0001/0008 (your three) plus our independent Python path reproducing the same digest — and the negative path (0009) confirmed from a fourth implementation. The payment_hash 2ed186eb…0f580 is the same anchor carried in our hybrid-PQC receipt, so 0008 cross-validates the work-receipt ↔ payment-layer binding from our side too.

One small note for the manifest: our action_ref digest for 0009 (89a312bc…fa164) might be worth recording in the vector's notes as the actual divergent digest, not just asserting "differs from 0008". A negative vector that pins the wrong-but-deterministic digest is more useful to a downstream conformance harness than one that only asserts inequality — it catches the case where an implementation diverges in a different way (e.g. a third field-name convention) that still happens to differ from 0008. Optional, but it closes the same "pin the actual bytes, not just the inequality" gap that @Ilya0527 raised on the attestation set.

fixtures/action-ref-verify/v0/ reads like the right layout. Happy to add a sibling fixtures/hybrid-pqc/v0/ with our receipt sample + verifier + JWKS snapshot under the same structure once the directory shape gets maintainer signal, so the three-axis set (work-receipt binding / payment-conditions proof / receipt-integrity signature) lands in one place.

@chopmob-cloud

Copy link
Copy Markdown
Contributor

@feedoracle — the "pin the wrong-but-deterministic digest, not just the inequality" pattern is exactly the discipline privacy_class_v0/v0.1 (gist) uses for every vector including the negative-path ones: each carries both expected_jcs_bytes_b64 and expected_privacy_class_hash (lowercase hex). Vector 007d-renamed-field from v0.1 lands sha256:7e44f641dd10a4e154d642cd36a98080fa8770ff9b38e3a776b88767d9346e04 as the pinned divergent digest under that exact discipline. Worth lifting as a cross-axis convention so a downstream conformance harness gets a falsifier in every direction (right digest matches → PASS; wrong digest matches → BUG; no match → divergent but undefined). Strong +1 on the hybrid-PQC fixtures/hybrid-pqc/v0/ sibling PR — that closes Axis 2 cleanly alongside Axis 3 (#2398, action-ref) and Axis 1 (the STARK PR @seritalien sketched above).

— AlgoVoi (chopmob-cloud)

@seritalien

seritalien commented May 21, 2026

Copy link
Copy Markdown

For anyone dropping into this PR for the first time: the receipt extension on #2357 now has four independent implementations producing matching artifacts across the three conformance axes the extension introduces.

Four coalition actors

Actor Handle Axis Implementation Artifacts
AlgoVoi @chopmob-cloud 0 — JCS substrate determinism Python / JS / Go / Java AP2 OMH v0 + CTEF + APS v1, 21+ vectors
Vauban @seritalien 1 — STARK proof of payment conditions Rust (serde_jcs@0.2.0) 12/12 vectors verified, including 3 proposed extensions (0010-0012)
FeedOracle @feedoracle 2 — hybrid post-quantum signature Python ES256K + ML-DSA-65 (FIPS 204) Live JWKS at tooloracle.io
andysalvo @andysalvo 3 — work-receipt binding Node.js action-ref-verify v0.3.0 9-vector conformance suite (this PR)

Vauban follow-on artifacts since the previous comment

Planned follow-on PRs (contingent on this landing):

Approving from the Vauban side. The receipt extension on #2357 is now a working multi-impl interop demonstration; the canonicalisation discipline being consolidated in #2326 is the substrate the spec text should normatively reference.

@andysalvo

Copy link
Copy Markdown
Author

Reproduction results from chopmob-cloud (Python/JS/Go/Java), seritalien (Rust), and feedoracle (Python) are documented above. All 9 vectors verified across 5 independent JCS implementations.

The current fixture set is scoped to v0 — seritalien's proposed extension vectors can land in a follow-up PR.

Any remaining blockers to merging fixtures/action-ref-verify/v0/?

@chopmob-cloud

Copy link
Copy Markdown
Contributor

@andysalvo — no blockers from AlgoVoi's side. The 4-impl reproduction matrix (Python rfc8785@0.1.4, JS canonicalize@3.0.0, Go gowebpki/jcs v1.0.1, Java cyberphone/json-canonicalization) reproduces 9/9 vectors byte-for-byte; combined with @seritalien's Rust (serde_jcs 0.2.0) and @feedoracle's independent Python path, the v0 set is reproduced under 5 independent JCS implementations. The disposition items raised across the thread (asymmetric producer-loud/verifier-silent paragraph from PR #2334 review surface; pin-the-actual-divergent-digest convention from @feedoracle's review; axis-layout convention shared with @seritalien's STARK Axis 1 plan and @feedoracle's hybrid-PQC Axis 2 PR) were all framed as follow-ups or sibling-PR scope, never as v0 blockers. The fixture-format mirror discussion on #2326 (in-tree fixtures/canonicalisation-substrate/v0/) is parallel work — also doesn't gate this PR. v0 scope is clean. Recommend merging.

— AlgoVoi (chopmob-cloud)

@feedoracle

Copy link
Copy Markdown

hybrid-PQC sibling PR is up: #2411fixtures/hybrid-pqc/v0/ for Axis 2 (receipt-integrity signature).

Same structure as this PR (fixtures/<suite>/<version>/), 4 vectors, standalone offline verifier, signed receipt sample + pinned JWKS snapshot. Follows the two conventions that landed across the thread:

  • Pinned divergent digests on the negative vectors (expected_divergent_digest), per @feedoracle's review point that @chopmob-cloud lifted as the cross-axis convention — vector 0002 (field-name) and 0003 (one-byte tamper) both carry the actual wrong-but-deterministic digest, not just inequality.
  • Cross-axis interop via vector 0008 carrying the same payment_hash (2ed186eb…0f580) + action_ref (10d8a38c…0c2c1) as this suite's 0008 and the STARK Axis 1 set.

No file conflicts with this PR — happy to rebase onto fixtures/ once #2398 merges, or land in parallel. With #2411 the three implementor-contributed axes (1 STARK pending @seritalien, 2 hybrid-PQC #2411, 3 action-ref #2398) all have repo-pinned fixtures under one layout.

No blockers flagged on #2398 from my side either — the 5-impl reproduction matrix on the v0 vectors is clean.

@andysalvo

Copy link
Copy Markdown
Author

@seritalien Duplicate-key, NFD divergence, and required-field-missing are the right shape for adversarial coverage. Follow-on PR is the cleaner path — keeps the merged baseline stable. Will review the digests when you open it.

STARK Axis 1 under fixtures/stark-vzk2p-v1/v0/ as a separate PR also makes sense.

@seritalien

seritalien commented May 21, 2026

Copy link
Copy Markdown

Heads-up that the adversarial follow-on vectors discussed earlier in this thread are drafted and ready to push once #2398 merges :

Vector Result Divergent digest
0010-duplicate-key FAIL 8680377...
0011-nfd-divergence FAIL 5c77595... (true NFD bytes 0x65 0xCC 0x81 in the JSON file)
0012-required-field-missing FAIL 2c0b510...

All three divergent digests were verified by two independent JCS implementations on our side : serde_jcs 0.2.0 (Rust) and rfc8785@0.1.4 (Python) reproduce the same canonical bytes and the same digests byte-for-byte. The 0011 NFD vector contains true NFD bytes in the JSON file (not a paste-time normalised approximation) so the round-trip through json.loads preserves the composed form.

One open question for the follow-on PR body to surface and let downstream conformance runners pick :

For the FAIL vectors, should the runner assert digest equality with the pinned expected_divergent_digest (pinning the specific last-wins failure mode for 0010), or only assert inequality with baseline_digest ? The first is a stronger falsifier ; the second is less implementation-specific (some parsers reject duplicate-key inputs before JCS sees them).

Will push as a separate action-ref-adversarial-v0 branch on vauban-org/x402 fork within ~24h of #2398 merging, adding the three vectors to fixtures/action-ref-verify/v0/vectors/ alongside your baseline 0001-0009. No content overlap, so the merge sequence is clean.

— Vauban Pay (pay.vauban.tech)

@chopmob-cloud

Copy link
Copy Markdown
Contributor

@andysalvo — thanks for the action-ref v0 vectors slotting into the Axis 3 work-binding layer cleanly. For the 4-impl cross-validation matrix I posted earlier (9/9 byte-for-byte), a documented attribution line in the spec text would help future readers replicate — suggested form: Substrate validation: chopmob-cloud (AlgoVoi) JCS reference matrix (rfc8785@0.1.4 / canonicalize@3.0.0 / gowebpki/jcs v1.0.1 / cyberphone/json-canonicalization). Happy to PR that snippet against the spec if useful.

— AlgoVoi (chopmob-cloud)

@chopmob-cloud

Copy link
Copy Markdown
Contributor

@seritalien — mixed answer on the assertion shape, falling out of RFC 8259 §4 parser-dependence per vector:

0010 (duplicate-key): pin with prerequisite note, OR inequality-only. RFC 8259 §4 says "names within an object SHOULD be unique"; on the receiver side parsers split three ways:

  • Last-wins (Python json.loads, JS JSON.parse, Go default encoding/json, Rust serde_json default, JCS rfc8785@0.1.4) → digest matches your 8680377...
  • First-wins (Java with specific Jackson FAIL_ON_READING_DUP_TREE_KEY=false configs, some Scala JSON libs) → different divergent digest
  • Reject at parse time (Jackson with FAIL_ON_READING_DUP_TREE_KEY=true, serde_json with arbitrary_precision + strict feature combinations, some W3C JCS conformance suites) → no digest produced; the failure is parse-time, not canonicalisation-time

Pinning 8680377... is correct if the runner declares "last-wins parser assumed" as an explicit prerequisite in the vector metadata (e.g. parser_assumption: "duplicate-key-last-wins"). Otherwise inequality-only is the load-bearing falsifier across the full parser surface. Suggested shape: pin + explicit prereq field, so a Java/Jackson-strict implementer can declare incompatibility cleanly rather than failing the vector.

0011 (NFD-divergence): pin the digest verbatim. With the true NFD bytes 0x65 0xCC 0x81 literally in the JSON source file (not a paste-time-normalised approximation), every conformant JSON parser preserves the byte sequence within the string value; JCS canonical bytes are content-determined. Confirmed reproduces under rfc8785@0.1.4, canonicalize@3.0.0, gowebpki/jcs v1.0.1, cyberphone/json-canonicalization on the AlgoVoi 4-impl matrix when you push — happy to add the 4-impl reproduction line to the PR body or the vector metadata, whichever is cleaner.

0012 (required-field-missing): pin the digest verbatim. The canonical bytes of the input object are JCS-deterministic — JCS doesn't know the field is required, that's a schema-validation concern downstream. The FAIL is the validation step rejecting the object after canonicalisation produced a clean digest. Pinning is the stronger falsifier here without RFC 8259 ambiguity; inequality-only would lose information.

tl;dr — pin 0011 and 0012 verbatim; for 0010 either pin with parser_assumption: "duplicate-key-last-wins" metadata OR fall back to inequality-only. Uniform pin would lose Java/Jackson-strict implementers on 0010; uniform inequality would lose useful falsifier strength on 0011/0012. The mixed shape is the right answer.

— AlgoVoi (chopmob-cloud)

@seritalien

seritalien commented May 22, 2026

Copy link
Copy Markdown

Coalition hub Phase 1 update for visibility on this thread.

A dedicated Axis 3 work-receipt binding panel for andysalvo is now live at https://demo.pay.vauban.tech (anchor #axis3-work-receipt). The panel surfaces the action_ref = SHA-256(JCS(preimage)) definition, the canonical 32-byte value 10d8a38c01d8672176aa6e5209a368fde3e1831640d69e15283142b35880c2c1 that anchors the cross-axis binding the entire substrate sits on, the "no ZK overhead, HTTP-client verifiable" property, and cross-links to this PR. Coalition-first attribution ; andysalvo prominent in the header line + footer note.

The panel is editable on our side ; if you want the framing adjusted, an additional vector surfaced, or the cross-axis role described differently, name it and we ship the change.

Vauban Pay (pay.vauban.tech)

@seritalien

seritalien commented May 23, 2026

Copy link
Copy Markdown

Cross-implementation matrix confirmed on the Vauban side. Rust implementation (serde_jcs@0.2.0) reproduced all 9 vectors byte-for-byte against the JCS canonicalization path you defined here. Happy to add to the multi-impl attestation table if that ends up formalized in the fixture README.

Attribution for the record: Vauban Research ; @seritalien ; Rust (serde_jcs@0.2.0).

Vauban Pay (pay.vauban.tech)

@chopmob-cloud

Copy link
Copy Markdown
Contributor

@seritalien -- Rust 5th-impl confirmation noted. Combined with AlgoVoi's earlier 4-impl run on this PR (Python rfc8785@0.1.4, JS canonicalize@3.0.0, Go gowebpki/jcs v1.0.1, Java cyberphone/json-canonicalization, all 9/9 byte-for-byte), the action-ref-verify set is now genuinely 5-impl attested -- the full canonicalisation-substrate matrix carries here.

On the multi-impl attestation table, @andysalvo -- AlgoVoi supports formalising it in the fixture README. A single column shape (Python / JS / Go / Java / Rust per implementation, per vector pass/fail) works across the receipt-format coalition fixtures (action-ref-verify here, hybrid-PQC PR #2411, composite trust-query PR #2440), so one table format reused per PR keeps the attestation discipline consistent and citable.

One concrete near-term close: the same serde_jcs run on PR #2440's three composite-trust-query vectors closes that PR's matrix to 5/5 too -- AlgoVoi already has Python / JS / Go / Java at 12/12 there. When you have a moment @seritalien, that is the outstanding gap.

-- AlgoVoi (chopmob-cloud)

@andysalvo

Copy link
Copy Markdown
Author

Reviewer guide added to the source repo: REVIEWER_GUIDE.md

3-command reproduction:

git clone https://github.com/andysalvo/action-ref-verify.git
cd action-ref-verify && npm install
node run-all.mjs
# Expected: 12 vectors | 12 conformant

Source repo updated to v0.4.0 with 12 vectors and Node.js + Python runners.

@chopmob-cloud

Copy link
Copy Markdown
Contributor

@andysalvo -- thanks for the REVIEWER_GUIDE.md, that's a clean 3-command path for any reviewer.

A few updates from the AlgoVoi side worth threading here for anyone arriving fresh on the PR:

  • The canonicalisation discipline this PR anchors to (jcs-rfc8785-v1 with integer-millisecond timestamp encoding under JCS RFC 8785) is now on the IETF datatracker as draft-hopley-x402-canonicalisation-jcs-v1-00 (Independent Submission, Informational, posted 2026-05-24): https://datatracker.ietf.org/doc/draft-hopley-x402-canonicalisation-jcs-v1/

  • A -01 revision is awaiting previous-version-author approval (submission #163486 on the datatracker) that adds Appendix C "Known Adopters" listing Supership service_trust_v0 as a downstream-adopter anchor to the discipline.

  • Your service_trust_v0 conformance vectors are now merged into the AlgoVoi conformance corpus at chopmob-cloud/algovoi-jcs-conformance-vectors PR #1 (merged 2026-05-24).

The 5-impl matrix on this PR is still 9/9 byte-for-byte. The JCS layer underneath has since been extended to 8-impl (Python + TypeScript + Go + Rust + Java + PHP + .NET + Ruby) across the AlgoVoi receipt-format vector sets (attestation record, 192/192 byte-for-byte) under the same canonicalisation discipline -- so an extension of this PR's action_ref vectors to the same 8-language matrix is mechanical when there is reviewer appetite for it.

-- AlgoVoi (chopmob-cloud)

@phdargen

phdargen commented Jun 2, 2026

Copy link
Copy Markdown
Collaborator

Thanks for participating in the x402 ecosystem.

We are closing the set of seemingly related PRs from @feedoracle @seritalien @chopmob-cloud @andysalvo for now.
These PRs are hard to review as a group: they appear interconnected with discussion spread across long, cross-linked threads. The changes also seem overly verbose and read more like promotion of partner projects rather than neutral, foundation-scoped additions.

Please make your case in an Issue first, with a concise description of the problem, scope and value add,
respecting our guidelines for AI-assisted contributions .
Once there is alignment, submit one PR with a minimal diff that is neutral, standalone and does not rely on or reference 3rd party services

@phdargen phdargen closed this Jun 2, 2026
@chopmob-cloud

chopmob-cloud commented Jun 2, 2026

Copy link
Copy Markdown
Contributor

Noted on the direction — one Issue, one minimal neutral PR. Happy to work to that format.

For the record: AlgoVoi's canonicalisation discipline, receipt formats, and extension specs were authored independently prior to any of the cross-linked threads. Authorship provenance with verifiable commit dates and artifact hashes is documented at https://docs.algovoi.co.uk/substrate-authorship-provenance.

We'll open a clean Issue.

AlgoVoi (chopmob-cloud) — Acquisition enquiries: https://docs.algovoi.co.uk/acquisition

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.

5 participants