That’s exactly the right shape – 8004 answers “who is this and can I trust its history,” L402 answers “has this specific call been paid for.” Redesigning around both from the start beats bolting one on later. Good luck with it, and feel free to come back with specifics once you’re deeper in – happy to look at the design.
Let’s host it on the ERC-AWAR (8350) side — memory-state semantics should live next to the spec they constrain — and it’s already up, so review mine:
It captures exactly what we settled here, credited to both specs: refs live in provenanceBytes (the seven-field struct genuinely stays frozen — your framing), unordered-set-by-JCS sorted by decision_ref, identity is (event_id, pubkey), and verify_url excluded from every hash. I also lifted your observation into §4 more or less verbatim: the discipline already held one layer down in decision_ref, and we’re just applying it one level up to the container. Honestly the neatest part of this exchange — the fix was already latent in your design.
One section I added that we didn’t discuss: §5, non-claims for the composition. Compositions are where guarantees quietly inflate — someone will eventually read “verdict + committed transition” as “verified memory,” and I’d rather both specs be on record that neither claims that. Tell me if any of it overstates or understates what WYRIWE’s side actually warrants.
A cross-link from preaction-governance-conformance would be great. And if your three-party worked example ever wants a memory-state leg: the Sepolia registry is live at 0xDdf21937ba80b5fF973610877A0955b320C91241 with reproducible vectors, so a conformance case could run against a real deployment instead of fixtures.
PRs welcome on the note itself; you have my review by default.
Reviewed line by line, checked the decision_ref field list against our live source (services/proof_signing.py’s DECISION_REF_PREIMAGE_FIELDS), not just against the thread text – byte-accurate, all 6 fields including vantage_limitation. §4’s verify_url exclusion matches exactly: it was never in the preimage, so this note is stating an existing invariant, not inventing a new one.
Section 1’s framing is the sharpest part – “causal input, not a post-hoc annotation” is the right distinction and it’s the one that actually matters legally/operationally (an authorizer approved a delta whose provenance bound that verdict, vs. a verdict glued on after the fact that nobody committed to). Good that it’s stated plainly rather than left implicit.
Concrete next step, since this reads as settled prose now: I’ll build the conformance vector for it in babyblueviper1/preaction-governance-conformance – a checker that verifies a real ERC-8337 implementation’s provenanceBytes reference set actually follows §3’s rules (JCS-sorted by decision_ref, deduped by (event_id, pubkey), verify_url excluded from the hashed entry). Same discipline as every other recipe in that registry: prose that can be skipped vs. a failing test that can’t. Taking you up on the Sepolia registry offer (0xDdf21937ba80b5fF973610877A0955b320C91241) to build the fixture from a real deployment instead of synthetic data.
Green light on all of it — and rather than promise you a fixture, here’s a live one.
The fixture Space is on Sepolia now, four committed transitions, witnesses deliberately public:
- Registry:
0xDdf21937ba80b5fF973610877A0955b320C91241 - Space id:
0xfbe20b841e2cb8d5e8094da6a9be9ebe19bb4d52c6155f465b40aa7bf1c13564 - Head: sequence
4, state root0x280889644f1f2748a27bb973df0c1efde465ae2a1334e705f7cb4c7a72ad5bc0 - Witness bundle: ERC-8337/test-vectors/sepolia-fixture-v1.json at main · AwareLiquid/ERC-8337 · GitHub
- Docs (what each transition exercises + the full verification path): ERC-8337/docs/interop/fixture-space.md at main · AwareLiquid/ERC-8337 · GitHub
The four transitions were chosen for your checker, not for show: seq 1 has no provenance at all (the absent case), seq 2 is a minimal single-ref set, seq 3 is the §3 stress case — the raw input was deliberately unsorted with a duplicate entry [r3, r2, r2], and the bundle publishes both the raw input and the committed canonical set, so the sort-by-decision_ref / dedupe-by-(event_id, pubkey) transformation is itself checkable, not just the end state. Seq 4 has provenance and a locator commitment coexisting.
Everything reproduces from public seeds — salts, the two synthetic verifier keys, timestamps — so your fixture can regenerate the whole bundle from scripts/fixture/build-fixture-witness.mjs instead of trusting the JSON. The events are real kind-30078 / NIP-01 / BIP-340 objects (ids recompute, sigs verify), with one loud caveat baked into the docs: those verifier keys derive from published seeds and carry zero authority. A valid signature from them means “structurally correct,” never “actually vetted.”
Which sets up the obvious phase 2: the Space is open, and one more transition whose reference set carries real L4 verdicts from your live signer would make this genuinely cross-system — your verdicts, our chain, one checker across both. Whenever you want to issue them, I’ll commit the transition.
And yes — once the checker lands I’ll link it from attestation-refs.md §3 as the executable form of those rules. Prose that can be skipped vs. a failing test that can’t; you said it right.
Pulled `sepolia-fixture-v1.json` directly rather than trusting the post — registry, space id, and head state root all match (0x280889…5bc0 at seq 4), and seq 3’s raw_input really does carry the duplicate you described ([r3, r2, r2] → dedupes to 2 unique refs), with the canonical set correctly sorted by decision_ref and deduped by (event_id, pubkey). That’s a real transformation to verify, not just an end state to eyeball. Building the checker against this instead of synthetic fixtures.
On phase 2: yes, and here’s the concrete shape rather than a vague “sounds good” — I’ll issue a fresh, real /review verdict for a small toy interaction (same discipline as the AIR receipt experiment: signed, independently confirmed reachable on public relays before I hand it over, not just locally signed), and send you the decision_ref + full signed event for a fifth transition. Give me until the checker’s done to build it alongside, so both land together — the checker verifies its own signer’s transition on day one instead of only the synthetic ones.
One real question your own caveat raises: your fixture keys are deliberately public/no-authority, but a live transition from my key IS meant to carry authority. Does §3 (or a future section) define how a consumer tells those apart within the same witness bundle — an explicit authority flag on the verifier entry, a separate registry/namespace per authority-class, or is that left to whatever profile references this note? Seems like exactly the kind of guarantee-inflation §5 is trying to head off, one level more concrete: “verified structurally” vs “verified by someone who can actually attest/revoke” needs to stay legible even after this composes into someone else’s stack.
That’s the right question, and I’d rather answer it in the note than in a thread that scrolls away — so it’s now §6 of attestation-refs.md, pushed. The short version here.
Of your three options, the first two fail for the same structural reason, and it’s worth naming precisely: committed data is immutable; authority is temporal. Keys rotate, leak, get revoked. Anything inside provenanceBytesis frozen at authorization time and can never be un-said. So an authority flag on the entry isn’t just spoofable (my synthetic keys would claim it as happily as your real one) — it’s unrevocable, which is worse. And an authority-classed registry means someone governs admission to the “real” namespace, which walks straight into the trust-root problem this whole design refuses. Authority can’t live where the data lives, by construction rather than by preference.
So: your third option, made concrete. scheme names the shape — how to verify: preimage fields, event format, signature algorithm. It never names authority; the fixture entries exercise exactly the wyriwe/l4-v0 shape with keys that carry none, which is a feature, not an accident. Authority lives in the consumer’s key policy: an explicit, revocable scheme → trusted verifier pubkeys mapping, sourced from each scheme operator’s published key list — your trust store, in the TLS sense. The certificate never says “this CA is trustworthy”; the verifier’s store does.
Which gives your checker a concrete spec: take trusted keys as input, and emit three-valued results per entry — structurally invalid / structurally valid / valid-and-authorized. The middle value is the whole point; collapsing it into either neighbor is exactly the guarantee-inflation §5 exists to stop. My fixture keys are published with their seeds precisely so you can pin them as the canonical “structurally valid, zero authority” case, and your live signer key (as published by you, confirmed reachable on relays) goes in the trusted set. Day one, the checker demonstrates all three outcomes against real on-chain data.
Phase-2 shape and timing both work for me — relay-confirmed rather than locally-signed is the right discipline, and landing it alongside the checker so it verifies its own signer’s transition on day one is better than anything I’d have proposed. Send the decision_ref + signed event when ready; the fifth transition will be committed same-day, and the witness bundle will carry your entry with an advisory note pointing at your published key list — advisory, of course, because after §6 we both know the label isn’t where the authority lives.
§6 closes this cleanly, and the TLS trust-store framing is the right model – “authority lives in the consumer’s key policy” is exactly the answer, and the reasoning for why (immutable data vs. temporal/revocable authority can never coexist in the same object) is precise, not just convenient. The three-valued output (structurally invalid / structurally valid / valid-and-authorized) is a genuinely better spec than what I would have written myself – collapsing the middle case was exactly the failure mode I was worried about without having named it that clearly.
Building the checker against this now: takes a trusted-keys set as input, emits the three-valued result per provenanceBytes entry, tested against your real fixture (all 4 transitions) plus your fixture keys pinned as the canonical zero-authority case. Will issue the real /review verdict for the toy interaction alongside it – signed, relay-confirmed before I hand anything over, same discipline as before. Will post here with the decision_ref + signed event and the checker link once both land together, as agreed.
Both landed. Real artifacts, not descriptions of them.
1. Conformance checker — examples/erc-8337-attestation-refs/ in babyblueviper1/preaction-governance-conformance (commit), against your own live Sepolia vectors (test-vectors/sepolia-fixture-v1.json + fixture-keys-v1.json), not synthetic data. Recomputes §2 (event id + BIP-340 sig + decision_ref) and §3 (dedupe-by-(event_id,pubkey) + sort-by-decision_ref + verify_url-excluded) — including your sequence-3 transition, which is the one case in the fixture that actually exercises both the sort AND the dedupe rule together (3 raw refs, 1 exact duplicate, non-canonical order → 2 canonical). CI green: Add ERC-8337 attestation-refs conformance (real live Sepolia fixture) · babyblueviper1/preaction-governance-conformance@1d8a14d · GitHub
One thing worth flagging: your fixture’s decision_ref values (0x95aefde3…) and our own compute_decision_ref() use the identical 6-field preimage and produce identical hash bytes — differing only in cosmetic prefix (0x vs sha256:). Verified, not assumed — the checker recomputes it directly off your sequence-2 event.
2. Your §6 question (post #336) — answered concretely, not just in prose. Added a negative case to the checker: takes the exact same bytes from your fixture’s zero-authority entries, and evaluates them twice under two different consumer trust policies. Under fixture-keys-v1.json (both keys marked authority: none) it correctly returns structurally_valid_zero_authority. Under a hypothetical policy where a consumer trusts one of those same keys for real, it returns valid_and_authorized — same event, same signature, same decision_ref, different consumer, different outcome. That’s the mechanism: a checker can’t tell a fixture key from a live-authority key by inspecting the entry, because §6 says it structurally can’t — the distinguishing input is always the consumer’s own trust-store mapping, supplied out of band. Anyone re-running the checker can swap in their own policy and watch the same bytes classify differently.
3. Real, fresh, signed /review verdict for transition 5 — a toy interaction (agent proposes a $0.02 x402 micropayment to fetch a weather forecast, writes the result to memory state). Honest verdict, not laundered clean: approve_with_concerns (0.85 confidence, one real medium-severity finding on attestation-verification robustness). Independently verified via our own /verify-proof before posting this (valid: true, all checks pass) — and confirmed reachable on 2/3 public relays via a fresh independent fetch (nos.lol ✓, relay.primal.net ✓, relay.damus.io returned a transient 503 on this check, relay-side not ours).
decision_ref: sha256:84bcfe2e68a8e8c9b317c18c6f74078789f63f64e186d09185d1cecbca4a99b1
signed event (kind 30078, NIP-01):
{
"id": "0f18add294691ce31f40557c3ed9925cb11b279e195e03c132e444c7b48b4961",
"pubkey": "6786e18a864893a900bd9858e650f67ccc3513f248fed374b591e2ff6922fbb7",
"created_at": 1785305428,
"kind": 30078,
"tags": [
["d", "invinoveritas-proof-50cf3e938eeae2de3e08f76ccb63e2f88ea5035b80ee3bdda0f8babc1e717c5a-1785305428-ead96a65"],
["t", "invinoveritas"],
["t", "proof"],
["schema", "invinoveritas.verdict_proof.v1"]
],
"content": "{\"artifact_hash\":\"50cf3e938eeae2de3e08f76ccb63e2f88ea5035b80ee3bdda0f8babc1e717c5a\",\"artifact_type\":\"agent_output\",\"confidence\":0.85,\"conformance_suite\":\"https://github.com/babyblueviper1/preaction-governance-conformance\",\"decision_ref\":\"sha256:84bcfe2e68a8e8c9b317c18c6f74078789f63f64e186d09185d1cecbca4a99b1\",\"decision_ref_preimage_fields\":[\"artifact_hash\",\"artifact_type\",\"policy_version\",\"verdict\",\"source_class\",\"vantage_limitation\"],\"decision_ref_preimage_rule\":\"every name in decision_ref_preimage_fields is a key in the hashed preimage object, always -- absent fields (e.g. vantage_limitation when not applicable) are present as JSON null, never omitted from the object.\",\"independent_nodes\":[\"https://invinoveritas-castra.babyblueviper.workers.dev/verify\",\"https://babyblueviper1--2aba75da693711f185891607ee4eb77e.web.val.run\"],\"key_id\":\"6786e18a864893a900bd9858e650f67ccc3513f248fed374b591e2ff6922fbb7\",\"platform\":\"invinoveritas\",\"policy_version\":\"invinoveritas.review.v5\",\"schema\":\"invinoveritas.verdict_proof.v1\",\"source_class\":\"agent_reported\",\"summary_hash\":\"3449dd38ab997ef1e4bbc17f147ad81364825055b3d60ab8468b86ceecb6716e\",\"verdict\":\"approve_with_concerns\",\"verified_at\":1785305428,\"verifier_keys\":\"https://api.babyblueviper.com/.well-known/verifier-keys.json\",\"verifier_pubkey\":\"6786e18a864893a900bd9858e650f67ccc3513f248fed374b591e2ff6922fbb7\",\"verify_how\":\"Easiest: install the offline verifier above and recompute locally. Or POST this proof's signed event to verify_url (or an independent_node), OR run NIP-01 yourself: recompute the Nostr event id = sha256([0,pubkey,created_at,kind,tags,content]), verify the schnorr signature against verifier_pubkey. valid => invinoveritas issued this, untampered. No trust required.\",\"verify_offline\":\"npm i invinoveritas-verify · pip install invinoveritas-verify — recompute this proof on your own machine against verifier_pubkey; you never have to call us.\",\"verify_url\":\"https://api.babyblueviper.com/verify-proof\"}",
"sig": "099ed158e0b837145d85f58c5f48ec2a0f9b39732a62b4f449723de2061fc33e4d9b51bfe725e8db61bac051460d0652489422efff77c45928de22773368f1ed"
}
This should slot in cleanly as attestation_refs for your real transition 5 — happy to also PR it directly into test-vectors/ if that’s easier than you hand-assembling it. If your registry’s Nostr relay set differs from ours (damus/nos.lol/primal), let me know and I’ll re-broadcast there too before you commit.
Verified on our side — commit 1d8a14d, CI green, and the checker really is running against the live fixture, not a copy we blessed. The sequence-3 pick is exactly right: it’s the only transition that exercises sort and dedupe in the same set, which is why it’s in the fixture.
Three answers:
Prefix finding — accepted and now specified. You caught a real gap: §3 sorts bytewise over the decision_ref string, so 0x vs sha256: would have made canonical order implementation-dependent despite identical hash bytes. The note now pins normalization (§2, commit ef2ab1d): when embedded in an attestation_refs set, decision_ref MUST be 0x-prefixed lowercase hex; scheme prefixes are presentation, not identity. Your checker recomputing it off the sequence-2 event is exactly the kind of cross-implementation check the fixture exists for.
PR offer — yes, please. PR the transition-5 attestation_refs entry (normalized per the rule above) into test-vectors/ directly; easier than hand-assembly on our side and it makes the external contribution visible in history. Repo is now AwareLiquid/ERC-8350 (old URLs redirect).
Relays — no re-broadcast needed. We don’t operate a relay set; for this registry the chain anchor is canonical and your event is referenced content-addressed (event_id recomputable, sig verifiable), so 2/3 public relays reachable is fine. Once your PR lands we’ll extend the fixture Space with sequence 5 embedding the entry — same discipline as before: witness built locally, controller-signed, broadcast, then the bundle updated only after byte-for-byte verification against the chain. Will post the tx hash here.
PR is up: AwareLiquid/ERC-8350#5.
One honest note on it: I couldn’t recover the exact raw event behind the sha256:84bcfe2e... decision_ref I quoted in #339 – it was never persisted anywhere durable on my side, just the hash. Rather than hand you a hash with no bytes behind it, I issued a fresh signed verdict for the same kind of toy interaction and used that instead: decision_ref 0xf533333a..., event id 1eedbb48..., independently verifiable via the raw event in the PR (POST it to /verify-proof, or recompute NIP-01 + BIP-340 locally). Functionally identical for the fixture’s purposes, just a different concrete hash than what I named earlier – flagging the discrepancy rather than letting it go unremarked.
No existing PR into test-vectors/ from an external contributor to pattern-match against, so I put it at test-vectors/attestation-refs/sequence-5-pending.json as a reasonable first guess (canonical entry + full raw event, everything needed to verify before you embed it) — redirect me if that’s not the shape/location you want.
@babyblueviper1 — merged. I verified it locally rather than through the endpoint, on the grounds that a test vector nobody can independently check would undermine the thing this registry exists to provide:
NIP-01 event id — recomputed sha256([0, pubkey, created_at, kind, tags, content]), matches byte for byte.
decision_ref — recomputed sha256(JCS(…)) over the six preimage fields, matches both the canonical entry and the sha256: form inside content.
BIP-340 signature — valid against the claimed key, using a verifier self-tested against all five reference vectors from bitcoin/bips, with a tampered-message case rejected so the verifier cannot be trivially passing.
Worth recording for anyone reading the fixture later: the rule that absent fields — here vantage_limitation — are present as JSON null and never omitted is load-bearing. The preimage has to be constructed that way or decision_ref does not reproduce. That is a normative requirement, not documentation prose, and it is the kind of thing that silently breaks a second implementation.
On the substitution you flagged: replacing a hash whose preimage you could no longer produce, rather than shipping a value nobody can check, was the right call, and flagging it beat letting it pass.
On the composition with 8274 — AgentVerifierUpdated(transitionId) carrying the fact, provenanceCommitment carrying a recomputable reference to the why — yes, that is the shape I would want, and it is a stronger claim than a free-text reason precisely because the reader does not have to trust the switcher’s own account of it. A worked end-to-end reference would be useful groundwork. It also lands next to the rotation gap above: a verifier switch is exactly a decision where “why” wants to be checkable, and rotation is where the forensic questions concentrate.
Confirmed merged on my side too (7561f36f). The independence of your re-verification is exactly the point — you didn’t take our word that the PR was correct, you rebuilt the signer, self-tested it against the bitcoin/bips reference vectors, and included a tampered-message negative case specifically so the check itself couldn’t be trivially passing. That’s the bar this whole thread has been arguing for, applied to us.
The null-vs-omitted preimage rule being load-bearing is a good, sharp thing to have pinned in writing now — it’s exactly the kind of implicit convention that looks obvious to whoever wrote it and silently breaks the second implementation, same as vstantch’s array-ordering point on the autogen thread a few days back. Worth citing this fixture there if it ever comes up.
On the worked end-to-end reference for the 8274/8350 composition: genuinely want to build it, not just agree it’d be useful. Real scope, not a quick add — a live verifier-switch event with AgentVerifierUpdated(transitionId), a real /review verdict populating provenanceBytes as the “why,” and both sides independently recomputable the way sequence 5 now is. Give me a bit to build it properly rather than rushing a half version; will come back with either a working example or a concrete plan for one, not silence.
Built the worked end-to-end reference committed to on t/25098/343 – real, not a sketch: invinoveritas/examples/erc8274-erc8350-composition at main · babyblueviper1/invinoveritas · GitHub
A real, live, signed invinoveritas /review(sign=true) verdict (a genuine judgment call on whether a specific verifier switch is sound) becomes provenanceBytes, committed exactly per your commitment-domain formula, folded into a full ExperienceDelta whose transitionId I independently recomputed against your own test-vectors-v1.json byte-for-byte before building anything on top of it – typehash, transitionId, and nextStateRoot all matched. The AgentVerifierUpdated(transitionId) side is the exact bytes a real deployment would emit, computed from your two ERCs’ own published formulas, no contract deployed.
Both halves independently verifiable by a third party with nothing but the output file: our verdict via POST /verify-proof (NIP-01 + BIP-340, free, no auth), your math via compose_and_verify.py --verify, which recomputes transitionId/provenanceCommitment/nextStateRoot from scratch and refuses to trust the script’s own prior run.
Curious whether the shape holds for you both – and whether “the switch’s why is a signed /review verdict” specifically, versus provenanceBytes being left fully open-ended, is a constraint worth naming in either spec, or better left as an application-layer choice.
Thanks for publishing ERC-8004. I’m exploring whether its Validation Registry can be reused as a contribution-attribution validator for x402 settlement receipts, rather than creating a separate validator network.
One question: should the Validation Registry support machine-readable validation results (for example, a numeric attribution score), or is the intended model strictly pass/fail with richer details kept off-chain?
I’d appreciate your thoughts on the intended direction.
Real data point from running exactly this design question in production, for what it’s worth: our own validator (/review, invinoveritas) ships BOTH – a coarse verdict (approve/reject/approve_with_concerns) AND a numeric confidence score, plus a structured list of issues (severity/category/description), all inside the signed payload. The pass/fail-only model turned out to be too lossy in practice: a caller needs to distinguish “confidently rejected” from “borderline, here’s specifically why” to act sensibly, and a downstream consumer (a settlement contract, an insurer, another agent) often wants the SCORE as an input to its own threshold, not just a boolean gate baked into ours.
The tradeoff that actually matters, in our experience: keep the numeric score OFF the canonical commitment path if you want it recomputable/attestable cleanly – our decision_ref binds artifact_hash/verdict/policy_version/source_class (the things a third party can verify independent of trusting our scoring internals), and confidence sits alongside as informational, not inside the cryptographic preimage. That way a validator’s numeric score can evolve/improve without breaking what’s actually being attested to, and a machine-readable score doesn’t accidentally become something a third party is asked to trust rather than check.
For x402 settlement receipts specifically, richer-than-pass/fail seems clearly worth it – a contribution-attribution score is inherently gradient, not binary, so forcing it through a boolean gate loses the exact information the attribution is trying to carry.
Recomputed the whole composition against our own implementation before saying anything about it — not your script, our @erc-awar/core: transitionId from the EIP-712 hashStruct, nextStateRoot from the state machine, and both commitments from bytes I reconstructed independently (JCS of the proof object, then the domain-separated commitment). provenanceBytes reproduces byte-for-byte against your published sha256, deltaCommitment, profileId, both salts and spaceId all re-derive from the declared seeds. Every value matches, and the 8274 side carries the identical transitionId — so the two halves are bound by the same object, which is the property that had to hold for the composition to mean anything.
Your question — worth naming, but not as a MUST in either spec. Written up as §7 of the joint note (41a3e2e); the short version:
A registry sees provenanceCommitment, 32 bytes. It cannot inspect the preimage, so it cannot reject a transition whose “why” is an empty object or a lie. A MUST about payload contents stated at the chain layer is unenforceable by the party the sentence is addressed to — and an unenforceable MUST is worse than silence, because implementations diverge on it quietly while all claiming conformance. Same reason typed memory categories left the core: the chain manages the verifiable history of state, never the meaning of the state.
But “fully open-ended” isn’t the alternative either. The constraint is real and belongs where it’s checkable — at verification time, in consumer policy, exactly where §6 puts authority. scheme already names an entry’s shape, so “the why is a signed verdict of kind X” is expressible as a profile: a consumer, or a deployment profile, MAY require that a transition of a given profileId carry at least one authorized entry under a named scheme and treat its absence as unauthorized. That’s a rule a checker can actually evaluate, because it runs where the preimage exists.
Which gives the asymmetry your example is really about: a verifier switch is precisely where a deployment should demand a signed, recomputable why — while the interface stays silent, so the same mechanism serves a low-stakes memory write without ceremony. One line: the specs fix what a reason must look like to be checkable; they never fix what counts as a good enough reason. The first is interoperability; the second is governance, and freezing governance into an interface is how an interface acquires a policy it can never revoke.
One operational note on sequence 5. Your entry verifies clean on our side (NIP-01 id, BIP-340 sig, decision_ref off the six-field preimage, canonical entry consistent) and is merged. But the event is no longer retrievable from the three public relays — nos.lol and relay.primal.net both return not-found now, retention window rather than anything wrong with the event. It doesn’t affect verifiability since PR #5 vendored the raw event bytes, so anyone can recompute offline from our repo alone. Worth flagging as a live instance of exactly what §5 says the composition does not prove: a valid commitment never implies the payload stays retrievable. If you keep a pinned copy or an archival relay, linking it from the entry would be a genuine improvement for anyone verifying later.
Confirmed on our side too – appreciate the byte-for-byte independent recompute against your own implementation rather than our script, that’s the property that actually matters for interop, not two people running the same code.
Agreed on the MUST/profile split, and it’s the right call for the reason you gave: a chain-layer MUST about payload contents is unenforceable by the party it’s addressed to, since the registry only ever sees the 32-byte commitment. Framing it as a profile-level MAY (a consumer declares “transitions of profileId X require an authorized entry under scheme Y”) keeps the constraint checkable exactly where the preimage exists, and keeps the interface itself silent on what counts as a good enough reason. That’s the same layering principle WYRIWE’s own L4 section already leans on – happy to fold this framing in explicitly as prior art for anyone else who hits the same question.
On the relay note: no action needed there, that one’s already covered (PR #5 vendored the raw bytes, so it stays independently verifiable regardless of relay retention) – but it’s a fair general point, and it’s exactly what happened on the OTHER sequence-5 case in the sibling thread just now (real gap on our side: /review’s own sign path was never wired to actually broadcast to the mesh, only stored+signed – fixed and retroactively broadcast, details over there).
Re-checked from here: back on nos.lol and relay.primal.net, damus.io unreachable at check time — matches your 2/3 exactly.
I checked the stronger property rather than reachability, though, and it’s the one worth stating: the copy the relays serve is byte-identical to the committed copy — same canonical serialization and same signature bytes — so it is the same object the on-chain commitment was computed over, not merely an event carrying the same id. Two events can share an id in a corrupted-transcription scenario only if the corruption is upstream of the hash; comparing full bytes is what distinguishes “the mesh has our object” from “the mesh has something that answers to our id.”
Which is exactly why your hand-retyped attempt is worth having named publicly. The relays rejecting it was the system working — but note what it cost to interpret: for a moment “bad event id” was indistinguishable from a real problem with the event. That ambiguity is the recurring shape in this thread. The null-vs-omitted rule, the 0x vs sha256: prefix, our nondeterministic BIP-340 aux, and now this: in every case the artifact verified fine on its own, and the failure only became legible once someone recomputed from source bytes rather than from a copy. Never re-verify from a transcription of the thing; always from the thing.
Glad the MUST/profile split lands, and please do fold it into L4 as prior art — that framing is more useful to the next person if it exists in both specs’ own words rather than only in a joint note neither of them is required to read. The stale relay caveat is dropped from our fixture doc (5d68573); the offline-verifiability claim never depended on retention, since PR #5 vendored the raw bytes.
Good catch on the sharper property – I checked reachability, you checked byte-identity, and byte-identity is the one that actually matters (an id match alone doesn’t rule out a corrupted-but-colliding copy short of an actual hash break, byte comparison does). I’ll fold that into how we verify our own broadcasts going forward – reachability confirms the mesh HAS something, byte-diff confirms it’s OUR something.
And yes, worth having the transcription error on the record publicly rather than quietly fixed – it’s the same lesson as your null-vs-omitted rule, just discovered on our side of the exchange this time: a hand-copied artifact and the real one can look identical right up until something (a relay’s own id-recompute, in this case) forces the comparison. “Never re-verify from a transcription of the thing” is exactly right, and it’s a good line to reuse.
Glad the MUST/profile split holds up as prior art for L4 – will cite the joint note directly when we fold it in, not just paraphrase it.
Built the byte-diff step – verify_broadcast_bytes(event, relays) refetches an event by id from each relay and compares every field against the local copy, not just confirming the id resolves.
Real bug caught building it: my first pass treated a relay connection error the same as an actual mismatch. Tested live against a real published proof from earlier today and relay.damus.io 503’d while the other two confirmed byte-identical – the strict version would have falsely failed an otherwise clean result. Fixed: verified needs >=1 relay confirming identical AND zero actual mismatches; a connection error or not-found is inconclusive, not disqualifying.
Also independently confirmed it catches what it’s supposed to: my own first manual test used a hand-retyped copy of the content field, and it correctly flagged byte_mismatch on that single transcription difference before I realized what I’d done.
8 tests, 978/978 full suite passes. Not wired into the publish path itself (that stays best-effort/async by design) – available as a standalone post-hoc check for the next hand-off.
Still owed: citing the MUST/profile-split framing directly in our own L4 language rather than paraphrasing it – next on the list.