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.
- 1
Kind first, then code
The verifier reads
kind,codeandcommit, and apositionfollows only whencommitis 1. A known code must carry its defined kind and commit presence, and an unknown code is classified by its kind alone. - 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
BusyorUnavailable. - 3
Effect unknown
Unresolved,ResponseLostandDeadlineExpireddo 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
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.
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:
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:
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:
/// 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:
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:
/// 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, ¶ms, 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:
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#
| Kind | Commit | Codes and results |
|---|---|---|
| 1 recorded | Absent | 1 Rejected, 2 Busy, 3 Unavailable, 4 exact original Debit closed without effect |
| 1 recorded | Present | 10 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 recorded | Present | 23 DuplicateDecision, 24 DuplicateConfirmed, 25 DuplicateReversed, 26 LegalEmitted, 27 LegalApplied, 28 SpendExecuted |
| 1 recorded | Absent | 30 LegalInvalidAuthority, 31 LegalInvalidState, 32 LegalInvalidBinding, 33 LegalMissingPriorFreeze, 34 LegalMissingPriorSeizure, 35 LegalAppealStillOpen, 36 LegalAppealDeadlineMismatch, 37 LegalMissingDestination, 38 LegalConflictingOrder |
| 2 delivery | Absent | 50 Unresolved, 51 ResponseLost, 52 DeadlineExpired, 53 Cancelled, 54 TtlExpired, 55 DeliveryBusy, 56 DeliveryUnavailable |
| 3 refusal | Absent | 70 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:
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:
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):
{ "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:
/// 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:
{ "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
| Codes | Effect of the original call | How to continue | Closest v0.5 code |
|---|---|---|---|
| 1 Rejected | No 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 Busy | No 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 Unavailable | No effect. The outcome is recorded and consumes the call identifier. | As for Busy. | None |
| 4 | The original Debit was closed without effect. | The original Debit cannot execute later. | None |
| 10 to 28 | The named effect is committed. | The same request returns the same outcome without another effect. | None |
| 30 to 38 | No 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 DeadlineExpired | Unknown. 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 DeliveryUnavailable | No effect. | Send a new signed request. | None |
| 54 TtlExpired | No effect. | Send a new signed request with a new transport UUID and issued time. | MESSAGE_TTL_EXPIRED |
| 70 to 75, 77 | No 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 ReplayConflict | No 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:
#[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),}var ( ErrFormat = errors.New("INVALID_FORMAT") ErrVersion = errors.New("UNSUPPORTED_VERSION") ErrSource = errors.New("INVALID_SOURCE") ErrSignature = errors.New("INVALID_SIGNATURE") ErrThreshold = errors.New("INVALID_THRESHOLD_SIGNATURE") ErrExpired = errors.New("TTL_EXPIRED") ErrTTL = errors.New("TTL_INVALID") ErrReplay = errors.New("REPLAY_CONFLICT"))The reference maps each error to its response code. An endpoint rejection becomes ProductRejected, an unavailable endpoint DeliveryUnavailable, and any other recovery error Unresolved:
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:
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(())}func checkScope(core *wirepb.Core, profile Profile, boundary Boundary, version uint64) error { family, op := Body(core) if profile.Version != version || core.GetVersion() != version || boundary.VersionFloor > version || !slices.Contains(profile.Families, family) || !slices.Contains(profile.Atomicities, core.GetAtomicity()) || (version == 5 && (op.GetOpcode() < 1 || op.GetOpcode() > 2 || (family != 4 && family != 6))) { return ErrVersion } if core.GetPurpose() != 1 || core.GetDeployment() != boundary.Deployment || core.GetSourceDomain() != boundary.Source || core.GetTargetDomain() != boundary.Target || core.GetPrincipal() != boundary.Principal || core.GetKeyId() != boundary.KeyID || core.GetAuthorityEpoch() != boundary.Epoch || core.GetRouteScope() != boundary.RouteScope || (core.GetPriority() == 0 && !boundary.CriticalAllowed) { return ErrSource } sent, ttl := core.GetIssuedSeconds(), max(core.GetTtlSeconds(), boundary.AssetTTLFloor) if ttl > math.MaxUint64-sent || boundary.FutureSkew > math.MaxUint64-boundary.Now || sent > boundary.Now+boundary.FutureSkew { return ErrTTL } if sent+ttl < boundary.Now { return ErrExpired } return nil}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:
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), }}func (store *TransportBindings) bind(key TransportKey, exact []byte) (Arrival, error) { if prior, ok := store.entries[key]; ok { if !bytes.Equal(prior, exact) { return First, ErrReplay } return Identical, nil } if store.entries == nil { store.entries = make(map[TransportKey][]byte) } store.entries[key] = bytes.Clone(exact) return First, nil}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:
/// 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:
/// 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:
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:
{ "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:
{ "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:
/// 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.
Related Pages#
Edition history#
- 0.6.0: Added this edition. The v0.5 specification remains available at its original addresses.
