This document specifies the covenant declaration API, where users declare policy functions and the compiler generates the corresponding covenant entrypoints and wrappers.
Without declarations, these patterns are written manually with OpAuth*/OpCov* plus readInputState/validateOutputState (or validateOutputStateWithTemplate for cross-template routing). The declaration layer standardizes that pattern, removes user boilerplate, and acts as a security guard so users do not need to be experts in covenant opcodes to write secure covenants.
Scope: syntax and lowering semantics.
- Dev writes only a transition/verification policy function and annotates it with a covenant macro.
- Entrypoint(s) are inferred by the compiler from that function’s shape.
- State is treated as one implicit
Statestruct synthesized from all contract fields:1:1usesState prev_state/State new_state1:NusesState prev_state/State[] new_statesN:MusesState[] prev_states/State[] new_states
1:Nauth always binds tothis.activeInputIndex;N:Mcov id is alwaysOpInputCovenantId(this.activeInputIndex).
Only policy functions are annotated.
Canonical form:
#[covenant(binding = auth|cov, from = X, to = Y, mode = verification|transition, groups = multiple|single, termination = disallowed|allowed)]Common form (with inferred defaults):
#[covenant(from = X, to = Y)]Sugar (aliases over from/to):
#[covenant.singleton] // == #[covenant(from = 1, to = 1)]
#[covenant.fanout(to = Y)] // == #[covenant(from = 1, to = Y)]Rules:
binding = authmeans auth-context lowering (OpAuth*).binding = covmeans shared covenant-context lowering (OpCov*).groupsapplies to both bindings.- Defaults:
auth -> groups = multiple,cov -> groups = single. - If
bindingis omitted:from == 1 -> auth, otherwisecov. - If
modeis omitted: no returns ->verification, has returns ->transition. binding = authwithfrom > 1is compile error.binding = covwithfrom = 1is allowed but emits a compiler warning recommendingbinding = auth.binding = covwithgroups = multipleis a compile error.terminationis valid only for singleton transition (from = 1, to = 1, mode = transition); there it defaults todisallowed, and using it elsewhere is a compile error.
#[covenant(binding = auth, from = 1, to = max_outs, mode = verification, groups = multiple)]
function split(State prev_state, State[] new_states, sig[] approvals) {
// require(...) rules
}#[covenant(binding = auth, from = 1, to = max_outs, mode = verification, groups = single)]
function split_single_group(State prev_state, State[] new_states, sig[] approvals) {
// require(...) rules
}contract C(int max_ins, int max_outs) {
int amount;
byte[32] owner;
int round;
#[covenant(binding = cov, from = max_ins, to = max_outs, mode = verification)]
function transition_ok(
State[] prev_states,
State[] new_states,
sig leader_sig
) {
// require(...) rules
}
}#[covenant(binding = cov, from = max_ins, to = max_outs, mode = transition)]
function transition(State[] prev_states, int fee) : (State[] new_states) {
// compute and return new_states
}#[covenant(binding = auth, from = 1, to = 1, mode = transition)]
function roll(State prev_state, byte[32] block_hash) : (State new_state) {
// compute and return next state
}Verification mode is the default convenience mode.
- Generated entrypoint args are
new_statesplus optional extra call args. - Wrapper reads prior state from tx context (
prev_stateorprev_states) and calls the policy verification with(prev_state(s), new_states, call_args...). - Wrapper enforces exact cardinality:
out_count == new_states.length. - Wrapper validates each output with
validateOutputState(...)againstnew_states.
Verification mode shape (mode = verification, both bindings):
- Policy params must begin with prior-state parameters:
binding = auth->State prev_statebinding = cov->State[] prev_states - Then comes
State[] new_states. - Remaining params are optional extra call args.
- Generated entrypoint exposes only
new_states+ extra args (not prior-state params). - Wrapper reconstructs/injects prior state from tx context:
authfrom current input state,covfrom covenant input set viareadInputState(...).
Transition mode allows extra call args (fee above, etc.) and the policy computes new_states.
Security note (both modes): extra call args (beyond state values validated on outputs) are not directly committed by tx structure. Compiler/runtime must enforce a commitment story and determinism for them.
Transition mode shape (mode = transition, both bindings):
- Policy params must begin with prior-state parameters:
binding = auth->State prev_statebinding = cov->State[] prev_states - Remaining params are optional extra call args.
- Compiler enforces this prefix exactly; invalid prior-state parameter types are compile errors.
- Wrapper sources prior state from tx context according to binding.
- Generated ABI behavior:
authentrypoint exposes only extra call args.covleader entrypoint exposesnew_statesor extra call args according to mode, while wrapper also enforces covenant structure checks.
Cardinality in transition mode:
- Single-state return shape -> exact one continuation (
out_count == 1) with directvalidateOutputState(...)(no loop). State[]return shape -> exact cardinality by returned length (out_count == returned_len) and per-output validation in a loop.- For singleton (
from=1,to=1),State[]returns are rejected by default. - Singleton
State[]returns are allowed only withtermination = allowed; this enables explicit zero-or-one continuation.
Default singleton transition is strict continuation:
#[covenant.singleton(mode = transition)]
function bump(State prev_state, int delta) : (State) {
return({ value: prev_state.value + delta });
}Termination-enabled singleton transition:
#[covenant.singleton(mode = transition, termination = allowed)]
function bump_or_terminate(State prev_state, State[] next_states) : (State[]) {
// [] => terminate
// [x] => continue with one successor
return(next_states);
}binding = auth, groups = multiple (default): no global uniqueness check across the tx.
binding = auth, groups = single: enforce that current covenant id has a single continuation auth group in this tx:
byte[32] cov_id = OpInputCovenantId(this.activeInputIndex);
require(OpCovOutputCount(cov_id) == OpAuthOutputCount(this.activeInputIndex));No explicit cov_id != false check is needed; OpCovOutputCount(cov_id) fails if cov_id is not valid covenant-id data.
binding = cov: groups = single only. groups = multiple is rejected.
Given policy function f:
-
1:Ngenerates one entrypoint:__f
-
N:Mgenerates two entrypoints:__leader_f__delegate_f
__delegate_f does not call policy. It enforces delegation-path invariants only.
pragma silverscript ^0.1.0;
contract VaultNM(
int max_ins,
int max_outs,
int init_amount,
byte[32] init_owner,
int init_round
) {
int amount = init_amount;
byte[32] owner = init_owner;
int round = init_round;
#[covenant(binding = cov, from = max_ins, to = max_outs, mode = verification)]
function conserve_and_bump(State[] prev_states, State[] new_states, sig leader_sig) {
require(new_states.length > 0);
int in_sum = 0;
for(i, 0, prev_states.length, max_ins) {
in_sum = in_sum + prev_states[i].amount;
}
int out_sum = 0;
for(i, 0, new_states.length, max_outs) {
out_sum = out_sum + new_states[i].amount;
// all outputs keep same owner as leader input
require(new_states[i].owner == prev_states[0].owner);
// round must advance exactly by 1
require(new_states[i].round == prev_states[0].round + 1);
}
require(in_sum >= out_sum);
}
}pragma silverscript ^0.1.0;
contract VaultNM(
int max_ins,
int max_outs,
int init_amount,
byte[32] init_owner,
int init_round
) {
int amount = init_amount;
byte[32] owner = init_owner;
int round = init_round;
// Compiler-lowered policy function (renamed to avoid collision with generated entrypoints)
// same body as source:
function __covenant_policy_conserve_and_bump(State[] prev_states, State[] new_states, sig leader_sig) { ... }
// Generated for N:M leader path
entrypoint function __leader_conserve_and_bump(State[] new_states, sig leader_sig) {
byte[32] cov_id = OpInputCovenantId(this.activeInputIndex);
int in_count = OpCovInputCount(cov_id);
int out_count = OpCovOutputCount(cov_id);
require(out_count == new_states.length);
// k=0 must execute leader path
require(OpCovInputIdx(cov_id, 0) == this.activeInputIndex);
State[] prev_states = [];
for(k, 0, in_count, max_ins) {
int in_idx = OpCovInputIdx(cov_id, k);
{
amount: int p_amount,
owner: byte[32] p_owner,
round: int p_round
} = readInputState(in_idx);
prev_states = prev_states.append({
amount: p_amount,
owner: p_owner,
round: p_round
});
}
__covenant_policy_conserve_and_bump(prev_states, new_states, leader_sig);
for(k, 0, out_count, max_outs) {
int out_idx = OpCovOutputIdx(cov_id, k);
validateOutputState(out_idx, {
amount: new_states[k].amount,
owner: new_states[k].owner,
round: new_states[k].round
});
}
}
// Generated for N:M delegate path
entrypoint function __delegate_conserve_and_bump() {
byte[32] cov_id = OpInputCovenantId(this.activeInputIndex);
// delegate path must not be leader
require(OpCovInputIdx(cov_id, 0) != this.activeInputIndex);
}
}State is seqcommit; call arg is block_hash.
pragma silverscript ^0.1.0;
contract SeqCommitMirror(byte[32] init_seqcommit) {
byte[32] seqcommit = init_seqcommit;
#[covenant(binding = auth, from = 1, to = 1, mode = transition)]
function roll_seqcommit(State prev_state, byte[32] block_hash) : (State new_state) {
byte[32] new_seqcommit = OpChainblockSeqCommit(block_hash);
return {
seqcommit: new_seqcommit
};
}
}pragma silverscript ^0.1.0;
contract SeqCommitMirror(byte[32] init_seqcommit) {
byte[32] seqcommit = init_seqcommit;
// Compiler-lowered policy function (renamed to avoid entrypoint name collision)
// same body as source:
function __covenant_policy_roll_seqcommit(State prev_state, byte[32] block_hash) : (State new_state) { ... }
// Generated 1:1 covenant entrypoint
entrypoint function __roll_seqcommit(byte[32] block_hash) {
State prev_state = {
seqcommit: seqcommit
};
(State new_state) = __covenant_policy_roll_seqcommit(prev_state, block_hash);
require(OpAuthOutputCount(this.activeInputIndex) == 1);
int out_idx = OpAuthOutputIdx(this.activeInputIndex, 0);
validateOutputState(out_idx, {
seqcommit: new_state.seqcommit
});
}
}Stateis an implicit compiler type synthesized from contract fields.- Internally the compiler can lower
State/State[]into any representation; this doc only fixes the user-facing API. - Existing
readInputState/validateOutputStateremain the codegen backbone;validateOutputStateWithTemplateis available for manual cross-template routing, not declaration lowering. N:Mlowering keeps one transition group per transaction.