mirror of
https://github.com/luxfi/evm.git
synced 2026-09-24 00:27:44 +00:00
evm: extract nextBuildTime pure helper + regression test for build pacing
Decomplect the build-pacing decision out of waitForEvent into a pure nextBuildTime(earliestBuild, lastBuildTime) function (no clock read), so the logic the network-halt fix depends on is deterministically testable. The wall-clock comparison stays in waitForEvent. TestNextBuildTime covers all 5 cases (first-build paces to target rate; past earliestBuild builds immediately; target rate dominates retry floor; retry floor prevents hot-loop; boundary tie-break). TestNextBuildTime_NeverEarlier ThanTargetRate locks the invariant the fix relies on: the next build is NEVER scheduled before parent.Time+TargetBlockRate, so a too-early high-fee block can never re-introduce the small-tip stall.
This commit is contained in:
@@ -122,13 +122,7 @@ func (b *blockBuilder) waitForEvent(ctx context.Context, earliestBuild time.Time
|
||||
if err != nil {
|
||||
return block.Message{}, err
|
||||
}
|
||||
nextBuild := earliestBuild
|
||||
if !lastBuildTime.IsZero() {
|
||||
if retry := lastBuildTime.Add(minBlockBuildingRetryDelay); retry.After(nextBuild) {
|
||||
nextBuild = retry
|
||||
}
|
||||
}
|
||||
timeUntilNextBuild := time.Until(nextBuild)
|
||||
timeUntilNextBuild := time.Until(nextBuildTime(earliestBuild, lastBuildTime))
|
||||
if timeUntilNextBuild <= 0 {
|
||||
return block.Message{Type: block.PendingTxs}, nil
|
||||
}
|
||||
@@ -141,6 +135,27 @@ func (b *blockBuilder) waitForEvent(ctx context.Context, earliestBuild time.Time
|
||||
}
|
||||
}
|
||||
|
||||
// nextBuildTime is the earliest wall-clock instant the next block may be built.
|
||||
// It is the later of two floors:
|
||||
// - earliestBuild: parent.Time + TargetBlockRate — building before this raises
|
||||
// the block's required fee, so a small-tip tx cannot cover it and the block
|
||||
// is rejected by verifyBlockFee (the stall this pacing exists to prevent).
|
||||
// - lastBuildTime + minBlockBuildingRetryDelay: a retry floor that keeps a
|
||||
// failed BuildBlock from hot-looping once earliestBuild has already passed.
|
||||
//
|
||||
// A zero lastBuildTime (no block built this session yet) means only earliestBuild
|
||||
// applies. Pure function of its inputs (no clock read) so the pacing decision is
|
||||
// deterministically testable; the wall-clock comparison stays in waitForEvent.
|
||||
func nextBuildTime(earliestBuild, lastBuildTime time.Time) time.Time {
|
||||
if lastBuildTime.IsZero() {
|
||||
return earliestBuild
|
||||
}
|
||||
if retry := lastBuildTime.Add(minBlockBuildingRetryDelay); retry.After(earliestBuild) {
|
||||
return retry
|
||||
}
|
||||
return earliestBuild
|
||||
}
|
||||
|
||||
// waitForNeedToBuild waits until needToBuild returns true.
|
||||
// It returns the last time a block was built.
|
||||
func (b *blockBuilder) waitForNeedToBuild(ctx context.Context) (time.Time, error) {
|
||||
|
||||
@@ -0,0 +1,103 @@
|
||||
// Copyright (C) 2019-2026, Lux Industries, Inc. All rights reserved.
|
||||
// See the file LICENSE for licensing terms.
|
||||
|
||||
package evm
|
||||
|
||||
import (
|
||||
"testing"
|
||||
"time"
|
||||
)
|
||||
|
||||
// TestNextBuildTime locks in the block-build pacing decision (the fix for the
|
||||
// network-wide block-production stall: building before parent.Time+TargetBlockRate
|
||||
// raises the block fee above what a small-tip tx can cover, so verifyBlockFee
|
||||
// rejects the block and the chain never advances).
|
||||
//
|
||||
// nextBuildTime returns the later of:
|
||||
// - earliestBuild (parent.Time + TargetBlockRate), and
|
||||
// - lastBuildTime + minBlockBuildingRetryDelay (retry floor, only when a block
|
||||
// was already built this session).
|
||||
func TestNextBuildTime(t *testing.T) {
|
||||
base := time.Unix(1_700_000_000, 0)
|
||||
|
||||
tests := []struct {
|
||||
name string
|
||||
earliestBuild time.Time
|
||||
lastBuildTime time.Time
|
||||
want time.Time
|
||||
}{
|
||||
{
|
||||
// First build of the session (lastBuildTime zero): pace purely to the
|
||||
// target rate. This is the path every frozen chain takes on its first
|
||||
// post-bootstrap block — it MUST honor earliestBuild, never fire early.
|
||||
name: "first build paces to earliestBuild",
|
||||
earliestBuild: base.Add(2 * time.Second),
|
||||
lastBuildTime: time.Time{},
|
||||
want: base.Add(2 * time.Second),
|
||||
},
|
||||
{
|
||||
// earliestBuild already in the past + no prior build: build now
|
||||
// (returned time is in the past, so time.Until <= 0). This is the
|
||||
// healthy catch-up case (a chain idle longer than the block rate).
|
||||
name: "first build with past earliestBuild is immediate",
|
||||
earliestBuild: base.Add(-time.Hour),
|
||||
lastBuildTime: time.Time{},
|
||||
want: base.Add(-time.Hour),
|
||||
},
|
||||
{
|
||||
// Target rate dominates the retry floor: a freshly built block must
|
||||
// still wait the full target rate, not just the retry floor.
|
||||
name: "target rate dominates retry floor",
|
||||
earliestBuild: base.Add(2 * time.Second),
|
||||
lastBuildTime: base.Add(-50 * time.Millisecond),
|
||||
want: base.Add(2 * time.Second),
|
||||
},
|
||||
{
|
||||
// earliestBuild already passed but we JUST built: the retry floor
|
||||
// prevents a hot-loop. Returns lastBuildTime + retry delay.
|
||||
name: "retry floor prevents hot-loop after earliestBuild passed",
|
||||
earliestBuild: base.Add(-time.Second),
|
||||
lastBuildTime: base,
|
||||
want: base.Add(minBlockBuildingRetryDelay),
|
||||
},
|
||||
{
|
||||
// Exact boundary: retry floor equal to earliestBuild is NOT "after",
|
||||
// so earliestBuild is returned (deterministic tie-break, no drift).
|
||||
name: "retry floor equal to earliestBuild yields earliestBuild",
|
||||
earliestBuild: base.Add(minBlockBuildingRetryDelay),
|
||||
lastBuildTime: base,
|
||||
want: base.Add(minBlockBuildingRetryDelay),
|
||||
},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
got := nextBuildTime(tt.earliestBuild, tt.lastBuildTime)
|
||||
if !got.Equal(tt.want) {
|
||||
t.Fatalf("nextBuildTime(%v, %v) = %v, want %v",
|
||||
tt.earliestBuild, tt.lastBuildTime, got, tt.want)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
// TestNextBuildTime_NeverEarlierThanTargetRate is the property the stall fix
|
||||
// depends on: regardless of the prior build time, the next build is NEVER
|
||||
// scheduled before earliestBuild (the target-rate floor). If this ever returns
|
||||
// a time before earliestBuild, a block could be built too soon, its fee would
|
||||
// exceed a small-tip tx's budget, and the chain would stall again.
|
||||
func TestNextBuildTime_NeverEarlierThanTargetRate(t *testing.T) {
|
||||
earliest := time.Unix(1_700_000_000, 0)
|
||||
for _, deltaMs := range []int{-10_000, -100, -1, 0, 1, 100, 10_000} {
|
||||
last := earliest.Add(time.Duration(deltaMs) * time.Millisecond)
|
||||
got := nextBuildTime(earliest, last)
|
||||
if got.Before(earliest) {
|
||||
t.Fatalf("nextBuildTime scheduled %v BEFORE earliestBuild %v (lastBuildTime=%v) — would re-introduce the fee stall",
|
||||
got, earliest, last)
|
||||
}
|
||||
}
|
||||
// And the zero-value (first build) case must equal earliestBuild exactly.
|
||||
if got := nextBuildTime(earliest, time.Time{}); !got.Equal(earliest) {
|
||||
t.Fatalf("first-build nextBuildTime = %v, want earliestBuild %v", got, earliest)
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user