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.

SEIZE needs the exact completed FREEZE; CONFISCATE needs the exact SEIZE, an elapsed deadline and a destination. An enrolled asset starts in mode Active, and every order is checked at source emission and again at receiving commit. FREEZE moves the asset to Frozen. SEIZE moves Frozen to Seized and is refused as MissingPriorFreeze when no active FREEZE is named. CONFISCATE moves Seized to the terminal mode Confiscated and is refused, in this order, as MissingPriorSeizure when no active SEIZE is named, as AppealDeadlineMismatch when the deadline is missing or not the recorded one, as AppealStillOpen before the recorded deadline and as MissingDestination when no destination is named. Each refusal is recorded and leaves the mode unchanged. UNFREEZE and RELEASE return the asset to Active when they name the active FREEZE or SEIZE, RESTRICT and UNRESTRICT move between Active and Restricted, and Frozen, Seized and Confiscated are protected modes in which ordinary spending is rejected. 1 every order Protected modes Active RESTRICT UNRESTRICT Restricted 5 FREEZE UNFREEZE RELEASE 4 Frozen SEIZE 2 no active FREEZE namedMissingPriorFreeze Seized CONFISCATE 3 no active SEIZE namedMissingPriorSeizure deadline missing or not the recorded oneAppealDeadlineMismatch before the recorded deadlineAppealStillOpen no destinationMissingDestination Confiscated
  1. 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. 2

    The FREEZE comes first

    SEIZE must name the active FREEZE of the same asset. A RESTRICT record never stands in for it.

  3. 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. 4

    Exact reversal

    UNFREEZE and RELEASE must name the active FREEZE or SEIZE they reverse, and both return the asset to Active.

  5. 5

    Restriction

    RESTRICT applies only to an Active asset and keeps an existing reservation. UNRESTRICT returns it to Active.

  • 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.

Figure 1: Legal modes of an asset and the checks that each escalation must pass

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:

RustObserve trusted regulatory timesrc/authority/regulation.rsL865-L893Highlighted lines are discussed in the textSource (opens the lines on GitHub in a new tab)
/// 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:

RustSigned time observationsrc/authority/recovery/wire/ed25519/maintenance.rsL151-L163Highlighted lines are discussed in the textSource (opens the lines on GitHub in a new tab)
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

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:

RustLegal state transitionssrc/authority/regulation.rsL453-L502Highlighted lines are discussed in the textSource (opens the lines on GitHub in a new tab)
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.

TextSeizure and confiscation vectorstests/vectors/regulatory-execution.tsvL5-L9Source (opens the lines on GitHub in a new tab)

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_appealappeal-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_confiscationunpermitted-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_entriesconfiscation-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_domainsrestrict-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:

RustProtected modessrc/authority/regulation.rsL37-L50Highlighted lines are discussed in the textSource (opens the lines on GitHub in a new tab)
#[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:

RustOrdinary and enforcement spendingsrc/authority/regulation.rsL643-L682Highlighted lines are discussed in the textSource (opens the lines on GitHub in a new tab)
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:

RustExact confirmed rootsrc/authority/regulation.rsL683-L718Highlighted lines are discussed in the textSource (opens the lines on GitHub in a new tab)
    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.

TextSpending after a freezetests/vectors/regulatory-execution.tsvL12-L13Source (opens the lines on GitHub in a new tab)

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

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:

JSONEmitLegalOrderspec/operations.jsonL403-L434Highlighted lines are discussed in the textSource (opens the lines on GitHub in a new tab)
{  "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:

RustFinite appeal deadlinesrc/wire/operands.rsL315-L320Source (opens the lines on GitHub in a new tab)
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:

RustElapsed appeal deadlinesrc/authority/regulation.rsL442-L451Highlighted lines are discussed in the textSource (opens the lines on GitHub in a new tab)
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:

TextShortened deadlinetests/vectors/regulatory-execution.tsvL16-L16Source (opens the lines on GitHub in a new tab)

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:

JSONLegal refusal codesspec/operations.jsonL181-L189Source (opens the lines on GitHub in a new tab)
{"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:

RustRefusal to response codesrc/authority/recovery/wire/response.rsL334-L357Highlighted lines are discussed in the textSource (opens the lines on GitHub in a new tab)
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.

TextRefused on every routetests/vectors/regulatory-execution.tsvL2-L4Source (opens the lines on GitHub in a new tab)

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_confiscationconfiscate-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_confiscationconfiscate-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.


Edition history#

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