refactor(bitcoin_client_js): migrate address validation to descriptors v3 and extend wallet address vectors for #428
What changed, and why it matters
This commit updates the JavaScript Bitcoin client used with Ledger hardware wallets so that address validation now uses a newer version of a third-party descriptor library. It also adds many more test cases covering newer Bitcoin address types such as Taproot and MuSig. The change itself is a refactoring and test expansion; there is no direct evidence in the commit that it fixes an active security vulnerability. It appears intended to keep the client compatible with newer wallet policies and to improve test coverage.
Treat this as a routine maintenance/refactoring change. Review the updated dependency version and its changelog for any security fixes, verify that the new test vectors match the expected addresses produced by the Ledger app, and ensure the fallback path still prevents invalid descriptors from being accepted as valid. No urgent security response is indicated by the commit alone.
Security signals we found
Migration of address-validation dependency to a newer major version (descriptors v3)
Removal of explicit Tapscript unsupported caveat, potentially enabling validation for previously skipped descriptor types
Addition of Taproot and MuSig test vectors that were not previously covered in JS tests
Graceful fallback when descriptor parsing fails, with a console warning
No direct patch of a memory-safety bug, cryptographic flaw, or injection vulnerability visible in the diff
Evidence from the diff
The change migrates bitcoin_client_js/src/lib/appClient.ts from @bitcoinerlab/descriptors v1-style Descriptor usage to v3-style Output usage (DescriptorsFactory(secp256k1)). The previous code explicitly noted that Tapscript descriptors were unsupported by the old library and therefore skipped third-party address validation for them. The new code removes that caveat and broadens validation to any descriptor the updated library can parse, while still falling back gracefully when parsing fails. The test file adds six new wallet-address vectors covering sh(sortedmulti(...)), sh(wsh(sortedmulti(...))), Taproot with script path, Taproot sortedmulti_a, and MuSig in keypath/scriptpath. A warning is now logged when third-party validation is unavailable.
Changed components
bitcoin_client_js/src/lib/appClient.tsbitcoin_client_js/src/__tests__/appClient.test.ts@bitcoinerlab/descriptors dependency (v3 API)Inspect captured patch +100 / −18
diff --git a/bitcoin_client_js/src/__tests__/appClient.test.ts b/bitcoin_client_js/src/__tests__/appClient.test.ts
index faff946..76622f9 100644
--- a/bitcoin_client_js/src/__tests__/appClient.test.ts
+++ b/bitcoin_client_js/src/__tests__/appClient.test.ts
@@ -104,7 +104,7 @@ describe("test AppClient", () => {
repoRootPath + "/bin/app.elf",
'--display', 'headless'
]);
-
+
sp.stdout.on('data', function(data) {
if (process.env.LOG_SPECULOS) {
console.log('stdout: ' + data);
@@ -230,11 +230,101 @@ describe("test AppClient", () => {
expResult: "tb1qmyauyzn08cduzdqweexgna2spwd0rndj55fsrkefry2cpuyt4cpsn2pg28",
walletHmac: Buffer.from("d7c7a60b4ab4a14c1bf8901ba627d72140b2fb907f2b4e35d2e693bce9fbb371", "hex")
},
+ // additional descriptor/address vectors from python tests (not previously covered in JS)
+ {
+ policy: new WalletPolicy(
+ "Cold storage",
+ "sh(sortedmulti(2,@0/**,@1/**))",
+ [
+ "[5c9e228d/48'/1'/0'/0']tpubDEGquuorgFNb8bjh5kNZQMPtABJzoWwNm78FUmeoPkfRtoPF7JLrtoZeT3J3ybq1HmC3Rn1Q8wFQ8J5usanzups5rj7PJoQLNyvq8QbJruW",
+ "[f5acc2fd/48'/1'/0'/0']tpubDFAqEGNyad35WQAZMmPD4vgBXnjH16RGciLdWekPe4f4d5JzoHVu1PS86Sy4Tm63vDf8rfV3UjifhrRuSUDfiZj5KPffTPyZ4ZXBKvjD8jm"
+ ]
+ ),
+ change: 0,
+ addrIndex: 0,
+ expResult: "2Mx69MjHC4ViZAH1koVXPvVgaazbBCdr89j",
+ walletHmac: Buffer.from("fa73e36119324fbe4cc1ca94aa842c6261526d44112a22164bc57c3335102b04", "hex")
+ },
+ {
+ policy: new WalletPolicy(
+ "Cold storage",
+ "sh(wsh(sortedmulti(2,@0/**,@1/**)))",
+ [
+ "[76223a6e/48'/1'/0'/1']tpubDE7NQymr4AFtcJXi9TaWZtrhAdy8QyKmT4U6b9qYByAxCzoyMJ8zw5d8xVLVpbTRAEqP8pVUxjLE2vDt1rSFjaiS8DSz1QcNZ8D1qxUMx1g",
+ "[f5acc2fd/48'/1'/0'/1']tpubDFAqEGNyad35YgH8zxvxFZqNUoPtr5mDojs7wzbXQBHTZ4xHeVXG6w2HvsKvjBpaRpTmjYDjdPg5w2c6Wvu8QBkyMDrmBWdCyqkDM7reSsY"
+ ]
+ ),
+ change: 0,
+ addrIndex: 0,
+ expResult: "2MxAUTJh27foYtyp9dcSxP7RgaSwkkVCHTU",
+ walletHmac: Buffer.from("1f498e7444841b883c4a63e2b88a5cad297c289d235794f8e3e17cf559ed0654", "hex")
+ },
+ {
+ policy: new WalletPolicy(
+ 'Taproot foreign internal key, and our script key',
+ 'tr(@0/**,pk(@1/**))',
+ [
+ "[76223a6e/48'/1'/0'/2']tpubDE7NQymr4AFtewpAsWtnreyq9ghkzQBXpCZjWLFVRAvnbf7vya2eMTvT2fPapNqL8SuVvLQdbUbMfWLVDCZKnsEBqp6UK93QEzL8Ck23AwF",
+ "[f5acc2fd/48'/1'/0'/2']tpubDFAqEGNyad35aBCKUAXbQGDjdVhNueno5ZZVEn3sQbW5ci457gLR7HyTmHBg93oourBssgUxuWz1jX5uhc1qaqFo9VsybY1J5FuedLfm4dK"
+ ]
+ ),
+ change: 0,
+ addrIndex: 0,
+ expResult: 'tb1pls9pp5cgcljpkjauxep03lv2c2yc2wcuua26p3ks6j2lq0vl9kjqf5rgm2',
+ walletHmac: Buffer.from(
+ 'dae925660e20859ed8833025d46444483ce264fdb77e34569aabe9d590da8fb7',
+ 'hex'
+ )
+ },
+ {
+ policy: new WalletPolicy(
+ "Taproot single-key or multisig 2-of-2",
+ "tr(@0/**,sortedmulti_a(2,@1/**,@2/**))",
+ [
+ "[f5acc2fd/48'/1'/0'/1']tpubDFAqEGNyad35YgH8zxvxFZqNUoPtr5mDojs7wzbXQBHTZ4xHeVXG6w2HvsKvjBpaRpTmjYDjdPg5w2c6Wvu8QBkyMDrmBWdCyqkDM7reSsY",
+ "[76223a6e/48'/1'/0'/2']tpubDE7NQymr4AFtewpAsWtnreyq9ghkzQBXpCZjWLFVRAvnbf7vya2eMTvT2fPapNqL8SuVvLQdbUbMfWLVDCZKnsEBqp6UK93QEzL8Ck23AwF",
+ "[f5acc2fd/48'/1'/0'/2']tpubDFAqEGNyad35aBCKUAXbQGDjdVhNueno5ZZVEn3sQbW5ci457gLR7HyTmHBg93oourBssgUxuWz1jX5uhc1qaqFo9VsybY1J5FuedLfm4dK"
+ ]
+ ),
+ change: 0,
+ addrIndex: 0,
+ expResult: "tb1pdzk72dnvz3246474p4m5a97u43h6ykt2qcjrrhk6y0fkg8hx2mvswwgvv7",
+ walletHmac: Buffer.from("a3f31e9d7b70d1d967413488bae136a8b6c7afd1de0524deb6cf74f5c509b9ab", "hex")
+ },
+ {
+ policy: new WalletPolicy(
+ "Musig in keypath",
+ "tr(musig(@0,@1)/**)",
+ [
+ "[f5acc2fd/44'/1'/0']tpubDCwYjpDhUdPGP5rS3wgNg13mTrrjBuG8V9VpWbyptX6TRPbNoZVXsoVUSkCjmQ8jJycjuDKBb9eataSymXakTTaGifxR6kmVsfFehH1ZgJT",
+ "tpubDCwYjpDhUdPGQWG6wG6hkBJuWFZEtrn7j3xwG3i8XcQabcGC53xWZm1hSXrUPFS5UvZ3QhdPSjXWNfWmFGTioARHuG5J7XguEjgg7p8PxAm"
+ ]
+ ),
+ change: 0,
+ addrIndex: 3,
+ expResult: "tb1pc87la0ksvw4pfq6qc3gn9en33kx7s9rx4c4epy578kfjsdjv6mks7u7dgn",
+ walletHmac: Buffer.from("05b7b4bccd3188effc24de8fd67e83231d8486772800884db0d81bad19f2be3e", "hex")
+ },
+ {
+ policy: new WalletPolicy(
+ "Musig in script path",
+ "tr(@0/**,pk(musig(@1,@2)/**))",
+ [
+ "tpubD6NzVbkrYhZ4WLczPJWReQycCJdd6YVWXubbVUFnJ5KgU5MDQrD998ZJLSmaB7GVcCnJSDWprxmrGkJ6SvgQC6QAffVpqSvonXmeizXcrkN",
+ "[f5acc2fd/44'/1'/0']tpubDCwYjpDhUdPGP5rS3wgNg13mTrrjBuG8V9VpWbyptX6TRPbNoZVXsoVUSkCjmQ8jJycjuDKBb9eataSymXakTTaGifxR6kmVsfFehH1ZgJT",
+ "tpubDCwYjpDhUdPGQWG6wG6hkBJuWFZEtrn7j3xwG3i8XcQabcGC53xWZm1hSXrUPFS5UvZ3QhdPSjXWNfWmFGTioARHuG5J7XguEjgg7p8PxAm"
+ ]
+ ),
+ change: 0,
+ addrIndex: 3,
+ expResult: "tb1pa423acwcjc8jgt36muavyun8e2hz3t5qwptsr3wr8afmdfk3wchswf9ntp",
+ walletHmac: Buffer.from("b22397b717949ede59c3c9f31c987acda098471211f754b6633c87054c1efb51", "hex")
+ },
];
for (const { policy, change, addrIndex, expResult, walletHmac } of testcases) {
const result = await app.getWalletAddress(policy, walletHmac || null, change, addrIndex, false);
- expect(result).toEqual(expResult);
+ expect(result).toEqual(expResult);
}
});
diff --git a/bitcoin_client_js/src/lib/appClient.ts b/bitcoin_client_js/src/lib/appClient.ts
index eee2983..990437c 100644
--- a/bitcoin_client_js/src/lib/appClient.ts
+++ b/bitcoin_client_js/src/lib/appClient.ts
@@ -1,6 +1,6 @@
-import * as descriptors from '@bitcoinerlab/descriptors';
+import { DescriptorsFactory } from '@bitcoinerlab/descriptors';
import * as secp256k1 from '@bitcoinerlab/secp256k1';
-const { Descriptor } = descriptors.DescriptorsFactory(secp256k1);
+const { Output } = DescriptorsFactory(secp256k1);
import Transport from '@ledgerhq/hw-transport';
import { networks } from 'bitcoinjs-lib';
@@ -451,22 +451,14 @@ export class AppClient {
let thirdPartyValidationApplicable = true;
let thirdPartyGeneratedAddress: string;
try {
- thirdPartyGeneratedAddress = new Descriptor({
- expression,
- network
+ thirdPartyGeneratedAddress = new Output({
+ descriptor: expression,
+ network,
}).getAddress();
} catch (err) {
- // Note: @bitcoinerlab/descriptors@1.0.x does not support Tapscript yet.
- // These are the supported descriptors:
- // - pkh(KEY)
- // - wpkh(KEY)
- // - sh(wpkh(KEY))
- // - sh(SCRIPT)
- // - wsh(SCRIPT)
- // - sh(wsh(SCRIPT)), where
- // SCRIPT is any of the (non-tapscript) fragments in: https://bitcoin.sipa.be/miniscript/
- //
- // Other expressions are not supported and third party validation would not be applicable:
+ // If this descriptor cannot be interpreted by @bitcoinerlab/descriptors,
+ // third-party validation is not applicable.
+ console.warn(`Third-party address validation unavailable for this descriptor. Address: ${address}`, err);
thirdPartyValidationApplicable = false;
}
if (
Why this scored 19/100
Community notes
Notes can correct, qualify, or add evidence to the AI analysis. Every note shown here has been validated by a human moderator.
The AI analysis stands alone for now. Submit a note if you can add evidence or important context.