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