comment signer un bitcoin psbt avec ledger?

11

J'essaie de signer une transaction Psbt à partir de bitcoinjs-lib en suivant ce que j'ai trouvé ici:

https://github.com/helperbit/helperbit-wallet/blob/master/app/components/dashboard.wallet/bitcoin.service/ledger.ts

J'ai vérifié que la clé publique compressée du livre et celle de bitcoinjsLib ont renvoyé la même valeur.

Je pourrais le signer avec l'ECPair bitcoinjs-lib, mais lorsque j'essaie de le signer à l'aide du registre, il est toujours invalide.

Quelqu'un peut-il m'aider à indiquer où j'ai fait une erreur?

Ces variables sont déjà mentionnées dans le code ci-dessous, mais à des fins de clarté:

- mnemonics: 
abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon about

- previousTx:
02000000000101869362410c61a69ab9390b2167d08219662196e869626e8b0350f1a8e4075efb0100000017160014ef3fdddccdb6b53e6dd1f5a97299a6ba2e1c11c3ffffffff0240420f000000000017a914f748afee815f78f97672be5a9840056d8ed77f4887df9de6050000000017a9142ff4aa6ffa987335c7bdba58ef4cbfecbe9e49938702473044022061a01bf0fbac4650a9b3d035b3d9282255a5c6040aa1d04fd9b6b52ed9f4d20a022064e8e2739ef532e6b2cb461321dd20f5a5d63cf34da3056c428475d42c9aff870121025fb5240daab4cee5fa097eef475f3f2e004f7be702c421b6607d8afea1affa9b00000000

- paths:
["0'/0/0"]

- redeemScript: (non-multisig segwit)
00144328adace54072cd069abf108f97cf80420b212b

Ceci est mon code reproductible minimum que j'ai.

/* 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)
}

Ramadoka
la source

Réponses:

1

Ooof, a finalement réussi à le faire fonctionner.

Mon erreur était que j'essayais de signer un p2sh-p2ms, en suivant une référence sur la façon de signer un p2sh-p2wsh-p2ms.

Et, aussi, ce dernier 2 bits manquant (01), qui je pense représente SIGHASH_ALL a provoqué une erreur lorsque j'essaie de décoder la signature.

ceci est mon exemple de travail finalisé.

// @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)
}
Ramadoka
la source
0

Je suppose que vous avez un espace dans la chaîne passée à la toByteArrayfonction. Cette fonction ne coupe pas l'entrée. Ne vérifie pas non plus si la longueur de l'entrée est paire.

Xaqron
la source
Hmm ... je ne pense pas que ce soit le problème, j'ai essayé d'utiliser la norme Buffer.from, mais le code ci-dessus est juste moi copypasting le code de la référence que j'ai mentionnée ci-dessus car il semble que le code original soit censé fonctionner dans le navigateur.
Ramadoka