Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -309,6 +309,60 @@ pub fn parse_issuance_policy(policy_proto: &[u8]) -> Result<IssuancePolicy, Stri
})
}

/// Why a committed policy refuses its asset as a DLV market leg.
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum MarketLegRefusal {
/// The committed policy restricts the asset to mint/burn. A market
/// successor moves control of units between the owner and an arbitrary
/// counterparty — exactly the movement `transferable: false` forbids —
/// so admitting the asset as an AMM leg would bypass the committed
/// restriction through the market door.
NotTransferable,
}

impl core::fmt::Display for MarketLegRefusal {
fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
match self {
Self::NotTransferable => write!(
f,
"the committed policy marks the asset non-transferable (mint/burn only), so it \
cannot be a market leg — a DLV successor moves control between parties"
),
}
}
}

impl std::error::Error for MarketLegRefusal {}

/// Decide whether the committed policy permits its asset as a DLV MARKET LEG
/// (fund, settle, owner-apply, close) — the "applicable token policy"
/// conjunct SoFi Def 4.1 / Req 4.4 / Req 4.6 place on every market successor
/// and every release.
///
/// The matrix is deliberately small, and what it does NOT consult is a
/// ruling, not an omission (owner, 2026-08-30 — the CoinGecko model): token
/// anchors are PUBLIC identifiers, discovery is external, adoption is open —
/// anyone holding the anchor may root to the token. Consequently:
///
/// - `allowlist_device_ids` is ISSUANCE-RECIPIENT-scoped (who may be minted
/// to, enforced by 0x0029) and has no market meaning — an
/// allowlisted-issuance asset trades freely once issued.
/// - `mint_burn_enabled`, `TokenAuthority` and the supply fields govern
/// issuance and are likewise none of the market's business.
/// - `transferable` is the ONE movement-relevant commitment the policy
/// carries, and it binds here.
///
/// Rooting itself is the caller's precondition: this function takes a PARSED
/// policy, so reaching it already required the canonical bytes that re-hash
/// to the leg's commit. ERA and dBTC are pre-rooted on every device by
/// construction and never reach this matrix.
pub fn check_market_leg_permitted(policy: &IssuancePolicy) -> Result<(), MarketLegRefusal> {
if !policy.transferable {
return Err(MarketLegRefusal::NotTransferable);
}
Ok(())
}

/// Decide whether the committed policy permits THIS issuance.
///
/// This is the V1 support matrix, and it is the ONLY one.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -579,6 +579,15 @@ pub fn advance_validated(
substrate_b_pair: accepted.dsm_successor_pair(),
verified_operation: accepted.dsm_verified_operation(),
};
// THE MARKET-LEG TOKEN-POLICY CONJUNCT: a DLV successor's legs must
// satisfy the applicable token policy (SoFi Def 4.1 / Req 4.4 / Req 4.6).
// Central, on the VERIFIED operation, so fund and close are bound even
// though their SameTransitionMove credits carry no evidence channel.
// Non-DLV operations pass vacuously.
if let Some(op) = accepted.dsm_verified_operation() {
crate::economic::provenance::verify_market_leg_policies(op, resolver)
.map_err(EconomicValidationError::Provenance)?;
}
let funded = verify_transition_provenance(witness, resolver, &ctx)
.map_err(EconomicValidationError::Provenance)?;

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -84,6 +84,14 @@ pub trait PeerEvidenceFetcher {
namespace: TaggedHashDomain<'static>,
addr: &[u8; 32],
) -> Result<Vec<u8>, PeerLineageFailure>;
/// The canonical `TokenPolicyV3` bytes rooted under `policy_commit` —
/// the walker's own anchoring (local store or the authoritative
/// content-addressed path). The verifier re-hashes against the commit;
/// unavailable is `Incomplete`, never `Invalid`.
fn anchored_policy_bytes(
&self,
policy_commit: &[u8; 32],
) -> Result<Vec<u8>, PeerLineageFailure>;
}

/// A trusted starting memo: a coordinate THIS verifier validated earlier
Expand Down Expand Up @@ -163,6 +171,13 @@ impl ProvenanceResolver for WalkingResolver<'_> {
) -> Result<Vec<u8>, PeerLineageFailure> {
self.fetcher.immutable(namespace, addr)
}

fn anchored_policy_bytes(
&self,
policy_commit: &[u8; 32],
) -> Result<Vec<u8>, PeerLineageFailure> {
self.fetcher.anchored_policy_bytes(policy_commit)
}
}

/// Validate a peer's lineage up to `target_position` and return that step's
Expand Down
101 changes: 101 additions & 0 deletions dsm_client/deterministic_state_machine/dsm/src/economic/provenance.rs
Original file line number Diff line number Diff line change
Expand Up @@ -206,6 +206,21 @@ pub trait ProvenanceResolver {
namespace: crate::crypto::domain::TaggedHashDomain<'static>,
addr: &[u8; 32],
) -> Result<Vec<u8>, PeerLineageFailure>;

/// The canonical `TokenPolicyV3` bytes rooted under `policy_commit` —
/// the VERIFIER'S OWN anchoring in the token's public anchor, never
/// counterparty-supplied bytes. Anchors are public identifiers (the
/// CoinGecko model): anyone holding the commit may root to the token, and
/// a verifier holding a `V_n`-authenticated commit IS a holder — so the
/// resolver serves its local rooting or fetches from the authoritative
/// content-addressed path. The verifier re-hashes whatever arrives
/// against the commit before trusting a byte; the resolver is a locator,
/// never authority. Unavailable bytes are `Incomplete` — an availability
/// condition, not a permission.
fn anchored_policy_bytes(
&self,
policy_commit: &[u8; 32],
) -> Result<Vec<u8>, PeerLineageFailure>;
}

/// Why a credit is not funded.
Expand All @@ -215,6 +230,12 @@ pub enum ProvenanceError {
/// the policy bytes, the signed body, the V1 support matrix, or the
/// k-of-N threshold over the exact issuance.
AuthorizedIssuanceInvalid(String),
/// A DLV successor's leg fails the applicable token policy — the SoFi
/// Def 4.1 / Req 4.4 / Req 4.6 conjunct on every market movement and
/// every release. The anchored bytes did not re-hash to the committed
/// leg, did not parse, or the parsed policy refuses the asset as a
/// market leg.
MarketLegPolicy(String),
/// The verifier holds no validated transition for the named peer position.
/// NOT a failure of the peer — a failure of *this* verifier to have
/// established the prerequisite, and it fails closed.
Expand Down Expand Up @@ -301,6 +322,9 @@ impl core::fmt::Display for ProvenanceError {
Self::AuthorizedIssuanceInvalid(m) => {
write!(f, "authorized-issuance credit is invalid: {m}")
}
Self::MarketLegPolicy(m) => {
write!(f, "market leg token policy: {m}")
}
Self::PeerTransitionNotValidated {
peer_economic_position,
failure,
Expand Down Expand Up @@ -1407,6 +1431,83 @@ fn requires_consumed_source_record(source: &CreditSource) -> bool {
)
}

/// THE MARKET-LEG TOKEN-POLICY CONJUNCT (SoFi Def 4.1, Req 4.4, Req 4.6):
/// every DLV successor's legs must satisfy the applicable token policy, and
/// this is where a foreign verifier reruns that decision — centrally, on the
/// VERIFIED operation, so fund and close are covered even though their
/// credits are `SameTransitionMove` and carry no evidence channel.
///
/// Per leg: a builtin commit (ERA, dBTC) is pre-rooted on every device by
/// construction and passes; any other commit requires the verifier's OWN
/// anchoring — `resolver.anchored_policy_bytes` — whose bytes must re-hash
/// under `TAG_DSM_POLICY` to the committed leg (the resolver locates, never
/// authorizes), parse as a v3 policy, and pass
/// [`crate::economic::issuance::check_market_leg_permitted`].
///
/// Non-DLV operations have no market legs and pass vacuously; their policy
/// conjuncts live elsewhere (0x0023 for issuance, the transfer path's
/// enforcement for sends).
/// The market-leg policy commits a DLV value operation moves — empty for
/// every non-DLV operation. ONE extraction, shared by the core conjunct, the
/// SDK advance funnel and the route pre-flights, so the four ops cannot
/// drift apart across layers.
pub fn market_leg_commits(operation: &crate::types::operations::Operation) -> Vec<[u8; 32]> {
use crate::types::operations::Operation;
match operation {
Operation::DlvCreateFundedV2 {
leg_a_policy_commit,
leg_b_policy_commit,
..
}
| Operation::DlvClose {
leg_a_policy_commit,
leg_b_policy_commit,
..
} => vec![*leg_a_policy_commit, *leg_b_policy_commit],
Operation::DlvSettle {
input_policy_commit,
output_policy_commit,
..
}
| Operation::DlvOwnerApplyV2 {
input_policy_commit,
output_policy_commit,
..
} => vec![*input_policy_commit, *output_policy_commit],
_ => Vec::new(),
}
}

pub fn verify_market_leg_policies(
operation: &crate::types::operations::Operation,
resolver: &dyn ProvenanceResolver,
) -> Result<(), ProvenanceError> {
for pc in market_leg_commits(operation) {
if crate::core::token::token_state_manager::builtin_token_id_for_policy_commit(&pc)
.is_some()
{
continue;
}
let bytes = resolver
.anchored_policy_bytes(&pc)
.map_err(ProvenanceError::OwnerLineage)?;
if crate::crypto::blake3::domain_hash_bytes(
crate::common::domain_tags::TAG_DSM_POLICY,
&bytes,
) != pc
{
return Err(ProvenanceError::MarketLegPolicy(
"anchored policy bytes do not hash to the committed leg".into(),
));
}
let policy = crate::economic::issuance::parse_issuance_policy(&bytes)
.map_err(|e| ProvenanceError::MarketLegPolicy(format!("leg policy: {e}")))?;
crate::economic::issuance::check_market_leg_permitted(&policy)
.map_err(|e| ProvenanceError::MarketLegPolicy(e.to_string()))?;
}
Ok(())
}

/// Verify provenance for an entire transition.
///
/// Returns the funded credits in source order. Checks, beyond each source
Expand Down
Loading
Loading