Skip to content

fix(online): a receipt binds only when its state rules hold - #1078

Merged
cryptskii merged 6 commits into
mainfrom
fix/online-receipt-state-rules-at-ingestion
Sep 30, 2026
Merged

cryptskii merged 6 commits into
mainfrom
fix/online-receipt-state-rules-at-ingestion

Conversation

@cryptskii

Copy link
Copy Markdown
Collaborator

The online receiver bound a sender's receipt to its transfer on the child
tip alone. It checked the signatures and that the child tip is the
operation's successor, but never that the receipt's writes fold from its
parent root to its child root. The BLE receiver checks this, and so does
every producer before it signs. MR-DSM-0092's online order has it as
"recompute hashes and roots".

Probe (2026-09-30, on main after #1073, run on isolated node databases):
a sender whose receipt claimed a child root one bit off what its writes
produce was credited by the receiver.

Binding now runs the one production verifier, verify_receipt_state,
against the Device Tree root and genesis this device pinned for the
sender:

  • holds: the pair binds, as before;
  • fails: the receipt is refused. It never binds, nothing is credited,
    and nothing negative is recorded; the sync reports it;
  • the sender's root is not pinned yet: the pair waits, staged and
    unbound, and binds on a later copy.

Tests:

  • a_signed_receipt_whose_state_writes_fail_binds_nothing: three
    forgeries re-signed by the sender's real per-step EK, every signature
    valid. A child root the writes do not fold to, a write whose path is
    not its leaf's, and a write the operation does not imply are each
    refused, and the honest receipt still binds and credits once.
  • a_receipt_waits_while_its_senders_device_tree_root_is_not_pinned.

Mutation controls: a failing state check that binds anyway, and an
unpinned root that binds, each turned its test red.

Checks run

  • SDK handlers:: suite on isolated node databases: 224 passed, 0 failed, 1 already-ignored. This covers online sends, admission, SoFi end-to-end, faucet and bilateral tests, all of which now bind through the new check.
  • make lint: Rust fmt and clippy pass. The frontend step did not run in this worktree (no node_modules), and no frontend file changed.
  • ci/conformance_evidence.py passes. The records are MR-DSM-0092 re-verified (still Partial), the new CONFORMANCE section §6.46, and a verification-matrix row.
  • Gemini review gate: satisfied in round 1.

The node-backed runs used #1075's per-URL database names, applied locally and not part of this PR, so they would not collide with other sessions' runs.

The online receiver bound a sender's receipt to its transfer on the child
tip alone. It checked the signatures and that the child tip is the
operation's successor, but never that the receipt's writes fold from its
parent root to its child root. The BLE receiver checks this, and so does
every producer before it signs. MR-DSM-0092's online order has it as
"recompute hashes and roots".

Probe (2026-09-30, on main after #1073, run on isolated node databases):
a sender whose receipt claimed a child root one bit off what its writes
produce was credited by the receiver.

Binding now runs the one production verifier, verify_receipt_state,
against the Device Tree root and genesis this device pinned for the
sender:
- holds: the pair binds, as before;
- fails: the receipt is refused. It never binds, nothing is credited,
  and nothing negative is recorded; the sync reports it;
- the sender's root is not pinned yet: the pair waits, staged and
  unbound, and binds on a later copy.

Tests:
- a_signed_receipt_whose_state_writes_fail_binds_nothing: three
  forgeries re-signed by the sender's real per-step EK, every signature
  valid. A child root the writes do not fold to, a write whose path is
  not its leaf's, and a write the operation does not imply are each
  refused, and the honest receipt still binds and credits once.
- a_receipt_waits_while_its_senders_device_tree_root_is_not_pinned.

Mutation controls: a failing state check that binds anyway, and an
unpinned root that binds, each turned its test red.
CodeQL flagged rust/cleartext-logging (alerts #733, #734) on the two
assertion messages in a_signed_receipt_whose_state_writes_fail_binds_nothing
and a_receipt_waits_while_its_senders_device_tree_root_is_not_pinned.
They formatted `out.unbound`, whose reason text carries the sender's
Base32 id, and a count of it still counted as tainted: the taint starts
at load_cert_chain_head_pubkey, read while the receipt is recognized.
Both messages are now fixed text. The assertions (`matches!` on
`[Unbound::Refused(_)]` and `[Unbound::Pending(_)]`) are unchanged, and
both tests pass.
…ate-rules-at-ingestion

# Conflicts:
#	specs/requirements/CONFORMANCE_GAPS.md
…ate-rules-at-ingestion

# Conflicts:
#	specs/requirements/CONFORMANCE_GAPS.md
cryptskii added a commit that referenced this pull request Sep 30, 2026
…fied (#1079)

* docs(core): eight more DSM core rows that cited removed code, re-verified

§6.44 listed nine MR-DSM rows citing items removed inside files that
still exist. MR-DSM-0092 is #1078's. The other eight are re-verified
against the specification and main.

- Met:
  - 0034: `route_seats` writes leader first, then each later seat in
    route order, each copy carrying the chain so far.
  - 0083: the same; a write cut short after the leader is carried to
    finality by the next party.
  - Mutation: the writer carrying a no-response slot in place of the
    leader's link turns the root-registration and carried-release tests
    red.
- Still Partial, with corrected citations:
  - 0002, 0004, 0005, 0014 and 0199: the receipt's state rules, the tip
    the receiver holds, adoption before a credit and the canonical apply
    are the checks now. The candidate, guard, Π and index structure
    remain absent.
  - 0068: both read paths re-hash; no test serves `fetch_verified` bytes
    that do not re-hash.
- §6.48 records it, with a verification-matrix row and intent-manifest
  rows for 0034 and 0083. DSM core totals: 78 Met, 108 Partial, 39
  Missing, 0 Violated.

* fix(map): pins for #1079's six new rows

Pinned at 51741a7 against CI's code map of that commit (run 36775030983);
its tree fingerprint equals this tree's. The rows are MR-DSM-0034
(route_seats::continue_write, from_leader, write_recorded) and MR-DSM-0083
(route_seats::continue_recorded_writes, from_leader, write_recorded). Their
evidence passed in that run's Pin evidence log.

MR-STOR-0116's two rows, stale on main since #1081, are repinned on main by
#1080 and arrive with it.
Code-class only: rows whose closures reach recipient_dispatch.rs,
storage_routes.rs and core_sdk.rs, including #1079's six route_seats
rows for MR-DSM-0034 and 0083. Taken from CI's code map of 9db43e0,
whose tree fingerprint equals this tree's; their 56 evidence tests ran
and passed at that commit. make requirement-map-intent against that map
reads 1,111 rows, 0 failing, and 598 pins, 0 failing.
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.

2 participants