Protocol Specification v0.6

Fault Tolerance

Recovery starts from the exact authenticated current authority, not from a process-local cache or merely valid old file. The retained state includes each durable request binding and outcome. Restarting a process must not create a second financial effect or treat an unanchored preparation as a committed result.

Anchored commit

A flushed record is committed only once compare-and-set makes the head name it, and every reopen starts from that head.

A flushed record is committed only once compare-and-set makes the head name it, and every reopen starts from that head. The authority appends record 2 to the journal file and flushes it; until compare-and-set moves the authenticated head from record 1 to record 2, the record is not committed. The authority then reads the head back, which must name record 2, otherwise Hold, and reads that exact record back, which must be unchanged, otherwise InvalidImage; only then is the reply exposed. On reopen, a fresh endpoint reads the head, refused with MissingHead, Hold or AttachmentRejected, and the exact record it names, refused with InvalidImage, before it serves. Authority Authenticated head Journal file record 1 records 0, 1 Commit 1 append_record write, then flush record 2 2 compare_and_set previous, candidate record 2 current() must name record 2 Hold exact_image must equal record 2 InvalidImage 3 reply exposed Reopen 4 open on a fresh process current() a head with this profile MissingHead, Hold AttachmentRejected exact_image verified record 2 InvalidImage serves from record 2
  1. 1

    Append and flush

    append_record writes record 2 at the end of the journal file, flushes it and returns a candidate AnchorHead with its offset, length and SHA-256 digest. Flushed alone, the record is not committed.

  2. 2

    Compare-and-set

    compare_and_set replaces the head only while it still equals record 2's previous, the head of record 1. Its own reply is not trusted, so the readback settles a lost or competing reply.

  3. 3

    Readback before reply

    The reply is exposed only after current() names record 2 and that exact record reads back unchanged. A different head gives Hold and a different record InvalidImage; neither exposes a reply.

  4. 4

    Reopen from the head

    A fresh endpoint reads the authenticated head first, and exact_image checks the SHA-256 digest, profile, sequence, exact encoding and predecessor. An older valid record or an unanchored one is never served.

  • Flushed, not yet named by the head
  • Commit: write, then compare-and-set
  • The record the head names after the commit
  • Result exposed only after both reads
  • Refused, nothing exposed

Schematic: the direct profile with one writer; the independent profile also checks a witness and its archive.

Figure 1: Anchored commit and reopen through the authenticated head

One durable authority commit#

NormativeBytes, test vectors and proofs

  • A registered authority MUST retain one canonical record of the state that supplies its effects and exposed observations. It includes pending source obligations and exact evidence, origin decisions and publication, complete request bindings and authoritative outcomes, completion state, regulatory and spend receipts, ownership and sessions, authoritative historical prefixes, and the separate secondary queue.
  • The record MUST bind the deployment, format and authority profile, a monotone commit sequence distinct from the product cut, the preceding record, and a digest recomputed from the exact canonical bytes. The store MUST durably flush the record before advancing the independent rollback-resistant head by compare-and-set. Only an anchored record is committed.
  • Mutating endpoints MUST confirm that commit before exposing any resulting reply or outbound work. An unanchored prepare is ignored. A conflicting or uncertain head update MUST be reconciled against the authoritative head and retained committed result before reporting a result. Concurrent execution MUST use the same serialized commit boundary.

Reading the record that a head names recomputes its SHA-256 digest and checks the profile, the sequence, the exact encoding and the predecessor.

RustReading an anchored recordsrc/authority/recovery.rsL364-L405Highlighted lines are discussed in the textSource (opens the lines on GitHub in a new tab)
fn read_record(file: &mut File, head: AnchorHead) -> Result<CommitRecord, RecoveryError> {    let end = recovery_bounds::checked_end(head.offset, head.length)        .ok_or(RecoveryError::InvalidImage)?;    if head.length == 0 || end > file.metadata().map_err(|_| RecoveryError::Io)?.len() {        return Err(RecoveryError::InvalidImage);    }    let size = usize::try_from(head.length).map_err(|_| RecoveryError::InvalidImage)?;    let mut bytes = Vec::new();    bytes        .try_reserve_exact(size)        .map_err(|_| RecoveryError::InvalidImage)?;    bytes.resize(size, 0);    file.seek(SeekFrom::Start(head.offset))        .map_err(|_| RecoveryError::Io)?;    file.read_exact(&mut bytes).map_err(|_| RecoveryError::Io)?;    let digest: [u8; 32] = Sha256::digest(&bytes).into();    if digest != head.digest {        return Err(RecoveryError::InvalidImage);    }    let record: CommitRecord =        serde_json::from_slice(&bytes).map_err(|_| RecoveryError::InvalidImage)?;    if record.profile != head.profile        || record.sequence != head.sequence        || encode(&record)? != bytes    {        return Err(RecoveryError::InvalidImage);    }    match record.previous {        None if head.sequence == 0 => (),        Some(previous)            if recovery_bounds::valid_predecessor(                previous.profile == head.profile,                previous.sequence,                previous.offset,                previous.length,                head.sequence,                head.offset,            ) => {}        _ => return Err(RecoveryError::InvalidImage),    }    Ok(record)}

Appending writes the record and runs the flush that the caller supplies, File::sync_all in the reference transaction, before it returns the candidate head.

RustAppend and flushsrc/authority/recovery.rsL483-L499Highlighted lines are discussed in the textSource (opens the lines on GitHub in a new tab)
fn append_record(    file: &mut File,    record: &CommitRecord,    flush: impl FnOnce(&File) -> io::Result<()>,) -> Result<AnchorHead, RecoveryError> {    let bytes = encode(record)?;    let offset = file.seek(SeekFrom::End(0)).map_err(|_| RecoveryError::Io)?;    file.write_all(&bytes).map_err(|_| RecoveryError::Io)?;    flush(file).map_err(|_| RecoveryError::Io)?;    Ok(AnchorHead {        profile: record.profile,        sequence: record.sequence,        offset,        length: u64::try_from(bytes.len()).map_err(|_| RecoveryError::Io)?,        digest: Sha256::digest(&bytes).into(),    })}

After the compare-and-set, the authority reads back the head and the exact record. A different head is Hold, and a different record is InvalidImage.

RustCommit confirmationsrc/authority/recovery.rsL573-L605Highlighted lines are discussed in the textSource (opens the lines on GitHub in a new tab)
fn confirm_authority_record(    file: &mut File,    anchor: &impl AuthenticatedHead,    expected: AnchorHead,    record: &CommitRecord,    checkpoint: CheckpointState,) -> Result<(), RecoveryError> {    let (head, floor) = match checkpoint {        CheckpointState::Direct => (anchor.current().map_err(|_| RecoveryError::Hold)?, None),        CheckpointState::Independent(observed) => {            let (head, current) = anchor.checkpoint_readback().map_err(|_| RecoveryError::Hold)?;            if current.writer_generation != observed.writer_generation                || current.writer_id != observed.writer_id                || current.protected_head != Some(expected)            {                return Err(RecoveryError::Hold);            }            (head, Some(current.head.ok_or(RecoveryError::Hold)?))        }    };    if head != Some(expected) {        return Err(RecoveryError::Hold);    }    let readback = exact_image(file, expected)?;    if readback != *record {        return Err(RecoveryError::InvalidImage);    }    if let Some(floor) = floor {        validate_lineage(file, expected, &readback, floor)?;        anchor.verify_independent_journal(file, expected).map_err(|_| RecoveryError::Hold)?;    }    Ok(())}

The recovery vectors keep a flushed record invisible until its compare-and-set, reopen only from the record the head names, and settle a lost compare-and-set reply from the head.

TextCommit and reopen vectorstests/vectors/persistent-recovery.tsvL2-L9Source (opens the lines on GitHub in a new tab)

Columnsidoperationinputsexpectedevidence

flush-before-cas-is-not-committed	prepare	deployment=7;profile=99;old_head=0;candidate=1;flushed=true;cas=false	head=0;new_reply_exposed=false	staged_commit_head_still_identifies_the_previous_image,new_reply_is_invisible_during_prepare_or_flushhead-confirms-the-exact-record	reopen	deployment=7;format=1;profile=99;old_head=0;candidate=1;flushed=true;cas=true	head=1;reopened=true	exact_new_head_reopens_the_records_only_product,independently_anchored_records_construct_a_fresh_endpointold-valid-record-replay	reopen	deployment=7;profile=99;trusted_head=1;supplied_sequence=0;old_record_valid=true	reopened=false;genesis_fallback=false	replaying_old_valid_bytes_cannot_replace_the_latest_image,an_old_valid_record_at_the_current_address_is_rejectedcurrent-sequence-altered-body	reopen	deployment=7;profile=99;trusted_head=1;supplied_sequence=1;body_changed=true	reopened=false	a_changed_body_cannot_match_the_anchored_digestmissing-anchored-record	reopen	deployment=7;profile=99;trusted_head=1;record_present=false	reopened=false;genesis_fallback=false	a_missing_anchor_record_does_not_restart_from_genesiswrong-authority-profile	reopen	deployment=7;trusted_profile=99;attachment_profile=98;head=1	reopened=false	independently_anchored_records_construct_a_fresh_endpoint,reopen_from_authenticated_head_defuncertain-commit-confirmed-by-head	commit	deployment=7;profile=99;old_head=0;candidate=1;durable=true;cas_applied=true;cas_reply_lost=true	head=1;reply_exposed_after_verified_head=true;reexecution=false	exact_committed_head_releases_the_prepared_response,exact_new_head_reopens_the_records_only_productcompeting-compare-and-set	commit	deployment=7;profile=99;expected_head=0;current_head=1;candidate=1;previous_reply_lost=true	head_unchanged=true;unconfirmed_reply_exposed=false	losing_a_compare_and_set_does_not_rewrite_the_current_head,new_reply_is_invisible_during_prepare_or_flush

Configured etcd control transport#

NormativeBytes, test vectors and proofs

Cluster identity and credentials#

The direct etcd transport uses a fixed endpoint, pinned etcdctl executable and explicit mTLS credentials.

  • The CA and server identity MUST be dedicated to the configured cluster, and the writer credentials MUST be dedicated to its authority key.
  • A different cluster MUST NOT reuse that trust identity.

The preliminary linearizable cluster check and the transaction use separate client connections, so the TLS identity is part of the cluster binding. The authority also validates the deployment, format, profile and writer generation inside the exact control value.

The trust identity is the canonical path and SHA-256 digest of the CA, the client certificate and the key, and each must be a non-empty file.

RustTrust identitysrc/authority/recovery/ordered/etcd.rsL315-L342Highlighted lines are discussed in the textSource (opens the lines on GitHub in a new tab)
#[cfg(target_os = "linux")]#[derive(Eq, PartialEq)]struct TrustIdentity {    paths: [PathBuf; 3],    digests: [[u8; 32]; 3],}#[cfg(target_os = "linux")]impl TrustIdentity {    fn read(configured: &[String; 3]) -> Result<Self, HeadError> {        let mut paths = [PathBuf::new(), PathBuf::new(), PathBuf::new()];        let mut digests = [[0; 32]; 3];        for ((path, digest), configured) in paths.iter_mut().zip(&mut digests).zip(configured) {            *path = fs::canonicalize(configured).map_err(|_| HeadError)?;            let mut file = fs::File::open(&path).map_err(|_| HeadError)?;            if !file.metadata().map_err(|_| HeadError)?.is_file() {                return Err(HeadError);            }            let mut hash = Sha256::new();            let bytes = std::io::copy(&mut file, &mut hash).map_err(|_| HeadError)?;            if bytes == 0 {                return Err(HeadError);            }            *digest = hash.finalize().into();        }        Ok(Self { paths, digests })    }}

Compare-and-set and readback#

  1. Read one configured key with linearizable consistency and verify its cluster identity, per-key modification revision and exact value bytes.
  2. Replace through one transaction that compares both the observed modification revision and its exact value.

A definite comparison failure has an empty write branch. Missing, malformed or unauthenticated responses and mismatching readback hold. A transport error can leave the transaction outcome unresolved. The authority MUST NOT expose an outcome before it confirms the exact selected head and its flushed record.

The transport reads the key with linearizable consistency, checks the endpoint again before the transaction and reads the key back after it.

RustRead, transaction and readbacksrc/authority/recovery/ordered/etcd.rsL180-L212Highlighted lines are discussed in the textSource (opens the lines on GitHub in a new tab)
/// Read one current, linearizable control value. Missing keys fail closed.pub fn current(&self) -> Result<ControlValue, HeadError> {    let stdout = self.run(&["get", "--consistency=l", "--", &self.key], None)?;    range_value(        &stdout,        self.cluster_id,        self.key.as_bytes(),        self.max_control_bytes,    )}/// Compare the exact previous value and per-key MOD in one transaction./// A false result means the server reported a definite comparison failure./// Any command or confirmation error may be ambiguous.pub fn replace(&self, expected: &ControlValue, candidate: &[u8]) -> Result<bool, HeadError> {    if expected.revision == 0        || expected.revision > i64::MAX as u64        || expected.bytes.len() > self.max_control_bytes        || candidate.len() > self.max_control_bytes    {        return Err(HeadError);    }    // Bind the configured endpoint before writing. The dedicated TLS    // identity also applies to the separate connection used by the CAS.    self.current()?;    let input = compare_input(&self.key, expected, candidate);    let stdout = self.run(&["txn"], Some(input))?;    let Some(revision) = txn_revision(&stdout, self.cluster_id, expected.revision)? else {        return Ok(false);    };    require_readback(self.current()?, revision, candidate)?;    Ok(true)}

The transaction compares the modification revision and the exact value of the key before its put.

RustTransaction comparesrc/authority/recovery/ordered/etcd.rsL527-L536Highlighted lines are discussed in the textSource (opens the lines on GitHub in a new tab)
fn compare_input(key: &str, expected: &ControlValue, candidate: &[u8]) -> Vec<u8> {    let key = quoted(key.as_bytes());    let revision = quoted(expected.revision.to_string().as_bytes());    format!(        "mod({key}) = {revision}\nvalue({key}) = {}\n\nput {key} {}\n\n\n",        quoted(&expected.bytes),        quoted(candidate),    )    .into_bytes()}

A failed comparison with an empty response list is the only definite failure, and a success must carry exactly one put. Any other response shape is an error.

RustTransaction resultsrc/authority/recovery/ordered/etcd.rsL646-L674Highlighted lines are discussed in the textSource (opens the lines on GitHub in a new tab)
fn txn_revision(bytes: &[u8], cluster: u64, expected: u64) -> Result<Option<u64>, HeadError> {    let response = TxnResponse::decode(bytes).map_err(|_| HeadError)?;    let revision = verified_revision(response.header, cluster)?;    match (response.succeeded, response.responses.as_slice()) {        (false, []) => Ok(None),        (            true,            [                ResponseOp {                    range: None,                    put: Some(put),                    delete: None,                    txn: None,                },            ],        ) if revision > expected && put.prev_kv.is_none() => {            if let Some(header) = &put.header {                // etcd may omit the nested header or populate only its revision.                if (header.cluster_id != 0 && header.cluster_id != cluster)                    || (header.revision != 0 && header.revision as u64 != revision)                {                    return Err(HeadError);                }            }            Ok(Some(revision))        }        _ => Err(HeadError),    }}

Restore and custody#

Direct attachment requires a trusted host policy that explicitly authenticates the direct profile. Restore administration and runtime writer credentials have separate custody.

An independently restored cluster or replayed control value requires a separately protected selection witness before it can serve authority state. The direct transport itself supplies only current-cluster CAS and readback.

The direct profile is served only through a verifier that the host's attachment policy supplies.

RustDirect attachment policysrc/authority/recovery/ordered/etcd.rsL260-L289Highlighted lines are discussed in the textSource (opens the lines on GitHub in a new tab)
/// Supplied by the host's authenticated direct-mode attachment policy./// A profile requiring an independent witness cannot use this verifier.pub trait DirectAttachmentVerifier {    type Proof;    fn verify_direct_attachment(        &self,        profile: RecoveryProfile,        connected: bool,        proof: &Self::Proof,    ) -> bool;}/// The raw transport can also be used by a separate independent witness./// This attachment wrapper serves only an explicitly authorized direct profile.pub struct EtcdControlStore<A> {    transport: EtcdTransport,    profile: RecoveryProfile,    attachment: A,}impl<A: DirectAttachmentVerifier> EtcdControlStore<A> {    pub fn new(transport: EtcdTransport, profile: RecoveryProfile, attachment: A) -> Self {        Self {            transport,            profile,            attachment,        }    }}

Independent archive selection#

NormativeBytes, test vectors and proofs

The independent ordered profile uses two configured and independently administered etcd clusters and a dedicated Linux archive directory. Authority and witness have separate endpoints, trust and writer credentials. An archive names the whole prefix of the journal selected by its exact head.

It binds the checked prefix length, full prefix digest and the digest and descriptor of the last record. A prepared suffix beyond the head remains outside that archive. New files and their directory entries become eligible only after synchronization, and restore creates a new path.

An archive copies only the prefix that the exact head selects. It becomes eligible after the staged file and its directory are synchronized.

RustArchive of the selected prefixsrc/authority/recovery/archive.rsL46-L77Highlighted lines are discussed in the textSource (opens the lines on GitHub in a new tab)
/// Retain only the prefix selected by this exact head. A staged or/// unanchored suffix after that prefix is never included.pub fn retain(&self, source: &mut File, head: AnchorHead) -> Result<ArchiveRef, RecoveryError> {    let prefix_bytes = self.prefix_length(head)?;    if source.metadata().map_err(io_error)?.len() < prefix_bytes {        return Err(RecoveryError::InvalidImage);    }    sync_directory(&self.directory)?;    source.sync_all().map_err(io_error)?;    let mut staged = Staged::create(&self.directory)?;    let prefix_sha256 = copy_prefix(source, prefix_bytes, &mut staged.file)?;    let archive = ArchiveRef {        head,        prefix_bytes,        prefix_sha256,    };    self.verify(&mut staged.file, archive)?;    staged.file.sync_all().map_err(io_error)?;    let destination = self.archive_path(archive);    match fs::hard_link(&staged.path, &destination) {        Ok(()) => {}        Err(error) if error.kind() == io::ErrorKind::AlreadyExists => {            // A concurrent or retried identical put must validate the            // existing bytes and reestablish the directory barrier.            self.open_verified(archive)?;        }        Err(_) => return Err(RecoveryError::Io),    }    sync_directory(&self.directory)?;    Ok(archive)}

The authority retains a bounded set of exact preparations for one witness revision and value. An exact witness CAS selects a preparation only after its archive is durable. The witness binds the profile, authority and witness identities, complete ordered control value, writer, archive and repair nonce. No preparation is an authoritative result.

The witness selection holds each of these bindings as a field.

RustWitness selectionsrc/authority/recovery/ordered/witness.rsL70-L82Source (opens the lines on GitHub in a new tab)
#[derive(Clone, Debug, Eq, PartialEq, Serialize, Deserialize)]struct Selection {    profile: RecoveryProfile,    authority: [u8; 32],    witness: [u8; 32],    control: Vec<u8>,    archive: ArchiveRef,    floor: AnchorHead,    writer: WitnessWriter,    previous: Option<Mark>,    repair_nonce: [u8; 32],    authorization: Option<[u8; 32]>,}

Independent publication archives the candidate and verifies that it extends the selected archive before it proposes and selects it on the witness.

RustIndependent publicationsrc/authority/recovery/ordered/witness.rsL645-L680Highlighted lines are discussed in the textSource (opens the lines on GitHub in a new tab)
fn publish_independent(    &self,    file: &mut File,    candidate: AnchorHead,    admission: &OrderedPublication,    checkpoint: CheckpointFence,) -> Result<Result<(), PublicationRefusal>, HeadError> {    let store = &self.ordered.store;    let previous = store.current_selection()?;    if previous.control() != admission.cut.stored || previous.checkpoint() != checkpoint {        return Ok(Err(PublicationRefusal::CheckpointChanged));    }    let Some(state) = proposed_publication::<WitnessStore<A>>(candidate, admission)? else {        return Ok(Err(PublicationRefusal::CheckpointChanged));    };    let archive = store        .archive        .retain(file, candidate)        .map_err(|_| HeadError)?;    let mut staged = store        .archive        .open_verified(archive)        .map_err(|_| HeadError)?;    store        .archive        .verify_journal_prefix(&mut staged, previous.envelope.archive)        .map_err(|_| HeadError)?;    let proposal = store.propose(&previous, state.encode()?, archive, store.writer);    store.select(&previous, &proposal).map(|selected| {        if selected {            Ok(())        } else {            Err(PublicationRefusal::CheckpointChanged)        }    })}

A missing authority mirror or lost reply can be reconciled through the exact selected witness and archive. Fresh admission requires an authority mirror or preparation that matches the selected witness. Earlier authority and journal snapshots hold even when their storage revisions have increased.

Provisioning starts from two pre-provisioned empty control keys. It archives a validated product genesis and selects it through a separate exact signed provisioning authorization. Repair uses a separate signed cut for the prospective authority configuration, destination, new writer and selected archive and nonce.

A resumed repair MAY verify and reuse the exact existing destination for the same authorization. A changed path or writer MUST consume the current nonce and can never overwrite an existing file. The independent profile uses signed ordered publication. Trusted maintenance actions require their own explicit authority entry.

Fresh endpoint and retained results#

NormativeBytes, test vectors and proofs

  • Reopen MUST reconstruct a fresh endpoint from the exact anchored record and current authenticated authority attachments. Recompute the digest and check the deployment, profile, sequence and exact head before serving. An old valid record, an unanchored record, or an absent or mismatched authenticated record cannot authorize a new endpoint.
  • Every exposed retry, current or historical read, cancellation, terminal use and spend MUST consume the reconstructed authority state. Current access and the original signed request remain mandatory. An exact admitted replay at the same epoch (opens in a new tab) reads its retained outcome before any fresh-cut, work or evidence check. A fresh stale request cannot execute. Changed bindings and obsolete epochs cannot read or replace an old result.
  • Cache data MUST be revalidated against the same current scope, cut, query, provider and actual authority value, using the same acceptance rules on an uninterrupted or reopened endpoint. Process-local caches and unverified source attachments do not supply effect authority.

A fresh service starts from the file, the head and the attachment alone, and it serves only after one empty transaction succeeds.

RustFresh servicesrc/authority/recovery.rsL750-L767Highlighted lines are discussed in the textSource (opens the lines on GitHub in a new tab)
/// Construct a fresh service without an earlier product or snapshot.pub fn open(    file: File,    anchor: H,    attachment: Attachment<S, H::AttachmentProof>,) -> Result<Self, RecoveryError> {    let mut service = Self {        file,        anchor,        profile: attachment.profile,        proof: attachment.proof,        security: Arc::new(attachment.security),        source_connected: attachment.source_connected,        independent_checkpoint: None,    };    service.transact(|_| ())?;    Ok(service)}

Each transaction reads the authenticated head, checks its profile and reads the exact record before it opens the product.

RustHead and record checkssrc/authority/recovery.rsL819-L858Highlighted lines are discussed in the textSource (opens the lines on GitHub in a new tab)
fn transact_guarded<T>(    &mut self,    act: impl FnOnce(&H, AnchorHead, &mut ProductAuthority<S>) -> std::ops::ControlFlow<T, T>,    wire: bool,    before_publish: impl FnOnce() -> Result<Option<PublicationGuard>, RecoveryError>,) -> Result<T, RecoveryError> {    Self::check_attachment(        &self.anchor,        self.profile,        self.source_connected,        &self.proof,    )?;    let locked = LockedFile::acquire(&mut self.file)?;    let head = self        .anchor        .current()        .map_err(|_| RecoveryError::Hold)?        .ok_or(RecoveryError::MissingHead)?;    if head.profile != self.profile {        return Err(RecoveryError::AttachmentRejected);    }    let checkpoint = Self::read_configured_checkpoint(        &self.anchor, self.profile, self.source_connected, &self.proof, self.independent_checkpoint,    )?;    let independent = matches!(checkpoint, CheckpointState::Independent(_));    let before = exact_image(locked.0, head)?;    if !before.image.accepts_security(self.security.as_ref()) {        return Err(RecoveryError::AttachmentRejected);    }    match checkpoint {        CheckpointState::Independent(witness) => {            if witness.protected_head != Some(head) {                return Err(RecoveryError::Hold);            }            let floor = witness.head.ok_or(RecoveryError::Hold)?;            validate_lineage(locked.0, head, &before, floor)?;            self.anchor.verify_independent_journal(locked.0, head).map_err(|_| RecoveryError::Hold)?;        }        CheckpointState::Direct => {}    }

After a reopen, an exact retry reads its retained outcome, Busy and Unavailable included, with no new effect.

TextRetained results after reopentests/vectors/persistent-recovery.tsvL10-L12Source (opens the lines on GitHub in a new tab)

Columnsidoperationinputsexpectedevidence

original-signed-debit-retry	retry	actor=7;epoch=9;source=0;receiver=2;asset=17;event=17;amount=5;source_initial=10;exact_original_request=true;cut_advanced=true;reopened=true	reply=retained;new_effects=0;source_balance=5;pending=5	an_admitted_exact_retry_checks_the_retained_result_before_fresh_conditions,prior_call_on_any_route_cannot_repeat_an_authority_effect,source_debit_puts_five_into_pendingcompleted-busy-survives-reopen	retry	actor=7;epoch=9;namespace=7;sequence=1;outcome=busy;anchor_committed=true;reopened=true	reply=busy;new_effects=0	retained_request_and_busy_outcome_survive_fresh_reconstruction,an_admitted_exact_retry_checks_the_retained_result_before_fresh_conditionsadmitted-unavailable-survives-reopen	retry	actor=7;epoch=9;namespace=7;sequence=1;authority_reachable=true;source_connected=false;anchor_committed=true	reply=unavailable;new_effects=0	disconnected_admitted_call_retains_unavailable_without_source_effect,product_round_trip_preserves_the_durable_projection

A cached value is returned on a cold open only when it equals the actual current value; otherwise the read is Busy.

TextCache vectorstests/vectors/persistent-recovery.tsvL16-L17Source (opens the lines on GitHub in a new tab)

Columnsidoperationinputsexpectedevidence

cache-revalidates-on-cold-open	current-read	provider=1;epoch=9;asset=17;query=application_value;same_authority=true;same_cut=true;same_value=true;reopened=true	reply=current;value_preserved=true	a_valid_same_authority_cache_preserves_the_actual_current_response,authenticated_records_only_recovery_is_a_forward_simulationcache-claimed-value-must-be-actual	current-read	provider=1;epoch=9;asset=17;query=application_value;same_authority=true;same_cut=true;same_value=false;reopened=true	reply=busy;forged_value_exposed=false	wrong_cache_value_cannot_be_returned_as_current

Unresolved effects#

NormativeBytes, test vectors and proofs

  • A dispatched source obligation MUST retain its original binding and stay queryable or held until exact source evidence resolves it. Timeout, a missing reply and a mismatched certificate cannot return funds or authorize a fresh source effect. A lawful return consumes an exact bound reversal at the current lock and evidence checks.
  • Secondary work MUST retain the exact committed primary and stable settlement key before release to its sender. Retries and acknowledgements use that key and binding; secondary uncertainty does not resolve the original source obligation.

The recovery probe holds an open source binding while the source is disconnected and queries it when no evidence exists. Only verified evidence for the same binding returns ConsiderConfirmed or ConsiderReversed, and the probe itself records no decision.

RustSource probesrc/authority/recovery.rsL1076-L1105Highlighted lines are discussed in the textSource (opens the lines on GitHub in a new tab)
fn source_probe_at<S: Security>(    product: &mut ProductAuthority<S>,    maintenance: &S::MaintenanceToken,    origin: OriginKey,    evidence: Option<TerminalEvidence>,) -> Result<SourceAction, RecoveryError> {    if product.maintenance(maintenance).is_none() {        return Err(RecoveryError::Endpoint(EndpointError::Rejected));    }    let authority = &product.reads.authority;    let Some(binding) = authority.state.primary.ledger.open_source_binding(origin) else {        return Ok(SourceAction::None);    };    if !authority.connected {        return Ok(SourceAction::Hold(binding));    }    match evidence {        None => Ok(SourceAction::Query(binding)),        Some(evidence)            if evidence.binding == binding                && product.security.verify_recovery_evidence(evidence) =>        {            match evidence.kind {                TerminalKind::Confirmed => Ok(SourceAction::ConsiderConfirmed(binding)),                TerminalKind::Reversed => Ok(SourceAction::ConsiderReversed(binding)),            }        }        Some(_) => Ok(SourceAction::Hold(binding)),    }}

The vectors query a dispatched debit after reopen, hold on a certificate for another event and keep the funds pending without an exact reversal.

TextSource obligation vectorstests/vectors/persistent-recovery.tsvL13-L15Source (opens the lines on GitHub in a new tab)

Columnsidoperationinputsexpectedevidence

dispatched-pending-needs-query	source-probe	source=0;receiver=2;asset=17;event=17;initial=10;pending=5;reopened=true;source_connected=true;evidence=none	next=query_exact;event=17;pending=5;source_balance=5	recovery_queries_the_same_origin_and_holds_mismatched_evidence,recovered_world_keeps_the_actual_pending_reservationwrong-origin-certificate-holds	source-probe	source=0;receiver=2;asset=17;event=17;pending=5;evidence_event=22;reopened=true	next=hold;event=17;pending=5;refund=0	recovery_queries_the_same_origin_and_holds_mismatched_evidence,mismatched_source_evidence_only_holds_the_exact_pending_bindingno-reversal-no-source-return	source-return	source=0;receiver=2;asset=17;event=17;pending=5;source_balance=5;exact_reversal=false;reopened=true	source_balance=5;pending=5;refund=0	missing_or_wrong_reversal_cannot_move_the_source_balance,pending_source_rejects_cancel_and_fence

Client timeout and safe recovery#

NormativeBytes, test vectors and proofs

The caller's wait deadline bounds waiting, while signed delivery TTL (opens in a new tab) bounds admission (opens in a new tab). In the ordered reference profile (opens in a new tab), a successfully admitted preparation may publish later only if its exact authority transaction still permits it. A prior revocation or cancellation defeats publication at that same ordered boundary.

After an uncertain reply, retain the original signed request and use current authorized result access. An exact known outcome returns its original commit. A held or unavailable authority leaves the outcome undetermined; it does not permit resubmitting the same financial work under a different identity.

The caller's wait ends at its own deadline with RequestUnresolved, while the ordered worker keeps its authority to finish.

RustCaller waitsrc/authority/recovery/wire.rsL126-L143Highlighted lines are discussed in the textSource (opens the lines on GitHub in a new tab)
/// Wait for an ordered worker's reply until the caller's monotonic deadline./// The worker keeps its authority to finish. A timed-out or disconnected/// waiter must resolve the original signed request before reissuing the work.pub fn wait_for_ordered_wire_reply<T>(    reply: &Receiver<Result<T, WireError>>,    deadline: Instant,) -> Result<T, WireError> {    let now = Instant::now();    if now >= deadline {        return Err(WireError::RequestUnresolved);    }    let result = reply.recv_timeout(deadline - now)        .map_err(|_| WireError::RequestUnresolved)?;    if Instant::now() >= deadline {        return Err(WireError::RequestUnresolved);    }    result}

In a signed response, a timed-out wait and a held authority become UNRESOLVED (50), an unavailable endpoint becomes DELIVERY_UNAVAILABLE (56), and a lost reply becomes RESPONSE_LOST (51).

RustError to response codesrc/authority/recovery/wire/response.rsL372-L396Highlighted lines are discussed in the textSource (opens the lines on GitHub in a new tab)
fn error(error: WireError) -> Self {    use ResponseCode::*;    Self::plain(match error {        WireError::InvalidFormat => InvalidFormat,        WireError::UnsupportedVersion => UnsupportedVersion,        WireError::InvalidSource => InvalidSource,        WireError::InvalidSignature => InvalidSignature,        WireError::InvalidThresholdSignature => InvalidThresholdSignature,        WireError::TtlInvalid => TtlInvalid,        WireError::ReplayConflict => ReplayConflict,        WireError::ProductRejected        | WireError::Recovery(RecoveryError::Endpoint(EndpointError::Rejected)) => {            ProductRejected        }        WireError::TtlExpired => TtlExpired,        WireError::Cancelled => Cancelled,        WireError::DeadlineExpired => DeadlineExpired,        WireError::ResponseLost => ResponseLost,        WireError::RequestUnresolved => Unresolved,        WireError::Recovery(RecoveryError::Endpoint(EndpointError::Unavailable)) => {            DeliveryUnavailable        }        WireError::Recovery(_) => Unresolved,    })}

In this test the debit is UNRESOLVED before delivery and RESPONSE_LOST after a committed delivery. The exact receipt then returns Debited with its commit position, while a caller with another token receives PRODUCT_REJECTED.

RustLost reply and exact receiptsrc/authority/recovery/wire/vector_tests/ordered_tests.rsL637-L679Highlighted lines are discussed in the textSource (opens the lines on GitHub in a new tab)
#[test]fn reference_response_distinguishes_delivery_and_authentication() {    use response::ResponseCode;    let path = JournalPath::create();    let mut service = memory_endpoint(&path);    let channel = channel(0);    let original = signed(debit_core(2));    let before = service.anchor.current().unwrap().unwrap();    let unresolved = service.resolve_wire_debit_response(&channel, &original, &wire_auth(), false).unwrap();    assert_response(&original, &unresolved, 2, ResponseCode::Unresolved, None);    // A frame whose own signature does not verify receives no signed answer.    let mut bad_signature = original.clone();    *bad_signature.last_mut().unwrap() ^= 1;    assert_eq!(service.receive_wire_response(        &channel, Entry::Adapter, &bad_signature, &wire_auth(), DeliveryWindow::default(),    ), Err(WireError::InvalidSignature));    assert_eq!(        service.resolve_wire_debit_response(&channel, &bad_signature, &wire_auth(), false),        Err(WireError::InvalidSignature),    );    assert_eq!(service.receive_wire_response(        &channel, Entry::Adapter, &[0], &wire_auth(), DeliveryWindow::default(),    ), Err(WireError::InvalidFormat));    assert_eq!(service.anchor.current().unwrap(), Some(before));    let lost = service.receive_wire_response(        &channel, Entry::Adapter, &original, &wire_auth(),        DeliveryWindow { response_lost: true, ..DeliveryWindow::default() },    ).unwrap();    assert_response(&original, &lost, 2, ResponseCode::ResponseLost, None);    let after = service.anchor.current().unwrap().unwrap();    assert_eq!(after.sequence, before.sequence + 1);    let other = &super::super::super::vector_tests::SUCCESSOR;    let refusal = service.resolve_wire_debit_response(        &channel, &original, &WireCredentials { token: other, ..wire_auth() }, false,    ).unwrap();    assert_response(&original, &refusal, 3, ResponseCode::ProductRejected, None);    let receipt = service.resolve_wire_debit_response(&channel, &original, &wire_auth(), false).unwrap();    assert_response(&original, &receipt, 1, ResponseCode::Debited, Some(2));    assert_eq!(service.anchor.current().unwrap(), Some(after));}

Operational assumptions#

The reference's rollback resistance depends on the separately protected witness, archive durability, authenticated transport and configured key custody. Its recovery behavior holds only under those premises. It does not recover unavailable keys, repair arbitrary storage corruption or guarantee that a failed external service will return.


Edition history#

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