Skip to content

refactor: the unreached receipt verifier is deleted; its checks run on production's decisions - #1073

Merged
cryptskii merged 3 commits into
mainfrom
refactor/delete-unreached-stitched-receipt-verifier
Sep 30, 2026
Merged

cryptskii merged 3 commits into
mainfrom
refactor/delete-unreached-stitched-receipt-verifier

Conversation

@cryptskii

Copy link
Copy Markdown
Collaborator

Owner ruling (2026-09-30): the unreached receipt verifier goes. Its checks are ported onto the production path first, then it is deleted.

Why

verification::receipt_verification::verify_stitched_receipt and its ParentConsumptionTracker had no production caller. That is recorded in CONFORMANCE §6.3 and §6.11, and in #1068's §6.44. Production decides a bilateral step with bilateral::offline:

  • decide_prepare and decide_confirm on the receiver's side, decide_commit_ack on the sender's;
  • a second child of one tip is refused because the receiver holds the tip it committed.

The verifier's only users were tests and tools/vertical_validation. So CI's attack suite, fork-exclusion property, Tripwire traces and TLA replays were reporting on code production never runs.

What changes

The validation harness runs production's step (tools/vertical_validation/src/live_device.rs):

  • the sender proposes on the shared tip it holds (h₀ from initial_chain_tip_from_device_ids, then each committed successor) and signs the step commitment (σ_A);
  • its stitched receipt is signed by a per-step EK that its chain head certifies over the parent tip;
  • the receiver decides with decide_prepare and decide_confirm, against its held tip and the keys its contact pins, including a real Kyber identity binding;
  • both devices commit and move the shared tip.

Every judge is ported onto it.

Check Production refusal it asserts
double spend (second child of one tip) decide_prepare → StaleTip
step replay StaleTip
receiver behind (a device restored from before a step) StaleTip
wrong-key and garbage σ_A decide_prepare: "is not signed over its commitment by the pinned AK"
receipt under an EK the sender's chain never certified decide_confirm: "does NOT chain"
forged child root, bent path decide_confirm: "do not fold"
fork_exclusion property; receiver_tripwire, tripwire_first_contact_binding, state_machine_fork_divergence traces first child commits, second is StaleTip
TLA DSM replay (net_deliver) every step delivered through the decisions
TLA Tripwire replay the model's tips map onto the shared tips real devices hold; every model receipt is a step production accepts

Deleted from dsm:

  • verify_stitched_receipt and its signature helper;
  • ReceiptVerificationContext, ReceiptAcceptance and ParentConsumptionTracker;
  • the tests that exercised only those, and the tripwire_parent_consumption trace.

dsm/tests/smt_tripwire_theorem.rs::theorem2_two_successors_same_parent_rejected now asserts the refusal through decide_prepare: StaleTip once h₁ is held, Consider while h₀ is held.

Evidence

  • vertical-validation adversarial: 6/6; property-tests --iterations 5 --seed 42: 6/6; implementation-traces: 15/15; tla-check: all 91 specs. The Tripwire, DSM_tiny, DSM_small and DSM_system trace replays pass literal and direct.
  • cargo test -p dsm -p dsm_vertical_validation --release: 1,630 passed, 0 failed.
  • Mutation control: decide_prepare's stale-tip arm disabled turns theorem2_two_successors_same_parent_rejected and a_stale_proposal_says_whether_its_claimed_tip_recomputes red, and fails the double-spend, replay and receiver-behind attacks. Restored.
  • cargo fmt --check, clippy --all-targets -D warnings (1.98.0), scripts/real_code_guard.py, ci/conformance_evidence.py: clean.
  • Gemini gate: satisfied.

Records

  • CONFORMANCE §6.45, new. The earlier findings in §6.3, §6.11 and §6.44 are marked resolved, and MR-DSM-0092's note is updated.
  • Verification matrix:
    • The row for a second child now names decide_prepare, with the mutation result above.
    • The row for the old verifier's rule 2c is deleted: a receipt's parent root compared with a root the verifier expects has no production counterpart. MR-DSM-0028 and MR-DSM-0041 are Met on other evidence.

Not driven by the tool

A forged countersignature (σ_B) is judged by the sender's decide_commit_ack. Core's dsm::bilateral::offline::tests::an_ack_is_only_the_receivers_counter_signed_receipt refuses it.

…n production's decisions

`verify_stitched_receipt` and its `ParentConsumptionTracker` had no
production caller. Production decides a bilateral step with
`bilateral::offline` (decide_prepare, decide_confirm, decide_commit_ack),
and a second child of one tip is refused because the receiver holds the tip
it committed. The verifier's only users were tests and the vertical-
validation tool, so the tool's attack suite, fork-exclusion property,
Tripwire traces and TLA replays reported on code production never runs.

Owner ruling (2026-09-30): port the checks onto the production path, then
delete.

Ported (tools/vertical_validation):
- A step is production's offline step. The sender proposes on the shared
  tip it holds, signs the step commitment, and signs its receipt with a
  per-step EK its chain head certifies. The receiver decides with
  decide_prepare and decide_confirm against its held tip and the keys its
  contact pins, including a real Kyber identity binding. Both commit.
- Attacks: double spend, replay and a receiver behind are refused as
  StaleTip; wrong-key, garbage and uncertified-EK signatures and forged
  post-states are refused by the decisions.
- The fork-exclusion property and the receiver_tripwire,
  first-contact and fork-divergence traces run on the same step.
- The TLA DSM replay delivers each step through the decisions. The Tripwire
  replay maps the model's tips onto the shared tips real devices hold.

Deleted (dsm): verify_stitched_receipt and its signature helper,
ReceiptVerificationContext, ReceiptAcceptance and ParentConsumptionTracker,
with the tests that exercised only them. theorem2_two_successors_same_
parent_rejected now asserts the refusal through decide_prepare.

Evidence:
- adversarial 6/6, property-tests 6/6, implementation-traces 15/15,
  tla-check 91 specs; Tripwire and DSM trace replays literal and direct
  PASS.
- dsm and dsm_vertical_validation: 1,630 passed, 0 failed (release).
- Mutation: disabling decide_prepare's stale-tip arm turns theorem2 and
  a_stale_proposal_says_whether_its_claimed_tip_recomputes red, and fails
  the double-spend, replay and receiver-behind attacks. Restored.

Records: CONFORMANCE §6.45 (and the three earlier findings marked
resolved), MR-DSM-0092's note, and the verification matrix. The matrix row
for the verifier's rule 2c is deleted; that rule has no production
counterpart. The row for a second child now names decide_prepare.
…ched-stitched-receipt-verifier

# Conflicts:
#	specs/requirements/CONFORMANCE_GAPS.md
@cryptskii
cryptskii merged commit 7cee0be into main Sep 30, 2026
22 of 23 checks passed
cryptskii added a commit that referenced this pull request Sep 30, 2026
#1073 merged with 64 pins stale, so main's Code map fails at 7cee0be
(run 36674764146): 525 PINNED, 64 PIN_STALE. Every move is code only: the
closures of the rows' evidence tests (and, for some, their production
code) changed with the receipt verifier's deletion.

Each key is repinned with a plain `ci/intent_pins.py repin`, which accepts
code alone, at 7cee0be against CI's code map of that commit; its tree
fingerprint equals this tree's. Their evidence, 58 tests, passed here in
release: dsm_sdk lib 25/0, dsm lib 28/0, and the economic_lineage_register,
economic_write_set, native_reserve_wire and economic_admission_lifecycle
suites.

Those tests ran on a Postgres server of their own. On the shared local
server, another session's dsm_sdk tests were running at the same time.
The SDK's node harness recreates fixed database names
(dsm_sdk_test_node_0..4) with DROP DATABASE ... WITH (FORCE), so each
process dropped the other's node databases mid-test, and a different
node-backed test failed on each shared run. Each passed alone.

The comparator reads 589 of 589 pins PINNED and 0 failing rows. Only
INTENT_PINS.tsv changes.
cryptskii added a commit that referenced this pull request Sep 30, 2026
* fix(online): a receipt binds only when its state rules hold

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.

* test(online): the new tests' assertion messages are fixed text

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.

* chore(pins): repin the 64 rows #1078's change moves

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.

1 participant