|
| 1 | +/** |
| 2 | + * Virtual size estimation for descriptor wallets. |
| 3 | + * Moved from @bitgo/utxo-core. |
| 4 | + */ |
| 5 | +import { Descriptor, Psbt } from "../index.js"; |
| 6 | +import { Dimensions } from "../fixedScriptWallet/Dimensions.js"; |
| 7 | + |
| 8 | +import { DescriptorMap } from "./DescriptorMap.js"; |
| 9 | + |
| 10 | +// Transaction overhead for segwit transactions |
| 11 | +// 4 (version) + 1 (marker) + 1 (flag) + 1 (input count) + 1 (output count) + 4 (locktime) = 12 |
| 12 | +// Weight units: 4*10 + 2 = 42, vsize = ceil(42/4) = 10.5 |
| 13 | +const TX_SEGWIT_OVERHEAD_VSIZE = 10.5; |
| 14 | + |
| 15 | +function getScriptPubKeyLength(descType: string): number { |
| 16 | + // See https://bitcoinops.org/en/tools/calc-size/ |
| 17 | + switch (descType) { |
| 18 | + case "Wpkh": |
| 19 | + // https://github.com/bitcoin/bips/blob/master/bip-0141.mediawiki#p2wpkh |
| 20 | + return 22; |
| 21 | + case "Sh": |
| 22 | + case "ShWsh": |
| 23 | + case "ShWpkh": |
| 24 | + // https://github.com/bitcoin/bips/blob/master/bip-0016.mediawiki#specification |
| 25 | + return 23; |
| 26 | + case "Pkh": |
| 27 | + return 25; |
| 28 | + case "Wsh": |
| 29 | + case "Tr": |
| 30 | + // P2WSH: https://github.com/bitcoin/bips/blob/master/bip-0141.mediawiki#p2wsh |
| 31 | + // P2TR: https://github.com/bitcoin/bips/blob/58ffd93812ff25e87d53d1f202fbb389fdfb85bb/bip-0341.mediawiki#script-validation-rules |
| 32 | + // > A Taproot output is a native SegWit output (see BIP141) with version number 1, and a 32-byte witness program. |
| 33 | + // 32 bytes for the hash, 1 byte for the version, 1 byte for the push opcode |
| 34 | + return 34; |
| 35 | + case "Bare": |
| 36 | + throw new Error("cannot determine scriptPubKey length for Bare descriptor"); |
| 37 | + default: |
| 38 | + throw new Error("unexpected descriptor type " + descType); |
| 39 | + } |
| 40 | +} |
| 41 | + |
| 42 | +function getInputVSizeForDescriptor(descriptor: Descriptor): number { |
| 43 | + // FIXME(BTC-1489): this can overestimate the size of the input significantly |
| 44 | + const maxWeight = descriptor.maxWeightToSatisfy(); |
| 45 | + const maxVSize = Math.ceil(maxWeight / 4); |
| 46 | + const sizeOpPushdata1 = 1; |
| 47 | + const sizeOpPushdata2 = 2; |
| 48 | + return ( |
| 49 | + // inputId |
| 50 | + 32 + |
| 51 | + // vOut |
| 52 | + 4 + |
| 53 | + // nSequence |
| 54 | + 4 + |
| 55 | + // script overhead |
| 56 | + (maxVSize < 255 ? sizeOpPushdata1 : sizeOpPushdata2) + |
| 57 | + // script |
| 58 | + maxVSize |
| 59 | + ); |
| 60 | +} |
| 61 | + |
| 62 | +export function getInputVSizesForDescriptors(descriptors: DescriptorMap): Record<string, number> { |
| 63 | + return Object.fromEntries( |
| 64 | + Array.from(descriptors.entries()).map(([name, d]) => { |
| 65 | + return [name, getInputVSizeForDescriptor(d)]; |
| 66 | + }), |
| 67 | + ); |
| 68 | +} |
| 69 | + |
| 70 | +export function getChangeOutputVSizesForDescriptor(d: Descriptor): { |
| 71 | + inputVSize: number; |
| 72 | + outputVSize: number; |
| 73 | +} { |
| 74 | + return { |
| 75 | + inputVSize: getInputVSizeForDescriptor(d), |
| 76 | + outputVSize: getScriptPubKeyLength(d.descType() as string), |
| 77 | + }; |
| 78 | +} |
| 79 | + |
| 80 | +type InputWithDescriptorName = { descriptorName: string }; |
| 81 | +type OutputWithScript = { script: Uint8Array }; |
| 82 | + |
| 83 | +type Tx<TInput> = { |
| 84 | + inputs: TInput[]; |
| 85 | + outputs: OutputWithScript[]; |
| 86 | +}; |
| 87 | + |
| 88 | +export function getVirtualSize(tx: Tx<Descriptor>): number; |
| 89 | +export function getVirtualSize(tx: Tx<InputWithDescriptorName>, descriptors: DescriptorMap): number; |
| 90 | +export function getVirtualSize( |
| 91 | + tx: Tx<Descriptor> | Tx<InputWithDescriptorName>, |
| 92 | + descriptorMap?: DescriptorMap, |
| 93 | +): number { |
| 94 | + const lookup = descriptorMap ? getInputVSizesForDescriptors(descriptorMap) : undefined; |
| 95 | + const inputVSize = tx.inputs.reduce((sum, input) => { |
| 96 | + if (input instanceof Descriptor) { |
| 97 | + return sum + getInputVSizeForDescriptor(input); |
| 98 | + } |
| 99 | + if ("descriptorName" in input) { |
| 100 | + if (!lookup) { |
| 101 | + throw new Error("missing descriptorMap"); |
| 102 | + } |
| 103 | + const vsize = lookup[input.descriptorName]; |
| 104 | + if (!vsize) { |
| 105 | + throw new Error(`Could not find descriptor ${input.descriptorName}`); |
| 106 | + } |
| 107 | + return sum + vsize; |
| 108 | + } |
| 109 | + throw new Error("unexpected input"); |
| 110 | + }, 0); |
| 111 | + |
| 112 | + const outputVSize = tx.outputs.reduce((sum, o) => { |
| 113 | + // Use the Dimensions class to calculate output vsize |
| 114 | + return sum + Dimensions.fromOutput({ length: o.script.length }).getOutputVSize(); |
| 115 | + }, 0); |
| 116 | + |
| 117 | + // we will just assume that we have at least one segwit input |
| 118 | + return inputVSize + outputVSize + TX_SEGWIT_OVERHEAD_VSIZE; |
| 119 | +} |
| 120 | + |
| 121 | +export function getVirtualSizeEstimateForPsbt(psbt: Psbt, descriptorMap: DescriptorMap): number { |
| 122 | + const inputCount = psbt.inputCount(); |
| 123 | + const outputCount = psbt.outputCount(); |
| 124 | + |
| 125 | + // Calculate a rough estimate based on descriptor map |
| 126 | + // For a more accurate estimation, we would need to deserialize the PSBT data |
| 127 | + let totalInputVSize = 0; |
| 128 | + for (const descriptor of descriptorMap.values()) { |
| 129 | + totalInputVSize += getInputVSizeForDescriptor(descriptor); |
| 130 | + } |
| 131 | + |
| 132 | + // Average input size * input count |
| 133 | + const avgInputVSize = descriptorMap.size > 0 ? totalInputVSize / descriptorMap.size : 100; // fallback |
| 134 | + |
| 135 | + const inputVSize = avgInputVSize * inputCount; |
| 136 | + |
| 137 | + // Assume P2WPKH outputs (34 bytes each) as a reasonable default |
| 138 | + const outputVSize = outputCount * Dimensions.fromOutput({ length: 34 }).getOutputVSize(); |
| 139 | + |
| 140 | + return inputVSize + outputVSize + TX_SEGWIT_OVERHEAD_VSIZE; |
| 141 | +} |
0 commit comments