diff --git a/dsm_client/deterministic_state_machine/dsm/src/common/domain_tags/dsm/misc/mod.rs b/dsm_client/deterministic_state_machine/dsm/src/common/domain_tags/dsm/misc/mod.rs index 35b1d673..029613ac 100644 --- a/dsm_client/deterministic_state_machine/dsm/src/common/domain_tags/dsm/misc/mod.rs +++ b/dsm_client/deterministic_state_machine/dsm/src/common/domain_tags/dsm/misc/mod.rs @@ -113,6 +113,8 @@ pub(super) const TAGS: &[TaggedHashDomain<'static>] = &[ TAG_DSM_DEVTREE_TRANSITION_GENESIS_SENTINEL_V1, TAG_DSM_DEVTREE_TRANSITION_SIGN, TAG_DSM_STORAGE_OBJECT, + TAG_DSM_BINDING_KEYSET, + TAG_DSM_SETTLEMENT_BUNDLE, TAG_DSM_VAULT_STATE_ANCHOR_V2, TAG_DSM_VAULT_STATE_ANCHOR_V3, TAG_DSM_VAULT_STATE_PARENT_GENESIS_V2, diff --git a/dsm_client/deterministic_state_machine/dsm/src/common/domain_tags/dsm/misc/protocol.rs b/dsm_client/deterministic_state_machine/dsm/src/common/domain_tags/dsm/misc/protocol.rs index 76291a4b..cd33731d 100644 --- a/dsm_client/deterministic_state_machine/dsm/src/common/domain_tags/dsm/misc/protocol.rs +++ b/dsm_client/deterministic_state_machine/dsm/src/common/domain_tags/dsm/misc/protocol.rs @@ -79,6 +79,16 @@ pub const TAG_DSM_VAULT_STATE_ANCHOR_V3: TaggedHashDomain<'static> = /// Req 15.2's idempotence meaningful rather than accidental. pub const TAG_DSM_STORAGE_OBJECT: TaggedHashDomain<'static> = crate::tagged_domain!(b"DSM/storage-object"); +/// `k_v = H_dom(DSM/binding-keyset, c_n)` — one settlement resource key from a +/// vault's committed parent state (Def 6.17). The vault id is NOT restated: +/// c_n commits it, so supplying both would admit a disagreeing pair. +pub const TAG_DSM_BINDING_KEYSET: TaggedHashDomain<'static> = + crate::tagged_domain!(b"DSM/binding-keyset"); +/// `b = H_dom(DSM/settlement-bundle, Canon(B))` — the immutable settlement +/// bundle identity (Def 6.14/6.19). Its content address is +/// `immutable_addr(TAG_DSM_SETTLEMENT_BUNDLE, Canon(B))`. +pub const TAG_DSM_SETTLEMENT_BUNDLE: TaggedHashDomain<'static> = + crate::tagged_domain!(b"DSM/settlement-bundle"); // --- Device Tree root progression (area 8 / registry §5.16–§5.18). Each // object has TWO domains over ONE preimage per registry §2.9: an identity diff --git a/dsm_client/deterministic_state_machine/dsm/src/dlv/mod.rs b/dsm_client/deterministic_state_machine/dsm/src/dlv/mod.rs index bbfd111b..d7c3a811 100644 --- a/dsm_client/deterministic_state_machine/dsm/src/dlv/mod.rs +++ b/dsm_client/deterministic_state_machine/dsm/src/dlv/mod.rs @@ -1,18 +1,19 @@ // SPDX-License-Identifier: Apache-2.0 -//! Tier 2 Foundation DLV primitives — pure-crypto helpers that the +//! Tier 2 Foundation DLV primitives — pure-crypto helpers that the //! `dsm_sdk` and storage layers compose into the off-device SoFi //! flow. This module deliberately holds no proto / I/O / runtime //! state; each submodule is a self-contained crypto primitive. -pub mod beta_storage_profile; // the deployed three-member beta profile — fixed, not a formula +pub mod beta_storage_profile; // the deployed three-member beta profile — fixed, not a formula pub mod controller_rotation; pub mod pair_identity; -pub mod quorum_bind; // Def 6.21 — Class K sans-IO quorum-binding decision engine +pub mod quorum_bind; // Def 6.21 — Class K sans-IO quorum-binding decision engine pub mod route_commit; +pub mod settlement_bundle; // Def 6.14 — the canonical immutable SettlementBundle + K(B) pub mod settlement_receipt_leaf; pub mod settlement_slot_claim; // write-once claim envelope for the settlement-slot quorum register -pub mod trader_fence; // Req 6.23 — the initiating-trader parent fence (pure state machine) +pub mod trader_fence; // Req 6.23 — the initiating-trader parent fence (pure state machine) pub mod vault_pending_pointer; pub mod vault_reserve_inclusion; pub mod vault_reserve_leaf; @@ -20,4 +21,4 @@ pub mod vault_smt_leaf; // vault_state_anchor (V1) and vault_state_anchor_v2 are DELETED by the // state-identity cut. Their names and domains are burned, never reused; the // only anchor form is V3 below, whose sole content is c_n. -pub mod vault_state_anchor_v3; // Def 6.4a — owner baseline over c_n; the only anchor form after the cut +pub mod vault_state_anchor_v3; // Def 6.4a — owner baseline over c_n; the only anchor form after the cut diff --git a/dsm_client/deterministic_state_machine/dsm/src/dlv/settlement_bundle.rs b/dsm_client/deterministic_state_machine/dsm/src/dlv/settlement_bundle.rs new file mode 100644 index 00000000..f1fa5209 --- /dev/null +++ b/dsm_client/deterministic_state_machine/dsm/src/dlv/settlement_bundle.rs @@ -0,0 +1,264 @@ +// SPDX-License-Identifier: Apache-2.0 + +//! THE CANONICAL SETTLEMENT BUNDLE (Rev 15 Def 6.14, 6.17, 6.19; Req 6.15). +//! +//! A [`SettlementBundleV1`] is the complete immutable object `B` a QuorumBind +//! transaction binds. It carries everything needed to verify the selected route +//! and recover the DLV decision **without a fresh constructor signature** +//! (Req 6.15): the committed storage set and `q`, the intent commitment `I` and +//! route-set commitment `X`, the selected route, the exact initiating-trader +//! parent and successor, the complete sorted per-vault transitions `{T_v}`, +//! proof material `{P_v}`, the bundle signatures, and the recovery material. +//! +//! This module is the pure canonical layer of `B`: +//! +//! - [`canon`] — the canonical bytes `Canon(B)` (the transitions must be sorted +//! strictly ascending by `vault_id`, and the message must re-encode to +//! itself). +//! - [`bundle_digest`] — `b = H(DSM/settlement-bundle ‖ Canon(B))` (Def 6.14). +//! - [`bundle_addr`] — `addr(B) = H(DSM/storage-object ‖ namespace ‖ b)` +//! (Def 6.19), via the shared immutable-object construction. +//! - [`resource_key`] / [`key_set`] — `k_v = H(DSM/binding-keyset ‖ c_n)` and +//! `K(B)`, the sorted distinct resource keys (Def 6.17), derived **only** from +//! the canonical bundle's committed parent states. +//! +//! `c_n` already commits `vault_id` (it is a field of `V_n`), so the resource +//! key does not restate the vault id — supplying both would admit a disagreeing +//! pair. No I/O, no clock. + +use crate::common::domain_tags::{TAG_DSM_BINDING_KEYSET, TAG_DSM_SETTLEMENT_BUNDLE}; +use crate::crypto::blake3::dsm_domain_hasher; +use crate::storage_object::{immutable_addr, immutable_inner}; +use crate::types::proto as generated; +use prost::Message; + +/// The only accepted bundle version. A canonical object is frozen on deploy; a +/// new shape is a new version, never a silent field change. +pub const SETTLEMENT_BUNDLE_VERSION_V1: u32 = 1; + +/// Why a bundle is not well-formed. Every variant is fail-closed. +#[derive(Debug, Clone, PartialEq, Eq)] +pub enum BundleError { + /// `version` is not [`SETTLEMENT_BUNDLE_VERSION_V1`]. + Version(u32), + /// A fixed-width field is not 32 bytes. + Width { field: &'static str, got: usize }, + /// A bundle consumes at least one vault. + NoTransitions, + /// The transitions are not strictly ascending by `vault_id` (unsorted or a + /// duplicate vault). + TransitionsNotSorted { at: usize }, + /// Two vaults committed the same `c_n`, so their resource keys collide. + DuplicateResourceKey, + /// The bytes decode but do not re-encode to themselves. + Noncanonical, +} + +impl core::fmt::Display for BundleError { + fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result { + match self { + BundleError::Version(v) => write!(f, "unsupported settlement-bundle version {v}"), + BundleError::Width { field, got } => { + write!(f, "field {field} is {got} bytes, not 32") + } + BundleError::NoTransitions => write!(f, "a settlement bundle consumes no vault"), + BundleError::TransitionsNotSorted { at } => { + write!(f, "vault transitions are not strictly ascending at {at}") + } + BundleError::DuplicateResourceKey => { + write!(f, "two vaults share a committed parent state (c_n)") + } + BundleError::Noncanonical => write!(f, "settlement bundle bytes are not canonical"), + } + } +} +impl std::error::Error for BundleError {} + +fn need32(field: &'static str, v: &[u8]) -> Result<[u8; 32], BundleError> { + <[u8; 32]>::try_from(v).map_err(|_| BundleError::Width { + field, + got: v.len(), + }) +} + +/// Validate structural well-formedness: version, fixed-width fields, at least +/// one transition, and transitions strictly ascending by `vault_id`. +pub fn validate(b: &generated::SettlementBundleV1) -> Result<(), BundleError> { + if b.version != SETTLEMENT_BUNDLE_VERSION_V1 { + return Err(BundleError::Version(b.version)); + } + need32("storage_set_id", &b.storage_set_id)?; + need32("intent_commitment", &b.intent_commitment)?; + need32("route_set_commitment", &b.route_set_commitment)?; + need32("trader_parent", &b.trader_parent)?; + need32("trader_successor", &b.trader_successor)?; + if b.vault_transitions.is_empty() { + return Err(BundleError::NoTransitions); + } + for (i, t) in b.vault_transitions.iter().enumerate() { + need32("vault_id", &t.vault_id)?; + need32("parent_state_commitment", &t.parent_state_commitment)?; + need32("parent_reserves_digest", &t.parent_reserves_digest)?; + need32("successor_ccb", &t.successor_ccb)?; + if i > 0 && b.vault_transitions[i - 1].vault_id >= t.vault_id { + return Err(BundleError::TransitionsNotSorted { at: i }); + } + } + Ok(()) +} + +/// `Canon(B)` — the canonical bytes. The bundle must be well-formed and must +/// re-encode to itself (unknown fields, non-minimal encodings, and duplicates +/// all fail). +pub fn canon(b: &generated::SettlementBundleV1) -> Result, BundleError> { + validate(b)?; + let bytes = b.encode_to_vec(); + let re = generated::SettlementBundleV1::decode(bytes.as_slice()) + .map_err(|_| BundleError::Noncanonical)?; + if re.encode_to_vec() != bytes { + return Err(BundleError::Noncanonical); + } + Ok(bytes) +} + +/// Decode bytes that must be the canonical encoding of a live-version bundle. +pub fn decode_canonical(bytes: &[u8]) -> Result { + let b = generated::SettlementBundleV1::decode(bytes).map_err(|_| BundleError::Noncanonical)?; + // canon() re-validates and re-encodes; equality proves canonical. + if canon(&b)? != bytes { + return Err(BundleError::Noncanonical); + } + Ok(b) +} + +/// `b = H(DSM/settlement-bundle ‖ Canon(B))` — the immutable bundle identity. +pub fn bundle_digest(canon_bytes: &[u8]) -> [u8; 32] { + immutable_inner(TAG_DSM_SETTLEMENT_BUNDLE, canon_bytes) +} + +/// `addr(B) = H(DSM/storage-object ‖ DSM/settlement-bundle ‖ b)` — the content +/// address the bundle is stored and retrieved under. +pub fn bundle_addr(canon_bytes: &[u8]) -> [u8; 32] { + immutable_addr(TAG_DSM_SETTLEMENT_BUNDLE, canon_bytes) +} + +/// `k_v = H(DSM/binding-keyset ‖ c_n)` — one settlement resource key from a +/// vault's committed parent state (Def 6.17). The vault id is not restated. +pub fn resource_key(c_n: &[u8; 32]) -> [u8; 32] { + let mut h = dsm_domain_hasher(TAG_DSM_BINDING_KEYSET); + h.update(c_n); + *h.finalize().as_bytes() +} + +/// `K(B)` — the sorted distinct resource keys the bundle consumes, derived +/// ONLY from the canonical bundle's committed parent states. Strictly ascending +/// (a duplicate `c_n` is refused), so it is a valid QuorumBind key set. +pub fn key_set(b: &generated::SettlementBundleV1) -> Result, BundleError> { + validate(b)?; + let mut keys: Vec<[u8; 32]> = Vec::with_capacity(b.vault_transitions.len()); + for t in &b.vault_transitions { + let c_n = need32("parent_state_commitment", &t.parent_state_commitment)?; + keys.push(resource_key(&c_n)); + } + keys.sort_unstable(); + for w in keys.windows(2) { + if w[0] == w[1] { + return Err(BundleError::DuplicateResourceKey); + } + } + Ok(keys) +} + +#[cfg(test)] +#[allow(clippy::disallowed_methods)] // test asserts; a failure here is the signal +mod tests { + use super::*; + + fn transition(vault: u8, c_n: u8) -> generated::VaultTransitionV1 { + generated::VaultTransitionV1 { + vault_id: vec![vault; 32], + parent_generation: 3, + parent_state_commitment: vec![c_n; 32], + parent_reserves_digest: vec![0x0A; 32], + successor_ccb: vec![0x5C; 32], + reserve_deltas: b"deltas".to_vec(), + witnesses: vec![b"w1".to_vec()], + } + } + + fn bundle(transitions: Vec) -> generated::SettlementBundleV1 { + generated::SettlementBundleV1 { + version: SETTLEMENT_BUNDLE_VERSION_V1, + storage_set_id: vec![0x6B; 32], + q: 2, + intent_commitment: vec![0x1D; 32], + route_set_commitment: vec![0x0C; 32], + selected_route: b"route".to_vec(), + trader_parent: vec![0xAA; 32], + trader_successor: vec![0xBB; 32], + vault_transitions: transitions, + proof_material: vec![b"P1".to_vec()], + bundle_signatures: vec![b"sig".to_vec()], + recovery_material: b"recovery".to_vec(), + } + } + + #[test] + fn canon_round_trips_and_the_address_is_the_storage_object_of_the_digest() { + let b = bundle(vec![transition(1, 0x11), transition(2, 0x22)]); + let c = canon(&b).unwrap(); + assert_eq!(decode_canonical(&c).unwrap(), b); + // b and addr(B) match the spec construction directly. + assert_eq!( + bundle_digest(&c), + immutable_inner(TAG_DSM_SETTLEMENT_BUNDLE, &c) + ); + assert_eq!( + bundle_addr(&c), + immutable_addr(TAG_DSM_SETTLEMENT_BUNDLE, &c) + ); + // Different bytes => different identity. + let b2 = bundle(vec![transition(1, 0x11), transition(2, 0x23)]); + assert_ne!(bundle_digest(&canon(&b2).unwrap()), bundle_digest(&c)); + } + + #[test] + fn a_bad_version_or_width_or_empty_bundle_is_refused() { + let mut bad = bundle(vec![transition(1, 0x11)]); + bad.version = 2; + assert_eq!(canon(&bad), Err(BundleError::Version(2))); + let mut wide = bundle(vec![transition(1, 0x11)]); + wide.trader_parent = vec![0xAA; 31]; + assert!(matches!(canon(&wide), Err(BundleError::Width { .. }))); + let empty = bundle(vec![]); + assert_eq!(canon(&empty), Err(BundleError::NoTransitions)); + } + + #[test] + fn transitions_must_be_strictly_ascending_by_vault_id() { + let unsorted = bundle(vec![transition(2, 0x22), transition(1, 0x11)]); + assert!(matches!( + canon(&unsorted), + Err(BundleError::TransitionsNotSorted { .. }) + )); + let dup_vault = bundle(vec![transition(1, 0x11), transition(1, 0x22)]); + assert!(matches!( + canon(&dup_vault), + Err(BundleError::TransitionsNotSorted { .. }) + )); + } + + #[test] + fn key_set_is_sorted_distinct_resource_keys_over_c_n() { + let b = bundle(vec![transition(1, 0x11), transition(2, 0x22)]); + let ks = key_set(&b).unwrap(); + assert_eq!(ks.len(), 2); + // Strictly ascending (a valid QuorumBind key set). + assert!(ks[0] < ks[1]); + // Derived from c_n only — the same c_n gives the same key regardless of + // vault_id, and two vaults sharing c_n are refused. + assert!(ks.contains(&resource_key(&[0x11; 32]))); + let shared_cn = bundle(vec![transition(1, 0x11), transition(2, 0x11)]); + assert_eq!(key_set(&shared_cn), Err(BundleError::DuplicateResourceKey)); + } +} diff --git a/dsm_client/frontend/src/proto/dsm_app_pb.ts b/dsm_client/frontend/src/proto/dsm_app_pb.ts index 2630e823..1d512242 100644 --- a/dsm_client/frontend/src/proto/dsm_app_pb.ts +++ b/dsm_client/frontend/src/proto/dsm_app_pb.ts @@ -10090,6 +10090,602 @@ export class SettlementSlotClaimV2 extends Message { } } +/** + * ── Generic conditional-binding interface (SoFi Rev 15 §15.5, Def 6.20) ── + * + * The node-side binding interface is APPLICATION-BLIND. A member holds at most + * one opaque generic binding record per resource key and applies a + * replacement to a whole strictly-sorted key set atomically or not at all + * (Req 15.6). It inspects ONLY the generic storage fields — schema, round + * ordering, the exact expected digest of the prior record set, and key-set + * equality (Req 15.7). It never decodes the value at `value_addr` and attaches + * no settlement, vault, claimant, or economic meaning to any field (§22 #12). + * Class K — never the node — decides what a record means (§15.6). + * + * @generated from message dsm.GenericBindingRecordV1 + */ +export class GenericBindingRecordV1 extends Message { + /** + * @generated from field: uint32 schema = 1; + */ + schema = 0; + + /** + * round = (counter, proposer_id), ordered lexicographically. `counter` is a + * proposer-local persisted monotonic integer — never a timestamp. A + * recovering proposer chooses a round strictly greater than every round it + * must supersede (Def 6.20). + * + * @generated from field: uint64 round_counter = 2; + */ + roundCounter = protoInt64.zero; + + /** + * @generated from field: bytes proposer_id = 3; + */ + proposerId = new Uint8Array(0); + + /** + * @generated from field: bytes tx_id = 4; + */ + txId = new Uint8Array(0); + + /** + * H(DSM/binding-record-set-keys || sorted keys). The node requires it to + * equal the digest of the request's own key set (key-set equality). + * + * @generated from field: bytes keyset_digest = 5; + */ + keysetDigest = new Uint8Array(0); + + /** + * @generated from field: bytes value_digest = 6; + */ + valueDigest = new Uint8Array(0); + + /** + * @generated from field: bytes value_addr = 7; + */ + valueAddr = new Uint8Array(0); + + /** + * Opaque to the node. Class K assigns meaning (prepared / accepted / …). + * + * @generated from field: uint32 status = 8; + */ + status = 0; + + constructor(data?: PartialMessage) { + super(); + proto3.util.initPartial(data, this); + } + + static readonly runtime: typeof proto3 = proto3; + static readonly typeName = "dsm.GenericBindingRecordV1"; + static readonly fields: FieldList = proto3.util.newFieldList(() => [ + { no: 1, name: "schema", kind: "scalar", T: 13 /* ScalarType.UINT32 */ }, + { no: 2, name: "round_counter", kind: "scalar", T: 4 /* ScalarType.UINT64 */ }, + { no: 3, name: "proposer_id", kind: "scalar", T: 12 /* ScalarType.BYTES */ }, + { no: 4, name: "tx_id", kind: "scalar", T: 12 /* ScalarType.BYTES */ }, + { no: 5, name: "keyset_digest", kind: "scalar", T: 12 /* ScalarType.BYTES */ }, + { no: 6, name: "value_digest", kind: "scalar", T: 12 /* ScalarType.BYTES */ }, + { no: 7, name: "value_addr", kind: "scalar", T: 12 /* ScalarType.BYTES */ }, + { no: 8, name: "status", kind: "scalar", T: 13 /* ScalarType.UINT32 */ }, + ]); + + static fromBinary(bytes: Uint8Array, options?: Partial): GenericBindingRecordV1 { + return new GenericBindingRecordV1().fromBinary(bytes, options); + } + + static fromJson(jsonValue: JsonValue, options?: Partial): GenericBindingRecordV1 { + return new GenericBindingRecordV1().fromJson(jsonValue, options); + } + + static fromJsonString(jsonString: string, options?: Partial): GenericBindingRecordV1 { + return new GenericBindingRecordV1().fromJsonString(jsonString, options); + } + + static equals(a: GenericBindingRecordV1 | PlainMessage | undefined, b: GenericBindingRecordV1 | PlainMessage | undefined): boolean { + return proto3.util.equals(GenericBindingRecordV1, a, b); + } +} + +/** + * @generated from message dsm.CompareExchangeManyRequestV1 + */ +export class CompareExchangeManyRequestV1 extends Message { + /** + * Strictly sorted, distinct, opaque 32-byte resource keys. + * + * @generated from field: repeated bytes keys = 1; + */ + keys: Uint8Array[] = []; + + /** + * Digest of the EXACT prior generic record set over `keys` (absent cells + * included as absent). The empty set has a defined digest so a first writer + * can exchange from nothing. + * + * @generated from field: bytes expected_digest = 2; + */ + expectedDigest = new Uint8Array(0); + + /** + * @generated from field: dsm.GenericBindingRecordV1 replacement = 3; + */ + replacement?: GenericBindingRecordV1; + + constructor(data?: PartialMessage) { + super(); + proto3.util.initPartial(data, this); + } + + static readonly runtime: typeof proto3 = proto3; + static readonly typeName = "dsm.CompareExchangeManyRequestV1"; + static readonly fields: FieldList = proto3.util.newFieldList(() => [ + { no: 1, name: "keys", kind: "scalar", T: 12 /* ScalarType.BYTES */, repeated: true }, + { no: 2, name: "expected_digest", kind: "scalar", T: 12 /* ScalarType.BYTES */ }, + { no: 3, name: "replacement", kind: "message", T: GenericBindingRecordV1 }, + ]); + + static fromBinary(bytes: Uint8Array, options?: Partial): CompareExchangeManyRequestV1 { + return new CompareExchangeManyRequestV1().fromBinary(bytes, options); + } + + static fromJson(jsonValue: JsonValue, options?: Partial): CompareExchangeManyRequestV1 { + return new CompareExchangeManyRequestV1().fromJson(jsonValue, options); + } + + static fromJsonString(jsonString: string, options?: Partial): CompareExchangeManyRequestV1 { + return new CompareExchangeManyRequestV1().fromJsonString(jsonString, options); + } + + static equals(a: CompareExchangeManyRequestV1 | PlainMessage | undefined, b: CompareExchangeManyRequestV1 | PlainMessage | undefined): boolean { + return proto3.util.equals(CompareExchangeManyRequestV1, a, b); + } +} + +/** + * @generated from message dsm.CompareExchangeManyResponseV1 + */ +export class CompareExchangeManyResponseV1 extends Message { + /** + * @generated from field: dsm.CompareExchangeManyResponseV1.Outcome outcome = 1; + */ + outcome = CompareExchangeManyResponseV1_Outcome.APPLIED; + + /** + * Digest of the record set over `keys` AFTER this call — what a caller + * must present as `expected_digest` next time. + * + * @generated from field: bytes resulting_digest = 2; + */ + resultingDigest = new Uint8Array(0); + + /** + * @generated from field: bytes member_id = 3; + */ + memberId = new Uint8Array(0); + + /** + * The register incarnation this member is serving. A write acknowledgement + * counts toward a quorum only when BOTH member_id and register_incarnation + * equal what the caller committed — the same rule the read side applies. + * + * @generated from field: bytes register_incarnation = 4; + */ + registerIncarnation = new Uint8Array(0); + + constructor(data?: PartialMessage) { + super(); + proto3.util.initPartial(data, this); + } + + static readonly runtime: typeof proto3 = proto3; + static readonly typeName = "dsm.CompareExchangeManyResponseV1"; + static readonly fields: FieldList = proto3.util.newFieldList(() => [ + { no: 1, name: "outcome", kind: "enum", T: proto3.getEnumType(CompareExchangeManyResponseV1_Outcome) }, + { no: 2, name: "resulting_digest", kind: "scalar", T: 12 /* ScalarType.BYTES */ }, + { no: 3, name: "member_id", kind: "scalar", T: 12 /* ScalarType.BYTES */ }, + { no: 4, name: "register_incarnation", kind: "scalar", T: 12 /* ScalarType.BYTES */ }, + ]); + + static fromBinary(bytes: Uint8Array, options?: Partial): CompareExchangeManyResponseV1 { + return new CompareExchangeManyResponseV1().fromBinary(bytes, options); + } + + static fromJson(jsonValue: JsonValue, options?: Partial): CompareExchangeManyResponseV1 { + return new CompareExchangeManyResponseV1().fromJson(jsonValue, options); + } + + static fromJsonString(jsonString: string, options?: Partial): CompareExchangeManyResponseV1 { + return new CompareExchangeManyResponseV1().fromJsonString(jsonString, options); + } + + static equals(a: CompareExchangeManyResponseV1 | PlainMessage | undefined, b: CompareExchangeManyResponseV1 | PlainMessage | undefined): boolean { + return proto3.util.equals(CompareExchangeManyResponseV1, a, b); + } +} + +/** + * @generated from enum dsm.CompareExchangeManyResponseV1.Outcome + */ +export enum CompareExchangeManyResponseV1_Outcome { + /** + * @generated from enum value: APPLIED = 0; + */ + APPLIED = 0, + + /** + * @generated from enum value: EXPECTATION_MISMATCH = 1; + */ + EXPECTATION_MISMATCH = 1, + + /** + * @generated from enum value: UNAVAILABLE = 2; + */ + UNAVAILABLE = 2, + + /** + * @generated from enum value: INVALID_STORAGE_ENCODING = 3; + */ + INVALID_STORAGE_ENCODING = 3, +} +// Retrieve enum metadata with: proto3.getEnumType(CompareExchangeManyResponseV1_Outcome) +proto3.util.setEnumType(CompareExchangeManyResponseV1_Outcome, "dsm.CompareExchangeManyResponseV1.Outcome", [ + { no: 0, name: "APPLIED" }, + { no: 1, name: "EXPECTATION_MISMATCH" }, + { no: 2, name: "UNAVAILABLE" }, + { no: 3, name: "INVALID_STORAGE_ENCODING" }, +]); + +/** + * @generated from message dsm.ReadBindingRequestV1 + */ +export class ReadBindingRequestV1 extends Message { + /** + * @generated from field: repeated bytes keys = 1; + */ + keys: Uint8Array[] = []; + + constructor(data?: PartialMessage) { + super(); + proto3.util.initPartial(data, this); + } + + static readonly runtime: typeof proto3 = proto3; + static readonly typeName = "dsm.ReadBindingRequestV1"; + static readonly fields: FieldList = proto3.util.newFieldList(() => [ + { no: 1, name: "keys", kind: "scalar", T: 12 /* ScalarType.BYTES */, repeated: true }, + ]); + + static fromBinary(bytes: Uint8Array, options?: Partial): ReadBindingRequestV1 { + return new ReadBindingRequestV1().fromBinary(bytes, options); + } + + static fromJson(jsonValue: JsonValue, options?: Partial): ReadBindingRequestV1 { + return new ReadBindingRequestV1().fromJson(jsonValue, options); + } + + static fromJsonString(jsonString: string, options?: Partial): ReadBindingRequestV1 { + return new ReadBindingRequestV1().fromJsonString(jsonString, options); + } + + static equals(a: ReadBindingRequestV1 | PlainMessage | undefined, b: ReadBindingRequestV1 | PlainMessage | undefined): boolean { + return proto3.util.equals(ReadBindingRequestV1, a, b); + } +} + +/** + * One cell of the answer. `record` is absent when the member holds nothing + * for `key`; absence is an assertion by the member, never an inference. + * + * @generated from message dsm.BindingCellV1 + */ +export class BindingCellV1 extends Message { + /** + * @generated from field: bytes key = 1; + */ + key = new Uint8Array(0); + + /** + * @generated from field: dsm.GenericBindingRecordV1 record = 2; + */ + record?: GenericBindingRecordV1; + + constructor(data?: PartialMessage) { + super(); + proto3.util.initPartial(data, this); + } + + static readonly runtime: typeof proto3 = proto3; + static readonly typeName = "dsm.BindingCellV1"; + static readonly fields: FieldList = proto3.util.newFieldList(() => [ + { no: 1, name: "key", kind: "scalar", T: 12 /* ScalarType.BYTES */ }, + { no: 2, name: "record", kind: "message", T: GenericBindingRecordV1 }, + ]); + + static fromBinary(bytes: Uint8Array, options?: Partial): BindingCellV1 { + return new BindingCellV1().fromBinary(bytes, options); + } + + static fromJson(jsonValue: JsonValue, options?: Partial): BindingCellV1 { + return new BindingCellV1().fromJson(jsonValue, options); + } + + static fromJsonString(jsonString: string, options?: Partial): BindingCellV1 { + return new BindingCellV1().fromJsonString(jsonString, options); + } + + static equals(a: BindingCellV1 | PlainMessage | undefined, b: BindingCellV1 | PlainMessage | undefined): boolean { + return proto3.util.equals(BindingCellV1, a, b); + } +} + +/** + * @generated from message dsm.ReadBindingResponseV1 + */ +export class ReadBindingResponseV1 extends Message { + /** + * @generated from field: repeated dsm.BindingCellV1 cells = 1; + */ + cells: BindingCellV1[] = []; + + /** + * Digest of the exact current record set over the requested keys. + * + * @generated from field: bytes set_digest = 2; + */ + setDigest = new Uint8Array(0); + + /** + * @generated from field: bytes member_id = 3; + */ + memberId = new Uint8Array(0); + + /** + * @generated from field: bytes register_incarnation = 4; + */ + registerIncarnation = new Uint8Array(0); + + constructor(data?: PartialMessage) { + super(); + proto3.util.initPartial(data, this); + } + + static readonly runtime: typeof proto3 = proto3; + static readonly typeName = "dsm.ReadBindingResponseV1"; + static readonly fields: FieldList = proto3.util.newFieldList(() => [ + { no: 1, name: "cells", kind: "message", T: BindingCellV1, repeated: true }, + { no: 2, name: "set_digest", kind: "scalar", T: 12 /* ScalarType.BYTES */ }, + { no: 3, name: "member_id", kind: "scalar", T: 12 /* ScalarType.BYTES */ }, + { no: 4, name: "register_incarnation", kind: "scalar", T: 12 /* ScalarType.BYTES */ }, + ]); + + static fromBinary(bytes: Uint8Array, options?: Partial): ReadBindingResponseV1 { + return new ReadBindingResponseV1().fromBinary(bytes, options); + } + + static fromJson(jsonValue: JsonValue, options?: Partial): ReadBindingResponseV1 { + return new ReadBindingResponseV1().fromJson(jsonValue, options); + } + + static fromJsonString(jsonString: string, options?: Partial): ReadBindingResponseV1 { + return new ReadBindingResponseV1().fromJsonString(jsonString, options); + } + + static equals(a: ReadBindingResponseV1 | PlainMessage | undefined, b: ReadBindingResponseV1 | PlainMessage | undefined): boolean { + return proto3.util.equals(ReadBindingResponseV1, a, b); + } +} + +/** + * ── Def 6.14: the complete immutable SettlementBundle a QuorumBind binds ── + * + * Canon(B) is the canonical encoding of SettlementBundleV1 (vault_transitions + * sorted by vault_id). b = H(DSM/settlement-bundle || Canon(B)); the content + * address is immutable_addr(DSM/settlement-bundle, Canon(B)). K(B) is derived + * ONLY from these bytes: one resource key k_v = H(DSM/binding-keyset || c_n) + * per transition (Def 6.17). Req 6.15: B carries everything needed to verify + * the route and recover the DLV decision without a fresh constructor signature. + * + * @generated from message dsm.VaultTransitionV1 + */ +export class VaultTransitionV1 extends Message { + /** + * @generated from field: bytes vault_id = 1; + */ + vaultId = new Uint8Array(0); + + /** + * @generated from field: uint64 parent_generation = 2; + */ + parentGeneration = protoInt64.zero; + + /** + * c_n = H(DSM/vault-state, CCB(V_n)); commits vault_id, reserves, pair, policy. + * + * @generated from field: bytes parent_state_commitment = 3; + */ + parentStateCommitment = new Uint8Array(0); + + /** + * @generated from field: bytes parent_reserves_digest = 4; + */ + parentReservesDigest = new Uint8Array(0); + + /** + * The exact committed DLV successor for this vault. + * + * @generated from field: bytes successor_ccb = 5; + */ + successorCcb = new Uint8Array(0); + + /** + * Canonical reserve-delta encoding for this transition. + * + * @generated from field: bytes reserve_deltas = 6; + */ + reserveDeltas = new Uint8Array(0); + + /** + * History-bound witnesses proving the parent binding. + * + * @generated from field: repeated bytes witnesses = 7; + */ + witnesses: Uint8Array[] = []; + + constructor(data?: PartialMessage) { + super(); + proto3.util.initPartial(data, this); + } + + static readonly runtime: typeof proto3 = proto3; + static readonly typeName = "dsm.VaultTransitionV1"; + static readonly fields: FieldList = proto3.util.newFieldList(() => [ + { no: 1, name: "vault_id", kind: "scalar", T: 12 /* ScalarType.BYTES */ }, + { no: 2, name: "parent_generation", kind: "scalar", T: 4 /* ScalarType.UINT64 */ }, + { no: 3, name: "parent_state_commitment", kind: "scalar", T: 12 /* ScalarType.BYTES */ }, + { no: 4, name: "parent_reserves_digest", kind: "scalar", T: 12 /* ScalarType.BYTES */ }, + { no: 5, name: "successor_ccb", kind: "scalar", T: 12 /* ScalarType.BYTES */ }, + { no: 6, name: "reserve_deltas", kind: "scalar", T: 12 /* ScalarType.BYTES */ }, + { no: 7, name: "witnesses", kind: "scalar", T: 12 /* ScalarType.BYTES */, repeated: true }, + ]); + + static fromBinary(bytes: Uint8Array, options?: Partial): VaultTransitionV1 { + return new VaultTransitionV1().fromBinary(bytes, options); + } + + static fromJson(jsonValue: JsonValue, options?: Partial): VaultTransitionV1 { + return new VaultTransitionV1().fromJson(jsonValue, options); + } + + static fromJsonString(jsonString: string, options?: Partial): VaultTransitionV1 { + return new VaultTransitionV1().fromJsonString(jsonString, options); + } + + static equals(a: VaultTransitionV1 | PlainMessage | undefined, b: VaultTransitionV1 | PlainMessage | undefined): boolean { + return proto3.util.equals(VaultTransitionV1, a, b); + } +} + +/** + * @generated from message dsm.SettlementBundleV1 + */ +export class SettlementBundleV1 extends Message { + /** + * @generated from field: uint32 version = 1; + */ + version = 0; + + /** + * @generated from field: bytes storage_set_id = 2; + */ + storageSetId = new Uint8Array(0); + + /** + * @generated from field: uint32 q = 3; + */ + q = 0; + + /** + * I + * + * @generated from field: bytes intent_commitment = 4; + */ + intentCommitment = new Uint8Array(0); + + /** + * X + * + * @generated from field: bytes route_set_commitment = 5; + */ + routeSetCommitment = new Uint8Array(0); + + /** + * canonical selected route + * + * @generated from field: bytes selected_route = 6; + */ + selectedRoute = new Uint8Array(0); + + /** + * exact sovereign parent + * + * @generated from field: bytes trader_parent = 7; + */ + traderParent = new Uint8Array(0); + + /** + * exact bilateral successor + * + * @generated from field: bytes trader_successor = 8; + */ + traderSuccessor = new Uint8Array(0); + + /** + * {Tv}, sorted by vault_id + * + * @generated from field: repeated dsm.VaultTransitionV1 vault_transitions = 9; + */ + vaultTransitions: VaultTransitionV1[] = []; + + /** + * {Pv} + * + * @generated from field: repeated bytes proof_material = 10; + */ + proofMaterial: Uint8Array[] = []; + + /** + * @generated from field: repeated bytes bundle_signatures = 11; + */ + bundleSignatures: Uint8Array[] = []; + + /** + * @generated from field: bytes recovery_material = 12; + */ + recoveryMaterial = new Uint8Array(0); + + constructor(data?: PartialMessage) { + super(); + proto3.util.initPartial(data, this); + } + + static readonly runtime: typeof proto3 = proto3; + static readonly typeName = "dsm.SettlementBundleV1"; + static readonly fields: FieldList = proto3.util.newFieldList(() => [ + { no: 1, name: "version", kind: "scalar", T: 13 /* ScalarType.UINT32 */ }, + { no: 2, name: "storage_set_id", kind: "scalar", T: 12 /* ScalarType.BYTES */ }, + { no: 3, name: "q", kind: "scalar", T: 13 /* ScalarType.UINT32 */ }, + { no: 4, name: "intent_commitment", kind: "scalar", T: 12 /* ScalarType.BYTES */ }, + { no: 5, name: "route_set_commitment", kind: "scalar", T: 12 /* ScalarType.BYTES */ }, + { no: 6, name: "selected_route", kind: "scalar", T: 12 /* ScalarType.BYTES */ }, + { no: 7, name: "trader_parent", kind: "scalar", T: 12 /* ScalarType.BYTES */ }, + { no: 8, name: "trader_successor", kind: "scalar", T: 12 /* ScalarType.BYTES */ }, + { no: 9, name: "vault_transitions", kind: "message", T: VaultTransitionV1, repeated: true }, + { no: 10, name: "proof_material", kind: "scalar", T: 12 /* ScalarType.BYTES */, repeated: true }, + { no: 11, name: "bundle_signatures", kind: "scalar", T: 12 /* ScalarType.BYTES */, repeated: true }, + { no: 12, name: "recovery_material", kind: "scalar", T: 12 /* ScalarType.BYTES */ }, + ]); + + static fromBinary(bytes: Uint8Array, options?: Partial): SettlementBundleV1 { + return new SettlementBundleV1().fromBinary(bytes, options); + } + + static fromJson(jsonValue: JsonValue, options?: Partial): SettlementBundleV1 { + return new SettlementBundleV1().fromJson(jsonValue, options); + } + + static fromJsonString(jsonString: string, options?: Partial): SettlementBundleV1 { + return new SettlementBundleV1().fromJsonString(jsonString, options); + } + + static equals(a: SettlementBundleV1 | PlainMessage | undefined, b: SettlementBundleV1 | PlainMessage | undefined): boolean { + return proto3.util.equals(SettlementBundleV1, a, b); + } +} + /** * A trader's claim on one cell of the economic root register: the write-once * value at K_root = H("DSM/trader-economic-root-register-key/v1" || 0x00 || G diff --git a/proto/dsm_app.proto b/proto/dsm_app.proto index 26642578..636c23ac 100644 --- a/proto/dsm_app.proto +++ b/proto/dsm_app.proto @@ -1728,6 +1728,43 @@ message ReadBindingResponseV1 { bytes register_incarnation = 4 [(dsm_fixed_len)=32]; } +// ── Def 6.14: the complete immutable SettlementBundle a QuorumBind binds ── +// +// Canon(B) is the canonical encoding of SettlementBundleV1 (vault_transitions +// sorted by vault_id). b = H(DSM/settlement-bundle || Canon(B)); the content +// address is immutable_addr(DSM/settlement-bundle, Canon(B)). K(B) is derived +// ONLY from these bytes: one resource key k_v = H(DSM/binding-keyset || c_n) +// per transition (Def 6.17). Req 6.15: B carries everything needed to verify +// the route and recover the DLV decision without a fresh constructor signature. +message VaultTransitionV1 { + bytes vault_id = 1 [(dsm_fixed_len)=32]; + uint64 parent_generation = 2; + // c_n = H(DSM/vault-state, CCB(V_n)); commits vault_id, reserves, pair, policy. + bytes parent_state_commitment = 3 [(dsm_fixed_len)=32]; + bytes parent_reserves_digest = 4 [(dsm_fixed_len)=32]; + // The exact committed DLV successor for this vault. + bytes successor_ccb = 5 [(dsm_fixed_len)=32]; + // Canonical reserve-delta encoding for this transition. + bytes reserve_deltas = 6; + // History-bound witnesses proving the parent binding. + repeated bytes witnesses = 7; +} + +message SettlementBundleV1 { + uint32 version = 1; + bytes storage_set_id = 2 [(dsm_fixed_len)=32]; + uint32 q = 3; + bytes intent_commitment = 4 [(dsm_fixed_len)=32]; // I + bytes route_set_commitment = 5 [(dsm_fixed_len)=32]; // X + bytes selected_route = 6; // canonical selected route + bytes trader_parent = 7 [(dsm_fixed_len)=32]; // exact sovereign parent + bytes trader_successor = 8 [(dsm_fixed_len)=32]; // exact bilateral successor + repeated VaultTransitionV1 vault_transitions = 9; // {Tv}, sorted by vault_id + repeated bytes proof_material = 10; // {Pv} + repeated bytes bundle_signatures = 11; + bytes recovery_material = 12; +} + // A trader's claim on one cell of the economic root register: the write-once // value at K_root = H("DSM/trader-economic-root-register-key/v1" || 0x00 || G // || DevID || u64_be(economic_position)).