【问题标题】:how to sign bitcoin psbt with ledger?如何用分类帐签署比特币 psbt?
【发布时间】: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)
}


【问题讨论】:

    标签: javascript typescript blockchain bitcoin bitcoinlib


    【解决方案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,但上面的代码只是我从上面提到的参考中复制粘贴代码,因为它似乎原始代码应该在浏览器中运行。
      猜你喜欢
      • 1970-01-01
      • 1970-01-01
      • 2020-05-31
      • 1970-01-01
      • 1970-01-01
      • 2018-08-23
      • 2019-09-07
      • 2013-11-29
      • 1970-01-01
      相关资源
      最近更新 更多