diff --git a/bootstrap/bootstrap.go b/bootstrap/bootstrap.go index acf7f323d..45d1f2320 100644 --- a/bootstrap/bootstrap.go +++ b/bootstrap/bootstrap.go @@ -21,6 +21,7 @@ import ( errs "github.com/onflow/flow-evm-gateway/models/errors" "github.com/onflow/flow-evm-gateway/services/ingestion" "github.com/onflow/flow-evm-gateway/services/requester" + "github.com/onflow/flow-evm-gateway/services/state" "github.com/onflow/flow-evm-gateway/services/traces" "github.com/onflow/flow-evm-gateway/storage" "github.com/onflow/flow-evm-gateway/storage/pebble" @@ -33,6 +34,7 @@ type Storages struct { Receipts storage.ReceiptIndexer Accounts storage.AccountIndexer Traces storage.TraceIndexer + Ledger *pebble.Ledger } type Publishers struct { @@ -44,14 +46,16 @@ type Publishers struct { type Bootstrap struct { logger zerolog.Logger config *config.Config - client *requester.CrossSporkClient - storages *Storages - publishers *Publishers + Client *requester.CrossSporkClient + Requester requester.Requester + Storages *Storages + Publishers *Publishers collector metrics.Collector - server *api.Server + Server *api.Server metrics *metrics.Server - events *ingestion.Engine - traces *traces.Engine + Events *ingestion.Engine + Traces *traces.Engine + State *state.Engine } func New(config *config.Config) (*Bootstrap, error) { @@ -69,15 +73,15 @@ func New(config *config.Config) (*Bootstrap, error) { } return &Bootstrap{ - publishers: &Publishers{ + Publishers: &Publishers{ Block: models.NewPublisher(), Transaction: models.NewPublisher(), Logs: models.NewPublisher(), }, - storages: storages, + Storages: storages, logger: logger, config: config, - client: client, + Client: client, collector: metrics.NewCollector(logger), }, nil } @@ -87,18 +91,18 @@ func (b *Bootstrap) StartEventIngestion(ctx context.Context) error { l.Info().Msg("bootstrap starting event ingestion") // get latest cadence block from the network and the database - latestCadenceBlock, err := b.client.GetLatestBlock(context.Background(), true) + latestCadenceBlock, err := b.Client.GetLatestBlock(context.Background(), true) if err != nil { return fmt.Errorf("failed to get latest cadence block: %w", err) } - latestCadenceHeight, err := b.storages.Blocks.LatestCadenceHeight() + latestCadenceHeight, err := b.Storages.Blocks.LatestCadenceHeight() if err != nil { return err } // make sure the provided block to start the indexing can be loaded - _, err = b.client.GetBlockHeaderByHeight(context.Background(), latestCadenceHeight) + _, err = b.Client.GetBlockHeaderByHeight(context.Background(), latestCadenceHeight) if err != nil { return fmt.Errorf( "failed to get provided cadence height %d: %w", @@ -115,27 +119,27 @@ func (b *Bootstrap) StartEventIngestion(ctx context.Context) error { // create event subscriber subscriber := ingestion.NewRPCSubscriber( - b.client, + b.Client, b.config.HeartbeatInterval, b.config.FlowNetworkID, b.logger, ) // initialize event ingestion engine - b.events = ingestion.NewEventIngestionEngine( + b.Events = ingestion.NewEventIngestionEngine( subscriber, - b.storages.Storage, - b.storages.Blocks, - b.storages.Receipts, - b.storages.Transactions, - b.storages.Accounts, - b.publishers.Block, - b.publishers.Logs, + b.Storages.Storage, + b.Storages.Blocks, + b.Storages.Receipts, + b.Storages.Transactions, + b.Storages.Accounts, + b.Publishers.Block, + b.Publishers.Logs, b.logger, b.collector, ) - startEngine(ctx, b.events, l) + startEngine(ctx, b.Events, l) return nil } @@ -150,39 +154,65 @@ func (b *Bootstrap) StartTraceDownloader(ctx context.Context) error { } // initialize trace downloader engine - b.traces = traces.NewTracesIngestionEngine( - b.publishers.Block, - b.storages.Blocks, - b.storages.Traces, + b.Traces = traces.NewTracesIngestionEngine( + b.Publishers.Block, + b.Storages.Blocks, + b.Storages.Traces, downloader, b.logger, b.collector, ) - startEngine(ctx, b.traces, l) + startEngine(ctx, b.Traces, l) return nil } func (b *Bootstrap) StopTraceDownloader() { - if b.traces == nil { + if b.Traces == nil { return } b.logger.Warn().Msg("stopping trace downloader engine") - b.traces.Stop() + b.Traces.Stop() } func (b *Bootstrap) StopEventIngestion() { - if b.events == nil { + if b.Events == nil { return } b.logger.Warn().Msg("stopping event ingestion engine") - b.events.Stop() + b.Events.Stop() +} + +func (b *Bootstrap) StartStateIndex(ctx context.Context) error { + l := b.logger.With().Str("component", "bootstrap-state").Logger() + l.Info().Msg("starting engine") + + b.State = state.NewStateEngine( + b.config.FlowNetworkID, + b.Storages.Ledger, + b.Publishers.Block, + b.Storages.Blocks, + b.Storages.Transactions, + b.Storages.Receipts, + b.logger, + ) + + startEngine(ctx, b.State, l) + return nil +} + +func (b *Bootstrap) StopStateIndex() { + if b.State == nil { + return + } + b.logger.Warn().Msg("stopping state index engine") + b.State.Stop() } func (b *Bootstrap) StartAPIServer(ctx context.Context) error { b.logger.Info().Msg("bootstrap starting metrics server") - b.server = api.NewServer(b.logger, b.collector, b.config) + b.Server = api.NewServer(b.logger, b.collector, b.config) // create the signer based on either a single coa key being provided and using a simple in-memory // signer, or multiple keys being provided and using signer with key-rotation mechanism. @@ -207,20 +237,21 @@ func (b *Bootstrap) StartAPIServer(ctx context.Context) error { } // create transaction pool - txPool := requester.NewTxPool(b.client, b.publishers.Transaction, b.logger) + txPool := requester.NewTxPool(b.Client, b.Publishers.Transaction, b.logger) evm, err := requester.NewEVM( - b.client, + b.Client, b.config, signer, b.logger, - b.storages.Blocks, + b.Storages.Blocks, txPool, b.collector, ) if err != nil { return fmt.Errorf("failed to create EVM requester: %w", err) } + b.Requester = evm // create rate limiter for requests on the APIs. Tokens are number of requests allowed per 1 second interval // if no limit is defined we specify max value, effectively disabling rate-limiting @@ -238,10 +269,10 @@ func (b *Bootstrap) StartAPIServer(ctx context.Context) error { b.logger, b.config, evm, - b.storages.Blocks, - b.storages.Transactions, - b.storages.Receipts, - b.storages.Accounts, + b.Storages.Blocks, + b.Storages.Transactions, + b.Storages.Receipts, + b.Storages.Accounts, ratelimiter, b.collector, ) @@ -252,27 +283,27 @@ func (b *Bootstrap) StartAPIServer(ctx context.Context) error { streamAPI := api.NewStreamAPI( b.logger, b.config, - b.storages.Blocks, - b.storages.Transactions, - b.storages.Receipts, - b.publishers.Block, - b.publishers.Transaction, - b.publishers.Logs, + b.Storages.Blocks, + b.Storages.Transactions, + b.Storages.Receipts, + b.Publishers.Block, + b.Publishers.Transaction, + b.Publishers.Logs, ratelimiter, ) pullAPI := api.NewPullAPI( b.logger, b.config, - b.storages.Blocks, - b.storages.Transactions, - b.storages.Receipts, + b.Storages.Blocks, + b.Storages.Transactions, + b.Storages.Receipts, ratelimiter, ) var debugAPI *api.DebugAPI if b.config.TracesEnabled { - debugAPI = api.NewDebugAPI(b.storages.Traces, b.storages.Blocks, b.logger, b.collector) + debugAPI = api.NewDebugAPI(b.Storages.Traces, b.Storages.Blocks, b.logger, b.collector) } var walletAPI *api.WalletAPI @@ -289,34 +320,34 @@ func (b *Bootstrap) StartAPIServer(ctx context.Context) error { b.config, ) - if err := b.server.EnableRPC(supportedAPIs); err != nil { + if err := b.Server.EnableRPC(supportedAPIs); err != nil { return err } if b.config.WSEnabled { - if err := b.server.EnableWS(supportedAPIs); err != nil { + if err := b.Server.EnableWS(supportedAPIs); err != nil { return err } } - if err := b.server.SetListenAddr(b.config.RPCHost, b.config.RPCPort); err != nil { + if err := b.Server.SetListenAddr(b.config.RPCHost, b.config.RPCPort); err != nil { return err } - if err := b.server.Start(); err != nil { + if err := b.Server.Start(); err != nil { return err } - b.logger.Info().Msgf("API server started: %s", b.server.ListenAddr()) + b.logger.Info().Msgf("API server started: %s", b.Server.ListenAddr()) return nil } func (b *Bootstrap) StopAPIServer() { - if b.server == nil { + if b.Server == nil { return } b.logger.Warn().Msg("shutting down API server") - b.server.Stop() + b.Server.Stop() } func (b *Bootstrap) StartMetricsServer(_ context.Context) error { @@ -448,6 +479,7 @@ func setupStorage( Receipts: pebble.NewReceipts(store), Accounts: pebble.NewAccounts(store), Traces: pebble.NewTraces(store), + Ledger: pebble.NewLedger(store), }, nil } @@ -466,6 +498,10 @@ func Run(ctx context.Context, cfg *config.Config, ready chan struct{}) error { } } + if err := boot.StartStateIndex(ctx); err != nil { + return fmt.Errorf("failed to start local state index engine: %w", err) + } + if err := boot.StartEventIngestion(ctx); err != nil { return fmt.Errorf("failed to start event ingestion engine: %w", err) } diff --git a/models/receipt.go b/models/receipt.go index 71a05f0f3..01f68ccb7 100644 --- a/models/receipt.go +++ b/models/receipt.go @@ -1,6 +1,7 @@ package models import ( + "bytes" "fmt" "math/big" @@ -69,6 +70,76 @@ func ReceiptsFromBytes(data []byte) ([]*Receipt, error) { return receipts, nil } +// EqualReceipts takes a geth Receipt type and EVM GW receipt and compares all the applicable values. +func EqualReceipts(gethReceipt *gethTypes.Receipt, receipt *Receipt) (bool, []error) { + errs := make([]error, 0) + + // fail if any receipt or both are nil + if gethReceipt == nil || receipt == nil { + errs = append(errs, fmt.Errorf("one or both receipts are nil")) + return false, errs + } + // compare logs + if len(gethReceipt.Logs) != len(receipt.Logs) { + errs = append(errs, fmt.Errorf("log length mismatch: geth logs length %d, receipt logs length %d", len(gethReceipt.Logs), len(receipt.Logs))) + return false, errs + } + + // compare each log entry + for i, l := range gethReceipt.Logs { + rl := receipt.Logs[i] + if rl.BlockNumber != l.BlockNumber { + errs = append(errs, fmt.Errorf("log block number mismatch at index %d: %d != %d", i, rl.BlockNumber, l.BlockNumber)) + } + if rl.Removed != l.Removed { + errs = append(errs, fmt.Errorf("log removed status mismatch at index %d: %v != %v", i, rl.Removed, l.Removed)) + } + if rl.TxHash.Cmp(l.TxHash) != 0 { + errs = append(errs, fmt.Errorf("log TxHash mismatch at index %d", i)) + } + if rl.Address.Cmp(l.Address) != 0 { + errs = append(errs, fmt.Errorf("log address mismatch at index %d", i)) + } + if !bytes.Equal(rl.Data, l.Data) { + errs = append(errs, fmt.Errorf("log data mismatch at index %d", i)) + } + if rl.TxIndex != l.TxIndex { + errs = append(errs, fmt.Errorf("log transaction index mismatch at index %d: %d != %d", i, rl.TxIndex, l.TxIndex)) + } + // compare all topics + for j, t := range rl.Topics { + if t.Cmp(l.Topics[j]) != 0 { + errs = append(errs, fmt.Errorf("log topic mismatch at index %d, topic %d", i, j)) + } + } + } + + // compare all receipt data + if gethReceipt.TxHash.Cmp(receipt.TxHash) != 0 { + errs = append(errs, fmt.Errorf("receipt TxHash mismatch")) + } + if gethReceipt.GasUsed != receipt.GasUsed { + errs = append(errs, fmt.Errorf("receipt GasUsed mismatch: %d != %d", gethReceipt.GasUsed, receipt.GasUsed)) + } + if gethReceipt.CumulativeGasUsed != receipt.CumulativeGasUsed { + errs = append(errs, fmt.Errorf("receipt CumulativeGasUsed mismatch: %d != %d", gethReceipt.CumulativeGasUsed, receipt.CumulativeGasUsed)) + } + if gethReceipt.Type != 0 && gethReceipt.Type != receipt.Type { // only compare if not direct call + errs = append(errs, fmt.Errorf("receipt Type mismatch: %d != %d", gethReceipt.Type, receipt.Type)) + } + if gethReceipt.ContractAddress.Cmp(receipt.ContractAddress) != 0 { + errs = append(errs, fmt.Errorf("receipt ContractAddress mismatch")) + } + if gethReceipt.Status != receipt.Status { + errs = append(errs, fmt.Errorf("receipt Status mismatch: %d != %d", gethReceipt.Status, receipt.Status)) + } + if !bytes.Equal(gethReceipt.Bloom.Bytes(), receipt.Bloom.Bytes()) { + errs = append(errs, fmt.Errorf("receipt Bloom mismatch")) + } + + return len(errs) == 0, errs +} + type BloomsHeight struct { Blooms []*gethTypes.Bloom Height uint64 diff --git a/models/transaction.go b/models/transaction.go index 8e19474ee..f563ba809 100644 --- a/models/transaction.go +++ b/models/transaction.go @@ -203,7 +203,11 @@ func decodeTransactionEvent(event cadence.Event) (Transaction, *Receipt, error) revertReason = txEvent.ReturnedData } - receipt := NewReceipt(gethReceipt, revertReason, txEvent.PrecompiledCalls) + receipt := NewReceipt( + gethReceipt, + revertReason, + txEvent.PrecompiledCalls, + ) var tx Transaction // check if the transaction payload is actually from a direct call, diff --git a/services/state/engine.go b/services/state/engine.go new file mode 100644 index 000000000..437b051ba --- /dev/null +++ b/services/state/engine.go @@ -0,0 +1,193 @@ +package state + +import ( + "context" + "fmt" + + "github.com/google/uuid" + "github.com/onflow/atree" + "github.com/onflow/flow-go/fvm/evm/precompiles" + "github.com/onflow/flow-go/fvm/evm/types" + flowGo "github.com/onflow/flow-go/model/flow" + "github.com/onflow/go-ethereum/common" + "github.com/rs/zerolog" + + "github.com/onflow/flow-evm-gateway/models" + "github.com/onflow/flow-evm-gateway/storage" +) + +var _ models.Engine = &Engine{} +var _ models.Subscriber = &Engine{} + +type Engine struct { + chainID flowGo.ChainID + logger zerolog.Logger + status *models.EngineStatus + blockPublisher *models.Publisher + blocks storage.BlockIndexer + transactions storage.TransactionIndexer + receipts storage.ReceiptIndexer + ledger atree.Ledger +} + +func NewStateEngine( + chainID flowGo.ChainID, + ledger atree.Ledger, + blockPublisher *models.Publisher, + blocks storage.BlockIndexer, + transactions storage.TransactionIndexer, + receipts storage.ReceiptIndexer, + logger zerolog.Logger, +) *Engine { + log := logger.With().Str("component", "state").Logger() + + return &Engine{ + chainID: chainID, + logger: log, + status: models.NewEngineStatus(), + blockPublisher: blockPublisher, + blocks: blocks, + transactions: transactions, + receipts: receipts, + ledger: ledger, + } +} + +// todo rethink whether it would be more robust to rely on blocks in the storage +// instead of receiving events, relying on storage and keeping a separate count of +// transactions executed would allow for independent restart and reexecution +// if we panic with events the missed tx won't get reexecuted since it's relying on +// event ingestion also not indexing that transaction + +func (e *Engine) Notify(data any) { + block, ok := data.(*models.Block) + if !ok { + e.logger.Error().Msg("invalid event type sent to state ingestion") + return + } + + l := e.logger.With().Uint64("evm-height", block.Height).Logger() + l.Info().Msg("received new block") + + if err := e.executeBlock(block); err != nil { + panic(fmt.Errorf("failed to execute block at height %d: %w", block.Height, err)) + } + + l.Info().Msg("successfully executed block") +} + +func (e *Engine) Run(ctx context.Context) error { + e.blockPublisher.Subscribe(e) + e.status.MarkReady() + return nil +} + +func (e *Engine) Stop() { + // todo cleanup + e.status.MarkStopped() +} + +func (e *Engine) Done() <-chan struct{} { + return e.status.IsDone() +} + +func (e *Engine) Ready() <-chan struct{} { + return e.status.IsReady() +} + +func (e *Engine) Error() <-chan error { + return nil +} + +func (e *Engine) ID() uuid.UUID { + return uuid.New() +} + +// executeBlock will execute all transactions in the provided block. +// If a transaction fails to execute or the result doesn't match expected +// result return an error. +// Transaction executed should match a receipt we have indexed from the network +// produced by execution nodes. This check makes sure we keep a correct state. +func (e *Engine) executeBlock(block *models.Block) error { + state, err := NewState(block, e.ledger, e.chainID, e.blocks, e.receipts, e.logger) + if err != nil { + return err + } + + // track gas usage in a virtual block + gasUsed := uint64(0) + + for i, h := range block.TransactionHashes { + e.logger.Info().Str("hash", h.String()).Msg("transaction execution") + + tx, err := e.transactions.Get(h) + if err != nil { + return err + } + + receipt, err := e.receipts.GetByTransactionID(tx.Hash()) + if err != nil { + return err + } + + ctx, err := e.blockContext(block, receipt, uint(i), gasUsed) + if err != nil { + return err + } + + resultReceipt, err := state.Execute(ctx, tx) + if err != nil { + return err + } + + // increment the gas used only after it's executed + gasUsed += receipt.GasUsed + + if ok, errs := models.EqualReceipts(resultReceipt, receipt); !ok { + return fmt.Errorf("state missmatch: %v", errs) + } + } + + return nil +} + +func (e *Engine) blockContext( + block *models.Block, + receipt *models.Receipt, + txIndex uint, + gasUsed uint64, +) (types.BlockContext, error) { + calls, err := types.AggregatedPrecompileCallsFromEncoded(receipt.PrecompiledCalls) + if err != nil { + return types.BlockContext{}, err + } + + precompileContracts := precompiles.AggregatedPrecompiledCallsToPrecompiledContracts(calls) + + return types.BlockContext{ + ChainID: types.EVMChainIDFromFlowChainID(e.chainID), + BlockNumber: block.Height, + BlockTimestamp: block.Timestamp, + DirectCallBaseGasUsage: types.DefaultDirectCallBaseGasUsage, // todo check + DirectCallGasPrice: types.DefaultDirectCallGasPrice, + GasFeeCollector: types.CoinbaseAddress, + GetHashFunc: func(n uint64) common.Hash { + b, err := e.blocks.GetByHeight(n) + if err != nil { + panic(err) + } + h, err := b.Hash() + if err != nil { + panic(err) + } + + return h + }, + Random: block.PrevRandao, + ExtraPrecompiledContracts: precompileContracts, + TxCountSoFar: txIndex, + TotalGasUsedSoFar: gasUsed, + // todo what to do with the tracer + Tracer: nil, + }, nil +} diff --git a/services/state/state.go b/services/state/state.go new file mode 100644 index 000000000..5b2818885 --- /dev/null +++ b/services/state/state.go @@ -0,0 +1,91 @@ +package state + +import ( + "fmt" + + "github.com/onflow/atree" + "github.com/onflow/flow-go/fvm/evm" + "github.com/onflow/flow-go/fvm/evm/emulator" + "github.com/onflow/flow-go/fvm/evm/emulator/state" + "github.com/onflow/flow-go/fvm/evm/types" + flowGo "github.com/onflow/flow-go/model/flow" + gethTypes "github.com/onflow/go-ethereum/core/types" + "github.com/rs/zerolog" + + "github.com/onflow/flow-evm-gateway/models" + "github.com/onflow/flow-evm-gateway/storage" +) + +type State struct { + types.StateDB // todo change to types.ReadOnlyView + emulator types.Emulator + chainID flowGo.ChainID + block *models.Block + blocks storage.BlockIndexer + receipts storage.ReceiptIndexer + logger zerolog.Logger +} + +func NewState( + block *models.Block, + ledger atree.Ledger, + chainID flowGo.ChainID, + blocks storage.BlockIndexer, + receipts storage.ReceiptIndexer, + logger zerolog.Logger, +) (*State, error) { + logger = logger.With().Str("component", "state-execution").Logger() + storageAddress := evm.StorageAccountAddress(chainID) + + s, err := state.NewStateDB(ledger, storageAddress) + if err != nil { + return nil, err + } + + emu := emulator.NewEmulator(ledger, storageAddress) + + return &State{ + StateDB: s, + emulator: emu, + chainID: chainID, + block: block, + blocks: blocks, + receipts: receipts, + logger: logger, + }, nil +} + +func (s *State) Execute(ctx types.BlockContext, tx models.Transaction) (*gethTypes.Receipt, error) { + l := s.logger.With().Str("tx-hash", tx.Hash().String()).Logger() + l.Info().Msg("executing new transaction") + + bv, err := s.emulator.NewBlockView(ctx) + if err != nil { + return nil, err + } + + var res *types.Result + + switch t := tx.(type) { + case models.DirectCall: + res, err = bv.DirectCall(t.DirectCall) + case models.TransactionCall: + res, err = bv.RunTransaction(t.Transaction) + default: + return nil, fmt.Errorf("invalid transaction type") + } + + if err != nil { + // todo is this ok, the service would restart and retry? + return nil, err + } + + // we should never produce invalid transaction, since if the transaction was emitted from the evm core + // it must have either been successful or failed, invalid transactions are not emitted + if res.Invalid() { + return nil, fmt.Errorf("invalid transaction %s: %w", tx.Hash(), res.ValidationError) + } + + l.Debug().Msg("transaction executed successfully") + return res.Receipt(), nil +} diff --git a/storage/pebble/ledger.go b/storage/pebble/ledger.go index 6669570ec..79815feba 100644 --- a/storage/pebble/ledger.go +++ b/storage/pebble/ledger.go @@ -101,7 +101,7 @@ func (l *Ledger) AllocateSlabIndex(owner []byte) (atree.SlabIndex, error) { copy(index[:], val) } - index.Next() + index = index.Next() if err := l.store.set(ledgerSlabIndex, owner, index[:], nil); err != nil { return atree.SlabIndexUndefined, fmt.Errorf( "slab index failed to set for owner %x: %w", diff --git a/tests/helpers.go b/tests/helpers.go index 91eb05aa7..6fbec8100 100644 --- a/tests/helpers.go +++ b/tests/helpers.go @@ -82,14 +82,15 @@ func startEmulator(createTestAccounts bool) (*server.EmulatorServer, error) { } srv := server.NewEmulatorServer(&log, &server.Config{ - ServicePrivateKey: pkey, - ServiceKeySigAlgo: sigAlgo, - ServiceKeyHashAlgo: hashAlgo, - GenesisTokenSupply: genesisToken, - WithContracts: true, - Host: "localhost", - TransactionExpiry: 10, - TransactionMaxGasLimit: flow.DefaultMaxTransactionGasLimit, + ServicePrivateKey: pkey, + ServiceKeySigAlgo: sigAlgo, + ServiceKeyHashAlgo: hashAlgo, + GenesisTokenSupply: genesisToken, + WithContracts: true, + Host: "localhost", + TransactionExpiry: 10, + TransactionMaxGasLimit: flow.DefaultMaxTransactionGasLimit, + SkipTransactionValidation: true, }) go func() { @@ -332,7 +333,8 @@ func evmSign( signer *ecdsa.PrivateKey, nonce uint64, to *common.Address, - data []byte) ([]byte, common.Hash, error) { + data []byte, +) ([]byte, common.Hash, error) { gasPrice := big.NewInt(0) evmTx := types.NewTx(&types.LegacyTx{Nonce: nonce, To: to, Value: weiValue, Gas: gasLimit, GasPrice: gasPrice, Data: data}) diff --git a/tests/state_integration_test.go b/tests/state_integration_test.go new file mode 100644 index 000000000..2959264f6 --- /dev/null +++ b/tests/state_integration_test.go @@ -0,0 +1,126 @@ +package tests + +import ( + "context" + "math/big" + "testing" + "time" + + "github.com/onflow/flow-go/fvm/evm/types" + flowGo "github.com/onflow/flow-go/model/flow" + "github.com/onflow/go-ethereum/common" + "github.com/onflow/go-ethereum/crypto" + "github.com/rs/zerolog" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + + "github.com/onflow/flow-evm-gateway/bootstrap" + "github.com/onflow/flow-evm-gateway/config" + "github.com/onflow/flow-evm-gateway/services/state" + "github.com/onflow/flow-evm-gateway/storage/pebble" +) + +func Test_StateExecution_Transfers(t *testing.T) { + srv, err := startEmulator(true) + require.NoError(t, err) + + emu := srv.Emulator() + service := emu.ServiceKey() + require.NoError(t, err) + + ctx, cancel := context.WithCancel(context.Background()) + defer func() { + cancel() + }() + + cfg := &config.Config{ + InitCadenceHeight: 0, + DatabaseDir: t.TempDir(), + FlowNetworkID: flowGo.Emulator, + HeartbeatInterval: 50, + EVMNetworkID: types.FlowEVMPreviewNetChainID, + AccessNodeHost: "localhost:3569", + Coinbase: common.HexToAddress(eoaTestAddress), + COAAddress: service.Address, + COAKey: service.PrivateKey, + CreateCOAResource: true, + GasPrice: new(big.Int).SetUint64(0), + LogLevel: zerolog.DebugLevel, + LogWriter: zerolog.NewConsoleWriter(), + } + + b, err := bootstrap.New(cfg) + require.NoError(t, err) + + require.NoError(t, b.StartStateIndex(ctx)) + require.NoError(t, b.StartAPIServer(ctx)) + require.NoError(t, b.StartEventIngestion(ctx)) + + blocks := b.Storages.Blocks + receipts := b.Storages.Receipts + store := b.Storages.Storage + requester := b.Requester + + latest, err := blocks.LatestEVMHeight() + require.NoError(t, err) + + block, err := blocks.GetByHeight(latest) + require.NoError(t, err) + + // wait for emulator to boot + time.Sleep(time.Second) + + st, err := state.NewState(block, pebble.NewLedger(store), cfg.FlowNetworkID, blocks, receipts, logger) + require.NoError(t, err) + + testAddr := common.HexToAddress("55253ed90B70b96C73092D8680915aaF50081194") + eoaKey, err := crypto.HexToECDSA(eoaTestPrivateKey) + + balance := st.GetBalance(testAddr) + assert.Equal(t, uint64(0), balance.Uint64()) + + amount := big.NewInt(1) + nonce := uint64(0) + evmTx, _, err := evmSign(amount, 21000, eoaKey, nonce, &testAddr, nil) + require.NoError(t, err) + + hash, err := requester.SendRawTransaction(ctx, evmTx) + require.NoError(t, err) + require.NotEmpty(t, hash) + + // wait for new block event + time.Sleep(time.Second) + latest, err = blocks.LatestEVMHeight() + require.NoError(t, err) + + block, err = blocks.GetByHeight(latest) + require.NoError(t, err) + + st, err = state.NewState(block, pebble.NewLedger(store), cfg.FlowNetworkID, blocks, receipts, logger) + require.NoError(t, err) + + balance = st.GetBalance(testAddr) + assert.Equal(t, amount.Uint64(), balance.Uint64()) + + amount2 := big.NewInt(2) + nonce++ + evmTx, _, err = evmSign(amount2, 21000, eoaKey, nonce, &testAddr, nil) + require.NoError(t, err) + + hash, err = requester.SendRawTransaction(ctx, evmTx) + require.NoError(t, err) + require.NotEmpty(t, hash) + + // wait for new block event + time.Sleep(time.Second) + latest, err = blocks.LatestEVMHeight() + require.NoError(t, err) + + st, err = state.NewState(block, pebble.NewLedger(store), cfg.FlowNetworkID, blocks, receipts, logger) + require.NoError(t, err) + + balance = st.GetBalance(testAddr) + assert.Equal(t, amount.Uint64()+amount2.Uint64(), balance.Uint64()) +} + +// todo test historic heights