Skip to content

Latest commit

 

History

History
361 lines (243 loc) · 18.1 KB

File metadata and controls

361 lines (243 loc) · 18.1 KB
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

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>,
}

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<Hash32>,
}

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 ZERO_HASH.
    • Otherwise, pushes the 32-byte covenant_id.

Stack diagram:

[..., idx] -> [..., (ZERO_HASH | 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.
  • OpCovOutputCount (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.

5.4 Output Covenant Binding Introspection

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): Input out_idx, output the 32-byte covenant_id declared by out[out_idx], or ZERO_HASH if the output has no covenant binding.
  • OpOutputAuthorizingInput (0xd6): Input out_idx, output the authorizing_input declared by out[out_idx], or -1 if the output has no covenant binding.

Stack diagrams:

[..., out_idx] -> [..., (ZERO_HASH | covenant_id[32])]
[..., out_idx] -> [..., (authorizing_input | -1)]

Note:

  • ZERO_HASH denotes the 32-byte all-zero hash.

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
  2. KIP-10: kip-0010.md