Author

Topic: How to sign P2WSH PSBT with sats-connect? (Read 74 times)

jr. member
Activity: 244
Merit: 1
April 12, 2024, 10:22:27 AM
#1
I have created a funded P2WSH script, which is an HTLC. I now want to create a PSBT which will be signed by another wallet to withdraw the funds.

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:

Code:
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 the P2WSH transaction and gives the following output to the screen

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?:

Code:
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 gives me the following PSBT:

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

When I decode it, so I can inspect the contents, I get the following

Code:
{
  "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:

Code:
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 was met with the following error

Code:
Input script doesn't have pubKey

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?
Jump to: