Skip to content

Add --check-lcm mode to verify captured LedgerCloseMeta golden data in CI - #5435

Open
sisuresh wants to merge 10 commits into
stellar:masterfrom
sisuresh:check-lcm
Open

Add --check-lcm mode to verify captured LedgerCloseMeta golden data in CI#5435
sisuresh wants to merge 10 commits into
stellar:masterfrom
sisuresh:check-lcm

Conversation

@sisuresh

Copy link
Copy Markdown
Contributor

Adds a --check-lcm DIRNAME test mode mirroring --check-test-tx-meta: each index.json under test-lcm-{current,next}/ is stamped with the protocol version / rng seed that produced it (validated up front, so a protocol bump without regeneration fails fast), and the LCM captured at each leaf section is compared against the checked-in golden file. Wired into ci-build.sh as an extra make check pass reusing the already-built binary.

The first CI run on this PR is expected to fail the new check — the existing golden data predates the header stamp. A follow-up commit regenerates it under both build configurations.

Copilot AI balanced review requested due to automatic review settings August 28, 2026 19:06

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Adds CI verification for captured LedgerCloseMeta golden data.

Changes:

  • Adds --check-lcm with header validation and semantic comparison.
  • Stamps captured indexes with protocol and RNG configuration.
  • Adds CI integration and documentation.

Reviewed changes

Copilot reviewed 5 out of 5 changed files in this pull request and generated 2 comments.

Show a summary per file
File Description
src/test/test.cpp Implements LCM checking and header validation.
src/test/test.h Updates LCM tracking documentation.
ci-build.sh Adds the CI golden-data check.
docs/software/commands.md Documents --check-lcm.
.github/copilot-instructions.md Documents CI enforcement.
Suppressed comments (1)

ci-build.sh:226

  • The repository's required test invocation flags include --abort, but this newly added CI pass omits it. Add the flag so an ordinary Catch failure stops the pass immediately rather than spending the remainder of this expensive [tx] run after the outcome is known.
export STELLAR_CORE_TEST_PARAMS="--ll fatal -r simple --disable-dots --rng-seed 12345 --check-lcm ${SRC_DIR}"

💡 Configure MCP servers for context-aware, tailored reviews. Learn more in the docs.

Comment thread ci-build.sh
# protocol version only.
echo Running fixed check-lcm tests
export TEST_SPEC='[tx]'
export STELLAR_CORE_TEST_PARAMS="--ll fatal -r simple --disable-dots --rng-seed 12345 --check-lcm ${SRC_DIR}"

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The regenerated golden trees (with the header stamps) landed in later commits on this PR — 50f847b for test-lcm-current and b18addf for test-lcm-next. The first CI round intentionally ran without them to demonstrate the failure mode.

Comment thread src/test/test.cpp
Comment on lines +385 to +389
if (startIndex >= allMetas.size())
{
// Capture mode would not have written a file for this leaf.
return;
}

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Good catch — fixed. The no-meta path now records a failure when a golden file exists for the leaf: capture mode never writes files for no-meta leaves, so an existing file means the test used to produce LCM and no longer does. This mirrors how the tx-meta check catches disappearance via un-consumed hashes at end of run.

Copilot AI review requested due to automatic review settings August 28, 2026 19:51

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 9 out of 1636 changed files in this pull request and generated no new comments.

Suppressed comments (1)

src/test/test.cpp:394

  • If a leaf previously produced a golden LCM but now produces none, this return treats it as success and leaves the existing golden file unchecked. That allows loss of LCM capture to pass CI; record a failure when the expected file exists.

Copilot AI review requested due to automatic review settings August 28, 2026 21:06

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 9 out of 1638 changed files in this pull request and generated 1 comment.

Suppressed comments (1)

Previously missed (1) — in code that hasn't changed since the last review.

src/test/test.cpp:394

  • --check-lcm silently succeeds when a leaf that has a checked-in golden file stops producing any LCM. This early return skips both the existence check and comparison, so a regression that removes a closeLedger/capture call is not detected. If the golden path exists, record a mismatch before returning; leaves that have never had golden data can still be ignored.

Comment thread docs/software/commands.md Outdated
Comment on lines +235 to +238
Meta is normalized (sorted) before writing so that output is
deterministic given a fixed `--rng-seed`.
deterministic given a fixed `--rng-seed`. Each `index.json` is stamped
with the protocol version, rng seed and protocol-version list that
produced the data.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed in f6be648 — the docs now say files preserve original entry order (with diagnostics zeroed) and that normalization happens only in the comparison copies, both for the capture-time skip-rewrite check and for --check-lcm.

Copilot AI review requested due to automatic review settings August 28, 2026 22:01

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 8 out of 1637 changed files in this pull request and generated 2 comments.

Suppressed comments (1)

docs/software/commands.md:238

  • This still states that meta is normalized before writing, but writeLcmToFile explicitly preserves original ordering and only normalizes copies for semantic comparison. Update the description so users do not expect the checked-in XDR stream itself to be sorted.

Comment thread src/test/test.h
Comment on lines +38 to +40
// Returns true if --capture-lcm or --check-lcm was passed to the test
// command. When enabled, LedgerCloseMeta from closeLedger/closeLedgerOn is
// accumulated and, at leaf section boundaries (or test case end for tests

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed in f6be648 — the TxTests.h declaration comment now mentions both --capture-lcm and --check-lcm.

Comment thread src/test/test.cpp
Comment on lines +1477 to +1480
in >> root;
for (auto const* key : {TESTKEY_PROTOCOL_VERSION, TESTKEY_RNG_SEED,
TESTKEY_ALL_VERSIONS, TESTKEY_VERSIONS_TO_TEST})
{

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Acknowledged as a known limitation, matching the tx-meta baseline flow: under partitioned make check, a single process cannot distinguish 'leaf not run in this partition' from 'leaf deleted', and there is no cross-process aggregation point, so unconsumed-entry detection would need infrastructure that doesn't exist today. Stale files/index entries are inert (never checked against anything), and the dangerous variant — a test that still runs but stops producing meta — does fail via the golden-exists-but-no-meta check. A sequential garbage-collect mode for capture could prune orphans; leaving that for a follow-up.

Copilot AI review requested due to automatic review settings August 28, 2026 22:31

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 8 out of 1637 changed files in this pull request and generated no new comments.

Suppressed comments (1)

src/test/test.cpp:1519

  • This only reports mismatches for leaves that executed. loadLcmHeaders discards every hash entry, so deleting a captured section/test (or removing its [tx] tag) leaves the old index/XDR entry unconsumed and CI still succeeds. Track expected hashes and fail on leftovers; since make check partitions tests, either aggregate consumption across partitions or run this check unpartitioned.

Copilot AI review requested due to automatic review settings August 28, 2026 23:11

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 8 out of 1636 changed files in this pull request and generated 1 comment.

Comment thread ci-build.sh
# protocol version only.
echo Running fixed check-lcm tests
export TEST_SPEC='[tx]'
export STELLAR_CORE_TEST_PARAMS="--ll fatal -r simple --disable-dots --rng-seed 12345 --check-lcm ${SRC_DIR}"

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Intentional, matching the check-test-tx-meta pass above it: for golden-data checks we want the run to continue and aggregate every mismatch into one end-of-run report (that is how multiple issues were diagnosed in single CI runs while iterating on this PR), rather than abort at the first failure. An ordinary test failure still fails make check via the nonzero exit.

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 9 out of 1637 changed files in this pull request and generated no new comments.

Copilot AI review requested due to automatic review settings September 2, 2026 21:17

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 12 out of 1978 changed files in this pull request and generated 1 comment.

Suppressed comments (1)

src/test/test.cpp:1594

  • When --prune-stale-lcm is not set, this still replaces index.json with only the entries observed in the current run. A filtered capture that touches one test file therefore drops mappings for all unrun leaves in that file and leaves their retained .xdr files orphaned, contrary to the documented non-pruning behavior. Merge existing non-header mappings when pruning is disabled, and rebuild from scratch only when gLcmMayPrune is true.

Comment on lines +89 to +92
// Entries injected straight into the bucket list appear in the ledger
// without any transaction producing them, so the resulting meta is
// not a faithful record of how the state came to be.
taintLcmCapture("injects ledger entries directly into the bucket list");

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed in 85f8eda — setNextArchiveBatchForBucketTesting now applies the same taint.

Copilot AI review requested due to automatic review settings September 2, 2026 21:26

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 12 out of 1978 changed files in this pull request and generated 3 comments.

Suppressed comments (1)

src/bucket/test/BucketTestUtils.h:92

  • This only taints the live-bucket setter. setNextArchiveBatchForBucketTesting also injects archive/restored entries directly (BucketTestUtils.cpp:247-248), and the addLiveBatchAndUpdateSnapshot / addHotArchiveBatchAndUpdateSnapshot helpers directly modify bucket state as well. Those paths can still produce golden LCM that omits how its starting state was created, despite the documented automatic exclusion; apply the taint hook to every direct-injection path.

Comment thread src/test/test.cpp Outdated
Comment on lines 330 to 331
std::ofstream out(dir + "/index.json", std::ios_base::trunc);
out << writer.write(root);

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed in 94644d2 — the index write now validates the stream, enables exceptions and closes explicitly, matching saveTestTxMeta.

Comment thread src/test/test.cpp Outdated
finalizeLcmCapture()
{
size_t pruned = 0;
for (auto const& [dir, entries] : gLcmCapturedIndex)

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed in 94644d2 — visited output directories are now tracked independently of captured entries, so a test file whose leaves have all become ineligible is still visited. Verified by planting a stale .xdr in such a directory: the full prune removes the file, the index and the directory, rather than leaving one holding only header keys.

Comment thread src/test/test.cpp Outdated
size_t pruned = 0;
for (auto const& [dir, entries] : gLcmCapturedIndex)
{
writeLcmIndex(dir, entries);

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Good catch, this was the worst of the batch — it defeated the reason pruning is opt-in. Fixed in 94644d2: existing non-header entries are merged when not pruning, and the index is replaced outright only in prune mode. Verified that a single-test capture of InvokeHostFunctionTests leaves all 680 index entries intact.

Copilot AI review requested due to automatic review settings September 2, 2026 22:16

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Copilot reviewed 12 out of 1978 changed files in this pull request and generated no new comments.

Copilot AI review requested due to automatic review settings September 3, 2026 21:37

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Copilot reviewed 13 out of 1979 changed files in this pull request and generated no new comments.

…n CI

The test-lcm-current/ and test-lcm-next/ trees were write-only: nothing
recorded what protocol version (or seed) produced them and nothing in CI ever
read them back, so a protocol bump silently invalidated both tiers.

Mirror the --check-test-tx-meta flow: stamp each index.json with the same
header keys the tx-meta baselines use ('!cfg protocol version', '!rng seed',
'!test all versions', '!versions to test'), validate them up front in check
mode so stale data fails fast, and compare the LCM captured at each leaf
section against the golden file, reporting mismatches at end of run.

Wire the check into ci-build.sh as an extra make check pass reusing the
binary already built for the check-test-tx-meta run.
--check-lcm surfaced that 'Soroban classic account authentication' had two
sibling SECTIONs named 'wrong key type' (silently sharing one golden file,
last writer wins) and two identical SECTIONs named 'missing signature field'.
Rename the former to 'wrong signature key type' and drop the redundant twin
of the latter. Capture and check now fail when two distinct sections would
produce the same golden file, for leaves that actually accumulate LCM.

Dropping a section means the test case re-runs once less, so the parent
contexts of that test accumulate one fewer set of setup-tx hashes; both
tx-meta baseline tiers are re-recorded accordingly, changing by exactly the
25 hashes the check reported as un-checked.
Review feedback: the no-meta early return let a regression that eliminates
expected LedgerCloseMeta pass silently. Capture never writes a file for a
no-meta leaf, so an existing golden means the meta disappeared.
--base-instance exists to keep OS-level resources (ports, databases,
directories) distinct across concurrent test processes, but it also shifted
NODE_SEED, whose identity lands in the signed StellarValue of ledger headers
and cascades through header hashes, previousLedgerHash chains and txSetHash.
Golden LCM captured at base instance 0 therefore failed --check-lcm under
partitioned CI runs (which use --base-instance j*50).

Only LCM capture/check runs need instance-invariant node identity, so the
change is scoped to them; every other run keeps the historical
offset-derived seed, and base instance 0 derivation is unchanged.
Files are written in original entry order with diagnostics zeroed;
normalization happens only in comparisons. Accumulation happens under
--check-lcm as well as --capture-lcm.
updateLcmIndex read-merge-wrote each index.json and writeLcmToFile never
deleted anything, so leaves that stopped being produced kept their files and
index entries forever: the checked-in data still carried 272 protocol 26 and
27 leaves from regens predating the protocol 28 bump, and every bump stacked
another set.

Capture now accumulates its index entries in memory and, at end of a clean
run, rewrites each touched directory's index from scratch and (with
--prune-stale-lcm) deletes .xdr files the run did not write. This mirrors
saveTestTxMeta, which rebuilds each baseline JSON from what the run observed
rather than merging into the existing file. Pruning is opt-in because a
filtered run visits only some leaves, and is per touched directory only,
matching the granularity of one baseline file per test file.
taintLcmCapture marks a test ineligible when it injects ledger entries
straight into the bucket list or the archive (the meta never shows the
entries being created), runs a multi-node Simulation, or uses a config whose
ledger content depends on thread scheduling or randomized nomination.
Capture skips those leaves with a named warning; --check-lcm fails if golden
data for them still exists, so a vector from before a test became ineligible
cannot linger.

The multi-node herder simulation is excluded by these rules rather than by
hand, so if that simulation is ever made deterministic its vector comes back
for free.
Three defects in the prune pass, all found in review:

Without --prune-stale-lcm the index was still rewritten from only the
entries the run observed, so a filtered capture dropped the hash-to-name
mappings of untouched leaves while leaving their .xdr files behind,
unidentifiable. Existing non-header entries are now merged when not
pruning; the index is replaced outright only in prune mode.

The prune pass iterated the directories that produced entries, so a test
file whose leaves had all become ineligible was never visited and kept its
stale goldens through a full-corpus prune. Visited output directories are
now tracked independently, and such a directory has its index and itself
removed rather than being left holding only header keys.

The index write checked neither that the file opened nor that the final
flush succeeded, so an unwritable path could leave a truncated index while
capture reported success. It now validates the stream, enables exceptions
and closes explicitly, as saveTestTxMeta does.
The TxMeta baselines have a section explaining how to re-record them; the
golden LCM data had nothing outside the command reference, which is part of
why it went stale at protocol 27 for a month without anyone noticing.
Both tiers were captured before the protocol 28 bump, so test-lcm-current
held protocol 27 meta and test-lcm-next held protocol 28. Re-captured at 28
and 29 respectively, with the index.json header stamps the check validates.

The regeneration also drops what had accumulated: leaves left behind by
regens at protocol 26 and 27, leaves orphaned by the section renames, and
the CAP-85 vector, whose test injects the executable-reference entry
directly into the bucket list so the meta never showed the entry being
created. Each tier goes from 1131 files to 844.
Copilot AI review requested due to automatic review settings September 3, 2026 22:05

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 13 out of 1979 changed files in this pull request and generated 1 comment.

Comment thread src/test/test.cpp
return prefix + lcmTierDirName() + "/" + file.filename().stem().string();
}

int32_t
@sisuresh
sisuresh requested review from dmkozh and jayz22 September 9, 2026 17:55

@dmkozh dmkozh left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Just a general thought to consider, not necessary for this PR. The existing test tx meta that we capture is basically just a subset of the full LCM, so we shouldn't need to keep both around given the CI integration. Also tx meta has pretty poor coverage for Soroban, as IIRC it mostly captures the classic transactions. Another benefit of the LCM capture is that it seems to be more parallel-friendly; I don't believe it runs with NUM_PARTITIONS>1 out of the box, but it doesn't seem too hard to make it do so, as the meta is now captured per scenario (instead of a single huge file). We'll still need to merge the index files, but that's hopefully not that hard to do.

Comment thread src/test/test.h
// reason; --check-lcm fails if golden data for them still exists, so a
// stale or misleading vector cannot linger. Calling this when LCM tracking
// is off is free, so hooks may call it unconditionally.
void taintLcmCapture(std::string const& reason);

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nit: this name is both too fancy and imprecise (because it disables meta and it does not spoil it). This should be just be called 'disableLcmCapture', and the underlying global variable should be called correspondingly.

Comment thread src/test/test.h
// Marks the running test as unsuitable for LedgerCloseMeta golden data,
// naming the reason (e.g. injecting ledger entries straight into the bucket
// list, or running a multi-node simulation whose progression depends on
// thread scheduling). Capture skips such a test's leaves and logs the

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is reason logging achieving anything? We're anyways leaving comments around this, so I'm not sure if additional logging is necessary.

"!rng seed" : 12345,
"!test all versions" : false,
"!versions to test" : [ 29 ],
"17846470c1f6514e" : "sponsor_future_reserves-protocol_version_29-add_sponsored_entry_before_adding_first_sponsored_signer",

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nit: Now that we're making this more productionized, I figured we might revisit the test name format. IIRC this semi-normalized format has been used for the raw test names, but since we need to hash the names anyways, can we use the proper human readable names, like the name format in txmeta (Soroban classic account authentication|account with required multisig etc.).

Comment thread src/test/test.cpp
META_TEST_CHECK
};

// Declared here (rather than with the other test globals below) because the

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I don't understand this comment, if it needs to to move above some code, why not just declare it inside namespace without comments (declaration order justification is not particularly interesting information)

Comment thread src/test/test.cpp
// In check mode the golden data lives under the directory passed to
// --check-lcm (the source tree root when building out-of-tree); in
// capture mode it is written relative to the current directory.
std::string prefix = gLcmCheckDir.empty() ? "" : gLcmCheckDir + "/";

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nit: it's weird that we mix path with strings (especially the manual "/").

REQUIRE(singleInvocation(signer, baseCredentials) ==
InvokeHostFunctionResultCode::INVOKE_HOST_FUNCTION_TRAPPED);
}
SECTION("missing signature field")

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why did you remove this? We should just rename the sections to make the names unique.

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.

3 participants