Skip to content

Commit aa9a2cc

Browse files
committed
apply min blok delay to block builder
1 parent a857a64 commit aa9a2cc

File tree

5 files changed

+303
-30
lines changed

5 files changed

+303
-30
lines changed

RELEASES.md

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -4,9 +4,11 @@
44

55
- Removed deprecated flags `coreth-admin-api-enabled`, `coreth-admin-api-dir`, `tx-regossip-frequency`, `tx-lookup-limit`. Use `admin-api-enabled`, `admin-api-dir`, `regossip-frequency`, `transaction-history` instead.
66
- Enabled RPC batch limits by default, and configurable with `batch-request-limit` and `batch-max-response-size`.
7-
- Implement ACP-226: Set expected block gas cost to 0 in Granite network upgrade, removing block gas cost requirements for block building.
8-
- Implement ACP-226: Add `timeMilliseconds` (Unix uint64) timestamp to block header for Granite upgrade.
9-
- Implement ACP-226: Add `minDelayExcess` (uint64) to block header for Granite upgrade.
7+
- ACP-226:
8+
- Set expected block gas cost to 0 in Granite network upgrade, removing block gas cost requirements for block building.
9+
- Add `timeMilliseconds` (Unix uint64) timestamp to block header for Granite upgrade.
10+
- Add `minDelayExcess` (uint64) to block header for Granite upgrade.
11+
- Add minimum block building delays to conform ACP-226 requirements to the block builder.
1012
- Update go version to 1.24.7
1113

1214
## [v0.15.3](https://github.com/ava-labs/coreth/releases/tag/v0.15.3)

plugin/evm/block_builder.go

Lines changed: 94 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@ import (
1010

1111
"github.com/ava-labs/avalanchego/snow"
1212
"github.com/ava-labs/avalanchego/utils/lock"
13+
"github.com/ava-labs/avalanchego/utils/timer/mockable"
1314
"github.com/ava-labs/libevm/common"
1415
"github.com/ava-labs/libevm/core/types"
1516
"github.com/ava-labs/libevm/log"
@@ -18,17 +19,29 @@ import (
1819

1920
"github.com/ava-labs/coreth/core"
2021
"github.com/ava-labs/coreth/core/txpool"
22+
"github.com/ava-labs/coreth/params/extras"
23+
"github.com/ava-labs/coreth/plugin/evm/customheader"
24+
"github.com/ava-labs/coreth/plugin/evm/customtypes"
2125
"github.com/ava-labs/coreth/plugin/evm/extension"
2226

2327
commonEng "github.com/ava-labs/avalanchego/snow/engine/common"
2428
)
2529

26-
// Minimum amount of time to wait after building a block before attempting to build a block
27-
// a second time without changing the contents of the mempool.
28-
const MinBlockBuildingRetryDelay = 500 * time.Millisecond
30+
const (
31+
// Minimum amount of time to wait after building a block before attempting to build a block
32+
// a second time without changing the contents of the mempool.
33+
PreGraniteMinBlockBuildingRetryDelay = 500 * time.Millisecond
34+
// Minimum amount of time to wait after attempting/build a block before attempting to build another block
35+
// This is only applied for retrying to build a block after a minimum delay has passed.
36+
// The initial minimum delay is applied according to parent minDelayExcess (if available)
37+
// TODO (ceyonur): Decide whether this a correct value.
38+
PostGraniteMinBlockBuildingRetryDelay = 100 * time.Millisecond
39+
)
2940

3041
type blockBuilder struct {
31-
ctx *snow.Context
42+
clock *mockable.Clock
43+
ctx *snow.Context
44+
chainConfig *extras.ChainConfig
3245

3346
txPool *txpool.TxPool
3447
extraMempool extension.BuilderMempool
@@ -51,10 +64,12 @@ type blockBuilder struct {
5164
func (vm *VM) NewBlockBuilder(extraMempool extension.BuilderMempool) *blockBuilder {
5265
b := &blockBuilder{
5366
ctx: vm.ctx,
67+
chainConfig: vm.chainConfigExtra(),
5468
txPool: vm.txPool,
5569
extraMempool: extraMempool,
5670
shutdownChan: vm.shutdownChan,
5771
shutdownWg: &vm.shutdownWg,
72+
clock: vm.clock,
5873
}
5974
b.pendingSignal = lock.NewCond(&b.buildBlockLock)
6075
return b
@@ -64,7 +79,7 @@ func (vm *VM) NewBlockBuilder(extraMempool extension.BuilderMempool) *blockBuild
6479
func (b *blockBuilder) handleGenerateBlock(currentParentHash common.Hash) {
6580
b.buildBlockLock.Lock()
6681
defer b.buildBlockLock.Unlock()
67-
b.lastBuildTime = time.Now()
82+
b.lastBuildTime = b.clock.Time()
6883
b.lastBuildParentHash = currentParentHash
6984
}
7085

@@ -124,20 +139,19 @@ func (b *blockBuilder) waitForEvent(ctx context.Context, currentHeader *types.He
124139
if err != nil {
125140
return 0, err
126141
}
127-
timeSinceLastBuildTime := time.Since(lastBuildTime)
128-
isRetry := lastBuildParentHash == currentHeader.ParentHash
129-
// 1. if this is not a retry
130-
// 2. if this is the first time we try to build a block
131-
// 3. if the time since the last build is greater than the minimum retry delay
132-
// then we can build a block immediately.
133-
if !isRetry || lastBuildTime.IsZero() || timeSinceLastBuildTime >= MinBlockBuildingRetryDelay {
134-
b.ctx.Log.Debug("Last time we built a block was long enough ago or this is not a retry, no need to wait",
135-
zap.Duration("timeSinceLastBuildTime", timeSinceLastBuildTime),
136-
zap.Bool("isRetry", isRetry),
137-
)
142+
timeUntilNextBuild, shouldBuildImmediately, err := b.calculateBlockBuildingDelay(
143+
lastBuildTime,
144+
lastBuildParentHash,
145+
currentHeader,
146+
)
147+
if err != nil {
148+
return 0, err
149+
}
150+
if shouldBuildImmediately {
151+
b.ctx.Log.Debug("Last time we built a block was long enough ago or this is not a retry, no need to wait")
138152
return commonEng.PendingTxs, nil
139153
}
140-
timeUntilNextBuild := MinBlockBuildingRetryDelay - timeSinceLastBuildTime
154+
141155
b.ctx.Log.Debug("Last time we built a block was too recent, waiting",
142156
zap.Duration("timeUntilNextBuild", timeUntilNextBuild),
143157
)
@@ -161,3 +175,66 @@ func (b *blockBuilder) waitForNeedToBuild(ctx context.Context) (time.Time, commo
161175
}
162176
return b.lastBuildTime, b.lastBuildParentHash, nil
163177
}
178+
179+
// getMinBlockBuildingDelays returns the initial min block building delay and the minimum retry delay.
180+
// It implements the following logic:
181+
// 1. If the current header is in Granite, return the remaining ACP-226 delay after the parent block time and the minimum retry delay.
182+
// 2. If the current header is not in Granite, return 0 and the minimum retry delay.
183+
func (b *blockBuilder) getMinBlockBuildingDelays(currentHeader *types.Header, config *extras.ChainConfig) (time.Duration, time.Duration, error) {
184+
// TODO Cleanup (ceyonur): this check can be removed after Granite is activated.
185+
currentTimestamp := b.clock.Unix()
186+
if !config.IsGranite(currentTimestamp) {
187+
return 0, PreGraniteMinBlockBuildingRetryDelay, nil // Pre-Granite: no initial delay
188+
}
189+
190+
acp226DelayExcess, err := customheader.MinDelayExcess(config, currentHeader, currentTimestamp, nil)
191+
if err != nil {
192+
return 0, 0, err
193+
}
194+
acp226Delay := time.Duration(acp226DelayExcess.Delay()) * time.Millisecond
195+
196+
// Calculate initial delay: time since parent minus ACP-226 delay (clamped to 0)
197+
parentBlockTime := customtypes.BlockTime(currentHeader)
198+
timeSinceParentBlock := b.clock.Time().Sub(parentBlockTime)
199+
// TODO question (ceyonur): should we just use acp226Delay if timeSinceParentBlock is negative?
200+
initialMinBlockBuildingDelay := acp226Delay - timeSinceParentBlock
201+
if initialMinBlockBuildingDelay < 0 {
202+
initialMinBlockBuildingDelay = 0
203+
}
204+
205+
return initialMinBlockBuildingDelay, PostGraniteMinBlockBuildingRetryDelay, nil
206+
}
207+
208+
// calculateBlockBuildingDelay calculates the delay needed before building the next block.
209+
// It returns the time to wait, a boolean indicating whether to build immediately, and any error.
210+
// It implements the following logic:
211+
// 1. If there is no initial min block building delay
212+
// 2. if this is not a retry
213+
// 3. if the time since the last build is greater than the minimum retry delay
214+
// then we can build a block immediately.
215+
func (b *blockBuilder) calculateBlockBuildingDelay(
216+
lastBuildTime time.Time,
217+
lastBuildParentHash common.Hash,
218+
currentHeader *types.Header,
219+
) (time.Duration, bool, error) {
220+
initialMinBlockBuildingDelay, minBlockBuildingRetryDelay, err := b.getMinBlockBuildingDelays(currentHeader, b.chainConfig)
221+
if err != nil {
222+
return 0, false, err
223+
}
224+
225+
isRetry := lastBuildParentHash == currentHeader.ParentHash && !lastBuildTime.IsZero() // if last build time is zero, this is not a retry
226+
227+
timeSinceLastBuildTime := b.clock.Time().Sub(lastBuildTime)
228+
var remainingMinDelay time.Duration
229+
if minBlockBuildingRetryDelay > timeSinceLastBuildTime {
230+
remainingMinDelay = minBlockBuildingRetryDelay - timeSinceLastBuildTime
231+
}
232+
233+
if initialMinBlockBuildingDelay > 0 {
234+
remainingMinDelay = max(initialMinBlockBuildingDelay, remainingMinDelay)
235+
} else if !isRetry || remainingMinDelay == 0 {
236+
return 0, true, nil // Build immediately
237+
}
238+
239+
return remainingMinDelay, false, nil // Need to wait
240+
}

plugin/evm/block_builder_test.go

Lines changed: 182 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,182 @@
1+
// Copyright (C) 2019-2025, Ava Labs, Inc. All rights reserved.
2+
// See the file LICENSE for licensing terms.
3+
4+
package evm
5+
6+
import (
7+
"testing"
8+
"time"
9+
10+
"github.com/ava-labs/avalanchego/utils/timer/mockable"
11+
"github.com/ava-labs/avalanchego/vms/evm/acp226"
12+
"github.com/ava-labs/libevm/common"
13+
"github.com/ava-labs/libevm/core/types"
14+
"github.com/stretchr/testify/require"
15+
16+
"github.com/ava-labs/coreth/params/extras"
17+
"github.com/ava-labs/coreth/plugin/evm/customtypes"
18+
)
19+
20+
func TestCalculateBlockBuildingDelay(t *testing.T) {
21+
now := time.UnixMilli(10000)
22+
nowSecUint64 := uint64(now.Unix())
23+
nowMilliUint64 := uint64(now.UnixMilli())
24+
clock := &mockable.Clock{}
25+
clock.Set(now)
26+
tests := []struct {
27+
name string
28+
config *extras.ChainConfig
29+
currentHeader *types.Header
30+
lastBuildTime time.Time
31+
lastBuildParentHash common.Hash
32+
expectedTimeToWait time.Duration
33+
expectedShouldBuildNow bool
34+
}{
35+
{
36+
name: "pre_granite_returns_build_immediately_zero_time",
37+
config: extras.TestFortunaChainConfig, // Pre-Granite config
38+
currentHeader: &types.Header{
39+
ParentHash: common.Hash{1},
40+
Time: nowSecUint64,
41+
},
42+
lastBuildTime: time.Time{}, // Zero time means not a retry
43+
lastBuildParentHash: common.Hash{1},
44+
expectedShouldBuildNow: true,
45+
},
46+
{
47+
name: "pre_granite_returns_build_immediately_different_parent_hash",
48+
config: extras.TestFortunaChainConfig, // Pre-Granite config
49+
currentHeader: &types.Header{
50+
ParentHash: common.Hash{2},
51+
Time: nowSecUint64,
52+
},
53+
lastBuildTime: now,
54+
lastBuildParentHash: common.Hash{1},
55+
expectedShouldBuildNow: true,
56+
},
57+
{
58+
name: "pre_granite_returns_build_delays_with_same_parent_hash",
59+
config: extras.TestFortunaChainConfig, // Pre-Granite config
60+
currentHeader: &types.Header{
61+
ParentHash: common.Hash{1},
62+
Time: nowSecUint64,
63+
},
64+
lastBuildTime: now,
65+
lastBuildParentHash: common.Hash{1},
66+
expectedTimeToWait: PreGraniteMinBlockBuildingRetryDelay,
67+
expectedShouldBuildNow: false,
68+
},
69+
{
70+
name: "pre_granite_returns_build_returns_immediately_if_enough_time_passed",
71+
config: extras.TestFortunaChainConfig, // Pre-Granite config
72+
currentHeader: &types.Header{
73+
ParentHash: common.Hash{1},
74+
Time: nowSecUint64,
75+
},
76+
lastBuildTime: now.Add(-PreGraniteMinBlockBuildingRetryDelay), // Less than retry delay ago
77+
lastBuildParentHash: common.Hash{1}, // Same as current parent
78+
expectedTimeToWait: 0,
79+
expectedShouldBuildNow: true,
80+
},
81+
{
82+
name: "pre_granite_returns_build_delays_only_remaining_min_delay",
83+
config: extras.TestFortunaChainConfig, // Pre-Granite config
84+
currentHeader: &types.Header{
85+
ParentHash: common.Hash{1},
86+
Time: nowSecUint64,
87+
},
88+
lastBuildTime: now.Add(-PreGraniteMinBlockBuildingRetryDelay / 2), // Less than retry delay ago
89+
lastBuildParentHash: common.Hash{1},
90+
expectedTimeToWait: PreGraniteMinBlockBuildingRetryDelay / 2,
91+
expectedShouldBuildNow: false,
92+
},
93+
{
94+
name: "granite_block_with_now_time",
95+
config: extras.TestGraniteChainConfig,
96+
currentHeader: createGraniteTestHeader(common.Hash{1}, nowMilliUint64, acp226.InitialDelayExcess),
97+
lastBuildTime: time.Time{},
98+
lastBuildParentHash: common.Hash{1},
99+
expectedTimeToWait: 2000 * time.Millisecond, // should wait for initial delay
100+
expectedShouldBuildNow: false,
101+
},
102+
{
103+
name: "granite_block_with_2_seconds_before_clock_no_retry",
104+
config: extras.TestGraniteChainConfig,
105+
currentHeader: createGraniteTestHeader(common.Hash{1}, nowMilliUint64-2000, acp226.InitialDelayExcess),
106+
lastBuildTime: time.Time{}, // Zero time means not a retry
107+
lastBuildParentHash: common.Hash{1},
108+
expectedTimeToWait: 0, // should not wait for initial delay
109+
expectedShouldBuildNow: true,
110+
},
111+
{
112+
name: "granite_block_with_2_seconds_before_clock_with_retry",
113+
config: extras.TestGraniteChainConfig,
114+
currentHeader: createGraniteTestHeader(common.Hash{1}, nowMilliUint64-2000, acp226.InitialDelayExcess),
115+
lastBuildTime: now,
116+
lastBuildParentHash: common.Hash{1},
117+
expectedTimeToWait: PostGraniteMinBlockBuildingRetryDelay,
118+
expectedShouldBuildNow: false,
119+
},
120+
{
121+
name: "granite_with_2_seconds_before_clock_only_waits_for_retry_delay",
122+
config: extras.TestGraniteChainConfig,
123+
currentHeader: createGraniteTestHeader(common.Hash{1}, nowMilliUint64-2000, 0), // 0 means min delay excess which is 1
124+
lastBuildTime: now,
125+
lastBuildParentHash: common.Hash{1},
126+
expectedTimeToWait: PostGraniteMinBlockBuildingRetryDelay,
127+
expectedShouldBuildNow: false,
128+
},
129+
{
130+
name: "granite_with_2_seconds_before_clock_only_waits_for_remaining_retry_delay",
131+
config: extras.TestGraniteChainConfig,
132+
currentHeader: createGraniteTestHeader(common.Hash{1}, nowMilliUint64-2000, 0), // 0 means min delay excess which is 1
133+
lastBuildTime: now.Add(-PostGraniteMinBlockBuildingRetryDelay / 2), // Less than retry delay ago
134+
lastBuildParentHash: common.Hash{1},
135+
expectedTimeToWait: PostGraniteMinBlockBuildingRetryDelay / 2,
136+
expectedShouldBuildNow: false,
137+
},
138+
{
139+
name: "granite_with_2_seconds_after_clock",
140+
config: extras.TestGraniteChainConfig,
141+
currentHeader: createGraniteTestHeader(common.Hash{1}, nowMilliUint64+2000, acp226.InitialDelayExcess),
142+
lastBuildTime: time.Time{}, // Zero time means not a retry
143+
lastBuildParentHash: common.Hash{1},
144+
expectedTimeToWait: 4000 * time.Millisecond,
145+
expectedShouldBuildNow: false,
146+
},
147+
}
148+
149+
for _, tt := range tests {
150+
t.Run(tt.name, func(t *testing.T) {
151+
b := &blockBuilder{
152+
clock: clock,
153+
chainConfig: tt.config,
154+
}
155+
156+
timeToWait, shouldBuildNow, err := b.calculateBlockBuildingDelay(
157+
tt.lastBuildTime,
158+
tt.lastBuildParentHash,
159+
tt.currentHeader,
160+
)
161+
162+
require.NoError(t, err)
163+
require.Equal(t, tt.expectedTimeToWait, timeToWait)
164+
require.Equal(t, tt.expectedShouldBuildNow, shouldBuildNow)
165+
})
166+
}
167+
}
168+
169+
func createGraniteTestHeader(parentHash common.Hash, timeMilliseconds uint64, minDelayExcess acp226.DelayExcess) *types.Header {
170+
header := &types.Header{
171+
Time: timeMilliseconds / 1000,
172+
}
173+
header.ParentHash = parentHash
174+
175+
extra := &customtypes.HeaderExtra{
176+
TimeMilliseconds: &timeMilliseconds,
177+
MinDelayExcess: &minDelayExcess,
178+
}
179+
customtypes.SetHeaderExtra(header, extra)
180+
181+
return header
182+
}

plugin/evm/customtypes/block_ext.go

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ package customtypes
66
import (
77
"math/big"
88
"slices"
9+
"time"
910

1011
"github.com/ava-labs/avalanchego/vms/evm/acp226"
1112
"github.com/ava-labs/libevm/common"
@@ -155,6 +156,13 @@ func CalcExtDataHash(extdata []byte) common.Hash {
155156
return ethtypes.RLPHash(extdata)
156157
}
157158

159+
func BlockTime(eth *ethtypes.Header) time.Time {
160+
if t := GetHeaderExtra(eth).TimeMilliseconds; t != nil {
161+
return time.UnixMilli(int64(*t))
162+
}
163+
return time.Unix(int64(eth.Time), 0)
164+
}
165+
158166
func NewBlockWithExtData(
159167
header *ethtypes.Header, txs []*ethtypes.Transaction, uncles []*ethtypes.Header, receipts []*ethtypes.Receipt,
160168
hasher ethtypes.TrieHasher, extdata []byte, recalc bool,

0 commit comments

Comments
 (0)