Skip to content

feat(sofi): route-wide unlock and completion, amendment 2c-H B2 - #891

Merged
cryptskii merged 1 commit into
mainfrom
feat/route-wide-unlock-and-completion
Sep 14, 2026
Merged

cryptskii merged 1 commit into
mainfrom
feat/route-wide-unlock-and-completion

Conversation

@cryptskii

Copy link
Copy Markdown
Collaborator

What this does

Amendment 2c-H, step B2. B1 (#890) put the route-wide settlement object in the core; until now the SDK still refused any route deeper than one hop. This PR makes dlv.unlockRouted settle a signed two-hop route as one settlement:

  • one grammar-33 DlvRouteSettle, one SettlementBundle carrying every consumed vault's T_v,
  • one QuorumBind over the complete K(B) (one binding transaction, both vault keys, one tx_id),
  • one advance of the trader's chain, whose deltas are the route's two ends (input debited, final output credited; the intermediate asset never touches the trader),
  • one completion over every leg: each vault certifies its own T_v, every vault receipt reaches quorum, and only then does the one fence release. Resume finishes every leg of the same settlement.

A one-hop route still settles under grammar 26, through the same code path it always did.

The invariant

No canonical trader-state advancement occurs before route-wide completion.

In code: every step before the route-wide binding COMMITS reads state and writes nothing to the trader's chain, the fleet, a fence or a register. The only trader advance is the one admitted successor after Committed, and the trader's fence is released only after every leg is certified and every receipt is durable. A receipt below quorum on any vault leaves the whole settlement bound and unrealized.

Changes

Dispatcher and profile (dlv_routes.rs, sofi_profile.rs, route_routes.rs)

  • BETA_MAX_HOPS = 2 by the H12 ruling. (hops, transitions, fanout) = (2, 2, 1) is pinned by a test, and a second test pins hops == transitions without aliasing one constant to the other. The compile-time guard now asserts BETA_MAX_HOPS <= ccb::MAX_TRANSITIONS.
  • A route deeper than the profile is refused before anything happens. A route of two or more hops goes to unlock_routed_route.

Route entry (unlock_routed_route)

  • H1: vault_id must be empty for a multi-hop route. Naming any vault, even one on the route, is refused.
  • The signed route is verified once with verify_route_commit_chain (signature, hop chain, ends, no repeated vault or parent), and X must be visible.
  • Every hop then passes its vault's gates, now gate_route_hop, extracted unchanged from the single-hop path.
  • Each signed hop must be exactly the trade its gates verified: parent, assets, amounts and fee. The curve gate prices at the vault's own rate, so without this check a hop signed at another fee would pass every gate and fail SAT.4-R only after the bind.

One settlement tail (settle_gated_legs, shared by one-hop and route)

  • Adoption is required only for the final output.
  • Every leg asset must be rooted, intermediates included.
  • Occupancy is checked at every vault under this X.
  • H15: every consumed parent commits one storage set and one quorum, refused before signing.
  • SAT.6-R: the signed total fee must equal the sum of the vaults' fees.
  • Route conservation is checked before signing.
  • Construction: one successor and one allocation per leg, one bundle, reserve-provenance preflight per leg, one bind.

Admission (economic_admission_flow.rs)

  • admitted_market_settle takes one reserve-consumption evidence per consumed vault, in route-leg order: 0x0026 for one leg, 0x0035 for a route.

Completion and resume (dlv_routes.rs)

  • MarketCompletion { legs }: per leg, recover or sign the receipt, certify through the walk, and freeze. The receipt closure is built once, and the release waits for every receipt at quorum.
  • Resume decodes grammar 26 or 33.

Readers that assumed one vault

  • vault_state_composition.rs reads the settler and route bytes from either settle.
  • sofi_receipt_publication.rs requires every transition to commit the fence's set.
  • Core acceptance_verify.rs accepts the settler of a route settle, with a new test.
  • Core lineage.rs classifies a route settle as Settle.

Bind (settlement_bind.rs)

  • H15, "bind_settlement refuses": a bundle whose successor commits another storage set, or this set at another quorum, is refused before publication, fence or round.
  • The bind, occupancy and composition test fixtures now commit the set they bind under, via in_settlement_domain. The core fixtures commit a fixed three-node set.

Frontend prose

  • The unlockVaultRouted doc and one error string still described the withdrawn per-hop settle, and so did a Jest test title and a SwapTab comment. No behaviour change; the wallet already submits an empty vault_id.

Tests

New:

  • a_two_hop_route_settles_as_one_bundle_one_binding_and_one_trader_advance, which checks:
    • the bundle consumes exactly both parents;
    • the bound operation is grammar 33 with two legs;
    • every CAS round over either key is the one transaction over both;
    • the trader moves once, from the bundle's parent to its successor;
    • a −1000, c +out, b unchanged;
    • both vaults at generation 1 by their own leg;
    • both receipts at quorum and the fence released;
    • each LP catches up from its own leg (b/c first, then a/b).
  • a_route_is_refused_before_any_bind_for_its_addressing_adoption_and_signed_fees: a named vault (H1, both vaults), an unadopted final output, a hop signed at 31 bps against a 30 bps vault (SAT.4-R), and a total of 59 against 30+30 (SAT.6-R). Each refusal names its rule, with no publication, CAS, fence or movement.
  • a_route_with_one_vault_bound_by_a_rival_takes_neither_vault: a rival is bound at b/c. The route is refused at occupancy, and a/b stays unmoved with its parent still Free.
  • a_route_one_receipt_short_of_quorum_stays_held_until_resume_finishes_every_leg: only the b/c receipt prefix fails. The a/b receipt is durable, the b/c receipt is not, and the fence is held. A resume while failing realizes 0. After healing, a resume realizes 1 with both receipts durable, the fence released, no CAS and no further advance.
  • settlement_bind::a_bundle_outside_the_binding_domain_is_refused_before_publication: another set, and another quorum.
  • settlement_bind::a_route_conflicting_at_one_parent_takes_neither: the route is CONFLICT_FINAL at the held parent, and the other parent still commits for a single bundle afterwards.
  • a_route_deeper_than_the_profile_is_refused_before_any_bind: replaces the one-hop refusal test at depth BETA_MAX_HOPS + 1.
  • Core: a_route_settle_acceptance_yields_the_witness.

Targeted runs (release, --test-threads=1):

  • dsm economic::acceptance_verify + economic::lineage: 7/0.
  • dsm_sdk sofi_profile, settlement_bind, binding_occupancy, vault_state_composition, sofi_receipt_publication, settlement_resume, the new route tests, and the single-hop settle, resume, catch-up, publication-miss and binder tests: 59/0.
  • After the H1 change, T2 and the depth test: 2/0.

Mutation controls

Each mutation is applied alone and the named test is run. The file is restored byte-for-byte and verified before the next mutation.

Control Mutation Red test
M1 multi-hop dispatch disabled (a route goes down the one-hop path) a_two_hop_route_settles_as_one_bundle_one_binding_and_one_trader_advance
M2 signed-hop fee check removed (SAT.4-R) a_route_is_refused_before_any_bind_for_its_addressing_adoption_and_signed_fees
M3 signed total fee check disabled (SAT.6-R) same
M4 adoption checked on the first leg's output instead of the final output same
M5 H1 named-vault refusal disabled same
M6 release after ANY receipt at quorum instead of every receipt a_route_one_receipt_short_of_quorum_stays_held_until_resume_finishes_every_leg
M7 resume refuses the grammar-33 arm same
M8 composition reads route_commit_bytes from DlvSettle only a_two_hop_route_settles_as_one_bundle_one_binding_and_one_trader_advance (the walk cannot certify; bound-unrealized)
M9 composition reads settler_devid from DlvSettle only a_two_hop_route_settles_as_one_bundle_one_binding_and_one_trader_advance (the walk cannot certify; bound-unrealized)
M10 bind domain check: storage-set part removed settlement_bind::a_bundle_outside_the_binding_domain_is_refused_before_publication
M11 bind domain check: quorum part removed same
M12 core acceptance verifier accepts DlvSettle only economic::acceptance_verify::a_route_settle_acceptance_yields_the_witness
M13 occupancy checked at the first vault only a_route_with_one_vault_bound_by_a_rival_takes_neither_vault
M14 depth check off by one a_route_deeper_than_the_profile_is_refused_before_any_bind
M15 reserve-consumption evidence reversed out of route-leg order a_two_hop_route_settles_as_one_bundle_one_binding_and_one_trader_advance (admission refuses)
M16 BETA_MAX_HOPS = 3 build refused by the BETA_MAX_HOPS <= ccb::MAX_TRANSITIONS guard

The tree was verified byte-identical after every control. The first cut of M8/M9 did not compile (a duplicated DlvSettle alternative is an unreachable pattern under #![deny(warnings)]); they were rerun with a local allow and went red.

Gates no fixture reaches:

  • The unlock's single-domain refusal: both fixture vaults are born under one set. The bind-level H15 check behind it is mutation-tested above.
  • The per-leg reserve-provenance preflight: there is no fixture with an unprovenanced second vault.
  • The SofiReceipt recovery check that every transition commits the fence's set.
  • Completion's cross-leg set comparison.

Not in this PR

  • B3, bounded graph discovery. The binder is still pair-scoped, so the wallet cannot yet produce a two-hop route. Only a hand-signed route reaches this path until B3 lands.
  • B4, hardware proof of "two-hop atomic route settlement hardware-proven".
  • A pre-existing proto defect is flagged separately: the doc comments of DlvUnlockRoutedV1/DlvReconcileV1 are detached and render on DlvCloseV1.

Verification

make lint on the pinned 1.98.0 toolchain:

  • cargo fmt --check and cargo clippy --all-targets -D warnings are clean.
  • Frontend npm run lint exits 0 with 0 errors. Its one warning is pre-existing, in RecoveryPipelineScreen.tsx, a file this PR does not touch.
  • Frontend Jest route_commit.test.ts passes 42/42.

ci/production_safety_checks.sh: clippy production safety checks and TLA+ formal verification passed.. CI is the board.

dlv.unlockRouted settles a signed route of up to two hops as ONE settlement:
one grammar-33 DlvRouteSettle, one bundle over every consumed vault, one
QuorumBind over K(B), one trader advance whose deltas are the route's two
ends, and one completion that releases the trader's fence only after every
leg certifies and every vault receipt is at quorum. A one-hop route settles
under grammar 26 through the same settlement tail.

- profile (hops, transitions, fanout) = (2, 2, 1); hops == transitions pinned
  by test, never aliased; the compile-time guard bounds hops by the core
  bundle cardinality
- per-hop vault gates extracted (gate_route_hop); a multi-hop route needs an
  empty vault_id (H1), verifies its chain once, and each signed hop must equal
  the trade its gates verified, fee included (SAT.4-R)
- one settlement tail: adoption of the final output only, every leg asset
  rooted, occupancy at every vault, one settlement domain (H15), signed total
  fee == sum of vault fees (SAT.6-R), route conservation before signing
- admission takes one reserve-consumption evidence per vault in route-leg
  order (0x0026 for one leg, 0x0035 for a route)
- completion per leg, release after every receipt; resume decodes 26 or 33
- composition, SofiReceipt recovery, the core acceptance verifier and lineage
  classification read route settles
- bind_settlement refuses a bundle whose successors commit another storage
  set or quorum (H15); bind/occupancy/composition fixtures now commit the set
  they bind under
- frontend prose no longer describes the withdrawn per-hop settle
@cryptskii
cryptskii merged commit 77bfda9 into main Sep 14, 2026
23 of 24 checks passed
@cryptskii
cryptskii deleted the feat/route-wide-unlock-and-completion branch September 14, 2026 23:07
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