Skip to content

added pmt builder considering the witnesses#44

Open
julianlen wants to merge 17 commits intomasterfrom
witness-pmt-builder
Open

added pmt builder considering the witnesses#44
julianlen wants to merge 17 commits intomasterfrom
witness-pmt-builder

Conversation

@julianlen
Copy link
Copy Markdown

No description provided.

@julianlen julianlen requested a review from a team as a code owner March 30, 2026 19:07
@github-actions
Copy link
Copy Markdown

github-actions bot commented Mar 30, 2026

Dependency Review

✅ No vulnerabilities or license issues or OpenSSF Scorecard issues found.

Snapshot Warnings

⚠️: No snapshots were found for the head SHA e578cf1.
Ensure that dependencies are being submitted on PR branches and consider enabling retry-on-snapshot-warnings. See the documentation for more information and troubleshooting advice.

Scanned Files

None

@julianlen julianlen force-pushed the witness-pmt-builder branch from 3c823ca to 252f877 Compare March 30, 2026 19:10
Copy link
Copy Markdown

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Adds support tooling to build Partial Merkle Trees using witness transaction IDs (wtxids), and extends the existing “registerBtcTransaction” helper to also return a PMT computed over wtxids.

Changes:

  • Added getWtxid helper for computing wtxids from raw transaction hex.
  • Added a new CLI tool to build a PMT using wtxids (tool/pmt-witness-builder.js).
  • Extended getInformationReadyForRegisterBtcTransaction to return an additional pmtConsideringWitness field and added a unit test for getWtxid.

Reviewed changes

Copilot reviewed 4 out of 4 changed files in this pull request and generated 6 comments.

File Description
tool/pmt-witness-builder.js New CLI script that builds a PMT using wtxids for a block containing a target tx.
tool/pmt-builder-utils.js New helper for deriving wtxid from raw tx hex via bitcoinjs-lib.
tool/getInformationReadyForRegisterBtcTransaction.js Now computes both txid-based PMT and wtxid-based PMT and returns both.
test/test.js Adds unit coverage for getWtxid and updates require style.

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

@@ -0,0 +1,9 @@
const bitcoin = require('bitcoinjs-lib');
Copy link

Copilot AI Mar 30, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This tool code depends on bitcoinjs-lib at runtime (via require('bitcoinjs-lib')), but bitcoinjs-lib is currently listed under devDependencies. If these tool/* scripts are intended to be usable from an installed package (without devDependencies), move bitcoinjs-lib to dependencies or document that tools require a dev install.

Copilot uses AI. Check for mistakes.
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

So far, all the code in /tool is just to use locally, not to be installed and used from another project. So, bitcoinjs-lib as a dev dependency is acceptable for now.

Copy link
Copy Markdown

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 5 out of 5 changed files in this pull request and generated 10 comments.


💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

const tx = bitcoin.Transaction.fromHex(rawTx);
const wtxid = tx.getHash(true).reverse().toString('hex');
return wtxid;
}
Copy link

Copilot AI Apr 1, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

getWtxid is missing a trailing semicolon after the function expression. The rest of this module uses semicolons consistently, so this stands out and can cause style/lint inconsistencies.

Suggested change
}
};

Copilot uses AI. Check for mistakes.
Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

done


const pmt = resultPmt.hex;
const blockHeight = transaction.status.block_height;
const rawTargetBtcTransaction = await getTransactionWithRetry(transactionsClient, transactionHash);
Copy link

Copilot AI Apr 1, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Same issue for the target transaction: rawTargetBtcTransaction can be null from getTransactionWithRetry, which will break getWtxid and also produce tx: 0xnull in the returned object. Handle the null case explicitly (e.g., throw with txid context) before computing targetWtxid / building the witness PMT.

Suggested change
const rawTargetBtcTransaction = await getTransactionWithRetry(transactionsClient, transactionHash);
const rawTargetBtcTransaction = await getTransactionWithRetry(transactionsClient, transactionHash);
if (rawTargetBtcTransaction == null) {
throw new Error(`Failed to fetch raw Bitcoin transaction for txid ${transactionHash}`);
}

Copilot uses AI. Check for mistakes.
Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

done

Comment on lines 48 to 52
(async () => {
try {

const network = process.argv[2];
const transactionHash = process.argv[3];

Copy link

Copilot AI Apr 1, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This module executes the CLI IIFE unconditionally. Importing/requiring it from another script will trigger network calls and console output as a side effect. Guard the CLI entry with if (require.main === module) (or move the CLI to a separate entry file) so the exported function can be reused safely.

Copilot uses AI. Check for mistakes.
Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

so far, each file is an independent tool

Comment on lines +21 to +24
const txid = blockTxIds[i];
const rawTx = await getTransactionWithRetry(transactionsClient, txid);
const wtxid = getWtxid(rawTx);
blockWtxids.push(wtxid);
Copy link

Copilot AI Apr 1, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

getTransactionWithRetry can return null; calling getWtxid(rawTx) will throw and make it hard to diagnose which tx in the block failed to fetch. Add an explicit null check here (include the txid in the error) before computing/pushing the wtxid.

Copilot uses AI. Check for mistakes.
Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

done

Comment on lines +31 to +33
const rawTx = await getTransactionWithRetry(transactionsClient, txHash);
const targetTxWTxId = getWtxid(rawTx);
const resultPmt = pmtBuilder.buildPMT(blockWtxids, targetTxWTxId);
Copy link

Copilot AI Apr 1, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Same null-handling issue for the target tx: rawTx can be null from getTransactionWithRetry, which will break getWtxid and PMT building. Handle the null case explicitly before computing targetTxWTxId / calling buildPMT.

Copilot uses AI. Check for mistakes.
Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

done

/**
* Retrieves all transaction objects in a block by their transaction IDs.
* @param {string[]} txIds - An array of txIds.
* @returns {Promise<string[]>} An array of transactions.
Copy link

Copilot AI Apr 1, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The getAllTxs JSDoc is now inaccurate: it says it returns Promise<string[]>, but the function pushes bitcoinJs.Transaction.fromHex(tx) and returns an array of Transaction objects. Update the return type/doc to match the actual return value.

Suggested change
* @returns {Promise<string[]>} An array of transactions.
* @returns {Promise<import('bitcoinjs-lib').Transaction[]>} A promise that resolves to an array of bitcoinjs-lib Transaction objects.

Copilot uses AI. Check for mistakes.
Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

done

Comment on lines +48 to +49
// mempool.js getTx method returns an object that directly contains 'wtxid'
const tx = await getTransactionWithRetry(txId);
const tx = await getTransactionWithRetry(transactionsClient, txId);
Copy link

Copilot AI Apr 1, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This inline comment is misleading: it references mempool.js getTx returning an object with wtxid, but the code here calls getTransactionWithRetry(...) which uses getTxHex and returns raw hex. Please update/remove the comment to reflect what’s actually happening.

Copilot uses AI. Check for mistakes.
Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

done

Comment on lines +38 to +50
(async () => {
try {
const network = process.argv[2];
const txHash = process.argv[3];

const pmtInformation = await getPmtInformationWithWitness(network, txHash);

console.log('PMT information considering wtxid: ', pmtInformation);

} catch (e) {
console.log(e);
}
})();
Copy link

Copilot AI Apr 1, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This file runs its CLI IIFE unconditionally. If it’s ever imported/reused programmatically, it will immediately perform network calls and write to stdout. Consider guarding the CLI entry with if (require.main === module) (or move the CLI into a dedicated entry file) to keep the module side-effect free on import.

Suggested change
(async () => {
try {
const network = process.argv[2];
const txHash = process.argv[3];
const pmtInformation = await getPmtInformationWithWitness(network, txHash);
console.log('PMT information considering wtxid: ', pmtInformation);
} catch (e) {
console.log(e);
}
})();
if (require.main === module) {
(async () => {
try {
const network = process.argv[2];
const txHash = process.argv[3];
const pmtInformation = await getPmtInformationWithWitness(network, txHash);
console.log('PMT information considering wtxid: ', pmtInformation);
} catch (e) {
console.log(e);
}
})();
}

Copilot uses AI. Check for mistakes.
Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

same

Comment on lines +32 to +33
const targetTxWTxId = getWtxid(rawTx);
const resultPmt = pmtBuilder.buildPMT(blockWtxids, targetTxWTxId);
Copy link

Copilot AI Apr 1, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Variable naming is inconsistent here (targetTxWTxId). Since this value is a wtxid, consider renaming to something like targetWtxid for clarity and consistency with blockWtxids.

Suggested change
const targetTxWTxId = getWtxid(rawTx);
const resultPmt = pmtBuilder.buildPMT(blockWtxids, targetTxWTxId);
const targetWtxid = getWtxid(rawTx);
const resultPmt = pmtBuilder.buildPMT(blockWtxids, targetWtxid);

Copilot uses AI. Check for mistakes.
Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

done

Copy link
Copy Markdown

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 5 out of 5 changed files in this pull request and generated 2 comments.

Comments suppressed due to low confidence (1)

tool/getInformationReadyForRegisterCoinbaseBtcTransaction.js:106

  • Witness merkle roots per BIP141 treat the coinbase leaf’s wtxid as 32 bytes of 00..00 (not the actual coinbase wtxid). hashesWithWitness is currently built from tx.getHash(true) for every tx including coinbase, so the computed witnessMerkleRoot will not match the block’s witness commitment. Special-case index 0 to Buffer.alloc(32, 0) before building the merkle tree.
    // Calculate witnessRoot
    const hashesWithWitness = txs.map( x => Buffer.from(x.getHash(true)));
    const witnessMerkleTree = merkleLib(hashesWithWitness, bitcoinJs.crypto.hash256);
    // Get witness merkleRoot from witnessMerkleTree. This is equal to the last element in witnessMerkleTree array
    const witnessMerkleRoot = witnessMerkleTree[witnessMerkleTree.length-1].reverse();


💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

* Retrieves all transaction objects in a block by their transaction IDs.
* @param {Object} transactionsClient - Client instance used to fetch transaction data (must provide `getTxHex`).
* @param {string[]} txIds - An array of txIds.
* @returns {Promise<string[]>} An array of transactions.
Copy link

Copilot AI Apr 1, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The getAllTxs JSDoc says it returns Promise<string[]>, but the implementation pushes bitcoinJs.Transaction.fromHex(tx) and returns an array of Transaction objects. Please update the return type to match the actual return value (e.g., Promise<bitcoinjs-lib.Transaction[]>).

Suggested change
* @returns {Promise<string[]>} An array of transactions.
* @returns {Promise<bitcoinJs.Transaction[]>} An array of transactions.

Copilot uses AI. Check for mistakes.
Comment on lines +19 to +33
const blockWtxids = [];
for (let i = 0; i < blockTxIds.length; i++) {
const txid = blockTxIds[i];
const rawTx = await getTransactionWithRetry(transactionsClient, txid);

if (!rawTx) {
throw new Error(`Failed to fetch transaction details for txId: ${txid}. It might not exist or is malformed.`);
}

const wtxid = getWtxid(rawTx);
blockWtxids.push(wtxid);

if (i < blockTxIds.length - 1) {
await sleep();
}
Copy link

Copilot AI Apr 1, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

For the witness merkle tree (BIP141), the coinbase leaf must be the all-zero wtxid (32 bytes of 00..00). This loop currently computes and includes the actual coinbase wtxid, so the resulting witness PMT won’t correspond to the block’s witness commitment. Please special-case the first tx in the block (coinbase) to use the zero wtxid, and ensure targetWtxid is also zero when the target tx is coinbase.

Copilot uses AI. Check for mistakes.
return await transactionsClient.getTxHex({txid: txId});
} catch (error) {
// mempool.js might wrap the error, so we check for common indicators of 429
const isRateLimitError = error.response && error.response.status === 429;
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Should we put this 429 in a constant? TOO_MANY_REQUEST_ERROR_CODE or something like that.

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

done

jeremy-then
jeremy-then previously approved these changes Apr 6, 2026
Copy link
Copy Markdown
Contributor

@jeremy-then jeremy-then left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

LGTM. Just left some suggestions.

try {
return await transactionsClient.getTxHex({txid: txId});
} catch (error) {
// mempool.js might wrap the error, so we check for common indicators of 429
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
// mempool.js might wrap the error, so we check for common indicators of 429
// mempool.js might wrap the error, so we check for common indicators of too many requests error code

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

done

test/test.js Outdated
Comment on lines +140 to +152
it('should return the txid as the wtxid for a block without witness transactions', async () => {
const blockTxids = ['2bf2c26cf756564f1d7f17f89882da32f20d694d9f2c8f04962c1116ccdf0bcf'];
const targetTxId = blockTxids[0];
const transactionsClient = createTransactionsClientMock();

const result = await getWtxids(transactionsClient, blockTxids, targetTxId);

expect(result.targetWtxid).to.equal(transactions[targetTxId].wtxid);
expect(result.blockWtxids.length).to.equal(1);
expect(result.blockWtxids).to.be.an('array').that.includes(transactions[targetTxId].wtxid);
});

it('should return the witness hash for a block with witness transactions', async () => {
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

test with an array bigger than one tx?
a block with both txs with and without witness?

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think intead of having 2 tests with 1 tx, I'll write 3 tests with many txs: 1 all with witness, 1 all without witness, and another mixed

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

that sounds good yes

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

done!

test/test.js Outdated

const result = await getWtxids(transactionsClient, blockTxids, targetTxId);

expect(result.targetWtxid).to.equal(transactions[targetTxId].wtxid);
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

if the tx does not have witness shouldnt the wtxid be equal to the txid?

Suggested change
expect(result.targetWtxid).to.equal(transactions[targetTxId].wtxid);
expect(result.targetWtxid).to.equal(transactions[targetTxId].txid);

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

done!

Comment on lines +6 to +21
const bitcoin = mempoolJS({
hostname: 'mempool.space',
network // 'testnet' | 'mainnet'
});

const transactionsClient = bitcoin.bitcoin.transactions;
const blocksClient = bitcoin.bitcoin.blocks;

const transaction = await transactionsClient.getTx({ txid: txHash });

const blockHash = transaction.status.block_hash;
const blockTxIds = await blocksClient.getBlockTxids({ hash: blockHash });

const { blockWtxids, targetWtxid } = await getWtxids(transactionsClient, blockTxIds, txHash);
const resultPmt = pmtBuilder.buildPMT(blockWtxids, targetWtxid);
return resultPmt;
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

except for the getWtxids part, you have almost the exact same code in pmt-builder

Copy link
Copy Markdown
Author

@julianlen julianlen Apr 7, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Done. Also, I extracted shared behavior between the two getInformationReadyForRegister tools. However, I am not sure about having a method getBlockInfoByTransactionHash that returns an object with multiple elements. So consider this solution as an alternative. wdyt? aaf2d0f

const isRateLimitError = error.response && error.response.status === TOO_MANY_REQUESTS_ERROR_CODE;

// Sometimes the error message might contain clues if status is not directly 429
// Sometimes the error message might contain clues if status code is not directly 429
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

// Sometimes the error message might contain clues if status code is not the too many requests error code

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

done

@julianlen julianlen force-pushed the witness-pmt-builder branch 2 times, most recently from 58e0fe3 to 915b83a Compare April 7, 2026 15:43
@julianlen julianlen force-pushed the witness-pmt-builder branch from 915b83a to db4801f Compare April 7, 2026 16:02
…ed a method. However this method does things similar to getBlockTxidsByTransactionHash, so this last calls getBlockInfoByTransactionHash and obtains blockTxIds
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants