Skip to content

Commit f0fcb49

Browse files
Merge pull request #93 from BitGo/BTC-2934.tweak-dimensions
feat(wasm-utxo): optimize output dimensions calculation
2 parents d1d4ce7 + f68729d commit f0fcb49

3 files changed

Lines changed: 107 additions & 9 deletions

File tree

packages/wasm-utxo/js/fixedScriptWallet/Dimensions.ts

Lines changed: 21 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
import { WasmDimensions } from "../wasm/wasm_utxo.js";
22
import type { BitGoPsbt, InputScriptType, SignPath } from "./BitGoPsbt.js";
33
import type { CoinName } from "../coinName.js";
4+
import type { OutputScriptType } from "./scriptType.js";
45
import { toOutputScriptWithCoin } from "../address.js";
56

67
type FromInputParams = { chain: number; signPath?: SignPath } | { scriptType: InputScriptType };
@@ -56,15 +57,30 @@ export class Dimensions {
5657
* Create dimensions for a single output from an address
5758
*/
5859
static fromOutput(address: string, network: CoinName): Dimensions;
59-
static fromOutput(scriptOrAddress: Uint8Array | string, network?: CoinName): Dimensions {
60-
if (typeof scriptOrAddress === "string") {
60+
/**
61+
* Create dimensions for a single output from script length only
62+
*/
63+
static fromOutput(params: { length: number }): Dimensions;
64+
/**
65+
* Create dimensions for a single output from script type
66+
*/
67+
static fromOutput(params: { scriptType: OutputScriptType }): Dimensions;
68+
static fromOutput(
69+
params: Uint8Array | string | { length: number } | { scriptType: OutputScriptType },
70+
network?: CoinName,
71+
): Dimensions {
72+
if (typeof params === "string") {
6173
if (network === undefined) {
6274
throw new Error("network is required when passing an address string");
6375
}
64-
const script = toOutputScriptWithCoin(scriptOrAddress, network);
65-
return new Dimensions(WasmDimensions.from_output_script(script));
76+
const script = toOutputScriptWithCoin(params, network);
77+
return new Dimensions(WasmDimensions.from_output_script_length(script.length));
78+
}
79+
if (typeof params === "object" && "scriptType" in params) {
80+
return new Dimensions(WasmDimensions.from_output_script_type(params.scriptType));
6681
}
67-
return new Dimensions(WasmDimensions.from_output_script(scriptOrAddress));
82+
// Both Uint8Array and { length: number } have .length
83+
return new Dimensions(WasmDimensions.from_output_script_length(params.length));
6884
}
6985

7086
/**

packages/wasm-utxo/src/wasm/fixed_script_wallet/dimensions.rs

Lines changed: 22 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,8 @@
33
//! This module provides weight-based estimation for transaction fees,
44
//! tracking min/max bounds to account for ECDSA signature variance.
55
6+
use std::str::FromStr;
7+
68
use crate::error::WasmUtxoError;
79
use crate::fixed_script_wallet::bitgo_psbt::psbt_wallet_input::{
810
parse_shared_chain_and_index, InputScriptType,
@@ -435,9 +437,9 @@ impl WasmDimensions {
435437
})
436438
}
437439

438-
/// Create dimensions for a single output from script bytes
439-
pub fn from_output_script(script: &[u8]) -> WasmDimensions {
440-
let weight = compute_output_weight(script.len());
440+
/// Create dimensions for a single output from script length
441+
pub fn from_output_script_length(length: u32) -> WasmDimensions {
442+
let weight = compute_output_weight(length as usize);
441443
WasmDimensions {
442444
input_weight_min: 0,
443445
input_weight_max: 0,
@@ -446,6 +448,23 @@ impl WasmDimensions {
446448
}
447449
}
448450

451+
/// Create dimensions for a single output from script type string
452+
///
453+
/// # Arguments
454+
/// * `script_type` - One of: "p2sh", "p2shP2wsh", "p2wsh", "p2tr"/"p2trLegacy", "p2trMusig2"
455+
pub fn from_output_script_type(script_type: &str) -> Result<WasmDimensions, WasmUtxoError> {
456+
let parsed = OutputScriptType::from_str(script_type).map_err(|e| WasmUtxoError::new(&e))?;
457+
let length = match parsed {
458+
// P2SH: OP_HASH160 [20 bytes] OP_EQUAL = 23 bytes
459+
OutputScriptType::P2sh | OutputScriptType::P2shP2wsh => 23,
460+
// P2WSH: OP_0 [32 bytes] = 34 bytes
461+
OutputScriptType::P2wsh => 34,
462+
// P2TR: OP_1 [32 bytes] = 34 bytes
463+
OutputScriptType::P2trLegacy | OutputScriptType::P2trMusig2 => 34,
464+
};
465+
Ok(Self::from_output_script_length(length))
466+
}
467+
449468
/// Combine with another Dimensions instance
450469
pub fn plus(&self, other: &WasmDimensions) -> WasmDimensions {
451470
WasmDimensions {

packages/wasm-utxo/test/dimensions.ts

Lines changed: 64 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -173,10 +173,73 @@ describe("Dimensions", function () {
173173

174174
it("should throw when address is provided without network", function () {
175175
assert.throws(() => {
176-
// @ts-expect-error - testing runtime error
176+
// String matches { length: number } but implementation detects string and throws
177177
Dimensions.fromOutput("bc1qw508d6qejxtdg4y5r3zarvary0c5xw7kv8f3t4");
178178
}, /network is required/);
179179
});
180+
181+
it("should create dimensions from script length only", function () {
182+
// Compare with actual script
183+
const script = Buffer.alloc(23);
184+
const fromScript = Dimensions.fromOutput(script);
185+
const fromLength = Dimensions.fromOutput({ length: 23 });
186+
187+
assert.strictEqual(fromLength.getWeight(), fromScript.getWeight());
188+
assert.strictEqual(fromLength.getVSize(), fromScript.getVSize());
189+
assert.strictEqual(fromLength.getOutputWeight(), fromScript.getOutputWeight());
190+
});
191+
192+
it("should calculate correct weight for different script lengths", function () {
193+
// p2pkh: 25 bytes -> weight = 4 * (8 + 1 + 25) = 136
194+
const p2pkh = Dimensions.fromOutput({ length: 25 });
195+
assert.strictEqual(p2pkh.getOutputWeight(), 136);
196+
197+
// p2wpkh: 22 bytes -> weight = 4 * (8 + 1 + 22) = 124
198+
const p2wpkh = Dimensions.fromOutput({ length: 22 });
199+
assert.strictEqual(p2wpkh.getOutputWeight(), 124);
200+
201+
// p2tr: 34 bytes -> weight = 4 * (8 + 1 + 34) = 172
202+
const p2tr = Dimensions.fromOutput({ length: 34 });
203+
assert.strictEqual(p2tr.getOutputWeight(), 172);
204+
});
205+
206+
it("should create dimensions from script type", function () {
207+
// p2sh/p2shP2wsh: 23 bytes -> weight = 4 * (8 + 1 + 23) = 128
208+
const p2sh = Dimensions.fromOutput({ scriptType: "p2sh" });
209+
assert.strictEqual(p2sh.getOutputWeight(), 128);
210+
211+
const p2shP2wsh = Dimensions.fromOutput({ scriptType: "p2shP2wsh" });
212+
assert.strictEqual(p2shP2wsh.getOutputWeight(), 128);
213+
214+
// p2wsh: 34 bytes -> weight = 4 * (8 + 1 + 34) = 172
215+
const p2wsh = Dimensions.fromOutput({ scriptType: "p2wsh" });
216+
assert.strictEqual(p2wsh.getOutputWeight(), 172);
217+
218+
// p2tr/p2trLegacy: 34 bytes -> weight = 4 * (8 + 1 + 34) = 172
219+
const p2tr = Dimensions.fromOutput({ scriptType: "p2tr" });
220+
assert.strictEqual(p2tr.getOutputWeight(), 172);
221+
222+
const p2trLegacy = Dimensions.fromOutput({ scriptType: "p2trLegacy" });
223+
assert.strictEqual(p2trLegacy.getOutputWeight(), 172);
224+
225+
// p2trMusig2: 34 bytes -> weight = 4 * (8 + 1 + 34) = 172
226+
const p2trMusig2 = Dimensions.fromOutput({ scriptType: "p2trMusig2" });
227+
assert.strictEqual(p2trMusig2.getOutputWeight(), 172);
228+
});
229+
230+
it("scriptType should match equivalent length", function () {
231+
// p2sh = 23 bytes
232+
assert.strictEqual(
233+
Dimensions.fromOutput({ scriptType: "p2sh" }).getOutputWeight(),
234+
Dimensions.fromOutput({ length: 23 }).getOutputWeight(),
235+
);
236+
237+
// p2wsh = 34 bytes
238+
assert.strictEqual(
239+
Dimensions.fromOutput({ scriptType: "p2wsh" }).getOutputWeight(),
240+
Dimensions.fromOutput({ length: 34 }).getOutputWeight(),
241+
);
242+
});
180243
});
181244

182245
describe("plus", function () {

0 commit comments

Comments
 (0)