Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
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
46 changes: 40 additions & 6 deletions execution/builder/block_builder.go
Original file line number Diff line number Diff line change
Expand Up @@ -28,19 +28,26 @@ import (
"github.com/erigontech/erigon/execution/types"
)

type BlockBuilderFunc func(param *Parameters, interrupt *atomic.Bool) (*types.BlockWithReceipts, error)
// BlockBuilderFunc builds a payload. Its context ends when the payload is discarded, so anything
// that can block - opening a read view, waiting on a transaction provider - has to honour it.
type BlockBuilderFunc func(ctx context.Context, param *Parameters, interrupt *atomic.Bool) (*types.BlockWithReceipts, error)

// BlockBuilder wraps a goroutine that builds Proof-of-Stake payloads (PoS "mining")
// BlockBuilder wraps a goroutine that builds Proof-of-Stake payloads (PoS "mining").
//
// It answers to two different requests. Interrupting asks for the block it has so far, which is how
// a payload is collected. Discarding says the payload is not wanted at all, and cancels the work.
type BlockBuilder struct {
interrupt atomic.Bool
discard context.CancelFunc
mu sync.Mutex
done chan struct{}
result *types.BlockWithReceipts
err error
}

func NewBlockBuilder(build BlockBuilderFunc, param *Parameters, maxBuildTime time.Duration) *BlockBuilder {
builder := &BlockBuilder{done: make(chan struct{})}
func NewBlockBuilder(ctx context.Context, build BlockBuilderFunc, param *Parameters, maxBuildTime time.Duration) *BlockBuilder {
buildCtx, discard := context.WithCancel(ctx)
builder := &BlockBuilder{done: make(chan struct{}), discard: discard}

go func() {
var result *types.BlockWithReceipts
Expand All @@ -58,13 +65,18 @@ func NewBlockBuilder(build BlockBuilderFunc, param *Parameters, maxBuildTime tim
builder.err = err
builder.mu.Unlock()
close(builder.done)
discard()
}()

log.Info("Building block...")
t := time.Now()
result, err = build(param, &builder.interrupt)
result, err = build(buildCtx, param, &builder.interrupt)
if err != nil {
log.Warn("Failed to build a block", "err", err)
if buildCtx.Err() != nil {
log.Debug("Block builder discarded", "err", err)
} else {
log.Warn("Failed to build a block", "err", err)
}
} else {
block := result.Block
log.Info("Built block", "hash", block.Hash(), "height", block.NumberU64(), "txs", len(block.Transactions()), "executionRequests", len(result.Requests), "gasUsedPct", 100*float64(block.GasUsed())/float64(block.GasLimit()), "time", time.Since(t))
Expand Down Expand Up @@ -102,6 +114,28 @@ func (b *BlockBuilder) Stop(ctx context.Context) (*types.BlockWithReceipts, erro
return b.result, b.err
}

// Discard abandons the build and releases what it holds. A read view or a transaction provider
// blocked on the builder's context returns at once instead of waiting out its own deadline, which
// is the difference between an evicted builder freeing its resources now and freeing them a slot
// from now.
func (b *BlockBuilder) Discard() {
b.interrupt.Store(true)
b.discard()
}

// Failed reports whether the builder has finished and ended in an error, which a caller looking to
// reuse it has to read as absent because that error is latched.
func (b *BlockBuilder) Failed() bool {
select {
case <-b.done:
default:
return false
}
b.mu.Lock()
defer b.mu.Unlock()
return b.err != nil
}

func (b *BlockBuilder) Block() *types.Block {
b.mu.Lock()
defer b.mu.Unlock()
Expand Down
84 changes: 84 additions & 0 deletions execution/builder/block_builder_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,84 @@
// Copyright 2026 The Erigon Authors
// This file is part of Erigon.
//
// Erigon is free software: you can redistribute it and/or modify
// it under the terms of the GNU Lesser General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// Erigon is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU Lesser General Public License for more details.
//
// You should have received a copy of the GNU Lesser General Public License
// along with Erigon. If not, see <http://www.gnu.org/licenses/>.

package builder

import (
"context"
"errors"
"sync/atomic"
"testing"
"time"

"github.com/stretchr/testify/require"

"github.com/erigontech/erigon/execution/types"
)

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

release := make(chan struct{})
t.Cleanup(func() { close(release) })
b := NewBlockBuilder(t.Context(), func(_ context.Context, _ *Parameters, _ *atomic.Bool) (*types.BlockWithReceipts, error) {
<-release
return nil, errors.New("builder stopped")
}, &Parameters{}, time.Minute)

require.Never(t, b.Failed, 50*time.Millisecond, 5*time.Millisecond)
}

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

b := NewBlockBuilder(t.Context(), func(_ context.Context, _ *Parameters, interrupt *atomic.Bool) (*types.BlockWithReceipts, error) {
for !interrupt.Load() {
time.Sleep(time.Millisecond)
}
return &types.BlockWithReceipts{Block: types.NewBlock(&types.Header{}, nil, nil, nil, nil)}, nil
}, &Parameters{}, time.Minute)

_, err := b.Stop(t.Context())
require.NoError(t, err)

// Collecting the payload is what a proposal does. Reading that as failure would make a repeated
// request rebuild from scratch instead of being handed the block that was just built.
require.False(t, b.Failed())
}

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

b := NewBlockBuilder(t.Context(), func(_ context.Context, _ *Parameters, _ *atomic.Bool) (*types.BlockWithReceipts, error) {
return nil, errors.New("build failed")
}, &Parameters{}, time.Minute)

require.Eventually(t, b.Failed, time.Second, time.Millisecond)
}

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

built := make(chan struct{})
b := NewBlockBuilder(t.Context(), func(_ context.Context, _ *Parameters, _ *atomic.Bool) (*types.BlockWithReceipts, error) {
defer close(built)
return &types.BlockWithReceipts{Block: types.NewBlock(&types.Header{}, nil, nil, nil, nil)}, nil
}, &Parameters{}, time.Minute)

<-built
// A builder that ran out of room holds a complete payload, so its id is still worth reusing.
require.Never(t, b.Failed, 50*time.Millisecond, 5*time.Millisecond)
}
16 changes: 8 additions & 8 deletions execution/builder/builder.go
Original file line number Diff line number Diff line change
Expand Up @@ -45,7 +45,6 @@ type SDProvider func() *execctx.SharedDomains
// without staged-sync machinery. Its Build method satisfies BlockBuilderFunc and can
// be passed directly to ExecModule.
type Builder struct {
ctx context.Context
db kv.TemporalRoDB
pendingBlockCh chan *types.Block
builderCfg *buildercfg.BuilderConfig
Expand All @@ -64,7 +63,6 @@ type Builder struct {
}

func NewBuilder(
ctx context.Context,
db kv.TemporalRoDB,
builderCfg *buildercfg.BuilderConfig,
chainConfig *chain.Config,
Expand All @@ -81,7 +79,6 @@ func NewBuilder(
logger log.Logger,
) *Builder {
return &Builder{
ctx: ctx,
db: db,
pendingBlockCh: make(chan *types.Block, 1),
builderCfg: builderCfg,
Expand All @@ -107,7 +104,10 @@ func (b *Builder) PendingBlockCh() chan *types.Block {
}

// Build satisfies BlockBuilderFunc. Pass b.Build directly to ExecModule.
func (b *Builder) Build(param *Parameters, interrupt *atomic.Bool) (result *types.BlockWithReceipts, err error) {
//
// Everything that can block runs under ctx, so discarding the payload releases the read view and
// unblocks the transaction provider instead of leaving them to finish on their own.
func (b *Builder) Build(ctx context.Context, param *Parameters, interrupt *atomic.Bool) (result *types.BlockWithReceipts, err error) {
defer func() {
if rec := recover(); rec != nil {
err = fmt.Errorf("%+v, trace: %s", rec, dbg.Stack())
Expand All @@ -124,7 +124,7 @@ func (b *Builder) Build(param *Parameters, interrupt *atomic.Bool) (result *type
BuiltBlock: &exec.AssembledBlock{},
}

tx, err := b.db.BeginTemporalRo(b.ctx)
tx, err := b.db.BeginTemporalRo(ctx)
if err != nil {
return nil, err
}
Expand All @@ -145,7 +145,7 @@ func (b *Builder) Build(param *Parameters, interrupt *atomic.Bool) (result *type
}
}

sd, err := execctx.NewSharedDomains(b.ctx, compositeTx, b.logger, execctx.WithoutDeferredBranchUpdates(), execctx.WithoutSharedBranchCache())
sd, err := execctx.NewSharedDomains(ctx, compositeTx, b.logger, execctx.WithoutDeferredBranchUpdates(), execctx.WithoutSharedBranchCache())
if err != nil {
return nil, err
}
Expand All @@ -172,10 +172,10 @@ func (b *Builder) Build(param *Parameters, interrupt *atomic.Bool) (result *type
execCfg := StageBuilderExecCfg(state, b.notifier, b.chainConfig, b.engine, b.vmConfig, b.tmpdir, interrupt, param.PayloadId, txnProvider, b.blockReader)
finishCfg := StageBuilderFinishCfg(b.chainConfig, b.engine, state, b.sealCancel, b.blockReader, b.latestBlockBuiltStore)

if err := createBlock(b.ctx, sd, compositeTx, executionAt, createCfg, b.logger); err != nil {
if err := createBlock(ctx, sd, compositeTx, executionAt, createCfg, b.logger); err != nil {
return nil, err
}
if err := execBlock(b.ctx, sd, compositeTx, executionAt, execCfg, b.executeBlockCfg, b.logger); err != nil {
if err := execBlock(ctx, sd, compositeTx, executionAt, execCfg, b.executeBlockCfg, b.logger); err != nil {
return nil, err
}
if err := finishBlock(compositeTx, finishCfg, b.logger); err != nil {
Expand Down
3 changes: 1 addition & 2 deletions execution/builder/builder_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -48,14 +48,13 @@ func TestBuilder_Build_DBError(t *testing.T) {

want := errors.New("db open failed")
b := &Builder{
ctx: context.Background(),
db: &errDB{err: want},
builderCfg: &buildercfg.BuilderConfig{},
pendingBlockCh: make(chan *types.Block, 1),
logger: log.New(),
}

_, err := b.Build(&Parameters{}, &atomic.Bool{})
_, err := b.Build(t.Context(), &Parameters{}, &atomic.Bool{})
require.ErrorIs(t, err, want)
}

Expand Down
Loading
Loading