Skip to content

Commit

Permalink
New Weight Methods and Optimizations
Browse files Browse the repository at this point in the history
- New Methods Added:
  - `guessOutput`: Determines the output type (PKH, WPKH, SH) for a given script.
  - `inputWeight`: Calculates the Weight Unit contributions of an Output as an input in a transaction.
  - `outputWeight`: Computes the Weight Unit contributions of an Output as an output in a transaction.

- Memoization Applied:
  Memoization has been implemented for the following methods to enhance performance and reduce redundant computations:
  - `guessOutput`
  - `getSequence`
  - `getLockTime`
  - `getScriptSatisfaction`
  - `inputWeight`
  - `outputWeight`

- Changes in Behavior:
  - `isSegwit()` now assumes `addr(SH_TYPE_ADDRESS)` descriptors as Segwit `SH_WPKH`. For non-standard script inputs, the format `sh(MINISCRIPT)` should be used.

- Rationale for Changes:
  guessOutput, inputWeight and outputWeight, previously part of `@bitcoinerlab/coinselect`, have been relocated to better align with the module's purpose. Centralizing these methods and implementing a caching strategy significantly improves performance, especially beneficial for the `coinselector` algorithms that frequently access `inputWeight` and `outputWeight`.

- Dependencies Added:
  - Imported `lodash.memoize` for memoization.
  - Imported `encodingLength` from `varuint-bitcoin` for accurate size calculations.

- Other Improvements:
  - Code comments and documentation have been updated for better clarity and to reflect the new logic.
  • Loading branch information
landabaso committed Dec 9, 2023
1 parent 48dd02e commit 9c45e99
Showing 1 changed file with 294 additions and 2 deletions.
296 changes: 294 additions & 2 deletions src/descriptors.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
// Copyright (c) 2023 Jose-Luis Landabaso - https://bitcoinerlab.com
// Distributed under the MIT software license

import memoize from 'lodash.memoize';
import {
address,
networks,
Expand All @@ -11,6 +12,7 @@ import {
Payment,
Psbt
} from 'bitcoinjs-lib';
import { encodingLength } from 'varuint-bitcoin';
import type { PartialSig } from 'bip174/src/lib/interfaces';
const { p2sh, p2wpkh, p2pkh, p2pk, p2wsh, p2tr } = payments;
import { BIP32Factory, BIP32API } from 'bip32';
Expand Down Expand Up @@ -50,6 +52,38 @@ function countNonPushOnlyOPs(script: Buffer): number {
).length;
}

function vectorSize(someVector: Buffer[]): number {
const length = someVector.length;

return (
encodingLength(length) +
someVector.reduce((sum, witness) => {
return sum + varSliceSize(witness);
}, 0)
);
}

function varSliceSize(someScript: Buffer): number {
const length = someScript.length;

return encodingLength(length) + length;
}

/**
* This function will typically return 73; since it assumes a signature size of
* 72 bytes (this is the max size of a DER encoded signature) and it adds 1
* extra byte for encoding its length
*/
function signatureSize(
signature: PartialSig | 'DANGEROUSLY_USE_FAKE_SIGNATURES'
) {
const length =
signature === 'DANGEROUSLY_USE_FAKE_SIGNATURES'
? 72
: signature.signature.length;
return encodingLength(length) + length;
}

/*
* Returns a bare descriptor without checksum and particularized for a certain
* index (if desc was a range descriptor)
Expand Down Expand Up @@ -271,8 +305,8 @@ export function DescriptorsFactory(ecc: TinySecp256k1Interface) {
} catch (e) {}
try {
payment = p2sh({ output, network });
//undefined isSegwit. Cannot know from looking at the address. Could
//be sh(wpkh), sh(wsh) or a plain old sh(SCRIPT)
// It assumes that an addr(SH_ADDRESS) is always a add(SH_WPKH) address
isSegwit = true;
} catch (e) {}
try {
payment = p2wpkh({ output, network });
Expand Down Expand Up @@ -689,6 +723,38 @@ export function DescriptorsFactory(ecc: TinySecp256k1Interface) {
this.#signersPubKeys = [this.getScriptPubKey()];
}
}
this.getSequence = memoize(this.getSequence);
this.getLockTime = memoize(this.getLockTime);
const getSignaturesKey = (
signatures: PartialSig[] | 'DANGEROUSLY_USE_FAKE_SIGNATURES'
) =>
signatures === 'DANGEROUSLY_USE_FAKE_SIGNATURES'
? signatures
: signatures
.map(
s =>
`${s.pubkey.toString('hex')}-${s.signature.toString('hex')}`
)
.join('|');
this.getScriptSatisfaction = memoize(
this.getScriptSatisfaction,
// resolver function:
getSignaturesKey
);
this.guessOutput = memoize(this.guessOutput);
this.inputWeight = memoize(
this.inputWeight,
// resolver function:
(
isSegwitTx: boolean,
signatures: PartialSig[] | 'DANGEROUSLY_USE_FAKE_SIGNATURES'
) => {
const segwitKey = isSegwitTx ? 'segwit' : 'non-segwit';
const signaturesKey = getSignaturesKey(signatures);
return `${segwitKey}-${signaturesKey}`;
}
);
this.outputWeight = memoize(this.outputWeight);
}

/**
Expand Down Expand Up @@ -867,11 +933,237 @@ export function DescriptorsFactory(ecc: TinySecp256k1Interface) {
}
/**
* Whether this `Output` is Segwit.
*
* *NOTE:* When the descriptor in an input is `addr(address)`, it is assumed
* that any `addr(SH_TYPE_ADDRESS)` is in fact a Segwit `SH_WPKH`
* (Script Hash-Witness Public Key Hash).
* For inputs using arbitrary scripts (not standard addresses),
* use a descriptor in the format `sh(MINISCRIPT)`.
*
*/
isSegwit(): boolean | undefined {
return this.#isSegwit;
}

/**
* Returns the tuple: `{ isPKH: boolean; isWPKH: boolean; isSH: boolean; }`
* for this Output.
*/
guessOutput() {
function guessSH(output: Buffer) {
try {
payments.p2sh({ output });
return true;
} catch (err) {
return false;
}
}
function guessWPKH(output: Buffer) {
try {
payments.p2wpkh({ output });
return true;
} catch (err) {
return false;
}
}
function guessPKH(output: Buffer) {
try {
payments.p2pkh({ output });
return true;
} catch (err) {
return false;
}
}
const isPKH = guessPKH(this.getScriptPubKey());
const isWPKH = guessWPKH(this.getScriptPubKey());
const isSH = guessSH(this.getScriptPubKey());

if ([isPKH, isWPKH, isSH].filter(Boolean).length > 1)
throw new Error('Cannot have multiple output types.');

return { isPKH, isWPKH, isSH };
}

// References for inputWeight & outputWeight:
// https://gist.github.com/junderw/b43af3253ea5865ed52cb51c200ac19c
// https://bitcoinops.org/en/tools/calc-size/
// Look for byteLength: https://github.com/bitcoinjs/bitcoinjs-lib/blob/master/ts_src/transaction.ts
// https://github.com/bitcoinjs/coinselect/blob/master/utils.js

/**
* Computes the Weight Unit contributions of this Output as if it were the
* input in a tx.
*
* *NOTE:* When the descriptor in an input is `addr(address)`, it is assumed
* that any `addr(SH_TYPE_ADDRESS)` is in fact a Segwit `SH_WPKH`
* (Script Hash-Witness Public Key Hash).
* For inputs using arbitrary scripts (not standard addresses),
* use a descriptor in the format `sh(MINISCRIPT)`.
*/
inputWeight(
/**
* Indicates if the transaction is a Segwit transaction.
* If a transaction isSegwitTx, a single byte is then also required for
* non-witness inputs to encode the length of the empty witness stack:
* encodeLength(0) + 0 = 1
* Read more:
* https://gist.github.com/junderw/b43af3253ea5865ed52cb51c200ac19c?permalink_comment_id=4760512#gistcomment-4760512
*/
isSegwitTx: boolean,
/*
* Array of `PartialSig`. Each `PartialSig` includes
* a public key and its corresponding signature. This parameter
* enables the accurate calculation of signature sizes.
* Pass 'DANGEROUSLY_USE_FAKE_SIGNATURES' to assume 72 bytes in length.
* Mainly used for testing.
*/
signatures: PartialSig[] | 'DANGEROUSLY_USE_FAKE_SIGNATURES'
) {
if (this.isSegwit() && !isSegwitTx)
throw new Error(`a tx is segwit if at least one input is segwit`);
const errorMsg =
'Input type not implemented. Currently supported: pkh(KEY), wpkh(KEY), \
sh(wpkh(KEY)), sh(wsh(MINISCRIPT)), sh(MINISCRIPT), wsh(MINISCRIPT), \
addr(PKH_ADDRESS), addr(WPKH_ADDRESS), addr(SH_WPKH_ADDRESS).';

//expand any miniscript-based descriptor. If not miniscript-based, then it's
//an addr() descriptor. For those, we can only guess their type.
const expansion = this.expand().expandedExpression;
const { isPKH, isWPKH, isSH } = this.guessOutput();
if (!expansion && !isPKH && !isWPKH && !isSH) throw new Error(errorMsg);

const firstSignature =
signatures && typeof signatures[0] === 'object'
? signatures[0]
: 'DANGEROUSLY_USE_FAKE_SIGNATURES';

if (expansion ? expansion.startsWith('pkh(') : isPKH) {
return (
// Non-segwit: (txid:32) + (vout:4) + (sequence:4) + (script_len:1) + (sig:73) + (pubkey:34)
(32 + 4 + 4 + 1 + signatureSize(firstSignature) + 34) * 4 +
//Segwit:
(isSegwitTx ? 1 : 0)
);
} else if (expansion ? expansion.startsWith('wpkh(') : isWPKH) {
if (!isSegwitTx) throw new Error('Should be SegwitTx');
return (
// Non-segwit: (txid:32) + (vout:4) + (sequence:4) + (script_len:1)
41 * 4 +
// Segwit: (push_count:1) + (sig:73) + (pubkey:34)
(1 + signatureSize(firstSignature) + 34)
);
} else if (expansion ? expansion.startsWith('sh(wpkh(') : isSH) {
if (!isSegwitTx) throw new Error('Should be SegwitTx');
return (
// Non-segwit: (txid:32) + (vout:4) + (sequence:4) + (script_len:1) + (p2wpkh:23)
// -> p2wpkh_script: OP_0 OP_PUSH20 <public_key_hash>
// -> p2wpkh: (script_len:1) + (script:22)
64 * 4 +
// Segwit: (push_count:1) + (sig:73) + (pubkey:34)
(1 + signatureSize(firstSignature) + 34)
);
} else if (expansion?.startsWith('sh(wsh(')) {
if (!isSegwitTx) throw new Error('Should be SegwitTx');
const witnessScript = this.getWitnessScript();
if (!witnessScript)
throw new Error('sh(wsh) must provide witnessScript');
const payment = payments.p2sh({
redeem: payments.p2wsh({
redeem: {
input: this.getScriptSatisfaction(
signatures || 'DANGEROUSLY_USE_FAKE_SIGNATURES'
),
output: witnessScript
}
})
});
if (!payment || !payment.input || !payment.witness)
throw new Error('Could not create payment');
return (
//Non-segwit
4 * (40 + varSliceSize(payment.input)) +
//Segwit
vectorSize(payment.witness)
);
} else if (expansion?.startsWith('sh(')) {
const redeemScript = this.getRedeemScript();
if (!redeemScript) throw new Error('sh() must provide redeemScript');
const payment = payments.p2sh({
redeem: {
input: this.getScriptSatisfaction(
signatures || 'DANGEROUSLY_USE_FAKE_SIGNATURES'
),
output: redeemScript
}
});
if (!payment || !payment.input)
throw new Error('Could not create payment');
if (payment.witness?.length)
throw new Error(
'A legacy p2sh payment should not cointain a witness'
);
return (
//Non-segwit
4 * (40 + varSliceSize(payment.input)) +
//Segwit:
(isSegwitTx ? 1 : 0)
);
} else if (expansion?.startsWith('wsh(')) {
const witnessScript = this.getWitnessScript();
if (!witnessScript) throw new Error('wsh must provide witnessScript');
const payment = payments.p2wsh({
redeem: {
input: this.getScriptSatisfaction(
signatures || 'DANGEROUSLY_USE_FAKE_SIGNATURES'
),
output: witnessScript
}
});
if (!payment || !payment.input || !payment.witness)
throw new Error('Could not create payment');
return (
//Non-segwit
4 * (40 + varSliceSize(payment.input)) +
//Segwit
vectorSize(payment.witness)
);
} else {
throw new Error(errorMsg);
}
}

/**
* Computes the Weight Unit contributions of this Output as if it were the
* output in a tx.
*/
outputWeight() {
const errorMsg =
'Output type not implemented. Currently supported: pkh(KEY), wpkh(KEY), \
sh(ANYTHING), wsh(ANYTHING), addr(PKH_ADDRESS), addr(WPKH_ADDRESS), \
addr(SH_WPKH_ADDRESS)';

//expand any miniscript-based descriptor. If not miniscript-based, then it's
//an addr() descriptor. For those, we can only guess their type.
const expansion = this.expand().expandedExpression;
const { isPKH, isWPKH, isSH } = this.guessOutput();
if (!expansion && !isPKH && !isWPKH && !isSH) throw new Error(errorMsg);
if (expansion ? expansion.startsWith('pkh(') : isPKH) {
// (p2pkh:26) + (amount:8)
return 34 * 4;
} else if (expansion ? expansion.startsWith('wpkh(') : isWPKH) {
// (p2wpkh:23) + (amount:8)
return 31 * 4;
} else if (expansion ? expansion.startsWith('sh(') : isSH) {
// (p2sh:24) + (amount:8)
return 32 * 4;
} else if (expansion?.startsWith('wsh(')) {
// (p2wsh:35) + (amount:8)
return 43 * 4;
} else {
throw new Error(errorMsg);
}
}

/** @deprecated - Use updatePsbtAsInput instead
* @hidden
*/
Expand Down

0 comments on commit 9c45e99

Please sign in to comment.