The problem is that the PSBT signing is throwing the following bitcoinlib-js error: Input script doesn't have pubKey. I am not sure why, or how to add this pubkey to the PSBT correctly.
The following is a details breakdown of the steps I am taking to create, fund and (failing to) redeem the HTLC.
Address: tb1pnz86gezf5dzmja9q86z5agnmgjj29f00nxjj9jx0fsss6kkyh03sjkqhpd
Public Key: 59fff87c1bb3f75d34ea9d1588b72d0df43540695671c7a5ad3ec6a71d44bd79
Script to create the HTLC:
import crypto from 'crypto';
function createHTLC(secret, lockduration, recipientPubKey, senderPubKey, networkType) {
const network = networkType === 'testnet' ? bitcoin.networks.testnet : bitcoin.networks.bitcoin;
const secretHash = crypto.createHash('sha256').update(Buffer.from(secret, 'utf-8')).digest();
const recipientHash = bitcoin.crypto.hash160(Buffer.from(recipientPubKey, 'hex'));
const senderHash = bitcoin.crypto.hash160(Buffer.from(senderPubKey, 'hex'));
const redeemScript = bitcoin.script.compile([
bitcoin.opcodes.OP_IF,
bitcoin.opcodes.OP_SHA256,
secretHash,
bitcoin.opcodes.OP_EQUALVERIFY,
bitcoin.opcodes.OP_DUP,
bitcoin.opcodes.OP_HASH160,
recipientHash, // Hashed recipient public key
bitcoin.opcodes.OP_ELSE,
bitcoin.script.number.encode(lockduration),
bitcoin.opcodes.OP_CHECKLOCKTIMEVERIFY,
bitcoin.opcodes.OP_DROP,
bitcoin.opcodes.OP_DUP,
bitcoin.opcodes.OP_HASH160,
senderHash, // Hashed sender public key
bitcoin.opcodes.OP_ENDIF,
bitcoin.opcodes.OP_EQUALVERIFY,
bitcoin.opcodes.OP_CHECKSIG,
]);
// Calculate the P2WSH address and scriptPubKey
const redeemScriptHash = bitcoin.crypto.sha256(redeemScript);
const scriptPubKey = bitcoin.script.compile([
bitcoin.opcodes.OP_0, // Witness version 0
redeemScriptHash
]);
const p2wshAddress = bitcoin.payments.p2wsh({
redeem: { output: redeemScript, network },
network
}).address;
console.log('\nCreated an HTLC Script!');
console.log('-------------------------------------------------');
console.log('P2WSH Bitcoin Deposit Address for HTLC:', p2wshAddress);
console.log('Witness Script Hex:', redeemScript.toString('hex'));
console.log('Redeem Block Number:', lockduration);
console.log('Secret (for spending):', secret);
console.log('SHA256(Secret) (for HTLC creation):', secretHash.toString('hex'));
console.log('ScriptPubKey Hex:', scriptPubKey.toString('hex'));
console.log('-------------------------------------------------');
// To fund the HTLC, send BTC to the p2wsh.address
// Redeeming the HTLC would involve creating a transaction that spends from this address
// using the provided witnessScript, which would be included in the transaction's witness field
}
// Example usage
createHTLC(
'mysecret',
1, // locktime in blocks
"59fff87c1bb3f75d34ea9d1588b72d0df43540695671c7a5ad3ec6a71d44bd79",
"59fff87c1bb3f75d34ea9d1588b72d0df43540695671c7a5ad3ec6a71d44bd79",
'testnet'
);
Created an HTLC Script!
-------------------------------------------------
P2WSH Bitcoin Deposit Address for HTLC: tb1q5cyw034kcvsp6fsfx9skeq93vhvuqghmjc9y8xdhjll8m9thrn9q5mv0nr
Witness Script Hex: 63a820652c7dc687d98c9889304ed2e408c74b611e86a40caa51c4b43f1dd5913c5cd08876a914e 399056c4ca63571aca44fc2d11b3fdac69a37e06751b17576a914e399056c4ca63571aca44fc2d1 1b3fdac69a37e06888ac
Redeem Block Number: 1
Secret (for spending): mysecret
SHA256(Secret) (for HTLC creation): 652c7dc687d98c9889304ed2e408c74b611e86a40caa51c4b43f1dd5913c5cd0
ScriptPubKey Hex: 0020a608e7c6b6c3201d260931616c80b165d9c022fb960a4399b797fe7d95771cca
-------------------------------------------------
Then I fund the script via xverse by sending bitcoin directly to
https://mempool.space/testnet/tx/be9cc0e300d1c01b7fdbeeff1c99acc0fb8a7d9e8d025547b7bfc9635dedcbb3
The ScriptPubKey on mempool.space seems to match mine
0020a608e7c6b6c3201d260931616c80b165d9c022fb960a4399b797fe7d95771cca
Then it's time to create the PSBT. I can't immediately see any issues with how I am creating this PSBT. Do I need to add the public key somewhere?:
import crypto from 'crypto';
import * as tinysecp256k1 from 'tiny-secp256k1';
// Initialize ECC library
import * as bitcoinjs from "bitcoinjs-lib";
import * as ecc from "tiny-secp256k1";
bitcoin.initEccLib(ecc);
function createSpendPSBT(secret, lockduration, scriptPubKeyHex, htlcTxId, htlcOutputIndex, refundAmount, recipientPubKey, senderPubKey, recipientAddress, networkType) {
const network = networkType === 'testnet' ? bitcoin.networks.testnet : bitcoin.networks.bitcoin;
const secretHash = crypto.createHash('sha256').update(Buffer.from(secret, 'utf-8')).digest();
// Recreate the HTLC script using the provided secret
const recipientHash = bitcoin.crypto.hash160(Buffer.from(recipientPubKey, 'hex'));
const senderHash = bitcoin.crypto.hash160(Buffer.from(senderPubKey, 'hex'));
const redeemScript = bitcoin.script.compile([
bitcoin.opcodes.OP_IF,
bitcoin.opcodes.OP_SHA256,
secretHash,
bitcoin.opcodes.OP_EQUALVERIFY,
bitcoin.opcodes.OP_DUP,
bitcoin.opcodes.OP_HASH160,
recipientHash, // Hashed recipient public key
bitcoin.opcodes.OP_ELSE,
bitcoin.script.number.encode(lockduration),
bitcoin.opcodes.OP_CHECKLOCKTIMEVERIFY,
bitcoin.opcodes.OP_DROP,
bitcoin.opcodes.OP_DUP,
bitcoin.opcodes.OP_HASH160,
senderHash, // Hashed sender public key
bitcoin.opcodes.OP_ENDIF,
bitcoin.opcodes.OP_EQUALVERIFY,
bitcoin.opcodes.OP_CHECKSIG,
]);
const scriptPubKey = Buffer.from(scriptPubKeyHex, 'hex');
console.log("Creating PSBT");
// Create a PSBT
const psbt = new bitcoin.Psbt({ network: network })
.addInput({
hash: htlcTxId,
index: htlcOutputIndex,
sequence: 0xfffffffe, // Necessary for OP_CHECKLOCKTIMEVERIFY
witnessUtxo: {
script: scriptPubKey,
value: refundAmount,
},
witnessScript: redeemScript,
})
.addOutput({
address: recipientAddress,
value: refundAmount - 1000, // Subtract a nominal fee
})
.setVersion(2)
.setLocktime(lockduration);
console.log("PSBT to be signed:", psbt.toBase64());
}
// Example usage (Fill in the actual values)
createSpendPSBT(
"mysecret",
0,
"0020a608e7c6b6c3201d260931616c80b165d9c022fb960a4399b797fe7d95771cca",
"be9cc0e300d1c01b7fdbeeff1c99acc0fb8a7d9e8d025547b7bfc9635dedcbb3",
0,
1000,
"59fff87c1bb3f75d34ea9d1588b72d0df43540695671c7a5ad3ec6a71d44bd79",
"59fff87c1bb3f75d34ea9d1588b72d0df43540695671c7a5ad3ec6a71d44bd79",
"tb1pnz86gezf5dzmja9q86z5agnmgjj29f00nxjj9jx0fsss6kkyh03sjkqhpd",
"testnet",
);
//createSpendPSBT(secret, lockduration, scriptPubKey, htlcTxId, htlcOutputIndex, refundAmount, recipientPubKey, senderPubKey, recipientAddress, networkType)
}
This gives me the following PSBT:
When I decode it, so I can inspect the contents, I get the following
"tx": {
"txid": "a1eaefe490f5d3be11fbd6a5afeffcff20a9e92cfde3363484168c9f5769c57a",
"hash": "a1eaefe490f5d3be11fbd6a5afeffcff20a9e92cfde3363484168c9f5769c57a",
"version": 2,
"size": 94,
"vsize": 94,
"weight": 376,
"locktime": 0,
"vin": [
{
"txid": "be9cc0e300d1c01b7fdbeeff1c99acc0fb8a7d9e8d025547b7bfc9635dedcbb3",
"vout": 0,
"scriptSig": {
"asm": "",
"hex": ""
},
"sequence": 4294967294
}
],
"vout": [
{
"value": 0.00000000,
"n": 0,
"scriptPubKey": {
"asm": "1 988fa46449a345b974a03e854ea27b44a4a2a5ef99a522c8cf4c210d5ac4bbe3",
"desc": "rawtr(988fa46449a345b974a03e854ea27b44a4a2a5ef99a522c8cf4c210d5ac4bbe3)#4xpnet5r",
"hex": "5120988fa46449a345b974a03e854ea27b44a4a2a5ef99a522c8cf4c210d5ac4bbe3",
"address": "tb1pnz86gezf5dzmja9q86z5agnmgjj29f00nxjj9jx0fsss6kkyh03sjkqhpd",
"type": "witness_v1_taproot"
}
}
]
},
"global_xpubs": [
],
"psbt_version": 0,
"proprietary": [
],
"unknown": {
},
"inputs": [
{
"witness_utxo": {
"amount": 0.00001000,
"scriptPubKey": {
"asm": "0 a608e7c6b6c3201d260931616c80b165d9c022fb960a4399b797fe7d95771cca",
"desc": "addr(tb1q5cyw034kcvsp6fsfx9skeq93vhvuqghmjc9y8xdhjll8m9thrn9q5mv0nr)#wjcfmgw8",
"hex": "0020a608e7c6b6c3201d260931616c80b165d9c022fb960a4399b797fe7d95771cca",
"address": "tb1q5cyw034kcvsp6fsfx9skeq93vhvuqghmjc9y8xdhjll8m9thrn9q5mv0nr",
"type": "witness_v0_scripthash"
}
},
"witness_script": {
"asm": "OP_IF OP_SHA256 652c7dc687d98c9889304ed2e408c74b611e86a40caa51c4b43f1dd5913c5cd0 OP_EQUALVERIFY OP_DUP OP_HASH160 e399056c4ca63571aca44fc2d11b3fdac69a37e0 OP_ELSE 0 OP_CHECKLOCKTIMEVERIFY OP_DROP OP_DUP OP_HASH160 e399056c4ca63571aca44fc2d11b3fdac69a37e0 OP_ENDIF OP_EQUALVERIFY OP_CHECKSIG",
"hex": "63a820652c7dc687d98c9889304ed2e408c74b611e86a40caa51c4b43f1dd5913c5cd08876a914e399056c4ca63571aca44fc2d11b3fdac69a37e06700b17576a914e399056c4ca63571aca44fc2d11b3fdac69a37e06888ac",
"type": "nonstandard"
}
}
],
"outputs": [
{
}
],
"fee": 0.00001000
}
When I look at the inputs part of the PSBT, I can see that there is some input in there with my HTLC funds, and a scriptPubKey. Is this not the public key that the error is complaining doesn't exist?
From there I tried to sign it anyway using sats-connect:
payload: {
network: {
type: 'Testnet' // Change to 'Regtest' or 'Mainnet' as necessary
},
psbtBase64: `cHNidP8BAF4CAAAAAbPL7V1jyb+3R1UCjZ59ivvArJkc/+7bfxvA0QDjwJy+AAAAAAD+////AQAAAAAAAAAAIlEgmI+kZEmjRbl0oD6FTqJ7RKSipe+ZpSLIz0whDVrEu+MAAAAAAAEBK+gDAAAAAAAAIgAgpgjnxrbDIB0mCTFhbICxZdnAIvuWCkOZt5f+fZV3HMoBBVljqCBlLH3Gh9mMmIkwTtLkCMdLYR6GpAyqUcS0Px3VkTxc0Ih2qRTjmQVsTKY1caykT8LRGz/axpo34GcAsXV2qRTjmQVsTKY1caykT8LRGz/axpo34GiIrAAA`,
broadcast: false, // Set to true if you want to broadcast after signing
inputsToSign: [
{
address: "tb1pnz86gezf5dzmja9q86z5agnmgjj29f00nxjj9jx0fsss6kkyh03sjkqhpd", //should this be the address of signer or the address of the input?
signingIndexes: [0] // Assuming you want to sign the first input
}
],
},
onFinish: (response) => {
console.log('Signed PSBT:', response.psbtBase64);
// Here, you could add additional code to handle the signed PSBT
},
onCancel: () => alert('Signing canceled'),
};
try {
await signTransaction(signPsbtOptions);
} catch (error) {
console.error('Error signing PSBT:', error);
alert('Failed to sign PSBT.');
}
and I was met with the following error
My Question
Why doesn't the input script have a public key, and isn't this the purpose of scriptPubKey? How do I provide the public key correctly to sign this psbt?