Maybe an example with a hardcoded-value would be helpful.
I think, I'm following as much as I could understood from this reference: https://github.com/helperbit/helperbit-wallet/blob/master/app/components/dashboard/bitcoin.service/ledger.ts
but it doesn't seems to be working.
this is my minimal reproducible example 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';
// stolen from: https://github.com/LedgerHQ/ledgerjs/blob/master/packages/hw-app-btc/tests/Btc.test.js
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)
}
I also asked this question on Stackoverflow hoping to get some help.
https://stackoverflow.com/questions/59082832/how-to-sign-bitcoin-psbt-with-ledger
What does await btc.splitTransaction do exactly?
Just FYI, the internal transaction of a PSBT only contains the hash, index, and sequence of each input. Any redeemScripts or witnessScripts are not anywhere inside the Transaction object.
Maybe that is what's causing the problem?
Also: await btc.splitTransaction(tx, true); you have segwit as true here... but since the internal PSBT tx doesn't contain any witness info toHex() will format it as a non-segwit formatted raw unsigned transaction.
What does
await btc.splitTransactiondo exactly?
hmmm... I think it is more or less doing something similar with bitcoin-cli decoderawtransaction
as for the output of btc.splitTransaction, it can be viewed at http://public.ramadoka.com/splittransaction.json
{ buffer: "<somehexadecimal" } is actually the usual Buffer, but I override the Buffer.prototype.toJSON with
Buffer.prototype.toJSON = function(): { type: 'Buffer', data: number[]; buffer: string; } {
return {
buffer: this.toString('hex'),
} as any;
}
for the sake of prettifying.
as for the segwit, I think it should be okay, because when I sign it in the signP2SHTransaction I called it with the segwit flag set to true.
const signatures = await btc.signP2SHTransaction(
inputs,
paths,
ledgerOutputs.toString('hex'),
LOCK_TIME,
undefined,
segwit, // <- here
2
);
I'm sorry, I don't know about the Ledger library... All I can say is you are not passing the redeemScript to any Ledger function anywhere... so that might be the problem.
I have no clue what Ledger needs... but it should need the redeemScript. As Ledger support and link this issue... hopefully we can figure it out for you.
Keep us updated if you figure it out. Thanks.
Ooof, finally got it working.
My mistake was I was trying to sign a p2sh-p2ms, By following a reference on how to sign a p2sh-p2wsh-p2ms.
And, also, that missing last 2 bit (01), which I think represent SIGHASH_ALL caused an error when I try to decode the signature.
this is my finalized code.
// @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 serializer = require('@ledgerhq/hw-app-btc/lib/serializeTransaction');
const bitcoin = require('bitcoinjs-lib');
const mnemonics = 'abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon about';
const NETWORK = bitcoin.networks.regtest;
const DEFAULT_LOCK_TIME = 0;
const SIGHASH_ALL = 1;
const PATHS = ["m/49'/1'/0'/0/0", "m/49'/1'/0'/0/1"];
async function appBtc() {
const transport = await Transport.create();
const btc = new AppBtc(transport);
return btc;
}
/**
* @param {string} pk
* @returns {string}
*/
function compressPublicKey(pk) {
const {
publicKey
} = bitcoin.ECPair.fromPublicKey(Buffer.from(pk, 'hex'));
return publicKey.toString('hex');
}
/**
* @param {AppBtc} ledger
* @param {bitcoin.Transaction} tx
*/
function splitTransaction(ledger, tx) {
return ledger.splitTransaction(tx.toHex(), tx.hasWitnesses());
}
const signTransaction = async() => {
const seed = await bip39.mnemonicToSeed(mnemonics);
const node = bitcoin.bip32.fromSeed(seed, NETWORK);
const signers = PATHS.map((p) => node.derivePath(p));
const publicKeys = signers.map((s) => s.publicKey);
const p2ms = bitcoin.payments.p2ms({ pubkeys: publicKeys, network: NETWORK, m: 1 });
const p2shP2ms = bitcoin.payments.p2sh({ redeem: p2ms, network: NETWORK });
const previousTx = '02000000000101588e8fc89afea9adb79de2650f0cdba762f7d0880c29a1f20e7b468f97da9f850100000017160014345766130a8f8e83aef8621122ca14fff88e6d51ffffffff0240420f000000000017a914a0546d83e5f8876045d7025a230d87bf69db893287df9de6050000000017a9142ff4aa6ffa987335c7bdba58ef4cbfecbe9e49938702483045022100c654271a891af98e46ca4d82ede8cccb0503a430e50745f959274294c98030750220331b455fed13ff4286f6db699eca06aa0c1c37c45c9f3aed3a77a3b0187ff4ac0121037ebcf3cf122678b9dc89b339017c5b76bee9fedd068c7401f4a8eb1d7e841c3a00000000';
const utxo = bitcoin.Transaction.fromHex(previousTx);
const txIndex = 0;
const destination = p2shP2ms;
const redeemScript = destination.redeem.output;
// const witnessScript = destination.redeem.redeem.output;
const ledgerRedeemScript = redeemScript;
// use witness script if the outgoing transaction was from a p2sh-p2wsh-p2ms instead of p2sh-p2ms
const fee = 1000;
/** @type {number} */
// @ts-ignore
const amount = utxo.outs[txIndex].value;
const withdrawAmount = amount - fee;
const psbt = new bitcoin.Psbt({ network: NETWORK });
const version = 1;
psbt.addInput({
hash: utxo.getId(),
index: txIndex,
nonWitnessUtxo: utxo.toBuffer(),
redeemScript,
});
psbt.addOutput({
address: '2MsK2NdiVEPCjBMFWbjFvQ39mxWPMopp5vp',
value: withdrawAmount
});
psbt.setVersion(version);
/** @type {bitcoin.Transaction} */
// @ts-ignore
const newTx = psbt.__CACHE.__TX;
const ledger = await appBtc();
const inLedgerTx = splitTransaction(ledger, utxo);
const outLedgerTx = splitTransaction(ledger, newTx);
const outputScriptHex = await serializer.serializeTransactionOutputs(outLedgerTx).toString('hex');
/** @param {string} path */
const signer = (path) => {
const ecPrivate = node.derivePath(path);
// actually only publicKey is needed, albeit ledger give an uncompressed one.
// const { publicKey: uncompressedPublicKey } = await ledger.getWalletPublicKey(path);
// const publicKey = compressPublicKey(publicKey);
return {
network: NETWORK,
publicKey: ecPrivate.publicKey,
/** @param {Buffer} $hash */
sign: async ($hash) => {
const ledgerTxSignatures = await ledger.signP2SHTransaction({
inputs: [[inLedgerTx, txIndex, ledgerRedeemScript.toString('hex')]],
associatedKeysets: [ path ],
outputScriptHex,
lockTime: DEFAULT_LOCK_TIME,
segwit: newTx.hasWitnesses(),
transactionVersion: version,
sigHashType: SIGHASH_ALL,
});
const [ ledgerSignature ] = ledgerTxSignatures;
const expectedSignature = ecPrivate.sign($hash);
const finalSignature = (() => {
if (newTx.hasWitnesses()) {
return Buffer.from(ledgerSignature, 'hex');
};
return Buffer.concat([
ledgerSignature,
Buffer.from('01', 'hex'), // SIGHASH_ALL
]);
})();
console.log({
expectedSignature: expectedSignature.toString('hex'),
finalSignature: finalSignature.toString('hex'),
});
const { signature } = bitcoin.script.signature.decode(finalSignature);
return signature;
},
};
}
await psbt.signInputAsync(0, signer(PATHS[0]));
const validate = await psbt.validateSignaturesOfAllInputs();
await psbt.finalizeAllInputs();
const hex = psbt.extractTransaction().toHex();
console.log({ validate, hex });
};
if (process.argv[1] === __filename) {
signTransaction().catch(console.error)
}
Very cool. Thanks for sharing the working code!
@SiestaMadokaist I'm struggling to achieve the same with p2sh-p2wpkh, which is what my ledger is using by default for BTC.
My code is very similar to yours already and I'm very unsure on where the error lies.. Did you have success signing a p2sh-p2wpkh using bitcoinjs and the ledger? I will keep trying but if you have any info it would be highly appreciated.
@lacksfish
sorry that I haven't fiddled with it again, but I think you should try using p2wpkh.output for the redeemscript, and just try it with witness-flag true or false...
if either doesn't work, consider trying it with ledger.createPaymentTransactionNew and see if that works.
if it works with createPaymentTransactionNew, then just add some logger in the ledger's transport.send method and make sure you're sending the same buffer on each call.
for example in my case, I did something like this:
// @ts-ignore
Buffer.prototype.toJSON = function () {
return `Buffer:${this.toString('hex')}`;
}
async function appBtc() {
const transport = await Transport.create();
const btc = new AppBtc(transport);
const send = transport.send.bind(transport);
transport.send = async (cla, ins, p1, p2, data) => {
const buffer = JSON.stringify(data);
const s0 = [cla, ins, p1, p2].map((x) => `0x${x.toString(16).toUpperCase()}`);
const s = [...s0, buffer];
const message = `await ledger.sendArgs(${s});`;
console.log(message);
const result = await send(cla, ins, p1, p2, data);
console.log(`<<< ${result.toString('hex')}`);
return result;
}
const splitT = btc.splitTransaction.bind(btc);
btc.splitTransaction = (...args) => {
console.log(`=== Split Transaction Start ====`);
const result = splitT(...args);
console.log(`=== Split Transaction End ===`);
return result;
};
const signp2sh = btc.signP2SHTransaction.bind(btc);
btc.signP2SHTransaction = async (...args) => {
console.log('SIGNP2SH START');
const result = await signp2sh(...args);
console.log('SIGNP2SH END')
return result;
}
return btc;
}
Also note that psbt.__CACHE.__TX does not contain any input information, so hasWitnesses will always be false regardless of whether or not the input is segwit.
It's a cache that is lazy loaded so that the inputs and witnesses are added once extractTransaction is called... which can only be called when all the inputs are signed and finalized.
oh, dang, lol, I guess that explain why I failed on some address, and I have to resort to determine whether the input address is a segwit or not, manually.
I should not used newTx.hasWitness() then.
thank you for the clarification.
@SiestaMadokaist I've tried your exact code snippet from above, only changing the mnemonic, txIndex, and previousTx.
I've changed appBtc() slightly so it works for me:
async function appBtc() {
const transport = await Transport.open("");
const btc = new AppBtc(transport);
return btc;
}
And I've adjusted the following line in your code:
const p2ms = bitcoin.payments.p2ms({ pubkeys: publicKeys, network: NETWORK, m: 1 });
to
const p2ms = bitcoin.payments.p2wpkh({ pubkey: publicKeys[0], network: NETWORK});
This is the result I get:
{
expectedSignature: '7a643609c5218cf3379613d194adcb59250e4be6ee54c7b8b551af948c2c0b0c42a1231e882671fa72775a40cd00bd0b6c4e9c0c1abcafd1cef2001a50501a77',
finalSignature: '304402207f21f9005d6d4f800b01236884948f3c75f7f35036ac2ab3529e4b86ec026df10220707ab6dba65075ab7814f66a9a824a7b6d58f9851b3943b8d1b5feb7c533cfb2'
}
Any help highly appreciated.
@lacksfish
you could just easily obtain more or less the same result by just using ledger.createPaymentTransactionNew (so you dont need to mix between 2 libraries ledger and bitcoinjs-lib) like so:
// this code is also shown in the 2nd example below on line 116.
const txHex = await ledger.createPaymentTransactionNew({
inputs: [[inLedgerTx, txIndex]],
associatedKeysets: [ path ],
outputScriptHex,
lockTime: LOCK_TIME,
segwit: newTxIsSegwit,
transactionVersion: version,
sigHashType: SIGHASH_ALL,
});
debug(txHex);
or, if you want ledger just to sign your tx, (not building the whole tx).
p.s: (run it with DEBUG=ledger node <thisscript.js>)
/* tslint:disable */
// @ts-check
require('regenerator-runtime');
const debug = require('debug')('ledger');
const { default: Transport } = require('@ledgerhq/hw-transport-node-hid');
const { default: AppBtc } = require('@ledgerhq/hw-app-btc');
const serializer = require('@ledgerhq/hw-app-btc/lib/serializeTransaction');
const bip39 = require('bip39');
const bitcoin = require('bitcoinjs-lib');
// const mnemonics = 'abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon about';
const NETWORK = bitcoin.networks.regtest;
const LOCK_TIME = 0;
const SIGHASH_ALL = 1;
// @ts-ignore
Buffer.prototype.toJSON = function () {
return `Buffer:${this.toString('hex')}`;
}
/**
* @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);
const send = transport.send.bind(transport);
transport.send = async (cla, ins, p1, p2, data) => {
const buffer = JSON.stringify(data);
const s0 = [cla, ins, p1, p2].map((x) => `0x${x.toString(16).toUpperCase()}`.padStart(6));
const s = [...s0, buffer];
// const message = `await ledger.sendArgs(${s});`;
debug(s.join(' '));
const result = await send(cla, ins, p1, p2, data);
return result;
}
const splitT = btc.splitTransaction.bind(btc);
btc.splitTransaction = (...args) => {
const result = splitT(...args);
return result;
};
const signp2sh = btc.signP2SHTransaction.bind(btc);
btc.signP2SHTransaction = async (...args) => {
console.log('SIGNP2SH START');
const result = await signp2sh(...args);
console.log('SIGNP2SH END')
return result;
}
return btc;
}
const signTransaction = async() => {
const ledger = await appBtc();
const paths = [ `m/49'/1'/0'/0/0` ];
const [ path ] = paths;
const previousTx = '0200000000010137e7d95a7602f9f3439e6cad26f05ce4106aa885d984294b2180e007c9e8b9980000000017160014ddcb8818df2ddd4efd5de0f12f86c66398110bfafeffffff02d1ccf0080000000017a914b819a1ae0def202339a6be575994f70a528965298700c2eb0b0000000017a914336caa13e08b96080a32b5d818d59b4ab3b36742870247304402207200b23733467bb1d91b23fc8283cd68101f3e9a988bfa391e968577e825ab420220182f154e335f7f24c1d986b84f685c26b5178e537f2ef3b67af36577202a74300121023694cc0c5dd5554d8a4bb6722583e8fb4ad2ae4a4910d29d636c9c9cdf8b15ebc3010000';
const utxo = bitcoin.Transaction.fromHex(previousTx);
const txIndex = 1;
// const seed = await bip39.mnemonicToSeed(mnemonics);
// const root = bitcoin.bip32.fromSeed(seed, NETWORK);
// const child = root.derivePath(path);
const fromLedger = await ledger.getWalletPublicKey(path, { format: 'p2sh' });
const pubkey = Buffer.from(compressPublicKey(fromLedger.publicKey), 'hex');
const p2wpkh = bitcoin.payments.p2wpkh({ pubkey, network: NETWORK });
debug(`${p2wpkh.name}: ${p2wpkh.output.toString('hex')}`);
const p2pkh = bitcoin.payments.p2pkh({ pubkey, network: NETWORK });
debug(`${p2pkh.name}: ${p2pkh.output.toString('hex')}`);
const p2sh = bitcoin.payments.p2sh({ redeem: p2wpkh, network: NETWORK });
debug(`${p2sh.name}: ${p2sh.output.toString('hex')}`);
const redeemScript = p2sh.redeem.output;
// const redeemScript = bitcoin.payments.p2pkh({ pubkey, network: NETWORK }).output;
const fee = 1000; // 1000 satoshi.
const amount = utxo.outs[txIndex].value - fee;
const version = 2; // doesn't really matter as long as it is consistent between the ledger and the psbt.
// ================ building psbt things ============
const psbt = new bitcoin.Psbt({ network: NETWORK });
psbt.addInput({
hash: utxo.getHash(),
index: txIndex,
nonWitnessUtxo: utxo.toBuffer(),
redeemScript,
});
// or alternatively:
// psbt.addInput({
// hash: utxo.getHash(),
// index: txIndex,
// redeemScript,
// witnessUtxo: {
// script: p2sh.output,
// value: utxo.outs[txIndex].value,
// },
// });
psbt.addOutput({
address: p2sh.address,
value: amount,
})
psbt.setVersion(version);
psbt.setMaximumFeeRate(100);
// ========= generating parameter for ledgers ===========
/** @type {bitcoin.Transaction} */
// @ts-ignore
const newTx = psbt.__CACHE.__TX;
const inLedgerTx = ledger.splitTransaction(utxo.toHex(), utxo.hasWitnesses());
const newTxIsSegwit = true;
const splitNewTx = await ledger.splitTransaction(newTx.toHex(), newTxIsSegwit);
const outputScriptHex = await serializer.serializeTransactionOutputs(splitNewTx).toString("hex");
// const txHex = await ledger.createPaymentTransactionNew({
// inputs: [[inLedgerTx, txIndex]],
// associatedKeysets: [ path ],
// outputScriptHex,
// lockTime: LOCK_TIME,
// segwit: newTxIsSegwit,
// transactionVersion: version,
// sigHashType: SIGHASH_ALL,
// });
// debug(txHex);
/** @param {string} path */
const generateSigner = async (path) => {
const { publicKey: uncompressedPublicKey } = await ledger.getWalletPublicKey(path);
const publicKey = compressPublicKey(uncompressedPublicKey);
return {
network: NETWORK,
publicKey: Buffer.from(publicKey, 'hex'),
/** @param {Buffer} $hash */
sign: async ($hash) => {
debug('signing with ledger');
const ledgerTxSignatures = await ledger.signP2SHTransaction({
inputs: [[
inLedgerTx,
txIndex,
p2pkh.output.toString('hex'),
// dont ask me why p2pkh.
// but I assume in ledger, p2pkh + (segwit = true) === p2wpkh
// while in the other p2pkh + (segwit = false) === p2pkh.
]],
associatedKeysets: [ path ],
outputScriptHex,
lockTime: LOCK_TIME,
segwit: newTxIsSegwit,
transactionVersion: version,
sigHashType: SIGHASH_ALL,
});
console.log({ ledgerTxSignatures });
/** @type {string[]} */
const [ ledgerSignature ] = ledgerTxSignatures;
const encodedSignature = (() => {
if (newTxIsSegwit) { return Buffer.from(ledgerSignature, 'hex'); }
return Buffer.concat([
Buffer.from(ledgerSignature, 'hex'),
Buffer.from('01', 'hex'), // SIGHASH_ALL
])
})();
const decoded = bitcoin.script.signature.decode(encodedSignature);
return decoded.signature;
},
};
}
const signer = await generateSigner(path);
await psbt.signInputAsync(0, signer);
const validate = await psbt.validateSignaturesOfAllInputs();
await psbt.finalizeAllInputs();
const hex = psbt.extractTransaction().toHex();
debug(`validate: ${validate}`);
debug(`${hex}`);
};
if (process.argv[1] === __filename) {
signTransaction().catch(console.error)
}
module.exports = { signTransaction };
@SiestaMadokaist sorry for the noob question, but in your example, where does the $hash belong? I don't see it being used while the OP's example uses it const expectedSignature = ecPrivate.sign($hash);
$hash is not defined anywhere, it is a function parameter.
bitcoinjs-lib will be the one who provide an argument for that parameter.
hm...
somewhere in bitcoin jslib, there's some code that more or less looks like.
class Psbt {
// the signer here is the result of generateSigner(path);
signInputAsync(index, signer){
const hash = someBitcoinPSBTMagic(index);
return signer.sign(hash);
}
}
i'm not sure how to explain it better, but maybe, you'll need to familiarize yourself with OOP / higher-order-function.
@SiestaMadokaist sorry for my lack of clarity in the question, to elaborate my question and to add more meat to it, what you are doing is basically "overriding" the Signer interface that Bitcoinjs-lib uses. but with a sign() method that requests a signature from Ledger via await ledger.signP2SHTransaction() instead of using ECPairs (ignoring that this will technically this will return a Promis<Buffer> instead of a Buffer, but that's unrelated).
My question here is, as you can see here, https://github.com/bitcoinjs/bitcoinjs-lib/blob/master/types/ecpair.d.ts#L10 the sign method must take a hash: Buffer as it's parameter, but the overridden .sign() method using Ledger will create a new UTXO from scratch so the hash is not required at all. Why would you just leave the $hash hanging, or not use something like https://github.com/LedgerHQ/ledgerjs/blob/master/packages/hw-app-btc/src/startUntrustedHashTransactionInput.js#L7 which (I think) can sign the hash directly instead of .signP2SHTransaction() or create a signature from the hash? I'm a bit new to this so sorry if my question was irrelevant
Eeeh, then probably me being noobs, but I have never used startUntrustedHashTransactionInput since it is not mentioned in ledger's documentation.
you could try it and see if it works, maybe it'll make it easier if I needs to works with ledger again :3.
yeah, In the last code I write, it is not used, I just leave it there in case I need to compare the signature generated from ledger and ecPrivate again.
@SiestaMadokaist ah okay, that explains, thank you.
I've added Type Definitions for startUntrustedHashTransactionInput and startUntrustedHashTransactionInputRaw https://github.com/DefinitelyTyped/DefinitelyTyped/pull/46566 because no one should be forced to use JavaScript ;) It looks like they merged it today. Hope this helps.
hmm... this might be unrelated,
but for security concerns, I don't think it is a good idea to use startUntrustedHashTransaction.
I could be wrong, but I think it is dangerous if your ledger doesn't show these kind of images to the user.
(sorry, I'm quite lazy to scaledown the image).



with startUntrustedHashTransaction, I don't think it'll show who do you send and how much, since you're only passing the hash.
for example, if someone make a phising website, in the website it is shown that the transaction goes to his designated address, but inside the ledger, what he sign is actually a transaction to the attacker address.
I agree with you 100%, though there are cases where you have to work with existing infrastructure and you don’t have the luxury to start from scratch. “Unsafe TX is slightly better than no TX” -my boss 2020
ahaha, alright then, as long as you know what you're doing.
I would also discourage using startUntrustedHashTransaction, but rather I would do the following inside the sign(hash) method.
If your app is compromised or your bitcoinjs-lib version is swapped with a malicious version, they could literally make your Ledger sign anything.
As long as you understand and are willing to take on that risk, startUntrustedHashTransaction is an option.
@SiestaMadokaist @junderw So, after this thread, I tried implementing things for my web app and I'm starting to see some problems... It's very related to this issue. I wonder if you can help me https://github.com/LedgerHQ/ledgerjs/issues/521
Sorry for the mess, but I wasn't sure if I should post the issue on this repo or on Ledgerjs, so I kindly ask for your review.
Most helpful comment
@lacksfish
you could just easily obtain more or less the same result by just using ledger.createPaymentTransactionNew (so you dont need to mix between 2 libraries ledger and bitcoinjs-lib) like so:
or, if you want ledger just to sign your tx, (not building the whole tx).
p.s: (run it with
DEBUG=ledger node <thisscript.js>)