Skip to content
Merged
Show file tree
Hide file tree
Changes from 9 commits
Commits
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
10 changes: 8 additions & 2 deletions blocks/block_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -9,10 +9,12 @@ import (

"github.com/ava-labs/avalanchego/utils/logging"
"github.com/ava-labs/libevm/core/types"
"github.com/ava-labs/libevm/ethdb"
"github.com/google/go-cmp/cmp"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"

"github.com/ava-labs/strevm/hook/hookstest"
"github.com/ava-labs/strevm/saetest"
)

Expand All @@ -34,7 +36,7 @@ func newBlock(tb testing.TB, eth *types.Block, parent, lastSettled *Block) *Bloc
return b
}

func newChain(tb testing.TB, startHeight, total uint64, lastSettledAtHeight map[uint64]uint64) []*Block {
func newChain(tb testing.TB, db ethdb.Database, startHeight, total uint64, lastSettledAtHeight map[uint64]uint64) []*Block {
tb.Helper()

var (
Expand Down Expand Up @@ -65,7 +67,11 @@ func newChain(tb testing.TB, startHeight, total uint64, lastSettledAtHeight map[
byNum[n] = b
blocks = append(blocks, b)
if synchronous {
require.NoError(tb, b.MarkSynchronous(), "MarkSynchronous()")
// The target and excess are irrelevant for the purposes of
// [newChain], and non-zero sub-second time for genesis is
// unnecessary.
h := &hookstest.Stub{Target: 1}
require.NoError(tb, b.MarkSynchronous(h, db, 0), "MarkSynchronous()")
}

parent = byNum[n]
Expand Down
16 changes: 3 additions & 13 deletions blocks/blockstest/blocks.go
Original file line number Diff line number Diff line change
Expand Up @@ -11,9 +11,7 @@ import (
"math"
"math/big"
"slices"
"sync/atomic"
"testing"
"time"

"github.com/ava-labs/avalanchego/utils/logging"
"github.com/ava-labs/avalanchego/vms/components/gas"
Expand All @@ -29,7 +27,7 @@ import (
"github.com/stretchr/testify/require"

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

Expand Down Expand Up @@ -124,16 +122,8 @@ func NewGenesis(tb testing.TB, db ethdb.Database, config *params.ChainConfig, al
require.NoErrorf(tb, tdb.Commit(hash, true), "%T.Commit(core.SetupGenesisBlock(...))", tdb)

b := NewBlock(tb, gen.ToBlock(), nil, nil)
require.NoErrorf(tb, b.MarkExecuted(
db,
gastime.New(gen.Timestamp, conf.gasTarget, conf.gasExcess),
time.Time{},
new(big.Int),
nil,
b.SettledStateRoot(),
new(atomic.Pointer[blocks.Block]),
), "%T.MarkExecuted()", b)
require.NoErrorf(tb, b.MarkSynchronous(), "%T.MarkSynchronous()", b)
h := &hookstest.Stub{Target: conf.gasTarget}
require.NoErrorf(tb, b.MarkSynchronous(h, db, conf.gasExcess), "%T.MarkSynchronous()", b)
return b
}

Expand Down
4 changes: 3 additions & 1 deletion blocks/execution.go
Original file line number Diff line number Diff line change
Expand Up @@ -48,7 +48,9 @@ type executionResults struct {
}

// MarkExecuted marks the block as having been executed at the specified time(s)
// and with the specified results. It also sets the chain's head block to b.
// and with the specified results. It also sets the chain's head block to b. The
// [gastime.Time] MUST have already been scaled to the target applicable after
// the block, as defined by the relevant [hook.Points].
//
// MarkExecuted guarantees that state is persisted to the database before
// in-memory indicators of execution are updated. [Block.Executed] returning
Expand Down
4 changes: 2 additions & 2 deletions blocks/execution_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -52,7 +52,7 @@ func TestMarkExecuted(t *testing.T) {
rawdb.WriteBlock(db, ethB)

settles := newBlock(t, newEthBlock(0, 0, nil), nil, nil)
settles.markExecutedForTests(t, db, gastime.New(0, 1, 0))
settles.markExecutedForTests(t, db, gastime.New(time.Unix(0, 0), 1, 0))
b := newBlock(t, ethB, nil, settles)

t.Run("before_MarkExecuted", func(t *testing.T) {
Expand Down Expand Up @@ -85,7 +85,7 @@ func TestMarkExecuted(t *testing.T) {
}
})

gasTime := gastime.New(42, 1e6, 42)
gasTime := gastime.New(time.Unix(42, 0), 1e6, 42)
wallTime := time.Unix(42, 100)
stateRoot := common.Hash{'s', 't', 'a', 't', 'e'}
baseFee := big.NewInt(314159)
Expand Down
37 changes: 32 additions & 5 deletions blocks/settlement.go
Original file line number Diff line number Diff line change
Expand Up @@ -7,13 +7,17 @@ import (
"context"
"errors"
"fmt"
"math/big"
"slices"
"sync/atomic"
"time"

"github.com/ava-labs/avalanchego/vms/components/gas"
"github.com/ava-labs/libevm/core/types"
"github.com/ava-labs/libevm/ethdb"
"go.uber.org/zap"

"github.com/ava-labs/strevm/gastime"
"github.com/ava-labs/strevm/hook"
"github.com/ava-labs/strevm/proxytime"
)
Expand Down Expand Up @@ -60,18 +64,41 @@ func (b *Block) markSettled(lastSettled *atomic.Pointer[Block]) error {
return nil
}

// MarkSynchronous is a special case of [Block.MarkSettled], reserved for the
// last pre-SAE block, which MAY be the genesis block. These are, by definition,
// self-settling so require special treatment as such behaviour is impossible
// under SAE rules.
// MarkSynchronous combines [Block.MarkExecuted] and [Block.MarkSettled], and is
// reserved for the last pre-SAE block, which MAY be the genesis block. These
// blocks are, by definition, self-settling so require special treatment as such
// behaviour is impossible under SAE rules.
//
// Arguments required by [Block.MarkExecuted] but not accepted by
// MarkSynchronous are derived from the block to maintain invariants. The
// `subSecondBlockTime` argument MUST follow the same constraints as the
// respective [hook.Points] method.
//
// MarkSynchronous and [Block.Synchronous] are not safe for concurrent use. This
// method MUST therefore be called *before* instantiating the SAE VM.
//
// Wherever MarkSynchronous results in different behaviour to
// [Block.MarkSettled], the respective methods are documented as such. They can
// otherwise be considered identical.
func (b *Block) MarkSynchronous() error {
func (b *Block) MarkSynchronous(hooks hook.Points, db ethdb.Database, excessAfter gas.Gas) error {
ethB := b.EthBlock()
baseFee := ethB.BaseFee()
if baseFee == nil { // genesis blocks
baseFee = new(big.Int)
}
execTime := gastime.New(
PreciseTime(hooks, b.Header()),
hooks.GasTargetAfter(b.Header()), // target _after_ is a requirement of [Block.MarkExecuted]
excessAfter,
)
// Receipts of a synchronous block have already been "settled" by the block
// itself. As the only reason to pass receipts here is for later settlement
// in another block, there is no need to pass anything meaningful as it
// would also require them to be received as an argument to MarkSynchronous.
var rs types.Receipts
if err := b.MarkExecuted(db, execTime, time.Time{}, baseFee, rs, ethB.Root(), new(atomic.Pointer[Block])); err != nil {
return err
}
b.synchronous = true
return b.markSettled(nil)
}
Expand Down
10 changes: 5 additions & 5 deletions blocks/settlement_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -58,7 +58,7 @@ func TestSettlementInvariants(t *testing.T) {

db := rawdb.NewMemoryDatabase()
for _, b := range []*Block{b, parent, lastSettled} {
b.markExecutedForTests(t, db, gastime.New(b.BuildTime(), 1, 0))
b.markExecutedForTests(t, db, gastime.New(preciseTime(b.Header(), 0), 1, 0))
}

t.Run("before_MarkSettled", func(t *testing.T) {
Expand Down Expand Up @@ -146,7 +146,7 @@ func TestSettles(t *testing.T) {
8: nil,
9: {4, 5, 6, 7},
}
blocks := newChain(t, 0, 10, lastSettledAtHeight)
blocks := newChain(t, rawdb.NewMemoryDatabase(), 0, 10, lastSettledAtHeight)

numsToBlocks := func(nums ...uint64) []*Block {
bs := make([]*Block, len(nums))
Expand Down Expand Up @@ -207,16 +207,16 @@ func TestSettles(t *testing.T) {
}

func TestLastToSettleAt(t *testing.T) {
blocks := newChain(t, 0, 30, nil)
db := rawdb.NewMemoryDatabase()
blocks := newChain(t, db, 0, 30, nil)
t.Run("helper_invariants", func(t *testing.T) {
for i, b := range blocks {
require.Equal(t, uint64(i), b.Height()) //nolint:gosec // Slice index won't overflow
require.Equal(t, b.BuildTime(), b.Height())
}
})

db := rawdb.NewMemoryDatabase()
tm := gastime.New(0, 5 /*target*/, 0)
tm := gastime.New(time.Unix(0, 0), 5 /*target*/, 0)
require.Equal(t, gas.Gas(10), tm.Rate())

requireTime := func(t *testing.T, sec uint64, numerator gas.Gas) {
Expand Down
6 changes: 5 additions & 1 deletion blocks/time.go
Original file line number Diff line number Diff line change
Expand Up @@ -18,9 +18,13 @@ import (
// the value, combined with the regular timestamp to provide a full-resolution
// block time.
func PreciseTime(hooks hook.Points, hdr *types.Header) time.Time {
return preciseTime(hdr, hooks.SubSecondBlockTime(hdr))
}

func preciseTime(hdr *types.Header, subSec time.Duration) time.Time {
return time.Unix(
int64(hdr.Time), //nolint:gosec // Won't overflow for a few millennia
int64(hooks.SubSecondBlockTime(hdr)),
subSec.Nanoseconds(),
)
}

Expand Down
8 changes: 5 additions & 3 deletions gastime/acp176_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@
package gastime

import (
"math"
"testing"
"time"

Expand All @@ -23,7 +24,7 @@ func TestTargetUpdateTiming(t *testing.T) {
initialTarget gas.Gas = 1_600_000
initialExcess = 1_234_567_890
)
tm := New(initialTime, initialTarget, initialExcess)
tm := New(time.Unix(initialTime, 0), initialTarget, initialExcess)
initialRate := tm.Rate()

const (
Expand Down Expand Up @@ -73,8 +74,9 @@ func FuzzWorstCasePrice(f *testing.F) {
) {
initTarget = max(initTarget, 1)

worstcase := New(initTimestamp, gas.Gas(initTarget), gas.Gas(initExcess))
actual := New(initTimestamp, gas.Gas(initTarget), gas.Gas(initExcess))
initUnix := int64(min(initTimestamp, math.MaxInt64)) //nolint:gosec // I can't believe I have to be explicit about this!
worstcase := New(time.Unix(initUnix, 0), gas.Gas(initTarget), gas.Gas(initExcess))
actual := New(time.Unix(initUnix, 0), gas.Gas(initTarget), gas.Gas(initExcess))

blocks := []struct {
time uint64
Expand Down
10 changes: 7 additions & 3 deletions gastime/gastime.go
Original file line number Diff line number Diff line change
Expand Up @@ -50,12 +50,16 @@ func (tm *Time) establishInvariants() {
tm.Time.SetRateInvariants(&tm.target, &tm.excess)
}

// New returns a new [Time], set from a Unix timestamp. The consumption of
// New returns a new [Time], derived from a [time.Time]. The consumption of
// `target` * [TargetToRate] units of [gas.Gas] is equivalent to a tick of 1
// second. Targets are clamped to [MaxTarget].
func New(unixSeconds uint64, target, startingExcess gas.Gas) *Time {
func New(at time.Time, target, startingExcess gas.Gas) *Time {
target = clampTarget(target)
return makeTime(proxytime.New(unixSeconds, rateOf(target)), target, startingExcess)
tm := proxytime.Of[gas.Gas](at)
// [proxytime.Time.SetRate] is documented as never returning an error when
// no invariants have been registered.
_ = tm.SetRate(rateOf(target))
return makeTime(tm, target, startingExcess)
}

// SubSecond scales the value returned by [hook.Points.SubSecondBlockTime] to
Expand Down
61 changes: 56 additions & 5 deletions gastime/gastime_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import (
"fmt"
"math"
"testing"
"time"

"github.com/ava-labs/avalanchego/vms/components/gas"
"github.com/google/go-cmp/cmp"
Expand All @@ -26,7 +27,7 @@ func (tm *Time) cloneViaCanotoRoundTrip(tb testing.TB) *Time {
}

func TestClone(t *testing.T) {
tm := New(42, 1e6, 1e5)
tm := New(time.Unix(42, 0), 1e6, 1e5)
tm.Tick(1)

if diff := cmp.Diff(tm, tm.Clone(), CmpOpt()); diff != "" {
Expand Down Expand Up @@ -66,6 +67,56 @@ func (tm *Time) requireState(tb testing.TB, desc string, want state, opts ...cmp
}
}

func TestNew(t *testing.T) {
frac := func(num, den gas.Gas) (f proxytime.FractionalSecond[gas.Gas]) {
f.Numerator = num
f.Denominator = den
return
}

ignore := cmpopts.IgnoreFields(state{}, "Rate", "Price")

tests := []struct {
name string
unix, nanos int64
target, excess gas.Gas
want state
}{
{
name: "rate at nanosecond resolution",
unix: 42,
nanos: 123_456,
target: 1e9 / TargetToRate,
want: state{
UnixTime: 42,
ConsumedThisSecond: frac(123_456, 1e9),
Target: 1e9 / TargetToRate,
},
},
{
name: "scaling in constructor not applied to starting excess",
unix: 100,
nanos: TargetToRate,
target: 50 / TargetToRate,
excess: 987_654,
want: state{
UnixTime: 100,
ConsumedThisSecond: frac(1, 50),
Target: 50 / TargetToRate,
Excess: 987_654,
},
},
}

for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
tm := time.Unix(tt.unix, tt.nanos)
got := New(tm, tt.target, tt.excess)
got.requireState(t, fmt.Sprintf("New(%v, %d, %d)", tm, tt.target, tt.excess), tt.want, ignore)
})
}
}

func (tm *Time) mustSetRate(tb testing.TB, rate gas.Gas) {
tb.Helper()
require.NoErrorf(tb, tm.SetRate(rate), "%T.%T.SetRate(%d)", tm, TimeMarshaler{}, rate)
Expand All @@ -78,7 +129,7 @@ func (tm *Time) mustSetTarget(tb testing.TB, target gas.Gas) {

func TestScaling(t *testing.T) {
const initExcess = gas.Gas(1_234_567_890)
tm := New(42, 1.6e6, initExcess)
tm := New(time.Unix(42, 0), 1.6e6, initExcess)

// The initial price isn't important in this test; what we care about is
// that it's invariant under scaling of the target etc.
Expand Down Expand Up @@ -154,7 +205,7 @@ func TestScaling(t *testing.T) {

func TestExcess(t *testing.T) {
const rate = gas.Gas(3.2e6)
tm := New(42, rate/2, 0)
tm := New(time.Unix(42, 0), rate/2, 0)

frac := func(num gas.Gas) (f proxytime.FractionalSecond[gas.Gas]) {
f.Numerator = num
Expand Down Expand Up @@ -298,15 +349,15 @@ func TestExcessScalingFactor(t *testing.T) {
{max, max},
}

tm := New(0, 1, 0)
tm := New(time.Unix(0, 0), 1, 0)
for _, tt := range tests {
require.NoErrorf(t, tm.SetTarget(tt.target), "%T.SetTarget(%v)", tm, tt.target)
assert.Equalf(t, tt.want, tm.excessScalingFactor(), "T = %d", tt.target)
}
}

func TestTargetClamping(t *testing.T) {
tm := New(0, MaxTarget+1, 0)
tm := New(time.Unix(0, 0), MaxTarget+1, 0)
require.Equal(t, MaxTarget, tm.Target(), "tm.Target() clamped by constructor")

tests := []struct {
Expand Down
Loading
Loading