Skip to content
Merged
Show file tree
Hide file tree
Changes from 7 commits
Commits
Show all changes
26 commits
Select commit Hold shift + click to select a range
8b038bf
Move BeforeBlock and AfterBlock into gastime
StephenButtolph Dec 5, 2025
af596f7
Replace full blocks with headers in hook.Points
StephenButtolph Dec 5, 2025
178724f
Update gas target immediately after block execution
StephenButtolph Dec 5, 2025
19c3fb9
Add test
StephenButtolph Dec 5, 2025
3b7e488
reduce comments
StephenButtolph Dec 5, 2025
6851d5a
Update hook.Points
StephenButtolph Dec 5, 2025
0d8cb33
Merged
StephenButtolph Dec 5, 2025
d8e67f3
merged
StephenButtolph Dec 8, 2025
b4450bb
ok
StephenButtolph Dec 8, 2025
f34665a
nit
StephenButtolph Dec 8, 2025
0373d0a
lint?
StephenButtolph Dec 8, 2025
94e1fb9
lint
StephenButtolph Dec 8, 2025
5c4a6e1
Update test
StephenButtolph Dec 9, 2025
16badd0
Add genesis block target override
StephenButtolph Dec 9, 2025
abe0d6d
merged
StephenButtolph Dec 9, 2025
8a8b9e2
address comments
StephenButtolph Dec 9, 2025
537bc52
re-push execution file
StephenButtolph Dec 10, 2025
ad66e07
Default to max gas target
StephenButtolph Dec 10, 2025
2339b16
Merge branch 'main' into StephenButtolph/update-target-sooner
StephenButtolph Dec 10, 2025
73f2f6c
Merge branch 'StephenButtolph/update-target-sooner' into StephenButto…
StephenButtolph Dec 10, 2025
cb79047
Merge branch 'main' into StephenButtolph/update-target-sooner
StephenButtolph Dec 10, 2025
f206ac4
Merge branch 'StephenButtolph/update-target-sooner' into StephenButto…
StephenButtolph Dec 10, 2025
67171b5
lint
StephenButtolph Dec 10, 2025
6a11f2a
Update gastime/acp176_test.go
StephenButtolph Dec 11, 2025
b7f4ce2
align test
StephenButtolph Dec 11, 2025
694b7f3
merged
StephenButtolph Dec 11, 2025
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
32 changes: 32 additions & 0 deletions gastime/acp176.go
Copy link
Collaborator

Choose a reason for hiding this comment

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

This precludes the hook package ever importing the blocks package. Not a problem for now, but flagging it so you're thinking about it. This will be OK if hooks only ever deal with Eth types, but that might not always be the case.

Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
// Copyright (C) 2025, Ava Labs, Inc. All rights reserved.
// See the file LICENSE for licensing terms.

// Package gastime measures time based on the consumption of gas.
package gastime

import (
"fmt"

"github.com/ava-labs/avalanchego/vms/components/gas"
"github.com/ava-labs/libevm/core/types"
"github.com/ava-labs/strevm/hook"
)

// BeforeBlock is intended to be called before processing a block, with the
// timestamp sourced from [hook.Points] and [types.Header].
func BeforeBlock(clock *Time, pts hook.Points, h *types.Header) {
r := clock.Rate()
toFrac := pts.SubSecondBlockTime(r, h)
clock.FastForwardTo(h.Time, toFrac)
}

// AfterBlock is intended to be called after processing a block, with the target
// sourced from [hook.Points] and [types.Header].
func AfterBlock(clock *Time, used gas.Gas, pts hook.Points, h *types.Header) error {
clock.Tick(used)
target := pts.GasTarget(h)
if err := clock.SetTarget(target); err != nil {
return fmt.Errorf("%T.SetTarget() after block: %w", clock, err)
}
return nil
}
Copy link
Collaborator

Choose a reason for hiding this comment

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

Suggested change
func BeforeBlock(clock *Time, pts hook.Points, h *types.Header) {
r := clock.Rate()
toFrac := pts.SubSecondBlockTime(r, h)
clock.FastForwardTo(h.Time, toFrac)
}
// AfterBlock is intended to be called after processing a block, with the target
// sourced from [hook.Points] and [types.Header].
func AfterBlock(clock *Time, used gas.Gas, pts hook.Points, h *types.Header) error {
clock.Tick(used)
target := pts.GasTarget(h)
if err := clock.SetTarget(target); err != nil {
return fmt.Errorf("%T.SetTarget() after block: %w", clock, err)
}
return nil
}
func (tm *Time) BeforeBlock(hooks hook.Points, h *types.Header) {
r := tm.Rate()
toFrac := hooks.SubSecondBlockTime(r, h)
tm.FastForwardTo(h.Time, toFrac)
}
// AfterBlock is intended to be called after processing a block, with the target
// sourced from [hook.Points] and [types.Header].
func (tm *Time) AfterBlock(used gas.Gas, hooks hook.Points, h *types.Header) error {
tm.Tick(used)
target := hooks.GasTarget(h)
if err := tm.SetTarget(target); err != nil {
return fmt.Errorf("%T.SetTarget() after block: %w", tm, err)
}
return nil
}

(subjective) Once they're here, being methods seems more natural, and the change from clock to tm is to match other receivers.

(readability) The name pts made more sense when within the context of the hook package, but in gastime I think something more explicit is warranted.

(subjective, optional pedantry) In addition to the above points, which are included in the suggestion, the arguments to AfterBlock() feel like they're in the wrong order. clock.AfterBlock(e.hooks, b.Header(), gasUsed) goes from most general to most specific. But really I'm just typing this out because I want to know if I'm the only one who feels uncomfortable with weird argument orders.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

But really I'm just typing this out because I want to know if I'm the only one who feels uncomfortable with weird argument orders.

I think that we are equally pedantic but with different optimization functions lol.

133 changes: 133 additions & 0 deletions gastime/acp176_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,133 @@
// Copyright (C) 2025, Ava Labs, Inc. All rights reserved.
// See the file LICENSE for licensing terms.

package gastime

import (
"testing"

"github.com/ava-labs/avalanchego/vms/components/gas"
"github.com/ava-labs/libevm/core/types"
"github.com/ava-labs/strevm/hook/hookstest"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)

// TestTargetUpdateTiming verifies that the gas target is modified in AfterBlock
// rather than BeforeBlock.
func TestTargetUpdateTiming(t *testing.T) {
const (
initialTime = 42
initialTarget gas.Gas = 1_600_000
initialExcess = 1_234_567_890
)
tm := New(initialTime, initialTarget, initialExcess)

const (
newTime uint64 = initialTime + 1
newTarget = initialTarget + 100_000
)
hook := &hookstest.Stub{
Target: newTarget,
}
header := &types.Header{
Time: newTime,
}

initialPrice := tm.Price()
BeforeBlock(tm, hook, header)
assert.Equal(t, newTime, tm.Unix(), "Unix time advanced by BeforeBlock()")
assert.Equal(t, initialTarget, tm.Target(), "Target not changed by BeforeBlock()")

enforcedPrice := tm.Price()
assert.LessOrEqual(t, enforcedPrice, initialPrice, "Price should not increase in BeforeBlock()")
if t.Failed() {
t.FailNow()
}

const (
secondsOfGasUsed = 3
initialRate = initialTarget * TargetToRate
used gas.Gas = initialRate * secondsOfGasUsed
expectedEndTime = newTime + secondsOfGasUsed
)
require.NoError(t, AfterBlock(tm, used, hook, header), "AfterBlock()")
assert.Equal(t, expectedEndTime, tm.Unix(), "Unix time advanced by AfterBlock()")
assert.Equal(t, newTarget, tm.Target(), "Target updated by AfterBlock()")
assert.GreaterOrEqual(t, tm.Price(), enforcedPrice, "Price should not decrease in AfterBlock()")
}

func FuzzWorstCasePrice(f *testing.F) {
f.Fuzz(func(
t *testing.T,
initTimestamp, initTarget, initExcess,
time0, timeFrac0, used0, limit0, target0,
time1, timeFrac1, used1, limit1, target1,
time2, timeFrac2, used2, limit2, target2,
time3, timeFrac3, used3, limit3, target3 uint64,
) {
initTarget = max(initTarget, 1)

worstcase := New(initTimestamp, gas.Gas(initTarget), gas.Gas(initExcess))
actual := New(initTimestamp, gas.Gas(initTarget), gas.Gas(initExcess))
require.LessOrEqual(t, actual.Price(), worstcase.Price())

blocks := []struct {
time uint64
timeFrac gas.Gas
used gas.Gas
limit gas.Gas
target gas.Gas
}{
{
time: time0,
timeFrac: gas.Gas(timeFrac0),
used: gas.Gas(used0),
limit: gas.Gas(limit0),
target: gas.Gas(target0),
},
{
time: time1,
timeFrac: gas.Gas(timeFrac1),
used: gas.Gas(used1),
limit: gas.Gas(limit1),
target: gas.Gas(target1),
},
{
time: time2,
timeFrac: gas.Gas(timeFrac2),
used: gas.Gas(used2),
limit: gas.Gas(limit2),
target: gas.Gas(target2),
},
{
time: time3,
timeFrac: gas.Gas(timeFrac3),
used: gas.Gas(used3),
limit: gas.Gas(limit3),
target: gas.Gas(target3),
},
}
for _, block := range blocks {
block.limit = max(block.used, block.limit)
block.target = clampTarget(max(block.target, 1))
block.timeFrac %= rateOf(block.target)

header := &types.Header{
Time: block.time,
}
hook := &hookstest.Stub{
Target: block.target,
SubSecondTime: block.timeFrac,
}

BeforeBlock(worstcase, hook, header)
BeforeBlock(actual, hook, header)

require.LessOrEqual(t, actual.Price(), worstcase.Price())

AfterBlock(worstcase, block.limit, hook, header)
AfterBlock(actual, block.used, hook, header)
}
})
}
38 changes: 11 additions & 27 deletions hook/hook.go
Original file line number Diff line number Diff line change
Expand Up @@ -8,48 +8,32 @@
package hook

import (
"fmt"

"github.com/ava-labs/avalanchego/vms/components/gas"
"github.com/ava-labs/libevm/core/state"
"github.com/ava-labs/libevm/core/types"
"github.com/ava-labs/libevm/params"

"github.com/ava-labs/strevm/blocks"
"github.com/ava-labs/strevm/gastime"
"github.com/ava-labs/strevm/intmath"
saeparams "github.com/ava-labs/strevm/params"
)

// Points define user-injected hook points.
type Points interface {
GasTarget(parent *types.Block) gas.Gas
SubSecondBlockTime(*types.Block) gas.Gas
// GasTarget returns the amount of gas per second that the chain should
// target to consume after executing the provided block.
GasTarget(*types.Header) gas.Gas
// SubSecondBlockTime returns the sub-second portion of the block time based
// on the provided gas rate.
//
// For example, if the block timestamp is 10.75 seconds and the gas rate is
// 100 gas/second, then this method should return 75 gas.
SubSecondBlockTime(gasRate gas.Gas, h *types.Header) gas.Gas
// BeforeBlock is called immediately prior to executing the block.
BeforeBlock(params.Rules, *state.StateDB, *types.Block) error
// AfterBlock is called immediately after executing the block.
AfterBlock(*state.StateDB, *types.Block, types.Receipts)
}

// BeforeBlock is intended to be called before processing a block, with the gas
// target sourced from [Points].
func BeforeBlock(pts Points, rules params.Rules, sdb *state.StateDB, b *blocks.Block, clock *gastime.Time) error {
clock.FastForwardTo(
b.BuildTime(),
pts.SubSecondBlockTime(b.EthBlock()),
)
target := pts.GasTarget(b.ParentBlock().EthBlock())
if err := clock.SetTarget(target); err != nil {
return fmt.Errorf("%T.SetTarget() before block: %w", clock, err)
}
return pts.BeforeBlock(rules, sdb, b.EthBlock())
}

// AfterBlock is intended to be called after processing a block, with the gas
// sourced from [types.Block.GasUsed] or equivalent.
func AfterBlock(pts Points, sdb *state.StateDB, b *types.Block, clock *gastime.Time, used gas.Gas, rs types.Receipts) {
clock.Tick(used)
pts.AfterBlock(sdb, b, rs)
}

// MinimumGasConsumption MUST be used as the implementation for the respective
// method on [params.RulesHooks]. The concrete type implementing the hooks MUST
// propagate incoming and return arguments unchanged.
Expand Down
12 changes: 7 additions & 5 deletions hook/hookstest/stub.go
Original file line number Diff line number Diff line change
Expand Up @@ -15,19 +15,21 @@ import (

// Stub implements [hook.Points].
type Stub struct {
Target gas.Gas
Target gas.Gas
SubSecondTime gas.Gas
}

var _ hook.Points = (*Stub)(nil)

// GasTarget ignores its argument and always returns [Stub.Target].
func (s *Stub) GasTarget(parent *types.Block) gas.Gas {
func (s *Stub) GasTarget(*types.Header) gas.Gas {
return s.Target
}

// SubSecondBlockTime time ignores its argument and always returns 0.
func (*Stub) SubSecondBlockTime(*types.Block) gas.Gas {
return 0
// SubSecondBlockTime time ignores its arguments and always returns
// [Stub.SubSecondTime].
func (s *Stub) SubSecondBlockTime(gas.Gas, *types.Header) gas.Gas {
return s.SubSecondTime
}

// BeforeBlock is a no-op that always returns nil.
Expand Down
16 changes: 9 additions & 7 deletions saexec/execution.go
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@ import (
"go.uber.org/zap"

"github.com/ava-labs/strevm/blocks"
"github.com/ava-labs/strevm/hook"
"github.com/ava-labs/strevm/gastime"
)

var errExecutorClosed = errors.New("saexec.Executor closed")
Expand Down Expand Up @@ -103,12 +103,14 @@ func (e *Executor) execute(b *blocks.Block, logger logging.Logger) error {
return fmt.Errorf("state.New(%#x, ...): %v", parent.PostExecutionStateRoot(), err)
}

rules := e.chainConfig.Rules(b.Number(), true /*isMerge*/, b.BuildTime())
gasClock := parent.ExecutedByGasTime().Clone()
if err := hook.BeforeBlock(e.hooks, rules, stateDB, b, gasClock); err != nil {
gastime.BeforeBlock(gasClock, e.hooks, b.Header())
perTxClock := gasClock.Time.Clone()

rules := e.chainConfig.Rules(b.Number(), true /*isMerge*/, b.BuildTime())
if err := e.hooks.BeforeBlock(rules, stateDB, b.EthBlock()); err != nil {
return fmt.Errorf("before-block hook: %v", err)
}
perTxClock := gasClock.Time.Clone()

header := types.CopyHeader(b.Header())
header.BaseFee = gasClock.BaseFee().ToBig()
Expand Down Expand Up @@ -161,10 +163,10 @@ func (e *Executor) execute(b *blocks.Block, logger logging.Logger) error {
// to access them before the end of the block.
receipts[ti] = receipt
}
e.hooks.AfterBlock(stateDB, b.EthBlock(), receipts)
endTime := time.Now()
hook.AfterBlock(e.hooks, stateDB, b.EthBlock(), gasClock, blockGasConsumed, receipts)
if gasClock.Time.Compare(perTxClock) != 0 {
return fmt.Errorf("broken invariant: block-resolution clock @ %s does not match tx-resolution clock @ %s", gasClock.String(), perTxClock.String())
if err := gastime.AfterBlock(gasClock, blockGasConsumed, e.hooks, b.Header()); err != nil {
return fmt.Errorf("after-block gas time update: %w", err)
}

logger.Debug(
Expand Down