UNPKG

@btc-vision/transaction

Version:

OPNet transaction library allows you to create and sign transactions for the OPNet network.

284 lines (224 loc) 9.92 kB
# Custom Script Transactions Execute arbitrary Bitcoin scripts using `CustomScriptTransaction`. ## Overview `CustomScriptTransaction` allows you to embed and execute arbitrary Bitcoin scripts within a Taproot or P2MR transaction. It uses a two-transaction model similar to interactions: a funding transaction creates a UTXO at a derived script address (P2TR or P2MR), and the custom script transaction spends that UTXO by satisfying the embedded script. ```mermaid flowchart LR subgraph TX1["Transaction 1: Funding"] U["User UTXOs"] --> SA["Script Address<br/>(P2TR / P2MR)"] U --> Change["Change Output"] end subgraph TX2["Transaction 2: Custom Script"] SA2["Script UTXO"] --> Out["Script Output"] SA2 --> RF["Refund"] end SA -->|"creates UTXO"| SA2 subgraph Witness["Witness Stack"] W1["Custom witnesses"] W2["Signature(s)"] W3["Script"] W4["Control block"] end Witness --> TX2 ``` ## Factory Method Custom script transactions are created through `TransactionFactory.createCustomScriptTransaction()`: ```typescript import { TransactionFactory } from '@btc-vision/transaction'; const factory = new TransactionFactory(); const [fundingTx, customTx, nextUTXOs, inputUtxos] = await factory.createCustomScriptTransaction(parameters); ``` The return type is a tuple: `[string, string, UTXO[], UTXO[]]`. | Index | Type | Description | |-------|------|-------------| | `[0]` | `string` | Funding transaction hex | | `[1]` | `string` | Custom script transaction hex | | `[2]` | `UTXO[]` | Change UTXOs for subsequent transactions | | `[3]` | `UTXO[]` | Original input UTXOs that were consumed | ## Parameters `ICustomTransactionParameters` extends `SharedInteractionParameters` (with `challenge` omitted): | Parameter | Type | Required | Default | Description | |-----------|------|----------|---------|-------------| | `signer` | `Signer \| UniversalSigner` | Yes | - | Key pair used to sign inputs | | `network` | `Network` | Yes | - | Bitcoin network | | `utxos` | `UTXO[]` | Yes | - | Available UTXOs to fund the transaction | | `from` | `string` | Yes | - | Sender address | | `to` | `string` | Yes | - | Target address for the script output | | `feeRate` | `number` | Yes | - | Fee rate in sat/vB | | `priorityFee` | `bigint` | Yes | - | OPNet priority fee in satoshis | | `gasSatFee` | `bigint` | Yes | - | OPNet gas fee in satoshis | | `script` | `(Uint8Array \| Stack)[]` | Yes | - | Array of script elements (data pushes and opcodes) | | `witnesses` | `Uint8Array[]` | Yes | - | Witness data to satisfy the script | | `randomBytes` | `Uint8Array` | No | Auto-generated | 32-byte random salt | | `annex` | `Uint8Array` | No | - | Optional Taproot annex data (without `0x50` prefix) | | `mldsaSigner` | `QuantumBIP32Interface \| null` | No | - | ML-DSA signer | | `useP2MR` | `boolean` | No | `false` | Use P2MR (BIP 360) instead of P2TR. Eliminates the quantum-vulnerable key-path spend. | ## Script and Witness Structure The `script` parameter is an array of Bitcoin script elements compiled into the Taproot leaf. The `witnesses` parameter provides the data that satisfies the script at spending time. ```mermaid flowchart TB subgraph ScriptDef["Script Definition"] S1["Data push (Uint8Array)"] S2["Opcode (e.g., OP_DROP)"] S3["Opcode (e.g., OP_CHECKSIG)"] end subgraph Compiled["Compiled Script"] CS["CustomGenerator.compile(script)"] end subgraph WitnessStack["Witness Stack (at spend time)"] W1["witnesses[0]"] W2["witnesses[1]"] W3["..."] Sig["Signature(s)"] Scr["Compiled script"] CB["Control block"] end ScriptDef --> CS CS --> WitnessStack ``` The witness stack is assembled in the custom finalizer: 1. All custom `witnesses` elements 2. Tap script signatures (from signing) 3. The compiled script (leaf script) 4. The Taproot control block 5. Optional annex data (if provided) ## Annex Data The optional `annex` parameter allows embedding arbitrary data in the Taproot annex field. If the provided data does not start with the `0x50` prefix, it is automatically prepended: ```typescript // Annex will be prefixed with 0x50 automatically const parameters = { // ... other params annex: new Uint8Array([0x01, 0x02, 0x03]), }; // Or provide the prefix yourself const parameters2 = { // ... other params annex: new Uint8Array([0x50, 0x01, 0x02, 0x03]), }; ``` ## Script Address Derivation The script address is derived from a seed generated by hashing the random bytes: ```mermaid flowchart LR RB["randomBytes"] --> Hash["hash256(randomBytes)"] Hash --> Seed["scriptSeed"] Seed --> Signer["EcKeyPair.fromSeedKeyPair()"] Seed --> Addr["AddressGenerator.generatePKSH()"] ``` The `CustomScriptTransaction` instance exposes: - `scriptAddress` -- the derived PKSH address - `p2trAddress` -- the P2TR address (either explicit `to` or computed) - `getRndBytes()` -- the random bytes used ## Two-Transaction Flow ```mermaid sequenceDiagram participant App participant Factory as TransactionFactory participant FT as FundingTransaction participant CST as CustomScriptTransaction App->>Factory: createCustomScriptTransaction(params) Factory->>Factory: Iterate funding amount estimation Factory->>FT: Create & sign funding tx FT-->>Factory: Signed funding tx + script UTXO Factory->>CST: Create custom script tx<br/>(funded by FT output) Factory->>CST: Sign custom script tx CST-->>Factory: Signed custom script tx Factory-->>App: [fundingHex, customHex, nextUTXOs, inputUtxos] ``` ## Complete Example ```typescript import { TransactionFactory, EcKeyPair, UTXO, } from '@btc-vision/transaction'; import { networks, opcodes } from '@btc-vision/bitcoin'; async function executeCustomScript() { const network = networks.bitcoin; const factory = new TransactionFactory(); // Create signer const signer = EcKeyPair.fromWIF(process.env.PRIVATE_KEY!, network); const address = EcKeyPair.getTaprootAddress(signer, network); // Fetch UTXOs const utxos: UTXO[] = await fetchUTXOs(address); // Define a custom script: // This script expects a preimage on the witness stack, // drops it, then requires a valid signature. const preimage = new TextEncoder().encode('secret-preimage'); const customScript = [ preimage, opcodes.OP_DROP, // The signer's pubkey is embedded by the generator ]; // Witnesses must satisfy the script const witnesses = [ preimage, // Matches the data push + OP_DROP ]; // Create and sign const [fundingTx, customTx, nextUTXOs, inputUtxos] = await factory.createCustomScriptTransaction({ signer, mldsaSigner: null, network, utxos, from: address, to: address, feeRate: 10, priorityFee: 1000n, gasSatFee: 500n, script: customScript, witnesses, // useP2MR: true, // Uncomment for quantum-safe P2MR output (bc1z...) }); // Broadcast both transactions in order await broadcastTransaction(fundingTx); await broadcastTransaction(customTx); console.log('Custom script executed!'); console.log('Change UTXOs:', nextUTXOs); } ``` ## Signing Process The custom script transaction uses a dual-signing process for input 0: ```mermaid flowchart TB subgraph Input0["Input 0 (Script Path)"] CS["Contract Signer<br/>(derived from seed)"] WS["Wallet Signer<br/>(user's key)"] CS --> CF["Custom Finalizer<br/>[witnesses, sigs,<br/>script, controlBlock, annex?]"] WS --> CF end subgraph InputN["Inputs 1+ (Key Path)"] WS2["Wallet Signer"] --> SF["Standard Finalizer"] end ``` Parallel signing is used for inputs 1+ when `isUniversalSigner` is available, falling back to sequential signing otherwise. ## Error Handling ```typescript try { const result = await factory.createCustomScriptTransaction(params); } catch (error) { const message = (error as Error).message; if (message.includes('Bitcoin script is required')) { // The script parameter is missing } else if (message.includes('Witness(es) are required')) { // The witnesses parameter is missing } else if (message.includes('Field "to" not provided')) { // The to address is missing } else if (message.includes('Field "from" not provided')) { // The from address is missing } else if (message.includes('Could not sign funding transaction')) { // Funding step failed } } ``` ## Best Practices 1. **Match witnesses to script.** The witness stack must satisfy the script exactly. An incorrect witness will cause the transaction to be invalid. 2. **Test scripts on regtest first.** Custom scripts can lock funds permanently if they contain errors. Always test on regtest before mainnet. 3. **Keep scripts minimal.** Larger scripts increase transaction weight and fees. Only include the opcodes you need. 4. **Use annex sparingly.** The Taproot annex is non-standard and may cause issues with some nodes or services. 5. **Broadcast in order.** The funding transaction must be accepted before the custom script transaction can spend its output. 6. **Track change UTXOs.** The third element of the return tuple (`nextUTXOs`) contains your new spendable outputs. 7. **Consider P2MR for quantum safety.** Set `useP2MR: true` to use P2MR outputs (BIP 360) instead of P2TR. P2MR commits directly to a Merkle root without a key-path spend, eliminating quantum-vulnerable internal pubkey exposure. --- [< MultiSig Transactions](./multisig-transactions.md) | [Cancel Transactions >](./cancel-transactions.md)