Repository navigation
mempool: save and reload parents before their children - #960
Merged
Merged
Conversation
dump_mempool wrote mempool.dat in the pool's map order, and load_mempool re-admitted records in order of their one-second entry time, keeping file order on a tie. A child that entered in the same second as its parent was often tried first, refused for missing inputs, and its descendants after it, so a restart could drop most of the saved mempool. Bitcoin Core writes the file from infoAll(), which is in topological order (src/txmempool.cpp, GetSortedScoreWithTopology), and LoadMempool admits in file order (src/node/mempool_persist.cpp). satd now writes the file oldest-first with every parent ahead of its children, and on load runs the same stable dependency pass over the records after the time sort, so a file written by 0.6.0 in arbitrary order also reloads in full. The pass is one depth-first walk over the in-file spend edges, linear in inputs. The file format is unchanged. Fixes #949. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
This was referenced Oct 14, 2026
Closed
bkeroack
added a commit
that referenced
this pull request
Oct 15, 2026
dump_mempool wrote mempool.dat in the pool's map order, and load_mempool re-admitted records in order of their one-second entry time, keeping file order on a tie. A child that entered in the same second as its parent was often tried first, refused for missing inputs, and its descendants after it, so a restart could drop most of the saved mempool. Bitcoin Core writes the file from infoAll(), which is in topological order (src/txmempool.cpp, GetSortedScoreWithTopology), and LoadMempool admits in file order (src/node/mempool_persist.cpp). satd now writes the file oldest-first with every parent ahead of its children, and on load runs the same stable dependency pass over the records after the time sort, so a file written by 0.6.0 in arbitrary order also reloads in full. The pass is one depth-first walk over the in-file spend edges, linear in inputs. The file format is unchanged. Fixes #949. Co-authored-by: Claude Opus 5.5 (1M context) <noreply@anthropic.com> (cherry picked from commit 1899cf2)
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Fixes #949
Problem
dump_mempoolwrotemempool.datin the pool's hash-map order (get_all_entries()), andload_mempoolre-admitted the records sorted by entrytimeonly, which has one-second resolution, keeping file order on a tie. A child that entered the pool in the same second as its parent was often tried first, refused withmissing-inputs, and every descendant behind it failed too. The error was discarded, so the only trace was theskipped=count. On a synced mainnet node, two clean restarts on 0.6.0 re-admitted 15,825 of 46,711 and 13,799 of 36,121 saved transactions (details in #949).Bitcoin Core writes the file in an order where parents precede children and loads it in file order:
DumpMempooltakespool.infoAll()(src/node/mempool_persist.cpp:169) and writes it as is (:196).infoAll()(src/txmempool.cpp:600) returnsGetSortedScoreWithTopology()(src/txmempool.cpp:572), sorted by the txgraph's main order, which is topological (through v28,infoAllusedGetSortedDepthAndScore, ancestor count first: v28.0src/txmempool.cpp:830).LoadMempooladmits each record as it reads it (src/node/mempool_persist.cpp:79onward), with no reordering.Fix
parent_first_orderinnode/src/mempool/persist.rs: a stable topological order of a list of transactions. One iterative depth-first pass over the spend edges whose parent is also in the list, so it is linear in the number of inputs and does not recurse on the call stack. Unrelated transactions keep their input order. A duplicate txid links its children to the first copy; nothing is dropped.(time, txid)and then put parent-first before they are written, so the file is in Core's shape: parents before children, oldest first otherwise.prioritisetransactiondeltas are applied before admission, expired records are skipped, and loading stays linear (one hash map and one pass; no retry loop).mempool.datformat is unchanged.Tests
New file
node/src/mempool/persist_reload_order_tests.rs(a module ofpersist.rs; the existing test helpers there becamepub(super)):dump_writes_same_second_parents_before_children_and_all_reloadrecord N comes before its parent at record M(file-order assertion)load_admits_a_file_written_children_first(accepted, skipped, expired)assertionload_admits_a_parent_stamped_later_than_its_child(accepted, skipped)assertionload_still_skips_a_child_whose_parent_is_missingparent_first_order_keeps_unrelated_records_in_placeparent_first_order_reorders_a_reversed_familyparent_first_order_handles_a_long_reversed_chain_and_duplicatesPerturbation, each run on the node lib
mempool::persisttests:dump_writes_same_second_parents_before_children_and_all_reloadfails (file-order assertion; the load pass alone would still reload it, which is why the test checks the file).load_admits_a_file_written_children_firstandload_admits_a_parent_stamped_later_than_its_childfail.parent_first_orderremoved: five tests fail, including bothparent_first_order_*ordering tests.Local gate:
cargo clippy --all-targets --all-features -D warnings,cargo clippy -p node --no-default-features --all-targets -D warnings,cargo test -p node --lib,cargo test -p satd --test regtest -- test_savemempool test_node_restart_persistence, and the--features e2etest build.Notes
load_mempoolstill counts skips without saying why; that is a separate observability change.CHANGELOG.mdbullet and a### A restart keeps the whole mempoolentry under Mempool indocs/release-notes/0.6.1-pre.md.Release notes
A clean restart could drop most of the saved mempool:
mempool.datwas re-admitted in order of one-second entry time and written in no particular order, so a child saved in the same second as its parent was often tried first and refused, along with its descendants. satd now writes the file parents first, as Bitcoin Core does, and on load puts every transaction after any parent in the same file, so amempool.datsaved by 0.6.0 also reloads in full. The file format is unchanged.🤖 Generated with Claude Code