如何用分类帐签署比特币 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?的主要内容,如果未能解决你的问题,请参考以下文章

如何用Express.js & Vue.js 创建一个用比特币支付的在线商店!

比特币的发行机制

生成比特币钱包地址java示例(动态生成)

关于比特币和区块链的3件重要事项

什么是比特币

比特币这么火热,看看这篇比特币初学者指南