Skip to content

feat(economic): recipient-side economic admission — prevalidation before the fence, the total credit gate, the release split - #738

Open
cryptskii wants to merge 1 commit into
mainfrom
feat/transfer-recipient-economic-admission
Open

feat(economic): recipient-side economic admission — prevalidation before the fence, the total credit gate, the release split#738
cryptskii wants to merge 1 commit into
mainfrom
feat/transfer-recipient-economic-admission

Conversation

@cryptskii

Copy link
Copy Markdown
Collaborator

3.5b PR4 — recipient-side economic admission: prevalidation before the fence, the total credit gate, and the release split

Final PR of the 3.5b series (#735 write-set semantics, #736 peer-lineage validation, #737 sender admission). Every online credit-direction Transfer now runs the FULL economic admission on the recipient — sender-ancestry prevalidation before any durable state, atomic accept with the acceptance bundle and pending admission, publish → register → validate → admit, and a post-admission RELEASE the sender independently verifies before finalizing. All five fifth-review corrections are implemented as specified.

The accepting gate (core, total — no transport exemption)

DeviceState::advance gains the credit-direction Transfer gate beside the FaucetClaim gate, at FULL discipline (correction 11b, both gates): the attached admission must be DsmBacked AND Prepared AND bound to exactly this operation's digest — None, wrong kind, wrong state, and digest mismatch each get a named refusal. Owner ruling: the BLE/USB receiver's online-credit path (byte-indistinguishable at this seam) fails closed until its own wiring lands; bearer-tier BLE (authority_policy: Some) is untouched. Mutation-verified: deleting the gate turns both a_credit_transfer_requires_the_full_admission_discipline and the BLE-shaped control red (run, red, restored).

Prevalidation — every foreign dependency before the fence exists (corrections 3, 5, 8)

In complete_ready_split_transfers, per row, after the finality barrier and before the relationship lock: decode the frozen wire (locators 13/14 as untrusted hints; peer identity from the stored contact, never wire metadata), walk the sender's lineage via the PR2 walker with the recorder AS the PeerEvidenceFetcher (correction 2 — every immutable object any nested verification fetched, including EK predecessor steps, is recorded at the actual fetch boundary), run prevalidate_sender_debit (extracted into core so the pre-accept and post-accept conjuncts can never drift), bind the wire bytes to the validated operation and the receipt to the debit successor, require EK-ancestry portability, and establish the q-durable closure by member-attributed republish of the exact recorded bytes.

Refusal taxonomy is load-bearing: walker Invalid/predicate refusal ⇒ TerminalReject before ANY durable state (THE correction-3 control, tested with fabricated debit indices); Quarantined ⇒ terminal with the divergence recorded; Incomplete ⇒ the row holds and retries — an outage is never an attack and never a wedge (tested: quorum down ⇒ zero durable state; heal ⇒ the same row completes).

Split durability memos (correction 4)

The peer_economic_lineage watermark covers the ECONOMIC DAG only; EK-step durability is memoized per exact immutable address (immutable_q_durable_memo), never inferred from an economic position. When the watermark does not cover the target, the walk runs from the activation root with the cache disabled so the recorder observes the full closure. Re-validation preserves the flag (INSERT OR IGNORE).

The release split (fifth-review correction 1)

PeerTransferAcceptanceEvidenceV1/sig_b is acceptance PROVENANCE only — it publishes with the admission evidence before ECON_ADMITTED and cannot finalize the sender. The new transport-only RecipientEconomicReleaseV1 (no CCB class) binds the receipt commitment, acceptance-evidence address, recipient (G, DevID), admitted position, post-R_econ root, and admission-manifest address, signed by the recipient. It is constructed and frozen in the accept transaction (every field an output of the build), the reply row is HELD, and the release object is frozen ONLY in the terminal admit transaction — promotion to deliverable and network publication are both admission-terminal. The countersign delta carries the release's 32-byte content address (a ~50KB inline SPHINCS+ release exceeded the reply-envelope cap — discovered live, fixed by content-addressing).

Sender finalization requires the release: fetch by address (re-hash verified), verify under the pinned contact AK, bind commitment + devid + contact genesis, then INDEPENDENTLY quorum-read the recipient's register cell at the named position and require exact post-root + manifest equality — a hostile recipient's private "admitted" flag is never trusted. Mutation-verified: disabling the gate finalizes on bare sig_b and turns the named test red (run, red, restored). Companion tests: a release naming a mismatching registered root is Rejected; an unregistered root defers (outage, not attack).

EK-step ancestry (correction 3 + the portability chain)

New ek_cert_step_chain table (per relationship, per SIGNER — never receipt role). The recipient appends both sides' EkCertStepV1 objects in the accept transaction (frozen with the admission evidence); the sender appends the byte-identical objects in its finalize transaction (deterministic derivation — both mirrors converge on the same content addresses, which is what keeps role reversal walkable). The BLE bilateral path records both signers' steps at its two completion points, frozen offline through the generic publication backlog (no BLE-specific protocol); prevalidation's per-address durability check fail-closes any online bundle depending on an unpublished step.

Core pre-fixes (latent PR2 defects, found during design)

  • verify_peer_transfer_acceptance compared the wire's UNSIGNED canonical bytes against the walker's SIGNED operation — unsatisfiable once a real bundle existed. Fixed with with_cleared_signature() at the comparison.
  • The countersigned B-side pair was self-selected by the bundle (correction 5): the expected (b_parent_tip, b_child_tip) now comes from the exact verified recipient successor (embedded_parent + C_dsm+, threaded through VerifiedDsmSuccessor, AcceptedSubstrate, coords, and the provenance context) and the bundle's pair must equal it — the_bundle_cannot_self_select_its_b_side_pair pins it.
  • build_write_set now refuses SourceAlreadyConsumed at build (the verifier refused independently; honest producers get a named error) — the_same_sender_debit_cannot_fund_a_second_credit drives it over the recipient's real post-admission tree.

Everything else

  • apply_incoming_transfer_staged gains admission: Option<AdmissionPlan>; the existing in-tx writes chain in the commit helper's also slot — no new transaction composition.
  • Poll-level resume (device-global) precedes every completion pass; find_current_payload_with_prefix gains a purpose filter; a crash-resumed recipient recovers its release from the journal by the manifest address the admission itself binds.
  • admit_economic_position promotes held replies and freezes post-admit artifacts atomically with ECON_ADMITTED.
  • canonical_rebuild replays previously-committed gated credits with a synthetic Prepared bound to the recorded operation digest, stripped after each step.
  • fund_unadmitted deleted (zero callers). Schema v11 → v12 (beta wipe): embedded_parent on the pending row, ek_cert_step_chain, immutable_q_durable_memo, closure_q_durable, held/release_bytes on the reply row, release_bytes on the acceptance journal.

Tests

11/11 bilateral finality tests green with BOTH admissions live per generation (role reversal, checkpoint gating, crossing sends, relationship-local barrier). New recipient_admission_tests (5): both-sides e2e with a register-backed release and the q-durable watermark; fabricated coordinates refused before any durable state; same-debit-cannot-fund-twice; the held-release race rule (mutation-verified); outage-holds-then-recovers. New core gate tests (2, mutation-verified). Release-gate tests (2). The finalize harness (18 tests) now runs against a full release fixture (signed release + published object + registered recipient root). Fixture sweep: core_sdk (22) and bcr (14) attach the honest Prepared precondition — the gate is total in tests exactly as in production.

Boards (exact CI commands, final tree, pinned 1.98.0)

  • Workspace: cargo test --locked --workspace --exclude dsm_storage_node -- --nocapture --test-threads=1 — 3855 passed / 0 failed (dsm lib 1666/0, dsm_sdk lib 1790/0 + 7 ignored), exit 0
  • Node (repo root): cargo test --locked -p dsm_storage_node --no-default-features --features local-dev,strict -- --nocapture — 270/0, exit 0
  • make lint (repo root) — exit 0; pinned repo-root cargo clippy --all-targets — exit 0
  • bash ci/production_safety_checks.sh — exit 0

Named residuals: BLE/USB online-credit receive is refused fail-closed until the BLE admission wiring PR (owner ruling — no transport exemption); a BLE step that recorded before this PR (or whose post-commit step recording failed) holds later online acceptance on that relationship as Incomplete until reconciled — fail-closed, logged, never corrupting.

…ore the fence, the total credit gate, the release split

3.5b PR4, final PR of the series. Every online credit-direction Transfer
runs the full economic admission on the recipient. Core: the accepting gate
beside the FaucetClaim gate at full discipline (DsmBacked + Prepared +
exact digest, both gates tightened; total across transports — the BLE/USB
online-credit receive fails closed until its own wiring lands, owner
ruling); prevalidate_sender_debit extracted so pre- and post-accept
conjuncts share one implementation; the acceptance verifier compares
cleared-signature operation bytes and binds the countersigned B-side pair
to the exact verified recipient successor (two latent PR2 defects);
build_write_set refuses SourceAlreadyConsumed at build.

SDK: prevalidation in complete_ready_split_transfers establishes every
foreign dependency BEFORE any durable state — the PR2 walker with the
recorder AS the PeerEvidenceFetcher, wire/receipt binding, EK-ancestry
portability, and the q-durable closure via member-attributed republish of
the exact recorded bytes; Invalid ⇒ TerminalReject, Quarantined ⇒
terminal, Incomplete ⇒ hold-and-retry. Durability memos are SPLIT: the
economic watermark never implies EK-closure durability (per-exact-address
memo). The atomic accept carries the acceptance bundle, both EK step
objects, the pending admission, and the HELD reply; finish publishes,
registers, validates, admits — and the terminal admit transaction promotes
the reply and freezes the RecipientEconomicReleaseV1, the transport-only
finalization authority: bare sig_b is provenance and cannot finalize; the
sender fetches the release by content address, verifies it under the
pinned contact identity, and independently quorum-reads the recipient's
registered root before finalizing. EK signer chains (ek_cert_step_chain)
are appended identically on both devices (accept tx / finalize tx) and at
the BLE completion points (frozen offline as publication debt). Schema
v11 -> v12 (beta wipe). fund_unadmitted deleted.
sender,
incoming_transfer_op(&sdk.device_info.device_id, 5, vec![0x31u8; 32]),
{
let op = incoming_transfer_op(&sdk.device_info.device_id, 5, vec![0x31u8; 32]);
sender,
incoming_transfer_op(&sdk.device_info.device_id, 7, vec![0x32u8; 32]),
{
let op = incoming_transfer_op(&sdk.device_info.device_id, 7, vec![0x32u8; 32]);
sender,
incoming_transfer_op(&sdk.device_info.device_id, 9, vec![0x41u8; 32]),
{
let op = incoming_transfer_op(&sdk.device_info.device_id, 9, vec![0x41u8; 32]);
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