fuzz: add end-to-end liveness target for opportunistic 1p1c relay #36371

pull instagibbs wants to merge 10 commits into bitcoin:master from instagibbs:2026-09-1p1c-liveness-fuzz changing 14 files +1131 −55
  1. instagibbs commented at 4:14 PM on September 28, 2026: member

    Based on #36369; only the last commit is new here.

    Adds p2p_1p1c_liveness, a fuzz target for the property that if an honest peer announces the child of an acceptable low-fee parent and child, the child ends up in the mempool whatever other peers do.

    It drives a real PeerManager, with real P2P messages and validation, against fuzzer-controlled peers that announce by wtxid or txid, deliver witness-stripped, padded or otherwise malleated versions of the transactions, send notfound, stall, and reconnect. The existing download-manager targets model validation results, so they cannot check how real results are attributed across peers.

    Known gaps are listed in https://gist.github.com/instagibbs/880a76535fd802fced9f9a5253cdbb40: no witnessless or honest-bump scenarios yet, no trimming environment, and no tip changes.

  2. p2p: keep orphan after a package feerate failure
    When a 1p1c package fails the package feerate check, the child gets a
    TX_RECONSIDERABLE result, and ProcessPackageResult passed it to
    ProcessInvalidTx, which erased the child from the orphanage for all of
    its announcers.
    
    The failure is a property of that pair of wtxids, which
    MempoolRejectedPackage already caches, not of the child. The child can
    still succeed with another version of the parent, such as the one an
    honest announcer holds, so it should stay available.
    
    Skip ProcessInvalidTx for package members with a TX_RECONSIDERABLE
    result. A child that is too low feerate on its own is still rejected
    when its parent's work set is processed.
    c0d03dddb7
  3. DrahtBot added the label Fuzzing on Sep 28, 2026
  4. DrahtBot commented at 4:14 PM on September 28, 2026: contributor

    <!--e57a25ab6845829454e8d69fc972939a-->

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

    <!--006a51241073e994b41acfe9ec718e94-->

    External sites

    <!--021abf342d371248e50ceaed478a90ca-->

    Reviews

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

    <!--174a7506f384e20aa4161008e828411d-->

    Conflicts

    Reviewers, this pull request conflicts with the following ones:

    • #36318 (validation: Ensure Invalid ValidationState has result by optout21)
    • #36015 (txorphanage: bound orphan memory by storing transactions serialized by brunoerg)
    • #35713 (Remove boost as a unit test runner by rustaceanrob)
    • #35569 (Encapsulation for CTransaction by purpleKarrot)
    • #35502 (refactor: extract per-message helpers from ProcessMessage (move-only) by w0xlt)
    • #29700 (kernel, refactor: return error status on all fatal errors by ryanofsky)

    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.

    <!--5faf32d7da4f0f540f40219e4f7537a3-->

  5. instagibbs force-pushed on Sep 28, 2026
  6. DrahtBot added the label CI failed on Sep 28, 2026
  7. DrahtBot commented at 4:21 PM on September 28, 2026: contributor

    <!--85328a0da195eb286784d51f73fa0af9-->

    🚧 At least one of the CI tasks failed. <sub>Task lint: https://github.com/bitcoin/bitcoin/actions/runs/36449633823/job/109020752250</sub> <sub>LLM reason (✨ experimental): CI failed because the Python lint (ruff/py_lint) reported a fixable unused import error (test_framework.messages.msg_notfound) in test/functional/p2p_orphan_handling.py.</sub>

    <details><summary>Hints</summary>

    Try to run the tests locally, according to the documentation. However, a CI failure may still happen due to a number of reasons, for example:

    • Possibly due to a silent merge conflict (the changes in this pull request being incompatible with the current code in the target branch). If so, make sure to rebase on the latest commit of the target branch.

    • A sanitizer issue, which can only be found by compiling with the sanitizer and running the affected test.

    • An intermittent issue.

    Leave a comment here, if you need help tracking down a confusing failure.

    </details>

  8. instagibbs force-pushed on Sep 28, 2026
  9. test: 1p1c child survives package feerate failure from another announcer
    Check that when another announcer of the child delivers a witness-padded
    parent that fails the package feerate check, the child stays in the
    orphanage and the honest peer's package is still accepted, for both v2
    and TRUC packages.
    3be618c556
  10. p2p: don't cancel parent requests when a witnessless parent is rejected
    A TX_RECONSIDERABLE rejection forgets the transaction's wtxid in
    TxRequestTracker for all peers. For a transaction without witness data
    the wtxid equals the txid, which is the hash orphan resolution uses to
    request a missing parent. Such a rejection therefore cancelled every
    announcer's request for the parent, and the orphan stayed unresolved
    until a new announcer appeared or the parent confirmed.
    
    Keep those requests when the rejected transaction has no witness and an
    orphan spends it.
    d8dc948e13
  11. test: orphan resolution survives rejection of a witnessless parent
    Check that after another peer delivers a witnessless parent that is
    rejected as reconsiderable, the child's announcer is still asked for the
    parent and the package is accepted, for both a witness-stripped and a
    nonsegwit parent.
    ffc79605db
  12. fuzz: check that one peer's outcome doesn't cancel others' requests
    Add invariants to the txdownloadman_impl target: an event about one
    transaction or peer must not remove other peers' transaction requests or
    orphan announcements beyond that transaction's own hashes and entries.
    
    Uniform random operations almost never reach the states where this
    matters, so bias the harness towards orphans with missing parents and
    towards TX_MISSING_INPUTS and TX_RECONSIDERABLE results. Without the
    bias, reverting the witnessless-parent fix survived 57k runs.
    3b16af7e7a
  13. p2p: handle orphans by their actually missing parents
    Orphan handling treated every input's parent as missing: each was
    checked against the reject filters and requested from the orphan's
    announcers. A confirmed parent whose txid had entered the reject filter,
    which a rejected witnessless copy of it can cause, made the node drop
    every orphan spending one of its outputs.
    
    Have validation report which parents it found missing, and use only
    those when storing a new orphan. Parents that are present are no longer
    held against the orphan or requested.
    
    Update the orphan handling functional test, which expected present
    parents to be requested.
    c2d498bab6
  14. test: orphans are judged only by their actually missing parents
    Check that a child spending a zero-fee parent and a confirmed output is
    stored and resolved even after the confirmed transaction's txid enters
    the reject filter, and that only the missing parent is requested.
    
    Have the txdownloadman_impl fuzz target pass a random non-empty subset
    of parents as missing.
    bf6bbf5bde
  15. p2p: don't add already-known transactions to the reject filter
    TX_CONFLICT means the transaction is already known, in the mempool or
    confirmed, not that it is invalid. Its wtxid still went into the reject
    filter, and for a witnessless copy that is also the txid. If the known
    parent then leaves the mempool before the next block, through eviction
    or replacement, a child arriving afterwards is dropped as having a
    rejected parent.
    
    Don't add TX_CONFLICT results to the reject filter. The entry only saved
    re-downloading and re-checking a transaction we already have.
    
    Update the rejection-type table for TX_CONFLICT.
    c8e49749af
  16. test: known parent's stripped replay does not poison it across eviction
    Check that after a stripped copy of a mempool parent is rejected as a
    conflict and the parent is evicted, a child spending it is still stored
    as an orphan.
    32cf270494
  17. fuzz: add end-to-end liveness target for 1p1c relay
    Add a target for the property that if an honest peer announces the
    child of an acceptable low-fee parent and child, the child ends up in
    the mempool whatever other peers do. Children also spend a confirmed
    output, as a child paying its own fee does, and peers may replay that
    confirmed transaction.
    
    It drives a real PeerManager with real messages and validation against
    fuzzer-controlled peers that may deliver witness-malleated versions of
    the transactions, stall, send notfound or reconnect. The existing
    targets model validation results, so they cannot check how real results
    are attributed across peers.
    43ee8e2b76
  18. instagibbs force-pushed on Sep 28, 2026
  19. DrahtBot removed the label CI failed on Sep 28, 2026
  20. instagibbs commented at 6:31 PM on September 28, 2026: member

    @marcofleon could you throw a bit of cpu at this if you have the time

  21. marcofleon commented at 6:24 PM on October 1, 2026: contributor

    Haven't gotten around to looking into it much but

    <details> <summary>crash</summary>

    FUZZ=p2p_1p1c_liveness ./fuzzbuild/bin/fuzz crash-1373b980ed285e4076457461baafe0fa1ce6d68a 
    INFO: Running with entropic power schedule (0xFF, 100).
    INFO: Seed: 2116994025
    INFO: Loaded 1 modules   (417851 inline 8-bit counters): 417851 [0x560893f6f7f0, 0x560893fd582b), 
    INFO: Loaded 1 PC tables (417851 PCs): 417851 [0x560893fd5830,0x560894635be0), 
    ./fuzzbuild/bin/fuzz: Running 1 inputs 1 time(s) each.
    Running: crash-1373b980ed285e4076457461baafe0fa1ce6d68a
    ../../../../src/test/fuzz/p2p_1p1c_liveness.cpp:390 void p2p_1p1c_liveness_fuzz_target(FuzzBufferType): Assertion `child_accepted()' failed.
    ==2192686== ERROR: libFuzzer: deadly signal
        [#0](/bitcoin-bitcoin/0/) 0x5608929e27b4 in __sanitizer_print_stack_trace (/root/bitcoin/fuzzbuild/bin/fuzz+0xac87b4) (BuildId: fd9e87d1176e5516d4fb5c2cec2c7e817d02e645)
        [#1](/bitcoin-bitcoin/1/) 0x5608929b50c8 in fuzzer::PrintStackTrace() (/root/bitcoin/fuzzbuild/bin/fuzz+0xa9b0c8) (BuildId: fd9e87d1176e5516d4fb5c2cec2c7e817d02e645)
        [#2](/bitcoin-bitcoin/2/) 0x56089299a6c3 in fuzzer::Fuzzer::CrashCallback() (/root/bitcoin/fuzzbuild/bin/fuzz+0xa806c3) (BuildId: fd9e87d1176e5516d4fb5c2cec2c7e817d02e645)
        [#3](/bitcoin-bitcoin/3/) 0x7f075bf51a6f  (/usr/lib/x86_64-linux-gnu/libc.so.6+0x40a6f) (BuildId: c9a199fd28ea54b305ea35a8b25500a79bfe684a)
        [#4](/bitcoin-bitcoin/4/) 0x7f075bfa83bb  (/usr/lib/x86_64-linux-gnu/libc.so.6+0x973bb) (BuildId: c9a199fd28ea54b305ea35a8b25500a79bfe684a)
        [#5](/bitcoin-bitcoin/5/) 0x7f075bf51941 in raise (/usr/lib/x86_64-linux-gnu/libc.so.6+0x40941) (BuildId: c9a199fd28ea54b305ea35a8b25500a79bfe684a)
        [#6](/bitcoin-bitcoin/6/) 0x7f075bf394ab in abort (/usr/lib/x86_64-linux-gnu/libc.so.6+0x284ab) (BuildId: c9a199fd28ea54b305ea35a8b25500a79bfe684a)
        [#7](/bitcoin-bitcoin/7/) 0x560893047284 in assertion_fail(std::source_location const&, std::basic_string_view<char, std::char_traits<char>>) /root/bitcoin/fuzzbuild/src/util/../../../src/util/check.cpp:41:5
        [#8](/bitcoin-bitcoin/8/) 0x560892c25cee in bool&& inline_assertion_check<true, bool>(bool&&, std::source_location const&, std::basic_string_view<char, std::char_traits<char>>) /root/bitcoin/fuzzbuild/src/test/fuzz/../../../../src/util/check.h:93:13
        [#9](/bitcoin-bitcoin/9/) 0x560892c25cee in p2p_1p1c_liveness_fuzz_target(std::span<unsigned char const, 18446744073709551615ul>) /root/bitcoin/fuzzbuild/src/test/fuzz/../../../../src/test/fuzz/p2p_1p1c_liveness.cpp:390:5
        [#10](/bitcoin-bitcoin/10/) 0x560892e4676c in std::function<void (std::span<unsigned char const, 18446744073709551615ul>)>::operator()(std::span<unsigned char const, 18446744073709551615ul>) const /usr/lib/gcc/x86_64-linux-gnu/14/../../../../include/c++/14/bits/std_function.h:591:9
        [#11](/bitcoin-bitcoin/11/) 0x560892e4676c in test_one_input(std::span<unsigned char const, 18446744073709551615ul>) /root/bitcoin/fuzzbuild/src/test/fuzz/util/../../../../../src/test/fuzz/fuzz.cpp:86:5
        [#12](/bitcoin-bitcoin/12/) 0x560892e4676c in LLVMFuzzerTestOneInput /root/bitcoin/fuzzbuild/src/test/fuzz/util/../../../../../src/test/fuzz/fuzz.cpp:214:5
        [#13](/bitcoin-bitcoin/13/) 0x56089299bbe5 in fuzzer::Fuzzer::ExecuteCallback(unsigned char const*, unsigned long) (/root/bitcoin/fuzzbuild/bin/fuzz+0xa81be5) (BuildId: fd9e87d1176e5516d4fb5c2cec2c7e817d02e645)
        [#14](/bitcoin-bitcoin/14/) 0x56089298457f in fuzzer::RunOneTest(fuzzer::Fuzzer*, char const*, unsigned long) (/root/bitcoin/fuzzbuild/bin/fuzz+0xa6a57f) (BuildId: fd9e87d1176e5516d4fb5c2cec2c7e817d02e645)
        [#15](/bitcoin-bitcoin/15/) 0x56089298a63f in fuzzer::FuzzerDriver(int*, char***, int (*)(unsigned char const*, unsigned long)) (/root/bitcoin/fuzzbuild/bin/fuzz+0xa7063f) (BuildId: fd9e87d1176e5516d4fb5c2cec2c7e817d02e645)
        [#16](/bitcoin-bitcoin/16/) 0x5608929b5ae2 in main (/root/bitcoin/fuzzbuild/bin/fuzz+0xa9bae2) (BuildId: fd9e87d1176e5516d4fb5c2cec2c7e817d02e645)
        [#17](/bitcoin-bitcoin/17/) 0x7f075bf3af74  (/usr/lib/x86_64-linux-gnu/libc.so.6+0x29f74) (BuildId: c9a199fd28ea54b305ea35a8b25500a79bfe684a)
        [#18](/bitcoin-bitcoin/18/) 0x7f075bf3b026 in __libc_start_main (/usr/lib/x86_64-linux-gnu/libc.so.6+0x2a026) (BuildId: c9a199fd28ea54b305ea35a8b25500a79bfe684a)
        [#19](/bitcoin-bitcoin/19/) 0x56089297f070 in _start (/root/bitcoin/fuzzbuild/bin/fuzz+0xa65070) (BuildId: fd9e87d1176e5516d4fb5c2cec2c7e817d02e645)
    
    NOTE: libFuzzer has rudimentary signal handlers.
          Combine libFuzzer with AddressSanitizer or similar for better crash reports.
    SUMMARY: libFuzzer: deadly signal
    

    </details>

    p2p-1p1c-liveness-input.txt

    base64:

    IwAAAAAALgAAAAAAAB8uzQBcTQAAAAAAACj+gL6sgiguAwD/dA7fFElVUSz/psEyPl9zHR0dGR0dHWxfZw1mIwLKzLblzaMZGk9TroshBo6PmfJA9YbEi91SHU4loA7LJjIAXiEA////AF8xMi0lVXM0MZFdEoFVX/E=
    

    Can look some more tomorrow.

  22. instagibbs commented at 6:27 PM on October 1, 2026: member

    @marcofleon thanks for running this, will take a look myself soon

  23. instagibbs commented at 3:34 PM on October 2, 2026: member

    @marcofleon Caught a known defficiency that wasn't excepted in the harness, that's a good sign I think!

    Bot repro:

      Trace (v2 cast with the 10-sat parent, honest peer 0 outbound, adversaries 1–3):
      1. Peer 1 announces the child's wtxid.
      2. Peer 2 sends the child. It is missing its parent, so it goes into the orphanage, with peers 2 and 1
         recorded as announcers.
      3. Peer 2 sends the parent with a smaller valid witness. It now clears the min relay feerate on its own
         and is accepted. AddChildrenToWorkSet picks one announcer at random to retry the child and picks peer
         1, which sets m_reconsider on peer 1's announcement.
      4. Peer 1 disconnects before its work set is processed. EraseForPeer erases that announcement, and Erase
         drops the wtxid from m_reconsiderable_wtxids. Peer 2's announcement remains but isn't marked, so no
         peer will ever retry the child.
      5. Peer 0 (honest) announces the child. AddTxAnnouncement finds it in the orphanage and goes down the
         orphan-resolution path. That path only requests parents. The parent is already in the mempool, so the
         only "missing" parent it finds is the confirmed FUND tx. It requests FUND from peer 0, gets notfound,
         and nothing else happens. The child is never requested from peer 0, since we already hold it.
    
      The child stays in the orphanage with all its inputs available until it's evicted. The comment in
      AddChildrenToWorkSet names this risk ("we don't want to create an issue in which the assigned peer can
      purposefully stop us from processing the orphan by disconnecting"), but nothing handles it. EraseForPeer
      is identical on master (69142eacd1).
    

    We should probably just patch it for real to make the harness correct rather than patch the harness.


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-10-04 04:51 UTC

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