UNPKG

@btc-vision/transaction

Version:

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

373 lines (290 loc) 12.1 kB
# Multi-Signature Transactions Create M-of-N multi-signature transactions using `MultiSignTransaction`. ## Overview `MultiSignTransaction` creates Taproot-based multi-signature transactions where M out of N key holders must sign before the transaction is valid. It uses a PSBT (Partially Signed Bitcoin Transaction) workflow to collect signatures from multiple parties. Supports both P2TR and P2MR (BIP 360) output types. ```mermaid flowchart TB subgraph Keys["Public Keys (N=3)"] A["PubKey A"] B["PubKey B"] C["PubKey C"] end Keys --> Script["Taproot MultiSig Script<br/>M-of-N (e.g., 2-of-3)"] subgraph PSBT["PSBT Workflow"] Create["Create PSBT<br/>(Signer 1)"] Sign1["Sign partial<br/>(Signer 2)"] Finalize["Finalize & broadcast"] Create --> Sign1 --> Finalize end Script --> PSBT subgraph Outputs Receiver["Receiver<br/>(requestedAmount)"] Refund["Refund Vault<br/>(remaining)"] end PSBT --> Outputs ``` ## Direct Construction Unlike other transaction types, `MultiSignTransaction` is used directly rather than through `TransactionFactory`: ```typescript import { MultiSignTransaction } from '@btc-vision/transaction'; const multiSigTx = new MultiSignTransaction(parameters); const psbt = await multiSigTx.signPSBT(); ``` ## Parameters `MultiSignParameters`: | Parameter | Type | Required | Default | Description | |-----------|------|----------|---------|-------------| | `network` | `Network` | Yes | - | Bitcoin network | | `utxos` | `UTXO[]` | Yes | - | UTXOs locked in the multisig address | | `feeRate` | `number` | Yes | - | Fee rate in sat/vB | | `pubkeys` | `Uint8Array[]` | Yes | - | Array of N public keys for the multisig | | `minimumSignatures` | `number` | Yes | - | Minimum signatures required (M) | | `receiver` | `string` | Yes | - | Address to receive the requested amount | | `requestedAmount` | `bigint` | Yes | - | Amount to send to the receiver in satoshis | | `refundVault` | `string` | Yes | - | Address to receive remaining funds | | `from` | `string` | No | - | Source address | | `to` | `string` | No | - | Target address | | `psbt` | `Psbt` | No | - | Existing PSBT to add signatures to | | `useP2MR` | `boolean` | No | `false` | Use P2MR (BIP 360) instead of P2TR. Eliminates the quantum-vulnerable key-path spend (no NUMS point needed). | Note: The `signer`, `priorityFee`, `gasSatFee`, `from`, and `to` fields from `ITransactionParameters` are omitted. `from` and `to` are re-declared as optional above. A dummy signer is used internally since actual signing happens via the PSBT workflow. The `mldsaSigner` field is inherited from `ITransactionParameters` (type `QuantumBIP32Interface | null`). ## Output Structure The multisig transaction creates two outputs: ```mermaid flowchart LR subgraph Inputs["Multisig UTXOs"] V1["Vault UTXO 1"] V2["Vault UTXO 2"] end subgraph Outputs R["Receiver<br/>(requestedAmount)"] RV["Refund Vault<br/>(total - requestedAmount)"] end V1 --> R V1 --> RV V2 --> R V2 --> RV ``` The refund amount is calculated as: ``` refundAmount = totalInputValue - requestedAmount ``` ## PSBT Workflow Multi-signature transactions use a PSBT-based workflow for collecting signatures from multiple parties: ```mermaid sequenceDiagram participant S1 as Signer 1 participant S2 as Signer 2 participant S3 as Signer 3 participant BTC as Bitcoin Network S1->>S1: Create MultiSignTransaction S1->>S1: signPSBT() -> PSBT S1->>S1: Export PSBT as base64 S1->>S2: Send base64 PSBT S2->>S2: Import PSBT from base64 S2->>S2: signPartial() Note over S2: Check if final (2/3) S2->>S2: attemptFinalizeInputs() alt 2-of-3 reached S2->>BTC: Broadcast finalized tx else Need more signatures S2->>S3: Forward PSBT S3->>S3: signPartial() S3->>S3: attemptFinalizeInputs() S3->>BTC: Broadcast finalized tx end ``` ## Key Static Methods ### `MultiSignTransaction.fromBase64()` Reconstruct a `MultiSignTransaction` from a base64-encoded PSBT: ```typescript const multiSigTx = MultiSignTransaction.fromBase64({ psbt: base64String, network, utxos: vaultUtxos, feeRate: 10, pubkeys: [pubkeyA, pubkeyB, pubkeyC], minimumSignatures: 2, receiver: recipientAddress, requestedAmount: 100000n, refundVault: vaultAddress, }); ``` ### `MultiSignTransaction.signPartial()` Add a signature from an additional signer: ```typescript const result = MultiSignTransaction.signPartial( psbt, // The PSBT to sign signer, // The signer's key pair originalInputCount, // Number of inputs before multisig inputs minimums, // Array of required signatures per input ); // result.signed - true if this signer added a signature // result.final - true if enough signatures have been collected ``` ### `MultiSignTransaction.verifyIfSigned()` Check whether a specific public key has already signed: ```typescript const alreadySigned = MultiSignTransaction.verifyIfSigned(psbt, signerPubKey); ``` ### `MultiSignTransaction.attemptFinalizeInputs()` Attempt to finalize all multisig inputs after collecting signatures: ```typescript const success = MultiSignTransaction.attemptFinalizeInputs( psbt, startIndex, // Index of first multisig input orderedPubKeys, // Array of ordered public key arrays per input isFinal, // Whether all required signatures are present ); ``` ### `MultiSignTransaction.dedupeSignatures()` Merge and deduplicate signature arrays (used when combining PSBTs from different signers): ```typescript const merged = MultiSignTransaction.dedupeSignatures( originalSignatures, newPartialSignatures, ); ``` ## Taproot Script Structure The multisig uses a Taproot script tree with two leaves: ```mermaid flowchart TB Root["Taproot Output<br/>(NUMS point internal key)"] Root --> L1["Leaf 1: MultiSig Script<br/>OP_CHECKSIG per key<br/>OP_ADD to count<br/>M OP_NUMEQUAL"] Root --> L2["Leaf 2: Lock Script<br/>OP_XOR OP_NOP<br/>OP_CODESEPARATOR"] ``` The internal public key is the NUMS (Nothing Up My Sleeve) point, which ensures that only the script path can be used -- there is no key-path spend. ```typescript // NUMS point used as internal key (no key-path spend possible) public static readonly numsPoint: PublicKey = fromHex( '50929b74c1a04954b78b4b6035e97a5e078a5a0f28ec96d547bfee9ace803ac0', ); ``` ### P2MR Multi-Signature When `useP2MR: true` is set, the multisig uses P2MR (BIP 360) instead of P2TR. P2MR eliminates the internal pubkey entirely -- the output commits directly to the Merkle root of the script tree. No NUMS point is needed. For convenience, the `P2MR_MS` utility class generates P2MR multisig addresses: ```typescript import { P2MR_MS } from '@btc-vision/transaction'; const p2mrMultisigAddress = P2MR_MS.generateMultiSigAddress( [pubkeyA, pubkeyB, pubkeyC], 2, // 2-of-3 networks.bitcoin, ); // Returns bc1z... address ``` Compare with the P2TR equivalent: ```typescript import { P2TR_MS } from '@btc-vision/transaction'; const p2trMultisigAddress = P2TR_MS.generateMultiSigAddress( [pubkeyA, pubkeyB, pubkeyC], 2, // 2-of-3 networks.bitcoin, ); // Returns bc1p... address ``` ## Complete 2-of-3 Example ```typescript import { MultiSignTransaction, EcKeyPair, UTXO, } from '@btc-vision/transaction'; import { networks, Psbt } from '@btc-vision/bitcoin'; const network = networks.bitcoin; // Three participants const signerA = EcKeyPair.fromWIF(process.env.KEY_A!, network); const signerB = EcKeyPair.fromWIF(process.env.KEY_B!, network); const signerC = EcKeyPair.fromWIF(process.env.KEY_C!, network); const pubkeys = [signerA.publicKey, signerB.publicKey, signerC.publicKey]; // UTXOs locked in the multisig address const vaultUtxos: UTXO[] = [ { transactionId: 'abcd...'.padEnd(64, '0'), outputIndex: 0, value: 200000n, scriptPubKey: { hex: '5120...', address: 'bc1p...multisigAddress', }, }, ]; // --- Step 1: First signer creates the PSBT --- const multiSigTx = new MultiSignTransaction({ network, utxos: vaultUtxos, feeRate: 10, pubkeys, minimumSignatures: 2, receiver: 'bc1p...recipientAddress', requestedAmount: 100000n, refundVault: 'bc1p...vaultAddress', }); const psbt = await multiSigTx.signPSBT(); const psbtBase64 = psbt.toBase64(); // --- Step 2: Send psbtBase64 to the second signer --- // Second signer reconstructs and signs const psbt2 = Psbt.fromBase64(psbtBase64, { network }); // Check if this signer already signed const alreadySigned = MultiSignTransaction.verifyIfSigned( psbt2, signerB.publicKey, ); if (!alreadySigned) { const result = MultiSignTransaction.signPartial( psbt2, signerB, 0, // originalInputCount [2], // minimums per input (need 2 sigs) ); if (result.final) { // 2-of-3 threshold reached - finalize const finalized = MultiSignTransaction.attemptFinalizeInputs( psbt2, 0, // startIndex [pubkeys], // orderedPubKeys per input true, // isFinal ); if (finalized) { const tx = psbt2.extractTransaction(); const txHex = tx.toHex(); await broadcastTransaction(txHex); console.log('Multi-sig transaction broadcast!'); } } else { // Need more signatures - forward to next signer const updatedPsbt = psbt2.toBase64(); // Send updatedPsbt to signer C... } } ``` ## Error Handling ```typescript try { const multiSigTx = new MultiSignTransaction(params); const psbt = await multiSigTx.signPSBT(); } catch (error) { const message = (error as Error).message; if (message.includes('Refund vault is required')) { // refundVault parameter is missing } else if (message.includes('Requested amount is required')) { // requestedAmount is missing or zero } else if (message.includes('Receiver is required')) { // receiver address is missing } else if (message.includes('Pubkeys are required')) { // pubkeys array is missing } else if (message.includes('Output value left is negative')) { // requestedAmount exceeds total UTXO value } else if (message.includes('Could not sign transaction')) { // PSBT signing failed } } ``` ## Best Practices 1. **Verify signatures before forwarding.** Use `verifyIfSigned()` to prevent duplicate signing. 2. **Use base64 for PSBT transport.** The `toBase64()` / `fromBase64()` methods provide a compact, safe encoding for transmitting PSBTs between signers. 3. **Order public keys consistently.** The same `pubkeys` array order must be used by all signers when constructing or reconstructing the `MultiSignTransaction`. 4. **Validate the refund amount.** Ensure `requestedAmount` does not exceed the total UTXO value to avoid a negative refund. 5. **Check `result.final` after each signature.** If the threshold is met, finalize and broadcast immediately rather than collecting unnecessary extra signatures. 6. **Keep the NUMS point.** The internal key is a standard NUMS point that ensures only script-path spending. Do not change this. 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. Use `P2MR_MS.generateMultiSigAddress()` for P2MR multisig address generation. --- [< Interaction Transactions](./interaction-transactions.md) | [Custom Script Transactions >](./custom-script-transactions.md)