Skip to content

snapshot: check that every reflected field is restored by the importer - #187

Merged
On1x merged 1 commit into
masterfrom
guard/snapshot-import-coverage
Oct 26, 2026
Merged

On1x merged 1 commit into
masterfrom
guard/snapshot-import-coverage

Conversation

@On1x

@On1x On1x commented Oct 26, 2026

Copy link
Copy Markdown
Member

Why

#184 restored four snapshot fields by hand. Nothing in the build or the tests would have stopped the next one from being forgotten the same way — and nothing had caught these two:

НАХОДКА pm_oracle_object (plugin.cpp:822): не присвоено banned_by, active_markets

pm_oracle_object.banned_by and pm_oracle_object.active_markets have been in the struct and in FC_REFLECT since HF14 (#124), so every snapshot written since contains them — the importer never copied them. On a restore a banned oracle comes back with an empty banned_by, and pm_unban matches on that field, so the ban can never be lifted; the active_markets gauge is seeded once from dgpo.pm_active_markets_seeded, which does not re-run, so it stays at 0 for good.

What

scripts/check_snapshot_import_coverage.py — for every object the snapshot plugin creates or modifies from the snapshot file, compare the fields listed in FC_REFLECT with the fields the importer body assigns, and name what is missing, with its plugin.cpp line.

  • Export is reflection-based (fc::to_variant), so FC_REFLECT is the file's field set; the importer is hand-written, and that is where fields disappear.
  • FC_REFLECT / FC_REFLECT_DERIVED are parsed from libraries/chain/include/** and libraries/chain/hardfork.d/*.hf (the hardfork object lives there), with inheritance resolved.
  • Comments and string literals are stripped before parsing, line numbers preserved — a commented-out obj.field = … must not count as an assignment.
  • Bodies copied with fc::from_variant are exempt by construction (reflection copies everything).
  • Assignment evidence is broader than obj.f = …: also container fills (obj.buffer.resize(), obj.flat_set.insert()) and shared_string setters.
  • db.modify() bodies count only if they read the snapshot variant. The in-place repairs (LIB promotion, ballot reconciliation, self-healing penalties) must not look like restore evidence, or a field missing from the importer would appear covered.
  • Types with no FC_REFLECT are listed in NO_REFLECT_EXPORTED with a reason (proposal_object: custom export because of buffer_type / bip::flat_set). A type that is missing reflection and missing from that table makes the run blind (2), not clean.

Codes: 0 every reflected field is assigned, 1 a field is missing, 2 blindness (nothing parsed, or a type could not be judged).

plugins/snapshot/plugin.cpp — import banned_by and active_markets under the same contains() guard as the neighbouring optional fields.

.github/workflows/tests.yml — run the guard, selftest first, as the second step of consensus-and-pm: a second, and it needs no submodules.

Proof

Scope boundary

The guard judges what FC_REFLECT describes. proposal_object is exported by hand as well, so it is out of scope by name, not by silence: a hand-written exporter can still drop a new field on both sides, and catching that needs the same comparison on the export side. That is not part of this PR.

A snapshot is exported by reflection, but the importer is written by hand,
field by field. A field added to a chain object and forgotten in the importer
is dropped silently on restore -- exactly what happened to the four PM seed
flags in #184: a restored node re-seeded on its first block and its later
snapshots disagreed with the network.

scripts/check_snapshot_import_coverage.py compares, for every object the
snapshot plugin creates or modifies from the file, the fields listed in
FC_REFLECT against the fields the body assigns; objects copied with
fc::from_variant are exempt by construction. db.modify() bodies that repair
in place (LIB promotion, ballot reconciliation, self-healing) are not counted
as import evidence.

The same script finds two more fields of the same class on master:
pm_oracle_object.banned_by and pm_oracle_object.active_markets, both present
since HF14 and never copied, so a restore made a ban un-liftable and reset the
live-market gauge to zero for good. Imported here under the same contains()
guard as the neighbouring optional fields, so the check can gate CI.

Codes: 0 every reflected field is assigned, 1 a field is missing (named with
its plugin.cpp line), 2 blindness (a type could not be judged, e.g. no
FC_REFLECT and not in the declared hand-written list) -- "could not check" is
never reported as clean. --selftest proves the arms and both blindness paths.

tested: selftest 5/5 green; on master the script names the four #184 flags;
on the #184 tree it names only banned_by/active_markets; with the banned_by
assignment removed from this tree it names only banned_by.
@On1x
On1x merged commit e85104e into master Oct 26, 2026
3 of 6 checks passed
@On1x
On1x deleted the guard/snapshot-import-coverage branch October 26, 2026 11:58
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant