Skip to content
Draft
4 changes: 2 additions & 2 deletions .envrc
Original file line number Diff line number Diff line change
@@ -1,2 +1,2 @@
export GOROOT="$(go1.23.7 env GOROOT)"
PATH_add "$(go1.23.7 env GOROOT)/bin"
export GOROOT="$(go1.24.7 env GOROOT)"
PATH_add "$(go1.24.7 env GOROOT)/bin"
2 changes: 1 addition & 1 deletion .github/workflows/lint.yml
Original file line number Diff line number Diff line change
Expand Up @@ -36,7 +36,7 @@ jobs:
- name: golangci-lint
uses: golangci/golangci-lint-action@v6
with:
version: v1.63.3
version: v1.64.8

yamllint:
runs-on: ubuntu-latest
Expand Down
5 changes: 5 additions & 0 deletions .golangci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -82,6 +82,11 @@ linters-settings:
severity: warning
disabled: false

testifylint:
enable-all: true
disable:
- require-error # Blanket usage of require over assert is an anti-pattern

issues:
include:
# Many of the default exclusions are because, verbatim "Annoying issue",
Expand Down
46 changes: 26 additions & 20 deletions blocks/block.go
Original file line number Diff line number Diff line change
@@ -1,3 +1,6 @@
// Copyright (C) 2025, Ava Labs, Inc. All rights reserved.
// See the file LICENSE for licensing terms.

// Package blocks defines [Streaming Asynchronous Execution] (SAE) blocks.
//
// [Streaming Asynchronous Execution]: https://github.com/avalanche-foundation/ACPs/tree/main/ACPs/194-streaming-asynchronous-execution
Expand All @@ -6,12 +9,10 @@ package blocks
import (
"errors"
"fmt"
"math"
"runtime"
"sync/atomic"

"github.com/ava-labs/avalanchego/utils/logging"
"github.com/ava-labs/libevm/common"
"github.com/ava-labs/libevm/core/types"
"go.uber.org/zap"
)
Expand All @@ -27,12 +28,21 @@ type Block struct {
// Rationale: the ancestral pointers form a linked list that would prevent
// garbage collection if not severed. Once a block is settled there is no
// need to inspect its history so we sacrifice the ancestors to the GC
// Overlord as a sign of our unwavering fealty.
ancestry atomic.Pointer[ancestry]
// Overlord as a sign of our unwavering fealty. See [InMemoryBlockCount] for
// observability.
ancestry atomic.Pointer[ancestry]
// Only the genesis block or the last pre-SAE block is synchronous. These
// are self-settling by definition so their `ancestry` MUST be nil.
synchronous bool
// Non-nil i.f.f. [Block.MarkExecuted] or [Block.ResotrePostExecutionState]
// have returned without error.
execution atomic.Pointer[executionResults]

// See [Block.SetInterimExecutionTime for setting and [LastToSettleAt] for
// usage. The pointer MAY be nil if execution is yet to commence.
// Allows this block to be ruled out as able to be settled at a particular
// time (i.e. if this field is >= said time). The pointer MAY be nil if
// execution is yet to commence. For more details, see
// [Block.SetInterimExecutionTime for setting and [LastToSettleAt] for
// usage.
executionExceededSecond atomic.Pointer[uint64]

executed chan struct{} // closed after `execution` is set
Expand All @@ -41,11 +51,11 @@ type Block struct {
log logging.Logger
}

var inMemoryBlockCount atomic.Uint64
var inMemoryBlockCount atomic.Int64

// InMemoryBlockCount returns the number of blocks created with [New] that are
// yet to have their GC finalizers run.
func InMemoryBlockCount() uint64 {
func InMemoryBlockCount() int64 {
return inMemoryBlockCount.Load()
}

Expand All @@ -56,12 +66,11 @@ func New(eth *types.Block, parent, lastSettled *Block, log logging.Logger) (*Blo
executed: make(chan struct{}),
settled: make(chan struct{}),
}
// TODO(arr4n) change to runtime.AddCleanup after the Go version has been
// bumped to >=1.24.0.

inMemoryBlockCount.Add(1)
runtime.SetFinalizer(b, func(*Block) {
inMemoryBlockCount.Add(math.MaxUint64) // -1
})
runtime.AddCleanup(b, func(struct{}) {
inMemoryBlockCount.Add(-1)
}, struct{}{})

if err := b.setAncestors(parent, lastSettled); err != nil {
return nil, err
Expand Down Expand Up @@ -94,6 +103,10 @@ func (b *Block) setAncestors(parent, lastSettled *Block) error {
// CopyAncestorsFrom populates the [Block.ParentBlock] and [Block.LastSettled]
// values, typically only required during database recovery. The source block
// MUST have the same hash as b.
//
// Although the individual ancestral blocks are shallow copied, calling
// [Block.MarkSettled] on either the source or destination will NOT clear the
// pointers of the other.
func (b *Block) CopyAncestorsFrom(c *Block) error {
if from, to := c.Hash(), b.Hash(); from != to {
return fmt.Errorf("%w: copying internals from block %#x to %#x", errHashMismatch, from, to)
Expand All @@ -106,10 +119,3 @@ func (b *Block) CopyAncestorsFrom(c *Block) error {
// embedded in the [Block]. Use [Block.PostExecutionStateRoot] or
// [Block.SettledStateRoot] instead.
func (b *Block) Root() {}

// SettledStateRoot returns the state root after execution of the last block
// settled by b. It is a convenience wrapper for calling [types.Block.Root] on
// the embedded [types.Block].
func (b *Block) SettledStateRoot() common.Hash {
return b.Block.Root()
}
44 changes: 28 additions & 16 deletions blocks/block_test.go
Original file line number Diff line number Diff line change
@@ -1,3 +1,6 @@
// Copyright (C) 2025, Ava Labs, Inc. All rights reserved.
// See the file LICENSE for licensing terms.

package blocks

import (
Expand All @@ -9,6 +12,8 @@ import (
"github.com/google/go-cmp/cmp"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"

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

func newEthBlock(num, time uint64, parent *types.Block) *types.Block {
Expand All @@ -24,7 +29,7 @@ func newEthBlock(num, time uint64, parent *types.Block) *types.Block {

func newBlock(tb testing.TB, eth *types.Block, parent, lastSettled *Block) *Block {
tb.Helper()
b, err := New(eth, parent, lastSettled, logging.NoLog{})
b, err := New(eth, parent, lastSettled, saetest.NewTBLogger(tb, logging.Warn))
require.NoError(tb, err, "New()")
return b
}
Expand All @@ -39,34 +44,41 @@ func newChain(tb testing.TB, startHeight, total uint64, lastSettledAtHeight map[
)
byNum := make(map[uint64]*Block)

if lastSettledAtHeight == nil {
lastSettledAtHeight = make(map[uint64]uint64)
}

for i := range total {
n := startHeight + i

var settle *Block
var (
settle *Block
synchronous bool
)
if s, ok := lastSettledAtHeight[n]; ok {
settle = byNum[s]
if s == n {
require.Zero(tb, s, "Only genesis block is self-settling")
synchronous = true
} else {
require.Less(tb, s, n, "Last-settled height MUST be <= current height")
settle = byNum[s]
}
}

byNum[n] = newBlock(tb, newEthBlock(n, n /*time*/, ethParent), parent, settle)
blocks = append(blocks, byNum[n])
b := newBlock(tb, newEthBlock(n, n /*time*/, ethParent), parent, settle)
byNum[n] = b
blocks = append(blocks, b)
if synchronous {
require.NoError(tb, b.MarkSynchronous(), "MarkSynchronous()")
}

parent = byNum[n]
ethParent = parent.Block
ethParent = parent.EthBlock()
}

return blocks
}

func TestSetAncestors(t *testing.T) {
t.Parallel()

parent := newBlock(t, newEthBlock(5, 5, nil), nil, nil)
lastSettled := newBlock(t, newEthBlock(3, 0, nil), nil, nil)
child := newEthBlock(6, 6, parent.Block)
child := newEthBlock(6, 6, parent.EthBlock())

t.Run("incorrect_parent", func(t *testing.T) {
// Note that the arguments to [New] are inverted.
Expand All @@ -78,8 +90,8 @@ func TestSetAncestors(t *testing.T) {
dest := newBlock(t, child, nil, nil)

t.Run("destination_before_copy", func(t *testing.T) {
assert.Nilf(t, dest.ParentBlock(), "%T.ParentBlock()")
assert.Nilf(t, dest.LastSettled(), "%T.LastSettled()")
assert.Nilf(t, dest.ParentBlock(), "%T.ParentBlock()", dest)
assert.Nilf(t, dest.LastSettled(), "%T.LastSettled()", dest)
})
if t.Failed() {
t.FailNow()
Expand All @@ -91,7 +103,7 @@ func TestSetAncestors(t *testing.T) {
}

t.Run("incompatible_destination_block", func(t *testing.T) {
ethB := newEthBlock(dest.Height()+1 /*mismatch*/, dest.Time(), parent.Block)
ethB := newEthBlock(dest.Height()+1 /*mismatch*/, dest.BuildTime(), parent.EthBlock())
dest := newBlock(t, ethB, nil, nil)
require.ErrorIs(t, dest.CopyAncestorsFrom(source), errHashMismatch)
})
Expand Down
61 changes: 25 additions & 36 deletions blocks/cmpopt.go
Original file line number Diff line number Diff line change
@@ -1,60 +1,49 @@
// Copyright (C) 2025, Ava Labs, Inc. All rights reserved.
// See the file LICENSE for licensing terms.

//go:build !prod && !nocmpopts

package blocks

import (
"sync/atomic"
"github.com/google/go-cmp/cmp"

"github.com/ava-labs/libevm/core/types"
"github.com/ava-labs/strevm/cmputils"
"github.com/ava-labs/strevm/saetest"
"github.com/google/go-cmp/cmp"
"github.com/google/go-cmp/cmp/cmpopts"
)

// CmpOpt returns a configuration for [cmp.Diff] to compare [Block] instances in
// tests.
func CmpOpt() cmp.Option {
return cmp.Options{
cmp.Comparer((*Block).equalForTests),
saetest.CmpBigInts(),
saetest.CmpTimes(),
saetest.CmpReceiptsByMerkleRoot(),
saetest.ComparerOptWithNilCheck(func(a, b *types.Block) bool {
return a.Hash() == b.Hash()
}),
cmpopts.EquateComparable(
// We're not running tests concurrently with anything that will
// modify [Block.accepted] nor [Block.executed] so this is safe.
// Using a [cmp.Transformer] would make the linter complain about
// copying.
atomic.Bool{},
),
}
return cmp.Comparer((*Block).equalForTests)
}

func (b *Block) equalForTests(c *Block) bool {
fn := saetest.ComparerWithNilCheck(func(b, c *Block) bool {
if b.Hash() != c.Hash() {
return false
}

fn := saetest.ComparerWithNilCheck(func(b, c *ancestry) bool {
return b.parent.equalForTests(c.parent) && b.lastSettled.equalForTests(c.lastSettled)
})
if !fn(b.ancestry.Load(), c.ancestry.Load()) {
return false
}

return b.execution.Load().equalForTests(c.execution.Load())
fn := cmputils.WithNilCheck(func(b, c *Block) bool {
return true &&
b.Hash() == c.Hash() &&
b.ancestry.Load().equalForTests(c.ancestry.Load()) &&
b.execution.Load().equalForTests(c.execution.Load())
})
return fn(b, c)
}

func (a *ancestry) equalForTests(b *ancestry) bool {
fn := cmputils.WithNilCheck(func(a, b *ancestry) bool {
return true &&
a.parent.equalForTests(b.parent) &&
a.lastSettled.equalForTests(b.lastSettled)
})
return fn(a, b)
}

func (e *executionResults) equalForTests(f *executionResults) bool {
fn := saetest.ComparerWithNilCheck(func(e, f *executionResults) bool {
return e.byGas.Compare(f.byGas.Time) == 0 &&
e.gasUsed == f.gasUsed &&
fn := cmputils.WithNilCheck(func(e, f *executionResults) bool {
return true &&
e.byGas.Rate() == f.byGas.Rate() &&
e.byGas.Compare(f.byGas.Time) == 0 && // N.B. Compare is only valid if rates are equal
e.receiptRoot == f.receiptRoot &&
saetest.MerkleRootsEqual(e.receipts, f.receipts) &&
e.stateRootPost == f.stateRootPost
})
return fn(e, f)
Expand Down
Loading