Skip to content

test(consensus): cut the reward-halving vanishing case to milliseconds - #2949

Merged
gzliudan merged 1 commit into
XinFinOrg:dev-upgradefrom
gzliudan:test-reward-halving-fast
Nov 4, 2026
Merged

gzliudan merged 1 commit into
XinFinOrg:dev-upgradefrom
gzliudan:test-reward-halving-fast

Conversation

@gzliudan

@gzliudan gzliudan commented Oct 30, 2026 •

Copy link
Copy Markdown
Collaborator

Summary

TestRewardHalvingVanishing walked the whole mainnet reward curve to watch it die out: 11,225,088 epochs × 3 roles × FloatPower ≈ 7.7e8 big.Float multiplications.

 TestRewardHalvingVanishing
-  for i in 0..11,225,088           # walk every epoch of the mainnet curve
-    reward = Σ roles RewardHalving(role, T, H, i)
-    if reward == 0: log the breakpoint, stop
-    sum += reward
-  assert sum < H
+  for i in 0..~3,450               # walk the whole curve of a scaled-down supply
+    reward = Σ roles RewardHalving(role, T', H', i)
+    if reward == 0: assert vanished, stop
+    sum += reward
+  assert sum < H'
+  assert RewardHalving(a1, T, H, 11225087) > 0     # mainnet breakpoint: two calls
+  assert RewardHalving(a1, T, H, 11225088) == 0

The series converges to the supply for any parameter pair - Σ a·q^i = a·H/T with q = 1 − T/H, and Σ roles a = T, gives H - so T/H only sets how fast the curve dies, while the two properties the case protects (the reward vanishes, the issuance stays below the supply) are scale-free. Shrinking T/H moves the breakpoint from 11,225,088 epochs to 3,450, and mainnet's own breakpoint is now asserted directly instead of being reached by accumulation.

Evidence

  • Before: go test -run '^TestRewardHalvingVanishing$' ./consensus/tests/engine_v2_tests → 24.7 s on an idle machine; under -race ~13 min, past the -timeout 10m the engine_v2 race lanes pass, so the lane died on it: run 40990118093, job 123850259002 (panic: test timed out after 10m0s, 6m25s into this case).
    After: the same command → 0.01 s; the same under -race → 1.1 s for the package's two reward cases. The package's -short run goes 12.1 s → 11.5 s.

Three mutations of the new assertions were checked, each restored afterwards:

mutation result
breakpoint moved one epoch later (11225088 → 11225087) FAIL: the 10,000 XDC reward shall pay nothing from epoch 11,225,088, got 1
breakpoint moved one epoch earlier FAIL: the 10,000 XDC reward shall still be paid at epoch 11,225,087, got 0
supply scaled to 6e18 so the small curve never vanishes FAIL: reward should be 0 before epoch 100000, issued 1404281543 of 6000000000 wei

Merge Danger

Door: two-way

The change is confined to one test function. eth/util.RewardHalving, FloatPower and the schedules they compute are untouched, so a revert restores the old case with no migration path to unwind.

Blast Radius: test-only

Mainnet's numbers are no longer replayed epoch by epoch: the scaled-down sweep covers the same invariant, and the mainnet breakpoint (11,225,088) is asserted rather than merely logged as before. This is the case that made the race (engine_v2 2/3) lane of #2912 time out; the other red lane there is the pre-existing Forensics.SetCommittedQCs write/write race (#2945, fixed by #2946), which this PR does not touch.

Types of changes

  • test: Adding missing tests or correcting existing tests

Impacted Components

  • Consensus

Checklist

  • This PR has sufficient test coverage (unit/integration test) OR I have provided reason in the PR description for not having test coverage
  • N/A - test-only change: no node behaviour, no genesis or config schema, no backwards-compatibility surface, and nothing to verify on a private network or devnet.

@coderabbitai

coderabbitai Bot commented Oct 30, 2026 •

Copy link
Copy Markdown

Important

Review skipped

Auto reviews are disabled on base/target branches other than the default branch.

Please check the settings in the CodeRabbit UI or the .coderabbit.yaml file in this repository. To trigger a single review, invoke the @coderabbitai review command.

Run configuration
  • Source mode: Auto
  • Repository: Unavailable
  • Branch: Unavailable
  • File: No configuration file from this repository applied
  • Commit: Unavailable
  • Layers: defaults
  • Review profile: CHILL
  • Plan: Advanced
  • Run ID: d12f2b51-e58d-4557-ace5-892235061544

You can disable this status message by setting the reviews.review_status to false in the CodeRabbit configuration file.

Use the checkbox below for a quick retry:

  • Trigger review
  • Autofix · Keep fixing CodeRabbit findings and required CI, and resolving merge conflicts

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

Share

@gzliudan
gzliudan force-pushed the test-reward-halving-fast branch 2 times, most recently from ceb11ec to 592bf0b Compare November 1, 2026 10:00
gzliudan added a commit to gzliudan/XDPoSChain that referenced this pull request Nov 1, 2026
`go test ./consensus/tests/engine_v2_tests` was the slowest package in the
tree at 219s; everything else under consensus finishes in under two seconds.
The time went to building fixtures (~72s for ~83k blocks at ~0.9ms each),
waiting on real countdown and mine periods (~70s), and fixed sleeps (~40s). A
fourth quarter - 24s walking 11M epochs of big.Float reward math - is not
touched here; it lands separately as XinFinOrg#2949.

- Timing knobs: the fixture now deep-clones the chain config and shortens the
  V2 periods (countdown 4/4/5s -> 1/1/2s, mine 2/3s -> 1/2s). Round 10 stays
  strictly above round 0 because TestIsYourTurnConsensusV2CrossConfig needs a
  larger post-switch period. Assertions read the fixture's own config instead
  of the package-level one.
- Waits: the two pool-hygiene tests slept 5s each for a hygiene pass that is
  synchronous, and those sleeps are gone. The forensics test keeps its 5s
  sleep: the goroutine it waits for writes without synchronisation (XinFinOrg#2945), so
  polling would trade a hidden race for a reported one rather than removing
  either - XinFinOrg#2946 fixes the write itself. Negative windows shrink to one
  countdown period.
- Reads: at one-second periods the countdown keeps broadcasting while a test
  waits for a vote, so the reads that follow a negative window - and every read
  whose message is then used as a vote - skip timeout and sync-info
  broadcasts, and expectNoVote drains whatever sender is already parked on the
  unbuffered channel when its window closes. Without that, a stale timeout
  reached a vote type assertion and panicked.
- Parallelism: 99 of the 108 tests call t.Parallel(). Nine stay serial: two
  count messages across consecutive periods, two read the global
  skipped-proposed-block counter, two install a global logger, and three keep
  their skip-path siblings in proposed_block_test.go company.

Verified: 219s -> ~67s on a 12-core box (the reward-halving quarter is the
rest, see XinFinOrg#2949), three back-to-back runs and two -race runs clean (the -race
soak is what surfaced the lastWrite race fixed in the previous commit).
gzliudan added a commit to gzliudan/XDPoSChain that referenced this pull request Nov 2, 2026
`go test ./consensus/tests/engine_v2_tests` was the slowest package in the
tree at 219s; everything else under consensus finishes in under two seconds.
The time went to building fixtures (~72s for ~83k blocks at ~0.9ms each),
waiting on real countdown and mine periods (~70s), and fixed sleeps (~40s). A
fourth quarter - 24s walking 11M epochs of big.Float reward math - is not
touched here; it lands separately as XinFinOrg#2949.

- Timing knobs: the fixture now deep-clones the chain config and shortens the
  V2 periods (countdown 4/4/5s -> 1/1/2s, mine 2/3s -> 1/2s). Round 10 stays
  strictly above round 0 because TestIsYourTurnConsensusV2CrossConfig needs a
  larger post-switch period. Assertions read the fixture's own config instead
  of the package-level one.
- Waits: the two pool-hygiene tests slept 5s each for a hygiene pass that is
  synchronous, and those sleeps are gone. The forensics test keeps its 5s
  sleep: the goroutine it waits for writes without synchronisation (XinFinOrg#2945), so
  polling would trade a hidden race for a reported one rather than removing
  either - XinFinOrg#2946 fixes the write itself. Negative windows shrink to one
  countdown period.
- Reads: at one-second periods the countdown keeps broadcasting while a test
  waits for a vote, so the reads that follow a negative window - and every read
  whose message is then used as a vote - skip timeout and sync-info
  broadcasts, and expectNoVote drains whatever sender is already parked on the
  unbuffered channel when its window closes. Without that, a stale timeout
  reached a vote type assertion and panicked.
- Parallelism: 99 of the 108 tests call t.Parallel(). Nine stay serial: two
  count messages across consecutive periods, two read the global
  skipped-proposed-block counter, two install a global logger, and three keep
  their skip-path siblings in proposed_block_test.go company.

Verified: 219s -> ~67s on a 12-core box (the reward-halving quarter is the
rest, see XinFinOrg#2949), three back-to-back runs and two -race runs clean (the -race
soak is what surfaced the lastWrite race fixed in the previous commit).
gzliudan added a commit that referenced this pull request Nov 2, 2026
…2944)

* fix(core): track the last flushed state block per chain

lastWrite was a package-level variable, so every BlockChain in a process
shared one flush high-water mark. That is wrong whenever more than one chain
is live in the same process - the test fixtures build one chain per test - and
it is a plain data race once those chains insert blocks concurrently, which
`go test -race` reports at writeBlockWithState.

Move it onto BlockChain, matching upstream geth (`bc.lastWrite`). The flush
throttle now compares against the high-water mark of the chain it is flushing,
which is what the surrounding code already assumed (bc.gcproc, bc.cachedb,
bc.triedb are all per-chain).

The executed-marker fixtures used to save and restore the package variable.
With the field there is nothing to restore - each case builds and drops its own
chain - so those save/restore pairs, and the comments explaining them, are
gone. The one pin a fixture still needs (lastWrite = TriesInMemory) stays, now
on the chain that pins it.

* test(consensus): cut engine v2 suite wall time from 219s to 67s

`go test ./consensus/tests/engine_v2_tests` was the slowest package in the
tree at 219s; everything else under consensus finishes in under two seconds.
The time went to building fixtures (~72s for ~83k blocks at ~0.9ms each),
waiting on real countdown and mine periods (~70s), and fixed sleeps (~40s). A
fourth quarter - 24s walking 11M epochs of big.Float reward math - is not
touched here; it lands separately as #2949.

- Timing knobs: the fixture now deep-clones the chain config and shortens the
  V2 periods (countdown 4/4/5s -> 1/1/2s, mine 2/3s -> 1/2s). Round 10 stays
  strictly above round 0 because TestIsYourTurnConsensusV2CrossConfig needs a
  larger post-switch period. Assertions read the fixture's own config instead
  of the package-level one.
- Waits: the two pool-hygiene tests slept 5s each for a hygiene pass that is
  synchronous, and those sleeps are gone. The forensics test keeps its 5s
  sleep: the goroutine it waits for writes without synchronisation (#2945), so
  polling would trade a hidden race for a reported one rather than removing
  either - #2946 fixes the write itself. Negative windows shrink to one
  countdown period.
- Reads: at one-second periods the countdown keeps broadcasting while a test
  waits for a vote, so the reads that follow a negative window - and every read
  whose message is then used as a vote - skip timeout and sync-info
  broadcasts, and expectNoVote drains whatever sender is already parked on the
  unbuffered channel when its window closes. Without that, a stale timeout
  reached a vote type assertion and panicked.
- Parallelism: 99 of the 108 tests call t.Parallel(). Nine stay serial: two
  count messages across consecutive periods, two read the global
  skipped-proposed-block counter, two install a global logger, and three keep
  their skip-path siblings in proposed_block_test.go company.

Verified: 219s -> ~67s on a 12-core box (the reward-halving quarter is the
rest, see #2949), three back-to-back runs and two -race runs clean (the -race
soak is what surfaced the lastWrite race fixed in the previous commit).

* test(consensus): assert the applied config, not the message mix

TestTimeoutPeriodAndThreadholdConfigChange read three broadcasts, triggered a
parameter update, read five more, and demanded exactly six timeouts and two
syncInfos. That split is not a property of the engine: it depends on where the
update lands inside a countdown period. It held at the old two-second period
and stopped holding once this branch shortened the periods to one and two
seconds - the same countdown produced five timeouts and three syncInfos, with
no dropped message and no failed signature (zero "authentication needed" lines,
zero unknown message types, the same nine timeouts and four threshold events).

Read with a deadline instead. Each read must produce a message within
messageTimeout (ten seconds, several periods), so a dropped broadcast still
fails and names the counters, and the five reads after the update stay bounded
by twenty seconds.

What the update itself must do is now asserted where it is observable: the
engine's own current config has to be the round-900 one (threshold 4,
two-second period). The engine keeps its own copy of the chain config, so a
GetCurrentConfigFaker() accessor sits next to the other test fakers. That is
the assertion which catches an UpdateParams that stops applying the config -
stubbing it out drops the old six/two split to five/three, exactly the bracket
this rewrite replaces, and the relaxed reads alone would not notice.

Verified on this branch with the runner's shape (four cores pinned and
saturated, -race, GOMAXPROCS=4): before, two of three runs failed with
"expected 6, actual 5"; after, ten of ten pass. Dropping a message still fails:
draining the channel and reading past the deadline reports "no message in
10s (timeout=0 syncinfo=0)".

* test(core/vm): keep the privacy tests out of the default suite

The privacy precompiles are unreachable - RING_SIG_VERIFY and
BULLET_PROOF_VERIFY have never been called on mainnet or Apothem (#2895) - and
the package is scheduled for deletion, so its 27 crypto cases cost the default
suite four seconds and the detector 328s for code nothing reaches. #2912 drops
the last CI job that ran them.

They are now behind -tags=privacytests, so `go test ./...` reports the package
as having no tests, and the cases stay runnable on demand:

	go test -tags=privacytests ./core/vm/privacy

The package still builds, because core/vm imports it for the two precompiles.
@gzliudan
gzliudan force-pushed the test-reward-halving-fast branch from 592bf0b to bb66798 Compare November 2, 2026 23:16
The case walked 11,225,088 epochs of big.Float arithmetic to reach the point where the geometric schedule pays nothing: 24.7 s on an idle machine and about 13 min under the race detector, past the 10m timeout the race lanes pass - the engine_v2 2/3 lane died on it (XinFinOrg#2912).

The series converges to the halving supply for 0 < epochRewardTotal < halvingSupply - the reward/supply ratio only sets how fast - so the sweep now runs on a scaled-down supply whose whole curve is 3,450 epochs, and asserts the same two properties: the per-epoch reward dies out and what is issued stays below the supply. Mainnet's numbers are checked at the two epochs the walk pinned down (11,225,087 pays 1 wei, 11,225,088 pays nothing), which costs two calls.

The -short gate goes away with the cost: the case runs in 10 ms.

The scaled curve's break point is asserted too (epoch 3,450), so a constant typo cannot turn the sweep into a no-op, and the log line that reports the break point now prints only once the reward is actually gone.
@gzliudan
gzliudan force-pushed the test-reward-halving-fast branch from bb66798 to 4421c5b Compare November 4, 2026 15:30
@gzliudan
gzliudan merged commit 3eff2d3 into XinFinOrg:dev-upgrade Nov 4, 2026
23 checks passed
@gzliudan
gzliudan deleted the test-reward-halving-fast branch November 4, 2026 15:38
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.

4 participants