Protocol Specification v0.6

Error Reference

The response kind determines how a result is interpreted: a recorded result, a delivery result or a validation refusal. The code refines that meaning. Before retrying, determine whether an effect is known, absent or unresolved and whether the original call identifier (opens in a new tab) has been consumed.

Response classes

Only codes 10 to 28 report a committed effect, and only delivery codes 50 to 52 leave the effect unknown.

Only codes 10 to 28 report a committed effect, and only delivery codes 50 to 52 leave the effect unknown. The response words run namespace, sequence, kind, code, commit and position, and the kind word selects one of three classes. Kind 1 is a recorded outcome: codes 10 to 28 come with a commit position and report a committed effect, while codes 1 to 4 and 30 to 38 come without one, change no state and are recorded. Kind 2 is a delivery status: codes 50 to 52 leave the effect pending until the original is resolved, and codes 53 to 56 report no effect. Kind 3 refuses the frame: codes 70 to 77 change no state and nothing is recorded. Response words namespace sequence kind code commit position 1 Kind Code Effect Next step Recorded kind 1 2 Delivery kind 2 Refusal kind 3 10 to 28 commit present 1 to 4, 30 to 38 commit absent 50 to 52 53 to 56 70 to 77 Committed No effect recorded Unknown 3 No effect No effect nothing recorded 4 The same requestreturns this outcome New work needsa new call Resolve the originalbefore reissuing Send a newsigned request Correct the cause,new frame, new UUID
  1. 1

    Kind first, then code

    The verifier reads kind, code and commit, and a position follows only when commit is 1. A known code must carry its defined kind and commit presence, and an unknown code is classified by its kind alone.

  2. 2

    Recorded outcome

    Kind 1 is the outcome retained for the call, and the same request returns it without another effect. New work needs a new call, also after Busy or Unavailable.

  3. 3

    Effect unknown

    Unresolved, ResponseLost and DeadlineExpired do not establish the outcome, and the effect can have been committed. Resolve the original signed request through an exact authorized resolution before reissuing the work.

  4. 4

    Refusal of the frame

    Nothing is recorded and the call identifier is not consumed. For ReplayConflict (76) the earlier request is unchanged, and identical bytes return the retained result.

  • A committed effect
  • Effect pending until the original is resolved
  • Refused: no state change
  • A word present only with a commit position

Schematic: next steps are summarized; Effect and retry by code gives each code's rule.

Figure 1: Response kinds and code ranges, with the effect and next step of each

Reference outbound Response#

NormativeBytes, test vectors and proofs

Response binding#

The reference authority signs an ACK (opens in a new tab) family operation with opcode 2 after its ordinary receive or exact Debit resolution returns. Its words are the original caller namespace, call sequence, kind, result code, and commit presence, followed by the commit position when present. The only blob is the original canonical Core in its original bytes.

The words come from the caller-qualified ID and the code's descriptor, and the commit position is appended only when the code requires one:

RustResponse wordssrc/authority/recovery/wire/response.rsL460-L476Highlighted lines are discussed in the textSource (opens the lines on GitHub in a new tab)
fn response_words(id: CallId, outcome: ResponseResult) -> Result<Vec<u64>, WireError> {    let (kind, needs_commit) = outcome.code.descriptor();    if needs_commit != outcome.commit.is_some() {        return Err(BAD);    }    let mut words = vec![        id.namespace,        id.sequence,        kind,        outcome.code as u64,        u64::from(needs_commit),    ];    if let Some(commit) = outcome.commit {        words.push(u64::try_from(commit.position).map_err(|_| BAD)?);    }    Ok(words)}

The operation has no deadline. The response uses the current configured local issuer, reverses source and target domains, and signs the canonical response Core with the negotiated local Ed25519 (opens in a new tab) key and the envelope signing domain. A response UUID can be reused only for identical response bytes.

The response Core takes its domains from the channel in reverse and carries the original Core as its only blob:

RustResponse Coresrc/authority/recovery/wire/response.rsL487-L520Highlighted lines are discussed in the textSource (opens the lines on GitHub in a new tab)
fn response_core(    channel: &Channel,    params: &ResponseParameters,    atomicity: u64,    words: Vec<u64>,    original: &[u8],) -> Core {    Core {        version: Some(6),        purpose: Some(1),        deployment: Some(channel.deployment()),        source_domain: Some(channel.local()),        target_domain: Some(channel.remote()),        principal: Some(params.principal),        key_id: Some(params.key_id),        authority_epoch: Some(params.authority_epoch),        issued_seconds: Some(params.issued_seconds),        ttl_seconds: Some(params.ttl_seconds),        uuid: Some(params.uuid.to_vec()),        family: Some(5),        primary: Some(1),        secondary: None,        signature_scheme: Some(1),        atomicity: Some(atomicity),        priority: Some(3),        route_scope: Some(params.route_scope),        body: Some(Body::Ack(Operation {            opcode: Some(2),            words,            blobs: vec![original.to_vec()],            appeal_deadline: None,        })),    }}

Verification and correlation#

The Rust reference response verifier uses the caller's retained canonical original and the current peer policy. It checks the signature, exact original Core, caller-qualified ID, kind, code, request compatibility and commit presence.

The boundary's verification key MUST equal the peer key authenticated by the channel; a mismatch receives INVALID_SOURCE. This response path applies the canonical version selector and returns UNSUPPORTED_VERSION for another declared version before canonical decoding. For Debit the commit position is the ledger revision of the original signed cut (opens in a new tab).

admit_response refuses a boundary whose key is not the channel's authenticated peer key, reads the declared version before decoding, admits the signed packet and then correlates it with the retained original:

RustVerify a responsesrc/authority/recovery/wire/response.rsL437-L458Highlighted lines are discussed in the textSource (opens the lines on GitHub in a new tab)
/// Verify the peer signature and correlate a reference response against the/// canonical original retained by the caller. The configured boundary/// supplies the current peer, key and envelope policy.pub fn admit_response(    channel: &Channel,    boundary: &Boundary,    expected: &CanonicalPacket,    response: &[u8],) -> Result<VerifiedResponse, WireError> {    if channel.local() != boundary.target        || channel.remote() != boundary.source        || channel.deployment() != boundary.deployment        || boundary.now > channel.expires()        || boundary.public_key != channel.peer_keys()[1]    {        return Err(WireError::InvalidSource);    }    check_declared_version(response, boundary.max_frame)?;    let response = decode_packet(response, boundary.max_frame)?;    admit_packet(&response, channel.profile(), boundary)?;    validate_response(&response, expected, boundary.max_nesting)}

The correlation compares the embedded original and the caller ID first, then checks the code's kind, its commit presence and its fit with the call:

RustCorrelate a response with the originalsrc/authority/recovery/wire/response.rsL408-L435Highlighted lines are discussed in the textSource (opens the lines on GitHub in a new tab)
fn validate_response(    response: &CanonicalPacket,    expected: &CanonicalPacket,    max_nesting: usize,) -> Result<VerifiedResponse, WireError> {    let Operands::Response {        id,        kind,        code,        commit,        original,    } = decode(response.core(), max_nesting)?    else {        return Err(BAD);    };    let expected_operands = decode(expected.core(), 1)?;    if original != expected.core_bytes() || id != caller_id(&expected_operands)? {        return Err(WireError::ReplayConflict);    }    let code = ResponseStatus::decode(kind, code, commit.is_some())?;    if code        .known()        .is_some_and(|code| !code.matches_call(&expected_operands))    {        return Err(BAD);    }    Ok(VerifiedResponse { id, code, commit })}

Duplicated terminal decisions or settlements use the commit reference in the retained outcome. The reference path handles native calls with a caller-qualified ID. An unidentifiable or noncanonical input produces a transport error without an invented call identifier.

The reference signs a response only for an original whose signature verifies with the channel's peer key. An original with an invalid signature receives an unsigned error and no response.

Response capacity and API scope#

Before executing a request, the reference encodes the largest response shape for that original Core, including a commit position and a 64-byte signature. If this exceeds the configured frame limit, the request receives INVALID_FORMAT before any effect.

The largest shape is a Debited response with a commit position and a 64-byte signature around the original Core:

RustLargest response for an originalsrc/authority/recovery/wire/response.rsL522-L562Highlighted lines are discussed in the textSource (opens the lines on GitHub in a new tab)
/// The length of the largest response that this authority can send for the/// original, measured by encoding a response of the same shape. Every field/// of that shape has a fixed width except the embedded original Core, and the/// commit position is the only optional word, so the measured length bounds/// every response for this original.fn largest_response_len(channel: &Channel, original: &CanonicalPacket) -> Result<usize, WireError> {    let words = response_words(        CallId {            namespace: 0,            sequence: 0,        },        ResponseResult {            code: ResponseCode::Debited,            commit: Some(CommitRef { position: 0 }),        },    )?;    let mut uuid = [0; 16];    uuid[6] = 0x40;    uuid[8] = 0x80;    let params = ResponseParameters {        principal: 0,        key_id: 0,        authority_epoch: 0,        issued_seconds: 0,        ttl_seconds: 0,        uuid,        route_scope: 0,    };    let core = response_core(        channel,        &params,        response_atomicity(channel)?,        words,        original.core_bytes(),    );    Ok(encode_packet(&Packet {        core: Some(core),        signature: Some(vec![0; 64]),    })?    .len())}

response_original runs before the call executes and refuses an original whose largest response would exceed the frame limit:

RustChecks before executionsrc/authority/recovery/wire/response.rsL653-L691Highlighted lines are discussed in the textSource (opens the lines on GitHub in a new tab)
fn response_original(    &self,    channel: &Channel,    bytes: &[u8],) -> Result<(CanonicalPacket, CallId), WireError> {    if channel.profile().version != 6 || !channel.profile().allows(5, 2) {        return Err(WireError::UnsupportedVersion);    }    if self.anchor.ordered_wire_control().is_none()        || self.security.ordered_revocation_profile() != Some(self.profile)        || !self.security.channel_current(channel)        || channel.deployment() != self.profile.deployment    {        return Err(WireError::InvalidSource);    }    let frame_limit = self.security.frame_limit(channel);    check_declared_version(bytes, frame_limit)?;    let original = decode_packet(bytes, frame_limit)?;    let core = original.core();    // The same operation check that runs in front of the admission gate.    super::check_operation(self.security.as_ref(), channel, core)?;    if core.version != Some(channel.profile().version)        || core.deployment != Some(channel.deployment())        || core.source_domain != Some(channel.remote())        || core.target_domain != Some(channel.local())    {        return Err(WireError::InvalidSource);    }    let id = caller_id(&decode(core, 1)?)?;    // The response embeds the original Core. A request whose response    // could not fit the frame limit is refused here, before any effect.    if largest_response_len(channel, &original)? > frame_limit {        return Err(WireError::InvalidFormat);    }    // Every check above reads only the frame and the channel. Nothing is    // signed for a frame whose own signature does not verify.    check_original_signature(channel, &original)?;    Ok((original, id))}

A timeout can be followed by an exact authorized resolution for a delivery status or the durable outcome. Go decodes response operands and their correlation fields but does not implement this signature-checked response-verifier API.

Result kinds and codes#

KindCommitCodes and results
1 recordedAbsent1 Rejected, 2 Busy, 3 Unavailable, 4 exact original Debit closed without effect
1 recordedPresent10 Reserved, 11 SourceDispatched, 12 Debited, 13 Closed, 14 Preempted, 15 TimeObserved, 16 Recorded, 17 Credited, 18 Reconciled, 19 Refunded, 20 DataUpdated, 21 EvidenceRefreshed, 22 Published
1 recordedPresent23 DuplicateDecision, 24 DuplicateConfirmed, 25 DuplicateReversed, 26 LegalEmitted, 27 LegalApplied, 28 SpendExecuted
1 recordedAbsent30 LegalInvalidAuthority, 31 LegalInvalidState, 32 LegalInvalidBinding, 33 LegalMissingPriorFreeze, 34 LegalMissingPriorSeizure, 35 LegalAppealStillOpen, 36 LegalAppealDeadlineMismatch, 37 LegalMissingDestination, 38 LegalConflictingOrder
2 deliveryAbsent50 Unresolved, 51 ResponseLost, 52 DeadlineExpired, 53 Cancelled, 54 TtlExpired, 55 DeliveryBusy, 56 DeliveryUnavailable
3 refusalAbsent70 InvalidFormat, 71 UnsupportedVersion, 72 InvalidSource, 73 InvalidSignature, 74 InvalidThresholdSignature, 75 TtlInvalid, 76 ReplayConflict, 77 ProductRejected

The table is one macro invocation in the reference. Each code carries its kind, its number and whether a commit position is present:

RustResponse codessrc/authority/recovery/wire/response.rsL44-L92Source (opens the lines on GitHub in a new tab)
response_codes! {    Rejected => (1, 1, false),    Busy => (1, 2, false),    Unavailable => (1, 3, false),    OriginalRejected => (1, 4, false),    Reserved => (1, 10, true),    SourceDispatched => (1, 11, true),    Debited => (1, 12, true),    Closed => (1, 13, true),    Preempted => (1, 14, true),    TimeObserved => (1, 15, true),    Recorded => (1, 16, true),    Credited => (1, 17, true),    Reconciled => (1, 18, true),    Refunded => (1, 19, true),    DataUpdated => (1, 20, true),    EvidenceRefreshed => (1, 21, true),    Published => (1, 22, true),    DuplicateDecision => (1, 23, true),    DuplicateConfirmed => (1, 24, true),    DuplicateReversed => (1, 25, true),    LegalEmitted => (1, 26, true),    LegalApplied => (1, 27, true),    SpendExecuted => (1, 28, true),    LegalInvalidAuthority => (1, 30, false),    LegalInvalidState => (1, 31, false),    LegalInvalidBinding => (1, 32, false),    LegalMissingPriorFreeze => (1, 33, false),    LegalMissingPriorSeizure => (1, 34, false),    LegalAppealStillOpen => (1, 35, false),    LegalAppealDeadlineMismatch => (1, 36, false),    LegalMissingDestination => (1, 37, false),    LegalConflictingOrder => (1, 38, false),    Unresolved => (2, 50, false),    ResponseLost => (2, 51, false),    DeadlineExpired => (2, 52, false),    Cancelled => (2, 53, false),    TtlExpired => (2, 54, false),    DeliveryBusy => (2, 55, false),    DeliveryUnavailable => (2, 56, false),    InvalidFormat => (3, 70, false),    UnsupportedVersion => (3, 71, false),    InvalidSource => (3, 72, false),    InvalidSignature => (3, 73, false),    InvalidThresholdSignature => (3, 74, false),    TtlInvalid => (3, 75, false),    ReplayConflict => (3, 76, false),    ProductRejected => (3, 77, false),}

Each ordinary effect code applies to the corresponding command. Closed covers Cancel and Fence. Recorded, EvidenceRefreshed and DuplicateDecision cover RecordTerminal. DuplicateConfirmed covers Credit and DuplicateReversed covers Refund. LegalEmitted and LegalApplied cover their respective stages, and legal codes 30 through 38 cover legal calls.

SpendExecuted covers Spend. Codes 4 and 12 apply only to Debit. Recorded Rejected, Busy and Unavailable apply only when the exact product outcome is retained. Delivery and refusal codes are independent of the native call type.

The verifier checks a known code against the decoded call. Debited and code 4 match only a Debit, while delivery and refusal codes and recorded Rejected, Busy and Unavailable are common to every call:

RustCodes and the calls they answersrc/authority/recovery/wire/response.rsL94-L140Highlighted lines are discussed in the textSource (opens the lines on GitHub in a new tab)
impl ResponseCode {    fn matches_call(self, operands: &Operands) -> bool {        use ResponseCode::*;        match operands {            Operands::Ordinary { payload, .. } => match self {                Reserved => matches!(payload.command, Command::Reserve { .. }),                SourceDispatched => matches!(payload.command, Command::Dispatch { .. }),                Debited | OriginalRejected => matches!(payload.command, Command::Debit { .. }),                Closed => matches!(                    payload.command,                    Command::Cancel { .. } | Command::Fence { .. }                ),                Preempted => matches!(payload.command, Command::Preempt { .. }),                TimeObserved => matches!(payload.command, Command::Timeout { .. }),                Recorded | EvidenceRefreshed | DuplicateDecision => {                    matches!(payload.command, Command::RecordTerminal(_))                }                Credited | DuplicateConfirmed => matches!(payload.command, Command::Credit { .. }),                Reconciled => matches!(payload.command, Command::Reconcile { .. }),                Refunded | DuplicateReversed => matches!(payload.command, Command::Refund { .. }),                DataUpdated => matches!(payload.command, Command::Write { .. }),                Published => matches!(payload.command, Command::PublishPrimary),                _ => self.is_common(),            },            Operands::Legal { stage, .. } => match self {                LegalEmitted => *stage == LegalStage::SourceEmission,                LegalApplied => *stage == LegalStage::ReceivingCommit,                LegalInvalidAuthority                | LegalInvalidState                | LegalInvalidBinding                | LegalMissingPriorFreeze                | LegalMissingPriorSeizure                | LegalAppealStillOpen                | LegalAppealDeadlineMismatch                | LegalMissingDestination                | LegalConflictingOrder => true,                _ => self.is_common(),            },            Operands::Spend { .. } => self == SpendExecuted || self.is_common(),            _ => false,        }    }    fn is_common(self) -> bool {        self.descriptor().0 != 1 || matches!(self, Self::Rejected | Self::Busy | Self::Unavailable)    }}

A client vector answers a retained Debit with Reserved (10) and a commit position, and the verifier ends in InvalidFormat (70):

JSONWrong success codetests/vectors/client-response-cases.jsonL22-L38Highlighted lines are discussed in the textSource (opens the lines on GitHub in a new tab)
{  "id": "semantic-wrong-success-code",  "expected_kind": "debit",  "query_opcode": null,  "query_words": [],  "embedded": "same",  "response_words": [    7,    33,    1,    10,    1,    2  ],  "expect_error": 70,  "expect_unknown": null},

A verifier MUST accept a response code that this version does not define when its kind is 1, 2 or 3 and its commit presence fits the kind. For an unknown code, kind 1 permits either an absent or a present commit position; kinds 2 and 3 require it to be absent. Other kinds receive INVALID_FORMAT.

A known code MUST have exactly the kind and commit presence listed in the table. The verifier accepts an unknown code as its kind only and MUST NOT report it as a result defined by this version.

ResponseStatus keeps an unknown code as its kind only and refuses a commit position on an unknown delivery or refusal code:

RustKnown and unknown codessrc/authority/recovery/wire/response.rsL164-L190Highlighted lines are discussed in the textSource (opens the lines on GitHub in a new tab)
/// The result code of a verified response.#[derive(Clone, Copy, Debug, Eq, PartialEq)]pub enum ResponseStatus {    /// A code of this version, checked against its kind, its commit presence    /// and the original call.    Known(ResponseCode),    /// A code this version does not define, accepted as its category only.    /// The verifier checks the category and commit presence but cannot check    /// the code against the original call, so it never reports such a code    /// as a result of this version.    Unknown { kind: ResponseKind, code: u64 },}impl ResponseStatus {    fn decode(kind: u64, number: u64, commit: bool) -> Result<Self, WireError> {        match ResponseCode::from_number(number) {            Some(code) if code.descriptor() == (kind, commit) => Ok(Self::Known(code)),            Some(_) => Err(BAD),            None => {                let kind = ResponseKind::from_number(kind).ok_or(BAD)?;                if kind != ResponseKind::Recorded && commit {                    return Err(BAD);                }                Ok(Self::Unknown { kind, code: number })            }        }    }

A client vector with the undefined code 999 under kind 1 is accepted as an unknown recorded result:

JSONUnknown code under kind 1tests/vectors/client-response-cases.jsonL353-L371Highlighted lines are discussed in the textSource (opens the lines on GitHub in a new tab)
{  "id": "unknown-category-1",  "expected_kind": "debit",  "query_opcode": null,  "query_words": [],  "embedded": "same",  "response_words": [    7,    33,    1,    999,    0  ],  "expect_error": null,  "expect_unknown": {    "kind": 1,    "number": 999  }},

Effect and retry by code#

NormativeBytes, test vectors and proofs

CodesEffect of the original callHow to continueClosest v0.5 code
1 RejectedNo effect. The outcome is recorded and consumes the call identifier.The same request returns this outcome. New work needs a new call.ACK type REJECTED
2 BusyNo effect, for example because the signed cut was not the current cut. The outcome is recorded and consumes the call identifier.The same request returns Busy. Send a new signed cut with a new call sequence number, transport UUID and signature.None
3 UnavailableNo effect. The outcome is recorded and consumes the call identifier.As for Busy.None
4The original Debit was closed without effect.The original Debit cannot execute later.None
10 to 28The named effect is committed.The same request returns the same outcome without another effect.None
30 to 38No legal effect. The outcome is recorded and consumes the call identifier.New work needs a new call.33 FREEZE_REQUIRED_BEFORE_SEIZE, 34 SEIZE_REQUIRED_BEFORE_CONFISCATE, 35 APPEAL_PERIOD_NOT_EXPIRED, 31 INVALID_STATE_TRANSITION
50 Unresolved, 51 ResponseLost, 52 DeadlineExpiredUnknown. The effect can have been committed.Resolve the original signed request through an exact authorized resolution before reissuing the work.DELIVERY_TIMEOUT for 50 and 52
53 Cancelled, 55 DeliveryBusy, 56 DeliveryUnavailableNo effect.Send a new signed request.None
54 TtlExpiredNo effect.Send a new signed request with a new transport UUID and issued time.MESSAGE_TTL_EXPIRED
70 to 75, 77No effect. Nothing is recorded, and the call identifier is not consumed.Correct the cause and send a new signed frame with a new transport UUID.70 INVALID_HEADER_FORMAT, 71 UNSUPPORTED_VERSION, 72 INVALID_SOURCE_AUTHENTICATION, 73 INVALID_SIGNATURE, 74 INVALID_THRESHOLD_SIGNATURE
76 ReplayConflictNo effect. The transport UUID is already bound to different bytes, and the earlier request is unchanged.Use a new transport UUID. Sending the identical bytes returns the retained result.DEDUPLICATION_DETECTED

Error values and where they are raised#

Both languages have one error value for each refusal code from 70 to 76 and one for an expired TTL (opens in a new tab), which the reference reports as delivery code 54. The Rust reference adds the delivery, product and recovery errors of its persistent authority:

Error valuessrc/wire/codec.rsL6-L22Source (opens the lines on GitHub in a new tab)
#[derive(Clone, Copy, Debug, Eq, PartialEq)]pub enum WireError {    InvalidFormat,    UnsupportedVersion,    InvalidSource,    InvalidSignature,    InvalidThresholdSignature,    TtlExpired,    TtlInvalid,    ReplayConflict,    RequestUnresolved,    Cancelled,    DeadlineExpired,    ResponseLost,    ProductRejected,    Recovery(crate::authority::recovery::RecoveryError),}

The reference maps each error to its response code. An endpoint rejection becomes ProductRejected, an unavailable endpoint DeliveryUnavailable, and any other recovery error Unresolved:

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,    })}

The admission (opens in a new tab) scope check raises the version, source and TTL refusals in the same order in both languages:

Scope checksrc/wire/admission.rsL35-L79Highlighted lines are discussed in the textSource (opens the lines on GitHub in a new tab)
pub(super) fn check_versioned_scope(    core: &Core,    profile: &Profile,    boundary: &Boundary,    version: u64,) -> Result<()> {    if profile.version != version        || core.version != Some(version)        || boundary.version_floor > version        || !body(core).is_ok_and(|(family, op)| {            op.opcode                .is_some_and(|opcode| profile.allows(family, opcode))        })        || !core            .atomicity            .is_some_and(|mode| profile.atomicities.contains(&mode))    {        return Err(WireError::UnsupportedVersion);    }    if core.purpose != Some(1)        || core.deployment != Some(boundary.deployment)        || core.source_domain != Some(boundary.source)        || core.target_domain != Some(boundary.target)        || core.principal != Some(boundary.principal)        || core.key_id != Some(boundary.key_id)        || core.authority_epoch != Some(boundary.epoch)        || core.route_scope != Some(boundary.route_scope)        || (core.priority == Some(0) && !boundary.critical_allowed)    {        return Err(WireError::InvalidSource);    }    let sent = core.issued_seconds.ok_or(WireError::InvalidFormat)?;    let expiry = wire_expiry(core, boundary)?;    let latest = boundary        .now        .checked_add(boundary.future_skew)        .ok_or(WireError::TtlInvalid)?;    if sent > latest {        return Err(WireError::TtlInvalid);    }    if expiry < boundary.now {        return Err(WireError::TtlExpired);    }    Ok(())}

A transport key that is already bound to different bytes raises the replay conflict, and identical bytes arrive as a repeat of the retained request:

Replay conflictsrc/wire/admission.rsL145-L151Highlighted lines are discussed in the textSource (opens the lines on GitHub in a new tab)
pub fn compare_binding(retained: Option<&Vec<u8>>, exact: &[u8]) -> Result<Arrival> {    match retained {        None => Ok(Arrival::First),        Some(bytes) if bytes == exact => Ok(Arrival::Identical),        Some(_) => Err(WireError::ReplayConflict),    }}

Unsigned refusal and signed result#

NormativeBytes, test vectors and proofs

A receiver MUST NOT sign a response to a frame whose own signature it has not verified. Such an error is returned to the local host unsigned. The ordinary request-admission path has the disclosure limitation described in Conformance; a failed signature must never be converted into an authenticated refusal for a different original.

The signature check reads only the frame and the channel key, and a frame that fails it receives an unsigned error:

RustSignature check before any signed answersrc/authority/recovery/wire/response.rsL564-L584Highlighted lines are discussed in the textSource (opens the lines on GitHub in a new tab)
/// Verify the signature of a received original with the peer key that the/// channel authenticated, before any answer about the frame is signed. A frame/// that fails here receives only an unsigned error. The check reads the frame/// and the channel key, and no registration of the principal, key or epoch/// that the frame names, so the error cannot reveal that registration.fn check_original_signature(channel: &Channel, original: &CanonicalPacket) -> Result<(), WireError> {    let scheme = original.core().signature_scheme.ok_or(BAD)?;    if scheme == 2 {        return Err(WireError::InvalidThresholdSignature);    }    if !channel.profile().schemes.contains(&scheme)        || !verify(            &channel.peer_keys()[1],            &original.signing_bytes()?,            original.signature(),        )    {        return Err(WireError::InvalidSignature);    }    Ok(())}

Only after response_original succeeds does the authority execute the call and sign its outcome, a refusal included:

RustSigned response after executionsrc/authority/recovery/wire/response.rsL693-L727Highlighted lines are discussed in the textSource (opens the lines on GitHub in a new tab)
/// Execute the signed call and return its correlated response after the/// existing authority transaction returns. Only call frames with a/// caller-qualified ID have a reference ACK Response.pub fn receive_wire_response(    &mut self,    channel: &Channel,    entry: Entry,    bytes: &[u8],    credentials: &WireCredentials<'_, S>,    delivery: DeliveryWindow,) -> Result<Vec<u8>, WireError> {    #[cfg(test)] let _source_scope=source_trace::begin(bytes);    let (original, id) = self.response_original(channel, bytes)?;    let security = self.security.clone();    let signer = security.response_signer().ok_or(WireError::InvalidSource)?;    let outcome =        match self.receive_wire_result(channel, entry, bytes, credentials, delivery, true) {            Ok((received, retained)) => {                #[cfg(test)] source_trace::received(&received,retained);                ResponseResult::from_received(received, retained)            },            Err(error) => {                #[cfg(test)] source_trace::wire_error(&error);                ResponseResult::error(error)            },        };    encode_response(        channel,        signer,        &original,        id,        outcome,        security.frame_limit(channel),    )}

The Rust signed-Response APIs require an Ordinary, Legal or Spend call with a caller-qualified call identifier. QUERY (opens in a new tab) operations 1 through 8 use the typed read/receipt path of receive_wire.

Only these three call kinds have a caller-qualified ID, and any other decoded operation is InvalidFormat on this path:

RustCaller-qualified IDsrc/authority/recovery/wire/response.rsL399-L406Source (opens the lines on GitHub in a new tab)
fn caller_id(operands: &Operands) -> Result<CallId, WireError> {    match operands {        Operands::Ordinary { id, .. } | Operands::Legal { id, .. } | Operands::Spend { id, .. } => {            Ok(*id)        }        _ => Err(BAD),    }}

If earlier checks succeed and, when verifying a response, its embedded original bytes equal the retained expected Core, a query original passed to a signed-response entry point ends with a local InvalidFormat error; the API creates no signed refusal packet.

After the expected Core decodes, an embedded/expected original-byte mismatch takes ReplayConflict precedence over caller-ID selection. Recursive parsing of a response around a nested receipt retains the receipt wrapper as its original.

Two client vectors carry the same refusal words for a retained query original with opcode 1. With the query embedded the verifier ends in InvalidFormat (70); with a Debit embedded instead, the byte mismatch ends in ReplayConflict (76). The vectors for opcodes 2 to 7 give the same two results:

JSONQuery originalstests/vectors/client-response-cases.jsonL39-L80Highlighted lines are discussed in the textSource (opens the lines on GitHub in a new tab)
{  "id": "query-1-same",  "expected_kind": "query",  "query_opcode": 1,  "query_words": [    2,    0,    1,    17  ],  "embedded": "same",  "response_words": [    7,    33,    3,    70,    0  ],  "expect_error": 70,  "expect_unknown": null},{  "id": "query-1-mismatch",  "expected_kind": "query",  "query_opcode": 1,  "query_words": [    2,    0,    1,    17  ],  "embedded": "debit",  "response_words": [    7,    33,    3,    70,    0  ],  "expect_error": 76,  "expect_unknown": null},

An exact receipt query (opcode 8) nests its original, so the retained query does not decode at the nesting of 1 that the verifier uses for it. Its mismatch vector therefore ends in InvalidFormat (70) before the bytes are compared:

JSONExact receipt querytests/vectors/client-response-cases.jsonL335-L352Highlighted lines are discussed in the textSource (opens the lines on GitHub in a new tab)
{  "id": "query-8-mismatch",  "expected_kind": "query",  "query_opcode": 8,  "query_words": [    2  ],  "embedded": "debit",  "response_words": [    7,    33,    3,    70,    0  ],  "expect_error": 70,  "expect_unknown": null},

Retry decisions#

Classify the result before retrying:

  • Known recorded outcome: collect the exact retained result.
  • Recorded Busy or Unavailable: a new cut, call sequence, UUID and signature.
  • Unresolved delivery (50 to 52): resolve the original before reissuing the work.
  • No-effect delivery status (53 to 56): send a new signed request, with a new transport UUID and issued time after TtlExpired (54).
  • No-effect refusal: correct the cause and follow its per-code retry rule.

A known code must carry its defined kind and commit presence. A code this edition does not know is classified by its kind word alone, as one of these three classes:

RustResponse kindssrc/authority/recovery/wire/response.rsL142-L151Source (opens the lines on GitHub in a new tab)
/// The category that the kind word of a response names.#[derive(Clone, Copy, Debug, Eq, PartialEq)]pub enum ResponseKind {    /// The exact product outcome retained for the original call.    Recorded = 1,    /// A delivery status. It does not establish the outcome of the call.    Delivery = 2,    /// A refusal of the original frame.    Refusal = 3,}

The transport UUID and durable call identifier remain separate. Reusing a UUID for different signed bytes creates a replay conflict. Allocating a fresh UUID alone does not undo a recorded result or authorize the same original event (opens in a new tab) to settle twice.


Edition history#

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