Skip to content

Commit eb533e6

Browse files
feat: blocks package (#18)
Introduces the `blocks.Block` type (and associated helpers), which satisfies the `adaptor.BlockProperties` interface introduced in #7. While the motivation of specific functionality may not be clear in isolation, context can be garnered from the prototype code (f79aff0) and how it uses this package in `saexec` and the primary SAE implementation. --------- Signed-off-by: Arran Schlosberg <[email protected]> Co-authored-by: Stephen Buttolph <[email protected]>
1 parent 13ad8fe commit eb533e6

File tree

18 files changed

+2043
-20
lines changed

18 files changed

+2043
-20
lines changed

.envrc

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
export GOROOT="$(go1.24.7 env GOROOT)"
2+
PATH_add "$(go1.24.7 env GOROOT)/bin"

.github/workflows/lint.yml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -36,7 +36,7 @@ jobs:
3636
- name: golangci-lint
3737
uses: golangci/golangci-lint-action@v6
3838
with:
39-
version: v1.63.3
39+
version: v1.64.8
4040

4141
yamllint:
4242
runs-on: ubuntu-latest

.golangci.yml

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -82,6 +82,11 @@ linters-settings:
8282
severity: warning
8383
disabled: false
8484

85+
testifylint:
86+
enable-all: true
87+
disable:
88+
- require-error # Blanket usage of require over assert is an anti-pattern
89+
8590
issues:
8691
include:
8792
# Many of the default exclusions are because, verbatim "Annoying issue",

blocks/block.go

Lines changed: 116 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,116 @@
1+
// Copyright (C) 2025, Ava Labs, Inc. All rights reserved.
2+
// See the file LICENSE for licensing terms.
3+
4+
// Package blocks defines [Streaming Asynchronous Execution] (SAE) blocks.
5+
//
6+
// [Streaming Asynchronous Execution]: https://github.com/avalanche-foundation/ACPs/tree/main/ACPs/194-streaming-asynchronous-execution
7+
package blocks
8+
9+
import (
10+
"errors"
11+
"fmt"
12+
"runtime"
13+
"sync/atomic"
14+
15+
"github.com/ava-labs/avalanchego/utils/logging"
16+
"github.com/ava-labs/libevm/core/types"
17+
"go.uber.org/zap"
18+
)
19+
20+
// A Block extends a [types.Block] to track SAE-defined concepts of async
21+
// execution and settlement. It MUST be constructed with [New].
22+
type Block struct {
23+
b *types.Block
24+
// Invariant: ancestry is non-nil and contains non-nil pointers i.f.f. the
25+
// block hasn't itself been settled. A synchronous block (e.g. SAE genesis
26+
// or the last pre-SAE block) is always considered settled.
27+
//
28+
// Rationale: the ancestral pointers form a linked list that would prevent
29+
// garbage collection if not severed. Once a block is settled there is no
30+
// need to inspect its history so we sacrifice the ancestors to the GC
31+
// Overlord as a sign of our unwavering fealty. See [InMemoryBlockCount] for
32+
// observability.
33+
ancestry atomic.Pointer[ancestry]
34+
// Only the genesis block or the last pre-SAE block is synchronous. These
35+
// are self-settling by definition so their `ancestry` MUST be nil.
36+
synchronous bool
37+
// Non-nil i.f.f. [Block.MarkExecuted] or [Block.ResotrePostExecutionState]
38+
// have returned without error.
39+
execution atomic.Pointer[executionResults]
40+
41+
// Allows this block to be ruled out as able to be settled at a particular
42+
// time (i.e. if this field is >= said time). The pointer MAY be nil if
43+
// execution is yet to commence. For more details, see
44+
// [Block.SetInterimExecutionTime for setting and [LastToSettleAt] for
45+
// usage.
46+
executionExceededSecond atomic.Pointer[uint64]
47+
48+
executed chan struct{} // closed after `execution` is set
49+
settled chan struct{} // closed after `ancestry` is cleared
50+
51+
log logging.Logger
52+
}
53+
54+
var inMemoryBlockCount atomic.Int64
55+
56+
// InMemoryBlockCount returns the number of blocks created with [New] that are
57+
// yet to have their GC finalizers run.
58+
func InMemoryBlockCount() int64 {
59+
return inMemoryBlockCount.Load()
60+
}
61+
62+
// New constructs a new Block.
63+
func New(eth *types.Block, parent, lastSettled *Block, log logging.Logger) (*Block, error) {
64+
b := &Block{
65+
b: eth,
66+
executed: make(chan struct{}),
67+
settled: make(chan struct{}),
68+
}
69+
70+
inMemoryBlockCount.Add(1)
71+
runtime.AddCleanup(b, func(struct{}) {
72+
inMemoryBlockCount.Add(-1)
73+
}, struct{}{})
74+
75+
if err := b.setAncestors(parent, lastSettled); err != nil {
76+
return nil, err
77+
}
78+
b.log = log.With(
79+
zap.Uint64("height", b.Height()),
80+
zap.Stringer("hash", b.Hash()),
81+
)
82+
return b, nil
83+
}
84+
85+
var (
86+
errParentHashMismatch = errors.New("block-parent hash mismatch")
87+
errHashMismatch = errors.New("block hash mismatch")
88+
)
89+
90+
func (b *Block) setAncestors(parent, lastSettled *Block) error {
91+
if parent != nil {
92+
if got, want := parent.Hash(), b.ParentHash(); got != want {
93+
return fmt.Errorf("%w: constructing Block with parent hash %v; expecting %v", errParentHashMismatch, got, want)
94+
}
95+
}
96+
b.ancestry.Store(&ancestry{
97+
parent: parent,
98+
lastSettled: lastSettled,
99+
})
100+
return nil
101+
}
102+
103+
// CopyAncestorsFrom populates the [Block.ParentBlock] and [Block.LastSettled]
104+
// values, typically only required during database recovery. The source block
105+
// MUST have the same hash as b.
106+
//
107+
// Although the individual ancestral blocks are shallow copied, calling
108+
// [Block.MarkSettled] on either the source or destination will NOT clear the
109+
// pointers of the other.
110+
func (b *Block) CopyAncestorsFrom(c *Block) error {
111+
if from, to := c.Hash(), b.Hash(); from != to {
112+
return fmt.Errorf("%w: copying internals from block %#x to %#x", errHashMismatch, from, to)
113+
}
114+
a := c.ancestry.Load()
115+
return b.setAncestors(a.parent, a.lastSettled)
116+
}

blocks/block_test.go

Lines changed: 110 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,110 @@
1+
// Copyright (C) 2025, Ava Labs, Inc. All rights reserved.
2+
// See the file LICENSE for licensing terms.
3+
4+
package blocks
5+
6+
import (
7+
"math/big"
8+
"testing"
9+
10+
"github.com/ava-labs/avalanchego/utils/logging"
11+
"github.com/ava-labs/libevm/core/types"
12+
"github.com/google/go-cmp/cmp"
13+
"github.com/stretchr/testify/assert"
14+
"github.com/stretchr/testify/require"
15+
16+
"github.com/ava-labs/strevm/saetest"
17+
)
18+
19+
func newEthBlock(num, time uint64, parent *types.Block) *types.Block {
20+
hdr := &types.Header{
21+
Number: new(big.Int).SetUint64(num),
22+
Time: time,
23+
}
24+
if parent != nil {
25+
hdr.ParentHash = parent.Hash()
26+
}
27+
return types.NewBlockWithHeader(hdr)
28+
}
29+
30+
func newBlock(tb testing.TB, eth *types.Block, parent, lastSettled *Block) *Block {
31+
tb.Helper()
32+
b, err := New(eth, parent, lastSettled, saetest.NewTBLogger(tb, logging.Warn))
33+
require.NoError(tb, err, "New()")
34+
return b
35+
}
36+
37+
func newChain(tb testing.TB, startHeight, total uint64, lastSettledAtHeight map[uint64]uint64) []*Block {
38+
tb.Helper()
39+
40+
var (
41+
ethParent *types.Block
42+
parent *Block
43+
blocks []*Block
44+
)
45+
byNum := make(map[uint64]*Block)
46+
47+
for i := range total {
48+
n := startHeight + i
49+
50+
var (
51+
settle *Block
52+
synchronous bool
53+
)
54+
if s, ok := lastSettledAtHeight[n]; ok {
55+
if s == n {
56+
require.Zero(tb, s, "Only genesis block is self-settling")
57+
synchronous = true
58+
} else {
59+
require.Less(tb, s, n, "Last-settled height MUST be <= current height")
60+
settle = byNum[s]
61+
}
62+
}
63+
64+
b := newBlock(tb, newEthBlock(n, n /*time*/, ethParent), parent, settle)
65+
byNum[n] = b
66+
blocks = append(blocks, b)
67+
if synchronous {
68+
require.NoError(tb, b.MarkSynchronous(), "MarkSynchronous()")
69+
}
70+
71+
parent = byNum[n]
72+
ethParent = parent.EthBlock()
73+
}
74+
75+
return blocks
76+
}
77+
78+
func TestSetAncestors(t *testing.T) {
79+
parent := newBlock(t, newEthBlock(5, 5, nil), nil, nil)
80+
lastSettled := newBlock(t, newEthBlock(3, 0, nil), nil, nil)
81+
child := newEthBlock(6, 6, parent.EthBlock())
82+
83+
t.Run("incorrect_parent", func(t *testing.T) {
84+
// Note that the arguments to [New] are inverted.
85+
_, err := New(child, lastSettled, parent, logging.NoLog{})
86+
require.ErrorIs(t, err, errParentHashMismatch, "New() with inverted parent and last-settled blocks")
87+
})
88+
89+
source := newBlock(t, child, parent, lastSettled)
90+
dest := newBlock(t, child, nil, nil)
91+
92+
t.Run("destination_before_copy", func(t *testing.T) {
93+
assert.Nilf(t, dest.ParentBlock(), "%T.ParentBlock()", dest)
94+
assert.Nilf(t, dest.LastSettled(), "%T.LastSettled()", dest)
95+
})
96+
if t.Failed() {
97+
t.FailNow()
98+
}
99+
100+
require.NoError(t, dest.CopyAncestorsFrom(source), "CopyAncestorsFrom()")
101+
if diff := cmp.Diff(source, dest, CmpOpt()); diff != "" {
102+
t.Errorf("After %T.CopyAncestorsFrom(); diff (-want +got):\n%s", dest, diff)
103+
}
104+
105+
t.Run("incompatible_destination_block", func(t *testing.T) {
106+
ethB := newEthBlock(dest.Height()+1 /*mismatch*/, dest.BuildTime(), parent.EthBlock())
107+
dest := newBlock(t, ethB, nil, nil)
108+
require.ErrorIs(t, dest.CopyAncestorsFrom(source), errHashMismatch)
109+
})
110+
}

blocks/cmpopt.go

Lines changed: 50 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,50 @@
1+
// Copyright (C) 2025, Ava Labs, Inc. All rights reserved.
2+
// See the file LICENSE for licensing terms.
3+
4+
//go:build !prod && !nocmpopts
5+
6+
package blocks
7+
8+
import (
9+
"github.com/google/go-cmp/cmp"
10+
11+
"github.com/ava-labs/strevm/cmputils"
12+
"github.com/ava-labs/strevm/saetest"
13+
)
14+
15+
// CmpOpt returns a configuration for [cmp.Diff] to compare [Block] instances in
16+
// tests.
17+
func CmpOpt() cmp.Option {
18+
return cmp.Comparer((*Block).equalForTests)
19+
}
20+
21+
func (b *Block) equalForTests(c *Block) bool {
22+
fn := cmputils.WithNilCheck(func(b, c *Block) bool {
23+
return true &&
24+
b.Hash() == c.Hash() &&
25+
b.ancestry.Load().equalForTests(c.ancestry.Load()) &&
26+
b.execution.Load().equalForTests(c.execution.Load())
27+
})
28+
return fn(b, c)
29+
}
30+
31+
func (a *ancestry) equalForTests(b *ancestry) bool {
32+
fn := cmputils.WithNilCheck(func(a, b *ancestry) bool {
33+
return true &&
34+
a.parent.equalForTests(b.parent) &&
35+
a.lastSettled.equalForTests(b.lastSettled)
36+
})
37+
return fn(a, b)
38+
}
39+
40+
func (e *executionResults) equalForTests(f *executionResults) bool {
41+
fn := cmputils.WithNilCheck(func(e, f *executionResults) bool {
42+
return true &&
43+
e.byGas.Rate() == f.byGas.Rate() &&
44+
e.byGas.Compare(f.byGas.Time) == 0 && // N.B. Compare is only valid if rates are equal
45+
e.receiptRoot == f.receiptRoot &&
46+
saetest.MerkleRootsEqual(e.receipts, f.receipts) &&
47+
e.stateRootPost == f.stateRootPost
48+
})
49+
return fn(e, f)
50+
}

0 commit comments

Comments
 (0)