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.
- 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
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
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
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.
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:
#[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],}type Boundary struct { Deployment, Source, Target, Principal, KeyID, Epoch, RouteScope uint64 Now, AssetTTLFloor, FutureSkew, VersionFloor uint64 MaxFrame, MaxNesting int CriticalAllowed bool PublicKey [32]byte}Every bound Core value must equal its boundary value; any difference is INVALID_SOURCE, decided before the signature is checked:
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}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:
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:
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_consumer forged-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_namespace wrong-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_write read-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:
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_downgrade unauthenticated-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:
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 Related Pages#
Edition history#
- 0.6.0: Added this edition. The v0.5 specification remains available at its original addresses.
