Protocol Specification v0.6
Regulatory Actions
A legal order has two guarded stages: source emission and receiving commitment. Both use the same legal admission rules at their own current cuts (opens in a new tab). A prior emitted message alone cannot authorize a receiving effect, and a transport route cannot bypass the final legal guard.
Legal escalation
SEIZE needs the exact completed FREEZE; CONFISCATE needs the exact SEIZE, an elapsed deadline and a destination.
- 1
Checked twice
Every order passes these checks at source emission and again at receiving commit, each at its own current cut. The mode changes at the commit.
- 2
The FREEZE comes first
SEIZE must name the active FREEZE of the same asset. A RESTRICT record never stands in for it.
- 3
Four checks in order
The active SEIZE, the deadline recorded with it, trusted time at or after that deadline, then a destination. The first failing check is the reply.
- 4
Exact reversal
UNFREEZEandRELEASEmust name the active FREEZE or SEIZE they reverse, and both return the asset toActive. - 5
Restriction
RESTRICT applies only to an
Activeasset and keeps an existing reservation.UNRESTRICTreturns it toActive.
- Escalation, one exact step at a time
- Return to
Active - Refused with this reason and recorded; the mode does not change
- Mode at enrollment
- Terminal mode
- Protected mode: ordinary spending is rejected
Schematic: the authority, binding and current-cut checks that every order passes first are not drawn. Any other action from a mode is refused as InvalidState, except that SEIZE outside Frozen answers MissingPriorFreeze and CONFISCATE outside Seized answers MissingPriorSeizure.
Trusted reference time#
NormativeBytes, test vectors and proofs
A time-enabled reference profile (opens in a new tab) selects its first trusted Unix-seconds observation before admitting lease and regulatory writes. Later observations use the same clock and exact current witness cut. An authorized clock observer signs the actual observation and the product checks current permission again before publication.
An ordinary elapsed-time command cannot advance this clock or open an appeal deadline (opens in a new tab). Caller timeout leaves any pending balance and source obligation in place.
Only a clock observer registered for the current authority epoch (opens in a new tab), at the exact current cut, can move the regulatory clock:
/// The verifier is a trusted time-source adapter, not a client clock.pub fn observe_regulatory_time( &mut self, actor: AuthenticatedCaller, session: SessionId, expected: CommitCut, now: u64, verify: impl FnOnce(AuthenticatedCaller, CommitCut, u64) -> bool,) -> bool { let current = &self.authority.state.primary; if !self.available() || expected != self.current_cut() || !current.ledger.session_bound(actor, session) || !current .regulation .policy .clock_observers .contains(&(actor.0, expected.authority_epoch)) || !verify(actor, expected, now) { return false; } let mut next = current.clone(); if !next.ledger.observe_clock(now) { return false; } self.authority.commit_primary(next); true}The time-source adapter accepts an observation only when the observer signed that exact time at that cut with the key registered for time:
pub(super) fn verify_time<C: AuthorityClock>( security: &Ed25519Security<C>, actor: Principal, cut: CommitCut, observed: u64, signature: &[u8],) -> bool { decode_signed(signature).is_ok_and(|signed| { signed.request.principal == actor && signed.request.cut == cut && matches!(signed.request.action, Action::ObserveTime { observed: time, .. } if time == observed) && verify(security, &signed) })}Regulatory entry#
NormativeBytes, test vectors and proofs
- The source emission and the receiving commitment MUST apply the same legal admission rules at their respective current authoritative cuts. Normal, administrative, adapter, and force-inclusion (opens in a new tab) entry points MUST use the same final guard.
- An order MUST bind its original event (opens in a new tab), action, asset and exact scope. Its authority, legal basis, and jurisdiction MUST be verified and authorized at the cut where the order is consumed. A confiscation order MUST identify its exact destination.
- SEIZE (opens in a new tab) requires a completed FREEZE (opens in a new tab) for the same asset and an exact reference to that event. A RESTRICT (opens in a new tab) event does not establish that provenance.
- CONFISCATE (opens in a new tab) requires the current SEIZED state, the exact prior SEIZE and its escalation (opens in a new tab) provenance, confiscation authority, and an elapsed appeal deadline. A timestamp at or after the committed deadline satisfies the elapsed-deadline check.
- UNFREEZE (opens in a new tab) and RELEASE (opens in a new tab) require current authority and the exact active freeze or seizure they reverse. UNRESTRICT (opens in a new tab) requires a current restricted state and authorization.
- A permitted RESTRICT MAY act on a reserved asset and MUST preserve the reservation. Refused orders MUST leave authoritative status, financial balances, supply, and existing terminal records unchanged and report a distinct reason.
One function decides every legal transition. SEIZE needs the active FREEZE as its exact predecessor, and CONFISCATE needs the active SEIZE, the deadline recorded with it and a destination:
fn transition(&self, order: &Order, now: u64) -> Result<Mode, Refusal> { let b = order.binding; let source = self.modes.get(&(b.origin.domain, b.asset)); let receiver = self.modes.get(&(b.destination, b.asset)); let mode = source .filter(|_| source == receiver) .copied() .ok_or(Refusal::InvalidState)?; match order.action { Action::Freeze if mode == Mode::Active => Ok(Mode::Frozen), Action::Restrict if mode == Mode::Active => Ok(Mode::Restricted), Action::Seize => { if mode != Mode::Frozen || order.predecessor.is_none() || order.predecessor != self.active_freeze(b) { return Err(Refusal::MissingPriorFreeze); } Ok(Mode::Seized) } Action::Confiscate => { let (key, seizure) = self.active_seizure(b).ok_or(Refusal::MissingPriorSeizure)?; if mode != Mode::Seized || order.predecessor != Some(key) { return Err(Refusal::MissingPriorSeizure); } Self::deadline(seizure, order.appeal_deadline, now)?; if order.destination.is_none() { return Err(Refusal::MissingDestination); } Ok(Mode::Confiscated) } Action::Unfreeze if mode == Mode::Frozen && order.predecessor.is_some() && order.predecessor == self.active_freeze(b) => { Ok(Mode::Active) } Action::Unrestrict if mode == Mode::Restricted => Ok(Mode::Active), Action::Release if mode == Mode::Seized && self .active_seizure(b) .is_some_and(|(key, _)| order.predecessor == Some(key)) => { Ok(Mode::Active) } _ => Err(Refusal::InvalidState), }}The regulatory vectors, which the Rust tests run, try these checks on one asset. A seized asset without the matching SEIZE record, an order one second before the recorded deadline and an officer without confiscation permission are each refused. The permitted order applies at the deadline, and a RESTRICT record never stands in for the FREEZE reference.
Columnsidoperationinputsexpectedevidence
seized-without-matching-record confiscate actor=7;epoch=9;asset=17;source=0;receiver=2;event=42;seizure=41;freeze=40;deadline=5;destination=3;jurisdiction=1;legal_basis=1;mode=seized;now=5;supplied_predecessor=99 reply=missing_prior_seizure;authority_unchanged=true seized_state_without_the_seizure_record_is_insufficient,confiscation_requires_current_seizure_and_elapsed_appeal appeal-still-open confiscate actor=7;epoch=9;asset=17;source=0;receiver=2;event=42;seizure=41;freeze=40;deadline=5;destination=3;jurisdiction=1;legal_basis=1;mode=seized;now=4 reply=appeal_still_open;authority_unchanged=true actual_legal_callback_stops_an_early_confiscation,removing_only_the_appeal_check_admits_the_same_early_confiscation unpermitted-confiscation-authority confiscate actor=7;epoch=9;asset=17;source=0;receiver=2;event=42;seizure=41;freeze=40;deadline=5;destination=3;jurisdiction=1;legal_basis=1;mode=seized;now=5;grant=false reply=invalid_authority;authority_unchanged=true authority_refusal_is_distinct_even_after_the_appeal_deadline,exact_seizure_and_elapsed_deadline_activate_both_legal_entries confiscation-at-the-elapsed-deadline confiscate actor=7;epoch=9;asset=17;source=0;receiver=2;event=42;seizure=41;freeze=40;deadline=5;destination=3;jurisdiction=1;legal_basis=1;mode=seized;now=5;grant=true reply=applied;source_mode=confiscated;receiver_mode=confiscated exact_seizure_and_elapsed_deadline_activate_both_legal_entries,the_public_transition_applies_lawful_seized_confiscation_to_both_domains restrict-is-not-freeze-provenance seize actor=7;epoch=9;asset=17;source=0;receiver=2;seizure=41;freeze=40;deadline=5;destination=3;jurisdiction=1;legal_basis=1;mode=frozen;event=41;supplied_predecessor=44;predecessor_kind=restrict reply=missing_prior_freeze;authority_unchanged=true restriction_record_does_not_substitute_for_the_freeze_reference,restriction_provenance_cannot_authorize_seizure Spending under a completed freeze#
NormativeBytes, test vectors and proofs
- After a completed FREEZE, ordinary spending MUST be rejected while the authoritative freeze protection remains in force. Current regulatory reads use the current published state and its cut.
- A lawful enforcement transfer MUST consume its exact confirmed root, the current enforcement authority and scope, and the bound asset, parties and amount. It remains executable under those conditions even while ordinary spending is blocked.
FROZEN, SEIZED and CONFISCATED are the protected modes:
#[derive(Clone, Copy, Debug, Eq, PartialEq, serde::Serialize, serde::Deserialize)]pub enum Mode { Active, Restricted, Frozen, Seized, Confiscated,}impl Mode { pub(super) fn protected(self) -> bool { matches!(self, Self::Frozen | Self::Seized | Self::Confiscated) }}In a protected mode an ordinary spend is rejected. An enforcement spend needs an order with the same scope and officer, a nonzero legal basis and the spent asset among its assets:
fn apply_spend(&mut self, call: &SpendCall<'_>) -> SpendReply { let payload = call.payload; let scope = payload.scope; let root = scope.root; let cut = self.ledger.current_cut(); if call.expected != cut || payload.cut != cut { return SpendReply::Busy; } let Some(&mode) = self.regulation.modes.get(&(root.destination, root.asset)) else { return SpendReply::Rejected; }; let (jurisdiction, legal_basis) = match (scope.kind, call.enforcement) { (SpendKind::Ordinary, None) if !mode.protected() => (0, 0), (SpendKind::Ordinary, _) => return SpendReply::Rejected, (_, Some(order)) if order.scope == scope && order.officer == call.actor.0 && order.legal_basis != 0 && order.assets.contains(&root.asset) => { if scope.kind == SpendKind::Confiscate { let Some((key, seizure)) = self.regulation.active_seizure(root) else { return SpendReply::Rejected; }; if !matches!(mode, Mode::Seized | Mode::Confiscated) || order.prior_seizure != Some(key) || RegulationState::deadline( seizure, order.appeal_deadline, self.ledger.clock(), ) .is_err() { return SpendReply::Rejected; } } (order.jurisdiction, order.legal_basis) } _ => return SpendReply::Rejected, };Before any balance moves, the spend must also hold a grant for its route and consume the exact confirmed, settled and published root:
if !self.regulation.policy.spend.contains(&SpendGrant { route: call.route, actor: call.actor.0, epoch: cut.authority_epoch, scope, jurisdiction, legal_basis, }) { return SpendReply::Rejected; } let Some(slot) = self.origins.get(&root.origin) else { return SpendReply::Rejected; }; let Some(decision) = slot.decision else { return SpendReply::Rejected; }; if slot.binding != root || slot.epoch != payload.origin_epoch || decision.kind != TerminalKind::Confirmed || decision.commit != payload.reference || slot.settled.is_none() || slot.published.is_none() { return SpendReply::Rejected; } match self .ledger .spend_funding(root, scope.sender, scope.recipient, scope.amount) { Some(position) => SpendReply::Executed { root: root.origin, commit: CommitRef { position }, }, None => SpendReply::Rejected, }}The regulatory vectors freeze an asset and then try both kinds of spend: the ordinary spend is rejected, and the recovery spend executes while the asset stays frozen.
Columnsidoperationinputsexpectedevidence
ordinary-spend-after-freeze ordinary-spend actor=7;epoch=9;asset=20;source=0;receiver=2;root_event=17;freeze_event=20;sender=3;recipient=4;initial=10;root_amount=5;amount=1 reply=rejected;sender_balance=5;recipient_balance=0 actual_ordinary_rejection_after_freeze,ordinary_spend_after_freeze_without_thaw_is_rejected,using_the_old_configured_policy_would_execute_the_frozen_ordinary_spend lawful-recovery-spend-after-freeze enforcement-spend actor=7;epoch=9;asset=20;source=0;receiver=2;root_event=17;freeze_event=20;sender=3;recipient=4;initial=10;root_amount=5;amount=1;kind=recover;jurisdiction=1;legal_basis=1;authorized=true reply=executed;sender_balance=4;recipient_balance=1;root_consumed=17;mode=frozen the_lawful_transfer_in_the_frozen_state_consumes_its_confirmed_root,enforcement_changes_only_lawful_funding_and_keeps_freeze Legal order layout#
NormativeBytes, test vectors and proofs
EmitLegalOrder and CommitLegalOrder carry the transfer binding, action, officer, jurisdiction, legal basis, issuance epoch and counted exact scope. Optional predecessor and confiscation destination fields carry exact provenance. Field Tables gives their positions and accepted action codes.
The operation table entry lists these words in order, with the two optional groups after the counted scope, and gives the order the appeal deadline mode:
{ "family": 2, "opcode": 1, "name": "EmitLegalOrder", "title": "emit legal order", "summary": "Legal order at the source emission stage.", "reference_accepts": true, "words": [ {"kind": "use", "group": "call_prefix"}, {"kind": "word", "name": "action", "type": "code", "enum": "legal_action", "accepted": "all"}, {"kind": "word", "name": "officer", "type": "principal_id"}, {"kind": "word", "name": "jurisdiction", "type": "u64", "description": "Jurisdiction code of the order."}, {"kind": "word", "name": "legal_basis", "type": "u64", "description": "Legal basis code of the order."}, {"kind": "word", "name": "issuance_epoch", "type": "epoch"}, {"kind": "counted_set", "name": "scope", "item": {"type": "asset_id"}}, {"kind": "optional", "name": "predecessor", "description": "Origin of the preceding order.", "fields": [ {"kind": "word", "name": "domain", "type": "domain_id"}, {"kind": "word", "name": "event", "type": "event_id"} ]}, {"kind": "optional", "name": "confiscation_destination", "fields": [ {"kind": "word", "name": "principal", "type": "principal_id"} ]} ], "blobs": [ {"name": "amount", "type": "amount_u256"}, {"name": "order_authorization", "type": "proof", "description": "Officer authorization of the exact order, verified by the configured legal adapter."} ], "deadline": "appeal", "target_domain": "origin_domain", "checks": [ {"when": "decode", "rule": "issuance_epoch equals origin_epoch.", "error": "INVALID_SOURCE"} ], "call": true, "recorded_codes": [1, 2, 3, 26, 30, 31, 32, 33, 34, 35, 36, 37, 38]},The appeal deadline uses the appeal_deadline alternative on the regulatory operation. Its finite value is UTC seconds. Delivery TTL (opens in a new tab) and appeal deadline are different inputs. A message timeout cannot open a legal deadline or establish reversal. Absent and indefinite appeal deadlines have the same product meaning in this edition while retaining different bytes.
The operand decoder keeps only the finite form as a deadline value, so the absent and the indefinite form reach the product alike:
fn deadline(op: &Operation) -> Option<u64> { match op.appeal_deadline { Some(AppealDeadline::AppealDeadlineUtcSeconds(value)) => Some(value), _ => None, }}A confiscation must repeat the deadline recorded with the seizure, and trusted time must have reached it:
fn deadline(recorded: &Order, supplied: Option<u64>, now: u64) -> Result<(), Refusal> { let deadline = supplied.ok_or(Refusal::AppealDeadlineMismatch)?; if recorded.appeal_deadline != Some(deadline) { return Err(Refusal::AppealDeadlineMismatch); } if now < deadline { return Err(Refusal::AppealStillOpen); } Ok(())}In the reference, a seizure recorded without a finite deadline cannot be confiscated: every confiscation of it answers AppealDeadlineMismatch.
A confiscation also cannot shorten the recorded deadline. In this vector the order names an earlier deadline that trusted time has already reached, and it is still refused as a mismatch:
Columnsidoperationinputsexpectedevidence
shortened-appeal-deadline confiscate actor=7;epoch=9;asset=17;source=0;receiver=2;event=42;seizure=41;freeze=40;deadline=4;recorded_deadline=5;destination=3;jurisdiction=1;legal_basis=1;mode=seized;now=4;grant=true;routes=normal,administrative,adapter,force_inclusion reply=appeal_deadline_mismatch;authority_unchanged=true actual_legal_callback_rejects_a_shortened_deadline,removing_only_the_recorded_deadline_binding_admits_the_shortened_request,shortened_appeal_deadline_cannot_authorize_confiscation Refused orders#
NormativeBytes, test vectors and proofs
A refusal leaves financial balances, supply, regulatory status and terminal records unchanged. The response reports the distinct reason, including missing destination or unelapsed appeal deadline. A corrected order must still satisfy the current cut and applicable call/retry rules.
The legal refusal reasons have their own response codes:
{"code": 30, "name": "LEGAL_INVALID_AUTHORITY", "kind": 1, "commit": false},{"code": 31, "name": "LEGAL_INVALID_STATE", "kind": 1, "commit": false},{"code": 32, "name": "LEGAL_INVALID_BINDING", "kind": 1, "commit": false},{"code": 33, "name": "LEGAL_MISSING_PRIOR_FREEZE", "kind": 1, "commit": false},{"code": 34, "name": "LEGAL_MISSING_PRIOR_SEIZURE", "kind": 1, "commit": false},{"code": 35, "name": "LEGAL_APPEAL_STILL_OPEN", "kind": 1, "commit": false},{"code": 36, "name": "LEGAL_APPEAL_DEADLINE_MISMATCH", "kind": 1, "commit": false},{"code": 37, "name": "LEGAL_MISSING_DESTINATION", "kind": 1, "commit": false},{"code": 38, "name": "LEGAL_CONFLICTING_ORDER", "kind": 1, "commit": false},The reference maps every refusal to a code, and a busy or unavailable authority answers with the shared BUSY and UNAVAILABLE codes instead of a legal code:
fn legal(reply: LegalReply) -> Self { use ResponseCode::*; let (code, commit) = match reply { LegalReply::Emitted(commit) => (LegalEmitted, Some(commit)), LegalReply::Applied(commit) => (LegalApplied, Some(commit)), LegalReply::Refused(reason) => ( match reason { Refusal::InvalidAuthority => LegalInvalidAuthority, Refusal::InvalidState => LegalInvalidState, Refusal::InvalidBinding => LegalInvalidBinding, Refusal::MissingPriorFreeze => LegalMissingPriorFreeze, Refusal::MissingPriorSeizure => LegalMissingPriorSeizure, Refusal::AppealStillOpen => LegalAppealStillOpen, Refusal::AppealDeadlineMismatch => LegalAppealDeadlineMismatch, Refusal::MissingDestination => LegalMissingDestination, Refusal::ConflictingOrder => LegalConflictingOrder, Refusal::Busy => Busy, Refusal::Unavailable => Unavailable, }, None, ), }; Self { code, commit }}The regulatory vectors send a confiscation from an active, a frozen and a restricted asset on all four routes. Each is refused and leaves modes, balances and terminal records unchanged; only the refusal is retained for an exact retry.
Columnsidoperationinputsexpectedevidence
confiscate-from-active confiscate actor=7;epoch=9;asset=17;source=0;receiver=2;event=42;seizure=41;freeze=40;deadline=5;destination=3;jurisdiction=1;legal_basis=1;mode=active;now=5;routes=normal,administrative,adapter,force_inclusion reply=missing_prior_seizure;authority_unchanged=true both_legal_entries_reject_confiscation_from_active_frozen_or_restricted,governed_legal_rejection_preserves_source_receiver_and_terminal_state,the_public_transition_without_the_legal_gate_accepts_active_confiscation confiscate-from-frozen confiscate actor=7;epoch=9;asset=17;source=0;receiver=2;event=42;seizure=41;freeze=40;deadline=5;destination=3;jurisdiction=1;legal_basis=1;mode=frozen;now=5;routes=normal,administrative,adapter,force_inclusion reply=missing_prior_seizure;authority_unchanged=true both_legal_entries_reject_confiscation_from_active_frozen_or_restricted,governed_legal_rejection_preserves_source_receiver_and_terminal_state,the_public_transition_without_the_legal_gate_accepts_active_confiscation confiscate-from-restricted confiscate actor=7;epoch=9;asset=17;source=0;receiver=2;event=42;seizure=41;freeze=40;deadline=5;destination=3;jurisdiction=1;legal_basis=1;mode=restricted;now=5;routes=normal,administrative,adapter,force_inclusion reply=missing_prior_seizure;authority_unchanged=true both_legal_entries_reject_confiscation_from_active_frozen_or_restricted,governed_legal_rejection_preserves_source_receiver_and_terminal_state,the_public_transition_without_the_legal_gate_accepts_active_confiscation Regulatory architecture context#
DesignCarried from v0.5, without bytes or test vectors in this version
The protocol's broad regulatory vocabulary derives from Regulatory Compliance. This edition implements the listed legal state and evidence guards. A validated order does not independently establish that an off-chain actor supplied a legally valid authorization; the configured legal adapter and trusted registrations supply that premise.
Related Pages#
Edition history#
- 0.6.0: Added this edition. The v0.5 specification remains available at its original addresses.
