|
| 1 | +import { Dimensions, VirtualSizes } from '@bitgo/unspents'; |
| 2 | +import { Descriptor } from '@bitgo/wasm-miniscript'; |
| 3 | + |
| 4 | +import { DescriptorMap } from './DescriptorMap'; |
| 5 | + |
| 6 | +function getScriptPubKeyLength(descType: string): number { |
| 7 | + // See https://bitcoinops.org/en/tools/calc-size/ |
| 8 | + switch (descType) { |
| 9 | + case 'Wpkh': |
| 10 | + // https://github.com/bitcoin/bips/blob/master/bip-0141.mediawiki#p2wpkh |
| 11 | + return 22; |
| 12 | + case 'Sh': |
| 13 | + case 'ShWsh': |
| 14 | + case 'ShWpkh': |
| 15 | + // https://github.com/bitcoin/bips/blob/master/bip-0016.mediawiki#specification |
| 16 | + return 23; |
| 17 | + case 'Pkh': |
| 18 | + return 25; |
| 19 | + case 'Wsh': |
| 20 | + case 'Tr': |
| 21 | + // P2WSH: https://github.com/bitcoin/bips/blob/master/bip-0141.mediawiki#p2wsh |
| 22 | + // P2TR: https://github.com/bitcoin/bips/blob/58ffd93812ff25e87d53d1f202fbb389fdfb85bb/bip-0341.mediawiki#script-validation-rules |
| 23 | + // > A Taproot output is a native SegWit output (see BIP141) with version number 1, and a 32-byte witness program. |
| 24 | + // 32 bytes for the hash, 1 byte for the version, 1 byte for the push opcode |
| 25 | + return 34; |
| 26 | + case 'Bare': |
| 27 | + throw new Error('cannot determine scriptPubKey length for Bare descriptor'); |
| 28 | + default: |
| 29 | + throw new Error('unexpected descriptor type ' + descType); |
| 30 | + } |
| 31 | +} |
| 32 | + |
| 33 | +function getInputVSizeForDescriptor(descriptor: Descriptor): number { |
| 34 | + // FIXME(BTC-1489): this can overestimate the size of the input significantly |
| 35 | + const maxWeight = descriptor.maxWeightToSatisfy(); |
| 36 | + const maxVSize = Math.ceil(maxWeight / 4); |
| 37 | + const sizeOpPushdata1 = 1; |
| 38 | + const sizeOpPushdata2 = 2; |
| 39 | + return ( |
| 40 | + // inputId |
| 41 | + 32 + |
| 42 | + // vOut |
| 43 | + 4 + |
| 44 | + // nSequence |
| 45 | + 4 + |
| 46 | + // script overhead |
| 47 | + (maxVSize < 255 ? sizeOpPushdata1 : sizeOpPushdata2) + |
| 48 | + // script |
| 49 | + maxVSize |
| 50 | + ); |
| 51 | +} |
| 52 | + |
| 53 | +export function getInputVSizesForDescriptors(descriptors: DescriptorMap): Record<string, number> { |
| 54 | + return Object.fromEntries( |
| 55 | + Array.from(descriptors.entries()).map(([name, d]) => { |
| 56 | + return [name, getInputVSizeForDescriptor(d)]; |
| 57 | + }) |
| 58 | + ); |
| 59 | +} |
| 60 | + |
| 61 | +export function getChangeOutputVSizesForDescriptor(d: Descriptor): { |
| 62 | + inputVSize: number; |
| 63 | + outputVSize: number; |
| 64 | +} { |
| 65 | + return { |
| 66 | + inputVSize: getInputVSizeForDescriptor(d), |
| 67 | + outputVSize: getScriptPubKeyLength(d.descType()), |
| 68 | + }; |
| 69 | +} |
| 70 | + |
| 71 | +type InputWithDescriptorName = { descriptorName: string }; |
| 72 | +type OutputWithScript = { script: Buffer }; |
| 73 | + |
| 74 | +type Tx<TInput> = { |
| 75 | + inputs: TInput[]; |
| 76 | + outputs: OutputWithScript[]; |
| 77 | +}; |
| 78 | + |
| 79 | +export function getVirtualSize(tx: Tx<Descriptor>): number; |
| 80 | +export function getVirtualSize(tx: Tx<InputWithDescriptorName>, descriptors: DescriptorMap): number; |
| 81 | +export function getVirtualSize( |
| 82 | + tx: Tx<Descriptor> | Tx<InputWithDescriptorName>, |
| 83 | + descriptorMap?: DescriptorMap |
| 84 | +): number { |
| 85 | + const lookup = descriptorMap ? getInputVSizesForDescriptors(descriptorMap) : undefined; |
| 86 | + const inputVSize = tx.inputs.reduce((sum, input) => { |
| 87 | + if (input instanceof Descriptor) { |
| 88 | + return sum + getInputVSizeForDescriptor(input); |
| 89 | + } |
| 90 | + if ('descriptorName' in input) { |
| 91 | + if (!lookup) { |
| 92 | + throw new Error('missing descriptorMap'); |
| 93 | + } |
| 94 | + const vsize = lookup[input.descriptorName]; |
| 95 | + if (!vsize) { |
| 96 | + throw new Error(`Could not find descriptor ${input.descriptorName}`); |
| 97 | + } |
| 98 | + return sum + vsize; |
| 99 | + } |
| 100 | + throw new Error('unexpected input'); |
| 101 | + }, 0); |
| 102 | + const outputVSize = tx.outputs.reduce((sum, o) => { |
| 103 | + return sum + Dimensions.getVSizeForOutputWithScriptLength(o.script.length); |
| 104 | + }, 0); |
| 105 | + // we will just assume that we have at least one segwit input |
| 106 | + return inputVSize + outputVSize + VirtualSizes.txSegOverheadVSize; |
| 107 | +} |
0 commit comments