KIP: 20
Layer: Consensus, Script Engine
Title: Covenant IDs
Authors: Michael Sutton <msutton@cs.huji.ac.il>
Status: Proposed, Implemented and activated in TN10
Updated: 2026-05-28
This KIP introduces covenant identifiers (covenant_id)---consensus-tracked, 32-byte identifiers carried by UTXOs and declared by transaction outputs. This establishes a stable covenant lineage for state and membership across UTXO transitions.
The mechanism provides:
- A stable covenant lineage identifier for covenant instance membership and off-chain indexing.
- A consensus-level non-forgeability property: an output can only carry a
covenant_idif it continues an existing covenant, or if it correctly genesis-initializes that covenant id from a unique outpoint and a committed set of authorized outputs. - Script-engine introspection opcodes for efficiently reasoning about covenant-related inputs/outputs within the spending transaction, without requiring parent/grandparent transactions as witness data.
This KIP is designed to be used in conjunction with KIP-17.
Idea: add an optional 32-byte covenant_id field to the UTXO entry.
Consensus rule (external to the script engine): allow an output to carry covenant_id = id only in one of the following two cases:
- (recursion) its authorizing input spends a UTXO with
covenant_id = id - (genesis)
id = covenant_id(in[a].previous_outpoint, auth_outputs)for the output set it declares (Section 4.3)
To make the mechanism complete, the covenant script itself should validate that both the output script_public_key and the output covenant binding (including covenant_id) match the covenant’s expected transition.
This means that a covenant-labeled UTXO cannot be forged “out of thin air”. In contrast, parent/grandparent-witness schemes can only ensure that a forged covenant UTXO is unspendable, whereas covenant ids make it uncreatable in the first place.
Proof sketch: argument by induction on the first forged covenant-labeled UTXO. Trying to create a forged covenant UTXO from a valid covenant UTXO would fail the covenant script’s transition check spending the valid UTXO; a forge must thus already appear in an input. In the genesis case, the covenant_id is a cryptographic hash digest that includes a specific authorizing input.outpoint, and outpoints never repeat, so covenant membership is not freely choosable.
KIP-17 enables covenants via transaction introspection. However, establishing a continuous "application identity" or lineage solely via script logic often requires recursive proofs. Validating that a referenced outpoint corresponds to a specific prior transaction structure forces spending transactions to carry parent (and often grandparent) transactions as witness data.
This imposes overhead in witness size and forces covenant logic to depend on canonical transaction encoding and hashing within the execution layer.
This KIP introduces a minimal, consensus-enforced mechanism to:
- First-Class Covenants: Provide a native covenant lineage/membership primitive, avoiding recursive transaction-witness workarounds and the resulting canonical-encoding and hashing burden.
- Prevent State Forgery: Make covenant-labeled UTXOs uncreatable unless they are valid continuations or valid genesis initializations.
- P2SH State: Strengthen the case for storing covenant state under P2SH rather than in ancestor transaction payloads: lineage no longer requires parent/grandparent witnesses, and the stable
covenant_iddecouples covenant identity from an evolvingscript_public_key.
Importantly, the consensus rules introduced here must be complemented by correctly-designed covenant scripts which enforce output continuation (e.g., that outputs' script_public_key and covenant binding match the covenant's expected next state and transition shape).
covenant_id: A 32-byte hash identifying a covenant protocol instance.- Covenant-bound output: A transaction output that includes a covenant binding (Section 2), declaring a
covenant_idand anauthorizing_input. - Covenant UTXO: A UTXO whose
UtxoEntrycarriescovenant_id = Some(id). - Continuation output: A covenant-bound output whose
authorizing_inputspends a covenant UTXO with the samecovenant_id. - Genesis output: A covenant-bound output that is not a continuation output, and whose
covenant_idmust be validated via the genesis hashing rule (Section 3).
Each transaction output is extended with an optional covenant binding:
TransactionOutput {
value: u64,
script_public_key: ScriptPublicKey,
covenant: Option<CovenantBinding>,
}
CovenantBinding {
authorizing_input: u16,
covenant_id: Hash32,
}
Semantics:
- If
covenantisNone, the output is not covenant-bound. - If
covenantisSome(binding), the output declares that it belongs tobinding.covenant_idand that the input at indexbinding.authorizing_inputauthorizes its creation.
Each UTXO entry is extended with an optional covenant identifier:
UtxoEntry {
amount: u64,
script_public_key: ScriptPublicKey,
block_daa_score: u64,
is_coinbase: bool,
covenant_id: Option<Hash32>,
}
UTXO creation rule:
- When a transaction output is accepted into the UTXO set, the resulting
UtxoEntry.covenant_idis set tooutput.covenant.map(|b| b.covenant_id).
Covenant bindings are only defined for transaction versions that support them.
- A transaction with
version < 1MUST NOT contain any output withcovenant.is_some().
This section defines how a covenant id is genesis-initialized.
Define CovenantIDHash as BLAKE2b-256 with domain separation tag "CovenantID".
For an outpoint O = (tx_id: Hash32, index: u32) and an ordered list of authorized outputs:
auth_outputs = [(out_idx_0: u32, out_0), (out_idx_1: u32, out_1), ..., (out_idx_{n-1}: u32, out_{n-1})]
The list auth_outputs MUST be ordered by strictly increasing out_idx (i.e., transaction output index).
Unlike a minimal genesis rule based on hashing only the authorizing outpoint, the genesis covenant_id here commits also to the initial authorized output set (indices, amounts, and script public keys). This anchors the covenant’s initial rules and state while excluding the covenant binding itself to avoid self-reference.
Define:
covenant_id(O, auth_outputs) =
CovenantIDHash(
O.tx_id
|| le_u32(O.index)
|| le_u64(len(auth_outputs))
|| for each (out_idx, out) in auth_outputs:
le_u32(out_idx)
|| le_u64(out.value)
|| le_u16(out.script_public_key.version)
|| le_u64(len(out.script_public_key.script))
|| out.script_public_key.script
)
Notes:
le_u{16,32,64}(x)denotes little-endian encoding of fixed-width integers.- The covenant binding itself is not included in the genesis hash (to avoid self-reference).
The rules below are evaluated when covenant functionality is active.
For each transaction output out[i]:
- If
out[i].covenantisNone: no covenant validation is performed for this output. - If
out[i].covenant = Some({ covenant_id, authorizing_input }):authorizing_inputMUST be a valid input index of the transaction.- The transaction version MUST satisfy
tx.version >= 1(see Section 2.3).
Let in[authorizing_input] be the authorizing input and let utxo(authorizing_input) be its spent UtxoEntry.
If utxo(authorizing_input).covenant_id == Some(covenant_id) then out[i] is a continuation output.
Otherwise, out[i] is a genesis output and is validated as described below.
Genesis outputs are validated in groups keyed by (authorizing_input, covenant_id):
For each group G = (authorizing_input = a, covenant_id = id):
- Let
O = in[a].previous_outpoint. - Let
auth_outputsbe the ordered list (by strictly increasingi) of pairs(i, out[i])for all outputs in the transaction that:- have
out[i].covenant = Some({ covenant_id = id, authorizing_input = a }), and - are classified as genesis outputs (Section 4.2).
- have
- Consensus MUST reject the transaction unless
covenant_id(O, auth_outputs) == id.
When covenant functionality is active, the script engine is provided with a pre-computed covenants context derived from the transaction and the spent input UTXOs.
The covenants context construction algorithm, the corresponding opcodes, and the genesis covenant-id hashing operations are designed such that the overall work is linear in transaction size (up to expected constant factors), avoiding per-opcode scans over the transaction and thus avoiding quadratic (or worse) costs.
This section standardizes the following opcodes (all invalid prior to activation):
Stack behavior:
- Input:
idx - Output:
- If
tx.utxo(idx).covenant_idisNone, pushesZERO_HASH. - Otherwise, pushes the 32-byte
covenant_id.
- If
Stack diagram:
[..., idx] -> [..., (ZERO_HASH | covenant_id[32])]
The following opcodes expose, for a given input index input_idx, the set of authorized outputs:
Definition (authorized output):
- Output
out[j]is an authorized output of inputinput_idxiff:out[j].covenant = Some({ covenant_id, authorizing_input = input_idx }), andtx.utxo(input_idx).covenant_id == Some(covenant_id)(i.e., it is a continuation output).
Opcodes:
OpAuthOutputCount(0xcb): Inputinput_idx, outputcount.OpAuthOutputIdx(0xcc): Inputinput_idx k, outputout_idxwhereout_idxis the index of thek-th authorized output ofinput_idx.
Stack diagrams:
[..., input_idx ] -> [..., count ]
[..., input_idx, k ] -> [..., out_idx ]
The following opcodes expose covenant participant indices for a given covenant_id id:
Definitions:
- Covenant input indices: all input indices
isuch thattx.utxo(i).covenant_id == Some(id). - Covenant output indices: all output indices
jsuch thatout[j].covenant = Some({ covenant_id = id, authorizing_input = a })for somea, andtx.utxo(a).covenant_id == Some(id).
Opcodes:
OpCovInputCount(0xd0): Inputcovenant_id, outputcount.OpCovInputIdx(0xd1): Inputcovenant_id k, outputinput_idx.OpCovOutputCount(0xd2): Inputcovenant_id, outputcount.OpCovOutputIdx(0xd3): Inputcovenant_id k, outputout_idx.
Stack diagrams:
[..., covenant_id[32] ] -> [..., count ]
[..., covenant_id[32], k] -> [..., input_idx]
[..., covenant_id[32] ] -> [..., count ]
[..., covenant_id[32], k] -> [..., out_idx ]
Notes:
- Genesis outputs are validated by consensus (Section 4.3) but are not included in the covenant contexts above.
The following opcodes expose the covenant binding declared by a transaction output. Unlike the authorized-output and shared covenant contexts above, these opcodes inspect the output binding directly and therefore apply to both continuation and genesis outputs.
Opcodes:
OpOutputCovenantId(0xd5): Inputout_idx, output the 32-bytecovenant_iddeclared byout[out_idx], orZERO_HASHif the output has no covenant binding.OpOutputAuthorizingInput(0xd6): Inputout_idx, output theauthorizing_inputdeclared byout[out_idx], or-1if the output has no covenant binding.
Stack diagrams:
[..., out_idx] -> [..., (ZERO_HASH | covenant_id[32])]
[..., out_idx] -> [..., (authorizing_input | -1)]
Note:
ZERO_HASHdenotes the 32-byte all-zero hash.
- Transaction ID and signature hashing:
- For transaction versions that include covenant bindings, the
TransactionOutput.covenantfield is committed to by the transaction ID and signature hash.
- For transaction versions that include covenant bindings, the
- UTXO commitment:
UtxoEntry.covenant_id, when present, is committed to by the UTXO set commitment.
- Storage mass / plurality:
- A covenant UTXO is treated as carrying an additional 32 bytes for storage accounting purposes.
covenant_id provides a stable handle for covenant lineage, instance identification, and off-chain state tracking, independent of frequently changing script public keys (e.g., when state is encoded in P2SH preimages or otherwise changes per transition).
For safe covenant development, the covenant id rules and the introspection opcodes should be paired with covenant authoring tools that reduce footguns.
One example is a parallel compiler effort (Silverscript) intended to make it easy to write secure covenants (including correct authorized-output validation) and hard to write insecure covenants, by expressing covenant type/shape declaratively and generating the required validation code.
This section is non-normative. It describes typical covenant scheme patterns intended to be correctly implemented using the covenant id rules and the introspection opcodes standardized above.
A common pattern is a single covenant input authorizing one or many covenant outputs in the spending transaction.
Recommended approach:
- Use
OpAuthOutputCount/OpAuthOutputIdxfrom the authorizing input to iterate its authorized outputs. - Since the script engine has no general loops, the covenant script should:
- enforce an upper bound on the maximum number of authorized outputs, and
- unroll verification logic for each possible authorized output index within that bound, validating each authorized output’s
script_public_key(and other constraints such as amounts or state commitments).
Scope note:
- A single transaction may include multiple isolated authorization groups with the same
covenant_id, because authorization validation is local to an authorizing input context.
This is a special case of one-to-many with exactly one authorized output per transition.
This pattern is relevant for covenant schemes that maintain a single state UTXO per covenant id (e.g., a zk-based rollup/app covenant), and has the property that the UTXO set maintains exactly one UTXO with the covenant id.
For covenant schemes where many covenant inputs and outputs participate in a single state transition (e.g., native assets), the recommended standard is a delegation pattern:
- Designate a single “leader” input responsible for validating the full N-to-N transition.
- Other covenant inputs act as “delegators”, validating only that the leader is correctly selected and is taking responsibility for validation.
- Branch selection can be driven by per-input witness/signature data (e.g., leader vs delegator branch).
Leader selection ideas (examples):
- Fix the leader input index by convention.
- In the delegator branch, enforce
leader_index < current_input_indexto avoid cycles. - Encode the leader index in the witness/signature and verify it, then additionally validate leader authorization relations using the authorized-outputs context.
Scope note:
- With this pattern, a transaction naturally contains a single N-to-N operation. Multiple operations in a single transaction may require additional scheme-specific grouping by indices (e.g., partitioning inputs/outputs into index ranges).
This KIP relies on the security properties of the hash function used in covenant_id genesis initialization:
- Hash-function resistance: preimage/second-preimage/collision resistance prevents constructing a genesis definition
(O, auth_outputs)that matches a targeted existingcovenant_id, and prevents ambiguous covenant identity across distinct genesis definitions. - Domain separation:
CovenantIDHashis domain separated from other protocol hashes.
Parent/grandparent-witness covenant constructions can only ensure that a forged covenant UTXO is unspendable. Covenant ids shift this to consensus: a forged covenant-labeled UTXO should thus be uncreatable in the first place.
Consider an output U that is a covenant UTXO with id id.
By the consensus validation rules:
- Either
Uis a continuation output, in which case it was created by spending an input UTXO already carryingid, and the spending transaction’s script validation can enforce additional constraints on the authorized outputs. - Or
Uis a genesis output, in which caseidis defined by an outpointO(which never repeats) and the committed set of authorized outputs viacovenant_id(O, auth_outputs).
Therefore, creating a covenant UTXO with id id that violates the covenant’s intended transition rules requires either:
- Creating it from an already-existing covenant UTXO while passing the covenant’s script checks, or
- Finding a genesis preimage
(O, auth_outputs)that hashes to a targetedidunderCovenantIDHash(preimage attack).
This proposal introduces new consensus rules, new transaction fields (for versions that include covenant bindings), changes to the UTXO commitment, and new script opcodes. It therefore requires a hard fork.
Transactions and UTXOs that do not use covenant bindings are unaffected in structure and semantics (except for activation gating of transaction versions and opcode validity).
- Consensus and context construction:
crypto/txscript/src/covenants.rs - Covenant id hashing:
consensus/core/src/hashing/covenant_id.rs - Covenant-related opcodes:
crypto/txscript/src/opcodes/mod.rs - Data structures:
consensus/core/src/tx.rs
Thanks to @someone235, @iziodev, and @biryukovmaxim for extensive brainstorming, ideas and implementation help.
- KIP-17: kip-0017.md
- KIP-10: kip-0010.md