RPC/txoutproof: Support including (and verifying) proofs of wtxid #32844

pull luke-jr wants to merge 3 commits into bitcoin:master from luke-jr:rpc_gettxoutproof_segwit changing 7 files +266 βˆ’28
  1. luke-jr commented at 11:05 pm on June 30, 2025: member

    Feature requested by @SomberNight for Electrum’s Lightning stuff. (Please confirm this does what you need)

    Unfortunately WIP because the wtxid merkle root calculation doesn’t match and I don’t have time to figure out why not yet (maybe after concept ACK? or if anyone else wants to take a look…)

    Mostly backward compatible, but a segwit-enabled proof will verify the generation tx in old versions.

  2. DrahtBot commented at 11:05 pm on June 30, 2025: contributor

    The following sections might be updated with supplementary metadata relevant to reviewers and maintainers.

    Code Coverage & Benchmarks

    For details see: https://corecheck.dev/bitcoin/bitcoin/pulls/32844.

    Reviews

    See the guideline for information on the review process.

    Type Reviewers
    Concept ACK Sjors

    If your review is incorrectly listed, please react with πŸ‘Ž to this comment and the bot will ignore it on the next update.

    Conflicts

    Reviewers, this pull request conflicts with the following ones:

    • #33116 (refactor: Convert uint256 to Txid by marcofleon)
    • #33094 (refactor: unify container presence checks by l0rinc)

    If you consider this pull request important, please also help to review the conflicting pull requests. Ideally, start with the one that should be merged first.

    LLM Linter (✨ experimental)

    Possible typos and grammar issues:

    • In RPCResult description: “If verify_witness is true and the proof invalid” -> “If verify_witness is true and the proof is invalid” [missing β€œis” makes the sentence ungrammatical]

    No other typos were found.

    drahtbot_id_4_m

  3. DrahtBot added the label CI failed on Jul 1, 2025
  4. in src/rpc/txoutproof.cpp:222 in 3abfd6d3d9 outdated
    220+                std::vector<unsigned int> wtxid_indexes;
    221+                const auto wtxid_root = merkleBlock.m_wtxid_tree.ExtractMatches(wtxid_matches, wtxid_indexes);
    222+                if (memcmp(wtxid_root.begin(), &merkleBlock.m_gentx->vout[witness_commit_outidx].scriptPubKey[6], 32)) {
    223+                    // FIXME: For some reason, this isn't matching currently
    224+                    LogPrintf("%s vs %s DEBUG", wtxid_root.GetHex(), HexStr(MakeUCharSpan(merkleBlock.m_gentx->vout[witness_commit_outidx].scriptPubKey)));
    225+                    return {};
    


    SomberNight commented at 0:13 am on July 1, 2025:
    Is the cause of the mismatch that you thought the scriptPubKey directly contains the wtxid tree root? It actually contains Double-SHA256(witness root hash|witness reserved value) So you also have to extract the witness reserved value from the coinbase input witness. see https://github.com/bitcoin/bips/blob/77b0bb297ad4e4e6b04b2c078c5e3b620a7090e2/bip-0141.mediawiki#commitment-structure

    luke-jr commented at 8:39 pm on July 8, 2025:
    Yes, that’s what I forgot.
  5. SomberNight commented at 0:25 am on July 1, 2025: contributor

    Feature requested by @SomberNight for Electrum’s Lightning stuff. (Please confirm this does what you need)

    Wow, thank you for working on this! I was meaning to open an issue to ask for this, but did not even get to that yet.

    The request I would like an electrum server, assuming txindex=1, to be able to serve is:

    bc.tx.get_merkle_witness(txid), (so given just a txid,)

    return a dict containing:

    • wtxid
    • block_height
    • block_hash
    • pos (index of tx in block)
    • merkle or wmerkle
      • exactly one of the two
      • merkle is the Satoshi-SPV proof, in case there is no witness commitment in the block
      • wmerkle is the witness-SPV proof otherwise
    • cb_tx, the generation/coinbase tx
    • cb_proof, the Satoshi-SPV proof for the generation tx

    Obviously the electrum server can act as middleware and transform the response, assuming the necessary fields are available or obtainable.

    As far as I can tell, but I haven’t tried yet, the RPC response from this PR can be transformed to what I describe above. So the RPC looks to be doing what I need. :)


    I think it is non-trivial, so let me detail how an electrum/light client would then use bc.tx.get_merkle_witness(txid):

    • it is assumed client already has a full raw tx, and wants to verify whether it has been mined
    • client also has full header chain, proof-of-work verified
    • client calls bc.tx.get_merkle_witness(txid)
    • client calculates wtxid of full tx it has, compares it with received wtxid
    • using cb_tx, cb_proof, block_height, block_hash
      • client Satoshi-SPVs the received coinbase tx, against block header merkle_root
    • client extracts witness_commitment from cb_tx
      1. if there is no witness_commitment,
        • client Satoshi-SPVs the txid, using merkle, block_height, block_hash, pos, against block header merkle_root
        • now client has an SPV guarantee that block only contains non-segwit txs, and tx in particular is also non-segwit (wtxid==txid) and it was mined in this block
      2. if there is a witness_commitment,
        • client must do witness-SPV, even if tx seems non-segwit (as e.g. malicious electrum server could have stripped away the witness)
        • client does witness-SPV using wmerkle and pos, against witness_commitment
        • client must check that length of wmerkle matches length of cb_proof. This ensures there are the same number of levels in the wtxid merkle tree and in the txid merkle tree. (the two trees should have the same number of leaves, but this seems hard to check as a light client) This is intended as some protection against merkle-tree-construction weaknesses (cf. banning of 64 byte transactions and similar).
    • now client has an SPV guarantee that wtxid was mined in this block
      • or if some check failed, then the client can connect to another server
  6. in src/rpc/txoutproof.cpp:124 in 3abfd6d3d9 outdated
    119             }
    120             if (ntxFound != setTxids.size()) {
    121                 throw JSONRPCError(RPC_INVALID_ADDRESS_OR_KEY, "Not all transactions found in specified or retrieved block");
    122             }
    123 
    124+            if (!any_witness) prove_witness = false;
    


    SomberNight commented at 0:50 am on July 1, 2025:

    My cpp is beyond rusty, so maybe I misunderstand, but note that just because the tx to be proved does not have a witness, we would still want a witness-SPV proof for that tx.

    The attack vector is that a malicious electrum server can notify the client about a tx touching their address and strip away the witness. That is, the actual tx has a witness, but the server sends it to the client using the old serialization format without it. Then when the client asks for the SPV proof, the server (still maliciously) would provide the old Satoshi-style SPV proof against the block header merkle root.

    The only way the client can tell that this attack is happening is by Satoshi-SPV-ing the coinbase tx, extracting the witness_commitment from it, and then doing witness-SPV on the tx.

    Basically whether to do witness-SPV or satoshi-SPV on a tx, the deciding factor is whether the coinbase contains a witness_commitment or not. If the client wants guarantees about the witness, it is immaterial whether the purported tx actually has a witness or not, the client must do witness-SPV if there is a witness_commitment. And as an extension of this logic, even though bitcoind knows for a fact whether the tx has a witness, to prove it to the client, it must provide the segwit-style witness-SPV proof.

  7. Sjors commented at 5:37 pm on July 1, 2025: member
    Concept ACK. @SomberNight it’s still useful if you open an issue to track the reason you need this, just in case this implementation PR doesn’t make.
  8. in src/consensus/validation.h:164 in 605b372b1f outdated
    160@@ -161,12 +161,12 @@ static inline int64_t GetTransactionInputWeight(const CTxIn& txin)
    161 }
    162 
    163 /** Compute at which vout of the block's coinbase transaction the witness commitment occurs, or -1 if not found */
    164-inline int GetWitnessCommitmentIndex(const CBlock& block)
    165+inline int GetWitnessCommitmentIndex(const CTransaction& tx)
    


    Sjors commented at 5:39 pm on July 1, 2025:
    605b372b1f62c4cd59d8056f0a6a1baf17c24014 nit: maybe rename tx to coinbase

    luke-jr commented at 3:49 am on July 9, 2025:
    Renamed to gentx (the coinbase is the input only)
  9. in src/consensus/validation.h:189 in 605b372b1f outdated
    180@@ -181,4 +181,12 @@ inline int GetWitnessCommitmentIndex(const CBlock& block)
    181     return commitpos;
    182 }
    183 
    184+inline int GetWitnessCommitmentIndex(const CBlock& block)
    185+{
    186+    if (block.vtx.empty()) {
    187+        return NO_WITNESS_COMMITMENT;
    188+    }
    189+    return GetWitnessCommitmentIndex(*block.vtx[0]);
    


    Sjors commented at 5:41 pm on July 1, 2025:
    605b372b1f62c4cd59d8056f0a6a1baf17c24014: *Assert(block)

    luke-jr commented at 7:44 pm on July 7, 2025:
    But block isn’t a pointer…?
  10. in src/consensus/validation.h:186 in 605b372b1f outdated
    180@@ -181,4 +181,12 @@ inline int GetWitnessCommitmentIndex(const CBlock& block)
    181     return commitpos;
    182 }
    183 
    184+inline int GetWitnessCommitmentIndex(const CBlock& block)
    185+{
    186+    if (block.vtx.empty()) {
    


    Sjors commented at 5:43 pm on July 1, 2025:

    605b372b1f62c4cd59d8056f0a6a1baf17c24014: to avoid confusion between an empty block and a pre-segwit block, could comment:

    0// Block without coinbase
    

    luke-jr commented at 7:45 pm on July 7, 2025:
    This is just a move, so seems out of scope
  11. in src/merkleblock.cpp:58 in 3abfd6d3d9 outdated
    53@@ -49,6 +54,21 @@ CMerkleBlock::CMerkleBlock(const CBlock& block, CBloomFilter* filter, const std:
    54             vMatch.push_back(false);
    55         }
    56         vHashes.push_back(hash);
    57+        if (prove_witness && i) {
    


    Sjors commented at 5:50 pm on July 1, 2025:

    3abfd6d3d99bbcceb09162e83d8f9f9769bd4036:

    0if (prove_witness) {
    1  if (i == 0) {
    2    // generation tx has null wtxid
    3    wtxids.emplace_back();
    4  } else {
    5    wtxids.push_back(block.vtx[i]->GetWitnessHash());
    6  }
    

    luke-jr commented at 8:26 pm on July 8, 2025:
    The null wtxid is already added at the top outside the loop.
  12. in src/merkleblock.h:131 in 3abfd6d3d9 outdated
    127@@ -128,6 +128,9 @@ class CMerkleBlock
    128     /** Public only for unit testing */
    129     CBlockHeader header;
    130     CPartialMerkleTree txn;
    131+    bool m_prove_gentx;
    


    Sjors commented at 5:52 pm on July 1, 2025:
    3abfd6d3d99bbcceb09162e83d8f9f9769bd4036: what does this do?

    luke-jr commented at 7:47 pm on July 7, 2025:
    An attempt to have a somewhat optional extension, but given the need to prove wtxid for non-witness txs, maybe not the best approach.

    luke-jr commented at 9:08 pm on July 8, 2025:

    Sorry, mixed that up with have_witness_proof.

    This is to explicitly specify that the proof is proving the generation tx itself, not just using it to prove the witness root.

  13. Sjors commented at 5:55 pm on July 1, 2025: member
    Took an initial look at the code, but got a bit confused. Will check again later.
  14. Docfix/RPC: txid is no longer the transaction hash 3004f2603f
  15. luke-jr force-pushed on Jul 9, 2025
  16. luke-jr marked this as ready for review on Jul 9, 2025
  17. Allow calling GetWitnessCommitmentIndex with just the generation transaction (not a CBlock) 528a8c9056
  18. luke-jr force-pushed on Jul 9, 2025
  19. luke-jr force-pushed on Jul 9, 2025
  20. luke-jr commented at 3:53 am on July 9, 2025: member
    Reworked this to cover @SomberNight ’s requirements and address feedback. Test is now passing.
  21. RPC/txoutproof: Support new proof format covering witness data 23edd3db4f
  22. luke-jr force-pushed on Jul 9, 2025
  23. luke-jr requested review from SomberNight on Jul 9, 2025
  24. luke-jr requested review from Sjors on Jul 9, 2025
  25. DrahtBot removed the label CI failed on Jul 9, 2025
  26. DrahtBot commented at 8:34 pm on August 13, 2025: contributor
    πŸ™ This pull request conflicts with the target branch and needs rebase.
  27. DrahtBot added the label Needs rebase on Aug 13, 2025

github-metadata-mirror

This is a metadata mirror of the GitHub repository bitcoin/bitcoin. This site is not affiliated with GitHub. Content is generated from a GitHub metadata backup.
generated: 2025-08-21 00:13 UTC

This site is hosted by @0xB10C
More mirrored repositories can be found on mirror.b10c.me