test: cover missing traversal bits in rpc_txoutproof #34780

pull tonykim525 wants to merge 1 commits into bitcoin:master from tonykim525:test-rpc-txoutproof-invalid-partial-merkle changing 1 files +11 −2
  1. tonykim525 commented at 0:28 am on March 10, 2026: none

    Add a malformed-proof test case to rpc_txoutproof.py that removes the traversal bits from an otherwise valid CMerkleBlock proof and checks that verifytxoutproof rejects it.

    The existing test already covers one tweaked invalid proof, but it still preserves traversal metadata in the partial merkle tree.

    This case is intended to cover a different invalid-proof class: malformed proofs where the traversal metadata itself is missing or unusable.

    In CPartialMerkleTree, the tree is encoded via a depth-first traversal that stores one flag bit per visited node. Those flag bits are part of the serialized format, and decoding consumes them together with the hashes to reconstruct the matched path (src/merkleblock.h). That means vBits is not incidental metadata, but part of the proof structure required for decoding. If those traversal bits are missing, the partial merkle tree cannot be decoded correctly.

    This is also an explicitly validated failure mode in the implementation. ExtractMatches() requires at least one bit per node in the partial tree, and at least one node per hash, rejecting cases where vBits.size() < vHash.size() (src/merkleblock.cpp). So this is not just an artificial mutation, but a concrete malformed-proof class that the decoder is expected to reject.

    This test removes vBits entirely rather than truncating them partially on purpose. A partially truncated vBits vector can be ambiguous as a test input, because decoding consumes bits as needed by the traversal in TraverseAndExtract(), and the final validation only checks that all bits were consumed except for the byte padding introduced by serialization in ExtractMatches() (src/merkleblock.cpp). Depending on the proof shape, removing only a suffix of bits may still leave a proof decodable. By contrast, emptying vBits in this otherwise valid proof makes the malformed input unambiguous and triggers the rejection path deterministically.

    The goal of this test is to cover that malformed-proof rejection path through verifytxoutproof at the functional/RPC level. This adds coverage that the RPC-facing path handles this invalid proof class correctly.

    Test

    • python3 -m ruff check test/functional/rpc_txoutproof.py
    • python3 -m py_compile test/functional/rpc_txoutproof.py
    • python3 build/test/functional/test_runner.py rpc_txoutproof.py
  2. test: add malformed-proof coverage in rpc_txoutproof e78bd9489a
  3. DrahtBot added the label Tests on Mar 10, 2026
  4. DrahtBot commented at 0:28 am on March 10, 2026: contributor

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

    Reviews

    See the guideline for information on the review process. A summary of reviews will appear here.

  5. maflcko commented at 6:37 am on March 10, 2026: member
    Which mutation testing mutant is this killing? Was this LLM generated? What are the steps to test this? What is the output before and after the changes here?
  6. tonykim525 commented at 9:05 am on March 10, 2026: none

    It checks the failure path in CPartialMerkleTree::ExtractMatches (src/merkleblock.cpp), specifically the if (vBits.size() < vHash.size()) condition (line 163), to verify that it correctly returns the failure hash value (0) via return uint256();(Covered the rejection path).

    If vBits.size() < vHash.size(), then vBits is shorter than vHash, which means the structure is invalid. The proof claims to contain hashes, but provides even fewer traversal bits to interpret them, so this models a case where even the minimum traversal information required to decode those hashes is missing.

    So this checks that, rather than descending further, the code correctly executes the failure return path.

    In other words, this coverage verifies that partial merkle tree reconstruction is aborted and that, instead of producing a valid merkle root, the code returns the zero hash so that the caller treats the proof as invalid.

    By setting vBits = [], the test covers the case where the tree traversal path (vBits) is empty or otherwise missing. vBits = [] directly hits the most extreme missing/insufficient length case among them. Since the minimum possible size is 0, whether the bits are effectively absent or missing, this deterministically satisfies vBits.size() < vHash.size().

    By contrast, in the existing assert_invalid_proof(tweaked_proof) test, what is being checked is not specifically that the bits are empty or insufficient, but more generally that a modified proof is ultimately rejected as invalid. So I do not think it overlaps with the coverage added by my test.


    No. I did not use an LLM to write this code. I looked through the project TODOs, found a place I could contribute to, and wrote the change myself. I only used a translator for some help with the PR text.


    To validate this change, I first identified that rpc_txoutproof.py did not directly cover the malformed-proof case where traversal bits are missing.

    I then started from an otherwise valid proof, deserialized it into CMerkleBlock, and modified it by setting txn.vBits = [] so that it would deterministically hit the if (vBits.size() < vHash.size()) rejection path in CPartialMerkleTree::ExtractMatches.

    Before finalizing the test, I first checked locally that this malformed proof is rejected by intentionally inverting the assertion (assert n.verifytxoutproof(invalid_proof_hex)), which failed as expected. I then kept the assert_invalid_proof(...) check to verify that rejection path and ran:

    python3 build/test/functional/test_runner.py rpc_txoutproof.py

    This passed locally, including the new malformed-proof case.


    This does not change runtime behavior or RPC output; it only adds test coverage. Before this change, rpc_txoutproof.py did not directly check a malformed-proof case with broken vBits. After this change, it adds a test that verifies such a proof is rejected.

  7. maflcko commented at 10:15 am on March 10, 2026: member

    TODOs

    Ok, I see. I just wonder what is missing to fully fix this todo. cc @brunoerg do you have a set of mutants for src/merkleblock.cpp?

  8. brunoerg commented at 1:41 pm on March 10, 2026: contributor

    TODOs

    Ok, I see. I just wonder what is missing to fully fix this todo. cc @brunoerg do you have a set of mutants for src/merkleblock.cpp?

    No, but I can do a mutation analysis for src/merkleblock.cpp.

  9. brunoerg commented at 4:21 pm on March 10, 2026: contributor

    TODOs

    Ok, I see. I just wonder what is missing to fully fix this todo. cc @brunoerg do you have a set of mutants for src/merkleblock.cpp?

    No, but I can do a mutation analysis for src/merkleblock.cpp.

    update: got basically 100% of mutation score by just running the unit tests, there is no remaining mutant to be addressed.

  10. sedited commented at 4:55 pm on March 10, 2026: contributor
    This additional test does not meaningfully improve the coverage of the rpc endpoint int the first place, so I think this should be closed.
  11. fanquake closed this on Mar 10, 2026

  12. tonykim525 deleted the branch on Mar 10, 2026

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: 2026-03-16 03:13 UTC

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