Skip to content
3 changes: 2 additions & 1 deletion RELEASES.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,11 +4,12 @@

- 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.
- Enabled RPC batch limits by default, and configurable with `batch-request-limit` and `batch-max-response-size`.
- ACP-226:
- ACP-226 (Granite):
- Set expected block gas cost to 0 in Granite network upgrade, removing block gas cost requirements for block building.
- Add `timeMilliseconds` (Unix uint64) timestamp to block header for Granite upgrade.
- Add `minDelayExcess` (uint64) to block header for Granite upgrade.
- Add minimum block building delays to conform the block builder to ACP-226 requirements.
- Add minimum delay verification.
- Update go version to 1.24.8

## [v0.15.3](https://github.com/ava-labs/coreth/releases/tag/v0.15.3)
Expand Down
10 changes: 5 additions & 5 deletions miner/worker.go
Original file line number Diff line number Diff line change
Expand Up @@ -144,13 +144,13 @@ func (w *worker) commitNewWork(predicateContext *precompileconfig.PredicateConte
w.mu.RLock()
defer w.mu.RUnlock()
var (
parent = w.chain.CurrentBlock()
tstart = w.clock.Time()
chainExtra = params.GetExtra(w.chainConfig)
parent = w.chain.CurrentBlock()
chainExtra = params.GetExtra(w.chainConfig)
tstart = customheader.GetNextTimestamp(parent, w.clock.Time())
timestamp = uint64(tstart.Unix())
timestampMS = uint64(tstart.UnixMilli())
)

timestamp, timestampMS := customheader.GetNextTimestamp(parent, tstart)

header := &types.Header{
ParentHash: parent.Hash(),
Number: new(big.Int).Add(parent.Number, common.Big1),
Expand Down
90 changes: 46 additions & 44 deletions plugin/evm/customheader/time.go
Original file line number Diff line number Diff line change
Expand Up @@ -24,30 +24,23 @@ var (
ErrTimeMillisecondsRequired = errors.New("TimeMilliseconds is required after Granite activation")
ErrTimeMillisecondsMismatched = errors.New("TimeMilliseconds does not match header.Time")
ErrTimeMillisecondsBeforeGranite = errors.New("TimeMilliseconds should be nil before Granite activation")
ErrMinDelayNotMet = errors.New("minimum block delay not met")
ErrGraniteClockBehindParent = errors.New("current timestamp is not allowed to be behind than parent timestamp in Granite")
)

// GetNextTimestamp calculates the timestamp (in seconds and milliseconds) for the next child block based on the parent's timestamp and the current time.
// First return value is the timestamp in seconds, second return value is the timestamp in milliseconds.
func GetNextTimestamp(parent *types.Header, now time.Time) (uint64, uint64) {
var (
timestamp = uint64(now.Unix())
timestampMS = uint64(now.UnixMilli())
)
// Note: in order to support asynchronous block production, blocks are allowed to have
// the same timestamp as their parent. This allows more than one block to be produced
// per second.
// GetNextTimestamp calculates the time for the next header based on the parent's timestamp and the current time.
// This can return the parent time if now is before the parent time and TimeMilliseconds is not set (pre-Granite).
func GetNextTimestamp(parent *types.Header, now time.Time) time.Time {
parentExtra := customtypes.GetHeaderExtra(parent)
if parent.Time >= timestamp ||
(parentExtra.TimeMilliseconds != nil && *parentExtra.TimeMilliseconds >= timestampMS) {
timestamp = parent.Time
// If the parent has a TimeMilliseconds, use it. Otherwise, use the parent time * 1000.
if parentExtra.TimeMilliseconds != nil {
timestampMS = *parentExtra.TimeMilliseconds
} else {
timestampMS = parent.Time * 1000 // TODO: establish minimum time
}
// In Granite, there is a minimum delay enforced, so we cannot adjust the time with the parent's timestamp.
// Instead we should have waited enough time before calling this function and before the block building.
// We return the current time instead regardless and defer the verification to VerifyTime.
if parent.Time < uint64(now.Unix()) || parentExtra.TimeMilliseconds != nil {
return now
}
return timestamp, timestampMS

// In pre-Granite, blocks are allowed to have the same timestamp as their parent.
return time.Unix(int64(parent.Time), 0)
}

// VerifyTime verifies that the header's Time and TimeMilliseconds fields are
Expand All @@ -58,26 +51,34 @@ func GetNextTimestamp(parent *types.Header, now time.Time) (uint64, uint64) {
// - Time matches TimeMilliseconds/1000 after Granite activation
// - Time/TimeMilliseconds is not too far in the future
// - Time/TimeMilliseconds is non-decreasing
// - (TODO) Minimum block delay is enforced
// - Minimum block delay is enforced
func VerifyTime(extraConfig *extras.ChainConfig, parent *types.Header, header *types.Header, now time.Time) error {
var (
headerExtra = customtypes.GetHeaderExtra(header)
parentExtra = customtypes.GetHeaderExtra(parent)
)

// These two variables are backward-compatible with Time (seconds) fields.
headerTimeMS := customtypes.HeaderTimeMilliseconds(header)
parentTimeMS := customtypes.HeaderTimeMilliseconds(parent)

// Verify the header's timestamp is not earlier than parent's
// it does include equality(==), so multiple blocks per second is ok
if header.Time < parent.Time {
return fmt.Errorf("%w: %d < parent %d", errBlockTooOld, header.Time, parent.Time)
// This includes equality(==), so multiple blocks per milliseconds is ok
// pre-Granite.
if headerTimeMS < parentTimeMS {
return fmt.Errorf("%w: %d < parent %d", errBlockTooOld, headerTimeMS, parentTimeMS)
}

// Do all checks that apply only before Granite
if !extraConfig.IsGranite(header.Time) {
// Make sure the block isn't too far in the future
if maxBlockTime := uint64(now.Add(MaxFutureBlockTime).Unix()); header.Time > maxBlockTime {
return fmt.Errorf("%w: %d > allowed %d", ErrBlockTooFarInFuture, header.Time, maxBlockTime)
}
// Verify if the header's timestamp is not too far in the future
if maxBlockTimeMS := uint64(now.Add(MaxFutureBlockTime).UnixMilli()); headerTimeMS > maxBlockTimeMS {
return fmt.Errorf("%w: %d > allowed %d",
ErrBlockTooFarInFuture,
headerTimeMS,
maxBlockTimeMS,
)
}

if !extraConfig.IsGranite(header.Time) {
// This field should not be set yet.
if headerExtra.TimeMilliseconds != nil {
return ErrTimeMillisecondsBeforeGranite
Expand All @@ -99,22 +100,23 @@ func VerifyTime(extraConfig *extras.ChainConfig, parent *types.Header, header *t
)
}

// Verify TimeMilliseconds is not earlier than parent's TimeMilliseconds
// TODO: Ensure minimum block delay is enforced
if parentExtra.TimeMilliseconds != nil && *headerExtra.TimeMilliseconds < *parentExtra.TimeMilliseconds {
return fmt.Errorf("%w: %d < parent %d",
errBlockTooOld,
*headerExtra.TimeMilliseconds,
*parentExtra.TimeMilliseconds,
)
// Verify minimum block delay is enforced
// Parent might not have a min delay excess if this is the first Granite block
// in this case we cannot verify the min delay,
// Otherwise parent should have been verified in VerifyMinDelayExcess
if parentExtra.MinDelayExcess == nil {
return nil
}

// Verify TimeMilliseconds is not too far in the future
if maxBlockTimeMillis := uint64(now.Add(MaxFutureBlockTime).UnixMilli()); *headerExtra.TimeMilliseconds > maxBlockTimeMillis {
return fmt.Errorf("%w: %d > allowed %d",
ErrBlockTooFarInFuture,
*headerExtra.TimeMilliseconds,
maxBlockTimeMillis,
// This should not be underflow as we have verified that the parent's
// TimeMilliseconds is earlier than the header's TimeMilliseconds above.
actualDelayMS := headerTimeMS - parentTimeMS
minRequiredDelayMS := parentExtra.MinDelayExcess.Delay()
if actualDelayMS < minRequiredDelayMS {
return fmt.Errorf("%w: actual delay %dms < required %dms",
ErrMinDelayNotMet,
actualDelayMS,
minRequiredDelayMS,
)
}

Expand Down
174 changes: 149 additions & 25 deletions plugin/evm/customheader/time_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import (
"testing"
"time"

"github.com/ava-labs/avalanchego/vms/evm/acp226"
"github.com/ava-labs/libevm/core/types"
"github.com/stretchr/testify/require"

Expand All @@ -15,17 +16,6 @@ import (
"github.com/ava-labs/coreth/utils"
)

func generateHeader(timeSeconds uint64, timeMilliseconds *uint64) *types.Header {
return customtypes.WithHeaderExtra(
&types.Header{
Time: timeSeconds,
},
&customtypes.HeaderExtra{
TimeMilliseconds: timeMilliseconds,
},
)
}

func TestVerifyTime(t *testing.T) {
var (
now = time.Unix(1714339200, 123_456_789)
Expand Down Expand Up @@ -138,6 +128,110 @@ func TestVerifyTime(t *testing.T) {
parentHeader: generateHeader(timeSeconds, nil),
extraConfig: extras.TestGraniteChainConfig,
},
// Min delay verification tests
{
name: "pre_granite_no_min_delay_verification",
header: generateHeader(timeSeconds, nil),
parentHeader: generateHeader(timeSeconds, nil),
extraConfig: extras.TestFortunaChainConfig,
},
{
name: "granite_first_block_no_parent_min_delay_excess",
header: generateHeaderWithMinDelayExcessAndTime(
timeSeconds,
utils.NewUint64(timeMillis),
utils.NewUint64(acp226.InitialDelayExcess),
),
parentHeader: generateHeader(timeSeconds-1, nil), // Pre-Granite parent
extraConfig: extras.TestGraniteChainConfig,
},
{
name: "granite_initial_delay_met",
header: generateHeaderWithMinDelayExcessAndTime(
timeSeconds,
utils.NewUint64(timeMillis),
utils.NewUint64(acp226.InitialDelayExcess),
),
parentHeader: generateHeaderWithMinDelayExcessAndTime(
timeSeconds-1,
utils.NewUint64(timeMillis-2000), // 2000 ms is the exact initial delay
utils.NewUint64(acp226.InitialDelayExcess),
),
extraConfig: extras.TestGraniteChainConfig,
},
{
name: "granite_initial_delay_not_met",
header: generateHeaderWithMinDelayExcessAndTime(
timeSeconds,
utils.NewUint64(timeMillis),
utils.NewUint64(acp226.InitialDelayExcess),
),
parentHeader: generateHeaderWithMinDelayExcessAndTime(
timeSeconds-1,
utils.NewUint64(timeMillis-1999), // 1 ms less than required
utils.NewUint64(acp226.InitialDelayExcess),
),
extraConfig: extras.TestGraniteChainConfig,
expectedErr: ErrMinDelayNotMet,
},
{
name: "granite_future_timestamp_within_limits",
header: generateHeaderWithMinDelayExcessAndTime(
timeSeconds+5, // 5 seconds in future
utils.NewUint64(timeMillis+5000),
utils.NewUint64(acp226.InitialDelayExcess),
),
parentHeader: generateHeaderWithMinDelayExcessAndTime(
timeSeconds-1,
utils.NewUint64(timeMillis-2000),
utils.NewUint64(acp226.InitialDelayExcess),
),
extraConfig: extras.TestGraniteChainConfig,
},
{
name: "granite_future_timestamp_abuse",
header: generateHeaderWithMinDelayExcessAndTime(
timeSeconds+15, // 15 seconds in future, exceeds MaxFutureBlockTime
utils.NewUint64(timeMillis+15000),
utils.NewUint64(acp226.InitialDelayExcess),
),
parentHeader: generateHeaderWithMinDelayExcessAndTime(
timeSeconds-1,
utils.NewUint64(timeMillis-2000),
utils.NewUint64(acp226.InitialDelayExcess),
),
extraConfig: extras.TestGraniteChainConfig,
expectedErr: ErrBlockTooFarInFuture,
},
{
name: "granite_zero_delay_excess",
header: generateHeaderWithMinDelayExcessAndTime(
timeSeconds,
utils.NewUint64(timeMillis),
utils.NewUint64(0),
),
parentHeader: generateHeaderWithMinDelayExcessAndTime(
timeSeconds,
utils.NewUint64(timeMillis-1), // 1ms delay, meets zero requirement
utils.NewUint64(0), // Parent has zero delay excess
),
extraConfig: extras.TestGraniteChainConfig,
},
{
name: "granite_zero_delay_excess_but_zero_delay",
header: generateHeaderWithMinDelayExcessAndTime(
timeSeconds,
utils.NewUint64(timeMillis),
utils.NewUint64(0),
),
parentHeader: generateHeaderWithMinDelayExcessAndTime(
timeSeconds,
utils.NewUint64(timeMillis), // Same timestamp, zero delay
utils.NewUint64(0), // Parent has zero delay excess
),
extraConfig: extras.TestGraniteChainConfig,
expectedErr: ErrMinDelayNotMet,
},
}

for _, test := range tests {
Expand Down Expand Up @@ -188,14 +282,7 @@ func TestGetNextTimestamp(t *testing.T) {
expectedMillis: nowSeconds * 1000, // parent.Time * 1000
},
{
name: "current_time_equals_parent_time_with_milliseconds",
parent: generateHeader(nowSeconds, utils.NewUint64(nowMillis)),
now: now,
expectedSec: nowSeconds,
expectedMillis: nowMillis, // parent's TimeMilliseconds
},
{
name: "current_time_before_parent_time",
name: "current_time_before_parent_time_no_milliseconds",
parent: generateHeader(nowSeconds+10, nil),
now: now,
expectedSec: nowSeconds + 10,
Expand All @@ -205,23 +292,60 @@ func TestGetNextTimestamp(t *testing.T) {
name: "current_time_before_parent_time_with_milliseconds",
parent: generateHeader(nowSeconds+10, utils.NewUint64(nowMillis)),
now: now,
expectedSec: nowSeconds + 10,
expectedMillis: nowMillis, // parent's TimeMilliseconds
expectedSec: nowSeconds,
expectedMillis: nowMillis,
},
{
name: "current_time_milliseconds_before_parent_time_milliseconds",
parent: generateHeader(nowSeconds, utils.NewUint64(nowMillis+10)),
now: now,
expectedSec: nowSeconds,
expectedMillis: nowMillis + 10, // parent's TimeMilliseconds
expectedMillis: nowMillis,
},
{
name: "current_time_equals_parent_time_with_milliseconds_granite",
parent: generateHeader(nowSeconds, utils.NewUint64(nowMillis)),
now: now,
expectedSec: nowSeconds,
expectedMillis: nowMillis,
},
{
name: "current_timesec_equals_parent_time_with_less_milliseconds",
parent: generateHeader(nowSeconds, utils.NewUint64(nowMillis-10)),
now: now,
expectedSec: nowSeconds,
expectedMillis: nowMillis,
},
}

for _, test := range tests {
t.Run(test.name, func(t *testing.T) {
sec, millis := GetNextTimestamp(test.parent, test.now)
require.Equal(t, test.expectedSec, sec)
require.Equal(t, test.expectedMillis, millis)
time := GetNextTimestamp(test.parent, test.now)
require.Equal(t, test.expectedSec, uint64(time.Unix()))
require.Equal(t, test.expectedMillis, uint64(time.UnixMilli()))
})
}
}

func generateHeader(timeSeconds uint64, timeMilliseconds *uint64) *types.Header {
return customtypes.WithHeaderExtra(
&types.Header{
Time: timeSeconds,
},
&customtypes.HeaderExtra{
TimeMilliseconds: timeMilliseconds,
},
)
}

func generateHeaderWithMinDelayExcessAndTime(timeSeconds uint64, timeMilliseconds *uint64, minDelayExcess *uint64) *types.Header {
return customtypes.WithHeaderExtra(
&types.Header{
Time: timeSeconds,
},
&customtypes.HeaderExtra{
TimeMilliseconds: timeMilliseconds,
MinDelayExcess: (*acp226.DelayExcess)(minDelayExcess),
},
)
}
Loading