Covenants and Improved Scripting Capabilities

KIP 17: Covenants and Improved Scripting Capabilities

Authors
Ori Newman <[email protected]>
Layer
Consensus, Script Engine
Updated
2026-05-31
Status
Active

Abstract

This proposal is a continuation of KIP 10 [1], introducing full covenant support to Kaspa by extending the scripting language with introspection opcodes to all transaction fields, along with byte-string manipulation primitives. These additions enable scripts to inspect and constrain properties of the spending transaction, allowing validation of stateful transitions encoded implicitly or explicitly in UTXOs.

Specification

1. New Opcodes

The following new opcodes are introduced to enhance script functionality:

Unless specified otherwise, numeric return values are pushed as script numbers. Byte fields such as transaction IDs, script public keys, subnet IDs, hashes, and sequences are pushed as raw bytes.

Transaction Level Introspection Opcodes:

  1. OpTxVersion (0xb2): Returns the version of the transaction.
  2. OpTxLockTime (0xb5): Returns the locktime field of the transaction.
  3. OpTxSubnetId (0xb6): Returns the subnetwork ID of the transaction as raw bytes.
  4. OpTxGas (0xb7): Returns the gas field of the transaction.
  5. OpTxPayloadSubstr(start, end) (0xb8): Returns the payload substring of the transaction from start to end. Returns error for invalid ranges, or when end-start > MAX_SCRIPT_ELEMENT_SIZE.

Input/Output Introspection Opcodes:

  1. OpOutpointTxId(idx) (0xba): Returns the transaction ID of the outpoint of input idx.
  2. OpOutpointIndex(idx) (0xbb): Returns the index of the outpoint of input idx.
  3. OpTxInputScriptSigSubstr(idx, start, end) (0xbc): Returns the script signature substring of input idx from start to end. Returns error for invalid ranges, or when end-start > MAX_SCRIPT_ELEMENT_SIZE.
  4. OpTxInputSeq(idx) (0xbd): Returns the sequence field of input idx as 8 little-endian bytes.
  5. OpTxInputDaaScore(idx) (0xc0): Returns the DAA score of the block that created the UTXO spent by input idx.
  6. OpTxInputIsCoinbase(idx) (0xc1): Returns whether the UTXO spent by input idx is a coinbase.
  7. OpTxPayloadLen (0xc4): Returns the payload length of the transaction.
  8. OpTxInputSpkLen(idx) (0xc5): Returns the script public key length of the UTXO spent by input idx.
  9. OpTxInputSpkSubstr(idx, start, end) (0xc6): Returns the script public key substring of the UTXO spent by input idx from start to end. Returns error for invalid ranges, or when end-start > MAX_SCRIPT_ELEMENT_SIZE.
  10. OpTxOutputSpkLen(idx) (0xc7): Returns the script public key length of output idx.
  11. OpTxOutputSpkSubstr(idx, start, end) (0xc8): Returns the script public key substring of output idx from start to end. Returns error for invalid ranges, or when end-start > MAX_SCRIPT_ELEMENT_SIZE.
  12. OpTxInputScriptSigLen(idx) (0xc9): Returns the script signature length of input idx.

Existing Introspection Opcodes:

The following KIP 10 opcodes were already active before Toccata and are listed here to show the full introspection surface:

  1. OpTxInputCount (0xb3): Returns the number of inputs in the transaction.
  2. OpTxOutputCount (0xb4): Returns the number of outputs in the transaction.
  3. OpTxInputIndex (0xb9): Returns the index of the current input being evaluated.
  4. OpTxInputAmount(idx) (0xbe): Returns the amount (value) of the UTXO spent by input idx.
  5. OpTxInputSpk(idx) (0xbf): Returns the serialized script public key of the UTXO spent by input idx.
  6. OpTxOutputAmount(idx) (0xc2): Returns the amount (value) of output idx.
  7. OpTxOutputSpk(idx) (0xc3): Returns the serialized script public key of output idx.
Note on Arguments and Substring Opcodes

Index and range arguments (idx, start, and end) are provided as script numbers and MUST be non-negative. On all substring related opcodes, byte positions are zero-based, start is inclusive, and end is exclusive. Substring opcodes fail if start > end or if the requested byte range is out of bounds.

MAX_SCRIPT_ELEMENT_SIZE

Post-Toccata activation, MAX_SCRIPT_ELEMENT_SIZE = 1,000,000. Pre-Toccata, MAX_SCRIPT_ELEMENT_SIZE = 520. Substring and concatenation opcodes are bounded by this value.

Signature Verification Opcodes:

  1. OpCheckSigFromStack(signature, msg_hash, pubkey) (0xd7): Verifies a Schnorr signature against a 32-byte msg_hash and pushes whether it is valid.
  2. OpCheckSigFromStackECDSA(signature, msg_hash, pubkey) (0xd8): Verifies an ECDSA signature against a 32-byte msg_hash and pushes whether it is valid.

Hash Opcodes:

  1. OpBlake2bWithKey(data, key) (0xa7): Returns the BLAKE2b-256 hash of data keyed with key. The key length MUST be at most 64 bytes.
  2. OpBlake3(data) (0xd9): Returns the BLAKE3 hash of data.
  3. OpBlake3WithKey(data, key) (0xda): Returns the BLAKE3 keyed hash of data using key. The key length MUST be exactly 32 bytes.

Other Opcodes:

  1. OpCat(str1, str2) (0x7e): Concatenates two byte strings. Returns error if the resulting string exceeds MAX_SCRIPT_ELEMENT_SIZE.
  2. OpSubstr(str, start, end) (0x7f): Returns the substring of a byte string (from start (inclusive) until end (exclusive)). Returns error for invalid ranges, or when end-start > MAX_SCRIPT_ELEMENT_SIZE.
  3. OpInvert(str) (0x83): Returns the bitwise NOT of a byte string.
  4. OpAnd(str1, str2) (0x84): Returns the bitwise AND of two byte strings. Returns error if the two strings are of different lengths.
  5. OpOr(str1, str2) (0x85): Returns the bitwise OR of two byte strings. Returns error if the two strings are of different lengths.
  6. OpXor(str1, str2) (0x86): Returns the bitwise XOR of two byte strings. Returns error if the two strings are of different lengths.
  7. OpMul (0x95): Returns the product of two numbers. Returns error on overflow.
  8. OpDiv (0x96): Returns the quotient of two numbers. Returns error on division by zero, or for signed overflow (i64::MIN / -1).
  9. OpMod (0x97): Returns the remainder of two numbers. Returns error on modulo by zero, or for signed overflow (i64::MIN mod -1).
  10. OpZkPrecompile (0xa6): Verifies a zero-knowledge proof (KIP-16).

Numeric Conversion Opcodes:

  1. OpNum2Bin(num, size) (0xcd): Encodes num as a signed script number occupying exactly size bytes. The target size MUST be at most 8 bytes and large enough to represent num.
  2. OpBin2Num(bytes) (0xce): Interprets bytes as a signed script number and pushes its minimal numeric encoding. The input MUST fit within the 8-byte script number range.

Disabled opcode slots:

  • OpLeft (0x80), OpRight (0x81), OpLShift (0x98), and OpRShift (0x99) remain disabled and are not activated by this KIP.

2. Activation

The features introduced in this KIP are activated as part of the Toccata hard fork. Activation is controlled by the toccata_activation DAA score parameter:

  1. Prior to activation:

    • New opcodes are treated as invalid and will cause script execution to fail
    • MAX_SCRIPT_ELEMENT_SIZE = 520
    • MAX_SCRIPTS_SIZE = 10,000
    • MAX_OPS_PER_SCRIPT = 201
  2. After activation:

    • All new opcodes become available
    • MAX_SCRIPT_ELEMENT_SIZE = 1,000,000
    • MAX_SCRIPTS_SIZE = 1,000,000
    • MAX_OPS_PER_SCRIPT = 1,000,000

Note: the post-activation MAX_SCRIPTS_SIZE and MAX_OPS_PER_SCRIPT values are script-engine ceilings; transient mass bounds transactions before these ceilings are reached in practice.

Motivation

The combination of introspection opcodes and OP_CAT/OP_SUBSTR can be used to implicitly attach state to UTXOs and enforce correct state transitions.

For example, we can implement a simple covenant that requires us to increase a counter stored in the transaction payload every time the UTXO is spent, and forces us to send the new transaction to the same scriptPubKey:

Given signature script of the form: <PREV_TX_REST> <PREV_TX_PAYLOAD>, the scriptPubKey will be:

OpDup OpRot OpRot OpCat
"TransactionID" OpBlake2bWithKey
OpTxInputIndex OpOutpointTxId OpEqualVerify
Op1Add
0 OpTxPayloadLen OpTxPayloadSubstr
OpEqualVerify
OpTxInputIndex OpTxInputSpk 0 OpTxOutputSpk
OpEqualVerify
OpTxOutputCount 1 OpEqual

This script can be written in pseudocode as:

validate(prev_tx, tx){
    return hash(prev_tx) == tx.inputs[curr_idx].prev_tx_id
    AND tx.outputs[0].script_pub_key == prev_tx.inputs[curr_idx].script_pub_key
    AND len(tx.outputs) == 1
    AND tx.payload == prev_tx.payload + 1;
}

In general, once we can encode some state transition function δ in script, we can validate that δ(prev_payload, tx) = new_payload, where δ can also introduce some constraints on tx using introspection opcodes (checking its signature, enforcing a locktime, etc).

Note on Transaction Encoding

Many covenant constructions require the spender to provide a full copy of the previous transaction as witness data in order to validate that tx.inputs[idx].prev_tx_id = hash(prev_tx).

As a result, the transaction ID hashing algorithm implicitly defines a canonical transaction encoding that scripts can rely on. Covenant scripts may reconstruct or inspect this encoding to verify that a provided previous transaction matches the referenced outpoint.

A reference implementation of the transaction ID hashing and encoding logic is provided in [3].

Use Cases

Covenants enable a variety of use cases, including but not limited to:

  1. Fungible and non-fungible tokens [4]: Enforce token rules through covenant-constrained UTXOs.
  2. Smart Vaults for enhanced security [5]: Implement timelocks, multisig, and other security constraints.
  3. Congestion control mechanisms [6]: Enforce rate limiting and priority rules.
  4. L1-L2 trustless bridges [8]: Combine KIP-16's ZK proof verification with covenant scripts to enable trustless cross-chain asset movement.
  5. Stateful applications: Encode application state in UTXOs and enforce transitions through covenant scripts.

See [7] for more use cases.

Backwards Compatibility

This proposal requires a hard fork, as it introduces new opcodes to the scripting language. Older software will require an update to support these new features. Existing scripts and addresses remain valid, but cannot use the new functionality without being updated.

Reference Implementation

The implementation is available in rusty-kaspa on the toccata and master branches.

References

  1. KIP 10: kip-0010.md
  2. OP_CAT BIP: https://github.com/bip420/bip420
  3. Reference implementation of transaction ID hashing: https://github.com/kaspanet/rusty-kaspa/blob/2ccc9e4ca9aa184c62c4fb14dbb463834264e01d/consensus/core/src/hashing/tx.rs#L40
  4. Cat Protocol for covenants based tokens: https://catprotocol.org/
  5. Vaults: https://utxos.org/uses/vaults/
  6. Congestion Control: https://utxos.org/uses/scaling/
  7. More covenants use cases: https://utxos.org/uses/, https://github.com/sCrypt-Inc/awesome-op-cat#op_cat-use-cases, https://covenants.info/use-cases/
  8. KIP 16: kip-0016.md