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.
- 1
Whole footprint at once
Reservelocks the asset and every dependency together. If any member is already locked, it returnsBusyand takes none. - 2
Exits before the debit
CancelfromHeldandFencefromDispatchedclose the reservation and release the footprint. Nothing has been debited yet. - 3
Debit
The source balance drops by the amount, which becomes one pending obligation. From here,
Cancel,Fenceand a secondDebitare rejected. - 4
Exact evidence settles once
RecordTerminalaccepts evidence only for the same binding, and later records keep its kind.Creditpays the destination onConfirmed;Refundreturns the amount to the source onReversed. - 5
Time does not settle
Timeoutonly advances the clock.Preemptgives an expired lease a new holder and generation, and the amount stays pending. - 6
Lock kept until reconciled
Creditkeeps the footprint locked untilReconcilepresents the confirmed record.ReconcileandRefundneed 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.
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:
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:
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:
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_result exact-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_effect same-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_effect denial-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_effect unavailable-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_effect busy-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:
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:
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_authority publication-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_exchange actual-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_exchange same-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_order same-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_slots same-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_order distinct-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_effect current-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:
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:
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_credential administrative-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_credential adapter-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_credential wrong-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_dispatch wrong-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_dispatch previous-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_dispatch old-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_dispatch authority-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:
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:
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:
/// 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:
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:
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:
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_pending pending-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_timeout pending-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_source pending-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_source source-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_again pending-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_state pending-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_activates confirmed-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_activates reversed-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_activates wrong-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_evidence confirmation-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_refund timeout-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:
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:
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:
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_cut named-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_prefix stale-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_value fresh-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_cut locked-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_cut write-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_nine stale-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_busy same-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_busy unavailable-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:
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:
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:
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}Related Pages#
Edition history#
- 0.6.0: Added this edition. The v0.5 specification remains available at its original addresses.
