From 0581f55487ed7c471fbd3615684ebba7bae47e63 Mon Sep 17 00:00:00 2001 From: Michael Sutton Date: Wed, 11 Feb 2026 13:47:06 +0000 Subject: [PATCH] kip 20 covenant ids initial --- kip-0020.md | 343 ++++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 343 insertions(+) create mode 100644 kip-0020.md diff --git a/kip-0020.md b/kip-0020.md new file mode 100644 index 0000000..64f5cb9 --- /dev/null +++ b/kip-0020.md @@ -0,0 +1,343 @@ +``` +KIP: 20 +Layer: Consensus, Script Engine +Title: Covenant IDs +Authors: Michael Sutton +Status: Proposed, Implemented, Activated in TN12 +``` + +## Abstract + +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_id` if 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. + +## Informal Summary + +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: +1. (recursion) its authorizing input spends a UTXO with `covenant_id = id` +2. (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. + +## Motivation + +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_id` decouples covenant identity from an evolving `script_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). + +## Specification + +### 1. Terminology + +- `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_id` and an `authorizing_input`. +- Covenant UTXO: A UTXO whose `UtxoEntry` carries `covenant_id = Some(id)`. +- Continuation output: A covenant-bound output whose `authorizing_input` spends a covenant UTXO with the *same* `covenant_id`. +- Genesis output: A covenant-bound output that is *not* a continuation output, and whose `covenant_id` must be validated via the genesis hashing rule (Section 3). + +### 2. Data Model + +#### 2.1 Transaction Output Covenant Binding + +Each transaction output is extended with an optional covenant binding: + +``` +TransactionOutput { + value: u64, + script_public_key: ScriptPublicKey, + covenant: Option, +} + +CovenantBinding { + authorizing_input: u16, + covenant_id: Hash32, +} +``` + +Semantics: +- If `covenant` is `None`, the output is not covenant-bound. +- If `covenant` is `Some(binding)`, the output declares that it belongs to `binding.covenant_id` and that the input at index `binding.authorizing_input` authorizes its creation. + +#### 2.2 UTXO Entry Covenant ID + +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, +} +``` + +UTXO creation rule: +- When a transaction output is accepted into the UTXO set, the resulting `UtxoEntry.covenant_id` is set to `output.covenant.map(|b| b.covenant_id)`. + +#### 2.3 Transaction Versioning + +Covenant bindings are only defined for transaction versions that support them. + +- A transaction with `version < 1` MUST NOT contain any output with `covenant.is_some()`. + +### 3. Covenant ID Genesis Hashing + +This section defines how a covenant id is *genesis-initialized*. + +#### 3.1 Hash Function + +Define `CovenantIDHash` as BLAKE2b-256 with domain separation tag `"CovenantID"`. + +#### 3.2 Canonical Encoding + +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). + +### 4. Consensus Validation Rules + +The rules below are evaluated when covenant functionality is active. + +#### 4.1 Basic Validity + +For each transaction output `out[i]`: +- If `out[i].covenant` is `None`: no covenant validation is performed for this output. +- If `out[i].covenant = Some({ covenant_id, authorizing_input })`: + - `authorizing_input` MUST be a valid input index of the transaction. + - The transaction version MUST satisfy `tx.version >= 1` (see Section 2.3). + +#### 4.2 Continuation vs Genesis Classification + +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. + +#### 4.3 Genesis Covenant ID Validation (Grouped) + +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_outputs` be the ordered list (by strictly increasing `i`) 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). +- Consensus MUST reject the transaction unless `covenant_id(O, auth_outputs) == id`. + +### 5. Script Engine Introspection + +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): + +#### 5.1 `OpInputCovenantId` (0xcf) + +Stack behavior: +- Input: `idx` +- Output: + - If `tx.utxo(idx).covenant_id` is `None`, pushes `false`. + - Otherwise, pushes the 32-byte `covenant_id`. + +Stack diagram: + +``` +[..., idx] -> [..., (false | covenant_id[32])] +``` + +#### 5.2 Authorized-Outputs Context (Per Input) + +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 input `input_idx` iff: + - `out[j].covenant = Some({ covenant_id, authorizing_input = input_idx })`, and + - `tx.utxo(input_idx).covenant_id == Some(covenant_id)` (i.e., it is a continuation output). + +Opcodes: +- `OpAuthOutputCount` (0xcb): Input `input_idx`, output `count`. +- `OpAuthOutputIdx` (0xcc): Input `input_idx k`, output `out_idx` where `out_idx` is the index of the `k`-th authorized output of `input_idx`. + +Stack diagrams: + +``` +[..., input_idx ] -> [..., count ] +[..., input_idx, k ] -> [..., out_idx ] +``` + +#### 5.3 Shared Covenant Context (Per Covenant ID) + +The following opcodes expose covenant participant indices for a given `covenant_id` `id`: + +Definitions: +- Covenant input indices: all input indices `i` such that `tx.utxo(i).covenant_id == Some(id)`. +- Covenant output indices: all output indices `j` such that `out[j].covenant = Some({ covenant_id = id, authorizing_input = a })` for some `a`, and `tx.utxo(a).covenant_id == Some(id)`. + +Opcodes: +- `OpCovInputCount` (0xd0): Input `covenant_id`, output `count`. +- `OpCovInputIdx` (0xd1): Input `covenant_id k`, output `input_idx`. +- `OpCovOutCount` (0xd2): Input `covenant_id`, output `count`. +- `OpCovOutputIdx` (0xd3): Input `covenant_id k`, output `out_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. +- Output-level introspection of covenant bindings is intentionally not part of this design. Covenant scripts either obtain the covenant id from the authorizing/current input (via `OpInputCovenantId`) or use a hardcoded covenant id when validating an external covenant. + +### 6. Commitments and Accounting + +- Transaction ID and signature hashing: + - For transaction versions that include covenant bindings, the `TransactionOutput.covenant` field is committed to by the transaction ID and signature hash. +- 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. + +## Rationale + +### Covenant Lineage & Identity + +`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). + +### Covenant Authoring Tooling + +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. + +## Architectural Patterns + +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. + +### Split / One-to-Many (Local Authorization) + +A common pattern is a single covenant input authorizing one or many covenant outputs in the spending transaction. + +Recommended approach: +- Use `OpAuthOutputCount`/`OpAuthOutputIdx` from 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. + +### Singleton / One-to-One + +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. + +### Merge / Many-to-One and Many-to-Many (Delegation) + +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_index` to 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). + +## Security Considerations + +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 existing `covenant_id`, and prevents ambiguous covenant identity across distinct genesis definitions. +- Domain separation: `CovenantIDHash` is domain separated from other protocol hashes. + +### Non-forgeability + +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 `U` is a continuation output, in which case it was created by spending an input UTXO already carrying `id`, and the spending transaction’s script validation can enforce additional constraints on the authorized outputs. +- Or `U` is a genesis output, in which case `id` is defined by an outpoint `O` (which never repeats) and the committed set of authorized outputs via `covenant_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 targeted `id` under `CovenantIDHash` (preimage attack). + +## Backwards Compatibility + +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). + +## Reference Implementation (Rusty-Kaspa) + +- 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` + +## Acknowledgements + +Thanks to @someone235, @iziodev, and @biryukovmaxim for extensive brainstorming, ideas and implementation help. + +## References + +1. KIP-17: [kip-0017.md](kip-0017.md) +2. KIP-10: [kip-0010.md](kip-0010.md)