spring-util: SHiP log inspection/repair/split/merge tooling + nodeos state-history-force-write - #1895
Open
heifner wants to merge 1 commit into
Open
spring-util: SHiP log inspection/repair/split/merge tooling + nodeos state-history-force-write#1895heifner wants to merge 1 commit into
heifner wants to merge 1 commit into
Conversation
Give node operators in-box tooling for state history (SHiP) logs, which previously could only be touched by letting state_history_log's constructor auto-mutate them at nodeos startup, with anything beyond tail corruption being fatal (the capabilities requested in AntelopeIO/leap#1664, motivated by AntelopeIO/leap#1593). New primitives in libraries/state_history/log_utils.{hpp,cpp}: a genuinely read-only scanner that validates every entry's header, payload bounds, block continuity, and position trailer, optionally decompresses every payload (zlib's adler32 detects bit rot the structural checks cannot see), and resynchronizes past damage so one pass maps every valid and damaged region of a file; index verification and a sequential forward index builder that produces byte-identical output to the library's backward regeneration; and truncate, trim-front, extract, split, merge, and repair operations that rewrite the absolute position trailers wherever entries move. New spring-util subcommands built on them: ship-log info, block-id, smoke-test [--deep], make-index, trim, extract-blocks, repair [--keep-tail] [--dry-run] [--deep] [--output-dir], vacuum, split, and merge. repair's default truncates at the first damage like nodeos's automatic recovery, but offline, previewable, and able to rebuild the index; --keep-tail instead salvages the last valid range, starting at its canonical entry so fork-superseded prefixes are excluded, for the case where damage is early in a large log and recent history matters more. split produces rotation-compatible retained bundles that load directly into a log_catalog; merge reverses it. block-id prints the id a log records for any block number, trusting the on-disk index only after verifying it really holds that block, which is what diagnosing a "missed a fork change" failure needs. New nodeos option state-history-force-write: never let damaged or inconsistent ship logs stop the node. An index that disagrees with its log is regenerated instead of fatal; a head log that fails its startup checks or cannot accept the next block (a gap after a snapshot restore, divergent fork history) is renamed aside to <stem>-corrupt-<n> -- kept on disk, never deleted -- and writing continues into a fresh log, escalating to setting aside the retained bundles only when a write predates the whole catalog. Holes in the retained set are tolerated with a warning, leaving just those blocks unserved. The bundles this sets aside remain valid logs that ship-log can inspect, trim, and merge back. The three fork-change asserts in state_history_log::pack_and_write_entry now report the block being written, the previous id it carries, and the id actually recorded for the prior block; the index-backed path additionally decodes the recorded id's own block number and states that a block number mismatch means a corrupt index rather than a fork, pointing at 'spring-util ship-log block-id' and 'make-index'. ~state_history_log no longer lets a vacuum-on-close failure throw out of the destructor (which would std::terminate). Also fixes spring-util block-log print-log --print-from to be an option rather than a flag: a CLI11 flag only accepts its value in --print-from=<value> form, so the conventional space-separated spelling failed validation. And marks log_config.hpp's boost_test_print_type inline -- it is defined in a header that now reaches more than one translation unit per binary, which without inline is a multiple-definition link error. Tested by tests/ship_log_utils.cpp (33 cases: torn tails, mid-file damage, payload bit flips, fork-overwritten entries, pruned logs, index corruption variants, trim/extract/split/merge round-trips all verified through real state_history_log/log_catalog reopens, and every force-write tier) and tests/ship_log_util_test.py (against a real nodeos: corrupt, repair with spring-util, byte-compare against pristine files, relaunch, plus both force-write scenarios). Ported from Wire-Network/wire-sysio#392, where the same code was also validated against a 61 GB / 8.05M-block production chain_state log.
heifner
force-pushed
the
feature/ship-log-util
branch
from
August 26, 2026 13:05
38e1a11 to
7b622d8
Compare
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.
Gives node operators in-box tooling for state history (SHiP) logs — the capabilities requested in #1664, motivated by AntelopeIO/leap#1593 (corruption at block 112M with 8.1 TB of salvageable history after it, and no recovery short of a multi-month replay).
Problem
state_history_log's constructor auto-mutates: merely opening a log truncates a corrupt tail, regenerates a bad index, and vacuums pruned logs, so a damaged log cannot even be inspected without modifying it — and only via a full nodeos. Anything beyond tail corruption ("is corrupted and cannot be repaired") is fatal, discarding everything after the first bad byte even when terabytes of valid data follow.spring-util ship-log
All subcommands operate on a
--state-history-dir/--logpair, accept retained<stem>-N-Mbundles, and the read-only ones really are read-only (the scanner never goes throughstate_history_log).infoblock-idsmoke-test [--deep]--deepalso decompresses every payload (adler32 catches bit rot); prints a map of valid/damaged regionsmake-indextrim --first/--lastextract-blocksrepair [--keep-tail --dry-run --deep --output-dir]--keep-tailsalvages the last valid range instead — the leap#1593 casevacuumsplit --stridelog_catalogmergenodeos --state-history-force-write
Never let damaged or inconsistent ship logs stop the node. An index that disagrees with its log is regenerated instead of fatal; a head log that fails its startup checks or cannot accept the next block (a gap after a snapshot restore, divergent fork history) is renamed aside to
<stem>-corrupt-<n>— kept on disk, never deleted — and writing continues into a fresh log, escalating to setting aside the retained bundles only when a write predates the whole catalog. Holes in the retained set are tolerated with a warning, leaving just those blocks unserved. Orphaned bundles remain valid logs thatship-logcan inspect, repair, and trim (and merge back once renamed off the-corrupt-suffix).Better fork-change diagnostics
The three fork-change asserts in
state_history_log::pack_and_write_entrynow report the block being written, the previous id it carries, and the id actually recorded for the prior block; the index-backed path additionally decodes the recorded id's own block number and states that a block number mismatch means a corrupt index rather than a fork, pointing atspring-util ship-log block-idandmake-index. On the incident that motivated this it would have printed "the index resolves block 8028595 to id 000000021a92..., an id for block 2", turning a day of forensics into one log line.Implementation notes
libraries/state_history/log_utils.{hpp,cpp}next to the format they understand; the CLI is a thin binding inprograms/spring-util/actions/shiplog.{hpp,cpp}following theblock-logaction pattern.state_history_logcannot open safely.~state_history_logno longer lets a vacuum-on-close failure throw out of the destructor (which wouldstd::terminate).Drive-by fixes
spring-util block-log print-log --print-fromis now an option rather than a flag: a CLI11 flag only accepts its value in--print-from=<value>form, so the conventional space-separated spelling failed validation.log_config.hpp'sboost_test_print_typeis nowinline— it is defined in a header that this change makes reach more than one translation unit per binary, which withoutinlineis a multiple-definition link error.Validation
tests/ship_log_utils.cpp— 33 cases: torn tails, mid-file damage, payload bit flips, fork-overwritten entries, pruned logs, index corruption variants, trim/extract/split/merge round-trips all verified by reopening the results through realstate_history_log/log_cataloginstances, and every force-write tier.tests/ship_log_util_test.py— end-to-end against a real nodeos: corrupt, repair with spring-util, byte-compare against pristine files, relaunch, plus both force-write scenarios (lying index regenerated; gapped log orphaned while the head advances).plugin_testandship_restart_testpass unchanged.chain_statelog: full structural scan + slot-by-slot index verification in ~3 minutes; deep payload validation of a 3.5 GB trace log in 38 s; 100-block extract from the 61 GB log in 37 ms; split→merge of the 3.5 GB log round-trips to a byte-exact prefix of the original; corrupt-then-repair cycles left both the 61 GB log and its rebuilt index byte-identical to a pristine backup.