Covenants¶
A covenant is a contract that carries its state from one UTXO to the next.
The state lives in the locking script, so each state results in its own P2SH address (see Compiling → Constructor args embed state). A transition spends the UTXO at the current address and creates a new output at a new address (which is derived from the newly created covenant state).
Calling a covenant entrypoint¶
Spend a covenant entrypoint with
build_sig_script_for_covenant_decl,
not
build_sig_script. It finds the covenant's
declaration entrypoint and builds the unlocking script for it:
contract = silverscript.compile(SOURCE, [current_count])
call = contract.build_sig_script_for_covenant_decl("add", [amount])
The is_leader keyword (default False) matters only for covenants that
spend several UTXOs of the same covenant in one transition and pick one
input as the leader. The leader input carries the entrypoint's
arguments and runs the covenant's logic; the other inputs are
delegates — they take no arguments and only prove they belong to the
same covenant, deferring to the leader. Build the leader's unlocking
script with is_leader=True and each delegate's with is_leader=False.
The Counter here never needs it. Its binding = auth compiles to a single
entrypoint with no leader/delegate split, so is_leader is ignored — you
reach for is_leader=True only when building the leader input of a
multi-input covenant (binding = cov).
The rest is the same as a plain spend: the input's signature_script is
the covenant call followed by the pushed redeem script (see
Unlocking Scripts → Spending a locked UTXO).
State transition via transactions¶
Covenant state advances (transitions) via transactions on the Kaspa network. Actual on-chain side of a covenant is core kaspa work, not the compiler's.
A transition is a transaction that:
- Spends the current covenant UTXO, with the covenant call as its
signature_script. - Creates a new output at the next state's address, carrying the
covenant forward with a
CovenantBindingso the chain keeps the same covenant id.
The first transaction (genesis) locks an ordinary funding UTXO into the
initial state. It derives the covenant id with
populate_genesis_covenants and a
GenesisCovenantGroup.
Every later transition reuses that id.
Worked example: Counter¶
examples/silverscript/counter.py
runs the whole loop live on testnet-10. The contract holds one count
in covenant state. Each spend is a 1:1 transition that updates the count
and re-locks the funds:
The contract:
pragma silverscript ^0.1.0;
contract Counter(int init_count) {
int count = init_count;
#[covenant(binding = auth, from = 1, to = 1, mode = transition)]
function add(State prev_state, int amount) : (State) {
return({ count: prev_state.count + amount });
}
#[covenant(binding = auth, from = 1, to = 1, mode = transition)]
function subtract(State prev_state, int amount) : (State) {
require(prev_state.count - amount >= 0);
return({ count: prev_state.count - amount });
}
}
A few things from the example worth noting:
- State is in the address.
count = 0andcount = 5are different scripts at different addresses. The example re-derives each address from the source and the count alone. - No signing on the transition. The Counter checks no signature, so the unlocking script is just the covenant call plus the redeem script — the transition is permissionless. Only the genesis funding input (a normal P2PK UTXO) is signed.
- The covenant id carries over. Genesis derives it; each transition
binds the new output to it with a
CovenantBinding.
The compiler's role is small: compile the contract at the current count,
and build the covenant call. Transaction building, fee sizing, covenant
binding, and submission are all core kaspa — see
Transactions.
Experimental
Covenants are the newest and least-settled part of SilverScript. Treat the contract above as a teaching example, not a template for production value. Verify everything on a test network first.