Protocol Specification v0.6

Routing

A route carries a request to an endpoint; the endpoint supplies the final authority check. Normal, administrative, force-inclusion (opens in a new tab), upgrade and adapter paths must enforce the same operation guard. Choosing another path cannot turn an unauthorized message into an executable action.

Route binding

A packet must match the registered values of its channel, and every entry meets the same final guard.

A packet must match the registered values of its channel, and every entry meets the same final guard. The receiving host compares seven values of the signed Core, deployment, source_domain, target_domain, principal, key_id, authority_epoch and route_scope, with the values registered for its channel. When a bound value differs the packet is refused as INVALID_SOURCE before the signature is checked, and when the signature does not verify it is refused as INVALID_SIGNATURE. An admitted request reaches the endpoint through one of five entries, Normal, Administrative, ForceInclusion, Upgrade or Adapter, and all five lead to the same final guard. That guard binds the caller to the call namespace and the signed request, checks role and command authority at the cut that execution uses, and applies the target action's own legal basis, scope, lock and evidence checks. When a check fails the request is rejected with no effect. Signed Core Registered for the channel deployment source_domain target_domain principal key_id authority_epoch route_scope 1 each pairmust be equal deployment source target principal key_id epoch route_scope a bound value differsINVALID_SOURCE signature does not verifyINVALID_SIGNATURE Normal Administrative ForceInclusion Upgrade Adapter 2 Final guard caller bound to the call namespace and the signed request 3 role and command authority at the cut that execution uses the target action's own legal basis, scope, lock and evidence 4 a check failsrejected Effect at the target endpoint
  1. 1

    Bound values

    The receiving host compares seven signed Core values with the values registered for the channel. Any difference is INVALID_SOURCE, decided before the signature is checked.

  2. 2

    No entry adds authority

    All five entries hand on the same request, and no check in the reference depends on which entry carried it.

  3. 3

    Identity and authority

    The caller is bound to the call namespace and the complete signed request, then checked at the cut that execution uses. Body fields and client-supplied role names establish no capability.

  4. 4

    The target action decides

    A force-inclusion route still needs the legal basis, scope, lock and exact evidence that the target action requires.

  • One of these entries carries each call
  • Path of an admitted request
  • Check of the final guard
  • Refused; nothing changes

Schematic: one channel and one endpoint are drawn, and the version, TTL and delivery checks are left out. The binding step also refuses a purpose other than 1 and a CRITICAL priority that the boundary does not allow.

Figure 1: Route binding values and the one final guard behind every entry

Registered route binding#

NormativeBytes, test vectors and proofs

The canonical header binds the deployment, source domain, target domain, principal, key identifier, authority epoch (opens in a new tab) and route scope. Domains and principals are registered numeric handles. The receiving host compares them with the authenticated negotiated channel and its current registrations.

The receiving host keeps the registered values of one channel in a boundary record:

Receiving boundarysrc/wire/admission.rsL12-L29Highlighted lines are discussed in the textSource (opens the lines on GitHub in a new tab)
#[derive(Clone, Copy, Debug, Eq, PartialEq)]pub struct Boundary {    pub deployment: u64,    pub source: u64,    pub target: u64,    pub principal: u64,    pub key_id: u64,    pub epoch: u64,    pub route_scope: u64,    pub now: u64,    pub asset_ttl_floor: u64,    pub future_skew: u64,    pub max_frame: usize,    pub max_nesting: usize,    pub version_floor: u64,    pub critical_allowed: bool,    pub public_key: [u8; 32],}

Every bound Core value must equal its boundary value; any difference is INVALID_SOURCE, decided before the signature is checked:

Route binding 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(())}

For a product request, the target depends on its operation. Source-side actions name the original domain; receiving actions name the bound destination. The operation layouts state the target checks. An accepted canonical packet still needs the exact current session, role, asset and command authority.

Same checks on every entry point#

NormativeBytes, test vectors and proofs

The endpoint takes the caller from the authenticated identity that its security adapter supplies. It binds that identity to the call namespace and complete signed request and checks current authority at the same cut used by execution. Body fields and client-supplied role names do not establish a capability.

On the reference receiving path the entry value is dropped before execution, so every entry reaches the same durable execution function, which takes no entry at all:

RustOne final consumer for every entrysrc/authority/recovery/wire.rsL356-L372Highlighted lines are discussed in the textSource (opens the lines on GitHub in a new tab)
if let Some(request) = request_at(target, operands) {    let auth = auth(source_session(operands).expect("typed call carries a session"));    // The caller cannot substitute a Request: this is the sole    // materialization of the independently decoded canonical operand.    let _ = entry; // all identity routes use the same final consumer    if !product.security.matches_recovery_endorsement(        operands, credentials.endorsement.map(|endorsement| endorsement.signature),    ) {        return Err(WireError::ProductRejected);    }    #[cfg(test)] response::source_trace::execute_enter(&request);    let executed=product.execute_durable(&auth, request, credentials.endorsement);    #[cfg(test)] response::source_trace::execute_return(&executed);    return executed        .map(WireReply::Executed)        .map_err(|_| WireError::ProductRejected);}

The identity vectors, which the Rust tests run, submit the same request on all five entries and expect the same result on each. A call under another namespace and an identity that differs from its registration are refused even with a valid signature, and so is a caller that holds only a read role:

TextSame result on every entrytests/vectors/identity-handover.tsvL2-L5Source (opens the lines on GitHub in a new tab)

Columnsidoperationinputsexpectedevidence

bound-caller	ordinary-submit	actor=7;domain=0;ocid=107;rai=207;session=1;epoch=9;asset=17;namespace=7;sequence=1;routes=normal,administrative,force_inclusion,upgrade,adapter	admission=allowed	current_identity_can_submit_the_bound_request,all_external_entries_use_the_same_final_consumerforged-call-namespace	ordinary-submit	actor=7;domain=0;ocid=107;rai=207;session=1;epoch=9;asset=17;namespace=8;sequence=1;signature_valid=true	admission=rejected;authority_unchanged=true	another_namespace_cannot_submit_even_with_a_valid_signature,endpoint_call_refines_authenticated_namespacewrong-identity-binding	ordinary-submit	actor=7;domain=0;ocid=107;rai=999;registered_rai=207;session=1;epoch=9;asset=17;namespace=7;sequence=1;signature_valid=true	admission=rejected;authority_unchanged=true	endpoint_call_refines_authenticated_namespace,denied_endpoint_has_no_effect_or_receipt_writeread-role-cannot-execute	ordinary-submit	actor=7;domain=0;session=1;epoch=9;asset=17;namespace=7;sequence=1;granted=read_receipt;routes=normal,administrative,force_inclusion,upgrade,adapter	admission=rejected;authority_unchanged=true	missing_exact_role_cannot_enter_the_consumer,all_external_entries_use_the_same_final_consumer

Legal and recovery operations retain their own authority requirements. A force-inclusion route must consume the final guard of the target action, including its legal basis, scope, lock and exact evidence when required. Regulatory Actions explains those guards.

Immutable origin and delivery information#

NormativeBytes, test vectors and proofs

The immutable signed origin core is distinct from mutable delivery information. A change to processing requires authenticated, authorized and linked transformation evidence. This edition defines single-hop delivery only. It supplies no relay implementation that demonstrates transformation across hops.

A new authenticated channel may negotiate the same profile without changing an already signed origin request. Parse, signature, timeout and rejection paths must not trigger fallback to another version.

The negotiation vectors keep the highest common version of authenticated offers. An unverified offer selects no profile, and a frame that fails canonical parsing is never passed to the legacy decoder:

TextNo downgrade and no fallbacktests/vectors/wire-conformance.tsvL19-L20Source (opens the lines on GitHub in a new tab)

Columnsidoperationinputsexpectedevidence

highest-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

Hierarchical routing context#

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

The v0.5 architecture describes routing between the Oracle State Synchronizer (opens in a new tab), cross-domain Drivers and participating chains. Dynamic routing-table updates, general multi-hop optimization, address resolution (opens in a new tab) and Canton (opens in a new tab) party mapping are outside the executable v0.6 profile. Their absence does not weaken the target endpoint's current guard: an unimplemented route cannot authorize a state change.

Integration check#

Configure the exact peer and target registrations, establish the authenticated channel, and pass the original packet to the matching endpoint. Treat INVALID_SOURCE as a failed binding, identity or target check. Do not repair it by rewriting a signed source, target or scope value in transit. Inspect the channel profile and authoritative registration first.

The shared vector changes the source, target, key, epoch and route scope one at a time and expects a refusal with the duplicate cache unchanged:

TextFive bound values, one at a timetests/vectors/wire-conformance.tsvL14-L14Source (opens the lines on GitHub in a new tab)

Columnsidoperationinputsexpectedevidence

current-source-key-and-epoch-required	receive	fixture=canonical-query-core;alternatives=wrong_source,wrong_target,wrong_key,wrong_epoch,wrong_scope	rejected=true;cache_unchanged=true	a_wire_admission_uses_the_exact_current_domain_key_epoch_and_signature,deduplication_changes_only_after_the_original_canonical_signature_and_identity

Edition history#

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