I'm trying to sign a PSBT transaction from bitcoinjs-lib following what I found here:
https://github.com/helperbit/helperbit-wallet/blob/master/app/components/dashboard.wallet/bitcoin.service/ledger.ts
I've checked that the compressed publicKey both from ledger, and the one from bitcoinjsLib returned the same value.
I could sign it with the bitcoinjs-lib ECPair, but when I try to sign it using a ledger, it is always invalid. Can someone help me point out where I made a mistake?
These variables are already mentioned in the code below, but for clarity purpose:
- mnemonics: abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon about
- previousTx: 02000000000101869362410c61a69ab9390b2167d08219662196e869626e8b0350f1a8e4075efb0100000017160014ef3fdddccdb6b53e6dd1f5a97299a6ba2e1c11c3ffffffff0240420f000000000017a914f748afee815f78f97672be5a9840056d8ed77f4887df9de6050000000017a9142ff4aa6ffa987335c7bdba58ef4cbfecbe9e49938702473044022061a01bf0fbac4650a9b3d035b3d9282255a5c6040aa1d04fd9b6b52ed9f4d20a022064e8e2739ef532e6b2cb461321dd20f5a5d63cf34da3056c428475d42c9aff870121025fb5240daab4cee5fa097eef475f3f2e004f7be702c421b6607d8afea1affa9b00000000
- paths:
["0'/0/0"]
- redeemScript: (non-multisig segwit)
00144328adace54072cd069abf108f97cf80420b212b
This is my minimum reproducible code I've got.
/* tslint:disable */
// @ts-check
require('regenerator-runtime');
const bip39 = require('bip39');
const { default: Transport } = require('@ledgerhq/hw-transport-node-hid');
const { default: AppBtc } = require('@ledgerhq/hw-app-btc');
const bitcoin = require('bitcoinjs-lib');
const mnemonics = 'abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon about';
const NETWORK = bitcoin.networks.regtest;
/**
* @param {string} pk
* @returns {string}
*/
function compressPublicKey(pk) {
const { publicKey } = bitcoin.ECPair.fromPublicKey(Buffer.from(pk, 'hex'));
return publicKey.toString('hex');
}
/** @returns {Promise<any>} */
async function appBtc() {
const transport = await Transport.create();
const btc = new AppBtc(transport); return btc; }
const signTransaction = async() => {
const ledger = await appBtc();
const paths = ["0'/0/0"];
const [ path ] = paths;
const previousTx = "02000000000101869362410c61a69ab9390b2167d08219662196e869626e8b0350f1a8e4075efb0100000017160014ef3fdddccdb6b53e6dd1f5a97299a6ba2e1c11c3ffffffff0240420f000000000017a914f748afee815f78f97672be5a9840056d8ed77f4887df9de6050000000017a9142ff4aa6ffa987335c7bdba58ef4cbfecbe9e49938702473044022061a01bf0fbac4650a9b3d035b3d9282255a5c6040aa1d04fd9b6b52ed9f4d20a022064e8e2739ef532e6b2cb461321dd20f5a5d63cf34da3056c428475d42c9aff870121025fb5240daab4cee5fa097eef475f3f2e004f7be702c421b6607d8afea1affa9b00000000"
const utxo = bitcoin.Transaction.fromHex(previousTx);
const segwit = utxo.hasWitnesses();
const txIndex = 0; // ecpairs things.
const seed = await bip39.mnemonicToSeed(mnemonics);
const node = bitcoin.bip32.fromSeed(seed, NETWORK);
const ecPrivate = node.derivePath(path);
const ecPublic = bitcoin.ECPair.fromPublicKey(ecPrivate.publicKey, { network: NETWORK }); const p2wpkh = bitcoin.payments.p2wpkh({ pubkey: ecPublic.publicKey, network: NETWORK }); const p2sh = bitcoin.payments.p2sh({ redeem: p2wpkh, network: NETWORK });
const redeemScript = p2sh.redeem.output;
const fromLedger = await ledger.getWalletPublicKey(path, { format: 'p2sh' });
const ledgerPublicKey = compressPublicKey(fromLedger.publicKey);
const bitcoinJsPublicKey = ecPublic.publicKey.toString('hex');
console.log({ ledgerPublicKey, bitcoinJsPublicKey, address: p2sh.address, segwit, fromLedger, redeemScript: redeemScript.toString('hex') });
var tx1 = ledger.splitTransaction(previousTx, true);
const psbt = new bitcoin.Psbt({ network: NETWORK });
psbt.addInput({
hash: utxo.getId(),
index: txIndex,
nonWitnessUtxo: Buffer.from(previousTx, 'hex'),
redeemScript,
});
psbt.addOutput({
address: 'mgWUuj1J1N882jmqFxtDepEC73Rr22E9GU',
value: 5000,
});
psbt.setMaximumFeeRate(1000 * 1000 * 1000); // ignore maxFeeRate we're testnet anyway. psbt.setVersion(2);
/** @type {string} */
// @ts-ignore
const newTx = psbt.__CACHE.__TX.toHex();
console.log({ newTx });
const splitNewTx = await ledger.splitTransaction(newTx, true);
const outputScriptHex = await ledger.serializeTransactionOutputs(splitNewTx).toString("hex"); const expectedOutscriptHex = '0188130000000000001976a9140ae1441568d0d293764a347b191025c51556cecd88ac'; // console.log({ outputScriptHex, expectedOutscriptHex, eq: expectedOutscriptHex === outputScriptHex }); const inputs = [ [tx1, 0, p2sh.redeem.output.toString('hex') /** ??? */] ]; const ledgerSignatures = await ledger.signP2SHTransaction( inputs, paths, outputScriptHex, 0, // lockTime, undefined, // sigHashType = SIGHASH_ALL ??? utxo.hasWitnesses(), 2, // version??, ); const signer = { network: NETWORK, publicKey: ecPrivate.publicKey, /** @param {Buffer} $hash */ sign: ($hash) => { const expectedSignature = ecPrivate.sign($hash); // just for comparison. const [ ledgerSignature0 ] = ledgerSignatures; const decodedLedgerSignature = bitcoin.script.signature.decode(Buffer.from(ledgerSignature0, 'hex')); console.log({ $hash: $hash.toString('hex'), expectedSignature: expectedSignature.toString('hex'), actualSignature: decodedLedgerSignature.signature.toString('hex'), }); // return signature; return decodedLedgerSignature.signature; }, }; psbt.signInput(0, signer); const validated = psbt.validateSignaturesOfInput(0); psbt.finalizeAllInputs(); const hex = psbt.extractTransaction().toHex(); console.log({ validated, hex }); }; if (process.argv[1] === __filename) { signTransaction().catch(console.error)