Skip to content

Commit

Permalink
Merge pull request #32 from bitcoinerlab/weights
Browse files Browse the repository at this point in the history
New Weight Calculation Functionalities and General Optimizations in Descriptors
  • Loading branch information
landabaso authored Dec 9, 2023
2 parents 4938cc4 + 0dc6685 commit 0511e64
Show file tree
Hide file tree
Showing 8 changed files with 103,440 additions and 1,095 deletions.
2,244 changes: 1,165 additions & 1,079 deletions package-lock.json

Large diffs are not rendered by default.

9 changes: 6 additions & 3 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@
"name": "@bitcoinerlab/descriptors",
"description": "This library parses and creates Bitcoin Miniscript Descriptors and generates Partially Signed Bitcoin Transactions (PSBTs). It provides PSBT finalizers and signers for single-signature, BIP32 and Hardware Wallets.",
"homepage": "https://github.com/bitcoinerlab/descriptors",
"version": "2.0.4",
"version": "2.1.0",
"author": "Jose-Luis Landabaso",
"license": "MIT",
"repository": {
Expand Down Expand Up @@ -32,7 +32,7 @@
"docs": "typedoc --options ./node_modules/@bitcoinerlab/configs/typedoc.json",
"build:src": "tsc --project ./node_modules/@bitcoinerlab/configs/tsconfig.src.json",
"build:fixtures": "node test/tools/generateBitcoinCoreFixtures.js -i test/fixtures/descriptor_tests.cpp | npx prettier --parser typescript > test/fixtures/bitcoinCore.ts",
"build:test": "npm run build:fixtures && tsc --project ./node_modules/@bitcoinerlab/configs/tsconfig.test.json",
"build:test": "npm run build:fixtures && tsc --project ./node_modules/@bitcoinerlab/configs/tsconfig.test.json --resolveJsonModule",
"build": "npm run build:src && npm run build:test",
"lint": "eslint --ignore-path .gitignore --ext .ts src/ test/",
"ensureTester": "./node_modules/@bitcoinerlab/configs/scripts/ensureTester.sh",
Expand All @@ -58,6 +58,7 @@
"devDependencies": {
"@bitcoinerlab/configs": "github:bitcoinerlab/configs",
"@ledgerhq/hw-transport-node-hid": "^6.27.12",
"@types/lodash.memoize": "^4.1.9",
"bip39": "^3.0.4",
"bip65": "^1.0.3",
"bip68": "^1.0.4",
Expand All @@ -70,6 +71,8 @@
"@bitcoinerlab/secp256k1": "^1.0.5",
"bip32": "^4.0.0",
"bitcoinjs-lib": "^6.1.3",
"ecpair": "^2.1.0"
"ecpair": "^2.1.0",
"lodash.memoize": "^4.1.2",
"varuint-bitcoin": "^1.1.2"
}
}
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
12 changes: 6 additions & 6 deletions src/ledger.ts
Original file line number Diff line number Diff line change
Expand Up @@ -389,12 +389,12 @@ export async function ledgerPolicyFromPsbtInput({
)
? 'pkh(@0/**)'
: originPath.match(new RegExp(`^/84'/${coinType}'/(\\d+)'$`))
? 'wpkh(@0/**)'
: originPath.match(new RegExp(`^/49'/${coinType}'/(\\d+)'$`))
? 'sh(wpkh(@0/**))'
: originPath.match(new RegExp(`^/86'/${coinType}'/(\\d+)'$`))
? 'tr(@0/**)'
: undefined;
? 'wpkh(@0/**)'
: originPath.match(new RegExp(`^/49'/${coinType}'/(\\d+)'$`))
? 'sh(wpkh(@0/**))'
: originPath.match(new RegExp(`^/86'/${coinType}'/(\\d+)'$`))
? 'tr(@0/**)'
: undefined;
if (standardTemplate) {
const xpub = await getLedgerXpub({
originPath,
Expand Down
Loading

0 comments on commit 0511e64

Please sign in to comment.