Covenant State
How Toccata covenants encode state, validate transitions, use covenant IDs, and scale from single contracts to multi-contract flows.
Covenants are easiest to understand by following one UTXO through a spend.
Suppose you want a counter. The policy is simple: only increase the number, and never increase it by more than max_step.
pragma silverscript ^0.1.0;
contract Counter(int init_count, int max_step) {
int count = init_count;
#[covenant.singleton(mode = transition)]
function increment(State prev_state, int step) : (State) {
require(step > 0);
require(step <= max_step);
return({ count: prev_state.count + step });
}
}Declaration structure:
init_countandmax_stepare constructor arguments. They are compile-time values for this contract instance: once the script instance is built, they are constants baked into the redeem script.countis a state field. Its current value is encoded in the redeem-script preimage, and a new value means a new P2SH commitment.
The interesting part is where count lives.
It is not an account variable. It is not a node-side object. It is data inside the redeem-script preimage of the current UTXO.
The P2SH state pattern
The live output is compact:
script_public_key = P2SH(hash(redeem_script_0))The hidden preimage is where the app lives:
redeem_script_0 = push state_0
push contract constants
contract codeWhen the UTXO is spent, the transaction reveals redeem_script_0. The pushed state becomes a constant for this one script execution. The script then uses the transaction's arguments and outputs to decide whether the spend is allowed.
flowchart TB
classDef chain fill:#eef6ff,stroke:#4b82c3,color:#172033
classDef witness fill:#fff7e8,stroke:#c58b2a,color:#1f1b12
classDef script fill:#eef9ef,stroke:#4d9f5b,color:#132016
classDef fail fill:#fff0f0,stroke:#c65d5d,color:#2b1111
U0["UTXO before spend<br/>P2SH(hash(redeem_script_0))"]:::chain
Open["signature script reveals<br/>redeem_script_0 + args"]:::witness
Read["script reads<br/>state_0 from pushdata"]:::script
Step["compute proposed<br/>state_1"]:::script
Build["construct expected<br/>redeem_script_1"]:::script
Check{"does an output commit<br/>to redeem_script_1?"}:::script
U1["UTXO after spend<br/>P2SH(hash(redeem_script_1))"]:::chain
Bad["invalid spend"]:::fail
U0 --> Open --> Read --> Step --> Build --> Check
Check -->|"yes"| U1
Check -->|"no"| BadThat is the covenant loop:
- The chain stores a short commitment.
- The spend opens the old contract and old state.
- The script computes the allowed next state.
- The script verifies that the transaction recreates the contract with that next state.
The fourth step is the part older script systems struggle with. A covenant has to build or inspect enough bytes to know what the next output means. Toccata's byte and hash tools, including OpCat, OpSubstr, OpBlake2b, OpBlake3, and output introspection, are what make this pattern practical.
What the macro expands into
Silverscript covenant declarations are designed to hide the repetitive part. The compiler lowers the Counter transition above into an ordinary entrypoint plus a policy function. The generated helper names are shown here in readable form:
contract Counter(int init_count, int max_step) {
int count = init_count;
function covenant_policy_increment(State prev_state, int step) : (State) {
require(step > 0);
require(step <= max_step);
return({ count: prev_state.count + step });
}
entrypoint function increment(int step) {
int auth_out_count = OpAuthOutputCount(this.activeInputIndex);
State prev_state = { count: count };
(State cov_new_state) = covenant_policy_increment(prev_state, step);
require(auth_out_count == 1);
int cov_out_idx = OpAuthOutputIdx(this.activeInputIndex, 0);
validateOutputState(cov_out_idx, cov_new_state);
}
}The wrapper does the transaction work around the policy:
countis the current state value pushed by the redeem-script preimage;covenant_policy_incrementis the user-written transition body, moved into a helper;entrypoint function increment(int step)is the generated spend entrypoint selected by the transaction;OpAuthOutputCountandOpAuthOutputIdxselect the output authorized by this input;require(auth_out_count == 1)enforces the singleton transition;validateOutputState(cov_out_idx, cov_new_state)checks the successor output.
That last call uses a reflection-like awareness of the current script. The spent script uses its own revealed bytes as the template for the successor. The compiler knows the layout of the contract currently being spent. It emits code that takes the current redeem script preimage, replaces the encoded state fields with cov_new_state, rebuilds the expected P2SH script public key, and compares it to the authorized output.
So the output is not merely "some covenant output with the same id." It must commit to this same contract template, with the state fields changed exactly as the transition computed.
Why the hash keeps changing
The P2SH hash commits to both contract and state. That supports state validation, but it creates a naming problem.
flowchart LR
classDef p fill:#eef6ff,stroke:#4b82c3,color:#172033
S0["P2SH(contract + state_0)"]:::p
S1["P2SH(contract + state_1)"]:::p
S2["P2SH(contract + state_2)"]:::p
S0 -->|"valid spend"| S1
S1 -->|"valid spend"| S2That changing P2SH commitment is useful: it is how the current state is committed. But it is a poor stable name for the application instance. Wallets, explorers, indexers, and other contracts need a handle that survives state transitions.
There is a second, separate issue. A transaction must not be able to mint a new UTXO that merely claims to be part of an existing covenant family. Script-only covenant designs often handle this by carrying parent or grandparent transaction data and proving the relationship inside the script. That can make forged successors unspendable later, but it does not give consensus a native rule for rejecting the forged covenant-labeled output at creation time.
KIP-20 gives Toccata that native rule.
Covenant IDs
KIP-20 adds a covenant label to UTXOs.
CovenantBinding {
authorizing_input: u16,
covenant_id: Hash,
}An output may declare that it belongs to a covenant. When it becomes a UTXO, the UTXO entry carries the same covenant_id.
Consensus enforces covenant membership at output creation:
- a continuation output may carry covenant id
idonly if itsauthorizing_inputspends a UTXO that already carriesid; - a genesis output may carry
idonly if the id is derived from the authorizing outpoint and the authorized initial outputs.
The script enforces the transition rule:
- the covenant input must verify the next state, output layout, amounts, and bindings.
Those two halves are easy to blur, but they do different jobs.
flowchart LR
classDef consensus fill:#eef6ff,stroke:#4b82c3,color:#172033
classDef script fill:#eef9ef,stroke:#4d9f5b,color:#132016
classDef out fill:#fff7e8,stroke:#c58b2a,color:#1f1b12
C["consensus<br/>may this covenant-labeled output exist?"]:::consensus
S["script<br/>is this the transition my program allows?"]:::script
O["next covenant UTXO<br/>same covenant_id, new P2SH"]:::out
C --> O
S --> OConsensus blocks unauthorized entrance into a covenant family. The script checks whether the next state is actually valid.
This is also why a covenant id on an ordinary P2PK-style output is not sufficient by itself. If the spending script only checks a signature, then it only checks a signature. Covenant behavior appears when the script actually reads covenant data and validates covenant outputs.
Auth groups and covenant groups
A transaction can carry several covenant flows at once. It can also include ordinary inputs and outputs for fees, change, routing, or batching. Scripts therefore need a clean way to ask two different questions.
The auth question is local:
Which outputs did this input authorize?
The shared covenant question is family-wide:
Which inputs and outputs in this transaction belong to this covenant id?
Toccata exposes both views.
| Question | Opcodes |
|---|---|
outputs authorized by input i | OpAuthOutputCount(i), OpAuthOutputIdx(i, k) |
inputs belonging to covenant id | OpCovInputCount(id), OpCovInputIdx(id, k) |
outputs belonging to covenant id | OpCovOutputCount(id), OpCovOutputIdx(id, k) |
binding data on output o | OpOutputCovenantId(o), OpOutputAuthorizingInput(o) |
Each count/index pair acts like an iterator over the relevant subset: first ask how many entries exist, then ask for the k-th input or output index.
This supports transactions like:
flowchart TB
classDef a fill:#eef9ef,stroke:#4d9f5b,color:#132016
classDef b fill:#f7f1ff,stroke:#8b68ca,color:#1d162b
classDef fee fill:#f4f7fb,stroke:#8794a8,color:#172033
subgraph TX["one transaction"]
A0["input 0<br/>covenant A leader"]:::a
A1["input 1<br/>covenant A delegate"]:::a
B0["input 2<br/>covenant B"]:::b
F["input 3<br/>ordinary fee"]:::fee
AO0["output 0<br/>covenant A"]:::a
AO1["output 1<br/>covenant A"]:::a
BO0["output 2<br/>covenant B"]:::b
CH["output 3<br/>change"]:::fee
end
A0 --> AO0
A0 --> AO1
A1 -. "same covenant group" .-> AO0
B0 --> BO0
F --> CHThis flexibility lets covenant transactions carry fees, change, batching inputs, or other covenant flows. A covenant script should not have to assume that the entire transaction belongs to it. It should be able to find the subset it must validate and ignore the rest.
Silverscript's covenant macros are mainly about generating this machinery reliably. Argent-style multi-contract source goes one level higher: it lets you describe roles and routes, then lower them into the Silverscript patterns.
Three useful patterns
Most covenant transitions are built from a small set of patterns.
Singleton
One input creates one successor.
State_n -> State_n_plus_1This is the counter, vault, or single-UTXO state machine pattern. It is simple, but it is sequential.
Fanout
One input creates many successors.
State -> State_A + State_B + State_CThis is how a covenant can split into parallel lineages: mint tickets, create positions, open games, allocate shards, or distribute token state.
Merge or many-to-many
Many inputs participate in one transition.
State_A + State_B -> State_C + State_DThe common pattern is leader plus delegates. One input performs the full transition check. The other inputs verify enough to know they are being used in the right family and role.
sequenceDiagram
participant L as Leader input
participant D1 as Delegate input
participant D2 as Delegate input
participant O as Outputs
L->>L: read covenant group
L->>L: validate all old states
L->>O: validate all new states
D1->>L: verify leader and shared covenant id
D2->>L: verify leader and shared covenant idThis is a recurring Toccata pattern because it centralizes the expensive reasoning while preventing passive participants from being dragged into the wrong transition.
Why high frequency changes the design space
Covenants are sometimes dismissed as "one UTXO, one bottleneck." That is true only for designs that force all state through one UTXO.
Kaspa's high-frequency DAG changes the design space. If your application can split state into many live UTXOs, those states can progress independently. The interesting work becomes state layout:
- which state should be durable and long-lived;
- which state should be episodic and short-lived;
- which transitions need merge authority;
- which commitments should be carried early and expanded only when needed.
This is where covenant systems start to look less like single contracts and more like small protocol families.
Indexers and launch proofs
The live UTXO stores a P2SH commitment, not a decoded state object. A reader that wants to show application state needs a decoding path.
A useful covenant indexer usually tracks:
- the
covenant_id; - every live UTXO carrying that id;
- the transaction that created each live UTXO;
- the redeem-script preimage or enough witness data to decode state;
- the genesis data used to derive the covenant id;
- template hashes and route commitments for multi-contract apps.
For serious applications, this becomes a launch proof or audit bundle.
flowchart TB
classDef proof fill:#fff7e8,stroke:#c58b2a,color:#1f1b12
classDef src fill:#eef6ff,stroke:#4b82c3,color:#172033
G["genesis outpoint"]:::src
A["authorized genesis outputs"]:::src
ID["derived covenant_id"]:::proof
T["template preimages"]:::src
U["current UTXOs"]:::src
P["reader proof<br/>or audit bundle"]:::proof
G --> ID
A --> ID
ID --> P
T --> P
U --> PAn explorer can show balances and UTXOs with less context. A covenant-aware wallet, app frontend, or auditor needs enough preimage material to explain what those UTXOs mean.
What to build next
If your app is one live UTXO or many independent UTXO lineages, continue with Silverscript.
If your app is a family of roles that route between templates, read Argent.
If your app has private transitions, expensive logic, or deeply shared state, read Inline ZK and Based Apps, then use the Decision Guide.