erigon-pulse/turbo/execution/eth1/ethereum_execution.go
Mark Holt 79ed8cad35
E2 snapshot uploading (#9056)
This change introduces additional processes to manage snapshot uploading
for E2 snapshots:

## erigon snapshots upload

The `snapshots uploader` command starts a version of erigon customized
for uploading snapshot files to
a remote location.  

It breaks the stage execution process after the senders stage and then
uses the snapshot stage to send
uploaded headers, bodies and (in the case of polygon) bor spans and
events to snapshot files. Because
this process avoids execution in run signifigantly faster than a
standard erigon configuration.

The uploader uses rclone to send seedable (100K or 500K blocks) to a
remote storage location specified
in the rclone config file.

The **uploader** is configured to minimize disk usage by doing the
following:

* It removes snapshots once they are loaded
* It aggressively prunes the database once entities are transferred to
snapshots

in addition to this it has the following performance related features:

* maximizes the workers allocated to snapshot processing to improve
throughput
* Can be started from scratch by downloading the latest snapshots from
the remote location to seed processing

## snapshots command

Is a stand alone command for managing remote snapshots it has the
following sub commands

* **cmp** - compare snapshots
* **copy** - copy snapshots
* **verify** - verify snapshots
* **manifest** - manage the manifest file in the root of remote snapshot
locations
* **torrent** - manage snapshot torrent files
2023-12-27 22:05:09 +00:00

261 lines
8.3 KiB
Go

package eth1
import (
"context"
"errors"
"math/big"
"github.com/ledgerwatch/erigon-lib/chain"
libcommon "github.com/ledgerwatch/erigon-lib/common"
"github.com/ledgerwatch/erigon-lib/gointerfaces"
"github.com/ledgerwatch/erigon-lib/gointerfaces/execution"
"github.com/ledgerwatch/log/v3"
"golang.org/x/sync/semaphore"
"google.golang.org/protobuf/types/known/emptypb"
"github.com/ledgerwatch/erigon-lib/kv"
"github.com/ledgerwatch/erigon/common/math"
"github.com/ledgerwatch/erigon/consensus"
"github.com/ledgerwatch/erigon/core"
"github.com/ledgerwatch/erigon/core/rawdb"
"github.com/ledgerwatch/erigon/core/types"
"github.com/ledgerwatch/erigon/eth/stagedsync"
"github.com/ledgerwatch/erigon/turbo/builder"
"github.com/ledgerwatch/erigon/turbo/engineapi/engine_helpers"
"github.com/ledgerwatch/erigon/turbo/engineapi/engine_types"
"github.com/ledgerwatch/erigon/turbo/services"
"github.com/ledgerwatch/erigon/turbo/shards"
"github.com/ledgerwatch/erigon/turbo/stages"
)
const maxBlocksLookBehind = 32
// EthereumExecutionModule describes ethereum execution logic and indexing.
type EthereumExecutionModule struct {
// Snapshots + MDBX
blockReader services.FullBlockReader
// MDBX database
db kv.RwDB // main database
semaphore *semaphore.Weighted
executionPipeline *stagedsync.Sync
forkValidator *engine_helpers.ForkValidator
logger log.Logger
// Block building
nextPayloadId uint64
lastParameters *core.BlockBuilderParameters
builderFunc builder.BlockBuilderFunc
builders map[uint64]*builder.BlockBuilder
// Changes accumulator
hook *stages.Hook
accumulator *shards.Accumulator
stateChangeConsumer shards.StateChangeConsumer
// configuration
config *chain.Config
historyV3 bool
// consensus
engine consensus.Engine
execution.UnimplementedExecutionServer
}
func NewEthereumExecutionModule(blockReader services.FullBlockReader, db kv.RwDB, executionPipeline *stagedsync.Sync, forkValidator *engine_helpers.ForkValidator,
config *chain.Config, builderFunc builder.BlockBuilderFunc, hook *stages.Hook, accumulator *shards.Accumulator, stateChangeConsumer shards.StateChangeConsumer, logger log.Logger, engine consensus.Engine, historyV3 bool) *EthereumExecutionModule {
return &EthereumExecutionModule{
blockReader: blockReader,
db: db,
executionPipeline: executionPipeline,
logger: logger,
forkValidator: forkValidator,
builders: make(map[uint64]*builder.BlockBuilder),
builderFunc: builderFunc,
config: config,
semaphore: semaphore.NewWeighted(1),
hook: hook,
accumulator: accumulator,
stateChangeConsumer: stateChangeConsumer,
engine: engine,
}
}
func (e *EthereumExecutionModule) getHeader(ctx context.Context, tx kv.Tx, blockHash libcommon.Hash, blockNumber uint64) (*types.Header, error) {
td, err := rawdb.ReadTd(tx, blockHash, blockNumber)
if err != nil {
return nil, err
}
if td == nil {
return nil, nil
}
if e.blockReader == nil {
return rawdb.ReadHeader(tx, blockHash, blockNumber), nil
}
return e.blockReader.Header(ctx, tx, blockHash, blockNumber)
}
func (e *EthereumExecutionModule) getTD(ctx context.Context, tx kv.Tx, blockHash libcommon.Hash, blockNumber uint64) (*big.Int, error) {
return rawdb.ReadTd(tx, blockHash, blockNumber)
}
func (e *EthereumExecutionModule) getBody(ctx context.Context, tx kv.Tx, blockHash libcommon.Hash, blockNumber uint64) (*types.Body, error) {
td, err := rawdb.ReadTd(tx, blockHash, blockNumber)
if err != nil {
return nil, err
}
if td == nil {
return nil, nil
}
if e.blockReader == nil {
body, _, _ := rawdb.ReadBody(tx, blockHash, blockNumber)
return body, nil
}
return e.blockReader.BodyWithTransactions(ctx, tx, blockHash, blockNumber)
}
func (e *EthereumExecutionModule) canonicalHash(ctx context.Context, tx kv.Tx, blockNumber uint64) (libcommon.Hash, error) {
var canonical libcommon.Hash
var err error
if e.blockReader == nil {
canonical, err = rawdb.ReadCanonicalHash(tx, blockNumber)
} else {
canonical, err = e.blockReader.CanonicalHash(ctx, tx, blockNumber)
}
if err != nil {
return libcommon.Hash{}, err
}
td, err := rawdb.ReadTd(tx, canonical, blockNumber)
if err != nil {
return libcommon.Hash{}, err
}
if td == nil {
return libcommon.Hash{}, nil
}
return canonical, nil
}
func (e *EthereumExecutionModule) ValidateChain(ctx context.Context, req *execution.ValidationRequest) (*execution.ValidationReceipt, error) {
if !e.semaphore.TryAcquire(1) {
return &execution.ValidationReceipt{
LatestValidHash: gointerfaces.ConvertHashToH256(libcommon.Hash{}),
ValidationStatus: execution.ExecutionStatus_Busy,
}, nil
}
defer e.semaphore.Release(1)
tx, err := e.db.BeginRw(ctx)
if err != nil {
return nil, err
}
defer tx.Rollback()
e.forkValidator.ClearWithUnwind(e.accumulator, e.stateChangeConsumer)
blockHash := gointerfaces.ConvertH256ToHash(req.Hash)
header, err := e.blockReader.Header(ctx, tx, blockHash, req.Number)
if err != nil {
return nil, err
}
body, err := e.blockReader.BodyWithTransactions(ctx, tx, blockHash, req.Number)
if err != nil {
return nil, err
}
if header == nil || body == nil {
return &execution.ValidationReceipt{
LatestValidHash: gointerfaces.ConvertHashToH256(libcommon.Hash{}),
ValidationStatus: execution.ExecutionStatus_MissingSegment,
}, nil
}
currentBlockNumber := rawdb.ReadCurrentBlockNumber(tx)
if math.AbsoluteDifference(*currentBlockNumber, req.Number) >= maxBlocksLookBehind {
return &execution.ValidationReceipt{
ValidationStatus: execution.ExecutionStatus_TooFarAway,
LatestValidHash: gointerfaces.ConvertHashToH256(libcommon.Hash{}),
}, tx.Commit()
}
currentHeadHash := rawdb.ReadHeadHeaderHash(tx)
extendingHash := e.forkValidator.ExtendingForkHeadHash()
extendCanonical := extendingHash == libcommon.Hash{} && header.ParentHash == currentHeadHash
status, lvh, validationError, criticalError := e.forkValidator.ValidatePayload(tx, header, body.RawBody(), extendCanonical)
if criticalError != nil {
return nil, criticalError
}
// if the block is deemed invalid then we delete it. perhaps we want to keep bad blocks and just keep an index of bad ones.
validationStatus := execution.ExecutionStatus_Success
if status == engine_types.AcceptedStatus {
validationStatus = execution.ExecutionStatus_MissingSegment
}
isInvalidChain := status == engine_types.InvalidStatus || status == engine_types.InvalidBlockHashStatus || validationError != nil
if isInvalidChain && (lvh != libcommon.Hash{}) && lvh != blockHash {
if err := e.purgeBadChain(ctx, tx, lvh, blockHash); err != nil {
return nil, err
}
}
if isInvalidChain {
e.logger.Warn("ethereumExecutionModule.ValidateChain: chain is invalid", "hash", libcommon.Hash(blockHash))
validationStatus = execution.ExecutionStatus_BadBlock
}
return &execution.ValidationReceipt{
ValidationStatus: validationStatus,
LatestValidHash: gointerfaces.ConvertHashToH256(lvh),
}, tx.Commit()
}
func (e *EthereumExecutionModule) purgeBadChain(ctx context.Context, tx kv.RwTx, latestValidHash, headHash libcommon.Hash) error {
tip := rawdb.ReadHeaderNumber(tx, headHash)
currentHash := headHash
currentNumber := *tip
for currentHash != latestValidHash {
currentHeader, err := e.getHeader(ctx, tx, currentHash, currentNumber)
if err != nil {
return err
}
rawdb.DeleteHeader(tx, currentHash, currentNumber)
currentHash = currentHeader.ParentHash
currentNumber--
}
return nil
}
func (e *EthereumExecutionModule) Start(ctx context.Context) {
e.semaphore.Acquire(ctx, 1)
defer e.semaphore.Release(1)
more := true
for more {
var err error
if more, err = e.executionPipeline.Run(e.db, nil, true); err != nil {
if !errors.Is(err, context.Canceled) {
e.logger.Error("Could not start execution service", "err", err)
}
continue
}
if err := e.executionPipeline.RunPrune(e.db, nil, true); err != nil {
if !errors.Is(err, context.Canceled) {
e.logger.Error("Could not start execution service", "err", err)
}
continue
}
}
}
func (e *EthereumExecutionModule) Ready(context.Context, *emptypb.Empty) (*execution.ReadyResponse, error) {
if !e.semaphore.TryAcquire(1) {
return &execution.ReadyResponse{Ready: false}, nil
}
defer e.semaphore.Release(1)
return &execution.ReadyResponse{Ready: true}, nil
}