bitcoinjs / bitcoinjs-lib

A javascript Bitcoin library for node.js and browsers.
MIT License
5.7k stars 2.11k forks source link

How to use an unsigned transaction with PSBT to create a digital signature? #1620

Closed codemaster101 closed 1 year ago

codemaster101 commented 4 years ago

Hi, I was going through some code to create transactions for bitcoin and use a serialized hash of the unsigned transaction to create a ECDSA digital signature and use that to sign the actual transaction but I think I am a little confused here as I have not been able to find any examples of doing so.

I have created a new address say: bc1qjkg99q654997uz5lxanjugzz2hfwum7ph9fcvw, with the compressed public key as 039b219ff489f9f5d3c674602e2280cc803f068f07db7295c7d2a2f9d51844cc45 (in hexadecimal representation).

Script used is P2WPKH. Say I want to transfer funds to 3HmEiQfaghizrNNn3tt5ydzvejLeYMzbD1, with some amount of BTC. Now I want to create an unsigned transaction so that I can generate a digital signature and then sign the transaction.

This is how I proceeded: First I created a payment object like so:

function createPayment(type, keys) {
        const network = bitcoin.networks.bitcoin;
        const payment = (bitcoin.payments)[type]({
                pubkey: keys[0].publicKey,
                network,
        });
        return {
                payment,
                keys,
        };
}
.
.
.
keys = [{ publicKey: Buffer.from('039b219ff489f9f5d3c674602e2280cc803f068f07db7295c7d2a2f9d51844cc45', 'hex') }];
const p2wpkh = createPayment('p2wpkh', keys);

Now I maybe going wrong here...

I then proceeded to create an input data object like so (maybe I am missing scriptPubKey)?!

        const inputData = {
                address: 'bc1qjkg99q654997uz5lxanjugzz2hfwum7ph9fcvw',
                hash: '07785f191c06b8805dec6ddd035bfd98fdd9fb87c6ad6e0b0d8349b984215170',
                index: 0
        };

I then proceed to do something like

        const psbt = new bitcoin.Psbt({ network: bitcoin.networks.bitcoin })
                .addInput(inputData)
                .addOutput({
                        address: '3HmEiQfaghizrNNn3tt5ydzvejLeYMzbD1',
                        value: 2e4,
                })

Can someone guide me on how to proceed or where to change the logic?

Also if I understand correctly all unspent inputs (if I have multiple) would need to be signed separately, so do digital signatures need to be created separately and then sign the inputs one by one? How to create a hash of the an unsigned transaction from PSBT?

Also please tell me if this does not make sense at all. I think I might be a little confused on how I should use digital signatures here.

itsMikeLowrey commented 4 years ago

Does this code give you any errors or are you looking for the next steps in how to sign?

ArnaudBrousseau commented 1 year ago

In case it's useful to anybody out there, I think I managed to get something working which does what @codemaster101 was asking about.

The trick is to implement SignerAsync. This type has 2 fields:

With the SignerAsync interface implemented, you can then call pbst.signInputAsync(inputIndex, yourAsyncSigner), and your own sign method will be called.

In the following snippet I'm constructing a transaction, then implementing sign as an async prompt to get the signature from stdin and combine it.

const bitcoin = require("bitcoinjs-lib");
const prompts = require("prompts");
const ecc = require("tiny-secp256k1");
const ecpair = require("ecpair");
const fetch = require("node-fetch");

async function run() {
  const hash = "6a94d6b2d27a3df8036ecf713af6418ef9e8bf5daa7ee170fa74d67a07d4ffae";
  const index = 0;
  const amount = 501;
  const publicKey = "040fc05389d5f98143cb037f7707e008f8154a5951b106c141e80af36184ca88c8e951bdfe7ee7f0e50afd42ab2f120ed1fbaaa5f8e5a528bdfabbd63276349da6";
  const destination = "2Mv28PpCuEynr6rU9rqNJ5VW3znGZFfAU7Y";

  // This is needed to derive the compressed format of the public key
  const ECPair = ecpair.ECPairFactory(ecc);
  const pair = ECPair.fromPublicKey(Buffer.from(publicKey, "hex"));

  // Get the transaction info from blockcypher API
  let resp = await fetch(
    `https://api.blockcypher.com/v1/btc/test3/txs/${hash}?limit=50&includeHex=true`
  );
  let respJson = await resp.json();

  const pbst = new bitcoin.Psbt({ network: bitcoin.networks.testnet });
  pbst.addInput({
    hash: hash,
    index: index,
    nonWitnessUtxo: Buffer.from(respJson.hex, "hex"),
    redeemScript: bitcoin.payments.p2sh({
      redeem: bitcoin.payments.p2wpkh({
        pubkey: pair.publicKey,
        network: bitcoin.networks.testnet,
      }),
    }).redeem.output,
  });

  pbst.addOutput({
    script: bitcoin.address.toOutputScript(
      destination,
      bitcoin.networks.testnet
    ),
    value: amount,
  });

  // This is really the crux of it!
  const myAsyncSigner = {
    publicKey: pair.publicKey,
    sign: (hash, lowerR) => {
      return new Promise((resolve, rejects) => {
        const signPrompt = [
          {
            type: "confirm",
            name: "sigPrompt",
            message: `Please go sign the following hash: ${hash.toString(
              "hex"
            )}\nReady to continue?`,
          },
          {
            type: "text",
            name: "sig",
            message: "sig (R + S, hex-encoded):",
          },
        ];
        prompts(signPrompt)
          .then(function (vals) {
            let signatureBuf = Buffer.from(vals.sig, "hex");
            resolve(signatureBuf);
          })
          .catch(function (reason) {
            rejects(reason);
          });
      });
    },
  });

  await pbst.signInputAsync(0, myAsyncSigner);
  pbst.finalizeAllInputs();
  return pbst.extractTransaction().toHex();
}

run().then((res) => console.log(res));

Executing the script above I get:

$ node demo.js         
✔ Please go sign the following hash: c5fd0d738920d4715e25d31432dfb72531d6a6dbc8c3d6b1ef2132249b4c5ead
Ready to continue? … yes
✔ sig (R + S, hex-encoded): … 683c75e1aa6c55a2f108693e7f04d183eeafe6be88b61dc3901b9e02479cee3943739ad0ab3e3d0f7ce1ad7fc686d506f07c035b7078c1899c09da2922437a34

02000000000101aeffd4077ad674fa70e17eaa5dbfe8f98e41f63a71cf6e03f83d7ad2b2d6946a00000000171600140be1390164efb9ff52f51e50269aabc646c8d0f7ffffffff01f50100000000000017a9141e6e4cca6c1e388e775412acc2dff967b510c40f87024730440220683c75e1aa6c55a2f108693e7f04d183eeafe6be88b61dc3901b9e02479cee39022043739ad0ab3e3d0f7ce1ad7fc686d506f07c035b7078c1899c09da2922437a340121020fc05389d5f98143cb037f7707e008f8154a5951b106c141e80af36184ca88c800000000

The signature that I fed into my script (683c...7a34) was obtained on a remote signer which does bare ECDSA signing, somewhere else.

junderw commented 1 year ago

This is a great explanation of the SignerAsync interface!

I think this answers OPs question, so closing.