mirror of
https://github.com/luxfi/evmgpu.git
synced 2026-10-30 16:57:11 +00:00
229 lines
6.4 KiB
Go
229 lines
6.4 KiB
Go
// Copyright (C) 2019-2025, Lux Industries, Inc. All rights reserved.
|
|
// See the file LICENSE for licensing terms.
|
|
|
|
package evm
|
|
|
|
import (
|
|
"context"
|
|
"sync"
|
|
"time"
|
|
|
|
"github.com/luxfi/consensus/engine/chain/block"
|
|
"github.com/luxfi/evmgpu/core"
|
|
"github.com/luxfi/evmgpu/core/txpool"
|
|
log "github.com/luxfi/log"
|
|
)
|
|
|
|
const (
|
|
// Minimum amount of time to wait after building a block before attempting to build a block
|
|
// a second time without changing the contents of the mempool.
|
|
minBlockBuildingRetryDelay = 500 * time.Millisecond
|
|
)
|
|
|
|
type blockBuilder struct {
|
|
ctx context.Context
|
|
|
|
txPool *txpool.TxPool
|
|
|
|
shutdownChan <-chan struct{}
|
|
shutdownWg *sync.WaitGroup
|
|
|
|
pendingSignal *sync.Cond
|
|
|
|
buildBlockLock sync.Mutex
|
|
|
|
// lastBuildTime is the time when the last block was built.
|
|
// This is used to ensure that we don't build blocks too frequently,
|
|
// but at least after a minimum delay of minBlockBuildingRetryDelay.
|
|
lastBuildTime time.Time
|
|
}
|
|
|
|
func (vm *VM) NewBlockBuilder() *blockBuilder {
|
|
b := &blockBuilder{
|
|
ctx: context.Background(),
|
|
txPool: vm.txPool,
|
|
shutdownChan: vm.shutdownChan,
|
|
shutdownWg: &vm.shutdownWg,
|
|
}
|
|
b.pendingSignal = sync.NewCond(&b.buildBlockLock)
|
|
return b
|
|
}
|
|
|
|
// handleGenerateBlock is called from the VM immediately after BuildBlock.
|
|
func (b *blockBuilder) handleGenerateBlock() {
|
|
b.buildBlockLock.Lock()
|
|
defer b.buildBlockLock.Unlock()
|
|
b.lastBuildTime = time.Now()
|
|
}
|
|
|
|
// needToBuild returns true if there are outstanding transactions to be issued
|
|
// into a block.
|
|
func (b *blockBuilder) needToBuild() bool {
|
|
// Use empty filter to check if ANY pending transactions exist
|
|
// The miner will apply proper fee filters when building the block
|
|
// Using MinTip filter here rejects valid legacy transactions that
|
|
// don't have GasTipCap set, causing block production to stall
|
|
size := b.txPool.PendingSize(txpool.PendingFilter{})
|
|
return size > 0
|
|
}
|
|
|
|
// signalCanBuild signals that a new block can be built.
|
|
func (b *blockBuilder) signalCanBuild() {
|
|
b.buildBlockLock.Lock()
|
|
defer b.buildBlockLock.Unlock()
|
|
b.pendingSignal.Broadcast()
|
|
}
|
|
|
|
// awaitSubmittedTxs waits for new transactions to be submitted
|
|
// and notifies the VM when the tx pool has transactions to be
|
|
// put into a new block.
|
|
func (b *blockBuilder) awaitSubmittedTxs() {
|
|
// txSubmitChan is invoked when new transactions are issued as well as on re-orgs which
|
|
// may orphan transactions that were previously in a preferred block.
|
|
txSubmitChan := make(chan core.NewTxsEvent)
|
|
b.txPool.SubscribeTransactions(txSubmitChan, true)
|
|
|
|
b.shutdownWg.Add(1)
|
|
go func() {
|
|
defer func() {
|
|
if r := recover(); r != nil {
|
|
log.Error("panic in awaitSubmittedTxs", "error", r)
|
|
panic(r)
|
|
}
|
|
}()
|
|
defer b.shutdownWg.Done()
|
|
|
|
for {
|
|
select {
|
|
case <-txSubmitChan:
|
|
log.Trace("New tx detected, trying to generate a block")
|
|
b.signalCanBuild()
|
|
case <-b.shutdownChan:
|
|
return
|
|
}
|
|
}
|
|
}()
|
|
}
|
|
|
|
// waitForEvent waits until a block needs to be built.
|
|
// It returns only after at least [minBlockBuildingRetryDelay] passed from the last time a block was built.
|
|
func (b *blockBuilder) waitForEvent(ctx context.Context) (block.Message, error) {
|
|
lastBuildTime, err := b.waitForNeedToBuild(ctx)
|
|
if err != nil {
|
|
return block.Message{}, err
|
|
}
|
|
timeSinceLastBuildTime := time.Since(lastBuildTime)
|
|
if b.lastBuildTime.IsZero() || timeSinceLastBuildTime >= minBlockBuildingRetryDelay {
|
|
log.Debug("Last time we built a block was long enough ago, no need to wait", "timeSinceLastBuildTime", timeSinceLastBuildTime)
|
|
return block.Message{Type: block.PendingTxs}, nil
|
|
}
|
|
timeUntilNextBuild := minBlockBuildingRetryDelay - timeSinceLastBuildTime
|
|
log.Debug("Last time we built a block was too recent, waiting", "timeUntilNextBuild", timeUntilNextBuild)
|
|
select {
|
|
case <-ctx.Done():
|
|
return block.Message{}, ctx.Err()
|
|
case <-time.After(timeUntilNextBuild):
|
|
return block.Message{Type: block.PendingTxs}, nil
|
|
}
|
|
}
|
|
|
|
// waitForNeedToBuild waits until needToBuild returns true.
|
|
// It returns the last time a block was built.
|
|
func (b *blockBuilder) waitForNeedToBuild(ctx context.Context) (time.Time, error) {
|
|
// Start a goroutine to broadcast when context is cancelled
|
|
// This wakes up the Wait() call so we can check context cancellation
|
|
done := make(chan struct{})
|
|
go func() {
|
|
select {
|
|
case <-ctx.Done():
|
|
b.pendingSignal.Broadcast()
|
|
case <-done:
|
|
}
|
|
}()
|
|
defer close(done)
|
|
|
|
b.buildBlockLock.Lock()
|
|
defer b.buildBlockLock.Unlock()
|
|
for !b.needToBuild() {
|
|
// Check if context is cancelled
|
|
select {
|
|
case <-ctx.Done():
|
|
return time.Time{}, ctx.Err()
|
|
default:
|
|
}
|
|
b.pendingSignal.Wait()
|
|
}
|
|
return b.lastBuildTime, nil
|
|
}
|
|
|
|
// AutominingConfig contains configuration for automining.
|
|
type AutominingConfig struct {
|
|
// BuildBlock builds a new block and returns it wrapped for consensus.
|
|
// The block must implement both Verify() and Accept() methods.
|
|
BuildBlock func(ctx context.Context) (interface {
|
|
Verify(context.Context) error
|
|
Accept(context.Context) error
|
|
}, error)
|
|
// Interval is the minimum time between block builds.
|
|
Interval time.Duration
|
|
}
|
|
|
|
// startAutomining starts the automining loop that builds and accepts blocks
|
|
// immediately when there are pending transactions.
|
|
func (b *blockBuilder) startAutomining(config AutominingConfig) {
|
|
// Subscribe to transaction pool events
|
|
txSubmitChan := make(chan core.NewTxsEvent)
|
|
b.txPool.SubscribeTransactions(txSubmitChan, true)
|
|
|
|
b.shutdownWg.Add(1)
|
|
go func() {
|
|
defer func() {
|
|
if r := recover(); r != nil {
|
|
log.Error("panic in automining", "error", r)
|
|
panic(r)
|
|
}
|
|
}()
|
|
defer b.shutdownWg.Done()
|
|
|
|
log.Info("Automining started")
|
|
|
|
for {
|
|
select {
|
|
case <-txSubmitChan:
|
|
// Small delay to batch transactions
|
|
time.Sleep(config.Interval)
|
|
b.automineBlock(config)
|
|
case <-b.shutdownChan:
|
|
log.Info("Automining stopped")
|
|
return
|
|
}
|
|
}
|
|
}()
|
|
}
|
|
|
|
// automineBlock builds, verifies, and accepts a block immediately.
|
|
func (b *blockBuilder) automineBlock(config AutominingConfig) {
|
|
ctx := context.Background()
|
|
|
|
// Build the block
|
|
blk, err := config.BuildBlock(ctx)
|
|
if err != nil {
|
|
log.Error("Automining: failed to build block", "err", err)
|
|
return
|
|
}
|
|
|
|
// Verify the block before accepting
|
|
if err := blk.Verify(ctx); err != nil {
|
|
log.Error("Automining: failed to verify block", "err", err)
|
|
return
|
|
}
|
|
|
|
// Accept the block (after verification)
|
|
if err := blk.Accept(ctx); err != nil {
|
|
log.Error("Automining: failed to accept block", "err", err)
|
|
return
|
|
}
|
|
|
|
log.Info("Automining: block built, verified, and accepted")
|
|
}
|