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
217 changes: 163 additions & 54 deletions dsm_client/deterministic_state_machine/dsm/src/sofi/facts.rs
Original file line number Diff line number Diff line change
Expand Up @@ -37,7 +37,8 @@ use super::derive;
use super::exercise::{AttemptCellRead, RecognizedExercise};
use super::registration::{Registration, RegistrationRead};
use super::resolution::{
AttemptWalk, LegFacts, ParentPosition, ParentStatus, RefutedInHand, RouteFacts, VaultChain,
AttemptWalk, GroundRouteFacts, LegFacts, ParentPosition, ParentStatus, RefutedInHand,
RouteFacts, VaultChain,
};
use super::validation::{route_invalid_in_hand, route_validation, vault_post_states, Evidence, Missing};
use super::wire::{ParentClaimRef, SettlementPreimage};
Expand Down Expand Up @@ -154,6 +155,68 @@ pub struct ExerciseReads<'a> {
pub legs: &'a [LegReads<'a>],
}

/// What is read about one exercise before any of its validation evidence:
/// the position pair, this verifier's own resolution of `p`, and each leg's
/// cell, chain and walk. Every item is one [`ExerciseReads`] holds too.
#[derive(Debug, Clone, Copy)]
pub struct GroundReads<'a> {
pub exercise: &'a RecognizedExercise,
pub registration: &'a RegistrationRead,
pub parent: Option<&'a AdmittedEconomicPosition>,
pub legs: &'a [LegReads<'a>],
}

/// The facts of one exercise that stand without its validation evidence:
/// registration, whether its position went to another claim, the trader
/// parent, and each leg's cell, liveness and consumption elsewhere. A leg's
/// parent is `Canonical` only where the vault's chain names it, and never
/// refuted: refuting one needs the generation the evidence places it at.
/// Enough for the skips that need no validation evidence (SoFi §23.5 and
/// MR-SOFI-0241; Amendment S14); never enough to resolve a position or to
/// consume a key. Built by [`establish_ground`] and by nothing else.
#[derive(Debug, Clone)]
pub struct GroundFacts {
pub(crate) external_commitment: D32,
pub(crate) registered: bool,
pub(crate) position_lost: bool,
pub(crate) parent: ParentPosition,
pub(crate) parent_pre_root: D32,
/// One per leg of `P`, in P's leg order.
pub(crate) legs: Vec<LegFacts>,
/// The key each leg's facts are about: `(vault, parent root, attempt)`.
pub(crate) keys: Vec<(D32, D32, u64)>,
}

impl GroundFacts {
pub fn external_commitment(&self) -> &D32 {
&self.external_commitment
}

pub(crate) fn route_ground(&self) -> GroundRouteFacts<'_> {
GroundRouteFacts {
external_commitment: self.external_commitment,
position_lost: self.position_lost,
parent: self.parent,
parent_pre_root: self.parent_pre_root,
legs: &self.legs,
}
}

/// The facts of the leg at `K^(attempt)` of `vault_id` at `parent_root`,
/// when `P` has one there.
pub(crate) fn leg_at(
&self,
vault_id: &D32,
parent_root: &D32,
attempt: u64,
) -> Option<LegFacts> {
self.keys
.iter()
.position(|k| *k == (*vault_id, *parent_root, attempt))
.and_then(|i| self.legs.get(i).copied())
}
}

/// The complete facts of one exercise, as this verifier established them:
/// what the ladder reads, and the evidence it was read over. Built by
/// [`establish`] and by nothing else; `pub(crate)` fields so that the
Expand Down Expand Up @@ -305,13 +368,94 @@ fn permanently_resolved(cell: &CellFact, e: &D32) -> bool {
///
/// Registration from the position pair (R10): the pair decides for every `F`
/// at `q` at once, and a position held by another claim is this `F`'s
/// position lost (Section 21.1, arm (v)). `FulfillmentConformance` and
/// position lost (Section 21.1; the skip of SoFi Amendment S14). `FulfillmentConformance` and
/// `RouteValidation` recomputed over the evidence (R5, R7). The trader
/// parent from the position this verifier resolved itself. Each leg's cell
/// at its successor key (R11), its parent against the vault's established
/// chain at the generation the evidence places it, and its liveness from the
/// walk over the earlier keys of that leg's chain.
pub fn establish(reads: &ExerciseReads<'_>) -> Result<EstablishedFacts, NotEstablished> {
let exercise = reads.exercise;
let precommit = &exercise.precommit().body;
let fulfillment = &exercise.fulfillment().body;
let ground = establish_ground(&GroundReads {
exercise,
registration: reads.registration,
parent: reads.parent,
legs: reads.legs,
})?;
let e = ground.external_commitment;

// FulfillmentConformance over the evidence, recomputed here (R7).
if reads.conformance.precommit.body != *precommit
|| reads.conformance.preimage != *exercise.preimage()
{
return Err(NotEstablished::NotThisExercise(
"the conformance evidence is of another operation",
));
}
let conformance = match fulfillment_conformance(
fulfillment,
&exercise.fulfillment().signature,
reads.conformance,
) {
Ok(verdict) => verdict.verdict(),
Err(missing) => return Err(NotEstablished::ConformanceEvidence(vec![missing])),
};

// RouteValidation over the evidence, recomputed here (R5).
let validation = match route_validation(precommit, exercise.preimage(), reads.evidence) {
Ok(validation) => validation,
Err(missing) => return Err(NotEstablished::RouteEvidence(vec![missing])),
};

// The GENERATION each leg's parent sits at, recomputed from the pre
// states the evidence holds rather than asserted by the operation that
// names the parent. Without it a parent cannot be refuted, only placed
// positively, so a vault missing here keeps the status the ground facts
// gave it: `Canonical` where its chain names the parent, else
// `Unavailable`, and never `Orphaned`.
let generations: BTreeMap<D32, u64> =
match vault_post_states(precommit, exercise.preimage(), reads.evidence) {
Ok(posts) => posts
.iter()
.map(|post| (*post.vault_id(), post.pre_generation()))
.collect(),
Err(..) => BTreeMap::new(),
};
let mut legs = ground.legs;
for ((leg, read), facts) in precommit.legs().iter().zip(reads.legs).zip(legs.iter_mut()) {
// The chain decides, three-valued, at the generation the evidence
// places the parent.
if let (Some(chain), Some(generation)) = (read.chain, generations.get(&leg.vault_id)) {
facts.parent = chain.status_of(*generation, &leg.parent_root);
}
}
let storage_resolved =
ground.registered && legs.iter().all(|l| permanently_resolved(&l.cell, &e));
Ok(EstablishedFacts {
fulfillment_id: derive::fulfillment_id(fulfillment),
external_commitment: e,
registered: ground.registered,
conformance,
position_lost: ground.position_lost,
parent: ground.parent,
parent_pre_root: ground.parent_pre_root,
validation,
storage_resolved,
legs,
keys: ground.keys,
preimage: exercise.preimage().clone(),
evidence: reads.evidence.clone(),
})
}

/// The facts of one exercise that need none of its validation evidence
/// ([`GroundFacts`]), over the reads a verifier makes before it acquires
/// any: the registration from the position pair, the trader parent from this
/// verifier's own resolution of `p`, and each leg's cell and walk. The same
/// rules [`establish`] applies, which builds its facts on these.
pub fn establish_ground(reads: &GroundReads<'_>) -> Result<GroundFacts, NotEstablished> {
let exercise = reads.exercise;
let precommit = &exercise.precommit().body;
let fulfillment = &exercise.fulfillment().body;
Expand All @@ -328,6 +472,16 @@ pub fn establish(reads: &ExerciseReads<'_>) -> Result<EstablishedFacts, NotEstab
(ours, !ours)
}
Registration::NeverRegistered { .. } => (false, true),
// SoFi Amendment S14: the position went to the claim holding K_root(q)
// unless that claim is this F's own C_q, whose fulfillment may still be
// relayed.
Registration::RootTaken { claim } => (
false,
*claim
!= crate::storage_cell::entry_digest(
&derive::resolution_claim(precommit, fulfillment).encode(),
),
),
Registration::Unresolved => (false, false),
};

Expand All @@ -354,43 +508,6 @@ pub fn establish(reads: &ExerciseReads<'_>) -> Result<EstablishedFacts, NotEstab
},
};

// FulfillmentConformance over the evidence, recomputed here (R7).
if reads.conformance.precommit.body != *precommit
|| reads.conformance.preimage != *exercise.preimage()
{
return Err(NotEstablished::NotThisExercise(
"the conformance evidence is of another operation",
));
}
let conformance = match fulfillment_conformance(
fulfillment,
&exercise.fulfillment().signature,
reads.conformance,
) {
Ok(verdict) => verdict.verdict(),
Err(missing) => return Err(NotEstablished::ConformanceEvidence(vec![missing])),
};

// RouteValidation over the evidence, recomputed here (R5).
let validation = match route_validation(precommit, exercise.preimage(), reads.evidence) {
Ok(validation) => validation,
Err(missing) => return Err(NotEstablished::RouteEvidence(vec![missing])),
};

// The GENERATION each leg's parent sits at, recomputed from the pre
// states the evidence holds rather than asserted by the operation that
// names the parent. Without it a parent cannot be refuted, only placed
// positively, so a vault missing here is `Unavailable` and never
// `Orphaned`.
let generations: BTreeMap<D32, u64> =
match vault_post_states(precommit, exercise.preimage(), reads.evidence) {
Ok(posts) => posts
.iter()
.map(|post| (*post.vault_id(), post.pre_generation()))
.collect(),
Err(..) => BTreeMap::new(),
};

// Every leg of P at the attempt F fixed for it. The attempts cover the
// legs exactly: that is conformance item 4, decided in hand.
if reads.legs.len() != precommit.legs().len() {
Expand Down Expand Up @@ -418,13 +535,12 @@ pub fn establish(reads: &ExerciseReads<'_>) -> Result<EstablishedFacts, NotEstab
));
}
let cell = read.cell.fact();
// The chain decides, three-valued, at the generation the evidence
// places the parent; without a generation it can still establish
// the root positively, but it cannot refute one it has not placed.
let parent = match (read.chain, generations.get(&leg.vault_id)) {
(Some(chain), Some(generation)) => chain.status_of(*generation, &leg.parent_root),
(Some(chain), None) if chain.names(&leg.parent_root) => ParentStatus::Canonical,
(Some(..), None) | (None, _) => ParentStatus::Unavailable,
// Without the generation the evidence places the parent at, the chain
// can establish the root positively but cannot refute one it has not
// placed.
let parent = match read.chain {
Some(chain) if chain.names(&leg.parent_root) => ParentStatus::Canonical,
Some(..) | None => ParentStatus::Unavailable,
};
// `AttemptLive`: every earlier key of this leg's chain is skipped,
// established by a walk from the first key.
Expand Down Expand Up @@ -453,21 +569,14 @@ pub fn establish(reads: &ExerciseReads<'_>) -> Result<EstablishedFacts, NotEstab
});
keys.push((leg.vault_id, leg.parent_root, attempt));
}
let storage_resolved = registered && legs.iter().all(|l| permanently_resolved(&l.cell, &e));
Ok(EstablishedFacts {
fulfillment_id: derive::fulfillment_id(fulfillment),
Ok(GroundFacts {
external_commitment: e,
registered,
conformance,
position_lost,
parent,
parent_pre_root: *precommit.void_root(),
validation,
storage_resolved,
legs,
keys,
preimage: exercise.preimage().clone(),
evidence: reads.evidence.clone(),
})
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -217,6 +217,13 @@ pub enum Registration {
fulfillment: Signed<TraderFulfillmentBody>,
settled_at: PositionCell,
},
/// No fulfillment is final at `K_ful(q)`, and `K_root(q)` is held by the
/// claim whose entry digest is `claim`: final, or holding the leader link,
/// so no other value will ever be final there (§9 finality 2). An `F` at
/// `q` whose own `C_q` is another claim can never register (SoFi
/// Amendment S14); one whose `C_q` it is waits for its fulfillment to be
/// relayed.
RootTaken { claim: D32 },
/// Not registered yet: a cell is open, or the value holding it is not
/// final yet.
Unresolved,
Expand Down Expand Up @@ -325,7 +332,15 @@ fn registration_of(
state: ChainState::LeaderHeld | ChainState::Preserved,
..
}
| CellReading::Open => return Ok(Registration::Unresolved),
| CellReading::Open => {
// No fulfillment is final at K_ful(q) yet. The position may still
// have gone to a claim at K_root(q), and whatever holds it is the
// claim every F at q is measured against (SoFi Amendment S14).
return Ok(match read_root_cell(&cells.root, root_evidence)? {
CellReading::Held { id, .. } => Registration::RootTaken { claim: id },
CellReading::Open => Registration::Unresolved,
});
}
};
// The root cell's reading identifies its claim by the entry digest of
// its exact bytes, so `C_q` holds the cell exactly when the ids agree.
Expand Down Expand Up @@ -592,16 +607,38 @@ mod tests {
reg(&final_ful, &root_cell(b"not a claim", ROUTE_LEN - 1)),
Ok(Registration::Unresolved)
);
// No fulfillment final at K_ful(q): the root cell's claim is named,
// and weighed by each F against its own C_q (SoFi Amendment S14).
let claim_id = crate::storage_cell::entry_digest(&claim);
let other_id = crate::storage_cell::entry_digest(&other_claim);
assert_eq!(
reg(&ful_cell(&bytes, 1), &root_cell(&claim, ROUTE_LEN - 1)),
Ok(Registration::Unresolved)
Ok(Registration::RootTaken { claim: claim_id })
);
assert_eq!(
reg(
&ful_cell(b"garbage", ROUTE_LEN - 1),
&root_cell(&claim, ROUTE_LEN - 1)
),
Ok(Registration::Unresolved)
Ok(Registration::RootTaken { claim: claim_id })
);
for last in [0, ROUTE_LEN - 1] {
assert_eq!(
reg(
&ful_cell(b"garbage", ROUTE_LEN - 1),
&root_cell(&other_claim, last)
),
Ok(Registration::RootTaken { claim: other_id }),
"no fulfillment final, another claim at K_root(q) through position {last}"
);
}
assert_eq!(
reg(
&ful_cell(b"garbage", ROUTE_LEN - 1),
&root_cell(b"not a claim", ROUTE_LEN - 1)
),
Ok(Registration::Unresolved),
"no fulfillment and no claim: nothing is decided"
);
let mut unread_root = root_cell(&claim, ROUTE_LEN - 1);
unread_root.seats[0].values = None;
Expand Down
Loading
Loading