bitcoinjs / bitcoinjs-lib

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

How to combine Blockcypher /txs/new API to do RBF #2121

Closed liudonghao closed 2 months ago

liudonghao commented 2 months ago
[{
  "tx": {
    "block_height": -1,
    "block_index": -1,
    "hash": "14ff4e86fba559779f7da91a13d087a64452c60fc6eacc4240f90dec00368ef7",
    "addresses": [
      "tb1qe55hg9ffxs46l4lfy0amc6dv0j4qnadwsd7sp0",
      "tb1qvz7fualryakkz7j4y7teyvrtal75um8k7jf4zx"
    ],
    "total": 10,
    "fees": 9971,
    "size": 116,
    "vsize": 116,
    "preference": "medium",
    "relayed_by": "110.152.152.80",
    "received": "2024-07-01T08:15:11.933024628Z",
    "ver": 1,
    "double_spend": false,
    "vin_sz": 1,
    "vout_sz": 1,
    "confirmations": 0,
    "inputs": [
      {
        "prev_hash": "59857c959b0337286dc478f7df5cf2bd3479bd22af93faa34760dd75b0e752ea",
        "output_index": 1,
        "output_value": 9981,
        "sequence": 4294967295,
        "addresses": [
          "tb1qe55hg9ffxs46l4lfy0amc6dv0j4qnadwsd7sp0"
        ],
        "script_type": "pay-to-witness-pubkey-hash",
        "age": 2861239
      }
    ],
    "outputs": [
      {
        "value": 10,
        "script": "001460bc9e77e3276d617a55279792306beffd4e6cf6",
        "addresses": [
          "tb1qvz7fualryakkz7j4y7teyvrtal75um8k7jf4zx"
        ],
        "script_type": "pay-to-witness-pubkey-hash"
      }
    ]
  },
  "tosign": [
    "406b1cbf2155c374ca0b934e3b70d15244d85370a8604bd94c5506af93ed281b"
  ]
}]

The above is what you can get from the Blockcypher /txs/new API. But this API doesn't give a chance to set sequence, therefore I failed to do RBF using Blockcypher API alone. This forced me to bitcoinjs-lib, but I don't know how to use Tx.hashForSignature() function or other functions to do this, I tried, but the hash given by hashForSignature() does not match with tosign above.

junderw commented 2 months ago

I'm sorry. I don't know the specifics about their API.

Please look at the README in the examples section to learn how to send and manage coins using this library.

liudonghao commented 2 months ago

Detailed the problem I was facing in issue #2124

junderw commented 2 months ago

Please post details here. I have pasted your new issue below:


My goal is to achieve opt-in RBF by setting smaller sequence number in an input and sign offline.

Previously I was using Blockcypher /txs/new API to create a Tx skeleton which has a field "tosign" that contains hash of each input and these hashes can be signed offline, this is perfectly working in PROD.

Unfortunately, Blockcypher API does not support setting sequence number for inputs, that's why I moved to bitcoinjs-lib to try to set sequence number.

Therefore, I am trying to use bitcoinjs-lib to produce input hashes and check them against "tosign" in Blockcypher Tx skeleton without setting smaller sequence number first to make sure bitcoinjs-lib can give me correct input hashes in the first place, but failed, more than that, even bitcoin.Transaction and bitcoin.Psbt gave different answers.

Tx skeleton given by Blockcypher (irrelevant stuff is removed):

{
    "tx": {      
      "fees": 981,
      "vsize": 116,
      "inputs": [
        {
          "prev_hash": "59857c959b0337286dc478f7df5cf2bd3479bd22af93faa34760dd75b0e752ea",
          "output_index": 1,
          "output_value": 9981,
          "sequence": 4294967295,
          "addresses": [
            "tb1qe55hg9ffxs46l4lfy0amc6dv0j4qnadwsd7sp0"
          ],
          "script_type": "pay-to-witness-pubkey-hash",
          "age": 2861239
        }
      ],
      "outputs": [
        {
          "value": 10,
          "script": "001460bc9e77e3276d617a55279792306beffd4e6cf6",
          "addresses": [
            "tb1qvz7fualryakkz7j4y7teyvrtal75um8k7jf4zx"
          ],
          "script_type": "pay-to-witness-pubkey-hash"
        },
        {
          "value": 8990,
          "script": "0014cd29741529342bafd7e923fbbc69ac7caa09f5ae",
          "addresses": [
            "tb1qe55hg9ffxs46l4lfy0amc6dv0j4qnadwsd7sp0"
          ],
          "script_type": "pay-to-witness-pubkey-hash"
        }
      ]
    },
    "tosign": [
      "54d5181ba0f88b1854c28af7021847d67bf9ce4a8f8fef8e623d1d97946cfcca"
    ]
  }

My runnable code:

const prev_hash = '59857c959b0337286dc478f7df5cf2bd3479bd22af93faa34760dd75b0e752ea'
const sender = {
    address: 'tb1qe55hg9ffxs46l4lfy0amc6dv0j4qnadwsd7sp0',
    pubkey: '020ae4375d81c2c927ef0370932d67c886fb309a31e3b41816640b5bff6282b8fe'
}

const receiver = {
    address: 'tb1qvz7fualryakkz7j4y7teyvrtal75um8k7jf4zx',
    pubkey: '02491f1effe381dc9240e63b561f0bfa8743ff9767a6e6c646a0eeac3d0c6dfb69'
}

const create_scriptPubKey = (party) => {
    let payment = bitcoin.payments.p2wpkh({ pubkey: Buffer.from(party.pubkey, "hex"), network: bitcoin.networks.testnet })
    return payment.output
}

const use_Psbt = () => {
    const psbt = new bitcoin.Psbt({ network: bitcoin.networks.testnet, version: 1 })
    psbt.addOutput({
        address: receiver.address,
        value: 10
      });

    psbt.addOutput({
        address: sender.address,
        value: 8990
    });

    var input = {
        hash: '59857c959b0337286dc478f7df5cf2bd3479bd22af93faa34760dd75b0e752ea',
        index: 1,
        witnessUtxo: {
            script: create_scriptPubKey(sender),
            value: 9981
        }
    };

    psbt.addInput(input)

    const sighash = psbt.__CACHE.__TX.hashForWitnessV0(
        0,
        psbt.data.inputs[0].witnessUtxo.script,
        psbt.data.inputs[0].witnessUtxo.value,
        bitcoin.Transaction.SIGHASH_ALL
    )
    console.log(`vsize use_Psbt: ${psbt.__CACHE.__TX.virtualSize()}`)
    return sighash.toString("hex")
}

const use_Transaction = () => {
    let tx = new bitcoin.Transaction(bitcoin.networks.testnet)
    tx.addInput(Buffer.from(prev_hash, 'hex'), 1)
    tx.addOutput(create_scriptPubKey(receiver), 10)
    tx.addOutput(create_scriptPubKey(sender), 8990)

    let sighash = tx.hashForWitnessV0(0, create_scriptPubKey(sender), 9981, bitcoin.Transaction.SIGHASH_ALL)
    console.log(`  vsize use_Tx: ${tx.virtualSize()}`)
    return sighash.toString("hex")
}

(async ()=>{
    let hash_use_Psbt = use_Psbt() // use bitcoin.Psbt to create a hash of an input for signing
    let hash_use_Tx = use_Transaction() // use bitcoin.Transaction to create a hash of an input for signing
    console.log(`hash_use_Psbt: ${hash_use_Psbt}`)
    console.log(`  hash_use_Tx: ${hash_use_Tx}`)
    console.log(`     expected: 54d5181ba0f88b1854c28af7021847d67bf9ce4a8f8fef8e623d1d97946cfcca`) // a hash from Blockcypher /txs/new API, already worked offline

})()

This code spits out (you can easily run and check): image

You can see they are all different from each other, I am confused and stuck here, please help.

BTW, the vsize given by these two bitcoinjs-lib methods are the same, i.e., 113, but different from the vsize 116 in the Blockcypher Tx skeleton.

Also, I saw someone did the following for P2WPKH:

const pkh = bitcoin.script.decompile(create_scriptPubKey(sender))[1];
const sighash2 = tx.hashForWitnessV0(
      0, bitcoin.script.compile([
          bitcoin.opcodes.OP_DUP,
          bitcoin.opcodes.OP_HASH160,
          pkh,
          bitcoin.opcodes.OP_EQUALVERIFY,
          bitcoin.opcodes.OP_CHECKSIG
      ]), 
      9981, 
      bitcoin.Transaction.SIGHASH_ALL
);

The two hashes given by this logic where I used the Tx objects created from bitcoin.Transaction and psbt.CACHE.TX are different from each other, also different from the screenshot above.

junderw commented 2 months ago

This is not an issue with this library.

You are using private APIs without any understanding of how the internals work, and asking why it's not what you expected.

I am closing this because it is not an issue with this library. However, conversation on closed issues is fine, so if someone wants to help you out, that's fine.

Please do not open another issue about this problem you're experiencing.

junderw commented 2 months ago

@jasonandjay because you self-assigned the other issue I assigned this issue to you.

Let's keep discussion in this issue.

liudonghao commented 2 months ago

Okay, that's not an issue, then there must be something wrong in my way of using bitcoinjs-lib, because "tosign" is tested on both testnet and mainnet, could you please point out what's wrong or give some hints. @jasonandjay, pls help.

jasonandjay commented 2 months ago

Okay, that's not an issue, then there must be something wrong in my way of using bitcoinjs-lib, because "tosign" is tested on both testnet and mainnet, could you please point out what's wrong or give some hints. @jasonandjay, pls help.

There are two problems in the above code that cause the Hash of PSBT and Transaction to be inconsistent

  1. As we all know, prev_hash is little-endian encoding, so you need to add reverserBuffer before addInput in Transaction
  2. PSBT uses version 2 and Transaction uses version 1

The following is the code I processed for your reference

import bitcoin from 'bitcoinjs-lib';

const prev_hash = '59857c959b0337286dc478f7df5cf2bd3479bd22af93faa34760dd75b0e752ea'
const sender = {
    address: 'tb1qe55hg9ffxs46l4lfy0amc6dv0j4qnadwsd7sp0',
    pubkey: '020ae4375d81c2c927ef0370932d67c886fb309a31e3b41816640b5bff6282b8fe'
}

const receiver = {
    address: 'tb1qvz7fualryakkz7j4y7teyvrtal75um8k7jf4zx',
    pubkey: '02491f1effe381dc9240e63b561f0bfa8743ff9767a6e6c646a0eeac3d0c6dfb69'
}

const create_scriptPubKey = (party) => {
    let payment = bitcoin.payments.p2wpkh({ pubkey: Buffer.from(party.pubkey, "hex"), network: bitcoin.networks.testnet })
    return payment.output
}

const use_Psbt = () => {
    const psbt = new bitcoin.Psbt({ network: bitcoin.networks.testnet, version: 1 })
    // set version 
    psbt.setVersion(1);
    psbt.addOutput({
        address: receiver.address,
        value: 10
      });

    psbt.addOutput({
        address: sender.address,
        value: 8990
    });

    var input = {
        hash: '59857c959b0337286dc478f7df5cf2bd3479bd22af93faa34760dd75b0e752ea',
        index: 1,
        witnessUtxo: {
            script: create_scriptPubKey(sender),
            value: 9981
        }
    };

    psbt.addInput(input)

    const sighash = psbt.__CACHE.__TX.hashForWitnessV0(
        0,
        psbt.data.inputs[0].witnessUtxo.script,
        psbt.data.inputs[0].witnessUtxo.value,
        bitcoin.Transaction.SIGHASH_ALL
    )
    console.log(`vsize use_Psbt: ${psbt.__CACHE.__TX.virtualSize()}`)
    return sighash.toString("hex")
}

const reverseBuffer = buffer=>{
  if (buffer.length < 1) return buffer;
  let j = buffer.length - 1;
  let tmp = 0;
  for (let i = 0; i < buffer.length / 2; i++) {
    tmp = buffer[i];
    buffer[i] = buffer[j];
    buffer[j] = tmp;
    j--;
  }
  return buffer;
}

const use_Transaction = () => {
    let tx = new bitcoin.Transaction(bitcoin.networks.testnet)
    // reverseBuffer before addInput
    _tx.addInput(reverseBuffer(Buffer.from(prev_hash, 'hex')), 1)
    tx.addOutput(create_scriptPubKey(receiver), 10)
    tx.addOutput(create_scriptPubKey(sender), 8990)

    let sighash = tx.hashForWitnessV0(0, create_scriptPubKey(sender), 9981, bitcoin.Transaction.SIGHASH_ALL)
    console.log(`  vsize use_Tx: ${tx.virtualSize()}`)
    return sighash.toString("hex")
}

(async ()=>{
    let hash_use_Psbt = use_Psbt() // use bitcoin.Psbt to create a hash of an input for signing
    let hash_use_Tx = use_Transaction() // use bitcoin.Transaction to create a hash of an input for signing
    console.log(`hash_use_Psbt: ${hash_use_Psbt}`)
    console.log(`  hash_use_Tx: ${hash_use_Tx}`)
    console.log(`     expected: 54d5181ba0f88b1854c28af7021847d67bf9ce4a8f8fef8e623d1d97946cfcca`) // a hash from Blockcypher /txs/new API, already worked offline

})()
jasonandjay commented 2 months ago

Another question, as you said, the transaction length constructed using bitcoinjs is different from that of Blockcypher.

Maybe you need to analyze the extra fields to construct the transaction in order to get the same hash.

acsonservice commented 2 months ago

To solve the problem of producing correct input hashs using the Bitcoinjs-lib library as well as setting the Sequence Number for RBF feature, you must do the following steps:

Create an initial transaction using Bitcoinjs-lib:

Create the transaction with the same information you have in the BlockCyPher Transaction Skeleton. Set the input sequence number to a smaller value to enable RBF. Production of input hashs for signature:

Use the TX.Hashforsignature method to produce the hashs needed for signature. Compare these hashs with the tosign in the BLOCKCYPER transaction skeleton to make sure the hashs are correct. Hash signature and complete transaction:

Sign the manufactured hashs offline. Add signatures to the transaction and create the final transaction. The following is a complete example of this process:

acsonservice commented 2 months ago

In this code:

We used TransactionBuilder to build a transaction. We set the input order number for RBF. We produced the input hashs using the HashforwitnessV0 method. We produced the signatures and added to the transaction. Finally, we produced the final transaction in Hex.

acsonservice commented 2 months ago

Const bitcoin = Require ('bitcoinjs-lib'); constraint ECC = Require ('Tiny-SECP256K1'); constant {ecpairface} = Require ('ecpair'); Const ecpair = ecpairfactory (ECC);

// Testing Network Const testnet = bitcoin.networks.testnet;

// Private key to signature constant wif = 'your-proveate-or-in-wif'; constircent (ecpair.fromwif (wif, testnet);

// Transaction Input Information Const prevtxid = '59857c959b0337286dc478f7df5cf2bd3479bd22af93FAA34760d75b0e752ea'; Const prevtxindex = 1; Const prevtxvalue = 9981;

// Addresses consnecutable inputaddress = 'TB1QE55HG9FFXS46L4LFY0AMC6DV0J4QNADWSD7SP0'; consumed outputaddress1 = 'tb1qvz7fualryakkz7j4y7tevrtal75U8k7jf4zx'; consisting outputaddress2 = 'TB1QE55HG9FFXS46L4L4MC6DV0J4QNadwsd7P0';

// construction of initial transaction Const txb = new bitcoin.TransactBuilder (testnet); txb.setversion (2); // for RBF TXB.addinput (prevtxid, prevtxindex, 0xfffffd); // Sequence Number less than maximum for RBF txb.addutput (outputaddress1, 10); // first output txb.addutput (Outputaddress2, 8990); // second output

// hash for signature constant tx = txb.buildincomplete (); consist Hashforsignature = tx.hashForwitness0 (0, buffer.from ('76a914' + bitcoin.crypto.hash160 (Keypair.publkey) .tostring ('HEX') + '88AC', 'hex'), prevtxvalue, bitcoin. Sighash_all);

// consist signature = keypair.sign (Hashforsignature); constant scriptsig = bitcoin.script.signature.encode (Signature, bitcoin.transighten_all);

// Add signature to transaction consist txwithsignature = bitcoin.transction.fromhex (tx.tohex ()); txwithsignature.setwitness (0, [scriptsig, keypair.publkey]);

// final transaction constant Finaltx = txwithsignature.tohex (); Console.log (Finaltx);

acsonservice commented 2 months ago

const bitcoin = require('bitcoinjs-lib'); const ecc = require('tiny-secp256k1'); const { ECPairFactory } = require('ecpair'); const ECPair = ECPairFactory(ecc);

// شبکه تست‌نت const testnet = bitcoin.networks.testnet;

// کلید خصوصی برای امضا const wif = 'your-private-key-in-wif'; const keyPair = ECPair.fromWIF(wif, testnet);

// اطلاعات ورودی تراکنش const prevTxId = '59857c959b0337286dc478f7df5cf2bd3479bd22af93faa34760dd75b0e752ea'; const prevTxIndex = 1; const prevTxValue = 9981;

// آدرس‌ها const inputAddress = 'tb1qe55hg9ffxs46l4lfy0amc6dv0j4qnadwsd7sp0'; const outputAddress1 = 'tb1qvz7fualryakkz7j4y7teyvrtal75um8k7jf4zx'; const outputAddress2 = 'tb1qe55hg9ffxs46l4lfy0amc6dv0j4qnadwsd7sp0';

// ساخت تراکنش اولیه const txb = new bitcoin.TransactionBuilder(testnet); txb.setVersion(2); // برای RBF txb.addInput(prevTxId, prevTxIndex, 0xfffffffd); // sequence number کمتر از حداکثر برای RBF txb.addOutput(outputAddress1, 10); // خروجی اول txb.addOutput(outputAddress2, 8990); // خروجی دوم

const tx = txb.buildIncomplete(); const hashForSignature = tx.hashForWitnessV0(0, Buffer.from('76a914' + bitcoin.crypto.hash160(keyPair.publicKey).toString('hex') + '88ac', 'hex'), prevTxValue, bitcoin.Transaction.SIGHASH_ALL);

// امضا کردن هش const signature = keyPair.sign(hashForSignature); const scriptSig = bitcoin.script.signature.encode(signature, bitcoin.Transaction.SIGHASH_ALL);

const txWithSignature = bitcoin.Transaction.fromHex(tx.toHex()); txWithSignature.setWitness(0, [scriptSig, keyPair.publicKey]);

const finalTx = txWithSignature.toHex(); console.log(finalTx);

acsonservice commented 2 months ago

Steps to Create an RBF-Enabled Transaction Using bitcoinjs-lib Install the necessary libraries:

sh Copy code npm install bitcoinjs-lib tiny-secp256k1 ecpair Create the transaction with bitcoinjs-lib:

javascript Copy code const bitcoin = require('bitcoinjs-lib'); const ecc = require('tiny-secp256k1'); const { ECPairFactory } = require('ecpair'); const ECPair = ECPairFactory(ecc);

// Define the testnet network const testnet = bitcoin.networks.testnet;

// Import the private key for signing const wif = 'your-private-key-in-wif'; const keyPair = ECPair.fromWIF(wif, testnet);

// Transaction input details const prevTxId = '59857c959b0337286dc478f7df5cf2bd3479bd22af93faa34760dd75b0e752ea'; const prevTxIndex = 1; const prevTxValue = 9981; // Satoshis const prevTxScriptPubKey = Buffer.from('0014cd29741529342bafd7e923fbbc69ac7caa09f5ae', 'hex'); // ScriptPubKey from the previous output

// Addresses for outputs const outputAddress1 = 'tb1qvz7fualryakkz7j4y7teyvrtal75um8k7jf4zx'; const changeAddress = 'tb1qe55hg9ffxs46l4lfy0amc6dv0j4qnadwsd7sp0';

// Create a new transaction builder const txb = new bitcoin.TransactionBuilder(testnet); txb.setVersion(2); // Set version to 2 for RBF

// Add input with a sequence number lower than 0xffffffff for RBF txb.addInput(prevTxId, prevTxIndex, 0xfffffffd, prevTxScriptPubKey);

// Add output (recipient) txb.addOutput(outputAddress1, 10); // 10 satoshis to recipient

// Add change output txb.addOutput(changeAddress, prevTxValue - 10 - 981); // Deduct the fee (981 satoshis)

// Sign the transaction txb.sign(0, keyPair, null, bitcoin.Transaction.SIGHASH_ALL, prevTxValue);

// Build the transaction const tx = txb.build(); const txHex = tx.toHex();

console.log(txHex); Explanation of the Code Library Imports:

bitcoinjs-lib: Main library for building Bitcoin transactions. tiny-secp256k1 and ecpair: For key pair operations and signing. Network and Key Pair:

Define the network as testnet. Import the private key using WIF (Wallet Import Format). Transaction Input Details:

Define the previous transaction ID and index. Define the value and scriptPubKey of the previous output. Output Addresses:

Define the recipient's address and change address. Transaction Builder:

Create a new transaction builder and set the version to 2 to enable RBF. Add the input with a sequence number lower than 0xffffffff to allow RBF. Add the output for the recipient and the change output. Sign the transaction with the private key. Build and Print the Transaction:

Build the transaction and print it in hexadecimal format. Comparing with Blockcypher API To compare the generated hash for signing (tosign in Blockcypher) with the one generated by bitcoinjs-lib, ensure the prevTxScriptPubKey and the values are correct. If the hashes still don't match, consider using the hashForWitnessV0 method for SegWit inputs:

javascript Copy code const tx = txb.buildIncomplete(); const hashForSignature = tx.hashForWitnessV0(0, prevTxScriptPubKey, prevTxValue, bitcoin.Transaction.SIGHASH_ALL);

console.log(hashForSignature.toString('hex')); This should match the tosign value provided by Blockcypher. If there are still mismatches, double-check the scriptPubKey and all transaction details to ensure accuracy.

liudonghao commented 2 months ago

@jasonandjay, verified the code you revised, thank you very much for the two points you mentioned: 1, version 2, reverse (actually Buffer.reverse() worked) This is truly helpful, now the remaining question is: why does it still not match with the "tosign" in the Blockcypher Tx skeleton?

jasonandjay commented 2 months ago

@jasonandjay, verified the code you revised, thank you very much for the two points you mentioned: 1, version 2, reverse (actually Buffer.reverse() worked) This is truly helpful, now the remaining question is: why does it still not match with the "tosign" in the Blockcypher Tx skeleton?

Sorry, i am not familiar with Blockcypher

liudonghao commented 2 months ago

@acsonservice I tried your last post, well structured code. What version of bitcoinjs-lib you are using? I am using the latest version which doesn't have bitcoin.TransactionBuilder anymore. Did you get the same hash as "tosign" in the Blockcypher Tx skeleton? If so, I would rather downgrade bitcoinjs-lib. Thank you so much for your very detailed instructions.

Does the code of your post with comments in Arabian language (only my guess) work? matched with "tosign"? or worked in your PROD env?

liudonghao commented 2 months ago

Hi @acsonservice , I tried the code in your last post, just changed a little bit, setVerion(1) and sequence=0xffffffff as that is what's shown in the Tx skeleton. This code gives: bcaec35004fe4ca5a76fa57acf3dcff6880e50d74e2b8c8f23287cc18d991aa5, exactly the same as those given by the code improved by @jasonandjay

Runnable code with bitcoinjs-lib ^4.0.5:

const bitcoin = require('bitcoinjs-lib');
const ecc = require('tiny-secp256k1');
const { ECPairFactory } = require('ecpair');
const ECPair = ECPairFactory(ecc);

(async () => {
    // Define the testnet network
    const testnet = bitcoin.networks.testnet;

    // Import the private key for signing
    const wif = 'your-private-key-in-wif';
    // const keyPair = ECPair.fromWIF(wif, testnet);

    // Transaction input details
    const prevTxId = '59857c959b0337286dc478f7df5cf2bd3479bd22af93faa34760dd75b0e752ea';
    const prevTxIndex = 1;
    const prevTxValue = 9981; // Satoshis
    const prevTxScriptPubKey = Buffer.from('0014cd29741529342bafd7e923fbbc69ac7caa09f5ae', 'hex'); // ScriptPubKey from the previous output

    // Addresses for outputs
    const outputAddress1 = 'tb1qvz7fualryakkz7j4y7teyvrtal75um8k7jf4zx';
    const changeAddress = 'tb1qe55hg9ffxs46l4lfy0amc6dv0j4qnadwsd7sp0';

    // Create a new transaction builder
    const txb = new bitcoin.TransactionBuilder(testnet);
    txb.setVersion(1); // Set version to 2 for RBF

    // Add input with a sequence number lower than 0xffffffff for RBF
    txb.addInput(prevTxId, prevTxIndex, 0xffffffff, prevTxScriptPubKey);

    // Add output (recipient)
    txb.addOutput(outputAddress1, 10); // 10 satoshis to recipient

    // Add change output
    txb.addOutput(changeAddress, prevTxValue - 10 - 981); // Deduct the fee (981 satoshis)

    // let tx = txb.build()
    let hash_buf= txb.__tx.hashForWitnessV0(0, prevTxScriptPubKey, 9981, bitcoin.Transaction.SIGHASH_ALL)
    console.log('sigbuf=', hash_buf.toString('hex'))

})()
acsonservice commented 2 months ago

To understand why the transaction data generated by your code does not match the "tosign" fields in the Blockcypher transaction skeleton, we need to ensure we are following the correct steps for creating and signing a Bitcoin transaction. Let's go through a detailed explanation and check each step to align with Blockcypher's expectations.

Steps for Creating and Signing a Bitcoin Transaction Fetch UTXOs and Transaction Skeleton from Blockcypher: Start by fetching UTXOs and transaction skeleton from Blockcypher. The transaction skeleton will provide the inputs (including tosign fields), outputs, and other necessary details for signing.

Create a Transaction Object: Use the bitcoinjs-lib library to create a new transaction object based on the fetched data.

Sign the Transaction: Sign the transaction inputs using the provided tosign data.

Finalize and Serialize the Transaction: Serialize the transaction to get the raw transaction hex, which can be broadcast to the Bitcoin network.

Let's use these steps to create and sign a transaction with bitcoinjs-lib and compare it to Blockcypher's tosign fields.

Example Code Here’s a comprehensive example to align your transaction creation and signing process with Blockcypher's API:

javascript Copy code const bitcoin = require('bitcoinjs-lib'); const request = require('request');

// Blockcypher API URLs const baseUrl = 'https://api.blockcypher.com/v1/btc/test3'; const newTxEndpoint = '/txs/new';

// Replace with your Blockcypher API token const token = 'YOUR_API_TOKEN';

// Step 1: Fetch the transaction skeleton from Blockcypher const txSkeletonRequest = { inputs: [ { addresses: ['YOUR_INPUT_ADDRESS'] } ], outputs: [ { addresses: ['YOUR_OUTPUT_ADDRESS'], value: 100000 // example value in satoshis } ] };

request.post( { url: baseUrl + newTxEndpoint, json: txSkeletonRequest, qs: { token } }, (err, res, txSkeleton) => { if (err) { console.error('Error fetching transaction skeleton:', err); return; }

console.log('Transaction Skeleton:', txSkeleton);

// Step 2: Create a new transaction builder
const network = bitcoin.networks.testnet; // or bitcoin.networks.bitcoin for mainnet
const txb = new bitcoin.TransactionBuilder(network);

// Step 3: Add inputs from the transaction skeleton
txSkeleton.tx.inputs.forEach((input, index) => {
  const utxo = txSkeleton.inputs[index];
  txb.addInput(
    utxo.prev_hash,
    utxo.output_index,
    null,
    Buffer.from(utxo.script, 'hex')
  );
});

// Step 4: Add outputs from the transaction skeleton
txSkeleton.tx.outputs.forEach((output) => {
  txb.addOutput(output.addresses[0], output.value);
});

// Step 5: Sign the inputs
const keyPair = bitcoin.ECPair.fromWIF('YOUR_PRIVATE_KEY', network);
txSkeleton.tosign.forEach((tosign, index) => {
  const hashToSign = Buffer.from(tosign, 'hex');
  const signature = bitcoin.script.signature.encode(
    keyPair.sign(hashToSign),
    bitcoin.Transaction.SIGHASH_ALL
  );
  txb.sign(index, keyPair, null, null, null, signature.slice(0, -1)); // Remove the sighash byte
});

// Step 6: Build and serialize the transaction
const tx = txb.build();
const rawTxHex = tx.toHex();

console.log('Signed transaction hex:', rawTxHex);

// Optional: Broadcast the transaction (to be done after verification)
/*
request.post(
  {
    url: baseUrl + '/txs/push',
    json: { tx: rawTxHex },
    qs: { token }
  },
  (err, res, body) => {
    if (err) {
      console.error('Error broadcasting transaction:', err);
      return;
    }
    console.log('Transaction broadcasted:', body);
  }
);
*/

} ); Key Points to Ensure Correct Signing Ensure Correct Input Order: The order of inputs in your transaction builder should match the order of tosign fields provided by Blockcypher.

Correct Signature Generation: The tosign fields are hashes of the data that need to be signed. Make sure the signature generation aligns with the tosign fields using the correct SIGHASH type.

Transaction Details: Ensure all transaction details (UTXOs, outputs, values) match the transaction skeleton provided by Blockcypher.

Serialize Correctly: Ensure the serialized transaction matches the format expected by the Bitcoin network and Blockcypher.

By carefully following these steps and ensuring all details align with Blockcypher’s expectations, you should be able to match the generated transaction data with the tosign fields. This approach ensures your transaction is correctly created and signed, ready for broadcasting.

acsonservice commented 2 months ago

This code includes adding inputs and outputs, signing inputs and finalizing the transaction:

javascript Copy Code Const bitcoin = Require ('bitcoinjs-lib');

// Assuming the following variables are set consist network = bitcoin.networks.testnet; // Bitcoin.networks.bitcoin for Mainnet

// UTXOS Example (Replace with Actual UTXOS DATA) constraint utxos = [ { TXID: 'A4F09F97B1B87F2E5A4C1b1879f83DC3B3e901358745D05B67A5E8E8B495A5398' ,/ Example Transaction ID Vout: 0, // Examput Index SCRIPT: '76a9149c7c7e583AD2C1b2D8548957A1e5fb70C7E7D8888', // Example Locking Script Value: 1000000, // Example Value in Satoshis Tapbip32Derivation: [ { Masterfingerprint: buffer.from ('00000000', 'hex'), Pubkey: buffer.from ('0250863ad64a87ae8a83C1AF1A8403CB53F53e486D8511dad8A04887E5b2352', 'HEX'), ' PATH: “M/86 '/0'/0. 0. 0”, Leafhasses: [], }, ], Tapinternalkey: buffer.from ('0250863AD64A87A8A83C1A83CB53F53F53e486D851Dad88A04887E5B2352', 'HEX')) tapkeysig: buffer.from ('30440220466654e70b4da131d5e137e5e18E18E18e18E0C755575A5e5e5e5e5e5C5F1F37E45D7977B2737e184 10c6111b9d8f88842f6a7a7a70a703a2a6d7b4e0d12188EA188ea5864ef9b3f5af7A174553A95D5F7C2AAF5C8E ', Hex'), }, ];

// Outputs Example (Replace with Actual Outputs Data) constraints outputs = [ { Address: 'TB1Q5znjl0d42H08U65H34CQ70Q56kgg2cg2cgk2px', // Example Bitcoin Address Amount: 900000, // Example Amount in Satoshis }, ];

// Create a New Transaction Builder Const txb = new bitcoin.TransactBuilder (Network);

// add inputs UTXOS.Foreach ((UTXO, Index) => { txb.addinput (utxo.txid, utxo.vout, null, buffer.from (utxo.script, 'hex'); });

// Add outputs outputs.foreach ((output) => { txb.addutput (output.address, output.amount); });

// Sign Inputs UTXOS.Foreach ((UTXO, Index) => { consist keypair = bitcoin.ecpair.frompublkey (utxo.tapbip32Derivation [0] .pubkey, {network}); constant Signature = buffer.from (utxo.tapkeysig, 'hex');

txb.sign ({ PrevoutScriptype: 'P2PKH', // Adjust Account to The Type of Locking Script VIN: Index, Keypair, Signature, }); });

// Build and Finalize the Transaction Const tx = txb.build (); Console.log ('Signed Transaction:', tx.tohex ()); Bitcoinjs-lib library Library Installation: You can use NPM to use Bitcoinjs-lib in node.js:

VBnet Copy Code NPM Install Bitcoinjs-lib Description:

Network Definition: In this example, Networks.testnet is used for the test network, but you can change it to Networks.bitcoin for the main network.

UTXOS and Outputs: UTXO and Outputs Input and Output Transaction Input must be replaced with your actual data.

Signature Inputs: Here, using Bitcoinjs-lib, each input is signed using the public key and the signature provided.

Construction and finalization of transaction: After adding inputs and outputs, the transaction is made and converted to Hex format using tohex ().

This code can be used as a pattern for the construction and signature of bitcoin transactions in your applications and services, given your actual inputs and outputs and specific conditions.

liudonghao commented 2 months ago

Hi @acsonservice,

The code with Arabian (forgive me if I'm wrong, I couldn't recognize) comments worked! thank you very much!

I think this is the right direction I need to dig into. @acsonservice @jasonandjay Could you please give me some links about encoding the script for tx.hashFor* functions?

Interestingly, playing the same script encoding trick did not work by bitcoin.Transaction and bitcoin.Psbt with the latest bitcoinjs-lib. So I have to stick with version that supports TransactionBuilder.

@acsonservice , I've read your last two posts, thank you so much. I think that would be helpful for me in the future. Right now, I want to focus on the script encoding needed by tx.hashFor* functions.

I don't need bitcoinjs-lib for signing, currently "tosign" is sent to another system written in Rust for signing and get signatures back, and put the signatures and public keys back into a Tx object and submit via a Blockcypher API. No matter how I tried its skeleton API /txs/new, just failures of enabling RBF, the skeleton always has sequence=0xffffffff, so I believed somehow I need to make the input hashes encode a smaller sequence number. That's why I came to bitcoinjs-lib.

I believe the magic happened in the script fed into hashForWitnessV0 function in your code. How did you come up with the prefix and suffix, '76a914' and '88ac'? These two did not appear in the result given by:

Working code with bitcoinjs-lib 4.0.5, consistent with the skeleton:

const bitcoin = require('bitcoinjs-lib');

(async ()=> {
    // شبکه تست‌نت
    const testnet = bitcoin.networks.testnet;

    // اطلاعات ورودی تراکنش
    const prevTxId = '59857c959b0337286dc478f7df5cf2bd3479bd22af93faa34760dd75b0e752ea';
    const prevTxIndex = 1;
    const prevTxValue = 9981;

    // آدرس‌ها
    const outputAddress1 = 'tb1qvz7fualryakkz7j4y7teyvrtal75um8k7jf4zx';
    const outputAddress2 = 'tb1qe55hg9ffxs46l4lfy0amc6dv0j4qnadwsd7sp0';

    // ساخت تراکنش اولیه
    const txb = new bitcoin.TransactionBuilder(testnet);
    txb.setVersion(1); // برای RBF
    txb.addInput(prevTxId, prevTxIndex, 0xffffffff); // sequence number کمتر از حداکثر برای RBF
    txb.addOutput(outputAddress1, 10); // خروجی اول
    txb.addOutput(outputAddress2, 8990); // خروجی دوم

    const tx = txb.buildIncomplete();
    let publicKey = Buffer.from('020ae4375d81c2c927ef0370932d67c886fb309a31e3b41816640b5bff6282b8fe', 'hex')
    let prevOutScript = Buffer.from('76a914' + bitcoin.crypto.hash160(publicKey).toString('hex') + '88ac', 'hex')
    const hashForSignature = tx.hashForWitnessV0(0, prevOutScript, prevTxValue, bitcoin.Transaction.SIGHASH_ALL);

    console.log('    hashForWitnessV0:', hashForSignature.toString('hex'))
    console.log('hash in the skeleton:', '54d5181ba0f88b1854c28af7021847d67bf9ce4a8f8fef8e623d1d97946cfcca')

})()

Result, finally they are the same: image

acsonservice commented 2 months ago

That's great to hear! Let's walk through the process of encoding the script for different Bitcoin transaction types to understand why certain encodings are used and how to replicate them in your transactions.

1. Pay-to-PubKey-Hash (P2PKH)

For P2PKH transactions, the script is:

This breaks down to:

2. Pay-to-Witness-PubKey-Hash (P2WPKH)

For P2WPKH transactions, the script is:

This breaks down to:

3. Pay-to-Taproot (P2TR)

For P2TR transactions, the script is:

This breaks down to:

Using bitcoinjs-lib to Encode Scripts

Here’s how you can encode these scripts in your code using bitcoinjs-lib:

  1. P2PKH:

    const publicKey = Buffer.from('020ae4375d81c2c927ef0370932d67c886fb309a31e3b41816640b5bff6282b8fe', 'hex');
    const pubKeyHash = bitcoin.crypto.hash160(publicKey);
    const scriptPubKey = bitcoin.script.compile([
        bitcoin.opcodes.OP_DUP,
        bitcoin.opcodes.OP_HASH160,
        pubKeyHash,
        bitcoin.opcodes.OP_EQUALVERIFY,
        bitcoin.opcodes.OP_CHECKSIG
    ]);
    console.log('P2PKH script:', scriptPubKey.toString('hex'));
  2. P2WPKH:

    const publicKey = Buffer.from('020ae4375d81c2c927ef0370932d67c886fb309a31e3b41816640b5bff6282b8fe', 'hex');
    const pubKeyHash = bitcoin.crypto.hash160(publicKey);
    const scriptPubKey = bitcoin.script.compile([
        bitcoin.opcodes.OP_0,
        pubKeyHash
    ]);
    console.log('P2WPKH script:', scriptPubKey.toString('hex'));
  3. P2TR:

    const publicKey = Buffer.from('02c6d2ad8b4d9448ebc918ef7c1e9af564a34eeebbc3e7f2474ed8f29fa7d9a4f0', 'hex');
    const xOnlyPubKey = publicKey.slice(1); // x-only public key
    const scriptPubKey = bitcoin.script.compile([
        bitcoin.opcodes.OP_1,
        xOnlyPubKey
    ]);
    console.log('P2TR script:', scriptPubKey.toString('hex'));

Creating a Transaction with bitcoinjs-lib

Here’s a complete example that creates a transaction using the correct script format for SegWit:

const bitcoin = require('bitcoinjs-lib');
const ecc = require('tiny-secp256k1');
const { ECPairFactory } = require('ecpair');
const ECPair = ECPairFactory(ecc);

(async () => {
    const network = bitcoin.networks.testnet;
    const prevTxId = '59857c959b0337286dc478f7df5cf2bd3479bd22af93faa34760dd75b0e752ea';
    const prevTxIndex = 1;
    const prevTxValue = 9981;
    const inputAddress = 'tb1qe55hg9ffxs46l4lfy0amc6dv0j4qnadwsd7sp0';
    const outputAddress1 = 'tb1qvz7fualryakkz7j4y7teyvrtal75um8k7jf4zx';
    const outputAddress2 = 'tb1qe55hg9ffxs46l4lfy0amc6dv0j4qnadwsd7sp0';

    const keyPair = ECPair.fromWIF(process.env.PRIVATE_KEY_WIF, network);
    const publicKey = keyPair.publicKey;
    const pubKeyHash = bitcoin.crypto.hash160(publicKey);
    const p2wpkhScript = bitcoin.script.compile([
        bitcoin.opcodes.OP_0,
        pubKeyHash
    ]);

    const txb = new bitcoin.TransactionBuilder(network);
    txb.setVersion(1);
    txb.addInput(prevTxId, prevTxIndex, 0xffffffff);
    txb.addOutput(outputAddress1, 10);
    txb.addOutput(outputAddress2, 8990);

    const tx = txb.buildIncomplete();
    const hashForSignature = tx.hashForWitnessV0(
        0,
        p2wpkhScript,
        prevTxValue,
        bitcoin.Transaction.SIGHASH_ALL
    );

    console.log('hashForWitnessV0:', hashForSignature.toString('hex'));
})();

Conclusion

To generate the correct script for different types of Bitcoin addresses, you need to understand the OP codes and the structure of the script. The examples above should give you a clear idea of how to encode these scripts for P2PKH, P2WPKH, and P2TR transactions. This will help ensure your transactions are correctly formatted and hash for signatures match as expected.

liudonghao commented 2 months ago

@acsonservice

Now I know what pointed me in a wrong direction is "script_type": "pay-to-witness-pubkey-hash" in the skeleton all the time. I thought I should always use P2WPKH script when using bitcoinjs-lib, now turned out that's wrong. I think I need to learn more about Bitcoin.

The input hash for signature can be produced from the following two methods:

the required script can be produced by bitcoin.payments.p2*() function or bitcoin.script.compile() function.

The second method looks more preferable as one does not need bother to consider the different types of scripts for hashFor*() and addOutput(). You can see in the following working code that the first method requires two different scripts.

Working code (everything worked well, the vsize is still inconsistent though, but doesn't matter), bitcoinjs-lib version = 6.1.6:

var bitcoin = require('bitcoinjs-lib');

const prev_hash = '59857c959b0337286dc478f7df5cf2bd3479bd22af93faa34760dd75b0e752ea'
const sender = {
    address: 'tb1qe55hg9ffxs46l4lfy0amc6dv0j4qnadwsd7sp0',
    pubkey: '020ae4375d81c2c927ef0370932d67c886fb309a31e3b41816640b5bff6282b8fe'
}

const recipient = {
    address: 'tb1qvz7fualryakkz7j4y7teyvrtal75um8k7jf4zx',
    pubkey: '02491f1effe381dc9240e63b561f0bfa8743ff9767a6e6c646a0eeac3d0c6dfb69'
}

const create_scriptPubKey_P2PKH = (party) => {
    let payment = bitcoin.payments.p2pkh({ pubkey: Buffer.from(party.pubkey, "hex"), network: bitcoin.networks.testnet })
    return payment.output
}

const create_scriptPubKey_P2WPKH = (party) => {
    let payment = bitcoin.payments.p2wpkh({ pubkey: Buffer.from(party.pubkey, "hex"), network: bitcoin.networks.testnet })
    return payment.output
}

const use_Psbt = () => {
    const psbt = new bitcoin.Psbt({ network: bitcoin.networks.testnet, version: 1 })

    psbt.setVersion(1)
    psbt.addOutput({
        address: recipient.address,
        value: 10
      });

    psbt.addOutput({
        address: sender.address,
        value: 8990
    });

    var input = {
        hash: prev_hash,
        index: 1,
        sequence: 4294967295,
        witnessUtxo: {
            script: create_scriptPubKey_P2PKH(sender),
            value: 9981
        }
    };

    psbt.addInput(input)

    const sighash = psbt.__CACHE.__TX.hashForWitnessV0(
        0,
        psbt.data.inputs[0].witnessUtxo.script,
        psbt.data.inputs[0].witnessUtxo.value,
        bitcoin.Transaction.SIGHASH_ALL
    )
    let hash_use_compile = use_compile_P2PKH(psbt.__CACHE.__TX, 0, 9981)

    console.log(`[use_Psbt]               hash by compiling: ${hash_use_compile.toString('hex')}`)
    console.log(`[use_Psbt]        hash by hashForWitnessV0: ${sighash.toString('hex')}`)
    console.log(`[use_Psbt]                           vsize: ${psbt.__CACHE.__TX.virtualSize()}`)
}

const use_Transaction = () => {
    let tx = new bitcoin.Transaction(bitcoin.networks.testnet)
    tx.addInput(Buffer.from(prev_hash, 'hex').reverse(), 1, 4294967295)
    tx.addOutput(create_scriptPubKey_P2WPKH(recipient), 10)
    tx.addOutput(create_scriptPubKey_P2WPKH(sender), 8990)

    let sighash = tx.hashForWitnessV0(0, create_scriptPubKey_P2PKH(sender), 9981, bitcoin.Transaction.SIGHASH_ALL)

    let hash_use_compile = use_compile_P2PKH(tx, 0, 9981)

    console.log(`[use_Transaction]        hash by compiling: ${hash_use_compile.toString('hex')}`)
    console.log(`[use_Transaction] hash by hashForWitnessV0: ${sighash.toString('hex')}`)
    console.log(`[use_Transaction]                    vsize: ${tx.virtualSize()}`)
}

const use_compile_P2PKH = (tx, inIndex, value) => {
    const sighash_buf = tx.hashForWitnessV0(
        inIndex, bitcoin.script.compile([
            bitcoin.opcodes.OP_DUP,
            bitcoin.opcodes.OP_HASH160,
            bitcoin.crypto.hash160(Buffer.from(sender.pubkey, 'hex')),
            bitcoin.opcodes.OP_EQUALVERIFY,
            bitcoin.opcodes.OP_CHECKSIG
        ]), 
        value, 
        bitcoin.Transaction.SIGHASH_ALL
    )
    return sighash_buf.toString("hex")
}

const main = () => {
    use_Psbt() // use bitcoin.Psbt to create a hash of an input for signing
    use_Transaction() // use bitcoin.Transaction to create a hash of an input for signing
    console.log(`[main]   tosign in Blockcypher Tx skeleton: 54d5181ba0f88b1854c28af7021847d67bf9ce4a8f8fef8e623d1d97946cfcca`)
    console.log(`[main]    vsize in Blockcypher Tx skeleton: 116`)
}

main()

Result: image