Protocol Specification v0.6
Cross-chain Protocol
A cross-domain operation retains one original event (opens in a new tab) decision through message retries, endpoint recovery and secondary settlement. A missing reply cannot turn an unresolved source debit into a refund. The source obligation and secondary propagation are separate state machines with separate evidence.
Secondary settlement
A published decision opens at most one job per key; retries keep the key and only an exact acknowledgement completes it.
- 1
Published decision
enqueue_secondaryhands the queue'senqueueonly the origin's primaryPublication, so a committed decision that is not yet published opens no job. The key is the origin and the decision's commit position. - 2
One job, many attempts
dispatchadds one attempt in any state exceptCompleted, so fromInFlightit is another attempt under the same key.failreturns an in-flight job toWaiting, and a second enqueue keeps the existing job. - 3
Exact acknowledgement
acknowledgeneeds the job's key, commitment and target, at least one attempt and a verified signature. A wrong commitment under the right key leaves the jobInFlight. - 4
Primary untouched
A secondary failure or retry cannot change the primary decision, re-open pending source mass or issue a refund.
failleaves the jobWaitingwith the samePublication.
- Start of the job
- Normal path of one job
- Another attempt under the same key
- The key every attempt keeps
- Final state
- No step reaches back to the primary
Schematic: one origin and one target endpoint. Labels name the queue's own steps, which callers reach through enqueue_secondary, dispatch_secondary, fail_secondary and acknowledge_secondary.
Call identity and completion#
NormativeBytes, test vectors and proofs
- An authority MUST bind a call identifier (opens in a new tab) to the authenticated caller namespace and the entire admitted request, including its operation and any lock credential. The transport message identifier and the original event key have separate meanings.
- The authority MUST record the accepted binding, reply and effect in one durable commit. Authenticated calls that complete with rejection, busy or unavailable MUST record that outcome through the same authority.
- Caller authentication, endpoint capability and complete signed-request verification are checked on every submission; their failure is refused without recording an outcome or consuming a fresh call identifier.
- When the authority endpoint is up, an existing call identifier is handled through the retained-result path, including the caller's current result-access authority, before fresh-call checks.
- For a fresh submission, pre-execution proof authentication, any required recovery endorsement, and consistency between the supplied expected cut and the signed request are checked before recording a new outcome; their failure leaves the call identifier unconsumed.
- For a fresh admitted ordinary call, an unavailable authority source records Unavailable before staleness is considered. If that source is available, a signed cut (opens in a new tab) that no longer equals the current cut records Busy. A command-specific grant refusal reached by the authoritative ordinary consumer records rejection.
- A recorded Unavailable outcome is distinct from a transport or storage error that leaves the authoritative outcome undetermined. A client that cannot reach the authority can wait and reconcile, since transport silence cannot establish a completed outcome.
- A retry of the exact admitted call MUST return the existing outcome without executing another effect. Reusing the identifier with different content MUST be rejected. Collection of the existing result still requires the caller's current result-access authority.
- Completion MUST follow the durable authoritative result, the primary publication when the operation requires one, and any release the operation requires. A debit that remains pending retains its obligation and its lock conditions.
Submission order#
The authority endpoint authorizes the caller on every submission. While the endpoint is down it returns EndpointError::Unavailable and records nothing, and a known call identifier goes to its retained result. A fresh call then reaches the cut and proof checks, and for an ordinary call an unavailable authority source is recorded as Unavailable before a stale cut is recorded as Busy.
pub(super) fn execute_durable( &mut self, auth: &Credentials<'_, S>, request: Request<'_>, endorsement: Option<&Credentials<'_, S>>,) -> Result<ActionReply, EndpointError> { let operation = request.action.claim().operation; let identity = self.authorize_call(auth, &request, operation)?; let actor = AuthenticatedCaller(identity.principal); let authority = &self.reads.authority; if !authority.up { return Err(EndpointError::Unavailable); } let primary = &authority.state.primary; if primary.calls.contains_key(&request.id) || primary.regulation.claimed(request.id) { if !self.security.permit( identity, request.target, Operation::Receipt, Scope::Asset(request.action.claim().asset), auth.proof.epoch, ) { return Err(EndpointError::Rejected); } return self.finish_retained(request); } let signed_cut = match request.action { Action::Ordinary(payload) => payload.cut, Action::Legal { payload, .. } => payload.cut, Action::Spend { payload, .. } => payload.cut, }; if request.expected != signed_cut { return Err(EndpointError::Rejected); } self.verify_facts(identity, auth.proof, &request, operation, auth.signature)?; if let Action::Ordinary(payload) = request.action { self.endorse_recovery(endorsement, &request, payload)?; let refusal = if !self.reads.available() { Some(super::OutcomeReply::Unavailable) } else if signed_cut != self.reads.current_cut() { Some(super::OutcomeReply::Busy) } else { None }; if let Some(refusal) = refusal { self.reads.authority.state.primary.calls.insert( request.id, super::StoredCall { payload: payload.clone(), outcome: super::Outcome::plain(refusal), }, ); return self.finish_retained(request); } } self.execute_bound(actor, request)}Exact retries#
A stored call matches a retry only when the whole payload is equal, and it is collected only when its effect is ready.
fn retained_completion(&self, id: CallId, payload: &CallPayload) -> (Collection, bool) { let view = self.state.primary.calls.get(&id).map(|stored| { let matches = stored.payload == *payload; (matches, matches && self.state.primary.ready(stored), stored.outcome) }); let (reply, complete) = retained_completion::retained_completion(view); let reply = match reply { retained_completion::Reply::Waiting => Collection::Waiting, retained_completion::Reply::Rejected => Collection::Rejected, retained_completion::Reply::Collected(outcome) => Collection::Collected(outcome), }; (reply, complete)}The retained decision is one function. When no call is stored, or the stored call matches but its effect is not ready, the reply is waiting; a different payload is rejected, and a ready match is collected.
pub(crate) fn retained_completion<T: Copy>(stored: Option<(bool, bool, T)>) -> (Reply<T>, bool) { let Some((matches, ready, outcome)) = stored else { return (Reply::Waiting, false); }; if !matches { (Reply::Rejected, false) } else if ready { (Reply::Collected(outcome), true) } else { (Reply::Waiting, false) }}The vectors retry one call after recovery, with another message, at another entry, and after Unavailable and Busy outcomes.
Columnsidoperationinputsexpectedevidence
first-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 Completion#
A final credit or refund is ready only when the primary publication exists and the asset's lock no longer belongs to its origin. A publication exists only after the decision is settled and published.
fn ready(&self, stored: &StoredCall) -> bool { let final_effect = matches!( stored.outcome.reply, OutcomeReply::Accepted(Reply::Credited | Reply::Refunded) | OutcomeReply::DuplicateSettlement(_) ); if !final_effect { return true; } let binding = stored.payload.binding; self.publication(binding.origin).is_some() && self.ledger.lock_owner(binding.asset) != Some(binding.origin)}fn publication(&self, origin: OriginKey) -> Option<Publication> { let slot = self.origins.get(&origin)?; slot.published?; slot.settled?; Some(Publication { origin, decision: slot.decision?, })}The completion vectors keep the credit reply waiting while the lock is held or the publication is missing.
Columnsidoperationinputsexpectedevidence
credit-reply-waits-for-lock-release completion caller=7;session=1;authority_epoch=9;origin_epoch=9;source_domain=0;event=17;asset=17;destination_domain=2;recipient=3;initial=10;amount=5;published=true;released=false reply=waiting final_effect_cannot_complete_while_its_lock_is_held credit-reply-waits-for-publication completion caller=7;session=1;authority_epoch=9;origin_epoch=9;source_domain=0;event=17;asset=17;destination_domain=2;recipient=3;initial=10;amount=5;published=false;released=true reply=waiting final_effect_cannot_complete_before_primary_publication credit-reply-after-publication-and-release completion caller=7;session=1;authority_epoch=9;origin_epoch=9;source_domain=0;event=17;asset=17;destination_domain=2;recipient=3;initial=10;amount=5;published=true;released=true reply=credited;completions=1 completed_result_has_a_committed_exact_binding,completion_is_idempotent Original event consumption#
NormativeBytes, test vectors and proofs
- Every terminal entry point MUST use one authoritative decision slot for the original event key, independent of the transport message or route. The slot binds the source event, asset, amount, destination, recipient and origin authority epoch (opens in a new tab).
- An identical binding and decision MUST be recognized as a duplicate. A conflicting binding or decision MUST be rejected before any new financial effect. Updating admissible supporting evidence preserves the committed binding and decision.
- An event has at most one settlement effect. A confirmed decision and a reversed decision for the same original event cannot both take effect.
One origin slot per original event key holds the binding, the origin epoch, the debit commit and at most one decision. A call matches the slot only with the same binding and origin epoch.
#[derive(Clone, Copy, Debug, Eq, PartialEq, serde::Serialize, serde::Deserialize)]struct OriginSlot { binding: Binding, epoch: u64, debit: CommitRef, decision: Option<OriginDecision>, settled: Option<CommitRef>, published: Option<CommitRef>,}impl OriginSlot { fn matches(self, payload: &CallPayload) -> bool { self.binding == payload.binding && self.epoch == payload.origin_epoch } fn matches_decision(self, payload: &CallPayload, kind: TerminalKind) -> bool { self.matches(payload) && self.decision.is_some_and(|decision| decision.kind == kind) }}Origin commands are checked against that slot. A Debit must match an existing slot or, without one, carry the current authority epoch; a terminal record must keep any recorded decision kind; a credit or reconciliation needs a confirmed decision and a refund a reversed one; and publication needs a settled slot. Other commands pass this check.
fn origin_allowed(&self, payload: &CallPayload) -> bool { let slot = self.origins.get(&payload.binding.origin).copied(); match payload.command { Command::Debit { .. } => slot .map_or(payload.origin_epoch == payload.cut.authority_epoch, |old| { old.matches(payload) }), Command::RecordTerminal(evidence) => { evidence.binding == payload.binding && slot.is_some_and(|old| { old.matches(payload) && old .decision .is_none_or(|decision| decision.kind == evidence.kind) }) } Command::Credit { .. } | Command::Reconcile { .. } => { slot.is_some_and(|old| old.matches_decision(payload, TerminalKind::Confirmed)) } Command::Refund { .. } => { slot.is_some_and(|old| old.matches_decision(payload, TerminalKind::Reversed)) } Command::PublishPrimary => { slot.is_some_and(|old| old.matches(payload) && old.settled.is_some()) } _ => true, }}Execution rejects a disallowed origin, and a Reserve, Dispatch or Debit that regulation blocks, before any effect. An identical retry returns its committed duplicate only when the caller's receipt credential matches.
fn execute( &mut self, route: Route, actor: AuthenticatedCaller, payload: &CallPayload,) -> Outcome { let request = payload.request(); if !self.ledger.request_admitted(route, actor, request) { return Outcome::plain(OutcomeReply::Rejected); } if matches!(payload.command, Command::PublishPrimary) && self.regulation.contains_origin(payload.binding.origin) { return self.publish_legal(payload); } if !self.origin_allowed(payload) || (matches!( payload.command, Command::Reserve { .. } | Command::Debit { .. } ) && self.regulation.contains_origin(payload.binding.origin)) || (matches!( payload.command, Command::Dispatch { .. } | Command::Debit { .. } ) && self .regulation .protected(payload.binding.origin.domain, payload.binding.asset)) { return Outcome::plain(OutcomeReply::Rejected); } if let Some(duplicate) = self.duplicate(payload) { return if self.ledger.receipt_credential_matches(actor, request) { duplicate } else { Outcome::plain(OutcomeReply::Rejected) }; }The vectors change the origin epoch, the decision kind and the route of one event. The last row shows that two separate authorities would credit the same event twice.
Columnsidoperationinputsexpectedevidence
same-origin-different-epoch conflicting-origin-epoch caller=7;session=1;authority_epoch=9;origin_epoch=9;source_domain=0;event=17;asset=17;destination_domain=2;recipient=3;initial=10;amount=5;retry_origin_epoch=10 reply=rejected;new_credits=0 same_event_with_a_different_origin_epoch_cannot_change_public_state same-origin-opposite-decision conflicting-terminal-kind caller=7;session=1;authority_epoch=9;origin_epoch=9;source_domain=0;event=17;asset=17;destination_domain=2;recipient=3;initial=10;amount=5;first_kind=confirmed;retry_kind=reversed decision=confirmed;refunds=0 one_origin_slot_cannot_confirm_and_reverse,any_origin_entry_keeps_a_committed_decision,every_exact_product_trace_settles_each_origin_at_most_once same-origin-different-route-and-message reroute-original-credit caller=7;session=1;authority_epoch=9;origin_epoch=9;source_domain=0;event=17;asset=17;destination_domain=2;recipient=3;initial=10;amount=5;first_route=normal;retry_route=adapter;first_message=1;retry_message=2 credits=1;destination=5 product_confirmed_consumer_credits_at_the_bound_entry,every_exact_product_trace_settles_each_origin_at_most_once independent-authority-removal split-authority-control caller=7;session=1;authority_epoch=9;origin_epoch=9;source_domain=0;event=17;asset=17;destination_domain=2;recipient=3;initial=10;amount=5;first_route=normal;second_route=adapter shared_new_credits=1;isolated_new_credits=2 splitting_the_authority_allows_two_new_credit_results,every_exact_product_trace_settles_each_origin_at_most_once Primary publication and secondary settlement#
NormativeBytes, test vectors and proofs
- A committed primary decision MUST remain immutable. Its publishable observation MUST refer to the exact committed decision and its completed financial effect.
- Secondary settlement MUST use a separate queue, keyed by the same route-independent settlement identity on every retry. That identity is derived from the immutable primary commitment and its original event.
- Acknowledgements MUST match the exact queue entry and primary commitment. A wrong acknowledgement cannot complete the entry. Secondary failure or retry cannot change the primary decision, re-open pending source mass, or issue a refund.
The settlement key is derived from the published decision, as its original event and its commit position.
#[derive(Clone, Copy, Debug, Eq, PartialEq, serde::Serialize, serde::Deserialize)]pub struct Publication { pub origin: OriginKey, pub decision: OriginDecision,}impl Publication { pub fn key(self) -> SettlementKey { SettlementKey { origin: self.origin, commit: self.decision.commit, } }}#[derive( Clone, Copy, Debug, Eq, Ord, PartialEq, PartialOrd, serde::Serialize, serde::Deserialize,)]pub struct SettlementKey { pub origin: OriginKey, pub commit: CommitRef,}Only a published primary decision opens a secondary job.
pub fn primary_publication(&self, origin: OriginKey) -> Option<Publication> { self.state.primary.publication(origin)}pub fn enqueue_secondary(&mut self, origin: OriginKey) -> Option<SettlementKey> { let publication = self.primary_publication(origin)?; Some(self.state.secondary.enqueue(publication))}The queue keeps one job per key. A dispatch counts an attempt, and a failure returns the job to waiting under the same key.
fn enqueue(&mut self, commitment: Publication) -> SettlementKey { let key = commitment.key(); self.jobs.entry(key).or_insert(SecondaryJob { commitment, target: self.target, attempts: 0, status: SettlementStatus::Waiting, }); key}fn dispatch(&mut self, key: SettlementKey) -> Option<SecondaryJob> { let job = self.jobs.get_mut(&key)?; if job.status == SettlementStatus::Completed { return None; } job.attempts = job.attempts.checked_add(1)?; job.status = SettlementStatus::InFlight; Some(*job)}fn fail(&mut self, key: SettlementKey) -> bool { let Some(job) = self.jobs.get_mut(&key) else { return false; }; if job.status != SettlementStatus::InFlight { return false; } job.status = SettlementStatus::Waiting; true}An acknowledgement completes the job only when its key, commitment and target all match a job that has been dispatched, and its signature verifies.
fn acknowledge( &mut self, ack: Acknowledgement, verify: impl FnOnce(&Acknowledgement) -> bool,) -> bool { let Some(job) = self.jobs.get_mut(&ack.key) else { return false; }; if ack.key != job.commitment.key() || ack.commitment != job.commitment || ack.target != job.target || ack.target != self.target || job.attempts == 0 || !verify(&ack) { return false; } job.status = SettlementStatus::Completed; true}The queue vectors try an early enqueue, a wrong acknowledgement, a retry and a failed secondary step.
Columnsidoperationinputsexpectedevidence
no-queue-before-primary-publish early-enqueue caller=7;session=1;authority_epoch=9;origin_epoch=9;source_domain=0;event=17;asset=17;destination_domain=2;recipient=3;initial=10;amount=5;target=99;primary_position=0 jobs=0 a_committed_but_unpublished_terminal_cannot_enqueue right-key-wrong-commitment-ack wrong-ack caller=7;session=1;authority_epoch=9;origin_epoch=9;source_domain=0;event=17;asset=17;destination_domain=2;recipient=3;initial=10;amount=5;target=99;primary_position=0;ack_position=1 status=in_flight;primary_unchanged=true wrong_commitment_with_the_right_key_is_rejected,removing_publication_equality_accepts_the_wrong_commitment matching-ack-after-retry retry-ack caller=7;session=1;authority_epoch=9;origin_epoch=9;source_domain=0;event=17;asset=17;destination_domain=2;recipient=3;initial=10;amount=5;target=99;primary_position=0;ack_position=0 attempts=2;status=completed;primary_unchanged=true matching_ack_after_a_retry_completes_without_changing_primary,retry_keeps_the_exact_publication_under_the_same_key secondary-failure-keeps-primary secondary-fail caller=7;session=1;authority_epoch=9;origin_epoch=9;source_domain=0;event=17;asset=17;destination_domain=2;recipient=3;initial=10;amount=5;target=99;primary_position=0 status=waiting;primary_unchanged=true;refunds=0 failed_secondary_step_cannot_change_pending_source_mass,every_product_continuation_keeps_the_same_primary_decision Pending source obligation#
NormativeBytes, test vectors and proofs
A source debit moves the bound amount to pending mass. Exact confirmed source evidence may credit the destination once; exact reversal evidence may return the source once. A deadline expiry, missing acknowledgement or driver failure cannot supply reversal evidence or release pending mass. State Management gives the lock and evidence conditions.
An acknowledgement binds the complete original event, exact primary commitment and queue entry. A changed target, commit or settlement identity cannot close that entry. A publication retry retains the already committed primary outcome; it cannot reopen source mass.
The settlement acknowledgement carries the whole original event and the published decision. Its target must be the sender of the frame.
{ "family": 5, "opcode": 1, "name": "SettlementAcknowledgement", "title": "exact settlement acknowledgement", "summary": "Exact settlement acknowledgement from the secondary target.", "reference_accepts": true, "words": [ {"kind": "word", "name": "origin_domain", "type": "domain_id"}, {"kind": "word", "name": "origin_event", "type": "event_id"}, {"kind": "word", "name": "commit_position", "type": "commit_position", "description": "Commit position of the published decision."}, {"kind": "word", "name": "target", "type": "domain_id", "description": "Secondary target that acknowledges the settlement."}, {"kind": "word", "name": "asset", "type": "asset_id"}, {"kind": "word", "name": "owner", "type": "principal_id"}, {"kind": "word", "name": "destination", "type": "domain_id"}, {"kind": "word", "name": "recipient", "type": "principal_id"}, {"kind": "word", "name": "origin_epoch", "type": "epoch"}, {"kind": "word", "name": "decision_kind", "type": "code", "enum": "decision_kind", "accepted": "all"} ], "blobs": [ {"name": "amount", "type": "amount_u256"} ], "deadline": "absent", "checks": [ {"when": "consumer", "rule": "target equals the Core source_domain.", "error": "INVALID_SOURCE"} ], "call": false, "reference_note": "The reference accepts it only from the configured acknowledger principal of the remote domain."},A retried decision, settlement or publication returns the outcome already committed in the origin slot, at its original commit position.
fn duplicate(&self, payload: &CallPayload) -> Option<Outcome> { let slot = self.origins.get(&payload.binding.origin)?; match payload.command { Command::RecordTerminal(evidence) if self.ledger.evidence(evidence.reference) == Some(evidence) => { Some(Outcome::committed( OutcomeReply::DuplicateDecision, slot.decision?.commit, )) } Command::Credit { reference } | Command::Refund { reference, .. } => { let kind = slot.decision?.kind; let commit = slot.settled?; if self.ledger.evidence(reference) != Some(TerminalEvidence { binding: payload.binding, kind, reference, }) { return None; } Some(Outcome::committed( OutcomeReply::DuplicateSettlement(kind), commit, )) } Command::PublishPrimary => Some(Outcome::committed( OutcomeReply::Accepted(Reply::Published), slot.published?, )), _ => None, }}Ordered acknowledgement and read batches#
NormativeBytes, test vectors and proofs
The ordered schedule accepts canonical v0.6 settlement acknowledgements and current reads up to the host's configured occurrence and swap limits. Each occurrence supplies its own signed frame, negotiated channel and credentials. Labels identify replies. Settlement slots, observed queries and transport UUID keys come from the decoded input.
An unsupported shape or invalid occurrence produces a refusal in its original label without blocking other valid occurrences in the batch.
A proposed exchange requires successful acknowledgements or current observations, independent settlement slots or observations, and distinct transport keys. Shared slots and shared UUID keys are evaluated in their original relative order.
For each accepted exchange, both orders MUST produce the same retained image and the same response at each original label. Supported acknowledgements and reads preserve the signed product cut.
All successful outcomes are rechecked against current identity, wire signature and product authorization after preparation. A changing batch publishes one flushed record with one exact authority CAS. A batch with no image change reads back the exact observed authority and head.
A change to the authority before confirmation withholds the prepared results. Ordered delivery TTL (opens in a new tab) limits admission (opens in a new tab) and does not become a second clock-based condition at publication.
Shapes and classes#
An occurrence has a shape only when it decodes to a settlement acknowledgement or a current read. Its class is the settlement slot or the observed query, and its transport key comes from the decoded Core.
fn shape<S: WireSecurity>( occurrence: &WireOccurrence<'_, S>, security: &S,) -> Result<Candidate, WireError> { if occurrence.channel.profile().version != 6 { return Err(WireError::UnsupportedVersion); } let frame_limit = security.frame_limit(occurrence.channel); check_declared_version(occurrence.bytes, frame_limit)?; let packet = decode_packet(occurrence.bytes, frame_limit)?; let core = packet.core(); if !security.accepts_operation(occurrence.channel, core) { return Err(WireError::UnsupportedVersion); } let class = match decode(core, 1)? { Operands::Ack(ack) => slot(ack.key), Operands::Read(read @ ReadOperand::Current { .. }) => { let epoch = core.authority_epoch.ok_or(WireError::InvalidFormat)?; Class::observation(query_origin(read_at(read, epoch))) } _ => return Err(WireError::InvalidFormat), }; Ok(Candidate { class, transport: TransportKey::for_core(core)?, })}Two classes can be exchanged only when neither writes what the other reads or writes.
fn footprint(self) -> (BTreeSet<Dependency>, BTreeSet<Dependency>) { let common = [ Dependency::Authority, Dependency::Revision, Dependency::CompletionHistory, Dependency::ReadProvider, ]; match self { Self::Observation { origin, publication, } => { let mut reads = BTreeSet::from(common); reads.extend(origin.map(Dependency::Origin)); reads.extend(publication.map(Dependency::Publication)); (reads, BTreeSet::new()) } Self::Slot { key, publication } => { let slot = Dependency::Settlement(key); let mut reads = BTreeSet::from([Dependency::Authority, slot]); reads.extend(publication.map(Dependency::Publication)); (reads, BTreeSet::from([slot])) } Self::Serial => (BTreeSet::from(common), BTreeSet::from(common)), }}pub(super) fn compatible(self, other: Self) -> bool { let (a_read, a_write) = self.footprint(); let (b_read, b_write) = other.footprint(); a_write.is_disjoint(&b_read) && b_write.is_disjoint(&a_read) && a_write.is_disjoint(&b_write)}These vectors exercise the service-action scheduler, which uses the same class footprints. Three independent pairs are exchanged, and three pairs on one settlement slot stay in input order.
Columnsidoperationinputsexpectedevidence
independent-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 Exchanges#
Each proposed swap reruns the batch from the initial image. A different reply or a different image holds the whole batch.
let initial = RetainedImage::capture(product);let mut order: Vec<_> = (0..occurrences.len()).collect();let mut selected = run_order( product, occurrences, &order, &candidates, profile.deployment, input,)?;let baseline_image = RetainedImage::capture(product);let mut exchanges = vec![Exchange::Serialized; adjacent.len()];for (attempt, &index) in adjacent.iter().enumerate() { let (left, right) = (order[index], order[index + 1]); let (Ok(a), Ok(b)) = (candidates[left], candidates[right]) else { continue; }; if !exchangeable(selected.replies[left]) || !exchangeable(selected.replies[right]) || a.transport == b.transport || !a.class.compatible(b.class) { continue; } order.swap(index, index + 1); let mut alternate = initial.clone().open(profile, security.clone(), connected); let exchanged = run_order( &mut alternate, occurrences, &order, &candidates, profile.deployment, input, )?; if selected.replies != exchanged.replies || baseline_image != RetainedImage::capture(&alternate) { return Err(hold()); } *product = alternate; selected = exchanged; exchanges[attempt] = Exchange::Exchanged;}Coordination design context#
DesignCarried from v0.5, without bytes or test vectors in this version
The PREPARE, COMMIT and FINALIZE vocabulary in v0.5 describes architecture-level coordination. This reference edition exposes concrete lock, debit, terminal evidence, publication, receipt and acknowledgement operations. It does not serialize that entire design vocabulary as a new network coordination service. Read the actual family (opens in a new tab)/opcode layouts when building an interoperable participant.
An uncertain acknowledgement#
After primary publication, the secondary sender can retry the stable settlement key. If its response is lost, it dispatches the same entry again rather than allocating another key. Even if the secondary service stays unavailable, the immutable primary remains committed and the original debit cannot be refunded from that uncertainty alone.
Related Pages#
Edition history#
- 0.6.0: Added this edition. The v0.5 specification remains available at its original addresses.
