diff --git a/x/evm/keeper/abci.go b/x/evm/keeper/abci.go index 8fcefb7a70..da6774aab1 100644 --- a/x/evm/keeper/abci.go +++ b/x/evm/keeper/abci.go @@ -17,10 +17,6 @@ package keeper import ( sdk "github.com/cosmos/cosmos-sdk/types" - ethcommon "github.com/ethereum/go-ethereum/common" - "github.com/ethereum/go-ethereum/core/tracing" - "github.com/ethereum/go-ethereum/core/types" - "math/big" ) // BeginBlock sets the sdk Context and EIP155 chain id to the Keeper. @@ -28,32 +24,20 @@ func (k *Keeper) BeginBlock(ctx sdk.Context) error { k.WithChainID(ctx) // cache parameters that's common for the whole block. - if _, err := k.EVMBlockConfig(ctx, k.ChainID()); err != nil { + evmBlockConfig, err := k.EVMBlockConfig(ctx, k.ChainID()) + if err != nil { return err } - if k.evmTracer != nil && k.evmTracer.OnBlockStart != nil { - b := types.NewBlock(&types.Header{ - Number: big.NewInt(ctx.BlockHeight()), - Time: uint64(ctx.BlockTime().Unix()), - ParentHash: ethcommon.BytesToHash(ctx.BlockHeader().LastBlockId.Hash), - Coinbase: ethcommon.BytesToAddress(ctx.BlockHeader().ProposerAddress), - }, nil, nil, nil) - - finalizedHeaderNumber := ctx.BlockHeight() - 1 - if ctx.BlockHeight() == 0 { - finalizedHeaderNumber = 0 - } - - finalizedHeader := &types.Header{ - Number: big.NewInt(finalizedHeaderNumber), - } - - k.evmTracer.OnBlockStart(tracing.BlockEvent{ - Block: b, - TD: big.NewInt(0), - Finalized: finalizedHeader, - }) + if k.evmTracer != nil && k.evmTracer.OnCosmosBlockStart != nil { + k.evmTracer.OnCosmosBlockStart( + ToCosmosStartBlockEvent( + k, + ctx, + evmBlockConfig.CoinBase.Bytes(), + ctx.BlockHeader(), + ), + ) } return nil @@ -66,9 +50,9 @@ func (k *Keeper) EndBlock(ctx sdk.Context) error { k.CollectTxBloom(ctx) k.RemoveParamsCache(ctx) - if k.evmTracer != nil && k.evmTracer.OnBlockEnd != nil { + if k.evmTracer != nil && k.evmTracer.OnCosmosBlockEnd != nil { defer func() { - k.evmTracer.OnBlockEnd(nil) + k.evmTracer.OnCosmosBlockEnd(ToCosmosEndBlockEvent(k, ctx), nil) }() } diff --git a/x/evm/keeper/config.go b/x/evm/keeper/config.go index fc00a823ad..61d9abff1d 100644 --- a/x/evm/keeper/config.go +++ b/x/evm/keeper/config.go @@ -16,13 +16,13 @@ package keeper import ( + "github.com/ethereum/go-ethereum/eth/tracers" "math/big" errorsmod "cosmossdk.io/errors" sdk "github.com/cosmos/cosmos-sdk/types" "github.com/ethereum/go-ethereum/common" "github.com/ethereum/go-ethereum/core/vm" - "github.com/ethereum/go-ethereum/eth/tracers" "github.com/ethereum/go-ethereum/params" rpctypes "github.com/evmos/ethermint/rpc/types" "github.com/evmos/ethermint/x/evm/statedb" @@ -132,7 +132,12 @@ func (k *Keeper) EVMConfig(ctx sdk.Context, chainID *big.Int, txHash common.Hash } if k.evmTracer != nil { - cfg.Tracer = k.evmTracer + t := &tracers.Tracer{ + Hooks: k.evmTracer.Hooks, + GetResult: nil, + Stop: nil, + } + cfg.Tracer = t } return cfg, nil diff --git a/x/evm/keeper/keeper.go b/x/evm/keeper/keeper.go index 45a1a026d1..4c44d3d60e 100644 --- a/x/evm/keeper/keeper.go +++ b/x/evm/keeper/keeper.go @@ -16,6 +16,7 @@ package keeper import ( + cosmostracing "github.com/evmos/ethermint/x/evm/tracing" "math/big" errorsmod "cosmossdk.io/errors" @@ -28,7 +29,6 @@ import ( "github.com/ethereum/go-ethereum/core" ethtypes "github.com/ethereum/go-ethereum/core/types" "github.com/ethereum/go-ethereum/core/vm" - "github.com/ethereum/go-ethereum/eth/tracers" "github.com/ethereum/go-ethereum/params" ethermint "github.com/evmos/ethermint/types" "github.com/evmos/ethermint/x/evm/statedb" @@ -67,7 +67,7 @@ type Keeper struct { eip155ChainID *big.Int // EVM Tracer - evmTracer *tracers.Tracer + evmTracer *cosmostracing.Hooks // EVM Hooks for tx post-processing hooks types.EvmHooks @@ -204,7 +204,7 @@ func (k *Keeper) PostTxProcessing(ctx sdk.Context, msg *core.Message, receipt *e return k.hooks.PostTxProcessing(ctx, msg, receipt) } -func (k *Keeper) SetTracer(tracer *tracers.Tracer) { +func (k *Keeper) SetTracer(tracer *cosmostracing.Hooks) { k.evmTracer = tracer } diff --git a/x/evm/keeper/keeper_firehose.go b/x/evm/keeper/keeper_firehose.go new file mode 100644 index 0000000000..f14ecc507c --- /dev/null +++ b/x/evm/keeper/keeper_firehose.go @@ -0,0 +1,58 @@ +package keeper + +import ( + "cosmossdk.io/store/prefix" + cmtproto "github.com/cometbft/cometbft/proto/tendermint/types" + cosmostypes "github.com/cometbft/cometbft/types" + sdk "github.com/cosmos/cosmos-sdk/types" + ethtypes "github.com/ethereum/go-ethereum/core/types" + "github.com/evmos/ethermint/x/evm/tracing" + "github.com/evmos/ethermint/x/evm/types" + "math/big" +) + +func BlocksBloom(k *Keeper, ctx sdk.Context) *big.Int { + store := prefix.NewObjStore(ctx.ObjectStore(k.objectKey), types.KeyPrefixObjectBloom) + it := store.Iterator(nil, nil) + defer it.Close() + + bloom := new(big.Int) + for ; it.Valid(); it.Next() { + bloom.Or(bloom, it.Value().(*big.Int)) + } + return bloom +} + +func ToCosmosStartBlockEvent(k *Keeper, ctx sdk.Context, coinbaseBytes []byte, blockHeader cmtproto.Header) tracing.CosmosStartBlockEvent { + // ignore the errors as we are sure that the block header is valid + h, _ := cosmostypes.HeaderFromProto(&blockHeader) + h.ValidatorsHash = ctx.CometInfo().GetValidatorsHash() + + keeperParams := k.GetParams(ctx) + ethCfg := keeperParams.ChainConfig.EthereumConfig(k.ChainID()) + baseFee := k.GetBaseFee(ctx, ethCfg) + gasLimit := uint64(ctx.ConsensusParams().Block.MaxGas) + + finalizedHeaderNumber := h.Height - 1 + if h.Height == 0 { + finalizedHeaderNumber = 0 + } + + finalizedHeader := ðtypes.Header{ + Number: big.NewInt(finalizedHeaderNumber), + } + + return tracing.CosmosStartBlockEvent{ + CosmosHeader: h, + BaseFee: baseFee, + GasLimit: gasLimit, + Coinbase: coinbaseBytes, + Finalized: finalizedHeader, + } +} + +func ToCosmosEndBlockEvent(k *Keeper, ctx sdk.Context) tracing.CosmosEndBlockEvent { + return tracing.CosmosEndBlockEvent{ + LogsBloom: BlocksBloom(k, ctx).Bytes(), + } +} diff --git a/x/evm/keeper/state_transition.go b/x/evm/keeper/state_transition.go index 5ce0291ef1..4e6dca2958 100644 --- a/x/evm/keeper/state_transition.go +++ b/x/evm/keeper/state_transition.go @@ -47,7 +47,7 @@ import ( // beneficiary of the coinbase transaction (since we're not mining). // // NOTE: the RANDOM opcode is currently not supported since it requires -// RANDAO implementation. See https://github.com/evmos/ethermint/pull/1520#pullrequestreview-1200504697 +// RANDOM implementation. See https://github.com/evmos/ethermint/pull/1520#pullrequestreview-1200504697 // for more information. func (k *Keeper) NewEVM( ctx sdk.Context, @@ -74,7 +74,12 @@ func (k *Keeper) NewEVM( // Set Config Tracer if it was not already initialized if k.evmTracer != nil { - cfg.Tracer = k.evmTracer + t := &tracers.Tracer{ + Hooks: k.evmTracer.Hooks, + GetResult: nil, + Stop: nil, + } + cfg.Tracer = t } vmConfig := k.VMConfig(ctx, cfg) @@ -179,7 +184,7 @@ func (k *Keeper) ApplyTransaction(ctx sdk.Context, msgEth *types.MsgEthereumTx) } msg := msgEth.AsMessage(cfg.BaseFee) - // snapshot to contain the tx processing and post processing in same scope + // snapshot to contain the tx processing and post-processing in same scope var commit func() tmpCtx := ctx if k.hooks != nil { diff --git a/x/evm/tracers/firehose.go b/x/evm/tracers/firehose.go index e731ff8b9c..f407e32f3f 100644 --- a/x/evm/tracers/firehose.go +++ b/x/evm/tracers/firehose.go @@ -8,6 +8,8 @@ import ( "encoding/json" "errors" "fmt" + "github.com/ethereum/go-ethereum/eth/tracers" + cosmostracing "github.com/evmos/ethermint/x/evm/tracing" "io" "math/big" "os" @@ -21,13 +23,13 @@ import ( "sync/atomic" "time" + cosmostypes "github.com/cometbft/cometbft/types" "github.com/ethereum/go-ethereum/common" "github.com/ethereum/go-ethereum/common/math" "github.com/ethereum/go-ethereum/core/tracing" "github.com/ethereum/go-ethereum/core/types" "github.com/ethereum/go-ethereum/core/vm" "github.com/ethereum/go-ethereum/crypto" - "github.com/ethereum/go-ethereum/eth/tracers" "github.com/ethereum/go-ethereum/log" "github.com/ethereum/go-ethereum/params" "github.com/ethereum/go-ethereum/rlp" @@ -66,6 +68,7 @@ func init() { staticFirehoseChainValidationOnInit() tracers.LiveDirectory.Register("firehose", newFirehoseTracer) + GlobalLiveTracerRegistry.Register("firehose", NewCosmosFirehoseTracer) // Those 2 are defined but not used in this branch, they are kept because used in other branches // so it's easier to keep them here and suppress the warning by faking a usage. @@ -82,6 +85,22 @@ func newFirehoseTracer(cfg json.RawMessage) (*tracing.Hooks, error) { return NewTracingHooksFromFirehose(firehoseTracer), nil } +func NewCosmosFirehoseTracer(backwardCompatibility bool) (*cosmostracing.Hooks, error) { + firehoseConfig := new(FirehoseConfig) + if backwardCompatibility { + firehoseConfig.ApplyBackwardCompatibility = ptr(true) + } + + f, err := newFirehose(firehoseConfig) + if err != nil { + return nil, fmt.Errorf("failed to create Firehose tracer: %w", err) + } + + hooks := NewTracingHooksFromFirehose(f) + + return NewCosmosTracingHooksFromFirehose(hooks, f), nil +} + func NewTracingHooksFromFirehose(tracer *Firehose) *tracing.Hooks { return &tracing.Hooks{ OnBlockchainInit: tracer.OnBlockchainInit, @@ -111,6 +130,15 @@ func NewTracingHooksFromFirehose(tracer *Firehose) *tracing.Hooks { } } +func NewCosmosTracingHooksFromFirehose(hooks *tracing.Hooks, firehose *Firehose) *cosmostracing.Hooks { + return &cosmostracing.Hooks{ + Hooks: hooks, + + OnCosmosBlockStart: firehose.OnCosmosBlockStart, + OnCosmosBlockEnd: firehose.OnCosmosBlockEnd, + } +} + type FirehoseConfig struct { ApplyBackwardCompatibility *bool `json:"applyBackwardCompatibility"` @@ -154,7 +182,13 @@ type Firehose struct { applyBackwardCompatibility *bool // Block state - block *pbeth.Block + block *pbeth.Block + cosmosBlockHeader cosmostypes.Header + + //fixme: this is a hack, waiting for proto changes + lastBlockHash []byte + lastParentBlockHash []byte + blockBaseFee *big.Int blockOrdinal *Ordinal blockFinality *FinalityStatus @@ -204,10 +238,15 @@ func NewFirehoseFromRawJSON(cfg json.RawMessage) (*Firehose, error) { } } - return NewFirehose(&config), nil + f, err := newFirehose(&config) + if err != nil { + return nil, err + } + + return f, nil } -func NewFirehose(config *FirehoseConfig) *Firehose { +func newFirehose(config *FirehoseConfig) (*Firehose, error) { log.Info("Firehose tracer created", config.LogKeyValues()...) firehose := &Firehose{ @@ -240,7 +279,7 @@ func NewFirehose(config *FirehoseConfig) *Firehose { } } - return firehose + return firehose, nil } func (f *Firehose) newIsolatedTransactionTracer(tracerID string) *Firehose { @@ -349,7 +388,7 @@ func (f *Firehose) OnBlockStart(event tracing.BlockEvent) { f.blockIsPrecompiledAddr = getActivePrecompilesChecker(f.blockRules) f.block = &pbeth.Block{ - Hash: b.Hash().Bytes(), + Hash: b.Header().Hash().Bytes(), Number: b.Number().Uint64(), Header: newBlockHeaderFromChainHeader(b.Header(), firehoseBigIntFromNative(new(big.Int).Add(event.TD, b.Difficulty()))), Size: b.Size(), @@ -372,6 +411,53 @@ func (f *Firehose) OnBlockStart(event tracing.BlockEvent) { f.blockFinality.populateFromChain(event.Finalized) } +func (f *Firehose) OnCosmosBlockStart(event cosmostracing.CosmosStartBlockEvent) { + header := event.CosmosHeader + + coinbase := event.Coinbase + gasLimit := event.GasLimit + baseFee := event.BaseFee + + f.cosmosBlockHeader = header + + firehoseInfo("block start (number=%d)", header.Height) + + f.ensureBlockChainInit() + + f.blockRules = f.chainConfig.Rules(new(big.Int).SetInt64(header.Height), f.chainConfig.TerminalTotalDifficultyPassed, uint64(header.Time.Unix())) + f.blockIsPrecompiledAddr = getActivePrecompilesChecker(f.blockRules) + + f.block = &pbeth.Block{ + Number: uint64(header.Height), + Header: newBlockHeaderFromCosmosChainHeader(header, coinbase, gasLimit, baseFee), + Ver: 4, + } + + // TODO: fetch the real parentHash from the event + if header.Height == 1 { + f.lastParentBlockHash = []byte("0000000000000000000000000000000000000000000000000000000000000000") + } + + if header.Height > 1 { + // move the parentHash to the previous block + f.lastParentBlockHash = f.lastBlockHash + fmt.Println("doudou", f.lastParentBlockHash) + f.block.Header.ParentHash = f.lastParentBlockHash + } + + f.lastBlockHash = f.block.Header.Hash + + if *f.applyBackwardCompatibility { + f.block.Ver = 3 + } + + if f.block.Header.BaseFeePerGas != nil { + f.blockBaseFee = f.block.Header.BaseFeePerGas.Native() + } + + f.blockFinality.populateFromChain(event.Finalized) +} + func (f *Firehose) OnSkippedBlock(event tracing.BlockEvent) { // Blocks that are skipped from blockchain that were known and should contain 0 transactions. // It happened in the past, on Polygon if I recall right, that we missed block because some block @@ -426,6 +512,35 @@ func (f *Firehose) OnBlockEnd(err error) { firehoseInfo("block end") } +func (f *Firehose) OnCosmosBlockEnd(event cosmostracing.CosmosEndBlockEvent, err error) { + firehoseInfo("block ending (err=%s)", errorView(err)) + f.block.Hash = f.cosmosBlockHeader.Hash() + f.block.Header.LogsBloom = types.BytesToBloom(event.LogsBloom).Bytes() + + f.block.Size = 10 // todo: find the right size + + for _, trx := range f.block.TransactionTraces { + f.block.Header.GasUsed += trx.GasUsed + } + + if err == nil { + if f.blockReorderOrdinal { + f.reorderIsolatedTransactionsAndOrdinals() + } + + f.ensureInBlockAndNotInTrx() + f.printBlockToFirehose(f.block, f.blockFinality) + } else { + // An error occurred, could have happen in transaction/call context, we must not check if in trx/call, only check in block + f.ensureInBlock(0) + } + + f.resetBlock() + f.resetTransaction() + + firehoseInfo("block end") +} + // reorderIsolatedTransactionsAndOrdinals is called right after all transactions have completed execution. It will sort transactions // according to their index. // @@ -1506,7 +1621,7 @@ func (f *Firehose) flushToFirehose(in []byte) { fmt.Fprint(writer, errstr) } -// TestingBuffer is an internal method only used for testing purposes +// InternalTestingBuffer TestingBuffer is an internal method only used for testing purposes // that should never be used in production code. // // There is no public api guaranteed for this method. @@ -1514,7 +1629,6 @@ func (f *Firehose) InternalTestingBuffer() *bytes.Buffer { return f.testingBuffer } -// FIXME: Create a unit test that is going to fail as soon as any header is added in func newBlockHeaderFromChainHeader(h *types.Header, td *pbeth.BigInt) *pbeth.BlockHeader { var withdrawalsHashBytes []byte if hash := h.WithdrawalsHash; hash != nil { @@ -1561,6 +1675,36 @@ func newBlockHeaderFromChainHeader(h *types.Header, td *pbeth.BigInt) *pbeth.Blo return pbHead } +// FIXME: Create a unit test that is going to fail as soon as any header is added in +func newBlockHeaderFromCosmosChainHeader(h cosmostypes.Header, coinbase []byte, gasLimit uint64, baseFee *big.Int) *pbeth.BlockHeader { + difficulty := firehoseBigIntFromNative(new(big.Int).SetInt64(0)) + + pbHead := &pbeth.BlockHeader{ + Hash: h.Hash().Bytes(), + Number: uint64(h.Height), + //ParentHash: h.LastBlockID.Hash, + UncleHash: types.EmptyUncleHash.Bytes(), // No uncles in Tendermint + Coinbase: coinbase, + StateRoot: h.AppHash, + TransactionsRoot: h.DataHash, + ReceiptRoot: types.EmptyRootHash.Bytes(), + LogsBloom: types.EmptyVerkleHash.Bytes(), // TODO: fix this + Difficulty: difficulty, + TotalDifficulty: difficulty, + GasLimit: gasLimit, + Timestamp: timestamppb.New(h.Time), + ExtraData: []byte("0x"), + MixHash: common.Hash{}.Bytes(), + Nonce: types.BlockNonce{}.Uint64(), // PoW specific, + BaseFeePerGas: firehoseBigIntFromNative(baseFee), + + // Only set on Polygon fork(s) + TxDependency: nil, + } + + return pbHead +} + // FIXME: Bring back Firehose test that ensures no new tx type are missed func transactionTypeFromChainTxType(txType uint8) pbeth.TransactionTrace_Type { switch txType { diff --git a/x/evm/tracers/firehose_test.go b/x/evm/tracers/firehose_test.go index fcf84bdeb3..6556518352 100644 --- a/x/evm/tracers/firehose_test.go +++ b/x/evm/tracers/firehose_test.go @@ -339,9 +339,10 @@ func TestFirehose_reorderIsolatedTransactionsAndOrdinals(t *testing.T) { for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { - f := NewFirehose(&FirehoseConfig{ + f, err := newFirehose(&FirehoseConfig{ ApplyBackwardCompatibility: ptr(false), }) + require.NoError(t, err) f.OnBlockchainInit(params.AllEthashProtocolChanges) tt.populate(f) diff --git a/x/evm/tracers/registry.go b/x/evm/tracers/registry.go new file mode 100644 index 0000000000..71731343dd --- /dev/null +++ b/x/evm/tracers/registry.go @@ -0,0 +1,31 @@ +package tracers + +var GlobalLiveTracerRegistry = NewLiveTracerRegistry() + +type LiveTracerRegistry interface { + GetFactoryByID(id string) (BlockchainTracerFactory, bool) + Register(id string, factory BlockchainTracerFactory) +} + +var _ LiveTracerRegistry = (*liveTracerRegistry)(nil) + +func NewLiveTracerRegistry() LiveTracerRegistry { + return &liveTracerRegistry{ + tracers: make(map[string]BlockchainTracerFactory), + } +} + +type liveTracerRegistry struct { + tracers map[string]BlockchainTracerFactory +} + +// Register implements LiveTracerRegistry. +func (r *liveTracerRegistry) Register(id string, factory BlockchainTracerFactory) { + r.tracers[id] = factory +} + +// GetFactoryByID implements LiveTracerRegistry. +func (r *liveTracerRegistry) GetFactoryByID(id string) (BlockchainTracerFactory, bool) { + v, found := r.tracers[id] + return v, found +} diff --git a/x/evm/tracers/tracing.go b/x/evm/tracers/tracing.go new file mode 100644 index 0000000000..c8d19ef146 --- /dev/null +++ b/x/evm/tracers/tracing.go @@ -0,0 +1,12 @@ +package tracers + +import ( + "github.com/evmos/ethermint/x/evm/tracing" +) + +// BlockchainTracerFactory is a function that creates a new [BlockchainTracer]. +// It's going to receive the parsed URL from the `live-evm-tracer` flag. +// +// The scheme of the URL is going to be used to determine which tracer to use +// by the registry. +type BlockchainTracerFactory = func(backwardCompatibility bool) (*tracing.Hooks, error) diff --git a/x/evm/tracing/hooks.go b/x/evm/tracing/hooks.go new file mode 100644 index 0000000000..8f4cfadb9c --- /dev/null +++ b/x/evm/tracing/hooks.go @@ -0,0 +1,32 @@ +package tracing + +import ( + "github.com/cometbft/cometbft/types" + "github.com/ethereum/go-ethereum/core/tracing" + ethtypes "github.com/ethereum/go-ethereum/core/types" + "math/big" +) + +type ( + OnCosmosBlockStart func(CosmosStartBlockEvent) + OnCosmosBlockEnd func(CosmosEndBlockEvent, error) +) + +type Hooks struct { + *tracing.Hooks + + OnCosmosBlockStart OnCosmosBlockStart + OnCosmosBlockEnd OnCosmosBlockEnd +} + +type CosmosStartBlockEvent struct { + CosmosHeader types.Header + BaseFee *big.Int + GasLimit uint64 + Coinbase []byte + Finalized *ethtypes.Header +} + +type CosmosEndBlockEvent struct { + LogsBloom []byte +} diff --git a/x/evm/types/tracer.go b/x/evm/types/tracer.go index ac4c034d77..87bafd1a68 100644 --- a/x/evm/types/tracer.go +++ b/x/evm/types/tracer.go @@ -17,9 +17,11 @@ package types import ( "fmt" + cosmostracing "github.com/evmos/ethermint/x/evm/tracing" "os" "github.com/ethereum/go-ethereum/eth/tracers" + cosmostracers "github.com/evmos/ethermint/x/evm/tracers" _ "github.com/ethereum/go-ethereum/eth/tracers/live" _ "github.com/evmos/ethermint/x/evm/tracers" @@ -83,6 +85,15 @@ func NewLiveTracer(tracer string) (*tracers.Tracer, error) { }, nil } +func NewFirehoseCosmosLiveTracer() (*cosmostracing.Hooks, error) { + h, err := cosmostracers.NewCosmosFirehoseTracer(false) + if err != nil { + return nil, fmt.Errorf("initializing live tracer firehose: %w", err) + } + + return h, nil +} + // TxTraceResult is the result of a single transaction trace during a block trace. type TxTraceResult struct { Result interface{} `json:"result,omitempty"` // Trace results produced by the tracer