Skip to content

feat(dlv): restart rebuilds a fenced settlement from its immutable bundle (5b of 5) - #777

Merged
cryptskii merged 1 commit into
mainfrom
feat/quorumbind-settlement-resume
Sep 6, 2026
Merged

feat(dlv): restart rebuilds a fenced settlement from its immutable bundle (5b of 5)#777
cryptskii merged 1 commit into
mainfrom
feat/quorumbind-settlement-resume

Conversation

@cryptskii

Copy link
Copy Markdown
Collaborator

PR 5b of the QuorumBind restoration — the concrete resume_one reconstruction (Req 16.5, Req 15.3, Req 6.15). Builds on 5a's frozen SettlementBundle (#776). No settle-path change yet — 5c does the cutover.

What this adds

recover_unresolved_fences (PR 4) hands each unresolved trader-parent fence to a resume_one that must rebuild the transaction and drive it to a terminal outcome without the original constructor's private state (Req 6.15). dsm_sdk::sdk::settlement_resume is that reconstruction:

  1. resolve the committed storage set from the catalog (storage_set_id);
  2. fetch the immutable bundle B by its content identity;
  3. re-hash the fetched bytes and refuse anything that does not hash to the fence's committed identity (Req 15.3), and whose own commitments do not match the fence;
  4. rebuild K(B) and the trader successor from B alone (5a's key_set);
  5. resume through the fenced runner, above the persisted ballot so no ballot is reused.

The fence's tx_id is the bundle digest b. One bundle is one transaction (Req 16.2), so tx_id is both the fetch key (the inner digest GetImmutable resolves) and the identity the re-hash must reproduce. value_digest therefore equals tx_id; value_addr is addr(B).

Shape

  • reconstruct (pure). Decodes B canonically, then verifies four identities against the fence before building anything: bundle_digest(Canon(B)) == fence.tx_id, bundle_addr(Canon(B)) == fence.value_addr, B.storage_set_id == fence.storage_set_id, and B.trader_parent == fence.trader_parent_state_commitment. A wrong, stale, or tampered bundle is refused, not driven. It then builds the BindingTransaction with base_ballot = fence.ballot, so the resumed engine opens the next ballot above the persisted one.
  • resume_one / recover_all (thin async wrapper, live only). Resolve the set, fetch B via GetImmutable (which itself re-verifies the bytes hash to the requested identity), reconstruct, build an HttpBindingTransport over the set's members, and run_fenced. Any failure — set not resolvable, bundle not retrievable, verification mismatch — returns without resolving, leaving the parent fenced for a later pass. recover_all drives every unresolved fence and reports how many remain.

Tests

  • reconstruct (settlement_resume, 2): rebuilds the transaction and K(B) from the bundle and fence, with base_ballot at the persisted ballot and value_digest == tx_id == b; and refuses a bundle that does not hash to the fence identity — wrong digest, wrong address, a trader_parent that disagrees with the fenced parent, a storage_set_id that disagrees, and non-bundle bytes each yield the specific refusal.

The async wrapper is thin glue over already-tested pieces (5a's canonical layer, PR 4's transport and recover_unresolved_fences, PR 3's run_fenced), so its correctness rests on the pure reconstruct above and those.

Verification (Rust 1.98.0)

  • root make lintexit 0
  • workspace board (--workspace --exclude dsm_storage_node --release) — 4026 passed, 0 failed (75 suites)
  • production_safety_checks (all-features clippy + TLA+) — exit 0 (TLA+ included)

Not in this PR (by design)

  • 5c — switch the DlvClose (one-phase fold, Req 6.30) and DlvSettle (full trader-acceptance, §16.2) settle paths to build B, PutImmutable it, set tx_id = b, and drive run_fenced; wire the fence gate into successor creation; call recover_all on restart.
  • 5d — delete the settlement-slot endpoint, table, driver, and settlement_slot_claim_local (§22 Create public SDK API boundary for third-party developers #12).
  • 5e — beta wipe + redeploy (operational).

…ndle (5b of 5)

Rev 15 Req 16.5, Req 15.3, Req 6.15. The concrete resume_one the restart driver
(PR 4) hands each unresolved trader-parent fence. Builds on 5a's frozen
SettlementBundle; no settle-path change (5c does the cutover).

dsm_sdk::sdk::settlement_resume rebuilds and drives a fenced transaction WITHOUT
the original constructor's private state:

1. resolve the committed storage set from the catalog (storage_set_id);
2. fetch the immutable bundle B by its content identity;
3. re-hash the fetched bytes and refuse anything that does not hash to the
   fence's committed identity (Req 15.3), and whose own commitments disagree;
4. rebuild K(B) and the trader successor from B alone;
5. resume through run_fenced, above the persisted ballot so none is reused.

The fence's tx_id IS the bundle digest b (one bundle is one transaction,
Req 16.2), so tx_id is both the GetImmutable fetch key and the identity the
re-hash reproduces; value_digest == tx_id, value_addr == addr(B).

`reconstruct` is pure and separately tested: it verifies FOUR identities against
the fence — bundle_digest == tx_id, bundle_addr == value_addr,
B.storage_set_id == fence.storage_set_id, B.trader_parent == fenced parent —
before building the transaction, so recovery cannot be steered by a substituted
bundle. `resume_one` / `recover_all` are the thin async wrapper (live only) over
the catalog, GetImmutable, the HTTP transport, and the runner.

Rust 1.98.0: workspace board 4026/0, make lint 0, production_safety_checks 0.
@cryptskii
cryptskii merged commit 22335ea into main Sep 6, 2026
16 checks passed
@cryptskii
cryptskii deleted the feat/quorumbind-settlement-resume branch September 6, 2026 17:08
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