Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
31 commits
Select commit Hold shift + click to select a range
04e02e9
feat: `blocks` package
ARR4N Sep 11, 2025
741f88d
chore: placate the linter
ARR4N Sep 12, 2025
3ddbe1e
chore: include `.envrc`
ARR4N Sep 15, 2025
6b07f43
refactor: stop embedding `types.Block` due to ambiguous methods
ARR4N Sep 16, 2025
f0e8bde
refactor: change `Block.Time()` to `Block.BuildTime()` to disambiguat…
ARR4N Sep 16, 2025
8585c42
refactor: use `int64` for in-memory block counter
ARR4N Sep 16, 2025
a844571
refactor: avoid `fmt.Sprintf` in logging
ARR4N Sep 16, 2025
4612b9c
refactor: avoid extra malloc for db keys
ARR4N Sep 16, 2025
c93f340
doc: `Block.executionExceededSecond` field
ARR4N Sep 16, 2025
c122500
fix: log `Block.Bytes()` errors at `ERROR`
ARR4N Sep 16, 2025
70e6459
refactor: sentinel error if ancestry changes during settlement
ARR4N Sep 16, 2025
23e2f65
fix: import needed by accepted suggestion
ARR4N Sep 16, 2025
6f79b42
refactor: rename to `Block.ChildSettles()` and improve comment
ARR4N Sep 16, 2025
7153bf7
test: propagate `Block` logs to `testing.TB`
ARR4N Sep 16, 2025
75720f9
chore: new line at end of `.envrc`
ARR4N Sep 16, 2025
5bb58d3
chore: `go mod tidy`
ARR4N Sep 16, 2025
e324ce8
fix: `Settles()` of synchronous (genesis) block
ARR4N Sep 16, 2025
4810c9c
refactor: remove override of `nil` map as read-only
ARR4N Sep 16, 2025
d3f5b04
refactor: rename (again) to `Block.WhenChildSettles()`
ARR4N Sep 17, 2025
abfc7f3
feat: `LastToSettleAt()` supports unknown grandchild execution time (…
StephenButtolph Sep 17, 2025
9216884
refactor: remove unnecessary `type brokenInvariant`
ARR4N Sep 17, 2025
6200fdd
refactor: move `Block.brokenInvariantErr()` implementation
ARR4N Sep 17, 2025
9c14814
chore!: delete `db.go` to defer implementation
ARR4N Sep 17, 2025
a7fa4a9
chore!: actually delete `db.go`, not just call sites
ARR4N Sep 17, 2025
cdbb8ca
fix: guarantee that `LastToSettleAt()` loop exits
ARR4N Sep 17, 2025
0753526
feat!: `MarkExecuted()` accepts gas base fee
ARR4N Sep 19, 2025
e45f696
chore: remove unused `canoto` functionality
ARR4N Sep 19, 2025
2d8b93c
chore: remove unused `canoto` tags
ARR4N Sep 19, 2025
ec708b0
doc: differences between `MarkSettled()` and `MarkSynchronous()` methods
ARR4N Sep 22, 2025
12890b7
refactor: use `require.Zero`
ARR4N Sep 26, 2025
321e284
doc: `LastSttled()` and `ParentBlock()` error logging
ARR4N Sep 26, 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
2 changes: 2 additions & 0 deletions .envrc
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
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
116 changes: 116 additions & 0 deletions blocks/block.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,116 @@
// 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
package blocks

import (
"errors"
"fmt"
"runtime"
"sync/atomic"

"github.com/ava-labs/avalanchego/utils/logging"
"github.com/ava-labs/libevm/core/types"
"go.uber.org/zap"
)

// A Block extends a [types.Block] to track SAE-defined concepts of async
// execution and settlement. It MUST be constructed with [New].
type Block struct {
b *types.Block
// Invariant: ancestry is non-nil and contains non-nil pointers i.f.f. the
// block hasn't itself been settled. A synchronous block (e.g. SAE genesis
// or the last pre-SAE block) is always considered settled.
//
// 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. 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]

// 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
settled chan struct{} // closed after `ancestry` is cleared

log logging.Logger
}

var inMemoryBlockCount atomic.Int64

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

// New constructs a new Block.
func New(eth *types.Block, parent, lastSettled *Block, log logging.Logger) (*Block, error) {
b := &Block{
b: eth,
executed: make(chan struct{}),
settled: make(chan struct{}),
}

inMemoryBlockCount.Add(1)
runtime.AddCleanup(b, func(struct{}) {
inMemoryBlockCount.Add(-1)
}, struct{}{})

if err := b.setAncestors(parent, lastSettled); err != nil {
return nil, err
}
b.log = log.With(
zap.Uint64("height", b.Height()),
zap.Stringer("hash", b.Hash()),
)
return b, nil
}

var (
errParentHashMismatch = errors.New("block-parent hash mismatch")
errHashMismatch = errors.New("block hash mismatch")
)

func (b *Block) setAncestors(parent, lastSettled *Block) error {
if parent != nil {
if got, want := parent.Hash(), b.ParentHash(); got != want {
return fmt.Errorf("%w: constructing Block with parent hash %v; expecting %v", errParentHashMismatch, got, want)
}
}
b.ancestry.Store(&ancestry{
parent: parent,
lastSettled: lastSettled,
})
return nil
}

// 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)
}
a := c.ancestry.Load()
return b.setAncestors(a.parent, a.lastSettled)
}
110 changes: 110 additions & 0 deletions blocks/block_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,110 @@
// Copyright (C) 2025, Ava Labs, Inc. All rights reserved.
// See the file LICENSE for licensing terms.

package blocks

import (
"math/big"
"testing"

"github.com/ava-labs/avalanchego/utils/logging"
"github.com/ava-labs/libevm/core/types"
"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 {
hdr := &types.Header{
Number: new(big.Int).SetUint64(num),
Time: time,
}
if parent != nil {
hdr.ParentHash = parent.Hash()
}
return types.NewBlockWithHeader(hdr)
}

func newBlock(tb testing.TB, eth *types.Block, parent, lastSettled *Block) *Block {
tb.Helper()
b, err := New(eth, parent, lastSettled, saetest.NewTBLogger(tb, logging.Warn))
require.NoError(tb, err, "New()")
return b
}

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

var (
ethParent *types.Block
parent *Block
blocks []*Block
)
byNum := make(map[uint64]*Block)

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

var (
settle *Block
synchronous bool
)
if s, ok := lastSettledAtHeight[n]; ok {
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]
}
}

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.EthBlock()
}

return blocks
}

func TestSetAncestors(t *testing.T) {
parent := newBlock(t, newEthBlock(5, 5, nil), nil, nil)
lastSettled := newBlock(t, newEthBlock(3, 0, nil), nil, nil)
child := newEthBlock(6, 6, parent.EthBlock())

t.Run("incorrect_parent", func(t *testing.T) {
// Note that the arguments to [New] are inverted.
_, err := New(child, lastSettled, parent, logging.NoLog{})
require.ErrorIs(t, err, errParentHashMismatch, "New() with inverted parent and last-settled blocks")
})

source := newBlock(t, child, parent, lastSettled)
dest := newBlock(t, child, nil, nil)

t.Run("destination_before_copy", func(t *testing.T) {
assert.Nilf(t, dest.ParentBlock(), "%T.ParentBlock()", dest)
assert.Nilf(t, dest.LastSettled(), "%T.LastSettled()", dest)
})
if t.Failed() {
t.FailNow()
}

require.NoError(t, dest.CopyAncestorsFrom(source), "CopyAncestorsFrom()")
if diff := cmp.Diff(source, dest, CmpOpt()); diff != "" {
t.Errorf("After %T.CopyAncestorsFrom(); diff (-want +got):\n%s", dest, diff)
}

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

//go:build !prod && !nocmpopts

package blocks

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

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

// CmpOpt returns a configuration for [cmp.Diff] to compare [Block] instances in
// tests.
func CmpOpt() cmp.Option {
return cmp.Comparer((*Block).equalForTests)
}

func (b *Block) equalForTests(c *Block) bool {
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 := 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)
}
Loading
Loading