Bitcoin

Hash Time Lock Contract – How to sign P2WSH PSBT with sats-connect?

I created a funded P2WSH script that is HTLC. Now I want to generate PSBT to sign in another wallet to withdraw funds.

The problem is that when signing PSBT I get the following bitcoinlib-js error: Input script doesn't have pubKey. I’m not sure why or how to properly add this public key to PSBT.

Below is a detailed breakdown of the steps you take to create, fund, and (or fail to) redeem an HTLC.

Address: tb1pnz86gezf5dzmja9q86z5agnmgjj29f00nxjj9jx0fsss6kkyh03sjkqhpd
Public Key: 59fff87c1bb3f75d34ea9d1588b72d0df43540695671c7a5ad3ec6a71d44bd79

Script to generate HTLC:

import bitcoin from 'bitcoinjs-lib';
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'
);

This successfully creates a P2WSH transaction and provides the following output to the screen:

Created an HTLC Script!
-------------------------------------------------
P2WSH Bitcoin Deposit Address for HTLC: tb1q5cyw034kcvsp6fsfx9skeq93vhvuqghmjc9y8xdhjll8m9thrn9q5mv0nr
Witness Script Hex: 63a820652c7dc687d98c9889304ed2e408c74b611e86a40caa51c4b43f1dd5913c5cd08876a914e399056c4ca63571aca44fc2d11b3fdac69a37e06751b17576a914e399056c4ca63571aca44fc2d11b3fdac69a37e06888ac
Redeem Block Number: 1
Secret (for spending): mysecret
SHA256(Secret) (for HTLC creation): 652c7dc687d98c9889304ed2e408c74b611e86a40caa51c4b43f1dd5913c5cd0
ScriptPubKey Hex: 0020a608e7c6b6c3201d260931616c80b165d9c022fb960a4399b797fe7d95771cca
-------------------------------------------------

You then fund your script by sending bitcoin directly through xverse.

tb1q5cyw034kcvsp6fsfx9skeq93vhvuqghmjc9y8xdhjll8m9thrn9q5mv0nr

https://mempool.space/testnet/tx/be9cc0e300d1c01b7fdbeeff1c99acc0fb8a7d9e8d025547b7bfc9635dedcbb3

The ScriptPubKey in mempool.space seems to match mine.

0020a608e7c6b6c3201d260931616c80b165d9c022fb960a4399b797fe7d95771cca

Then it’s time to create the PSBT. I can’t immediately see any issues with how this PSBT is created. Do I need to add my public key somewhere?:

import * as bitcoin from 'bitcoinjs-lib';
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 provides the following PSBT:

cHNidP8BAF4CAAAAAbPL7V1jyb+3R1UCjZ59ivvArJkc/+7bfxvA0QDjwJy+AAAAAAD+////AQAAAAAAAAAAIlEgmI+kZEmjRbl0oD6FTqJ7RKSipe+ZpSLIz0whDVrEu+MAAAAAAAEBK+gDAAAAAAAAIgAgpgjnxrbDIB0mCTFhbICxZdnAIvuWCkOZt5f+fZV3HMoBBVljqCBlLH3Gh9mMmIkwTtLkCMdLYR6GpAyqUcS0Px3VkTxc0Ih2qRTjmQVsTKY1caykT8LRGz/axpo34GcAsXV2qRTjmQVsTKY1caykT8LRGz/axpo34GiIrAAA

When we decode it so we can inspect its contents, we get the following result:


  "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

If you look at the inputs section of PSBT, you can see that there are some inputs in there, including my HTLC funds and scriptPubKey. Public key does not exist causing error?

From there I tried signing using sats-connect anyway.

const signPsbtOptions = 
      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 met the following error

Input script doesn't have pubKey

my question

Why doesn’t the input script have a public key and isn’t that the purpose of scriptPubKey? How do I properly provide the public key to sign this psbt?

Related Articles

Back to top button