Skip to content

Commit

Permalink
Implement UTXO selection.
Browse files Browse the repository at this point in the history
  • Loading branch information
paulmillr committed Jan 5, 2024
1 parent 2f50fcd commit a63ed79
Show file tree
Hide file tree
Showing 5 changed files with 1,789 additions and 121 deletions.
152 changes: 145 additions & 7 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,10 +5,10 @@ Audited & minimal library for creating, signing & decoding Bitcoin transactions.
- 🔒 [**Audited**](#security) by an independent security firm
- ✍️ Create transactions, inputs, outputs, sign them
- 📡 No network code: simplified audits and offline usage
- 🔀 UTXO selection with different strategies
- 🎻 Classic & SegWit: P2PK, P2PKH, P2WPKH, P2SH, P2WSH, P2MS
- 🧪 Schnorr & Taproot BIP340/BIP341: P2TR, P2TR-NS, P2TR-MS
- 📨 BIP174 PSBT
- 👥 Multisig support
- 🪶 ~2600 lines

Initial development has been funded by [Ryan Shea](https://shea.io). Check out [the demo](https://signerdemo.micro-btc.dev/) & [its github](https://github.com/shea256/micro-btc-web-demo).
Expand Down Expand Up @@ -40,8 +40,6 @@ import * as btc from '@scure/btc-signer';
// import * as btc from "npm:@scure/[email protected]"; // Deno
```

### Table of Contents

- [Payments](#payments)
- [P2PK Pay To Public Key](#p2pk-pay-to-public-key)
- [P2PKH Public Key Hash](#p2pkh-public-key-hash)
Expand All @@ -53,17 +51,22 @@ import * as btc from '@scure/btc-signer';
- [P2TR Taproot](#p2tr-taproot)
- [P2TR-NS Taproot multisig](#p2tr-ns-taproot-multisig)
- [P2TR-MS Taproot M-of-N multisig](#p2tr-ms-taproot-m-of-n-multisig)
- [P2TR-PK Taproot single P2PK script](#p2tr-pk-taproot-single-p2pk-script)
- [Transaction](#transaction)
- [Encode/decode](#encodedecode)
- [Inputs](#inputs)
- [Outputs](#outputs)
- [Basic transaction sign](#basic-transaction-sign)
- [BIP174 PSBT multi-sig example](#bip174-psbt-multi-sig-example)
- [UTXO selection](#utxo-selection)
- [Utils](#utils)
- [getAddress](#getaddress)
- [WIF](#wif)
- [Script](#script)
- [OutScript](#outscript)
- [Security](#security)
- [Supply chain security](#supply-chain-security)
- [License](#license)

## Payments

Expand Down Expand Up @@ -624,8 +627,14 @@ const hdkey = bip32.HDKey.fromExtendedKey(epriv, testnet.bip32);
// const seed = 'cUkG8i1RFfWGWy5ziR11zJ5V4U4W3viSFCfyJmZnvQaUsd1xuF3T';
const tx = new btc.Transaction();
// A creator creating a PSBT for a transaction which creates the following outputs:
tx.addOutput({ script: '0014d85c2b71d0060b09c9886aeb815e50991dda124d', amount: btc.Decimal.decode('1.49990000') });
tx.addOutput({ script: '001400aea9a2e5f0f876a588df5546e8742d1d87008f', amount: btc.Decimal.decode('1.00000000') });
tx.addOutput({
script: '0014d85c2b71d0060b09c9886aeb815e50991dda124d',
amount: btc.Decimal.decode('1.49990000'),
});
tx.addOutput({
script: '001400aea9a2e5f0f876a588df5546e8742d1d87008f',
amount: btc.Decimal.decode('1.00000000'),
});
// and spends the following inputs:
tx.addInput({
txid: '75ddabb27b8845f5247975c8a5ba7c6f336c4570708ebe230caf6db5217ae858',
Expand Down Expand Up @@ -696,8 +705,7 @@ tx2.updateOutput(1, {
const psbt2 = tx2.toPSBT();
// An updater which adds SIGHASH_ALL to the above PSBT must create this PSBT:
const tx3 = btc.Transaction.fromPSBT(psbt2);
for (let i = 0; i < tx3.inputs.length; i++)
tx3.updateInput(i, { sighashType: btc.SigHash.ALL });
for (let i = 0; i < tx3.inputs.length; i++) tx3.updateInput(i, { sighashType: btc.SigHash.ALL });
const psbt3 = tx3.toPSBT();
/*
Given the above updated PSBT, a signer that supports SIGHASH_ALL for P2PKH and P2WPKH spends and uses RFC6979 for nonce generation and has the following keys:
Expand Down Expand Up @@ -734,6 +742,134 @@ deepStrictEqual(
);
```

### UTXO selection

UTXO selection is the process of choosing which UTXOs to use as inputs
when making an on-chain bitcoin payment. The library:

- can create tx, integrated with the signer
- ensures change address is always specified
- supports bip69
- supports segwit + taproot
- calculates weight with good precision
- implements multiple strategies

Taproot estimation is precise, but you have to pass sighash if you want to use non-default one,
because it changes signature size. For complex taproot trees you need to filter tapLeafScript
to include only leafs which you can sign we estimate size with smallest leaf (same as finalization),
but in specific case keys for this leaf can be unavailable (complex multisig)

`Oldest` / `Newest` expects UTXO provided in historical order (oldest first),
otherwise we have no way to detect age of tx.

#### Strategies

Strategy selection is complicated. Best should be: `exactBiggest/accumSmallest`.

`exactBiggest/accumBiggest` creates tx with smallest fees,
but it breaks big outputs to small ones, which in the end will create
a lot of outputs close to dust.

- `all`: send all coins to change address (consolidation)
- `coinselect`: good for privacy, same as `exactBiggest/accumBiggest`
- `accum`: accumulates inputs until the target value (+fees) is reached, skipping detrimental inputs
- `exact`: accumulates inputs until the target value (+fees) is matched, does not accumulate inputs
that go over the target value (within a threshold)
- `accumNewest`
- `accumOldest`
- `accumSmallest`
- `accumBiggest`
- `exactNewest/accumNewest`
- `exactNewest/accumOldest`
- `exactNewest/accumSmallest`
- `exactNewest/accumBiggest`
- `exactOldest/accumNewest`
- `exactOldest/accumOldest`
- `exactOldest/accumSmallest`
- `exactOldest/accumBiggest`
- `exactSmallest/accumNewest`
- `exactSmallest/accumOldest`
- `exactSmallest/accumSmallest`
- `exactSmallest/accumBiggest`
- `exactBiggest/accumNewest`
- `exactBiggest/accumOldest`
- `exactBiggest/accumSmallest`
- `exactBiggest/accumBiggest`

#### Example

```ts
const privKey = hex.decode('0101010101010101010101010101010101010101010101010101010101010101');
const pubKey = secp256k1.getPublicKey(privKey, true);
const spend = btc.p2wpkh(pubKey, regtest);
const utxo = [
{
...spend, // add witness/redeem scripts from spend
// Get txid, index from explorer/network
txid: hex.decode('0af50a00a22f74ece24c12cd667c290d3a35d48124a69f4082700589172a3aa2'),
index: 0,
// utxo tx information
// script can be used from spend itself or from explorer
witnessUtxo: { script: spend.script, amount: 100_000n }, // value in satoshi
},
{
...spend,
txid: hex.decode('0af50a00a22f74ece24c12cd667c290d3a35d48124a69f4082700589172a3aa2'),
index: 1,
witnessUtxo: { script: spend.script, amount: btc.Decimal.decode('1.5') }, // value in btc
},
// {
// ...spend,
// txid: hex.decode('75ddabb27b8845f5247975c8a5ba7c6f336c4570708ebe230caf6db5217ae858'),
// index: 0,
// // tx hex from blockchain (required for non-SegWit UTXO)
// nonWitnessUtxo: hex.decode(
// '0200000001aad73931018bd25f84ae400b68848be09db706eac2ac18298babee71ab656f8b0000000048473044022058f6fc7c6a33e1b31548d481c826c015bd30135aad42cd67790dab66d2ad243b02204a1ced2604c6735b6393e5b41691dd78b00f0c5942fb9f751856faa938157dba01feffffff0280f0fa020000000017a9140fb9463421696b82c833af241c78c17ddbde493487d0f20a270100000017a91429ca74f8a08f81999428185c97b5d852e4063f618765000000'
// ),
// },
];
const outputs = [
{ address: '2MvpbAgedBzJUBZWesDwdM7p3FEkBEwq3n3', amount: 50_000n }, // amount in satoshi
{
address: 'bcrt1pw53jtgez0wf69n06fchp0ctk48620zdscnrj8heh86wykp9mv20q7vd3gm',
amount: btc.Decimal.decode('0.5'), // amount in btc
},
];
// Send all utxo to specific address (consolidation):
// const selected = btc.selectUTXO(utxo, [], 'all', {
// changeAddress: 'bcrt1pea3850rzre54e53eh7suwmrwc66un6nmu9npd7eqrhd6g4lh8uqsxcxln8', ...
const selected = btc.selectUTXO(utxo, outputs, 'coinselect', {
changeAddress: 'bcrt1pea3850rzre54e53eh7suwmrwc66un6nmu9npd7eqrhd6g4lh8uqsxcxln8', // required, address to send change
feePerByte: 2n, // require, fee per vbyte in satoshi
bip69: true, // lexicographical Indexing of Transaction Inputs and Outputs
createTx: true, // create tx with selected inputs/outputs
network: regtest,
});
// NOTE: 'selected' will 'undefined' if there is not enough funds
deepStrictEqual(selected.fee, 394n); // estimated fee
deepStrictEqual(selected.change, true); // change address used
deepStrictEqual(selected.outputs, [
{ address: '2MvpbAgedBzJUBZWesDwdM7p3FEkBEwq3n3', amount: 50000n },
{
address: 'bcrt1pw53jtgez0wf69n06fchp0ctk48620zdscnrj8heh86wykp9mv20q7vd3gm',
amount: 50_000_000n,
},
// Change address
// NOTE: with bip69 it is not neccesarily last item in outputs
{
address: 'bcrt1pea3850rzre54e53eh7suwmrwc66un6nmu9npd7eqrhd6g4lh8uqsxcxln8',
amount: 99_949_606n,
},
]);
// No need to create tx manually!
const { tx } = selected;
tx.sign(privKey);
tx.finalize();
deepStrictEqual(tx.id, 'b702078d65edd65a84b2a97a669df5631b06f42a67b0d7090e540b02cc65aed5');
// real tx fee, can be bigger than estimated, since we expect signatures of maximal size
deepStrictEqual(tx.fee, 394n);
```

## Utils

### getAddress
Expand Down Expand Up @@ -844,6 +980,8 @@ The library has been independently audited:
- [Changes since audit](https://github.com/paulmillr/scure-btc-signer/compare/0.3.0..main).
- The audit has been funded by [Ryan Shea](https://shea.io)

UTXO selection functionality has not been audited yet.

### Supply chain security

1. **Commits** are signed with PGP keys, to prevent forgery. Make sure to verify commit signatures.
Expand Down
Loading

0 comments on commit a63ed79

Please sign in to comment.