Skip to content
This repository was archived by the owner on Jun 9, 2026. It is now read-only.

Commit 21ff5ff

Browse files
alarso16StephenButtolphARR4N
authored
feat: Remaining ChainVM methods (#183)
Add GetBlock and GetBlockIDByHeight with basic tests. The refactoring necessary to expose the settling action is really gross, so we might just need to simplify some of that eventually (very panic-prone). Additionally, whether a block is synchronous or not is not persisted, so it cannot be loaded. --------- Co-authored-by: Stephen Buttolph <stephen@avalabs.org> Co-authored-by: Arran Schlosberg <519948+ARR4N@users.noreply.github.com>
1 parent ce3ae38 commit 21ff5ff

8 files changed

Lines changed: 248 additions & 69 deletions

File tree

blocks/block.go

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,9 +17,12 @@ import (
1717
"github.com/ava-labs/avalanchego/vms/components/gas"
1818
"github.com/ava-labs/libevm/common"
1919
"github.com/ava-labs/libevm/core/types"
20+
"github.com/ava-labs/libevm/ethdb"
21+
"github.com/ava-labs/libevm/params"
2022
"go.uber.org/zap"
2123

2224
"github.com/ava-labs/strevm/proxytime"
25+
"github.com/ava-labs/strevm/saedb"
2326
)
2427

2528
// A Block extends a [types.Block] to track SAE-defined concepts of async
@@ -96,6 +99,26 @@ func New(eth *types.Block, parent, lastSettled *Block, log logging.Logger) (*Blo
9699
return b, nil
97100
}
98101

102+
// RestoreSettledBlock constructs a new block with [New] and restores it to an
103+
// settled state before returning it. By definition of being settled, the
104+
// returned block also includes post-execution artefacts.
105+
func RestoreSettledBlock(eth *types.Block, log logging.Logger, db ethdb.Database, xdb saedb.ExecutionResults, config *params.ChainConfig) (*Block, error) {
106+
b, err := New(eth, nil, nil, log)
107+
if err != nil {
108+
return nil, err
109+
}
110+
if err := b.RestoreExecutionArtefacts(db, xdb, config); err != nil {
111+
return nil, fmt.Errorf("restoring to executed state: %v", err)
112+
}
113+
if err := b.markSettled(nil); err != nil {
114+
return nil, fmt.Errorf("restoring to settled state: %v", err)
115+
}
116+
if err := b.CheckInvariants(Settled); err != nil {
117+
return nil, err
118+
}
119+
return b, nil
120+
}
121+
99122
var (
100123
errParentHashMismatch = errors.New("block-parent hash mismatch")
101124
errBlockHeightNotIncrementing = errors.New("block height not incrementing")

blocks/settlement.go

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -83,6 +83,8 @@ func (b *Block) markSettled(lastSettled *atomic.Pointer[Block]) error {
8383
// Unlike [Block.MarkExecuted], MarkSynchronous does not call
8484
// [Block.SetAsHeadBlock], which MUST be done by the caller, i.f.f. the chain
8585
// has not yet commenced asynchronous execution.
86+
//
87+
// TODO(arr4n) refactor to avoid requiring DB writes.
8688
func (b *Block) MarkSynchronous(hooks hook.Points, db ethdb.Database, xdb saedb.ExecutionResults, excessAfter gas.Gas) error {
8789
ethB := b.EthBlock()
8890
// Receipts of a synchronous block have already been "settled" by the block

sae/blocks.go

Lines changed: 67 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@ import (
1111

1212
"github.com/ava-labs/avalanchego/database"
1313
"github.com/ava-labs/avalanchego/ids"
14+
"github.com/ava-labs/avalanchego/snow/consensus/snowman"
1415
"github.com/ava-labs/avalanchego/snow/engine/snowman/block"
1516
"github.com/ava-labs/libevm/common"
1617
"github.com/ava-labs/libevm/core"
@@ -364,23 +365,82 @@ func canonicalBlock(db ethdb.Database, num uint64) (*types.Block, error) {
364365
}
365366

366367
// GetBlock returns the block with the given ID, or [database.ErrNotFound].
368+
//
369+
// It is expected that blocks that have been successfully verified should be
370+
// returned correctly. It is also expected that blocks that have been
371+
// accepted by the consensus engine should be able to be fetched. It is not
372+
// required for blocks that have been rejected by the consensus engine to be
373+
// able to be fetched.
367374
func (vm *VM) GetBlock(ctx context.Context, id ids.ID) (*blocks.Block, error) {
368-
b, ok := vm.blocks.Load(common.Hash(id))
369-
if !ok {
370-
return nil, database.ErrNotFound
371-
}
372-
return b, nil
375+
var _ snowman.Block // protect the input to allow comment linking
376+
377+
return readByHash(
378+
vm,
379+
common.Hash(id),
380+
func(b *blocks.Block) *blocks.Block {
381+
return b
382+
},
383+
func(db ethdb.Reader, hash common.Hash, num uint64) (*blocks.Block, error) {
384+
// A block that's not in memory has either been rejected, not yet
385+
// verified, or settled. Of these, only the latter would be in the
386+
// database.
387+
//
388+
// There is, however, a negligible (read: near impossible) but
389+
// non-zero chance that [VM.VerifyBlock] and [VM.AcceptBlock] were
390+
// *both* called between [readByHash] checking the in-memory block
391+
// store and loading the canonical number from the database. That
392+
// could result in an unexecuted block, which would cause an error
393+
// when restoring it.
394+
//
395+
// TODO(arr4n) I think [readHash] should be providing this guarantee
396+
// as it has access to the [syncMap] and its lock.
397+
if vm.last.settled.Load().Height() < num {
398+
return nil, database.ErrNotFound
399+
}
400+
401+
ethB := rawdb.ReadBlock(db, hash, num)
402+
if num > vm.last.synchronous {
403+
return blocks.RestoreSettledBlock(
404+
ethB,
405+
vm.log(),
406+
vm.db,
407+
vm.xdb,
408+
vm.exec.ChainConfig(),
409+
)
410+
}
411+
412+
b, err := vm.newBlock(ethB, nil, nil)
413+
if err != nil {
414+
return nil, err
415+
}
416+
// Excess is only used for executing the next block, which can never
417+
// be the case if `b` isn't actually the last synchronous block, so
418+
// passing the same value for all is OK.
419+
if err := b.MarkSynchronous(vm.hooks(), vm.db, vm.xdb, vm.config.ExcessAfterLastSynchronous); err != nil {
420+
return nil, err
421+
}
422+
return b, nil
423+
},
424+
database.ErrNotFound,
425+
)
373426
}
374427

375428
// GetBlockIDAtHeight returns the accepted block at the given height, or
376429
// [database.ErrNotFound].
377-
func (vm *VM) GetBlockIDAtHeight(context.Context, uint64) (ids.ID, error) {
378-
return ids.Empty, errUnimplemented
430+
func (vm *VM) GetBlockIDAtHeight(ctx context.Context, height uint64) (ids.ID, error) {
431+
id := ids.ID(rawdb.ReadCanonicalHash(vm.db, height))
432+
if id == ids.Empty {
433+
return id, database.ErrNotFound
434+
}
435+
return id, nil
379436
}
380437

381438
var _ blocks.Source = (*VM)(nil).blockSource
382439

383440
func (vm *VM) blockSource(hash common.Hash, num uint64) (*blocks.Block, bool) {
441+
// TODO(arr4n) this MUST be updated to support all blocks (256) necessary to
442+
// back a [core.ChainContext] in [saexec.Executor] otherwise op codes like
443+
// BLOCKHASH will panic.
384444
b, ok := vm.blocks.Load(hash)
385445
if !ok || b.NumberU64() != num {
386446
return nil, false

sae/rpc.go

Lines changed: 29 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -337,27 +337,27 @@ func (b *ethAPIBackend) SyncProgress() ethereum.SyncProgress {
337337
}
338338

339339
func (b *ethAPIBackend) HeaderByNumber(ctx context.Context, n rpc.BlockNumber) (*types.Header, error) {
340-
return readByNumber(b, n, rawdb.ReadHeader)
340+
return readByNumber(b, n, neverErrs(rawdb.ReadHeader))
341341
}
342342

343343
func (b *ethAPIBackend) BlockByNumber(ctx context.Context, n rpc.BlockNumber) (*types.Block, error) {
344-
return readByNumber(b, n, rawdb.ReadBlock)
344+
return readByNumber(b, n, neverErrs(rawdb.ReadBlock))
345345
}
346346

347347
func (b *ethAPIBackend) HeaderByHash(ctx context.Context, hash common.Hash) (*types.Header, error) {
348-
return readByHash(b, hash, (*blocks.Block).Header, rawdb.ReadHeader), nil
348+
return readByHash(b.vm, hash, (*blocks.Block).Header, neverErrs(rawdb.ReadHeader), nil /* errWhenNotFound */)
349349
}
350350

351351
func (b *ethAPIBackend) BlockByHash(ctx context.Context, hash common.Hash) (*types.Block, error) {
352-
return readByHash(b, hash, (*blocks.Block).EthBlock, rawdb.ReadBlock), nil
352+
return readByHash(b.vm, hash, (*blocks.Block).EthBlock, neverErrs(rawdb.ReadBlock), nil /* errWhenNotFound */)
353353
}
354354

355355
func (b *ethAPIBackend) HeaderByNumberOrHash(ctx context.Context, blockNrOrHash rpc.BlockNumberOrHash) (*types.Header, error) {
356-
return readByNumberOrHash(b, blockNrOrHash, (*blocks.Block).Header, rawdb.ReadHeader)
356+
return readByNumberOrHash(b, blockNrOrHash, (*blocks.Block).Header, neverErrs(rawdb.ReadHeader))
357357
}
358358

359359
func (b *ethAPIBackend) BlockByNumberOrHash(ctx context.Context, blockNrOrHash rpc.BlockNumberOrHash) (*types.Block, error) {
360-
return readByNumberOrHash(b, blockNrOrHash, (*blocks.Block).EthBlock, rawdb.ReadBlock)
360+
return readByNumberOrHash(b, blockNrOrHash, (*blocks.Block).EthBlock, neverErrs(rawdb.ReadBlock))
361361
}
362362

363363
func (b *ethAPIBackend) GetTransaction(ctx context.Context, txHash common.Hash) (exists bool, tx *types.Transaction, blockHash common.Hash, blockNumber uint64, index uint64, err error) {
@@ -415,29 +415,43 @@ func (b *ethAPIBackend) GetPoolTransactions() (types.Transactions, error) {
415415
}
416416

417417
type (
418-
canonicalReader[T any] func(ethdb.Reader, common.Hash, uint64) *T
418+
canonicalReader[T any] func(ethdb.Reader, common.Hash, uint64) (*T, error)
419419
blockAccessor[T any] func(*blocks.Block) *T
420420
)
421421

422+
func neverErrs[T any](fn func(ethdb.Reader, common.Hash, uint64) *T) canonicalReader[T] {
423+
return func(r ethdb.Reader, h common.Hash, n uint64) (*T, error) {
424+
return fn(r, h, n), nil
425+
}
426+
}
427+
422428
func readByNumber[T any](b *ethAPIBackend, n rpc.BlockNumber, read canonicalReader[T]) (*T, error) {
423429
num, err := b.resolveBlockNumber(n)
424430
if errors.Is(err, errFutureBlockNotResolved) {
425431
return nil, nil
426432
} else if err != nil {
427433
return nil, err
428434
}
429-
return read(b.vm.db, rawdb.ReadCanonicalHash(b.vm.db, num), num), nil
435+
return read(b.vm.db, rawdb.ReadCanonicalHash(b.vm.db, num), num)
430436
}
431437

432-
func readByHash[T any](b *ethAPIBackend, hash common.Hash, fromMem blockAccessor[T], fromDB canonicalReader[T]) *T {
433-
if blk, ok := b.vm.blocks.Load(hash); ok {
434-
return fromMem(blk)
438+
// readByHash returns `fromMem(b)` if a block with the specified hash is in the
439+
// VM's memory, otherwise it returns `fromDB()` i.f.f. the block was previously
440+
// accepted. If `fromDB()` is called then the block is guaranteed to exist if
441+
// read with [rawdb] functions.
442+
//
443+
// A hash that is in neither of the VM's memory nor the database will result in
444+
// a return of `(nil, errWhenNotFound)` to allow for usage with the [rawdb]
445+
// pattern of returning `(nil, nil)`.
446+
func readByHash[T any](vm *VM, hash common.Hash, fromMem blockAccessor[T], fromDB canonicalReader[T], errWhenNotFound error) (*T, error) {
447+
if blk, ok := vm.blocks.Load(hash); ok {
448+
return fromMem(blk), nil
435449
}
436-
num := rawdb.ReadHeaderNumber(b.vm.db, hash)
450+
num := rawdb.ReadHeaderNumber(vm.db, hash)
437451
if num == nil {
438-
return nil
452+
return nil, errWhenNotFound
439453
}
440-
return fromDB(b.vm.db, hash, *num)
454+
return fromDB(vm.db, hash, *num)
441455
}
442456

443457
// TODO(arr4n) DRY [readByHash] and [readByNumberOrHash]
@@ -450,7 +464,7 @@ func readByNumberOrHash[T any](b *ethAPIBackend, blockNrOrHash rpc.BlockNumberOr
450464
if blk, ok := b.vm.blocks.Load(hash); ok {
451465
return fromMem(blk), nil
452466
}
453-
return fromDB(b.vm.db, hash, n), nil
467+
return fromDB(b.vm.db, hash, n)
454468
}
455469

456470
var (

sae/rpc_stateful_test.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -43,7 +43,7 @@ func TestEthCall(t *testing.T) {
4343
}
4444

4545
sign := sut.wallet.SetNonceAndSign
46-
b := sut.createAndAcceptBlock(t, sign(t, 0, deploy), sign(t, 0, deposit))
46+
b := sut.runConsensusLoopFromLastAccepted(t, sign(t, 0, deploy), sign(t, 0, deposit))
4747
require.Len(t, b.Transactions(), 2, "tx count")
4848
require.NoErrorf(t, b.WaitUntilExecuted(ctx), "%T.WaitUntilExecuted()", b)
4949
for _, r := range b.Receipts() {

sae/rpc_test.go

Lines changed: 11 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -436,15 +436,15 @@ func TestEthGetters(t *testing.T) {
436436

437437
// Once a block is settled, its ancestors are only accessible from the
438438
// database.
439-
onDisk := sut.createAndAcceptBlock(t, createTx(t, zeroAddr))
439+
onDisk := sut.runConsensusLoopFromLastAccepted(t, createTx(t, zeroAddr))
440440

441-
settled := sut.createAndAcceptBlock(t, createTx(t, zeroAddr))
441+
settled := sut.runConsensusLoopFromLastAccepted(t, createTx(t, zeroAddr))
442442
vmTime.advanceToSettle(ctx, t, settled)
443443

444-
executed := sut.createAndAcceptBlock(t, createTx(t, zeroAddr))
444+
executed := sut.runConsensusLoopFromLastAccepted(t, createTx(t, zeroAddr))
445445
require.NoErrorf(t, executed.WaitUntilExecuted(ctx), "%T.WaitUntilExecuted()", executed)
446446

447-
pending := sut.createAndAcceptBlock(t, createTx(t, blockingPrecompile))
447+
pending := sut.runConsensusLoopFromLastAccepted(t, createTx(t, blockingPrecompile))
448448

449449
for _, b := range []*blocks.Block{genesis, onDisk, settled, executed, pending} {
450450
t.Run(fmt.Sprintf("block_num_%d", b.Height()), func(t *testing.T) {
@@ -532,15 +532,15 @@ func TestGetLogs(t *testing.T) {
532532
// and therefore moved to disk.
533533
indexed := make([]*blocks.Block, bloomSectionSize)
534534
for i := range indexed {
535-
indexed[i] = sut.createAndAcceptBlock(t, txWithLog(t))
535+
indexed[i] = sut.runConsensusLoopFromLastAccepted(t, txWithLog(t))
536536
}
537537

538-
settled := sut.createAndAcceptBlock(t, txWithLog(t))
538+
settled := sut.runConsensusLoopFromLastAccepted(t, txWithLog(t))
539539
vmTime.advanceToSettle(ctx, t, settled)
540540

541-
noLogs := sut.createAndAcceptBlock(t, txWithoutLog(t))
541+
noLogs := sut.runConsensusLoopFromLastAccepted(t, txWithoutLog(t))
542542

543-
executed := sut.createAndAcceptBlock(t, txWithLog(t))
543+
executed := sut.runConsensusLoopFromLastAccepted(t, txWithLog(t))
544544
require.NoErrorf(t, executed.WaitUntilExecuted(ctx), "%T.WaitUntilExecuted()", executed)
545545

546546
// Although the FiltersAPI will work without any blocks indexed, such a
@@ -698,7 +698,7 @@ func TestGetReceipts(t *testing.T) {
698698

699699
slice := func(t *testing.T, from, to int) (*blocks.Block, []*types.Receipt) {
700700
t.Helper()
701-
b := sut.createAndAcceptBlock(t, txs[from:to]...)
701+
b := sut.runConsensusLoopFromLastAccepted(t, txs[from:to]...)
702702
rs := want[from:to]
703703

704704
var totalGas uint64
@@ -725,7 +725,7 @@ func TestGetReceipts(t *testing.T) {
725725
Gas: params.TxGas,
726726
GasPrice: big.NewInt(1),
727727
})
728-
pending := sut.createAndAcceptBlock(t, pendingTx)
728+
pending := sut.runConsensusLoopFromLastAccepted(t, pendingTx)
729729

730730
var tests []rpcTest
731731
for _, tc := range []struct {
@@ -913,7 +913,7 @@ func TestDebugGetRawTransaction(t *testing.T) {
913913
Gas: params.TxGas,
914914
GasFeeCap: big.NewInt(1),
915915
})
916-
b := sut.createAndAcceptBlock(t, tx)
916+
b := sut.runConsensusLoopFromLastAccepted(t, tx)
917917
require.NoErrorf(t, b.WaitUntilExecuted(ctx), "%T.WaitUntilExecuted()", b)
918918

919919
marshaled, err := tx.MarshalBinary()

sae/vm.go

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -60,6 +60,7 @@ type VM struct {
6060
preference atomic.Pointer[blocks.Block]
6161
last struct {
6262
accepted, settled atomic.Pointer[blocks.Block]
63+
synchronous uint64
6364
}
6465

6566
exec *saexec.Executor
@@ -142,9 +143,9 @@ func NewVM(
142143
if err != nil {
143144
return nil, fmt.Errorf("blocks.New([last synchronous], ...): %v", err)
144145
}
146+
vm.last.synchronous = lastSync.Height()
145147

146148
{ // ========== Sync -> Async ==========
147-
// TODO(arr4n) refactor to avoid DB writes on every startup.
148149
if err := lastSync.MarkSynchronous(cfg.Hooks, db, xdb, cfg.ExcessAfterLastSynchronous); err != nil {
149150
return nil, fmt.Errorf("%T{genesis}.MarkSynchronous(): %v", lastSync, err)
150151
}

0 commit comments

Comments
 (0)