Skip to content
Open
Show file tree
Hide file tree
Changes from 53 commits
Commits
Show all changes
58 commits
Select commit Hold shift + click to select a range
171cd6f
feat: `saexec` package
ARR4N Sep 26, 2025
0b59c0e
feat: `txgossip` package
ARR4N Nov 11, 2025
5eca897
Merge branch 'main' into arr4n/saexec
ARR4N Nov 17, 2025
1fd6ecf
refactor!: use MIT contract for testing
ARR4N Nov 17, 2025
bd0a0c3
fix: log errored tx execution and continue
ARR4N Nov 17, 2025
f3ca5e7
doc: fix misspelling
ARR4N Nov 17, 2025
d04027f
refactor: `escrow` package provides contract helpers
ARR4N Nov 17, 2025
f442e76
fix: remove duplicated log fields
ARR4N Nov 17, 2025
d74bccc
refactor: `newSUT()` constructs memory database
ARR4N Nov 17, 2025
90a49eb
feat: `saetest.TBLogger` errors cancel context
ARR4N Nov 17, 2025
0245a1b
feat: user-defined before- and after-block hooks
ARR4N Nov 17, 2025
fc2f6fc
Merge branch 'main' into arr4n/saexec
ARR4N Nov 17, 2025
0183d17
feat: `hook.Points` supports sub-second block times
ARR4N Nov 17, 2025
e7772ce
chore: placate the linter
ARR4N Nov 17, 2025
03aa639
chore: if `golangci-lint` just said what it wanted then this would al…
ARR4N Nov 17, 2025
3023e06
Merge branch 'arr4n/saexec' into arr4n/mempool
ARR4N Nov 17, 2025
4e46f0b
fix: `tsgossip` test
ARR4N Nov 17, 2025
60c8273
build: bump `crate-crypto/go-kzg-4844`
ARR4N Nov 17, 2025
472f9fb
chore: Linty McLintface
ARR4N Nov 18, 2025
433494f
fix: contextual opcodes, particularly `BLOCKHASH`
ARR4N Nov 18, 2025
d56614d
refactor: `defer` unlocking `CondVar.L`
ARR4N Nov 18, 2025
f7bcfb1
chore: no points for guessing
ARR4N Nov 18, 2025
476e73c
refactor: use `zap.Stringer` for block hash
ARR4N Nov 18, 2025
18ee4b5
refactor: move `TimeNotThreadsafe` to test file and un-export
ARR4N Nov 18, 2025
6feac2a
refactor: simplify `ChainBuilder.NewBlock` options handling
ARR4N Nov 18, 2025
eb40cdd
test: `BASEFEE` op code
ARR4N Nov 18, 2025
9e314c4
test: additional contextual op codes
ARR4N Nov 18, 2025
a4b244c
Merge branch 'arr4n/saexec' into arr4n/mempool
ARR4N Nov 18, 2025
dcccec9
fix: push-gossiper test
ARR4N Nov 18, 2025
5b5304c
chore: placate the linter
ARR4N Nov 18, 2025
e042828
refactor: exponential backoff for `Enqueue()` wait warning
ARR4N Nov 18, 2025
894b190
doc: permalink to `Escrow.sol`
ARR4N Nov 18, 2025
88d0728
refactor: return `consensus.Engine(nil)`
ARR4N Nov 18, 2025
f55b3b9
refactor: correct parenthesis matching
ARR4N Nov 18, 2025
00e5417
refactor: stop embedding `BlockSource` in `chainContext`
ARR4N Nov 18, 2025
c1f12b8
fix: `ChainBuilder` uses `sync.Map` for blocks stored by hash
ARR4N Nov 18, 2025
4fe05c1
refactor: remove specific type from unindexed `for range`
ARR4N Nov 18, 2025
0fafec4
refactor: determine contract address from `Nonce()` value of tx
ARR4N Nov 18, 2025
e64de9c
refactor: use `math.Pow()` for calculating gas price in test
ARR4N Nov 18, 2025
beed9ca
refactor: remove `executionScratchSpace` and `Executor.init()`
ARR4N Nov 18, 2025
c6d1ef2
refactor: make gas clock internal variable in `Executor.execute()`
ARR4N Nov 18, 2025
48342cd
fix: `ApplyTransaction` errors are `FATAL`
ARR4N Nov 19, 2025
caf3242
feat: `snapshot.Tree.Cap(..., 0)` on shutdown to persist top layer
ARR4N Nov 19, 2025
2f17888
Merge branch 'arr4n/saexec' into arr4n/mempool
ARR4N Nov 19, 2025
0789c80
refactor: abstract `blocks.Source`
ARR4N Nov 19, 2025
15ea194
doc: improved comments / error messages in `Executor` integration test
ARR4N Nov 19, 2025
97b474a
refactor: abstract `cmputils.TransactionsByHash()`
ARR4N Nov 19, 2025
0e3a778
refactor: review suggestions
ARR4N Nov 19, 2025
06256a9
refactor!: don't disable snapshot generation on shutdown
ARR4N Nov 19, 2025
f41f1a7
refactor: base block-ordering assertion on parent hash
ARR4N Nov 19, 2025
eb39d06
Merge branch 'arr4n/saexec' into arr4n/mempool
ARR4N Nov 19, 2025
3d9b416
Merge branch 'main' into arr4n/mempool
ARR4N Nov 19, 2025
7ed61a4
build: revert `go.mod` changes
ARR4N Nov 19, 2025
ef30b88
refactor: `Close()` method on `Set()`
ARR4N Nov 21, 2025
4ae521b
Merge branch 'main' into arr4n/mempool
ARR4N Nov 21, 2025
46199a2
feat: Bloom filter populated at `Set` construciton w/ txs already in …
ARR4N Nov 21, 2025
7358fcb
test: txs added directly to `TxPool` still populate Bloom filter
ARR4N Nov 21, 2025
9c79186
feat: dynamically resize Bloom filter based on mempool size
ARR4N Nov 21, 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
25 changes: 25 additions & 0 deletions blocks/block.go
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ import (
"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 Down Expand Up @@ -114,3 +115,27 @@ func (b *Block) CopyAncestorsFrom(c *Block) error {
a := c.ancestry.Load()
return b.setAncestors(a.parent, a.lastSettled)
}

// A Source returns a [Block] that matches both a hash and number, and a boolean
// indicating if such a block was found.
type Source func(hash common.Hash, number uint64) (*Block, bool)

// EthBlock returns the [types.Block] with the given hash and number, or nil if
// not found.
func (s Source) EthBlock(h common.Hash, n uint64) *types.Block {
b, ok := s(h, n)
if !ok {
return nil
}
return b.EthBlock()
}

// Header returns the [types.Header] with the given hash and number, or nil if
// not found.
func (s Source) Header(h common.Hash, n uint64) *types.Header {
b, ok := s(h, n)
if !ok {
return nil
}
return b.Header()
}
2 changes: 2 additions & 0 deletions blocks/blockstest/chain.go
Original file line number Diff line number Diff line change
Expand Up @@ -99,6 +99,8 @@ func (cb *ChainBuilder) AllExceptGenesis() []*blocks.Block {
return slices.Clone(cb.chain[1:])
}

var _ blocks.Source = (*ChainBuilder)(nil).GetBlock

// GetBlock returns the block with specified hash and height, and a flag
// indicating if it was found. If either argument does not match, it returns
// `nil, false`.
Expand Down
8 changes: 8 additions & 0 deletions cmputils/types.go
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,14 @@ func BlocksByHash() cmp.Option {
})
}

// TransactionsByHash returns a [cmp.Comparer] for [types.Transaction] pointers,
// equating them by hash alone.
func TransactionsByHash() cmp.Option {
return ComparerWithNilCheck(func(t, u *types.Transaction) bool {
return t.Hash() == u.Hash()
})
}

// ReceiptsByTxHash returns a [cmp.Comparer] for [types.Receipt] pointers,
// equating them by transaction hash alone.
func ReceiptsByTxHash() cmp.Option {
Expand Down
2 changes: 1 addition & 1 deletion go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -76,7 +76,7 @@ require (
github.com/matttproud/golang_protobuf_extensions v1.0.4 // indirect
github.com/mr-tron/base58 v1.2.0 // indirect
github.com/pmezard/go-difflib v1.0.0 // indirect
github.com/prometheus/client_golang v1.16.0 // indirect
github.com/prometheus/client_golang v1.16.0
github.com/prometheus/client_model v0.3.0 // indirect
github.com/prometheus/common v0.42.0 // indirect
github.com/prometheus/procfs v0.10.1 // indirect
Expand Down
12 changes: 2 additions & 10 deletions saexec/context.go
Original file line number Diff line number Diff line change
Expand Up @@ -13,23 +13,15 @@ import (
"github.com/ava-labs/strevm/blocks"
)

// A BlockSource returns a block that matches both a hash and number, or nil
// if not found.
type BlockSource func(hash common.Hash, number uint64) *blocks.Block

var _ core.ChainContext = (*chainContext)(nil)

type chainContext struct {
blocks BlockSource
blocks blocks.Source
log logging.Logger
}

func (c *chainContext) GetHeader(h common.Hash, n uint64) *types.Header {
b := c.blocks(h, n)
if b == nil {
return nil
}
return b.Header()
return c.blocks.Header(h, n)
}

func (c *chainContext) Engine() consensus.Engine {
Expand Down
2 changes: 2 additions & 0 deletions saexec/execution.go
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,8 @@ func (e *Executor) Enqueue(ctx context.Context, block *blocks.Block) error {
for {
select {
case e.queue <- block:
e.lastEnqueued.Store(block)
e.enqueueEvents.Send(block.EthBlock())
return nil

case <-ctx.Done():
Expand Down
19 changes: 13 additions & 6 deletions saexec/saexec.go
Original file line number Diff line number Diff line change
Expand Up @@ -31,12 +31,13 @@ type Executor struct {
log logging.Logger
hooks hook.Points

queue chan *blocks.Block
lastExecuted atomic.Pointer[blocks.Block]
queue chan *blocks.Block
lastEnqueued, lastExecuted atomic.Pointer[blocks.Block]

headEvents event.FeedOf[core.ChainHeadEvent]
chainEvents event.FeedOf[core.ChainEvent]
logEvents event.FeedOf[[]*types.Log]
enqueueEvents event.FeedOf[*types.Block]
headEvents event.FeedOf[core.ChainHeadEvent]
chainEvents event.FeedOf[core.ChainEvent]
logEvents event.FeedOf[[]*types.Log]

chainContext core.ChainContext
chainConfig *params.ChainConfig
Expand All @@ -55,7 +56,7 @@ type Executor struct {
// executed block after shutdown and recovery.
func New(
lastExecuted *blocks.Block,
blockSrc BlockSource,
blockSrc blocks.Source,
chainConfig *params.ChainConfig,
db ethdb.Database,
triedbConfig *triedb.Config,
Expand Down Expand Up @@ -84,6 +85,7 @@ func New(
stateCache: cache,
snaps: snaps,
}
e.lastEnqueued.Store(lastExecuted)
e.lastExecuted.Store(lastExecuted)

go e.processQueue()
Expand Down Expand Up @@ -127,3 +129,8 @@ func (e *Executor) StateCache() state.Database {
func (e *Executor) LastExecuted() *blocks.Block {
return e.lastExecuted.Load()
}

// LastEnqueued returns the last-enqueued block in a threadsafe manner.
func (e *Executor) LastEnqueued() *blocks.Block {
return e.lastEnqueued.Load()
}
10 changes: 1 addition & 9 deletions saexec/saexec_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -32,7 +32,6 @@ import (
"github.com/stretchr/testify/require"
"go.uber.org/goleak"

"github.com/ava-labs/strevm/blocks"
"github.com/ava-labs/strevm/blocks/blockstest"
"github.com/ava-labs/strevm/cmputils"
"github.com/ava-labs/strevm/gastime"
Expand Down Expand Up @@ -84,15 +83,8 @@ func newSUT(tb testing.TB, hooks hook.Points) (context.Context, SUT) {
chain.SetDefaultOptions(blockstest.WithBlockOptions(
blockstest.WithLogger(logger),
))
src := BlockSource(func(h common.Hash, n uint64) *blocks.Block {
b, ok := chain.GetBlock(h, n)
if !ok {
return nil
}
return b
})

e, err := New(genesis, src, config, db, tdbConfig, hooks, logger)
e, err := New(genesis, chain.GetBlock, config, db, tdbConfig, hooks, logger)
require.NoError(tb, err, "New()")
tb.Cleanup(e.Close)

Expand Down
6 changes: 6 additions & 0 deletions saexec/subscription.go
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,12 @@ func (e *Executor) sendPostExecutionEvents(b *types.Block, receipts types.Receip
e.logEvents.Send(logs)
}

// SubscribeBlockEnqueueEvent returns a new subscription for each block queued
// [Executor.Enqueue].
func (e *Executor) SubscribeBlockEnqueueEvent(ch chan<- *types.Block) event.Subscription {
return e.enqueueEvents.Subscribe(ch)
}

// SubscribeChainHeadEvent returns a new subscription for each
// [core.ChainHeadEvent] emitted after execution of a [blocks.Block].
func (e *Executor) SubscribeChainHeadEvent(ch chan<- core.ChainHeadEvent) event.Subscription {
Expand Down
108 changes: 108 additions & 0 deletions txgossip/blockchain.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,108 @@
// Copyright (C) 2025, Ava Labs, Inc. All rights reserved.
// See the file LICENSE for licensing terms.

package txgossip

import (
"github.com/ava-labs/libevm/common"
"github.com/ava-labs/libevm/core"
"github.com/ava-labs/libevm/core/state"
"github.com/ava-labs/libevm/core/txpool"
"github.com/ava-labs/libevm/core/txpool/legacypool"
"github.com/ava-labs/libevm/core/types"
"github.com/ava-labs/libevm/event"
"github.com/ava-labs/libevm/params"

"github.com/ava-labs/strevm/blocks"
"github.com/ava-labs/strevm/saexec"
)

// A BlockChain is the union of [txpool.BlockChain] and [legacypool.BlockChain].
type BlockChain interface {
txpool.BlockChain
legacypool.BlockChain
}

// NewBlockChain wraps an [saexec.Executor] to be compatible with a
// non-blob-transaction mempool.
func NewBlockChain(exec *saexec.Executor, blocks blocks.Source) BlockChain {
return &blockchain{
exec: exec,
blocks: blocks,
}
}

type blockchain struct {
exec *saexec.Executor
blocks blocks.Source
}

func (bc *blockchain) Config() *params.ChainConfig {
return bc.exec.ChainConfig()
}

func (bc *blockchain) CurrentBlock() *types.Header {
return bc.exec.LastEnqueued().Header()
}

func (bc *blockchain) GetBlock(hash common.Hash, number uint64) *types.Block {
return bc.blocks.EthBlock(hash, number)
}

func (bc *blockchain) StateAt(root common.Hash) (*state.StateDB, error) {
return state.New(root, bc.exec.StateCache(), nil)
}
Comment on lines +52 to +54
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

So, with this PR the mempool's validity checking is based on the settled state right?

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Intent is currently undefined. Sorry, I should have been clearer in what I meant in the PR description (i.e. I'm kicking that can down the road entirely):

Any specific rules for when to reject a tx from the mempool will likely require some libevm mods so can follow later.

The p2p architecture and gossip was all so new to me that I found this PR relatively difficult, so wanted to focus on learning it and getting the wiring correct. I'll concentrate on things like validity when we have the VM to orchestrate all the moving parts.


// SubscribeChainHeadEvent subscribes to block enqueueing, NOT to regular head
// events as these only occur after execution. Enqueuing is equivalent to block
// acceptance, which is when a transaction SHOULD be removed from the mempool.
func (bc *blockchain) SubscribeChainHeadEvent(ch chan<- core.ChainHeadEvent) event.Subscription {
bCh := make(chan *types.Block)
sub := bc.exec.SubscribeBlockEnqueueEvent(bCh)

p := &pipe{
in: bCh,
out: ch,
sub: sub,
quit: make(chan struct{}),
done: make(chan struct{}),
}
go p.loop()
return p
}

// A pipe is an [event.Subscription] that converts from a [types.Block] channel
// to a [core.ChainHeadEvent] one.
type pipe struct {
in <-chan *types.Block
out chan<- core.ChainHeadEvent
sub event.Subscription
quit, done chan struct{}
}

func (p *pipe) loop() {
defer close(p.done)
for {
select {
case b := <-p.in:
select {
case p.out <- core.ChainHeadEvent{Block: b}:

case <-p.quit:
return
}
case <-p.quit:
return
}
Comment on lines +86 to +96
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I feel like this makes the "receive" followed by "send" logic a bit more clear

Suggested change
select {
case b := <-p.in:
select {
case p.out <- core.ChainHeadEvent{Block: b}:
case <-p.quit:
return
}
case <-p.quit:
return
}
var b *types.Block
select {
case b = <-p.in:
case <-p.quit:
return
}
select {
case p.out <- core.ChainHeadEvent{Block: b}:
case <-p.quit:
return
}

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I have a strong dislike of the "variable look-back" pattern as it causes a non-linearity in reading the code.

I'm being hyperbolic here, but this is the cognitive pattern I'm trying to avoid:

  1. Push b to my brain stack without knowing how it will be used (context aids memory).
  2. Set some b, making sure to notice that it's = and not :=, recalling my earlier stack push.
  3. Parse some unrelated "distraction" code, keeping b at the back of my mind.
  4. Use b.

vs

  1. Declare and set b, pushing it to my brain stack but with context as to where it came from.
  2. Use b.

Granted in this particular solution it's not all that significant because the "distraction" code is minimal, but I still feel like it's an anti-pattern. All that said, I'm open to having my mind changed if you think there are sufficient reasons to change.

}
}

func (p *pipe) Err() <-chan error {
return p.sub.Err()
}

func (p *pipe) Unsubscribe() {
p.sub.Unsubscribe()
close(p.quit)
<-p.done
}
54 changes: 54 additions & 0 deletions txgossip/priority.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
// Copyright (C) 2025, Ava Labs, Inc. All rights reserved.
// See the file LICENSE for licensing terms.

package txgossip

import (
"slices"

"github.com/ava-labs/libevm/common"
"github.com/ava-labs/libevm/core/txpool"
)

// A LazyTransaction couples a [txpool.LazyTransaction] with its sender.
type LazyTransaction struct {
*txpool.LazyTransaction
Sender common.Address
}

// TransactionsByPriority calls [txpool.TxPool.Pending] with the given filter,
// collapses the results into a slice, and sorts said slice by decreasing gas
// tip then chronologically. Transactions from the same sender are merely sorted
// by increasing nonce.
func (s *Set) TransactionsByPriority(filter txpool.PendingFilter) []*LazyTransaction {
pending := s.Pool.Pending(filter)
var n int
for _, txs := range pending {
n += len(txs)
}

all := make([]*LazyTransaction, n)
var i int
for from, txs := range pending {
for _, tx := range txs {
all[i] = &LazyTransaction{
LazyTransaction: tx,
Sender: from,
}
i++
}
}

slices.SortStableFunc(all, func(a, b *LazyTransaction) int {
if a.Sender == b.Sender {
// [txpool.TxPool.Pending] already returns each slice in nonce order
// and we're performing a stable sort.
return 0
}
if tip := a.GasTipCap.Cmp(b.GasTipCap); tip != 0 {
return -tip // Higher tips first
}
return a.Time.Compare(b.Time)
})
return all
}
Loading