TSP-0009 CTV

Template-Constrained Spending via Tapscript Covenants

Proposal Number: TSP-0009
Proposal Name: Template-Constrained Spending via Tapscript Covenants
Category: Consensus / Covenant
Status: Draft
Author: Tondi Foundation Development Team
Created: 2025-09-05
Target: Tondi Mainnet (v2026b)
Scope: New covenant opcode, tapscript validation rules, transaction template hashing, interoperability with TSP-0007 (ANYPREVOUT), TSP-0008 (CISA), future TSP-0010 (Channel Factories).

This document uses RFC 2119 keywords (MUST/SHOULD/MAY).


1. Abstract

TSP-0009 introduces OP_CHECKTEMPLATEVERIFY (CTV) as a covenant primitive for Tondi.
CTV enables outputs to commit to a fixed transaction template hash, restricting how they can be spent.
This mechanism allows scalable constructions such as:

  • Channel Factories (TSP-0010)
  • Vaults and Recovery Flows
  • Congestion-Controlled Batching
  • Deterministic Multi-party Coordination

CTV is implemented as a new tapscript opcode (OP_CHECKTEMPLATEVERIFY) encoded under OP_SUCCESSx, with a Tondi-specific transaction template hash that extends BIP-119 to include DAG consensus fields (subnetwork_id, gas).
CTV provides non-recursive covenants, ensuring safety from infinite recursion, while enabling a wide range of Layer 2 and Layer 3 protocols. This proposal is a soft fork upgrade, activated via miner signaling in Tondi's DAG consensus.


2. Summary

OP_CHECKTEMPLATEVERIFY uses opcode 0xB8 as a soft fork upgrade. OP_CHECKTEMPLATEVERIFY does the following:

  • There is at least one element on the stack, fail otherwise
  • The element on the stack is 32 bytes long, NOP otherwise
  • The DefaultCheckTemplateVerifyHash of the transaction at the current input index is equal to the element on the stack, fail otherwise

The DefaultCheckTemplateVerifyHash commits to the serialized version, locktime, scriptSigs hash (if any non-null scriptSigs), number of inputs, sequences hash, number of outputs, outputs hash, currently executing input index, subnetwork_id, and gas.

The recommended standardness rules additionally:

  • Reject non-32 byte as SCRIPT_ERR_DISCOURAGE_UPGRADABLE_NOPS.

3. Background and Motivation

3.1 What is CTV?

CTV is a covenant mechanism that constrains how an output can be spent:

  • The script includes OP_CHECKTEMPLATEVERIFY <templateHash>.
  • When spent, the transaction must match the template (version, locktime, outputs).
  • If mismatch, script fails.

This allows participants to predetermine possible exit paths without pre-signing every transaction. Covenants like CTV enable outputs to enforce rules on how their funds are spent in future transactions, effectively "programming" the flow of funds.

3.2 Bitcoin History (BIP-119)

  • Proposed by Jeremy Rubin (2019).

  • Enabled batch exit transactions, congestion control, and simple vaults.

  • Debate slowed activation on Bitcoin mainnet, but tested on signet.

  • Key features:

    • Commits to outputs (and optionally version/locktime).
    • Non-recursive, avoiding infinite loops.
    • Commits to a template hash, allowing non-interactive setups.

BIP-119's design focused on simplicity and safety, avoiding general-purpose covenants to minimize risks like recursion or Turing-completeness. It has been extensively discussed in Bitcoin development circles, with implementations tested for security and efficiency.

3.3 Why Tondi Needs CTV

  • Channel Factories (TSP-0010): require covenant-enforced exit paths.
  • Vaults: enable safe custody with delayed recovery.
  • Congestion Control: mass exits from Tondi Flash or DEXes.
  • Gaming / DeFi: deterministic multi-party allocations.
  • Tondi-Specific Needs: In a DAG-based system like Tondi, high TPS demands efficient batching to avoid congestion during settlements. CTV complements TSP-0007 (ANYPREVOUT) for eltoo updates and TSP-0008 (CISA) for aggregated funding, enabling scalable Layer 2 without trusted intermediaries.

Without CTV, all these require massive pre-signed transaction trees, impractical at scale due to interactivity and storage costs.


4. Detailed Specification

The below code is the main logic for verifying CHECKTEMPLATEVERIFY, described in pythonic pseudocode. The canonical specification for the semantics of OP_CHECKTEMPLATEVERIFY as implemented in C++ in the context of Tondi Core can be seen in the reference implementation.

The execution of the opcode is as follows:

def execute_bip_119(self):
    # Before soft-fork activation / failed activation
    # continue to treat as NOP4
    if not self.flags.script_verify_default_check_template_verify_hash:
        # Potentially set for node-local policy to discourage premature use
        if self.flags.script_verify_discourage_upgradable_nops:
            return self.errors_with(errors.script_err_discourage_upgradable_nops)
        return self.return_as_nop()

    # CTV always requires at least one stack argument
    if len(self.stack) < 1:
        return self.errors_with(errors.script_err_invalid_stack_operation)

    # CTV only verifies the hash against a 32 byte argument
    if len(self.stack[-1]) == 32:
        # Ensure the precomputed data required for anti-DoS is available,
        # or cache it on first use
        if self.context.precomputed_ctv_data == None:
            self.context.precomputed_ctv_data = self.context.tx.get_default_check_template_precomputed_data()
        # If the hashes do not match, return error
        if stack[-1] != self.context.tx.get_default_check_template_hash(self.context.nIn, self.context.precomputed_ctv_data):
            return self.errors_with(errors.script_err_template_mismatch)
        return self.return_as_nop()

    # future upgrade can add semantics for this opcode with different length args
    # so discourage use when applicable
    if self.flags.script_verify_discourage_upgradable_nops:
        return self.errors_with(errors.script_err_discourage_upgradable_nops)
    else:
        return self.return_as_nop()

The computation of this hash can be implemented as specified below (where self is the transaction type). Care must be taken that in any validation context, the precomputed data must be initialized to prevent Denial-of-Service attacks. Any implementation must cache these parts of the hash computation to avoid quadratic hashing DoS. All variable length computations must be precomputed including hashes of the scriptsigs, sequences, and outputs. See the section "Denial of Service and Validation Costs" below. This is not a performance optimization.

def ser_compact_size(l):
    r = b""
    if l < 253:
        # Serialize as unsigned char
        r = struct.pack("B", l)
    elif l < 0x10000:
        # Serialize as unsigned char 253 followed by unsigned 2 byte integer (little endian)
        r = struct.pack("B", 253) + struct.pack("<H", l)
    elif l < 0x100000000:
        # Serialize as unsigned char 254 followed by unsigned 4 byte integer (little endian)
        r = struct.pack("B", 254) + struct.pack("<I", l)
    else:
        # Serialize as unsigned char 255 followed by unsigned 8 byte integer (little endian)
        r = struct.pack("B", 255) + struct.pack("<Q", l)
    return r

def get_default_check_template_precomputed_data(self):
    sha_prevouts = b"\x00" * 32
    sha_amounts = b"\x00" * 32
    sha_scriptpubkeys = b"\x00" * 32
    sha_sequences = b"\x00" * 32
    sha_outputs = b"\x00" * 32

    if not self.is_coinbase() and len(self.vin) > 0:
        # sha_prevouts
        h = SHA256.new()
        for inp in self.vin:
            h.update(inp.prevout.serialize())
        sha_prevouts = h.digest()

        # sha_amounts
        h = SHA256.new()
        for amount in self.vamount:
            h.update(struct.pack("<Q", amount))
        sha_amounts = h.digest()

        # sha_scriptpubkeys
        h = SHA256.new()
        for spk in self.vspk:
            h.update(ser_compact_size(len(spk)))
            h.update(spk)
        sha_scriptpubkeys = h.digest()

        # sha_sequences
        h = SHA256.new()
        for inp in self.vin:
            h.update(struct.pack("<I", inp.nSequence))
        sha_sequences = h.digest()

    # sha_outputs
    h = SHA256.new()
    for out in self.vout:
        h.update(out.serialize())
    sha_outputs = h.digest()

    return {
        "sha_prevouts": sha_prevouts,
        "sha_amounts": sha_amounts,
        "sha_scriptpubkeys": sha_scriptpubkeys,
        "sha_sequences": sha_sequences,
        "sha_outputs": sha_outputs,
    }

def get_default_check_template_hash(self, nIn, precomputed):
    h = TaggedHash("TSP-0009/CTVTemplate")

    # version
    h.update(struct.pack("<i", self.nVersion))

    # nLockTime
    h.update(struct.pack("<q", self.nLockTime))  # Tondi: 64-bit

    # sha_prevouts (if any)
    if len(self.vin) > 0 and not self.is_coinbase():
        h.update(precomputed["sha_prevouts"])

    # sha_amounts (if any)
    if len(self.vin) > 0 and not self.is_coinbase():
        h.update(precomputed["sha_amounts"])

    # sha_scriptpubkeys (if any)
    if len(self.vin) > 0 and not self.is_coinbase():
        h.update(precomputed["sha_scriptpubkeys"])

    # sha_sequences (if any)
    if len(self.vin) > 0 and not self.is_coinbase():
        h.update(precomputed["sha_sequences"])

    # sha_outputs
    h.update(precomputed["sha_outputs"])

    # spend_type
    spend_type = 0  # Tondi: Adjust for annex if needed
    h.update(struct.pack("B", spend_type))

    # input_index
    h.update(struct.pack("<I", nIn))

    # Tondi-specific: subnetwork_id
    h.update(self.subnetwork_id)

    # Tondi-specific: gas
    h.update(struct.pack("<q", self.gas))

    return h.digest()

TaggedHash is defined as SHA256(SHA256(tag) || SHA256(tag) || x).

4.1 New Opcode

OpCheckTemplateVerify  (OP_SUCCESSx - 0xB8)
  • Consensus Rule: Pops 32-byte template hash from stack, computes transaction template hash, compares equality. If mismatch โ†’ FAIL.
  • Non-upgraded nodes: In Tapscript context, treat as OP_SUCCESSx โ†’ soft-fork safe.
  • Implementation Note: Tondi uses native OP_SUCCESSx (0xB8) mechanism from the reserved range 0xB2-0xBF defined in TSP-0001, providing soft-fork safety while maintaining Bitcoin BIP compatibility.

4.2 Template Hash Definition

Tondi extends BIP-119 with DAG-specific fields.
Hash function:

template_hash = H_tag("TSP-0009/CTVTemplate",
    le16:version ||      // Tondi uses u16 (16-bit)
    le64:lock_time ||    // 64-bit instead of 32-bit
    H(sha_prevouts) ||
    H(sha_amounts) ||
    H(sha_scriptpubkeys) ||
    H(sha_sequences) ||
    H(sha_outputs) ||
    u8:spend_type ||     // 0 for no annex
    le32:input_index ||
    H(subnetwork_id) ||  // Tondi-specific: 20-byte fixed length
    le64:gas ||          // Tondi-specific: maximum constraint
    H(payload)           // Tondi-specific: transaction payload
)

Field Specifications:

  • version: MUST be encoded as 16-bit little-endian integer (matching Tondi's u16 type). This differs from Bitcoin's 32-bit version field.

  • subnetwork_id: MUST be exactly 20 bytes (matching Tondi's SUBNETWORK_ID_SIZE). This field identifies the DAG subnetwork for parallel processing. The value MUST be zero-padded to 20 bytes if shorter, or truncated to 20 bytes if longer. This ensures deterministic hashing and prevents implementation divergence.

  • gas: Represents the maximum gas limit constraint, not an exact match. The transaction's actual gas consumption MUST NOT exceed this value. This allows for gas optimization while maintaining deterministic exit paths. The field is encoded as a 64-bit little-endian integer.

  • payload: MUST be included in the template hash computation. This field contains transaction-specific data and is hashed as a variable-length byte array.

Differences from Bitcoin BIP-119:

  • 16-bit version (vs 32-bit).
  • 64-bit locktime (vs 32-bit).
  • Includes subnetwork_id (20-byte fixed), gas (maximum constraint), and payload.
  • Domain separation "TSP-0009/CTVTemplate".
  • No annex support in v1 (spend_type=0).

4.3 Validation Rules

  • MUST be used inside tapscript leafs (v=0xc0).
  • Consumes 32-byte hash argument.
  • If tx hash != pushed hash โ†’ FAIL.
  • Template covers outputs, preventing mutation.
  • Precompute hashes to avoid O(n^2) DoS (mandatory for validators).

5. Design Rationale

5.1 Choice of Template Commitment

The template commits to outputs, locktime, and inputs indirectly (via hashes), balancing flexibility and security. Committing to exact outputs enables vaults and factories, while hashing inputs prevents malleability. Tondi extensions (subnetwork_id, gas) ensure compatibility with DAG features like parallel processing and resource limits.

5.2 Non-Recursive Design

CTV is non-recursive to avoid infinite loops or complex analysis. This limits depth but suffices for most use cases (e.g., single-level factories). Future opcodes (e.g., OP_CAT) could enable composition.

5.3 Soft Fork via OP_SUCCESSx

Using OP_SUCCESSx ensures backwards compatibility, as non-upgraded nodes treat it as NOP. Standardness rules discourage premature use.

5.4 Tondi-Specific Extensions

Subnetwork_id and gas are included to bind to Tondi's DAG consensus, preventing cross-subnetwork replays and ensuring gas costs are deterministic.


6. Use Cases

6.1 Channel Factories (TSP-0010)

  • Funding output includes CTV โ†’ enforces exit transaction.
  • Guarantees every participant can recover funds via predetermined outputs.

6.2 Vaults

  • Vault output โ†’ CTV restricts spends to recovery transaction with timelocks.
  • Enables cold storage with delayed hot-wallet spends.

6.3 Congestion Control

  • Exchanges can batch withdrawals, using one on-chain tx with thousands of pre-committed outputs.
  • Reduces DAG load during high-TPS events.

6.4 Deterministic Multi-party Coordination

  • DAOs and payment pools can enforce specific allocations.
  • Complements CISA for aggregated funding.

6.5 Script Usage Examples

6.5.1 Simple Vault Script

# Vault with 24-hour recovery delay
OP_CHECKTEMPLATEVERIFY <recovery_template_hash>
OP_CHECKSEQUENCEVERIFY <24_hours_in_blocks>
OP_CHECKSIG <recovery_pubkey>

Template Hash Commitment:

  • recovery_template_hash: Pre-computed hash of the recovery transaction
  • Recovery transaction MUST have specific outputs and locktime
  • Any deviation from template โ†’ script fails

6.5.2 Factory Funding Script

# Channel factory funding output
OP_CHECKTEMPLATEVERIFY <factory_exit_template_hash>
OP_CHECKSIGVERIFY_CISA <factory_pubkey>

Template Requirements:

  • factory_exit_template_hash: Commits to specific exit transaction structure
  • Exit transaction MUST distribute funds according to predetermined ratios
  • Gas limit MUST NOT exceed committed maximum
  • Subnetwork_id MUST match factory's designated subnetwork

6.5.3 Batch Withdrawal Script

# Exchange batch withdrawal
OP_CHECKTEMPLATEVERIFY <batch_template_hash>
OP_CHECKSIGVERIFY_CISA <exchange_pubkey>
OP_CHECKSIG_TSP <hot_wallet_pubkey>  # ANYPREVOUT for updates

Use Case: Exchange can batch thousands of user withdrawals into single transaction while maintaining deterministic exit paths and enabling non-interactive updates.


7. Security Considerations

  • Non-recursive: prevents infinite loops.
  • Privacy: CTV outputs look like normal tapscript until spent.
  • Griefing Resistance: ensures deterministic exit paths.
  • Replay Risks: mitigated by binding to outputs and locktime.
  • DoS and Validation Costs: Precomputing hashes avoids quadratic costs; nodes MUST cache to prevent attacks.
  • Malleability: Commits prevent third-party malleability of committed fields.
  • Upgradeability: Non-32-byte arguments are NOP, allowing future extensions.

8. Economic Impact

  • Reduces mempool congestion during mass exits.
  • Allows batching of thousands of withdrawals.
  • Saves ~90% on-chain space in factory scenarios.
  • In Tondi DAG, enables efficient high-TPS settlements without fee spikes.

9. Interoperability

9.1 Compatibility Overview

CTV works seamlessly with:

  • TSP-0007 ANYPREVOUT: Eltoo channels inside factories.
  • TSP-0008 CISA: Aggregated signatures for funding tx.
  • Future TSP-0010 Factories: Covenant backbone.

No conflicts; CTV can be composed in scripts with existing opcodes.

9.2 Combined Usage Example: Channel Factory Funding

The following example demonstrates how CTV, ANYPREVOUT, and CISA work together in a channel factory funding transaction:

Scenario: A channel factory with 4 participants needs to create funding transaction with deterministic exit paths.

Transaction Structure:

Factory Funding Transaction:
โ”œโ”€โ”€ Inputs (4 participants):
โ”‚   โ”œโ”€โ”€ Input 0: Alice's funding (uses CISA aggregation)
โ”‚   โ”œโ”€โ”€ Input 1: Bob's funding (uses CISA aggregation)  
โ”‚   โ”œโ”€โ”€ Input 2: Carol's funding (uses CISA aggregation)
โ”‚   โ””โ”€โ”€ Input 3: Dave's funding (uses CISA aggregation)
โ”œโ”€โ”€ Outputs:
โ”‚   โ”œโ”€โ”€ Output 0: Factory funding output (CTV-constrained)
โ”‚   โ””โ”€โ”€ Output 1: Change outputs (normal)
โ””โ”€โ”€ CISA Envelope: Single aggregated signature for all 4 inputs

Script Composition:

# Factory funding output script (Output 0)
OP_CHECKTEMPLATEVERIFY <factory_exit_template_hash>
OP_CHECKSIGVERIFY_CISA <factory_pubkey>  # CISA aggregation
OP_CHECKSIG_TSP <alice_pubkey>           # ANYPREVOUT for channel updates

Complete Flow:

  1. Funding Phase: All 4 participants sign with CISA aggregation (TSP-0008)
  2. Channel Updates: Use ANYPREVOUT signatures (TSP-0007) for eltoo-style updates
  3. Exit Phase: CTV enforces predetermined exit transaction template (TSP-0009)

Benefits:

  • CISA: Reduces transaction size from 4ร—64B signatures to 1ร—64B aggregated signature
  • ANYPREVOUT: Enables non-interactive channel updates without new signatures
  • CTV: Guarantees deterministic exit paths without pre-signed transaction trees

This "three-piece suite" enables scalable Layer 2 protocols with minimal on-chain footprint and maximum flexibility.


10. Backwards Compatibility

This is a soft fork: non-upgraded nodes see OP_SUCCESSx as unconditional success. Upgraded nodes enforce the new rules. Legacy outputs remain spendable. Standardness rules prevent premature use on mainnet.


11. Deployment

11.1 Soft Fork Activation

  • Soft Fork Path: tapscript-only, OP_SUCCESSx โ†’ OP_CTV.
  • Activation: miner signaling, DAG window 80% over ~3 months (adjusted for GHOSTDAG heaviest chain).
  • Testnet: 6 months minimum, adversarial covenant testing.
  • Observation Period: Monitor for issues post-signaling.

11.2 DAG Consensus Integration

Confirmation Window Definition:

  • Window Size: 144 blocks (approximately 24 hours at 10-second block intervals)
  • Confirmation Threshold: 80% of blocks in the confirmation window MUST signal activation
  • GHOSTDAG Adjustment: Activation considers the heaviest chain in GHOSTDAG, not just the longest chain
  • Parallel Processing: Signaling occurs across all subnetworks simultaneously

Speedy Trial Mechanism:

  • LOT (Lock-in On Timeout): YES - If 80% threshold not met within 3 months, activation automatically proceeds
  • Grace Period: 2 weeks between threshold achievement and activation
  • Rollback Protection: Once activated, CTV rules become permanent consensus rules

Miner Signaling Protocol:

Block Header Extension:
โ”œโ”€โ”€ Version Field: Bits 16-31 reserved for feature flags
โ”œโ”€โ”€ CTV Signal Bit: Bit 16 = 1 indicates CTV support
โ”œโ”€โ”€ Validation: Nodes count signals in confirmation window
โ””โ”€โ”€ Activation: Automatic after threshold + grace period

11.3 Implementation Timeline

Phase 1 (Months 1-3): Core implementation and testnet deployment Phase 2 (Months 4-6): Miner signaling begins, adversarial testing Phase 3 (Months 7-9): Mainnet activation if threshold met Phase 4 (Months 10-12): Ecosystem integration and monitoring


12. Reference Pseudocode

fn op_checktemplateverify_tondi(tx: &Transaction, template_hash: [u8; 32]) -> Result<(), Error> {
    let computed = compute_ctv_template_hash(tx);
    if computed != template_hash {
        return Err(Error::CTVTemplateMismatch);
    }
    Ok(())
}

fn compute_ctv_template_hash(tx: &Transaction) -> [u8; 32] {
    tag_hash("TSP-0009/CTVTemplate", [
        &tx.version.to_le_bytes(),
        &tx.lock_time.to_le_bytes(),      // 64-bit
        &hash_outputs(tx.outputs),
        &hash_prevouts(tx.inputs),
        &hash_sequences(tx.inputs),
        &tx.subnetwork_id,
        &tx.gas.to_le_bytes(),
    ])
}

13. Test Vectors

13.1 Valid Cases

13.1.1 Single Output Vault

Input:

  • tx.version = 2 (16-bit)
  • tx.lock_time = 0
  • outputs = [ (1000 sats, P2WPKH) ]
  • subnetwork_id = 0x0000000000000000000000000000000000000001 (20 bytes)
  • gas = 1000 (maximum constraint)
  • payload = [] (empty)

Expected template hash = 0xabc123... (vector to be computed).

13.1.2 Multi-Input Multi-Output Transaction

Input:

  • tx.version = 2 (16-bit)
  • tx.lock_time = 1000
  • inputs = [ (prevout1, 5000 sats), (prevout2, 3000 sats) ]
  • outputs = [ (2000 sats, P2WPKH), (4000 sats, P2WSH), (2000 sats, change) ]
  • subnetwork_id = 0x0000000000000000000000000000000000000002 (20 bytes)
  • gas = 2000 (maximum constraint)
  • payload = [] (empty)

Expected template hash = 0xdef456... (vector to be computed).

13.1.3 Maximum Gas Constraint

Input:

  • tx.version = 2 (16-bit)
  • tx.lock_time = 0
  • outputs = [ (1000 sats, P2WPKH) ]
  • subnetwork_id = 0x0000000000000000000000000000000000000000 (20 bytes)
  • gas = 0xFFFFFFFFFFFFFFFF (maximum 64-bit value)
  • payload = [] (empty)

Expected template hash = 0x789abc... (vector to be computed).

13.2 Invalid Cases

13.2.1 Output Amount Mismatch

Original template commits to 1000 sats output.
Modified transaction has 1001 sats output โ†’ hash mismatch โ†’ FAIL.

13.2.2 Subnetwork ID Modification

Original template commits to subnetwork_id = 0x0000000000000000000000000000000000000001.
Modified transaction has subnetwork_id = 0x0000000000000000000000000000000000000002 โ†’ hash mismatch โ†’ FAIL.

13.2.3 Gas Exceeds Maximum Constraint

Original template commits to gas = 1000 (maximum constraint).
Modified transaction has gas = 1001 โ†’ exceeds maximum โ†’ FAIL.

13.2.4 Locktime Mismatch

Original template commits to locktime = 0.
Modified transaction has locktime = 1 โ†’ hash mismatch โ†’ FAIL.

13.2.5 Version Mismatch

Original template commits to version = 2.
Modified transaction has version = 1 โ†’ hash mismatch โ†’ FAIL.

13.2.6 Subnetwork ID Length Violation

Original template commits to 20-byte subnetwork_id.
Modified transaction has subnetwork_id = 0x01 (1 byte) โ†’ length violation โ†’ FAIL.

13.3 Edge Cases

13.3.1 Zero Outputs

Input:

  • tx.version = 2 (16-bit)
  • tx.lock_time = 0
  • outputs = [] (empty)
  • subnetwork_id = 0x0000000000000000000000000000000000000000 (20 bytes)
  • gas = 0
  • payload = [] (empty)

Expected template hash = 0x000000... (vector to be computed).

13.3.2 Maximum Locktime

Input:

  • tx.version = 2 (16-bit)
  • tx.lock_time = 0xFFFFFFFFFFFFFFFF (maximum 64-bit value)
  • outputs = [ (1000 sats, P2WPKH) ]
  • subnetwork_id = 0x0000000000000000000000000000000000000000 (20 bytes)
  • gas = 0
  • payload = [] (empty)

Expected template hash = 0x111111... (vector to be computed).

Additional vectors for complex scenarios (e.g., mixed input types, annex support) to be provided in implementation.


14. Roadmap

  • TSP-0009 (CTV) โ†’ covenant primitive.
  • TSP-0010 (Channel Factories) โ†’ built on CTV.
  • Future Vault Spec โ†’ optional TSP.

15. Implementation Recommendations

15.1 Code Corrections Based on Tondi Client Analysis

After reviewing the Tondi client codebase, the following corrections have been made to this proposal:

  1. Subnetwork ID Length: Changed from 32 bytes to 20 bytes to match Tondi's SUBNETWORK_ID_SIZE constant
  2. Opcode Implementation: Using OpSuccess80 (0x50) following Bitcoin Taproot specification for Tapscript soft fork upgrades
  3. Hash Function: Updated to use Tondi's 20-byte subnetwork_id in template hash computation
  4. OpSuccessx Implementation: Tondi needs to implement OpSuccessx mechanism for proper Taproot compatibility

15.2 Implementation Steps

Step 0: Implement OpSuccessx Mechanism for Tapscript Compatibility

Prerequisite: Tondi needs to implement OpSuccessx mechanism for proper Taproot compatibility.

// In /home/arthur/AvatoLabs/Tondi/crypto/txscript/src/opcodes/mod.rs
// Modify OpReserved to implement OpSuccessx in Tapscript context:

opcode OpReserved<0x50, 1>(self, vm) {
    // In Tapscript context, OpReserved should act as OpSuccessx
    if vm.is_tapscript_context() {
        Ok(()) // Treat as success for soft-fork compatibility
    } else {
        Err(TxScriptError::OpcodeReserved(format!("{self:?}")))
    }
}

Note: This requires adding is_tapscript_context() method to TxScriptEngine and modifying the always_illegal() check for Tapscript.

Step 1: Modify OpReserved to Implement CTV

Implementation: Modify the existing OpReserved to implement CTV when activated.

// In /home/arthur/AvatoLabs/Tondi/crypto/txscript/src/opcodes/mod.rs
// Modify OpReserved<0x50, 1> to implement CTV:

opcode OpReserved<0x50, 1>(self, vm) {
    // In Tapscript context, OpReserved acts as OpSuccessx
    if vm.is_tapscript_context() {
        // Check if CTV is activated
        if vm.ctv_enabled {
            // Pop 32-byte template hash from stack
            let template_hash = vm.dstack.pop()?;
            if template_hash.len() != 32 {
                return Err(TxScriptError::InvalidState("expected 32-byte template hash".to_string()));
            }
            
            // Compute transaction template hash
            let computed_hash = compute_ctv_template_hash(vm.tx, vm.input_index)?;
            
            // Compare hashes
            if template_hash != computed_hash {
                return Err(TxScriptError::CtvTemplateMismatch);
            }
        }
        // If CTV not activated, treat as OpSuccessx (always succeed)
        Ok(())
    } else {
        // In non-Tapscript context, return reserved error
        Err(TxScriptError::OpcodeReserved(format!("{self:?}")))
    }
}

Step 2: Implement Template Hash Function

// In /home/arthur/AvatoLabs/Tondi/crypto/txscript/src/opcodes/mod.rs

fn compute_ctv_template_hash<T: VerifiableTransaction>(
    tx: &T,
    input_index: usize,
) -> Result<Vec<u8>, TxScriptError> {
    use sha2::{Digest, Sha256};
    
    let mut hasher = Sha256::new();
    
    // Tag: "TSP-0009/CTVTemplate"
    let tag = b"TSP-0009/CTVTemplate";
    hasher.update(tag);
    hasher.update(tag); // Double tag for tagged hash
    
    // Version (16-bit little-endian - Tondi uses u16)
    hasher.update(tx.tx().version.to_le_bytes());
    
    // Lock time (64-bit little-endian)
    hasher.update(tx.tx().lock_time.to_le_bytes());
    
    // Precomputed hashes (if any inputs)
    if !tx.tx().inputs.is_empty() && !tx.tx().is_coinbase() {
        // sha_prevouts
        let mut prevouts_hasher = Sha256::new();
        for input in &tx.tx().inputs {
            prevouts_hasher.update(input.previous_outpoint.transaction_id.as_bytes());
            prevouts_hasher.update(input.previous_outpoint.index.to_le_bytes());
        }
        hasher.update(prevouts_hasher.finalize());
        
        // sha_amounts
        let mut amounts_hasher = Sha256::new();
        for (input, utxo_entry) in tx.populated_inputs() {
            amounts_hasher.update(utxo_entry.amount.to_le_bytes());
        }
        hasher.update(amounts_hasher.finalize());
        
        // sha_scriptpubkeys
        let mut scriptpubkeys_hasher = Sha256::new();
        for (input, utxo_entry) in tx.populated_inputs() {
            let spk_bytes = utxo_entry.script_public_key.to_bytes();
            scriptpubkeys_hasher.update(spk_bytes.len().to_le_bytes());
            scriptpubkeys_hasher.update(spk_bytes);
        }
        hasher.update(scriptpubkeys_hasher.finalize());
        
        // sha_sequences
        let mut sequences_hasher = Sha256::new();
        for input in &tx.tx().inputs {
            sequences_hasher.update(input.sequence.to_le_bytes());
        }
        hasher.update(sequences_hasher.finalize());
    }
    
    // sha_outputs
    let mut outputs_hasher = Sha256::new();
    for output in tx.tx().outputs.iter() {
        outputs_hasher.update(output.value.to_le_bytes());
        outputs_hasher.update(output.script_public_key.version().to_le_bytes());
        let script_bytes = output.script_public_key.script();
        outputs_hasher.update(script_bytes.len().to_le_bytes());
        outputs_hasher.update(script_bytes);
    }
    hasher.update(outputs_hasher.finalize());
    
    // Spend type (0 for no annex)
    hasher.update([0u8]);
    
    // Input index
    hasher.update((input_index as u32).to_le_bytes());
    
    // Subnetwork ID (20 bytes)
    hasher.update(tx.tx().subnetwork_id.as_ref());
    
    // Gas (64-bit little-endian)
    hasher.update(tx.tx().gas.to_le_bytes());
    
    // Payload (variable-length byte array)
    hasher.update(tx.tx().payload.len().to_le_bytes());
    hasher.update(&tx.tx().payload);
    
    Ok(hasher.finalize().to_vec())
}

Step 3: Add CTV Error Type

// In /home/arthur/AvatoLabs/Tondi/crypto/txscript/errors/src/lib.rs

#[derive(Debug, Clone, PartialEq, Eq)]
pub enum TxScriptError {
    // ... existing variants ...
    CtvTemplateMismatch,
    CtvNotActivated,
}

Step 4: Add Activation Flag

// In /home/arthur/AvatoLabs/Tondi/crypto/txscript/src/lib.rs

pub struct TxScriptEngine<T: VerifiableTransaction, Reused: SigHashReusedValues> {
    // ... existing fields ...
    pub ctv_enabled: bool,
}

impl<T: VerifiableTransaction, Reused: SigHashReusedValues> TxScriptEngine<T, Reused> {
    pub fn new_with_features(
        reused_values: &Reused,
        cache: &Cache,
        kip10_enabled: bool,
        ctv_enabled: bool,
    ) -> Self {
        Self {
            // ... existing initialization ...
            ctv_enabled,
        }
    }
}

15.3 Testing Strategy

  1. Unit Tests: Test template hash computation with various transaction structures
  2. Integration Tests: Test CTV opcode execution in different script contexts
  3. Activation Tests: Verify soft fork behavior (NOP before activation, validation after)
  4. Edge Cases: Test with zero outputs, maximum values, invalid subnetwork_id lengths

15.4 Deployment Considerations

  1. Soft Fork Activation: Use Tondi's existing miner signaling mechanism
  2. Feature Flag: Add CTV activation flag to consensus parameters
  3. Backward Compatibility: In Tapscript context, OpReserved acts as OpSuccessx (always succeed)
  4. Performance: Precompute hashes to avoid O(nยฒ) DoS attacks
  5. Tapscript Context: CTV only works in Tapscript context (P2TR outputs)
  6. OpSuccessx Implementation: Modify OpReserved to act as OpSuccessx in Tapscript context

16. Conclusion

TSP-0009 introduces a minimal but powerful covenant primitive (CTV) to Tondi, enabling scalable off-chain protocols, vaults, and congestion control.
It is a prerequisite for TSP-0010 Channel Factories, and a cornerstone for the Tondi Layer 2 ecosystem.

The implementation has been corrected based on Tondi's actual codebase structure, using OpReserved (0x50) as OpSuccessx in Tapscript context for soft-fork safety and ensuring compatibility with Tondi's transaction format and opcode system.


References

  • BIP-119: OP_CHECKTEMPLATEVERIFY by Jeremy Rubin
  • Decker et al., "Scalable Funding of Bitcoin Micropayment Channel Networks"
  • Tondi TSP-0007 (ANYPREVOUT) and TSP-0008 (CISA)