@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
Markdown
# 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)