Skip to content

Commit e865dfe

Browse files
OttoAllmendingerllm-git
andcommitted
feat(wasm-utxo): add test for large dogecoin amounts
Add test cases to verify proper handling of large Dogecoin values (up to 1e19). This ensures PSBT creation, signing, and extraction work correctly with amounts that exceed JavaScript's safe integer limits but fit in u64. Issue: BTC-2659 Co-authored-by: llm-git <[email protected]>
1 parent 393fb9e commit e865dfe

File tree

2 files changed

+152
-7
lines changed

2 files changed

+152
-7
lines changed

packages/wasm-utxo/src/fixed_script_wallet/bitgo_psbt/mod.rs

Lines changed: 96 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -3696,7 +3696,7 @@ mod tests {
36963696
let prev_tx: Option<Vec<u8>> = orig_psbt_input
36973697
.non_witness_utxo
36983698
.as_ref()
3699-
.map(|tx| miniscript::bitcoin::consensus::serialize(tx));
3699+
.map(miniscript::bitcoin::consensus::serialize);
37003700

37013701
let result = reconstructed.add_wallet_input(
37023702
txid,
@@ -3730,7 +3730,7 @@ mod tests {
37303730
let prev_tx = orig_psbt_input
37313731
.non_witness_utxo
37323732
.as_ref()
3733-
.map(|tx| miniscript::bitcoin::consensus::encode::serialize(tx));
3733+
.map(miniscript::bitcoin::consensus::encode::serialize);
37343734

37353735
reconstructed.add_replay_protection_input(
37363736
compressed_pubkey,
@@ -3853,9 +3853,6 @@ mod tests {
38533853
.enumerate()
38543854
{
38553855
// Compare utxo fields - either witness_utxo or non_witness_utxo should match
3856-
// For segwit: witness_utxo is used
3857-
// For non-segwit with prev_tx: non_witness_utxo is used
3858-
// For non-segwit without prev_tx: witness_utxo is used as fallback
38593856
let orig_has_utxo = orig.witness_utxo.is_some() || orig.non_witness_utxo.is_some();
38603857
let recon_has_utxo = recon.witness_utxo.is_some() || recon.non_witness_utxo.is_some();
38613858
assert!(
@@ -3906,7 +3903,6 @@ mod tests {
39063903
}
39073904

39083905
// For taproot wallet inputs, compare tap_internal_key
3909-
// (but not tap_leaf_script which depends on signer/cosigner choice)
39103906
if orig.tap_internal_key.is_some() {
39113907
assert_eq!(
39123908
orig.tap_internal_key, recon.tap_internal_key,
@@ -3925,7 +3921,7 @@ mod tests {
39253921
.zip(reconstructed_outputs.iter())
39263922
.enumerate()
39273923
{
3928-
// Skip metadata comparison for non-wallet outputs (external or from different keys)
3924+
// Skip metadata comparison for non-wallet outputs
39293925
if !wallet_output_indices.contains(&idx) {
39303926
continue;
39313927
}
@@ -3971,4 +3967,97 @@ mod tests {
39713967
crate::test_psbt_fixtures!(test_psbt_reconstruction, network, format, {
39723968
test_psbt_reconstruction_for_network(network, format);
39733969
}, ignore: [Zcash]);
3970+
3971+
#[test]
3972+
fn test_dogecoin_single_input_single_output_large_amount() {
3973+
use crate::fixed_script_wallet::test_utils::get_test_wallet_keys;
3974+
use miniscript::bitcoin::bip32::{DerivationPath, Xpriv};
3975+
use miniscript::bitcoin::consensus::{deserialize, serialize};
3976+
use miniscript::bitcoin::hashes::{sha256, Hash};
3977+
use miniscript::bitcoin::psbt::Psbt as BitcoinPsbt;
3978+
use miniscript::bitcoin::secp256k1::Secp256k1;
3979+
use miniscript::bitcoin::{Network as BitcoinNetwork, Txid};
3980+
use std::str::FromStr;
3981+
3982+
let wallet_keys =
3983+
crate::fixed_script_wallet::RootWalletKeys::new(get_test_wallet_keys("doge_1e19"));
3984+
3985+
let mut psbt = BitGoPsbt::new(Network::Dogecoin, &wallet_keys, Some(2), Some(0));
3986+
3987+
// Large output amount (1e19) should fit in u64 and round-trip.
3988+
let value: u64 = 10_000_000_000_000_000_000;
3989+
3990+
let txid = Txid::all_zeros();
3991+
let vout = 0u32;
3992+
let script_id = ScriptId { chain: 0, index: 0 };
3993+
3994+
psbt.add_wallet_input(
3995+
txid,
3996+
vout,
3997+
value,
3998+
&wallet_keys,
3999+
script_id,
4000+
WalletInputOptions::default(),
4001+
)
4002+
.expect("add_wallet_input");
4003+
4004+
psbt.add_wallet_output(0, 0, value, &wallet_keys)
4005+
.expect("add_wallet_output");
4006+
4007+
assert_eq!(psbt.psbt().unsigned_tx.input.len(), 1);
4008+
assert_eq!(psbt.psbt().unsigned_tx.output.len(), 1);
4009+
assert_eq!(psbt.psbt().unsigned_tx.output[0].value.to_sat(), value);
4010+
4011+
// Sign, finalize, and extract the signed transaction.
4012+
//
4013+
// This mirrors utxo-lib testutil.getKeyTriple("doge_1e19") which uses sha256("seed.{i}")
4014+
// and RootWalletKeys derives at m/0/0/{chain}/{index}.
4015+
let secp = Secp256k1::new();
4016+
let seed = "doge_1e19";
4017+
let user_seed_hash = sha256::Hash::hash(format!("{}.0", seed).as_bytes()).to_byte_array();
4018+
let bitgo_seed_hash = sha256::Hash::hash(format!("{}.2", seed).as_bytes()).to_byte_array();
4019+
let user_xpriv =
4020+
Xpriv::new_master(BitcoinNetwork::Testnet, &user_seed_hash).expect("user xpriv");
4021+
let bitgo_xpriv =
4022+
Xpriv::new_master(BitcoinNetwork::Testnet, &bitgo_seed_hash).expect("bitgo xpriv");
4023+
let user_path = DerivationPath::from_str("m/0/0/0/0").expect("derivation path");
4024+
let derived = user_xpriv
4025+
.derive_priv(&secp, &user_path)
4026+
.expect("derive user xpriv");
4027+
let user_privkey = derived.private_key;
4028+
let derived_bitgo = bitgo_xpriv
4029+
.derive_priv(&secp, &user_path)
4030+
.expect("derive bitgo xpriv");
4031+
let bitgo_privkey = derived_bitgo.private_key;
4032+
4033+
psbt.sign_with_privkey(0, &user_privkey)
4034+
.expect("sign_with_privkey");
4035+
psbt.sign_with_privkey(0, &bitgo_privkey)
4036+
.expect("sign_with_privkey (bitgo)");
4037+
4038+
// P2SH multisig needs 2 signatures to finalize.
4039+
assert!(
4040+
psbt.psbt().inputs[0].partial_sigs.len() >= 2,
4041+
"expected at least 2 partial signatures before finalization"
4042+
);
4043+
4044+
let finalized_psbt: BitcoinPsbt = psbt.finalize(&secp).expect("finalize");
4045+
let extracted_tx = finalized_psbt.extract_tx().expect("extract_tx");
4046+
let extracted_bytes = serialize(&extracted_tx);
4047+
4048+
// Sanity checks: has spend data and preserves amounts.
4049+
assert_eq!(extracted_tx.input.len(), 1);
4050+
assert!(
4051+
!extracted_tx.input[0].script_sig.is_empty()
4052+
|| !extracted_tx.input[0].witness.is_empty(),
4053+
"expected script_sig or witness to be present after finalization"
4054+
);
4055+
assert_eq!(extracted_tx.output.len(), 1);
4056+
assert_eq!(extracted_tx.output[0].value.to_sat(), value);
4057+
4058+
// Also ensure the extracted tx bytes can be decoded again.
4059+
let decoded = deserialize::<miniscript::bitcoin::Transaction>(&extracted_bytes)
4060+
.expect("decode extracted tx");
4061+
assert_eq!(decoded.compute_txid(), extracted_tx.compute_txid());
4062+
}
39744063
}
Lines changed: 56 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,56 @@
1+
import assert from "node:assert";
2+
import * as utxolib from "@bitgo/utxo-lib";
3+
import { BIP32, fixedScriptWallet } from "../../js/index.js";
4+
import type { RootWalletKeys } from "../../js/fixedScriptWallet/RootWalletKeys.js";
5+
6+
function getWalletKeysForSeed(seed: string): RootWalletKeys {
7+
const triple = utxolib.testutil.getKeyTriple(seed);
8+
const neutered = triple.map((k) => k.neutered()) as [
9+
utxolib.BIP32Interface,
10+
utxolib.BIP32Interface,
11+
utxolib.BIP32Interface,
12+
];
13+
return fixedScriptWallet.RootWalletKeys.from({
14+
triple: neutered,
15+
derivationPrefixes: ["0/0", "0/0", "0/0"],
16+
});
17+
}
18+
19+
describe("Dogecoin large output limit amount (LOL amounts) (1-in/1-out)", function () {
20+
it("should sign, finalize, and extract tx with 1e19 output value", function () {
21+
const networkName = "dogecoin";
22+
const seed = "doge_1e19";
23+
const walletKeys = getWalletKeysForSeed(seed);
24+
25+
const psbt = fixedScriptWallet.BitGoPsbt.createEmpty(networkName, walletKeys, {
26+
version: 2,
27+
lockTime: 0,
28+
});
29+
30+
const value = 10_000_000_000_000_000_000n; // 1e19
31+
const txid = "00".repeat(32);
32+
33+
psbt.addWalletInput({ txid, vout: 0, value }, walletKeys, { scriptId: { chain: 0, index: 0 } });
34+
psbt.addWalletOutput(walletKeys, { chain: 0, index: 0, value });
35+
36+
const parsed = psbt.parseTransactionWithWalletKeys(walletKeys, { publicKeys: [] });
37+
assert.strictEqual(parsed.inputs.length, 1);
38+
assert.strictEqual(parsed.outputs.length, 1);
39+
assert.strictEqual(parsed.inputs[0].value, value);
40+
assert.strictEqual(parsed.outputs[0].value, value);
41+
42+
// P2SH multisig needs 2 signatures to finalize. Use user + bitgo keys.
43+
const xprvs = utxolib.testutil.getKeyTriple(seed);
44+
const userXpriv = BIP32.fromBase58(xprvs[0].toBase58());
45+
const bitgoXpriv = BIP32.fromBase58(xprvs[2].toBase58());
46+
47+
psbt.sign(0, userXpriv);
48+
assert.strictEqual(psbt.verifySignature(0, userXpriv), true, "user signature missing");
49+
psbt.sign(0, bitgoXpriv);
50+
assert.strictEqual(psbt.verifySignature(0, bitgoXpriv), true, "bitgo signature missing");
51+
52+
psbt.finalizeAllInputs();
53+
const extractedTx = psbt.extractTransaction();
54+
assert.ok(extractedTx.length > 0, "expected extracted tx bytes");
55+
});
56+
});

0 commit comments

Comments
 (0)