From 6222175eea6da6a0f27f4a203480c9f06d44f1f1 Mon Sep 17 00:00:00 2001 From: Cryptskii <47649969+cryptskii@users.noreply.github.com> Date: Thu, 3 Sep 2026 23:23:17 -0400 Subject: [PATCH] feat(dlv): a trader can find and verify the owner's reserve proof MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The generic proof artifact landed with nothing pointing at it. Its content address was carried by no published object, and nothing anywhere mapped a vault to its owner's economic position, so the two inputs the 0x0026 arm needs from the owner's side — the reserve leaves' 256-sibling inclusion paths, and the position whose registered root they prove into — had no source a counterparty could reach. Both are the same missing thing: a locator. admitted create publishes the artifact -> record stamps (addr, position) -> routing advertisement carries them -> trader resolves the position's root from the OWNER's register -> fetches by content address, re-derives every path -> verified reserve leaves The locator is untrusted at every hop, and the advertisement is unsigned. A reader never believes either half: it resolves the root from the publisher's own write-once register cell and recomputes the inclusion paths against it. A wrong address or position can only make the lookup FAIL — it cannot yield leaves under a root the owner did not register — and what it does yield is still just "what that root committed", never "the state you should trade against". - `AdmittedOutcome` carries the artifact's address, so the create can stamp what it just published. A funded create always writes two reserve leaves, so a missing artifact is a contradiction between the route and the producer and is refused rather than left to surface later as an unexplained absence. - `amm_vault_records` gains `economic_proof_addr` + `economic_proof_position`, read back as one `Option` — both halves or neither, a present address of the wrong width dropping the row exactly as a bad policy commit does. Written by a narrow updater that refuses a zero address. - `RoutingVaultAdvertisementV1` carries the same pair; the publish route fills it from the record, never from the request, like the policy digest and the baseline beside it. - `verified_owner_reserve_leaves` is the trader's read: lineage walk to the owner's validated root, fetch, recompute, filter to this vault. Proven across TWO DEVICES: the owner creates and publishes; a trader on its own database, holding no record of the vault, finds the ad through storage alone, and turns the advertised pair into both reserve legs at the create's vault generation. A locator naming another position, or another artifact, yields nothing. Three mutation cycles — the stamp's UPDATE, the ad's fill from the record, and the reader's verification — each reddening only its own named test, restored checksum-verified. Two earlier attempts were no-ops that proved nothing and were redone; one of them showed this test does not isolate the artifact verification, which its own test does. WIPE BOUNDARY (CHANGELOG): client schema 12 -> 13. `CREATE TABLE IF NOT EXISTS` does not add columns, so an older database is structurally invalid and is refused by design. No migration, deliberately. NOT in this change: the 0x0026 bundle itself. `ReserveConsumptionEvidenceV1` still carries its own leaf and path fields rather than consuming the generic artifact, and nothing builds one. That is the next cut, which this one exists to make possible. Boards: workspace 74 binaries, 3965 passed / 0 failed / 17 ignored (dsm lib 1692/0, dsm_sdk lib 1814/0/7 ignored), exit 0; node 11 binaries, 276/0, exit 0; make lint exit 0; frontend type-check, lint and jest green; production safety PASS (pinned 1.98.0); ci_scan PASS. --- CHANGELOG.md | 17 ++ .../dsm_sdk/src/handlers/dlv_routes.rs | 241 +++++++++++++++++- .../dsm_sdk/src/handlers/route_routes.rs | 6 + .../src/sdk/economic_admission_flow.rs | 23 +- .../dsm_sdk/src/sdk/economic_registers.rs | 57 +++++ .../dsm_sdk/src/sdk/route_commit_sdk.rs | 2 + .../dsm_sdk/src/sdk/routing_path_sdk.rs | 4 + .../dsm_sdk/src/sdk/routing_sdk.rs | 16 ++ .../dsm_sdk/src/sdk/vault_rehydration.rs | 2 + .../storage/client_db/amm_vault_records.rs | 72 +++++- .../dsm_sdk/src/storage/client_db/mod.rs | 14 +- dsm_client/frontend/src/proto/dsm_app_pb.ts | 24 ++ proto/dsm_app.proto | 13 + 13 files changed, 471 insertions(+), 20 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 16e8ff12..18a41855 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -83,6 +83,23 @@ Versioning: [Semantic Versioning](https://semver.org/spec/v2.0.0.html) persisted format changes; no wipe: the vault-state leaf this relies on has been required since the rehydration gate. +- **Client database schema 12 → 13: the AMM vault record gains a reserve-proof + locator, and beta does not migrate.** `amm_vault_records` now carries + `economic_proof_addr` and `economic_proof_position` — the content address of + the `EconomicProofArtifactV1` the admitted create publishes for the vault's + reserve leaves, and the economic position whose registered root that proof + names. `CREATE TABLE IF NOT EXISTS` does not add columns to an existing file, + so an older database is structurally invalid and `enforce_schema_version` + refuses it by design: wipe the app database and re-provision from the wallet + seed. Beta runs on `dsm-testnet` with no production user state. + + The routing advertisement carries the same pair, which is what lets a trader + find the proof at all. Both are LOCATORS on an unsigned object: a reader + resolves the position's root from the owner's own write-once register cell + and recomputes every inclusion path against it, so a wrong address or + position can only make a lookup fail, never make one succeed against a root + the owner did not register. + --- ## [0.1.0-beta.3] — 2026-04-22 diff --git a/dsm_client/deterministic_state_machine/dsm_sdk/src/handlers/dlv_routes.rs b/dsm_client/deterministic_state_machine/dsm_sdk/src/handlers/dlv_routes.rs index 4cbe2e02..f71ed3c5 100644 --- a/dsm_client/deterministic_state_machine/dsm_sdk/src/handlers/dlv_routes.rs +++ b/dsm_client/deterministic_state_machine/dsm_sdk/src/handlers/dlv_routes.rs @@ -931,6 +931,11 @@ impl AppRouterImpl { // Stamped after finalize + policy stamping below, // the earliest point at which the bytes are final. vault_post_proto: Vec::new(), + // Stamped after the admitted create publishes + // the artifact — it does not exist yet, and a + // placeholder would be a locator pointing + // nowhere. + economic_proof: None, }, ) } @@ -1068,20 +1073,53 @@ impl AppRouterImpl { // birth objects are still signed off this advance's own root // and frozen in its transaction — the difference is that the // economic transition is now real rather than head-only. - if let Err(e) = crate::sdk::economic_admission_flow::admitted_dlv_create_funded( - &self.core_sdk, - op, - rel_key, - actor, - init_tip, - funding_mutation, - display_name_for, - build, - write, - ) - .await + let admitted = + match crate::sdk::economic_admission_flow::admitted_dlv_create_funded( + &self.core_sdk, + op, + rel_key, + actor, + init_tip, + funding_mutation, + display_name_for, + build, + write, + ) + .await + { + Ok((_outcome, admitted)) => admitted, + Err(e) => return err(format!("dlv.create: funded creation failed: {e}")), + }; + // THE LOCATOR, stamped once the thing it points at exists. The + // admitted create published an inclusion proof for the reserve + // leaves it just wrote; the address and the position whose + // registered root it names are what a trader needs to find it, + // and the routing advertisement carries them from here. + // + // A funded create ALWAYS writes two reserve leaves, so the + // artifact is always published: its absence is a contradiction + // between this route and the producer, not a vault without a + // proof, and is refused rather than left to surface later as an + // unexplained missing locator. + let Some(proof_addr) = admitted.economic_proof_addr else { + return err( + "dlv.create: the admitted creation published no reserve-proof artifact — \ + refusing to leave the vault without a locator" + .into(), + ); + }; + if let Err(e) = + crate::storage::client_db::amm_vault_records::update_economic_proof_locator( + &vault_id, + &crate::storage::client_db::amm_vault_records::EconomicProofLocator { + addr: proof_addr, + position: admitted.economic_position, + }, + ) { - return err(format!("dlv.create: funded creation failed: {e}")); + return err(format!( + "dlv.create: stamping the reserve-proof locator: {e}" + )); } } // A non-AMM vault: the plain advance, nothing to freeze. @@ -6323,6 +6361,183 @@ mod funded_creation_tests { } } + /// THE LOCATOR, END TO END, ACROSS TWO DEVICES. + /// + /// The owner's admitted create publishes an inclusion proof for the reserve + /// leaves it wrote, and stamps WHERE it lives onto the vault record; the + /// advertisement carries that address and the economic position whose + /// registered root the proof names. A trader on its own device, holding no + /// record of this vault, reads the advertisement and turns that untrusted + /// pair into VERIFIED reserve leaves — resolving the position's root from + /// the owner's own register cell and recomputing every path against it. + /// + /// Before this, both halves of the trader's 0x0026 evidence had no source: + /// the 256-sibling paths existed only inside the owner's tree, and nothing + /// mapped a vault to its owner's economic position. + /// + /// The last two arms are the point of an unsigned advertisement: a locator + /// naming another position, or another artifact, FAILS. It cannot yield + /// leaves under a root the owner did not register. + #[test] + #[serial] + fn a_trader_turns_the_advertised_locator_into_verified_owner_reserves() { + use prost::Message as _; + + install_identity(); + let owner_dev = participant("owner", 0x49); + let owner = owner_dev.router(); + let (pc_a, pc_b) = + crate::sdk::funded_vault_fixture::admitted_device_holding(owner, 20_000, 5_000); + let create = generated::DlvInstantiateV1 { + spec: Some(generated::DlvSpecV1 { + policy_digest: Vec::new(), + fulfillment_bytes: amm_fulfillment_bytes(&pc_a, &pc_b, 30), + anchor_enforcement: generated::AnchorEnforcement::Required as i32, + ..Default::default() + }), + creator_public_key: Vec::new(), + signature: Vec::new(), + funding_legs: vec![ + generated::DlvFundingLegV1 { + policy_commit: pc_a.to_vec(), + amount: 10_000, + }, + generated::DlvFundingLegV1 { + policy_commit: pc_b.to_vec(), + amount: 5_000, + }, + ], + }; + let res = crate::runtime::get_runtime().block_on(async { + owner + .invoke(AppInvoke { + method: "dlv.create".to_string(), + args: pack(create.encode_to_vec()), + }) + .await + }); + assert!(res.success, "create failed: {:?}", res.error_message); + let rec = crate::storage::client_db::amm_vault_records::list_amm_vault_records() + .expect("list") + .pop() + .expect("one vault"); + let vault_id = rec.vault_id; + + // (1) The create stamped the locator onto the record. + let locator = rec + .economic_proof + .expect("the admitted create stamps where its reserve proof lives"); + assert_ne!(locator.addr, [0u8; 32]); + let (owner_genesis, owner_devid) = (rec.owner_genesis, rec.owner_devid); + + // (2) The advertisement carries it. + let publish = generated::PublishRoutingAdvertisementRequest { + vault_id: vault_id.to_vec(), + token_a: pc_a.to_vec(), + token_b: pc_b.to_vec(), + fee_bps: 30, + unlock_spec_digest: Vec::new(), + unlock_spec_key: "sofi/spec/locator".to_string(), + owner_public_key: Vec::new(), + vault_proto_bytes: Vec::new(), + }; + let res = crate::runtime::get_runtime().block_on(async { + owner + .invoke(AppInvoke { + method: "route.publishRoutingAdvertisement".to_string(), + args: pack(publish.encode_to_vec()), + }) + .await + }); + assert!(res.success, "publish failed: {:?}", res.error_message); + + // ── TRADER: its own device, no record of this vault ────────────────── + let trader_dev = participant("trader", 0x59); + trader_dev.enter(); + assert!( + crate::storage::client_db::amm_vault_records::get_amm_vault_record(&vault_id) + .expect("record read") + .is_none(), + "the trader holds no record of the owner's vault" + ); + let ads = crate::runtime::get_runtime() + .block_on(crate::sdk::routing_sdk::load_all_advertisements_for_pair( + &pc_a, &pc_b, + )) + .expect("advertisements load"); + let ad = ads + .into_iter() + .find(|a| a.advertisement.vault_id == vault_id.to_vec()) + .expect("the trader finds the vault through storage alone") + .advertisement; + let advertised_addr: [u8; 32] = ad + .economic_proof_addr + .as_slice() + .try_into() + .expect("the ad carries a 32-byte reserve-proof address"); + assert_eq!( + advertised_addr, locator.addr, + "the ad carries the record's locator" + ); + assert_eq!(ad.economic_proof_position, locator.position); + + // (3) THE READ. The untrusted pair becomes verified leaves. + let network_id = + crate::sdk::economic_admission_flow::committed_network_id().expect("network id"); + let set = crate::sdk::economic_admission_flow::canonical_set(&network_id).expect("set"); + let leaves = crate::runtime::get_runtime() + .block_on(async { + crate::sdk::economic_registers::verified_owner_reserve_leaves( + &set, + &network_id, + &owner_genesis, + &owner_devid, + &vault_id, + &advertised_addr, + ad.economic_proof_position, + ) + }) + .expect("a trader verifies the owner's reserve leaves from the advertised locator"); + let mut got: Vec<([u8; 32], u64, u64)> = leaves + .iter() + .map(|l| (l.policy_commit, l.amount, l.vault_sequence)) + .collect(); + got.sort(); + let mut want = vec![(pc_a, 10_000u64, 0u64), (pc_b, 5_000u64, 0u64)]; + want.sort(); + assert_eq!( + got, want, + "both reserve legs, at the create's vault generation" + ); + + // (4) A LOCATOR IS NOT A WARRANT. A different position, or a different + // artifact, cannot produce leaves under a root the owner registered. + let network_id2 = + crate::sdk::economic_admission_flow::committed_network_id().expect("network id"); + let set2 = crate::sdk::economic_admission_flow::canonical_set(&network_id2).expect("set"); + for (name, addr, position) in [ + ( + "another position", + advertised_addr, + ad.economic_proof_position + 1, + ), + ("another artifact", [0x99u8; 32], ad.economic_proof_position), + ] { + let e = crate::runtime::get_runtime().block_on(async { + crate::sdk::economic_registers::verified_owner_reserve_leaves( + &set2, + &network_id2, + &owner_genesis, + &owner_devid, + &vault_id, + &addr, + position, + ) + }); + assert!(e.is_err(), "{name} must not yield verified leaves"); + } + } + /// One trader's full production settle against `vault_id` at generation /// `seq`, whose reserves the trader believes to be `(ra, rb)`: mirror the /// vault, bind a hop to `(seq, reserves_digest, anchor_digest)`, sign the diff --git a/dsm_client/deterministic_state_machine/dsm_sdk/src/handlers/route_routes.rs b/dsm_client/deterministic_state_machine/dsm_sdk/src/handlers/route_routes.rs index 52aaaa4a..f8a38870 100644 --- a/dsm_client/deterministic_state_machine/dsm_sdk/src/handlers/route_routes.rs +++ b/dsm_client/deterministic_state_machine/dsm_sdk/src/handlers/route_routes.rs @@ -607,6 +607,12 @@ impl AppRouterImpl { owner_public_key: &req.owner_public_key, vault_proto_bytes: &req.vault_proto_bytes, anchor_presentation_digest: presentation_digest, + // THE RESERVE-PROOF LOCATOR, from the vault's own record — the + // same place the policy digest and the baseline come from, and + // never from the request. The ad authenticates nothing, so this + // only tells a trader WHERE to look; what it finds is checked + // against the owner's registered root, not against this ad. + economic_proof: record.economic_proof.map(|l| (l.addr, l.position)), }; if let Err(e) = crate::sdk::routing_sdk::publish_active_advertisement(publish_input).await { return err(format!( diff --git a/dsm_client/deterministic_state_machine/dsm_sdk/src/sdk/economic_admission_flow.rs b/dsm_client/deterministic_state_machine/dsm_sdk/src/sdk/economic_admission_flow.rs index 3c1c9e50..83f6da15 100644 --- a/dsm_client/deterministic_state_machine/dsm_sdk/src/sdk/economic_admission_flow.rs +++ b/dsm_client/deterministic_state_machine/dsm_sdk/src/sdk/economic_admission_flow.rs @@ -66,6 +66,11 @@ pub(crate) fn committed_network_id() -> Result, DsmError> { #[derive(Debug, Clone, Copy)] pub struct AdmittedOutcome { pub economic_position: u64, + /// Content address of the `EconomicProofArtifactV1` this admission + /// published, when its transition wrote a leaf a counterparty can be asked + /// to verify. `None` means the transition wrote none — not that publishing + /// failed, which is an error rather than an absence. + pub economic_proof_addr: Option<[u8; 32]>, } fn storage_err(what: &str, e: impl core::fmt::Display) -> DsmError { @@ -1027,11 +1032,18 @@ pub(crate) async fn finish_admission( // taking the paths. The equality below states it rather than assuming // it, and `EconomicProofArtifact::new` re-derives every path against // that same root before the bytes exist. - if let Some(proof) = - economic_proof_artifact_for(&tree, &witness, &genesis, &devid, &new_validated)? - { - post_admit_artifacts.push(proof); - } + let economic_proof_addr = + match economic_proof_artifact_for(&tree, &witness, &genesis, &devid, &new_validated)? { + Some((key, bytes, purpose)) => { + let addr = dsm::storage_object::immutable_inner( + dsm::common::domain_tags::TAG_DSM_ECONOMIC_PROOF_ARTIFACT, + &bytes, + ); + post_admit_artifacts.push((key, bytes, purpose)); + Some(addr) + } + None => None, + }; let had_post_admit = !post_admit_artifacts.is_empty(); core.admit_economic_position( @@ -1056,6 +1068,7 @@ pub(crate) async fn finish_admission( Ok(AdmittedOutcome { economic_position: new_validated.economic_position(), + economic_proof_addr, }) } diff --git a/dsm_client/deterministic_state_machine/dsm_sdk/src/sdk/economic_registers.rs b/dsm_client/deterministic_state_machine/dsm_sdk/src/sdk/economic_registers.rs index 7628e985..8558d08a 100644 --- a/dsm_client/deterministic_state_machine/dsm_sdk/src/sdk/economic_registers.rs +++ b/dsm_client/deterministic_state_machine/dsm_sdk/src/sdk/economic_registers.rs @@ -790,6 +790,63 @@ pub(crate) fn economic_root_path(k_root: &[u8; 32]) -> String { /// The republish-sweep object key for an immutable blob: /// `immutable::{namespace}::{addr_b32}`. Same shape `dlv_routes` uses, so the /// one generic sweep carries faucet evidence too. +/// THE TRADER'S READ of a vault owner's reserve proof, from an untrusted +/// locator to verified leaves. +/// +/// The advertisement is unsigned and carries only `(addr, position)`. Neither +/// is believed. The position's root is resolved from the OWNER's own +/// write-once register cell by the same lineage walk a foreign verifier runs, +/// the artifact is fetched by content address and re-hashed to it, and every +/// leaf key, commitment and inclusion path is recomputed against that root. A +/// locator naming the wrong position or a different artifact therefore fails; +/// it can never yield leaves under a root the owner did not register. +/// +/// Returns the vault-reserve leaves for `vault_id` only. A caller still has to +/// decide whether the amounts are the ones it expects — this answers "what did +/// the owner's registered root commit", never "is that the right state". +pub(crate) fn verified_owner_reserve_leaves( + set: &StorageSet, + expected_network_id: &[u8], + owner_genesis: &[u8; 32], + owner_devid: &[u8; 32], + vault_id: &[u8; 32], + proof_addr: &[u8; 32], + economic_position: u64, +) -> Result, DsmError> { + let resolver = LiveRegisterResolver { + set, + runtime: tokio::runtime::Handle::current(), + expected_network_id: expected_network_id.to_vec(), + }; + let owner = resolver + .validated_peer_transition(owner_genesis, owner_devid, economic_position) + .map_err(|e| { + DsmError::verification(format!( + "owner reserve proof: the owner's economic lineage at position \ + {economic_position} does not validate: {e:?}" + )) + })?; + let root = owner.validated_root.economic_root(); + let artifact = tokio::task::block_in_place(|| { + resolver.runtime.block_on(fetch_verified_economic_proof( + proof_addr, + owner_genesis, + owner_devid, + economic_position, + &root, + )) + })?; + Ok(artifact + .states() + .filter_map(|s| match s { + dsm::economic::state::EconomicLeafState::VaultReserve(v) if v.vault_id == *vault_id => { + Some(v.clone()) + } + _ => None, + }) + .collect()) +} + /// Fetch an economic proof artifact by content address and verify it against /// coordinates the caller established INDEPENDENTLY — the publisher's /// write-once register cell at that position, read at quorum. diff --git a/dsm_client/deterministic_state_machine/dsm_sdk/src/sdk/route_commit_sdk.rs b/dsm_client/deterministic_state_machine/dsm_sdk/src/sdk/route_commit_sdk.rs index 26b459d6..25c9c8cc 100644 --- a/dsm_client/deterministic_state_machine/dsm_sdk/src/sdk/route_commit_sdk.rs +++ b/dsm_client/deterministic_state_machine/dsm_sdk/src/sdk/route_commit_sdk.rs @@ -2303,6 +2303,7 @@ mod tests { owner_public_key: &bob.public_key, vault_proto_bytes: &vault_proto_bytes, anchor_presentation_digest: [0u8; 32], + economic_proof: None, }, ) .await @@ -2497,6 +2498,7 @@ mod tests { owner_public_key: &bob.public_key, vault_proto_bytes: &vault_proto_bytes, anchor_presentation_digest: [0u8; 32], + economic_proof: None, }, ) .await diff --git a/dsm_client/deterministic_state_machine/dsm_sdk/src/sdk/routing_path_sdk.rs b/dsm_client/deterministic_state_machine/dsm_sdk/src/sdk/routing_path_sdk.rs index 41c10641..1f7e1ee9 100644 --- a/dsm_client/deterministic_state_machine/dsm_sdk/src/sdk/routing_path_sdk.rs +++ b/dsm_client/deterministic_state_machine/dsm_sdk/src/sdk/routing_path_sdk.rs @@ -606,6 +606,8 @@ mod tests { lifecycle_state: LIFECYCLE_ACTIVE.to_string(), updated_state_number: state_number, anchor_presentation_digest: vec![0u8; 32], + economic_proof_addr: Vec::new(), + economic_proof_position: 0, } } @@ -742,6 +744,7 @@ mod tests { owner_public_key: &[0xABu8; 64], vault_proto_bytes: &good_proto, anchor_presentation_digest: [0u8; 32], + economic_proof: None, }) .await .expect("publish good"); @@ -757,6 +760,7 @@ mod tests { owner_public_key: &[0xABu8; 64], vault_proto_bytes: &bad_proto, anchor_presentation_digest: [0u8; 32], + economic_proof: None, }) .await .expect("publish bad"); diff --git a/dsm_client/deterministic_state_machine/dsm_sdk/src/sdk/routing_sdk.rs b/dsm_client/deterministic_state_machine/dsm_sdk/src/sdk/routing_sdk.rs index aca7e335..63d1a44e 100644 --- a/dsm_client/deterministic_state_machine/dsm_sdk/src/sdk/routing_sdk.rs +++ b/dsm_client/deterministic_state_machine/dsm_sdk/src/sdk/routing_sdk.rs @@ -112,6 +112,11 @@ pub(crate) struct PublishRoutingAdInput<'a> { /// discovery material for the trader's verification chain; the ad itself /// authenticates nothing. pub anchor_presentation_digest: [u8; 32], + /// The owner's reserve-proof locator: the artifact's content address and + /// the economic position whose registered root it names. `None` for a + /// vault whose record carries none; the ad then advertises no locator and + /// a trader that needs one fails closed rather than guessing. + pub economic_proof: Option<([u8; 32], u64)>, } /// Publish an active-state advertisement + the full vault proto mirror. @@ -158,6 +163,11 @@ pub(crate) async fn publish_active_advertisement( lifecycle_state: LIFECYCLE_ACTIVE.to_string(), updated_state_number: 1, anchor_presentation_digest: input.anchor_presentation_digest.to_vec(), + economic_proof_addr: input + .economic_proof + .map(|(addr, _)| addr.to_vec()) + .unwrap_or_default(), + economic_proof_position: input.economic_proof.map(|(_, pos)| pos).unwrap_or(0), }; BitcoinTapSdk::storage_put_bytes(&proto_key_str, input.vault_proto_bytes).await?; @@ -489,6 +499,7 @@ mod tests { owner_public_key: &[0xABu8; 64], vault_proto_bytes: b"vault-proto", anchor_presentation_digest: [0u8; 32], + economic_proof: None, }) .await .expect("publish"); @@ -545,6 +556,7 @@ mod tests { owner_public_key: &[0xABu8; 64], vault_proto_bytes: b"vault-proto", anchor_presentation_digest: [0u8; 32], + economic_proof: None, }) .await .expect("publish"); @@ -618,6 +630,7 @@ mod tests { owner_public_key: &[0xABu8; 64], vault_proto_bytes: &proto, anchor_presentation_digest: [0u8; 32], + economic_proof: None, }) .await .expect("publish_active_advertisement"); @@ -684,6 +697,7 @@ mod tests { owner_public_key: &[0xABu8; 64], vault_proto_bytes: &fake_vault_proto_bytes(0x02), anchor_presentation_digest: [0u8; 32], + economic_proof: None, }) .await .expect("publish"); @@ -829,6 +843,8 @@ mod tests { lifecycle_state: LIFECYCLE_ACTIVE.to_string(), updated_state_number: 5, anchor_presentation_digest: vec![0u8; 32], + economic_proof_addr: Vec::new(), + economic_proof_position: 0, }; BitcoinTapSdk::storage_put_bytes(&proto_key_str, &proto) .await diff --git a/dsm_client/deterministic_state_machine/dsm_sdk/src/sdk/vault_rehydration.rs b/dsm_client/deterministic_state_machine/dsm_sdk/src/sdk/vault_rehydration.rs index 109f3b7e..3e914c9c 100644 --- a/dsm_client/deterministic_state_machine/dsm_sdk/src/sdk/vault_rehydration.rs +++ b/dsm_client/deterministic_state_machine/dsm_sdk/src/sdk/vault_rehydration.rs @@ -511,6 +511,7 @@ mod tests { baseline_state_ccb: Vec::new(), baseline_presentation: Vec::new(), vault_post_proto: Vec::new(), + economic_proof: None, }; (record, head) } @@ -872,6 +873,7 @@ mod tests { baseline_state_ccb: Vec::new(), baseline_presentation: Vec::new(), vault_post_proto: Vec::new(), + economic_proof: None, }; assert_eq!( rehydrate_amm_vault(&record, &head), diff --git a/dsm_client/deterministic_state_machine/dsm_sdk/src/storage/client_db/amm_vault_records.rs b/dsm_client/deterministic_state_machine/dsm_sdk/src/storage/client_db/amm_vault_records.rs index 2c929a8a..a3952b94 100644 --- a/dsm_client/deterministic_state_machine/dsm_sdk/src/storage/client_db/amm_vault_records.rs +++ b/dsm_client/deterministic_state_machine/dsm_sdk/src/storage/client_db/amm_vault_records.rs @@ -75,6 +75,23 @@ pub struct AmmVaultRecord { /// publishing survives a restart without consulting the in-memory /// DLVManager. Empty means the producer never ran; consumers fail closed. pub vault_post_proto: Vec, + /// WHERE THIS VAULT'S RESERVE PROOF LIVES — the content address of the + /// `EconomicProofArtifactV1` the admitted create published, and the + /// economic position whose registered root it names. + /// + /// A LOCATOR, on the way in and on the way out. A reader resolves the + /// position's root from the publisher's own register cell and re-derives + /// every inclusion path, so a wrong address or position here can only make + /// a lookup fail; it can never make one succeed against a root the owner + /// did not register. `None` when the create published no artifact. + pub economic_proof: Option, +} + +/// The two halves of a reserve-proof locator; only ever present together. +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub struct EconomicProofLocator { + pub addr: [u8; 32], + pub position: u64, } /// TEST-ONLY full-row writer. @@ -142,6 +159,38 @@ pub fn update_baseline_with_conn( Ok(()) } +/// Stamp this vault's reserve-proof locator onto its record. Runs once, at +/// `dlv.create`, after the admitted create published the artifact — the +/// earliest point at which both halves exist. Refuses a zero address so an +/// absent locator can never be written as a present one. +pub fn update_economic_proof_locator( + vault_id: &[u8; 32], + locator: &EconomicProofLocator, +) -> Result<()> { + if locator.addr == [0u8; 32] { + anyhow::bail!("refusing to stamp a zero economic-proof address"); + } + let binding = get_connection()?; + let conn = binding.lock().unwrap_or_else(|poisoned| { + log::warn!("DB lock poisoned in update_economic_proof_locator, recovering"); + poisoned.into_inner() + }); + let changed = conn.execute( + "UPDATE amm_vault_records + SET economic_proof_addr = ?2, economic_proof_position = ?3 + WHERE vault_id = ?1", + params![ + vault_id.as_slice(), + locator.addr.as_slice(), + locator.position as i64 + ], + )?; + if changed != 1 { + anyhow::bail!("economic-proof locator stamp touched {changed} rows for one vault id"); + } + Ok(()) +} + /// Stamp the vault's frozen `VaultPostProto` bytes onto its record. Runs once, /// at `dlv.create`, after the vault is finalized and its enforcement/policy /// digest are stamped — the earliest point at which the bytes are final. @@ -181,7 +230,8 @@ pub fn get_amm_vault_record(vault_id: &[u8; 32]) -> Result Result>(9)?, r.get::<_, Vec>(10)?, r.get::<_, Vec>(11)?, + r.get::<_, Vec>(12)?, + r.get::<_, i64>(13)?, )) }, ) @@ -215,6 +267,8 @@ pub fn get_amm_vault_record(vault_id: &[u8; 32]) -> Result Result Result Result { /// DAG only); `recipient_outbound_reply` gains `held` (the B→A release is /// frozen at accept and promoted to deliverable in the terminal admission /// transaction — ECON_ADMITTED releases it atomically). -pub const CLIENT_DB_SCHEMA_VERSION: i64 = 12; +pub const CLIENT_DB_SCHEMA_VERSION: i64 = 13; /// Honest incompatibility detection — NOT legacy support. /// @@ -1377,6 +1377,18 @@ fn create_schema(conn: &Connection) -> Result<()> { -- in-memory DLVManager. Empty means the producer never ran: the -- ad publisher fails closed rather than re-deriving. vault_post_proto BLOB NOT NULL DEFAULT X'', + -- WHERE THE OWNER'S ECONOMIC RESERVE PROOF LIVES. The admitted + -- funded create publishes an `EconomicProofArtifactV1` proving the + -- vault's reserve leaves under the owner's registered economic + -- root; these two carry its content address and the position that + -- root sits at, so the routing advertisement can hand a trader a + -- LOCATOR. Untrusted on the way out and on the way back: a reader + -- resolves the position's root from the register itself and + -- re-derives every path, so a wrong value here can only fail. + -- Empty address means the create predates the artifact or its + -- publication never ran; consumers fail closed rather than guess. + economic_proof_addr BLOB NOT NULL DEFAULT X'', + economic_proof_position INTEGER NOT NULL DEFAULT 0, created_at INTEGER NOT NULL ); diff --git a/dsm_client/frontend/src/proto/dsm_app_pb.ts b/dsm_client/frontend/src/proto/dsm_app_pb.ts index 185faef3..c3523f83 100644 --- a/dsm_client/frontend/src/proto/dsm_app_pb.ts +++ b/dsm_client/frontend/src/proto/dsm_app_pb.ts @@ -10728,6 +10728,28 @@ export class RoutingVaultAdvertisementV1 extends Message) { super(); proto3.util.initPartial(data, this); @@ -10751,6 +10773,8 @@ export class RoutingVaultAdvertisementV1 extends Message): RoutingVaultAdvertisementV1 { diff --git a/proto/dsm_app.proto b/proto/dsm_app.proto index 743e825f..2132c15e 100644 --- a/proto/dsm_app.proto +++ b/proto/dsm_app.proto @@ -1866,6 +1866,19 @@ message RoutingVaultAdvertisementV1 { // P0–P6 and the c_n it carries resolves the exact `CCB(V_n)` bytes. A // trader quotes against NOTHING from this ad — reserves here are hints. bytes anchor_presentation_digest = 19 [(dsm_fixed_len)=32]; + // WHERE THE OWNER'S RESERVE PROOF LIVES: the content address of the + // `EconomicProofArtifactV1` published by the admission that wrote this + // vault's reserve leaves, and the economic position whose registered root + // it names. + // + // A LOCATOR, on an ad that authenticates nothing and is unsigned. A reader + // resolves the position's root from the owner's own write-once register + // cell and re-derives every inclusion path against it, so a wrong address + // or position here can only make the lookup FAIL — never succeed against a + // root the owner did not register — and the reserve amounts it yields are + // re-checked against the composed `V_n` afterwards. + bytes economic_proof_addr = 15 [(dsm_fixed_len)=32]; + uint64 economic_proof_position = 16; } // Typed request for `dlv.unlockRouted` — the routed atomic-unlock