Protocol Specification v0.6

Message Protocol

Each message has a family discriminator (opens in a new tab) and an opcode. The family selects exactly one body field at tag 15 + family; the opcode selects its positional layout. The seven family numbers are retained from v0.5, while some family meanings and operation payloads have changed.

Family and opcode

The family selects the body field and the opcode the operand words; the reference set separately decides what runs.

The family selects the body field and the opcode the operand words; the reference set separately decides what runs. Family 3, LOCK_MANAGEMENT, selects body field 18, lock_management, because the body tag is 15 plus the family; a body at any other tag is INVALID_FORMAT (70). Its opcode must be 1 to 8, otherwise the canonical Core decode refuses it with INVALID_FORMAT (70). check_operation passes the reference set, opcodes 1 to 6, and refuses 7, Write, and 8, Timeout, with UNSUPPORTED_VERSION (71) before the signature check. After signature and admission each layout starts with the call_prefix, credential and version_vector groups, followed by the opcode's own words: duration for Reserve and Preempt, generation for Dispatch, Cancel and Fence, and generation then evidence_reference for Reconcile. Core tag 12, varint family 3 LOCK_MANAGEMENT tag = 15 + family tag 18, bytes lock_management tags 16, 17, 19 to 22 a body at another tag INVALID_FORMAT (70) 1 Opcode 2 Reference set 3 Operand words 4 check_operation signature, admission call_prefix, credential, version_vector 1 Reserve duration 2 Dispatch generation 3 Cancel generation 4 Fence generation 5 Preempt duration 6 Reconcile generation evidence_reference 7 Write UNSUPPORTED_VERSION (71) 8 Timeout UNSUPPORTED_VERSION (71) other opcode INVALID_FORMAT (70)
  1. 1

    Body field

    family 3 is LOCK_MANAGEMENT, so the body must be field 18, lock_management. A body at another tag, or a second body, is INVALID_FORMAT (70).

  2. 2

    Known opcode

    The family has opcodes 1 to 8. The canonical Core decode refuses any other opcode with INVALID_FORMAT (70).

  3. 3

    Reference set

    check_operation passes only the reference set, opcodes 1 to 6 of this family. Write and Timeout are defined operations, and it refuses both with UNSUPPORTED_VERSION (71) before the signature check.

  4. 4

    Operand words

    After signature and admission, every family 3 layout starts with call_prefix, an optional credential and a counted version_vector, so its length varies. The opcode fixes the words after them, and words that do not fit are INVALID_FORMAT (70).

  • Selected by the family or the opcode
  • Path of a family 3 message
  • Passes the reference set check
  • Refused, with its response code
  • Not a body or opcode of family 3

Schematic: blobs are not drawn and cell widths are not sizes. Each opcode word is one fixed64; the three leading groups take a variable number of words.

Figure 1: Body field, operand layout and reference operation set for a lock management message

Families and operations#

NormativeBytes, test vectors and proofs

STATE_SYNC carries settlement execution commands in this edition. It is not the general asset-state message of v0.5. GOVERNANCE carries owner handover and authority epoch (opens in a new tab) installation layouts, not general routing-table or Registry edits.

FamilyOpcodeOperationReference delivery
1 STATE_SYNC1DebitAccepted
1 STATE_SYNC2RecordTerminalAccepted
1 STATE_SYNC3DeliverCreditAccepted
1 STATE_SYNC4RefundAccepted
1 STATE_SYNC5PublishPrimaryAccepted
1 STATE_SYNC6OrdinarySpendAccepted
2 REGULATORY_ACTION1EmitLegalOrderAccepted
2 REGULATORY_ACTION2CommitLegalOrderAccepted
2 REGULATORY_ACTION3EnforcementSpendAccepted
3 LOCK_MANAGEMENT1ReserveAccepted
3 LOCK_MANAGEMENT2DispatchAccepted
3 LOCK_MANAGEMENT3CancelAccepted
3 LOCK_MANAGEMENT4FenceAccepted
3 LOCK_MANAGEMENT5PreemptAccepted
3 LOCK_MANAGEMENT6ReconcileAccepted
3 LOCK_MANAGEMENT7WriteUNSUPPORTED_VERSION (71)
3 LOCK_MANAGEMENT8TimeoutUNSUPPORTED_VERSION (71)
4 QUERY1CurrentReadAccepted
4 QUERY2HistoricalReadAccepted
4 QUERY3RawReadAccepted
4 QUERY4OperationStatusAccepted
4 QUERY5JournalAccepted
4 QUERY6CaptureCacheAccepted
4 QUERY7OwnerReadAccepted
4 QUERY8ExactReceiptAccepted
5 ACK1SettlementAcknowledgementAccepted
5 ACK2CorrelatedResponseUNSUPPORTED_VERSION (71)
6 HEARTBEAT1CapabilitiesUNSUPPORTED_VERSION (71)
6 HEARTBEAT2LivenessUNSUPPORTED_VERSION (71)
7 GOVERNANCE1OwnerHandoverAccepted
7 GOVERNANCE2InstallEpochUNSUPPORTED_VERSION (71)

The reference profile (opens in a new tab)'s acceptance column applies to inbound deliveries. An outbound correlated response can be defined by the protocol and still be unsupported as a request to that profile. Decoding a known operation and accepting it for execution are separate decisions.

Family and opcode dispatch#

The operation table names the body field of each family, and the tag of that field is 15 plus the family code.

JSONFamily codes and body fieldsspec/operations.jsonL61-L72Highlighted lines are discussed in the textSource (opens the lines on GitHub in a new tab)
"family": {  "description": "Core family (tag 12). The Core body field of a family has tag 15 plus the family code.",  "values": [    {"code": 1, "name": "STATE_SYNC", "body_field": "state_sync"},    {"code": 2, "name": "REGULATORY_ACTION", "body_field": "regulatory_action"},    {"code": 3, "name": "LOCK_MANAGEMENT", "body_field": "lock_management"},    {"code": 4, "name": "QUERY", "body_field": "query"},    {"code": 5, "name": "ACK", "body_field": "ack"},    {"code": 6, "name": "HEARTBEAT", "body_field": "heartbeat"},    {"code": 7, "name": "GOVERNANCE", "body_field": "governance"}  ]},

Each family and opcode pair has exactly one layout. Both decoders send the two spend operations to one spend decoder, the other STATE_SYNC (opens in a new tab) operations and every LOCK_MANAGEMENT (opens in a new tab) operation to the ordinary decoder, and each remaining family to its own decoder.

Operand decoder by family and opcodesrc/wire/operands.rsL793-L808Highlighted lines are discussed in the textSource (opens the lines on GitHub in a new tab)
pub fn decode(core: &Core, max_nesting: usize) -> Result<Operands> {    if max_nesting == 0 {        return Err(BAD);    }    let (family, op) = body(core)?;    match (family, op.opcode) {        (1, Some(6)) | (2, Some(3)) => spend(core, family, op),        (1 | 3, _) => ordinary(core, family, op),        (2, _) => legal(core, op),        (4, _) => read(op, max_nesting),        (5, _) => ack(core, op, max_nesting),        (6, _) => heartbeat(core, op),        (7, _) => governance(core, op),        _ => Err(BAD),    }}

The two spend layouts differ. Under REGULATORY_ACTION (opens in a new tab) the spend decoder also reads the enforcement order words, a non-empty third blob with the proof and the appeal deadline (opens in a new tab); under STATE_SYNC it requires exactly two blobs and no deadline.

Spend layouts by familysrc/wire/operands.rsL534-L558Highlighted lines are discussed in the textSource (opens the lines on GitHub in a new tab)
let (authorization, proof) = if family == 2 {    let [_, _, proof] = op.blobs.as_slice() else {        return Err(BAD);    };    if proof.is_empty() {        return Err(BAD);    }    (        Some(EnforcementOrder {            scope,            officer: words.word()?,            jurisdiction: words.word()?,            legal_basis: words.word()?,            assets: words.set()?,            prior_seizure: words.optional_origin()?,            appeal_deadline: deadline(op),        }),        Some(proof.clone()),    )} else {    if op.blobs.len() != 2 || op.appeal_deadline.is_some() {        return Err(BAD);    }    (None, None)};

Both encoders hold the opcode count of each family. Decoding encodes every Core again and compares the bytes, so an opcode outside 1 through that count is refused as INVALID_FORMAT.

Opcodes per familysrc/wire/codec.rsL121-L126Highlighted lines are discussed in the textSource (opens the lines on GitHub in a new tab)
fn opcode_allowed(family: u64, opcode: u64) -> bool {    [6, 3, 8, 8, 2, 2, 2]        .get((family.saturating_sub(1)) as usize)        .is_some_and(|last| (1..=*last).contains(&opcode))        && family != 0}

Reference operation set#

Both implementations write the reference set as one opcode range per family, and both ranges give the same 25 operations.

Operations the reference executessrc/authority/recovery/wire/ed25519.rsL455-L465Source (opens the lines on GitHub in a new tab)
fn supported_frame(core: &Core) -> bool {    match core.body.as_ref() {        Some(Body::StateSync(op)) => matches!(op.opcode, Some(1..=6)),        Some(Body::RegulatoryAction(op)) => matches!(op.opcode, Some(1..=3)),        Some(Body::Ack(op)) => op.opcode == Some(1),        Some(Body::Query(op)) => matches!(op.opcode, Some(1..=8)),        Some(Body::LockManagement(op)) => matches!(op.opcode, Some(1..=6)),        Some(Body::Governance(op)) => op.opcode == Some(1),        _ => false,    }}

In the Rust authority entry paths, check_operation runs right after canonical decoding and before the signature check, and Go ReceiveAccepting runs its configured check before AdmitPacket. Both refuse an operation outside the set as UNSUPPORTED_VERSION and decode operands only after admission (opens in a new tab).

Configured operation checksrc/authority/recovery/wire.rsL601-L631Highlighted lines are discussed in the textSource (opens the lines on GitHub in a new tab)
let frame_limit = product.security.frame_limit(channel);if bytes.len() > frame_limit {    return Err(WireError::InvalidFormat);}let input = match channel.profile().version {    6 => {        check_declared_version(bytes, frame_limit)?;        Input::Modern(Box::new(decode_packet(bytes, frame_limit)?))    }    5 if product.security.legacy_configured(channel) => Input::Legacy {        bytes,        core: Box::new(legacy::decode_shape(bytes, frame_limit)?),    },    _ => return Err(WireError::UnsupportedVersion),};check_operation(product.security.as_ref(), channel, input.core())?;if !product.security.matches_signed_packet(bytes, credentials.signature) {    return Err(WireError::InvalidSignature);}let core = input.core();let boundary = boundary_for(product.security.as_ref(), channel, core, deployment, cut)?;if bytes.len() > boundary.max_frame {    return Err(WireError::InvalidFormat);}gate_delivery(delivery, boundary.now, ordered.is_none())?;if ordered.is_none() && product.security.delivery_cancelled(core) {    return Err(WireError::Cancelled);}let legacy = input.verify(channel, &boundary)?;check_delivery_ttl(product.security.as_ref(), channel, core)?;let operands = decode(core, boundary.max_nesting)?;

A test walks all 31 operations through the reference profile and counts the accepted ones.

Rust25 of 31 operations acceptedsrc/authority/recovery/wire/ed25519/vector_tests.rsL320-L343Highlighted lines are discussed in the textSource (opens the lines on GitHub in a new tab)
#[test]fn reference_ed25519_profile_accepts_only_the_operations_it_executes() {    use crate::authority::recovery::wire::vector_tests::{channel, core_with};    let security = Ed25519Security::new(config(), key(6), FixedClock(105)).unwrap();    let channel = channel(0);    let refused = BTreeSet::from([(3, 7), (3, 8), (5, 2), (6, 1), (6, 2), (7, 2)]);    let mut accepted = 0;    for (family, count) in (1..=7).zip([6, 3, 8, 8, 2, 2, 2]) {        for opcode in 1..=count {            let core = core_with(family, opcode, 0, 1, Vec::new(), Vec::new());            let accepts = security.accepts_operation(&channel, &core);            assert_eq!(accepts, !refused.contains(&(family, opcode)), "{family}/{opcode}");            accepted += usize::from(accepts);        }    }    assert_eq!(accepted, 25);    assert_eq!(security.max_delivery_ttl(&channel), None);    let limited = Ed25519Profile {        max_delivery_ttl_seconds: Some(30),        ..config()    };    let limited = Ed25519Security::new(limited, key(6), FixedClock(105)).unwrap();    assert_eq!(limited.max_delivery_ttl(&channel), Some(30));}

Immutable request and response#

NormativeBytes, test vectors and proofs

A Packet authenticates its complete canonical Core. The header's principal, target and epoch supply the caller and current authority; an operand cannot substitute another authenticated identity. Execution credentials come from host adapters and are not extra wire fields.

An exact receipt query carries the immutable original Core as nested canonical data. It does not re-admit that original as a delivery. A response binds the full original request and caller, the result kind and code, and the commit position when the result code requires one. Error Reference states which outcomes may be retried safely.

The exact receipt query has one session word, and its only blob is the original call.

JSONExactReceipt entryspec/operations.jsonL732-L749Highlighted lines are discussed in the textSource (opens the lines on GitHub in a new tab)
{  "family": 4, "opcode": 8, "name": "ExactReceipt", "title": "exact receipt",  "summary": "Receipt for an exact original call.",  "reference_accepts": true,  "words": [    {"kind": "word", "name": "session", "type": "session_id"}  ],  "blobs": [    {"name": "original_core", "type": "canonical_core", "description": "Complete immutable original call."}  ],  "deadline": "absent",  "checks": [    {"when": "decode", "rule": "The nesting limit is at least 2, which leaves one level for original_core.", "error": "INVALID_FORMAT"},    {"when": "decode", "rule": "original_core decodes as an ordinary, legal or spend call.", "error": "INVALID_FORMAT"},    {"when": "consumer", "rule": "The original call has the deployment, source domain, target domain, principal and authority epoch of this Core and version 6.", "error": "INVALID_SOURCE"}  ],  "call": false},

The correlated response carries the caller's call identifier (opens in a new tab), the result kind and code, an optional commit position and the original bytes. Each check in the entry names the refusal it returns.

JSONCorrelatedResponse entryspec/operations.jsonL776-L805Highlighted lines are discussed in the textSource (opens the lines on GitHub in a new tab)
{  "family": 5, "opcode": 2, "name": "CorrelatedResponse", "title": "correlated response",  "summary": "Signed response correlated with an original call.",  "reference_accepts": false,  "words": [    {"kind": "word", "name": "caller_namespace", "type": "principal_id"},    {"kind": "word", "name": "call_sequence", "type": "call_sequence"},    {"kind": "word", "name": "kind", "type": "code", "enum": "response_kind", "accepted": "all"},    {"kind": "word", "name": "code", "type": "code", "enum": "response_code", "accepted": "any", "description": "The response verifier checks the code against the original call."},    {"kind": "optional", "name": "commit", "fields": [      {"kind": "word", "name": "position", "type": "commit_position"}    ]}  ],  "blobs": [    {"name": "original_core", "type": "canonical_core", "description": "Canonical Core of the original call in its original bytes."}  ],  "deadline": "absent",  "checks": [    {"when": "decode", "rule": "The nesting limit is at least 2, which leaves one level for original_core.", "error": "INVALID_FORMAT"},    {"when": "decode", "rule": "A commit is present only when kind is RECORDED.", "error": "INVALID_FORMAT"},    {"when": "decode", "rule": "The original has the version and deployment of this Core, its principal equals caller_namespace, and its source and target domains are this Core's target and source domains.", "error": "INVALID_SOURCE"},    {"when": "decode", "rule": "The original is not a correlated response.", "error": "INVALID_FORMAT"},    {"when": "decode", "rule": "An original call has the call identifier (caller_namespace, call_sequence). When it is a Debit, a present commit position equals its revision.", "error": "INVALID_FORMAT"},    {"when": "decode", "rule": "An original exact receipt query carries a call with the deployment, source domain, target domain, principal and authority epoch of that query and version 6.", "error": "INVALID_SOURCE"},    {"when": "decode", "rule": "An original exact receipt query carries a call with the call identifier (caller_namespace, call_sequence). When that call is a Debit, a present commit position equals its revision.", "error": "INVALID_FORMAT"},    {"when": "consumer", "rule": "original_core equals the caller's retained canonical request and the call identifier equals that request's call identifier.", "error": "REPLAY_CONFLICT"}  ],  "call": false,  "reference_note": "The reference authority signs it after the original call returns. Callers verify it with the reference response verifier. The reference authority does not accept it as a request."},

Both decoders accept a response only when the original was sent by the named caller in the opposite direction, with the same version and deployment. They then match the nested call identifier, and a commit position given for a Debit must equal its revision.

Response operandssrc/wire/operands.rsL616-L654Highlighted lines are discussed in the textSource (opens the lines on GitHub in a new tab)
if nested.version != core.version    || nested.deployment != core.deployment    || nested.principal != Some(id.namespace)    || nested.source_domain != core.target_domain    || nested.target_domain != core.source_domain{    return Err(WireError::InvalidSource);}let matches_debit_revision =    |revision| commit.is_none_or(|reference| reference.position == revision);match decoded {    Operands::Ordinary { id: nested, payload, .. } => {        if nested != id || (matches!(payload.command, Command::Debit { .. })            && !matches_debit_revision(payload.cut.revision))        {            return Err(BAD);        }    }    Operands::Legal { id: nested, .. }    | Operands::Spend { id: nested, .. }        if nested != id =>    {        return Err(BAD);    }    Operands::Receipt { original, .. } => {        if !same_original(&nested, &original) {            return Err(WireError::InvalidSource);        }        let (family, operation) = body(&original)?;        let (nested_id, payload, _) = native(&original, operation)?;        if nested_id != id || (family == 1 && operation.opcode == Some(1)            && !matches_debit_revision(payload.cut.revision))        {            return Err(BAD);        }    }    Operands::Response { .. } => return Err(BAD),    _ => (),}

The Rust response verifier then compares the response with the request the caller retained. A different original or call identifier is a REPLAY_CONFLICT, and a known code for another call type is INVALID_FORMAT. Go has no counterpart of this check.

RustResponse correlationsrc/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 })}

Version coexistence#

NormativeBytes, test vectors and proofs

  • Peers select a channel profile from two signed offers and a signed confirmation. These use binary signing inputs separate from the envelope.
  • An offer is ASCII OIP, NUL, ASCII HEARTBEAT-OFFER, NUL, then the deployment, sender, receiver, challenge and expiry as 8-byte big-endian integers, then the version, family, atomicity (opens in a new tab) and scheme sets.
  • Each set is an 8-byte big-endian count followed by its values in ascending order as 8-byte big-endian integers.
  • The confirmation is ASCII OIP, NUL, ASCII HEARTBEAT-PROFILE, NUL, then the local and remote offers, each preceded by its length as an 8-byte big-endian integer, then the selected version as an 8-byte big-endian integer and the selected family, atomicity and scheme sets in the same set form.
  • The local offer is signed with the local key. The remote offer and the confirmation are signed with the remote key.
  • The selection MUST bind the two peers, the deployment, a fresh challenge supplied by the host, each offer's expiry and complete sets, and the selected profile. The selected version is the highest version that both offers contain at or above the registered peer floor. Version 5 is eligible only when legacy is allowed.
  • The selected sets are the intersections of the offered sets, and an intersection can be empty. Under version 5 the families are limited to QUERY (opens in a new tab) and HEARTBEAT (opens in a new tab) and the schemes to SINGLE (opens in a new tab). The channel expires at the earlier of the two offer expiries. An unverified, mismatched or expired offer cannot select a profile.
  • An offer carries no size or nesting limit. The maximum frame size and nesting depth are configuration values of the receiving host.
  • The reference sets up channels out of band. Its host verifies the signed offers and confirmation and installs the resulting channel. The reference does not accept HEARTBEAT frames as deliveries.
  • Use explicit v0.5 compatibility only with an authenticated permitted legacy peer and a configured legacy codec.
  • The reference legacy codec is a reference compatibility record and does not interoperate with the wire format of the public v0.5 documents. v0.5 correspondence (opens in a new tab) records how this version treats those documents. Deny v0.6-only capabilities in a legacy profile.
  • Parse, signature, timeout and rejection paths cannot trigger fallback. Retire legacy through a trusted allowlist and version floor change after compatible routes and unresolved obligations have been handled.
  • A fresh channel MAY negotiate the same profile without changing an already signed origin request. The signed immutable origin core is separate from mutable delivery information. A hop that changes processing MUST carry an authenticated, authorized, linked transformation.
  • This version defines single-hop delivery only, so the reference provides no relay against which this requirement can be tested. The reference validates the immutable origin and its authorized local delivery scope before effect.

Offer and confirmation bytes#

Both implementations build the two signing inputs from the domain strings, big-endian integers and counted sets described above. Rust sets are ordered by type. Go writes each set in the order the caller supplies, and SelectProfile refuses an unordered set after the offer signatures verify.

Offer and confirmation signing inputssrc/wire/negotiation.rsL88-L129Highlighted lines are discussed in the textSource (opens the lines on GitHub in a new tab)
fn append_set(out: &mut Vec<u8>, set: &BTreeSet<u64>) {    out.extend((set.len() as u64).to_be_bytes());    for value in set {        out.extend(value.to_be_bytes());    }}pub fn offer_bytes(offer: &Offer) -> Vec<u8> {    let mut out = b"OIP\0HEARTBEAT-OFFER\0".to_vec();    for value in [        offer.deployment,        offer.sender,        offer.receiver,        offer.challenge,        offer.expires,    ] {        out.extend(value.to_be_bytes());    }    for set in [        &offer.versions,        &offer.families,        &offer.atomicities,        &offer.schemes,    ] {        append_set(&mut out, set);    }    out}pub fn confirmation_bytes(local: &Offer, remote: &Offer, profile: &Profile) -> Vec<u8> {    let mut out = b"OIP\0HEARTBEAT-PROFILE\0".to_vec();    for offer in [local, remote] {        let bytes = offer_bytes(offer);        out.extend((bytes.len() as u64).to_be_bytes());        out.extend(bytes);    }    out.extend(profile.version.to_be_bytes());    for set in [&profile.families, &profile.atomicities, &profile.schemes] {        append_set(&mut out, set);    }    out}

Profile selection#

Selection tries version 6 before version 5, and a version 5 profile keeps only QUERY, HEARTBEAT and SINGLE. Go refuses a set that is not strictly ascending here as INVALID_FORMAT. In Rust the offer sets are ordered sets, and the capabilities decoder refuses a non-ascending set as INVALID_FORMAT.

Profile selectionsrc/wire/negotiation.rsL153-L188Highlighted lines are discussed in the textSource (opens the lines on GitHub in a new tab)
pub fn select_profile(    local: &Offer,    remote: &Offer,    floor: u64,    legacy_allowed: bool,) -> Result<Profile> {    let version = [6, 5]        .into_iter()        .find(|&version| {            version >= floor                && (version != 5 || legacy_allowed)                && local.versions.contains(&version)                && remote.versions.contains(&version)        })        .ok_or(WireError::UnsupportedVersion)?;    Ok(Profile {        version,        families: local            .families            .intersection(&remote.families)            .copied()            .filter(|family| version != 5 || matches!(family, 4 | 6))            .collect(),        atomicities: local            .atomicities            .intersection(&remote.atomicities)            .copied()            .collect(),        schemes: local            .schemes            .intersection(&remote.schemes)            .copied()            .filter(|scheme| version != 5 || *scheme == 1)            .collect(),    })}

Negotiation checks#

A mismatched or expired offer is INVALID_SOURCE and a failed offer signature is INVALID_SIGNATURE, both before a profile is selected. The confirmation is then verified with the remote key, and the channel takes the earlier expiry.

Channel negotiationsrc/wire/negotiation.rsL190-L234Highlighted lines are discussed in the textSource (opens the lines on GitHub in a new tab)
pub fn negotiate(context: &Negotiation, signed: SignedOffers<'_>) -> Result<Channel> {    let local = signed.local;    let remote = signed.remote;    if local.deployment != context.deployment        || remote.deployment != context.deployment        || local.sender != context.local        || local.receiver != context.remote        || remote.sender != context.remote        || remote.receiver != context.local        || local.challenge != context.challenge        || remote.challenge != context.challenge        || local.expires < context.now        || remote.expires < context.now    {        return Err(WireError::InvalidSource);    }    if !verify(        &context.local_key,        &offer_bytes(local),        signed.local_signature,    ) || !verify(        &context.remote_key,        &offer_bytes(remote),        signed.remote_signature,    ) {        return Err(WireError::InvalidSignature);    }    let profile = select_profile(local, remote, context.floor, context.legacy_allowed)?;    if !verify(        &context.remote_key,        &confirmation_bytes(local, remote, &profile),        signed.confirmation,    ) {        return Err(WireError::InvalidSignature);    }    Ok(Channel {        profile,        deployment: context.deployment,        local: context.local,        remote: context.remote,        expires: local.expires.min(remote.expires),        challenge: context.challenge,        peer_keys: [context.local_key, context.remote_key],    })}

In the conformance vectors, only an authenticated legacy-only peer reaches version 5, and an unauthenticated offer selects no profile.

TextNegotiation vectorstests/vectors/wire-conformance.tsvL18-L20Source (opens the lines on GitHub in a new tab)

Columnsidoperationinputsexpectedevidence

explicit-authenticated-legacy-common-profile	negotiate	local_versions=5,6;remote_versions=5;floor=5;auth=true;common_operation=query	selected=5;legacy_common_query=true;fallback=false	an_authenticated_legacy_only_peer_negotiates_an_explicit_compatibility_profile,legacy_profile_uses_only_the_explicit_compatibility_adapterhighest-common-no-authenticated-downgrade	negotiate	local_versions=5,6;remote_versions=5,6;floor=5;auth=true;requested_lower=5	selected=6;lower_rejected=true	the_same_authenticated_offer_negotiates_current_when_both_support_it,authenticated_highest_common_version_cannot_downgradeunauthenticated-offer-and-no-parse-fallback	negotiate	local_versions=5,6;remote_versions=5;auth=false;modern_parse_failed=true	selected=none;legacy_called=false	an_unverified_offer_never_selects_a_legacy_or_current_profile,canonical_parse_failure_never_calls_the_legacy_decoder

Network coordination context#

DesignCarried from v0.5, without bytes or test vectors in this version

Peer discovery and the complete network exchange of coordination messages belong to the architecture described in v0.5. The reference host installs an authenticated channel out of band and defines a single-hop delivery boundary. A HEARTBEAT layout in the schema does not supply an executable peer-discovery service.


Edition history#

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