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
16 changes: 16 additions & 0 deletions dsm_client/deterministic_state_machine/dsm/src/ccb/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -243,6 +243,22 @@ pub mod class {
/// trader's signature over it, under a key the claim's own `AttA` binds
/// to its `DevID` (DSM Amendment A10, SoFi Amendment S20).
pub const SOFI_SIGNED_RESOLUTION_CLAIM: u16 = 0x0062;
/// `0x0063` — an escrow vault's terms (SoFi Amendment S21): the held
/// token, the external commitment `Y`, and the branches, each an outcome,
/// the exact signer set that decides it, and the identity it pays. The
/// three policy slots of an escrow vault's state name this object.
pub const ESCROW_TERMS: u16 = 0x0063;
/// `0x0064` — a verdict on an external commitment (SoFi Amendment S21):
/// `Y`, the outcome table, the outcome, and its signers' signatures over
/// the statement for the verdict cell. It occupies the cell only when it
/// proves its own authority from these bytes.
pub const ESCROW_VERDICT: u16 = 0x0064;
/// `B°`, the Release branch (SoFi Amendment S21): an escrow vault's whole
/// amount to the recipient of the branch whose outcome the canonical
/// verdict names.
pub const SOFI_SETTLEMENT_RELEASE: u16 = 0x0065;
/// `X_route` preimage branch: a release.
pub const SOFI_ROUTE_DIGEST_RELEASE: u16 = 0x0066;
}

/// Discriminants **allocated but not encodable** — see [`class`] for the ones
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,58 @@
// SPDX-License-Identifier: MIT OR Apache-2.0

//! DSM namespace tags: escrow vaults (SoFi Amendment S21).
//!
//! An escrow vault is a SoFi vault whose terms are a set of precommitted
//! branches, released by the canonical verdict on an external commitment
//! `Y = H(DSM/external/v1 ‖ X)` (Explainer §60). The derivations live in
//! `crate::sofi::escrow`; this file only allocates the domains.

use crate::crypto::domain::TaggedHashDomain;

/// `Y = H(DSM/external/v1 ‖ X)` — an external commitment (Explainer §60).
/// DSM never reads `X`; it verifies only predicates bound to `Y`.
pub const TAG_DSM_EXTERNAL: TaggedHashDomain<'static> = crate::tagged_domain!(b"DSM/external/v1");
/// `A_T = immutable_addr(tag, CCB(EscrowTerms))` — the address an escrow
/// vault's three policy slots name.
pub const TAG_DSM_ESCROW_TERMS_OBJECT: TaggedHashDomain<'static> =
crate::tagged_domain!(b"DSM/escrow/terms-object/v1");
/// `τ = H(tag ‖ u8(|O|) ‖ entries)` — the outcome table: each outcome with the
/// exact signer set that decides it.
pub const TAG_DSM_ESCROW_OUTCOME_TABLE: TaggedHashDomain<'static> =
crate::tagged_domain!(b"DSM/escrow/outcome-table/v1");
/// `K_verdict = H(tag ‖ Y ‖ τ)` — the one cell every vault bound to `Y` under
/// table `τ` settles against.
pub const TAG_DSM_ESCROW_VERDICT_CELL: TaggedHashDomain<'static> =
crate::tagged_domain!(b"DSM/escrow/verdict-cell/v1");
/// `s_verdict = H(tag ‖ K_verdict)` — the seed of the verdict cell's route.
pub const TAG_DSM_ESCROW_VERDICT_SEED: TaggedHashDomain<'static> =
crate::tagged_domain!(b"DSM/escrow/verdict-seed/v1");
/// `m(o) = H(tag ‖ K_verdict ‖ u32be(|o|) ‖ o)` — what a signer signs to
/// decide outcome `o` at the cell.
pub const TAG_DSM_ESCROW_STATEMENT: TaggedHashDomain<'static> =
crate::tagged_domain!(b"DSM/escrow/statement/v1");
/// `immutable_addr(tag, CCB(EscrowVerdict))` — a gathered verdict, put as an
/// object while its signers' signatures are collected.
pub const TAG_DSM_ESCROW_VERDICT_OBJECT: TaggedHashDomain<'static> =
crate::tagged_domain!(b"DSM/escrow/verdict-object/v1");
/// `H(tag ‖ K_verdict)` — where the genesis of every escrow vault bound to a
/// verdict cell is indexed.
pub const TAG_DSM_ESCROW_CELL_LOCATOR: TaggedHashDomain<'static> =
crate::tagged_domain!(b"DSM/escrow/cell-locator/v1");
/// `H(tag ‖ K_verdict ‖ u32be(|o|) ‖ o)` — where gathered signatures deciding
/// outcome `o` at a cell are indexed.
pub const TAG_DSM_ESCROW_STATEMENT_LOCATOR: TaggedHashDomain<'static> =
crate::tagged_domain!(b"DSM/escrow/statement-locator/v1");

#[cfg(test)]
pub(crate) const ESCROW_TAGS: &[TaggedHashDomain<'static>] = &[
TAG_DSM_EXTERNAL,
TAG_DSM_ESCROW_TERMS_OBJECT,
TAG_DSM_ESCROW_OUTCOME_TABLE,
TAG_DSM_ESCROW_VERDICT_CELL,
TAG_DSM_ESCROW_VERDICT_SEED,
TAG_DSM_ESCROW_STATEMENT,
TAG_DSM_ESCROW_VERDICT_OBJECT,
TAG_DSM_ESCROW_CELL_LOCATOR,
TAG_DSM_ESCROW_STATEMENT_LOCATOR,
];
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ use crate::crypto::domain::TaggedHashDomain;
mod addressing;
mod connect;
mod economic;
mod escrow;
mod protocol;
mod sofi;
mod testing;
Expand All @@ -18,6 +19,7 @@ mod token_ops;
pub use addressing::*;
pub use connect::*;
pub use economic::*;
pub use escrow::*;
pub use protocol::*;
pub use sofi::*;
pub use testing::*;
Expand All @@ -30,6 +32,12 @@ pub(super) fn sofi_tags() -> &'static [TaggedHashDomain<'static>] {
sofi::SOFI_TAGS
}

/// The escrow-vault tags (SoFi Amendment S21), collected the same way.
#[cfg(test)]
pub(super) fn escrow_tags() -> &'static [TaggedHashDomain<'static>] {
escrow::ESCROW_TAGS
}

#[cfg(test)]
#[cfg(test)]
pub(super) const TAGS: &[TaggedHashDomain<'static>] = &[
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,7 @@ pub(super) fn all_tags() -> Vec<TaggedHashDomain<'static>> {
tags.extend_from_slice(genesis_identity::TAGS);
tags.extend_from_slice(misc::TAGS);
tags.extend_from_slice(misc::sofi_tags());
tags.extend_from_slice(misc::escrow_tags());
tags.extend_from_slice(policy_registry::TAGS);
tags.extend_from_slice(recovery::TAGS);
tags.extend_from_slice(vault_dbtc::TAGS);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -176,7 +176,9 @@ mod tests {
// +6 with a Web2 application connected to a wallet (DSM Amendment A11):
// the offer, its digest, the accept, the session id, the request and the
// response. The endpoint's certificate pin is the TLS certificate hash.
const EXPECTED_TAG_COUNT: usize = 359;
// +9 with escrow vaults (SoFi Amendment S21): `DSM/external/v1` and the
// eight `DSM/escrow/*` domains.
const EXPECTED_TAG_COUNT: usize = 368;

/// Scan the crate source for every declared domain-tag constant.
///
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -597,7 +597,12 @@ pub fn handle_envelope_universal(env_bytes: &[u8]) -> Vec<u8> {
| gp::envelope::Payload::SofiPositionResponse(_)
| gp::envelope::Payload::SofiRelayResponse(_)
| gp::envelope::Payload::SofiVaultsResponse(_)
| gp::envelope::Payload::ConnectReply(_),
| gp::envelope::Payload::ConnectReply(_)
| gp::envelope::Payload::EscrowPartyResponse(_)
| gp::envelope::Payload::EscrowCreatedResponse(_)
| gp::envelope::Payload::EscrowSignedResponse(_)
| gp::envelope::Payload::EscrowVerdictResponse(_)
| gp::envelope::Payload::EscrowVaultsResponse(_),
) => gp::envelope::Payload::Error(gp::Error {
code: 409,
message: "Responses should not be sent as requests".to_string(),
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -118,7 +118,7 @@ pub fn classify(operation: &Operation) -> EconomicEffect {
// Creation debits the funding into a vault's reserves, and a
// fulfillment commits the trader's position: both move value under a
// write set fixed by the operation's own preimage.
SofiVaultCreate { .. } | SofiFulfill { .. } => ClosedWriteSet,
SofiVaultCreate { .. } | SofiFulfill { .. } | EscrowVaultCreate { .. } => ClosedWriteSet,
}
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -374,6 +374,57 @@ pub fn peer_root_at(
Ok((point.root, accepted))
}

/// The claim a peer's lineage accepted AT `position` (SoFi Amendment S15,
/// MR-SOFI-0347): what a setup naming that position is checked against. The
/// segment from this receiver's frontier is validated in full as a payer's
/// is, and `position` itself as its last step, or, when its claim is
/// conditional, resolved through `conditional` exactly as an interior
/// position is. A trader's setup made right after a SoFi position names the
/// claim that position's resolution accepted, so a conditional position is a
/// claim to resolve here, never a refusal. Nothing is recorded.
pub fn peer_claim_at(
fetcher: &dyn PeerEvidenceFetcher,
expected_network_id: &[u8],
peer_genesis: &[u8; 32],
peer_devid: &[u8; 32],
position: u64,
frontiers: &dyn PeerFrontiers,
conditional: &dyn ConditionalPositionResolver,
) -> Result<AcceptedClaim, PeerLineageFailure> {
if position == 0 {
return Err(invalid(
"position 0 is the activation root: no claim was accepted there",
));
}
let verifier = Verifier {
fetcher,
expected_network_id,
register: Register::resolve(fetcher, expected_network_id)?,
frontiers,
conditional,
steps_remaining: std::cell::Cell::new(WALK_STEP_BUDGET),
sources_reached: std::cell::RefCell::new(Vec::new()),
};
let frontier = verifier.frontier_below(peer_genesis, peer_devid, position)?;
let point = verifier.chain_through(peer_genesis, peer_devid, &frontier, position - 1)?;
verifier.spend_step()?;
match verifier.final_claim(peer_genesis, peer_devid, position, &point.root)? {
RegisteredEconomicClaim::SingleRoot(claim) => Ok(*verifier
.full_step(
peer_genesis,
peer_devid,
position,
&point,
&claim,
StepRole::Segment,
)?
.accepted_claim()),
RegisteredEconomicClaim::ConditionalSofi(held) => verifier
.resolve_conditional(peer_genesis, peer_devid, position, &point, &held)
.map(|(_, accepted)| accepted),
}
}

/// A failure met inside a step, kept in its own class and located at the
/// position where it was met.
fn at_position(position: u64, failure: PeerLineageFailure) -> PeerLineageFailure {
Expand Down Expand Up @@ -741,6 +792,26 @@ impl Verifier<'_> {
point: &ChainPoint,
held: &SofiResolutionClaim,
) -> Result<ChainPoint, PeerLineageFailure> {
let (root, _) = self.resolve_conditional(genesis, device_id, position, point, held)?;
Ok(ChainPoint {
root,
accepted: Some(ParentClaimRef::Conditional {
fulfillment_id: held.fulfillment_id,
}),
})
}

/// The root a conditional position selected and the claim accepted
/// there, as the resolver derives them, held to this position of this
/// peer.
fn resolve_conditional(
&self,
genesis: &[u8; 32],
device_id: &[u8; 32],
position: u64,
point: &ChainPoint,
held: &SofiResolutionClaim,
) -> Result<(ValidatedEconomicRoot, AcceptedClaim), PeerLineageFailure> {
let parent = point.accepted.as_ref().ok_or_else(|| {
invalid(format!(
"position {position}: a conditional position cannot follow the activation root, \
Expand All @@ -765,12 +836,7 @@ impl Verifier<'_> {
accepted.economic_position()
)));
}
Ok(ChainPoint {
root,
accepted: Some(ParentClaimRef::Conditional {
fulfillment_id: held.fulfillment_id,
}),
})
Ok((root, accepted))
}

/// One step validated in full from its own evidence: the transition that
Expand Down Expand Up @@ -1458,6 +1524,35 @@ mod tests {
assert!(!matches!(err, PeerLineageFailure::Incomplete(_)));
}

/// SoFi Amendment S15, MR-SOFI-0347: a setup made right after a SoFi
/// position names the claim that position's resolution accepted, so the
/// claim at a conditional position is asked of the resolver, never
/// refused as a payer's conditional target is. With no resolution in
/// hand the answer is the resolver's own, in its class.
#[test]
fn the_claim_at_a_conditional_position_is_asked_of_its_resolution() {
let position = 4;
let fetcher = ConditionalCellFetcher {
position,
writes: vec![peer_c_q(conditional_claim(position))],
last: crate::route_chain::ROUTE_LEN - 1,
};
match peer_claim_at(
&fetcher,
NETWORK,
&PEER_G,
&peer_d(),
position,
&Recorded::below(position),
&NoResolution,
) {
Err(PeerLineageFailure::Incomplete(why)) => {
assert!(why.contains("no conditional resolution"), "{why}")
}
other => panic!("the resolver answers for a conditional position, not {other:?}"),
}
}

/// A claim naming other coordinates never holds the root cell (storage
/// spec §9 rule 3): only a claim whose own coordinates derive `K_root(q)`
/// is recognized there, so a foreign claim written first blocks nothing,
Expand Down
Loading
Loading