Protocol Specification v0.6

State Management

State safety depends on the authoritative cut consumed by an operation, not on a caller's last cached observation. The cut binds the authority epoch (opens in a new tab) and product revision. A new effect must use the current cut (opens in a new tab); an exact retained retry follows the result path without executing again.

Reservation lifecycle

After the debit, the amount stays pending until exact terminal evidence credits or refunds it, once.

After the debit, the amount stays pending until exact terminal evidence credits or refunds it, once. Reserve locks the whole footprint into Held, Dispatch moves the reservation to Dispatched and Debit takes the amount from the source into Pending; before the debit, Cancel or Fence closes the reservation. In Pending, Timeout and Preempt keep the phase, and RecordTerminal stores evidence for the same binding. Credit with confirmed evidence pays the destination and keeps the lock until Reconcile releases it, and Refund with reversed evidence returns the amount to the source. Each settlement happens once. Reserve 1 Held Dispatch Dispatched TerminalEvidence 4 kind Confirmed or Reversed for this exact binding RecordTerminal Cancel Fence Closed 2 Debit source − amount 3 Pending Timeout Preempt 5 Credit [confirmed] Credited 6 Refund [reversed] Refunded Reconcile [confirmed] Reconciled
  1. 1

    Whole footprint at once

    Reserve locks the asset and every dependency together. If any member is already locked, it returns Busy and takes none.

  2. 2

    Exits before the debit

    Cancel from Held and Fence from Dispatched close the reservation and release the footprint. Nothing has been debited yet.

  3. 3

    Debit

    The source balance drops by the amount, which becomes one pending obligation. From here, Cancel, Fence and a second Debit are rejected.

  4. 4

    Exact evidence settles once

    RecordTerminal accepts evidence only for the same binding, and later records keep its kind. Credit pays the destination on Confirmed; Refund returns the amount to the source on Reversed.

  5. 5

    Time does not settle

    Timeout only advances the clock. Preempt gives an expired lease a new holder and generation, and the amount stays pending.

  6. 6

    Lock kept until reconciled

    Credit keeps the footprint locked until Reconcile presents the confirmed record. Reconcile and Refund need an unexpired lease.

  • Start of a reservation
  • Phase that holds the pending amount
  • Moves the amount
  • Terminal evidence and the guards that need it
  • Keeps the phase
  • Terminal: the footprint is released

Schematic: one reservation of the reference ledger. Timeout (any phase) and Preempt (an expired lease, before release) are drawn only at Pending, and other commands that keep the phase, such as Write in Held and PublishPrimary after settlement, are not drawn.

Figure 1: Phases of one reservation from the footprint lock to settlement by exact terminal evidence

Identity and request binding#

NormativeBytes, test vectors and proofs

  • A public submission MUST use the authenticated caller identity supplied by its entry point. It MUST bind the call identifier (opens in a new tab) namespace, active session, and claimed caller to that identity. A request body cannot supply the authenticated identity.
  • The envelope MUST distinguish the message identifier from the original event key (opens in a new tab). The signed payload MUST bind those identifiers to the asset, caller, session, authority epoch, revision, and the operation data. The wire specification defines the canonical bytes.
  • A new effect MUST use the current authority epoch and revision at its commit cut. The commit cut is the state boundary at which the check and the effect take place. An existing durable outcome is collected through the result path and cannot authorize another effect.
  • A lock token MUST bind its asset, lock holder, and generation. The operation consuming the token MUST check the authoritative lock holder and generation before applying an effect.

The reference ledger admits a request only from the authenticated caller in a session bound to that caller. The request must carry the current cut and name the endpoint that owns the effect, and that endpoint's context must grant the command to the caller on this route:

RustRequest admissionsrc/lock_finality.rsL698-L724Highlighted lines are discussed in the textSource (opens the lines on GitHub in a new tab)
pub(crate) fn request_admitted(    &self,    route: Route,    actor: AuthenticatedCaller,    request: Request<'_>,) -> bool {    let endpoint = match request.command {        Command::Credit { .. } => request.binding.destination,        _ => request.binding.origin.domain,    };    let grant = (actor.0, route, request.command.permission());    self.connected        && request.caller == actor.0        && request.cut == self.state.cut        && request.endpoint == endpoint        && self.state.assets.contains_key(&request.binding.asset)        && self            .state            .contexts            .contains_key(&request.binding.destination)        && self            .state            .contexts            .get(&endpoint)            .is_some_and(|context| context.grants.contains(&grant))        && self.session_bound(actor, request.session)}

Crash and recovery#

NormativeBytes, test vectors and proofs

  • The authoritative effect and its recorded result MUST share a durable commit boundary. Durable state includes the original request binding, its outcome, and evidence needed to distinguish an unresolved dispatch from a terminal outcome. A local response cache can be lost independently.
  • Crash invalidates the local response cache. Recovery MUST obtain current authoritative state and preserve durable request bindings and outcomes. A recovered endpoint MUST use that state when collecting a prior result. When the current authoritative state is unavailable or conflicts with the supplied recovery evidence, the endpoint MUST hold or reject the operation.
  • An unresolved dispatch MUST remain available for reconciliation. Cache loss or a local timeout cannot establish a reversal.

The authority runs a new call on a copy of its primary state, stores the outcome beside the effect and commits both at once. A call identifier it already holds never runs again. An exact retry collects the stored outcome, and different content under that identifier is rejected:

RustEffect and outcome in one commitsrc/authority.rsL645-L679Highlighted lines are discussed in the textSource (opens the lines on GitHub in a new tab)
fn submit_bound(    &mut self,    route: Route,    actor: AuthenticatedCaller,    id: CallId,    payload: &CallPayload,) -> Collection {    if !self.up {        return Collection::Waiting;    }    if self.state.primary.regulation.claimed(id) {        return Collection::Rejected;    }    if self.state.primary.calls.contains_key(&id) {        return self.finish(id, payload);    }    let mut primary = self.state.primary.clone();    let before = primary.ledger.current_cut();    let outcome = if self.connected {        primary.execute(route, actor, payload)    } else {        Outcome::plain(OutcomeReply::Unavailable)    };    primary.note_execution(id, payload.endpoint, before);    primary.calls.insert(        id,        StoredCall {            payload: payload.clone(),            outcome,        },    );    self.commit_primary(primary);    self.finish(id, payload)}

These vectors restart the authority between two identical calls, reuse a call identifier for a different message and retry after a denial, an unavailable result and a busy result. The different message is rejected, and no retry adds an effect:

TextRecorded outcomestests/vectors/authoritative-outcomes.tsvL1-L7Source (opens the lines on GitHub in a new tab)
id	operation	inputs	expected	evidencefirst-authority-result	first-dispatch	caller=7;namespace=7;sequence=1;message=1;session=1;epoch=9;origin_epoch=9;source_domain=0;event=17;asset=17	reply=source_dispatched;authority_effects=1	authority_stores_the_bound_source_dispatch_resultexact-retry-after-recovery	restart-retry	caller=7;namespace=7;sequence=1;message=1;session=1;epoch=9;origin_epoch=9;source_domain=0;event=17;asset=17	reply=source_dispatched;authority_effects=1	exact_recovery_preserves_reply_and_execution_history,exact_retry_reads_the_committed_outcome,authoritative_call_retry_does_not_repeat_effectsame-id-different-message	conflicting-call	caller=7;namespace=7;sequence=1;message=1;session=1;epoch=9;origin_epoch=9;source_domain=0;event=17;asset=17;retry_message=2	reply=rejected;authority_effects=1	conflicting_payload_is_observably_rejected,authoritative_call_retry_does_not_repeat_effectdenial-survives-entry-change	denied-retry	caller=7;namespace=7;sequence=1;message=1;session=1;epoch=9;origin_epoch=9;source_domain=0;event=17;asset=17;first_route=administrative;retry_route=normal	reply=rejected;authority_effects=0	a_same_call_retry_at_an_allowed_entry_cannot_replace_the_denial,loss_of_the_stored_denial_allows_a_later_effectunavailable-result-survives-reconnect	unavailable-retry	caller=7;namespace=7;sequence=1;message=1;session=1;epoch=9;origin_epoch=9;source_domain=0;event=17;asset=17	reply=unavailable;authority_effects=0	an_unavailable_unissued_call_records_an_outcome_without_execution,authoritative_call_retry_does_not_repeat_effectbusy-result-survives-retry	busy-retry	caller=7;namespace=7;sequence=1;message=1;session=1;epoch=9;origin_epoch=9;source_domain=0;event=17;asset=17;first_asset=17;conflicting_asset=30;conflicting_event=30	reply=busy;new_reservations=0	first_connected_call_records_its_public_result,opposite_order_pair_cannot_take_a_partial_lock,authoritative_call_keeps_older_results

Interleaving#

NormativeBytes, test vectors and proofs

  • When two actions meet the exchange conditions of compatible service actions (opens in a new tab), scheduling them in either order MUST preserve each request's result and effect and the state observed by subsequent actions. An action's dependencies include the authority epoch, completion history, and any state or evidence it reads. Different assets alone do not establish that the actions can be exchanged.
  • Implementations MUST preserve the order of conflicting actions and the history needed to serve revision-bound reads. A shared authority read can coexist with another read. An authority update can change the result of a concurrent action that reads it.

In the executable interleaving model, rotating the authority changes the result of a write that names the previous epoch:

RustAuthority rotation and a writesrc/interleaving.rsL36-L56Highlighted lines are discussed in the textSource (opens the lines on GitHub in a new tab)
pub fn apply(&mut self, action: Action) -> bool {    match action {        Action::Write {            asset,            expected_epoch,            value,        } => {            if expected_epoch != self.authority_epoch {                return false;            }            self.values.insert(asset, value);        }        Action::RotateAuthority { .. } => {            let Some(epoch) = self.authority_epoch.checked_add(1) else {                return false;            };            self.authority_epoch = epoch;        }    }    true}

The vectors record which pairs may run in either order. Two executions on different assets that share one signed cut run in input order, and the second one is busy:

TextExchangeable and serial pairstests/vectors/parallel-composition.tsvL1-L9Highlighted lines are discussed in the textSource (opens the lines on GitHub in a new tab)
id	operation	inputs	expected	evidenceindependent-actual-dispatches	pair	operation_a=dispatch;operation_b=dispatch;origin_a=17;origin_b=22;commit_a=0;commit_b=1;attempt_a=0;attempt_b=0;request_exchange=true	schedule=exchange;reply_a_attempt=1;reply_b_attempt=1;retained_state_equal=true	different_actual_settlement_slots_exchange,compatible_actual_product_actions_exchange,different_exact_jobs_can_exchange_under_the_same_authoritypublication-derived-enqueue-and-other-dispatch	pair	operation_a=enqueue;operation_b=dispatch;origin_a=17;origin_b=22;commit_a=0;commit_b=1;publication_a=committed;job_a=absent;job_b=waiting;request_exchange=true	schedule=exchange;enqueue_key_a=17;reply_b_attempt=1;retained_state_equal=true	enqueue_reads_publication_even_when_no_job_exists,exact_queue_effect_is_the_existing_endpoint_effect,compatible_actual_product_actions_exchangeactual-credit-read-and-queue-write	pair	operation_a=raw_read;operation_b=dispatch;origin_b=17;epoch=9;domain=2;asset=17;recipient=3;amount=5;request_exchange=true	schedule=exchange;reply_a=5;reply_b_attempt=1;retained_state_equal=true	authorized_raw_observation_of_the_published_credit_is_five,exact_parallel_job_dispatch_is_nonvacuous,compatible_actual_product_actions_exchangesame-slot-enqueue-dispatch-conflict	pair	operation_a=enqueue;operation_b=dispatch;origin_a=17;origin_b=17;commit_a=0;commit_b=0;job_a=absent;request_exchange=true	schedule=serial;reply_b_attempt=1;unsafe_reverse_reply_b=none	same_slot_dispatch_and_enqueue_change_the_reply_when_reordered,conflicting_pair_is_revalidated_in_input_ordersame-slot-labeled-attempts	pair	operation_a=dispatch;operation_b=dispatch;origin_a=17;origin_b=17;commit_a=0;commit_b=0;attempt_a=0;request_exchange=true	schedule=serial;reply_a_attempt=1;reply_b_attempt=2;unsafe_reverse_changes_replies=true	two_dispatches_of_one_job_return_different_attempts_to_the_occurrences,compatible_settlement_writes_use_distinct_exact_slotssame-slot-failure-conflict	pair	operation_a=dispatch;operation_b=fail;origin_a=17;origin_b=17;commit_a=0;commit_b=0;request_exchange=true	schedule=serial	same_slot_actions_cannot_exchange_even_with_unchanged_authority,conflicting_pair_is_revalidated_in_input_orderdistinct-assets-share-the-authority-cut	pair	operation_a=execute;operation_b=execute;asset_a=17;asset_b=22;epoch=9;same_initial_cut=true;request_exchange=true	schedule=serial;second_fresh_result=busy;second_effect=false	compatible_actions_have_no_shared_writer,actual_primary_write_changes_the_reply_to_the_same_signed_cut,prior_call_on_any_route_cannot_repeat_an_authority_effectcurrent-read-after-shared-cut-writes	pair	operation_a=execute;operation_b=current_read;asset=17;old_revision=0;new_revision=3;old_value=0;new_value=9;request_exchange=true	schedule=serial;old_expected_read=busy;new_expected_read=9	actual_primary_write_changes_the_reply_to_the_same_signed_cut,stale_cache_returns_busy_and_direct_current_read_returns_nine,conflicting_pair_is_revalidated_in_input_order

Lock authority#

NormativeBytes, test vectors and proofs

  • Every endpoint that changes a lock or consumes its authority MUST check the same authoritative asset, original event key, lock holder, generation, and authority epoch at the operation's commit cut. Reserve and Preempt carry no lock credential and check no lock holder or generation.
  • Reserve requires that no lock exists for the original event and that no active lock covers its footprint. Preempt requires that the existing lock for the same binding has expired and still covers its footprint.
  • Reassigning a lock MUST invalidate the previous generation. Changing the authority context MUST invalidate tokens issued under the previous context. Endpoints sharing an authority epoch MUST install the replacement contexts together. Normal, administrative, and adapter entry points MUST enforce these conditions before state changes.
  • A lock covers the requested asset and the dependencies designated by its authoritative lock context. Acquisition MUST reserve that entire footprint atomically. A lock-consuming request MUST match the current version of every recorded footprint member. A member that the request's version list omits is compared as version 0. Credit and Preempt do not check versions.
  • A lock's holder and the asset's owner are separate roles. Cancellation, fencing, and preemption MUST consume their own required permission at the current cut. Holding one role does not confer another role's permissions.
  • Before the source debit, Cancel of a held lock or Fence of a dispatched lock MAY end an obligation, since it prevents any later source effect for that binding. After the source debit, Cancel and Fence MUST be rejected, and Preempt MUST preserve the pending financial obligation and its recovery authority.

A lock-consuming command carries a credential. It must come from the current holder and name the current reservation, its generation and the current authority epoch:

RustLock credentialsrc/lock_finality.rsL749-L767Highlighted lines are discussed in the textSource (opens the lines on GitHub in a new tab)
fn bound_credential(    &self,    actor: AuthenticatedCaller,    request: Request<'_>,    signed_generation: Option<u64>,) -> Option<&Reservation> {    let token = request.credential?;    let binding = request.binding;    let current = self.reservation(binding.origin)?;    (token.asset == binding.asset        && token.origin == binding.origin        && current.binding == binding        && token.holder == current.holder        && current.holder == actor.0        && token.generation == current.generation        && signed_generation.is_none_or(|g| g == current.generation)        && token.epoch == self.state.cut.authority_epoch)        .then_some(current)}

These vectors present a credential with a wrong holder, a wrong generation, the event of another reservation or an old epoch, and one issued before the authority context was replaced. Each dispatch is rejected and changes nothing:

TextCredential vectorstests/vectors/lock-finality.tsvL1-L9Highlighted lines are discussed in the textSource (opens the lines on GitHub in a new tab)
id	operation	inputs	expected	evidencenormal-current-dispatch	dispatch	route=normal;caller=7;asset=17;source_domain=0;event=17;epoch=9;holder=7;generation=0;token_epoch=9;token_event=17	reply=source_dispatched	normal_admin_and_adapter_dispatch_with_current_credentialadministrative-current-dispatch	dispatch	route=administrative;caller=7;asset=17;source_domain=0;event=17;epoch=9;holder=7;generation=0;token_epoch=9;token_event=17	reply=source_dispatched	normal_admin_and_adapter_dispatch_with_current_credentialadapter-current-dispatch	dispatch	route=adapter;caller=7;asset=17;source_domain=0;event=17;epoch=9;holder=7;generation=0;token_epoch=9;token_event=17	reply=source_dispatched	normal_admin_and_adapter_dispatch_with_current_credentialwrong-holder	dispatch	route=normal;caller=7;asset=17;source_domain=0;event=17;epoch=9;holder=3;generation=0;token_epoch=9;token_event=17	reply=rejected;unchanged=true	credential_holder_and_generation_are_checked,removing_only_holder_or_generation_check_would_admit_dispatchwrong-generation	dispatch	route=administrative;caller=7;asset=17;source_domain=0;event=17;epoch=9;holder=7;generation=1;token_epoch=9;token_event=17	reply=rejected;unchanged=true	credential_holder_and_generation_are_checked,removing_only_holder_or_generation_check_would_admit_dispatchprevious-reservation-token	dispatch	route=adapter;caller=7;asset=17;source_domain=0;event=17;epoch=9;holder=7;generation=0;token_epoch=9;token_event=23	reply=rejected;unchanged=true	credential_from_another_reservation_cannot_be_reused,removing_reservation_binding_would_admit_the_same_actual_dispatchold-authority-token	dispatch	route=adapter;caller=7;asset=17;source_domain=0;event=17;epoch=9;holder=7;generation=0;token_epoch=8;token_event=17	reply=rejected;unchanged=true	normal_admin_and_adapter_reject_epoch_removal_control,removing_epoch_check_would_admit_the_same_actual_dispatchauthority-replacement	rotate-and-dispatch	route=administrative;caller=7;asset=17;source_domain=0;event=17;epoch=9;holder=7;generation=0;token_epoch=9;token_event=17;registered=0,2;new_epoch=10	epoch=10;reply=rejected;unchanged_after_install=true	authorized_context_install_sets_current_epoch,context_install_invalidates_old_credential

Reserve compares the versions of the whole footprint, returns Busy when any member is locked, and only then marks every member:

RustReserve the whole footprintsrc/lock_finality.rsL932-L974Highlighted lines are discussed in the textSource (opens the lines on GitHub in a new tab)
fn reserve(    &mut self,    actor: AuthenticatedCaller,    request: Request<'_>,    duration: u64,) -> Reply {    let Some(expires) = self.state.clock.checked_add(duration) else {        return Reply::Rejected;    };    let binding = request.binding;    let footprint = self.state.contexts[&request.endpoint].footprint(binding.asset);    if duration == 0        || self            .state            .reservations            .contains_key(&origin_key(binding.origin))        || !self.versions_match(&footprint, request.versions)    {        return Reply::Rejected;    }    if footprint        .iter()        .any(|asset| self.state.active.contains_key(asset))    {        return Reply::Busy;    }    for &asset in &footprint {        self.state.active.insert(asset, binding.origin);    }    self.state.reservations.insert(        origin_key(binding.origin),        Reservation {            binding,            footprint,            holder: actor.0,            generation: 0,            expires,            phase: Phase::Held,            terminal: None,        },    );    Reply::Reserved}

Each of these four commands applies in one phase. Cancel needs Held and Fence needs Dispatched, so neither applies once the debit has moved the reservation to Pending; in any other phase they are Rejected:

RustCommands by phasesrc/lock_finality.rsL834-L871Highlighted lines are discussed in the textSource (opens the lines on GitHub in a new tab)
let reply = match request.command {    Command::Dispatch { .. }        if reservation.phase == Phase::Held            && self.state.clock < reservation.expires            && binding.amount > 0            && self.unallocated_balance(binding.source()) >= binding.amount =>    {        reservation.phase = Phase::Dispatched;        Reply::SourceDispatched    }    Command::Debit { .. }        if self.state.clock < reservation.expires && binding.amount > 0 =>    {        if reservation.phase != Phase::Dispatched {            return Reply::Rejected;        }        if self.unallocated_balance(binding.source()) < binding.amount {            return Reply::Rejected;        }        let Some(balance) = self.balance(binding.source()).checked_sub(binding.amount)        else {            return Reply::Rejected;        };        self.state.balances.insert(binding.source(), balance);        reservation.phase = Phase::Pending;        Reply::Debited    }    Command::Cancel { .. }        if reservation.phase == Phase::Held && self.state.clock < reservation.expires =>    {        self.close(&mut reservation)    }    Command::Fence { .. }        if reservation.phase == Phase::Dispatched            && self.state.clock < reservation.expires =>    {        self.close(&mut reservation)    }

Reference source attestations#

NormativeBytes, test vectors and proofs

A registered external source commits its exact terminal decision to its own durable source journal using its decision key. The source journal accepts one exact decision per origin. A registered quorum signer reads that record through its source reader and signs a purpose-specific statement.

The signer receives neither the journal writer handle nor a caller-selected terminal result. Each statement binds the deployment profile, full transfer binding, kind, reference and preserved origin epoch.

The receiving authority verifies distinct signer identifiers and distinct registered public keys against the applicable source and origin epoch. The current caller's authenticated session and epoch are separate. A confirmed credit consumes a retained exact source decision.

Refund and reconciliation also verify the matching source proof and the current owner's endorsement of the exact request. Source storage MUST supply its own latest authenticated record. The receiving authority's independent archive and witness protect its own retained consumption.

The reference source uses its own independent selection store with a separate authenticated cluster, endpoint, trust and key. Each quorum signer reads the journal first and the current independent selection last. It signs only if the journal's exact value and key revision match that selection.

The source writer reserves one exact signed addition at the witness, prepares the journal, then selects that exact prepared value with a witness compare-and-set. While reserved, unavailable or mismatched, the source cannot issue votes or skip to another addition. Initialization explicitly binds two empty keys.

A separately restored journal can resume signing after the trusted writer restores the exact selected bytes and binds their new journal revision. This recovery depends on separate custody of the source witness.

A quorum signer holds only its identifier and signing key. Its one signing entry takes a journal reader and an origin identifier, and it signs the statement recorded in the journal under the quorum purpose, only when its key is the one registered for its identifier:

RustSign from the journalsrc/authority/recovery/wire/ed25519/source.rsL579-L614Highlighted lines are discussed in the textSource (opens the lines on GitHub in a new tab)
/// Held by one registered source attester. The only signing entry consumes/// the authoritative journal and an origin identifier.pub struct SourceSigner {    id: u64,    key: SigningKey,}impl SourceSigner {    pub fn new(id: u64, key: SigningKey) -> Self {        Self { id, key }    }    pub fn issue(        &self,        journal: &SourceReader,        origin: OriginKey,    ) -> Result<(SourceStatement, SourceVote), WireError> {        let decision = journal.decision(origin).map_err(|_| INVALID)?;        let rule = rule_for(&journal.authorities, &decision.statement).ok_or(INVALID)?;        if rule.signers.get(&self.id) != Some(&self.key.verifying_key().to_bytes()) {            return Err(INVALID);        }        let signature = self            .key            .sign(&decision.statement.quorum_signing_bytes()?)            .to_bytes()            .to_vec();        Ok((            decision.statement,            SourceVote {                signer: self.id,                signature,            },        ))    }}

The receiving authority selects the signer set by the binding's origin domain and the preserved origin epoch, and the certificate must carry the exact expected statement. A vote from an unknown or repeated signer, or with a bad signature, fails the whole certificate, and the votes must cover at least the quorum of distinct registered keys:

RustVerify the quorum certificatesrc/authority/recovery/wire/ed25519/source.rsL96-L131Highlighted lines are discussed in the textSource (opens the lines on GitHub in a new tab)
fn rule_for<'a>(    rules: &'a SourceAuthorities,    statement: &SourceStatement,) -> Option<&'a SourceAuthority> {    rules        .get(&statement.evidence.binding.origin.domain)?        .get(&statement.origin_epoch)}pub(super) fn verify_certificate(    rules: &SourceAuthorities,    expected: SourceStatement,    proof: &[u8],) -> bool {    let Ok(certificate) = canonical::<SourceCertificate>(proof) else {        return false;    };    if certificate.statement != expected {        return false;    }    let Some(rule) = rule_for(rules, &certificate.statement).filter(|rule| rule.valid()) else {        return false;    };    let Ok(message) = expected.quorum_signing_bytes() else {        return false;    };    let mut ids = BTreeSet::new();    let mut keys = BTreeSet::new();    certificate.votes.iter().all(|vote| {        rule.signers.get(&vote.signer).is_some_and(|key| {            ids.insert(vote.signer)                && keys.insert(*key)                && verify_strict(key, &message, &vote.signature)        })    }) && keys.len() >= rule.quorum}

Pending mass and terminal evidence#

NormativeBytes, test vectors and proofs

  • A source debit MUST move the corresponding amount from active mass to a pending obligation bound to its original event and immutable transfer details. Initialization (genesis) MUST establish the initial balance and supply conditions used for conservation.
  • Each original debit MUST be consumed at most once. A pending obligation cannot be spent, debited a second time, or reissued. Active mass plus pending mass is conserved by each covered transfer transition.
  • Exact confirmed source evidence MAY move the pending amount to the bound destination once. Exact reversal evidence MAY return it to the bound source once. Acceptance MUST check the original event and binding, including the amount, owner, and destination. Once terminal, an obligation cannot reopen or reach the opposite outcome.
  • A successful credit consumes pending mass and leaves the reservation in place. A reconciliation with the exact confirmed record and a current unexpired lease releases it without moving funds. An expired lease MUST be reassigned before reconciliation or reversal refund.
  • A deadline expiry, missing acknowledgement, or driver failure MAY trigger retry, reconciliation, or escalation. It cannot supply reversal evidence or release pending mass. The source debit obligation is distinct from a publication retry after the authoritative decision.

Settlement needs the reservation in Pending and recorded evidence of the requested kind for the same binding. A credit keeps the footprint, and a refund releases it:

RustSettle a pending obligationsrc/lock_finality.rsL1055-L1085Highlighted lines are discussed in the textSource (opens the lines on GitHub in a new tab)
fn settle(    &mut self,    reservation: &mut Reservation,    reference: RecordRef,    kind: TerminalKind,) -> Reply {    if reservation.phase != Phase::Pending || !self.exact_terminal(reservation, reference, kind)    {        return Reply::Rejected;    }    let binding = reservation.binding;    let (account, phase, reply) = match kind {        TerminalKind::Confirmed => (binding.destination(), Phase::Credited, Reply::Credited),        TerminalKind::Reversed => (binding.source(), Phase::Refunded, Reply::Refunded),    };    let Some(balance) = self.balance(account).checked_add(binding.amount) else {        return Reply::Rejected;    };    self.state.balances.insert(account, balance);    // Credit consumes pending mass but retains the lock until source    // reconciliation. Reversal returns the funds and closes the lock.    if kind == TerminalKind::Reversed {        self.release_footprint(reservation);    } else {        self.state            .root_funding            .insert((binding.origin, account), binding.amount);    }    reservation.phase = phase;    reply}

These vectors debit 5 from a source of 10 (the refund row starts from 5). A timeout, a cancel, a fence, a second debit, a driver loss and a preemption all leave 5 pending; only exact evidence credits or refunds it:

TextPending mass vectorstests/vectors/lock-finality.tsvL10-L21Source (opens the lines on GitHub in a new tab)

Columnsidoperationinputsexpectedevidence

source-debit	debit	source_domain=0;event=17;asset=17;destination_domain=2;recipient=3;caller=7;initial=10;amount=5;epoch=9;generation=0	source=5;pending=5;destination=0	source_debit_puts_five_into_pendingpending-timeout	timeout	source_domain=0;event=17;asset=17;destination_domain=2;recipient=3;caller=7;initial=10;amount=5;epoch=9;generation=0;elapsed=10	source=5;pending=5;destination=0	pending_source_survives_actual_timeoutpending-cancel	cancel	source_domain=0;event=17;asset=17;destination_domain=2;recipient=3;caller=7;initial=10;amount=5;epoch=9;generation=0	reply=rejected;source=5;pending=5;destination=0	pending_source_rejects_cancel_and_fence,actual_cancel_cannot_release_a_pending_sourcepending-fence	fence	source_domain=0;event=17;asset=17;destination_domain=2;recipient=3;caller=7;initial=10;amount=5;epoch=9;generation=0	reply=rejected;source=5;pending=5;destination=0	pending_source_rejects_cancel_and_fence,actual_fence_cannot_release_a_debited_sourcesource-retry	retry-debit	source_domain=0;event=17;asset=17;destination_domain=2;recipient=3;caller=7;initial=10;amount=5;epoch=9;generation=0	source=5;pending=5;destination=0	actual_source_retry_cannot_debit_againpending-driver-loss	driver-loss	source_domain=0;event=17;asset=17;destination_domain=2;recipient=3;caller=7;initial=10;amount=5;epoch=9;generation=0	source=5;pending=5;destination=0	driver_failure_preserves_authoritative_statepending-preempt	preempt	source_domain=0;event=17;asset=17;destination_domain=2;recipient=3;caller=7;initial=10;amount=5;epoch=9;generation=0;elapsed=10;duration=5	generation=1;source=5;pending=5;destination=0	actual_reassignment_preserves_pending_and_source_evidence,expired_worker_reassignment_activatesconfirmed-credit	credit	source_domain=0;event=17;asset=17;destination_domain=2;recipient=3;caller=7;initial=10;amount=5;epoch=9;generation=0;evidence=confirmed;record_index=0;request_index=0	reply=credited;source=5;pending=0;destination=5	product_confirmed_consumer_credits_at_the_bound_entry,full_credit_consumer_activatesreversed-refund	refund	source_domain=0;event=22;asset=22;destination_domain=2;recipient=3;caller=7;initial=5;amount=5;epoch=9;generation=0;evidence=reversed;record_index=0;request_index=0	reply=refunded;source=5;pending=0;destination=0	product_reversed_consumer_refunds_at_the_bound_entry,actual_reversal_release_activateswrong-terminal-index	credit	source_domain=0;event=17;asset=17;destination_domain=2;recipient=3;caller=7;initial=10;amount=5;epoch=9;generation=0;evidence=confirmed;record_index=0;request_index=1	reply=rejected;source=5;pending=5;destination=0	missing_terminal_reference_mutation,actual_delivery_requires_exact_confirmed_evidenceconfirmation-cannot-refund	refund	source_domain=0;event=17;asset=17;destination_domain=2;recipient=3;caller=7;initial=10;amount=5;epoch=9;generation=0;evidence=confirmed;record_index=0;request_index=0	reply=rejected;source=5;pending=5;destination=0	actual_return_requires_exact_reversal_evidence,valid_finalized_evidence_cannot_refundtimeout-then-late-credit	timeout-and-credit	source_domain=0;event=17;asset=17;destination_domain=2;recipient=3;caller=7;initial=10;amount=5;epoch=9;generation=0;elapsed=10;evidence=confirmed;record_index=0;request_index=0	reply=credited;source=5;pending=0;destination=5	pending_timeout_still_allows_late_credit,timeout_refund_mutation

Current observations#

NormativeBytes, test vectors and proofs

  • A current response MUST identify its authoritative revision and authority epoch. The endpoint MUST check the authenticated caller's read permission and the expected cut at the same boundary that supplies the value.
  • A cache entry MUST be bound to the source authority, query identity, revision and epoch that supplied its value. The endpoint MUST compare those bindings with the same authoritative cut before serving a current response.
  • A query touching a source effect or terminal record awaiting primary publication MUST remain busy at that cut. An application value under an active lock MUST remain busy at that cut. These busy rules apply to current, historical and capture-cache reads. Raw data and operation status reads are observations that reader authorization permits, and they return the recorded value without these busy rules.
  • If the expected cut is stale, authority is unavailable, or permission has changed, the endpoint MUST fail closed. A failed current check cannot authorize an effect. A cached result cannot supply authority for an effect.
  • A source balance is the unallocated source pool at the requested domain and asset, including its retained funded allocation distinction. The pool comes from the selected current or historical cut. The Registry owner controls recovery and does not select the account for this read.

A current read checks the caller, then the expected cut, and serves a cached value only when it was captured from the same source at the same revision and epoch:

RustCurrent readsrc/authority/reads.rsL444-L478Highlighted lines are discussed in the textSource (opens the lines on GitHub in a new tab)
pub fn current(    &self,    reader: Reader,    query: Query,    expected: CommitCut,    cache: Option<&CacheEntry>,    permitted: impl FnOnce(AuthenticatedCaller, ReadKind, Query) -> bool,) -> Result<CurrentRead, ReadError> {    self.authorize(reader, ReadKind::Current, query, permitted)?;    let cut = self.current_cut();    if expected != cut {        return Err(ReadError::Busy);    }    let value = self        .authority        .state        .read_history        .value(cut.revision, query)?;    if let Some(entry) = cache {        if entry.query != query            || entry.provider != self.provider            || !entry.matches_source(self.profile, &self.source)            || entry.observed.cut.revision != cut.revision            || entry.observed.cut.authority_epoch != cut.authority_epoch            || entry.observed.value != value        {            return Err(ReadError::Busy);        }        return Ok(CurrentRead {            cut,            value: entry.observed.value,        });    }    Ok(CurrentRead { cut, value })}

The value itself is busy while the query touches a source effect or terminal record awaiting primary publication, or an application value under an active lock:

RustBusy rulessrc/authority/reads.rsL208-L225Highlighted lines are discussed in the textSource (opens the lines on GitHub in a new tab)
fn value(&self, query: Query) -> Result<Value, ReadError> {    if self        .origins        .values()        .any(|origin| !origin.published && query.touches(origin.binding))        || self            .legal            .values()            .any(|origin| !origin.published && query.touches(origin.binding))    {        return Err(ReadError::Busy);    }    if matches!(query, Query::ApplicationValue(asset) if self.ledger.locked_assets.contains(&asset))    {        return Err(ReadError::Busy);    }    self.raw_value(query)}

In these vectors a reservation, a write of 9 and a release make 9 the current value at revision 3. The rows read and execute against current, cached, stale and historical cuts, and a read while the reservation holds the value is busy:

TextRead vectorstests/vectors/current-and-historical-reads.tsvL1-L10Source (opens the lines on GitHub in a new tab)
id	operation	inputs	expected	evidencecurrent-after-write-and-release	current-direct	provider=100;caller=7;session=1;epoch=9;asset=17;source_domain=0;event=17;initial_value=0;written_value=9;expected_revision=3	kind=current;revision=3;epoch=9;value=9	stale_cache_returns_busy_and_direct_current_read_returns_nine,generated_current_reads_are_the_actual_cutnamed-old-prefix	historical	provider=100;caller=7;session=1;epoch=9;asset=17;source_domain=0;event=17;initial_value=0;written_value=9;requested_revision=0	kind=historical;revision=0;value=0	historical_revision_zero_is_still_the_old_value,successful_historical_product_read_is_the_named_prefixstale-cache-cannot-answer-current	stale-cache	provider=100;caller=7;session=1;epoch=9;asset=17;source_domain=0;event=17;initial_value=0;written_value=9;cached_revision=0;expected_revision=3	kind=busy;authority_unchanged=true	stale_cache_returns_busy_and_direct_current_read_returns_nine,removing_only_the_revision_comparison_returns_a_stale_current_valuefresh-cache-at-actual-cut	current-cache	provider=100;caller=7;session=1;epoch=9;asset=17;source_domain=0;event=17;initial_value=0;written_value=9;cached_revision=3;expected_revision=3	kind=current;revision=3;epoch=9;value=9	cache_capture_preserves_source_values,generated_current_reads_are_the_actual_cutlocked-application-value-is-hidden	held-read	provider=100;caller=7;session=1;epoch=9;asset=17;source_domain=0;event=17;initial_value=0;written_value=9;expected_revision_offset=0	kind=busy;authority_unchanged=true	held_application_value_is_not_visible_at_its_cutwrite-at-live-cut-while-read-is-held	held-write	provider=100;caller=7;session=1;epoch=9;asset=17;source_domain=0;event=17;initial_value=0;written_value=9;expected_revision_offset=0	write=accepted;held_read=busy;released_value=9	active_lock_can_be_written_at_its_checked_cut,held_application_value_is_not_visible_at_its_cut,actual_authority_write_replaces_value_zero_with_ninestale-revision-cannot-execute	stale-execute	provider=100;caller=7;session=1;epoch=9;asset=17;source_domain=0;event=17;initial_value=0;written_value=9;expected_revision=0;actual_revision=3	kind=busy;authority_unchanged=true;value=9	stale_observation_cannot_authorize_a_write_after_the_cut_moves,stale_cut_at_authorized_live_endpoint_is_busysame-revision-old-epoch-cannot-execute	stale-epoch	provider=100;caller=7;session=1;epoch=9;asset=17;source_domain=0;event=17;initial_value=0;written_value=9;expected_epoch=8;expected_revision=3;actual_revision=3	kind=busy;authority_unchanged=true	changed_authority_epoch_blocks_checked_execution,stale_cut_at_authorized_live_endpoint_is_busyunavailable-historical-prefix	historical	provider=100;caller=7;session=1;epoch=9;asset=17;source_domain=0;event=17;initial_value=0;written_value=9;requested_revision=4	kind=unavailable	authority_prefix_snapshot_has_exact_requested_history,read_historical_product_def

Historical observations#

NormativeBytes, test vectors and proofs

  • A historical request MUST name an explicit revision. The endpoint MUST derive the value from that prefix of the authoritative history and label the response as historical.
  • The same publication and lock visibility checks apply to the named historical prefix.
  • Reading a historical revision MUST satisfy the current caller's read policy. A historical value cannot be consumed as a current precondition.

A historical read names its revision, passes the current caller's authorization and is answered from that prefix under the same busy rules:

RustHistorical readsrc/authority/reads.rsL496-L506Highlighted lines are discussed in the textSource (opens the lines on GitHub in a new tab)
pub fn historical(    &self,    reader: Reader,    query: Query,    revision: usize,    permitted: impl FnOnce(AuthenticatedCaller, ReadKind, Query) -> bool,) -> Result<HistoricalRead, ReadError> {    self.authorize(reader, ReadKind::Historical, query, permitted)?;    let value = self.authority.state.read_history.value(revision, query)?;    Ok(HistoricalRead { revision, value })}

Execution#

NormativeBytes, test vectors and proofs

  • An operation relying on a current observation MUST recheck the expected revision and authority epoch inside the same commit boundary that applies the operation.
  • Execution uses the operation's own authorization, lock and evidence checks. The publication and lock visibility rules govern observation replies.
  • A revision or policy change between the observation and that boundary MUST result in a busy or rejected outcome with no financial effect. A successful operation consumes the cut it checked.

Execution compares the observed cut with the current one before it forwards the call to the authority:

RustExecute at the checked cutsrc/authority/reads.rsL508-L531Highlighted lines are discussed in the textSource (opens the lines on GitHub in a new tab)
pub fn execute_at_checked_cut(    &mut self,    route: Route,    actor: AuthenticatedCaller,    expected: CommitCut,    id: CallId,    payload: &CallPayload,    verify: impl FnOnce(AuthenticatedCaller, CallId, &CallPayload) -> bool,) -> CheckedExecution {    if !self.authority.access(actor, id, payload, verify) {        return CheckedExecution::Rejected;    }    if !self.available() {        return CheckedExecution::Unavailable;    }    let actual = self.current_cut();    if expected.revision != actual.revision        || expected.authority_epoch != actual.authority_epoch        || payload.cut != actual    {        return CheckedExecution::Busy;    }    CheckedExecution::Forwarded(self.authority.submit_bound(route, actor, id, payload))}

The ledger checks the signed cut again when it applies the command. Any reply other than Rejected or Busy advances the revision, so the next request needs the new cut:

RustCommit advances the revisionsrc/lock_finality.rsL726-L747Highlighted lines are discussed in the textSource (opens the lines on GitHub in a new tab)
pub(crate) fn submit(    &mut self,    route: Route,    actor: AuthenticatedCaller,    request: Request<'_>,    verify: impl FnOnce(AuthenticatedCaller, Route, &Request<'_>) -> bool,) -> Reply {    let Some(revision) = self.state.cut.revision.checked_add(1) else {        return Reply::Rejected;    };    if !self.request_admitted(route, actor, request)        || !verify(actor, route, &request)        || !self.credential_matches(actor, request)    {        return Reply::Rejected;    }    let reply = self.apply(actor, request);    if !matches!(reply, Reply::Rejected | Reply::Busy) {        self.state.cut.revision = revision;    }    reply}

Edition history#

  • 0.6.0: Added this edition. The v0.5 specification remains available at its original addresses.