@btc-vision/transaction
Version:
OPNet transaction library allows you to create and sign transactions for the OPNet network.
245 lines (190 loc) • 9.71 kB
Markdown
# Deployment Transactions
Deploy smart contracts to the OPNet network using `DeploymentTransaction`.
## Overview
A `DeploymentTransaction` deploys a compiled smart contract to the Bitcoin blockchain via OPNet. The process produces **two transactions** that must be broadcast in order: a funding transaction that sends BTC to a derived script address (P2TR or P2MR), and a deployment transaction that reveals the contract bytecode in the witness.
```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: Deployment"]
SA2["Script UTXO"] --> EP["Epoch Reward<br/>(Challenge)"]
SA2 --> RF["Refund"]
end
SA -->|"creates UTXO"| SA2
subgraph Result["Deployment Result"]
CA["Contract Address<br/>(deterministic)"]
CPK["Contract PubKey"]
end
TX2 --> Result
```
## Factory Method
Deployments are created through `TransactionFactory.signDeployment()`:
```typescript
import { TransactionFactory } from '@btc-vision/transaction';
const factory = new TransactionFactory();
const result = await factory.signDeployment(parameters);
```
## Parameters
`IDeploymentParameters` extends `ITransactionParameters` (with `to` omitted since the target address is derived automatically):
| Parameter | Type | Required | Default | Description |
|-----------|------|----------|---------|-------------|
| `signer` | `Signer \| UniversalSigner` | Yes | - | Key pair used to sign the transaction |
| `mldsaSigner` | `QuantumBIP32Interface \| null` | Yes | - | ML-DSA signer (required for deployment; provides the hashed public key) |
| `network` | `Network` | Yes | - | Bitcoin network |
| `utxos` | `UTXO[]` | Yes | - | Available UTXOs to fund the deployment |
| `from` | `string` | Yes | - | Deployer address |
| `feeRate` | `number` | Yes | - | Fee rate in sat/vB |
| `priorityFee` | `bigint` | Yes | - | OPNet priority fee in satoshis |
| `gasSatFee` | `bigint` | Yes | - | OPNet gas fee in satoshis |
| `bytecode` | `Uint8Array` | Yes | - | Compiled contract bytecode |
| `calldata` | `Uint8Array` | No | - | Constructor calldata (ABI-encoded constructor arguments) |
| `randomBytes` | `Uint8Array` | No | Auto-generated | 32-byte salt for contract address derivation |
| `challenge` | `IChallengeSolution` | Yes | - | Epoch challenge solution (proof of work) |
| `useP2MR` | `boolean` | No | `false` | Use P2MR (BIP 360) instead of P2TR. Eliminates the quantum-vulnerable key-path spend. |
### Constraints
| Constraint | Value | Description |
|------------|-------|-------------|
| Maximum contract size | **128 KB** (`128 * 1024` bytes) | After compression. Enforced by `DeploymentTransaction.MAXIMUM_CONTRACT_SIZE`. |
| Maximum calldata size | **1 MB** (`1024 * 1024` bytes) | Constructor calldata limit. Enforced by `SharedInteractionTransaction.MAXIMUM_CALLDATA_SIZE`. |
| ML-DSA signer | Required | The `mldsaSigner` (hashed public key) must be defined to deploy a contract. |
## Response Type
`DeploymentResult`:
| Field | Type | Description |
|-------|------|-------------|
| `transaction` | `[string, string]` | Tuple of `[fundingTxHex, deploymentTxHex]` -- broadcast in this order |
| `contractAddress` | `string` | The deterministic P2OP contract address |
| `contractPubKey` | `string` | The contract's public key (hex, `0x`-prefixed) |
| `challenge` | `RawChallenge` | The raw epoch challenge used |
| `utxos` | `UTXO[]` | Refund UTXOs (change from the funding transaction) |
| `inputUtxos` | `UTXO[]` | Original UTXOs consumed as inputs |
## Contract Address Derivation
Contract addresses are deterministic. The address is derived from the deployer's public key, a random salt, and the bytecode hash:
```mermaid
flowchart TB
subgraph Inputs
DPK["Deployer x-only PubKey"]
Salt["hash256(randomBytes)"]
BC["hash256(bytecode)"]
end
Concat["concat(deployerPubKey, salt, sha256Bytecode)"]
Seed["contractSeed = hash256(concat)"]
Addr["contractAddress = P2OP(seed, network)"]
DPK --> Concat
Salt --> Concat
BC --> Concat
Concat --> Seed
Seed --> Addr
```
This means the same deployer deploying the same bytecode with the same salt will always produce the same contract address. To deploy a distinct instance, use different `randomBytes`.
## Bytecode Compression
Contract bytecode is automatically compressed before embedding in the transaction. The `Compressor` module handles this transparently:
```typescript
// Internally in DeploymentTransaction constructor:
this.bytecode = Compressor.compress(
new Uint8Array([...versionBuffer, ...parameters.bytecode]),
);
```
A version prefix is prepended before compression. The compressed bytecode must remain under the 128 KB limit.
## Two-Transaction Model
The factory's `signDeployment()` method handles the full two-transaction flow internally:
```mermaid
sequenceDiagram
participant App
participant Factory as TransactionFactory
participant FT as FundingTransaction
participant DT as DeploymentTransaction
App->>Factory: signDeployment(params)
Factory->>Factory: Iterate funding amount estimation
Factory->>FT: Create & sign funding tx
FT-->>Factory: Signed funding tx + UTXO
Factory->>DT: Create deployment tx<br/>(funded by FT output)
Factory->>DT: Sign deployment tx
DT-->>Factory: Signed deployment tx
Factory-->>App: DeploymentResult
```
1. The factory estimates the required funding amount through iterative simulation.
2. A `FundingTransaction` sends the estimated amount to the derived Taproot script address.
3. A `DeploymentTransaction` spends that UTXO, embedding the bytecode in the witness.
4. Both hex-encoded transactions are returned for sequential broadcast.
## Complete Example
```typescript
import {
TransactionFactory,
EcKeyPair,
ChallengeSolution,
UTXO,
} from '-vision/transaction';
import { networks, crypto as bitCrypto } from '@btc-vision/bitcoin';
async function deployContract() {
const network = networks.bitcoin;
const factory = new TransactionFactory();
// Create signers
const signer = EcKeyPair.fromWIF(process.env.PRIVATE_KEY!, network);
const address = EcKeyPair.getTaprootAddress(signer, network);
// ML-DSA signer is required for deployment
const mldsaSigner = /* your ML-DSA signer instance */;
// Fetch UTXOs
const utxos: UTXO[] = await fetchUTXOs(address);
// Prepare contract bytecode (compiled .wasm or equivalent)
const bytecode: Uint8Array = await readContractBytecode('./my_contract.wasm');
// Optional: constructor calldata (ABI-encoded)
const calldata: Uint8Array = encodeConstructorArgs(/* ... */);
// Obtain the epoch challenge solution
const challenge: ChallengeSolution = await solveEpochChallenge(/* ... */);
// Deploy
const result = await factory.signDeployment({
signer,
mldsaSigner,
network,
utxos,
from: address,
feeRate: 10,
priorityFee: 10000n,
gasSatFee: 5000n,
bytecode,
calldata,
challenge,
// useP2MR: true, // Uncomment for quantum-safe P2MR output (bc1z...)
});
// Broadcast both transactions in order
await broadcastTransaction(result.transaction[0]); // Funding
await broadcastTransaction(result.transaction[1]); // Deployment
console.log('Contract deployed!');
console.log('Contract address:', result.contractAddress);
console.log('Contract pubkey:', result.contractPubKey);
console.log('Refund UTXOs:', result.utxos);
}
```
## Error Handling
```typescript
try {
const result = await factory.signDeployment(params);
} catch (error) {
const message = (error as Error).message;
if (message.includes('MLDSA signer must be defined')) {
// mldsaSigner is null or missing -- required for deployment
} else if (message.includes('Contract size overflow')) {
// Compressed bytecode exceeds 128 KB
} else if (message.includes('Calldata size overflow')) {
// Constructor calldata exceeds 1 MB
} else if (message.includes('Bytecode is required')) {
// bytecode parameter is missing or empty
} else if (message.includes('Challenge solution is required')) {
// challenge parameter is missing
} else if (message.includes('Could not sign funding transaction')) {
// Funding step failed (likely insufficient UTXOs)
}
}
```
## Best Practices
1. **Pre-check bytecode size.** Compress and measure your bytecode before attempting deployment to avoid wasted fees.
2. **Save the contract address.** The `contractAddress` and `contractPubKey` are needed for all future interactions with the contract.
3. **Use consistent random bytes for reproducibility.** If you need a predictable contract address (e.g., for CREATE2-style deployment), pass explicit `randomBytes`.
4. **Broadcast in order.** The funding transaction must confirm (or at least enter the mempool) before the deployment transaction can be accepted.
5. **Track refund UTXOs.** Use `result.utxos` as inputs for your next transaction.
6. **Verify contract size.** The 128 KB limit applies to the compressed bytecode including the version prefix.
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.
---
[< Funding Transactions](./funding-transactions.md) | [Interaction Transactions >](./interaction-transactions.md)