bitcoinjs-lib
bitcoinjs-lib copied to clipboard
How to use an unsigned transaction with PSBT to create a digital signature?
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.
Does this code give you any errors or are you looking for the next steps in how to sign?
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:
publicKey: the public key bytes signing tx inputsfunction sign(hash, lowerR): callback being fed thehashvalue to sign and alowerRboolean. Not sure what this boolean is used for tbh, in my experience it was always set toundefined🤷
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.
This is a great explanation of the SignerAsync interface!
I think this answers OPs question, so closing.