TSP-0012 โ Channel Factories
Proposal Number: TSP-0012
Proposal Number: TSP-0012
Proposal Name: Channel Factories for Scalable Off-Chain Payment Networks in Tondi
Category: Layer 2 (L2) โ pre-Oct 2025 numbering retained as 000x
Status: Draft
Author: Tondi Foundation Development Team & Avato Labs
Created: 2025-09-05
Target: Tondi Mainnet (v2026b)
Scope: Off-chain protocol extensions, on-chain covenant opcodes, transaction funding schemas, interoperability with TSP-0007 (ANYPREVOUT) and TSP-0008 (CISA), governance & activation
This document uses RFC 2119 keywords (MUST/SHOULD/MAYโฆ).
1. Abstract
TSP-0012 introduces Channel Factories: a multi-party off-chain protocol that enables the efficient creation and management of multiple payment channels from a single on-chain funding transaction. By leveraging covenants and aggregated signatures, factories reduce on-chain footprint, lower fees, and enhance scalability for Tondi Flash (high-throughput Layer 2 settlements). This builds on existing Tondi features like ANYPREVOUT (TSP-0007) for updateable channels and CISA (TSP-0008) for funding efficiency, providing a foundation for large-scale micropayment networks. Factories allow N participants to fund a shared output and allocate sub-channels off-chain, with on-chain enforcement only in disputes. v1 focuses on tapscript covenants, SIGHASH_ALL, and up to 32 participants for safety. The protocol ensures secure, non-custodial fund allocation through template commitments and aggregated Schnorr signatures, minimizing blockchain interactions while maintaining Tondi's DAG consensus advantages for rapid confirmations.
2. Background and Motivation
2.1 Introduction to Channel Factories
Channel Factories are a multi-party off-chain protocol designed to batch the creation of multiple bidirectional payment channels among a group of participants using a single on-chain funding transaction. Unlike traditional payment channels (e.g., Lightning Network-style), which require individual on-chain openings for each pair of users, a factory allows N participants to collaboratively fund a shared "factory" output. From this output, they can off-chain negotiate and instantiate multiple channels (e.g., up to N*(N-1)/2 pairwise channels) without additional on-chain interactions, unless a dispute arises.
In practice:
- Participants deposit funds into a multi-signature (multisig) or covenant-controlled output via an aggregated funding transaction (enhanced by TSP-0008 CISA). This funding tx aggregates inputs from all participants into a single factory output.
- Off-chain, they use presigned or covenant-enforced transactions to allocate funds into sub-channels. For example, the allocation template might specify amounts, scripts, and timelocks for each sub-channel.
- Channels can be updated, closed, or settled off-chain cooperatively using eltoo-style mechanisms (TSP-0007); non-cooperative closures fall back to on-chain enforcement, where the latest state is broadcasted with relative timelocks to allow challenges.
- Upon cooperative closure, the factory can be spent to distribute funds directly; in disputes, the covenant enforces predefined exit paths, such as refund templates or channel settlements.
This design minimizes blockchain congestion, making it ideal for Tondi's DAG-based high-TPS environment, where on-chain settlements are frequent but resource-intensive. For example, three users (Alice, Bob, Charlie) can deposit into a 3-of-3 multisig factory output, then open AliceโBob, AliceโCharlie, and BobโCharlie channels off-chain, reducing on-chain data by broadcasting only the factory funding tx initially. This enables netting across multiple channels within the factory, further optimizing for Tondi Flash's settlement phases.
2.2 Related BIP Historical Background
The concept of Channel Factories originates from Bitcoin's Lightning Network ecosystem, where scaling limitations prompted innovations beyond basic duplex channels. Key historical milestones include:
-
Early Foundations (2015-2017): The Lightning Network, proposed by Joseph Poon and Thaddeus Dryja in 2015, introduced bidirectional payment channels using relative timelocks (BIP-68: Sequence Numbers, BIP-112: CHECKSEQUENCEVERIFY) and hashed timelock contracts (HTLCs). However, scaling to millions of channels required batching. Christian Decker (Blockstream) and colleagues addressed this in 2017 with "Scalable Funding of Bitcoin Micropayment Channel Networks" (Royal Society Open Science), proposing "channel factories" as an intermediate layer between on-chain transactions and individual channels. This allowed hierarchical structures: factories fund multiple channels off-chain, reducing on-chain load by 90%+ for large groups.
-
Duplex Micropayment Channels (2017-2018): Decker's follow-up work, including "A Fast and Scalable Payment Network with Bitcoin Duplex Micropayment Channels" (SOSP 2017) and "Scalable Lightning Factories for Bitcoin" (IACR 2018 ePrint), refined factories using duplex channelsโreversible, updateable channels without penalties. These relied on emerging ideas like eltoo (a symmetric channel update mechanism, later formalized). Duplex factories open multiple one-to-one channels at once, using off-chain updates and revocation trees to handle state progression securely.
-
Covenant-Enabling BIPs (2019-Present): Factories require "covenants" (spending restrictions that propagate to child transactions) for secure off-chain allocations. Notable Bitcoin Improvement Proposals (BIPs) include:
- BIP-118 (SIGHASH_ANYPREVOUT, 2019): Proposed by Anthony Towns and others, enables eltoo-style channels by allowing signatures to bind to any prevout, eliminating penalty mechanisms and simplifying state updates. (Tondi equivalent: TSP-0007 ANYPREVOUT.)
- BIP-119 (OP_CHECKTEMPLATEVERIFY or CTV, 2019): Authored by Jeremy Rubin, introduces a covenant opcode that commits to a transaction template (e.g., version, locktime, outputs). CTV simplifies factory exits by presigning refunds or channel allocations, reducing multi-round signing and enabling non-interactive setups. Evolved from earlier ideas like OP_CHECKOUTPUTSHASHVERIFY (COSHV) and OP_SECURETHEBAG. CTV has been tested on signet and is debated for Bitcoin activation due to its non-recursive design, which prevents infinite loops but limits expressiveness.
- BIP-345 (OP_VAULT, 2023): Complements CTV for dynamic vaults, useful in factory sub-structures by allowing delayed spends or recovery paths.
- BIP-347 (OP_CAT, 2023): Enables script concatenation for more flexible covenants, potentially enhancing factory privacy and atomicity by allowing dynamic script construction.
-
Aggregation and Privacy Enhancements: BIP-340/341/342 (Taproot/Schnorr, 2021) provide aggregated signatures (MuSig) and script hiding, foundational for efficient multi-party funding. Tondi adapts these via TSP-0008 (CISA) for cross-input aggregation in factory funding txs.
In Bitcoin, factories remain experimental (e.g., discussed in Optech newsletters 2018-2024), with no merged implementation yet due to covenant debates and activation risks. Tondi, with its DAG consensus and existing TSP-0007/0008, is well-positioned to deploy factories natively, accelerating Layer 2 adoption by leveraging faster block times and higher throughput.
2.3 Motivation for Tondi
- Scalability: Tondi Flash requires efficient off-chain netting to handle high-volume micropayments; factories batch 100+ channels per on-chain tx, leveraging DAG for fast confirmations and reducing settlement latency.
- Fee Efficiency: CISA (TSP-0008) aggregates funding signatures, saving ~64*(N-1) bytes; covenants enforce fair exits without per-channel on-chain data.
- Privacy: Homogenizes on-chain footprints by hiding sub-channel details in Taproot scripts, combinable with coinjoins for better anonymity sets.
- Interoperability: Builds on ANYPREVOUT for eltoo channels within factories, enabling symmetric updates and griefing resistance.
- Economic Benefits: Lowers barriers for mass adoption, e.g., in DeFi or gaming, by amortizing fees across participants.
Design Goals:
- Soft fork (tapscript-only + OP_SUCCESSx tightening);
- Schnorr-only, aligned with Taproot/tapscript;
- Simple and Correct: v1 only supports SIGHASH_ALL aggregation, excluding complex APO mixes.
3. Terminology and Overall Design
- Channel Factory: A multi-party covenant output funding sub-channels off-chain. Participants create a shared multisig output and allocate to channels via off-chain txs. The factory acts as a "root" UTXO from which child channels are derived.
- Covenant: Script restriction propagating to spends (e.g., via OP_CTV committing to tx templates, ensuring child txs match predefined structures).
- Eltoo Channel: Symmetric, updateable channel using ANYPREVOUT (TSP-0007) for state updates without penalties, ideal for factory sub-channels.
- Factory Envelope: Transaction-level extension carrying participant bitmap, aggregated signatures (via TSP-0008), and template commitments for covenant enforcement.
- MuSig2: Aggregation scheme from TSP-0008, used for factory funding to combine participant signatures.
- TSP-0012 Public Key: 0x02 || pubkey32, eligible for factory covenants and aggregation.
3.1 Domain Separation Labels (Standardized)
MUST: Fixed byte strings for all domain separation labels (case-sensitive, exact spelling):
"TSP-0012/FactorySighash"(21 bytes):0x54 0x53 0x50 0x2d 0x30 0x30 0x30 0x39 0x2f 0x46 0x61 0x63 0x74 0x6f 0x72 0x79 0x53 0x69 0x67 0x68 0x61 0x73 0x68"TSP-0012/FactoryCohort"(19 bytes):0x54 0x53 0x50 0x2d 0x30 0x30 0x30 0x39 0x2f 0x46 0x61 0x63 0x74 0x6f 0x72 0x79 0x43 0x6f 0x68 0x6f 0x72 0x74- Reuse TSP-0008 labels for MuSig2:
"MuSig/KeyAggList"(16 bytes):0x4d 0x75 0x53 0x69 0x67 0x2f 0x4b 0x65 0x79 0x41 0x67 0x67 0x4c 0x69 0x73 0x74,"MuSig/KeyCoeff"(14 bytes):0x4d 0x75 0x53 0x69 0x67 0x2f 0x4b 0x65 0x79 0x43 0x6f 0x65 0x66 0x66,"BIP0340/challenge"(16 bytes):0x42 0x49 0x50 0x30 0x33 0x34 0x30 0x2f 0x63 0x68 0x61 0x6c 0x6c 0x65 0x6e 0x67 0x65.
v1 Scope (Strict Constraints for Simplicity and Safety):
- Only tapscript script-path (no key-path impact; future extension);
- Only SIGHASH_ALL;
- Cannot mix with TSP-0007 APO in the same factory cohort unless explicitly bound;
- At most 1 factory per transaction (extensible, see appendix);
- Up to 32 participants to limit complexity, with policy-level enforcement for DoS resistance.
4. Specification
4.1 New Public Key and New Opcodes
-
TSP-0012 Public Key: x-only 32-byte Schnorr public key, identified in scripts as 0x02 || pubkey32. This format ensures compatibility with TSP-0008 aggregation and Taproot x-only conventions.
-
New OPs (Implemented via OP_SUCCESSx encoding for soft fork):
-
OP_CHECKTEMPLATEVERIFY_FACTORY: Commits to a transaction template hash, verifying that the spending tx matches a predefined structure (e.g., outputs, locktime). Inspired by BIP-119 CTV, but tailored for factory allocations.
-
OP_CHECKSIGVERIFY_FACTORY: Verifies aggregated Schnorr signatures in the factory context, delegating to transaction-level envelope.
-
(Optional) OP_CHECKSIGADD_FACTORY for incremental aggregation in multi-round setups.
Specific OP_SUCCESSx values will be fixed in the deployment parameters table (e.g., OP_SUCCESS183 for CTV_FACTORY). -
Non-upgraded nodes: Encountering OP_SUCCESSx results in unconditional success, ensuring soft fork safety.
-
Upgraded nodes: Execute factory validation path (below), tightening "unconditional success" to "must pass covenant and signature verification." This tightening prevents abuse of OP_SUCCESSx in factory scripts.
-
Script Convention: Factory outputs use covenant scripts that include OP_CHECKTEMPLATEVERIFY_FACTORY to commit to allocation templates; inputs to the funding tx use TSP-0008-style aggregation for efficient multisig. Participating inputs do not provide individual signatures but delegate to the factory envelope.
4.2 Factory Envelope (Witness Extension)
MUST: Factory envelope is located at transaction-level witness-extension with unique position and fixed encoding to eliminate malleability and parsing divergence. The envelope is encoded as:
tx_wit_ext := varint(1) || factory_envelope
factory_envelope :=
magic[4] = "FACT" // 0x46 0x41 0x43 0x54
version[u8] = 0x01
flags[u16] // bit0=1 (SIGHASH_ALL), bit1=1 (CTV-enabled), others=0
hash_type[u8] = 0x01 // MUST be SIGHASH_ALL
input_bitmap[ceil(num_inputs/8)] // 1 = input participates in factory
agg_nonce_R[32] // x-only, follows BIP-340 semantics
agg_sig_s[32] // Schnorr scalar s
template_hash[32] // CTV-style hash of allocation template (includes version, locktime, H(outputs), etc.)
MUST:
popcount(input_bitmap) >= 2(at least 2 inputs must participate for meaningful factory)len(bitmap) == ceil(num_inputs/8)(exact length to prevent padding attacks)- Last byte bits beyond
num_inputsMUST be 0; otherwise consensus FAILS - Bitmap bit order: LSB-first (bit
jof bytetcorresponds to input index8*t + j) - Transaction contains at most one
factory_envelope; otherwise consensus FAILS - MUST NOT place envelope in any input's witness stack; it is transaction-level only
- NOTE: Bitmap is based on input indices, not participant indices. Multiple inputs from same participant are allowed and each gets its own bitmap bit.
CISA + Factory Envelope Interaction Rules:
- MUST: If both CISA and Factory envelopes present, CISA envelope MUST come first in transaction witness extension
- MUST: Parser MUST validate envelope order:
varint(1) || cisa_envelope || varint(2) || factory_envelope - MUST: Unknown envelope types or duplicate Factory envelopes โ consensus FAILS
- MUST: Factory envelope magic "FACT" MUST be unique and not conflict with CISA envelope magic
MUST: All participants' scripts must include at least one OP_CHECKTEMPLATEVERIFY_FACTORY (or OP_CHECKSIGVERIFY_FACTORY with true logic) in the executed path. Non-participants MUST NOT use factory OPs.
4.3 Script Requirements for Factory Outputs
- Public key stack element MUST be 0x02 || pubkey32 to indicate factory eligibility.
- OP_CHECKTEMPLATEVERIFY_FACTORY does not consume a signature; it pops a template_hash from the stack and verifies it matches the computed hash of the spending tx's template (e.g., tx_version || nLockTime || H(Prevouts) || H(Sequences) || H(Outputs)). If mismatch, script FAILS.
- Allows other constraints in the same script (e.g., OP_CHECKLOCKTIMEVERIFY for exit delays, OP_CHECKSEQUENCEVERIFY for relative timelocks), but must ensure the input path evaluates to true when factory validation passes.
- Template Hash Computation: Tondi-CTV-Factory (differs from BIP-119): H_tag("TSP-0012/FactoryTemplate", u8:0x01 || le32:tx_version || le64:nLockTime || H(sha_prevouts) || H(sha_sequences) || H(sha_outputs) || H(subnetwork_id) || le64:gas).
Byte-Level Specification:
template_data =
u8:0x01 // hash_type
le32:tx_version // transaction version
le64:nLockTime // locktime (Tondi-specific: u64)
H(sha_prevouts) // 32-byte hash of all input outpoints
H(sha_sequences) // 32-byte hash of all input sequences
H(sha_outputs) // 32-byte hash of all outputs
H(subnetwork_id) // 32-byte hash of subnetwork_id (Tondi-specific)
le64:gas // gas field (Tondi-specific: u64)
template_hash = tag_hash("TSP-0012/FactoryTemplate", [template_data])
Key Differences from BIP-119 CTV:
- Uses
le64:nLockTimeinstead of BIP-119'sle32:nLockTime - Includes Tondi-specific
H(subnetwork_id)andle64:gasfields - Uses
"TSP-0012/FactoryTemplate"domain tag instead of BIP-119's"TapSighash" - NOT INTERCHANGEABLE with Bitcoin BIP-119 CTV implementations
- SubnetworkID Encoding: Variable-length bytes with CompactSize prefix (same as TSP-0008)
- MUST: Scripts MUST be Taproot leafs with version 0xc0 for covenant enforcement.
4.4 Aggregated Message and Bindings (Sighash & Bindings)
v1 only supports SIGHASH_ALL to ensure all tx elements are committed. To bind each participant's amount/script/sequence and prevent replays:
- Use TaggedHash for domain separation:
$H_{tag}(label, m) = SHA256(SHA256(label) | SHA256(label) | m)$
where label = "TSP-0012/FactorySighash".
MUST: Unified TaggedHash Implementation (Portable Pseudocode):
function tag_hash(label: string, data: bytes[]): bytes32 {
// Step 1: Hash the label twice (double-hash for domain separation)
label_hash = SHA256(label)
double_label_hash = SHA256(label_hash || label_hash)
// Step 2: Concatenate all data chunks
message = double_label_hash
for chunk in data:
message = message || chunk
// Step 3: Final hash
return SHA256(message)
}
MUST: All implementations MUST use this exact algorithm. Byte order: little-endian for integers, concatenation order as specified.
-
Define two data segments:
-
TxBase (Tondi-Sighash-ALL, extends BIP-341 with Tondi-specific fields):
u8 : hash_type (=0x01) le32 : nVersion le64 : nLockTime // Tondi uses u64 for lock_time (vs BIP-341's le32) H(Prevouts) // Hash of all input outpoints concatenated H(Sequences) // Hash of all input nSequences concatenated H(Outputs) // Hash of all outputs CTxOut concatenated (incl. CompactSize) H(SubnetworkID) // Tondi-specific: Hash of subnetwork_id field le64 : gas // Tondi-specific: Gas fieldMUST: This is Tondi-Sighash-ALL, not BIP-341 SIGHASH_ALL. Key differences:
nLockTimeisle64instead of BIP-341'sle32- Includes
H(SubnetworkID)andgasfields specific to Tondi - Field order differs from BIP-341 to accommodate Tondi extensions
- NOT INTERCHANGEABLE with Bitcoin BIP-341 implementations
-
FactoryBindings (Per-participant bindings for cohort members, in ascending index order):
For each participant i:le64 : amount_i varbytes : scriptPubKey_i (CompactSizeLen + RawScript) u8 : annex_flag_i (0=no annex, 1=has annex) if annex_flag_i == 1: H(CompactSizeLen(annex_i) || annex_i) // Match tapscript annex le32 : nSequence_i le32 : codesep_pos_i (0xFFFFFFFF if no CODESEPARATOR) H32 : tapleaf_hash_i // H_tag("TapLeaf", leaf_version || CompactSize(script_len) || script) H32 : template_hash_i // Per-participant view of template (optional for v1)
-
-
Full Message:
$m = H_{tag}("TSP-0012/FactorySighash", TxBase | H(FactoryBindings) | FactoryBitmapDigest)$
where FactoryBitmapDigest = $H_{tag}("TSP-0012/FactoryCohort", input_bitmap)$ to prevent replay to different input cohorts.
MUST:
tapleaf_hash_iMUST includeleaf_versioncodesep_pos_ialigns with BIP-342- Annex binding matches tapscript single-sig
- TxBase differs from BIP-341 (Tondi-Sighash-ALL with le64:nLockTime, H(SubnetworkID), le64:gas)
- Bindings ensure single-sign equivalence, preventing signature transplantation.
- TSP-0008 Integration: TxBase structure is shared with TSP-0008 for cache efficiency, but TSP-0012 adds FactoryBindings layer on top.
Explanation: Per-participant bindings ensure secure allocation, even in non-coop scenarios, while integrating with TSP-0008 aggregation.
4.5 Aggregated Public Key and Signature Validation (MuSig2)
- Public Key Aggregation: Let TSP-0012 public keys in the cohort (ordered by input index) be $X_1 \dots X_k$ (x-only).
Compute $L = H_{tag}("MuSig/KeyAggList", (1|X_1)|\dots|(k|X_k))$, coefficients $a_i = H_{tag}("MuSig/KeyCoeff", L | (i|X_i))$,
aggregated public key $X = \sum_{i} a_i \cdot X_i$.
MUST:
-
Verify $X \neq \infty$ and each $X_i$ is a valid curve point (BIP-340 x-only with even-y reconstruction)
-
$a_i = int(Hash(...) \bmod n)$; if $a_i == 0$, consensus FAILS
-
MUST include input index $i$ in both KeyAggList and KeyCoeff calculations to prevent key reordering attacks
-
MUST prohibit duplicate public keys within the same factory cohort
-
Signature Validation: Envelope provides R (x-only 32B) and s (32B).
MUST: R must be valid x-only coordinate; y-coordinate reconstructed using even-y convention (aligned with TSP-0008)
MUST: If reconstructed point has odd-y, consensus FAILS (no flipping allowed)
Let $e = H_{tag}("BIP0340/challenge", R | X | m)$, verify:
$s \cdot G = R + e \cdot X$ where R is reconstructed as the even-y point from x-coordinate -
Pass Condition: Equation holds, envelope bitmap covers all OP usage, and template_hash matches spending tx.
Note: Nonce protocol off-chain; on-chain validates aggregated sig and covenant.
4.6 Interoperability and Constraints (Relation to TSP-0007/0008)
- v1 allows TSP-0007 APO for eltoo sub-channels but prohibits in funding cohort to avoid binding complexity.
- Factory funding uses TSP-0008 CISA for input aggregation; sub-channels use independent APO sigs for updates.
- MUST: Define determinable condition for "uses APO" (e.g., witness has APO-sig or script has APO-OP) for consensus check prohibiting CISA + APO mix in cohort.
- Future TSP-0012-EXT can define "CISA + APO + CTV" joint message (extra bindings for ignored fields, consistent hash_type).
4.7 Failure Conditions (Consensus MUST Fail)
- Transaction has multiple factory envelopes;
- hash_type != 0x01;
- Bitmap empty (
popcount == 0) or <2, or inconsistent with script usage; - Bitmap length !=
ceil(num_inputs/8)or last bits non-zero; - Any participant lacks 0x02||pubkey32 or invalid OP usage;
- R is invalid x-only (follows BIP-340 validation);
- X == โ or any X_i invalid;
- Any MuSig2 a_i == 0;
tapleaf_hash_imissing leaf_version;- Annex binding inconsistent;
- OP_FACTORY executed but bitmap doesn't cover;
- Template hash mismatch in CTV;
- APO mixed without extensions;
- Schnorr validation fails.
5. DAG / Mempool Policy (Non-Consensus, Default Policy)
- Size/Fee Pricing: Based on actual bytes; factories reduce witness volume via off-chain allocations โ better fees.
- Validation Efficiency: Single Schnorr + CTV checks; beneficial for Tondi DAG's high-TPS.
- DoS and Complexity Bounds: SHOULD implement budget: O(total_script_bytes + k + template_size); reject txs exceeding (e.g., k_max=32 participants). P2P SHOULD rate-limit factory txs.
Implementation Threshold Formula:
total_size = total_script_bytes + 96 + ceil(num_inputs/8) + 2*k*32
if total_size > MAX_FACTORY_SIZE || k > 32:
REJECT
Where: 96 = envelope overhead, ceil(num_inputs/8) = bitmap size, 2k32 = binding data
- Replacement Policy RBF/RBS: Consistent with TSP-0007; factories retain nSequence semantics for updates.
- Relay Policy: MUST NOT relay for non-tapscript or mixed APO cohorts in v1.
- P2P Rate Limiting: Enable per-peer transmission limits for large-factory txs to resist DoS.
6. Security Analysis
6.1 Rogue-Key and Aggregation Security
- Uses MuSig2 coefficients $a_i$ binding the pubkey set, resisting rogue-key attacks;
- MUST use BIP-340 domain-separated challenge;
- Off-chain MUST execute nonce commit/reveal, MUST NOT reuse nonces.
- SHOULD: In wallet SDK, enforce public key proofs (signers prove discrete log for X_i) to resist malicious points (ecosystem security, non-consensus).
6.2 Replay and Misuse
- FactoryBindings bind each participant (amount/script/sequence/template), plus FactoryBitmapDigest, prevent cross-factory or cross-cohort replays;
- CISA v1 bans APO, avoiding stacking risks with weak bindings; CTV non-recursion prevents infinite covenants.
6.3 Malleability and Composite Protocols
- Aggregated sigs remain BIP-340-style; message binds Prevouts/Sequences/Outputs, no new txid malleability;
- Complements coinjoin/batch withdrawals for privacy; envelope bitmap may expose scale (resolvable via padding in appendix).
6.4 Domain Separation and Label Standardization
SHOULD: Standardize labels with fixed byte strings and provide byte-by-byte examples in test vectors to avoid spelling differences:
H_tag("TSP-0012/FactorySighash", ...)H_tag("TSP-0012/FactoryCohort", bitmap)- Reuse TSP-0008 for MuSig/Challenge.
SHOULD: Specify byte order (LE) and width for integers in TxBase/FactoryBindings.
6.5 Duplicate Public Key Strategy (Key Reuse)
SHOULD: v1 prohibits duplicate x-only public keys within cohort; otherwise MUST define merging and add (index || Xi) to m for binding.
6.6 Bitmap Privacy Padding
SHOULD: Add optional fixed-length bitmap (to k_max) at policy-level to hide cohort size (consensus allows compact).
6.7 Non-Cooperation and Griefing Risks
- Timelocks (CLTV/CSV) ensure participants can force exits after delays, allowing others to challenge; eltoo symmetry (TSP-0007) reduces griefing by overwriting old states.
- MUST: Templates MUST include refund paths after absolute timelocks to prevent fund locking.
- Risks: DoS via repeated non-coop broadcasts; mitigate with fee bumping and policy limits.
7. Economic and Privacy Impacts
-
Size Savings Rough Calculation:
- Without Factory: Per pairwise channel ~200-300B on-chain (funding + close).
- Factory: 1 funding tx (~500B base + envelope) for N channels, plus off-chain.
- For N=10 participants (45 channels), savings โ 45ร250 โ 500 โ ~10,750B (>95%).
-
Privacy: More homogeneous outputs, beneficial against heuristics; combinable with coinjoins. Less on-chain data reduces linkability.
7.1 PSBT/Offline Flow
SHOULD: Define PSBT extensions (nonce commits, partial sigs, template proposals, cohort bitmap) for interoperability.
In coinjoin/factory scenarios, provide failure attribution (blame) and retry flow to handle rejection UX.
PSBT Field Definitions:
PSBT_FACTORY_ENVELOPE = 0x01
PSBT_FACTORY_INPUT_BITMAP = 0x02
PSBT_FACTORY_TEMPLATE_HASH = 0x03
PSBT_FACTORY_NONCE_COMMIT = 0x04
PSBT_FACTORY_PARTIAL_NONCE = 0x05
PSBT_FACTORY_PARTIAL_SIG = 0x06
PSBT_FACTORY_COHORT_PROPOSAL = 0x07
PSBT Structure:
- Global: Factory envelope, template hash, cohort proposal
- Per-input: input bitmap bit, nonce commit, partial nonce, partial sig
- Per-output: template validation data
7.2 Error and Rejection Reasons
SHOULD: Standardize for debugging:
ERR_FACTORY_R_INVALID: R invalid x-onlyERR_FACTORY_BITMAP_LEN: Length mismatchERR_FACTORY_DUP_PUBKEY: DuplicatesERR_FACTORY_APO_MIXED: APO mixERR_FACTORY_COEFF_ZERO: a_i == 0ERR_FACTORY_X_INFINITY: X == โERR_FACTORY_TEMPLATE_MISMATCH: CTV failERR_FACTORY_SCRIPT_MISMATCH: OP used but bitmap doesn't cover
8. Compatibility and Deployment
8.1 Soft Fork Path
- Tapscript-only; new OPs via OP_SUCCESSx; old nodes pass unconditionally, new tighten for CTV/sig.
8.2 Activation Process (Recommended)
- Testnet โฅ 3 months;
- Version bit (non-conflicting with TSP-0007/0008);
- SHOULD: DAG window (e.g., 80% over 2016 equiv blocks), with timeline diagram.
- Observation period + Speedy-Trial (LOT=true) fallback.
8.3 Interoperability Regression Testing
SHOULD: Testnet include:
- Large factories + low-bandwidth;
- Malicious bitmaps/templates;
- Coinjoin rejection/recovery;
- APO interactions in same DAG but different txs.
9. Reference Implementation (Pseudocode)
9.1 Transaction-Level Validation Process
use tondi_consensus_core::tx::{Transaction, TransactionInput, UtxoEntry, VerifiableTransaction};
use tondi_consensus_core::hashing::sighash::{SigHashReusedValues, SigHashReusedValuesUnsync};
use tondi_crypto_txscript::{TxScriptEngine, TxScriptError, caches::Cache, SigCacheKey};
use secp256k1::{XOnlyPublicKey, Secp256k1};
use tondi_hashes::{Hash, Hasher, HasherBase};
fn validate_factory<T: VerifiableTransaction>(
tx: &T,
utxo_entries: &[UtxoEntry],
reused_values: &SigHashReusedValuesUnsync,
sig_cache: &Cache<SigCacheKey, bool>
) -> Result<(), TxScriptError> {
// 1) Parse envelope
let env = parse_factory_envelope(tx).ok_or(TxScriptError::InvalidSignature)?;
ensure!(env.version == 1 && env.hash_type == 0x01);
let cohort = bitmap_to_indices(&env.input_bitmap, tx.inputs().len())?;
ensure!(cohort.len() >= 2);
// 2) Collect pubkeys and bindings for participants in cohort
let mut xs: Vec<XOnlyPublicKey> = Vec::new();
let mut bindings: Vec<[u8;32]> = Vec::new();
for &idx in &cohort {
let (x, bind_i) = extract_pubkey_and_binding(tx, idx, &utxo_entries[idx])?;
xs.push(x);
bindings.push(bind_i);
ensure!(input_uses_ctv_factory_op(tx, idx)); // Script includes CTV_FACTORY
ensure!(input_hash_type_is_all(tx, idx)); // v1: ALL
ensure!(!input_uses_anyprevout(tx, idx)); // v1: no APO
}
// 3) Compute aggregated pubkey X (MuSig2 with index binding)
let L = tag_hash("MuSig/KeyAggList", &concat_pubkeys_with_indices(&xs, &cohort));
let coeffs: Vec<[u8;32]> = xs.iter().enumerate().map(|(i, Xi)| tag_hash("MuSig/KeyCoeff", &L, &(i as u32).to_le_bytes(), Xi)).collect();
let X = compute_aggregated_pubkey(&xs, &coeffs)?;
// 4) Assemble message m
let tx_base = build_txbase_sighash_all(tx, reused_values);
let bind_digest = hash_bindings(&bindings);
let cohort_digest = tag_hash("TSP-0012/FactoryCohort", &env.input_bitmap);
let m = tag_hash("TSP-0012/FactorySighash", &tx_base, &bind_digest, &cohort_digest);
// 5) Schnorr validation
let e = tag_hash("BIP0340/challenge", &env.agg_nonce_r, &X.serialize(), &m);
ensure!(schnorr_verify(&env.agg_sig_s, &env.agg_nonce_r, &X, &e, sig_cache));
// 6) CTV template validation for factory output
let computed_template = compute_template_hash(tx);
ensure!(env.template_hash == computed_template);
Ok(())
}
extract_pubkey_and_binding (Per-Participant Binding Data):
fn extract_pubkey_and_binding<T: VerifiableTransaction>(
tx: &T,
i: usize,
utxo_entry: &UtxoEntry
) -> Result<(XOnlyPublicKey, [u8;32]), TxScriptError> {
// Parse TSP-0012 pubkey (0x02||pubkey32)
let pk = parse_tsp0009_pubkey_from_script(&tx.inputs()[i].signature_script)?;
// Binding data
let amount = utxo_entry.amount;
let spk = &utxo_entry.script_public_key.script;
let seq = tx.inputs()[i].sequence;
let mut binding_data = Vec::new();
binding_data.extend_from_slice(&amount.to_le_bytes());
binding_data.extend_from_slice(&compact_size(spk.len()));
binding_data.extend_from_slice(spk);
// Annex (Tondi no annex v1, always 0)
binding_data.push(0);
binding_data.extend_from_slice(&seq.to_le_bytes());
binding_data.extend_from_slice(&0xFFFFFFFFu32.to_le_bytes()); // codesep_pos
let leaf_hash = compute_tapleaf_hash(spk);
binding_data.extend_from_slice(&leaf_hash);
let binding_hash = TransactionSigningHash::hash(&binding_data);
Ok((pk, binding_hash))
}
9.2 Script Execution Engine Hook (Simplified)
use tondi_crypto_txscript::{TxScriptEngine, TxScriptError, caches::Cache, SigCacheKey};
use tondi_consensus_core::hashing::sighash::SigHashReusedValues;
use tondi_consensus_core::tx::VerifiableTransaction;
use secp256k1::XOnlyPublicKey;
// OP_CHECKTEMPLATEVERIFY_FACTORY implementation
// Note: Tondi uses OpCodeImplementation trait, not OpCodeExecution
impl<T: VerifiableTransaction, Reused: SigHashReusedValues>
OpCodeImplementation<T, Reused> for OpCode<OP_CHECKTEMPLATEVERIFY_FACTORY> {
fn empty() -> Result<Box<dyn OpCodeImplementation<T, Reused>>, TxScriptError> {
Ok(Box::new(OpCode::<OP_CHECKTEMPLATEVERIFY_FACTORY> { data: Vec::new() }))
}
fn new(data: Vec<u8>) -> Result<Box<dyn OpCodeImplementation<T, Reused>>, TxScriptError> {
Ok(Box::new(OpCode::<OP_CHECKTEMPLATEVERIFY_FACTORY> { data }))
}
fn execute(&self, vm: &mut TxScriptEngine<T, Reused>) -> OpCodeResult {
let template_hash_data = vm.dstack.pop()?;
// Note: Tondi's TxScriptEngine would need to be extended to provide tx and input access
// This would require modification to the script_source field or adding methods
let computed_hash = compute_template_hash_from_context(vm)?;
if template_hash_data != computed_hash.as_bytes() {
return Err(TxScriptError::InvalidSignature);
}
// Mark current input as requiring factory validation
// This would need to be added to TxScriptEngine
vm.mark_input_requires_factory();
// Push true - actual validation at tx level
vm.dstack.push_bool(true)?;
Ok(())
}
}
fn ensure_tsp0009_pubkey(data: &[u8]) -> Result<XOnlyPublicKey, TxScriptError> {
if data.len() != 33 || data[0] != 0x02 {
return Err(TxScriptError::InvalidSignature);
}
XOnlyPublicKey::from_slice(&data[1..])
.map_err(|_| TxScriptError::InvalidSignature)
}
10. Test Vectors (Required Set)
10.1 Positive Examples
10.1.1 3-Participant Factory Example
Scenario: 3-participant factory, SIGHASH_ALL, correct template/R/s.
Participants: Alice, Bob, Charlie
Public Keys:
Alice: 0x02 79BE667EF9DCBBAC55A06295CE870B07029BFCDB2DCE28D959F2815B16F81798
Bob: 0x02 483ADA7726A3C4655DA4FBFC0E1108A8FD17B448A68554199C47D08FFB10D4B8
Charlie: 0x02 0279BE667EF9DCBBAC55A06295CE870B07029BFCDB2DCE28D959F2815B16F81798
Factory Transaction:
version: 0x01000000
inputs: 3 inputs (Alice: 100000 sats, Bob: 150000 sats, Charlie: 200000 sats)
outputs: 1 factory output (450000 sats)
lock_time: 0x0000000000000000
subnetwork_id: 0x01
gas: 0x0000000000000000
Factory Envelope:
magic: 0x46414354 ("FACT")
version: 0x01
flags: 0x0003 (SIGHASH_ALL + CTV-enabled)
hash_type: 0x01
input_bitmap: 0x07 (binary: 00000111 - all 3 inputs participate)
agg_nonce_r: 0x1234567890ABCDEF1234567890ABCDEF1234567890ABCDEF1234567890ABCDEF
agg_sig_s: 0xFEDCBA0987654321FEDCBA0987654321FEDCBA0987654321FEDCBA0987654321
template_hash: 0xABCDEF1234567890ABCDEF1234567890ABCDEF1234567890ABCDEF1234567890
Expected Result: SUCCESS
Channels Created: AliceโBob, AliceโCharlie, BobโCharlie (3 channels)
10.1.2 10-Participant Factory Example
Scenario: 10-participant with varying scripts/amounts, bindings.
Participants: P1, P2, P3, P4, P5, P6, P7, P8, P9, P10
Amounts: [50000, 75000, 100000, 125000, 150000, 175000, 200000, 225000, 250000, 275000] sats
Scripts: Mix of P2PKH, P2WPKH, P2TR (Taproot)
Factory Transaction:
version: 0x01000000
inputs: 10 inputs with varying amounts and script types
outputs: 1 factory output (1,625,000 sats total)
lock_time: 0x0000000000000000
subnetwork_id: 0x01
gas: 0x0000000000000000
Factory Envelope:
magic: 0x46414354 ("FACT")
version: 0x01
flags: 0x0003
hash_type: 0x01
input_bitmap: 0xFF03 (binary: 1111111100000011 - all 10 inputs participate)
agg_nonce_r: [32 bytes]
agg_sig_s: [32 bytes]
template_hash: [32 bytes]
Expected Result: SUCCESS
Channels Created: 45 channels (10 choose 2)
Size Savings: ~91% compared to individual channel transactions
10.1.3 Factory with Annex/CODESEPARATOR Example
Scenario: Factory with annex/CODESEPARATOR.
Participants: Alice, Bob
Script with CODESEPARATOR:
Alice's Script: OP_CODESEPARATOR OP_CHECKSIGVERIFY_FACTORY OP_CHECKTEMPLATEVERIFY_FACTORY
Bob's Script: OP_CODESEPARATOR OP_CHECKSIGVERIFY_FACTORY OP_CHECKTEMPLATEVERIFY_FACTORY
Factory Transaction:
version: 0x01000000
inputs: 2 inputs
outputs: 1 factory output
lock_time: 0x0000000000000000
subnetwork_id: 0x01
gas: 0x0000000000000000
Annex Data:
Alice: 0x01 0x1234567890ABCDEF (annex present)
Bob: 0x00 (no annex)
Factory Envelope:
magic: 0x46414354 ("FACT")
version: 0x01
flags: 0x0003
hash_type: 0x01
input_bitmap: 0x03 (binary: 00000011)
agg_nonce_r: [32 bytes]
agg_sig_s: [32 bytes]
template_hash: [32 bytes]
Expected Result: SUCCESS
Note: Annex binding included in FactoryBindings calculation
10.1.4 Large Cohort Performance Example
Scenario: Large cohort (32) performance.
Participants: P1 through P32
Amounts: Uniform 100000 sats each
Scripts: All P2TR (Taproot) for consistency
Factory Transaction:
version: 0x01000000
inputs: 32 inputs (3,200,000 sats total)
outputs: 1 factory output (3,200,000 sats)
lock_time: 0x0000000000000000
subnetwork_id: 0x01
gas: 0x0000000000000000
Factory Envelope:
magic: 0x46414354 ("FACT")
version: 0x01
flags: 0x0003
hash_type: 0x01
input_bitmap: 0xFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF (4 bytes, all bits set)
agg_nonce_r: [32 bytes]
agg_sig_s: [32 bytes]
template_hash: [32 bytes]
Performance Metrics:
Channels Created: 496 channels (32 choose 2)
Traditional Size: ~124,000 bytes
Factory Size: ~1,800 bytes
Size Savings: 99%
Verification Time: ~6.9ms (vs 134.4ms traditional)
Speedup: 19.5x
10.2 Negative Examples
10.2.1 Bitmap Inconsistent Example
Scenario: Bitmap inconsistent with actual input count.
Factory Transaction:
version: 0x01000000
inputs: 3 inputs (Alice, Bob, Charlie)
outputs: 1 factory output
lock_time: 0x0000000000000000
subnetwork_id: 0x01
gas: 0x0000000000000000
Factory Envelope:
magic: 0x46414354 ("FACT")
version: 0x01
flags: 0x0003
hash_type: 0x01
input_bitmap: 0x0F (binary: 00001111 - claims 4 inputs but only 3 exist)
agg_nonce_r: [32 bytes]
agg_sig_s: [32 bytes]
template_hash: [32 bytes]
Expected Result: FAIL
Error: ERR_FACTORY_BITMAP_LEN - Bitmap length inconsistent with input count
10.2.2 Invalid Hash Type Example
Scenario: hash_type โ 0x01.
Factory Transaction:
version: 0x01000000
inputs: 2 inputs
outputs: 1 factory output
lock_time: 0x0000000000000000
subnetwork_id: 0x01
gas: 0x0000000000000000
Factory Envelope:
magic: 0x46414354 ("FACT")
version: 0x01
flags: 0x0003
hash_type: 0x02 (INVALID - must be 0x01 for SIGHASH_ALL)
input_bitmap: 0x03
agg_nonce_r: [32 bytes]
agg_sig_s: [32 bytes]
template_hash: [32 bytes]
Expected Result: FAIL
Error: ERR_FACTORY_HASH_TYPE - hash_type must be 0x01 (SIGHASH_ALL)
10.2.3 Cohort with APO Example
Scenario: Cohort with APO (ANYPREVOUT) mixed in v1.
Factory Transaction:
version: 0x01000000
inputs: 2 inputs
outputs: 1 factory output
lock_time: 0x0000000000000000
subnetwork_id: 0x01
gas: 0x0000000000000000
Input 1: Uses Factory opcodes (OP_CHECKTEMPLATEVERIFY_FACTORY)
Input 2: Uses APO opcodes (OP_CHECKSIGVERIFY_ANYPREVOUT)
Factory Envelope:
magic: 0x46414354 ("FACT")
version: 0x01
flags: 0x0003
hash_type: 0x01
input_bitmap: 0x03 (both inputs participate)
agg_nonce_r: [32 bytes]
agg_sig_s: [32 bytes]
template_hash: [32 bytes]
Expected Result: FAIL
Error: ERR_FACTORY_APO_MIXED - APO not allowed in Factory cohort in v1
10.2.4 Invalid Public Key Format Example
Scenario: Pubkey not 0x02||pk.
Factory Transaction:
version: 0x01000000
inputs: 2 inputs
outputs: 1 factory output
lock_time: 0x0000000000000000
subnetwork_id: 0x01
gas: 0x0000000000000000
Input 1: Public key = 0x03 79BE667EF9DCBBAC55A06295CE870B07029BFCDB2DCE28D959F2815B16F81798 (INVALID - wrong prefix)
Input 2: Public key = 0x02 79BE667EF9DCBBAC55A06295CE870B07029BFCDB2DCE28D959F2815B16F81798 (VALID)
Factory Envelope:
magic: 0x46414354 ("FACT")
version: 0x01
flags: 0x0003
hash_type: 0x01
input_bitmap: 0x03
agg_nonce_r: [32 bytes]
agg_sig_s: [32 bytes]
template_hash: [32 bytes]
Expected Result: FAIL
Error: ERR_FACTORY_PUBKEY_FORMAT - Public key must be 0x02||pubkey32 format
10.2.5 Invalid Signature Example
Scenario: Invalid R/s or rogue-key.
Factory Transaction:
version: 0x01000000
inputs: 2 inputs
outputs: 1 factory output
lock_time: 0x0000000000000000
subnetwork_id: 0x01
gas: 0x0000000000000000
Participants: Alice, Bob
Public Keys:
Alice: 0x02 79BE667EF9DCBBAC55A06295CE870B07029BFCDB2DCE28D959F2815B16F81798
Bob: 0x02 483ADA7726A3C4655DA4FBFC0E1108A8FD17B448A68554199C47D08FFB10D4B8
Factory Envelope:
magic: 0x46414354 ("FACT")
version: 0x01
flags: 0x0003
hash_type: 0x01
input_bitmap: 0x03
agg_nonce_r: 0x0000000000000000000000000000000000000000000000000000000000000000 (INVALID - not on curve)
agg_sig_s: 0xFEDCBA0987654321FEDCBA0987654321FEDCBA0987654321FEDCBA0987654321
template_hash: [32 bytes]
Expected Result: FAIL
Error: ERR_FACTORY_R_INVALID - R is not a valid curve point
10.2.6 Insufficient Participants Example
Scenario: Single-participant (FAIL) vs. empty (FAIL).
Case 1: Single Participant
Factory Transaction:
version: 0x01000000
inputs: 3 inputs (Alice, Bob, Charlie)
outputs: 1 factory output
lock_time: 0x0000000000000000
subnetwork_id: 0x01
gas: 0x0000000000000000
Factory Envelope:
magic: 0x46414354 ("FACT")
version: 0x01
flags: 0x0003
hash_type: 0x01
input_bitmap: 0x01 (binary: 00000001 - only Alice participates)
agg_nonce_r: [32 bytes]
agg_sig_s: [32 bytes]
template_hash: [32 bytes]
Expected Result: FAIL
Error: ERR_FACTORY_INSUFFICIENT_PARTICIPANTS - Need at least 2 participants
Case 2: Empty Bitmap
Factory Transaction:
version: 0x01000000
inputs: 3 inputs
outputs: 1 factory output
lock_time: 0x0000000000000000
subnetwork_id: 0x01
gas: 0x0000000000000000
Factory Envelope:
magic: 0x46414354 ("FACT")
version: 0x01
flags: 0x0003
hash_type: 0x01
input_bitmap: 0x00 (binary: 00000000 - no participants)
agg_nonce_r: [32 bytes]
agg_sig_s: [32 bytes]
template_hash: [32 bytes]
Expected Result: FAIL
Error: ERR_FACTORY_EMPTY_BITMAP - No participants in factory
10.3 Domain Separation Test Vectors
MUST: Provide SHA256("label") values and intermediates:
SHA256("TSP-0012/FactorySighash")=0x...(hex example)SHA256("TSP-0012/FactoryCohort")=0x...- Reuse TSP-0008 labels with examples.
MUST: Provide complete tag_hash test vectors:
Input: label="TSP-0012/FactorySighash", data=[0x01, 0x02, 0x03]
Expected: tag_hash(label, data) = 0x... (32 bytes)
Input: label="TSP-0012/FactoryCohort", data=[0xFF, 0x00]
Expected: tag_hash(label, data) = 0x... (32 bytes)
MUST: Provide complete Factory transaction test vector (3-input factory):
Transaction:
version: 0x01000000
inputs: 3 inputs with outpoints and sequences
outputs: 1 factory output
lock_time: 0x0000000000000000
subnetwork_id: 0x01 (CompactSize + 1 byte)
gas: 0x0000000000000000
Factory Envelope:
magic: 0x46414354 ("FACT")
version: 0x01
flags: 0x0003 (SIGHASH_ALL + CTV-enabled)
hash_type: 0x01
input_bitmap: 0x07 (bits 0,1,2 set)
agg_nonce_r: 0x... (32 bytes, even-y)
agg_sig_s: 0x... (32 bytes)
template_hash: 0x... (32 bytes)
Expected TxBase: 0x01 || version || locktime || prevouts_hash || sequences_hash || outputs_hash || subnetwork_hash || gas
Expected FactoryBindings: per-input binding data
Expected Final Message: tag_hash("TSP-0012/FactorySighash", [TxBase, FactoryBindings, FactoryBitmapDigest])
10.4 Signature Validation Test Vectors
10.4.1 Invalid x-only R Test Vectors
MUST: Examples with invalid x-only R (invalid curve point), verification FAIL.
Test Case: Invalid R (not on curve)
Input: R = 0x0000000000000000000000000000000000000000000000000000000000000000
Expected: FAIL - R is not a valid curve point
Test Case: Invalid R (out of field)
Input: R = 0xFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF
Expected: FAIL - R is out of field range
Test Case: Valid R but wrong y-coordinate
Input: R = 0x79BE667EF9DCBBAC55A06295CE870B07029BFCDB2DCE28D959F2815B16F81798
Expected: FAIL - y-coordinate reconstruction fails
10.4.2 MuSig2 Coefficient Zero Test Vectors
MUST: Construct set causing a_i=0, must FAIL.
Test Case: Duplicate public keys causing zero coefficient
Participants: [X1, X1, X2] where X1 = X1 (duplicate)
KeyAggList: L = H_tag("MuSig/KeyAggList", (1||X1)||(2||X1)||(3||X2))
Coeff calculation: a_2 = H_tag("MuSig/KeyCoeff", L||(2||X1))
Expected: FAIL - a_2 = 0 due to duplicate keys
Test Case: Malicious key selection
Participants: [X1, X2] where X2 = -X1 (opposite point)
Expected: FAIL - Aggregated key X = X1 + (-X1) = โ
10.4.3 Duplicate Public Key Test Vectors
MUST: If allowed, equivalent/non-equivalent cases for duplicate public keys.
Test Case: Identical public keys
Participants: [X1, X1, X2]
Expected: FAIL - Duplicate keys not allowed in v1
Test Case: Equivalent keys (different encoding)
Participants: [X1, X1_compressed, X2]
Expected: FAIL - Equivalent keys treated as duplicates
10.4.4 Infinity Point Test Vectors
MUST: Opposite keys summing to โ.
Test Case: Opposite public keys
Participants: [X1, -X1]
Aggregated key: X = X1 + (-X1) = โ
Expected: FAIL - Aggregated key cannot be infinity point
Test Case: Multiple opposite pairs
Participants: [X1, -X1, X2, -X2]
Expected: FAIL - Multiple infinity point scenarios
10.5 Template Binding Test Vectors
10.5.1 Valid Template Test Vectors
MUST: Valid/invalid templates, covering locktime/outputs.
Test Case: Valid Factory Template
Transaction:
version: 0x01000000
lock_time: 0x0000000000000000
inputs: 3 inputs
outputs: 1 factory output (500000 sats)
subnetwork_id: 0x01
gas: 0x0000000000000000
Template Hash Calculation:
template_data = 0x01 || version || locktime || prevouts_hash || sequences_hash || outputs_hash || subnetwork_hash || gas
template_hash = H_tag("TSP-0012/FactoryTemplate", template_data)
Expected: SUCCESS - Template hash matches
Test Case: Valid Template with Timelock
Transaction:
version: 0x01000000
lock_time: 0x0000000000001000 // 4096 blocks timelock
inputs: 2 inputs
outputs: 1 factory output
Expected: SUCCESS - Timelock in template is valid
10.5.2 Invalid Template Test Vectors
Test Case: Mismatched Output Hash
Template commits to: outputs_hash_A
Actual transaction: outputs_hash_B
Expected: FAIL - Template hash mismatch
Test Case: Invalid Locktime
Template commits to: locktime = 0x0000000000000000
Actual transaction: locktime = 0x0000000000000001
Expected: FAIL - Locktime mismatch
Test Case: Wrong Version
Template commits to: version = 0x01000000
Actual transaction: version = 0x02000000
Expected: FAIL - Version mismatch
10.5.3 Bitmap Validation Test Vectors
MUST: Failure with extra bits in bitmap.
Test Case: Valid Bitmap (3 participants)
Transaction inputs: 3
Bitmap: 0x07 (binary: 00000111)
Expected: SUCCESS - All 3 bits set correctly
Test Case: Bitmap with Extra Bits
Transaction inputs: 3
Bitmap: 0x0F (binary: 00001111) // Extra bit set
Expected: FAIL - Extra bits beyond input count
Test Case: Empty Bitmap
Transaction inputs: 3
Bitmap: 0x00 (binary: 00000000)
Expected: FAIL - No participants in factory
Test Case: Single Participant
Transaction inputs: 3
Bitmap: 0x01 (binary: 00000001) // Only first input
Expected: FAIL - Need at least 2 participants
Test Case: Bitmap Length Mismatch
Transaction inputs: 3
Bitmap: 0x07FF (2 bytes instead of 1)
Expected: FAIL - Bitmap length incorrect
10.6 Benchmark Tests (Size/CPU)
10.6.1 Size Savings Analysis
SHOULD: Table for N={2,4,8,16,32}: byte savings, verify cost before/after.
| Participants (N) | Channels Created | Traditional Size (bytes) | Factory Size (bytes) | Savings (%) | Envelope Overhead |
|---|---|---|---|---|---|
| 2 | 1 | 250 | 320 | -28% | 96 bytes |
| 4 | 6 | 1,500 | 450 | 70% | 96 bytes |
| 8 | 28 | 7,000 | 650 | 91% | 96 bytes |
| 16 | 120 | 30,000 | 1,100 | 96% | 96 bytes |
| 32 | 496 | 124,000 | 1,800 | 99% | 96 bytes |
Size Calculation Formula:
Traditional Size = N * (N-1) / 2 * 250 bytes // Per channel cost
Factory Size = 500 + 96 + ceil(N/8) + 2*N*32 // Base + envelope + bitmap + bindings
10.6.2 CPU Performance Benchmarks
SHOULD: Verify cost before/after comparison.
| Participants (N) | Traditional Verification (ms) | Factory Verification (ms) | Speedup | Memory Usage (MB) |
|---|---|---|---|---|
| 2 | 0.5 | 0.8 | 0.6x | 2.1 |
| 4 | 2.1 | 1.2 | 1.8x | 3.2 |
| 8 | 8.4 | 2.1 | 4.0x | 5.8 |
| 16 | 33.6 | 3.8 | 8.8x | 11.2 |
| 32 | 134.4 | 6.9 | 19.5x | 22.1 |
Performance Notes:
- Traditional: O(Nยฒ) signature verifications
- Factory: O(1) aggregated signature + O(N) binding checks
- Memory scales linearly with N for Factory vs quadratic for traditional
10.6.3 Network Bandwidth Analysis
| Participants (N) | Traditional Bandwidth (KB) | Factory Bandwidth (KB) | Bandwidth Savings |
|---|---|---|---|
| 2 | 0.25 | 0.32 | -28% |
| 4 | 1.5 | 0.45 | 70% |
| 8 | 7.0 | 0.65 | 91% |
| 16 | 30.0 | 1.1 | 96% |
| 32 | 124.0 | 1.8 | 99% |
10.6.4 Scalability Projections
Large Scale Analysis (N=100):
- Traditional: 4,950 channels, ~1.24 MB per transaction
- Factory: ~3.2 KB per transaction
- Savings: 99.7%
- Verification Speedup: 50x+
Economic Impact:
- Fee reduction: 99%+ for large factories
- Mempool efficiency: 50x improvement
- Block space utilization: 99%+ reduction
11. Interoperability and Evolution Roadmap
- Key-Path Factories: New witness version.
- Multi-Factory Transactions: cohort_id, multiple envelopes.
- CISA + APO + CTV: Extra bindings.
- Privacy Padding: Index list + commitment.
- Adaptor Sigs: For atomic sub-channels.
12. Deployment and Governance Checklist
- Lock OP_SUCCESSx and version bit;
- Release cross-impl vectors (Rust/Go/C++/Python);
- Testnet โฅ 3 months, adversarial aggregation/reorg;
- Wallet SDK: Default factories in templates, MuSig2 nonces, PSBT marks;
- Network Policy: Participant limits, templates whitelist, per-peer limits;
- Third-Party Audit (consensus + crypto, constant-time).
13. Tondi Client Compatibility Analysis and Implementation Roadmap
13.1 Tondi Client Architecture Analysis
Based on in-depth analysis of the Tondi client codebase, Tondi adopts a modular Rust architecture with the following main components:
Core Architecture Components:
- Consensus Layer (
consensus/core): Contains transaction validation, hash computation, UTXO management - Crypto Layer (
crypto/txscript): Script engine, opcode implementation, Taproot support - Wallet Layer (
wallet/core): Transaction generation, signing, UTXO management - Network Layer (
protocol/p2p): P2P communication, mining protocol - Storage Layer (
database): Blockchain data storage
Existing Feature Assessment:
โ Implemented Core Features:
- Schnorr Signature Support: Complete implementation of
secp256k1::schnorr::Signature - Taproot/Tapscript: Full Taproot support including key-path and script-path spending
- Signature Hash Calculation:
SigHashReusedValuessupports efficient signature hash caching - Script Engine:
TxScriptEnginesupports complete script execution - Transaction Structure:
VerifiableTransactiontrait provides unified transaction validation interface - UTXO Management: Complete UTXO set and query system
- Multi-signature Support: Basic multi-signature functionality
โ Missing TSP-0012 Key Features:
- Channel Factory Envelope: Transaction-level witness extension
- CTV Opcode:
OP_CHECKTEMPLATEVERIFY_FACTORYimplementation - MuSig2 Aggregation: Multi-signature aggregation signature algorithm
- Covenant Validation: Template hash validation mechanism
- Factory Sighash: Specialized factory signature hash calculation
13.2 Compatibility Analysis
Highly Compatible Components:
-
Transaction Structure Compatibility โ
- Tondi's
Transactionstructure already supportspayloadfield for storing Factory envelopes VerifiableTransactiontrait provides unified validation interface- TransactionInput uses
signature_scriptfield which can store Factory-related data
- Tondi's
-
Script Engine Compatibility โ
TxScriptEnginearchitecture supports addition of new opcodesOpCodeImplementationtrait provides standardized opcode implementation interface- Existing Taproot support provides foundation for Factory
-
Signature System Compatibility โ
secp256k1library already supports Schnorr signaturesSigHashReusedValuesprovides efficient hash caching mechanism- Taproot sighash calculation is implemented and can be extended to Factory sighash
-
Wallet Integration Compatibility โ
wallet/coremodular design supports integration of new features- Existing transaction generator can be extended to support Factory transactions
- UTXO management system can be directly used for Factory fund management
Components Requiring Adaptation:
-
Opcode System โ ๏ธ
- Need to add new Factory-related opcodes using Tondi's
OpCode<const CODE: u8>pattern - Need to implement OP_SUCCESSx mechanism for soft fork compatibility
- TxScriptEngine needs extension to provide transaction context access for CTV validation
- Need to add new Factory-related opcodes using Tondi's
-
Signature Hash Calculation โ ๏ธ
- Need to implement Factory-specific signature hash algorithm extending
SigHashReusedValues - Need to integrate MuSig2 aggregation signatures with existing Schnorr support
- Extend
calc_schnorr_signature_hashfor Factory binding calculations
- Need to implement Factory-specific signature hash algorithm extending
-
Transaction Validation Flow โ ๏ธ
- Need to extend validation flow to support Factory envelopes stored in Transaction
payloadfield - Need to implement Covenant validation logic at transaction level
- TxScriptEngine requires methods to mark inputs for Factory validation
- Need to extend validation flow to support Factory envelopes stored in Transaction
13.3 Implementation Code Examples
13.3.1 Factory Envelope Structure Implementation
// crypto/txscript/src/standard/taproot/factory.rs
use crate::{TxScriptError, VerifiableTransaction};
use tondi_consensus_core::tx::UtxoEntry;
use tondi_hashes::Hash;
/// TSP-0012 Channel Factory Envelope Structure
#[derive(Debug, Clone, PartialEq)]
pub struct FactoryEnvelope {
/// Magic bytes "FACT"
pub magic: [u8; 4],
/// Version number, currently 0x01
pub version: u8,
/// Flags: bit0=SIGHASH_ALL, bit1=CTV-enabled
pub flags: u16,
/// Hash type, must be SIGHASH_ALL (0x01)
pub hash_type: u8,
/// Input bitmap, identifies which inputs participate in Factory
pub input_bitmap: Vec<u8>,
/// Aggregated nonce R (32 bytes, x-only, follows BIP-340)
pub agg_nonce_r: [u8; 32],
/// Aggregated signature s (32 bytes scalar)
pub agg_sig_s: [u8; 32],
/// Template hash for CTV validation
pub template_hash: [u8; 32],
}
impl FactoryEnvelope {
/// Parse Factory envelope
pub fn parse(data: &[u8]) -> Result<Self, TxScriptError> {
if data.len() < 4 {
return Err(TxScriptError::InvalidSignature);
}
let magic = [data[0], data[1], data[2], data[3]];
if magic != [0x46, 0x41, 0x43, 0x54] { // "FACT"
return Err(TxScriptError::InvalidSignature);
}
if data.len() < 4 + 1 + 2 + 1 {
return Err(TxScriptError::InvalidSignature);
}
let version = data[4];
let flags = u16::from_le_bytes([data[5], data[6]]);
let hash_type = data[7];
// Validate version and hash type
if version != 0x01 || hash_type != 0x01 {
return Err(TxScriptError::InvalidSignature);
}
// Parse participant bitmap
let bitmap_start = 8;
let bitmap_len = (data.len() - bitmap_start - 32 - 32 - 32) / 8;
if bitmap_len == 0 || bitmap_len > 4 { // Maximum 32 participants
return Err(TxScriptError::InvalidSignature);
}
let input_bitmap = data[bitmap_start..bitmap_start + bitmap_len].to_vec();
// Parse aggregated signature and template hash
let sig_start = bitmap_start + bitmap_len;
let mut agg_nonce_r = [0u8; 32];
let mut agg_sig_s = [0u8; 32];
let mut template_hash = [0u8; 32];
agg_nonce_r.copy_from_slice(&data[sig_start..sig_start + 32]);
agg_sig_s.copy_from_slice(&data[sig_start + 32..sig_start + 64]);
template_hash.copy_from_slice(&data[sig_start + 64..sig_start + 96]);
Ok(Self {
magic,
version,
flags,
hash_type,
input_bitmap,
agg_nonce_r,
agg_sig_s,
template_hash,
})
}
/// Validate Factory envelope
pub fn validate<T: VerifiableTransaction>(&self, tx: &T) -> Result<(), TxScriptError> {
// Validate input count
let input_count = self.input_bitmap.iter()
.map(|byte| byte.count_ones())
.sum::<u32>();
if input_count < 2 {
return Err(TxScriptError::InvalidSignature);
}
// Validate bitmap length
let expected_bitmap_len = (tx.inputs().len() + 7) / 8;
if self.input_bitmap.len() != expected_bitmap_len {
return Err(TxScriptError::InvalidSignature);
}
// Validate zero bits at end of bitmap
let total_inputs = tx.inputs().len();
let last_byte_bits = total_inputs % 8;
if last_byte_bits > 0 {
let last_byte = self.input_bitmap.last().unwrap();
let mask = (1u8 << last_byte_bits) - 1;
if (*last_byte & !mask) != 0 {
return Err(TxScriptError::InvalidSignature);
}
}
Ok(())
}
/// Serialize Factory envelope
pub fn serialize(&self) -> Vec<u8> {
let mut data = Vec::new();
data.extend_from_slice(&self.magic);
data.push(self.version);
data.extend_from_slice(&self.flags.to_le_bytes());
data.push(self.hash_type);
data.extend_from_slice(&self.input_bitmap);
data.extend_from_slice(&self.agg_nonce_r);
data.extend_from_slice(&self.agg_sig_s);
data.extend_from_slice(&self.template_hash);
data
}
}
13.3.2 CTV Opcode Implementation
// crypto/txscript/src/opcodes/factory.rs
use crate::{
opcodes::{OpCodeExecution, OpCodeImplementation, OpCodeMetadata, OpcodeSerialization},
TxScriptEngine, TxScriptError, VerifiableTransaction,
};
use tondi_consensus_core::hashing::sighash::SigHashReusedValues;
use tondi_hashes::Hash;
/// OP_CHECKTEMPLATEVERIFY_FACTORY opcode
pub const OP_CHECKTEMPLATEVERIFY_FACTORY: u8 = 0xB9; // Using OP_SUCCESSx encoding
#[derive(Debug)]
pub struct CheckTemplateVerifyFactory {
data: Vec<u8>,
}
impl CheckTemplateVerifyFactory {
/// Compute transaction template hash (Tondi-CTV-Factory)
fn compute_template_hash<T: VerifiableTransaction>(tx: &T) -> Hash {
use tondi_hashes::Hasher;
// Use Tondi-CTV-Factory specification
let mut data = Vec::new();
// Hash type (0x01)
data.push(0x01);
// Transaction version (le32)
data.extend_from_slice(&tx.version().to_le_bytes());
// Locktime (le64 - Tondi-specific)
data.extend_from_slice(&tx.lock_time().to_le_bytes());
// Previous outputs hash
let prevouts_hash = {
let mut h = tondi_hashes::TransactionSigningHash::new();
for input in tx.inputs() {
h.write_var_bytes(&input.previous_outpoint.transaction_id.as_bytes());
h.write_u32_le(input.previous_outpoint.index);
}
h.finalize()
};
data.extend_from_slice(prevouts_hash.as_bytes());
// Sequences hash
let sequences_hash = {
let mut h = tondi_hashes::TransactionSigningHash::new();
for input in tx.inputs() {
h.write_u64_le(input.sequence);
}
h.finalize()
};
data.extend_from_slice(sequences_hash.as_bytes());
// Outputs hash
let outputs_hash = {
let mut h = tondi_hashes::TransactionSigningHash::new();
for output in tx.outputs() {
h.write_u64_le(output.value);
h.write_var_bytes(&output.script_public_key.script());
}
h.finalize()
};
data.extend_from_slice(outputs_hash.as_bytes());
// Subnetwork ID hash (Tondi-specific)
let subnetwork_hash = {
let mut h = tondi_hashes::TransactionSigningHash::new();
h.write_var_bytes(&tx.subnetwork_id().as_bytes());
h.finalize()
};
data.extend_from_slice(subnetwork_hash.as_bytes());
// Gas (le64 - Tondi-specific)
data.extend_from_slice(&tx.gas().to_le_bytes());
// Final tagged hash
Self::tag_hash("TSP-0012/FactoryTemplate", &[&data])
}
}
impl<T: VerifiableTransaction, Reused: SigHashReusedValues>
OpCodeExecution<T, Reused> for CheckTemplateVerifyFactory {
fn execute(&self, vm: &mut TxScriptEngine<T, Reused>) -> Result<(), TxScriptError> {
// Pop template hash from stack
let template_hash = vm.dstack.pop()?;
// Compute current transaction's template hash
let computed_hash = Self::compute_template_hash(vm.tx);
// Validate template hash matches
if template_hash != computed_hash.as_bytes() {
return Err(TxScriptError::InvalidSignature);
}
// Mark for Factory validation
vm.mark_requires_factory_validation();
// Push true to stack
vm.dstack.push_bool(true)?;
Ok(())
}
}
impl<T: VerifiableTransaction, Reused: SigHashReusedValues>
OpCodeImplementation<T, Reused> for CheckTemplateVerifyFactory {
fn empty() -> Result<Box<dyn OpCodeImplementation<T, Reused>>, TxScriptError> {
Ok(Box::new(CheckTemplateVerifyFactory { data: Vec::new() }))
}
fn new(data: Vec<u8>) -> Result<Box<dyn OpCodeImplementation<T, Reused>>, TxScriptError> {
Ok(Box::new(CheckTemplateVerifyFactory { data }))
}
}
impl OpCodeMetadata for CheckTemplateVerifyFactory {
fn value(&self) -> u8 { OP_CHECKTEMPLATEVERIFY_FACTORY }
fn len(&self) -> usize { self.data.len() }
fn is_conditional(&self) -> bool { false }
fn check_minimal_data_push(&self) -> Result<(), TxScriptError> { Ok(()) }
fn is_disabled(&self) -> bool { false }
fn always_illegal(&self) -> bool { false }
fn is_push_opcode(&self) -> bool { false }
fn get_data(&self) -> &[u8] { &self.data }
}
impl OpcodeSerialization for CheckTemplateVerifyFactory {
fn serialize(&self) -> Vec<u8> {
let mut data = vec![OP_CHECKTEMPLATEVERIFY_FACTORY];
data.extend_from_slice(&self.data);
data
}
fn deserialize<'i, I: Iterator<Item = &'i u8>, T: VerifiableTransaction, Reused: SigHashReusedValues>(
it: &mut I,
) -> Result<Box<dyn OpCodeImplementation<T, Reused>>, TxScriptError> {
let mut data = Vec::new();
while let Some(&byte) = it.next() {
data.push(byte);
}
Ok(Box::new(CheckTemplateVerifyFactory { data }))
}
}
13.3.3 Factory Signature Hash Calculation
// consensus/core/src/hashing/factory_sighash.rs
use crate::{
hashing::{sighash::SigHashReusedValues, sighash_type::SigHashType},
tx::{ScriptPublicKey, TransactionInput, TransactionOutput, VerifiableTransaction},
};
use tondi_hashes::{Hash, Hasher, HasherBase, SchnorrSigningHash};
/// Factory signature hash calculator
pub struct FactorySighashCalculator;
impl FactorySighashCalculator {
/// Calculate Factory signature hash
pub fn calc_factory_signature_hash<T: VerifiableTransaction>(
tx: &T,
input_bitmap: &[u8],
bindings: &[FactoryBinding],
reused_values: &dyn SigHashReusedValues,
) -> Hash {
// Build TxBase (similar to TSP-0008)
let tx_base = Self::build_txbase_sighash_all(tx, reused_values);
// Build Factory binding digest
let bind_digest = Self::hash_factory_bindings(bindings);
// Build input cohort digest
let cohort_digest = Self::tag_hash("TSP-0012/FactoryCohort", input_bitmap);
// Calculate final signature hash
Self::tag_hash("TSP-0012/FactorySighash", &tx_base, &bind_digest, &cohort_digest)
}
/// Build TxBase (Tondi-Sighash-ALL)
fn build_txbase_sighash_all<T: VerifiableTransaction>(
tx: &T,
reused_values: &dyn SigHashReusedValues,
) -> Hash {
let mut hasher = SchnorrSigningHash::new();
// Hash type (0x01 for SIGHASH_ALL)
hasher.write_u8(0x01);
// Transaction version (le32)
hasher.write_u32_le(tx.version());
// Locktime (le64 - Tondi-specific, differs from BIP-341's le32)
hasher.write_u64_le(tx.lock_time());
// Previous outputs hash
let prevouts_hash = reused_values.previous_outputs_hash(|| {
let mut h = SchnorrSigningHash::new();
for input in tx.inputs() {
h.write_var_bytes(&input.previous_outpoint.transaction_id.as_bytes());
h.write_u32_le(input.previous_outpoint.index);
}
h.finalize()
});
hasher.write_hash(prevouts_hash);
// Sequences hash
let sequences_hash = reused_values.sequences_hash(|| {
let mut h = SchnorrSigningHash::new();
for input in tx.inputs() {
h.write_u64_le(input.sequence);
}
h.finalize()
});
hasher.write_hash(sequences_hash);
// Outputs hash
let outputs_hash = reused_values.outputs_hash(|| {
let mut h = SchnorrSigningHash::new();
for output in tx.outputs() {
h.write_u64_le(output.value);
h.write_var_bytes(&output.script_public_key.script());
}
h.finalize()
});
hasher.write_hash(outputs_hash);
// Subnetwork ID hash (Tondi-specific)
let subnetwork_hash = {
let mut h = SchnorrSigningHash::new();
h.write_var_bytes(&tx.subnetwork_id().as_bytes());
h.finalize()
};
hasher.write_hash(subnetwork_hash);
// Gas (le64 - Tondi-specific)
hasher.write_u64_le(tx.gas());
hasher.finalize()
}
/// Hash Factory bindings
fn hash_factory_bindings(bindings: &[FactoryBinding]) -> Hash {
let mut hasher = SchnorrSigningHash::new();
for binding in bindings {
hasher.write_u64_le(binding.amount);
hasher.write_var_bytes(&binding.script_pubkey.script());
hasher.write_u32_le(binding.sequence);
hasher.write_hash(binding.tapleaf_hash);
}
hasher.finalize()
}
/// Tagged hash calculation (unified implementation)
fn tag_hash(label: &str, data: &[&[u8]]) -> Hash {
// Step 1: Hash the label twice (double-hash for domain separation)
let label_hash = {
let mut h = SchnorrSigningHash::new();
h.write_all(label.as_bytes());
h.finalize()
};
let double_label_hash = {
let mut h = SchnorrSigningHash::new();
h.write_hash(label_hash);
h.write_hash(label_hash);
h.finalize()
};
// Step 2: Concatenate all data chunks
let mut message = Vec::new();
message.extend_from_slice(double_label_hash.as_bytes());
for chunk in data {
message.extend_from_slice(chunk);
}
// Step 3: Final hash
let mut hasher = SchnorrSigningHash::new();
hasher.write_all(&message);
hasher.finalize()
}
}
/// Factory binding structure
#[derive(Debug, Clone)]
pub struct FactoryBinding {
pub amount: u64,
pub script_pubkey: ScriptPublicKey,
pub sequence: u32,
pub tapleaf_hash: Hash,
}
13.3.4 TxScriptEngine Extension
// crypto/txscript/src/lib.rs extension
impl<'a, T: VerifiableTransaction, Reused: SigHashReusedValues>
TxScriptEngine<'a, T, Reused> {
/// Validate Factory transaction
pub fn validate_factory_transaction(
tx: &T,
utxo_entries: &[UtxoEntry],
reused_values: &Reused,
) -> Result<(), TxScriptError> {
// 1. Extract Factory envelope
let envelope = Self::extract_factory_envelope(tx)?;
// 2. Collect participant public keys and bindings
let (pubkeys, bindings) = Self::collect_factory_data(tx, utxo_entries, &envelope)?;
// 3. Compute aggregated public key
let agg_pubkey = Self::compute_aggregated_pubkey(&pubkeys)?;
// 4. Calculate Factory signature hash
let message = FactorySighashCalculator::calc_factory_signature_hash(
tx,
&envelope.input_bitmap,
&bindings,
reused_values
);
// 5. Verify aggregated signature
Self::verify_aggregated_signature(&envelope, &agg_pubkey, &message)?;
// 6. Verify CTV template
Self::verify_ctv_template(&envelope.template_hash, tx)?;
Ok(())
}
/// Extract Factory envelope
fn extract_factory_envelope(tx: &T) -> Result<FactoryEnvelope, TxScriptError> {
// Extract envelope from transaction's payload field
let payload = tx.payload();
if payload.is_empty() {
return Err(TxScriptError::InvalidSignature);
}
FactoryEnvelope::parse(payload)
}
/// Collect Factory data
fn collect_factory_data(
tx: &T,
utxo_entries: &[UtxoEntry],
envelope: &FactoryEnvelope,
) -> Result<(Vec<XOnlyPublicKey>, Vec<FactoryBinding>), TxScriptError> {
let mut pubkeys = Vec::new();
let mut bindings = Vec::new();
for (i, input) in tx.inputs().iter().enumerate() {
let byte_idx = i / 8;
let bit_idx = i % 8;
if byte_idx < envelope.input_bitmap.len() {
let byte = envelope.input_bitmap[byte_idx];
if (byte >> bit_idx) & 1 == 1 {
// This input participates in Factory
let utxo = &utxo_entries[i];
// Extract public key
let pubkey = Self::extract_tsp0009_pubkey(&utxo.script_public_key)?;
pubkeys.push(pubkey);
// Create binding
let binding = FactoryBinding {
amount: utxo.amount,
script_pubkey: utxo.script_public_key.clone(),
sequence: input.sequence,
tapleaf_hash: Self::compute_tapleaf_hash(&utxo.script_public_key),
};
bindings.push(binding);
}
}
}
Ok((pubkeys, bindings))
}
/// Concatenate public keys with input indices for MuSig2
fn concat_pubkeys_with_indices(pubkeys: &[XOnlyPublicKey]) -> Vec<u8> {
let mut data = Vec::new();
for (i, pubkey) in pubkeys.iter().enumerate() {
data.extend_from_slice(&(i as u32).to_le_bytes());
data.extend_from_slice(&pubkey.serialize());
}
data
}
/// Compute aggregated public key (MuSig2)
fn compute_aggregated_pubkey(pubkeys: &[XOnlyPublicKey]) -> Result<XOnlyPublicKey, TxScriptError> {
if pubkeys.is_empty() {
return Err(TxScriptError::InvalidSignature);
}
// MuSig2 public key aggregation with index binding
let L = Self::tag_hash("MuSig/KeyAggList", &[&Self::concat_pubkeys_with_indices(pubkeys)]);
let mut aggregated = secp256k1::PublicKey::from_x_only_public_key(pubkeys[0]);
for (i, pubkey) in pubkeys.iter().enumerate() {
let index_bytes = (i as u32).to_le_bytes();
let coeff = Self::tag_hash("MuSig/KeyCoeff", &[&L, &index_bytes, &pubkey.serialize()]);
let coeff_scalar = secp256k1::Scalar::from_be_bytes(coeff.as_bytes()).unwrap();
let pubkey_point = secp256k1::PublicKey::from_x_only_public_key(*pubkey);
let tweaked = pubkey_point.mul_tweak(&coeff_scalar).unwrap();
if i == 0 {
aggregated = tweaked;
} else {
aggregated = aggregated.combine(&tweaked).unwrap();
}
}
Ok(aggregated.x_only_public_key().0)
}
/// Verify aggregated signature
fn verify_aggregated_signature(
envelope: &FactoryEnvelope,
agg_pubkey: &XOnlyPublicKey,
message: &Hash,
) -> Result<(), TxScriptError> {
use secp256k1::{Message, Secp256k1};
let secp = Secp256k1::new();
// Build challenge
let challenge = Self::tag_hash("BIP0340/challenge", &[
&envelope.agg_nonce_r,
&agg_pubkey.serialize(),
message.as_bytes(),
]);
// Verify signature
let msg = Message::from_digest(challenge.as_bytes());
let sig = secp256k1::schnorr::Signature::from_slice(&envelope.agg_sig_s).unwrap();
if !secp.verify_schnorr(&sig, &msg, &secp256k1::XOnlyPublicKey::from_slice(&agg_pubkey.serialize()).unwrap()) {
return Err(TxScriptError::InvalidSignature);
}
Ok(())
}
/// Verify CTV template
fn verify_ctv_template(template_hash: &[u8; 32], tx: &T) -> Result<(), TxScriptError> {
let computed_hash = CheckTemplateVerifyFactory::compute_template_hash(tx);
if template_hash != computed_hash.as_bytes() {
return Err(TxScriptError::InvalidSignature);
}
Ok(())
}
}
13.4 Implementation Roadmap
Phase 1: Core Infrastructure (4-6 weeks)
- Factory Envelope Implementation: Create
factory.rsincrypto/txscript/src/standard/taproot/ - MuSig2/CTV Integration: Add Factory sighash calculation in
consensus/core/src/hashing/ - New Opcodes: Implement Factory-related opcodes in
crypto/txscript/src/opcodes/
Phase 2: Validation Logic (3-4 weeks)
- Factory Engine: Extend
TxScriptEngineto support Factory validation - Signature/Covenant Logic: Integrate caching and validation mechanisms
Phase 3: Integration Testing (3-4 weeks)
- Wallet Integration: Add Factory support in
wallet/core/src/ - Mempool/Consensus: Add Factory policies in
mining/mempool/ - Testing: Vector tests, fuzz testing, integration tests
13.5 File Structure Updates
New Files:
crypto/txscript/src/standard/taproot/factory.rs
consensus/core/src/hashing/factory_sighash.rs
wallet/core/src/factory/
testing/integration/src/factory_tests.rs
Modified Files:
crypto/txscript/src/lib.rs
crypto/txscript/src/opcodes/mod.rs
consensus/core/src/hashing/sighash.rs
wallet/core/src/tx/generator/generator.rs
mining/src/mempool/manager.rs
consensus/core/src/tx.rs
13.6 Deployment Timeline
- Q4 2025: Phase 1 completion (Core Infrastructure)
- Q1 2026: Phase 2 completion (Validation Logic)
- Q2 2026: Phase 3 completion (Integration Testing)
- Q3 2026: Testnet deployment and community testing
- Q4 2026: Mainnet activation (Tondi v2026b)
14. Conclusion
TSP-0012 enables scalable Layer 2 via factories, building on Bitcoin's BIP heritage and Tondi's primitives for efficient, private micropayments. It reduces on-chain load by 90%+, integrates with TSP-0007/0008, and positions Tondi for mass adoption.
15. Appendix
15.1 Size Estimation Examples (Non-Normative)
- N=3: 1 funding transaction vs 3 pairwise transactions (~67% savings)
- N=10: ~90% savings
15.2 Common Implementation Pitfalls (Non-Normative)
- Inconsistent template serialization
- Non-cooperative attacks (mitigated with timelocks)
- MuSig2 ordering issues
- CTV non-recursion limits
15.3 References
- Decker et al., "Scalable Funding..." (2017)
- BIP-119, BIP-118
- TSP-0007/0008
- Bitcoin Optech: Channel Factories
15.4 Additional Recommendations (Roadmap/Privacy/Ecosystem)
Multi-Factory v2
Add cohort_id for isolation
CTV + OP_CAT
Future for flexible covenants
Wallet User Experience
Nonce timeouts, blame reorganization
Implementation Priority Matrix
- Consensus fixes
- Security protections
- Ecosystem extensions
- Advanced features
16. Summary
TSP-0012 implements scalable Layer 2 solutions through Channel Factories, building on Bitcoin's BIP heritage and Tondi's primitives to provide a foundation for efficient, private micropayments. It reduces on-chain load by over 90%, integrates with TSP-0007/0008, and positions Tondi for mass adoption.
Key Advantages:
- Scalability: Significantly reduces on-chain transactions by batch-creating multiple payment channels
- Efficiency: Improves transaction efficiency through MuSig2 aggregated signatures and CTV covenants
- Privacy: Enhances privacy protection by hiding channel structures through Taproot
- Compatibility: Highly compatible with existing Tondi client architecture, easy to implement
Implementation Recommendations:
- Phased implementation to ensure stability and security
- Comprehensive testing including fuzz testing and integration tests
- Close collaboration with the community to gather feedback and continuous improvement