Message Protocol
This page defines the OIP message protocol for v0.5: the unified message header that every OIP message carries, the routing metadata that governs delivery decisions, the payload structure of each of the seven message types, and the integrated OIPMessage interface that combines header and payload. The type definitions referenced here are specified in Data Model.
Two implementations are interoperable on the OIP message protocol surface when they agree on four things: (1) the header field set and field semantics, (2) the routing metadata semantics including TTL and atomicity, (3) the payload structure for each message type, and (4) the dispatch from messageType to payload. This page is the normative definition of those four agreements.
Unified Message Header
Every OIP message carries a unified header. The header contains the metadata required for message routing, validation, and response correlation. The header is divided into four functional groups: identification and version, origin, signature, and routing. Figure 1 shows the header anatomy.
Figure 1: OIP Message Header Anatomy
interface OIPMessageHeader {
// Identification and version
version: string; // "0.5.0" (SemVer)
messageId: string; // UUID v4
messageType: OIPMessageType;
timestamp: string; // ISO 8601, send time
// Origin
sourceChainId: string; // CAIP-2
sourceNodeId: string; // sender node identifier
// Signature
signatureType: SignatureType; // SINGLE | THRESHOLD
signature: string; // 0x-prefixed hex
// SINGLE: Ed25519 or ECDSA single signature
// THRESHOLD: BLS aggregated signature
signerSet?: string[]; // signer identifiers when THRESHOLD
threshold?: number; // quorum (f+1) when THRESHOLD
// Routing
routing: RoutingMetadata;
}
Identification and Version Fields
versionfollows SemVer. v0.5 messages MUST set this field to"0.5.0". The leadingvprefix is a prose convention only and MUST NOT appear in the field value. When the .proto refinement lands in v0.6, the version field also serves as the discriminator for protobuf wire compatibility checks.messageIdis a UUID v4 unique to this message instance. It is used for ACK correlation, idempotency enforcement at the receiver, and routing slip recording. Implementations MUST treatmessageIdas opaque and MUST NOT derive semantics from its bit pattern.messageTypeis one of the seven values inOIPMessageType. The receiver dispatches the payload structure based on this field. The mapping frommessageTypeto payload type is fixed by theOIPMessageintegrated interface defined later in this page.timestampis the send time in ISO 8601 with explicit timezone (Zfor UTC). The receiver uses this together withrouting.ttlSecondsfor TTL enforcement. Locale-dependent formats are forbidden.
Origin Fields
sourceChainIdis the originating chain in CAIP-2 form (e.g.,"eip155:42161"). For off-chain origins such as the Canton Driver, the field carries a CAIP-2-compatible identifier reserved for that origin class. The exact namespace registration for Canton-class origins is finalized as part of the v0.6 .proto refinement.sourceNodeIdidentifies the originating node within its domain. Format is implementation-defined; the OIP requirement is uniqueness within the originating domain so that ACKs and routing slip entries can correlate back to the source.
Signature: SINGLE and THRESHOLD
OIP supports two signature schemes within a single header structure. signatureType: SINGLE signs the message with the originating node’s single key (Ed25519 or ECDSA). signatureType: THRESHOLD uses a BLS aggregated signature, with signerSet listing the participating signer node IDs and threshold indicating the quorum required for the signature to be considered valid.
Verification Procedure
- SINGLE: the receiver verifies the single signature against the originating node’s public key, looked up via
sourceNodeIdin the NodeRegistry. IfsignerSetorthresholdis present, the receiver MUST ignore them. - THRESHOLD: the receiver verifies (a)
|signerSet| >= threshold, and (b) the BLS aggregated signature is valid against the combined public key derived fromsignerSet. Both checks MUST pass; either failure rejects the message withINVALID_THRESHOLD_SIGNATURE.
Interoperability Requirements
- v0.5-compliant implementations MUST support SINGLE and SHOULD support THRESHOLD. The complete classification of every requirement is consolidated on the Conformance page.
- An implementation that does not support THRESHOLD MUST reject incoming THRESHOLD messages with the error
INVALID_THRESHOLD_SIGNATURE(see Error Reference). Silent downgrade or fallback to single-signature semantics is forbidden. - D-quencer already uses BLS for consensus, so the additional infrastructure cost of supporting THRESHOLD is minimal for nodes that participate in D-quencer consensus.
Why two schemes in one field. SINGLE is the simpler and lower-cost path for messages originating from a single trusted node, such as routine state synchronization from a Canton domain or query messages from a single client. THRESHOLD provides Byzantine-fault-tolerant origin authentication for messages that span untrusted relayer sets, particularly for cross-chain delivery where multiple relayers must collectively attest to the message. Carrying both schemes in a single header field with a discriminator avoids two parallel header formats while preserving the security guarantee for each origin class. The discriminator pattern also aligns with the v0.6 .proto refinement where this becomes a oneof field.
Routing Metadata
The RoutingMetadata structure governs how the message is delivered: which priority class it carries, which target scope it applies to, what atomicity guarantee is requested, when it expires, and how earlier routing decisions have transformed it.
interface RoutingMetadata {
priority: Priority;
targetScope: TargetScope;
atomicity: AtomicityStrategy;
ttlSeconds: number; // seconds
correlationId?: string; // UUID v4 (for ACK, QUERY response)
routingSlip?: RoutingSlipEntry[]; // accumulated routing decisions
}
interface TargetScope {
targetChains?: string[]; // ERC-3770 shortName list
targetAssets?: string[]; // asset ID list
targetAddresses?: string[]; // ERC-3770 addresses
}
interface RoutingSlipEntry {
stage: string; // pipeline stage identifier
nodeId: string; // node that made the decision
timestamp: string; // ISO 8601
decision: object; // stage-specific decision data
mutations?: RoutingMutations; // append-only transformations
}
interface RoutingMutations {
typeChanged?: OIPMessageType; // type escalation (e.g., STATE_SYNC -> REGULATORY_ACTION)
priorityChanged?: Priority;
atomicityChanged?: AtomicityStrategy;
scopeExpanded?: string[]; // chains/assets added
scopeNarrowed?: string[]; // chains/assets removed
}
TTL Determination
Message TTL determines the freshness window of a single message: the time within which the message must be delivered, after which it is rejected as stale. OIP splits TTL responsibility between the sender and the OSS asset-class policy. The final TTL applied to a message is computed by the receiver as final_ttl = max(sender_ttl, asset_min_ttl). Figure 2 shows the decision flow.
MESSAGE_TTL_EXPIREDFigure 2: TTL Decision Flow
Why max, Not min or Average
The choice of max is deliberate. The asset-class minimum is a floor: the OSS guarantees that no message touching this asset is rejected as expired before asset_min_ttl elapses, because shorter windows would risk dropping legitimate messages on assets that need verification time. The sender recommendation is a ceiling preference: senders may want longer windows for non-urgent messages, but cannot shorten below the floor. max respects both: senders can extend, but never undercut, the policy floor. min would let a careless sender bypass the floor; an average would dilute the floor’s guarantee.
Recommended TTL by Message Type
| Message Type | Recommended TTL (s) | Rationale |
|---|---|---|
STATE_SYNC (incremental) | 30 | Routine sync, freshness matters |
STATE_SYNC (initial / full) | 300 | Initial / full sync, queuing acceptable |
REGULATORY_ACTION (CRITICAL) | 60 | Rapid processing required, but too short impedes retries |
REGULATORY_ACTION (other) | 600 | Sufficient verification time |
LOCK_MANAGEMENT (acquire) | 30 | Lock contention resolved quickly |
LOCK_MANAGEMENT (release / extend) | 60 | Cleanup actions, slight margin |
QUERY | 10 | Read-only, kept short |
ACK | 5 | Response, very short |
HEARTBEAT | 30 | Node liveness, one period |
GOVERNANCE (PROPOSAL) | 600 | Delivery of the proposal message itself; voting period is separate |
GOVERNANCE (VOTE) | 300 | Delivery of the vote message; within the voting window |
GOVERNANCE (EXECUTE) | 600 | Execution message of an approved proposal |
Asset-Class Minimum TTL (OSS Policy)
- High-frequency trading assets: minimum 5 seconds. Longer TTLs risk acting on stale state because price-sensitive assets move faster than the window allows.
- General RWA assets: minimum 30 seconds. Sufficient for routine cross-chain delivery without freshness loss.
- Regulatory action targets: minimum 600 seconds. Authority verification, escalation chain checks, and cross-chain coordination need substantial verification time.
- Expiry-imminent assets: minimum
max(60, expiryTime - now). Avoids accepting messages that would only finalize after the underlying asset has expired.
TTL Expiry Handling
A message whose TTL has expired is rejected with the error MESSAGE_TTL_EXPIRED. On receipt, an implementation MUST evaluate header.timestamp + final_ttl < now and reject if the comparison holds. The expired message MUST NOT be processed, forwarded, or partially executed; rejection is total.
Clock synchronization assumption. All OIP nodes are assumed to be time-synchronized via NTP or equivalent, with a tolerance of 5 seconds for clock skew. Larger skew is treated as a node operations issue rather than a protocol concern. The asset_min_ttl policy values are chosen with this 5-second skew tolerance in mind, ensuring that legitimate messages are not rejected due to clock drift alone. Detailed time-skew attack defense is recorded as a Pending Definition item in Open Issues.
Routing Slip
The routing slip is an append-only record of the routing decisions a message has accumulated as it traverses the pipeline. Each entry records the stage, the deciding node, the timestamp, and any mutations the stage applied (for example, a type escalation from STATE_SYNC to REGULATORY_ACTION, or a priority change). The append-only property is essential: downstream stages can read the history of decisions but cannot rewrite earlier entries, which preserves the audit trail for cross-chain coordination and compliance verification. The semantics of routing slip processing are specified in Routing.
Message Type Map
The seven message types of OIP fall into four functional categories, each with distinct processing patterns. Figure 3 shows the categorization, the type-to-payload dispatch, and the principal responsibility of each type.
Figure 3: OIP Message Type Map
This categorization clarifies why the OIP message protocol defines exactly these seven types: they cover the full spectrum of operations needed for cross-domain state coordination. State-changing messages drive the actual synchronization work. Read queries observe state without affecting it, supporting compliance audits and external integrations. ACK closes the loop on processing outcomes and coordinates phase-based protocols. Operational messages keep the network itself alive and governable.
Message Type Payloads
This section specifies the payload structure of each of the seven message types. The dispatch from header.messageType to the payload type is fixed by the OIPMessage integrated interface defined at the end of this page.
STATE_SYNC
Asset state synchronization. The most common message type, used for routine state updates within an Oracle Session as well as initial registration and full resynchronization.
interface StateSyncPayload {
syncType: "INITIAL_SYNC" | "INCREMENTAL_SYNC" | "FULL_RESYNC";
asset: OIPAsset;
previousStateHash?: string; // required when INCREMENTAL_SYNC
delta?: StateDelta; // required when INCREMENTAL_SYNC
driverAttestation?: CantonDriverAttestation; // when message originates from Canton
}
interface StateDelta {
changedFields: string[]; // dot-paths (e.g., "lockStatus.primary")
oldValues: Record<string, unknown>;
newValues: Record<string, unknown>;
}
syncType Selection
INITIAL_SYNC: the first state registration at the start of an Oracle Session.INCREMENTAL_SYNC: routine in-session state changes.previousStateHashanddeltaare required, allowing the receiver to verify continuity from the prior state.FULL_RESYNC: full state resynchronization after a fork or restart. The fullassetis attached anddeltais omitted, since there is no continuous prior state to delta against.
The driverAttestation field is populated when the message originates from an off-chain domain such as Canton. The receiver verifies the attestation as part of the Validation pipeline.
REGULATORY_ACTION
One of the six regulatory actions being applied. The detailed action parameters and escalation rules are specified in Regulatory Actions.
interface RegulatoryActionMessage {
payload: RegulatoryPayload; // defined in Data Model
crossChainScope: CrossChainScope;
scheduledExecution?: ScheduledExecution; // when execution is not immediate
}
interface CrossChainScope {
targetChains: string[]; // ERC-3770 shortName
atomicity: AtomicityStrategy;
timeoutSeconds: number;
fallbackPolicy: "RETRY" | "PARTIAL" | "ABORT";
}
interface ScheduledExecution {
executeAt: string; // ISO 8601
conditions?: string[]; // execution preconditions (Restricted DSL)
cancellationPolicy: "AUTO_ON_CONDITION" | "MANUAL_ONLY";
}
The CrossChainScope structure determines how the action is propagated across chains. ScheduledExecution defers execution to a future time, which is useful for actions like a planned freeze that takes effect at a regulator-specified moment.
CRITICAL priority and authority verification. A REGULATORY_ACTION carrying routing.priority: CRITICAL triggers forced escalation to ALL_OR_NOTHING + ALL_CHAINS coordination (see Routing). Because this has system-wide impact, CRITICAL authorization is restricted by a registered authority hierarchy and scope limitation. The receiver verifies that the issuing authority is registered and operating within its permitted scope; on failure the message is rejected with CRITICAL_PRIORITY_NOT_AUTHORIZED. The complete authority hierarchy levels and scope verification procedure are recorded as Pending Definition in Open Issues.
LOCK_MANAGEMENT
Acquisition, release, extension, and escalation of preemptive locks. The lock protocol semantics are specified in State Management.
interface LockManagementPayload {
operation: "LOCK_ACQUIRE" | "LOCK_RELEASE" | "LOCK_EXTEND" | "LOCK_ESCALATE";
targetAssetId: string;
lockToken?: string; // issued token (RELEASE / EXTEND / ESCALATE)
durationSeconds?: number; // ACQUIRE / EXTEND
newPrimaryState?: PrimaryLockState; // ESCALATE (e.g., RESTRICTED -> SEIZED)
escalationReason?: string;
conflictPolicy?: "FAIL" | "WAIT" | "PREEMPT"; // contention handling on ACQUIRE
}
conflictPolicy: PREEMPT is permitted only on messages with CRITICAL priority and is subject to the same authority verification as REGULATORY_ACTION CRITICAL messages described above. The PREEMPT policy allows the message to displace an existing lock holder; without authority, this would be an attack vector for arbitrary lock takeover.
QUERY
Read-only state queries. The receiver returns the requested data via an ACK message correlated by messageId. asOfTimestamp enables historical queries against past OSS state, useful for compliance audits and reconciliation.
interface QueryPayload {
queryType: "ASSET_STATE" | "LOCK_STATUS" | "REGULATORY_HISTORY" | "CONTRACT_STATUS";
targetIdentifier: string; // asset ID, address, etc.
requestedFields?: string[]; // for partial queries
asOfTimestamp?: string; // historical query (ISO 8601)
permissionContext?: PermissionContext;
}
interface PermissionContext {
requestorOCID: string;
attestation?: string; // permission proof (optional)
}
The requestorOCID field carries the requestor’s OCID, which the receiver checks against access policies before returning the requested fields.
ACK
Receipt confirmation and execution result response. ACK serves two distinct purposes that share the same message type but are distinguished by ackType: a general ACK notifies a single message’s processing outcome, and a phase ACK responds to PREPARE / COMMIT / FINALIZE coordination phases (see Cross-chain Protocol).
interface AckPayload {
acknowledgedMessageId: string; // messageId of the message being acked
ackType: AckType;
resultCode?: string; // error code or success code
resultData?: object; // result data when ackType is PROCESSED
errorDetails?: ErrorDetails; // when ackType is FAILED or REJECTED
phaseContext?: PhaseAckContext; // when ackType is PHASE_*
}
type AckType =
// General ACK (single-message processing outcome)
| "RECEIVED" // receipt confirmed
| "PROCESSED" // processing completed
| "FAILED" // failure during processing
| "REJECTED" // message itself invalid
// Phase ACK (PREPARE / COMMIT / FINALIZE phase response)
| "PHASE_PREPARE_OK" // PREPARE phase: ready to participate
| "PHASE_COMMIT_OK" // COMMIT phase: applied
| "PHASE_FINALIZE_OK" // FINALIZE phase: confirmed
| "PHASE_REJECT"; // any phase: rejected
interface PhaseAckContext {
phase: "PREPARE" | "COMMIT" | "FINALIZE";
originalMessageId: string; // messageId of the original phase message
chainId: string; // responding node's chain (CAIP-2)
reservedResources?: string[]; // resources reserved (PHASE_PREPARE_OK)
}
General ACK vs Phase ACK
- General ACK (
RECEIVED,PROCESSED,FAILED,REJECTED): notifies the processing outcome of a single message. The sender learns “how was my message handled”. This is the terminal point of the original message. - Phase ACK (
PHASE_PREPARE_OK,PHASE_COMMIT_OK,PHASE_FINALIZE_OK,PHASE_REJECT): responds to one stage of the three-phase coordination protocol. The sender (the coordinator) learns “how does the participant respond to this phase”. This is an intermediate step in a multi-stage procedure, not a terminal acknowledgement. - Why one type with two meanings: both share the same correlation pattern (correlated by
acknowledgedMessageId) and the same delivery requirements. Branching byackTyperather than by message type avoids unnecessary type proliferation while keeping the semantic distinction explicit.
ACK does not generate ACK (to avoid infinite loops). The exception is regulatory action acknowledgements, which are recorded separately for audit purposes but do not themselves produce a further ACK.
HEARTBEAT
Node liveness signal and synchronization progress report. HEARTBEAT also carries the node’s capabilities, which advertises what the node can support to peers. The capabilities channel allows peers to discover each other’s supported message types and atomicity levels, forming the basis for compatibility decisions in mixed-version environments as the protocol evolves toward v0.6.
interface HeartbeatPayload {
nodeStatus: "HEALTHY" | "DEGRADED" | "RECOVERING" | "MAINTENANCE";
syncProgress: {
primaryFinalityHeight: number; // cumulative primary finality issuances
secondaryFinalityHeight: number; // cumulative secondary finality reaches
lagSeconds: number; // primary / secondary gap
};
activeChains: string[]; // chains currently being synchronized (CAIP-2)
capabilities: NodeCapabilities;
}
interface NodeCapabilities {
supportedMessageTypes: OIPMessageType[];
supportedSignatureTypes: SignatureType[];
supportedAtomicityLevels: AtomicityStrategy[];
protocolVersions: string[]; // supported OIP versions
}
GOVERNANCE
Protocol parameter changes, authority registration, and routing table updates. GOVERNANCE covers the entire procedural lifecycle of governance decisions through governanceType discrimination.
interface GovernancePayload {
governanceType: GovernanceType;
proposalId?: string; // for PROPOSAL / VOTE
proposalDetails?: ProposalDetails; // for PROPOSAL
vote?: VotePayload; // for VOTE
executionDetails?: ExecutionDetails; // for EXECUTE
}
type GovernanceType =
| "PROPOSAL" // proposal creation
| "VOTE" // vote
| "EXECUTE" // post-approval execution
| "AUTHORITY_REGISTRATION" // authority registration
| "ROUTING_TABLE_UPDATE" // routing table update
| "PARAMETER_CHANGE"; // protocol parameter change
interface ProposalDetails {
title: string;
description: string;
proposalKind: GovernanceType;
proposedChanges: object; // proposal content (varies by kind)
votingPeriodSeconds: number;
executionDelaySeconds: number; // execution delay after approval
}
interface VotePayload {
voterOCID: string;
voterStake: string; // big number string
voteDirection: "FOR" | "AGAINST" | "ABSTAIN";
voteSignature: string;
}
interface ExecutionDetails {
proposalId: string;
executionResult: "SUCCESS" | "FAILED" | "PARTIAL";
appliedChanges: object;
}
Routing Table Updates and Authority Registration
Two GOVERNANCE subtypes are particularly significant for OIP operation. ROUTING_TABLE_UPDATE manages four routing-relevant registries: ChainRegistry (supported chains), AssetLocationMap (asset-to-chain mapping), AuthorityRegistry (regulatory authorities and their permitted scope), and NodeRegistry (OIP node identities and supported features). All four are modified through the same governance procedure (PROPOSAL → voting period → EXECUTE) rather than through automatic discovery, ensuring that routing-critical state changes are auditable and consensus-driven. AUTHORITY_REGISTRATION is the channel through which regulatory authorities are added to the AuthorityRegistry, including the authority’s hierarchy level, jurisdictional scope, and verification key; it is the prerequisite for the CRITICAL priority verification described earlier in this page.
Deterministic activation. When a routing table update is approved and executed, the effectiveAt timestamp is interpreted against the deterministic time recorded in the OSS State Root, not the local clock of each receiving node. This ensures that all nodes processing the same OSS State Root reach the same routing decision, eliminating divergence due to clock skew. The semantics of dynamic routing table updates are specified in Routing; automatic discovery mechanisms beyond explicit governance are recorded as a Deferred item in Open Issues.
Procedural Time vs Message TTL
GOVERNANCE proceeds through multiple stages (PROPOSAL → voting period → EXECUTE), each handled by a separate message. The TTL of each message is independent of the procedural time windows.
- Message TTL (
header.routing.ttlSeconds): the freshness window for delivery of a single message. The recommended values are listed earlier in this page. - Procedural time (
votingPeriodSeconds,executionDelaySeconds): the time windows of the governance procedure itself, such as the voting period and the execution delay after approval. - Independence: a 600-second TTL on a
PROPOSALmessage means “thisPROPOSALmessage must be delivered within 600 seconds”, whilevotingPeriodSeconds: 86400means “the voting window is 24 hours”. The two values are independent. - In-window messages:
VOTEmessages arrive throughout the voting period, each with its own TTL (300 seconds recommended). The voting result is tallied at the end ofvotingPeriodSeconds.
This separation has a practical consequence for fault tolerance: retry policies (specified in Fault Tolerance) operate on individual messages, not on procedural windows. A failed VOTE message can be retried within its 300-second TTL without affecting the overall voting period.
OIPMessage Integrated Interface
Every OIP message follows the integrated structure below. The concrete payload type is determined by header.messageType: this discriminated union is the integrated form of the seven payload types described above. In the v0.6 .proto refinement, this discriminated union is expressed as a Protocol Buffers oneof.
interface OIPMessage {
header: OIPMessageHeader;
payload: OIPPayload;
crossChainScope?: CrossChainScope; // present for cross-chain messages
proof?: MessageProof; // present when a proof is attached
}
type OIPPayload =
| StateSyncPayload
| RegulatoryActionMessage
| LockManagementPayload
| QueryPayload
| AckPayload
| HeartbeatPayload
| GovernancePayload;
interface MessageProof {
proofType: "FULL" | "INCREMENTAL" | "AGGREGATE" | "GKR";
zkProof?: string; // 0x-prefixed hex (zk proof)
merkleProof?: MerkleProof; // Merkle proof (optional)
proofMetadata?: object;
}
interface MerkleProof {
root: string;
leaf: string;
path: string[];
pathDirections: ("LEFT" | "RIGHT")[];
}
crossChainScope is present when the message targets multiple chains, and its content matches the CrossChainScope defined in the RegulatoryActionMessage payload above. proof is attached when the message carries cryptographic evidence: zk proofs (including GKR proofs from StateSync-GKR), Merkle proofs, or aggregate proofs.
Related Pages
- Data Model defines the enumerations, address system, and type primitives referenced in the header and payloads.
- Routing defines content-based routing, atomicity-driven coordination, and routing slip processing.
- State Management defines the lock protocol semantics for
LOCK_MANAGEMENT. - Validation defines the per-message-type validation pipelines and Driver Attestation verification.
- Regulatory Actions defines the six regulatory actions referenced in
REGULATORY_ACTION. - Cross-chain Protocol defines the three-phase broadcast that Phase ACK responds to.
- Fault Tolerance defines retry policies, idempotency, and DLQ.
- Error Reference lists every error code referenced on this page.
- Conformance aggregates the MUST / SHOULD / MAY classification of every requirement on this page.
- Open Issues records the items deferred or pending definition that this page references.
