如何用分类帐签署比特币 psbt?
Posted
技术标签:
【中文标题】如何用分类帐签署比特币 psbt?【英文标题】:how to sign bitcoin psbt with ledger? 【发布时间】:2020-03-23 18:07:31 【问题描述】:我正在尝试按照我在此处找到的内容从 bitcoinjs-lib 签署 Psbt 交易:
https://github.com/helperbit/helperbit-wallet/blob/master/app/components/dashboard.wallet/bitcoin.service/ledger.ts
我已经检查了来自分类帐的压缩公钥和来自 bitcoinjsLib 的压缩公钥返回了相同的值。
我可以使用 bitcoinjs-lib ECPair 对其进行签名,但是当我尝试使用 ledger 对其进行签名时,它总是无效。
谁能帮我指出我哪里做错了?
下面的代码中已经提到了这些变量,但为了清楚起见:
- mnemonics:
abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon about
- previousTx:
02000000000101869362410c61a69ab9390b2167d08219662196e869626e8b0350f1a8e4075efb0100000017160014ef3fdddccdb6b53e6dd1f5a97299a6ba2e1c11c3ffffffff0240420f000000000017a914f748afee815f78f97672be5a9840056d8ed77f4887df9de6050000000017a9142ff4aa6ffa987335c7bdba58ef4cbfecbe9e49938702473044022061a01bf0fbac4650a9b3d035b3d9282255a5c6040aa1d04fd9b6b52ed9f4d20a022064e8e2739ef532e6b2cb461321dd20f5a5d63cf34da3056c428475d42c9aff870121025fb5240daab4cee5fa097eef475f3f2e004f7be702c421b6607d8afea1affa9b00000000
- paths:
["0'/0/0"]
- redeemScript: (non-multisig segwit)
00144328adace54072cd069abf108f97cf80420b212b
这是我拥有的最小可重现代码。
/* 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)
【问题讨论】:
【参考方案1】:哎呀,终于搞定了。
我的错误是我试图签署 p2sh-p2ms,通过参考如何签署 p2sh-p2wsh-p2ms。
而且,当我尝试解码签名时,我认为代表 SIGHASH_ALL 的最后 2 位 (01) 缺失会导致错误。
这是我最终确定的工作示例。
// @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)
【讨论】:
【参考方案2】:我猜你在传递给toByteArray
函数的字符串中有一个空格。此功能不修剪输入。也不检查输入的长度是否是偶数。
【讨论】:
嗯...我不认为这是问题所在,我尝试使用标准Buffer.from
,但上面的代码只是我从上面提到的参考中复制粘贴代码,因为它似乎原始代码应该在浏览器中运行。以上是关于如何用分类帐签署比特币 psbt?的主要内容,如果未能解决你的问题,请参考以下文章