From 0446472d3c578c3dd8af98afb7314b2997ea14ac Mon Sep 17 00:00:00 2001 From: Janez Podhostnik Date: Tue, 29 Oct 2024 15:55:08 +0100 Subject: [PATCH 001/174] clenup of event subscriber --- bootstrap/bootstrap.go | 2 +- .../{subscriber.go => event_subscriber.go} | 131 ++++++++++-------- ...riber_test.go => event_subscriber_test.go} | 10 +- 3 files changed, 77 insertions(+), 66 deletions(-) rename services/ingestion/{subscriber.go => event_subscriber.go} (83%) rename services/ingestion/{subscriber_test.go => event_subscriber_test.go} (96%) diff --git a/bootstrap/bootstrap.go b/bootstrap/bootstrap.go index dbed6f484..bc8ccc1d3 100644 --- a/bootstrap/bootstrap.go +++ b/bootstrap/bootstrap.go @@ -117,7 +117,7 @@ func (b *Bootstrap) StartEventIngestion(ctx context.Context) error { Msg("indexing cadence height information") // create event subscriber - subscriber := ingestion.NewRPCSubscriber( + subscriber := ingestion.NewRPCEventSubscriber( b.client, b.config.HeartbeatInterval, b.config.FlowNetworkID, diff --git a/services/ingestion/subscriber.go b/services/ingestion/event_subscriber.go similarity index 83% rename from services/ingestion/subscriber.go rename to services/ingestion/event_subscriber.go index 6d347cd9f..24fc4dd72 100644 --- a/services/ingestion/subscriber.go +++ b/services/ingestion/event_subscriber.go @@ -27,30 +27,40 @@ type EventSubscriber interface { Subscribe(ctx context.Context, height uint64) <-chan models.BlockEvents } -var _ EventSubscriber = &RPCSubscriber{} +var _ EventSubscriber = &RPCEventSubscriber{} -type RPCSubscriber struct { - client *requester.CrossSporkClient - chain flowGo.ChainID - heartbeatInterval uint64 - logger zerolog.Logger +type RPCEventSubscriberConfig struct { + HeartbeatInterval uint64 +} + +type RPCEventSubscriber struct { + RPCEventSubscriberConfig + + logger zerolog.Logger + + client *requester.CrossSporkClient + chain flowGo.ChainID recovery bool recoveredEvents []flow.Event } -func NewRPCSubscriber( +func NewRPCEventSubscriber( client *requester.CrossSporkClient, heartbeatInterval uint64, chainID flowGo.ChainID, logger zerolog.Logger, -) *RPCSubscriber { +) *RPCEventSubscriber { logger = logger.With().Str("component", "subscriber").Logger() - return &RPCSubscriber{ - client: client, - heartbeatInterval: heartbeatInterval, - chain: chainID, - logger: logger, + return &RPCEventSubscriber{ + RPCEventSubscriberConfig: RPCEventSubscriberConfig{ + HeartbeatInterval: heartbeatInterval, + }, + + logger: logger, + + client: client, + chain: chainID, } } @@ -59,23 +69,23 @@ func NewRPCSubscriber( // to listen all new events in the current spork. // // If error is encountered during backfill the subscription will end and the response chanel will be closed. -func (r *RPCSubscriber) Subscribe(ctx context.Context, height uint64) <-chan models.BlockEvents { - events := make(chan models.BlockEvents) +func (r *RPCEventSubscriber) Subscribe(ctx context.Context, height uint64) <-chan models.BlockEvents { + eventsChan := make(chan models.BlockEvents) go func() { defer func() { - close(events) + close(eventsChan) }() - // if the height is from the previous spork, backfill all the events from previous sporks first + // if the height is from the previous spork, backfill all the eventsChan from previous sporks first if r.client.IsPastSpork(height) { r.logger.Info(). Uint64("height", height). Msg("height found in previous spork, starting to backfill") - // backfill all the missed events, handling of context cancellation is done by the producer + // backfill all the missed eventsChan, handling of context cancellation is done by the producer for ev := range r.backfill(ctx, height) { - events <- ev + eventsChan <- ev if ev.Err != nil { return @@ -96,21 +106,22 @@ func (r *RPCSubscriber) Subscribe(ctx context.Context, height uint64) <-chan mod Msg("backfilling done, subscribe for live data") // subscribe in the current spork, handling of context cancellation is done by the producer - for ev := range r.subscribe(ctx, height, access.WithHeartbeatInterval(r.heartbeatInterval)) { - events <- ev + // TODO(JanezP): I think the heartbeat interval should always be 1 here + for ev := range r.subscribe(ctx, height, access.WithHeartbeatInterval(r.HeartbeatInterval)) { + eventsChan <- ev } r.logger.Warn().Msg("ended subscription for events") }() - return events + return eventsChan } // subscribe to events by the provided height and handle any errors. // // Subscribing to EVM specific events and handle any disconnection errors // as well as context cancellations. -func (r *RPCSubscriber) subscribe(ctx context.Context, height uint64, opts ...access.SubscribeOption) <-chan models.BlockEvents { +func (r *RPCEventSubscriber) subscribe(ctx context.Context, height uint64, opts ...access.SubscribeOption) <-chan models.BlockEvents { eventsChan := make(chan models.BlockEvents) _, err := r.client.GetBlockHeaderByHeight(ctx, height) @@ -120,7 +131,7 @@ func (r *RPCSubscriber) subscribe(ctx context.Context, height uint64, opts ...ac return eventsChan } - eventStream, errChan, err := r.client.SubscribeEventsByBlockHeight(ctx, height, r.blocksFilter(), opts...) + eventStream, errChan, err := r.client.SubscribeEventsByBlockHeight(ctx, height, blocksFilter(r.chain), opts...) if err != nil { eventsChan <- models.NewBlockEventsError( fmt.Errorf("failed to subscribe to events by block height: %d, with: %w", height, err), @@ -187,12 +198,12 @@ func (r *RPCSubscriber) subscribe(ctx context.Context, height uint64, opts ...ac // and check for each event it receives whether we reached the end, if we reach the end it will increase // the height by one (next height), and check if we are still in previous sporks, if so repeat everything, // otherwise return. -func (r *RPCSubscriber) backfill(ctx context.Context, height uint64) <-chan models.BlockEvents { - events := make(chan models.BlockEvents) +func (r *RPCEventSubscriber) backfill(ctx context.Context, height uint64) <-chan models.BlockEvents { + eventsChan := make(chan models.BlockEvents) go func() { defer func() { - close(events) + close(eventsChan) }() for { @@ -207,7 +218,7 @@ func (r *RPCSubscriber) backfill(ctx context.Context, height uint64) <-chan mode latestHeight, err := r.client.GetLatestHeightForSpork(ctx, height) if err != nil { - events <- models.NewBlockEventsError(err) + eventsChan <- models.NewBlockEventsError(err) return } @@ -217,7 +228,7 @@ func (r *RPCSubscriber) backfill(ctx context.Context, height uint64) <-chan mode Msg("backfilling spork") for ev := range r.subscribe(ctx, height, access.WithHeartbeatInterval(1)) { - events <- ev + eventsChan <- ev if ev.Err != nil { return @@ -238,33 +249,7 @@ func (r *RPCSubscriber) backfill(ctx context.Context, height uint64) <-chan mode } }() - return events -} - -// blockFilter define events we subscribe to: -// A.{evm}.EVM.BlockExecuted and A.{evm}.EVM.TransactionExecuted, -// where {evm} is EVM deployed contract address, which depends on the chain ID we configure. -func (r *RPCSubscriber) blocksFilter() flow.EventFilter { - evmAddress := common.Address(systemcontracts.SystemContractsForChain(r.chain).EVMContract.Address) - - blockExecutedEvent := common.NewAddressLocation( - nil, - evmAddress, - string(events.EventTypeBlockExecuted), - ).ID() - - transactionExecutedEvent := common.NewAddressLocation( - nil, - evmAddress, - string(events.EventTypeTransactionExecuted), - ).ID() - - return flow.EventFilter{ - EventTypes: []string{ - blockExecutedEvent, - transactionExecutedEvent, - }, - } + return eventsChan } // fetchMissingData is used as a backup mechanism for fetching EVM-related @@ -272,14 +257,14 @@ func (r *RPCSubscriber) blocksFilter() flow.EventFilter { // An inconsistent response could be an EVM block that references EVM // transactions which are not present in the response. It falls back // to using grpc requests instead of streaming. -func (r *RPCSubscriber) fetchMissingData( +func (r *RPCEventSubscriber) fetchMissingData( ctx context.Context, blockEvents flow.BlockEvents, ) models.BlockEvents { // remove existing events blockEvents.Events = nil - for _, eventType := range r.blocksFilter().EventTypes { + for _, eventType := range blocksFilter(r.chain).EventTypes { recoveredEvents, err := r.client.GetEventsForHeightRange( ctx, eventType, @@ -309,7 +294,7 @@ func (r *RPCSubscriber) fetchMissingData( // accumulateEventsMissingBlock will keep receiving transaction events until it can produce a valid // EVM block event containing a block and transactions. At that point it will reset the recovery mode // and return the valid block events. -func (r *RPCSubscriber) accumulateEventsMissingBlock(events flow.BlockEvents) models.BlockEvents { +func (r *RPCEventSubscriber) accumulateEventsMissingBlock(events flow.BlockEvents) models.BlockEvents { r.recoveredEvents = append(r.recoveredEvents, events.Events...) events.Events = r.recoveredEvents @@ -329,7 +314,7 @@ func (r *RPCSubscriber) accumulateEventsMissingBlock(events flow.BlockEvents) mo // in which case we might miss one of the events (missing transaction), or it can be // due to a failure from the system transaction which commits an EVM block, which results // in missing EVM block event but present transactions. -func (r *RPCSubscriber) recover( +func (r *RPCEventSubscriber) recover( ctx context.Context, events flow.BlockEvents, err error, @@ -349,3 +334,29 @@ func (r *RPCSubscriber) recover( return models.NewBlockEventsError(err) } + +// blockFilter define events we subscribe to: +// A.{evm}.EVM.BlockExecuted and A.{evm}.EVM.TransactionExecuted, +// where {evm} is EVM deployed contract address, which depends on the chain ID we configure. +func blocksFilter(chainId flowGo.ChainID) flow.EventFilter { + evmAddress := common.Address(systemcontracts.SystemContractsForChain(chainId).EVMContract.Address) + + blockExecutedEvent := common.NewAddressLocation( + nil, + evmAddress, + string(events.EventTypeBlockExecuted), + ).ID() + + transactionExecutedEvent := common.NewAddressLocation( + nil, + evmAddress, + string(events.EventTypeTransactionExecuted), + ).ID() + + return flow.EventFilter{ + EventTypes: []string{ + blockExecutedEvent, + transactionExecutedEvent, + }, + } +} diff --git a/services/ingestion/subscriber_test.go b/services/ingestion/event_subscriber_test.go similarity index 96% rename from services/ingestion/subscriber_test.go rename to services/ingestion/event_subscriber_test.go index 22ac61e16..22865728b 100644 --- a/services/ingestion/subscriber_test.go +++ b/services/ingestion/event_subscriber_test.go @@ -43,7 +43,7 @@ func Test_Subscribing(t *testing.T) { ) require.NoError(t, err) - subscriber := NewRPCSubscriber(client, 100, flowGo.Previewnet, zerolog.Nop()) + subscriber := NewRPCEventSubscriber(client, 100, flowGo.Previewnet, zerolog.Nop()) events := subscriber.Subscribe(context.Background(), 1) @@ -83,7 +83,7 @@ func Test_MissingBlockEvent(t *testing.T) { ) require.NoError(t, err) - subscriber := NewRPCSubscriber(client, 100, flowGo.Previewnet, zerolog.Nop()) + subscriber := NewRPCEventSubscriber(client, 100, flowGo.Previewnet, zerolog.Nop()) events := subscriber.Subscribe(context.Background(), 1) @@ -185,7 +185,7 @@ func Test_SubscribingWithRetryOnError(t *testing.T) { ) require.NoError(t, err) - subscriber := NewRPCSubscriber(client, 100, flowGo.Previewnet, zerolog.Nop()) + subscriber := NewRPCEventSubscriber(client, 100, flowGo.Previewnet, zerolog.Nop()) events := subscriber.Subscribe(context.Background(), 1) @@ -248,7 +248,7 @@ func Test_SubscribingWithRetryOnErrorMultipleBlocks(t *testing.T) { ) require.NoError(t, err) - subscriber := NewRPCSubscriber(client, 100, flowGo.Previewnet, zerolog.Nop()) + subscriber := NewRPCEventSubscriber(client, 100, flowGo.Previewnet, zerolog.Nop()) events := subscriber.Subscribe(context.Background(), 1) @@ -310,7 +310,7 @@ func Test_SubscribingWithRetryOnErrorEmptyBlocks(t *testing.T) { ) require.NoError(t, err) - subscriber := NewRPCSubscriber(client, 100, flowGo.Previewnet, zerolog.Nop()) + subscriber := NewRPCEventSubscriber(client, 100, flowGo.Previewnet, zerolog.Nop()) events := subscriber.Subscribe(context.Background(), 1) From c9d37528b26994a5369d5024114558eec386fa84 Mon Sep 17 00:00:00 2001 From: Janez Podhostnik Date: Tue, 29 Oct 2024 16:24:23 +0100 Subject: [PATCH 002/174] pull start height out of event subscriber --- bootstrap/bootstrap.go | 7 +--- services/ingestion/engine.go | 14 +++----- services/ingestion/engine_test.go | 21 ++++++------ services/ingestion/event_subscriber.go | 25 +++++++------- services/ingestion/event_subscriber_test.go | 36 ++++++++++----------- services/ingestion/mocks/EventSubscriber.go | 10 +++--- 6 files changed, 52 insertions(+), 61 deletions(-) diff --git a/bootstrap/bootstrap.go b/bootstrap/bootstrap.go index bc8ccc1d3..df8e53a73 100644 --- a/bootstrap/bootstrap.go +++ b/bootstrap/bootstrap.go @@ -117,12 +117,7 @@ func (b *Bootstrap) StartEventIngestion(ctx context.Context) error { Msg("indexing cadence height information") // create event subscriber - subscriber := ingestion.NewRPCEventSubscriber( - b.client, - b.config.HeartbeatInterval, - b.config.FlowNetworkID, - b.logger, - ) + subscriber := ingestion.NewRPCEventSubscriber(b.logger, b.client, b.config.FlowNetworkID, latestCadenceHeight, b.config.HeartbeatInterval) // initialize event ingestion engine b.events = ingestion.NewEventIngestionEngine( diff --git a/services/ingestion/engine.go b/services/ingestion/engine.go index 37aeae0b7..62bde4493 100644 --- a/services/ingestion/engine.go +++ b/services/ingestion/engine.go @@ -98,25 +98,19 @@ func (e *Engine) Stop() { // drops. // All other errors are unexpected. func (e *Engine) Run(ctx context.Context) error { - latestCadence, err := e.blocks.LatestCadenceHeight() - if err != nil { - return fmt.Errorf("failed to get latest cadence height: %w", err) - } - - e.log.Info().Uint64("start-cadence-height", latestCadence).Msg("starting ingestion") + e.log.Info().Msg("starting ingestion") e.MarkReady() - for events := range e.subscriber.Subscribe(ctx, latestCadence) { + for events := range e.subscriber.Subscribe(ctx) { if events.Err != nil { return fmt.Errorf( - "failure in event subscription at height %d, with: %w", - latestCadence, + "failure in event subscription with: %w", events.Err, ) } - err = e.processEvents(events.Events) + err := e.processEvents(events.Events) if err != nil { e.log.Error().Err(err).Msg("failed to process EVM events") return err diff --git a/services/ingestion/engine_test.go b/services/ingestion/engine_test.go index c7f6a77bb..0fddb5e73 100644 --- a/services/ingestion/engine_test.go +++ b/services/ingestion/engine_test.go @@ -56,8 +56,8 @@ func TestSerialBlockIngestion(t *testing.T) { subscriber := &mocks.EventSubscriber{} subscriber. - On("Subscribe", mock.Anything, mock.AnythingOfType("uint64")). - Return(func(ctx context.Context, latest uint64) <-chan models.BlockEvents { + On("Subscribe", mock.Anything). + Return(func(ctx context.Context) <-chan models.BlockEvents { return eventsChan }) @@ -136,8 +136,8 @@ func TestSerialBlockIngestion(t *testing.T) { eventsChan := make(chan models.BlockEvents) subscriber := &mocks.EventSubscriber{} subscriber. - On("Subscribe", mock.Anything, mock.AnythingOfType("uint64")). - Return(func(ctx context.Context, latest uint64) <-chan models.BlockEvents { + On("Subscribe", mock.Anything). + Return(func(ctx context.Context) <-chan models.BlockEvents { return eventsChan }) @@ -246,8 +246,8 @@ func TestBlockAndTransactionIngestion(t *testing.T) { eventsChan := make(chan models.BlockEvents) subscriber := &mocks.EventSubscriber{} subscriber. - On("Subscribe", mock.Anything, mock.AnythingOfType("uint64")). - Return(func(ctx context.Context, latest uint64) <-chan models.BlockEvents { + On("Subscribe", mock.Anything). + Return(func(ctx context.Context) <-chan models.BlockEvents { return eventsChan }) @@ -349,8 +349,8 @@ func TestBlockAndTransactionIngestion(t *testing.T) { eventsChan := make(chan models.BlockEvents) subscriber := &mocks.EventSubscriber{} subscriber. - On("Subscribe", mock.Anything, mock.AnythingOfType("uint64")). - Return(func(ctx context.Context, latest uint64) <-chan models.BlockEvents { + On("Subscribe", mock.Anything). + Return(func(ctx context.Context) <-chan models.BlockEvents { return eventsChan }) @@ -448,9 +448,8 @@ func TestBlockAndTransactionIngestion(t *testing.T) { eventsChan := make(chan models.BlockEvents) subscriber := &mocks.EventSubscriber{} subscriber. - On("Subscribe", mock.Anything, mock.AnythingOfType("uint64")). - Return(func(ctx context.Context, latest uint64) <-chan models.BlockEvents { - assert.Equal(t, latestCadenceHeight, latest) + On("Subscribe", mock.Anything). + Return(func(ctx context.Context) <-chan models.BlockEvents { return eventsChan }). Once() diff --git a/services/ingestion/event_subscriber.go b/services/ingestion/event_subscriber.go index 24fc4dd72..d8508e68b 100644 --- a/services/ingestion/event_subscriber.go +++ b/services/ingestion/event_subscriber.go @@ -24,7 +24,7 @@ type EventSubscriber interface { // // The BlockEvents type will contain an optional error in case // the error happens, the consumer of the chanel should handle it. - Subscribe(ctx context.Context, height uint64) <-chan models.BlockEvents + Subscribe(ctx context.Context) <-chan models.BlockEvents } var _ EventSubscriber = &RPCEventSubscriber{} @@ -40,16 +40,18 @@ type RPCEventSubscriber struct { client *requester.CrossSporkClient chain flowGo.ChainID + height uint64 recovery bool recoveredEvents []flow.Event } func NewRPCEventSubscriber( + logger zerolog.Logger, client *requester.CrossSporkClient, - heartbeatInterval uint64, chainID flowGo.ChainID, - logger zerolog.Logger, + startHeight uint64, + heartbeatInterval uint64, ) *RPCEventSubscriber { logger = logger.With().Str("component", "subscriber").Logger() return &RPCEventSubscriber{ @@ -61,6 +63,7 @@ func NewRPCEventSubscriber( client: client, chain: chainID, + height: startHeight, } } @@ -69,7 +72,7 @@ func NewRPCEventSubscriber( // to listen all new events in the current spork. // // If error is encountered during backfill the subscription will end and the response chanel will be closed. -func (r *RPCEventSubscriber) Subscribe(ctx context.Context, height uint64) <-chan models.BlockEvents { +func (r *RPCEventSubscriber) Subscribe(ctx context.Context) <-chan models.BlockEvents { eventsChan := make(chan models.BlockEvents) go func() { @@ -78,13 +81,13 @@ func (r *RPCEventSubscriber) Subscribe(ctx context.Context, height uint64) <-cha }() // if the height is from the previous spork, backfill all the eventsChan from previous sporks first - if r.client.IsPastSpork(height) { + if r.client.IsPastSpork(r.height) { r.logger.Info(). - Uint64("height", height). + Uint64("height", r.height). Msg("height found in previous spork, starting to backfill") // backfill all the missed eventsChan, handling of context cancellation is done by the producer - for ev := range r.backfill(ctx, height) { + for ev := range r.backfill(ctx, r.height) { eventsChan <- ev if ev.Err != nil { @@ -93,21 +96,21 @@ func (r *RPCEventSubscriber) Subscribe(ctx context.Context, height uint64) <-cha // keep updating height, so after we are done back-filling // it will be at the first height in the current spork - height = ev.Events.CadenceHeight() + r.height = ev.Events.CadenceHeight() } // after back-filling is done, increment height by one, // so we start with the height in the current spork - height = height + 1 + r.height = r.height + 1 } r.logger.Info(). - Uint64("next-height", height). + Uint64("next-height", r.height). Msg("backfilling done, subscribe for live data") // subscribe in the current spork, handling of context cancellation is done by the producer // TODO(JanezP): I think the heartbeat interval should always be 1 here - for ev := range r.subscribe(ctx, height, access.WithHeartbeatInterval(r.HeartbeatInterval)) { + for ev := range r.subscribe(ctx, r.height, access.WithHeartbeatInterval(r.HeartbeatInterval)) { eventsChan <- ev } diff --git a/services/ingestion/event_subscriber_test.go b/services/ingestion/event_subscriber_test.go index 22865728b..19151ea28 100644 --- a/services/ingestion/event_subscriber_test.go +++ b/services/ingestion/event_subscriber_test.go @@ -43,9 +43,9 @@ func Test_Subscribing(t *testing.T) { ) require.NoError(t, err) - subscriber := NewRPCEventSubscriber(client, 100, flowGo.Previewnet, zerolog.Nop()) + subscriber := NewRPCEventSubscriber(zerolog.Nop(), client, flowGo.Previewnet, 1, 100) - events := subscriber.Subscribe(context.Background(), 1) + events := subscriber.Subscribe(context.Background()) var prevHeight uint64 @@ -83,9 +83,9 @@ func Test_MissingBlockEvent(t *testing.T) { ) require.NoError(t, err) - subscriber := NewRPCEventSubscriber(client, 100, flowGo.Previewnet, zerolog.Nop()) + subscriber := NewRPCEventSubscriber(zerolog.Nop(), client, flowGo.Previewnet, 1, 100) - events := subscriber.Subscribe(context.Background(), 1) + events := subscriber.Subscribe(context.Background()) missingHashes := make([]gethCommon.Hash, 0) @@ -160,7 +160,7 @@ func Test_MissingBlockEvent(t *testing.T) { // EVM events through the gRPC API, returns the correct data. func Test_SubscribingWithRetryOnError(t *testing.T) { endHeight := uint64(10) - sporkClients := []access.Client{} + var sporkClients []access.Client currentClient := testutils.SetupClientForRange(1, endHeight) cadenceHeight := uint64(5) @@ -185,9 +185,9 @@ func Test_SubscribingWithRetryOnError(t *testing.T) { ) require.NoError(t, err) - subscriber := NewRPCEventSubscriber(client, 100, flowGo.Previewnet, zerolog.Nop()) + subscriber := NewRPCEventSubscriber(zerolog.Nop(), client, flowGo.Previewnet, 1, 100) - events := subscriber.Subscribe(context.Background(), 1) + events := subscriber.Subscribe(context.Background()) var prevHeight uint64 @@ -214,7 +214,7 @@ func Test_SubscribingWithRetryOnError(t *testing.T) { } // this makes sure we indexed all the events - require.Equal(t, uint64(endHeight), prevHeight) + require.Equal(t, endHeight, prevHeight) } // Test that back-up fetching of EVM events is triggered when the @@ -223,7 +223,7 @@ func Test_SubscribingWithRetryOnError(t *testing.T) { // of EVM events through the gRPC API, returns duplicate EVM blocks. func Test_SubscribingWithRetryOnErrorMultipleBlocks(t *testing.T) { endHeight := uint64(10) - sporkClients := []access.Client{} + var sporkClients []access.Client currentClient := testutils.SetupClientForRange(1, endHeight) cadenceHeight := uint64(5) @@ -248,9 +248,9 @@ func Test_SubscribingWithRetryOnErrorMultipleBlocks(t *testing.T) { ) require.NoError(t, err) - subscriber := NewRPCEventSubscriber(client, 100, flowGo.Previewnet, zerolog.Nop()) + subscriber := NewRPCEventSubscriber(zerolog.Nop(), client, flowGo.Previewnet, 1, 100) - events := subscriber.Subscribe(context.Background(), 1) + events := subscriber.Subscribe(context.Background()) var prevHeight uint64 @@ -286,7 +286,7 @@ func Test_SubscribingWithRetryOnErrorMultipleBlocks(t *testing.T) { // of EVM events through the gRPC API, returns no EVM blocks. func Test_SubscribingWithRetryOnErrorEmptyBlocks(t *testing.T) { endHeight := uint64(10) - sporkClients := []access.Client{} + var sporkClients []access.Client currentClient := testutils.SetupClientForRange(1, endHeight) cadenceHeight := uint64(5) @@ -310,9 +310,9 @@ func Test_SubscribingWithRetryOnErrorEmptyBlocks(t *testing.T) { ) require.NoError(t, err) - subscriber := NewRPCEventSubscriber(client, 100, flowGo.Previewnet, zerolog.Nop()) + subscriber := NewRPCEventSubscriber(zerolog.Nop(), client, flowGo.Previewnet, 1, 100) - events := subscriber.Subscribe(context.Background(), 1) + events := subscriber.Subscribe(context.Background()) var prevHeight uint64 @@ -405,16 +405,16 @@ func setupClientForBackupEventFetching( "GetEventsForHeightRange", mock.AnythingOfType("context.backgroundCtx"), "A.b6763b4399a888c8.EVM.BlockExecuted", - uint64(cadenceHeight), - uint64(cadenceHeight), + cadenceHeight, + cadenceHeight, ).Return(evmBlockEvents, nil).Once() client.On( "GetEventsForHeightRange", mock.AnythingOfType("context.backgroundCtx"), "A.b6763b4399a888c8.EVM.TransactionExecuted", - uint64(cadenceHeight), - uint64(cadenceHeight), + cadenceHeight, + cadenceHeight, ).Return([]flow.BlockEvents{evmTxEvents}, nil).Once() client.SubscribeEventsByBlockHeightFunc = func( diff --git a/services/ingestion/mocks/EventSubscriber.go b/services/ingestion/mocks/EventSubscriber.go index 021c708d6..11b05e897 100644 --- a/services/ingestion/mocks/EventSubscriber.go +++ b/services/ingestion/mocks/EventSubscriber.go @@ -15,17 +15,17 @@ type EventSubscriber struct { mock.Mock } -// Subscribe provides a mock function with given fields: ctx, height -func (_m *EventSubscriber) Subscribe(ctx context.Context, height uint64) <-chan models.BlockEvents { - ret := _m.Called(ctx, height) +// Subscribe provides a mock function with given fields: ctx +func (_m *EventSubscriber) Subscribe(ctx context.Context) <-chan models.BlockEvents { + ret := _m.Called(ctx) if len(ret) == 0 { panic("no return value specified for Subscribe") } var r0 <-chan models.BlockEvents - if rf, ok := ret.Get(0).(func(context.Context, uint64) <-chan models.BlockEvents); ok { - r0 = rf(ctx, height) + if rf, ok := ret.Get(0).(func(context.Context) <-chan models.BlockEvents); ok { + r0 = rf(ctx) } else { if ret.Get(0) != nil { r0 = ret.Get(0).(<-chan models.BlockEvents) From bc3b99b237ef917af860d81c26a4769fa53fc6d7 Mon Sep 17 00:00:00 2001 From: Janez Podhostnik Date: Wed, 30 Oct 2024 15:04:53 +0100 Subject: [PATCH 003/174] remove heartbeat interval setting --- bootstrap/bootstrap.go | 7 +++++- cmd/run/cmd.go | 1 - config/config.go | 2 -- services/ingestion/event_subscriber.go | 28 ++++++++------------- services/ingestion/event_subscriber_test.go | 10 ++++---- 5 files changed, 22 insertions(+), 26 deletions(-) diff --git a/bootstrap/bootstrap.go b/bootstrap/bootstrap.go index df8e53a73..27bc648c5 100644 --- a/bootstrap/bootstrap.go +++ b/bootstrap/bootstrap.go @@ -117,7 +117,12 @@ func (b *Bootstrap) StartEventIngestion(ctx context.Context) error { Msg("indexing cadence height information") // create event subscriber - subscriber := ingestion.NewRPCEventSubscriber(b.logger, b.client, b.config.FlowNetworkID, latestCadenceHeight, b.config.HeartbeatInterval) + subscriber := ingestion.NewRPCEventSubscriber( + b.logger, + b.client, + b.config.FlowNetworkID, + latestCadenceHeight, + ) // initialize event ingestion engine b.events = ingestion.NewEventIngestionEngine( diff --git a/cmd/run/cmd.go b/cmd/run/cmd.go index aa90aed2a..2d9182c4e 100644 --- a/cmd/run/cmd.go +++ b/cmd/run/cmd.go @@ -268,7 +268,6 @@ func init() { Cmd.Flags().Float64Var(&cfg.StreamLimit, "stream-limit", 10, "Rate-limits the events sent to the client within one second") Cmd.Flags().Uint64Var(&cfg.RateLimit, "rate-limit", 50, "Rate-limit requests per second made by the client over any protocol (ws/http)") Cmd.Flags().StringVar(&cfg.AddressHeader, "address-header", "", "Address header that contains the client IP, this is useful when the server is behind a proxy that sets the source IP of the client. Leave empty if no proxy is used.") - Cmd.Flags().Uint64Var(&cfg.HeartbeatInterval, "heartbeat-interval", 100, "Heartbeat interval for AN event subscription") Cmd.Flags().UintVar(&cfg.CacheSize, "script-cache-size", 10000, "Cache size used for script execution in items kept in cache") Cmd.Flags().IntVar(&streamTimeout, "stream-timeout", 3, "Defines the timeout in seconds the server waits for the event to be sent to the client") Cmd.Flags().Uint64Var(&forceStartHeight, "force-start-height", 0, "Force set starting Cadence height. WARNING: This should only be used locally or for testing, never in production.") diff --git a/config/config.go b/config/config.go index 0c8b65590..13f572070 100644 --- a/config/config.go +++ b/config/config.go @@ -74,8 +74,6 @@ type Config struct { FilterExpiry time.Duration // ForceStartCadenceHeight will force set the starting Cadence height, this should be only used for testing or locally. ForceStartCadenceHeight uint64 - // HeartbeatInterval sets custom heartbeat interval for events - HeartbeatInterval uint64 // TracesBucketName sets the GCP bucket name where transaction traces are being stored. TracesBucketName string // TracesEnabled sets whether the node is supporting transaction traces. diff --git a/services/ingestion/event_subscriber.go b/services/ingestion/event_subscriber.go index d8508e68b..47da89723 100644 --- a/services/ingestion/event_subscriber.go +++ b/services/ingestion/event_subscriber.go @@ -29,13 +29,7 @@ type EventSubscriber interface { var _ EventSubscriber = &RPCEventSubscriber{} -type RPCEventSubscriberConfig struct { - HeartbeatInterval uint64 -} - type RPCEventSubscriber struct { - RPCEventSubscriberConfig - logger zerolog.Logger client *requester.CrossSporkClient @@ -51,14 +45,9 @@ func NewRPCEventSubscriber( client *requester.CrossSporkClient, chainID flowGo.ChainID, startHeight uint64, - heartbeatInterval uint64, ) *RPCEventSubscriber { logger = logger.With().Str("component", "subscriber").Logger() return &RPCEventSubscriber{ - RPCEventSubscriberConfig: RPCEventSubscriberConfig{ - HeartbeatInterval: heartbeatInterval, - }, - logger: logger, client: client, @@ -86,7 +75,7 @@ func (r *RPCEventSubscriber) Subscribe(ctx context.Context) <-chan models.BlockE Uint64("height", r.height). Msg("height found in previous spork, starting to backfill") - // backfill all the missed eventsChan, handling of context cancellation is done by the producer + // backfill all the missed events, handling of context cancellation is done by the producer for ev := range r.backfill(ctx, r.height) { eventsChan <- ev @@ -109,8 +98,7 @@ func (r *RPCEventSubscriber) Subscribe(ctx context.Context) <-chan models.BlockE Msg("backfilling done, subscribe for live data") // subscribe in the current spork, handling of context cancellation is done by the producer - // TODO(JanezP): I think the heartbeat interval should always be 1 here - for ev := range r.subscribe(ctx, r.height, access.WithHeartbeatInterval(r.HeartbeatInterval)) { + for ev := range r.subscribe(ctx, r.height) { eventsChan <- ev } @@ -124,7 +112,7 @@ func (r *RPCEventSubscriber) Subscribe(ctx context.Context) <-chan models.BlockE // // Subscribing to EVM specific events and handle any disconnection errors // as well as context cancellations. -func (r *RPCEventSubscriber) subscribe(ctx context.Context, height uint64, opts ...access.SubscribeOption) <-chan models.BlockEvents { +func (r *RPCEventSubscriber) subscribe(ctx context.Context, height uint64) <-chan models.BlockEvents { eventsChan := make(chan models.BlockEvents) _, err := r.client.GetBlockHeaderByHeight(ctx, height) @@ -134,7 +122,13 @@ func (r *RPCEventSubscriber) subscribe(ctx context.Context, height uint64, opts return eventsChan } - eventStream, errChan, err := r.client.SubscribeEventsByBlockHeight(ctx, height, blocksFilter(r.chain), opts...) + // we always use heartbeat interval of 1 to have the least amount of delay from the access node + eventStream, errChan, err := r.client.SubscribeEventsByBlockHeight( + ctx, + height, + blocksFilter(r.chain), + access.WithHeartbeatInterval(1), + ) if err != nil { eventsChan <- models.NewBlockEventsError( fmt.Errorf("failed to subscribe to events by block height: %d, with: %w", height, err), @@ -230,7 +224,7 @@ func (r *RPCEventSubscriber) backfill(ctx context.Context, height uint64) <-chan Uint64("last-spork-height", latestHeight). Msg("backfilling spork") - for ev := range r.subscribe(ctx, height, access.WithHeartbeatInterval(1)) { + for ev := range r.subscribe(ctx, height) { eventsChan <- ev if ev.Err != nil { diff --git a/services/ingestion/event_subscriber_test.go b/services/ingestion/event_subscriber_test.go index 19151ea28..04626af21 100644 --- a/services/ingestion/event_subscriber_test.go +++ b/services/ingestion/event_subscriber_test.go @@ -43,7 +43,7 @@ func Test_Subscribing(t *testing.T) { ) require.NoError(t, err) - subscriber := NewRPCEventSubscriber(zerolog.Nop(), client, flowGo.Previewnet, 1, 100) + subscriber := NewRPCEventSubscriber(zerolog.Nop(), client, flowGo.Previewnet, 1) events := subscriber.Subscribe(context.Background()) @@ -83,7 +83,7 @@ func Test_MissingBlockEvent(t *testing.T) { ) require.NoError(t, err) - subscriber := NewRPCEventSubscriber(zerolog.Nop(), client, flowGo.Previewnet, 1, 100) + subscriber := NewRPCEventSubscriber(zerolog.Nop(), client, flowGo.Previewnet, 1) events := subscriber.Subscribe(context.Background()) @@ -185,7 +185,7 @@ func Test_SubscribingWithRetryOnError(t *testing.T) { ) require.NoError(t, err) - subscriber := NewRPCEventSubscriber(zerolog.Nop(), client, flowGo.Previewnet, 1, 100) + subscriber := NewRPCEventSubscriber(zerolog.Nop(), client, flowGo.Previewnet, 1) events := subscriber.Subscribe(context.Background()) @@ -248,7 +248,7 @@ func Test_SubscribingWithRetryOnErrorMultipleBlocks(t *testing.T) { ) require.NoError(t, err) - subscriber := NewRPCEventSubscriber(zerolog.Nop(), client, flowGo.Previewnet, 1, 100) + subscriber := NewRPCEventSubscriber(zerolog.Nop(), client, flowGo.Previewnet, 1) events := subscriber.Subscribe(context.Background()) @@ -310,7 +310,7 @@ func Test_SubscribingWithRetryOnErrorEmptyBlocks(t *testing.T) { ) require.NoError(t, err) - subscriber := NewRPCEventSubscriber(zerolog.Nop(), client, flowGo.Previewnet, 1, 100) + subscriber := NewRPCEventSubscriber(zerolog.Nop(), client, flowGo.Previewnet, 1) events := subscriber.Subscribe(context.Background()) From 30fe3248d1af34ba09b696eb2acfe296e71fb465 Mon Sep 17 00:00:00 2001 From: Ardit Marku Date: Fri, 25 Oct 2024 16:12:27 +0300 Subject: [PATCH 004/174] Aggregate and expose EVM-related event payloads --- models/block.go | 6 +++--- models/block_test.go | 4 ++-- models/events.go | 29 +++++++++++++++++++++++------ models/events_test.go | 14 +++++++++++++- models/receipt_test.go | 2 +- models/transaction.go | 29 +++++++++++++++++++++++------ models/transaction_test.go | 8 ++++---- 7 files changed, 69 insertions(+), 23 deletions(-) diff --git a/models/block.go b/models/block.go index 8ed43ea26..5f312f361 100644 --- a/models/block.go +++ b/models/block.go @@ -71,10 +71,10 @@ func (b *Block) Hash() (gethCommon.Hash, error) { // decodeBlockEvent takes a cadence event that contains executed block payload and // decodes it into the Block type. -func decodeBlockEvent(event cadence.Event) (*Block, error) { +func decodeBlockEvent(event cadence.Event) (*Block, *events.BlockEventPayload, error) { payload, err := events.DecodeBlockEventPayload(event) if err != nil { - return nil, fmt.Errorf( + return nil, nil, fmt.Errorf( "failed to Cadence-decode EVM block event [%s]: %w", event.String(), err, @@ -102,7 +102,7 @@ func decodeBlockEvent(event cadence.Event) (*Block, error) { PrevRandao: payload.PrevRandao, }, FixedHash: fixedHash, - }, nil + }, payload, nil } // blockV0 is the block format, prior to adding the PrevRandao field. diff --git a/models/block_test.go b/models/block_test.go index f86167986..0dfee6f50 100644 --- a/models/block_test.go +++ b/models/block_test.go @@ -112,7 +112,7 @@ func Test_DecodeBlockExecutedEvent(t *testing.T) { encEv, err := ev.Payload.ToCadence(flowGo.Previewnet) require.NoError(t, err) - decBlock, err := decodeBlockEvent(encEv) + decBlock, _, err := decodeBlockEvent(encEv) require.NoError(t, err) assert.Equal(t, decBlock, block) @@ -150,7 +150,7 @@ func Test_DecodingLegacyBlockExecutedEvent(t *testing.T) { hashToCadenceArrayValue(block.TransactionHashRoot), }).WithType(eventType) - b, err := decodeBlockEvent(legacyEvent) + b, _, err := decodeBlockEvent(legacyEvent) require.NoError(t, err) require.Equal(t, block.ParentBlockHash, b.ParentBlockHash) diff --git a/models/events.go b/models/events.go index 53af03c6a..b8ad5a539 100644 --- a/models/events.go +++ b/models/events.go @@ -36,10 +36,12 @@ func isTransactionExecutedEvent(event cadence.Event) bool { // CadenceEvents contains Flow emitted events containing one or zero evm block executed event, // and multiple or zero evm transaction events. type CadenceEvents struct { - events flow.BlockEvents // Flow events for a specific flow block - block *Block // EVM block (at most one per Flow block) - transactions []Transaction // transactions in the EVM block - receipts []*Receipt // receipts for transactions + events flow.BlockEvents // Flow events for a specific flow block + block *Block // EVM block (at most one per Flow block) + blockEventPayload *events.BlockEventPayload // EVM.BlockExecuted event payload (at most one per Flow block) + transactions []Transaction // transactions in the EVM block + txEventPayloads []events.TransactionEventPayload // EVM.TransactionExecuted event payloads + receipts []*Receipt // receipts for transactions } // NewCadenceEvents decodes the events into evm types. @@ -111,22 +113,24 @@ func decodeCadenceEvents(events flow.BlockEvents) (*CadenceEvents, error) { return nil, fmt.Errorf("EVM block was already set for Flow block: %d", events.Height) } - block, err := decodeBlockEvent(val) + block, blockEventPayload, err := decodeBlockEvent(val) if err != nil { return nil, err } e.block = block + e.blockEventPayload = blockEventPayload continue } if isTransactionExecutedEvent(val) { - tx, receipt, err := decodeTransactionEvent(val) + tx, receipt, txEventPayload, err := decodeTransactionEvent(val) if err != nil { return nil, err } e.transactions = append(e.transactions, tx) + e.txEventPayloads = append(e.txEventPayloads, *txEventPayload) e.receipts = append(e.receipts, receipt) } } @@ -162,12 +166,25 @@ func (c *CadenceEvents) Block() *Block { return c.block } +// BlockEventPayload returns the EVM.BlockExecuted event payload. If the Flow block +// events do not contain an EVM block, the return value is nil. +func (c *CadenceEvents) BlockEventPayload() *events.BlockEventPayload { + return c.blockEventPayload +} + // Transactions included in the EVM block, if event doesn't // contain EVM transactions the return value is nil. func (c *CadenceEvents) Transactions() []Transaction { return c.transactions } +// TxEventPayloads returns the EVM.TransactionExecuted event payloads for the +// current EVM block. If the Flow block events do not contain any EVM transactions +// the return value is nil. +func (c *CadenceEvents) TxEventPayloads() []events.TransactionEventPayload { + return c.txEventPayloads +} + // Receipts included in the EVM block, if event doesn't // contain EVM transactions the return value is nil. func (c *CadenceEvents) Receipts() []*Receipt { diff --git a/models/events_test.go b/models/events_test.go index 241031501..e79f03d33 100644 --- a/models/events_test.go +++ b/models/events_test.go @@ -194,7 +194,7 @@ func TestCadenceEvents_Block(t *testing.T) { } // generate single block - _, blockEvent, err := newBlock(1, hashes) + block, blockEvent, err := newBlock(1, hashes) require.NoError(t, err) blockEvent.TransactionIndex = 4 blockEvent.EventIndex = 0 @@ -216,6 +216,12 @@ func TestCadenceEvents_Block(t *testing.T) { cdcEvents.events.Events, ) + // assert we have collected the EVM.BlockExecuted event payload + blockEventPayload := cdcEvents.BlockEventPayload() + blockHash, err := block.Hash() + require.NoError(t, err) + assert.Equal(t, blockHash, blockEventPayload.Hash) + // assert that EVM transactions & receipts are sorted by their // TransactionIndex field for i := 0; i < txCount; i++ { @@ -223,6 +229,12 @@ func TestCadenceEvents_Block(t *testing.T) { receipt := cdcEvents.receipts[i] assert.Equal(t, tx.Hash(), receipt.TxHash) assert.Equal(t, uint(i), receipt.TransactionIndex) + + // assert we have collected the EVM.TransactionExecuted event payloads + // in their correct order. + txEventPayload := cdcEvents.TxEventPayloads()[i] + assert.Equal(t, tx.Hash(), txEventPayload.Hash) + assert.Equal(t, blockEventPayload.Height, txEventPayload.BlockHeight) } }) } diff --git a/models/receipt_test.go b/models/receipt_test.go index 4bc2e0abe..adae146f8 100644 --- a/models/receipt_test.go +++ b/models/receipt_test.go @@ -10,7 +10,7 @@ import ( func Test_DecodeReceipts(t *testing.T) { cdcEv, rec := createTestEvent(t, evmTxBinary) - _, receipt, err := decodeTransactionEvent(cdcEv) + _, receipt, _, err := decodeTransactionEvent(cdcEv) require.NoError(t, err) for i, l := range rec.Logs { diff --git a/models/transaction.go b/models/transaction.go index 8e19474ee..c4919497e 100644 --- a/models/transaction.go +++ b/models/transaction.go @@ -167,10 +167,19 @@ func (tc TransactionCall) MarshalBinary() ([]byte, error) { // decodeTransactionEvent takes a cadence event for transaction executed // and decodes its payload into a Transaction interface and a Receipt. // The concrete type will be either a TransactionCall or a DirectCall. -func decodeTransactionEvent(event cadence.Event) (Transaction, *Receipt, error) { +func decodeTransactionEvent(event cadence.Event) ( + Transaction, + *Receipt, + *events.TransactionEventPayload, + error, +) { txEvent, err := events.DecodeTransactionEventPayload(event) if err != nil { - return nil, nil, fmt.Errorf("failed to Cadence decode transaction event [%s]: %w", event.String(), err) + return nil, nil, nil, fmt.Errorf( + "failed to Cadence decode transaction event [%s]: %w", + event.String(), + err, + ) } gethReceipt := &gethTypes.Receipt{ @@ -186,7 +195,7 @@ func decodeTransactionEvent(event cadence.Event) (Transaction, *Receipt, error) if len(txEvent.Logs) > 0 { err = rlp.Decode(bytes.NewReader(txEvent.Logs), &gethReceipt.Logs) if err != nil { - return nil, nil, fmt.Errorf("failed to RLP-decode logs: %w", err) + return nil, nil, nil, fmt.Errorf("failed to RLP-decode logs: %w", err) } } @@ -211,19 +220,27 @@ func decodeTransactionEvent(event cadence.Event) (Transaction, *Receipt, error) if txEvent.TransactionType == types.DirectCallTxType { directCall, err := types.DirectCallFromEncoded(txEvent.Payload) if err != nil { - return nil, nil, fmt.Errorf("failed to RLP-decode direct call [%x]: %w", txEvent.Payload, err) + return nil, nil, nil, fmt.Errorf( + "failed to RLP-decode direct call [%x]: %w", + txEvent.Payload, + err, + ) } tx = DirectCall{DirectCall: directCall} } else { gethTx := &gethTypes.Transaction{} if err := gethTx.UnmarshalBinary(txEvent.Payload); err != nil { - return nil, nil, fmt.Errorf("failed to RLP-decode transaction [%x]: %w", txEvent.Payload, err) + return nil, nil, nil, fmt.Errorf( + "failed to RLP-decode transaction [%x]: %w", + txEvent.Payload, + err, + ) } receipt.EffectiveGasPrice = gethTx.EffectiveGasTipValue(nil) tx = TransactionCall{Transaction: gethTx} } - return tx, receipt, nil + return tx, receipt, txEvent, nil } func UnmarshalTransaction(value []byte) (Transaction, error) { diff --git a/models/transaction_test.go b/models/transaction_test.go index 3e11e1261..09e693f13 100644 --- a/models/transaction_test.go +++ b/models/transaction_test.go @@ -85,7 +85,7 @@ func createTestEvent(t *testing.T, txBinary string) (cadence.Event, *types.Resul func Test_DecodeEVMTransaction(t *testing.T) { cdcEv, _ := createTestEvent(t, evmTxBinary) - decTx, _, err := decodeTransactionEvent(cdcEv) + decTx, _, _, err := decodeTransactionEvent(cdcEv) require.NoError(t, err) require.IsType(t, TransactionCall{}, decTx) @@ -131,7 +131,7 @@ func Test_DecodeEVMTransaction(t *testing.T) { func Test_DecodeDirectCall(t *testing.T) { cdcEv, _ := createTestEvent(t, directCallBinary) - decTx, _, err := decodeTransactionEvent(cdcEv) + decTx, _, _, err := decodeTransactionEvent(cdcEv) require.NoError(t, err) require.IsType(t, DirectCall{}, decTx) @@ -179,7 +179,7 @@ func Test_UnmarshalTransaction(t *testing.T) { cdcEv, _ := createTestEvent(t, evmTxBinary) - tx, _, err := decodeTransactionEvent(cdcEv) + tx, _, _, err := decodeTransactionEvent(cdcEv) require.NoError(t, err) encodedTx, err := tx.MarshalBinary() @@ -233,7 +233,7 @@ func Test_UnmarshalTransaction(t *testing.T) { cdcEv, _ := createTestEvent(t, directCallBinary) - tx, _, err := decodeTransactionEvent(cdcEv) + tx, _, _, err := decodeTransactionEvent(cdcEv) require.NoError(t, err) encodedTx, err := tx.MarshalBinary() From eaf0db2dbd4d4bca6a3d8087a5ce9d6abbc68f52 Mon Sep 17 00:00:00 2001 From: Ardit Marku Date: Mon, 28 Oct 2024 14:54:50 +0200 Subject: [PATCH 005/174] Implement BlockProvider for block snapshotting --- bootstrap/bootstrap.go | 27 ++ services/ingestion/engine.go | 12 + services/ingestion/engine_test.go | 6 + services/replayer/blocks_provider.go | 112 +++++++++ services/replayer/blocks_provider_test.go | 291 ++++++++++++++++++++++ storage/mocks/mocks.go | 13 +- 6 files changed, 457 insertions(+), 4 deletions(-) create mode 100644 services/replayer/blocks_provider.go create mode 100644 services/replayer/blocks_provider_test.go diff --git a/bootstrap/bootstrap.go b/bootstrap/bootstrap.go index 27bc648c5..17da8425c 100644 --- a/bootstrap/bootstrap.go +++ b/bootstrap/bootstrap.go @@ -2,6 +2,7 @@ package bootstrap import ( "context" + "encoding/json" "errors" "fmt" "math" @@ -11,6 +12,7 @@ import ( "github.com/onflow/flow-go-sdk/access/grpc" "github.com/onflow/flow-go-sdk/crypto" gethTypes "github.com/onflow/go-ethereum/core/types" + "github.com/onflow/go-ethereum/eth/tracers" "github.com/rs/zerolog" "github.com/sethvargo/go-limiter/memorystore" grpcOpts "google.golang.org/grpc" @@ -21,10 +23,20 @@ import ( "github.com/onflow/flow-evm-gateway/models" errs "github.com/onflow/flow-evm-gateway/models/errors" "github.com/onflow/flow-evm-gateway/services/ingestion" + "github.com/onflow/flow-evm-gateway/services/replayer" "github.com/onflow/flow-evm-gateway/services/requester" "github.com/onflow/flow-evm-gateway/services/traces" "github.com/onflow/flow-evm-gateway/storage" "github.com/onflow/flow-evm-gateway/storage/pebble" + + // this import is needed for side-effects, because the + // tracers.DefaultDirectory is relying on the init function + _ "github.com/onflow/go-ethereum/eth/tracers/native" +) + +const ( + callTracerConfig = `{ "onlyTopCall": true }` + callTracerName = "callTracer" ) type Storages struct { @@ -124,9 +136,24 @@ func (b *Bootstrap) StartEventIngestion(ctx context.Context) error { latestCadenceHeight, ) + tracer, err := tracers.DefaultDirectory.New( + callTracerName, + &tracers.Context{}, + json.RawMessage(callTracerConfig), + ) + if err != nil { + return err + } + blocksProvider := replayer.NewBlocksProvider( + b.storages.Blocks, + b.config.FlowNetworkID, + tracer, + ) + // initialize event ingestion engine b.events = ingestion.NewEventIngestionEngine( subscriber, + blocksProvider, b.storages.Storage, b.storages.Blocks, b.storages.Receipts, diff --git a/services/ingestion/engine.go b/services/ingestion/engine.go index 62bde4493..6ca1d027a 100644 --- a/services/ingestion/engine.go +++ b/services/ingestion/engine.go @@ -11,6 +11,7 @@ import ( "github.com/onflow/flow-evm-gateway/metrics" "github.com/onflow/flow-evm-gateway/models" + "github.com/onflow/flow-evm-gateway/services/replayer" "github.com/onflow/flow-evm-gateway/storage" "github.com/onflow/flow-evm-gateway/storage/pebble" ) @@ -35,6 +36,7 @@ type Engine struct { *models.EngineStatus subscriber EventSubscriber + blocksProvider *replayer.BlocksProvider store *pebble.Storage blocks storage.BlockIndexer receipts storage.ReceiptIndexer @@ -49,6 +51,7 @@ type Engine struct { func NewEventIngestionEngine( subscriber EventSubscriber, + blocksProvider *replayer.BlocksProvider, store *pebble.Storage, blocks storage.BlockIndexer, receipts storage.ReceiptIndexer, @@ -65,6 +68,7 @@ func NewEventIngestionEngine( EngineStatus: models.NewEngineStatus(), subscriber: subscriber, + blocksProvider: blocksProvider, store: store, blocks: blocks, receipts: receipts, @@ -179,6 +183,14 @@ func (e *Engine) processEvents(events *models.CadenceEvents) error { return fmt.Errorf("failed to index receipts for block %d event: %w", events.Block().Height, err) } + if err := e.blocksProvider.OnBlockReceived(events.Block()); err != nil { + return fmt.Errorf( + "failed to call OnBlockReceived for block %d, with: %w", + events.Block().Height, + err, + ) + } + if err := batch.Commit(pebbleDB.Sync); err != nil { return fmt.Errorf("failed to commit indexed data for Cadence block %d: %w", events.CadenceHeight(), err) } diff --git a/services/ingestion/engine_test.go b/services/ingestion/engine_test.go index 0fddb5e73..ff87ea6fb 100644 --- a/services/ingestion/engine_test.go +++ b/services/ingestion/engine_test.go @@ -12,6 +12,7 @@ import ( "github.com/onflow/flow-evm-gateway/metrics" "github.com/onflow/flow-evm-gateway/services/ingestion/mocks" + "github.com/onflow/flow-evm-gateway/services/replayer" "github.com/onflow/flow-evm-gateway/storage/pebble" "github.com/onflow/cadence" @@ -63,6 +64,7 @@ func TestSerialBlockIngestion(t *testing.T) { engine := NewEventIngestionEngine( subscriber, + replayer.NewBlocksProvider(blocks, flowGo.Emulator, nil), store, blocks, receipts, @@ -143,6 +145,7 @@ func TestSerialBlockIngestion(t *testing.T) { engine := NewEventIngestionEngine( subscriber, + replayer.NewBlocksProvider(blocks, flowGo.Emulator, nil), store, blocks, receipts, @@ -258,6 +261,7 @@ func TestBlockAndTransactionIngestion(t *testing.T) { engine := NewEventIngestionEngine( subscriber, + replayer.NewBlocksProvider(blocks, flowGo.Emulator, nil), store, blocks, receipts, @@ -361,6 +365,7 @@ func TestBlockAndTransactionIngestion(t *testing.T) { engine := NewEventIngestionEngine( subscriber, + replayer.NewBlocksProvider(blocks, flowGo.Emulator, nil), store, blocks, receipts, @@ -456,6 +461,7 @@ func TestBlockAndTransactionIngestion(t *testing.T) { engine := NewEventIngestionEngine( subscriber, + replayer.NewBlocksProvider(blocks, flowGo.Emulator, nil), store, blocks, receipts, diff --git a/services/replayer/blocks_provider.go b/services/replayer/blocks_provider.go new file mode 100644 index 000000000..f87d398da --- /dev/null +++ b/services/replayer/blocks_provider.go @@ -0,0 +1,112 @@ +package replayer + +import ( + "fmt" + + "github.com/onflow/flow-evm-gateway/models" + "github.com/onflow/flow-evm-gateway/storage" + evmTypes "github.com/onflow/flow-go/fvm/evm/types" + flowGo "github.com/onflow/flow-go/model/flow" + gethCommon "github.com/onflow/go-ethereum/common" + "github.com/onflow/go-ethereum/eth/tracers" +) + +type blockSnapshot struct { + *BlocksProvider + block models.Block +} + +var _ evmTypes.BlockSnapshot = (*blockSnapshot)(nil) + +func (bs *blockSnapshot) BlockContext() (evmTypes.BlockContext, error) { + return evmTypes.BlockContext{ + ChainID: evmTypes.EVMChainIDFromFlowChainID(bs.chainID), + BlockNumber: bs.block.Height, + BlockTimestamp: bs.block.Timestamp, + DirectCallBaseGasUsage: evmTypes.DefaultDirectCallBaseGasUsage, + DirectCallGasPrice: evmTypes.DefaultDirectCallGasPrice, + GasFeeCollector: evmTypes.CoinbaseAddress, + GetHashFunc: func(n uint64) gethCommon.Hash { + // For block heights greater than or equal to the current, + // return an empty block hash. + if n >= bs.block.Height { + return gethCommon.Hash{} + } + // If the given block height, is more than 256 blocks + // in the past, return an empty block hash. + if bs.block.Height-n > 256 { + return gethCommon.Hash{} + } + + block, err := bs.blocks.GetByHeight(n) + if err != nil { + return gethCommon.Hash{} + } + blockHash, err := block.Hash() + if err != nil { + return gethCommon.Hash{} + } + + return blockHash + }, + Random: bs.block.PrevRandao, + Tracer: bs.tracer, + }, nil +} + +type BlocksProvider struct { + blocks storage.BlockIndexer + chainID flowGo.ChainID + tracer *tracers.Tracer + latestBlock *models.Block +} + +var _ evmTypes.BlockSnapshotProvider = (*BlocksProvider)(nil) + +func NewBlocksProvider( + blocks storage.BlockIndexer, + chainID flowGo.ChainID, + tracer *tracers.Tracer, +) *BlocksProvider { + return &BlocksProvider{ + blocks: blocks, + chainID: chainID, + tracer: tracer, + } +} + +func (bp *BlocksProvider) OnBlockReceived(block *models.Block) error { + if bp.latestBlock != nil && bp.latestBlock.Height != (block.Height-1) { + return fmt.Errorf( + "received new block: %d, non-sequential of latest block: %d", + block.Height, + bp.latestBlock.Height, + ) + } + + bp.latestBlock = block + + return nil +} + +func (bp *BlocksProvider) GetSnapshotAt(height uint64) ( + evmTypes.BlockSnapshot, + error, +) { + if bp.latestBlock != nil && bp.latestBlock.Height == height { + return &blockSnapshot{ + BlocksProvider: bp, + block: *bp.latestBlock, + }, nil + } + + block, err := bp.blocks.GetByHeight(height) + if err != nil { + return nil, err + } + + return &blockSnapshot{ + BlocksProvider: bp, + block: *block, + }, nil +} diff --git a/services/replayer/blocks_provider_test.go b/services/replayer/blocks_provider_test.go new file mode 100644 index 000000000..b3becf71b --- /dev/null +++ b/services/replayer/blocks_provider_test.go @@ -0,0 +1,291 @@ +package replayer + +import ( + "testing" + + "github.com/goccy/go-json" + "github.com/onflow/flow-evm-gateway/config" + "github.com/onflow/flow-evm-gateway/models" + "github.com/onflow/flow-evm-gateway/storage" + "github.com/onflow/flow-evm-gateway/storage/mocks" + "github.com/onflow/flow-evm-gateway/storage/pebble" + "github.com/onflow/flow-go-sdk" + evmTypes "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/eth/tracers" + "github.com/rs/zerolog" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + + // this import is needed for side-effects, because the + // tracers.DefaultDirectory is relying on the init function + _ "github.com/onflow/go-ethereum/eth/tracers/native" +) + +func TestOnBlockReceived(t *testing.T) { + + t.Run("without latest block", func(t *testing.T) { + blocks := setupBlocksDB(t) + blocksProvider := NewBlocksProvider(blocks, flowGo.Emulator, nil) + + block := mocks.NewBlock(1) + err := blocksProvider.OnBlockReceived(block) + require.NoError(t, err) + }) + + t.Run("with new block non-sequential to latest block", func(t *testing.T) { + blocks := setupBlocksDB(t) + blocksProvider := NewBlocksProvider(blocks, flowGo.Emulator, nil) + + block1 := mocks.NewBlock(1) + err := blocksProvider.OnBlockReceived(block1) + require.NoError(t, err) + + block2 := mocks.NewBlock(3) + err = blocksProvider.OnBlockReceived(block2) + require.Error(t, err) + assert.ErrorContains( + t, + err, + "received new block: 3, non-sequential of latest block: 1", + ) + }) + + t.Run("with new block non-sequential to latest block", func(t *testing.T) { + blocks := setupBlocksDB(t) + blocksProvider := NewBlocksProvider(blocks, flowGo.Emulator, nil) + + block1 := mocks.NewBlock(10) + err := blocksProvider.OnBlockReceived(block1) + require.NoError(t, err) + + block2 := mocks.NewBlock(11) + err = blocksProvider.OnBlockReceived(block2) + require.NoError(t, err) + }) +} + +func TestBlockContext(t *testing.T) { + + t.Run("for latest block", func(t *testing.T) { + blocks := setupBlocksDB(t) + tracer := newCallTracer(t) + blocksProvider := NewBlocksProvider(blocks, flowGo.Emulator, tracer) + + block := mocks.NewBlock(1) + err := blocksProvider.OnBlockReceived(block) + require.NoError(t, err) + + blockSnapshopt, err := blocksProvider.GetSnapshotAt(block.Height) + require.NoError(t, err) + + blockContext, err := blockSnapshopt.BlockContext() + require.NoError(t, err) + + assert.Equal(t, evmTypes.FlowEVMPreviewNetChainID, blockContext.ChainID) + assert.Equal(t, block.Height, blockContext.BlockNumber) + assert.Equal(t, block.Timestamp, blockContext.BlockTimestamp) + assert.Equal(t, evmTypes.DefaultDirectCallBaseGasUsage, blockContext.DirectCallBaseGasUsage) + assert.Equal(t, evmTypes.DefaultDirectCallGasPrice, blockContext.DirectCallGasPrice) + assert.Equal(t, evmTypes.CoinbaseAddress, blockContext.GasFeeCollector) + blockHash := blockContext.GetHashFunc(block.Height) + assert.Equal(t, common.Hash{}, blockHash) + assert.Equal(t, block.PrevRandao, blockContext.Random) + assert.Equal(t, tracer, blockContext.Tracer) + }) +} + +func TestGetHashFunc(t *testing.T) { + blocks := setupBlocksDB(t) + missingHeight := uint64(100) + + blockMapping := make(map[uint64]*models.Block, 0) + for i := uint64(1); i <= 300; i++ { + // simulate a missing block + if i == missingHeight { + continue + } + + block := mocks.NewBlock(i) + err := blocks.Store(i, flow.Identifier{0x1}, block, nil) + require.NoError(t, err) + blockMapping[i] = block + } + + t.Run("with requested height >= latest block height", func(t *testing.T) { + tracer := newCallTracer(t) + blocksProvider := NewBlocksProvider(blocks, flowGo.Emulator, tracer) + + latestBlock := blockMapping[200] + err := blocksProvider.OnBlockReceived(latestBlock) + require.NoError(t, err) + + blockSnapshopt, err := blocksProvider.GetSnapshotAt(latestBlock.Height) + require.NoError(t, err) + + blockContext, err := blockSnapshopt.BlockContext() + require.NoError(t, err) + require.Equal(t, latestBlock.Height, blockContext.BlockNumber) + + // GetHashFunc should return empty block hash for block heights >= latest + blockHash := blockContext.GetHashFunc(latestBlock.Height) + assert.Equal(t, common.Hash{}, blockHash) + + blockHash = blockContext.GetHashFunc(latestBlock.Height + 1) + assert.Equal(t, common.Hash{}, blockHash) + }) + + t.Run("with requested height within 256 block height range", func(t *testing.T) { + tracer := newCallTracer(t) + blocksProvider := NewBlocksProvider(blocks, flowGo.Emulator, tracer) + + latestBlock := blockMapping[257] + err := blocksProvider.OnBlockReceived(latestBlock) + require.NoError(t, err) + + blockSnapshopt, err := blocksProvider.GetSnapshotAt(latestBlock.Height) + require.NoError(t, err) + + blockContext, err := blockSnapshopt.BlockContext() + require.NoError(t, err) + require.Equal(t, latestBlock.Height, blockContext.BlockNumber) + + blockHash := blockContext.GetHashFunc(latestBlock.Height - 256) + expectedBlock := blockMapping[latestBlock.Height-256] + expectedHash, err := expectedBlock.Hash() + require.NoError(t, err) + assert.Equal(t, expectedHash, blockHash) + }) + + t.Run("with requested height outside the 256 block height range", func(t *testing.T) { + tracer := newCallTracer(t) + blocksProvider := NewBlocksProvider(blocks, flowGo.Emulator, tracer) + + latestBlock := blockMapping[260] + err := blocksProvider.OnBlockReceived(latestBlock) + require.NoError(t, err) + + blockSnapshopt, err := blocksProvider.GetSnapshotAt(latestBlock.Height) + require.NoError(t, err) + + blockContext, err := blockSnapshopt.BlockContext() + require.NoError(t, err) + require.Equal(t, latestBlock.Height, blockContext.BlockNumber) + + blockHash := blockContext.GetHashFunc(latestBlock.Height - 259) + assert.Equal(t, common.Hash{}, blockHash) + }) + + t.Run("with requested height missing from Blocks DB", func(t *testing.T) { + tracer := newCallTracer(t) + blocksProvider := NewBlocksProvider(blocks, flowGo.Emulator, tracer) + + latestBlock := blockMapping[260] + err := blocksProvider.OnBlockReceived(latestBlock) + require.NoError(t, err) + + blockSnapshopt, err := blocksProvider.GetSnapshotAt(latestBlock.Height) + require.NoError(t, err) + + blockContext, err := blockSnapshopt.BlockContext() + require.NoError(t, err) + require.Equal(t, latestBlock.Height, blockContext.BlockNumber) + + blockHash := blockContext.GetHashFunc(missingHeight) + assert.Equal(t, common.Hash{}, blockHash) + }) +} + +func TestGetSnapshotAt(t *testing.T) { + + t.Run("for latest block", func(t *testing.T) { + blocks := setupBlocksDB(t) + tracer := newCallTracer(t) + blocksProvider := NewBlocksProvider(blocks, flowGo.Emulator, tracer) + + block := mocks.NewBlock(1) + err := blocksProvider.OnBlockReceived(block) + require.NoError(t, err) + + blockSnapshot, err := blocksProvider.GetSnapshotAt(block.Height) + require.NoError(t, err) + + blockContext, err := blockSnapshot.BlockContext() + require.NoError(t, err) + assert.Equal(t, block.Height, blockContext.BlockNumber) + assert.Equal(t, block.Timestamp, blockContext.BlockTimestamp) + assert.Equal(t, block.PrevRandao, blockContext.Random) + assert.Equal(t, tracer, blockContext.Tracer) + }) + + t.Run("for historic block", func(t *testing.T) { + blocks := setupBlocksDB(t) + tracer := newCallTracer(t) + blocksProvider := NewBlocksProvider(blocks, flowGo.Emulator, tracer) + + block1 := mocks.NewBlock(1) + err := blocks.Store(1, flow.Identifier{0x1}, block1, nil) + require.NoError(t, err) + + block2 := mocks.NewBlock(2) + err = blocksProvider.OnBlockReceived(block2) + require.NoError(t, err) + + blockSnapshot, err := blocksProvider.GetSnapshotAt(block1.Height) + require.NoError(t, err) + + blockContext, err := blockSnapshot.BlockContext() + require.NoError(t, err) + assert.Equal(t, block1.Height, blockContext.BlockNumber) + assert.Equal(t, block1.Timestamp, blockContext.BlockTimestamp) + assert.Equal(t, block1.PrevRandao, blockContext.Random) + assert.Equal(t, tracer, blockContext.Tracer) + }) + + t.Run("for missing historic block", func(t *testing.T) { + blocks := setupBlocksDB(t) + tracer := newCallTracer(t) + blocksProvider := NewBlocksProvider(blocks, flowGo.Emulator, tracer) + + // `block1` is not stored on Blocks DB + block1 := mocks.NewBlock(1) + + block2 := mocks.NewBlock(2) + err := blocksProvider.OnBlockReceived(block2) + require.NoError(t, err) + + _, err = blocksProvider.GetSnapshotAt(block1.Height) + require.Error(t, err) + assert.ErrorContains( + t, + err, + "entity not found", + ) + }) +} + +func setupBlocksDB(t *testing.T) storage.BlockIndexer { + dir := t.TempDir() + db, err := pebble.New(dir, zerolog.Nop()) + require.NoError(t, err) + + chainID := flowGo.Emulator + blocks := pebble.NewBlocks(db, chainID) + + err = blocks.InitHeights(config.EmulatorInitCadenceHeight, flow.Identifier{0x1}) + require.NoError(t, err) + + return blocks +} + +func newCallTracer(t *testing.T) *tracers.Tracer { + tracer, err := tracers.DefaultDirectory.New( + "callTracer", + &tracers.Context{}, + json.RawMessage(`{ "onlyTopCall": true }`), + ) + require.NoError(t, err) + + return tracer +} diff --git a/storage/mocks/mocks.go b/storage/mocks/mocks.go index 706e20f6b..c7be2a73a 100644 --- a/storage/mocks/mocks.go +++ b/storage/mocks/mocks.go @@ -3,6 +3,7 @@ package mocks import ( "fmt" "math/big" + "time" "github.com/onflow/flow-go/fvm/evm/types" "github.com/onflow/go-ethereum/common" @@ -20,10 +21,14 @@ func NewBlock(height uint64) *models.Block { return &models.Block{ Block: &types.Block{ - ParentBlockHash: parent, - Height: height, - TotalSupply: big.NewInt(1000), - ReceiptRoot: common.HexToHash(fmt.Sprintf("0x1337%d", height)), + ParentBlockHash: parent, + Height: height, + Timestamp: uint64(time.Now().Second()), + TotalSupply: big.NewInt(1000), + ReceiptRoot: common.HexToHash(fmt.Sprintf("0x100%d", height)), + TransactionHashRoot: common.HexToHash(fmt.Sprintf("0x200%d", height)), + TotalGasUsed: uint64(30_000), + PrevRandao: common.HexToHash(fmt.Sprintf("0x300%d", height)), }, TransactionHashes: make([]common.Hash, 0), } From d8e1146273fca3cb217f741eb8fabad202b14411 Mon Sep 17 00:00:00 2001 From: Janez Podhostnik Date: Wed, 30 Oct 2024 15:58:00 +0100 Subject: [PATCH 006/174] remove traces --- Makefile | 1 - README.md | 1 - bootstrap/bootstrap.go | 79 +------- cmd/run/cmd.go | 11 +- config/config.go | 8 - go.mod | 4 +- services/traces/downloader.go | 78 -------- services/traces/engine.go | 193 ------------------- services/traces/engine_test.go | 288 ---------------------------- services/traces/mocks/Downloader.go | 61 ------ 10 files changed, 4 insertions(+), 720 deletions(-) delete mode 100644 services/traces/downloader.go delete mode 100644 services/traces/engine.go delete mode 100644 services/traces/engine_test.go delete mode 100644 services/traces/mocks/Downloader.go diff --git a/Makefile b/Makefile index 026a75590..9205c2706 100644 --- a/Makefile +++ b/Makefile @@ -35,7 +35,6 @@ generate: mockery --dir=storage --name=TransactionIndexer --output=storage/mocks mockery --dir=storage --name=AccountIndexer --output=storage/mocks mockery --dir=storage --name=TraceIndexer --output=storage/mocks - mockery --all --dir=services/traces --output=services/traces/mocks mockery --all --dir=services/ingestion --output=services/ingestion/mocks mockery --dir=models --name=Engine --output=models/mocks diff --git a/README.md b/README.md index 393dd6396..fb14da313 100644 --- a/README.md +++ b/README.md @@ -222,7 +222,6 @@ The application can be configured using the following flags at runtime: | `stream-limit` | `10` | Rate-limit for client events sent per second | | `rate-limit` | `50` | Requests per second limit for clients over any protocol (ws/http) | | `address-header` | `""` | Header for client IP when server is behind a proxy | -| `heartbeat-interval` | `100` | Interval for AN event subscription heartbeats | | `stream-timeout` | `3` | Timeout in seconds for sending events to clients | | `force-start-height` | `0` | Force-set starting Cadence height (local/testing use only) | | `wallet-api-key` | `""` | ECDSA private key for wallet APIs (local/testing use only) | diff --git a/bootstrap/bootstrap.go b/bootstrap/bootstrap.go index 17da8425c..9398019d8 100644 --- a/bootstrap/bootstrap.go +++ b/bootstrap/bootstrap.go @@ -25,7 +25,6 @@ import ( "github.com/onflow/flow-evm-gateway/services/ingestion" "github.com/onflow/flow-evm-gateway/services/replayer" "github.com/onflow/flow-evm-gateway/services/requester" - "github.com/onflow/flow-evm-gateway/services/traces" "github.com/onflow/flow-evm-gateway/storage" "github.com/onflow/flow-evm-gateway/storage/pebble" @@ -64,7 +63,6 @@ type Bootstrap struct { server *api.Server metrics *metrics.Server events *ingestion.Engine - traces *traces.Engine profiler *api.ProfileServer } @@ -169,71 +167,6 @@ func (b *Bootstrap) StartEventIngestion(ctx context.Context) error { return nil } -func (b *Bootstrap) StartTraceDownloader(ctx context.Context) error { - l := b.logger.With().Str("component", "bootstrap-traces").Logger() - l.Info().Msg("starting engine") - - // create gcp downloader - downloader, err := traces.NewGCPDownloader(b.config.TracesBucketName, b.logger) - if err != nil { - return err - } - - // initialize trace downloader engine - b.traces = traces.NewTracesIngestionEngine( - b.publishers.Block, - b.storages.Blocks, - b.storages.Traces, - downloader, - b.logger, - b.collector, - ) - - StartEngine(ctx, b.traces, l) - - if b.config.TracesBackfillStartHeight > 0 { - startHeight := b.config.TracesBackfillStartHeight - if _, err := b.storages.Blocks.GetByHeight(startHeight); err != nil { - return fmt.Errorf("failed to get provided start height %d in db: %w", startHeight, err) - } - - cadenceStartHeight, err := b.storages.Blocks.GetCadenceHeight(startHeight) - if err != nil { - return fmt.Errorf("failed to get cadence height for backfill start height %d: %w", startHeight, err) - } - - if cadenceStartHeight < b.config.InitCadenceHeight { - b.logger.Warn(). - Uint64("evm-start-height", startHeight). - Uint64("cadence-start-height", cadenceStartHeight). - Uint64("init-cadence-height", b.config.InitCadenceHeight). - Msg("backfill start height is before initial cadence height. data may be missing from configured traces bucket") - } - - endHeight := b.config.TracesBackfillEndHeight - if endHeight == 0 { - endHeight, err = b.storages.Blocks.LatestEVMHeight() - if err != nil { - return fmt.Errorf("failed to get latest EVM height: %w", err) - } - } else if _, err := b.storages.Blocks.GetByHeight(endHeight); err != nil { - return fmt.Errorf("failed to get provided end height %d in db: %w", endHeight, err) - } - - go b.traces.Backfill(startHeight, endHeight) - } - - return nil -} - -func (b *Bootstrap) StopTraceDownloader() { - if b.traces == nil { - return - } - b.logger.Warn().Msg("stopping trace downloader engine") - b.traces.Stop() -} - func (b *Bootstrap) StopEventIngestion() { if b.events == nil { return @@ -336,10 +269,7 @@ func (b *Bootstrap) StartAPIServer(ctx context.Context) error { ratelimiter, ) - var debugAPI *api.DebugAPI - if b.config.TracesEnabled { - debugAPI = api.NewDebugAPI(b.storages.Traces, b.storages.Blocks, b.logger, b.collector) - } + var debugAPI = api.NewDebugAPI(b.storages.Traces, b.storages.Blocks, b.logger, b.collector) var walletAPI *api.WalletAPI if b.config.WalletEnabled { @@ -562,12 +492,6 @@ func Run(ctx context.Context, cfg *config.Config, ready chan struct{}) error { return err } - if cfg.TracesEnabled { - if err := boot.StartTraceDownloader(ctx); err != nil { - return fmt.Errorf("failed to start trace downloader engine: %w", err) - } - } - if err := boot.StartEventIngestion(ctx); err != nil { return fmt.Errorf("failed to start event ingestion engine: %w", err) } @@ -593,7 +517,6 @@ func Run(ctx context.Context, cfg *config.Config, ready chan struct{}) error { boot.StopEventIngestion() boot.StopMetricsServer() - boot.StopTraceDownloader() boot.StopAPIServer() return nil diff --git a/cmd/run/cmd.go b/cmd/run/cmd.go index 2d9182c4e..332ae7c01 100644 --- a/cmd/run/cmd.go +++ b/cmd/run/cmd.go @@ -72,7 +72,7 @@ func parseConfigFromFlags() error { if g, ok := new(big.Int).SetString(gas, 10); ok { cfg.GasPrice = g - } else if !ok { + } else { return fmt.Errorf("invalid gas price") } @@ -201,12 +201,6 @@ func parseConfigFromFlags() error { cfg.ForceStartCadenceHeight = forceStartHeight } - cfg.TracesEnabled = cfg.TracesBucketName != "" - - if cfg.TracesBackfillStartHeight > 0 && cfg.TracesBackfillEndHeight > 0 && cfg.TracesBackfillStartHeight > cfg.TracesBackfillEndHeight { - return fmt.Errorf("traces backfill start height must be less than the end height") - } - if walletKey != "" { k, err := gethCrypto.HexToECDSA(walletKey) if err != nil { @@ -272,9 +266,6 @@ func init() { Cmd.Flags().IntVar(&streamTimeout, "stream-timeout", 3, "Defines the timeout in seconds the server waits for the event to be sent to the client") Cmd.Flags().Uint64Var(&forceStartHeight, "force-start-height", 0, "Force set starting Cadence height. WARNING: This should only be used locally or for testing, never in production.") Cmd.Flags().StringVar(&filterExpiry, "filter-expiry", "5m", "Filter defines the time it takes for an idle filter to expire") - Cmd.Flags().StringVar(&cfg.TracesBucketName, "traces-gcp-bucket", "", "GCP bucket name where transaction traces are stored") - Cmd.Flags().Uint64Var(&cfg.TracesBackfillStartHeight, "traces-backfill-start-height", 0, "evm block height from which to start backfilling missing traces.") - Cmd.Flags().Uint64Var(&cfg.TracesBackfillEndHeight, "traces-backfill-end-height", 0, "evm block height until which to backfill missing traces. If 0, backfill until the latest block") Cmd.Flags().StringVar(&cloudKMSProjectID, "coa-cloud-kms-project-id", "", "The project ID containing the KMS keys, e.g. 'flow-evm-gateway'") Cmd.Flags().StringVar(&cloudKMSLocationID, "coa-cloud-kms-location-id", "", "The location ID where the key ring is grouped into, e.g. 'global'") Cmd.Flags().StringVar(&cloudKMSKeyRingID, "coa-cloud-kms-key-ring-id", "", "The key ring ID where the KMS keys exist, e.g. 'tx-signing'") diff --git a/config/config.go b/config/config.go index 13f572070..ef71500be 100644 --- a/config/config.go +++ b/config/config.go @@ -74,14 +74,6 @@ type Config struct { FilterExpiry time.Duration // ForceStartCadenceHeight will force set the starting Cadence height, this should be only used for testing or locally. ForceStartCadenceHeight uint64 - // TracesBucketName sets the GCP bucket name where transaction traces are being stored. - TracesBucketName string - // TracesEnabled sets whether the node is supporting transaction traces. - TracesEnabled bool - // TracesBackfillStartHeight sets the starting block height for backfilling missing traces. - TracesBackfillStartHeight uint64 - // TracesBackfillEndHeight sets the ending block height for backfilling missing traces. - TracesBackfillEndHeight uint64 // WalletEnabled sets whether wallet APIs are enabled WalletEnabled bool // WalletKey used for signing transactions diff --git a/go.mod b/go.mod index 462b19566..8358b3b3a 100644 --- a/go.mod +++ b/go.mod @@ -3,7 +3,6 @@ module github.com/onflow/flow-evm-gateway go 1.22 require ( - cloud.google.com/go/storage v1.36.0 github.com/cockroachdb/pebble v1.1.1 github.com/goccy/go-json v0.10.2 github.com/hashicorp/golang-lru/v2 v2.0.7 @@ -23,7 +22,6 @@ require ( github.com/stretchr/testify v1.9.0 golang.org/x/exp v0.0.0-20240119083558-1b970713d09a golang.org/x/sync v0.8.0 - google.golang.org/api v0.162.0 google.golang.org/grpc v1.63.2 ) @@ -33,6 +31,7 @@ require ( cloud.google.com/go/compute/metadata v0.2.3 // indirect cloud.google.com/go/iam v1.1.6 // indirect cloud.google.com/go/kms v1.15.7 // indirect + cloud.google.com/go/storage v1.36.0 // indirect github.com/DataDog/zstd v1.5.2 // indirect github.com/Microsoft/go-winio v0.6.2 // indirect github.com/SaveTheRbtz/mph v0.1.1-0.20240117162131-4166ec7869bc // indirect @@ -200,6 +199,7 @@ require ( golang.org/x/time v0.5.0 // indirect golang.org/x/xerrors v0.0.0-20231012003039-104605ab7028 // indirect gonum.org/v1/gonum v0.14.0 // indirect + google.golang.org/api v0.162.0 // indirect google.golang.org/appengine v1.6.8 // indirect google.golang.org/genproto v0.0.0-20240227224415-6ceb2ff114de // indirect google.golang.org/genproto/googleapis/api v0.0.0-20240227224415-6ceb2ff114de // indirect diff --git a/services/traces/downloader.go b/services/traces/downloader.go deleted file mode 100644 index 4647ee594..000000000 --- a/services/traces/downloader.go +++ /dev/null @@ -1,78 +0,0 @@ -package traces - -import ( - "context" - "encoding/json" - "fmt" - "io" - "time" - - "cloud.google.com/go/storage" - "github.com/onflow/flow-go-sdk" - "github.com/onflow/go-ethereum/common" - "github.com/rs/zerolog" - "google.golang.org/api/option" -) - -const downloadTimeout = 60 * time.Minute - -type Downloader interface { - // Download traces or returning an error with the failure - Download(txID common.Hash, blockIO flow.Identifier) (json.RawMessage, error) -} - -var _ Downloader = &GCPDownloader{} - -type GCPDownloader struct { - client *storage.Client - logger zerolog.Logger - bucket *storage.BucketHandle -} - -func NewGCPDownloader(bucketName string, logger zerolog.Logger) (*GCPDownloader, error) { - if bucketName == "" { - return nil, fmt.Errorf("must provide bucket name") - } - - ctx := context.Background() - // we don't require authentication for public bucket - client, err := storage.NewClient(ctx, option.WithoutAuthentication()) - if err != nil { - return nil, fmt.Errorf("failed to create Google Cloud Storage client: %w", err) - } - - return &GCPDownloader{ - client: client, - logger: logger, - bucket: client.Bucket(bucketName), - }, nil -} - -func (g *GCPDownloader) Download(txID common.Hash, blockID flow.Identifier) (json.RawMessage, error) { - l := g.logger.With(). - Str("tx-id", txID.String()). - Str("cadence-block-id", blockID.String()). - Logger() - - l.Debug().Msg("downloading transaction trace") - - ctx, cancel := context.WithTimeout(context.Background(), downloadTimeout) - defer cancel() - - id := fmt.Sprintf("%s-%s", blockID.String(), txID.String()) - - rc, err := g.bucket.Object(id).NewReader(ctx) - if err != nil { - return nil, fmt.Errorf("failed to download object id %s: %w", id, err) - } - defer rc.Close() - - trace, err := io.ReadAll(rc) - if err != nil { - return nil, fmt.Errorf("failed to read trace id %s: %w", id, err) - } - - l.Info().Int("trace-size", len(trace)).Msg("transaction trace downloaded") - - return trace, nil -} diff --git a/services/traces/engine.go b/services/traces/engine.go deleted file mode 100644 index a76830c01..000000000 --- a/services/traces/engine.go +++ /dev/null @@ -1,193 +0,0 @@ -package traces - -import ( - "context" - "sync" - "time" - - "github.com/onflow/flow-go-sdk" - gethCommon "github.com/onflow/go-ethereum/common" - "github.com/rs/zerolog" - "github.com/sethvargo/go-retry" - - "github.com/onflow/flow-evm-gateway/metrics" - "github.com/onflow/flow-evm-gateway/models" - "github.com/onflow/flow-evm-gateway/storage" -) - -var _ models.Engine = &Engine{} - -// Engine is an implementation of the trace downloader engine. -// -// Traces are ethereum transaction execution traces: https://geth.ethereum.org/docs/developers/evm-tracing -// Currently EVM gateway doesn't produce the traces since it doesn't -// execute the transactions and is thus relying on the execution node -// to produce and upload the traces during execution. This engine -// listens for new transaction events and then downloads and index the -// traces from the transaction execution. -type Engine struct { - *models.EngineStatus - - logger zerolog.Logger - blocksPublisher *models.Publisher[*models.Block] - blocks storage.BlockIndexer - traces storage.TraceIndexer - downloader Downloader - collector metrics.Collector -} - -// NewTracesIngestionEngine creates a new instance of the engine. -func NewTracesIngestionEngine( - blocksPublisher *models.Publisher[*models.Block], - blocks storage.BlockIndexer, - traces storage.TraceIndexer, - downloader Downloader, - logger zerolog.Logger, - collector metrics.Collector, -) *Engine { - return &Engine{ - EngineStatus: models.NewEngineStatus(), - - logger: logger.With().Str("component", "trace-ingestion").Logger(), - blocksPublisher: blocksPublisher, - blocks: blocks, - traces: traces, - downloader: downloader, - collector: collector, - } -} - -// Run the engine. -// TODO: use the context to stop the engine. -func (e *Engine) Run(ctx context.Context) error { - // subscribe to new blocks - e.blocksPublisher.Subscribe(e) - - e.MarkReady() - return nil -} - -// Notify is a handler that is being used to subscribe for new EVM block notifications. -// This method should be non-blocking. -func (e *Engine) Notify(block *models.Block) { - // If the block has no transactions, we simply return early - // as there are no transaction traces to index. - if len(block.TransactionHashes) == 0 { - return - } - - l := e.logger.With().Uint64("evm-height", block.Height).Logger() - - cadenceID, err := e.blocks.GetCadenceID(block.Height) - if err != nil { - l.Error().Err(err).Msg("failed to get cadence block ID") - return - } - - go e.indexBlockTraces(block, cadenceID, false) -} - -// indexBlockTraces iterates the block transaction hashes and tries to download the traces -func (e *Engine) indexBlockTraces(evmBlock *models.Block, cadenceBlockID flow.Identifier, skipExisting bool) { - ctx, cancel := context.WithTimeout(context.Background(), downloadTimeout) - defer cancel() - - const maxConcurrentDownloads = 5 // limit number of concurrent downloads - limiter := make(chan struct{}, maxConcurrentDownloads) - - wg := sync.WaitGroup{} - - for _, h := range evmBlock.TransactionHashes { - wg.Add(1) - limiter <- struct{}{} // acquire a slot - - go func(h gethCommon.Hash) { - defer wg.Done() - defer func() { <-limiter }() // release a slot after done - - l := e.logger.With(). - Str("tx-id", h.String()). - Uint64("evm-height", evmBlock.Height). - Str("cadence-block-id", cadenceBlockID.String()). - Logger() - - if skipExisting { - if _, err := e.traces.GetTransaction(h); err == nil { - l.Debug().Msg("trace already downloaded") - return - } - } - - err := retry.Fibonacci(ctx, time.Second*1, func(ctx context.Context) error { - trace, err := e.downloader.Download(h, cadenceBlockID) - if err != nil { - l.Warn().Err(err).Msg("retrying failed download") - return retry.RetryableError(err) - } - - return e.traces.StoreTransaction(h, trace, nil) - }) - if err != nil { - e.collector.TraceDownloadFailed() - l.Error().Err(err).Msg("failed to download trace") - return - } - l.Info().Msg("trace downloaded successfully") - }(h) - } - - wg.Wait() -} - -// Error is required by the publisher, and we just return a nil, -// since the errors are handled gracefully in the indexBlockTraces -func (e *Engine) Error() <-chan error { - return nil -} - -func (e *Engine) Stop() { - e.MarkStopped() -} - -// Backfill redownloads traces for blocks from EVM start to end height. -func (e *Engine) Backfill(start uint64, end uint64) { - select { - case <-e.Ready(): - case <-e.Done(): - return - } - - lg := e.logger.With().Uint64("start", start).Uint64("end", end).Logger() - - lg.Info().Msg("backfilling traces") - for height := start; height <= end; height++ { - select { - case <-e.Done(): - return - case <-e.Stopped(): - return - default: - } - - l := lg.With().Uint64("evm-height", height).Logger() - - block, err := e.blocks.GetByHeight(height) - if err != nil { - l.Error().Err(err).Msg("failed to get block by height") - return - } - - if len(block.TransactionHashes) == 0 { - continue - } - - cadenceID, err := e.blocks.GetCadenceID(block.Height) - if err != nil { - l.Error().Err(err).Msg("failed to get cadence block ID") - return - } - - e.indexBlockTraces(block, cadenceID, true) - } - lg.Info().Msg("done backfilling traces") -} diff --git a/services/traces/engine_test.go b/services/traces/engine_test.go deleted file mode 100644 index 028309918..000000000 --- a/services/traces/engine_test.go +++ /dev/null @@ -1,288 +0,0 @@ -package traces - -import ( - "context" - "encoding/json" - "fmt" - "slices" - "strings" - "testing" - "time" - - pebbleDB "github.com/cockroachdb/pebble" - "github.com/onflow/flow-go-sdk" - gethCommon "github.com/onflow/go-ethereum/common" - "github.com/rs/zerolog" - "github.com/stretchr/testify/mock" - "github.com/stretchr/testify/require" - - "github.com/onflow/flow-evm-gateway/metrics" - "github.com/onflow/flow-evm-gateway/models" - "github.com/onflow/flow-evm-gateway/services/traces/mocks" - storageMock "github.com/onflow/flow-evm-gateway/storage/mocks" -) - -// this test makes sure once a notification for a new block is triggered -// the block transaction hashes are iterated, and for each a trace is -// downloaded and stored. -func TestTraceIngestion(t *testing.T) { - t.Run("successful single block ingestion", func(t *testing.T) { - blockPublisher := models.NewPublisher[*models.Block]() - blocks := &storageMock.BlockIndexer{} - trace := &storageMock.TraceIndexer{} - downloader := &mocks.Downloader{} - - txTrace := func(id gethCommon.Hash) json.RawMessage { - return json.RawMessage(fmt.Sprintf(`{ - "id": "%s", - "from":"0x42fdd562221741a1db62a0f69a5a680367f07e33", - "gas":"0x15f900", - "gasUsed":"0x387dc", - "to":"0xca11bde05977b3631167028862be2a173976ca11" - }`, id.String())) - } - - latestHeight := uint64(0) - blockID := flow.Identifier{0x09} - hashes := []gethCommon.Hash{{0x1}, {0x2}, {0x3}} - block := storageMock.NewBlock(latestHeight + 1) - block.TransactionHashes = hashes - - blocks. - On("GetByHeight", mock.Anything). - Return(func(height uint64) (*models.Block, error) { - require.Equal(t, latestHeight+1, height) // make sure it gets next block - return block, nil - }) - - blocks. - On("GetCadenceID", mock.Anything). - Return(func(height uint64) (flow.Identifier, error) { - require.Equal(t, latestHeight+1, height) - return blockID, nil - }) - - downloader. - On("Download", mock.Anything, mock.Anything). - Return(func(txID gethCommon.Hash, blkID flow.Identifier) (json.RawMessage, error) { - require.Equal(t, blockID, blkID) - time.Sleep(time.Millisecond * 200) // simulate download delay - return txTrace(txID), nil - }) - - stored := make(chan gethCommon.Hash, len(hashes)) - trace. - On("StoreTransaction", mock.Anything, mock.Anything, mock.Anything). - Return(func(ID gethCommon.Hash, trace json.RawMessage, _ *pebbleDB.Batch) error { - require.Equal(t, txTrace(ID), trace) - stored <- ID - return nil - }) - - engine := NewTracesIngestionEngine( - blockPublisher, - blocks, - trace, - downloader, - zerolog.Nop(), - metrics.NopCollector, - ) - - err := engine.Run(context.Background()) - require.NoError(t, err) - - blockPublisher.Publish(block) - - // make sure stored was called as many times as block contained hashes - require.Eventuallyf(t, func() bool { - return len(stored) == len(hashes) - }, time.Second, time.Millisecond*50, "index not run") - - close(stored) - storedHashes := make([]string, 0) - for h := range stored { - storedHashes = append(storedHashes, h.String()) - } - - // make sure we stored all the hashes in the block - for _, h := range hashes { - require.True(t, slices.Contains(storedHashes, h.String())) - } - }) - - t.Run("successful multiple blocks ingestion", func(t *testing.T) { - blocksPublisher := models.NewPublisher[*models.Block]() - blocks := &storageMock.BlockIndexer{} - trace := &storageMock.TraceIndexer{} - downloader := &mocks.Downloader{} - - txTrace := func(id gethCommon.Hash) json.RawMessage { - return json.RawMessage(fmt.Sprintf(`{ - "id": "%s", - "from":"0x42fdd562221741a1db62a0f69a5a680367f07e33", - "gas":"0x15f900", - "gasUsed":"0x387dc", - "to":"0xca11bde05977b3631167028862be2a173976ca11" - }`, id.String())) - } - - latestHeight := uint64(0) - - const blockCount = 10 - const txCount = 50 - - // generate mock blocks, each with mock transactions - mockBlocks := make([]*models.Block, blockCount+1) - mockCadenceIDs := make([]flow.Identifier, blockCount+1) - - for i := range mockBlocks { - b := storageMock.NewBlock(uint64(i)) - cid := flow.Identifier{byte(i + 10)} - - h := make([]gethCommon.Hash, txCount) - for j := range h { - h[j] = gethCommon.Hash{byte(j), byte(i)} - } - - b.TransactionHashes = h - mockBlocks[i] = b - mockCadenceIDs[i] = cid - } - - blocks. - On("GetCadenceID", mock.Anything). - Return(func(height uint64) (flow.Identifier, error) { - latestHeight++ - require.Equal(t, latestHeight, height) - require.Less(t, int(height), len(mockCadenceIDs)) - return mockCadenceIDs[height], nil - }) - - downloadedIDs := make(chan string, blockCount*txCount) - downloader. - On("Download", mock.Anything, mock.Anything). - Return(func(txID gethCommon.Hash, blkID flow.Identifier) (json.RawMessage, error) { - id := fmt.Sprintf("%s-%s", blkID.String(), txID.String()) - downloadedIDs <- id - time.Sleep(time.Millisecond * 200) // simulate download delay - return txTrace(txID), nil - }) - - stored := make(chan gethCommon.Hash, blockCount*txCount) - trace. - On("StoreTransaction", mock.Anything, mock.Anything, mock.Anything). - Return(func(ID gethCommon.Hash, trace json.RawMessage, _ *pebbleDB.Batch) error { - require.Equal(t, txTrace(ID), trace) - stored <- ID - return nil - }) - - engine := NewTracesIngestionEngine( - blocksPublisher, - blocks, - trace, - downloader, - zerolog.Nop(), - metrics.NopCollector, - ) - - err := engine.Run(context.Background()) - require.NoError(t, err) - - for i := 0; i < blockCount; i++ { - blocksPublisher.Publish(mockBlocks[i+1]) - time.Sleep(time.Millisecond * 100) // simulate block delay - } - - // make sure download was called as many times as all blocks times the hashes it contained - require.Eventuallyf(t, func() bool { - return len(downloadedIDs) == blockCount*txCount - }, time.Second*10, time.Millisecond*100, "traces not downloaded") - - close(downloadedIDs) - - // make sure stored was called as many times as all blocks times the hashes it contained - require.Eventuallyf(t, func() bool { - return len(stored) == blockCount*txCount - }, time.Second*10, time.Millisecond*100, "traces not indexed") - - close(stored) - - // make sure we downloaded and indexed all the hashes in the block - for id := range downloadedIDs { - found := false - for _, b := range mockBlocks { - for _, h := range b.TransactionHashes { - txID := strings.Split(id, "-")[1] - if txID == h.String() { - found = true - break - } - } - if found { - break - } - } - require.True(t, found, fmt.Sprintf("id %s not found", id)) - } - }) - - t.Run("failed download retries", func(t *testing.T) { - blockBroadcaster := models.NewPublisher[*models.Block]() - blocks := &storageMock.BlockIndexer{} - downloader := &mocks.Downloader{} - trace := &storageMock.TraceIndexer{} - logger := zerolog.New(zerolog.NewTestWriter(t)) - collector := metrics.NopCollector - - latestHeight := uint64(0) - blockID := flow.Identifier{0x09} - hashes := []gethCommon.Hash{{0x1}} - block := storageMock.NewBlock(latestHeight + 1) - block.TransactionHashes = hashes - - blocks. - On("GetByHeight", mock.Anything). - Return(func(height uint64) (*models.Block, error) { - require.Equal(t, latestHeight+1, height) // make sure it gets next block - return block, nil - }) - - blocks. - On("GetCadenceID", mock.Anything). - Return(func(height uint64) (flow.Identifier, error) { - require.Equal(t, latestHeight+1, height) - return blockID, nil - }) - - const retriesNum = 3 - downloads := make(chan struct{}, retriesNum) - downloader. - On("Download", mock.Anything, mock.Anything). - Return(func(txID gethCommon.Hash, blkID flow.Identifier) (json.RawMessage, error) { - downloads <- struct{}{} - return nil, fmt.Errorf("failed download") - }) - - engine := NewTracesIngestionEngine( - blockBroadcaster, - blocks, - trace, - downloader, - logger, - collector, - ) - - err := engine.Run(context.Background()) - require.NoError(t, err) - - blockBroadcaster.Publish(block) - - // make sure stored was called as many times as block contained hashes - require.Eventuallyf(t, func() bool { - return len(downloads) == retriesNum - }, time.Second*10, time.Millisecond*200, "download not retried") - - close(downloads) - }) -} diff --git a/services/traces/mocks/Downloader.go b/services/traces/mocks/Downloader.go deleted file mode 100644 index 83353acb0..000000000 --- a/services/traces/mocks/Downloader.go +++ /dev/null @@ -1,61 +0,0 @@ -// Code generated by mockery v2.43.2. DO NOT EDIT. - -package mocks - -import ( - flow "github.com/onflow/flow-go-sdk" - common "github.com/onflow/go-ethereum/common" - - json "encoding/json" - - mock "github.com/stretchr/testify/mock" -) - -// Downloader is an autogenerated mock type for the Downloader type -type Downloader struct { - mock.Mock -} - -// Download provides a mock function with given fields: txID, blockIO -func (_m *Downloader) Download(txID common.Hash, blockIO flow.Identifier) (json.RawMessage, error) { - ret := _m.Called(txID, blockIO) - - if len(ret) == 0 { - panic("no return value specified for Download") - } - - var r0 json.RawMessage - var r1 error - if rf, ok := ret.Get(0).(func(common.Hash, flow.Identifier) (json.RawMessage, error)); ok { - return rf(txID, blockIO) - } - if rf, ok := ret.Get(0).(func(common.Hash, flow.Identifier) json.RawMessage); ok { - r0 = rf(txID, blockIO) - } else { - if ret.Get(0) != nil { - r0 = ret.Get(0).(json.RawMessage) - } - } - - if rf, ok := ret.Get(1).(func(common.Hash, flow.Identifier) error); ok { - r1 = rf(txID, blockIO) - } else { - r1 = ret.Error(1) - } - - return r0, r1 -} - -// NewDownloader creates a new instance of Downloader. It also registers a testing interface on the mock and a cleanup function to assert the mocks expectations. -// The first argument is typically a *testing.T value. -func NewDownloader(t interface { - mock.TestingT - Cleanup(func()) -}) *Downloader { - mock := &Downloader{} - mock.Mock.Test(t) - - t.Cleanup(func() { mock.AssertExpectations(t) }) - - return mock -} From 9a9604b1d2f54a1a00f884794eee0f9e3f5e51f8 Mon Sep 17 00:00:00 2001 From: Janez Podhostnik Date: Thu, 31 Oct 2024 16:22:13 +0100 Subject: [PATCH 007/174] removed traces settings from the readme --- README.md | 3 --- 1 file changed, 3 deletions(-) diff --git a/README.md b/README.md index fb14da313..8cc4d35e3 100644 --- a/README.md +++ b/README.md @@ -226,9 +226,6 @@ The application can be configured using the following flags at runtime: | `force-start-height` | `0` | Force-set starting Cadence height (local/testing use only) | | `wallet-api-key` | `""` | ECDSA private key for wallet APIs (local/testing use only) | | `filter-expiry` | `5m` | Expiry time for idle filters | -| `traces-gcp-bucket` | `""` | GCP bucket name for transaction traces | -| `traces-backfill-start-height` | `0` | Start height for backfilling transaction traces | -| `traces-backfill-end-height` | `0` | End height for backfilling transaction traces | | `index-only` | `false` | Run in index-only mode, allowing state queries and indexing but no transaction sending | | `profiler-enabled` | `false` | Enable the pprof profiler server | | `profiler-host` | `localhost` | Host for the pprof profiler | From f12cebc1fddbf243ad7b669b0ca10f2bf22d74d2 Mon Sep 17 00:00:00 2001 From: Janez Podhostnik Date: Fri, 1 Nov 2024 15:52:24 +0100 Subject: [PATCH 008/174] Add special EVM height handling --- api/api.go | 74 ++++++++++++++++++++++----------- services/requester/requester.go | 1 + 2 files changed, 51 insertions(+), 24 deletions(-) diff --git a/api/api.go b/api/api.go index e1e61c1fd..fb0a313d0 100644 --- a/api/api.go +++ b/api/api.go @@ -287,7 +287,7 @@ func (b *BlockChainAPI) GetBalance( return handleError[*hexutil.Big](err, l, b.collector) } - balance, err := b.evm.GetBalance(ctx, address, evmHeight) + balance, err := b.evm.GetBalance(ctx, address, int64(evmHeight)) if err != nil { return handleError[*hexutil.Big](err, l, b.collector) } @@ -518,21 +518,12 @@ func (b *BlockChainAPI) GetBlockReceipts( return nil, err } - var ( - block *models.Block - err error - ) - if blockNumberOrHash.BlockHash != nil { - block, err = b.blocks.GetByID(*blockNumberOrHash.BlockHash) - } else if blockNumberOrHash.BlockNumber != nil { - block, err = b.blocks.GetByHeight(uint64(blockNumberOrHash.BlockNumber.Int64())) - } else { - return handleError[[]map[string]interface{}]( - fmt.Errorf("%w: block number or hash not provided", errs.ErrInvalid), - l, - b.collector, - ) + evmHeight, err := b.getBlockNumber(&blockNumberOrHash) + if err != nil { + return handleError[[]map[string]interface{}](err, l, b.collector) } + + block, err := b.blocks.GetByHeight(evmHeight) if err != nil { return handleError[[]map[string]interface{}](err, l, b.collector) } @@ -660,7 +651,7 @@ func (b *BlockChainAPI) Call( from = *args.From } - res, err := b.evm.Call(ctx, tx, from, evmHeight) + res, err := b.evm.Call(ctx, tx, from, int64(evmHeight)) if err != nil { return handleError[hexutil.Bytes](err, l, b.collector) } @@ -767,7 +758,7 @@ func (b *BlockChainAPI) GetTransactionCount( return handleError[*hexutil.Uint64](err, l, b.collector) } - networkNonce, err := b.evm.GetNonce(ctx, address, evmHeight) + networkNonce, err := b.evm.GetNonce(ctx, address, int64(evmHeight)) if err != nil { return handleError[*hexutil.Uint64](err, l, b.collector) } @@ -833,7 +824,7 @@ func (b *BlockChainAPI) EstimateGas( return handleError[hexutil.Uint64](err, l, b.collector) } - estimatedGas, err := b.evm.EstimateGas(ctx, tx, from, evmHeight) + estimatedGas, err := b.evm.EstimateGas(ctx, tx, from, int64(evmHeight)) if err != nil { return handleError[hexutil.Uint64](err, l, b.collector) } @@ -862,7 +853,7 @@ func (b *BlockChainAPI) GetCode( return handleError[hexutil.Bytes](err, l, b.collector) } - code, err := b.evm.GetCode(ctx, address, evmHeight) + code, err := b.evm.GetCode(ctx, address, int64(evmHeight)) if err != nil { return handleError[hexutil.Bytes](err, l, b.collector) } @@ -987,7 +978,7 @@ func (b *BlockChainAPI) GetStorageAt( return handleError[hexutil.Bytes](err, l, b.collector) } - result, err := b.evm.GetStorageAt(ctx, address, key, evmHeight) + result, err := b.evm.GetStorageAt(ctx, address, key, int64(evmHeight)) if err != nil { return handleError[hexutil.Bytes](err, l, b.collector) } @@ -1097,27 +1088,62 @@ func (b *BlockChainAPI) prepareBlockResponse( return blockResponse, nil } -func (b *BlockChainAPI) getBlockNumber(blockNumberOrHash *rpc.BlockNumberOrHash) (int64, error) { +func (b *BlockChainAPI) getBlockNumber(blockNumberOrHash *rpc.BlockNumberOrHash) (uint64, error) { err := fmt.Errorf("%w: neither block number nor hash specified", errs.ErrInvalid) if blockNumberOrHash == nil { return 0, err } if number, ok := blockNumberOrHash.Number(); ok { - return number.Int64(), nil + height, err := resolveBlockNumber(number, b.blocks) + if err != nil { + b.logger.Error().Err(err). + Stringer("block_number", number). + Msg("failed to resolve block by hash") + return 0, err + } + return height, nil } if hash, ok := blockNumberOrHash.Hash(); ok { evmHeight, err := b.blocks.GetHeightByID(hash) if err != nil { - b.logger.Error().Err(err).Msg("failed to get block by hash") + b.logger.Error().Err(err). + Stringer("block_hash", hash). + Msg("failed to get block by hash") return 0, err } - return int64(evmHeight), nil + return evmHeight, nil } return 0, err } +func resolveBlockNumber( + number rpc.BlockNumber, + blocksDB storage.BlockIndexer, +) (uint64, error) { + height := number.Int64() + + // if special values (latest) we return latest executed height + // + // all the special values are: + // SafeBlockNumber = BlockNumber(-4) + // FinalizedBlockNumber = BlockNumber(-3) + // LatestBlockNumber = BlockNumber(-2) + // PendingBlockNumber = BlockNumber(-1) + // + // EVM on Flow does not have these concepts, but the latest block is the closest fit + if height < 0 { + executed, err := blocksDB.LatestEVMHeight() + if err != nil { + return 0, err + } + height = int64(executed) + } + + return uint64(height), nil +} + // handleError takes in an error and in case the error is of type ErrEntityNotFound // it returns nil instead of an error since that is according to the API spec, // if the error is not of type ErrEntityNotFound it will return the error and the generic diff --git a/services/requester/requester.go b/services/requester/requester.go index 60e0916d9..a3050f913 100644 --- a/services/requester/requester.go +++ b/services/requester/requester.go @@ -82,6 +82,7 @@ const coaFundingBalance = minFlowBalance - 1 const LatestBlockHeight uint64 = math.MaxUint64 - 1 +// TODO(janezp): Requester does need to know about special EVM block heights. evmHeight should be uint64. type Requester interface { // SendRawTransaction will submit signed transaction data to the network. // The submitted EVM transaction hash is returned. From 0aff71cb0b2f0e488ec239ca2f556d4a451ad4fd Mon Sep 17 00:00:00 2001 From: Janez Podhostnik Date: Fri, 1 Nov 2024 16:57:23 +0100 Subject: [PATCH 009/174] split tests --- tests/e2e_web3js_test.go | 14 + tests/helpers.go | 4 +- tests/web3js/build_evm_state_test.js | 79 --- tests/web3js/debug_traces_test.js | 455 ++++++++++++++++++ tests/web3js/eth_get_storage_at_test.js | 259 ++++++++++ tests/web3js/eth_rate_limit_test.js | 4 +- tests/web3js/eth_revert_reason_test.js | 43 +- .../web3js/verify_cadence_arch_calls_test.js | 148 ++++++ 8 files changed, 902 insertions(+), 104 deletions(-) create mode 100644 tests/web3js/debug_traces_test.js create mode 100644 tests/web3js/eth_get_storage_at_test.js create mode 100644 tests/web3js/verify_cadence_arch_calls_test.js diff --git a/tests/e2e_web3js_test.go b/tests/e2e_web3js_test.go index 70276762f..7f68e0949 100644 --- a/tests/e2e_web3js_test.go +++ b/tests/e2e_web3js_test.go @@ -28,6 +28,16 @@ func TestWeb3_E2E(t *testing.T) { runWeb3Test(t, "build_evm_state_test") }) + t.Run("verify Cadence arch calls", func(t *testing.T) { + t.Skip("not implemented yet") + runWeb3Test(t, "verify_cadence_arch_calls_test") + }) + + t.Run("test transaction traces", func(t *testing.T) { + t.Skip("not yet added back") + runWeb3Test(t, "debug_traces_test") + }) + t.Run("test setup sanity check", func(t *testing.T) { runWeb3Test(t, "setup_test") }) @@ -56,6 +66,10 @@ func TestWeb3_E2E(t *testing.T) { runWeb3Test(t, "eth_deploy_contract_and_interact_test") }) + t.Run("test eth_getStorageAt", func(t *testing.T) { + runWeb3Test(t, "eth_get_storage_at_test") + }) + t.Run("deploy multicall3 contract and call methods", func(t *testing.T) { runWeb3Test(t, "eth_multicall3_contract_test") }) diff --git a/tests/helpers.go b/tests/helpers.go index a62bb2e11..7c644c7f7 100644 --- a/tests/helpers.go +++ b/tests/helpers.go @@ -153,7 +153,7 @@ func servicesSetup(t *testing.T) (emulator.Emulator, func()) { LogWriter: testLogWriter(), StreamTimeout: time.Second * 30, StreamLimit: 10, - RateLimit: 50, + RateLimit: 500, WSEnabled: true, MetricsPort: 8443, FilterExpiry: time.Second * 5, @@ -177,7 +177,7 @@ func servicesSetup(t *testing.T) (emulator.Emulator, func()) { // and will report failure or success of the test. func executeTest(t *testing.T, testFile string) { command := fmt.Sprintf( - "./web3js/node_modules/.bin/mocha ./web3js/%s.js --timeout 120s", + "./web3js/node_modules/.bin/mocha ./web3js/%s.js --timeout 360s", testFile, ) parts := strings.Fields(command) diff --git a/tests/web3js/build_evm_state_test.js b/tests/web3js/build_evm_state_test.js index d52eb715c..a9490492c 100644 --- a/tests/web3js/build_evm_state_test.js +++ b/tests/web3js/build_evm_state_test.js @@ -237,85 +237,6 @@ it('should handle a large number of EVM interactions', async () => { gasPrice: conf.minGasPrice, }) assert.equal(res.receipt.status, conf.successStatus) - - // submit a transaction that calls verifyArchCallToRandomSource(uint64 height) - let getRandomSourceData = deployed.contract.methods.verifyArchCallToRandomSource(120).encodeABI() - res = await helpers.signAndSend({ - from: conf.eoa.address, - to: contractAddress, - data: getRandomSourceData, - value: '0', - gasPrice: conf.minGasPrice, - }) - assert.equal(res.receipt.status, conf.successStatus) - - // make a contract call for verifyArchCallToRandomSource(uint64 height) - res = await web3.eth.call({ to: contractAddress, data: getRandomSourceData }, latest) - assert.notEqual( - res, - '0x0000000000000000000000000000000000000000000000000000000000000000' - ) - assert.lengthOf(res, 66) - - // submit a transaction that calls verifyArchCallToRevertibleRandom() - let revertibleRandomData = deployed.contract.methods.verifyArchCallToRevertibleRandom().encodeABI() - res = await helpers.signAndSend({ - from: conf.eoa.address, - to: contractAddress, - data: revertibleRandomData, - value: '0', - gasPrice: conf.minGasPrice, - }) - assert.equal(res.receipt.status, conf.successStatus) - - // make a contract call for verifyArchCallToRevertibleRandom() - res = await web3.eth.call({ to: contractAddress, data: revertibleRandomData }, latest) - assert.notEqual( - res, - '0x0000000000000000000000000000000000000000000000000000000000000000' - ) - assert.lengthOf(res, 66) - - // submit a transaction that calls verifyArchCallToFlowBlockHeight() - let flowBlockHeightData = deployed.contract.methods.verifyArchCallToFlowBlockHeight().encodeABI() - res = await helpers.signAndSend({ - from: conf.eoa.address, - to: contractAddress, - data: flowBlockHeightData, - value: '0', - gasPrice: conf.minGasPrice, - }) - assert.equal(res.receipt.status, conf.successStatus) - - // make a contract call for verifyArchCallToFlowBlockHeight() - res = await web3.eth.call({ to: contractAddress, data: flowBlockHeightData }, latest) - assert.equal( - web3.eth.abi.decodeParameter('uint64', res), - latest, - ) - - // submit a transaction that calls verifyArchCallToVerifyCOAOwnershipProof(address,bytes32,bytes) - let tx = await web3.eth.getTransactionFromBlock(conf.startBlockHeight, 1) - let verifyCOAOwnershipProofData = deployed.contract.methods.verifyArchCallToVerifyCOAOwnershipProof( - tx.to, - '0x1bacdb569847f31ade07e83d6bb7cefba2b9290b35d5c2964663215e73519cff', - web3.utils.hexToBytes('f853c18088f8d6e0586b0a20c78365766df842b840b90448f4591df2639873be2914c5560149318b7e2fcf160f7bb8ed13cfd97be2f54e6889606f18e50b2c37308386f840e03a9fff915f57b2164cba27f0206a95') - ).encodeABI() - res = await helpers.signAndSend({ - from: conf.eoa.address, - to: contractAddress, - data: verifyCOAOwnershipProofData, - value: '0', - gasPrice: conf.minGasPrice, - }) - assert.equal(res.receipt.status, conf.successStatus) - - // make a contract call for verifyArchCallToVerifyCOAOwnershipProof(address,bytes32,bytes) - res = await web3.eth.call({ to: contractAddress, data: verifyCOAOwnershipProofData }, latest) - assert.equal( - web3.eth.abi.decodeParameter('bool', res), - false, - ) }) function randomItem(items) { diff --git a/tests/web3js/debug_traces_test.js b/tests/web3js/debug_traces_test.js new file mode 100644 index 000000000..75794cd03 --- /dev/null +++ b/tests/web3js/debug_traces_test.js @@ -0,0 +1,455 @@ +const { assert } = require('chai') +const conf = require('./config') +const helpers = require('./helpers') +const web3 = conf.web3 + +let deployed = null +let contractAddress = null + +before(async () => { + deployed = await helpers.deployContract('storage') + contractAddress = deployed.receipt.contractAddress + + assert.equal(deployed.receipt.status, conf.successStatus) +}) + +it('should retrieve transaction traces', async () => { + assert.equal(deployed.receipt.status, conf.successStatus) + + let receipt = await web3.eth.getTransactionReceipt(deployed.receipt.transactionHash) + assert.equal(receipt.contractAddress, contractAddress) + + let callTracer = { + tracer: 'callTracer', + tracerConfig: { + onlyTopCall: true + } + } + response = await helpers.callRPCMethod( + 'debug_traceTransaction', + [receipt.transactionHash, callTracer] + ) + assert.equal(response.status, 200) + assert.isDefined(response.body.result) + + // Assert proper response for `callTracer` + let txTrace = response.body.result + assert.equal(txTrace.from, '0xfacf71692421039876a5bb4f10ef7a439d8ef61e') + assert.equal(txTrace.gas, '0x118e0c') + assert.equal(txTrace.gasUsed, '0x114010') + assert.equal(txTrace.to, '0x99a64c993965f8d69f985b5171bc20065cc32fab') + assert.lengthOf(txTrace.input, 9856n) + assert.lengthOf(txTrace.output, 9806n) + assert.equal(txTrace.value, '0x0') + assert.equal(txTrace.type, 'CREATE') + + let jsTracer = '{hist: {}, nops: 0, step: function(log, db) { var op = log.op.toString(); if (this.hist[op]){ this.hist[op]++; } else { this.hist[op] = 1; } this.nops++; }, fault: function(log, db) {}, result: function(ctx) { return this.hist; }}' + response = await helpers.callRPCMethod( + 'debug_traceTransaction', + [receipt.transactionHash, { tracer: jsTracer }] + ) + assert.equal(response.status, 200) + assert.isDefined(response.body.result) + + // Assert proper response for custom JavaScript tracer + txTrace = response.body.result + assert.deepEqual( + txTrace, + { + PUSH1: 2, + MSTORE: 1, + PUSH2: 3, + PUSH0: 3, + DUP2: 1, + SWAP1: 1, + SSTORE: 1, + POP: 1, + DUP1: 1, + CODECOPY: 1, + RETURN: 1 + } + ) + + let updateData = deployed.contract.methods.store(100n).encodeABI() + let res = await helpers.signAndSend({ + from: conf.eoa.address, + to: contractAddress, + data: updateData, + value: '0', + gasPrice: conf.minGasPrice, + }) + assert.equal(res.receipt.status, conf.successStatus) + + receipt = await web3.eth.getTransactionReceipt(res.receipt.transactionHash) + + response = await helpers.callRPCMethod( + 'debug_traceTransaction', + [receipt.transactionHash, callTracer] + ) + assert.equal(response.status, 200) + assert.isDefined(response.body.result) + + // Assert proper response for `callTracer` + txTrace = response.body.result + assert.equal(txTrace.from, '0xfacf71692421039876a5bb4f10ef7a439d8ef61e') + assert.equal(txTrace.gas, '0x72c3') + assert.equal(txTrace.gasUsed, '0x6827') + assert.equal(txTrace.to, '0x99a64c993965f8d69f985b5171bc20065cc32fab') + assert.equal( + txTrace.input, + updateData + ) + assert.equal(txTrace.value, '0x0') + assert.equal(txTrace.type, 'CALL') + + let prestateTracer = { + tracer: 'prestateTracer', + tracerConfig: { + diffMode: true + } + } + response = await helpers.callRPCMethod( + 'debug_traceTransaction', + [receipt.transactionHash, prestateTracer] + ) + assert.equal(response.status, 200) + assert.isDefined(response.body.result) + + // Assert proper response for `prestateTracer` + txTrace = response.body.result + assert.deepEqual( + txTrace.pre['0x0000000000000000000000030000000000000000'], + { balance: '0x0', nonce: 1 } + ) + assert.deepEqual( + txTrace.pre['0xfacf71692421039876a5bb4f10ef7a439d8ef61e'], + { balance: '0x456391823ad876a0', nonce: 1 } + ) + assert.deepEqual( + txTrace.post['0x0000000000000000000000030000000000000000'], + { balance: '0x3d06da' } + ) + assert.deepEqual( + txTrace.post['0xfacf71692421039876a5bb4f10ef7a439d8ef61e'], + { balance: '0x456391823a9b6fc6', nonce: 2 } + ) + + response = await helpers.callRPCMethod( + 'debug_traceTransaction', + [receipt.transactionHash, { tracer: '4byteTracer' }] + ) + assert.equal(response.status, 200) + assert.isDefined(response.body.result) + + // Assert proper response for `4byteTracer` + txTrace = response.body.result + assert.deepEqual( + txTrace, + { '0x6057361d-32': 1 } + ) + + response = await helpers.callRPCMethod( + 'debug_traceBlockByNumber', + [web3.utils.toHex(receipt.blockNumber), callTracer] + ) + assert.equal(response.status, 200) + assert.isDefined(response.body.result) + + let txTraces = response.body.result + assert.lengthOf(txTraces, 2) // the 2nd tx trace is from the transfer of fees to coinbase + assert.deepEqual( + txTraces, + [ + { + txHash: '0x87449feedc004c75c0e8b12d01656f2e28366c7d73b1b5336beae20aaa5033dd', + result: { + from: '0xfacf71692421039876a5bb4f10ef7a439d8ef61e', + gas: '0x72c3', + gasUsed: '0x6827', + to: '0x99a64c993965f8d69f985b5171bc20065cc32fab', + input: '0x6057361d0000000000000000000000000000000000000000000000000000000000000064', + value: '0x0', + type: 'CALL' + } + }, + { + txHash: '0x6039ef1f7dc8d40b74f58e502f5b0b535a46c1b4ddd780c23cb97cf4d681bb47', + result: { + from: '0x0000000000000000000000030000000000000000', + gas: '0x5b04', + gasUsed: '0x5208', + to: '0x658bdf435d810c91414ec09147daa6db62406379', + input: '0x', + value: '0x3d06da', + type: 'CALL' + } + } + ] + ) + + response = await helpers.callRPCMethod( + 'debug_traceBlockByHash', + [web3.utils.toHex(receipt.blockHash), callTracer] + ) + assert.equal(response.status, 200) + assert.isDefined(response.body.result) + + txTraces = response.body.result + assert.lengthOf(txTraces, 2) // the 2nd tx trace is from the transfer of fees to coinbase + assert.deepEqual( + txTraces, + [ + { + txHash: '0x87449feedc004c75c0e8b12d01656f2e28366c7d73b1b5336beae20aaa5033dd', + result: { + from: '0xfacf71692421039876a5bb4f10ef7a439d8ef61e', + gas: '0x72c3', + gasUsed: '0x6827', + to: '0x99a64c993965f8d69f985b5171bc20065cc32fab', + input: '0x6057361d0000000000000000000000000000000000000000000000000000000000000064', + value: '0x0', + type: 'CALL' + } + }, + { + txHash: '0x6039ef1f7dc8d40b74f58e502f5b0b535a46c1b4ddd780c23cb97cf4d681bb47', + result: { + from: '0x0000000000000000000000030000000000000000', + gas: '0x5b04', + gasUsed: '0x5208', + to: '0x658bdf435d810c91414ec09147daa6db62406379', + input: '0x', + value: '0x3d06da', + type: 'CALL' + } + } + ] + ) +}) + +it('should retrieve call traces', async () => { + let receipt = await web3.eth.getTransactionReceipt(deployed.receipt.transactionHash) + assert.equal(receipt.contractAddress, contractAddress) + + let callTracer = { + tracer: 'callTracer', + tracerConfig: { + onlyTopCall: true + } + } + + let callData = deployed.contract.methods.store(500).encodeABI() + let traceCall = { + from: conf.eoa.address, + to: contractAddress, + data: callData, + value: '0x0', + gasPrice: web3.utils.toHex(conf.minGasPrice), + gas: '0x95ab' + } + response = await helpers.callRPCMethod( + 'debug_traceCall', + [traceCall, 'latest', callTracer] + ) + assert.equal(response.status, 200) + assert.isDefined(response.body) + + let updateTrace = response.body.result + assert.equal(updateTrace.from, '0xfacf71692421039876a5bb4f10ef7a439d8ef61e') + assert.equal(updateTrace.gas, '0x95ab') + assert.equal(updateTrace.gasUsed, '0x6833') + assert.equal(updateTrace.to, '0x99a64c993965f8d69f985b5171bc20065cc32fab') + assert.equal( + updateTrace.input, + '0x6057361d00000000000000000000000000000000000000000000000000000000000001f4' + ) + assert.equal(updateTrace.value, '0x0') + assert.equal(updateTrace.type, 'CALL') + + callData = deployed.contract.methods.retrieve().encodeABI() + traceCall = { + from: conf.eoa.address, + to: contractAddress, + gas: '0x75ab', + gasPrice: web3.utils.toHex(conf.minGasPrice), + value: '0x0', + data: callData, + } + response = await helpers.callRPCMethod( + 'debug_traceCall', + [traceCall, 'latest', callTracer] + ) + assert.equal(response.status, 200) + assert.isDefined(response.body) + + let callTrace = response.body.result + assert.equal(callTrace.from, '0xfacf71692421039876a5bb4f10ef7a439d8ef61e') + assert.equal(callTrace.gas, '0x75ab') + assert.equal(callTrace.gasUsed, '0x5be0') + assert.equal(callTrace.to, '0x99a64c993965f8d69f985b5171bc20065cc32fab') + assert.equal(callTrace.input, '0x2e64cec1') + assert.equal( + callTrace.output, + '0x0000000000000000000000000000000000000000000000000000000000000064' + ) + assert.equal(callTrace.value, '0x0') + assert.equal(callTrace.type, 'CALL') + + let prestateTracer = { + tracer: 'prestateTracer', + tracerConfig: { + diffMode: true + } + } + response = await helpers.callRPCMethod( + 'debug_traceCall', + [traceCall, 'latest', prestateTracer] + ) + assert.equal(response.status, 200) + assert.isDefined(response.body) + + // Assert proper response for `prestateTracer` + txTrace = response.body.result + assert.deepEqual( + txTrace, + { + post: { '0xfacf71692421039876a5bb4f10ef7a439d8ef61e': { nonce: 3 } }, + pre: { + '0xfacf71692421039876a5bb4f10ef7a439d8ef61e': { balance: '0x456391823a9b6fc6', nonce: 2 } + } + } + ) + + response = await helpers.callRPCMethod( + 'debug_traceCall', + [traceCall, 'latest', { tracer: '4byteTracer' }] + ) + assert.equal(response.status, 200) + assert.isDefined(response.body) + + // Assert proper response for `4byteTracer` + txTrace = response.body.result + assert.deepEqual( + txTrace, + { '0x2e64cec1-0': 1 } + ) + + let jsTracer = '{hist: {}, nops: 0, step: function(log, db) { var op = log.op.toString(); if (this.hist[op]){ this.hist[op]++; } else { this.hist[op] = 1; } this.nops++; }, fault: function(log, db) {}, result: function(ctx) { return this.hist; }}' + response = await helpers.callRPCMethod( + 'debug_traceCall', + [traceCall, 'latest', { tracer: jsTracer }] + ) + assert.equal(response.status, 200) + assert.isDefined(response.body) + + // Assert proper response for custom JavaScript tracer + txTrace = response.body.result + assert.deepEqual( + txTrace, + { + PUSH1: 7, + MSTORE: 2, + CALLVALUE: 1, + DUP1: 6, + ISZERO: 1, + PUSH2: 13, + JUMPI: 5, + JUMPDEST: 12, + POP: 9, + CALLDATASIZE: 1, + LT: 1, + PUSH0: 5, + CALLDATALOAD: 1, + SHR: 1, + PUSH4: 3, + GT: 2, + EQ: 1, + JUMP: 8, + SLOAD: 1, + SWAP1: 7, + MLOAD: 2, + SWAP2: 4, + DUP3: 2, + ADD: 2, + DUP4: 1, + DUP5: 1, + DUP2: 2, + SWAP3: 1, + SUB: 1, + RETURN: 1 + } + ) + + let callTracerWithStateOverrides = { + tracer: 'callTracer', + tracerConfig: { + onlyTopCall: true + }, + stateOverrides: { + [contractAddress]: { + stateDiff: { + '0x0000000000000000000000000000000000000000000000000000000000000000': '0x00000000000000000000000000000000000000000000000000000000000003e8' + } + } + } + } + response = await helpers.callRPCMethod( + 'debug_traceCall', + [traceCall, 'latest', callTracerWithStateOverrides] + ) + assert.equal(response.status, 200) + assert.isDefined(response.body) + + callTrace = response.body.result + assert.equal(callTrace.from, '0xfacf71692421039876a5bb4f10ef7a439d8ef61e') + assert.equal(callTrace.gas, '0x75ab') + assert.equal(callTrace.gasUsed, '0x5be0') + assert.equal(callTrace.to, '0x99a64c993965f8d69f985b5171bc20065cc32fab') + assert.equal(callTrace.input, '0x2e64cec1') + assert.equal( + callTrace.output, + '0x00000000000000000000000000000000000000000000000000000000000003e8' + ) + assert.equal(callTrace.value, '0x0') + assert.equal(callTrace.type, 'CALL') + + let updateData = deployed.contract.methods.store(1500).encodeABI() + let res = await helpers.signAndSend({ + from: conf.eoa.address, + to: contractAddress, + data: updateData, + value: '0', + gasPrice: conf.minGasPrice, + }) + assert.equal(res.receipt.status, conf.successStatus) + + let latestHeight = await web3.eth.getBlockNumber() + + // Assert value on previous block + response = await helpers.callRPCMethod( + 'debug_traceCall', + [traceCall, web3.utils.toHex(latestHeight - 1n), callTracer] + ) + assert.equal(response.status, 200) + assert.isDefined(response.body) + + callTrace = response.body.result + assert.equal( + callTrace.output, + '0x0000000000000000000000000000000000000000000000000000000000000064' + ) + + // Assert value on latest block + response = await helpers.callRPCMethod( + 'debug_traceCall', + [traceCall, web3.utils.toHex(latestHeight), callTracer] + ) + assert.equal(response.status, 200) + assert.isDefined(response.body) + + callTrace = response.body.result + assert.equal( + callTrace.output, + '0x00000000000000000000000000000000000000000000000000000000000005dc' + ) +}) diff --git a/tests/web3js/eth_get_storage_at_test.js b/tests/web3js/eth_get_storage_at_test.js new file mode 100644 index 000000000..2969f0aa8 --- /dev/null +++ b/tests/web3js/eth_get_storage_at_test.js @@ -0,0 +1,259 @@ +const { assert } = require('chai') +const conf = require('./config') +const helpers = require('./helpers') +const web3 = conf.web3 + +it('deploy contract and interact', async () => { + let deployed = await helpers.deployContract('storage') + let contractAddress = deployed.receipt.contractAddress + + // make sure deploy results are correct + assert.equal(deployed.receipt.status, conf.successStatus) + assert.isString(deployed.receipt.transactionHash) + assert.isString(contractAddress) + assert.equal(deployed.receipt.from, conf.eoa.address) + assert.isUndefined(deployed.receipt.to) + + let rcp = await web3.eth.getTransactionReceipt(deployed.receipt.transactionHash) + assert.equal(rcp.contractAddress, contractAddress) + assert.equal(rcp.status, conf.successStatus) + assert.isUndefined(rcp.to) + assert.equal(rcp.gasUsed, 1130512n) + assert.equal(rcp.gasUsed, rcp.cumulativeGasUsed) + + // check if latest block contains the deploy results + let latestHeight = await web3.eth.getBlockNumber() + let deployTx = await web3.eth.getTransactionFromBlock(latestHeight, 0) + assert.equal(deployTx.hash, deployed.receipt.transactionHash) + assert.isUndefined(deployTx.to) + + // check that getCode supports specific block heights + let code = await web3.eth.getCode(contractAddress, latestHeight - 1n) + assert.equal(code, '0x') // empty at previous height + + code = await web3.eth.getCode(contractAddress) + // deploy data has more than just the contract + // since it contains the initialization code, + // but subset of the data is the contract code + assert.isTrue(deployTx.data.includes(code.replace('0x', ''))) + + let deployReceipt = await web3.eth.getTransactionReceipt(deployed.receipt.transactionHash) + assert.deepEqual(deployReceipt, deployed.receipt) + + // get the default deployed value on contract + const initValue = 1337 + let callRetrieve = await deployed.contract.methods.retrieve().encodeABI() + result = await web3.eth.call({ to: contractAddress, data: callRetrieve }, 'latest') + assert.equal(result, initValue) + + let slot = 0 // The slot for the 'number' variable + let stored = await web3.eth.getStorageAt(contractAddress, slot, latestHeight) + let value = web3.utils.hexToNumberString(stored) + assert.equal(value, initValue) + + // set the value on the contract, to its current value + let updateData = deployed.contract.methods.store(initValue).encodeABI() + // store a value in the contract + let res = await helpers.signAndSend({ + from: conf.eoa.address, + to: contractAddress, + data: updateData, + value: '0', + gasPrice: conf.minGasPrice, + }) + assert.equal(res.receipt.status, conf.successStatus) + + // check the new value on contract + result = await web3.eth.call({ to: contractAddress, data: callRetrieve }, "latest") + assert.equal(result, initValue) + + // update the value on the contract + newValue = 100 + updateData = deployed.contract.methods.store(newValue).encodeABI() + // store a value in the contract + res = await helpers.signAndSend({ + from: conf.eoa.address, + to: contractAddress, + data: updateData, + value: '0', + gasPrice: conf.minGasPrice, + }) + assert.equal(res.receipt.status, conf.successStatus) + + stored = await web3.eth.getStorageAt(contractAddress, slot, res.receipt.blockNumber) + value = web3.utils.hexToNumberString(stored) + assert.equal(value, '100') + + stored = await web3.eth.getStorageAt(contractAddress, slot, res.receipt.blockNumber - 1n) + value = web3.utils.hexToNumberString(stored) + assert.equal(value, '1337') + + stored = await web3.eth.getStorageAt(contractAddress, slot, res.receipt.blockNumber - 2n) + value = web3.utils.hexToNumberString(stored) + assert.equal(value, '1337') + + stored = await web3.eth.getStorageAt(contractAddress, slot, res.receipt.blockNumber - 3n) + value = web3.utils.hexToNumberString(stored) + assert.equal(value, '0') + + // check the new value on contract + result = await web3.eth.call({ to: contractAddress, data: callRetrieve }, "latest") + assert.equal(result, newValue) + + // make sure receipts and txs are indexed + latestHeight = await web3.eth.getBlockNumber() + let updateTx = await web3.eth.getTransactionFromBlock(latestHeight, 0) + let updateRcp = await web3.eth.getTransactionReceipt(updateTx.hash) + assert.equal(updateRcp.status, conf.successStatus) + assert.equal(updateTx.data, updateData) + + // check that call can handle specific block heights + result = await web3.eth.call({ to: contractAddress, data: callRetrieve }, latestHeight - 1n) + assert.equal(result, initValue) + + // submit a transaction that emits logs + res = await helpers.signAndSend({ + from: conf.eoa.address, + to: contractAddress, + data: deployed.contract.methods.sum(100, 200).encodeABI(), + gas: 1_000_000, + gasPrice: conf.minGasPrice + }) + assert.equal(res.receipt.status, conf.successStatus) + + // assert that logsBloom from transaction receipt and block match + latestHeight = await web3.eth.getBlockNumber() + let block = await web3.eth.getBlock(latestHeight) + assert.equal(block.logsBloom, res.receipt.logsBloom) + + // check that revert reason for custom error is correctly returned for signed transaction + try { + let callCustomError = deployed.contract.methods.customError().encodeABI() + result = await helpers.signAndSend({ + from: conf.eoa.address, + to: contractAddress, + data: callCustomError, + gas: 1_000_000, + gasPrice: conf.minGasPrice + }) + } catch (error) { + assert.equal(error.reason, 'execution reverted') + assert.equal(error.signature, '0x9195785a') + assert.equal( + error.data, + '00000000000000000000000000000000000000000000000000000000000000050000000000000000000000000000000000000000000000000000000000000040000000000000000000000000000000000000000000000000000000000000001056616c756520697320746f6f206c6f7700000000000000000000000000000000' + ) + } + + // check that revert reason for custom error is correctly returned for contract call + // and it is properly ABI decoded. + try { + result = await deployed.contract.methods.customError().call({ from: conf.eoa.address }) + } catch (err) { + let error = err.innerError + assert.equal( + error.data, + '0x9195785a00000000000000000000000000000000000000000000000000000000000000050000000000000000000000000000000000000000000000000000000000000040000000000000000000000000000000000000000000000000000000000000001056616c756520697320746f6f206c6f7700000000000000000000000000000000' + ) + assert.equal(error.errorName, 'MyCustomError') + assert.equal(error.errorSignature, 'MyCustomError(uint256,string)') + assert.equal(error.errorArgs.value, 5n) + assert.equal(error.errorArgs.message, 'Value is too low') + } + + // check that assertion error is correctly returned for signed transaction + try { + let callAssertError = deployed.contract.methods.assertError().encodeABI() + result = await helpers.signAndSend({ + from: conf.eoa.address, + to: contractAddress, + data: callAssertError, + gas: 1_000_000, + gasPrice: conf.minGasPrice + }) + } catch (error) { + assert.equal(error.reason, 'execution reverted: Assert Error Message') + assert.equal(error.signature, '0x08c379a0') + assert.equal( + error.data, + '00000000000000000000000000000000000000000000000000000000000000200000000000000000000000000000000000000000000000000000000000000014417373657274204572726f72204d657373616765000000000000000000000000' + ) + } + + // check that assertion error is correctly returned for contract call + // and it is properly ABI decoded. + try { + result = await deployed.contract.methods.assertError().call({ from: conf.eoa.address }) + } catch (err) { + let error = err.innerError + assert.equal( + error.message, + 'execution reverted: Assert Error Message' + ) + assert.equal( + error.data, + '0x08c379a000000000000000000000000000000000000000000000000000000000000000200000000000000000000000000000000000000000000000000000000000000014417373657274204572726f72204d657373616765000000000000000000000000' + ) + } + + // check that revert reason for custom error is correctly returned for gas estimation + try { + let callCustomError = deployed.contract.methods.customError().encodeABI() + result = await web3.eth.estimateGas({ + from: conf.eoa.address, + to: contractAddress, + data: callCustomError, + gas: 1_000_000, + gasPrice: conf.minGasPrice + }) + } catch (error) { + assert.equal(error.innerError.message, 'execution reverted') + assert.equal( + error.innerError.data, + '0x9195785a00000000000000000000000000000000000000000000000000000000000000050000000000000000000000000000000000000000000000000000000000000040000000000000000000000000000000000000000000000000000000000000001056616c756520697320746f6f206c6f7700000000000000000000000000000000' + ) + } + + // check that assertion error is correctly returned for gas estimation + try { + let callAssertError = deployed.contract.methods.assertError().encodeABI() + result = await web3.eth.estimateGas({ + from: conf.eoa.address, + to: contractAddress, + data: callAssertError, + gas: 1_000_000, + gasPrice: conf.minGasPrice + }) + } catch (error) { + assert.equal(error.innerError.message, 'execution reverted: Assert Error Message') + assert.equal( + error.innerError.data, + '0x08c379a000000000000000000000000000000000000000000000000000000000000000200000000000000000000000000000000000000000000000000000000000000014417373657274204572726f72204d657373616765000000000000000000000000' + ) + } + + let gasEstimate = await web3.eth.estimateGas( + { + from: conf.eoa.address, + to: contractAddress, + data: deployed.contract.methods.sum(100, 200).encodeABI(), + gas: 1_000_000, + gasPrice: 0 + }, + '0x1' + ) + assert.equal(gasEstimate, 23977n) + + gasEstimate = await web3.eth.estimateGas( + { + from: conf.eoa.address, + to: contractAddress, + data: deployed.contract.methods.sum(100, 200).encodeABI(), + gas: 1_000_000, + gasPrice: 0 + }, + 'latest' + ) + assert.equal(gasEstimate, 27398n) + +}) diff --git a/tests/web3js/eth_rate_limit_test.js b/tests/web3js/eth_rate_limit_test.js index 4bcdce3f5..f886586de 100644 --- a/tests/web3js/eth_rate_limit_test.js +++ b/tests/web3js/eth_rate_limit_test.js @@ -10,10 +10,10 @@ it('rate limit after X requests', async function () { await new Promise(res => setTimeout(res, 1500)) // this should be synced with the value on server config - let requestLimit = 50 + let requestLimit = 500 let requestsMade = 0 let requestsFailed = 0 - let requests = 60 + let requests = 1000 for (let i = 0; i < requests; i++) { try { diff --git a/tests/web3js/eth_revert_reason_test.js b/tests/web3js/eth_revert_reason_test.js index 4578abfad..75113160a 100644 --- a/tests/web3js/eth_revert_reason_test.js +++ b/tests/web3js/eth_revert_reason_test.js @@ -40,18 +40,19 @@ it('store revertReason field in transaction receipts', async () => { [signedTx.rawTransaction] ) assert.equal(200, response.status) + let txHash = response.body.result - let latestHeight = await web3.eth.getBlockNumber() - let block = await web3.eth.getBlock(latestHeight) - assert.equal(block.number, conf.startBlockHeight + 2n) + let rcp = null + while (rcp == null) { + rcp = await helpers.callRPCMethod( + 'eth_getTransactionReceipt', + [txHash] + ) + if (rcp.body.result == null) { + rcp = null + } + } - let revertedTx = await web3.eth.getTransactionFromBlock(latestHeight, 0) - // Give some time to the engine to ingest the latest transaction - await new Promise(res => setTimeout(res, 1500)) - rcp = await helpers.callRPCMethod( - 'eth_getTransactionReceipt', - [revertedTx.hash] - ) // make sure the `revertReason` field is included in the response assert.equal( rcp.body['result'].revertReason, @@ -74,22 +75,22 @@ it('store revertReason field in transaction receipts', async () => { [signedTx.rawTransaction] ) assert.equal(200, response.status) + txHash = response.body.result - latestHeight = await web3.eth.getBlockNumber() - block = await web3.eth.getBlock(latestHeight) - assert.equal(block.number, conf.startBlockHeight + 3n) + rcp = null + while (rcp == null) { + rcp = await helpers.callRPCMethod( + 'eth_getTransactionReceipt', + [txHash] + ) + if (rcp.body.result == null) { + rcp = null + } + } - revertedTx = await web3.eth.getTransactionFromBlock(latestHeight, 0) - // Give some time to the engine to ingest the latest transaction - await new Promise(res => setTimeout(res, 1500)) - rcp = await helpers.callRPCMethod( - 'eth_getTransactionReceipt', - [revertedTx.hash] - ) // make sure the `revertReason` field is included in the response assert.equal( rcp.body['result'].revertReason, '0x9195785a00000000000000000000000000000000000000000000000000000000000000050000000000000000000000000000000000000000000000000000000000000040000000000000000000000000000000000000000000000000000000000000001056616c756520697320746f6f206c6f7700000000000000000000000000000000' ) - }) diff --git a/tests/web3js/verify_cadence_arch_calls_test.js b/tests/web3js/verify_cadence_arch_calls_test.js new file mode 100644 index 000000000..7188cfac3 --- /dev/null +++ b/tests/web3js/verify_cadence_arch_calls_test.js @@ -0,0 +1,148 @@ +const utils = require('web3-utils') +const { assert } = require('chai') +const conf = require('./config') +const helpers = require('./helpers') +const web3 = conf.web3 + +it('should be able to use Cadence Arch calls', async () => { + let latest = await web3.eth.getBlockNumber() + let expectedBlockHeight = conf.startBlockHeight + assert.equal(latest, expectedBlockHeight) + + let deployed = await helpers.deployContract('storage') + let contractAddress = deployed.receipt.contractAddress + + // submit a transaction that calls blockNumber() + let blockNumberData = deployed.contract.methods.blockNumber().encodeABI() + let res = await helpers.signAndSend({ + from: conf.eoa.address, + to: contractAddress, + data: blockNumberData, + value: '0', + gasPrice: conf.minGasPrice, + }) + assert.equal(res.receipt.status, conf.successStatus) + + // submit a transaction that calls blockTime() + let blockTimeData = deployed.contract.methods.blockNumber().encodeABI() + res = await helpers.signAndSend({ + from: conf.eoa.address, + to: contractAddress, + data: blockTimeData, + value: '0', + gasPrice: conf.minGasPrice, + }) + assert.equal(res.receipt.status, conf.successStatus) + + // submit a transaction that calls blockHash(uint num) + let blockHashData = deployed.contract.methods.blockHash(110).encodeABI() + res = await helpers.signAndSend({ + from: conf.eoa.address, + to: contractAddress, + data: blockHashData, + value: '0', + gasPrice: conf.minGasPrice, + }) + assert.equal(res.receipt.status, conf.successStatus) + + // submit a transaction that calls random() + let randomData = deployed.contract.methods.random().encodeABI() + res = await helpers.signAndSend({ + from: conf.eoa.address, + to: contractAddress, + data: randomData, + value: '0', + gasPrice: conf.minGasPrice, + }) + assert.equal(res.receipt.status, conf.successStatus) + + // submit a transaction that calls chainID() + let chainIDData = deployed.contract.methods.chainID().encodeABI() + res = await helpers.signAndSend({ + from: conf.eoa.address, + to: contractAddress, + data: chainIDData, + value: '0', + gasPrice: conf.minGasPrice, + }) + assert.equal(res.receipt.status, conf.successStatus) + + // submit a transaction that calls verifyArchCallToRandomSource(uint64 height) + let getRandomSourceData = deployed.contract.methods.verifyArchCallToRandomSource(120).encodeABI() + res = await helpers.signAndSend({ + from: conf.eoa.address, + to: contractAddress, + data: getRandomSourceData, + value: '0', + gasPrice: conf.minGasPrice, + }) + assert.equal(res.receipt.status, conf.successStatus) + + // make a contract call for verifyArchCallToRandomSource(uint64 height) + res = await web3.eth.call({ to: contractAddress, data: getRandomSourceData }, latest) + assert.notEqual( + res, + '0x0000000000000000000000000000000000000000000000000000000000000000' + ) + assert.lengthOf(res, 66) + + // submit a transaction that calls verifyArchCallToRevertibleRandom() + let revertibleRandomData = deployed.contract.methods.verifyArchCallToRevertibleRandom().encodeABI() + res = await helpers.signAndSend({ + from: conf.eoa.address, + to: contractAddress, + data: revertibleRandomData, + value: '0', + gasPrice: conf.minGasPrice, + }) + assert.equal(res.receipt.status, conf.successStatus) + + // make a contract call for verifyArchCallToRevertibleRandom() + res = await web3.eth.call({ to: contractAddress, data: revertibleRandomData }, latest) + assert.notEqual( + res, + '0x0000000000000000000000000000000000000000000000000000000000000000' + ) + assert.lengthOf(res, 66) + + // submit a transaction that calls verifyArchCallToFlowBlockHeight() + let flowBlockHeightData = deployed.contract.methods.verifyArchCallToFlowBlockHeight().encodeABI() + res = await helpers.signAndSend({ + from: conf.eoa.address, + to: contractAddress, + data: flowBlockHeightData, + value: '0', + gasPrice: conf.minGasPrice, + }) + assert.equal(res.receipt.status, conf.successStatus) + + // make a contract call for verifyArchCallToFlowBlockHeight() + res = await web3.eth.call({ to: contractAddress, data: flowBlockHeightData }, latest) + assert.equal( + web3.eth.abi.decodeParameter('uint64', res), + latest, + ) + + // submit a transaction that calls verifyArchCallToVerifyCOAOwnershipProof(address,bytes32,bytes) + let tx = await web3.eth.getTransactionFromBlock(conf.startBlockHeight, 1) + let verifyCOAOwnershipProofData = deployed.contract.methods.verifyArchCallToVerifyCOAOwnershipProof( + tx.to, + '0x1bacdb569847f31ade07e83d6bb7cefba2b9290b35d5c2964663215e73519cff', + web3.utils.hexToBytes('f853c18088f8d6e0586b0a20c78365766df842b840b90448f4591df2639873be2914c5560149318b7e2fcf160f7bb8ed13cfd97be2f54e6889606f18e50b2c37308386f840e03a9fff915f57b2164cba27f0206a95') + ).encodeABI() + res = await helpers.signAndSend({ + from: conf.eoa.address, + to: contractAddress, + data: verifyCOAOwnershipProofData, + value: '0', + gasPrice: conf.minGasPrice, + }) + assert.equal(res.receipt.status, conf.successStatus) + + // make a contract call for verifyArchCallToVerifyCOAOwnershipProof(address,bytes32,bytes) + res = await web3.eth.call({ to: contractAddress, data: verifyCOAOwnershipProofData }, latest) + assert.equal( + web3.eth.abi.decodeParameter('bool', res), + false, + ) +}) From 97ba7d7355a20d756b140494eb32d09b8f576556 Mon Sep 17 00:00:00 2001 From: Ardit Marku Date: Mon, 4 Nov 2024 10:34:52 +0200 Subject: [PATCH 010/174] Simplify added test files --- tests/e2e_web3js_test.go | 2 +- tests/helpers.go | 2 +- tests/web3js/eth_get_storage_at_test.js | 239 ++---------------- tests/web3js/eth_revert_reason_test.js | 4 + .../web3js/verify_cadence_arch_calls_test.js | 56 ---- 5 files changed, 32 insertions(+), 271 deletions(-) diff --git a/tests/e2e_web3js_test.go b/tests/e2e_web3js_test.go index 7f68e0949..57f06e808 100644 --- a/tests/e2e_web3js_test.go +++ b/tests/e2e_web3js_test.go @@ -66,7 +66,7 @@ func TestWeb3_E2E(t *testing.T) { runWeb3Test(t, "eth_deploy_contract_and_interact_test") }) - t.Run("test eth_getStorageAt", func(t *testing.T) { + t.Run("test retrieval of contract storage slots", func(t *testing.T) { runWeb3Test(t, "eth_get_storage_at_test") }) diff --git a/tests/helpers.go b/tests/helpers.go index 7c644c7f7..600922f8d 100644 --- a/tests/helpers.go +++ b/tests/helpers.go @@ -177,7 +177,7 @@ func servicesSetup(t *testing.T) (emulator.Emulator, func()) { // and will report failure or success of the test. func executeTest(t *testing.T, testFile string) { command := fmt.Sprintf( - "./web3js/node_modules/.bin/mocha ./web3js/%s.js --timeout 360s", + "./web3js/node_modules/.bin/mocha ./web3js/%s.js --timeout 150s", testFile, ) parts := strings.Fields(command) diff --git a/tests/web3js/eth_get_storage_at_test.js b/tests/web3js/eth_get_storage_at_test.js index 2969f0aa8..b1a5237cd 100644 --- a/tests/web3js/eth_get_storage_at_test.js +++ b/tests/web3js/eth_get_storage_at_test.js @@ -3,55 +3,23 @@ const conf = require('./config') const helpers = require('./helpers') const web3 = conf.web3 -it('deploy contract and interact', async () => { +it('should retrieve storage slots of contracts', async () => { let deployed = await helpers.deployContract('storage') let contractAddress = deployed.receipt.contractAddress // make sure deploy results are correct assert.equal(deployed.receipt.status, conf.successStatus) - assert.isString(deployed.receipt.transactionHash) - assert.isString(contractAddress) - assert.equal(deployed.receipt.from, conf.eoa.address) - assert.isUndefined(deployed.receipt.to) - - let rcp = await web3.eth.getTransactionReceipt(deployed.receipt.transactionHash) - assert.equal(rcp.contractAddress, contractAddress) - assert.equal(rcp.status, conf.successStatus) - assert.isUndefined(rcp.to) - assert.equal(rcp.gasUsed, 1130512n) - assert.equal(rcp.gasUsed, rcp.cumulativeGasUsed) - - // check if latest block contains the deploy results - let latestHeight = await web3.eth.getBlockNumber() - let deployTx = await web3.eth.getTransactionFromBlock(latestHeight, 0) - assert.equal(deployTx.hash, deployed.receipt.transactionHash) - assert.isUndefined(deployTx.to) - - // check that getCode supports specific block heights - let code = await web3.eth.getCode(contractAddress, latestHeight - 1n) - assert.equal(code, '0x') // empty at previous height - - code = await web3.eth.getCode(contractAddress) - // deploy data has more than just the contract - // since it contains the initialization code, - // but subset of the data is the contract code - assert.isTrue(deployTx.data.includes(code.replace('0x', ''))) - - let deployReceipt = await web3.eth.getTransactionReceipt(deployed.receipt.transactionHash) - assert.deepEqual(deployReceipt, deployed.receipt) // get the default deployed value on contract - const initValue = 1337 let callRetrieve = await deployed.contract.methods.retrieve().encodeABI() - result = await web3.eth.call({ to: contractAddress, data: callRetrieve }, 'latest') - assert.equal(result, initValue) + let result = await web3.eth.call({ to: contractAddress, data: callRetrieve }, 'latest') let slot = 0 // The slot for the 'number' variable - let stored = await web3.eth.getStorageAt(contractAddress, slot, latestHeight) - let value = web3.utils.hexToNumberString(stored) - assert.equal(value, initValue) + let stored = await web3.eth.getStorageAt(contractAddress, slot, 'latest') + assert.equal(stored, result) // set the value on the contract, to its current value + let initValue = 1337 let updateData = deployed.contract.methods.store(initValue).encodeABI() // store a value in the contract let res = await helpers.signAndSend({ @@ -64,7 +32,7 @@ it('deploy contract and interact', async () => { assert.equal(res.receipt.status, conf.successStatus) // check the new value on contract - result = await web3.eth.call({ to: contractAddress, data: callRetrieve }, "latest") + result = await web3.eth.call({ to: contractAddress, data: callRetrieve }, 'latest') assert.equal(result, initValue) // update the value on the contract @@ -80,180 +48,25 @@ it('deploy contract and interact', async () => { }) assert.equal(res.receipt.status, conf.successStatus) - stored = await web3.eth.getStorageAt(contractAddress, slot, res.receipt.blockNumber) - value = web3.utils.hexToNumberString(stored) - assert.equal(value, '100') - - stored = await web3.eth.getStorageAt(contractAddress, slot, res.receipt.blockNumber - 1n) - value = web3.utils.hexToNumberString(stored) - assert.equal(value, '1337') - - stored = await web3.eth.getStorageAt(contractAddress, slot, res.receipt.blockNumber - 2n) - value = web3.utils.hexToNumberString(stored) - assert.equal(value, '1337') - - stored = await web3.eth.getStorageAt(contractAddress, slot, res.receipt.blockNumber - 3n) - value = web3.utils.hexToNumberString(stored) - assert.equal(value, '0') - - // check the new value on contract - result = await web3.eth.call({ to: contractAddress, data: callRetrieve }, "latest") - assert.equal(result, newValue) - - // make sure receipts and txs are indexed - latestHeight = await web3.eth.getBlockNumber() - let updateTx = await web3.eth.getTransactionFromBlock(latestHeight, 0) - let updateRcp = await web3.eth.getTransactionReceipt(updateTx.hash) - assert.equal(updateRcp.status, conf.successStatus) - assert.equal(updateTx.data, updateData) - - // check that call can handle specific block heights - result = await web3.eth.call({ to: contractAddress, data: callRetrieve }, latestHeight - 1n) - assert.equal(result, initValue) - - // submit a transaction that emits logs - res = await helpers.signAndSend({ - from: conf.eoa.address, - to: contractAddress, - data: deployed.contract.methods.sum(100, 200).encodeABI(), - gas: 1_000_000, - gasPrice: conf.minGasPrice - }) - assert.equal(res.receipt.status, conf.successStatus) - - // assert that logsBloom from transaction receipt and block match - latestHeight = await web3.eth.getBlockNumber() - let block = await web3.eth.getBlock(latestHeight) - assert.equal(block.logsBloom, res.receipt.logsBloom) - - // check that revert reason for custom error is correctly returned for signed transaction - try { - let callCustomError = deployed.contract.methods.customError().encodeABI() - result = await helpers.signAndSend({ - from: conf.eoa.address, - to: contractAddress, - data: callCustomError, - gas: 1_000_000, - gasPrice: conf.minGasPrice - }) - } catch (error) { - assert.equal(error.reason, 'execution reverted') - assert.equal(error.signature, '0x9195785a') - assert.equal( - error.data, - '00000000000000000000000000000000000000000000000000000000000000050000000000000000000000000000000000000000000000000000000000000040000000000000000000000000000000000000000000000000000000000000001056616c756520697320746f6f206c6f7700000000000000000000000000000000' - ) - } - - // check that revert reason for custom error is correctly returned for contract call - // and it is properly ABI decoded. - try { - result = await deployed.contract.methods.customError().call({ from: conf.eoa.address }) - } catch (err) { - let error = err.innerError - assert.equal( - error.data, - '0x9195785a00000000000000000000000000000000000000000000000000000000000000050000000000000000000000000000000000000000000000000000000000000040000000000000000000000000000000000000000000000000000000000000001056616c756520697320746f6f206c6f7700000000000000000000000000000000' - ) - assert.equal(error.errorName, 'MyCustomError') - assert.equal(error.errorSignature, 'MyCustomError(uint256,string)') - assert.equal(error.errorArgs.value, 5n) - assert.equal(error.errorArgs.message, 'Value is too low') - } - - // check that assertion error is correctly returned for signed transaction - try { - let callAssertError = deployed.contract.methods.assertError().encodeABI() - result = await helpers.signAndSend({ - from: conf.eoa.address, - to: contractAddress, - data: callAssertError, - gas: 1_000_000, - gasPrice: conf.minGasPrice - }) - } catch (error) { - assert.equal(error.reason, 'execution reverted: Assert Error Message') - assert.equal(error.signature, '0x08c379a0') - assert.equal( - error.data, - '00000000000000000000000000000000000000000000000000000000000000200000000000000000000000000000000000000000000000000000000000000014417373657274204572726f72204d657373616765000000000000000000000000' - ) - } - - // check that assertion error is correctly returned for contract call - // and it is properly ABI decoded. - try { - result = await deployed.contract.methods.assertError().call({ from: conf.eoa.address }) - } catch (err) { - let error = err.innerError - assert.equal( - error.message, - 'execution reverted: Assert Error Message' - ) - assert.equal( - error.data, - '0x08c379a000000000000000000000000000000000000000000000000000000000000000200000000000000000000000000000000000000000000000000000000000000014417373657274204572726f72204d657373616765000000000000000000000000' - ) - } - - // check that revert reason for custom error is correctly returned for gas estimation - try { - let callCustomError = deployed.contract.methods.customError().encodeABI() - result = await web3.eth.estimateGas({ - from: conf.eoa.address, - to: contractAddress, - data: callCustomError, - gas: 1_000_000, - gasPrice: conf.minGasPrice - }) - } catch (error) { - assert.equal(error.innerError.message, 'execution reverted') - assert.equal( - error.innerError.data, - '0x9195785a00000000000000000000000000000000000000000000000000000000000000050000000000000000000000000000000000000000000000000000000000000040000000000000000000000000000000000000000000000000000000000000001056616c756520697320746f6f206c6f7700000000000000000000000000000000' - ) - } - - // check that assertion error is correctly returned for gas estimation - try { - let callAssertError = deployed.contract.methods.assertError().encodeABI() - result = await web3.eth.estimateGas({ - from: conf.eoa.address, - to: contractAddress, - data: callAssertError, - gas: 1_000_000, - gasPrice: conf.minGasPrice - }) - } catch (error) { - assert.equal(error.innerError.message, 'execution reverted: Assert Error Message') - assert.equal( - error.innerError.data, - '0x08c379a000000000000000000000000000000000000000000000000000000000000000200000000000000000000000000000000000000000000000000000000000000014417373657274204572726f72204d657373616765000000000000000000000000' - ) - } - - let gasEstimate = await web3.eth.estimateGas( - { - from: conf.eoa.address, - to: contractAddress, - data: deployed.contract.methods.sum(100, 200).encodeABI(), - gas: 1_000_000, - gasPrice: 0 - }, - '0x1' - ) - assert.equal(gasEstimate, 23977n) - - gasEstimate = await web3.eth.estimateGas( - { - from: conf.eoa.address, - to: contractAddress, - data: deployed.contract.methods.sum(100, 200).encodeABI(), - gas: 1_000_000, - gasPrice: 0 - }, - 'latest' - ) - assert.equal(gasEstimate, 27398n) + let latestHeight = await web3.eth.getBlockNumber() + // assert the storage slot on latest block + stored = await web3.eth.getStorageAt(contractAddress, slot, latestHeight) + value = web3.eth.abi.decodeParameter('uint256', stored) + assert.equal(value, 100n) + + // // assert the storage slot on previous block + stored = await web3.eth.getStorageAt(contractAddress, slot, latestHeight - 1n) + value = web3.eth.abi.decodeParameter('uint256', stored) + assert.equal(value, 1337n) + + // assert the storage slot on block of contract deployment + stored = await web3.eth.getStorageAt(contractAddress, slot, deployed.receipt.blockNumber) + value = web3.eth.abi.decodeParameter('uint256', stored) + assert.equal(value, 1337n) + + // assert the storage slot on block prior to contract deployment + stored = await web3.eth.getStorageAt(contractAddress, slot, deployed.receipt.blockNumber - 1n) + value = web3.eth.abi.decodeParameter('uint256', stored) + assert.equal(value, 0n) }) diff --git a/tests/web3js/eth_revert_reason_test.js b/tests/web3js/eth_revert_reason_test.js index 75113160a..fa6617885 100644 --- a/tests/web3js/eth_revert_reason_test.js +++ b/tests/web3js/eth_revert_reason_test.js @@ -43,6 +43,8 @@ it('store revertReason field in transaction receipts', async () => { let txHash = response.body.result let rcp = null + // wait until the transaction is executed & indexed, and its + // receipt becomes available. while (rcp == null) { rcp = await helpers.callRPCMethod( 'eth_getTransactionReceipt', @@ -78,6 +80,8 @@ it('store revertReason field in transaction receipts', async () => { txHash = response.body.result rcp = null + // wait until the transaction is executed & indexed, and its + // receipt becomes available. while (rcp == null) { rcp = await helpers.callRPCMethod( 'eth_getTransactionReceipt', diff --git a/tests/web3js/verify_cadence_arch_calls_test.js b/tests/web3js/verify_cadence_arch_calls_test.js index 7188cfac3..f52496434 100644 --- a/tests/web3js/verify_cadence_arch_calls_test.js +++ b/tests/web3js/verify_cadence_arch_calls_test.js @@ -1,4 +1,3 @@ -const utils = require('web3-utils') const { assert } = require('chai') const conf = require('./config') const helpers = require('./helpers') @@ -12,61 +11,6 @@ it('should be able to use Cadence Arch calls', async () => { let deployed = await helpers.deployContract('storage') let contractAddress = deployed.receipt.contractAddress - // submit a transaction that calls blockNumber() - let blockNumberData = deployed.contract.methods.blockNumber().encodeABI() - let res = await helpers.signAndSend({ - from: conf.eoa.address, - to: contractAddress, - data: blockNumberData, - value: '0', - gasPrice: conf.minGasPrice, - }) - assert.equal(res.receipt.status, conf.successStatus) - - // submit a transaction that calls blockTime() - let blockTimeData = deployed.contract.methods.blockNumber().encodeABI() - res = await helpers.signAndSend({ - from: conf.eoa.address, - to: contractAddress, - data: blockTimeData, - value: '0', - gasPrice: conf.minGasPrice, - }) - assert.equal(res.receipt.status, conf.successStatus) - - // submit a transaction that calls blockHash(uint num) - let blockHashData = deployed.contract.methods.blockHash(110).encodeABI() - res = await helpers.signAndSend({ - from: conf.eoa.address, - to: contractAddress, - data: blockHashData, - value: '0', - gasPrice: conf.minGasPrice, - }) - assert.equal(res.receipt.status, conf.successStatus) - - // submit a transaction that calls random() - let randomData = deployed.contract.methods.random().encodeABI() - res = await helpers.signAndSend({ - from: conf.eoa.address, - to: contractAddress, - data: randomData, - value: '0', - gasPrice: conf.minGasPrice, - }) - assert.equal(res.receipt.status, conf.successStatus) - - // submit a transaction that calls chainID() - let chainIDData = deployed.contract.methods.chainID().encodeABI() - res = await helpers.signAndSend({ - from: conf.eoa.address, - to: contractAddress, - data: chainIDData, - value: '0', - gasPrice: conf.minGasPrice, - }) - assert.equal(res.receipt.status, conf.successStatus) - // submit a transaction that calls verifyArchCallToRandomSource(uint64 height) let getRandomSourceData = deployed.contract.methods.verifyArchCallToRandomSource(120).encodeABI() res = await helpers.signAndSend({ From 4be3166608590e20520942238ed92df6f29c5b33 Mon Sep 17 00:00:00 2001 From: Ardit Marku Date: Mon, 4 Nov 2024 12:41:20 +0200 Subject: [PATCH 011/174] Simplify usage of FixedHash field on Block type --- models/block.go | 35 ++++++++++-------------- models/block_test.go | 16 ++++++++--- tests/web3js/eth_non_interactive_test.js | 2 +- 3 files changed, 27 insertions(+), 26 deletions(-) diff --git a/models/block.go b/models/block.go index 5f312f361..3282578ff 100644 --- a/models/block.go +++ b/models/block.go @@ -28,8 +28,7 @@ func GenesisBlock(chainID flow.ChainID) *Block { func NewBlockFromBytes(data []byte) (*Block, error) { var b *Block - err := rlp.DecodeBytes(data, &b) - if err != nil { + if err := rlp.DecodeBytes(data, &b); err != nil { pastBlock, err := decodeBlockBreakingChanges(data) if err != nil { return nil, err @@ -37,10 +36,6 @@ func NewBlockFromBytes(data []byte) (*Block, error) { b = pastBlock } - // this is added because RLP decoding will decode into an empty string - if b.FixedHash != nil && *b.FixedHash == "" { - b.FixedHash = nil - } return b, nil } @@ -51,10 +46,8 @@ type Block struct { // will have more fields than before, so we make sure the hash we calculated // with the previous format is fixed by assigning it to this field and then // on hash calculation we check if this field is set we just return it. - // We must make the FixedHash exported so RLP encoding preserve it, and - // we must use string not common.Hash since RLP decoding has an issue - // with decoding into nil pointer slice. - FixedHash *string + // We must make the FixedHash exported so RLP encoding preserves it. + FixedHash gethCommon.Hash TransactionHashes []gethCommon.Hash } @@ -63,8 +56,8 @@ func (b *Block) ToBytes() ([]byte, error) { } func (b *Block) Hash() (gethCommon.Hash, error) { - if b.FixedHash != nil && *b.FixedHash != "" { - return gethCommon.HexToHash(*b.FixedHash), nil + if b.FixedHash != zeroGethHash { + return b.FixedHash, nil } return b.Block.Hash() } @@ -81,13 +74,12 @@ func decodeBlockEvent(event cadence.Event) (*Block, *events.BlockEventPayload, e ) } - var fixedHash *string + fixedHash := gethCommon.Hash{} // If the `PrevRandao` field is the zero hash, we know that // this is a block with the legacy format, and we need to // fix its hash, due to the hash calculation breaking change. if payload.PrevRandao == zeroGethHash { - hash := payload.Hash.String() - fixedHash = &hash + fixedHash = payload.Hash } return &Block{ @@ -115,7 +107,10 @@ type blockV0 struct { // the fields from the blockV0Fields type. func (b *blockV0) Hash() (gethCommon.Hash, error) { data, err := b.Block.ToBytes() - return gethCrypto.Keccak256Hash(data), err + if err != nil { + return gethCommon.Hash{}, err + } + return gethCrypto.Keccak256Hash(data), nil } // blockV0Fields needed for decoding & computing the hash of blocks @@ -136,12 +131,11 @@ func (b *blockV0Fields) ToBytes() ([]byte, error) { } // decodeBlockBreakingChanges will try to decode the bytes into all -// previous versions of block type, if it succeeds it will return the +// previous versions of block type. If it succeeds it will return the // migrated block, otherwise it will return the decoding error. func decodeBlockBreakingChanges(encoded []byte) (*Block, error) { b0 := &blockV0{} - err := rlp.DecodeBytes(encoded, b0) - if err != nil { + if err := rlp.DecodeBytes(encoded, b0); err != nil { return nil, err } @@ -149,7 +143,6 @@ func decodeBlockBreakingChanges(encoded []byte) (*Block, error) { if err != nil { return nil, err } - h := blockHash.String() return &Block{ Block: &types.Block{ @@ -161,7 +154,7 @@ func decodeBlockBreakingChanges(encoded []byte) (*Block, error) { TransactionHashRoot: b0.Block.TransactionHashRoot, TotalGasUsed: b0.Block.TotalGasUsed, }, - FixedHash: &h, + FixedHash: blockHash, TransactionHashes: b0.TransactionHashes, }, nil } diff --git a/models/block_test.go b/models/block_test.go index 0dfee6f50..f91c5caf6 100644 --- a/models/block_test.go +++ b/models/block_test.go @@ -32,6 +32,14 @@ func Test_DecodePastBlockFormat(t *testing.T) { block, err := NewBlockFromBytes(blockBytes) require.NoError(t, err) + blockHash, err := block.Hash() + require.NoError(t, err) + + assert.Equal( + t, + gethCommon.HexToHash("0xcad79e3019da8014f623f351f01c88d1bcb4613352d4801548c6b07992fd1393"), + blockHash, + ) assert.Equal( t, gethCommon.HexToHash("0x05aa4a6edbcf6fa81178566596be1c7fff7b721615c8b3bbd14ff76d9c81ec9b"), @@ -66,12 +74,12 @@ func Test_DecodePastBlockFormat(t *testing.T) { } func Test_FixedHashBlock(t *testing.T) { - fixed := gethCommon.HexToHash("0x2").String() + fixed := gethCommon.HexToHash("0x2") block := Block{ Block: &types.Block{ Height: 1, }, - FixedHash: &fixed, + FixedHash: fixed, TransactionHashes: []gethCommon.Hash{ gethCommon.HexToHash("0x3"), gethCommon.HexToHash("0x4"), @@ -80,7 +88,7 @@ func Test_FixedHashBlock(t *testing.T) { h, err := block.Hash() require.NoError(t, err) - assert.Equal(t, fixed, h.String()) + assert.Equal(t, fixed, h) data, err := block.ToBytes() require.NoError(t, err) @@ -91,7 +99,7 @@ func Test_FixedHashBlock(t *testing.T) { // make sure fixed hash and transaction hashes persists after decoding h, err = decoded.Hash() require.NoError(t, err) - require.Equal(t, fixed, h.String()) + require.Equal(t, fixed, h) require.Equal(t, block.TransactionHashes, decoded.TransactionHashes) } diff --git a/tests/web3js/eth_non_interactive_test.js b/tests/web3js/eth_non_interactive_test.js index 8cde10279..a260bc83c 100644 --- a/tests/web3js/eth_non_interactive_test.js +++ b/tests/web3js/eth_non_interactive_test.js @@ -27,7 +27,7 @@ it('get block', async () => { block.transactionsRoot, '0x0000000000000000000000000000000000000000000000000000000000000000' ) - assert.equal(block.size, 3995n) + assert.equal(block.size, 4028n) assert.equal(block.gasLimit, 120000000n) assert.equal(block.miner, '0x0000000000000000000000030000000000000000') assert.equal( From 72d2891c5aaae769cdcf7949aadee3f12d39b088 Mon Sep 17 00:00:00 2001 From: Janez Podhostnik Date: Wed, 30 Oct 2024 20:59:37 +0100 Subject: [PATCH 012/174] add register storage --- storage/pebble/register.go | 126 +++++++++++++++++++++++++++++ storage/pebble/register_test.go | 135 ++++++++++++++++++++++++++++++++ 2 files changed, 261 insertions(+) create mode 100644 storage/pebble/register.go create mode 100644 storage/pebble/register_test.go diff --git a/storage/pebble/register.go b/storage/pebble/register.go new file mode 100644 index 000000000..f66cd4f35 --- /dev/null +++ b/storage/pebble/register.go @@ -0,0 +1,126 @@ +package pebble + +import ( + "fmt" + + "github.com/cockroachdb/pebble" + "github.com/onflow/atree" + + "github.com/onflow/flow-go/fvm/evm/types" +) + +var _ types.BackendStorage = &Register{} + +type Register struct { + store *Storage + height uint64 + batch *pebble.Batch +} + +// NewRegister creates a new index instance at the provided height, all reads and +// writes of the registers will happen at that height. +// this is not concurrency safe. +func NewRegister( + store *Storage, + height uint64, + batch *pebble.Batch, +) *Register { + return &Register{ + store: store, + height: height, + batch: batch, + } +} + +func (r *Register) GetValue(owner, key []byte) ([]byte, error) { + var db pebble.Reader = r.store.db + if r.batch != nil { + db = r.batch + } + + iter, err := db.NewIter(&pebble.IterOptions{ + LowerBound: r.idLower(owner, key), + UpperBound: r.idUpper(owner, key), + }) + if err != nil { + return nil, fmt.Errorf("failed to create register range iterator: %w", err) + } + defer func() { + if err := iter.Close(); err != nil { + r.store.log.Error().Err(err).Msg("failed to close register iterator") + } + }() + + found := iter.Last() + if !found { + // as per interface expectation we need to return nil if not found + return nil, nil + } + + val, err := iter.ValueAndErr() + if err != nil { + return nil, fmt.Errorf( + "failed to get ledger value at owner %x and key %x: %w", + owner, + key, + err, + ) + } + + return val, nil +} + +func (r *Register) SetValue(owner, key, value []byte) error { + id := r.id(owner, key) + if err := r.store.set(ledgerValue, id, value, r.batch); err != nil { + return fmt.Errorf( + "failed to store ledger value for owner %x and key %x: %w", + owner, + key, + err, + ) + } + + return nil +} + +func (r *Register) ValueExists(owner, key []byte) (bool, error) { + val, err := r.GetValue(owner, key) + if err != nil { + return false, err + } + + return len(val) == 0, nil +} + +func (r *Register) AllocateSlabIndex(_ []byte) (atree.SlabIndex, error) { + return atree.SlabIndexUndefined, fmt.Errorf( + "unexpected call to allocate slab index", + ) +} + +// id calculates a ledger id with embedded block height for owner and key. +// The key for a register has the following schema: +// {owner}{key}{height} +func (r *Register) id(owner, key []byte) []byte { + id := append(owner, key...) + h := uint64Bytes(r.height) + return append(id, h...) +} + +func (r *Register) idUpper(owner, key []byte) []byte { + id := []byte{ledgerValue} + id = append(id, owner...) + id = append(id, key...) + // increase height +1 because upper bound is exclusive + h := uint64Bytes(r.height + 1) + return append(id, h...) +} + +func (r *Register) idLower(owner, key []byte) []byte { + id := []byte{ledgerValue} + id = append(id, owner...) + id = append(id, key...) + // lower height is always 0 + return append(id, uint64Bytes(0)...) +} diff --git a/storage/pebble/register_test.go b/storage/pebble/register_test.go new file mode 100644 index 000000000..9211f8b50 --- /dev/null +++ b/storage/pebble/register_test.go @@ -0,0 +1,135 @@ +package pebble + +import ( + "testing" + + "github.com/stretchr/testify/require" +) + +func Test_Register(t *testing.T) { + owner := []byte{0x01} + key := []byte{0x03} + value1 := []byte{0x05} + value2 := []byte{0x06} + + runDB("get register", t, func(t *testing.T, db *Storage) { + t.Parallel() + + r := NewRegister(db, 0, nil) + + v, err := r.GetValue(owner, key) + require.NoError(t, err) + require.Len(t, v, 0) + }) + + runDB("set register", t, func(t *testing.T, db *Storage) { + t.Parallel() + + r := NewRegister(db, 0, nil) + + err := r.SetValue(owner, key, value1) + require.NoError(t, err) + }) + + runDB("set-get register", t, func(t *testing.T, db *Storage) { + t.Parallel() + + r := NewRegister(db, 0, nil) + + err := r.SetValue(owner, key, value1) + require.NoError(t, err) + + v, err := r.GetValue(owner, key) + require.NoError(t, err) + require.Equal(t, value1, v) + }) + + runDB("set-set-get register", t, func(t *testing.T, db *Storage) { + t.Parallel() + + r := NewRegister(db, 0, nil) + + err := r.SetValue(owner, key, value1) + require.NoError(t, err) + + err = r.SetValue(owner, key, value2) + require.NoError(t, err) + + v, err := r.GetValue(owner, key) + require.NoError(t, err) + require.Equal(t, value2, v) + }) + + runDB("set-unset-get register", t, func(t *testing.T, db *Storage) { + t.Parallel() + + r := NewRegister(db, 0, nil) + + err := r.SetValue(owner, key, value1) + require.NoError(t, err) + + err = r.SetValue(owner, key, nil) + require.NoError(t, err) + + v, err := r.GetValue(owner, key) + require.NoError(t, err) + // not actually nil, but empty + require.Len(t, v, 0) + }) + + runDB("set-next-get register", t, func(t *testing.T, db *Storage) { + t.Parallel() + + r := NewRegister(db, 0, nil) + + err := r.SetValue(owner, key, value1) + require.NoError(t, err) + + r = NewRegister(db, 1, nil) + + v, err := r.GetValue(owner, key) + require.NoError(t, err) + require.Equal(t, value1, v) + }) + + runDB("set-next-set-next-get register", t, func(t *testing.T, db *Storage) { + t.Parallel() + + r := NewRegister(db, 0, nil) + + err := r.SetValue(owner, key, value1) + require.NoError(t, err) + + r = NewRegister(db, 1, nil) + + err = r.SetValue(owner, key, value2) + require.NoError(t, err) + + r = NewRegister(db, 2, nil) + + v, err := r.GetValue(owner, key) + require.NoError(t, err) + require.Equal(t, value2, v) + }) + + runDB("set-next-unset-next-get register", t, func(t *testing.T, db *Storage) { + t.Parallel() + + r := NewRegister(db, 0, nil) + + err := r.SetValue(owner, key, value1) + require.NoError(t, err) + + r = NewRegister(db, 1, nil) + + err = r.SetValue(owner, key, nil) + require.NoError(t, err) + + r = NewRegister(db, 2, nil) + + v, err := r.GetValue(owner, key) + require.NoError(t, err) + // not actually nil, but empty + require.Len(t, v, 0) + }) +} From 2198eed6ffbee990950526ac371f16326e099f4a Mon Sep 17 00:00:00 2001 From: Janez Podhostnik Date: Thu, 31 Oct 2024 16:15:21 +0100 Subject: [PATCH 013/174] register storage fix lookup and remove owner --- storage/pebble/register.go | 179 +++++++++++++++++++++----------- storage/pebble/register_test.go | 47 ++++++--- storage/pebble/storage.go | 3 + 3 files changed, 154 insertions(+), 75 deletions(-) diff --git a/storage/pebble/register.go b/storage/pebble/register.go index f66cd4f35..0a0548215 100644 --- a/storage/pebble/register.go +++ b/storage/pebble/register.go @@ -1,20 +1,41 @@ package pebble import ( + "encoding/binary" "fmt" + "github.com/onflow/flow-go/model/flow" + "github.com/onflow/flow-go/storage/pebble/registers" + "github.com/cockroachdb/pebble" "github.com/onflow/atree" "github.com/onflow/flow-go/fvm/evm/types" ) +var ( + // MinLookupKeyLen defines the minimum length for a valid lookup key + // + // Lookup keys use the following format: + // [key] / [height] + // Where: + // - key: optional variable length field + // - height: 8 bytes representing the block height (uint64) + // - separator: '/' is used to separate variable length field + // + // Therefore the minimum key would be 1 byte + # of bytes for height + // / [height] + MinLookupKeyLen = 1 + registers.HeightSuffixLen +) + var _ types.BackendStorage = &Register{} type Register struct { store *Storage height uint64 - batch *pebble.Batch + owner flow.Address + + batch *pebble.Batch } // NewRegister creates a new index instance at the provided height, all reads and @@ -23,62 +44,41 @@ type Register struct { func NewRegister( store *Storage, height uint64, + owner flow.Address, batch *pebble.Batch, ) *Register { return &Register{ store: store, height: height, - batch: batch, + owner: owner, + + batch: batch, } } func (r *Register) GetValue(owner, key []byte) ([]byte, error) { - var db pebble.Reader = r.store.db - if r.batch != nil { - db = r.batch + if r.owner != flow.BytesToAddress(owner) { + return nil, fmt.Errorf("owner mismatch. Storage expects a single owner %s, given %s", r.owner.Hex(), flow.BytesToAddress(owner).Hex()) } - iter, err := db.NewIter(&pebble.IterOptions{ - LowerBound: r.idLower(owner, key), - UpperBound: r.idUpper(owner, key), - }) - if err != nil { - return nil, fmt.Errorf("failed to create register range iterator: %w", err) - } - defer func() { - if err := iter.Close(); err != nil { - r.store.log.Error().Err(err).Msg("failed to close register iterator") - } - }() - - found := iter.Last() - if !found { - // as per interface expectation we need to return nil if not found - return nil, nil - } + lookupKey := newLookupKey(r.height, key) + return r.lookupRegister(lookupKey.Bytes()) +} - val, err := iter.ValueAndErr() - if err != nil { - return nil, fmt.Errorf( - "failed to get ledger value at owner %x and key %x: %w", - owner, - key, - err, - ) +func (r *Register) SetValue(owner, key, value []byte) error { + if r.owner != flow.BytesToAddress(owner) { + return fmt.Errorf("owner mismatch. Storage expects a single owner %s, given %s", r.owner.Hex(), flow.BytesToAddress(owner).Hex()) } - return val, nil -} + encoded := newLookupKey(r.height, key).Bytes() -func (r *Register) SetValue(owner, key, value []byte) error { - id := r.id(owner, key) - if err := r.store.set(ledgerValue, id, value, r.batch); err != nil { - return fmt.Errorf( - "failed to store ledger value for owner %x and key %x: %w", - owner, - key, - err, - ) + var db pebble.Writer = r.store.db + if r.batch != nil { + db = r.batch + } + err := db.Set(encoded, value, nil) + if err != nil { + return fmt.Errorf("failed to set key: %w", err) } return nil @@ -99,28 +99,83 @@ func (r *Register) AllocateSlabIndex(_ []byte) (atree.SlabIndex, error) { ) } -// id calculates a ledger id with embedded block height for owner and key. -// The key for a register has the following schema: -// {owner}{key}{height} -func (r *Register) id(owner, key []byte) []byte { - id := append(owner, key...) - h := uint64Bytes(r.height) - return append(id, h...) +func (r *Register) lookupRegister(key []byte) (flow.RegisterValue, error) { + var db pebble.Reader = r.store.db + if r.batch != nil { + db = r.batch + } + + iter, err := db.NewIter(&pebble.IterOptions{ + UseL6Filters: true, + }) + if err != nil { + return nil, err + } + + defer func() { + if err := iter.Close(); err != nil { + r.store.log.Error().Err(err).Msg("failed to close register iterator") + } + }() + + ok := iter.SeekPrefixGE(key) + if !ok { + // no such register found (which is equivalent to the register being nil) + return nil, nil + } + + binaryValue, err := iter.ValueAndErr() + if err != nil { + return nil, fmt.Errorf("failed to get value: %w", err) + } + // preventing caller from modifying the iterator's value slices + valueCopy := make([]byte, len(binaryValue)) + copy(valueCopy, binaryValue) + + return valueCopy, nil +} + +// lookupKey is the encoded format of the storage key for looking up register value +type lookupKey struct { + encoded []byte } -func (r *Register) idUpper(owner, key []byte) []byte { - id := []byte{ledgerValue} - id = append(id, owner...) - id = append(id, key...) - // increase height +1 because upper bound is exclusive - h := uint64Bytes(r.height + 1) - return append(id, h...) +// Bytes returns the encoded lookup key. +func (h lookupKey) Bytes() []byte { + return h.encoded } -func (r *Register) idLower(owner, key []byte) []byte { - id := []byte{ledgerValue} - id = append(id, owner...) - id = append(id, key...) - // lower height is always 0 - return append(id, uint64Bytes(0)...) +// String returns the encoded lookup key as a string. +func (h lookupKey) String() string { + return string(h.encoded) +} + +// newLookupKey takes a height and registerID, returns the key for storing the register value in storage +func newLookupKey(height uint64, key []byte) *lookupKey { + lookupKey := lookupKey{ + // 1 byte gaps for db prefix and '/' separators + encoded: make([]byte, 0, MinLookupKeyLen+len(key)), + } + + // The lookup lookupKey used to find most recent value for a register. + // + // The "" part is the register lookupKey, which is used as a prefix to filter and iterate + // through updated values at different heights, and find the most recent updated value at or below + // a certain height. + lookupKey.encoded = append(lookupKey.encoded, key...) + lookupKey.encoded = append(lookupKey.encoded, '/') + + // Encode the height getting it to 1s compliment (all bits flipped) and big-endian byte order. + // + // Registers are a sparse dataset stored with a single entry per update. To find the value at a particular + // height, we need to do a scan across the entries to find the highest height that is less than or equal + // to the target height. + // + // Pebble does not support reverse iteration, so we use the height's one's complement to effectively + // reverse sort on the height. This allows us to use a bitwise forward scan for the next most recent + // entry. + onesCompliment := ^height + lookupKey.encoded = binary.BigEndian.AppendUint64(lookupKey.encoded, onesCompliment) + + return &lookupKey } diff --git a/storage/pebble/register_test.go b/storage/pebble/register_test.go index 9211f8b50..fefe9240e 100644 --- a/storage/pebble/register_test.go +++ b/storage/pebble/register_test.go @@ -3,11 +3,14 @@ package pebble import ( "testing" + flowGo "github.com/onflow/flow-go/model/flow" + "github.com/stretchr/testify/require" ) func Test_Register(t *testing.T) { owner := []byte{0x01} + owner2 := []byte{0x02} key := []byte{0x03} value1 := []byte{0x05} value2 := []byte{0x06} @@ -15,7 +18,7 @@ func Test_Register(t *testing.T) { runDB("get register", t, func(t *testing.T, db *Storage) { t.Parallel() - r := NewRegister(db, 0, nil) + r := NewRegister(db, 0, flowGo.BytesToAddress(owner), nil) v, err := r.GetValue(owner, key) require.NoError(t, err) @@ -25,7 +28,7 @@ func Test_Register(t *testing.T) { runDB("set register", t, func(t *testing.T, db *Storage) { t.Parallel() - r := NewRegister(db, 0, nil) + r := NewRegister(db, 0, flowGo.BytesToAddress(owner), nil) err := r.SetValue(owner, key, value1) require.NoError(t, err) @@ -34,7 +37,7 @@ func Test_Register(t *testing.T) { runDB("set-get register", t, func(t *testing.T, db *Storage) { t.Parallel() - r := NewRegister(db, 0, nil) + r := NewRegister(db, 0, flowGo.BytesToAddress(owner), nil) err := r.SetValue(owner, key, value1) require.NoError(t, err) @@ -47,7 +50,7 @@ func Test_Register(t *testing.T) { runDB("set-set-get register", t, func(t *testing.T, db *Storage) { t.Parallel() - r := NewRegister(db, 0, nil) + r := NewRegister(db, 0, flowGo.BytesToAddress(owner), nil) err := r.SetValue(owner, key, value1) require.NoError(t, err) @@ -63,7 +66,7 @@ func Test_Register(t *testing.T) { runDB("set-unset-get register", t, func(t *testing.T, db *Storage) { t.Parallel() - r := NewRegister(db, 0, nil) + r := NewRegister(db, 0, flowGo.BytesToAddress(owner), nil) err := r.SetValue(owner, key, value1) require.NoError(t, err) @@ -80,12 +83,12 @@ func Test_Register(t *testing.T) { runDB("set-next-get register", t, func(t *testing.T, db *Storage) { t.Parallel() - r := NewRegister(db, 0, nil) + r := NewRegister(db, 0, flowGo.BytesToAddress(owner), nil) err := r.SetValue(owner, key, value1) require.NoError(t, err) - r = NewRegister(db, 1, nil) + r = NewRegister(db, 1, flowGo.BytesToAddress(owner), nil) v, err := r.GetValue(owner, key) require.NoError(t, err) @@ -95,17 +98,17 @@ func Test_Register(t *testing.T) { runDB("set-next-set-next-get register", t, func(t *testing.T, db *Storage) { t.Parallel() - r := NewRegister(db, 0, nil) + r := NewRegister(db, 0, flowGo.BytesToAddress(owner), nil) err := r.SetValue(owner, key, value1) require.NoError(t, err) - r = NewRegister(db, 1, nil) + r = NewRegister(db, 1, flowGo.BytesToAddress(owner), nil) err = r.SetValue(owner, key, value2) require.NoError(t, err) - r = NewRegister(db, 2, nil) + r = NewRegister(db, 2, flowGo.BytesToAddress(owner), nil) v, err := r.GetValue(owner, key) require.NoError(t, err) @@ -115,21 +118,39 @@ func Test_Register(t *testing.T) { runDB("set-next-unset-next-get register", t, func(t *testing.T, db *Storage) { t.Parallel() - r := NewRegister(db, 0, nil) + r := NewRegister(db, 0, flowGo.BytesToAddress(owner), nil) err := r.SetValue(owner, key, value1) require.NoError(t, err) - r = NewRegister(db, 1, nil) + r = NewRegister(db, 1, flowGo.BytesToAddress(owner), nil) err = r.SetValue(owner, key, nil) require.NoError(t, err) - r = NewRegister(db, 2, nil) + r = NewRegister(db, 2, flowGo.BytesToAddress(owner), nil) v, err := r.GetValue(owner, key) require.NoError(t, err) // not actually nil, but empty require.Len(t, v, 0) }) + + runDB("get with wrong owner", t, func(t *testing.T, db *Storage) { + t.Parallel() + + r := NewRegister(db, 0, flowGo.BytesToAddress(owner), nil) + + _, err := r.GetValue(owner2, key) + require.Error(t, err) + }) + + runDB("set with wrong owner", t, func(t *testing.T, db *Storage) { + t.Parallel() + + r := NewRegister(db, 0, flowGo.BytesToAddress(owner), nil) + + err := r.SetValue(owner2, key, value1) + require.Error(t, err) + }) } diff --git a/storage/pebble/storage.go b/storage/pebble/storage.go index a8624cfb3..c49a8d85a 100644 --- a/storage/pebble/storage.go +++ b/storage/pebble/storage.go @@ -5,6 +5,8 @@ import ( "fmt" "io" + "github.com/onflow/flow-go/storage/pebble/registers" + "github.com/cockroachdb/pebble" "github.com/rs/zerolog" @@ -38,6 +40,7 @@ func New(dir string, log zerolog.Logger) (*Storage, error) { MemTableStopWritesThreshold: 4, // The default is 1. MaxConcurrentCompactions: func() int { return 4 }, + Comparer: registers.NewMVCCComparer(), } for i := 0; i < len(opts.Levels); i++ { From 04a6806759dbd99bef4a6655979ad888eba133d2 Mon Sep 17 00:00:00 2001 From: Janez Podhostnik Date: Fri, 1 Nov 2024 15:20:09 +0100 Subject: [PATCH 014/174] register store add tests --- storage/pebble/register_test.go | 51 +++++++++++++++++++++++++++++++++ 1 file changed, 51 insertions(+) diff --git a/storage/pebble/register_test.go b/storage/pebble/register_test.go index fefe9240e..92d1fe10a 100644 --- a/storage/pebble/register_test.go +++ b/storage/pebble/register_test.go @@ -3,6 +3,8 @@ package pebble import ( "testing" + "github.com/cockroachdb/pebble" + flowGo "github.com/onflow/flow-go/model/flow" "github.com/stretchr/testify/require" @@ -153,4 +155,53 @@ func Test_Register(t *testing.T) { err := r.SetValue(owner2, key, value1) require.Error(t, err) }) + + runDB("non-indexed batch", t, func(t *testing.T, db *Storage) { + t.Parallel() + + batch := db.db.NewBatch() + + r := NewRegister(db, 0, flowGo.BytesToAddress(owner), batch) + + err := r.SetValue(owner, key, value1) + require.NoError(t, err) + + // non-indexed batch will panic on set + + require.Panics(t, func() { + _, _ = r.GetValue(owner, key) + }) + }) + + runDB("indexed batch", t, func(t *testing.T, db *Storage) { + t.Parallel() + + // note that the Storage already creates an indexed batch + batch := db.NewBatch() + + r := NewRegister(db, 0, flowGo.BytesToAddress(owner), batch) + + err := r.SetValue(owner, key, value1) + require.NoError(t, err) + + // indexed batch will not panic on set + v, err := r.GetValue(owner, key) + require.NoError(t, err) + require.Equal(t, value1, v) + + // outside of the batch the value is still empty + r = NewRegister(db, 0, flowGo.BytesToAddress(owner), nil) + v, err = r.GetValue(owner, key) + require.NoError(t, err) + require.Len(t, v, 0) + + // commit the batch + err = batch.Commit(pebble.Sync) + require.NoError(t, err) + + // now the value is set + v, err = r.GetValue(owner, key) + require.NoError(t, err) + require.Equal(t, value1, v) + }) } From bb847c595e5ed620e49c88bb2a141d197d0c7244 Mon Sep 17 00:00:00 2001 From: Janez Podhostnik Date: Tue, 5 Nov 2024 16:25:34 +0100 Subject: [PATCH 015/174] separate delta and index --- services/ingestion/engine.go | 2 +- .../pebble/{register.go => register_index.go} | 101 ++++---- storage/pebble/register_index_test.go | 91 ++++++++ storage/pebble/register_test.go | 207 ---------------- storage/pebble/storage.go | 6 +- storage/pebble/storage_test.go | 6 +- storage/registers_delta.go | 112 +++++++++ storage/registers_delta_test.go | 221 ++++++++++++++++++ 8 files changed, 475 insertions(+), 271 deletions(-) rename storage/pebble/{register.go => register_index.go} (62%) create mode 100644 storage/pebble/register_index_test.go delete mode 100644 storage/pebble/register_test.go create mode 100644 storage/registers_delta.go create mode 100644 storage/registers_delta_test.go diff --git a/services/ingestion/engine.go b/services/ingestion/engine.go index 6ca1d027a..e31c48a06 100644 --- a/services/ingestion/engine.go +++ b/services/ingestion/engine.go @@ -155,7 +155,7 @@ func (e *Engine) processEvents(events *models.CadenceEvents) error { return nil // nothing else to do this was heartbeat event with not event payloads } - batch := e.store.NewBatch() + batch := e.store.NewIndexedBatch() defer batch.Close() // we first index the block diff --git a/storage/pebble/register.go b/storage/pebble/register_index.go similarity index 62% rename from storage/pebble/register.go rename to storage/pebble/register_index.go index 0a0548215..7d1e4f999 100644 --- a/storage/pebble/register.go +++ b/storage/pebble/register_index.go @@ -8,9 +8,6 @@ import ( "github.com/onflow/flow-go/storage/pebble/registers" "github.com/cockroachdb/pebble" - "github.com/onflow/atree" - - "github.com/onflow/flow-go/fvm/evm/types" ) var ( @@ -28,82 +25,64 @@ var ( MinLookupKeyLen = 1 + registers.HeightSuffixLen ) -var _ types.BackendStorage = &Register{} - -type Register struct { - store *Storage - height uint64 - owner flow.Address - - batch *pebble.Batch +type RegisterIndex struct { + store *Storage + owner flow.Address } -// NewRegister creates a new index instance at the provided height, all reads and +// NewRegisters creates a new index instance at the provided height, all reads and // writes of the registers will happen at that height. // this is not concurrency safe. -func NewRegister( +// +// The register store does verify that the owner supplied is the one that was used before, +// or that the heights are sequential. +// This should be done by the caller. +func NewRegisters( store *Storage, - height uint64, owner flow.Address, - batch *pebble.Batch, -) *Register { - return &Register{ - store: store, - height: height, - owner: owner, - - batch: batch, +) *RegisterIndex { + return &RegisterIndex{ + store: store, + owner: owner, } } -func (r *Register) GetValue(owner, key []byte) ([]byte, error) { - if r.owner != flow.BytesToAddress(owner) { - return nil, fmt.Errorf("owner mismatch. Storage expects a single owner %s, given %s", r.owner.Hex(), flow.BytesToAddress(owner).Hex()) +// Get returns the register value for the given register ID at the given height. +// Get will check that the owner is the same as the one used to create the index. +func (r *RegisterIndex) Get(id flow.RegisterID, height uint64) (flow.RegisterValue, error) { + owner := flow.BytesToAddress([]byte(id.Owner)) + if r.owner != flow.BytesToAddress([]byte(id.Owner)) { + return nil, registerOwnerMismatch(r.owner, owner) } - lookupKey := newLookupKey(r.height, key) + lookupKey := newLookupKey(height, []byte(id.Key)) return r.lookupRegister(lookupKey.Bytes()) } -func (r *Register) SetValue(owner, key, value []byte) error { - if r.owner != flow.BytesToAddress(owner) { - return fmt.Errorf("owner mismatch. Storage expects a single owner %s, given %s", r.owner.Hex(), flow.BytesToAddress(owner).Hex()) - } +// Store stores the register entries for the given height to the given batch. +// The batch does need to be indexed. +// +// Store will check that all the register entries are for the same owner. +func (r *RegisterIndex) Store(entries flow.RegisterEntries, height uint64, batch *pebble.Batch) error { + for _, entry := range entries { + owner := flow.BytesToAddress([]byte(entry.Key.Owner)) + if r.owner != owner { + return registerOwnerMismatch(r.owner, owner) + } - encoded := newLookupKey(r.height, key).Bytes() + encoded := newLookupKey(height, []byte(entry.Key.Key)).Bytes() - var db pebble.Writer = r.store.db - if r.batch != nil { - db = r.batch - } - err := db.Set(encoded, value, nil) - if err != nil { - return fmt.Errorf("failed to set key: %w", err) + err := batch.Set(encoded, entry.Value, nil) + if err != nil { + return fmt.Errorf("failed to set key: %w", err) + } } return nil } -func (r *Register) ValueExists(owner, key []byte) (bool, error) { - val, err := r.GetValue(owner, key) - if err != nil { - return false, err - } - - return len(val) == 0, nil -} - -func (r *Register) AllocateSlabIndex(_ []byte) (atree.SlabIndex, error) { - return atree.SlabIndexUndefined, fmt.Errorf( - "unexpected call to allocate slab index", - ) -} - -func (r *Register) lookupRegister(key []byte) (flow.RegisterValue, error) { - var db pebble.Reader = r.store.db - if r.batch != nil { - db = r.batch - } +func (r *RegisterIndex) lookupRegister(key []byte) (flow.RegisterValue, error) { + db := r.store.db iter, err := db.NewIter(&pebble.IterOptions{ UseL6Filters: true, @@ -167,7 +146,7 @@ func newLookupKey(height uint64, key []byte) *lookupKey { // Encode the height getting it to 1s compliment (all bits flipped) and big-endian byte order. // - // Registers are a sparse dataset stored with a single entry per update. To find the value at a particular + // RegisterIndex are a sparse dataset stored with a single entry per update. To find the value at a particular // height, we need to do a scan across the entries to find the highest height that is less than or equal // to the target height. // @@ -179,3 +158,7 @@ func newLookupKey(height uint64, key []byte) *lookupKey { return &lookupKey } + +func registerOwnerMismatch(expected flow.Address, owner flow.Address) error { + return fmt.Errorf("owner mismatch. Storage expects a single owner %s, given %s", expected.Hex(), owner.Hex()) +} diff --git a/storage/pebble/register_index_test.go b/storage/pebble/register_index_test.go new file mode 100644 index 000000000..f3693236c --- /dev/null +++ b/storage/pebble/register_index_test.go @@ -0,0 +1,91 @@ +package pebble + +import ( + "testing" + + "github.com/cockroachdb/pebble" + "github.com/onflow/flow-go/model/flow" + "github.com/stretchr/testify/require" +) + +func Test_RegisterIndex(t *testing.T) { + t.Parallel() + owner := "0x1" + ownerAddress := flow.BytesToAddress([]byte(owner)) + owner2 := "0x2" + key := "0x3" + value := []byte{0x4} + + runDB("get register", t, func(t *testing.T, db *Storage) { + t.Parallel() + + r := NewRegisters(db, ownerAddress) + + v, err := r.Get(flow.RegisterID{Owner: owner, Key: key}, 0) + require.NoError(t, err) + require.Empty(t, v) + }) + + runDB("get register - owner2", t, func(t *testing.T, db *Storage) { + t.Parallel() + + r := NewRegisters(db, ownerAddress) + + _, err := r.Get(flow.RegisterID{Owner: owner2, Key: key}, 0) + require.Error(t, err) + }) + + runDB("store registers", t, func(t *testing.T, db *Storage) { + t.Parallel() + + r := NewRegisters(db, ownerAddress) + + batch := db.NewBatch() + + err := r.Store( + flow.RegisterEntries{ + flow.RegisterEntry{ + Key: flow.RegisterID{Owner: owner, Key: key}, + Value: value, + }, + }, + 0, + batch, + ) + require.NoError(t, err) + + v, err := r.Get(flow.RegisterID{Owner: owner, Key: key}, 0) + require.NoError(t, err) + // not commited, so value is still empty + require.Empty(t, v) + + err = batch.Commit(pebble.Sync) + require.NoError(t, err) + + v, err = r.Get(flow.RegisterID{Owner: owner, Key: key}, 0) + require.NoError(t, err) + require.Equal(t, value, v) + + require.NoError(t, err) + }) + + runDB("store registers - owner2", t, func(t *testing.T, db *Storage) { + t.Parallel() + + r := NewRegisters(db, ownerAddress) + + batch := db.NewBatch() + + err := r.Store( + flow.RegisterEntries{ + flow.RegisterEntry{ + Key: flow.RegisterID{Owner: owner2, Key: key}, + Value: value, + }, + }, + 0, + batch, + ) + require.Error(t, err) + }) +} diff --git a/storage/pebble/register_test.go b/storage/pebble/register_test.go deleted file mode 100644 index 92d1fe10a..000000000 --- a/storage/pebble/register_test.go +++ /dev/null @@ -1,207 +0,0 @@ -package pebble - -import ( - "testing" - - "github.com/cockroachdb/pebble" - - flowGo "github.com/onflow/flow-go/model/flow" - - "github.com/stretchr/testify/require" -) - -func Test_Register(t *testing.T) { - owner := []byte{0x01} - owner2 := []byte{0x02} - key := []byte{0x03} - value1 := []byte{0x05} - value2 := []byte{0x06} - - runDB("get register", t, func(t *testing.T, db *Storage) { - t.Parallel() - - r := NewRegister(db, 0, flowGo.BytesToAddress(owner), nil) - - v, err := r.GetValue(owner, key) - require.NoError(t, err) - require.Len(t, v, 0) - }) - - runDB("set register", t, func(t *testing.T, db *Storage) { - t.Parallel() - - r := NewRegister(db, 0, flowGo.BytesToAddress(owner), nil) - - err := r.SetValue(owner, key, value1) - require.NoError(t, err) - }) - - runDB("set-get register", t, func(t *testing.T, db *Storage) { - t.Parallel() - - r := NewRegister(db, 0, flowGo.BytesToAddress(owner), nil) - - err := r.SetValue(owner, key, value1) - require.NoError(t, err) - - v, err := r.GetValue(owner, key) - require.NoError(t, err) - require.Equal(t, value1, v) - }) - - runDB("set-set-get register", t, func(t *testing.T, db *Storage) { - t.Parallel() - - r := NewRegister(db, 0, flowGo.BytesToAddress(owner), nil) - - err := r.SetValue(owner, key, value1) - require.NoError(t, err) - - err = r.SetValue(owner, key, value2) - require.NoError(t, err) - - v, err := r.GetValue(owner, key) - require.NoError(t, err) - require.Equal(t, value2, v) - }) - - runDB("set-unset-get register", t, func(t *testing.T, db *Storage) { - t.Parallel() - - r := NewRegister(db, 0, flowGo.BytesToAddress(owner), nil) - - err := r.SetValue(owner, key, value1) - require.NoError(t, err) - - err = r.SetValue(owner, key, nil) - require.NoError(t, err) - - v, err := r.GetValue(owner, key) - require.NoError(t, err) - // not actually nil, but empty - require.Len(t, v, 0) - }) - - runDB("set-next-get register", t, func(t *testing.T, db *Storage) { - t.Parallel() - - r := NewRegister(db, 0, flowGo.BytesToAddress(owner), nil) - - err := r.SetValue(owner, key, value1) - require.NoError(t, err) - - r = NewRegister(db, 1, flowGo.BytesToAddress(owner), nil) - - v, err := r.GetValue(owner, key) - require.NoError(t, err) - require.Equal(t, value1, v) - }) - - runDB("set-next-set-next-get register", t, func(t *testing.T, db *Storage) { - t.Parallel() - - r := NewRegister(db, 0, flowGo.BytesToAddress(owner), nil) - - err := r.SetValue(owner, key, value1) - require.NoError(t, err) - - r = NewRegister(db, 1, flowGo.BytesToAddress(owner), nil) - - err = r.SetValue(owner, key, value2) - require.NoError(t, err) - - r = NewRegister(db, 2, flowGo.BytesToAddress(owner), nil) - - v, err := r.GetValue(owner, key) - require.NoError(t, err) - require.Equal(t, value2, v) - }) - - runDB("set-next-unset-next-get register", t, func(t *testing.T, db *Storage) { - t.Parallel() - - r := NewRegister(db, 0, flowGo.BytesToAddress(owner), nil) - - err := r.SetValue(owner, key, value1) - require.NoError(t, err) - - r = NewRegister(db, 1, flowGo.BytesToAddress(owner), nil) - - err = r.SetValue(owner, key, nil) - require.NoError(t, err) - - r = NewRegister(db, 2, flowGo.BytesToAddress(owner), nil) - - v, err := r.GetValue(owner, key) - require.NoError(t, err) - // not actually nil, but empty - require.Len(t, v, 0) - }) - - runDB("get with wrong owner", t, func(t *testing.T, db *Storage) { - t.Parallel() - - r := NewRegister(db, 0, flowGo.BytesToAddress(owner), nil) - - _, err := r.GetValue(owner2, key) - require.Error(t, err) - }) - - runDB("set with wrong owner", t, func(t *testing.T, db *Storage) { - t.Parallel() - - r := NewRegister(db, 0, flowGo.BytesToAddress(owner), nil) - - err := r.SetValue(owner2, key, value1) - require.Error(t, err) - }) - - runDB("non-indexed batch", t, func(t *testing.T, db *Storage) { - t.Parallel() - - batch := db.db.NewBatch() - - r := NewRegister(db, 0, flowGo.BytesToAddress(owner), batch) - - err := r.SetValue(owner, key, value1) - require.NoError(t, err) - - // non-indexed batch will panic on set - - require.Panics(t, func() { - _, _ = r.GetValue(owner, key) - }) - }) - - runDB("indexed batch", t, func(t *testing.T, db *Storage) { - t.Parallel() - - // note that the Storage already creates an indexed batch - batch := db.NewBatch() - - r := NewRegister(db, 0, flowGo.BytesToAddress(owner), batch) - - err := r.SetValue(owner, key, value1) - require.NoError(t, err) - - // indexed batch will not panic on set - v, err := r.GetValue(owner, key) - require.NoError(t, err) - require.Equal(t, value1, v) - - // outside of the batch the value is still empty - r = NewRegister(db, 0, flowGo.BytesToAddress(owner), nil) - v, err = r.GetValue(owner, key) - require.NoError(t, err) - require.Len(t, v, 0) - - // commit the batch - err = batch.Commit(pebble.Sync) - require.NoError(t, err) - - // now the value is set - v, err = r.GetValue(owner, key) - require.NoError(t, err) - require.Equal(t, value1, v) - }) -} diff --git a/storage/pebble/storage.go b/storage/pebble/storage.go index c49a8d85a..10731f0b2 100644 --- a/storage/pebble/storage.go +++ b/storage/pebble/storage.go @@ -127,6 +127,10 @@ func (s *Storage) batchGet(batch *pebble.Batch, keyCode byte, key ...[]byte) ([] return s.get(keyCode, key...) } -func (s *Storage) NewBatch() *pebble.Batch { +func (s *Storage) NewIndexedBatch() *pebble.Batch { return s.db.NewIndexedBatch() } + +func (s *Storage) NewBatch() *pebble.Batch { + return s.db.NewBatch() +} diff --git a/storage/pebble/storage_test.go b/storage/pebble/storage_test.go index 4a031b3c9..89717875c 100644 --- a/storage/pebble/storage_test.go +++ b/storage/pebble/storage_test.go @@ -140,7 +140,7 @@ func TestBatch(t *testing.T) { blocks := NewBlocks(db, flowGo.Emulator) trace := NewTraces(db) - batch := db.NewBatch() + batch := db.NewIndexedBatch() defer func() { require.NoError(t, batch.Close()) }() @@ -168,7 +168,7 @@ func TestBatch(t *testing.T) { runDB("should not contain data without committing", t, func(t *testing.T, db *Storage) { blocks := NewBlocks(db, flowGo.Emulator) - batch := db.NewBatch() + batch := db.NewIndexedBatch() defer func() { require.NoError(t, batch.Close()) }() @@ -189,7 +189,7 @@ func TestBatch(t *testing.T) { evmHeight := uint64(10 + i) bl := mocks.NewBlock(evmHeight) - batch := db.NewBatch() + batch := db.NewIndexedBatch() err := blocks.Store(cadenceHeight, flow.HexToID("0x1"), bl, batch) require.NoError(t, err) diff --git a/storage/registers_delta.go b/storage/registers_delta.go new file mode 100644 index 000000000..e7940f097 --- /dev/null +++ b/storage/registers_delta.go @@ -0,0 +1,112 @@ +package storage + +import ( + "github.com/onflow/atree" + "github.com/onflow/flow-go/fvm/evm/types" + "github.com/onflow/flow-go/model/flow" +) + +var _ types.BackendStorage = &RegistersDelta{} + +type RegisterValueAtHeight interface { + Get(id flow.RegisterID, height uint64) (flow.RegisterValue, error) +} + +// RegistersDelta holds the register changes for a current height. +// It is used to collect register changes for a block, while also serving register data +// for the previous heights. +// once all changes were collected, use the `GetUpdates` method to get the register updates. +// and apply them to storage. +// Not safe for concurrent usage. +// +// To avoid creating a new instance of RegistersDelta for every block, use the `Reset` method. +type RegistersDelta struct { + // the cache is used to cache register reads for the current height. + cache map[flow.RegisterID]flow.RegisterValue + + // deltas is a map of register IDs to their respective deltas. + deltas map[flow.RegisterID]flow.RegisterEntry + // height is the height at which the deltas were applied. + height uint64 + + registers RegisterValueAtHeight +} + +// NewRegistersDelta creates a new instance of RegistersDelta. +// height is used for `GetValue` to fetch the register value at the given height. +// height is not checked against the latest register height. The caller is responsible +// for ensuring that the height is sequential. +func NewRegistersDelta( + height uint64, + registers RegisterValueAtHeight, +) *RegistersDelta { + return &RegistersDelta{ + cache: make(map[flow.RegisterID]flow.RegisterValue), + deltas: make(map[flow.RegisterID]flow.RegisterEntry), + height: height, + registers: registers, + } +} + +func (r *RegistersDelta) GetValue(owner []byte, key []byte) ([]byte, error) { + id := flow.CadenceRegisterID(owner, key) + + // get from delta first + if delta, ok := r.deltas[id]; ok { + return delta.Value, nil + } + + // get from cache if not found in delta + if value, ok := r.cache[id]; ok { + return value, nil + } + + // get from storage + value, err := r.registers.Get(id, r.height) + if err != nil { + return nil, err + } + + r.cache[id] = value + return value, nil +} + +func (r *RegistersDelta) SetValue(owner, key, value []byte) error { + id := flow.CadenceRegisterID(owner, key) + + r.deltas[id] = flow.RegisterEntry{Key: id, Value: value} + + return nil +} + +func (r *RegistersDelta) ValueExists(owner []byte, key []byte) (bool, error) { + value, err := r.GetValue(owner, key) + if err != nil { + return false, err + } + return len(value) > 0, nil +} + +// GetUpdates returns the register updates for the current height to be applied to storage. +func (r *RegistersDelta) GetUpdates() flow.RegisterEntries { + entries := make(flow.RegisterEntries, 0, len(r.deltas)) + for id, delta := range r.deltas { + entries = append(entries, flow.RegisterEntry{Key: id, Value: delta.Value}) + } + + return entries +} + +// Reset resets the state of the registers delta to the provided height. +// This can be used to as an optimization to avoid creating a new instance of RegistersDelta +// for every block. +func (r *RegistersDelta) Reset(height uint64) { + r.height = height + clear(r.deltas) + clear(r.cache) +} + +func (r *RegistersDelta) AllocateSlabIndex(_ []byte) (atree.SlabIndex, error) { + // TODO: If needed add later + panic("should not be called") +} diff --git a/storage/registers_delta_test.go b/storage/registers_delta_test.go new file mode 100644 index 000000000..3d2bc3385 --- /dev/null +++ b/storage/registers_delta_test.go @@ -0,0 +1,221 @@ +package storage_test + +import ( + "testing" + + "github.com/cockroachdb/pebble" + "github.com/rs/zerolog" + "github.com/stretchr/testify/require" + + flowGo "github.com/onflow/flow-go/model/flow" + + "github.com/onflow/flow-evm-gateway/storage" + pebbleStorage "github.com/onflow/flow-evm-gateway/storage/pebble" +) + +func Test_RegisterDeltaWithStorage(t *testing.T) { + owner := []byte{0x01} + ownerAddress := flowGo.BytesToAddress(owner) + owner2 := []byte{0x02} + key := []byte{0x03} + value1 := []byte{0x05} + value2 := []byte{0x06} + + runDB("get register", t, func(t *testing.T, db *pebbleStorage.Storage) { + t.Parallel() + + s := pebbleStorage.NewRegisters(db, ownerAddress) + r := storage.NewRegistersDelta(0, s) + + v, err := r.GetValue(owner, key) + require.NoError(t, err) + require.Len(t, v, 0) + }) + + runDB("set register", t, func(t *testing.T, db *pebbleStorage.Storage) { + t.Parallel() + + s := pebbleStorage.NewRegisters(db, ownerAddress) + r := storage.NewRegistersDelta(0, s) + + err := r.SetValue(owner, key, value1) + require.NoError(t, err) + }) + + runDB("set-get register", t, func(t *testing.T, db *pebbleStorage.Storage) { + t.Parallel() + + s := pebbleStorage.NewRegisters(db, ownerAddress) + r := storage.NewRegistersDelta(0, s) + + err := r.SetValue(owner, key, value1) + require.NoError(t, err) + + v, err := r.GetValue(owner, key) + require.NoError(t, err) + require.Equal(t, value1, v) + }) + + runDB("set-set-get register", t, func(t *testing.T, db *pebbleStorage.Storage) { + t.Parallel() + + s := pebbleStorage.NewRegisters(db, ownerAddress) + r := storage.NewRegistersDelta(0, s) + + err := r.SetValue(owner, key, value1) + require.NoError(t, err) + + err = r.SetValue(owner, key, value2) + require.NoError(t, err) + + v, err := r.GetValue(owner, key) + require.NoError(t, err) + require.Equal(t, value2, v) + }) + + runDB("set-unset-get register", t, func(t *testing.T, db *pebbleStorage.Storage) { + t.Parallel() + + s := pebbleStorage.NewRegisters(db, ownerAddress) + r := storage.NewRegistersDelta(0, s) + + err := r.SetValue(owner, key, value1) + require.NoError(t, err) + + err = r.SetValue(owner, key, nil) + require.NoError(t, err) + + v, err := r.GetValue(owner, key) + require.NoError(t, err) + // not actually nil, but empty + require.Len(t, v, 0) + }) + + runDB("set-next-get register", t, func(t *testing.T, db *pebbleStorage.Storage) { + t.Parallel() + + s := pebbleStorage.NewRegisters(db, ownerAddress) + r := storage.NewRegistersDelta(0, s) + + err := r.SetValue(owner, key, value1) + require.NoError(t, err) + + err = commit(t, db, r, s) + require.NoError(t, err) + + r = storage.NewRegistersDelta(1, s) + + v, err := r.GetValue(owner, key) + require.NoError(t, err) + require.Equal(t, value1, v) + }) + + runDB("set-next-set-next-get register", t, func(t *testing.T, db *pebbleStorage.Storage) { + t.Parallel() + + s := pebbleStorage.NewRegisters(db, ownerAddress) + r := storage.NewRegistersDelta(0, s) + + err := r.SetValue(owner, key, value1) + require.NoError(t, err) + + err = commit(t, db, r, s) + require.NoError(t, err) + + r = storage.NewRegistersDelta(1, s) + + err = r.SetValue(owner, key, value2) + require.NoError(t, err) + + err = commit(t, db, r, s) + require.NoError(t, err) + + r = storage.NewRegistersDelta(2, s) + + v, err := r.GetValue(owner, key) + require.NoError(t, err) + require.Equal(t, value2, v) + }) + + runDB("set-next-unset-next-get register", t, func(t *testing.T, db *pebbleStorage.Storage) { + t.Parallel() + + s := pebbleStorage.NewRegisters(db, ownerAddress) + r := storage.NewRegistersDelta(0, s) + + err := r.SetValue(owner, key, value1) + require.NoError(t, err) + + err = commit(t, db, r, s) + require.NoError(t, err) + + r = storage.NewRegistersDelta(0, s) + + err = r.SetValue(owner, key, nil) + require.NoError(t, err) + + err = commit(t, db, r, s) + require.NoError(t, err) + + r = storage.NewRegistersDelta(1, s) + + v, err := r.GetValue(owner, key) + require.NoError(t, err) + // not actually nil, but empty + require.Len(t, v, 0) + }) + + runDB("get with wrong owner", t, func(t *testing.T, db *pebbleStorage.Storage) { + t.Parallel() + + s := pebbleStorage.NewRegisters(db, ownerAddress) + r := storage.NewRegistersDelta(0, s) + + _, err := r.GetValue(owner2, key) + require.Error(t, err) + }) + + runDB("commit with wrong owner", t, func(t *testing.T, db *pebbleStorage.Storage) { + t.Parallel() + + s := pebbleStorage.NewRegisters(db, ownerAddress) + r := storage.NewRegistersDelta(0, s) + + err := r.SetValue(owner2, key, value1) + require.NoError(t, err) + + err = commit(t, db, r, s) + require.Error(t, err) + }) +} + +func runDB(name string, t *testing.T, f func(t *testing.T, db *pebbleStorage.Storage)) { + dir := t.TempDir() + + db, err := pebbleStorage.New(dir, zerolog.New(zerolog.NewTestWriter(t))) + require.NoError(t, err) + + t.Run(name, func(t *testing.T) { + f(t, db) + }) +} + +// commit is an example on how to commit the delta to storage. +func commit( + t *testing.T, + db *pebbleStorage.Storage, + d *storage.RegistersDelta, + r *pebbleStorage.RegisterIndex, +) error { + batch := db.NewBatch() + + err := r.Store(d.GetUpdates(), 0, batch) + + if err != nil { + return err + } + + err = batch.Commit(pebble.Sync) + require.NoError(t, err) + return nil +} From dc486bc6eecc9f7b4beca2b56f1f1d3cf009328a Mon Sep 17 00:00:00 2001 From: Janez Podhostnik Date: Tue, 5 Nov 2024 16:48:07 +0100 Subject: [PATCH 016/174] add more comments --- storage/pebble/register_index.go | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/storage/pebble/register_index.go b/storage/pebble/register_index.go index 7d1e4f999..8ee530724 100644 --- a/storage/pebble/register_index.go +++ b/storage/pebble/register_index.go @@ -37,6 +37,14 @@ type RegisterIndex struct { // The register store does verify that the owner supplied is the one that was used before, // or that the heights are sequential. // This should be done by the caller. +// +// The RegisterIndex is modeled after `pebble.Registers` from `flow-go` but there are a few differences: +// 1. The `flow-go` implementation creates its own independent batch when saving registers. +// The gateway needs to save the registers together with blocks and transaction so the batch +// is shared with that. +// 2. The gateway does not need to store the owner address as all the registers are for the same owner. +// 3. The gateway does not need pruning (yet) as the db is supposed to be much smaller. +// 4. The owner and height checks are expected to be performed by the caller. func NewRegisters( store *Storage, owner flow.Address, From 2bfd1ebf4ee73a252c5b1bc202c3b390c0291680 Mon Sep 17 00:00:00 2001 From: Janez Podhostnik Date: Tue, 5 Nov 2024 20:34:20 +0100 Subject: [PATCH 017/174] address comments --- storage/registers_delta.go | 25 ++++++-- storage/registers_delta_test.go | 102 ++++++++++++++++++++++++++++++-- 2 files changed, 115 insertions(+), 12 deletions(-) diff --git a/storage/registers_delta.go b/storage/registers_delta.go index e7940f097..e3ee1eb69 100644 --- a/storage/registers_delta.go +++ b/storage/registers_delta.go @@ -21,14 +21,14 @@ type RegisterValueAtHeight interface { // // To avoid creating a new instance of RegistersDelta for every block, use the `Reset` method. type RegistersDelta struct { - // the cache is used to cache register reads for the current height. - cache map[flow.RegisterID]flow.RegisterValue - // deltas is a map of register IDs to their respective deltas. deltas map[flow.RegisterID]flow.RegisterEntry - // height is the height at which the deltas were applied. + // height is the height at which the deltas are to be applied. height uint64 + // The cache is used to cache register reads from the registers storage (RegisterValueAtHeight) + cache map[flow.RegisterID]flow.RegisterValue + registers RegisterValueAtHeight } @@ -48,6 +48,11 @@ func NewRegistersDelta( } } +// GetValue gets the value for the given register ID. +// 1. It checks if the value was changed at this height, it then returns the value from the delta. +// 2. If the value was not changed at this height, it checks if the value was read from storage at height-1 +// and returns it from the cache. +// 3. It reads the value from storage at the current height and caches it. func (r *RegistersDelta) GetValue(owner []byte, key []byte) ([]byte, error) { id := flow.CadenceRegisterID(owner, key) @@ -61,8 +66,13 @@ func (r *RegistersDelta) GetValue(owner []byte, key []byte) ([]byte, error) { return value, nil } - // get from storage - value, err := r.registers.Get(id, r.height) + // if height is 0, there should be nothing in storage + if r.height == 0 { + return nil, nil + } + + // get from storage at height-1. This is because we are currently building registers at height + value, err := r.registers.Get(id, r.height-1) if err != nil { return nil, err } @@ -71,6 +81,8 @@ func (r *RegistersDelta) GetValue(owner []byte, key []byte) ([]byte, error) { return value, nil } +// SetValue sets the value for the given register ID. It sets it in the delta and not in the storage. +// To set the value in the storage, use the `GetUpdates` to get the updates and apply them to storage. func (r *RegistersDelta) SetValue(owner, key, value []byte) error { id := flow.CadenceRegisterID(owner, key) @@ -79,6 +91,7 @@ func (r *RegistersDelta) SetValue(owner, key, value []byte) error { return nil } +// ValueExists checks if the value for the given register ID exists. func (r *RegistersDelta) ValueExists(owner []byte, key []byte) (bool, error) { value, err := r.GetValue(owner, key) if err != nil { diff --git a/storage/registers_delta_test.go b/storage/registers_delta_test.go index 3d2bc3385..8ba9955c7 100644 --- a/storage/registers_delta_test.go +++ b/storage/registers_delta_test.go @@ -1,6 +1,7 @@ package storage_test import ( + "fmt" "testing" "github.com/cockroachdb/pebble" @@ -103,13 +104,29 @@ func Test_RegisterDeltaWithStorage(t *testing.T) { err = commit(t, db, r, s) require.NoError(t, err) - r = storage.NewRegistersDelta(1, s) + r.Reset(1) v, err := r.GetValue(owner, key) require.NoError(t, err) require.Equal(t, value1, v) }) + runDB("set-dont-commit-get register", t, func(t *testing.T, db *pebbleStorage.Storage) { + t.Parallel() + + s := pebbleStorage.NewRegisters(db, ownerAddress) + r := storage.NewRegistersDelta(0, s) + + err := r.SetValue(owner, key, value1) + require.NoError(t, err) + + r.Reset(1) + + v, err := r.GetValue(owner, key) + require.NoError(t, err) + require.Empty(t, v) + }) + runDB("set-next-set-next-get register", t, func(t *testing.T, db *pebbleStorage.Storage) { t.Parallel() @@ -122,7 +139,7 @@ func Test_RegisterDeltaWithStorage(t *testing.T) { err = commit(t, db, r, s) require.NoError(t, err) - r = storage.NewRegistersDelta(1, s) + r.Reset(1) err = r.SetValue(owner, key, value2) require.NoError(t, err) @@ -130,7 +147,7 @@ func Test_RegisterDeltaWithStorage(t *testing.T) { err = commit(t, db, r, s) require.NoError(t, err) - r = storage.NewRegistersDelta(2, s) + r.Reset(2) v, err := r.GetValue(owner, key) require.NoError(t, err) @@ -149,7 +166,7 @@ func Test_RegisterDeltaWithStorage(t *testing.T) { err = commit(t, db, r, s) require.NoError(t, err) - r = storage.NewRegistersDelta(0, s) + r.Reset(1) err = r.SetValue(owner, key, nil) require.NoError(t, err) @@ -157,7 +174,7 @@ func Test_RegisterDeltaWithStorage(t *testing.T) { err = commit(t, db, r, s) require.NoError(t, err) - r = storage.NewRegistersDelta(1, s) + r.Reset(2) v, err := r.GetValue(owner, key) require.NoError(t, err) @@ -169,7 +186,7 @@ func Test_RegisterDeltaWithStorage(t *testing.T) { t.Parallel() s := pebbleStorage.NewRegisters(db, ownerAddress) - r := storage.NewRegistersDelta(0, s) + r := storage.NewRegistersDelta(1, s) _, err := r.GetValue(owner2, key) require.Error(t, err) @@ -187,6 +204,79 @@ func Test_RegisterDeltaWithStorage(t *testing.T) { err = commit(t, db, r, s) require.Error(t, err) }) + + runDB("cache db values", t, func(t *testing.T, db *pebbleStorage.Storage) { + t.Parallel() + + s := &mockRegisterIndex{ + callback: func(id flowGo.RegisterID, height uint64) (flowGo.RegisterValue, error) { + return value1, nil + }, + } + r := storage.NewRegistersDelta(1, s) + + v, err := r.GetValue(owner, key) + require.NoError(t, err) + require.Equal(t, value1, v) + + v, err = r.GetValue(owner, key) + require.NoError(t, err) + require.Equal(t, value1, v) + + // only one call to the index + require.Equal(t, uint(1), s.callCount) + }) + + runDB("cache nil db values", t, func(t *testing.T, db *pebbleStorage.Storage) { + t.Parallel() + + s := &mockRegisterIndex{ + callback: func(id flowGo.RegisterID, height uint64) (flowGo.RegisterValue, error) { + return nil, nil + }, + } + r := storage.NewRegistersDelta(1, s) + + v, err := r.GetValue(owner, key) + require.NoError(t, err) + require.Empty(t, v) + + v, err = r.GetValue(owner, key) + require.NoError(t, err) + require.Empty(t, v) + + // only one call to the index + require.Equal(t, uint(1), s.callCount) + }) + + runDB("dont cache err", t, func(t *testing.T, db *pebbleStorage.Storage) { + t.Parallel() + + s := &mockRegisterIndex{ + callback: func(id flowGo.RegisterID, height uint64) (flowGo.RegisterValue, error) { + return nil, fmt.Errorf("error") + }, + } + r := storage.NewRegistersDelta(1, s) + + _, err := r.GetValue(owner, key) + require.Error(t, err) + + _, err = r.GetValue(owner, key) + require.Error(t, err) + + require.Equal(t, uint(2), s.callCount) + }) +} + +type mockRegisterIndex struct { + callCount uint + callback func(id flowGo.RegisterID, height uint64) (flowGo.RegisterValue, error) +} + +func (m *mockRegisterIndex) Get(id flowGo.RegisterID, height uint64) (flowGo.RegisterValue, error) { + m.callCount++ + return m.callback(id, height) } func runDB(name string, t *testing.T, f func(t *testing.T, db *pebbleStorage.Storage)) { From 55c2ba72b3c9ee839e195dd84340240bc29f7e45 Mon Sep 17 00:00:00 2001 From: Janez Podhostnik Date: Wed, 6 Nov 2024 18:25:51 +0100 Subject: [PATCH 018/174] extract cached register snapshot --- ...{register_index.go => register_storage.go} | 69 +++- ...index_test.go => register_storage_test.go} | 77 ++++- storage/register_delta.go | 74 +++++ storage/register_delta_test.go | 244 ++++++++++++++ storage/registers_delta.go | 125 ------- storage/registers_delta_test.go | 311 ------------------ 6 files changed, 450 insertions(+), 450 deletions(-) rename storage/pebble/{register_index.go => register_storage.go} (69%) rename storage/pebble/{register_index_test.go => register_storage_test.go} (50%) create mode 100644 storage/register_delta.go create mode 100644 storage/register_delta_test.go delete mode 100644 storage/registers_delta.go delete mode 100644 storage/registers_delta_test.go diff --git a/storage/pebble/register_index.go b/storage/pebble/register_storage.go similarity index 69% rename from storage/pebble/register_index.go rename to storage/pebble/register_storage.go index 8ee530724..2a3de5797 100644 --- a/storage/pebble/register_index.go +++ b/storage/pebble/register_storage.go @@ -4,6 +4,7 @@ import ( "encoding/binary" "fmt" + "github.com/onflow/flow-go/fvm/evm/types" "github.com/onflow/flow-go/model/flow" "github.com/onflow/flow-go/storage/pebble/registers" @@ -25,12 +26,14 @@ var ( MinLookupKeyLen = 1 + registers.HeightSuffixLen ) -type RegisterIndex struct { +type RegisterStorage struct { store *Storage owner flow.Address } -// NewRegisters creates a new index instance at the provided height, all reads and +var _ types.StorageProvider = &RegisterStorage{} + +// NewRegisterStorage creates a new index instance at the provided height, all reads and // writes of the registers will happen at that height. // this is not concurrency safe. // @@ -38,18 +41,18 @@ type RegisterIndex struct { // or that the heights are sequential. // This should be done by the caller. // -// The RegisterIndex is modeled after `pebble.Registers` from `flow-go` but there are a few differences: +// The RegisterStorage is modeled after `pebble.Registers` from `flow-go` but there are a few differences: // 1. The `flow-go` implementation creates its own independent batch when saving registers. // The gateway needs to save the registers together with blocks and transaction so the batch // is shared with that. // 2. The gateway does not need to store the owner address as all the registers are for the same owner. // 3. The gateway does not need pruning (yet) as the db is supposed to be much smaller. // 4. The owner and height checks are expected to be performed by the caller. -func NewRegisters( +func NewRegisterStorage( store *Storage, owner flow.Address, -) *RegisterIndex { - return &RegisterIndex{ +) *RegisterStorage { + return &RegisterStorage{ store: store, owner: owner, } @@ -57,7 +60,7 @@ func NewRegisters( // Get returns the register value for the given register ID at the given height. // Get will check that the owner is the same as the one used to create the index. -func (r *RegisterIndex) Get(id flow.RegisterID, height uint64) (flow.RegisterValue, error) { +func (r *RegisterStorage) Get(id flow.RegisterID, height uint64) (flow.RegisterValue, error) { owner := flow.BytesToAddress([]byte(id.Owner)) if r.owner != flow.BytesToAddress([]byte(id.Owner)) { return nil, registerOwnerMismatch(r.owner, owner) @@ -71,7 +74,7 @@ func (r *RegisterIndex) Get(id flow.RegisterID, height uint64) (flow.RegisterVal // The batch does need to be indexed. // // Store will check that all the register entries are for the same owner. -func (r *RegisterIndex) Store(entries flow.RegisterEntries, height uint64, batch *pebble.Batch) error { +func (r *RegisterStorage) Store(entries flow.RegisterEntries, height uint64, batch *pebble.Batch) error { for _, entry := range entries { owner := flow.BytesToAddress([]byte(entry.Key.Owner)) if r.owner != owner { @@ -89,7 +92,7 @@ func (r *RegisterIndex) Store(entries flow.RegisterEntries, height uint64, batch return nil } -func (r *RegisterIndex) lookupRegister(key []byte) (flow.RegisterValue, error) { +func (r *RegisterStorage) lookupRegister(key []byte) (flow.RegisterValue, error) { db := r.store.db iter, err := db.NewIter(&pebble.IterOptions{ @@ -154,7 +157,7 @@ func newLookupKey(height uint64, key []byte) *lookupKey { // Encode the height getting it to 1s compliment (all bits flipped) and big-endian byte order. // - // RegisterIndex are a sparse dataset stored with a single entry per update. To find the value at a particular + // RegisterStorage are a sparse dataset stored with a single entry per update. To find the value at a particular // height, we need to do a scan across the entries to find the highest height that is less than or equal // to the target height. // @@ -167,6 +170,52 @@ func newLookupKey(height uint64, key []byte) *lookupKey { return &lookupKey } +// GetSnapshotAt returns a snapshot of the register index at the given block height. +// the snapshot has a cache. Nil values are cached. +func (r *RegisterStorage) GetSnapshotAt(evmBlockHeight uint64) (types.BackendStorageSnapshot, error) { + return NewStorageSnapshot(r.Get, evmBlockHeight), nil +} + func registerOwnerMismatch(expected flow.Address, owner flow.Address) error { return fmt.Errorf("owner mismatch. Storage expects a single owner %s, given %s", expected.Hex(), owner.Hex()) } + +type GetAtHeightFunc func(id flow.RegisterID, height uint64) (flow.RegisterValue, error) + +type StorageSnapshot struct { + cache map[flow.RegisterID]flow.RegisterValue + + evmBlockHeight uint64 + storageGet GetAtHeightFunc +} + +// NewStorageSnapshot creates a new snapshot of the register index at the given block height. +// the snapshot has a cache. Nil values are cached. +func NewStorageSnapshot(get GetAtHeightFunc, evmBlockHeight uint64) *StorageSnapshot { + return &StorageSnapshot{ + cache: make(map[flow.RegisterID]flow.RegisterValue), + storageGet: get, + evmBlockHeight: evmBlockHeight, + } +} + +// GetValue returns the value for the given register ID at the snapshot block height. +// If the value is not found in the cache, it is fetched from the register index. +func (s StorageSnapshot) GetValue(owner []byte, key []byte) ([]byte, error) { + id := flow.CadenceRegisterID(owner, key) + value, ok := s.cache[id] + if ok { + return value, nil + } + + // get from index + val, err := s.storageGet(id, s.evmBlockHeight) + if err != nil { + return nil, err + } + + s.cache[id] = val + return val, nil +} + +var _ types.BackendStorageSnapshot = &StorageSnapshot{} diff --git a/storage/pebble/register_index_test.go b/storage/pebble/register_storage_test.go similarity index 50% rename from storage/pebble/register_index_test.go rename to storage/pebble/register_storage_test.go index f3693236c..8d500ca2f 100644 --- a/storage/pebble/register_index_test.go +++ b/storage/pebble/register_storage_test.go @@ -19,7 +19,7 @@ func Test_RegisterIndex(t *testing.T) { runDB("get register", t, func(t *testing.T, db *Storage) { t.Parallel() - r := NewRegisters(db, ownerAddress) + r := NewRegisterStorage(db, ownerAddress) v, err := r.Get(flow.RegisterID{Owner: owner, Key: key}, 0) require.NoError(t, err) @@ -29,7 +29,7 @@ func Test_RegisterIndex(t *testing.T) { runDB("get register - owner2", t, func(t *testing.T, db *Storage) { t.Parallel() - r := NewRegisters(db, ownerAddress) + r := NewRegisterStorage(db, ownerAddress) _, err := r.Get(flow.RegisterID{Owner: owner2, Key: key}, 0) require.Error(t, err) @@ -38,7 +38,7 @@ func Test_RegisterIndex(t *testing.T) { runDB("store registers", t, func(t *testing.T, db *Storage) { t.Parallel() - r := NewRegisters(db, ownerAddress) + r := NewRegisterStorage(db, ownerAddress) batch := db.NewBatch() @@ -72,7 +72,7 @@ func Test_RegisterIndex(t *testing.T) { runDB("store registers - owner2", t, func(t *testing.T, db *Storage) { t.Parallel() - r := NewRegisters(db, ownerAddress) + r := NewRegisterStorage(db, ownerAddress) batch := db.NewBatch() @@ -89,3 +89,72 @@ func Test_RegisterIndex(t *testing.T) { require.Error(t, err) }) } + +func Test_StorageSnapshot(t *testing.T) { + + t.Parallel() + owner := []byte("0x1") + ownerAddress := flow.BytesToAddress(owner) + key := []byte("0x3") + value := []byte{0x4} + + runDB("get register", t, func(t *testing.T, db *Storage) { + t.Parallel() + + r := NewRegisterStorage(db, ownerAddress) + s, err := r.GetSnapshotAt(0) + require.NoError(t, err) + + v, err := s.GetValue(owner, key) + require.NoError(t, err) + require.Empty(t, v) + }) + + runDB("get register", t, func(t *testing.T, db *Storage) { + t.Parallel() + + count := uint64(0) + + storageGet := func(id flow.RegisterID, height uint64) (flow.RegisterValue, error) { + count++ + return value, nil + } + + s := NewStorageSnapshot(storageGet, 0) + + v, err := s.GetValue(owner, key) + require.NoError(t, err) + require.Equal(t, value, v) + + v, err = s.GetValue(owner, key) + require.NoError(t, err) + require.Equal(t, value, v) + + // value should be cached + require.Equal(t, uint64(1), count) + }) + + runDB("get register - cache nil", t, func(t *testing.T, db *Storage) { + t.Parallel() + + count := uint64(0) + + storageGet := func(id flow.RegisterID, height uint64) (flow.RegisterValue, error) { + count++ + return nil, nil + } + + s := NewStorageSnapshot(storageGet, 0) + + v, err := s.GetValue(owner, key) + require.NoError(t, err) + require.Empty(t, v) + + v, err = s.GetValue(owner, key) + require.NoError(t, err) + require.Empty(t, v) + + // value should be cached + require.Equal(t, uint64(1), count) + }) +} diff --git a/storage/register_delta.go b/storage/register_delta.go new file mode 100644 index 000000000..e266d41b4 --- /dev/null +++ b/storage/register_delta.go @@ -0,0 +1,74 @@ +package storage + +import ( + "github.com/onflow/atree" + "github.com/onflow/flow-go/fvm/evm/types" + "github.com/onflow/flow-go/model/flow" +) + +var _ types.BackendStorage = &RegisterDelta{} +var _ types.BackendStorageSnapshot = &RegisterDelta{} + +type RegisterDelta struct { + deltas map[flow.RegisterID]flow.RegisterEntry + registers types.BackendStorageSnapshot +} + +// NewRegisterDelta creates a new instance of RegisterDelta. It is not concurrency safe. +// This allows for the caller to build new state on top of the provided snapshot. +// The new state is not persisted. The caller is responsible for persisting the state using +// the `GetUpdates` method. +func NewRegisterDelta( + registers types.BackendStorageSnapshot, +) *RegisterDelta { + return &RegisterDelta{ + deltas: make(map[flow.RegisterID]flow.RegisterEntry), + registers: registers, + } +} + +// GetValue gets the value for the given register ID. If the value was set, it returns that value. +// If the value was not set, it reads the value from the snapshot. +func (r *RegisterDelta) GetValue(owner []byte, key []byte) ([]byte, error) { + id := flow.CadenceRegisterID(owner, key) + + // get from delta first + if delta, ok := r.deltas[id]; ok { + return delta.Value, nil + } + + // get from storage + return r.registers.GetValue(owner, key) +} + +// SetValue sets the value for the given register ID. It sets it in the delta, not in the storage. +func (r *RegisterDelta) SetValue(owner, key, value []byte) error { + id := flow.CadenceRegisterID(owner, key) + + r.deltas[id] = flow.RegisterEntry{Key: id, Value: value} + + return nil +} + +func (r *RegisterDelta) ValueExists(owner []byte, key []byte) (bool, error) { + value, err := r.GetValue(owner, key) + if err != nil { + return false, err + } + return len(value) > 0, nil +} + +// GetUpdates returns the register updates from the delta to be applied to storage. +func (r *RegisterDelta) GetUpdates() flow.RegisterEntries { + entries := make(flow.RegisterEntries, 0, len(r.deltas)) + for id, delta := range r.deltas { + entries = append(entries, flow.RegisterEntry{Key: id, Value: delta.Value}) + } + + return entries +} + +func (r *RegisterDelta) AllocateSlabIndex(_ []byte) (atree.SlabIndex, error) { + // TODO: If needed add later + panic("should not be called") +} diff --git a/storage/register_delta_test.go b/storage/register_delta_test.go new file mode 100644 index 000000000..10c2e501e --- /dev/null +++ b/storage/register_delta_test.go @@ -0,0 +1,244 @@ +package storage_test + +import ( + "testing" + + "github.com/cockroachdb/pebble" + "github.com/rs/zerolog" + "github.com/stretchr/testify/require" + + flowGo "github.com/onflow/flow-go/model/flow" + + "github.com/onflow/flow-evm-gateway/storage" + pebbleStorage "github.com/onflow/flow-evm-gateway/storage/pebble" +) + +func Test_RegisterDeltaWithStorage(t *testing.T) { + owner := []byte{0x01} + ownerAddress := flowGo.BytesToAddress(owner) + owner2 := []byte{0x02} + key := []byte{0x03} + value1 := []byte{0x05} + value2 := []byte{0x06} + + // helper to create a new register delta + delta := func(t *testing.T, r *pebbleStorage.RegisterStorage, evmBlockHeight uint64) *storage.RegisterDelta { + ss, err := r.GetSnapshotAt(0) + require.NoError(t, err) + return storage.NewRegisterDelta(ss) + } + + runDB("get register", t, func(t *testing.T, db *pebbleStorage.Storage) { + t.Parallel() + + r := pebbleStorage.NewRegisterStorage(db, ownerAddress) + d := delta(t, r, 0) + + v, err := d.GetValue(owner, key) + require.NoError(t, err) + require.Len(t, v, 0) + }) + + runDB("set register", t, func(t *testing.T, db *pebbleStorage.Storage) { + t.Parallel() + + r := pebbleStorage.NewRegisterStorage(db, ownerAddress) + d := delta(t, r, 0) + + err := d.SetValue(owner, key, value1) + require.NoError(t, err) + }) + + runDB("set-get register", t, func(t *testing.T, db *pebbleStorage.Storage) { + t.Parallel() + + r := pebbleStorage.NewRegisterStorage(db, ownerAddress) + d := delta(t, r, 0) + + err := d.SetValue(owner, key, value1) + require.NoError(t, err) + + v, err := d.GetValue(owner, key) + require.NoError(t, err) + require.Equal(t, value1, v) + }) + + runDB("set-set-get register", t, func(t *testing.T, db *pebbleStorage.Storage) { + t.Parallel() + + r := pebbleStorage.NewRegisterStorage(db, ownerAddress) + d := delta(t, r, 0) + + err := d.SetValue(owner, key, value1) + require.NoError(t, err) + + err = d.SetValue(owner, key, value2) + require.NoError(t, err) + + v, err := d.GetValue(owner, key) + require.NoError(t, err) + require.Equal(t, value2, v) + }) + + runDB("set-unset-get register", t, func(t *testing.T, db *pebbleStorage.Storage) { + t.Parallel() + + r := pebbleStorage.NewRegisterStorage(db, ownerAddress) + d := delta(t, r, 0) + + err := d.SetValue(owner, key, value1) + require.NoError(t, err) + + err = d.SetValue(owner, key, nil) + require.NoError(t, err) + + v, err := d.GetValue(owner, key) + require.NoError(t, err) + // not actually nil, but empty + require.Len(t, v, 0) + }) + + runDB("set-next-get register", t, func(t *testing.T, db *pebbleStorage.Storage) { + t.Parallel() + + r := pebbleStorage.NewRegisterStorage(db, ownerAddress) + d := delta(t, r, 0) + + err := d.SetValue(owner, key, value1) + require.NoError(t, err) + + err = commit(t, db, d, r) + require.NoError(t, err) + + d = delta(t, r, 1) + + v, err := d.GetValue(owner, key) + require.NoError(t, err) + require.Equal(t, value1, v) + }) + + runDB("set-dont-commit-get register", t, func(t *testing.T, db *pebbleStorage.Storage) { + t.Parallel() + + r := pebbleStorage.NewRegisterStorage(db, ownerAddress) + d := delta(t, r, 0) + + err := d.SetValue(owner, key, value1) + require.NoError(t, err) + + d = delta(t, r, 1) + + v, err := d.GetValue(owner, key) + require.NoError(t, err) + require.Empty(t, v) + }) + + runDB("set-next-set-next-get register", t, func(t *testing.T, db *pebbleStorage.Storage) { + t.Parallel() + + r := pebbleStorage.NewRegisterStorage(db, ownerAddress) + d := delta(t, r, 0) + + err := d.SetValue(owner, key, value1) + require.NoError(t, err) + + err = commit(t, db, d, r) + require.NoError(t, err) + + d = delta(t, r, 1) + + err = d.SetValue(owner, key, value2) + require.NoError(t, err) + + err = commit(t, db, d, r) + require.NoError(t, err) + + d = delta(t, r, 2) + + v, err := d.GetValue(owner, key) + require.NoError(t, err) + require.Equal(t, value2, v) + }) + + runDB("set-next-unset-next-get register", t, func(t *testing.T, db *pebbleStorage.Storage) { + t.Parallel() + + r := pebbleStorage.NewRegisterStorage(db, ownerAddress) + d := delta(t, r, 0) + + err := d.SetValue(owner, key, value1) + require.NoError(t, err) + + err = commit(t, db, d, r) + require.NoError(t, err) + + d = delta(t, r, 1) + + err = d.SetValue(owner, key, nil) + require.NoError(t, err) + + err = commit(t, db, d, r) + require.NoError(t, err) + + d = delta(t, r, 2) + + v, err := d.GetValue(owner, key) + require.NoError(t, err) + // not actually nil, but empty + require.Len(t, v, 0) + }) + + runDB("get with wrong owner", t, func(t *testing.T, db *pebbleStorage.Storage) { + t.Parallel() + + r := pebbleStorage.NewRegisterStorage(db, ownerAddress) + d := delta(t, r, 0) + + _, err := d.GetValue(owner2, key) + require.Error(t, err) + }) + + runDB("commit with wrong owner", t, func(t *testing.T, db *pebbleStorage.Storage) { + t.Parallel() + + s := pebbleStorage.NewRegisterStorage(db, ownerAddress) + d := delta(t, s, 0) + + err := d.SetValue(owner2, key, value1) + require.NoError(t, err) + + err = commit(t, db, d, s) + require.Error(t, err) + }) +} + +func runDB(name string, t *testing.T, f func(t *testing.T, db *pebbleStorage.Storage)) { + dir := t.TempDir() + + db, err := pebbleStorage.New(dir, zerolog.New(zerolog.NewTestWriter(t))) + require.NoError(t, err) + + t.Run(name, func(t *testing.T) { + f(t, db) + }) +} + +// commit is an example on how to commit the delta to storage. +func commit( + t *testing.T, + db *pebbleStorage.Storage, + d *storage.RegisterDelta, + r *pebbleStorage.RegisterStorage, +) error { + batch := db.NewBatch() + + err := r.Store(d.GetUpdates(), 0, batch) + + if err != nil { + return err + } + + err = batch.Commit(pebble.Sync) + require.NoError(t, err) + return nil +} diff --git a/storage/registers_delta.go b/storage/registers_delta.go deleted file mode 100644 index e3ee1eb69..000000000 --- a/storage/registers_delta.go +++ /dev/null @@ -1,125 +0,0 @@ -package storage - -import ( - "github.com/onflow/atree" - "github.com/onflow/flow-go/fvm/evm/types" - "github.com/onflow/flow-go/model/flow" -) - -var _ types.BackendStorage = &RegistersDelta{} - -type RegisterValueAtHeight interface { - Get(id flow.RegisterID, height uint64) (flow.RegisterValue, error) -} - -// RegistersDelta holds the register changes for a current height. -// It is used to collect register changes for a block, while also serving register data -// for the previous heights. -// once all changes were collected, use the `GetUpdates` method to get the register updates. -// and apply them to storage. -// Not safe for concurrent usage. -// -// To avoid creating a new instance of RegistersDelta for every block, use the `Reset` method. -type RegistersDelta struct { - // deltas is a map of register IDs to their respective deltas. - deltas map[flow.RegisterID]flow.RegisterEntry - // height is the height at which the deltas are to be applied. - height uint64 - - // The cache is used to cache register reads from the registers storage (RegisterValueAtHeight) - cache map[flow.RegisterID]flow.RegisterValue - - registers RegisterValueAtHeight -} - -// NewRegistersDelta creates a new instance of RegistersDelta. -// height is used for `GetValue` to fetch the register value at the given height. -// height is not checked against the latest register height. The caller is responsible -// for ensuring that the height is sequential. -func NewRegistersDelta( - height uint64, - registers RegisterValueAtHeight, -) *RegistersDelta { - return &RegistersDelta{ - cache: make(map[flow.RegisterID]flow.RegisterValue), - deltas: make(map[flow.RegisterID]flow.RegisterEntry), - height: height, - registers: registers, - } -} - -// GetValue gets the value for the given register ID. -// 1. It checks if the value was changed at this height, it then returns the value from the delta. -// 2. If the value was not changed at this height, it checks if the value was read from storage at height-1 -// and returns it from the cache. -// 3. It reads the value from storage at the current height and caches it. -func (r *RegistersDelta) GetValue(owner []byte, key []byte) ([]byte, error) { - id := flow.CadenceRegisterID(owner, key) - - // get from delta first - if delta, ok := r.deltas[id]; ok { - return delta.Value, nil - } - - // get from cache if not found in delta - if value, ok := r.cache[id]; ok { - return value, nil - } - - // if height is 0, there should be nothing in storage - if r.height == 0 { - return nil, nil - } - - // get from storage at height-1. This is because we are currently building registers at height - value, err := r.registers.Get(id, r.height-1) - if err != nil { - return nil, err - } - - r.cache[id] = value - return value, nil -} - -// SetValue sets the value for the given register ID. It sets it in the delta and not in the storage. -// To set the value in the storage, use the `GetUpdates` to get the updates and apply them to storage. -func (r *RegistersDelta) SetValue(owner, key, value []byte) error { - id := flow.CadenceRegisterID(owner, key) - - r.deltas[id] = flow.RegisterEntry{Key: id, Value: value} - - return nil -} - -// ValueExists checks if the value for the given register ID exists. -func (r *RegistersDelta) ValueExists(owner []byte, key []byte) (bool, error) { - value, err := r.GetValue(owner, key) - if err != nil { - return false, err - } - return len(value) > 0, nil -} - -// GetUpdates returns the register updates for the current height to be applied to storage. -func (r *RegistersDelta) GetUpdates() flow.RegisterEntries { - entries := make(flow.RegisterEntries, 0, len(r.deltas)) - for id, delta := range r.deltas { - entries = append(entries, flow.RegisterEntry{Key: id, Value: delta.Value}) - } - - return entries -} - -// Reset resets the state of the registers delta to the provided height. -// This can be used to as an optimization to avoid creating a new instance of RegistersDelta -// for every block. -func (r *RegistersDelta) Reset(height uint64) { - r.height = height - clear(r.deltas) - clear(r.cache) -} - -func (r *RegistersDelta) AllocateSlabIndex(_ []byte) (atree.SlabIndex, error) { - // TODO: If needed add later - panic("should not be called") -} diff --git a/storage/registers_delta_test.go b/storage/registers_delta_test.go deleted file mode 100644 index 8ba9955c7..000000000 --- a/storage/registers_delta_test.go +++ /dev/null @@ -1,311 +0,0 @@ -package storage_test - -import ( - "fmt" - "testing" - - "github.com/cockroachdb/pebble" - "github.com/rs/zerolog" - "github.com/stretchr/testify/require" - - flowGo "github.com/onflow/flow-go/model/flow" - - "github.com/onflow/flow-evm-gateway/storage" - pebbleStorage "github.com/onflow/flow-evm-gateway/storage/pebble" -) - -func Test_RegisterDeltaWithStorage(t *testing.T) { - owner := []byte{0x01} - ownerAddress := flowGo.BytesToAddress(owner) - owner2 := []byte{0x02} - key := []byte{0x03} - value1 := []byte{0x05} - value2 := []byte{0x06} - - runDB("get register", t, func(t *testing.T, db *pebbleStorage.Storage) { - t.Parallel() - - s := pebbleStorage.NewRegisters(db, ownerAddress) - r := storage.NewRegistersDelta(0, s) - - v, err := r.GetValue(owner, key) - require.NoError(t, err) - require.Len(t, v, 0) - }) - - runDB("set register", t, func(t *testing.T, db *pebbleStorage.Storage) { - t.Parallel() - - s := pebbleStorage.NewRegisters(db, ownerAddress) - r := storage.NewRegistersDelta(0, s) - - err := r.SetValue(owner, key, value1) - require.NoError(t, err) - }) - - runDB("set-get register", t, func(t *testing.T, db *pebbleStorage.Storage) { - t.Parallel() - - s := pebbleStorage.NewRegisters(db, ownerAddress) - r := storage.NewRegistersDelta(0, s) - - err := r.SetValue(owner, key, value1) - require.NoError(t, err) - - v, err := r.GetValue(owner, key) - require.NoError(t, err) - require.Equal(t, value1, v) - }) - - runDB("set-set-get register", t, func(t *testing.T, db *pebbleStorage.Storage) { - t.Parallel() - - s := pebbleStorage.NewRegisters(db, ownerAddress) - r := storage.NewRegistersDelta(0, s) - - err := r.SetValue(owner, key, value1) - require.NoError(t, err) - - err = r.SetValue(owner, key, value2) - require.NoError(t, err) - - v, err := r.GetValue(owner, key) - require.NoError(t, err) - require.Equal(t, value2, v) - }) - - runDB("set-unset-get register", t, func(t *testing.T, db *pebbleStorage.Storage) { - t.Parallel() - - s := pebbleStorage.NewRegisters(db, ownerAddress) - r := storage.NewRegistersDelta(0, s) - - err := r.SetValue(owner, key, value1) - require.NoError(t, err) - - err = r.SetValue(owner, key, nil) - require.NoError(t, err) - - v, err := r.GetValue(owner, key) - require.NoError(t, err) - // not actually nil, but empty - require.Len(t, v, 0) - }) - - runDB("set-next-get register", t, func(t *testing.T, db *pebbleStorage.Storage) { - t.Parallel() - - s := pebbleStorage.NewRegisters(db, ownerAddress) - r := storage.NewRegistersDelta(0, s) - - err := r.SetValue(owner, key, value1) - require.NoError(t, err) - - err = commit(t, db, r, s) - require.NoError(t, err) - - r.Reset(1) - - v, err := r.GetValue(owner, key) - require.NoError(t, err) - require.Equal(t, value1, v) - }) - - runDB("set-dont-commit-get register", t, func(t *testing.T, db *pebbleStorage.Storage) { - t.Parallel() - - s := pebbleStorage.NewRegisters(db, ownerAddress) - r := storage.NewRegistersDelta(0, s) - - err := r.SetValue(owner, key, value1) - require.NoError(t, err) - - r.Reset(1) - - v, err := r.GetValue(owner, key) - require.NoError(t, err) - require.Empty(t, v) - }) - - runDB("set-next-set-next-get register", t, func(t *testing.T, db *pebbleStorage.Storage) { - t.Parallel() - - s := pebbleStorage.NewRegisters(db, ownerAddress) - r := storage.NewRegistersDelta(0, s) - - err := r.SetValue(owner, key, value1) - require.NoError(t, err) - - err = commit(t, db, r, s) - require.NoError(t, err) - - r.Reset(1) - - err = r.SetValue(owner, key, value2) - require.NoError(t, err) - - err = commit(t, db, r, s) - require.NoError(t, err) - - r.Reset(2) - - v, err := r.GetValue(owner, key) - require.NoError(t, err) - require.Equal(t, value2, v) - }) - - runDB("set-next-unset-next-get register", t, func(t *testing.T, db *pebbleStorage.Storage) { - t.Parallel() - - s := pebbleStorage.NewRegisters(db, ownerAddress) - r := storage.NewRegistersDelta(0, s) - - err := r.SetValue(owner, key, value1) - require.NoError(t, err) - - err = commit(t, db, r, s) - require.NoError(t, err) - - r.Reset(1) - - err = r.SetValue(owner, key, nil) - require.NoError(t, err) - - err = commit(t, db, r, s) - require.NoError(t, err) - - r.Reset(2) - - v, err := r.GetValue(owner, key) - require.NoError(t, err) - // not actually nil, but empty - require.Len(t, v, 0) - }) - - runDB("get with wrong owner", t, func(t *testing.T, db *pebbleStorage.Storage) { - t.Parallel() - - s := pebbleStorage.NewRegisters(db, ownerAddress) - r := storage.NewRegistersDelta(1, s) - - _, err := r.GetValue(owner2, key) - require.Error(t, err) - }) - - runDB("commit with wrong owner", t, func(t *testing.T, db *pebbleStorage.Storage) { - t.Parallel() - - s := pebbleStorage.NewRegisters(db, ownerAddress) - r := storage.NewRegistersDelta(0, s) - - err := r.SetValue(owner2, key, value1) - require.NoError(t, err) - - err = commit(t, db, r, s) - require.Error(t, err) - }) - - runDB("cache db values", t, func(t *testing.T, db *pebbleStorage.Storage) { - t.Parallel() - - s := &mockRegisterIndex{ - callback: func(id flowGo.RegisterID, height uint64) (flowGo.RegisterValue, error) { - return value1, nil - }, - } - r := storage.NewRegistersDelta(1, s) - - v, err := r.GetValue(owner, key) - require.NoError(t, err) - require.Equal(t, value1, v) - - v, err = r.GetValue(owner, key) - require.NoError(t, err) - require.Equal(t, value1, v) - - // only one call to the index - require.Equal(t, uint(1), s.callCount) - }) - - runDB("cache nil db values", t, func(t *testing.T, db *pebbleStorage.Storage) { - t.Parallel() - - s := &mockRegisterIndex{ - callback: func(id flowGo.RegisterID, height uint64) (flowGo.RegisterValue, error) { - return nil, nil - }, - } - r := storage.NewRegistersDelta(1, s) - - v, err := r.GetValue(owner, key) - require.NoError(t, err) - require.Empty(t, v) - - v, err = r.GetValue(owner, key) - require.NoError(t, err) - require.Empty(t, v) - - // only one call to the index - require.Equal(t, uint(1), s.callCount) - }) - - runDB("dont cache err", t, func(t *testing.T, db *pebbleStorage.Storage) { - t.Parallel() - - s := &mockRegisterIndex{ - callback: func(id flowGo.RegisterID, height uint64) (flowGo.RegisterValue, error) { - return nil, fmt.Errorf("error") - }, - } - r := storage.NewRegistersDelta(1, s) - - _, err := r.GetValue(owner, key) - require.Error(t, err) - - _, err = r.GetValue(owner, key) - require.Error(t, err) - - require.Equal(t, uint(2), s.callCount) - }) -} - -type mockRegisterIndex struct { - callCount uint - callback func(id flowGo.RegisterID, height uint64) (flowGo.RegisterValue, error) -} - -func (m *mockRegisterIndex) Get(id flowGo.RegisterID, height uint64) (flowGo.RegisterValue, error) { - m.callCount++ - return m.callback(id, height) -} - -func runDB(name string, t *testing.T, f func(t *testing.T, db *pebbleStorage.Storage)) { - dir := t.TempDir() - - db, err := pebbleStorage.New(dir, zerolog.New(zerolog.NewTestWriter(t))) - require.NoError(t, err) - - t.Run(name, func(t *testing.T) { - f(t, db) - }) -} - -// commit is an example on how to commit the delta to storage. -func commit( - t *testing.T, - db *pebbleStorage.Storage, - d *storage.RegistersDelta, - r *pebbleStorage.RegisterIndex, -) error { - batch := db.NewBatch() - - err := r.Store(d.GetUpdates(), 0, batch) - - if err != nil { - return err - } - - err = batch.Commit(pebble.Sync) - require.NoError(t, err) - return nil -} From 5a8de37c6b2860d3cc74c8399474e7f29de91f61 Mon Sep 17 00:00:00 2001 From: Janez Podhostnik <67895329+janezpodhostnik@users.noreply.github.com> Date: Thu, 7 Nov 2024 15:06:11 +0100 Subject: [PATCH 019/174] Update storage/pebble/register_storage.go Co-authored-by: Leo Zhang --- storage/pebble/register_storage.go | 1 + 1 file changed, 1 insertion(+) diff --git a/storage/pebble/register_storage.go b/storage/pebble/register_storage.go index 2a3de5797..ebe5ba680 100644 --- a/storage/pebble/register_storage.go +++ b/storage/pebble/register_storage.go @@ -214,6 +214,7 @@ func (s StorageSnapshot) GetValue(owner []byte, key []byte) ([]byte, error) { return nil, err } + // non-existing key will also be cached with `nil` value. s.cache[id] = val return val, nil } From 7e56601917c58e4fd3fba10e0e1d48fad681ea25 Mon Sep 17 00:00:00 2001 From: Janez Podhostnik Date: Thu, 7 Nov 2024 15:08:09 +0100 Subject: [PATCH 020/174] add comment --- storage/pebble/register_storage.go | 1 + 1 file changed, 1 insertion(+) diff --git a/storage/pebble/register_storage.go b/storage/pebble/register_storage.go index ebe5ba680..64436f516 100644 --- a/storage/pebble/register_storage.go +++ b/storage/pebble/register_storage.go @@ -191,6 +191,7 @@ type StorageSnapshot struct { // NewStorageSnapshot creates a new snapshot of the register index at the given block height. // the snapshot has a cache. Nil values are cached. +// The snapshot is not concurrency-safe. func NewStorageSnapshot(get GetAtHeightFunc, evmBlockHeight uint64) *StorageSnapshot { return &StorageSnapshot{ cache: make(map[flow.RegisterID]flow.RegisterValue), From e7030cd6142809b81f95a2812d96364e59836ade Mon Sep 17 00:00:00 2001 From: Ardit Marku Date: Mon, 4 Nov 2024 13:28:35 +0200 Subject: [PATCH 021/174] Integrate & incorporate flow-go's onchain package --- api/debug.go | 374 +++++++++++++++++-- api/encode_transaction.go | 30 +- bootstrap/bootstrap.go | 71 +++- go.mod | 4 + go.sum | 21 ++ services/evm/executor.go | 172 +++++++++ services/ingestion/engine.go | 73 +++- services/ingestion/engine_test.go | 89 ++++- services/replayer/blocks_provider.go | 3 +- services/replayer/call_tracer_collector.go | 214 +++++++++++ services/replayer/config.go | 12 + services/requester/requester.go | 408 ++++++--------------- storage/pebble/register.go | 172 +++++++++ tests/e2e_web3js_test.go | 1 - tests/go.mod | 4 + tests/go.sum | 28 +- tests/web3js/eth_rate_limit_test.js | 16 +- 17 files changed, 1281 insertions(+), 411 deletions(-) create mode 100644 services/evm/executor.go create mode 100644 services/replayer/call_tracer_collector.go create mode 100644 services/replayer/config.go create mode 100644 storage/pebble/register.go diff --git a/api/debug.go b/api/debug.go index bf3a0f891..79fdca187 100644 --- a/api/debug.go +++ b/api/debug.go @@ -2,16 +2,32 @@ package api import ( "context" + "fmt" + "math/big" + "slices" "github.com/goccy/go-json" + "github.com/onflow/flow-go/fvm/evm/offchain/query" gethCommon "github.com/onflow/go-ethereum/common" "github.com/onflow/go-ethereum/eth/tracers" + "github.com/onflow/go-ethereum/eth/tracers/logger" "github.com/onflow/go-ethereum/rpc" "github.com/rs/zerolog" + "github.com/onflow/flow-evm-gateway/config" "github.com/onflow/flow-evm-gateway/metrics" "github.com/onflow/flow-evm-gateway/models" + errs "github.com/onflow/flow-evm-gateway/models/errors" + "github.com/onflow/flow-evm-gateway/services/evm" + "github.com/onflow/flow-evm-gateway/services/replayer" "github.com/onflow/flow-evm-gateway/storage" + "github.com/onflow/flow-evm-gateway/storage/pebble" + flowEVM "github.com/onflow/flow-go/fvm/evm" + + // this import is needed for side-effects, because the + // tracers.DefaultDirectory is relying on the init function + _ "github.com/onflow/go-ethereum/eth/tracers/js" + _ "github.com/onflow/go-ethereum/eth/tracers/native" ) // txTraceResult is the result of a single transaction trace. @@ -22,77 +38,373 @@ type txTraceResult struct { } type DebugAPI struct { - logger zerolog.Logger - tracer storage.TraceIndexer - blocks storage.BlockIndexer - collector metrics.Collector + store *pebble.Storage + logger zerolog.Logger + tracer storage.TraceIndexer + blocks storage.BlockIndexer + transactions storage.TransactionIndexer + receipts storage.ReceiptIndexer + config *config.Config + collector metrics.Collector } -func NewDebugAPI(tracer storage.TraceIndexer, blocks storage.BlockIndexer, logger zerolog.Logger, collector metrics.Collector) *DebugAPI { +func NewDebugAPI( + store *pebble.Storage, + tracer storage.TraceIndexer, + blocks storage.BlockIndexer, + transactions storage.TransactionIndexer, + receipts storage.ReceiptIndexer, + config *config.Config, + logger zerolog.Logger, + collector metrics.Collector, +) *DebugAPI { return &DebugAPI{ - logger: logger, - tracer: tracer, - blocks: blocks, - collector: collector, + store: store, + logger: logger, + tracer: tracer, + blocks: blocks, + transactions: transactions, + receipts: receipts, + config: config, + collector: collector, } } // TraceTransaction will return a debug execution trace of a transaction if it exists, // currently we only support CALL traces, so the config is ignored. func (d *DebugAPI) TraceTransaction( - _ context.Context, + ctx context.Context, hash gethCommon.Hash, - _ *tracers.TraceConfig, + config *tracers.TraceConfig, ) (json.RawMessage, error) { - res, err := d.tracer.GetTransaction(hash) + if config != nil { + if *config.Tracer == replayer.TracerName && + slices.Equal(config.TracerConfig, json.RawMessage(replayer.TracerConfig)) { + trace, err := d.tracer.GetTransaction(hash) + if err == nil { + return trace, nil + } + } + } + + receipt, err := d.receipts.GetByTransactionID(hash) + if err != nil { + return nil, err + } + + tracer, err := tracerForReceipt(config, receipt) + if err != nil { + return nil, err + } + + block, err := d.blocks.GetByHeight(receipt.BlockNumber.Uint64()) + if err != nil { + return nil, err + } + // We need to re-execute the given transaction and all the + // transactions that precede it in the same block, based on + // the previous block state, to generate the correct trace. + previousBlock, err := d.blocks.GetByHeight(block.Height - 1) + if err != nil { + return nil, err + } + + blockExecutor, err := d.executorAtBlock(previousBlock) if err != nil { - return handleError[json.RawMessage](err, d.logger, d.collector) + return nil, err } - return res, nil + + // Re-execute the transactions in the order they appear, for the block + // that contains the given transaction. We set the tracer only for + // the given transaction, as we don't need it for the preceding + // transactions. Once we re-execute the desired transaction, we ignore + // the rest of the transactions in the block, and simply return the trace + // result. + txExecuted := false + var txTracer *tracers.Tracer + for _, h := range block.TransactionHashes { + if txExecuted { + break + } + + tx, err := d.transactions.Get(h) + if err != nil { + return nil, err + } + + if h == hash { + txTracer = tracer + txExecuted = true + } + + _, err = blockExecutor.Run(tx, txTracer) + if err != nil { + return nil, err + } + } + + return txTracer.GetResult() } func (d *DebugAPI) TraceBlockByNumber( ctx context.Context, number rpc.BlockNumber, - cfg *tracers.TraceConfig, + config *tracers.TraceConfig, ) ([]*txTraceResult, error) { block, err := d.blocks.GetByHeight(uint64(number.Int64())) if err != nil { - return handleError[[]*txTraceResult](err, d.logger, d.collector) + return nil, err + } + + results := make([]*txTraceResult, len(block.TransactionHashes)) + + if config != nil { + if *config.Tracer == replayer.TracerName && + slices.Equal(config.TracerConfig, json.RawMessage(replayer.TracerConfig)) { + for i, hash := range block.TransactionHashes { + trace, err := d.tracer.GetTransaction(hash) + + if err != nil { + results[i] = &txTraceResult{TxHash: hash, Error: err.Error()} + } else { + results[i] = &txTraceResult{TxHash: hash, Result: trace} + } + } + + return results, nil + } + } + + // We need to re-execute all the transactions from the given block, + // on top of the previous block state, to generate the correct traces. + previousBlock, err := d.blocks.GetByHeight(block.Height - 1) + if err != nil { + return nil, err } - return d.traceBlock(ctx, block, cfg) + blockExecutor, err := d.executorAtBlock(previousBlock) + if err != nil { + return nil, err + } + + for i, h := range block.TransactionHashes { + tx, err := d.transactions.Get(h) + if err != nil { + return nil, err + } + + receipt, err := d.receipts.GetByTransactionID(tx.Hash()) + if err != nil { + return nil, err + } + + tracer, err := tracerForReceipt(config, receipt) + if err != nil { + return nil, err + } + + _, err = blockExecutor.Run(tx, tracer) + if err != nil { + results[i] = &txTraceResult{TxHash: h, Error: err.Error()} + continue + } + + txTrace, err := tracer.GetResult() + if err != nil { + results[i] = &txTraceResult{TxHash: h, Error: err.Error()} + } else { + results[i] = &txTraceResult{TxHash: h, Result: txTrace} + } + } + + return results, nil } func (d *DebugAPI) TraceBlockByHash( ctx context.Context, hash gethCommon.Hash, - cfg *tracers.TraceConfig, + config *tracers.TraceConfig, ) ([]*txTraceResult, error) { block, err := d.blocks.GetByID(hash) if err != nil { - return handleError[[]*txTraceResult](err, d.logger, d.collector) + return nil, err } - return d.traceBlock(ctx, block, cfg) + return d.TraceBlockByNumber(ctx, rpc.BlockNumber(block.Height), config) } -func (d *DebugAPI) traceBlock( +func (d *DebugAPI) TraceCall( ctx context.Context, - block *models.Block, - _ *tracers.TraceConfig, -) ([]*txTraceResult, error) { - results := make([]*txTraceResult, len(block.TransactionHashes)) - for i, h := range block.TransactionHashes { + args TransactionArgs, + blockNrOrHash rpc.BlockNumberOrHash, + config *tracers.TraceCallConfig, +) (interface{}, error) { + tx, err := encodeTxFromArgs(args) + if err != nil { + return nil, err + } + + // Default address in case user does not provide one + from := d.config.Coinbase + if args.From != nil { + from = *args.From + } + + var traceConfig *tracers.TraceConfig + if config != nil { + traceConfig = &config.TraceConfig + } + + tracer, err := tracerForReceipt(traceConfig, nil) + if err != nil { + return nil, err + } - txTrace, err := d.TraceTransaction(ctx, h, nil) + height, err := d.resolveBlockNumberOrHash(&blockNrOrHash) + if err != nil { + return nil, err + } + + block, err := d.blocks.GetByHeight(height) + if err != nil { + return nil, err + } + + ledger := pebble.NewRegister(d.store, block.Height, nil) + blocksProvider := replayer.NewBlocksProvider( + d.blocks, + d.config.FlowNetworkID, + tracer, + ) + viewProvider := query.NewViewProvider( + d.config.FlowNetworkID, + flowEVM.StorageAccountAddress(d.config.FlowNetworkID), + ledger, + blocksProvider, + 120_000_000, + ) + view, err := viewProvider.GetBlockView(block.Height) + if err != nil { + return nil, err + } + + to := gethCommon.Address{} + if tx.To != nil { + to = *tx.To + } + opts := []query.DryCallOption{} + opts = append(opts, query.WithTracer(tracer)) + if config.StateOverrides != nil { + for addr, overrideAccount := range *config.StateOverrides { + if overrideAccount.Nonce != nil { + opts = append(opts, query.WithStateOverrideNonce(addr, uint64(*overrideAccount.Nonce))) + } + if overrideAccount.Code != nil { + opts = append(opts, query.WithStateOverrideCode(addr, *overrideAccount.Code)) + } + if overrideAccount.Balance != nil { + opts = append(opts, query.WithStateOverrideBalance(addr, (*big.Int)(*overrideAccount.Balance))) + } + if overrideAccount.State != nil { + opts = append(opts, query.WithStateOverrideState(addr, *overrideAccount.State)) + } + if overrideAccount.StateDiff != nil { + opts = append(opts, query.WithStateOverrideStateDiff(addr, *overrideAccount.StateDiff)) + } + } + } + _, err = view.DryCall( + from, + to, + tx.Data, + tx.Value, + tx.Gas, + opts..., + ) + + if err != nil { + return nil, err + } + + return tracer.GetResult() +} + +func (d *DebugAPI) executorAtBlock(block *models.Block) (*evm.BlockExecutor, error) { + ledger := pebble.NewRegister(d.store, block.Height, d.store.NewBatch()) + + return evm.NewBlockExecutor( + block, + ledger, + d.config.FlowNetworkID, + d.blocks, + d.receipts, + d.logger, + ) +} + +func (d *DebugAPI) resolveBlockNumberOrHash(block *rpc.BlockNumberOrHash) (uint64, error) { + err := fmt.Errorf("%w: neither block number nor hash specified", errs.ErrInvalid) + if block == nil { + return 0, err + } + if number, ok := block.Number(); ok { + return d.resolveBlockNumber(number) + } + + if hash, ok := block.Hash(); ok { + evmHeight, err := d.blocks.GetHeightByID(hash) if err != nil { - results[i] = &txTraceResult{TxHash: h, Error: err.Error()} - } else { - results[i] = &txTraceResult{TxHash: h, Result: txTrace} + return 0, err } + return evmHeight, nil } - return results, nil + return 0, err +} + +func (d *DebugAPI) resolveBlockNumber(number rpc.BlockNumber) (uint64, error) { + height := number.Int64() + + // if special values (latest) we return latest executed height + if height < 0 { + executed, err := d.blocks.LatestEVMHeight() + if err != nil { + return 0, err + } + height = int64(executed) + } + + return uint64(height), nil +} + +func tracerForReceipt( + config *tracers.TraceConfig, + receipt *models.Receipt, +) (*tracers.Tracer, error) { + tracerCtx := &tracers.Context{} + if receipt != nil { + tracerCtx = &tracers.Context{ + BlockHash: receipt.BlockHash, + BlockNumber: receipt.BlockNumber, + TxIndex: int(receipt.TransactionIndex), + TxHash: receipt.TxHash, + } + } + + if config == nil { + config = &tracers.TraceConfig{} + } + + // Default tracer is the struct logger + if config.Tracer == nil { + logger := logger.NewStructLogger(config.Config) + return &tracers.Tracer{ + Hooks: logger.Hooks(), + GetResult: logger.GetResult, + Stop: logger.Stop, + }, nil + } + + return tracers.DefaultDirectory.New(*config.Tracer, tracerCtx, config.TracerConfig) } diff --git a/api/encode_transaction.go b/api/encode_transaction.go index 44dacec8a..b6e4d7a1a 100644 --- a/api/encode_transaction.go +++ b/api/encode_transaction.go @@ -1,12 +1,9 @@ package api import ( - "fmt" "math/big" "github.com/onflow/go-ethereum/core/types" - - errs "github.com/onflow/flow-evm-gateway/models/errors" ) const blockGasLimit uint64 = 120_000_000 @@ -16,7 +13,7 @@ const blockGasLimit uint64 = 120_000_000 // `EVM.dryRun` inside Cadence scripts, meaning that no state change // will occur. // This is only useful for `eth_estimateGas` and `eth_call` endpoints. -func encodeTxFromArgs(args TransactionArgs) ([]byte, error) { +func encodeTxFromArgs(args TransactionArgs) (*types.LegacyTx, error) { var data []byte if args.Data != nil { data = *args.Data @@ -36,21 +33,12 @@ func encodeTxFromArgs(args TransactionArgs) ([]byte, error) { value = args.Value.ToInt() } - tx := types.NewTx( - &types.LegacyTx{ - Nonce: 0, - To: args.To, - Value: value, - Gas: gasLimit, - GasPrice: big.NewInt(0), - Data: data, - }, - ) - - enc, err := tx.MarshalBinary() - if err != nil { - return nil, fmt.Errorf("%w: %w", errs.ErrInvalid, err) - } - - return enc, nil + return &types.LegacyTx{ + Nonce: 0, + To: args.To, + Value: value, + Gas: gasLimit, + GasPrice: big.NewInt(0), + Data: data, + }, nil } diff --git a/bootstrap/bootstrap.go b/bootstrap/bootstrap.go index 9398019d8..ed2caa057 100644 --- a/bootstrap/bootstrap.go +++ b/bootstrap/bootstrap.go @@ -2,7 +2,6 @@ package bootstrap import ( "context" - "encoding/json" "errors" "fmt" "math" @@ -11,8 +10,10 @@ import ( "github.com/onflow/flow-go-sdk/access" "github.com/onflow/flow-go-sdk/access/grpc" "github.com/onflow/flow-go-sdk/crypto" + "github.com/onflow/flow-go/fvm/environment" + "github.com/onflow/flow-go/fvm/evm" + flowGo "github.com/onflow/flow-go/model/flow" gethTypes "github.com/onflow/go-ethereum/core/types" - "github.com/onflow/go-ethereum/eth/tracers" "github.com/rs/zerolog" "github.com/sethvargo/go-limiter/memorystore" grpcOpts "google.golang.org/grpc" @@ -27,15 +28,6 @@ import ( "github.com/onflow/flow-evm-gateway/services/requester" "github.com/onflow/flow-evm-gateway/storage" "github.com/onflow/flow-evm-gateway/storage/pebble" - - // this import is needed for side-effects, because the - // tracers.DefaultDirectory is relying on the init function - _ "github.com/onflow/go-ethereum/eth/tracers/native" -) - -const ( - callTracerConfig = `{ "onlyTopCall": true }` - callTracerName = "callTracer" ) type Storages struct { @@ -126,27 +118,31 @@ func (b *Bootstrap) StartEventIngestion(ctx context.Context) error { Uint64("missed-heights", latestCadenceBlock.Height-latestCadenceHeight). Msg("indexing cadence height information") + chainID := b.config.FlowNetworkID + // create event subscriber subscriber := ingestion.NewRPCEventSubscriber( b.logger, b.client, - b.config.FlowNetworkID, + chainID, latestCadenceHeight, ) - tracer, err := tracers.DefaultDirectory.New( - callTracerName, - &tracers.Context{}, - json.RawMessage(callTracerConfig), - ) + callTracerCollector, err := replayer.NewCallTracerCollector(b.logger) if err != nil { return err } blocksProvider := replayer.NewBlocksProvider( b.storages.Blocks, - b.config.FlowNetworkID, - tracer, + chainID, + callTracerCollector.TxTracer(), ) + replayerConfig := replayer.Config{ + ChainID: chainID, + RootAddr: evm.StorageAccountAddress(chainID), + CallTracerCollector: callTracerCollector, + ValidateResults: true, + } // initialize event ingestion engine b.events = ingestion.NewEventIngestionEngine( @@ -157,10 +153,12 @@ func (b *Bootstrap) StartEventIngestion(ctx context.Context) error { b.storages.Receipts, b.storages.Transactions, b.storages.Accounts, + b.storages.Traces, b.publishers.Block, b.publishers.Logs, b.logger, b.collector, + replayerConfig, ) StartEngine(ctx, b.events, l) @@ -209,7 +207,19 @@ func (b *Bootstrap) StartAPIServer(ctx context.Context) error { b.logger, ) + tracer, err := replayer.DefaultCallTracer() + if err != nil { + return err + } + blocksProvider := replayer.NewBlocksProvider( + b.storages.Blocks, + b.config.FlowNetworkID, + tracer, + ) + evm, err := requester.NewEVM( + b.storages.Storage, + blocksProvider, b.client, b.config, signer, @@ -269,7 +279,16 @@ func (b *Bootstrap) StartAPIServer(ctx context.Context) error { ratelimiter, ) - var debugAPI = api.NewDebugAPI(b.storages.Traces, b.storages.Blocks, b.logger, b.collector) + debugAPI := api.NewDebugAPI( + b.storages.Storage, + b.storages.Traces, + b.storages.Blocks, + b.storages.Transactions, + b.storages.Receipts, + b.config, + b.logger, + b.collector, + ) var walletAPI *api.WalletAPI if b.config.WalletEnabled { @@ -462,6 +481,18 @@ func setupStorage( return nil, fmt.Errorf("could not fetch provided cadence height, make sure it's correct: %w", err) } + storageProvider := pebble.NewRegister(store, 0, nil) + storageAddress := evm.StorageAccountAddress(config.FlowNetworkID) + accountStatus := environment.NewAccountStatus() + err = storageProvider.SetValue( + storageAddress[:], + []byte(flowGo.AccountStatusKey), + accountStatus.ToBytes(), + ) + if err != nil { + return nil, fmt.Errorf("could not initialize state index: %w", err) + } + if err := blocks.InitHeights(cadenceHeight, cadenceBlock.ID); err != nil { return nil, fmt.Errorf( "failed to init the database for block height: %d and ID: %s, with : %w", diff --git a/go.mod b/go.mod index 8358b3b3a..09c4cc00c 100644 --- a/go.mod +++ b/go.mod @@ -59,6 +59,8 @@ require ( github.com/dgraph-io/badger/v2 v2.2007.4 // indirect github.com/dgraph-io/ristretto v0.1.0 // indirect github.com/dgryski/go-farm v0.0.0-20190423205320-6a90982ecee2 // indirect + github.com/dlclark/regexp2 v1.7.0 // indirect + github.com/dop251/goja v0.0.0-20230806174421-c933cf95e127 // indirect github.com/dustin/go-humanize v1.0.1 // indirect github.com/ef-ds/deque v1.0.4 // indirect github.com/ethereum/c-kzg-4844 v1.0.0 // indirect @@ -74,12 +76,14 @@ require ( github.com/go-logr/logr v1.4.1 // indirect github.com/go-logr/stdr v1.2.2 // indirect github.com/go-ole/go-ole v1.3.0 // indirect + github.com/go-sourcemap/sourcemap v2.1.3+incompatible // indirect github.com/gofrs/flock v0.8.1 // indirect github.com/gogo/protobuf v1.3.2 // indirect github.com/golang/glog v1.2.0 // indirect github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da // indirect github.com/golang/protobuf v1.5.4 // indirect github.com/golang/snappy v0.0.5-0.20220116011046-fa5810519dcb // indirect + github.com/google/pprof v0.0.0-20231229205709-960ae82b1e42 // indirect github.com/google/s2a-go v0.1.7 // indirect github.com/google/uuid v1.6.0 // indirect github.com/googleapis/enterprise-certificate-proxy v0.3.2 // indirect diff --git a/go.sum b/go.sum index 2160cd173..1ea7c5d4e 100644 --- a/go.sum +++ b/go.sum @@ -91,8 +91,11 @@ github.com/cespare/xxhash/v2 v2.2.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XL github.com/cespare/xxhash/v2 v2.3.0 h1:UL815xU9SqsFlibzuggzjXhog7bL6oX9BbNZnL2UFvs= github.com/cespare/xxhash/v2 v2.3.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs= github.com/chzyer/logex v1.1.10/go.mod h1:+Ywpsq7O8HXn0nuIou7OrIPyXbp3wmkHB+jjWRnGsAI= +github.com/chzyer/logex v1.2.0/go.mod h1:9+9sk7u7pGNWYMkh0hdiL++6OeibzJccyQU4p4MedaY= github.com/chzyer/readline v0.0.0-20180603132655-2972be24d48e/go.mod h1:nSuG5e5PlCu98SY8svDHJxuZscDgtXS6KTTbou5AhLI= +github.com/chzyer/readline v1.5.0/go.mod h1:x22KAscuvRqlLoK9CsoYsmxoXZMMFVyOl86cAH8qUic= github.com/chzyer/test v0.0.0-20180213035817-a1ea475d72b1/go.mod h1:Q3SI9o4m/ZMnBNeIyt5eFwwo7qiLfzFZmjNmxjkiQlU= +github.com/chzyer/test v0.0.0-20210722231415-061457976a23/go.mod h1:Q3SI9o4m/ZMnBNeIyt5eFwwo7qiLfzFZmjNmxjkiQlU= github.com/client9/misspell v0.3.4/go.mod h1:qj6jICC3Q7zFZvVWo7KLAzC3yx5G7kyvSDkc90ppPyw= github.com/cncf/udpa/go v0.0.0-20191209042840-269d4d468f6f/go.mod h1:M8M6+tZqaGXZJjfX53e64911xZQV5JYwmTeXPW+k8Sc= github.com/cncf/udpa/go v0.0.0-20200629203442-efcf912fb354/go.mod h1:WmhPx2Nbnhtbo57+VJT5O0JRkEi1Wbu0z5j0R8u5Hbk= @@ -153,8 +156,16 @@ github.com/dgrijalva/jwt-go v3.2.0+incompatible/go.mod h1:E3ru+11k8xSBh+hMPgOLZm github.com/dgryski/go-farm v0.0.0-20190423205320-6a90982ecee2 h1:tdlZCpZ/P9DhczCTSixgIKmwPv6+wP5DGjqLYw5SUiA= github.com/dgryski/go-farm v0.0.0-20190423205320-6a90982ecee2/go.mod h1:SqUrOPUnsFjfmXRMNPybcSiG0BgUW2AuFH8PAnS2iTw= github.com/dgryski/go-sip13 v0.0.0-20181026042036-e10d5fee7954/go.mod h1:vAd38F8PWV+bWy6jNmig1y/TA+kYO4g3RSRF0IAv0no= +github.com/dlclark/regexp2 v1.4.1-0.20201116162257-a2a8dda75c91/go.mod h1:2pZnwuY/m+8K6iRw6wQdMtk+rH5tNGR1i55kozfMjCc= +github.com/dlclark/regexp2 v1.7.0 h1:7lJfhqlPssTb1WQx4yvTHN0uElPEv52sbaECrAQxjAo= +github.com/dlclark/regexp2 v1.7.0/go.mod h1:DHkYz0B9wPfa6wondMfaivmHpzrQ3v9q8cnmRbL6yW8= github.com/docker/go-units v0.5.0 h1:69rxXcBk27SvSaaxTtLh/8llcHD8vYHT7WSdRZ/jvr4= github.com/docker/go-units v0.5.0/go.mod h1:fgPhTUdO+D/Jk86RDLlptpiXQzgHJF7gydDDbaIK4Dk= +github.com/dop251/goja v0.0.0-20211022113120-dc8c55024d06/go.mod h1:R9ET47fwRVRPZnOGvHxxhuZcbrMCuiqOz3Rlrh4KSnk= +github.com/dop251/goja v0.0.0-20230806174421-c933cf95e127 h1:qwcF+vdFrvPSEUDSX5RVoRccG8a5DhOdWdQ4zN62zzo= +github.com/dop251/goja v0.0.0-20230806174421-c933cf95e127/go.mod h1:QMWlm50DNe14hD7t24KEqZuUdC9sOTy8W6XbCU1mlw4= +github.com/dop251/goja_nodejs v0.0.0-20210225215109-d91c329300e7/go.mod h1:hn7BA7c8pLvoGndExHudxTDKZ84Pyvv+90pbBjbTz0Y= +github.com/dop251/goja_nodejs v0.0.0-20211022123610-8dd9abb0616d/go.mod h1:DngW8aVqWbuLRMHItjPUyqdj+HWPvnQe8V8y1nDpIbM= github.com/dustin/go-humanize v1.0.0/go.mod h1:HtrtbFcZ19U5GC7JDqmcUSB87Iq5E25KnS6fMYU6eOk= github.com/dustin/go-humanize v1.0.1 h1:GzkhY7T5VNhEkwH0PVJgjz+fX1rhBrR7pRT3mDkpeCY= github.com/dustin/go-humanize v1.0.1/go.mod h1:Mu1zIs6XwVuF/gI1OepvI0qD18qycQx+mFykh5fBlto= @@ -219,6 +230,8 @@ github.com/go-ole/go-ole v1.3.0 h1:Dt6ye7+vXGIKZ7Xtk4s6/xVdGDQynvom7xCFEdWr6uE= github.com/go-ole/go-ole v1.3.0/go.mod h1:5LS6F96DhAwUc7C+1HLexzMXY1xGRSryjyPPKW6zv78= github.com/go-playground/locales v0.12.1/go.mod h1:IUMDtCfWo/w/mtMfIE/IG2K+Ey3ygWanZIBtBW0W2TM= github.com/go-playground/universal-translator v0.16.0/go.mod h1:1AnU7NaIRDWWzGEKwgtJRd2xk99HeFyHw3yid4rvQIY= +github.com/go-sourcemap/sourcemap v2.1.3+incompatible h1:W1iEw64niKVGogNgBN3ePyLFfuisuzeidWPMPWmECqU= +github.com/go-sourcemap/sourcemap v2.1.3+incompatible/go.mod h1:F8jJfvm2KbVjc5NqelyYJmf/v5J0dwNLS2mL4sNA1Jg= github.com/go-stack/stack v1.8.0/go.mod h1:v0f6uXyyMGvRgIKkXu+yp6POWl0qKG85gN/melR3HDY= github.com/go-task/slim-sprig v0.0.0-20210107165309-348f09dbbbc0/go.mod h1:fyg7847qk6SyHyPtNmDHnmrv/HOrqktSC+C9fM+CJOE= github.com/goccy/go-json v0.10.2 h1:CrxCmQqYDkv1z7lO7Wbh2HN93uovUHgrECaO5ZrCXAU= @@ -308,6 +321,9 @@ github.com/google/pprof v0.0.0-20201023163331-3e6fc7fc9c4c/go.mod h1:kpwsk12EmLe github.com/google/pprof v0.0.0-20201203190320-1bf35d6f28c2/go.mod h1:kpwsk12EmLew5upagYY7GY0pfYCcupk39gWOCRROcvE= github.com/google/pprof v0.0.0-20201218002935-b9804c9f04c2/go.mod h1:kpwsk12EmLew5upagYY7GY0pfYCcupk39gWOCRROcvE= github.com/google/pprof v0.0.0-20210407192527-94a9f03dee38/go.mod h1:kpwsk12EmLew5upagYY7GY0pfYCcupk39gWOCRROcvE= +github.com/google/pprof v0.0.0-20230207041349-798e818bf904/go.mod h1:uglQLonpP8qtYCYyzA+8c/9qtqgA3qsXGYqCPKARAFg= +github.com/google/pprof v0.0.0-20231229205709-960ae82b1e42 h1:dHLYa5D8/Ta0aLR2XcPsrkpAgGeFs6thhMcQK0oQ0n8= +github.com/google/pprof v0.0.0-20231229205709-960ae82b1e42/go.mod h1:czg5+yv1E0ZGTi6S6vVK1mke0fV+FaUhNGcd6VRS9Ik= github.com/google/renameio v0.1.0/go.mod h1:KWCgfxg9yswjAJkECMjeO8J8rahYeXnNhOm40UhjYkI= github.com/google/s2a-go v0.1.7 h1:60BLSyTrOV4/haCDW4zb1guZItoSq8foHCXrAnjBo/o= github.com/google/s2a-go v0.1.7/go.mod h1:50CgR4k1jNlWBu4UfS4AcfhVe1r6pdZPygJ3R8F0Qdw= @@ -363,6 +379,7 @@ github.com/huin/goupnp v1.3.0 h1:UvLUlWDNpoUdYzb2TCn+MuTWtcjXKSza2n6CBdQ0xXc= github.com/huin/goupnp v1.3.0/go.mod h1:gnGPsThkYa7bFi/KWmEysQRf48l2dvR5bxr2OFckNX8= github.com/ianlancetaylor/demangle v0.0.0-20181102032728-5e5cf60278f6/go.mod h1:aSSvb/t6k1mPoxDqO4vJh6VOCGPwU4O0C2/Eqndh1Sc= github.com/ianlancetaylor/demangle v0.0.0-20200824232613-28f6c0f3b639/go.mod h1:aSSvb/t6k1mPoxDqO4vJh6VOCGPwU4O0C2/Eqndh1Sc= +github.com/ianlancetaylor/demangle v0.0.0-20220319035150-800ac71e25c2/go.mod h1:aYm2/VgdVmcIU8iMfdMvDMsRAQjcfZSKFby6HOFvi/w= github.com/inconshreveable/mousetrap v1.0.0/go.mod h1:PxqpIevigyE2G7u3NXJIT2ANytuPF1OarO4DADm73n8= github.com/inconshreveable/mousetrap v1.1.0 h1:wN+x4NVGpMsO7ErUn/mUI3vEoE6Jt13X2s0bqwp9tc8= github.com/inconshreveable/mousetrap v1.1.0/go.mod h1:vpF70FUmC8bwa3OWnCshd2FqLfsEA9PFc4w1p2J65bw= @@ -417,6 +434,8 @@ github.com/konsorten/go-windows-terminal-sequences v1.0.1/go.mod h1:T0+1ngSBFLxv github.com/kr/fs v0.1.0/go.mod h1:FFnZGqtBN9Gxj7eW1uZ42v5BccTP0vu6NEaFoC2HwRg= github.com/kr/logfmt v0.0.0-20140226030751-b84e30acd515/go.mod h1:+0opPa2QZZtGFBFZlji/RkVcI2GknAs/DXo4wKdlNEc= github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo= +github.com/kr/pretty v0.2.1/go.mod h1:ipq/a2n7PKx3OHsz4KJII5eveXtPO4qwEXGdVfWzfnI= +github.com/kr/pretty v0.3.0/go.mod h1:640gp4NfQd8pI5XOwp5fnNeVWj67G7CFk/SaSQn7NBk= github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE= github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk= github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ= @@ -591,6 +610,7 @@ github.com/rivo/uniseg v0.4.4 h1:8TfxU8dW6PdqD27gjM8MVNuicgxIjxpm4K7x4jp8sis= github.com/rivo/uniseg v0.4.4/go.mod h1:FN3SvrM+Zdj16jyLfmOkMNblXMcoc8DfTHruCPUcx88= github.com/rogpeppe/fastuuid v0.0.0-20150106093220-6724a57986af/go.mod h1:XWv6SoW27p1b0cqNHllgS5HIMJraePCO15w5zCzIWYg= github.com/rogpeppe/go-internal v1.3.0/go.mod h1:M8bDsm7K2OlrFYOpmOWEs/qY81heoFRclV5y23lUDJ4= +github.com/rogpeppe/go-internal v1.6.1/go.mod h1:xXDCJY+GAPziupqXw64V24skbSoqbTEfhy4qGm1nDQc= github.com/rogpeppe/go-internal v1.9.0/go.mod h1:WtVeX8xhTBvf0smdhujwtBcq4Qrzq/fJaraNFVN+nFs= github.com/rogpeppe/go-internal v1.10.0 h1:TMyTOH3F/DB16zRVcYyreMH6GnZZrwQVAoYjRBZyWFQ= github.com/rogpeppe/go-internal v1.10.0/go.mod h1:UQnix2H7Ngw/k4C5ijL5+65zddjncjaFoBhdsK/akog= @@ -924,6 +944,7 @@ golang.org/x/sys v0.0.0-20210510120138-977fb7262007/go.mod h1:oPkhp1MJrh7nUepCBc golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20210630005230-0f9fa26af87c/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20211216021012-1d35b9e2eb4e/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20220310020820-b874c991c1a5/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= diff --git a/services/evm/executor.go b/services/evm/executor.go new file mode 100644 index 000000000..ff6900b26 --- /dev/null +++ b/services/evm/executor.go @@ -0,0 +1,172 @@ +package evm + +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/precompiles" + "github.com/onflow/flow-go/fvm/evm/types" + flowGo "github.com/onflow/flow-go/model/flow" + "github.com/onflow/go-ethereum/common" + gethTypes "github.com/onflow/go-ethereum/core/types" + "github.com/onflow/go-ethereum/eth/tracers" + "github.com/rs/zerolog" + + "github.com/onflow/flow-evm-gateway/models" + "github.com/onflow/flow-evm-gateway/storage" +) + +type BlockExecutor struct { + types.StateDB // todo change to types.ReadOnlyView + emulator types.Emulator + chainID flowGo.ChainID + block *models.Block + blocks storage.BlockIndexer + logger zerolog.Logger + receipts storage.ReceiptIndexer + + // block dynamic data + txIndex uint + gasUsed uint64 +} + +func NewBlockExecutor( + block *models.Block, + ledger atree.Ledger, + chainID flowGo.ChainID, + blocks storage.BlockIndexer, + receipts storage.ReceiptIndexer, + logger zerolog.Logger, +) (*BlockExecutor, error) { + logger = logger.With().Str("component", "state-execution").Logger() + storageAddress := evm.StorageAccountAddress(chainID) + + stateDB, err := state.NewStateDB(ledger, storageAddress) + if err != nil { + return nil, err + } + + return &BlockExecutor{ + emulator: emulator.NewEmulator(ledger, storageAddress), + StateDB: stateDB, + chainID: chainID, + block: block, + blocks: blocks, + receipts: receipts, + logger: logger, + }, nil +} + +func (s *BlockExecutor) Run( + tx models.Transaction, + tracer *tracers.Tracer, +) (*gethTypes.Receipt, error) { + l := s.logger.With().Str("tx-hash", tx.Hash().String()).Logger() + l.Info().Msg("executing new transaction") + + receipt, err := s.receipts.GetByTransactionID(tx.Hash()) + if err != nil { + return nil, err + } + + ctx, err := s.blockContext(receipt) + ctx.Tracer = tracer + if err != nil { + return nil, err + } + + 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 { + 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) + } + + // increment values as part of a virtual block + s.gasUsed += res.GasConsumed + s.txIndex++ + + l.Debug().Msg("transaction executed successfully") + + return res.LightReceipt().ToReceipt(), nil +} + +// blockContext produces a context that is used by the block view during the execution. +// It can be used for transaction execution and calls. Receipt is not required when +// producing the context for calls. +func (s *BlockExecutor) blockContext(receipt *models.Receipt) (types.BlockContext, error) { + ctx := types.BlockContext{ + ChainID: types.EVMChainIDFromFlowChainID(s.chainID), + BlockNumber: s.block.Height, + BlockTimestamp: s.block.Timestamp, + DirectCallBaseGasUsage: types.DefaultDirectCallBaseGasUsage, + DirectCallGasPrice: types.DefaultDirectCallGasPrice, + GasFeeCollector: types.CoinbaseAddress, + GetHashFunc: func(n uint64) common.Hash { + // For block heights greater than or equal to the current, + // return an empty block hash. + if n >= s.block.Height { + return common.Hash{} + } + // If the given block height, is more than 256 blocks + // in the past, return an empty block hash. + if s.block.Height-n > 256 { + return common.Hash{} + } + + block, err := s.blocks.GetByHeight(n) + if err != nil { + return common.Hash{} + } + blockHash, err := block.Hash() + if err != nil { + return common.Hash{} + } + + return blockHash + }, + Random: s.block.PrevRandao, + TxCountSoFar: s.txIndex, + TotalGasUsedSoFar: s.gasUsed, + Tracer: nil, + } + + // only add precompile cadence arch mocks if we have a receipt, + // in case of call and dry run we don't produce receipts + // todo when a call is made that uses cadence arch precompiles, it will fail, because + // the precompiled contracts won't be set since we don't have a receipt for them + // this failure should be detected and we should in such a case execute a call against the + // EN using an AN + if receipt != nil { + calls, err := types.AggregatedPrecompileCallsFromEncoded(receipt.PrecompiledCalls) + if err != nil { + return types.BlockContext{}, err + } + + ctx.ExtraPrecompiledContracts = precompiles.AggregatedPrecompiledCallsToPrecompiledContracts(calls) + } + + return ctx, nil +} diff --git a/services/ingestion/engine.go b/services/ingestion/engine.go index e31c48a06..8a852d14c 100644 --- a/services/ingestion/engine.go +++ b/services/ingestion/engine.go @@ -14,6 +14,8 @@ import ( "github.com/onflow/flow-evm-gateway/services/replayer" "github.com/onflow/flow-evm-gateway/storage" "github.com/onflow/flow-evm-gateway/storage/pebble" + + "github.com/onflow/flow-go/fvm/evm/offchain/sync" ) var _ models.Engine = &Engine{} @@ -42,11 +44,13 @@ type Engine struct { receipts storage.ReceiptIndexer transactions storage.TransactionIndexer accounts storage.AccountIndexer + traces storage.TraceIndexer log zerolog.Logger evmLastHeight *models.SequentialHeight blocksPublisher *models.Publisher[*models.Block] logsPublisher *models.Publisher[[]*gethTypes.Log] collector metrics.Collector + replayerConfig replayer.Config } func NewEventIngestionEngine( @@ -57,10 +61,12 @@ func NewEventIngestionEngine( receipts storage.ReceiptIndexer, transactions storage.TransactionIndexer, accounts storage.AccountIndexer, + traces storage.TraceIndexer, blocksPublisher *models.Publisher[*models.Block], logsPublisher *models.Publisher[[]*gethTypes.Log], log zerolog.Logger, collector metrics.Collector, + replayerConfig replayer.Config, ) *Engine { log = log.With().Str("component", "ingestion").Logger() @@ -74,10 +80,12 @@ func NewEventIngestionEngine( receipts: receipts, transactions: transactions, accounts: accounts, + traces: traces, log: log, blocksPublisher: blocksPublisher, logsPublisher: logsPublisher, collector: collector, + replayerConfig: replayerConfig, } } @@ -158,8 +166,47 @@ func (e *Engine) processEvents(events *models.CadenceEvents) error { batch := e.store.NewIndexedBatch() defer batch.Close() - // we first index the block - err := e.indexBlock( + // Step 1: Re-execute all transactions on the latest EVM block + + // Step 1.1: Notify the `BlocksProvider` of the newly received EVM block + if err := e.blocksProvider.OnBlockReceived(events.Block()); err != nil { + return err + } + + storageProvider := pebble.NewRegister( + e.store, + events.Block().Height, + batch, + ) + cr := sync.NewReplayer( + e.replayerConfig.ChainID, + e.replayerConfig.RootAddr, + storageProvider, + e.blocksProvider, + e.log, + e.replayerConfig.CallTracerCollector.TxTracer(), + e.replayerConfig.ValidateResults, + ) + + // Step 1.2: Replay all block transactions + // If `ReplayBlock` returns any error, we abort the EVM events processing + res, err := cr.ReplayBlock(events.TxEventPayloads(), events.BlockEventPayload()) + if err != nil { + return fmt.Errorf("failed to replay block on height: %d, with: %w", events.Block().Height, err) + } + + // Step 2: Write all the necessary changes to each storage + + // Step 2.1: Write all the EVM state changes to `StorageProvider` + for k, v := range res.StorageRegisterUpdates() { + err = storageProvider.SetValue([]byte(k.Owner), []byte(k.Key), v) + if err != nil { + return fmt.Errorf("failed to commit state changes on block: %d", events.Block().Height) + } + } + + // Step 2.2: Write the latest EVM block to `Blocks` storage + err = e.indexBlock( events.CadenceHeight(), events.CadenceBlockID(), events.Block(), @@ -169,6 +216,8 @@ func (e *Engine) processEvents(events *models.CadenceEvents) error { return fmt.Errorf("failed to index block %d event: %w", events.Block().Height, err) } + // Step 2.3: Write all EVM transactions of the current block, + // to `Transactions` storage for i, tx := range events.Transactions() { receipt := events.Receipts()[i] @@ -178,17 +227,25 @@ func (e *Engine) processEvents(events *models.CadenceEvents) error { } } + // Step 2.4: Write all EVM transaction receipts of the current block, + // to `Receipts` storage err = e.indexReceipts(events.Receipts(), batch) if err != nil { return fmt.Errorf("failed to index receipts for block %d event: %w", events.Block().Height, err) } - if err := e.blocksProvider.OnBlockReceived(events.Block()); err != nil { - return fmt.Errorf( - "failed to call OnBlockReceived for block %d, with: %w", - events.Block().Height, - err, - ) + traceCollector := e.replayerConfig.CallTracerCollector + for _, tx := range events.Transactions() { + txHash := tx.Hash() + traceResult, err := traceCollector.Collect(txHash) + if err != nil { + return err + } + + err = e.traces.StoreTransaction(txHash, traceResult, batch) + if err != nil { + return err + } } if err := batch.Commit(pebbleDB.Sync); err != nil { diff --git a/services/ingestion/engine_test.go b/services/ingestion/engine_test.go index ff87ea6fb..7eb14c67c 100644 --- a/services/ingestion/engine_test.go +++ b/services/ingestion/engine_test.go @@ -3,10 +3,13 @@ package ingestion import ( "context" "encoding/hex" + "encoding/json" "math/big" "testing" pebbleDB "github.com/cockroachdb/pebble" + "github.com/onflow/flow-go/fvm/environment" + "github.com/onflow/flow-go/fvm/evm" "github.com/onflow/flow-go/fvm/evm/events" flowGo "github.com/onflow/flow-go/model/flow" @@ -32,13 +35,13 @@ import ( ) func TestSerialBlockIngestion(t *testing.T) { + t.Run("successfully ingest serial blocks", func(t *testing.T) { receipts := &storageMock.ReceiptIndexer{} transactions := &storageMock.TransactionIndexer{} latestHeight := uint64(10) - store, err := pebble.New(t.TempDir(), zerolog.Nop()) - require.NoError(t, err) + store := setupStore(t) blocks := &storageMock.BlockIndexer{} blocks. @@ -53,6 +56,8 @@ func TestSerialBlockIngestion(t *testing.T) { On("Update"). Return(func() error { return nil }) + traces := &storageMock.TraceIndexer{} + eventsChan := make(chan models.BlockEvents) subscriber := &mocks.EventSubscriber{} @@ -70,10 +75,12 @@ func TestSerialBlockIngestion(t *testing.T) { receipts, transactions, accounts, + traces, models.NewPublisher[*models.Block](), models.NewPublisher[[]*gethTypes.Log](), zerolog.Nop(), metrics.NopCollector, + defaultReplayerConfig(), ) done := make(chan struct{}) @@ -119,8 +126,7 @@ func TestSerialBlockIngestion(t *testing.T) { transactions := &storageMock.TransactionIndexer{} latestHeight := uint64(10) - store, err := pebble.New(t.TempDir(), zerolog.Nop()) - require.NoError(t, err) + store := setupStore(t) blocks := &storageMock.BlockIndexer{} blocks. @@ -135,6 +141,8 @@ func TestSerialBlockIngestion(t *testing.T) { On("Update", mock.Anything, mock.Anything). Return(func(t models.TransactionCall, r *gethTypes.Receipt) error { return nil }) + traces := &storageMock.TraceIndexer{} + eventsChan := make(chan models.BlockEvents) subscriber := &mocks.EventSubscriber{} subscriber. @@ -151,10 +159,12 @@ func TestSerialBlockIngestion(t *testing.T) { receipts, transactions, accounts, + traces, models.NewPublisher[*models.Block](), models.NewPublisher[[]*gethTypes.Log](), zerolog.Nop(), metrics.NopCollector, + defaultReplayerConfig(), ) waitErr := make(chan struct{}) @@ -162,7 +172,7 @@ func TestSerialBlockIngestion(t *testing.T) { go func() { err := engine.Run(context.Background()) assert.ErrorIs(t, err, models.ErrInvalidHeight) - assert.EqualError(t, err, "failed to index block 20 event: invalid block height, expected 11, got 20: invalid height") + assert.EqualError(t, err, "invalid height: received new block: 20, non-sequential of latest block: 11") close(waitErr) }() @@ -216,6 +226,7 @@ func TestSerialBlockIngestion(t *testing.T) { } func TestBlockAndTransactionIngestion(t *testing.T) { + t.Run("successfully ingest transaction and block", func(t *testing.T) { receipts := &storageMock.ReceiptIndexer{} transactions := &storageMock.TransactionIndexer{} @@ -223,8 +234,7 @@ func TestBlockAndTransactionIngestion(t *testing.T) { nextHeight := latestHeight + 1 blockID := flow.Identifier{0x01} - store, err := pebble.New(t.TempDir(), zerolog.Nop()) - require.NoError(t, err) + store := setupStore(t) blocks := &storageMock.BlockIndexer{} blocks. @@ -259,6 +269,14 @@ func TestBlockAndTransactionIngestion(t *testing.T) { blockCdc, block, blockEvent, err := newBlock(nextHeight, []gethCommon.Hash{result.TxHash}) require.NoError(t, err) + traces := &storageMock.TraceIndexer{} + traces. + On("StoreTransaction", mock.AnythingOfType("common.Hash"), mock.AnythingOfType("json.RawMessage"), mock.Anything). + Return(func(txID gethCommon.Hash, trace json.RawMessage, batch *pebbleDB.Batch) error { + assert.Equal(t, transaction.Hash(), txID) + return nil + }) + engine := NewEventIngestionEngine( subscriber, replayer.NewBlocksProvider(blocks, flowGo.Emulator, nil), @@ -267,10 +285,12 @@ func TestBlockAndTransactionIngestion(t *testing.T) { receipts, transactions, accounts, + traces, models.NewPublisher[*models.Block](), models.NewPublisher[[]*gethTypes.Log](), zerolog.Nop(), metrics.NopCollector, + defaultReplayerConfig(), ) done := make(chan struct{}) @@ -333,8 +353,7 @@ func TestBlockAndTransactionIngestion(t *testing.T) { latestHeight := uint64(10) nextHeight := latestHeight + 1 - store, err := pebble.New(t.TempDir(), zerolog.Nop()) - require.NoError(t, err) + store := setupStore(t) blocks := &storageMock.BlockIndexer{} blocks. @@ -358,11 +377,19 @@ func TestBlockAndTransactionIngestion(t *testing.T) { return eventsChan }) - txCdc, txEvent, _, res, err := newTransaction(nextHeight) + txCdc, txEvent, transaction, res, err := newTransaction(nextHeight) require.NoError(t, err) blockCdc, _, blockEvent, err := newBlock(nextHeight, []gethCommon.Hash{res.TxHash}) require.NoError(t, err) + traces := &storageMock.TraceIndexer{} + traces. + On("StoreTransaction", mock.AnythingOfType("common.Hash"), mock.AnythingOfType("json.RawMessage"), mock.Anything). + Return(func(txID gethCommon.Hash, trace json.RawMessage, batch *pebbleDB.Batch) error { + assert.Equal(t, transaction.Hash(), txID) + return nil + }) + engine := NewEventIngestionEngine( subscriber, replayer.NewBlocksProvider(blocks, flowGo.Emulator, nil), @@ -371,10 +398,12 @@ func TestBlockAndTransactionIngestion(t *testing.T) { receipts, transactions, accounts, + traces, models.NewPublisher[*models.Block](), models.NewPublisher[[]*gethTypes.Log](), zerolog.Nop(), metrics.NopCollector, + defaultReplayerConfig(), ) done := make(chan struct{}) @@ -434,8 +463,7 @@ func TestBlockAndTransactionIngestion(t *testing.T) { transactions := &storageMock.TransactionIndexer{} latestCadenceHeight := uint64(0) - store, err := pebble.New(t.TempDir(), zerolog.Nop()) - require.NoError(t, err) + store := setupStore(t) blocks := &storageMock.BlockIndexer{} blocks. @@ -450,6 +478,8 @@ func TestBlockAndTransactionIngestion(t *testing.T) { On("Update", mock.Anything, mock.AnythingOfType("*models.Receipt"), mock.Anything). Return(func(t models.Transaction, r *models.Receipt, _ *pebbleDB.Batch) error { return nil }) + traces := &storageMock.TraceIndexer{} + eventsChan := make(chan models.BlockEvents) subscriber := &mocks.EventSubscriber{} subscriber. @@ -467,10 +497,12 @@ func TestBlockAndTransactionIngestion(t *testing.T) { receipts, transactions, accounts, + traces, models.NewPublisher[*models.Block](), models.NewPublisher[[]*gethTypes.Log](), zerolog.Nop(), metrics.NopCollector, + defaultReplayerConfig(), ) done := make(chan struct{}) @@ -508,6 +540,13 @@ func TestBlockAndTransactionIngestion(t *testing.T) { Return(func(receipts []*models.Receipt, _ *pebbleDB.Batch) error { return nil }). Once() + traces. + On("StoreTransaction", mock.AnythingOfType("common.Hash"), mock.AnythingOfType("json.RawMessage"), mock.Anything). + Return(func(txID gethCommon.Hash, trace json.RawMessage, batch *pebbleDB.Batch) error { + assert.Equal(t, transaction.Hash(), txID) + return nil + }) + events = append(events, flow.Event{ Type: string(txEvent.Etype), Value: txCdc, @@ -609,3 +648,29 @@ func newTransaction(height uint64) (cadence.Event, *events.Event, models.Transac cdcEv, err := ev.Payload.ToCadence(flowGo.Previewnet) return cdcEv, ev, models.TransactionCall{Transaction: tx}, res, err } + +func defaultReplayerConfig() replayer.Config { + return replayer.Config{ + ChainID: flowGo.Emulator, + RootAddr: evm.StorageAccountAddress(flowGo.Emulator), + CallTracerCollector: replayer.NopTracer, + ValidateResults: false, + } +} + +func setupStore(t *testing.T) *pebble.Storage { + store, err := pebble.New(t.TempDir(), zerolog.Nop()) + require.NoError(t, err) + + storageProvider := pebble.NewRegister(store, 0, nil) + storageAddress := evm.StorageAccountAddress(flowGo.Emulator) + accountStatus := environment.NewAccountStatus() + err = storageProvider.SetValue( + storageAddress[:], + []byte(flowGo.AccountStatusKey), + accountStatus.ToBytes(), + ) + require.NoError(t, err) + + return store +} diff --git a/services/replayer/blocks_provider.go b/services/replayer/blocks_provider.go index f87d398da..d43749a0c 100644 --- a/services/replayer/blocks_provider.go +++ b/services/replayer/blocks_provider.go @@ -78,7 +78,8 @@ func NewBlocksProvider( func (bp *BlocksProvider) OnBlockReceived(block *models.Block) error { if bp.latestBlock != nil && bp.latestBlock.Height != (block.Height-1) { return fmt.Errorf( - "received new block: %d, non-sequential of latest block: %d", + "%w: received new block: %d, non-sequential of latest block: %d", + models.ErrInvalidHeight, block.Height, bp.latestBlock.Height, ) diff --git a/services/replayer/call_tracer_collector.go b/services/replayer/call_tracer_collector.go new file mode 100644 index 000000000..171ccc00c --- /dev/null +++ b/services/replayer/call_tracer_collector.go @@ -0,0 +1,214 @@ +package replayer + +import ( + "encoding/json" + "fmt" + "math/big" + + "github.com/onflow/go-ethereum/common" + "github.com/onflow/go-ethereum/core/tracing" + "github.com/onflow/go-ethereum/core/types" + "github.com/onflow/go-ethereum/eth/tracers" + "github.com/rs/zerolog" +) + +const ( + TracerConfig = `{"onlyTopCall":true}` + TracerName = "callTracer" +) + +func DefaultCallTracer() (*tracers.Tracer, error) { + tracer, err := tracers.DefaultDirectory.New( + TracerName, + &tracers.Context{}, + json.RawMessage(TracerConfig), + ) + if err != nil { + return nil, err + } + + return tracer, nil +} + +type EVMTracer interface { + TxTracer() *tracers.Tracer + ResetTracer() error + Collect(txID common.Hash) (json.RawMessage, error) +} + +type CallTracerCollector struct { + tracer *tracers.Tracer + resultsByTxID map[common.Hash]json.RawMessage + logger zerolog.Logger +} + +var _ EVMTracer = (*CallTracerCollector)(nil) + +func NewCallTracerCollector(logger zerolog.Logger) ( + *CallTracerCollector, + error, +) { + tracer, err := DefaultCallTracer() + if err != nil { + return nil, err + } + + return &CallTracerCollector{ + tracer: tracer, + resultsByTxID: make(map[common.Hash]json.RawMessage), + logger: logger.With().Str("component", "evm-tracer").Logger(), + }, nil +} + +func (t *CallTracerCollector) TxTracer() *tracers.Tracer { + return NewSafeTxTracer(t) +} + +func (t *CallTracerCollector) ResetTracer() error { + var err error + t.tracer, err = DefaultCallTracer() + return err +} + +func (ct *CallTracerCollector) Collect(txID common.Hash) (json.RawMessage, error) { + // collect the trace result + result, found := ct.resultsByTxID[txID] + if !found { + return nil, fmt.Errorf("trace result not found") + } + + // remove the result + delete(ct.resultsByTxID, txID) + + return result, nil +} + +func NewSafeTxTracer(ct *CallTracerCollector) *tracers.Tracer { + wrapped := &tracers.Tracer{ + Hooks: &tracing.Hooks{}, + GetResult: ct.tracer.GetResult, + Stop: ct.tracer.Stop, + } + + l := ct.logger + + wrapped.OnTxStart = func( + vm *tracing.VMContext, + tx *types.Transaction, + from common.Address, + ) { + defer func() { + if r := recover(); r != nil { + err, ok := r.(error) + if !ok { + err = fmt.Errorf("panic: %v", r) + } + l.Err(err).Stack().Msg("OnTxStart trace collection failed") + } + }() + if ct.tracer.OnTxStart != nil { + ct.tracer.OnTxStart(vm, tx, from) + } + } + + wrapped.OnTxEnd = func(receipt *types.Receipt, err error) { + defer func() { + if r := recover(); r != nil { + err, ok := r.(error) + if !ok { + err = fmt.Errorf("panic: %v", r) + } + l.Err(err).Stack().Msg("OnTxEnd trace collection failed") + } + }() + if ct.tracer.OnTxEnd != nil { + ct.tracer.OnTxEnd(receipt, err) + } + + // collect results for the tracer + res, err := ct.tracer.GetResult() + if err != nil { + l.Error().Err(err).Msg("failed to produce trace results") + return + } + ct.resultsByTxID[receipt.TxHash] = res + + // reset tracing to have fresh state + if err := ct.ResetTracer(); err != nil { + l.Error().Err(err).Msg("failed to reset tracer") + return + } + } + + wrapped.OnEnter = func( + depth int, + typ byte, + from, to common.Address, + input []byte, + gas uint64, + value *big.Int, + ) { + defer func() { + if r := recover(); r != nil { + err, ok := r.(error) + if !ok { + err = fmt.Errorf("panic: %v", r) + } + l.Err(err).Stack().Msg("OnEnter trace collection failed") + } + }() + if ct.tracer.OnEnter != nil { + ct.tracer.OnEnter(depth, typ, from, to, input, gas, value) + } + } + + wrapped.OnExit = func(depth int, output []byte, gasUsed uint64, err error, reverted bool) { + defer func() { + if r := recover(); r != nil { + err, ok := r.(error) + if !ok { + err = fmt.Errorf("panic: %v", r) + } + l.Err(err).Stack().Msg("OnExit trace collection failed") + } + }() + if ct.tracer.OnExit != nil { + ct.tracer.OnExit(depth, output, gasUsed, err, reverted) + } + } + + wrapped.OnLog = func(log *types.Log) { + defer func() { + if r := recover(); r != nil { + err, ok := r.(error) + if !ok { + err = fmt.Errorf("panic: %v", r) + } + l.Err(err).Stack().Msg("OnLog trace collection failed") + } + }() + if ct.tracer.OnLog != nil { + ct.tracer.OnLog(log) + } + } + + return wrapped +} + +var NopTracer = &nopTracer{} + +var _ EVMTracer = &nopTracer{} + +type nopTracer struct{} + +func (n nopTracer) TxTracer() *tracers.Tracer { + return nil +} + +func (n nopTracer) Collect(_ common.Hash) (json.RawMessage, error) { + return nil, nil +} + +func (n nopTracer) ResetTracer() error { + return nil +} diff --git a/services/replayer/config.go b/services/replayer/config.go new file mode 100644 index 000000000..72fb0a88d --- /dev/null +++ b/services/replayer/config.go @@ -0,0 +1,12 @@ +package replayer + +import ( + "github.com/onflow/flow-go/model/flow" +) + +type Config struct { + ChainID flow.ChainID + RootAddr flow.Address + CallTracerCollector EVMTracer + ValidateResults bool +} diff --git a/services/requester/requester.go b/services/requester/requester.go index a3050f913..a90200ba9 100644 --- a/services/requester/requester.go +++ b/services/requester/requester.go @@ -4,7 +4,6 @@ import ( "context" _ "embed" "encoding/hex" - "errors" "fmt" "math" "math/big" @@ -15,12 +14,10 @@ import ( "github.com/hashicorp/golang-lru/v2/expirable" "github.com/onflow/cadence" "github.com/onflow/flow-go-sdk" - "github.com/onflow/flow-go-sdk/access/grpc" "github.com/onflow/flow-go-sdk/crypto" "github.com/onflow/flow-go/fvm/evm" "github.com/onflow/flow-go/fvm/evm/emulator" - "github.com/onflow/flow-go/fvm/evm/emulator/state" - evmImpl "github.com/onflow/flow-go/fvm/evm/impl" + "github.com/onflow/flow-go/fvm/evm/offchain/query" evmTypes "github.com/onflow/flow-go/fvm/evm/types" "github.com/onflow/flow-go/fvm/systemcontracts" "github.com/onflow/go-ethereum/common" @@ -33,7 +30,11 @@ import ( "github.com/onflow/flow-evm-gateway/metrics" "github.com/onflow/flow-evm-gateway/models" errs "github.com/onflow/flow-evm-gateway/models/errors" + "github.com/onflow/flow-evm-gateway/services/replayer" "github.com/onflow/flow-evm-gateway/storage" + "github.com/onflow/flow-evm-gateway/storage/pebble" + + gethParams "github.com/onflow/go-ethereum/params" ) var ( @@ -95,12 +96,12 @@ type Requester interface { // Call executes the given signed transaction data on the state for the given EVM block height. // Note, this function doesn't make and changes in the state/blockchain and is // useful to execute and retrieve values. - Call(ctx context.Context, data []byte, from common.Address, evmHeight int64) ([]byte, error) + Call(ctx context.Context, tx *types.LegacyTx, from common.Address, evmHeight int64) ([]byte, error) // EstimateGas executes the given signed transaction data on the state for the given EVM block height. // Note, this function doesn't make any changes in the state/blockchain and is // useful to executed and retrieve the gas consumption and possible failures. - EstimateGas(ctx context.Context, data []byte, from common.Address, evmHeight int64) (uint64, error) + EstimateGas(ctx context.Context, tx *types.LegacyTx, from common.Address, evmHeight int64) (uint64, error) // GetNonce gets nonce from the network at the given EVM block height. GetNonce(ctx context.Context, address common.Address, evmHeight int64) (uint64, error) @@ -119,14 +120,16 @@ type Requester interface { var _ Requester = &EVM{} type EVM struct { - client *CrossSporkClient - config *config.Config - signer crypto.Signer - txPool *TxPool - logger zerolog.Logger - blocks storage.BlockIndexer - mux sync.Mutex - scriptCache *expirable.LRU[string, cadence.Value] + store *pebble.Storage + blocksProvider *replayer.BlocksProvider + client *CrossSporkClient + config *config.Config + signer crypto.Signer + txPool *TxPool + logger zerolog.Logger + blocks storage.BlockIndexer + mux sync.Mutex + scriptCache *expirable.LRU[string, cadence.Value] head *types.Header evmSigner types.Signer @@ -136,6 +139,8 @@ type EVM struct { } func NewEVM( + store *pebble.Storage, + blocksProvider *replayer.BlocksProvider, client *CrossSporkClient, config *config.Config, signer crypto.Signer, @@ -194,6 +199,8 @@ func NewEVM( } evm := &EVM{ + store: store, + blocksProvider: blocksProvider, client: client, config: config, signer: signer, @@ -340,45 +347,12 @@ func (e *EVM) GetBalance( address common.Address, evmHeight int64, ) (*big.Int, error) { - hexEncodedAddress, err := addressToCadenceString(address) - if err != nil { - return nil, err - } - - height, err := e.evmToCadenceHeight(evmHeight) + view, err := e.getBlockView(uint64(evmHeight)) if err != nil { return nil, err } - val, err := e.executeScriptAtHeight( - ctx, - getBalance, - height, - []cadence.Value{hexEncodedAddress}, - ) - if err != nil { - if !errors.Is(err, errs.ErrHeightOutOfRange) { - e.logger.Error(). - Err(err). - Str("address", address.String()). - Int64("evm-height", evmHeight). - Uint64("cadence-height", height). - Msg("failed to get get balance") - } - return nil, fmt.Errorf( - "failed to get balance of address: %s at height: %d, with: %w", - address, - evmHeight, - err, - ) - } - - // sanity check, should never occur - if _, ok := val.(cadence.UInt); !ok { - return nil, fmt.Errorf("failed to convert balance %v to UInt, got type: %T", val, val) - } - - return val.(cadence.UInt).Big(), nil + return view.GetBalance(address) } func (e *EVM) GetNonce( @@ -386,79 +360,12 @@ func (e *EVM) GetNonce( address common.Address, evmHeight int64, ) (uint64, error) { - hexEncodedAddress, err := addressToCadenceString(address) - if err != nil { - return 0, err - } - - height, err := e.evmToCadenceHeight(evmHeight) + view, err := e.getBlockView(uint64(evmHeight)) if err != nil { return 0, err } - val, err := e.executeScriptAtHeight( - ctx, - getNonce, - height, - []cadence.Value{hexEncodedAddress}, - ) - if err != nil { - if !errors.Is(err, errs.ErrHeightOutOfRange) { - e.logger.Error().Err(err). - Str("address", address.String()). - Int64("evm-height", evmHeight). - Uint64("cadence-height", height). - Msg("failed to get nonce") - } - return 0, fmt.Errorf( - "failed to get nonce of address: %s at height: %d, with: %w", - address, - evmHeight, - err, - ) - } - - // sanity check, should never occur - if _, ok := val.(cadence.UInt64); !ok { - return 0, fmt.Errorf("failed to convert nonce %v to UInt64, got type: %T", val, val) - } - - nonce := uint64(val.(cadence.UInt64)) - - e.logger.Debug(). - Uint64("nonce", nonce). - Int64("evm-height", evmHeight). - Uint64("cadence-height", height). - Msg("get nonce executed") - - return nonce, nil -} - -func (e *EVM) stateAt(evmHeight int64) (*state.StateDB, error) { - cadenceHeight, err := e.evmToCadenceHeight(evmHeight) - if err != nil { - return nil, err - } - - if cadenceHeight == LatestBlockHeight { - h, err := e.client.GetLatestBlockHeader(context.Background(), true) - if err != nil { - return nil, err - } - cadenceHeight = h.Height - } - - exeClient, ok := e.client.Client.(*grpc.Client) - if !ok { - return nil, fmt.Errorf("could not convert to execution client") - } - ledger, err := newRemoteLedger(exeClient.ExecutionDataRPCClient(), cadenceHeight) - if err != nil { - return nil, fmt.Errorf("could not create remote ledger for height: %d, with: %w", cadenceHeight, err) - } - - storageAddress := evm.StorageAccountAddress(e.config.FlowNetworkID) - return state.NewStateDB(ledger, storageAddress) + return view.GetNonce(address) } func (e *EVM) GetStorageAt( @@ -467,125 +374,104 @@ func (e *EVM) GetStorageAt( hash common.Hash, evmHeight int64, ) (common.Hash, error) { - stateDB, err := e.stateAt(evmHeight) + view, err := e.getBlockView(uint64(evmHeight)) if err != nil { return common.Hash{}, err } - result := stateDB.GetState(address, hash) - return result, stateDB.Error() + return view.GetSlab(address, hash) } func (e *EVM) Call( ctx context.Context, - data []byte, + tx *types.LegacyTx, from common.Address, evmHeight int64, ) ([]byte, error) { - hexEncodedTx, err := cadence.NewString(hex.EncodeToString(data)) + view, err := e.getBlockView(uint64(evmHeight)) if err != nil { return nil, err } - hexEncodedAddress, err := addressToCadenceString(from) - if err != nil { - return nil, err + to := common.Address{} + if tx.To != nil { + to = *tx.To } - - height, err := e.evmToCadenceHeight(evmHeight) - if err != nil { - return nil, err - } - - scriptResult, err := e.executeScriptAtHeight( - ctx, - dryRun, - height, - []cadence.Value{hexEncodedTx, hexEncodedAddress}, + result, err := view.DryCall( + from, + to, + tx.Data, + tx.Value, + tx.Gas, ) - if err != nil { - if !errors.Is(err, errs.ErrHeightOutOfRange) { - e.logger.Error(). - Err(err). - Uint64("cadence-height", height). - Int64("evm-height", evmHeight). - Str("from", from.String()). - Str("data", hex.EncodeToString(data)). - Msg("failed to execute call") - } - return nil, fmt.Errorf("failed to execute script at height: %d, with: %w", height, err) - } - evmResult, err := parseResult(scriptResult) - if err != nil { - return nil, err + resultSummary := result.ResultSummary() + if resultSummary.ErrorCode != 0 { + if resultSummary.ErrorCode == evmTypes.ExecutionErrCodeExecutionReverted { + return nil, errs.NewRevertError(resultSummary.ReturnedData) + } + return nil, errs.NewFailedTransactionError(resultSummary.ErrorMessage) } - result := evmResult.ReturnedData - - e.logger.Debug(). - Str("result", hex.EncodeToString(result)). - Int64("evm-height", evmHeight). - Uint64("cadence-height", height). - Msg("call executed") - - return result, nil + return result.ReturnedData, err } func (e *EVM) EstimateGas( ctx context.Context, - data []byte, + tx *types.LegacyTx, from common.Address, evmHeight int64, ) (uint64, error) { - hexEncodedTx, err := cadence.NewString(hex.EncodeToString(data)) + view, err := e.getBlockView(uint64(evmHeight)) if err != nil { return 0, err } - hexEncodedAddress, err := addressToCadenceString(from) - if err != nil { - return 0, err + to := common.Address{} + if tx.To != nil { + to = *tx.To } - - height, err := e.evmToCadenceHeight(evmHeight) + result, err := view.DryCall( + from, + to, + tx.Data, + tx.Value, + tx.Gas, + ) if err != nil { return 0, err } - scriptResult, err := e.executeScriptAtHeight( - ctx, - dryRun, - height, - []cadence.Value{hexEncodedTx, hexEncodedAddress}, - ) - if err != nil { - if !errors.Is(err, errs.ErrHeightOutOfRange) { - e.logger.Error(). - Err(err). - Uint64("cadence-height", height). - Int64("evm-height", evmHeight). - Str("from", from.String()). - Str("data", hex.EncodeToString(data)). - Msg("failed to execute estimateGas") + resultSummary := result.ResultSummary() + if resultSummary.ErrorCode != 0 { + if resultSummary.ErrorCode == evmTypes.ExecutionErrCodeExecutionReverted { + return 0, errs.NewRevertError(resultSummary.ReturnedData) } - return 0, fmt.Errorf("failed to execute script at height: %d, with: %w", height, err) + return 0, errs.NewFailedTransactionError(resultSummary.ErrorMessage) } - evmResult, err := parseResult(scriptResult) - if err != nil { - return 0, err - } + if result.Successful() { + // As mentioned in https://github.com/ethereum/EIPs/blob/master/EIPS/eip-150.md#specification + // Define "all but one 64th" of N as N - floor(N / 64). + // If a call asks for more gas than the maximum allowed amount + // (i.e. the total amount of gas remaining in the parent after subtracting + // the gas cost of the call and memory expansion), do not return an OOG error; + // instead, if a call asks for more gas than all but one 64th of the maximum + // allowed amount, call with all but one 64th of the maximum allowed amount of + // gas (this is equivalent to a version of EIP-901 plus EIP-1142). + // CREATE only provides all but one 64th of the parent gas to the child call. + result.GasConsumed = AddOne64th(result.GasConsumed) - gasConsumed := evmResult.GasConsumed + // Adding `gethParams.SstoreSentryGasEIP2200` is needed for this condition: + // https://github.com/onflow/go-ethereum/blob/master/core/vm/operations_acl.go#L29-L32 + result.GasConsumed += gethParams.SstoreSentryGasEIP2200 - e.logger.Debug(). - Uint64("gas", gasConsumed). - Int64("evm-height", evmHeight). - Uint64("cadence-height", height). - Msg("estimateGas executed") + // Take into account any gas refunds, which are calculated only after + // transaction execution. + result.GasConsumed += result.GasRefund + } - return gasConsumed, nil + return result.GasConsumed, err } func (e *EVM) GetCode( @@ -593,53 +479,12 @@ func (e *EVM) GetCode( address common.Address, evmHeight int64, ) ([]byte, error) { - hexEncodedAddress, err := addressToCadenceString(address) + view, err := e.getBlockView(uint64(evmHeight)) if err != nil { return nil, err } - height, err := e.evmToCadenceHeight(evmHeight) - if err != nil { - return nil, err - } - - value, err := e.executeScriptAtHeight( - ctx, - getCode, - height, - []cadence.Value{hexEncodedAddress}, - ) - if err != nil { - if !errors.Is(err, errs.ErrHeightOutOfRange) { - e.logger.Error(). - Err(err). - Uint64("cadence-height", height). - Int64("evm-height", evmHeight). - Str("address", address.String()). - Msg("failed to get code") - } - - return nil, fmt.Errorf( - "failed to execute script for get code of address: %s at height: %d, with: %w", - address, - height, - err, - ) - } - - code, err := cadenceStringToBytes(value) - if err != nil { - return nil, err - } - - e.logger.Debug(). - Str("address", address.Hex()). - Int64("evm-height", evmHeight). - Uint64("cadence-height", height). - Str("code size", fmt.Sprintf("%d", len(code))). - Msg("get code executed") - - return code, nil + return view.GetCode(address) } func (e *EVM) GetLatestEVMHeight(ctx context.Context) (uint64, error) { @@ -715,37 +560,6 @@ func (e *EVM) replaceAddresses(script []byte) []byte { return []byte(s) } -func (e *EVM) evmToCadenceHeight(height int64) (uint64, error) { - if height < 0 { - return LatestBlockHeight, nil - } - - evmHeight := uint64(height) - evmLatest, err := e.blocks.LatestEVMHeight() - if err != nil { - return 0, fmt.Errorf( - "failed to map evm height: %d to cadence height, getting latest evm height: %w", - evmHeight, - err, - ) - } - - // if provided evm height equals to latest evm height indexed we - // return latest height special value to signal requester to execute - // script at the latest block, not at the cadence height we get from the - // index, that is because at that point the height might already be pruned - if evmHeight == evmLatest { - return LatestBlockHeight, nil - } - - cadenceHeight, err := e.blocks.GetCadenceHeight(uint64(evmHeight)) - if err != nil { - return 0, fmt.Errorf("failed to map evm height: %d to cadence height: %w", evmHeight, err) - } - - return cadenceHeight, nil -} - // executeScriptAtHeight will execute the given script, at the given // block height, with the given arguments. A height of `LatestBlockHeight` // (math.MaxUint64 - 1) is a special value, which means the script will be @@ -806,45 +620,22 @@ func (e *EVM) executeScriptAtHeight( return res, err } -func addressToCadenceString(address common.Address) (cadence.String, error) { - return cadence.NewString( - strings.TrimPrefix(address.Hex(), "0x"), +func (e *EVM) getBlockView(evmHeight uint64) (*query.View, error) { + ledger := pebble.NewRegister(e.store, uint64(evmHeight), nil) + blocksProvider := replayer.NewBlocksProvider( + e.blocks, + e.config.FlowNetworkID, + nil, + ) + viewProvider := query.NewViewProvider( + e.config.FlowNetworkID, + evm.StorageAccountAddress(e.config.FlowNetworkID), + ledger, + blocksProvider, + 120_000_000, ) -} - -func cadenceStringToBytes(value cadence.Value) ([]byte, error) { - cdcString, ok := value.(cadence.String) - if !ok { - return nil, fmt.Errorf( - "failed to convert cadence value of type: %T to string: %v", - value, - value, - ) - } - - code, err := hex.DecodeString(string(cdcString)) - if err != nil { - return nil, fmt.Errorf("failed to hex-decode string to byte array [%s]: %w", cdcString, err) - } - - return code, nil -} - -// parseResult -func parseResult(res cadence.Value) (*evmTypes.ResultSummary, error) { - result, err := evmImpl.ResultSummaryFromEVMResultValue(res) - if err != nil { - return nil, fmt.Errorf("failed to decode EVM result of type: %s, with: %w", res.Type().ID(), err) - } - - if result.ErrorCode != 0 { - if result.ErrorCode == evmTypes.ExecutionErrCodeExecutionReverted { - return nil, errs.NewRevertError(result.ReturnedData) - } - return nil, errs.NewFailedTransactionError(result.ErrorMessage) - } - return result, err + return viewProvider.GetBlockView(uint64(evmHeight)) } // cacheKey builds the cache key from the script type, height and arguments. @@ -872,3 +663,8 @@ func cacheKey(scriptType scriptType, height uint64, args []cadence.Value) string return key } + +func AddOne64th(n uint64) uint64 { + // NOTE: Go's integer division floors, but that is desirable here + return n + (n / 64) +} diff --git a/storage/pebble/register.go b/storage/pebble/register.go new file mode 100644 index 000000000..bee82bf52 --- /dev/null +++ b/storage/pebble/register.go @@ -0,0 +1,172 @@ +package pebble + +import ( + "errors" + "fmt" + "sync" + + "github.com/cockroachdb/pebble" + "github.com/onflow/atree" + + errs "github.com/onflow/flow-evm-gateway/models/errors" + + "github.com/onflow/flow-go/fvm/evm/types" +) + +var _ atree.Ledger = &Register{} +var _ types.StorageProvider = &Register{} + +type Register struct { + store *Storage + height uint64 + batch *pebble.Batch + mux sync.RWMutex +} + +// NewRegister creates a new index instance at the provided height, all reads and +// writes of the registers will happen at that height. +func NewRegister(store *Storage, height uint64, batch *pebble.Batch) *Register { + return &Register{ + store: store, + height: height, + batch: batch, + mux: sync.RWMutex{}, + } +} + +func (r *Register) GetSnapshotAt(evmBlockHeight uint64) (types.BackendStorageSnapshot, error) { + return &Register{ + store: r.store, + height: evmBlockHeight, + mux: sync.RWMutex{}, + }, nil +} + +func (r *Register) GetValue(owner, key []byte) ([]byte, error) { + r.mux.RLock() + defer r.mux.RUnlock() + + var db pebble.Reader = r.store.db + if r.batch != nil { + db = r.batch + } + + iter, err := db.NewIter(&pebble.IterOptions{ + LowerBound: r.idLower(owner, key), + UpperBound: r.idUpper(owner, key), + }) + if err != nil { + return nil, fmt.Errorf("failed to create register range iterator: %w", err) + } + defer func() { + if err := iter.Close(); err != nil { + r.store.log.Error().Err(err).Msg("failed to close register iterator") + } + }() + + found := iter.Last() + if !found { + // as per interface expectation we need to return nil if not found + return nil, nil + } + + val, err := iter.ValueAndErr() + if err != nil { + return nil, fmt.Errorf( + "failed to get ledger value at owner %x and key %x: %w", + owner, + key, + err, + ) + } + + return val, nil +} + +func (r *Register) SetValue(owner, key, value []byte) error { + r.mux.Lock() + defer r.mux.Unlock() + + id := r.id(owner, key) + if err := r.store.set(ledgerValue, id, value, r.batch); err != nil { + return fmt.Errorf( + "failed to store ledger value for owner %x and key %x: %w", + owner, + key, + err, + ) + } + + return nil +} + +func (r *Register) ValueExists(owner, key []byte) (bool, error) { + val, err := r.GetValue(owner, key) + if err != nil { + return false, err + } + + return val != nil, nil +} + +func (r *Register) AllocateSlabIndex(owner []byte) (atree.SlabIndex, error) { + r.mux.Lock() + defer r.mux.Unlock() + + var index atree.SlabIndex + + val, err := r.store.batchGet(r.batch, ledgerSlabIndex, owner) + if err != nil { + if !errors.Is(err, errs.ErrEntityNotFound) { + return atree.SlabIndexUndefined, err + } + } + + if val != nil { + if len(val) != len(index) { + return atree.SlabIndexUndefined, fmt.Errorf( + "slab index was not stored in correct format for owner %x", + owner, + ) + } + + copy(index[:], val) + } + + index = index.Next() + if err := r.store.set(ledgerSlabIndex, owner, index[:], r.batch); err != nil { + return atree.SlabIndexUndefined, fmt.Errorf( + "slab index failed to set for owner %x: %w", + owner, + err, + ) + } + + return index, nil +} + +// id calculates a ledger id with embedded block height for owner and key. +// The key for a register has the following schema: +// {owner}{key}{height} +func (r *Register) id(owner, key []byte) []byte { + id := append(owner, key...) + h := uint64Bytes(r.height) + return append(id, h...) +} + +func (r *Register) idUpper(owner, key []byte) []byte { + id := []byte{ledgerValue} + id = append(id, owner...) + id = append(id, key...) + // increase height +1 because upper bound is exclusive + h := uint64Bytes(r.height + 1) + return append(id, h...) +} + +func (r *Register) idLower(owner, key []byte) []byte { + id := []byte{ledgerValue} + id = append(id, owner...) + id = append(id, key...) + // lower height is always 0 + return append(id, uint64Bytes(0)...) +} diff --git a/tests/e2e_web3js_test.go b/tests/e2e_web3js_test.go index 57f06e808..79d5d8518 100644 --- a/tests/e2e_web3js_test.go +++ b/tests/e2e_web3js_test.go @@ -34,7 +34,6 @@ func TestWeb3_E2E(t *testing.T) { }) t.Run("test transaction traces", func(t *testing.T) { - t.Skip("not yet added back") runWeb3Test(t, "debug_traces_test") }) diff --git a/tests/go.mod b/tests/go.mod index ac19862fe..c61c7ed2d 100644 --- a/tests/go.mod +++ b/tests/go.mod @@ -55,7 +55,9 @@ require ( github.com/dgraph-io/ristretto v0.1.0 // indirect github.com/dgryski/go-farm v0.0.0-20190423205320-6a90982ecee2 // indirect github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f // indirect + github.com/dlclark/regexp2 v1.7.0 // indirect github.com/docker/go-units v0.5.0 // indirect + github.com/dop251/goja v0.0.0-20230806174421-c933cf95e127 // indirect github.com/dustin/go-humanize v1.0.1 // indirect github.com/ef-ds/deque v1.0.4 // indirect github.com/ethereum/c-kzg-4844 v1.0.0 // indirect @@ -75,6 +77,7 @@ require ( github.com/go-logr/stdr v1.2.2 // indirect github.com/go-ole/go-ole v1.3.0 // indirect github.com/go-redis/redis/v8 v8.11.5 // indirect + github.com/go-sourcemap/sourcemap v2.1.3+incompatible // indirect github.com/gofrs/flock v0.8.1 // indirect github.com/gogo/protobuf v1.3.2 // indirect github.com/golang/glog v1.2.0 // indirect @@ -83,6 +86,7 @@ require ( github.com/golang/snappy v0.0.5-0.20220116011046-fa5810519dcb // indirect github.com/google/go-cmp v0.6.0 // indirect github.com/google/go-dap v0.11.0 // indirect + github.com/google/pprof v0.0.0-20231229205709-960ae82b1e42 // indirect github.com/google/s2a-go v0.1.7 // indirect github.com/google/uuid v1.6.0 // indirect github.com/googleapis/enterprise-certificate-proxy v0.3.2 // indirect diff --git a/tests/go.sum b/tests/go.sum index e7b099561..1311f1f08 100644 --- a/tests/go.sum +++ b/tests/go.sum @@ -141,8 +141,11 @@ github.com/cespare/xxhash/v2 v2.2.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XL github.com/cespare/xxhash/v2 v2.3.0 h1:UL815xU9SqsFlibzuggzjXhog7bL6oX9BbNZnL2UFvs= github.com/cespare/xxhash/v2 v2.3.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs= github.com/chzyer/logex v1.1.10/go.mod h1:+Ywpsq7O8HXn0nuIou7OrIPyXbp3wmkHB+jjWRnGsAI= +github.com/chzyer/logex v1.2.0/go.mod h1:9+9sk7u7pGNWYMkh0hdiL++6OeibzJccyQU4p4MedaY= github.com/chzyer/readline v0.0.0-20180603132655-2972be24d48e/go.mod h1:nSuG5e5PlCu98SY8svDHJxuZscDgtXS6KTTbou5AhLI= +github.com/chzyer/readline v1.5.0/go.mod h1:x22KAscuvRqlLoK9CsoYsmxoXZMMFVyOl86cAH8qUic= github.com/chzyer/test v0.0.0-20180213035817-a1ea475d72b1/go.mod h1:Q3SI9o4m/ZMnBNeIyt5eFwwo7qiLfzFZmjNmxjkiQlU= +github.com/chzyer/test v0.0.0-20210722231415-061457976a23/go.mod h1:Q3SI9o4m/ZMnBNeIyt5eFwwo7qiLfzFZmjNmxjkiQlU= github.com/clbanning/x2j v0.0.0-20191024224557-825249438eec/go.mod h1:jMjuTZXRI4dUb/I5gc9Hdhagfvm9+RyrPryS/auMzxE= github.com/client9/misspell v0.3.4/go.mod h1:qj6jICC3Q7zFZvVWo7KLAzC3yx5G7kyvSDkc90ppPyw= github.com/cncf/udpa/go v0.0.0-20191209042840-269d4d468f6f/go.mod h1:M8M6+tZqaGXZJjfX53e64911xZQV5JYwmTeXPW+k8Sc= @@ -222,8 +225,16 @@ github.com/dgryski/go-farm v0.0.0-20190423205320-6a90982ecee2/go.mod h1:SqUrOPUn github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f h1:lO4WD4F/rVNCu3HqELle0jiPLLBs70cWOduZpkS1E78= github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f/go.mod h1:cuUVRXasLTGF7a8hSLbxyZXjz+1KgoB3wDUb6vlszIc= github.com/dgryski/go-sip13 v0.0.0-20181026042036-e10d5fee7954/go.mod h1:vAd38F8PWV+bWy6jNmig1y/TA+kYO4g3RSRF0IAv0no= +github.com/dlclark/regexp2 v1.4.1-0.20201116162257-a2a8dda75c91/go.mod h1:2pZnwuY/m+8K6iRw6wQdMtk+rH5tNGR1i55kozfMjCc= +github.com/dlclark/regexp2 v1.7.0 h1:7lJfhqlPssTb1WQx4yvTHN0uElPEv52sbaECrAQxjAo= +github.com/dlclark/regexp2 v1.7.0/go.mod h1:DHkYz0B9wPfa6wondMfaivmHpzrQ3v9q8cnmRbL6yW8= github.com/docker/go-units v0.5.0 h1:69rxXcBk27SvSaaxTtLh/8llcHD8vYHT7WSdRZ/jvr4= github.com/docker/go-units v0.5.0/go.mod h1:fgPhTUdO+D/Jk86RDLlptpiXQzgHJF7gydDDbaIK4Dk= +github.com/dop251/goja v0.0.0-20211022113120-dc8c55024d06/go.mod h1:R9ET47fwRVRPZnOGvHxxhuZcbrMCuiqOz3Rlrh4KSnk= +github.com/dop251/goja v0.0.0-20230806174421-c933cf95e127 h1:qwcF+vdFrvPSEUDSX5RVoRccG8a5DhOdWdQ4zN62zzo= +github.com/dop251/goja v0.0.0-20230806174421-c933cf95e127/go.mod h1:QMWlm50DNe14hD7t24KEqZuUdC9sOTy8W6XbCU1mlw4= +github.com/dop251/goja_nodejs v0.0.0-20210225215109-d91c329300e7/go.mod h1:hn7BA7c8pLvoGndExHudxTDKZ84Pyvv+90pbBjbTz0Y= +github.com/dop251/goja_nodejs v0.0.0-20211022123610-8dd9abb0616d/go.mod h1:DngW8aVqWbuLRMHItjPUyqdj+HWPvnQe8V8y1nDpIbM= github.com/dustin/go-humanize v0.0.0-20171111073723-bb3d318650d4/go.mod h1:HtrtbFcZ19U5GC7JDqmcUSB87Iq5E25KnS6fMYU6eOk= github.com/dustin/go-humanize v1.0.0/go.mod h1:HtrtbFcZ19U5GC7JDqmcUSB87Iq5E25KnS6fMYU6eOk= github.com/dustin/go-humanize v1.0.1 h1:GzkhY7T5VNhEkwH0PVJgjz+fX1rhBrR7pRT3mDkpeCY= @@ -330,16 +341,21 @@ github.com/go-playground/validator/v10 v10.14.1 h1:9c50NUPC30zyuKprjL3vNZ0m5oG+j github.com/go-playground/validator/v10 v10.14.1/go.mod h1:9iXMNT7sEkjXb0I+enO7QXmzG6QCsPWY4zveKFVRSyU= github.com/go-redis/redis/v8 v8.11.5 h1:AcZZR7igkdvfVmQTPnu9WE37LRrO/YrBH5zWyjDC0oI= github.com/go-redis/redis/v8 v8.11.5/go.mod h1:gREzHqY1hg6oD9ngVRbLStwAWKhA0FEgq8Jd4h5lpwo= +github.com/go-sourcemap/sourcemap v2.1.3+incompatible h1:W1iEw64niKVGogNgBN3ePyLFfuisuzeidWPMPWmECqU= +github.com/go-sourcemap/sourcemap v2.1.3+incompatible/go.mod h1:F8jJfvm2KbVjc5NqelyYJmf/v5J0dwNLS2mL4sNA1Jg= github.com/go-sql-driver/mysql v1.4.0/go.mod h1:zAC/RDZ24gD3HViQzih4MyKcchzm+sOG5ZlKdlhCg5w= github.com/go-stack/stack v1.8.0/go.mod h1:v0f6uXyyMGvRgIKkXu+yp6POWl0qKG85gN/melR3HDY= github.com/go-task/slim-sprig v0.0.0-20230315185526-52ccab3ef572 h1:tfuBGBXKqDEevZMzYi5KSi8KkcZtzBcTgAUUtapy0OI= github.com/go-task/slim-sprig v0.0.0-20230315185526-52ccab3ef572/go.mod h1:9Pwr4B2jHnOSGXyyzV8ROjYa2ojvAY6HCGYYfMoC3Ls= -github.com/gobwas/httphead v0.0.0-20180130184737-2c6c146eadee h1:s+21KNqlpePfkah2I+gwHF8xmJWRjooY+5248k6m4A0= github.com/gobwas/httphead v0.0.0-20180130184737-2c6c146eadee/go.mod h1:L0fX3K22YWvt/FAX9NnzrNzcI4wNYi9Yku4O0LKYflo= -github.com/gobwas/pool v0.2.0 h1:QEmUOlnSjWtnpRGHF3SauEiOsy82Cup83Vf2LcMlnc8= +github.com/gobwas/httphead v0.1.0 h1:exrUm0f4YX0L7EBwZHuCF4GDp8aJfVeBrlLQrs6NqWU= +github.com/gobwas/httphead v0.1.0/go.mod h1:O/RXo79gxV8G+RqlR/otEwx4Q36zl9rqC5u12GKvMCM= github.com/gobwas/pool v0.2.0/go.mod h1:q8bcK0KcYlCgd9e7WYLm9LpyS+YeLd8JVDW6WezmKEw= -github.com/gobwas/ws v1.0.2 h1:CoAavW/wd/kulfZmSIBt6p24n4j7tHgNVCjsfHVNUbo= +github.com/gobwas/pool v0.2.1 h1:xfeeEhW7pwmX8nuLVlqbzVc7udMDrwetjEv+TZIz1og= +github.com/gobwas/pool v0.2.1/go.mod h1:q8bcK0KcYlCgd9e7WYLm9LpyS+YeLd8JVDW6WezmKEw= github.com/gobwas/ws v1.0.2/go.mod h1:szmBTxLgaFppYjEmNtny/v3w89xOydFnnZMcgRRu/EM= +github.com/gobwas/ws v1.2.1 h1:F2aeBZrm2NDsc7vbovKrWSogd4wvfAxg0FQ89/iqOTk= +github.com/gobwas/ws v1.2.1/go.mod h1:hRKAFb8wOxFROYNsT1bqfWnhX+b5MFeJM9r2ZSwg/KY= github.com/goccy/go-json v0.10.2 h1:CrxCmQqYDkv1z7lO7Wbh2HN93uovUHgrECaO5ZrCXAU= github.com/goccy/go-json v0.10.2/go.mod h1:6MelG93GURQebXPDq3khkgXZkazVtN9CRI+MGFi0w8I= github.com/godbus/dbus/v5 v5.0.4/go.mod h1:xhWf0FNVPg57R7Z0UbKHbJfkEywrmjJnf7w5xrFpKfA= @@ -435,6 +451,7 @@ github.com/google/pprof v0.0.0-20200708004538-1a94d8640e99/go.mod h1:ZgVRPoUq/hf github.com/google/pprof v0.0.0-20201023163331-3e6fc7fc9c4c/go.mod h1:kpwsk12EmLew5upagYY7GY0pfYCcupk39gWOCRROcvE= github.com/google/pprof v0.0.0-20201203190320-1bf35d6f28c2/go.mod h1:kpwsk12EmLew5upagYY7GY0pfYCcupk39gWOCRROcvE= github.com/google/pprof v0.0.0-20201218002935-b9804c9f04c2/go.mod h1:kpwsk12EmLew5upagYY7GY0pfYCcupk39gWOCRROcvE= +github.com/google/pprof v0.0.0-20230207041349-798e818bf904/go.mod h1:uglQLonpP8qtYCYyzA+8c/9qtqgA3qsXGYqCPKARAFg= github.com/google/pprof v0.0.0-20231229205709-960ae82b1e42 h1:dHLYa5D8/Ta0aLR2XcPsrkpAgGeFs6thhMcQK0oQ0n8= github.com/google/pprof v0.0.0-20231229205709-960ae82b1e42/go.mod h1:czg5+yv1E0ZGTi6S6vVK1mke0fV+FaUhNGcd6VRS9Ik= github.com/google/renameio v0.1.0/go.mod h1:KWCgfxg9yswjAJkECMjeO8J8rahYeXnNhOm40UhjYkI= @@ -524,6 +541,7 @@ github.com/huin/goupnp v1.3.0 h1:UvLUlWDNpoUdYzb2TCn+MuTWtcjXKSza2n6CBdQ0xXc= github.com/huin/goupnp v1.3.0/go.mod h1:gnGPsThkYa7bFi/KWmEysQRf48l2dvR5bxr2OFckNX8= github.com/ianlancetaylor/demangle v0.0.0-20181102032728-5e5cf60278f6/go.mod h1:aSSvb/t6k1mPoxDqO4vJh6VOCGPwU4O0C2/Eqndh1Sc= github.com/ianlancetaylor/demangle v0.0.0-20200824232613-28f6c0f3b639/go.mod h1:aSSvb/t6k1mPoxDqO4vJh6VOCGPwU4O0C2/Eqndh1Sc= +github.com/ianlancetaylor/demangle v0.0.0-20220319035150-800ac71e25c2/go.mod h1:aYm2/VgdVmcIU8iMfdMvDMsRAQjcfZSKFby6HOFvi/w= github.com/improbable-eng/grpc-web v0.15.0 h1:BN+7z6uNXZ1tQGcNAuaU1YjsLTApzkjt2tzCixLaUPQ= github.com/improbable-eng/grpc-web v0.15.0/go.mod h1:1sy9HKV4Jt9aEs9JSnkWlRJPuPtwNr0l57L4f878wP8= github.com/inconshreveable/mousetrap v1.0.0/go.mod h1:PxqpIevigyE2G7u3NXJIT2ANytuPF1OarO4DADm73n8= @@ -607,6 +625,8 @@ github.com/koron/go-ssdp v0.0.4/go.mod h1:oDXq+E5IL5q0U8uSBcoAXzTzInwy5lEgC91HoK github.com/kr/fs v0.1.0/go.mod h1:FFnZGqtBN9Gxj7eW1uZ42v5BccTP0vu6NEaFoC2HwRg= github.com/kr/logfmt v0.0.0-20140226030751-b84e30acd515/go.mod h1:+0opPa2QZZtGFBFZlji/RkVcI2GknAs/DXo4wKdlNEc= github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo= +github.com/kr/pretty v0.2.1/go.mod h1:ipq/a2n7PKx3OHsz4KJII5eveXtPO4qwEXGdVfWzfnI= +github.com/kr/pretty v0.3.0/go.mod h1:640gp4NfQd8pI5XOwp5fnNeVWj67G7CFk/SaSQn7NBk= github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE= github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk= github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ= @@ -908,6 +928,7 @@ github.com/rivo/uniseg v0.4.4 h1:8TfxU8dW6PdqD27gjM8MVNuicgxIjxpm4K7x4jp8sis= github.com/rivo/uniseg v0.4.4/go.mod h1:FN3SvrM+Zdj16jyLfmOkMNblXMcoc8DfTHruCPUcx88= github.com/rogpeppe/fastuuid v0.0.0-20150106093220-6724a57986af/go.mod h1:XWv6SoW27p1b0cqNHllgS5HIMJraePCO15w5zCzIWYg= github.com/rogpeppe/go-internal v1.3.0/go.mod h1:M8bDsm7K2OlrFYOpmOWEs/qY81heoFRclV5y23lUDJ4= +github.com/rogpeppe/go-internal v1.6.1/go.mod h1:xXDCJY+GAPziupqXw64V24skbSoqbTEfhy4qGm1nDQc= github.com/rogpeppe/go-internal v1.9.0/go.mod h1:WtVeX8xhTBvf0smdhujwtBcq4Qrzq/fJaraNFVN+nFs= github.com/rogpeppe/go-internal v1.10.0 h1:TMyTOH3F/DB16zRVcYyreMH6GnZZrwQVAoYjRBZyWFQ= github.com/rogpeppe/go-internal v1.10.0/go.mod h1:UQnix2H7Ngw/k4C5ijL5+65zddjncjaFoBhdsK/akog= @@ -1306,6 +1327,7 @@ golang.org/x/sys v0.0.0-20210510120138-977fb7262007/go.mod h1:oPkhp1MJrh7nUepCBc golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20210630005230-0f9fa26af87c/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20210809222454-d867a43fc93e/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20220310020820-b874c991c1a5/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= diff --git a/tests/web3js/eth_rate_limit_test.js b/tests/web3js/eth_rate_limit_test.js index f886586de..5f55c0e3c 100644 --- a/tests/web3js/eth_rate_limit_test.js +++ b/tests/web3js/eth_rate_limit_test.js @@ -1,10 +1,10 @@ const { assert } = require('chai') -const {Web3} = require("web3") +const { Web3 } = require('web3') it('rate limit after X requests', async function () { this.timeout(0) setTimeout(() => process.exit(0), 5000) // make sure the process exits - let ws = new Web3("ws://127.0.0.1:8545") + let ws = new Web3('ws://127.0.0.1:8545') // wait for ws connection to establish and reset rate-limit timer await new Promise(res => setTimeout(res, 1500)) @@ -19,14 +19,14 @@ it('rate limit after X requests', async function () { try { await ws.eth.getBlockNumber() requestsMade++ - } catch(e) { + } catch (e) { assert.equal(e.innerError.message, 'limit of requests per second reached') requestsFailed++ } } - assert.equal(requestsMade, requestLimit, "more requests made than the limit") - assert.equal(requestsFailed, requests-requestLimit, "failed requests don't match expected value") + assert.equal(requestsMade, requestLimit, 'more requests made than the limit') + assert.equal(requestsFailed, requests - requestLimit, 'failed requests don\'t match expected value') await new Promise(res => setTimeout(res, 1000)) @@ -38,14 +38,14 @@ it('rate limit after X requests', async function () { try { await ws.eth.getBlockNumber() requestsMade++ - } catch(e) { + } catch (e) { assert.equal(e.innerError.message, 'limit of requests per second reached') requestsFailed++ } } - assert.equal(requestsMade, requestLimit, "more requests made than the limit") - assert.equal(requestsFailed, requests-requestLimit, "failed requests don't match expected value") + assert.equal(requestsMade, requestLimit, 'more requests made than the limit') + assert.equal(requestsFailed, requests - requestLimit, 'failed requests don\'t match expected value') await ws.currentProvider.disconnect() }) From 64d33e02634e6d1269dc4d07969339bdfb7b428e Mon Sep 17 00:00:00 2001 From: Ardit Marku Date: Mon, 4 Nov 2024 17:03:10 +0200 Subject: [PATCH 022/174] Implement RemoteCadenceArch type for fetching precompiled calls in contract calls --- services/requester/remote_cadence_arch.go | 154 ++++++++++++++++++ services/requester/requester.go | 25 +++ tests/e2e_web3js_test.go | 1 - .../web3js/verify_cadence_arch_calls_test.js | 12 +- 4 files changed, 185 insertions(+), 7 deletions(-) create mode 100644 services/requester/remote_cadence_arch.go diff --git a/services/requester/remote_cadence_arch.go b/services/requester/remote_cadence_arch.go new file mode 100644 index 000000000..da4287d3a --- /dev/null +++ b/services/requester/remote_cadence_arch.go @@ -0,0 +1,154 @@ +package requester + +import ( + "context" + "encoding/hex" + "fmt" + "math/big" + "strings" + + "github.com/onflow/cadence" + errs "github.com/onflow/flow-evm-gateway/models/errors" + evmImpl "github.com/onflow/flow-go/fvm/evm/impl" + evmTypes "github.com/onflow/flow-go/fvm/evm/types" + "github.com/onflow/flow-go/fvm/systemcontracts" + flowGo "github.com/onflow/flow-go/model/flow" + gethCommon "github.com/onflow/go-ethereum/common" + "github.com/onflow/go-ethereum/core/types" + "github.com/onflow/go-ethereum/crypto" +) + +var cadenceArchAddress = gethCommon.HexToAddress("0x0000000000000000000000010000000000000001") + +type RemoteCadenceArch struct { + blockHeight uint64 + client *CrossSporkClient + chainID flowGo.ChainID + cachedCalls map[string]evmTypes.Data +} + +var _ evmTypes.PrecompiledContract = (*RemoteCadenceArch)(nil) + +func NewRemoteCadenceArch( + blockHeight uint64, + client *CrossSporkClient, + chainID flowGo.ChainID, +) *RemoteCadenceArch { + return &RemoteCadenceArch{ + blockHeight: blockHeight, + client: client, + chainID: chainID, + cachedCalls: map[string]evmTypes.Data{}, + } +} + +func (rca *RemoteCadenceArch) Address() evmTypes.Address { + return evmTypes.NewAddress(cadenceArchAddress) +} + +func (rca *RemoteCadenceArch) RequiredGas(input []byte) uint64 { + evmResult, err := rca.runCall(input) + if err != nil { + return 0 + } + + key := hex.EncodeToString(crypto.Keccak256(input)) + rca.cachedCalls[key] = evmResult.ReturnedData + + return evmResult.GasConsumed +} + +func (rca *RemoteCadenceArch) Run(input []byte) ([]byte, error) { + key := hex.EncodeToString(crypto.Keccak256(input)) + result, ok := rca.cachedCalls[key] + + if !ok { + evmResult, err := rca.runCall(input) + if err != nil { + return nil, err + } + return evmResult.ReturnedData, nil + } + + return result, nil +} + +func (rca *RemoteCadenceArch) replaceAddresses(script []byte) []byte { + // make the list of all contracts we should replace address for + sc := systemcontracts.SystemContractsForChain(rca.chainID) + contracts := []systemcontracts.SystemContract{sc.EVMContract, sc.FungibleToken, sc.FlowToken} + + s := string(script) + // iterate over all the import name and address pairs and replace them in script + for _, contract := range contracts { + s = strings.ReplaceAll(s, + fmt.Sprintf("import %s", contract.Name), + fmt.Sprintf("import %s from %s", contract.Name, contract.Address.HexWithPrefix()), + ) + } + + return []byte(s) +} + +func (rca *RemoteCadenceArch) runCall(input []byte) (*evmTypes.ResultSummary, error) { + tx := types.NewTx( + &types.LegacyTx{ + Nonce: 0, + To: &cadenceArchAddress, + Value: big.NewInt(0), + Gas: 55_000, + GasPrice: big.NewInt(0), + Data: input, + }, + ) + encodedTx, err := tx.MarshalBinary() + if err != nil { + return nil, err + } + hexEncodedTx, err := cadence.NewString(hex.EncodeToString(encodedTx)) + if err != nil { + return nil, err + } + + hexEncodedAddress, err := addressToCadenceString(evmTypes.CoinbaseAddress.ToCommon()) + if err != nil { + return nil, err + } + + scriptResult, err := rca.client.ExecuteScriptAtBlockHeight( + context.Background(), + rca.blockHeight, + rca.replaceAddresses(dryRunScript), + []cadence.Value{hexEncodedTx, hexEncodedAddress}, + ) + if err != nil { + return nil, err + } + + evmResult, err := parseResult(scriptResult) + if err != nil { + return nil, err + } + + return evmResult, nil +} + +func addressToCadenceString(address gethCommon.Address) (cadence.String, error) { + return cadence.NewString(strings.TrimPrefix(address.Hex(), "0x")) +} + +func parseResult(res cadence.Value) (*evmTypes.ResultSummary, error) { + result, err := evmImpl.ResultSummaryFromEVMResultValue(res) + if err != nil { + return nil, fmt.Errorf("failed to decode EVM result of type: %s, with: %w", res.Type().ID(), err) + } + + if result.ErrorCode != 0 { + if result.ErrorCode == evmTypes.ExecutionErrCodeExecutionReverted { + return nil, errs.NewRevertError(result.ReturnedData) + } + return nil, errs.NewFailedTransactionError(result.ErrorMessage) + } + + return result, err +} diff --git a/services/requester/requester.go b/services/requester/requester.go index a90200ba9..0f8fde87e 100644 --- a/services/requester/requester.go +++ b/services/requester/requester.go @@ -397,12 +397,18 @@ func (e *EVM) Call( if tx.To != nil { to = *tx.To } + cdcHeight, err := e.evmToCadenceHeight(evmHeight) + if err != nil { + return nil, err + } + rca := NewRemoteCadenceArch(cdcHeight, e.client, e.config.FlowNetworkID) result, err := view.DryCall( from, to, tx.Data, tx.Value, tx.Gas, + query.WithExtraPrecompiledContracts([]evmTypes.PrecompiledContract{rca}), ) resultSummary := result.ResultSummary() @@ -431,12 +437,18 @@ func (e *EVM) EstimateGas( if tx.To != nil { to = *tx.To } + cdcHeight, err := e.evmToCadenceHeight(evmHeight) + if err != nil { + return 0, err + } + rca := NewRemoteCadenceArch(cdcHeight, e.client, e.config.FlowNetworkID) result, err := view.DryCall( from, to, tx.Data, tx.Value, tx.Gas, + query.WithExtraPrecompiledContracts([]evmTypes.PrecompiledContract{rca}), ) if err != nil { return 0, err @@ -668,3 +680,16 @@ func AddOne64th(n uint64) uint64 { // NOTE: Go's integer division floors, but that is desirable here return n + (n / 64) } + +func (e *EVM) evmToCadenceHeight(height int64) (uint64, error) { + cadenceHeight, err := e.blocks.GetCadenceHeight(uint64(height)) + if err != nil { + return 0, fmt.Errorf( + "failed to map evm height: %d to cadence height: %w", + height, + err, + ) + } + + return cadenceHeight, nil +} diff --git a/tests/e2e_web3js_test.go b/tests/e2e_web3js_test.go index 79d5d8518..03c8b3860 100644 --- a/tests/e2e_web3js_test.go +++ b/tests/e2e_web3js_test.go @@ -29,7 +29,6 @@ func TestWeb3_E2E(t *testing.T) { }) t.Run("verify Cadence arch calls", func(t *testing.T) { - t.Skip("not implemented yet") runWeb3Test(t, "verify_cadence_arch_calls_test") }) diff --git a/tests/web3js/verify_cadence_arch_calls_test.js b/tests/web3js/verify_cadence_arch_calls_test.js index f52496434..914dc352f 100644 --- a/tests/web3js/verify_cadence_arch_calls_test.js +++ b/tests/web3js/verify_cadence_arch_calls_test.js @@ -12,7 +12,7 @@ it('should be able to use Cadence Arch calls', async () => { let contractAddress = deployed.receipt.contractAddress // submit a transaction that calls verifyArchCallToRandomSource(uint64 height) - let getRandomSourceData = deployed.contract.methods.verifyArchCallToRandomSource(120).encodeABI() + let getRandomSourceData = deployed.contract.methods.verifyArchCallToRandomSource(2).encodeABI() res = await helpers.signAndSend({ from: conf.eoa.address, to: contractAddress, @@ -23,7 +23,7 @@ it('should be able to use Cadence Arch calls', async () => { assert.equal(res.receipt.status, conf.successStatus) // make a contract call for verifyArchCallToRandomSource(uint64 height) - res = await web3.eth.call({ to: contractAddress, data: getRandomSourceData }, latest) + res = await web3.eth.call({ to: contractAddress, data: getRandomSourceData }, 'latest') assert.notEqual( res, '0x0000000000000000000000000000000000000000000000000000000000000000' @@ -42,7 +42,7 @@ it('should be able to use Cadence Arch calls', async () => { assert.equal(res.receipt.status, conf.successStatus) // make a contract call for verifyArchCallToRevertibleRandom() - res = await web3.eth.call({ to: contractAddress, data: revertibleRandomData }, latest) + res = await web3.eth.call({ to: contractAddress, data: revertibleRandomData }, 'latest') assert.notEqual( res, '0x0000000000000000000000000000000000000000000000000000000000000000' @@ -61,10 +61,10 @@ it('should be able to use Cadence Arch calls', async () => { assert.equal(res.receipt.status, conf.successStatus) // make a contract call for verifyArchCallToFlowBlockHeight() - res = await web3.eth.call({ to: contractAddress, data: flowBlockHeightData }, latest) + res = await web3.eth.call({ to: contractAddress, data: flowBlockHeightData }, 'latest') assert.equal( web3.eth.abi.decodeParameter('uint64', res), - latest, + 7n, ) // submit a transaction that calls verifyArchCallToVerifyCOAOwnershipProof(address,bytes32,bytes) @@ -84,7 +84,7 @@ it('should be able to use Cadence Arch calls', async () => { assert.equal(res.receipt.status, conf.successStatus) // make a contract call for verifyArchCallToVerifyCOAOwnershipProof(address,bytes32,bytes) - res = await web3.eth.call({ to: contractAddress, data: verifyCOAOwnershipProofData }, latest) + res = await web3.eth.call({ to: contractAddress, data: verifyCOAOwnershipProofData }, 'latest') assert.equal( web3.eth.abi.decodeParameter('bool', res), false, From 18045bf7c040093afa3876a1a9ef6d87f2083209 Mon Sep 17 00:00:00 2001 From: Janez Podhostnik Date: Wed, 6 Nov 2024 18:36:57 +0100 Subject: [PATCH 023/174] swap storage --- api/debug.go | 44 ++++---- bootstrap/bootstrap.go | 44 +++++++- services/ingestion/engine.go | 34 +++--- services/ingestion/engine_test.go | 37 +++++-- services/requester/requester.go | 8 +- storage/pebble/register.go | 172 ------------------------------ 6 files changed, 120 insertions(+), 219 deletions(-) delete mode 100644 storage/pebble/register.go diff --git a/api/debug.go b/api/debug.go index 79fdca187..b9734fc0d 100644 --- a/api/debug.go +++ b/api/debug.go @@ -38,18 +38,20 @@ type txTraceResult struct { } type DebugAPI struct { - store *pebble.Storage - logger zerolog.Logger - tracer storage.TraceIndexer - blocks storage.BlockIndexer - transactions storage.TransactionIndexer - receipts storage.ReceiptIndexer - config *config.Config - collector metrics.Collector + store *pebble.Storage + registerStore *pebble.RegisterStorage + logger zerolog.Logger + tracer storage.TraceIndexer + blocks storage.BlockIndexer + transactions storage.TransactionIndexer + receipts storage.ReceiptIndexer + config *config.Config + collector metrics.Collector } func NewDebugAPI( store *pebble.Storage, + registerStore *pebble.RegisterStorage, tracer storage.TraceIndexer, blocks storage.BlockIndexer, transactions storage.TransactionIndexer, @@ -59,14 +61,15 @@ func NewDebugAPI( collector metrics.Collector, ) *DebugAPI { return &DebugAPI{ - store: store, - logger: logger, - tracer: tracer, - blocks: blocks, - transactions: transactions, - receipts: receipts, - config: config, - collector: collector, + store: store, + registerStore: registerStore, + logger: logger, + tracer: tracer, + blocks: blocks, + transactions: transactions, + receipts: receipts, + config: config, + collector: collector, } } @@ -270,7 +273,6 @@ func (d *DebugAPI) TraceCall( return nil, err } - ledger := pebble.NewRegister(d.store, block.Height, nil) blocksProvider := replayer.NewBlocksProvider( d.blocks, d.config.FlowNetworkID, @@ -279,7 +281,7 @@ func (d *DebugAPI) TraceCall( viewProvider := query.NewViewProvider( d.config.FlowNetworkID, flowEVM.StorageAccountAddress(d.config.FlowNetworkID), - ledger, + d.registerStore, blocksProvider, 120_000_000, ) @@ -331,7 +333,11 @@ func (d *DebugAPI) TraceCall( } func (d *DebugAPI) executorAtBlock(block *models.Block) (*evm.BlockExecutor, error) { - ledger := pebble.NewRegister(d.store, block.Height, d.store.NewBatch()) + snapshot, err := d.registerStore.GetSnapshotAt(block.Height) + if err != nil { + return nil, fmt.Errorf("failed to get register snapshot at block height %d: %w", block.Height, err) + } + ledger := storage.NewRegisterDelta(snapshot) return evm.NewBlockExecutor( block, diff --git a/bootstrap/bootstrap.go b/bootstrap/bootstrap.go index ed2caa057..2918cd466 100644 --- a/bootstrap/bootstrap.go +++ b/bootstrap/bootstrap.go @@ -7,6 +7,8 @@ import ( "math" "time" + pebbleDB "github.com/cockroachdb/pebble" + "github.com/onflow/flow-go-sdk/access" "github.com/onflow/flow-go-sdk/access/grpc" "github.com/onflow/flow-go-sdk/crypto" @@ -32,6 +34,7 @@ import ( type Storages struct { Storage *pebble.Storage + Registers *pebble.RegisterStorage Blocks storage.BlockIndexer Transactions storage.TransactionIndexer Receipts storage.ReceiptIndexer @@ -149,6 +152,7 @@ func (b *Bootstrap) StartEventIngestion(ctx context.Context) error { subscriber, blocksProvider, b.storages.Storage, + b.storages.Registers, b.storages.Blocks, b.storages.Receipts, b.storages.Transactions, @@ -219,6 +223,7 @@ func (b *Bootstrap) StartAPIServer(ctx context.Context) error { evm, err := requester.NewEVM( b.storages.Storage, + b.storages.Registers, blocksProvider, b.client, b.config, @@ -281,6 +286,7 @@ func (b *Bootstrap) StartAPIServer(ctx context.Context) error { debugAPI := api.NewDebugAPI( b.storages.Storage, + b.storages.Registers, b.storages.Traces, b.storages.Blocks, b.storages.Transactions, @@ -464,6 +470,8 @@ func setupStorage( } blocks := pebble.NewBlocks(store, config.FlowNetworkID) + storageAddress := evm.StorageAccountAddress(config.FlowNetworkID) + registerStore := pebble.NewRegisterStorage(store, storageAddress) // hard set the start cadence height, this is used when force reindexing if config.ForceStartCadenceHeight != 0 { @@ -475,22 +483,40 @@ func setupStorage( // if database is not initialized require init height if _, err := blocks.LatestCadenceHeight(); errors.Is(err, errs.ErrStorageNotInitialized) { + batch := store.NewBatch() + defer func(batch *pebbleDB.Batch) { + err := batch.Close() + if err != nil { + // we don't know what went wrong, so this is fatal + logger.Fatal().Err(err).Msg("failed to close batch") + } + }(batch) + cadenceHeight := config.InitCadenceHeight cadenceBlock, err := client.GetBlockHeaderByHeight(context.Background(), cadenceHeight) if err != nil { return nil, fmt.Errorf("could not fetch provided cadence height, make sure it's correct: %w", err) } - storageProvider := pebble.NewRegister(store, 0, nil) - storageAddress := evm.StorageAccountAddress(config.FlowNetworkID) + snapshot, err := registerStore.GetSnapshotAt(0) + if err != nil { + return nil, fmt.Errorf("could not get register snapshot at block height %d: %w", 0, err) + } + + delta := storage.NewRegisterDelta(snapshot) accountStatus := environment.NewAccountStatus() - err = storageProvider.SetValue( + err = delta.SetValue( storageAddress[:], []byte(flowGo.AccountStatusKey), accountStatus.ToBytes(), ) if err != nil { - return nil, fmt.Errorf("could not initialize state index: %w", err) + return nil, fmt.Errorf("could not set account status: %w", err) + } + + err = registerStore.Store(delta.GetUpdates(), cadenceHeight, batch) + if err != nil { + return nil, fmt.Errorf("could not store register updates: %w", err) } if err := blocks.InitHeights(cadenceHeight, cadenceBlock.ID); err != nil { @@ -501,12 +527,22 @@ func setupStorage( err, ) } + + err = batch.Commit(pebbleDB.Sync) + if err != nil { + return nil, fmt.Errorf("could not commit register updates: %w", err) + } + logger.Info().Msgf("database initialized with cadence height: %d", cadenceHeight) } + //else { + // // TODO(JanezP): verify storage account owner is correct + //} return &Storages{ Storage: store, Blocks: blocks, + Registers: registerStore, Transactions: pebble.NewTransactions(store), Receipts: pebble.NewReceipts(store), Accounts: pebble.NewAccounts(store), diff --git a/services/ingestion/engine.go b/services/ingestion/engine.go index 8a852d14c..bcfb7ae3d 100644 --- a/services/ingestion/engine.go +++ b/services/ingestion/engine.go @@ -4,6 +4,8 @@ import ( "context" "fmt" + flowGo "github.com/onflow/flow-go/model/flow" + pebbleDB "github.com/cockroachdb/pebble" "github.com/onflow/flow-go-sdk" gethTypes "github.com/onflow/go-ethereum/core/types" @@ -40,6 +42,7 @@ type Engine struct { subscriber EventSubscriber blocksProvider *replayer.BlocksProvider store *pebble.Storage + registerStore *pebble.RegisterStorage blocks storage.BlockIndexer receipts storage.ReceiptIndexer transactions storage.TransactionIndexer @@ -57,6 +60,7 @@ func NewEventIngestionEngine( subscriber EventSubscriber, blocksProvider *replayer.BlocksProvider, store *pebble.Storage, + registerStore *pebble.RegisterStorage, blocks storage.BlockIndexer, receipts storage.ReceiptIndexer, transactions storage.TransactionIndexer, @@ -76,6 +80,7 @@ func NewEventIngestionEngine( subscriber: subscriber, blocksProvider: blocksProvider, store: store, + registerStore: registerStore, blocks: blocks, receipts: receipts, transactions: transactions, @@ -173,15 +178,11 @@ func (e *Engine) processEvents(events *models.CadenceEvents) error { return err } - storageProvider := pebble.NewRegister( - e.store, - events.Block().Height, - batch, - ) + blockEvents := events.BlockEventPayload() cr := sync.NewReplayer( e.replayerConfig.ChainID, e.replayerConfig.RootAddr, - storageProvider, + e.registerStore, e.blocksProvider, e.log, e.replayerConfig.CallTracerCollector.TxTracer(), @@ -190,7 +191,7 @@ func (e *Engine) processEvents(events *models.CadenceEvents) error { // Step 1.2: Replay all block transactions // If `ReplayBlock` returns any error, we abort the EVM events processing - res, err := cr.ReplayBlock(events.TxEventPayloads(), events.BlockEventPayload()) + res, err := cr.ReplayBlock(events.TxEventPayloads(), blockEvents) if err != nil { return fmt.Errorf("failed to replay block on height: %d, with: %w", events.Block().Height, err) } @@ -198,11 +199,9 @@ func (e *Engine) processEvents(events *models.CadenceEvents) error { // Step 2: Write all the necessary changes to each storage // Step 2.1: Write all the EVM state changes to `StorageProvider` - for k, v := range res.StorageRegisterUpdates() { - err = storageProvider.SetValue([]byte(k.Owner), []byte(k.Key), v) - if err != nil { - return fmt.Errorf("failed to commit state changes on block: %d", events.Block().Height) - } + err = e.registerStore.Store(registerEntriesFromKeyValue(res.StorageRegisterUpdates()), blockEvents.Height, batch) + if err != nil { + return fmt.Errorf("failed to store state changes on block: %d", events.Block().Height) } // Step 2.2: Write the latest EVM block to `Blocks` storage @@ -341,3 +340,14 @@ func (e *Engine) indexReceipts( return nil } + +func registerEntriesFromKeyValue(keyValue map[flowGo.RegisterID]flowGo.RegisterValue) []flowGo.RegisterEntry { + entries := make([]flowGo.RegisterEntry, 0, len(keyValue)) + for k, v := range keyValue { + entries = append(entries, flowGo.RegisterEntry{ + Key: k, + Value: v, + }) + } + return entries +} diff --git a/services/ingestion/engine_test.go b/services/ingestion/engine_test.go index 7eb14c67c..41f61c412 100644 --- a/services/ingestion/engine_test.go +++ b/services/ingestion/engine_test.go @@ -7,6 +7,8 @@ import ( "math/big" "testing" + "github.com/onflow/flow-evm-gateway/storage" + pebbleDB "github.com/cockroachdb/pebble" "github.com/onflow/flow-go/fvm/environment" "github.com/onflow/flow-go/fvm/evm" @@ -41,7 +43,7 @@ func TestSerialBlockIngestion(t *testing.T) { transactions := &storageMock.TransactionIndexer{} latestHeight := uint64(10) - store := setupStore(t) + store, registerStore := setupStore(t) blocks := &storageMock.BlockIndexer{} blocks. @@ -71,6 +73,7 @@ func TestSerialBlockIngestion(t *testing.T) { subscriber, replayer.NewBlocksProvider(blocks, flowGo.Emulator, nil), store, + registerStore, blocks, receipts, transactions, @@ -126,7 +129,7 @@ func TestSerialBlockIngestion(t *testing.T) { transactions := &storageMock.TransactionIndexer{} latestHeight := uint64(10) - store := setupStore(t) + store, registerStore := setupStore(t) blocks := &storageMock.BlockIndexer{} blocks. @@ -155,6 +158,7 @@ func TestSerialBlockIngestion(t *testing.T) { subscriber, replayer.NewBlocksProvider(blocks, flowGo.Emulator, nil), store, + registerStore, blocks, receipts, transactions, @@ -234,7 +238,7 @@ func TestBlockAndTransactionIngestion(t *testing.T) { nextHeight := latestHeight + 1 blockID := flow.Identifier{0x01} - store := setupStore(t) + store, registerStore := setupStore(t) blocks := &storageMock.BlockIndexer{} blocks. @@ -281,6 +285,7 @@ func TestBlockAndTransactionIngestion(t *testing.T) { subscriber, replayer.NewBlocksProvider(blocks, flowGo.Emulator, nil), store, + registerStore, blocks, receipts, transactions, @@ -353,7 +358,7 @@ func TestBlockAndTransactionIngestion(t *testing.T) { latestHeight := uint64(10) nextHeight := latestHeight + 1 - store := setupStore(t) + store, registerStore := setupStore(t) blocks := &storageMock.BlockIndexer{} blocks. @@ -394,6 +399,7 @@ func TestBlockAndTransactionIngestion(t *testing.T) { subscriber, replayer.NewBlocksProvider(blocks, flowGo.Emulator, nil), store, + registerStore, blocks, receipts, transactions, @@ -463,7 +469,7 @@ func TestBlockAndTransactionIngestion(t *testing.T) { transactions := &storageMock.TransactionIndexer{} latestCadenceHeight := uint64(0) - store := setupStore(t) + store, registerStore := setupStore(t) blocks := &storageMock.BlockIndexer{} blocks. @@ -493,6 +499,7 @@ func TestBlockAndTransactionIngestion(t *testing.T) { subscriber, replayer.NewBlocksProvider(blocks, flowGo.Emulator, nil), store, + registerStore, blocks, receipts, transactions, @@ -658,19 +665,31 @@ func defaultReplayerConfig() replayer.Config { } } -func setupStore(t *testing.T) *pebble.Storage { +func setupStore(t *testing.T) (*pebble.Storage, *pebble.RegisterStorage) { store, err := pebble.New(t.TempDir(), zerolog.Nop()) require.NoError(t, err) - storageProvider := pebble.NewRegister(store, 0, nil) storageAddress := evm.StorageAccountAddress(flowGo.Emulator) + registerStore := pebble.NewRegisterStorage(store, storageAddress) + snapshot, err := registerStore.GetSnapshotAt(0) + require.NoError(t, err) + delta := storage.NewRegisterDelta(snapshot) accountStatus := environment.NewAccountStatus() - err = storageProvider.SetValue( + err = delta.SetValue( storageAddress[:], []byte(flowGo.AccountStatusKey), accountStatus.ToBytes(), ) require.NoError(t, err) - return store + batch := store.NewBatch() + defer func() { + require.NoError(t, batch.Close()) + }() + err = registerStore.Store(delta.GetUpdates(), 0, batch) + require.NoError(t, err) + err = batch.Commit(pebbleDB.Sync) + require.NoError(t, err) + + return store, registerStore } diff --git a/services/requester/requester.go b/services/requester/requester.go index 0f8fde87e..ebf54f43f 100644 --- a/services/requester/requester.go +++ b/services/requester/requester.go @@ -121,6 +121,7 @@ var _ Requester = &EVM{} type EVM struct { store *pebble.Storage + registerStore *pebble.RegisterStorage blocksProvider *replayer.BlocksProvider client *CrossSporkClient config *config.Config @@ -140,6 +141,7 @@ type EVM struct { func NewEVM( store *pebble.Storage, + registerStore *pebble.RegisterStorage, blocksProvider *replayer.BlocksProvider, client *CrossSporkClient, config *config.Config, @@ -200,6 +202,7 @@ func NewEVM( evm := &EVM{ store: store, + registerStore: registerStore, blocksProvider: blocksProvider, client: client, config: config, @@ -633,7 +636,6 @@ func (e *EVM) executeScriptAtHeight( } func (e *EVM) getBlockView(evmHeight uint64) (*query.View, error) { - ledger := pebble.NewRegister(e.store, uint64(evmHeight), nil) blocksProvider := replayer.NewBlocksProvider( e.blocks, e.config.FlowNetworkID, @@ -642,12 +644,12 @@ func (e *EVM) getBlockView(evmHeight uint64) (*query.View, error) { viewProvider := query.NewViewProvider( e.config.FlowNetworkID, evm.StorageAccountAddress(e.config.FlowNetworkID), - ledger, + e.registerStore, blocksProvider, 120_000_000, ) - return viewProvider.GetBlockView(uint64(evmHeight)) + return viewProvider.GetBlockView(evmHeight) } // cacheKey builds the cache key from the script type, height and arguments. diff --git a/storage/pebble/register.go b/storage/pebble/register.go deleted file mode 100644 index bee82bf52..000000000 --- a/storage/pebble/register.go +++ /dev/null @@ -1,172 +0,0 @@ -package pebble - -import ( - "errors" - "fmt" - "sync" - - "github.com/cockroachdb/pebble" - "github.com/onflow/atree" - - errs "github.com/onflow/flow-evm-gateway/models/errors" - - "github.com/onflow/flow-go/fvm/evm/types" -) - -var _ atree.Ledger = &Register{} -var _ types.StorageProvider = &Register{} - -type Register struct { - store *Storage - height uint64 - batch *pebble.Batch - mux sync.RWMutex -} - -// NewRegister creates a new index instance at the provided height, all reads and -// writes of the registers will happen at that height. -func NewRegister(store *Storage, height uint64, batch *pebble.Batch) *Register { - return &Register{ - store: store, - height: height, - batch: batch, - mux: sync.RWMutex{}, - } -} - -func (r *Register) GetSnapshotAt(evmBlockHeight uint64) (types.BackendStorageSnapshot, error) { - return &Register{ - store: r.store, - height: evmBlockHeight, - mux: sync.RWMutex{}, - }, nil -} - -func (r *Register) GetValue(owner, key []byte) ([]byte, error) { - r.mux.RLock() - defer r.mux.RUnlock() - - var db pebble.Reader = r.store.db - if r.batch != nil { - db = r.batch - } - - iter, err := db.NewIter(&pebble.IterOptions{ - LowerBound: r.idLower(owner, key), - UpperBound: r.idUpper(owner, key), - }) - if err != nil { - return nil, fmt.Errorf("failed to create register range iterator: %w", err) - } - defer func() { - if err := iter.Close(); err != nil { - r.store.log.Error().Err(err).Msg("failed to close register iterator") - } - }() - - found := iter.Last() - if !found { - // as per interface expectation we need to return nil if not found - return nil, nil - } - - val, err := iter.ValueAndErr() - if err != nil { - return nil, fmt.Errorf( - "failed to get ledger value at owner %x and key %x: %w", - owner, - key, - err, - ) - } - - return val, nil -} - -func (r *Register) SetValue(owner, key, value []byte) error { - r.mux.Lock() - defer r.mux.Unlock() - - id := r.id(owner, key) - if err := r.store.set(ledgerValue, id, value, r.batch); err != nil { - return fmt.Errorf( - "failed to store ledger value for owner %x and key %x: %w", - owner, - key, - err, - ) - } - - return nil -} - -func (r *Register) ValueExists(owner, key []byte) (bool, error) { - val, err := r.GetValue(owner, key) - if err != nil { - return false, err - } - - return val != nil, nil -} - -func (r *Register) AllocateSlabIndex(owner []byte) (atree.SlabIndex, error) { - r.mux.Lock() - defer r.mux.Unlock() - - var index atree.SlabIndex - - val, err := r.store.batchGet(r.batch, ledgerSlabIndex, owner) - if err != nil { - if !errors.Is(err, errs.ErrEntityNotFound) { - return atree.SlabIndexUndefined, err - } - } - - if val != nil { - if len(val) != len(index) { - return atree.SlabIndexUndefined, fmt.Errorf( - "slab index was not stored in correct format for owner %x", - owner, - ) - } - - copy(index[:], val) - } - - index = index.Next() - if err := r.store.set(ledgerSlabIndex, owner, index[:], r.batch); err != nil { - return atree.SlabIndexUndefined, fmt.Errorf( - "slab index failed to set for owner %x: %w", - owner, - err, - ) - } - - return index, nil -} - -// id calculates a ledger id with embedded block height for owner and key. -// The key for a register has the following schema: -// {owner}{key}{height} -func (r *Register) id(owner, key []byte) []byte { - id := append(owner, key...) - h := uint64Bytes(r.height) - return append(id, h...) -} - -func (r *Register) idUpper(owner, key []byte) []byte { - id := []byte{ledgerValue} - id = append(id, owner...) - id = append(id, key...) - // increase height +1 because upper bound is exclusive - h := uint64Bytes(r.height + 1) - return append(id, h...) -} - -func (r *Register) idLower(owner, key []byte) []byte { - id := []byte{ledgerValue} - id = append(id, owner...) - id = append(id, key...) - // lower height is always 0 - return append(id, uint64Bytes(0)...) -} From 69bba54a8a798e40e27cf33a2e4ad6910c043e38 Mon Sep 17 00:00:00 2001 From: Ardit Marku Date: Thu, 7 Nov 2024 13:52:48 +0200 Subject: [PATCH 024/174] Add no-op implementation for RegisterDelta.AllocateSlabIndex --- storage/register_delta.go | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/storage/register_delta.go b/storage/register_delta.go index e266d41b4..aec8d0ac0 100644 --- a/storage/register_delta.go +++ b/storage/register_delta.go @@ -69,6 +69,5 @@ func (r *RegisterDelta) GetUpdates() flow.RegisterEntries { } func (r *RegisterDelta) AllocateSlabIndex(_ []byte) (atree.SlabIndex, error) { - // TODO: If needed add later - panic("should not be called") + return atree.SlabIndex{}, nil } From a63925e0d837aa518e630310708a7bef0c4e078e Mon Sep 17 00:00:00 2001 From: Ardit Marku Date: Thu, 7 Nov 2024 14:50:55 +0200 Subject: [PATCH 025/174] Extract method for detecting whether the tracer config is equal to the call tracer used during block replay --- api/debug.go | 53 ++++++++++++++++++++++++++++++++-------------------- 1 file changed, 33 insertions(+), 20 deletions(-) diff --git a/api/debug.go b/api/debug.go index b9734fc0d..23b1947b0 100644 --- a/api/debug.go +++ b/api/debug.go @@ -80,13 +80,13 @@ func (d *DebugAPI) TraceTransaction( hash gethCommon.Hash, config *tracers.TraceConfig, ) (json.RawMessage, error) { - if config != nil { - if *config.Tracer == replayer.TracerName && - slices.Equal(config.TracerConfig, json.RawMessage(replayer.TracerConfig)) { - trace, err := d.tracer.GetTransaction(hash) - if err == nil { - return trace, nil - } + // If the given trace config is equal to the default call tracer used + // in block replay during ingestion, then we fetch the trace result + // from the Traces DB. + if isDefaultCallTracer(config) { + trace, err := d.tracer.GetTransaction(hash) + if err == nil { + return trace, nil } } @@ -161,21 +161,21 @@ func (d *DebugAPI) TraceBlockByNumber( results := make([]*txTraceResult, len(block.TransactionHashes)) - if config != nil { - if *config.Tracer == replayer.TracerName && - slices.Equal(config.TracerConfig, json.RawMessage(replayer.TracerConfig)) { - for i, hash := range block.TransactionHashes { - trace, err := d.tracer.GetTransaction(hash) - - if err != nil { - results[i] = &txTraceResult{TxHash: hash, Error: err.Error()} - } else { - results[i] = &txTraceResult{TxHash: hash, Result: trace} - } - } + // If the given trace config is equal to the default call tracer used + // in block replay during ingestion, then we fetch the trace result + // from the Traces DB. + if isDefaultCallTracer(config) { + for i, hash := range block.TransactionHashes { + trace, err := d.tracer.GetTransaction(hash) - return results, nil + if err != nil { + results[i] = &txTraceResult{TxHash: hash, Error: err.Error()} + } else { + results[i] = &txTraceResult{TxHash: hash, Result: trace} + } } + + return results, nil } // We need to re-execute all the transactions from the given block, @@ -414,3 +414,16 @@ func tracerForReceipt( return tracers.DefaultDirectory.New(*config.Tracer, tracerCtx, config.TracerConfig) } + +func isDefaultCallTracer(config *tracers.TraceConfig) bool { + if config == nil { + return false + } + + if *config.Tracer != replayer.TracerName { + return false + } + + tracerConfig := json.RawMessage(replayer.TracerConfig) + return slices.Equal(config.TracerConfig, tracerConfig) +} From 3d981d46ba5ac8709e42c23aea39a5dbfa9032c8 Mon Sep 17 00:00:00 2001 From: Ardit Marku Date: Thu, 7 Nov 2024 15:49:54 +0200 Subject: [PATCH 026/174] Remove StateDB from BlockExecutor --- services/evm/executor.go | 20 ++++++-------------- 1 file changed, 6 insertions(+), 14 deletions(-) diff --git a/services/evm/executor.go b/services/evm/executor.go index ff6900b26..abe792fd5 100644 --- a/services/evm/executor.go +++ b/services/evm/executor.go @@ -6,7 +6,6 @@ import ( "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/precompiles" "github.com/onflow/flow-go/fvm/evm/types" flowGo "github.com/onflow/flow-go/model/flow" @@ -20,13 +19,12 @@ import ( ) type BlockExecutor struct { - types.StateDB // todo change to types.ReadOnlyView - emulator types.Emulator - chainID flowGo.ChainID - block *models.Block - blocks storage.BlockIndexer - logger zerolog.Logger - receipts storage.ReceiptIndexer + emulator types.Emulator + chainID flowGo.ChainID + block *models.Block + blocks storage.BlockIndexer + logger zerolog.Logger + receipts storage.ReceiptIndexer // block dynamic data txIndex uint @@ -44,14 +42,8 @@ func NewBlockExecutor( logger = logger.With().Str("component", "state-execution").Logger() storageAddress := evm.StorageAccountAddress(chainID) - stateDB, err := state.NewStateDB(ledger, storageAddress) - if err != nil { - return nil, err - } - return &BlockExecutor{ emulator: emulator.NewEmulator(ledger, storageAddress), - StateDB: stateDB, chainID: chainID, block: block, blocks: blocks, From c3e83ab16357237bf09364d4777cc8568dc7a523 Mon Sep 17 00:00:00 2001 From: Ardit Marku Date: Thu, 7 Nov 2024 16:05:39 +0200 Subject: [PATCH 027/174] Remove out-dated comment regarding precompiles --- services/evm/executor.go | 7 +------ 1 file changed, 1 insertion(+), 6 deletions(-) diff --git a/services/evm/executor.go b/services/evm/executor.go index abe792fd5..9d12d7d6c 100644 --- a/services/evm/executor.go +++ b/services/evm/executor.go @@ -145,12 +145,7 @@ func (s *BlockExecutor) blockContext(receipt *models.Receipt) (types.BlockContex Tracer: nil, } - // only add precompile cadence arch mocks if we have a receipt, - // in case of call and dry run we don't produce receipts - // todo when a call is made that uses cadence arch precompiles, it will fail, because - // the precompiled contracts won't be set since we don't have a receipt for them - // this failure should be detected and we should in such a case execute a call against the - // EN using an AN + // only add precompile cadence arch contract if we have a receipt if receipt != nil { calls, err := types.AggregatedPrecompileCallsFromEncoded(receipt.PrecompiledCalls) if err != nil { From c2524d7eff88ac00a13c6d2f479d67110c0e9860 Mon Sep 17 00:00:00 2001 From: Ardit Marku Date: Thu, 7 Nov 2024 16:13:12 +0200 Subject: [PATCH 028/174] Move tracer assignment in blockContext method --- services/evm/executor.go | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/services/evm/executor.go b/services/evm/executor.go index 9d12d7d6c..dbc3ee7e8 100644 --- a/services/evm/executor.go +++ b/services/evm/executor.go @@ -64,8 +64,7 @@ func (s *BlockExecutor) Run( return nil, err } - ctx, err := s.blockContext(receipt) - ctx.Tracer = tracer + ctx, err := s.blockContext(receipt, tracer) if err != nil { return nil, err } @@ -108,7 +107,10 @@ func (s *BlockExecutor) Run( // blockContext produces a context that is used by the block view during the execution. // It can be used for transaction execution and calls. Receipt is not required when // producing the context for calls. -func (s *BlockExecutor) blockContext(receipt *models.Receipt) (types.BlockContext, error) { +func (s *BlockExecutor) blockContext( + receipt *models.Receipt, + tracer *tracers.Tracer, +) (types.BlockContext, error) { ctx := types.BlockContext{ ChainID: types.EVMChainIDFromFlowChainID(s.chainID), BlockNumber: s.block.Height, @@ -142,7 +144,7 @@ func (s *BlockExecutor) blockContext(receipt *models.Receipt) (types.BlockContex Random: s.block.PrevRandao, TxCountSoFar: s.txIndex, TotalGasUsedSoFar: s.gasUsed, - Tracer: nil, + Tracer: tracer, } // only add precompile cadence arch contract if we have a receipt From d7b86ee3afa8649ec8e4c3b1e9830b5385328738 Mon Sep 17 00:00:00 2001 From: Ardit Marku Date: Thu, 7 Nov 2024 16:25:11 +0200 Subject: [PATCH 029/174] Remove receipt return value from BlockExecutor.Run --- api/debug.go | 9 +++------ services/evm/executor.go | 17 ++++++++--------- 2 files changed, 11 insertions(+), 15 deletions(-) diff --git a/api/debug.go b/api/debug.go index 23b1947b0..beccee3e9 100644 --- a/api/debug.go +++ b/api/debug.go @@ -140,8 +140,7 @@ func (d *DebugAPI) TraceTransaction( txExecuted = true } - _, err = blockExecutor.Run(tx, txTracer) - if err != nil { + if err = blockExecutor.Run(tx, txTracer); err != nil { return nil, err } } @@ -206,14 +205,12 @@ func (d *DebugAPI) TraceBlockByNumber( return nil, err } - _, err = blockExecutor.Run(tx, tracer) - if err != nil { + if err = blockExecutor.Run(tx, tracer); err != nil { results[i] = &txTraceResult{TxHash: h, Error: err.Error()} continue } - txTrace, err := tracer.GetResult() - if err != nil { + if txTrace, err := tracer.GetResult(); err != nil { results[i] = &txTraceResult{TxHash: h, Error: err.Error()} } else { results[i] = &txTraceResult{TxHash: h, Result: txTrace} diff --git a/services/evm/executor.go b/services/evm/executor.go index dbc3ee7e8..7d9304de7 100644 --- a/services/evm/executor.go +++ b/services/evm/executor.go @@ -10,7 +10,6 @@ import ( "github.com/onflow/flow-go/fvm/evm/types" flowGo "github.com/onflow/flow-go/model/flow" "github.com/onflow/go-ethereum/common" - gethTypes "github.com/onflow/go-ethereum/core/types" "github.com/onflow/go-ethereum/eth/tracers" "github.com/rs/zerolog" @@ -55,23 +54,23 @@ func NewBlockExecutor( func (s *BlockExecutor) Run( tx models.Transaction, tracer *tracers.Tracer, -) (*gethTypes.Receipt, error) { +) error { l := s.logger.With().Str("tx-hash", tx.Hash().String()).Logger() l.Info().Msg("executing new transaction") receipt, err := s.receipts.GetByTransactionID(tx.Hash()) if err != nil { - return nil, err + return err } ctx, err := s.blockContext(receipt, tracer) if err != nil { - return nil, err + return err } bv, err := s.emulator.NewBlockView(ctx) if err != nil { - return nil, err + return err } var res *types.Result @@ -82,17 +81,17 @@ func (s *BlockExecutor) Run( case models.TransactionCall: res, err = bv.RunTransaction(t.Transaction) default: - return nil, fmt.Errorf("invalid transaction type") + return fmt.Errorf("invalid transaction type") } if err != nil { - return nil, err + return 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) + return fmt.Errorf("invalid transaction %s: %w", tx.Hash(), res.ValidationError) } // increment values as part of a virtual block @@ -101,7 +100,7 @@ func (s *BlockExecutor) Run( l.Debug().Msg("transaction executed successfully") - return res.LightReceipt().ToReceipt(), nil + return nil } // blockContext produces a context that is used by the block view during the execution. From 00715555075c1aadb0841fa3a8984bab5ff5b56c Mon Sep 17 00:00:00 2001 From: Ardit Marku Date: Thu, 7 Nov 2024 16:48:25 +0200 Subject: [PATCH 030/174] Check that txTracer is not nil before getting the result --- api/debug.go | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/api/debug.go b/api/debug.go index beccee3e9..bcf742250 100644 --- a/api/debug.go +++ b/api/debug.go @@ -145,7 +145,11 @@ func (d *DebugAPI) TraceTransaction( } } - return txTracer.GetResult() + if txTracer != nil { + return txTracer.GetResult() + } + + return nil, fmt.Errorf("failed to trace transaction with hash: %s", hash) } func (d *DebugAPI) TraceBlockByNumber( From b74a3ab29e7faf1d52b4a2fea57393fbeabbdae1 Mon Sep 17 00:00:00 2001 From: Janez Podhostnik Date: Thu, 7 Nov 2024 15:16:40 +0100 Subject: [PATCH 031/174] use batch to init blocks --- bootstrap/bootstrap.go | 2 +- services/ingestion/engine.go | 11 +++++++++-- services/replayer/blocks_provider_test.go | 2 +- storage/pebble/blocks.go | 8 ++++---- storage/pebble/storage_test.go | 10 +++++----- 5 files changed, 20 insertions(+), 13 deletions(-) diff --git a/bootstrap/bootstrap.go b/bootstrap/bootstrap.go index 2918cd466..33869ee42 100644 --- a/bootstrap/bootstrap.go +++ b/bootstrap/bootstrap.go @@ -519,7 +519,7 @@ func setupStorage( return nil, fmt.Errorf("could not store register updates: %w", err) } - if err := blocks.InitHeights(cadenceHeight, cadenceBlock.ID); err != nil { + if err := blocks.InitHeights(cadenceHeight, cadenceBlock.ID, batch); err != nil { return nil, fmt.Errorf( "failed to init the database for block height: %d and ID: %s, with : %w", cadenceHeight, diff --git a/services/ingestion/engine.go b/services/ingestion/engine.go index bcfb7ae3d..9cb83223a 100644 --- a/services/ingestion/engine.go +++ b/services/ingestion/engine.go @@ -168,8 +168,13 @@ func (e *Engine) processEvents(events *models.CadenceEvents) error { return nil // nothing else to do this was heartbeat event with not event payloads } - batch := e.store.NewIndexedBatch() - defer batch.Close() + batch := e.store.NewBatch() + defer func(batch *pebbleDB.Batch) { + err := batch.Close() + if err != nil { + e.log.Fatal().Err(err).Msg("failed to close batch") + } + }(batch) // Step 1: Re-execute all transactions on the latest EVM block @@ -205,6 +210,8 @@ func (e *Engine) processEvents(events *models.CadenceEvents) error { } // Step 2.2: Write the latest EVM block to `Blocks` storage + // This verifies the EVM height is sequential, and if not it will return an error + // TODO(janezp): can we do this before re-execution of the block? err = e.indexBlock( events.CadenceHeight(), events.CadenceBlockID(), diff --git a/services/replayer/blocks_provider_test.go b/services/replayer/blocks_provider_test.go index b3becf71b..4fce81292 100644 --- a/services/replayer/blocks_provider_test.go +++ b/services/replayer/blocks_provider_test.go @@ -273,7 +273,7 @@ func setupBlocksDB(t *testing.T) storage.BlockIndexer { chainID := flowGo.Emulator blocks := pebble.NewBlocks(db, chainID) - err = blocks.InitHeights(config.EmulatorInitCadenceHeight, flow.Identifier{0x1}) + err = blocks.InitHeights(config.EmulatorInitCadenceHeight, flow.Identifier{0x1}, nil) require.NoError(t, err) return blocks diff --git a/storage/pebble/blocks.go b/storage/pebble/blocks.go index e7286596e..5643ba70b 100644 --- a/storage/pebble/blocks.go +++ b/storage/pebble/blocks.go @@ -222,24 +222,24 @@ func (b *Blocks) SetLatestCadenceHeight(height uint64, batch *pebble.Batch) erro } // InitHeights sets the Cadence height to zero as well as EVM heights. Used for empty database init. -func (b *Blocks) InitHeights(cadenceHeight uint64, cadenceID flow.Identifier) error { +func (b *Blocks) InitHeights(cadenceHeight uint64, cadenceID flow.Identifier, batch *pebble.Batch) error { // sanity check, make sure we don't have any heights stored, disable overwriting the database _, err := b.LatestEVMHeight() if !errors.Is(err, errs.ErrStorageNotInitialized) { return fmt.Errorf("can't init the database that already has data stored") } - if err := b.store.set(latestCadenceHeightKey, nil, uint64Bytes(cadenceHeight), nil); err != nil { + if err := b.store.set(latestCadenceHeightKey, nil, uint64Bytes(cadenceHeight), batch); err != nil { return fmt.Errorf("failed to init latest Cadence height at: %d, with: %w", cadenceHeight, err) } - if err := b.store.set(latestEVMHeightKey, nil, uint64Bytes(0), nil); err != nil { + if err := b.store.set(latestEVMHeightKey, nil, uint64Bytes(0), batch); err != nil { return fmt.Errorf("failed to init latest EVM height at: %d, with: %w", 0, err) } // we store genesis block because it isn't emitted over the network genesisBlock := models.GenesisBlock(b.chainID) - if err := b.Store(cadenceHeight, cadenceID, genesisBlock, nil); err != nil { + if err := b.Store(cadenceHeight, cadenceID, genesisBlock, batch); err != nil { return fmt.Errorf("failed to store genesis block at Cadence height: %d, with: %w", cadenceHeight, err) } diff --git a/storage/pebble/storage_test.go b/storage/pebble/storage_test.go index 89717875c..d61a172b6 100644 --- a/storage/pebble/storage_test.go +++ b/storage/pebble/storage_test.go @@ -23,7 +23,7 @@ import ( func TestBlocks(t *testing.T) { runDB("blocks", t, func(t *testing.T, db *Storage) { bl := NewBlocks(db, flowGo.Emulator) - err := bl.InitHeights(config.EmulatorInitCadenceHeight, flow.Identifier{0x1}) + err := bl.InitHeights(config.EmulatorInitCadenceHeight, flow.Identifier{0x1}, nil) require.NoError(t, err) suite.Run(t, &storage.BlockTestSuite{Blocks: bl}) }) @@ -33,7 +33,7 @@ func TestReceipts(t *testing.T) { runDB("receipts", t, func(t *testing.T, db *Storage) { // prepare the blocks database since they track heights which are used in receipts as well bl := NewBlocks(db, flowGo.Emulator) - err := bl.InitHeights(config.EmulatorInitCadenceHeight, flow.Identifier{0x1}) + err := bl.InitHeights(config.EmulatorInitCadenceHeight, flow.Identifier{0x1}, nil) require.NoError(t, err) err = bl.Store(30, flow.Identifier{0x1}, mocks.NewBlock(10), nil) // update first and latest height require.NoError(t, err) @@ -67,7 +67,7 @@ func TestBlock(t *testing.T) { runDB("store block", t, func(t *testing.T, db *Storage) { bl := mocks.NewBlock(10) blocks := NewBlocks(db, flowGo.Emulator) - err := blocks.InitHeights(config.EmulatorInitCadenceHeight, flow.Identifier{0x1}) + err := blocks.InitHeights(config.EmulatorInitCadenceHeight, flow.Identifier{0x1}, nil) require.NoError(t, err) err = blocks.Store(20, flow.Identifier{0x1}, bl, nil) @@ -81,7 +81,7 @@ func TestBlock(t *testing.T) { bl := mocks.NewBlock(height) blocks := NewBlocks(db, flowGo.Emulator) - err := blocks.InitHeights(config.EmulatorInitCadenceHeight, flow.Identifier{0x1}) + err := blocks.InitHeights(config.EmulatorInitCadenceHeight, flow.Identifier{0x1}, nil) require.NoError(t, err) err = blocks.Store(cadenceHeight, cadenceID, bl, nil) @@ -109,7 +109,7 @@ func TestBlock(t *testing.T) { runDB("get not found block error", t, func(t *testing.T, db *Storage) { blocks := NewBlocks(db, flowGo.Emulator) - err := blocks.InitHeights(config.EmulatorInitCadenceHeight, flow.Identifier{0x1}) + err := blocks.InitHeights(config.EmulatorInitCadenceHeight, flow.Identifier{0x1}, nil) require.NoError(t, err) _ = blocks.Store(2, flow.Identifier{0x1}, mocks.NewBlock(1), nil) // init From e068017b1ebd191d5a0faa18f0ae2a4fed727090 Mon Sep 17 00:00:00 2001 From: Janez Podhostnik Date: Thu, 7 Nov 2024 15:54:54 +0100 Subject: [PATCH 032/174] fix db init issue in tests --- services/ingestion/engine.go | 3 +- services/replayer/blocks_provider_test.go | 8 +++- storage/pebble/storage_test.go | 46 ++++++++++++++++++----- 3 files changed, 45 insertions(+), 12 deletions(-) diff --git a/services/ingestion/engine.go b/services/ingestion/engine.go index 9cb83223a..40691afcd 100644 --- a/services/ingestion/engine.go +++ b/services/ingestion/engine.go @@ -168,7 +168,8 @@ func (e *Engine) processEvents(events *models.CadenceEvents) error { return nil // nothing else to do this was heartbeat event with not event payloads } - batch := e.store.NewBatch() + // TODO(JanezP): accounts need an indexed batch. Investigate why and try to switch to non-indexed batch + batch := e.store.NewIndexedBatch() defer func(batch *pebbleDB.Batch) { err := batch.Close() if err != nil { diff --git a/services/replayer/blocks_provider_test.go b/services/replayer/blocks_provider_test.go index 4fce81292..57eeb0f1f 100644 --- a/services/replayer/blocks_provider_test.go +++ b/services/replayer/blocks_provider_test.go @@ -3,6 +3,8 @@ package replayer import ( "testing" + pebble2 "github.com/cockroachdb/pebble" + "github.com/goccy/go-json" "github.com/onflow/flow-evm-gateway/config" "github.com/onflow/flow-evm-gateway/models" @@ -269,11 +271,15 @@ func setupBlocksDB(t *testing.T) storage.BlockIndexer { dir := t.TempDir() db, err := pebble.New(dir, zerolog.Nop()) require.NoError(t, err) + batch := db.NewBatch() chainID := flowGo.Emulator blocks := pebble.NewBlocks(db, chainID) - err = blocks.InitHeights(config.EmulatorInitCadenceHeight, flow.Identifier{0x1}, nil) + err = blocks.InitHeights(config.EmulatorInitCadenceHeight, flow.Identifier{0x1}, batch) + require.NoError(t, err) + + err = batch.Commit(pebble2.Sync) require.NoError(t, err) return blocks diff --git a/storage/pebble/storage_test.go b/storage/pebble/storage_test.go index d61a172b6..9c3ebbe3d 100644 --- a/storage/pebble/storage_test.go +++ b/storage/pebble/storage_test.go @@ -23,8 +23,14 @@ import ( func TestBlocks(t *testing.T) { runDB("blocks", t, func(t *testing.T, db *Storage) { bl := NewBlocks(db, flowGo.Emulator) - err := bl.InitHeights(config.EmulatorInitCadenceHeight, flow.Identifier{0x1}, nil) + batch := db.NewBatch() + + err := bl.InitHeights(config.EmulatorInitCadenceHeight, flow.Identifier{0x1}, batch) + require.NoError(t, err) + + err = batch.Commit(pebble.Sync) require.NoError(t, err) + suite.Run(t, &storage.BlockTestSuite{Blocks: bl}) }) } @@ -33,11 +39,16 @@ func TestReceipts(t *testing.T) { runDB("receipts", t, func(t *testing.T, db *Storage) { // prepare the blocks database since they track heights which are used in receipts as well bl := NewBlocks(db, flowGo.Emulator) - err := bl.InitHeights(config.EmulatorInitCadenceHeight, flow.Identifier{0x1}, nil) + batch := db.NewBatch() + + err := bl.InitHeights(config.EmulatorInitCadenceHeight, flow.Identifier{0x1}, batch) + require.NoError(t, err) + err = bl.Store(30, flow.Identifier{0x1}, mocks.NewBlock(10), batch) // update first and latest height require.NoError(t, err) - err = bl.Store(30, flow.Identifier{0x1}, mocks.NewBlock(10), nil) // update first and latest height + err = bl.Store(30, flow.Identifier{0x1}, mocks.NewBlock(300), batch) // update latest require.NoError(t, err) - err = bl.Store(30, flow.Identifier{0x1}, mocks.NewBlock(300), nil) // update latest + + err = batch.Commit(pebble.Sync) require.NoError(t, err) suite.Run(t, &storage.ReceiptTestSuite{ReceiptIndexer: NewReceipts(db)}) @@ -67,10 +78,15 @@ func TestBlock(t *testing.T) { runDB("store block", t, func(t *testing.T, db *Storage) { bl := mocks.NewBlock(10) blocks := NewBlocks(db, flowGo.Emulator) - err := blocks.InitHeights(config.EmulatorInitCadenceHeight, flow.Identifier{0x1}, nil) + batch := db.NewBatch() + + err := blocks.InitHeights(config.EmulatorInitCadenceHeight, flow.Identifier{0x1}, batch) require.NoError(t, err) - err = blocks.Store(20, flow.Identifier{0x1}, bl, nil) + err = blocks.Store(20, flow.Identifier{0x1}, bl, batch) + require.NoError(t, err) + + err = batch.Commit(pebble.Sync) require.NoError(t, err) }) @@ -81,10 +97,14 @@ func TestBlock(t *testing.T) { bl := mocks.NewBlock(height) blocks := NewBlocks(db, flowGo.Emulator) - err := blocks.InitHeights(config.EmulatorInitCadenceHeight, flow.Identifier{0x1}, nil) + batch := db.NewBatch() + err := blocks.InitHeights(config.EmulatorInitCadenceHeight, flow.Identifier{0x1}, batch) + require.NoError(t, err) + + err = blocks.Store(cadenceHeight, cadenceID, bl, batch) require.NoError(t, err) - err = blocks.Store(cadenceHeight, cadenceID, bl, nil) + err = batch.Commit(pebble.Sync) require.NoError(t, err) block, err := blocks.GetByHeight(height) @@ -109,9 +129,15 @@ func TestBlock(t *testing.T) { runDB("get not found block error", t, func(t *testing.T, db *Storage) { blocks := NewBlocks(db, flowGo.Emulator) - err := blocks.InitHeights(config.EmulatorInitCadenceHeight, flow.Identifier{0x1}, nil) + + batch := db.NewBatch() + err := blocks.InitHeights(config.EmulatorInitCadenceHeight, flow.Identifier{0x1}, batch) + require.NoError(t, err) + err = blocks.Store(2, flow.Identifier{0x1}, mocks.NewBlock(1), batch) // init + require.NoError(t, err) + + err = batch.Commit(pebble.Sync) require.NoError(t, err) - _ = blocks.Store(2, flow.Identifier{0x1}, mocks.NewBlock(1), nil) // init bl, err := blocks.GetByHeight(11) require.ErrorIs(t, err, errors.ErrEntityNotFound) From 20f3c2b5e0e25af8995c89ff3a4702a8170a0313 Mon Sep 17 00:00:00 2001 From: Ardit Marku Date: Thu, 7 Nov 2024 18:18:27 +0200 Subject: [PATCH 033/174] Simplify condition in TraceBlockByNumber --- api/debug.go | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/api/debug.go b/api/debug.go index bcf742250..64f73674e 100644 --- a/api/debug.go +++ b/api/debug.go @@ -211,10 +211,7 @@ func (d *DebugAPI) TraceBlockByNumber( if err = blockExecutor.Run(tx, tracer); err != nil { results[i] = &txTraceResult{TxHash: h, Error: err.Error()} - continue - } - - if txTrace, err := tracer.GetResult(); err != nil { + } else if txTrace, err := tracer.GetResult(); err != nil { results[i] = &txTraceResult{TxHash: h, Error: err.Error()} } else { results[i] = &txTraceResult{TxHash: h, Result: txTrace} From 5f1f05f9e1d3266408be33aa1de84984df9bbed0 Mon Sep 17 00:00:00 2001 From: Ardit Marku Date: Fri, 8 Nov 2024 14:36:29 +0200 Subject: [PATCH 034/174] Extract common methods for resolving RPC block tags into a utils file --- api/api.go | 123 ++++-------------------------------------------- api/debug.go | 38 +-------------- api/utils.go | 129 +++++++++++++++++++++++++++++++++++++++++++++++++++ 3 files changed, 138 insertions(+), 152 deletions(-) create mode 100644 api/utils.go diff --git a/api/api.go b/api/api.go index fb0a313d0..66709819e 100644 --- a/api/api.go +++ b/api/api.go @@ -3,11 +3,8 @@ package api import ( "context" _ "embed" - "encoding/hex" - "errors" "fmt" "math/big" - "strings" "github.com/onflow/go-ethereum/common" "github.com/onflow/go-ethereum/common/hexutil" @@ -76,6 +73,7 @@ var validMethods = map[string]struct{}{ "debug_traceTransaction": {}, "debug_traceBlockByNumber": {}, "debug_traceBlockByHash": {}, + "debug_traceCall": {}, // web3 namespace "web3_clientVersion": {}, @@ -282,7 +280,7 @@ func (b *BlockChainAPI) GetBalance( return nil, err } - evmHeight, err := b.getBlockNumber(&blockNumberOrHash) + evmHeight, err := resolveBlockTag(&blockNumberOrHash, b.blocks, b.logger) if err != nil { return handleError[*hexutil.Big](err, l, b.collector) } @@ -518,7 +516,7 @@ func (b *BlockChainAPI) GetBlockReceipts( return nil, err } - evmHeight, err := b.getBlockNumber(&blockNumberOrHash) + evmHeight, err := resolveBlockTag(&blockNumberOrHash, b.blocks, b.logger) if err != nil { return handleError[[]map[string]interface{}](err, l, b.collector) } @@ -635,7 +633,7 @@ func (b *BlockChainAPI) Call( blockNumberOrHash = &latestBlockNumberOrHash } - evmHeight, err := b.getBlockNumber(blockNumberOrHash) + evmHeight, err := resolveBlockTag(blockNumberOrHash, b.blocks, b.logger) if err != nil { return handleError[hexutil.Bytes](err, l, b.collector) } @@ -753,7 +751,7 @@ func (b *BlockChainAPI) GetTransactionCount( return nil, err } - evmHeight, err := b.getBlockNumber(&blockNumberOrHash) + evmHeight, err := resolveBlockTag(&blockNumberOrHash, b.blocks, b.logger) if err != nil { return handleError[*hexutil.Uint64](err, l, b.collector) } @@ -819,7 +817,7 @@ func (b *BlockChainAPI) EstimateGas( blockNumberOrHash = &latestBlockNumberOrHash } - evmHeight, err := b.getBlockNumber(blockNumberOrHash) + evmHeight, err := resolveBlockTag(blockNumberOrHash, b.blocks, b.logger) if err != nil { return handleError[hexutil.Uint64](err, l, b.collector) } @@ -848,7 +846,7 @@ func (b *BlockChainAPI) GetCode( return nil, err } - evmHeight, err := b.getBlockNumber(&blockNumberOrHash) + evmHeight, err := resolveBlockTag(&blockNumberOrHash, b.blocks, b.logger) if err != nil { return handleError[hexutil.Bytes](err, l, b.collector) } @@ -973,7 +971,7 @@ func (b *BlockChainAPI) GetStorageAt( ) } - evmHeight, err := b.getBlockNumber(&blockNumberOrHash) + evmHeight, err := resolveBlockTag(&blockNumberOrHash, b.blocks, b.logger) if err != nil { return handleError[hexutil.Bytes](err, l, b.collector) } @@ -1088,111 +1086,6 @@ func (b *BlockChainAPI) prepareBlockResponse( return blockResponse, nil } -func (b *BlockChainAPI) getBlockNumber(blockNumberOrHash *rpc.BlockNumberOrHash) (uint64, error) { - err := fmt.Errorf("%w: neither block number nor hash specified", errs.ErrInvalid) - if blockNumberOrHash == nil { - return 0, err - } - if number, ok := blockNumberOrHash.Number(); ok { - height, err := resolveBlockNumber(number, b.blocks) - if err != nil { - b.logger.Error().Err(err). - Stringer("block_number", number). - Msg("failed to resolve block by hash") - return 0, err - } - return height, nil - } - - if hash, ok := blockNumberOrHash.Hash(); ok { - evmHeight, err := b.blocks.GetHeightByID(hash) - if err != nil { - b.logger.Error().Err(err). - Stringer("block_hash", hash). - Msg("failed to get block by hash") - return 0, err - } - return evmHeight, nil - } - - return 0, err -} - -func resolveBlockNumber( - number rpc.BlockNumber, - blocksDB storage.BlockIndexer, -) (uint64, error) { - height := number.Int64() - - // if special values (latest) we return latest executed height - // - // all the special values are: - // SafeBlockNumber = BlockNumber(-4) - // FinalizedBlockNumber = BlockNumber(-3) - // LatestBlockNumber = BlockNumber(-2) - // PendingBlockNumber = BlockNumber(-1) - // - // EVM on Flow does not have these concepts, but the latest block is the closest fit - if height < 0 { - executed, err := blocksDB.LatestEVMHeight() - if err != nil { - return 0, err - } - height = int64(executed) - } - - return uint64(height), nil -} - -// handleError takes in an error and in case the error is of type ErrEntityNotFound -// it returns nil instead of an error since that is according to the API spec, -// if the error is not of type ErrEntityNotFound it will return the error and the generic -// empty type. -func handleError[T any](err error, log zerolog.Logger, collector metrics.Collector) (T, error) { - var ( - zero T - revertedErr *errs.RevertError - ) - - switch { - // as per specification returning nil and nil for not found resources - case errors.Is(err, errs.ErrEntityNotFound): - return zero, nil - case errors.Is(err, errs.ErrInvalid): - return zero, err - case errors.Is(err, errs.ErrFailedTransaction): - return zero, err - case errors.As(err, &revertedErr): - return zero, revertedErr - default: - collector.ApiErrorOccurred() - log.Error().Err(err).Msg("api error") - return zero, errs.ErrInternal - } -} - -// decodeHash parses a hex-encoded 32-byte hash. The input may optionally -// be prefixed by 0x and can have a byte length up to 32. -func decodeHash(s string) (h common.Hash, inputLength int, err error) { - if strings.HasPrefix(s, "0x") || strings.HasPrefix(s, "0X") { - s = s[2:] - } - if (len(s) & 1) > 0 { - s = "0" + s - } - b, err := hex.DecodeString(s) - if err != nil { - return common.Hash{}, 0, fmt.Errorf("invalid hex string: %s", s) - } - if len(b) > common.HashLength { - return common.Hash{}, len(b), fmt.Errorf( - "hex string too long, want at most 32 bytes, have %d bytes", - len(b), - ) - } - return common.BytesToHash(b), len(b), nil -} - /* Static responses section diff --git a/api/debug.go b/api/debug.go index 64f73674e..1ad5b9b41 100644 --- a/api/debug.go +++ b/api/debug.go @@ -17,7 +17,6 @@ import ( "github.com/onflow/flow-evm-gateway/config" "github.com/onflow/flow-evm-gateway/metrics" "github.com/onflow/flow-evm-gateway/models" - errs "github.com/onflow/flow-evm-gateway/models/errors" "github.com/onflow/flow-evm-gateway/services/evm" "github.com/onflow/flow-evm-gateway/services/replayer" "github.com/onflow/flow-evm-gateway/storage" @@ -261,7 +260,7 @@ func (d *DebugAPI) TraceCall( return nil, err } - height, err := d.resolveBlockNumberOrHash(&blockNrOrHash) + height, err := resolveBlockTag(&blockNrOrHash, d.blocks, d.logger) if err != nil { return nil, err } @@ -347,41 +346,6 @@ func (d *DebugAPI) executorAtBlock(block *models.Block) (*evm.BlockExecutor, err ) } -func (d *DebugAPI) resolveBlockNumberOrHash(block *rpc.BlockNumberOrHash) (uint64, error) { - err := fmt.Errorf("%w: neither block number nor hash specified", errs.ErrInvalid) - if block == nil { - return 0, err - } - if number, ok := block.Number(); ok { - return d.resolveBlockNumber(number) - } - - if hash, ok := block.Hash(); ok { - evmHeight, err := d.blocks.GetHeightByID(hash) - if err != nil { - return 0, err - } - return evmHeight, nil - } - - return 0, err -} - -func (d *DebugAPI) resolveBlockNumber(number rpc.BlockNumber) (uint64, error) { - height := number.Int64() - - // if special values (latest) we return latest executed height - if height < 0 { - executed, err := d.blocks.LatestEVMHeight() - if err != nil { - return 0, err - } - height = int64(executed) - } - - return uint64(height), nil -} - func tracerForReceipt( config *tracers.TraceConfig, receipt *models.Receipt, diff --git a/api/utils.go b/api/utils.go new file mode 100644 index 000000000..3ca157cce --- /dev/null +++ b/api/utils.go @@ -0,0 +1,129 @@ +package api + +import ( + "encoding/hex" + "errors" + "fmt" + "strings" + + "github.com/onflow/flow-evm-gateway/metrics" + errs "github.com/onflow/flow-evm-gateway/models/errors" + "github.com/onflow/flow-evm-gateway/storage" + "github.com/onflow/go-ethereum/common" + "github.com/onflow/go-ethereum/rpc" + "github.com/rs/zerolog" +) + +func resolveBlockTag( + blockNumberOrHash *rpc.BlockNumberOrHash, + blocksDB storage.BlockIndexer, + logger zerolog.Logger, +) (uint64, error) { + if blockNumberOrHash == nil { + return 0, fmt.Errorf( + "%w: neither block number nor hash specified", + errs.ErrInvalid, + ) + } + if number, ok := blockNumberOrHash.Number(); ok { + height, err := resolveBlockNumber(number, blocksDB) + if err != nil { + logger.Error().Err(err). + Stringer("block_number", number). + Msg("failed to resolve block by hash") + return 0, err + } + return height, nil + } + + if hash, ok := blockNumberOrHash.Hash(); ok { + evmHeight, err := blocksDB.GetHeightByID(hash) + if err != nil { + logger.Error().Err(err). + Stringer("block_hash", hash). + Msg("failed to get block by hash") + return 0, err + } + return evmHeight, nil + } + + return 0, fmt.Errorf( + "%w: neither block number nor hash specified", + errs.ErrInvalid, + ) +} + +func resolveBlockNumber( + number rpc.BlockNumber, + blocksDB storage.BlockIndexer, +) (uint64, error) { + height := number.Int64() + + // if special values (latest) we return latest executed height + // + // all the special values are: + // SafeBlockNumber = BlockNumber(-4) + // FinalizedBlockNumber = BlockNumber(-3) + // LatestBlockNumber = BlockNumber(-2) + // PendingBlockNumber = BlockNumber(-1) + // + // EVM on Flow does not have these concepts, but the latest block is the closest fit + if height < 0 { + executed, err := blocksDB.LatestEVMHeight() + if err != nil { + return 0, err + } + height = int64(executed) + } + + return uint64(height), nil +} + +// decodeHash parses a hex-encoded 32-byte hash. The input may optionally +// be prefixed by 0x and can have a byte length up to 32. +func decodeHash(s string) (h common.Hash, inputLength int, err error) { + if strings.HasPrefix(s, "0x") || strings.HasPrefix(s, "0X") { + s = s[2:] + } + if (len(s) & 1) > 0 { + s = "0" + s + } + b, err := hex.DecodeString(s) + if err != nil { + return common.Hash{}, 0, fmt.Errorf("invalid hex string: %s", s) + } + if len(b) > common.HashLength { + return common.Hash{}, len(b), fmt.Errorf( + "hex string too long, want at most 32 bytes, have %d bytes", + len(b), + ) + } + return common.BytesToHash(b), len(b), nil +} + +// handleError takes in an error and in case the error is of type ErrEntityNotFound +// it returns nil instead of an error since that is according to the API spec, +// if the error is not of type ErrEntityNotFound it will return the error and the generic +// empty type. +func handleError[T any](err error, log zerolog.Logger, collector metrics.Collector) (T, error) { + var ( + zero T + revertedErr *errs.RevertError + ) + + switch { + // as per specification returning nil and nil for not found resources + case errors.Is(err, errs.ErrEntityNotFound): + return zero, nil + case errors.Is(err, errs.ErrInvalid): + return zero, err + case errors.Is(err, errs.ErrFailedTransaction): + return zero, err + case errors.As(err, &revertedErr): + return zero, revertedErr + default: + collector.ApiErrorOccurred() + log.Error().Err(err).Msg("api error") + return zero, errs.ErrInternal + } +} From 7ca9956b03f93c138043a8b335dde9d77e805a1b Mon Sep 17 00:00:00 2001 From: Ardit Marku Date: Fri, 8 Nov 2024 14:45:48 +0200 Subject: [PATCH 035/174] Move down creation of tracers.Context object --- api/debug.go | 20 ++++++++++---------- 1 file changed, 10 insertions(+), 10 deletions(-) diff --git a/api/debug.go b/api/debug.go index 1ad5b9b41..7e71ec362 100644 --- a/api/debug.go +++ b/api/debug.go @@ -350,16 +350,6 @@ func tracerForReceipt( config *tracers.TraceConfig, receipt *models.Receipt, ) (*tracers.Tracer, error) { - tracerCtx := &tracers.Context{} - if receipt != nil { - tracerCtx = &tracers.Context{ - BlockHash: receipt.BlockHash, - BlockNumber: receipt.BlockNumber, - TxIndex: int(receipt.TransactionIndex), - TxHash: receipt.TxHash, - } - } - if config == nil { config = &tracers.TraceConfig{} } @@ -374,6 +364,16 @@ func tracerForReceipt( }, nil } + tracerCtx := &tracers.Context{} + if receipt != nil { + tracerCtx = &tracers.Context{ + BlockHash: receipt.BlockHash, + BlockNumber: receipt.BlockNumber, + TxIndex: int(receipt.TransactionIndex), + TxHash: receipt.TxHash, + } + } + return tracers.DefaultDirectory.New(*config.Tracer, tracerCtx, config.TracerConfig) } From e5040929ce2dbdb808fc715b3bfac4d853b4b26b Mon Sep 17 00:00:00 2001 From: Ardit Marku Date: Fri, 8 Nov 2024 15:00:16 +0200 Subject: [PATCH 036/174] Move caching of returned data from precompiled call to runCall method --- services/requester/remote_cadence_arch.go | 27 +++++++++-------------- 1 file changed, 11 insertions(+), 16 deletions(-) diff --git a/services/requester/remote_cadence_arch.go b/services/requester/remote_cadence_arch.go index da4287d3a..33de0db87 100644 --- a/services/requester/remote_cadence_arch.go +++ b/services/requester/remote_cadence_arch.go @@ -52,25 +52,21 @@ func (rca *RemoteCadenceArch) RequiredGas(input []byte) uint64 { return 0 } - key := hex.EncodeToString(crypto.Keccak256(input)) - rca.cachedCalls[key] = evmResult.ReturnedData - return evmResult.GasConsumed } func (rca *RemoteCadenceArch) Run(input []byte) ([]byte, error) { key := hex.EncodeToString(crypto.Keccak256(input)) - result, ok := rca.cachedCalls[key] - if !ok { - evmResult, err := rca.runCall(input) - if err != nil { - return nil, err - } - return evmResult.ReturnedData, nil + if result, ok := rca.cachedCalls[key]; ok { + return result, nil } - return result, nil + evmResult, err := rca.runCall(input) + if err != nil { + return nil, err + } + return evmResult.ReturnedData, nil } func (rca *RemoteCadenceArch) replaceAddresses(script []byte) []byte { @@ -110,7 +106,7 @@ func (rca *RemoteCadenceArch) runCall(input []byte) (*evmTypes.ResultSummary, er return nil, err } - hexEncodedAddress, err := addressToCadenceString(evmTypes.CoinbaseAddress.ToCommon()) + hexEncodedAddress, err := cadence.NewString(evmTypes.CoinbaseAddress.String()) if err != nil { return nil, err } @@ -130,11 +126,10 @@ func (rca *RemoteCadenceArch) runCall(input []byte) (*evmTypes.ResultSummary, er return nil, err } - return evmResult, nil -} + key := hex.EncodeToString(crypto.Keccak256(input)) + rca.cachedCalls[key] = evmResult.ReturnedData -func addressToCadenceString(address gethCommon.Address) (cadence.String, error) { - return cadence.NewString(strings.TrimPrefix(address.Hex(), "0x")) + return evmResult, nil } func parseResult(res cadence.Value) (*evmTypes.ResultSummary, error) { From 3c8ed31b888b3a68cf71758ad0e976c6facc5938 Mon Sep 17 00:00:00 2001 From: Ardit Marku Date: Fri, 8 Nov 2024 15:24:56 +0200 Subject: [PATCH 037/174] Change evm height in requester to uint64 --- api/api.go | 12 +++++----- services/requester/requester.go | 41 ++++++++++++++++----------------- 2 files changed, 26 insertions(+), 27 deletions(-) diff --git a/api/api.go b/api/api.go index 66709819e..d90f310e4 100644 --- a/api/api.go +++ b/api/api.go @@ -285,7 +285,7 @@ func (b *BlockChainAPI) GetBalance( return handleError[*hexutil.Big](err, l, b.collector) } - balance, err := b.evm.GetBalance(ctx, address, int64(evmHeight)) + balance, err := b.evm.GetBalance(ctx, address, evmHeight) if err != nil { return handleError[*hexutil.Big](err, l, b.collector) } @@ -649,7 +649,7 @@ func (b *BlockChainAPI) Call( from = *args.From } - res, err := b.evm.Call(ctx, tx, from, int64(evmHeight)) + res, err := b.evm.Call(ctx, tx, from, evmHeight) if err != nil { return handleError[hexutil.Bytes](err, l, b.collector) } @@ -756,7 +756,7 @@ func (b *BlockChainAPI) GetTransactionCount( return handleError[*hexutil.Uint64](err, l, b.collector) } - networkNonce, err := b.evm.GetNonce(ctx, address, int64(evmHeight)) + networkNonce, err := b.evm.GetNonce(ctx, address, evmHeight) if err != nil { return handleError[*hexutil.Uint64](err, l, b.collector) } @@ -822,7 +822,7 @@ func (b *BlockChainAPI) EstimateGas( return handleError[hexutil.Uint64](err, l, b.collector) } - estimatedGas, err := b.evm.EstimateGas(ctx, tx, from, int64(evmHeight)) + estimatedGas, err := b.evm.EstimateGas(ctx, tx, from, evmHeight) if err != nil { return handleError[hexutil.Uint64](err, l, b.collector) } @@ -851,7 +851,7 @@ func (b *BlockChainAPI) GetCode( return handleError[hexutil.Bytes](err, l, b.collector) } - code, err := b.evm.GetCode(ctx, address, int64(evmHeight)) + code, err := b.evm.GetCode(ctx, address, evmHeight) if err != nil { return handleError[hexutil.Bytes](err, l, b.collector) } @@ -976,7 +976,7 @@ func (b *BlockChainAPI) GetStorageAt( return handleError[hexutil.Bytes](err, l, b.collector) } - result, err := b.evm.GetStorageAt(ctx, address, key, int64(evmHeight)) + result, err := b.evm.GetStorageAt(ctx, address, key, evmHeight) if err != nil { return handleError[hexutil.Bytes](err, l, b.collector) } diff --git a/services/requester/requester.go b/services/requester/requester.go index ebf54f43f..efa61c284 100644 --- a/services/requester/requester.go +++ b/services/requester/requester.go @@ -83,7 +83,6 @@ const coaFundingBalance = minFlowBalance - 1 const LatestBlockHeight uint64 = math.MaxUint64 - 1 -// TODO(janezp): Requester does need to know about special EVM block heights. evmHeight should be uint64. type Requester interface { // SendRawTransaction will submit signed transaction data to the network. // The submitted EVM transaction hash is returned. @@ -91,30 +90,30 @@ type Requester interface { // GetBalance returns the amount of wei for the given address in the state of the // given EVM block height. - GetBalance(ctx context.Context, address common.Address, evmHeight int64) (*big.Int, error) + GetBalance(ctx context.Context, address common.Address, evmHeight uint64) (*big.Int, error) // Call executes the given signed transaction data on the state for the given EVM block height. // Note, this function doesn't make and changes in the state/blockchain and is // useful to execute and retrieve values. - Call(ctx context.Context, tx *types.LegacyTx, from common.Address, evmHeight int64) ([]byte, error) + Call(ctx context.Context, tx *types.LegacyTx, from common.Address, evmHeight uint64) ([]byte, error) // EstimateGas executes the given signed transaction data on the state for the given EVM block height. // Note, this function doesn't make any changes in the state/blockchain and is // useful to executed and retrieve the gas consumption and possible failures. - EstimateGas(ctx context.Context, tx *types.LegacyTx, from common.Address, evmHeight int64) (uint64, error) + EstimateGas(ctx context.Context, tx *types.LegacyTx, from common.Address, evmHeight uint64) (uint64, error) // GetNonce gets nonce from the network at the given EVM block height. - GetNonce(ctx context.Context, address common.Address, evmHeight int64) (uint64, error) + GetNonce(ctx context.Context, address common.Address, evmHeight uint64) (uint64, error) // GetCode returns the code stored at the given address in // the state for the given EVM block height. - GetCode(ctx context.Context, address common.Address, evmHeight int64) ([]byte, error) + GetCode(ctx context.Context, address common.Address, evmHeight uint64) ([]byte, error) // GetLatestEVMHeight returns the latest EVM height of the network. GetLatestEVMHeight(ctx context.Context) (uint64, error) // GetStorageAt returns the storage from the state at the given address, key and block number. - GetStorageAt(ctx context.Context, address common.Address, hash common.Hash, evmHeight int64) (common.Hash, error) + GetStorageAt(ctx context.Context, address common.Address, hash common.Hash, evmHeight uint64) (common.Hash, error) } var _ Requester = &EVM{} @@ -348,9 +347,9 @@ func (e *EVM) buildTransaction(ctx context.Context, script []byte, args ...caden func (e *EVM) GetBalance( ctx context.Context, address common.Address, - evmHeight int64, + evmHeight uint64, ) (*big.Int, error) { - view, err := e.getBlockView(uint64(evmHeight)) + view, err := e.getBlockView(evmHeight) if err != nil { return nil, err } @@ -361,9 +360,9 @@ func (e *EVM) GetBalance( func (e *EVM) GetNonce( ctx context.Context, address common.Address, - evmHeight int64, + evmHeight uint64, ) (uint64, error) { - view, err := e.getBlockView(uint64(evmHeight)) + view, err := e.getBlockView(evmHeight) if err != nil { return 0, err } @@ -375,9 +374,9 @@ func (e *EVM) GetStorageAt( ctx context.Context, address common.Address, hash common.Hash, - evmHeight int64, + evmHeight uint64, ) (common.Hash, error) { - view, err := e.getBlockView(uint64(evmHeight)) + view, err := e.getBlockView(evmHeight) if err != nil { return common.Hash{}, err } @@ -389,9 +388,9 @@ func (e *EVM) Call( ctx context.Context, tx *types.LegacyTx, from common.Address, - evmHeight int64, + evmHeight uint64, ) ([]byte, error) { - view, err := e.getBlockView(uint64(evmHeight)) + view, err := e.getBlockView(evmHeight) if err != nil { return nil, err } @@ -429,9 +428,9 @@ func (e *EVM) EstimateGas( ctx context.Context, tx *types.LegacyTx, from common.Address, - evmHeight int64, + evmHeight uint64, ) (uint64, error) { - view, err := e.getBlockView(uint64(evmHeight)) + view, err := e.getBlockView(evmHeight) if err != nil { return 0, err } @@ -492,9 +491,9 @@ func (e *EVM) EstimateGas( func (e *EVM) GetCode( ctx context.Context, address common.Address, - evmHeight int64, + evmHeight uint64, ) ([]byte, error) { - view, err := e.getBlockView(uint64(evmHeight)) + view, err := e.getBlockView(evmHeight) if err != nil { return nil, err } @@ -683,8 +682,8 @@ func AddOne64th(n uint64) uint64 { return n + (n / 64) } -func (e *EVM) evmToCadenceHeight(height int64) (uint64, error) { - cadenceHeight, err := e.blocks.GetCadenceHeight(uint64(height)) +func (e *EVM) evmToCadenceHeight(height uint64) (uint64, error) { + cadenceHeight, err := e.blocks.GetCadenceHeight(height) if err != nil { return 0, fmt.Errorf( "failed to map evm height: %d to cadence height: %w", From 11e4b99f9f40981a5638447d5bd16340ec8c478a Mon Sep 17 00:00:00 2001 From: Ardit Marku Date: Mon, 11 Nov 2024 10:13:29 +0200 Subject: [PATCH 038/174] Simplify Requester API interface --- api/api.go | 14 +++++----- api/utils.go | 8 +++--- services/requester/requester.go | 48 +++++++++++++++------------------ 3 files changed, 32 insertions(+), 38 deletions(-) diff --git a/api/api.go b/api/api.go index d90f310e4..6186d62d3 100644 --- a/api/api.go +++ b/api/api.go @@ -285,7 +285,7 @@ func (b *BlockChainAPI) GetBalance( return handleError[*hexutil.Big](err, l, b.collector) } - balance, err := b.evm.GetBalance(ctx, address, evmHeight) + balance, err := b.evm.GetBalance(address, evmHeight) if err != nil { return handleError[*hexutil.Big](err, l, b.collector) } @@ -649,7 +649,7 @@ func (b *BlockChainAPI) Call( from = *args.From } - res, err := b.evm.Call(ctx, tx, from, evmHeight) + res, err := b.evm.Call(tx, from, evmHeight) if err != nil { return handleError[hexutil.Bytes](err, l, b.collector) } @@ -756,7 +756,7 @@ func (b *BlockChainAPI) GetTransactionCount( return handleError[*hexutil.Uint64](err, l, b.collector) } - networkNonce, err := b.evm.GetNonce(ctx, address, evmHeight) + networkNonce, err := b.evm.GetNonce(address, evmHeight) if err != nil { return handleError[*hexutil.Uint64](err, l, b.collector) } @@ -822,7 +822,7 @@ func (b *BlockChainAPI) EstimateGas( return handleError[hexutil.Uint64](err, l, b.collector) } - estimatedGas, err := b.evm.EstimateGas(ctx, tx, from, evmHeight) + estimatedGas, err := b.evm.EstimateGas(tx, from, evmHeight) if err != nil { return handleError[hexutil.Uint64](err, l, b.collector) } @@ -851,7 +851,7 @@ func (b *BlockChainAPI) GetCode( return handleError[hexutil.Bytes](err, l, b.collector) } - code, err := b.evm.GetCode(ctx, address, evmHeight) + code, err := b.evm.GetCode(address, evmHeight) if err != nil { return handleError[hexutil.Bytes](err, l, b.collector) } @@ -962,7 +962,7 @@ func (b *BlockChainAPI) GetStorageAt( return nil, err } - key, _, err := decodeHash(storageSlot) + key, err := decodeHash(storageSlot) if err != nil { return handleError[hexutil.Bytes]( fmt.Errorf("%w: %w", errs.ErrInvalid, err), @@ -976,7 +976,7 @@ func (b *BlockChainAPI) GetStorageAt( return handleError[hexutil.Bytes](err, l, b.collector) } - result, err := b.evm.GetStorageAt(ctx, address, key, evmHeight) + result, err := b.evm.GetStorageAt(address, key, evmHeight) if err != nil { return handleError[hexutil.Bytes](err, l, b.collector) } diff --git a/api/utils.go b/api/utils.go index 3ca157cce..1b8809d34 100644 --- a/api/utils.go +++ b/api/utils.go @@ -81,7 +81,7 @@ func resolveBlockNumber( // decodeHash parses a hex-encoded 32-byte hash. The input may optionally // be prefixed by 0x and can have a byte length up to 32. -func decodeHash(s string) (h common.Hash, inputLength int, err error) { +func decodeHash(s string) (h common.Hash, err error) { if strings.HasPrefix(s, "0x") || strings.HasPrefix(s, "0X") { s = s[2:] } @@ -90,15 +90,15 @@ func decodeHash(s string) (h common.Hash, inputLength int, err error) { } b, err := hex.DecodeString(s) if err != nil { - return common.Hash{}, 0, fmt.Errorf("invalid hex string: %s", s) + return common.Hash{}, fmt.Errorf("invalid hex string: %s", s) } if len(b) > common.HashLength { - return common.Hash{}, len(b), fmt.Errorf( + return common.Hash{}, fmt.Errorf( "hex string too long, want at most 32 bytes, have %d bytes", len(b), ) } - return common.BytesToHash(b), len(b), nil + return common.BytesToHash(b), nil } // handleError takes in an error and in case the error is of type ErrEntityNotFound diff --git a/services/requester/requester.go b/services/requester/requester.go index efa61c284..d8ba4ecaa 100644 --- a/services/requester/requester.go +++ b/services/requester/requester.go @@ -90,30 +90,30 @@ type Requester interface { // GetBalance returns the amount of wei for the given address in the state of the // given EVM block height. - GetBalance(ctx context.Context, address common.Address, evmHeight uint64) (*big.Int, error) + GetBalance(address common.Address, evmHeight uint64) (*big.Int, error) // Call executes the given signed transaction data on the state for the given EVM block height. // Note, this function doesn't make and changes in the state/blockchain and is // useful to execute and retrieve values. - Call(ctx context.Context, tx *types.LegacyTx, from common.Address, evmHeight uint64) ([]byte, error) + Call(tx *types.LegacyTx, from common.Address, evmHeight uint64) ([]byte, error) // EstimateGas executes the given signed transaction data on the state for the given EVM block height. // Note, this function doesn't make any changes in the state/blockchain and is // useful to executed and retrieve the gas consumption and possible failures. - EstimateGas(ctx context.Context, tx *types.LegacyTx, from common.Address, evmHeight uint64) (uint64, error) + EstimateGas(tx *types.LegacyTx, from common.Address, evmHeight uint64) (uint64, error) // GetNonce gets nonce from the network at the given EVM block height. - GetNonce(ctx context.Context, address common.Address, evmHeight uint64) (uint64, error) + GetNonce(address common.Address, evmHeight uint64) (uint64, error) // GetCode returns the code stored at the given address in // the state for the given EVM block height. - GetCode(ctx context.Context, address common.Address, evmHeight uint64) ([]byte, error) + GetCode(address common.Address, evmHeight uint64) ([]byte, error) + + // GetStorageAt returns the storage from the state at the given address, key and block number. + GetStorageAt(address common.Address, hash common.Hash, evmHeight uint64) (common.Hash, error) // GetLatestEVMHeight returns the latest EVM height of the network. GetLatestEVMHeight(ctx context.Context) (uint64, error) - - // GetStorageAt returns the storage from the state at the given address, key and block number. - GetStorageAt(ctx context.Context, address common.Address, hash common.Hash, evmHeight uint64) (common.Hash, error) } var _ Requester = &EVM{} @@ -345,7 +345,6 @@ func (e *EVM) buildTransaction(ctx context.Context, script []byte, args ...caden } func (e *EVM) GetBalance( - ctx context.Context, address common.Address, evmHeight uint64, ) (*big.Int, error) { @@ -358,7 +357,6 @@ func (e *EVM) GetBalance( } func (e *EVM) GetNonce( - ctx context.Context, address common.Address, evmHeight uint64, ) (uint64, error) { @@ -371,7 +369,6 @@ func (e *EVM) GetNonce( } func (e *EVM) GetStorageAt( - ctx context.Context, address common.Address, hash common.Hash, evmHeight uint64, @@ -385,7 +382,6 @@ func (e *EVM) GetStorageAt( } func (e *EVM) Call( - ctx context.Context, tx *types.LegacyTx, from common.Address, evmHeight uint64, @@ -425,7 +421,6 @@ func (e *EVM) Call( } func (e *EVM) EstimateGas( - ctx context.Context, tx *types.LegacyTx, from common.Address, evmHeight uint64, @@ -489,7 +484,6 @@ func (e *EVM) EstimateGas( } func (e *EVM) GetCode( - ctx context.Context, address common.Address, evmHeight uint64, ) ([]byte, error) { @@ -651,6 +645,19 @@ func (e *EVM) getBlockView(evmHeight uint64) (*query.View, error) { return viewProvider.GetBlockView(evmHeight) } +func (e *EVM) evmToCadenceHeight(height uint64) (uint64, error) { + cadenceHeight, err := e.blocks.GetCadenceHeight(height) + if err != nil { + return 0, fmt.Errorf( + "failed to map evm height: %d to cadence height: %w", + height, + err, + ) + } + + return cadenceHeight, nil +} + // cacheKey builds the cache key from the script type, height and arguments. func cacheKey(scriptType scriptType, height uint64, args []cadence.Value) string { key := fmt.Sprintf("%d%d", scriptType, height) @@ -681,16 +688,3 @@ func AddOne64th(n uint64) uint64 { // NOTE: Go's integer division floors, but that is desirable here return n + (n / 64) } - -func (e *EVM) evmToCadenceHeight(height uint64) (uint64, error) { - cadenceHeight, err := e.blocks.GetCadenceHeight(height) - if err != nil { - return 0, fmt.Errorf( - "failed to map evm height: %d to cadence height: %w", - height, - err, - ) - } - - return cadenceHeight, nil -} From 9576606c17cbd48df93f84dd5f73d0e647708c43 Mon Sep 17 00:00:00 2001 From: Ardit Marku Date: Mon, 11 Nov 2024 10:38:46 +0200 Subject: [PATCH 039/174] Remove dead code from Requester --- go.mod | 4 +- go.sum | 5 - services/requester/cadence/get_balance.cdc | 8 - services/requester/cadence/get_code.cdc | 8 - services/requester/cadence/get_nonce.cdc | 8 - services/requester/remote_state.go | 75 ------- services/requester/remote_state_test.go | 99 --------- services/requester/requester.go | 118 +---------- services/requester/requester_test.go | 228 --------------------- 9 files changed, 3 insertions(+), 550 deletions(-) delete mode 100644 services/requester/cadence/get_balance.cdc delete mode 100644 services/requester/cadence/get_code.cdc delete mode 100644 services/requester/cadence/get_nonce.cdc delete mode 100644 services/requester/remote_state.go delete mode 100644 services/requester/remote_state_test.go delete mode 100644 services/requester/requester_test.go diff --git a/go.mod b/go.mod index 09c4cc00c..41485ae78 100644 --- a/go.mod +++ b/go.mod @@ -10,7 +10,6 @@ require ( github.com/onflow/cadence v1.2.1 github.com/onflow/flow-go v0.38.0-preview.0.0.20241022154145-6a254edbec23 github.com/onflow/flow-go-sdk v1.2.2 - github.com/onflow/flow/protobuf/go/flow v0.4.7 github.com/onflow/go-ethereum v1.14.7 github.com/prometheus/client_golang v1.18.0 github.com/rs/cors v1.8.0 @@ -96,8 +95,6 @@ require ( github.com/hashicorp/hcl v1.0.0 // indirect github.com/holiman/bloomfilter/v2 v2.0.3 // indirect github.com/holiman/uint256 v1.3.0 // indirect - github.com/huandu/go-clone v1.6.0 // indirect - github.com/huandu/go-clone/generic v1.7.2 // indirect github.com/huin/goupnp v1.3.0 // indirect github.com/inconshreveable/mousetrap v1.1.0 // indirect github.com/ipfs/bbloom v0.0.4 // indirect @@ -145,6 +142,7 @@ require ( github.com/onflow/flow-ft/lib/go/templates v1.0.1 // indirect github.com/onflow/flow-nft/lib/go/contracts v1.2.2 // indirect github.com/onflow/flow-nft/lib/go/templates v1.2.1 // indirect + github.com/onflow/flow/protobuf/go/flow v0.4.7 // indirect github.com/onflow/sdks v0.6.0-preview.1 // indirect github.com/onsi/gomega v1.18.1 // indirect github.com/pbnjay/memory v0.0.0-20210728143218-7b4eea64cf58 // indirect diff --git a/go.sum b/go.sum index 1ea7c5d4e..6fe1a45ae 100644 --- a/go.sum +++ b/go.sum @@ -341,10 +341,7 @@ github.com/googleapis/google-cloud-go-testing v0.0.0-20200911160855-bcd43fbb19e8 github.com/gorilla/websocket v1.4.0/go.mod h1:E7qHFY5m1UJ88s3WnNqhKjPHQ0heANvMoAMk2YaljkQ= github.com/gorilla/websocket v1.5.0 h1:PPwGk2jz7EePpoHN/+ClbZu8SPxiqlu12wZP/3sWmnc= github.com/gorilla/websocket v1.5.0/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE= -github.com/grpc-ecosystem/go-grpc-middleware v1.0.0 h1:Iju5GlWwrvL6UBg4zJJt3btmonfrMlCDdsejg4CZE7c= github.com/grpc-ecosystem/go-grpc-middleware v1.0.0/go.mod h1:FiyG127CGDf3tlThmgyCl78X/SZQqEOJBCDaAfeWzPs= -github.com/grpc-ecosystem/go-grpc-middleware/v2 v2.1.0 h1:pRhl55Yx1eC7BZ1N+BBWwnKaMyD8uC+34TLdndZMAKk= -github.com/grpc-ecosystem/go-grpc-middleware/v2 v2.1.0/go.mod h1:XKMd7iuf/RGPSMJ/U4HP0zS2Z9Fh8Ps9a+6X26m/tmI= github.com/grpc-ecosystem/go-grpc-prometheus v1.2.0/go.mod h1:8NvIoxWQoOIhqOTXgfV/d3M/q6VIi02HzZEHgUlZvzk= github.com/grpc-ecosystem/grpc-gateway v1.9.0/go.mod h1:vNeuVxBJEsws4ogUvrchl83t/GYV9WGTSLVdBhOQFDY= github.com/grpc-ecosystem/grpc-gateway/v2 v2.19.0 h1:Wqo399gCIufwto+VfwCSvsnfGpF/w5E9CNxSwbpD6No= @@ -369,8 +366,6 @@ github.com/holiman/bloomfilter/v2 v2.0.3/go.mod h1:zpoh+gs7qcpqrHr3dB55AMiJwo0iU github.com/holiman/uint256 v1.3.0 h1:4wdcm/tnd0xXdu7iS3ruNvxkWwrb4aeBQv19ayYn8F4= github.com/holiman/uint256 v1.3.0/go.mod h1:EOMSn4q6Nyt9P6efbI3bueV4e1b3dGlUCXeiRV4ng7E= github.com/hpcloud/tail v1.0.0/go.mod h1:ab1qPbhIpdTxEkNHXyeSf5vhxWSCs/tWer42PpOxQnU= -github.com/huandu/go-assert v1.1.5 h1:fjemmA7sSfYHJD7CUqs9qTwwfdNAx7/j2/ZlHXzNB3c= -github.com/huandu/go-assert v1.1.5/go.mod h1:yOLvuqZwmcHIC5rIzrBhT7D3Q9c3GFnd0JrPVhn/06U= github.com/huandu/go-clone v1.6.0 h1:HMo5uvg4wgfiy5FoGOqlFLQED/VGRm2D9Pi8g1FXPGc= github.com/huandu/go-clone v1.6.0/go.mod h1:ReGivhG6op3GYr+UY3lS6mxjKp7MIGTknuU5TbTVaXE= github.com/huandu/go-clone/generic v1.7.2 h1:47pQphxs1Xc9cVADjOHN+Bm5D0hNagwH9UXErbxgVKA= diff --git a/services/requester/cadence/get_balance.cdc b/services/requester/cadence/get_balance.cdc deleted file mode 100644 index b8c954133..000000000 --- a/services/requester/cadence/get_balance.cdc +++ /dev/null @@ -1,8 +0,0 @@ -import EVM - -access(all) -fun main(hexEncodedAddress: String): UInt { - let address = EVM.addressFromString(hexEncodedAddress) - - return address.balance().inAttoFLOW() -} diff --git a/services/requester/cadence/get_code.cdc b/services/requester/cadence/get_code.cdc deleted file mode 100644 index 419f8395a..000000000 --- a/services/requester/cadence/get_code.cdc +++ /dev/null @@ -1,8 +0,0 @@ -import EVM - -access(all) -fun main(hexEncodedAddress: String): String { - let address = EVM.addressFromString(hexEncodedAddress) - - return String.encodeHex(address.code()) -} diff --git a/services/requester/cadence/get_nonce.cdc b/services/requester/cadence/get_nonce.cdc deleted file mode 100644 index 39e2efe2a..000000000 --- a/services/requester/cadence/get_nonce.cdc +++ /dev/null @@ -1,8 +0,0 @@ -import EVM - -access(all) -fun main(hexEncodedAddress: String): UInt64 { - let address = EVM.addressFromString(hexEncodedAddress) - - return address.nonce() -} diff --git a/services/requester/remote_state.go b/services/requester/remote_state.go deleted file mode 100644 index 2e0d79a6c..000000000 --- a/services/requester/remote_state.go +++ /dev/null @@ -1,75 +0,0 @@ -package requester - -import ( - "context" - - "github.com/onflow/atree" - "github.com/onflow/flow-go/engine/common/rpc/convert" - "github.com/onflow/flow-go/model/flow" - "github.com/onflow/flow/protobuf/go/flow/entities" - "github.com/onflow/flow/protobuf/go/flow/executiondata" - "google.golang.org/grpc/codes" - "google.golang.org/grpc/status" -) - -var _ atree.Ledger = &remoteLedger{} - -func newRemoteLedger( - client executiondata.ExecutionDataAPIClient, - cadenceHeight uint64, -) (*remoteLedger, error) { - return &remoteLedger{ - execution: client, - height: cadenceHeight, - }, nil -} - -// remoteLedger is a ledger that uses execution data APIs to fetch register values, -// thus simulating execution against the host network. -// -// The ledger implements atree.Ledger interface which is used by the type.stateDB -// to inspect the state. -type remoteLedger struct { - execution executiondata.ExecutionDataAPIClient - height uint64 -} - -func (l *remoteLedger) GetValue(owner, key []byte) ([]byte, error) { - id := flow.RegisterID{ - Key: string(key), - Owner: string(owner), - } - registerID := convert.RegisterIDToMessage(id) - - response, err := l.execution.GetRegisterValues( - context.Background(), - &executiondata.GetRegisterValuesRequest{ - BlockHeight: l.height, - RegisterIds: []*entities.RegisterID{registerID}, - }, - ) - errorCode := status.Code(err) - if err != nil && errorCode != codes.NotFound && errorCode != codes.OutOfRange { - return nil, err - } - - if response != nil && len(response.Values) > 0 { - // we only request one register so 0 index - return response.Values[0], nil - } - - return nil, nil -} - -func (l *remoteLedger) ValueExists(owner, key []byte) (exists bool, err error) { - val, err := l.GetValue(owner, key) - return val != nil, err -} - -func (l *remoteLedger) SetValue(owner, key, value []byte) (err error) { - panic("read only") -} - -func (l *remoteLedger) AllocateSlabIndex(owner []byte) (atree.SlabIndex, error) { - panic("read only") -} diff --git a/services/requester/remote_state_test.go b/services/requester/remote_state_test.go deleted file mode 100644 index 69e494b8c..000000000 --- a/services/requester/remote_state_test.go +++ /dev/null @@ -1,99 +0,0 @@ -package requester - -import ( - "context" - "encoding/hex" - "os" - "testing" - - grpcClient "github.com/onflow/flow-go-sdk/access/grpc" - "github.com/onflow/flow-go/fvm/evm" - "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" - gethCommon "github.com/onflow/go-ethereum/common" - "github.com/stretchr/testify/assert" - "github.com/stretchr/testify/require" - "google.golang.org/grpc" -) - -var previewnetStorageAddress = evm.StorageAccountAddress(flowGo.Previewnet) - -func Test_E2E_Previewnet_RemoteLedger(t *testing.T) { - executionAPI := os.Getenv("E2E_EXECUTION_API") // "access-001.previewnet1.nodes.onflow.org:9000" - if executionAPI == "" { - t.Skip() - } - - ledger, err := newPreviewnetLedger(executionAPI) - require.NoError(t, err) - - // this is a pre-established test account on previewnet - addrBytes, err := hex.DecodeString("BC9985a24c0846cbEdd6249868020A84Df83Ea85") - require.NoError(t, err) - testAddress := types.NewAddressFromBytes(addrBytes).ToCommon() - - stateDB, err := state.NewStateDB(ledger, previewnetStorageAddress) - require.NoError(t, err) - - assert.NotEmpty(t, stateDB.GetCode(testAddress)) - assert.NotEmpty(t, stateDB.GetNonce(testAddress)) - assert.Empty(t, stateDB.GetBalance(testAddress)) - assert.NotEmpty(t, stateDB.GetCodeSize(testAddress)) - assert.NotEmpty(t, stateDB.GetState(testAddress, gethCommon.Hash{})) -} - -/* -Testing from local machine (bottleneck is network delay to previewnet AN) - -Benchmark_RemoteLedger_GetBalance-8 9 1144204361 ns/op -*/ -func Benchmark_RemoteLedger_GetBalance(b *testing.B) { - executionAPI := os.Getenv("E2E_EXECUTION_API") // "access-001.previewnet1.nodes.onflow.org:9000" - if executionAPI == "" { - b.Skip() - } - - client, err := grpcClient.NewClient(executionAPI, - grpcClient.WithGRPCDialOptions(grpc.WithDefaultCallOptions(grpc.MaxCallRecvMsgSize(1024*1024*1024))), - ) - require.NoError(b, err) - execClient := client.ExecutionDataRPCClient() - - latest, err := client.GetLatestBlockHeader(context.Background(), true) - require.NoError(b, err) - - // we have to include ledger creation since the loading of the collection - // will be done only once per height, all the subsequent requests for - // getting the balance will work on already loaded state and thus be fast - for i := 0; i < b.N; i++ { - ledger, err := newRemoteLedger(execClient, latest.Height) - require.NoError(b, err) - - stateDB, err := state.NewStateDB(ledger, previewnetStorageAddress) - require.NoError(b, err) - - addrBytes, err := hex.DecodeString("BC9985a24c0846cbEdd6249868020A84Df83Ea85") - require.NoError(b, err) - testAddress := types.NewAddressFromBytes(addrBytes).ToCommon() - - assert.Empty(b, stateDB.GetBalance(testAddress)) - } -} - -func newPreviewnetLedger(host string) (*remoteLedger, error) { - client, err := grpcClient.NewClient(host, - grpcClient.WithGRPCDialOptions(grpc.WithDefaultCallOptions(grpc.MaxCallRecvMsgSize(1024*1024*1024))), - ) - if err != nil { - return nil, err - } - execClient := client.ExecutionDataRPCClient() - - latest, err := client.GetLatestBlockHeader(context.Background(), true) - if err != nil { - return nil, err - } - - return newRemoteLedger(execClient, latest.Height) -} diff --git a/services/requester/requester.go b/services/requester/requester.go index d8ba4ecaa..d70d48fa2 100644 --- a/services/requester/requester.go +++ b/services/requester/requester.go @@ -44,40 +44,13 @@ var ( //go:embed cadence/run.cdc runTxScript []byte - //go:embed cadence/get_balance.cdc - getBalanceScript []byte - //go:embed cadence/create_coa.cdc createCOAScript []byte - //go:embed cadence/get_nonce.cdc - getNonceScript []byte - - //go:embed cadence/get_code.cdc - getCodeScript []byte - //go:embed cadence/get_latest_evm_height.cdc getLatestEVMHeight []byte ) -type scriptType int - -const ( - dryRun scriptType = iota - getBalance - getNonce - getCode - getLatest -) - -var scripts = map[scriptType][]byte{ - dryRun: dryRunScript, - getBalance: getBalanceScript, - getNonce: getNonceScript, - getCode: getCodeScript, - getLatest: getLatestEVMHeight, -} - const minFlowBalance = 2 const coaFundingBalance = minFlowBalance - 1 @@ -496,10 +469,9 @@ func (e *EVM) GetCode( } func (e *EVM) GetLatestEVMHeight(ctx context.Context) (uint64, error) { - val, err := e.executeScriptAtHeight( + val, err := e.client.ExecuteScriptAtLatestBlock( ctx, - getLatest, - LatestBlockHeight, + e.replaceAddresses(getLatestEVMHeight), nil, ) if err != nil { @@ -568,66 +540,6 @@ func (e *EVM) replaceAddresses(script []byte) []byte { return []byte(s) } -// executeScriptAtHeight will execute the given script, at the given -// block height, with the given arguments. A height of `LatestBlockHeight` -// (math.MaxUint64 - 1) is a special value, which means the script will be -// executed at the latest sealed block. -func (e *EVM) executeScriptAtHeight( - ctx context.Context, - scriptType scriptType, - height uint64, - arguments []cadence.Value, -) (cadence.Value, error) { - script, ok := scripts[scriptType] - if !ok { - return nil, fmt.Errorf("unknown script type") - } - - // try and get the value from the cache if key is supported - key := cacheKey(scriptType, height, arguments) - if key != "" && e.scriptCache != nil { - val, ok := e.scriptCache.Get(key) - if ok { - e.logger.Info(). - Uint64("evm-height", height). - Int("script", int(scriptType)). - Str("result", val.String()). - Msg("cache hit") - return val, nil - } - } - - var res cadence.Value - var err error - - if height == LatestBlockHeight { - res, err = e.client.ExecuteScriptAtLatestBlock( - ctx, - e.replaceAddresses(script), - arguments, - ) - } else { - res, err = e.client.ExecuteScriptAtBlockHeight( - ctx, - height, - e.replaceAddresses(script), - arguments, - ) - } - if err != nil { - // if snapshot doesn't exist on EN, the height at which script was executed is out - // of the boundaries the EN keeps state, so return out of range - const storageError = "failed to create storage snapshot" - if strings.Contains(err.Error(), storageError) { - return nil, errs.NewHeightOutOfRangeError(height) - } - } else if key != "" && e.scriptCache != nil { // if error is nil and key is supported add to cache - e.scriptCache.Add(key, res) - } - - return res, err -} - func (e *EVM) getBlockView(evmHeight uint64) (*query.View, error) { blocksProvider := replayer.NewBlocksProvider( e.blocks, @@ -658,32 +570,6 @@ func (e *EVM) evmToCadenceHeight(height uint64) (uint64, error) { return cadenceHeight, nil } -// cacheKey builds the cache key from the script type, height and arguments. -func cacheKey(scriptType scriptType, height uint64, args []cadence.Value) string { - key := fmt.Sprintf("%d%d", scriptType, height) - - switch scriptType { - case getBalance: - if len(args) != 1 { - return "" - } - v := args[0].(cadence.String) - key = fmt.Sprintf("%s%s", key, string(v)) - case getNonce: - if len(args) != 1 { - return "" - } - v := args[0].(cadence.String) - key = fmt.Sprintf("%s%s", key, string(v)) - case getLatest: - // no additional arguments - default: - return "" - } - - return key -} - func AddOne64th(n uint64) uint64 { // NOTE: Go's integer division floors, but that is desirable here return n + (n / 64) diff --git a/services/requester/requester_test.go b/services/requester/requester_test.go deleted file mode 100644 index f40ab8641..000000000 --- a/services/requester/requester_test.go +++ /dev/null @@ -1,228 +0,0 @@ -package requester - -import ( - "context" - "fmt" - "testing" - "time" - - "github.com/hashicorp/golang-lru/v2/expirable" - "github.com/onflow/cadence" - "github.com/onflow/flow-go-sdk/access/mocks" - flowGo "github.com/onflow/flow-go/model/flow" - "github.com/rs/zerolog" - "github.com/stretchr/testify/mock" - "github.com/stretchr/testify/require" - - "github.com/onflow/flow-evm-gateway/config" -) - -func Test_Caching(t *testing.T) { - t.Run("Get balance at height cached", func(t *testing.T) { - mockClient := &mocks.Client{} - - cache := expirable.NewLRU[string, cadence.Value](1000, nil, time.Second) - e := createEVM(t, cache, mockClient) - - height := uint64(100) - address, _ := cadence.NewString("123") - balance := cadence.NewInt(1) - - mockClient. - On("ExecuteScriptAtBlockHeight", mock.Anything, mock.Anything, mock.Anything, mock.Anything). - Return(balance, nil). - Once() - - require.Equal(t, 0, cache.Len()) // empty cache - - // first request goes through the above mock client, - // additional requests should be processed with cache, note the above mock client - // is only set to once, so if cache is a miss it would fail to call the client again - for i := 0; i < 5; i++ { - val, err := e.executeScriptAtHeight(context.Background(), getBalance, height, []cadence.Value{address}) - require.NoError(t, err) - require.Equal(t, balance, val) - // cache should be filled - require.Equal(t, 1, cache.Len()) - } - }) - - t.Run("Get balance at latest height cached", func(t *testing.T) { - mockClient := &mocks.Client{} - - cache := expirable.NewLRU[string, cadence.Value](1000, nil, time.Second) - e := createEVM(t, cache, mockClient) - - height := LatestBlockHeight - address, _ := cadence.NewString("123") - balance := cadence.NewInt(1) - - mockClient. - On("ExecuteScriptAtLatestBlock", mock.Anything, mock.Anything, mock.Anything, mock.Anything). - Return(balance, nil). - Once() - - require.Equal(t, 0, cache.Len()) // empty cache - - // first request goes through the above mock client, - // additional requests should be processed with cache, note the above mock client - // is only set to once, so if cache is a miss it would fail to call the client again - for i := 0; i < 5; i++ { - val, err := e.executeScriptAtHeight(context.Background(), getBalance, height, []cadence.Value{address}) - require.NoError(t, err) - require.Equal(t, balance, val) - // cache should be filled - require.Equal(t, 1, cache.Len()) - } - }) - - t.Run("Get balance cache expires and is added again", func(t *testing.T) { - mockClient := &mocks.Client{} - - cacheExpiry := time.Millisecond * 100 - cache := expirable.NewLRU[string, cadence.Value](1000, nil, cacheExpiry) - e := createEVM(t, cache, mockClient) - - height := LatestBlockHeight - address, _ := cadence.NewString("123") - balance := cadence.NewInt(1) - - mockClient. - On("ExecuteScriptAtLatestBlock", mock.Anything, mock.Anything, mock.Anything, mock.Anything). - Return(balance, nil). - Once() - - require.Equal(t, 0, cache.Len()) // empty cache - - // first request goes through the above mock client, - // additional requests should be processed with cache, note the above mock client - // is only set to once, so if cache is a miss it would fail to call the client again - for i := 0; i < 5; i++ { - val, err := e.executeScriptAtHeight(context.Background(), getBalance, height, []cadence.Value{address}) - require.NoError(t, err) - require.Equal(t, balance, val) - // cache should be filled - require.Equal(t, 1, cache.Len()) - } - - // wait for cache expiry - time.Sleep(cacheExpiry + 100*time.Millisecond) - - require.Equal(t, 0, cache.Len()) // make sure cache is empty - - // re-set the mock - mockClient. - On("ExecuteScriptAtLatestBlock", mock.Anything, mock.Anything, mock.Anything, mock.Anything). - Return(balance, nil). - Once() - val, err := e.executeScriptAtHeight(context.Background(), getBalance, height, []cadence.Value{address}) - require.NoError(t, err) - require.Equal(t, balance, val) - require.Equal(t, 1, cache.Len()) - }) - - t.Run("Get balance multiple addresses and heights", func(t *testing.T) { - mockClient := &mocks.Client{} - - cache := expirable.NewLRU[string, cadence.Value](1000, nil, time.Second) - e := createEVM(t, cache, mockClient) - - type acc struct { - height uint64 - address cadence.String - balance cadence.Int - } - - tests := []acc{{ - height: 1002233, - address: cadence.String("1AC87F33D10b76E8BDd4fb501445A5ec413eb121"), - balance: cadence.NewInt(23958395), - }, { - height: 2002233, - address: cadence.String("A3014d9F6162a162BAD9Ff15346A4B82A56F841f"), - balance: cadence.NewInt(1), - }, { - height: 3002233, - address: cadence.String("53e6A4b36a56CB68fe54661416Be2c5b3Ee193c9"), - balance: cadence.NewInt(4), - }, { - height: 4002233, - address: cadence.String("839fEfa0750798B3A0BD9c925871e3f5027a5d44"), - balance: cadence.NewInt(3), - }, { - height: 7002233, - address: cadence.String("243a064089cF765E1F270B90913Db31cdDf299F5"), - balance: cadence.NewInt(5), - }} - - for i, test := range tests { - mockClient. - On("ExecuteScriptAtBlockHeight", mock.Anything, mock.Anything, mock.Anything, mock.Anything). - Return(test.balance, nil). - Once() - - val, err := e.executeScriptAtHeight(context.Background(), getBalance, test.height, []cadence.Value{test.address}) - require.NoError(t, err) - require.Equal(t, test.balance, val) - // cache should be filled - require.Equal(t, i+1, cache.Len()) - } - - require.Equal(t, len(tests), cache.Len()) - - // first request goes through the above mock client, - // additional requests should be processed with cache, note the above mock client - // is only set to once, so if cache is a miss it would fail to call the client again - for _, test := range tests { - val, err := e.executeScriptAtHeight(context.Background(), getBalance, test.height, []cadence.Value{test.address}) - require.NoError(t, err) - require.Equal(t, test.balance, val) - // cache should be filled - require.Equal(t, len(tests), cache.Len()) - } - }) -} - -func Test_CacheKey(t *testing.T) { - addr, _ := cadence.NewString("0x1") - h := uint64(100) - - key := cacheKey(getBalance, h, []cadence.Value{addr}) - require.Equal(t, fmt.Sprintf("%d%d%s", getBalance, h, string(addr)), key) - - key = cacheKey(getBalance, LatestBlockHeight, []cadence.Value{addr}) - require.Equal(t, fmt.Sprintf("%d%d%s", getBalance, LatestBlockHeight, string(addr)), key) - - key = cacheKey(getNonce, LatestBlockHeight, []cadence.Value{addr}) - require.Equal(t, fmt.Sprintf("%d%d%s", getNonce, LatestBlockHeight, string(addr)), key) - - key = cacheKey(getNonce, h, []cadence.Value{addr}) - require.Equal(t, fmt.Sprintf("%d%d%s", getNonce, h, string(addr)), key) - - key = cacheKey(getLatest, LatestBlockHeight, nil) - require.Equal(t, fmt.Sprintf("%d%d", getLatest, LatestBlockHeight), key) - - key = cacheKey(getCode, LatestBlockHeight, nil) - require.Equal(t, "", key) - - key = cacheKey(getBalance, LatestBlockHeight, []cadence.Value{addr, addr}) - require.Equal(t, "", key) - -} - -func createEVM(t *testing.T, cache *expirable.LRU[string, cadence.Value], mockClient *mocks.Client) *EVM { - networkID := flowGo.Emulator - log := zerolog.New(zerolog.NewTestWriter(t)) - - client, err := NewCrossSporkClient(mockClient, nil, log, networkID) - require.NoError(t, err) - - return &EVM{ - client: client, - logger: log, - scriptCache: cache, - config: &config.Config{ - FlowNetworkID: networkID, - }, - } -} From f91ec404f015b4ef9113c663d9561b866b41bba9 Mon Sep 17 00:00:00 2001 From: Ardit Marku Date: Mon, 11 Nov 2024 10:57:13 +0200 Subject: [PATCH 040/174] Remove unused const --- services/requester/requester.go | 3 --- 1 file changed, 3 deletions(-) diff --git a/services/requester/requester.go b/services/requester/requester.go index d70d48fa2..d90bd6566 100644 --- a/services/requester/requester.go +++ b/services/requester/requester.go @@ -5,7 +5,6 @@ import ( _ "embed" "encoding/hex" "fmt" - "math" "math/big" "strings" "sync" @@ -54,8 +53,6 @@ var ( const minFlowBalance = 2 const coaFundingBalance = minFlowBalance - 1 -const LatestBlockHeight uint64 = math.MaxUint64 - 1 - type Requester interface { // SendRawTransaction will submit signed transaction data to the network. // The submitted EVM transaction hash is returned. From 2a4444f0b3b5efafc309d1760cbf98051d9cab08 Mon Sep 17 00:00:00 2001 From: Ardit Marku Date: Mon, 11 Nov 2024 11:13:01 +0200 Subject: [PATCH 041/174] Move dryRunScript var to RemoteCadenceArch and bump the gas limit --- services/requester/remote_cadence_arch.go | 14 ++++++++++---- services/requester/requester.go | 3 --- 2 files changed, 10 insertions(+), 7 deletions(-) diff --git a/services/requester/remote_cadence_arch.go b/services/requester/remote_cadence_arch.go index 33de0db87..d2d4ec525 100644 --- a/services/requester/remote_cadence_arch.go +++ b/services/requester/remote_cadence_arch.go @@ -2,6 +2,7 @@ package requester import ( "context" + _ "embed" "encoding/hex" "fmt" "math/big" @@ -12,18 +13,23 @@ import ( evmImpl "github.com/onflow/flow-go/fvm/evm/impl" evmTypes "github.com/onflow/flow-go/fvm/evm/types" "github.com/onflow/flow-go/fvm/systemcontracts" - flowGo "github.com/onflow/flow-go/model/flow" + "github.com/onflow/flow-go/model/flow" gethCommon "github.com/onflow/go-ethereum/common" "github.com/onflow/go-ethereum/core/types" "github.com/onflow/go-ethereum/crypto" ) +var ( + //go:embed cadence/dry_run.cdc + dryRunScript []byte +) + var cadenceArchAddress = gethCommon.HexToAddress("0x0000000000000000000000010000000000000001") type RemoteCadenceArch struct { blockHeight uint64 client *CrossSporkClient - chainID flowGo.ChainID + chainID flow.ChainID cachedCalls map[string]evmTypes.Data } @@ -32,7 +38,7 @@ var _ evmTypes.PrecompiledContract = (*RemoteCadenceArch)(nil) func NewRemoteCadenceArch( blockHeight uint64, client *CrossSporkClient, - chainID flowGo.ChainID, + chainID flow.ChainID, ) *RemoteCadenceArch { return &RemoteCadenceArch{ blockHeight: blockHeight, @@ -92,7 +98,7 @@ func (rca *RemoteCadenceArch) runCall(input []byte) (*evmTypes.ResultSummary, er Nonce: 0, To: &cadenceArchAddress, Value: big.NewInt(0), - Gas: 55_000, + Gas: 155_000, GasPrice: big.NewInt(0), Data: input, }, diff --git a/services/requester/requester.go b/services/requester/requester.go index d90bd6566..8799d9b27 100644 --- a/services/requester/requester.go +++ b/services/requester/requester.go @@ -37,9 +37,6 @@ import ( ) var ( - //go:embed cadence/dry_run.cdc - dryRunScript []byte - //go:embed cadence/run.cdc runTxScript []byte From ec9f40a030802bfcd6b0d179e598cca7923ef0f7 Mon Sep 17 00:00:00 2001 From: Ardit Marku Date: Mon, 11 Nov 2024 11:27:44 +0200 Subject: [PATCH 042/174] Extract replaceAddresses helper function to its own file for re-usability --- services/requester/remote_cadence_arch.go | 23 ++--------------- services/requester/requester.go | 29 +++------------------ services/requester/utils.go | 31 +++++++++++++++++++++++ 3 files changed, 36 insertions(+), 47 deletions(-) create mode 100644 services/requester/utils.go diff --git a/services/requester/remote_cadence_arch.go b/services/requester/remote_cadence_arch.go index d2d4ec525..28e331ea2 100644 --- a/services/requester/remote_cadence_arch.go +++ b/services/requester/remote_cadence_arch.go @@ -6,13 +6,11 @@ import ( "encoding/hex" "fmt" "math/big" - "strings" "github.com/onflow/cadence" errs "github.com/onflow/flow-evm-gateway/models/errors" evmImpl "github.com/onflow/flow-go/fvm/evm/impl" evmTypes "github.com/onflow/flow-go/fvm/evm/types" - "github.com/onflow/flow-go/fvm/systemcontracts" "github.com/onflow/flow-go/model/flow" gethCommon "github.com/onflow/go-ethereum/common" "github.com/onflow/go-ethereum/core/types" @@ -75,23 +73,6 @@ func (rca *RemoteCadenceArch) Run(input []byte) ([]byte, error) { return evmResult.ReturnedData, nil } -func (rca *RemoteCadenceArch) replaceAddresses(script []byte) []byte { - // make the list of all contracts we should replace address for - sc := systemcontracts.SystemContractsForChain(rca.chainID) - contracts := []systemcontracts.SystemContract{sc.EVMContract, sc.FungibleToken, sc.FlowToken} - - s := string(script) - // iterate over all the import name and address pairs and replace them in script - for _, contract := range contracts { - s = strings.ReplaceAll(s, - fmt.Sprintf("import %s", contract.Name), - fmt.Sprintf("import %s from %s", contract.Name, contract.Address.HexWithPrefix()), - ) - } - - return []byte(s) -} - func (rca *RemoteCadenceArch) runCall(input []byte) (*evmTypes.ResultSummary, error) { tx := types.NewTx( &types.LegacyTx{ @@ -120,7 +101,7 @@ func (rca *RemoteCadenceArch) runCall(input []byte) (*evmTypes.ResultSummary, er scriptResult, err := rca.client.ExecuteScriptAtBlockHeight( context.Background(), rca.blockHeight, - rca.replaceAddresses(dryRunScript), + replaceAddresses(dryRunScript, rca.chainID), []cadence.Value{hexEncodedTx, hexEncodedAddress}, ) if err != nil { @@ -151,5 +132,5 @@ func parseResult(res cadence.Value) (*evmTypes.ResultSummary, error) { return nil, errs.NewFailedTransactionError(result.ErrorMessage) } - return result, err + return result, nil } diff --git a/services/requester/requester.go b/services/requester/requester.go index 8799d9b27..169d49ac6 100644 --- a/services/requester/requester.go +++ b/services/requester/requester.go @@ -6,7 +6,6 @@ import ( "encoding/hex" "fmt" "math/big" - "strings" "sync" "time" @@ -18,7 +17,6 @@ import ( "github.com/onflow/flow-go/fvm/evm/emulator" "github.com/onflow/flow-go/fvm/evm/offchain/query" evmTypes "github.com/onflow/flow-go/fvm/evm/types" - "github.com/onflow/flow-go/fvm/systemcontracts" "github.com/onflow/go-ethereum/common" "github.com/onflow/go-ethereum/core/txpool" "github.com/onflow/go-ethereum/core/types" @@ -187,7 +185,7 @@ func NewEVM( if config.CreateCOAResource { tx, err := evm.buildTransaction( context.Background(), - evm.replaceAddresses(createCOAScript), + replaceAddresses(createCOAScript, config.FlowNetworkID), cadence.UFix64(coaFundingBalance), ) if err != nil { @@ -232,7 +230,7 @@ func (e *EVM) SendRawTransaction(ctx context.Context, data []byte) (common.Hash, return common.Hash{}, err } - script := e.replaceAddresses(runTxScript) + script := replaceAddresses(runTxScript, e.config.FlowNetworkID) flowTx, err := e.buildTransaction(ctx, script, hexEncodedTx, coinbaseAddress) if err != nil { e.logger.Error().Err(err).Str("data", txData).Msg("failed to build transaction") @@ -465,7 +463,7 @@ func (e *EVM) GetCode( func (e *EVM) GetLatestEVMHeight(ctx context.Context) (uint64, error) { val, err := e.client.ExecuteScriptAtLatestBlock( ctx, - e.replaceAddresses(getLatestEVMHeight), + replaceAddresses(getLatestEVMHeight, e.config.FlowNetworkID), nil, ) if err != nil { @@ -513,27 +511,6 @@ func (e *EVM) getSignerNetworkInfo(ctx context.Context) (uint32, uint64, error) ) } -// replaceAddresses replace the addresses based on the network -func (e *EVM) replaceAddresses(script []byte) []byte { - // make the list of all contracts we should replace address for - sc := systemcontracts.SystemContractsForChain(e.config.FlowNetworkID) - contracts := []systemcontracts.SystemContract{sc.EVMContract, sc.FungibleToken, sc.FlowToken} - - s := string(script) - // iterate over all the import name and address pairs and replace them in script - for _, contract := range contracts { - s = strings.ReplaceAll(s, - fmt.Sprintf("import %s", contract.Name), - fmt.Sprintf("import %s from %s", contract.Name, contract.Address.HexWithPrefix()), - ) - } - - // also replace COA address if used (in scripts) - s = strings.ReplaceAll(s, "0xCOA", e.config.COAAddress.HexWithPrefix()) - - return []byte(s) -} - func (e *EVM) getBlockView(evmHeight uint64) (*query.View, error) { blocksProvider := replayer.NewBlocksProvider( e.blocks, diff --git a/services/requester/utils.go b/services/requester/utils.go new file mode 100644 index 000000000..d3ee4d7fc --- /dev/null +++ b/services/requester/utils.go @@ -0,0 +1,31 @@ +package requester + +import ( + "fmt" + "strings" + + "github.com/onflow/flow-go/fvm/systemcontracts" + "github.com/onflow/flow-go/model/flow" +) + +// replaceAddresses replace the addresses based on the network +func replaceAddresses(script []byte, chainID flow.ChainID) []byte { + // make the list of all contracts we should replace address for + sc := systemcontracts.SystemContractsForChain(chainID) + contracts := []systemcontracts.SystemContract{ + sc.EVMContract, + sc.FungibleToken, + sc.FlowToken, + } + + s := string(script) + // iterate over all the import name and address pairs and replace them in script + for _, contract := range contracts { + s = strings.ReplaceAll(s, + fmt.Sprintf("import %s", contract.Name), + fmt.Sprintf("import %s from %s", contract.Name, contract.Address.HexWithPrefix()), + ) + } + + return []byte(s) +} From 4760f2b79abc17b96f7931b64ad4b8a8d3fe34a7 Mon Sep 17 00:00:00 2001 From: Ardit Marku Date: Mon, 11 Nov 2024 11:42:09 +0200 Subject: [PATCH 043/174] Minor improvements for resolveBlockTag function --- api/utils.go | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/api/utils.go b/api/utils.go index 1b8809d34..f064c9ca6 100644 --- a/api/utils.go +++ b/api/utils.go @@ -30,21 +30,21 @@ func resolveBlockTag( if err != nil { logger.Error().Err(err). Stringer("block_number", number). - Msg("failed to resolve block by hash") + Msg("failed to resolve block by number") return 0, err } return height, nil } if hash, ok := blockNumberOrHash.Hash(); ok { - evmHeight, err := blocksDB.GetHeightByID(hash) + height, err := blocksDB.GetHeightByID(hash) if err != nil { logger.Error().Err(err). Stringer("block_hash", hash). - Msg("failed to get block by hash") + Msg("failed to resolve block by hash") return 0, err } - return evmHeight, nil + return height, nil } return 0, fmt.Errorf( From 0ea0c6f83b95a5498cd67e8c122ca30505626288 Mon Sep 17 00:00:00 2001 From: Ardit Marku Date: Mon, 11 Nov 2024 12:03:22 +0200 Subject: [PATCH 044/174] Simplify Requester initialization from bootstrap --- bootstrap/bootstrap.go | 7 +------ services/requester/requester.go | 10 +--------- 2 files changed, 2 insertions(+), 15 deletions(-) diff --git a/bootstrap/bootstrap.go b/bootstrap/bootstrap.go index 33869ee42..722b1a8db 100644 --- a/bootstrap/bootstrap.go +++ b/bootstrap/bootstrap.go @@ -211,18 +211,13 @@ func (b *Bootstrap) StartAPIServer(ctx context.Context) error { b.logger, ) - tracer, err := replayer.DefaultCallTracer() - if err != nil { - return err - } blocksProvider := replayer.NewBlocksProvider( b.storages.Blocks, b.config.FlowNetworkID, - tracer, + nil, ) evm, err := requester.NewEVM( - b.storages.Storage, b.storages.Registers, blocksProvider, b.client, diff --git a/services/requester/requester.go b/services/requester/requester.go index 169d49ac6..58b5d91cd 100644 --- a/services/requester/requester.go +++ b/services/requester/requester.go @@ -84,7 +84,6 @@ type Requester interface { var _ Requester = &EVM{} type EVM struct { - store *pebble.Storage registerStore *pebble.RegisterStorage blocksProvider *replayer.BlocksProvider client *CrossSporkClient @@ -104,7 +103,6 @@ type EVM struct { } func NewEVM( - store *pebble.Storage, registerStore *pebble.RegisterStorage, blocksProvider *replayer.BlocksProvider, client *CrossSporkClient, @@ -165,7 +163,6 @@ func NewEVM( } evm := &EVM{ - store: store, registerStore: registerStore, blocksProvider: blocksProvider, client: client, @@ -512,16 +509,11 @@ func (e *EVM) getSignerNetworkInfo(ctx context.Context) (uint32, uint64, error) } func (e *EVM) getBlockView(evmHeight uint64) (*query.View, error) { - blocksProvider := replayer.NewBlocksProvider( - e.blocks, - e.config.FlowNetworkID, - nil, - ) viewProvider := query.NewViewProvider( e.config.FlowNetworkID, evm.StorageAccountAddress(e.config.FlowNetworkID), e.registerStore, - blocksProvider, + e.blocksProvider, 120_000_000, ) From 20455c7b7ebf6fe1c38d072927ac7e63f4481798 Mon Sep 17 00:00:00 2001 From: Ardit Marku Date: Mon, 11 Nov 2024 12:12:45 +0200 Subject: [PATCH 045/174] Simplify DebugAPI initialization from bootstrap --- api/debug.go | 3 --- bootstrap/bootstrap.go | 1 - 2 files changed, 4 deletions(-) diff --git a/api/debug.go b/api/debug.go index 7e71ec362..1fb5c8bff 100644 --- a/api/debug.go +++ b/api/debug.go @@ -37,7 +37,6 @@ type txTraceResult struct { } type DebugAPI struct { - store *pebble.Storage registerStore *pebble.RegisterStorage logger zerolog.Logger tracer storage.TraceIndexer @@ -49,7 +48,6 @@ type DebugAPI struct { } func NewDebugAPI( - store *pebble.Storage, registerStore *pebble.RegisterStorage, tracer storage.TraceIndexer, blocks storage.BlockIndexer, @@ -60,7 +58,6 @@ func NewDebugAPI( collector metrics.Collector, ) *DebugAPI { return &DebugAPI{ - store: store, registerStore: registerStore, logger: logger, tracer: tracer, diff --git a/bootstrap/bootstrap.go b/bootstrap/bootstrap.go index 722b1a8db..06cf93468 100644 --- a/bootstrap/bootstrap.go +++ b/bootstrap/bootstrap.go @@ -280,7 +280,6 @@ func (b *Bootstrap) StartAPIServer(ctx context.Context) error { ) debugAPI := api.NewDebugAPI( - b.storages.Storage, b.storages.Registers, b.storages.Traces, b.storages.Blocks, From e0d0150a7c16127cc1fb0c2f0074e865c1e0223d Mon Sep 17 00:00:00 2001 From: Ardit Marku Date: Mon, 11 Nov 2024 12:32:15 +0200 Subject: [PATCH 046/174] Move hard-coded values to constants --- api/api.go | 8 +++--- api/debug.go | 2 +- api/encode_transaction.go | 44 --------------------------------- api/stream.go | 2 +- api/utils.go | 37 +++++++++++++++++++++++++++ services/requester/requester.go | 5 ++-- 6 files changed, 47 insertions(+), 51 deletions(-) delete mode 100644 api/encode_transaction.go diff --git a/api/api.go b/api/api.go index 6186d62d3..5e49af91f 100644 --- a/api/api.go +++ b/api/api.go @@ -27,6 +27,8 @@ import ( "github.com/onflow/flow-evm-gateway/storage" ) +const BlockGasLimit uint64 = 120_000_000 + const maxFeeHistoryBlockCount = 1024 var baseFeesPerGas = big.NewInt(1) @@ -804,7 +806,7 @@ func (b *BlockChainAPI) EstimateGas( tx, err := encodeTxFromArgs(args) if err != nil { - return hexutil.Uint64(blockGasLimit), nil // return block gas limit + return hexutil.Uint64(BlockGasLimit), nil // return block gas limit } // Default address in case user does not provide one @@ -932,7 +934,7 @@ func (b *BlockChainAPI) FeeHistory( rewards = append(rewards, blockRewards) - gasUsedRatio := float64(block.TotalGasUsed) / float64(blockGasLimit) + gasUsedRatio := float64(block.TotalGasUsed) / float64(BlockGasLimit) gasUsedRatios = append(gasUsedRatios, gasUsedRatio) } @@ -1041,7 +1043,7 @@ func (b *BlockChainAPI) prepareBlockResponse( TransactionsRoot: block.TransactionHashRoot, Transactions: block.TransactionHashes, Uncles: []common.Hash{}, - GasLimit: hexutil.Uint64(blockGasLimit), + GasLimit: hexutil.Uint64(BlockGasLimit), Nonce: types.BlockNonce{0x1}, Timestamp: hexutil.Uint64(block.Timestamp), BaseFeePerGas: hexutil.Big(*baseFeesPerGas), diff --git a/api/debug.go b/api/debug.go index 1fb5c8bff..cedde4cef 100644 --- a/api/debug.go +++ b/api/debug.go @@ -277,7 +277,7 @@ func (d *DebugAPI) TraceCall( flowEVM.StorageAccountAddress(d.config.FlowNetworkID), d.registerStore, blocksProvider, - 120_000_000, + BlockGasLimit, ) view, err := viewProvider.GetBlockView(block.Height) diff --git a/api/encode_transaction.go b/api/encode_transaction.go deleted file mode 100644 index b6e4d7a1a..000000000 --- a/api/encode_transaction.go +++ /dev/null @@ -1,44 +0,0 @@ -package api - -import ( - "math/big" - - "github.com/onflow/go-ethereum/core/types" -) - -const blockGasLimit uint64 = 120_000_000 - -// encodeTxFromArgs will create a transaction from the given arguments. -// The resulting unsigned transaction is only supposed to be used through -// `EVM.dryRun` inside Cadence scripts, meaning that no state change -// will occur. -// This is only useful for `eth_estimateGas` and `eth_call` endpoints. -func encodeTxFromArgs(args TransactionArgs) (*types.LegacyTx, error) { - var data []byte - if args.Data != nil { - data = *args.Data - } else if args.Input != nil { - data = *args.Input - } - - // provide a high enough gas for the tx to be able to execute, - // capped by the gas set in transaction args. - gasLimit := blockGasLimit - if args.Gas != nil { - gasLimit = uint64(*args.Gas) - } - - value := big.NewInt(0) - if args.Value != nil { - value = args.Value.ToInt() - } - - return &types.LegacyTx{ - Nonce: 0, - To: args.To, - Value: value, - Gas: gasLimit, - GasPrice: big.NewInt(0), - Data: data, - }, nil -} diff --git a/api/stream.go b/api/stream.go index 11e81f816..57a979de1 100644 --- a/api/stream.go +++ b/api/stream.go @@ -140,7 +140,7 @@ func (s *StreamAPI) prepareBlockHeader( TransactionsRoot: block.TransactionHashRoot, ReceiptsRoot: block.ReceiptRoot, Miner: evmTypes.CoinbaseAddress.ToCommon(), - GasLimit: hexutil.Uint64(blockGasLimit), + GasLimit: hexutil.Uint64(BlockGasLimit), Timestamp: hexutil.Uint64(block.Timestamp), } diff --git a/api/utils.go b/api/utils.go index f064c9ca6..7e056e758 100644 --- a/api/utils.go +++ b/api/utils.go @@ -4,12 +4,14 @@ import ( "encoding/hex" "errors" "fmt" + "math/big" "strings" "github.com/onflow/flow-evm-gateway/metrics" errs "github.com/onflow/flow-evm-gateway/models/errors" "github.com/onflow/flow-evm-gateway/storage" "github.com/onflow/go-ethereum/common" + "github.com/onflow/go-ethereum/core/types" "github.com/onflow/go-ethereum/rpc" "github.com/rs/zerolog" ) @@ -127,3 +129,38 @@ func handleError[T any](err error, log zerolog.Logger, collector metrics.Collect return zero, errs.ErrInternal } } + +// encodeTxFromArgs will create a transaction from the given arguments. +// The resulting unsigned transaction is only supposed to be used through +// `EVM.dryRun` inside Cadence scripts, meaning that no state change +// will occur. +// This is only useful for `eth_estimateGas` and `eth_call` endpoints. +func encodeTxFromArgs(args TransactionArgs) (*types.LegacyTx, error) { + var data []byte + if args.Data != nil { + data = *args.Data + } else if args.Input != nil { + data = *args.Input + } + + // provide a high enough gas for the tx to be able to execute, + // capped by the gas set in transaction args. + gasLimit := BlockGasLimit + if args.Gas != nil { + gasLimit = uint64(*args.Gas) + } + + value := big.NewInt(0) + if args.Value != nil { + value = args.Value.ToInt() + } + + return &types.LegacyTx{ + Nonce: 0, + To: args.To, + Value: value, + Gas: gasLimit, + GasPrice: big.NewInt(0), + Data: data, + }, nil +} diff --git a/services/requester/requester.go b/services/requester/requester.go index 58b5d91cd..fe73a191a 100644 --- a/services/requester/requester.go +++ b/services/requester/requester.go @@ -47,6 +47,7 @@ var ( const minFlowBalance = 2 const coaFundingBalance = minFlowBalance - 1 +const blockGasLimit = 120_000_000 type Requester interface { // SendRawTransaction will submit signed transaction data to the network. @@ -138,7 +139,7 @@ func NewEVM( head := &types.Header{ Number: big.NewInt(20_182_324), Time: uint64(time.Now().Unix()), - GasLimit: 30_000_000, + GasLimit: blockGasLimit, } emulatorConfig := emulator.NewConfig( emulator.WithChainID(config.EVMNetworkID), @@ -514,7 +515,7 @@ func (e *EVM) getBlockView(evmHeight uint64) (*query.View, error) { evm.StorageAccountAddress(e.config.FlowNetworkID), e.registerStore, e.blocksProvider, - 120_000_000, + blockGasLimit, ) return viewProvider.GetBlockView(evmHeight) From e1b65068f7dc233b2f30405a992244c40b5ad4a2 Mon Sep 17 00:00:00 2001 From: Ardit Marku Date: Mon, 11 Nov 2024 12:46:47 +0200 Subject: [PATCH 047/174] Minor improvements for DebugAPI --- api/debug.go | 53 ++++++++++++++++++++++++++++++---------------------- 1 file changed, 31 insertions(+), 22 deletions(-) diff --git a/api/debug.go b/api/debug.go index cedde4cef..6cb96ed40 100644 --- a/api/debug.go +++ b/api/debug.go @@ -69,10 +69,9 @@ func NewDebugAPI( } } -// TraceTransaction will return a debug execution trace of a transaction if it exists, -// currently we only support CALL traces, so the config is ignored. +// TraceTransaction will return a debug execution trace of a transaction, if it exists. func (d *DebugAPI) TraceTransaction( - ctx context.Context, + _ context.Context, hash gethCommon.Hash, config *tracers.TraceConfig, ) (json.RawMessage, error) { @@ -91,15 +90,11 @@ func (d *DebugAPI) TraceTransaction( return nil, err } - tracer, err := tracerForReceipt(config, receipt) - if err != nil { - return nil, err - } - block, err := d.blocks.GetByHeight(receipt.BlockNumber.Uint64()) if err != nil { return nil, err } + // We need to re-execute the given transaction and all the // transactions that precede it in the same block, based on // the previous block state, to generate the correct trace. @@ -113,6 +108,11 @@ func (d *DebugAPI) TraceTransaction( return nil, err } + tracer, err := tracerForReceipt(config, receipt) + if err != nil { + return nil, err + } + // Re-execute the transactions in the order they appear, for the block // that contains the given transaction. We set the tracer only for // the given transaction, as we don't need it for the preceding @@ -149,7 +149,7 @@ func (d *DebugAPI) TraceTransaction( } func (d *DebugAPI) TraceBlockByNumber( - ctx context.Context, + _ context.Context, number rpc.BlockNumber, config *tracers.TraceConfig, ) ([]*txTraceResult, error) { @@ -231,7 +231,7 @@ func (d *DebugAPI) TraceBlockByHash( } func (d *DebugAPI) TraceCall( - ctx context.Context, + _ context.Context, args TransactionArgs, blockNrOrHash rpc.BlockNumberOrHash, config *tracers.TraceCallConfig, @@ -292,21 +292,26 @@ func (d *DebugAPI) TraceCall( opts := []query.DryCallOption{} opts = append(opts, query.WithTracer(tracer)) if config.StateOverrides != nil { - for addr, overrideAccount := range *config.StateOverrides { - if overrideAccount.Nonce != nil { - opts = append(opts, query.WithStateOverrideNonce(addr, uint64(*overrideAccount.Nonce))) + for addr, account := range *config.StateOverrides { + // Override account nonce. + if account.Nonce != nil { + opts = append(opts, query.WithStateOverrideNonce(addr, uint64(*account.Nonce))) } - if overrideAccount.Code != nil { - opts = append(opts, query.WithStateOverrideCode(addr, *overrideAccount.Code)) + // Override account(contract) code. + if account.Code != nil { + opts = append(opts, query.WithStateOverrideCode(addr, *account.Code)) } - if overrideAccount.Balance != nil { - opts = append(opts, query.WithStateOverrideBalance(addr, (*big.Int)(*overrideAccount.Balance))) + // Override account balance. + if account.Balance != nil { + opts = append(opts, query.WithStateOverrideBalance(addr, (*big.Int)(*account.Balance))) } - if overrideAccount.State != nil { - opts = append(opts, query.WithStateOverrideState(addr, *overrideAccount.State)) + // Replace entire state if caller requires. + if account.State != nil { + opts = append(opts, query.WithStateOverrideState(addr, *account.State)) } - if overrideAccount.StateDiff != nil { - opts = append(opts, query.WithStateOverrideStateDiff(addr, *overrideAccount.StateDiff)) + // Apply state diff into specified accounts. + if account.StateDiff != nil { + opts = append(opts, query.WithStateOverrideStateDiff(addr, *account.StateDiff)) } } } @@ -329,7 +334,11 @@ func (d *DebugAPI) TraceCall( func (d *DebugAPI) executorAtBlock(block *models.Block) (*evm.BlockExecutor, error) { snapshot, err := d.registerStore.GetSnapshotAt(block.Height) if err != nil { - return nil, fmt.Errorf("failed to get register snapshot at block height %d: %w", block.Height, err) + return nil, fmt.Errorf( + "failed to get register snapshot at block height %d: %w", + block.Height, + err, + ) } ledger := storage.NewRegisterDelta(snapshot) From 7ad60894bc401eb5c63e29d299c959fae4aaacda Mon Sep 17 00:00:00 2001 From: Ardit Marku Date: Mon, 11 Nov 2024 12:52:23 +0200 Subject: [PATCH 048/174] Remove redundant error return value from BlockExecutor constructor --- api/debug.go | 2 +- services/evm/executor.go | 6 +++--- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/api/debug.go b/api/debug.go index 6cb96ed40..b517d7335 100644 --- a/api/debug.go +++ b/api/debug.go @@ -349,7 +349,7 @@ func (d *DebugAPI) executorAtBlock(block *models.Block) (*evm.BlockExecutor, err d.blocks, d.receipts, d.logger, - ) + ), nil } func tracerForReceipt( diff --git a/services/evm/executor.go b/services/evm/executor.go index 7d9304de7..a4ce19dea 100644 --- a/services/evm/executor.go +++ b/services/evm/executor.go @@ -37,8 +37,8 @@ func NewBlockExecutor( blocks storage.BlockIndexer, receipts storage.ReceiptIndexer, logger zerolog.Logger, -) (*BlockExecutor, error) { - logger = logger.With().Str("component", "state-execution").Logger() +) *BlockExecutor { + logger = logger.With().Str("component", "trace-generation").Logger() storageAddress := evm.StorageAccountAddress(chainID) return &BlockExecutor{ @@ -48,7 +48,7 @@ func NewBlockExecutor( blocks: blocks, receipts: receipts, logger: logger, - }, nil + } } func (s *BlockExecutor) Run( From c5083dccd585f4a4bd3e10be92c3a0c56d916836 Mon Sep 17 00:00:00 2001 From: Ardit Marku Date: Mon, 11 Nov 2024 12:56:25 +0200 Subject: [PATCH 049/174] Minor improvements for event ingestion engine --- services/ingestion/engine.go | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/services/ingestion/engine.go b/services/ingestion/engine.go index 40691afcd..80594c1e3 100644 --- a/services/ingestion/engine.go +++ b/services/ingestion/engine.go @@ -184,8 +184,7 @@ func (e *Engine) processEvents(events *models.CadenceEvents) error { return err } - blockEvents := events.BlockEventPayload() - cr := sync.NewReplayer( + replayer := sync.NewReplayer( e.replayerConfig.ChainID, e.replayerConfig.RootAddr, e.registerStore, @@ -197,7 +196,8 @@ func (e *Engine) processEvents(events *models.CadenceEvents) error { // Step 1.2: Replay all block transactions // If `ReplayBlock` returns any error, we abort the EVM events processing - res, err := cr.ReplayBlock(events.TxEventPayloads(), blockEvents) + blockEvents := events.BlockEventPayload() + res, err := replayer.ReplayBlock(events.TxEventPayloads(), blockEvents) if err != nil { return fmt.Errorf("failed to replay block on height: %d, with: %w", events.Block().Height, err) } From 1daf28a23eca840c5b9b37160102e751a6f5ab0a Mon Sep 17 00:00:00 2001 From: Ardit Marku Date: Mon, 11 Nov 2024 13:02:10 +0200 Subject: [PATCH 050/174] Minor improvements for CallTracerCollector --- services/replayer/call_tracer_collector.go | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/services/replayer/call_tracer_collector.go b/services/replayer/call_tracer_collector.go index 171ccc00c..dcb3fee47 100644 --- a/services/replayer/call_tracer_collector.go +++ b/services/replayer/call_tracer_collector.go @@ -74,7 +74,7 @@ func (ct *CallTracerCollector) Collect(txID common.Hash) (json.RawMessage, error // collect the trace result result, found := ct.resultsByTxID[txID] if !found { - return nil, fmt.Errorf("trace result not found") + return nil, fmt.Errorf("trace result for tx: %s, not found", txID.String()) } // remove the result @@ -197,7 +197,7 @@ func NewSafeTxTracer(ct *CallTracerCollector) *tracers.Tracer { var NopTracer = &nopTracer{} -var _ EVMTracer = &nopTracer{} +var _ EVMTracer = (*nopTracer)(nil) type nopTracer struct{} From be3728d97fe6102d2982e643b210a033e63465d9 Mon Sep 17 00:00:00 2001 From: Ardit Marku Date: Mon, 11 Nov 2024 14:17:01 +0200 Subject: [PATCH 051/174] Remove Cadence script caching from Requester --- api/api.go | 28 +++++++++--------- cmd/run/cmd.go | 1 - config/config.go | 2 -- go.mod | 2 +- services/requester/requester.go | 52 ++++++++++++++------------------- 5 files changed, 37 insertions(+), 48 deletions(-) diff --git a/api/api.go b/api/api.go index 5e49af91f..23d90d137 100644 --- a/api/api.go +++ b/api/api.go @@ -282,12 +282,12 @@ func (b *BlockChainAPI) GetBalance( return nil, err } - evmHeight, err := resolveBlockTag(&blockNumberOrHash, b.blocks, b.logger) + height, err := resolveBlockTag(&blockNumberOrHash, b.blocks, b.logger) if err != nil { return handleError[*hexutil.Big](err, l, b.collector) } - balance, err := b.evm.GetBalance(address, evmHeight) + balance, err := b.evm.GetBalance(address, height) if err != nil { return handleError[*hexutil.Big](err, l, b.collector) } @@ -518,12 +518,12 @@ func (b *BlockChainAPI) GetBlockReceipts( return nil, err } - evmHeight, err := resolveBlockTag(&blockNumberOrHash, b.blocks, b.logger) + height, err := resolveBlockTag(&blockNumberOrHash, b.blocks, b.logger) if err != nil { return handleError[[]map[string]interface{}](err, l, b.collector) } - block, err := b.blocks.GetByHeight(evmHeight) + block, err := b.blocks.GetByHeight(height) if err != nil { return handleError[[]map[string]interface{}](err, l, b.collector) } @@ -635,7 +635,7 @@ func (b *BlockChainAPI) Call( blockNumberOrHash = &latestBlockNumberOrHash } - evmHeight, err := resolveBlockTag(blockNumberOrHash, b.blocks, b.logger) + height, err := resolveBlockTag(blockNumberOrHash, b.blocks, b.logger) if err != nil { return handleError[hexutil.Bytes](err, l, b.collector) } @@ -651,7 +651,7 @@ func (b *BlockChainAPI) Call( from = *args.From } - res, err := b.evm.Call(tx, from, evmHeight) + res, err := b.evm.Call(tx, from, height) if err != nil { return handleError[hexutil.Bytes](err, l, b.collector) } @@ -753,12 +753,12 @@ func (b *BlockChainAPI) GetTransactionCount( return nil, err } - evmHeight, err := resolveBlockTag(&blockNumberOrHash, b.blocks, b.logger) + height, err := resolveBlockTag(&blockNumberOrHash, b.blocks, b.logger) if err != nil { return handleError[*hexutil.Uint64](err, l, b.collector) } - networkNonce, err := b.evm.GetNonce(address, evmHeight) + networkNonce, err := b.evm.GetNonce(address, height) if err != nil { return handleError[*hexutil.Uint64](err, l, b.collector) } @@ -819,12 +819,12 @@ func (b *BlockChainAPI) EstimateGas( blockNumberOrHash = &latestBlockNumberOrHash } - evmHeight, err := resolveBlockTag(blockNumberOrHash, b.blocks, b.logger) + height, err := resolveBlockTag(blockNumberOrHash, b.blocks, b.logger) if err != nil { return handleError[hexutil.Uint64](err, l, b.collector) } - estimatedGas, err := b.evm.EstimateGas(tx, from, evmHeight) + estimatedGas, err := b.evm.EstimateGas(tx, from, height) if err != nil { return handleError[hexutil.Uint64](err, l, b.collector) } @@ -848,12 +848,12 @@ func (b *BlockChainAPI) GetCode( return nil, err } - evmHeight, err := resolveBlockTag(&blockNumberOrHash, b.blocks, b.logger) + height, err := resolveBlockTag(&blockNumberOrHash, b.blocks, b.logger) if err != nil { return handleError[hexutil.Bytes](err, l, b.collector) } - code, err := b.evm.GetCode(address, evmHeight) + code, err := b.evm.GetCode(address, height) if err != nil { return handleError[hexutil.Bytes](err, l, b.collector) } @@ -973,12 +973,12 @@ func (b *BlockChainAPI) GetStorageAt( ) } - evmHeight, err := resolveBlockTag(&blockNumberOrHash, b.blocks, b.logger) + height, err := resolveBlockTag(&blockNumberOrHash, b.blocks, b.logger) if err != nil { return handleError[hexutil.Bytes](err, l, b.collector) } - result, err := b.evm.GetStorageAt(address, key, evmHeight) + result, err := b.evm.GetStorageAt(address, key, height) if err != nil { return handleError[hexutil.Bytes](err, l, b.collector) } diff --git a/cmd/run/cmd.go b/cmd/run/cmd.go index 332ae7c01..3b93e45a1 100644 --- a/cmd/run/cmd.go +++ b/cmd/run/cmd.go @@ -262,7 +262,6 @@ func init() { Cmd.Flags().Float64Var(&cfg.StreamLimit, "stream-limit", 10, "Rate-limits the events sent to the client within one second") Cmd.Flags().Uint64Var(&cfg.RateLimit, "rate-limit", 50, "Rate-limit requests per second made by the client over any protocol (ws/http)") Cmd.Flags().StringVar(&cfg.AddressHeader, "address-header", "", "Address header that contains the client IP, this is useful when the server is behind a proxy that sets the source IP of the client. Leave empty if no proxy is used.") - Cmd.Flags().UintVar(&cfg.CacheSize, "script-cache-size", 10000, "Cache size used for script execution in items kept in cache") Cmd.Flags().IntVar(&streamTimeout, "stream-timeout", 3, "Defines the timeout in seconds the server waits for the event to be sent to the client") Cmd.Flags().Uint64Var(&forceStartHeight, "force-start-height", 0, "Force set starting Cadence height. WARNING: This should only be used locally or for testing, never in production.") Cmd.Flags().StringVar(&filterExpiry, "filter-expiry", "5m", "Filter defines the time it takes for an idle filter to expire") diff --git a/config/config.go b/config/config.go index ef71500be..ee3d37ac4 100644 --- a/config/config.go +++ b/config/config.go @@ -82,8 +82,6 @@ type Config struct { MetricsPort int // IndexOnly configures the gateway to not accept any transactions but only queries of the state IndexOnly bool - // Cache size in units of items in cache, one unit in cache takes approximately 64 bytes - CacheSize uint // ProfilerEnabled sets whether the profiler server is enabled ProfilerEnabled bool // ProfilerHost is the host for the profiler server will listen to (e.g. localhost, 0.0.0.0) diff --git a/go.mod b/go.mod index 41485ae78..335ec24ef 100644 --- a/go.mod +++ b/go.mod @@ -5,7 +5,6 @@ go 1.22 require ( github.com/cockroachdb/pebble v1.1.1 github.com/goccy/go-json v0.10.2 - github.com/hashicorp/golang-lru/v2 v2.0.7 github.com/onflow/atree v0.8.0 github.com/onflow/cadence v1.2.1 github.com/onflow/flow-go v0.38.0-preview.0.0.20241022154145-6a254edbec23 @@ -92,6 +91,7 @@ require ( github.com/hashicorp/errwrap v1.1.0 // indirect github.com/hashicorp/go-multierror v1.1.1 // indirect github.com/hashicorp/golang-lru v1.0.2 // indirect + github.com/hashicorp/golang-lru/v2 v2.0.7 // indirect github.com/hashicorp/hcl v1.0.0 // indirect github.com/holiman/bloomfilter/v2 v2.0.3 // indirect github.com/holiman/uint256 v1.3.0 // indirect diff --git a/services/requester/requester.go b/services/requester/requester.go index fe73a191a..a703e81cf 100644 --- a/services/requester/requester.go +++ b/services/requester/requester.go @@ -9,7 +9,6 @@ import ( "sync" "time" - "github.com/hashicorp/golang-lru/v2/expirable" "github.com/onflow/cadence" "github.com/onflow/flow-go-sdk" "github.com/onflow/flow-go-sdk/crypto" @@ -56,27 +55,27 @@ type Requester interface { // GetBalance returns the amount of wei for the given address in the state of the // given EVM block height. - GetBalance(address common.Address, evmHeight uint64) (*big.Int, error) + GetBalance(address common.Address, height uint64) (*big.Int, error) // Call executes the given signed transaction data on the state for the given EVM block height. // Note, this function doesn't make and changes in the state/blockchain and is // useful to execute and retrieve values. - Call(tx *types.LegacyTx, from common.Address, evmHeight uint64) ([]byte, error) + Call(tx *types.LegacyTx, from common.Address, height uint64) ([]byte, error) // EstimateGas executes the given signed transaction data on the state for the given EVM block height. // Note, this function doesn't make any changes in the state/blockchain and is // useful to executed and retrieve the gas consumption and possible failures. - EstimateGas(tx *types.LegacyTx, from common.Address, evmHeight uint64) (uint64, error) + EstimateGas(tx *types.LegacyTx, from common.Address, height uint64) (uint64, error) // GetNonce gets nonce from the network at the given EVM block height. - GetNonce(address common.Address, evmHeight uint64) (uint64, error) + GetNonce(address common.Address, height uint64) (uint64, error) // GetCode returns the code stored at the given address in // the state for the given EVM block height. - GetCode(address common.Address, evmHeight uint64) ([]byte, error) + GetCode(address common.Address, height uint64) ([]byte, error) // GetStorageAt returns the storage from the state at the given address, key and block number. - GetStorageAt(address common.Address, hash common.Hash, evmHeight uint64) (common.Hash, error) + GetStorageAt(address common.Address, hash common.Hash, height uint64) (common.Hash, error) // GetLatestEVMHeight returns the latest EVM height of the network. GetLatestEVMHeight(ctx context.Context) (uint64, error) @@ -94,7 +93,6 @@ type EVM struct { logger zerolog.Logger blocks storage.BlockIndexer mux sync.Mutex - scriptCache *expirable.LRU[string, cadence.Value] head *types.Header evmSigner types.Signer @@ -158,11 +156,6 @@ func NewEVM( MinTip: new(big.Int), } - var cache *expirable.LRU[string, cadence.Value] - if config.CacheSize != 0 { - cache = expirable.NewLRU[string, cadence.Value](int(config.CacheSize), nil, time.Second) - } - evm := &EVM{ registerStore: registerStore, blocksProvider: blocksProvider, @@ -176,7 +169,6 @@ func NewEVM( evmSigner: evmSigner, validationOptions: validationOptions, collector: collector, - scriptCache: cache, } // create COA on the account @@ -309,9 +301,9 @@ func (e *EVM) buildTransaction(ctx context.Context, script []byte, args ...caden func (e *EVM) GetBalance( address common.Address, - evmHeight uint64, + height uint64, ) (*big.Int, error) { - view, err := e.getBlockView(evmHeight) + view, err := e.getBlockView(height) if err != nil { return nil, err } @@ -321,9 +313,9 @@ func (e *EVM) GetBalance( func (e *EVM) GetNonce( address common.Address, - evmHeight uint64, + height uint64, ) (uint64, error) { - view, err := e.getBlockView(evmHeight) + view, err := e.getBlockView(height) if err != nil { return 0, err } @@ -334,9 +326,9 @@ func (e *EVM) GetNonce( func (e *EVM) GetStorageAt( address common.Address, hash common.Hash, - evmHeight uint64, + height uint64, ) (common.Hash, error) { - view, err := e.getBlockView(evmHeight) + view, err := e.getBlockView(height) if err != nil { return common.Hash{}, err } @@ -347,9 +339,9 @@ func (e *EVM) GetStorageAt( func (e *EVM) Call( tx *types.LegacyTx, from common.Address, - evmHeight uint64, + height uint64, ) ([]byte, error) { - view, err := e.getBlockView(evmHeight) + view, err := e.getBlockView(height) if err != nil { return nil, err } @@ -358,7 +350,7 @@ func (e *EVM) Call( if tx.To != nil { to = *tx.To } - cdcHeight, err := e.evmToCadenceHeight(evmHeight) + cdcHeight, err := e.evmToCadenceHeight(height) if err != nil { return nil, err } @@ -386,9 +378,9 @@ func (e *EVM) Call( func (e *EVM) EstimateGas( tx *types.LegacyTx, from common.Address, - evmHeight uint64, + height uint64, ) (uint64, error) { - view, err := e.getBlockView(evmHeight) + view, err := e.getBlockView(height) if err != nil { return 0, err } @@ -397,7 +389,7 @@ func (e *EVM) EstimateGas( if tx.To != nil { to = *tx.To } - cdcHeight, err := e.evmToCadenceHeight(evmHeight) + cdcHeight, err := e.evmToCadenceHeight(height) if err != nil { return 0, err } @@ -448,9 +440,9 @@ func (e *EVM) EstimateGas( func (e *EVM) GetCode( address common.Address, - evmHeight uint64, + height uint64, ) ([]byte, error) { - view, err := e.getBlockView(evmHeight) + view, err := e.getBlockView(height) if err != nil { return nil, err } @@ -509,7 +501,7 @@ func (e *EVM) getSignerNetworkInfo(ctx context.Context) (uint32, uint64, error) ) } -func (e *EVM) getBlockView(evmHeight uint64) (*query.View, error) { +func (e *EVM) getBlockView(height uint64) (*query.View, error) { viewProvider := query.NewViewProvider( e.config.FlowNetworkID, evm.StorageAccountAddress(e.config.FlowNetworkID), @@ -518,7 +510,7 @@ func (e *EVM) getBlockView(evmHeight uint64) (*query.View, error) { blockGasLimit, ) - return viewProvider.GetBlockView(evmHeight) + return viewProvider.GetBlockView(height) } func (e *EVM) evmToCadenceHeight(height uint64) (uint64, error) { From 2c663f4d5819c0089b02a83d2d710dc08a2c0a1a Mon Sep 17 00:00:00 2001 From: Ardit Marku Date: Mon, 11 Nov 2024 14:50:20 +0200 Subject: [PATCH 052/174] Properly initialize trace call config in debug_traceCall --- api/debug.go | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/api/debug.go b/api/debug.go index b517d7335..4f55b38f0 100644 --- a/api/debug.go +++ b/api/debug.go @@ -247,12 +247,11 @@ func (d *DebugAPI) TraceCall( from = *args.From } - var traceConfig *tracers.TraceConfig - if config != nil { - traceConfig = &config.TraceConfig + if config == nil { + config = &tracers.TraceCallConfig{} } - tracer, err := tracerForReceipt(traceConfig, nil) + tracer, err := tracerForReceipt(&config.TraceConfig, nil) if err != nil { return nil, err } From a0aa4587a2a1c09053a8fb05c61e71eda94ea9ed Mon Sep 17 00:00:00 2001 From: Ardit Marku Date: Mon, 11 Nov 2024 15:41:56 +0200 Subject: [PATCH 053/174] Add test case for debug_traceTransaction with Cadence arch call --- tests/web3js/debug_traces_test.js | 63 ++++++++++++++++++++++++++++++- 1 file changed, 61 insertions(+), 2 deletions(-) diff --git a/tests/web3js/debug_traces_test.js b/tests/web3js/debug_traces_test.js index 75794cd03..db13797a5 100644 --- a/tests/web3js/debug_traces_test.js +++ b/tests/web3js/debug_traces_test.js @@ -225,6 +225,58 @@ it('should retrieve transaction traces', async () => { } ] ) + + callTracer = { + tracer: 'callTracer', + tracerConfig: { + onlyTopCall: false + } + } + + // submit a transaction that calls verifyArchCallToFlowBlockHeight() + let flowBlockHeightData = deployed.contract.methods.verifyArchCallToFlowBlockHeight().encodeABI() + res = await helpers.signAndSend({ + from: conf.eoa.address, + to: contractAddress, + data: flowBlockHeightData, + value: '0', + gasPrice: conf.minGasPrice, + }) + assert.equal(res.receipt.status, conf.successStatus) + + response = await helpers.callRPCMethod( + 'debug_traceTransaction', + [web3.utils.toHex(res.receipt.transactionHash), callTracer] + ) + assert.equal(response.status, 200) + assert.isDefined(response.body.result) + + txTrace = response.body.result + + assert.deepEqual( + txTrace, + { + from: conf.eoa.address.toLowerCase(), + gas: '0xc9c7', + gasUsed: '0x6147', + to: contractAddress.toLowerCase(), + input: '0xc550f90f', + output: '0x0000000000000000000000000000000000000000000000000000000000000006', + calls: [ + { + from: contractAddress.toLowerCase(), + gas: '0x6948', + gasUsed: '0x2', + to: '0x0000000000000000000000010000000000000001', + input: '0x53e87d66', + output: '0x0000000000000000000000000000000000000000000000000000000000000006', + type: 'STATICCALL' + } + ], + value: '0x0', + type: 'CALL' + } + ) }) it('should retrieve call traces', async () => { @@ -313,9 +365,16 @@ it('should retrieve call traces', async () => { assert.deepEqual( txTrace, { - post: { '0xfacf71692421039876a5bb4f10ef7a439d8ef61e': { nonce: 3 } }, + post: { + '0xfacf71692421039876a5bb4f10ef7a439d8ef61e': { + nonce: 4 + } + }, pre: { - '0xfacf71692421039876a5bb4f10ef7a439d8ef61e': { balance: '0x456391823a9b6fc6', nonce: 2 } + '0xfacf71692421039876a5bb4f10ef7a439d8ef61e': { + balance: '0x456391823a62702c', + nonce: 3 + } } } ) From 603cd32b888f0d9a9d306a1ed9c8270820404607 Mon Sep 17 00:00:00 2001 From: Ardit Marku Date: Mon, 11 Nov 2024 17:29:02 +0200 Subject: [PATCH 054/174] Enable Cadence Arch calls for debug_traceCall --- api/debug.go | 13 ++++++++ bootstrap/bootstrap.go | 1 + tests/web3js/debug_traces_test.js | 50 +++++++++++++++++++++++++++++++ 3 files changed, 64 insertions(+) diff --git a/api/debug.go b/api/debug.go index 4f55b38f0..ca34c3bc4 100644 --- a/api/debug.go +++ b/api/debug.go @@ -8,6 +8,7 @@ import ( "github.com/goccy/go-json" "github.com/onflow/flow-go/fvm/evm/offchain/query" + "github.com/onflow/flow-go/fvm/evm/types" gethCommon "github.com/onflow/go-ethereum/common" "github.com/onflow/go-ethereum/eth/tracers" "github.com/onflow/go-ethereum/eth/tracers/logger" @@ -19,6 +20,7 @@ import ( "github.com/onflow/flow-evm-gateway/models" "github.com/onflow/flow-evm-gateway/services/evm" "github.com/onflow/flow-evm-gateway/services/replayer" + "github.com/onflow/flow-evm-gateway/services/requester" "github.com/onflow/flow-evm-gateway/storage" "github.com/onflow/flow-evm-gateway/storage/pebble" flowEVM "github.com/onflow/flow-go/fvm/evm" @@ -43,6 +45,7 @@ type DebugAPI struct { blocks storage.BlockIndexer transactions storage.TransactionIndexer receipts storage.ReceiptIndexer + client *requester.CrossSporkClient config *config.Config collector metrics.Collector } @@ -53,6 +56,7 @@ func NewDebugAPI( blocks storage.BlockIndexer, transactions storage.TransactionIndexer, receipts storage.ReceiptIndexer, + client *requester.CrossSporkClient, config *config.Config, logger zerolog.Logger, collector metrics.Collector, @@ -64,6 +68,7 @@ func NewDebugAPI( blocks: blocks, transactions: transactions, receipts: receipts, + client: client, config: config, collector: collector, } @@ -261,6 +266,11 @@ func (d *DebugAPI) TraceCall( return nil, err } + cdcHeight, err := d.blocks.GetCadenceHeight(height) + if err != nil { + return nil, err + } + block, err := d.blocks.GetByHeight(height) if err != nil { return nil, err @@ -288,8 +298,11 @@ func (d *DebugAPI) TraceCall( if tx.To != nil { to = *tx.To } + rca := requester.NewRemoteCadenceArch(cdcHeight, d.client, d.config.FlowNetworkID) + opts := []query.DryCallOption{} opts = append(opts, query.WithTracer(tracer)) + opts = append(opts, query.WithExtraPrecompiledContracts([]types.PrecompiledContract{rca})) if config.StateOverrides != nil { for addr, account := range *config.StateOverrides { // Override account nonce. diff --git a/bootstrap/bootstrap.go b/bootstrap/bootstrap.go index 06cf93468..6ea08b35b 100644 --- a/bootstrap/bootstrap.go +++ b/bootstrap/bootstrap.go @@ -285,6 +285,7 @@ func (b *Bootstrap) StartAPIServer(ctx context.Context) error { b.storages.Blocks, b.storages.Transactions, b.storages.Receipts, + b.client, b.config, b.logger, b.collector, diff --git a/tests/web3js/debug_traces_test.js b/tests/web3js/debug_traces_test.js index db13797a5..da20c5a71 100644 --- a/tests/web3js/debug_traces_test.js +++ b/tests/web3js/debug_traces_test.js @@ -511,4 +511,54 @@ it('should retrieve call traces', async () => { callTrace.output, '0x00000000000000000000000000000000000000000000000000000000000005dc' ) + + let flowBlockHeightData = deployed.contract.methods.verifyArchCallToFlowBlockHeight().encodeABI() + traceCall = { + from: conf.eoa.address, + to: contractAddress, + gas: '0xcdd4', + data: flowBlockHeightData, + value: '0x0', + gasPrice: web3.utils.toHex(conf.minGasPrice), + } + + callTracer = { + tracer: 'callTracer', + tracerConfig: { + onlyTopCall: false + } + } + + response = await helpers.callRPCMethod( + 'debug_traceCall', + [traceCall, web3.utils.toHex(latestHeight), callTracer] + ) + assert.equal(response.status, 200) + assert.isDefined(response.body) + + callTrace = response.body.result + assert.deepEqual( + callTrace, + { + from: conf.eoa.address.toLowerCase(), + gas: '0xcdd4', + gasUsed: '0xbdd4', + to: contractAddress.toLowerCase(), + input: '0xc550f90f', + output: '0x0000000000000000000000000000000000000000000000000000000000000007', + calls: [ + { + from: contractAddress.toLowerCase(), + gas: '0x6d44', + gasUsed: '0x5c8f', + to: '0x0000000000000000000000010000000000000001', + input: '0x53e87d66', + output: '0x0000000000000000000000000000000000000000000000000000000000000007', + type: 'STATICCALL' + } + ], + value: '0x0', + type: 'CALL' + } + ) }) From b4368e1b06404a2501c19ebc86fd206c20bb7486 Mon Sep 17 00:00:00 2001 From: Ardit Marku Date: Tue, 12 Nov 2024 12:23:04 +0200 Subject: [PATCH 055/174] Remove unused embed import --- api/api.go | 1 - 1 file changed, 1 deletion(-) diff --git a/api/api.go b/api/api.go index 23d90d137..1b24bf30e 100644 --- a/api/api.go +++ b/api/api.go @@ -2,7 +2,6 @@ package api import ( "context" - _ "embed" "fmt" "math/big" From b54fb43fa25fc55f05f61440969b85116e47e6c5 Mon Sep 17 00:00:00 2001 From: Ardit Marku Date: Tue, 12 Nov 2024 15:43:06 +0200 Subject: [PATCH 056/174] Move gas limit for Cadence Arch call to a named constant --- services/requester/remote_cadence_arch.go | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/services/requester/remote_cadence_arch.go b/services/requester/remote_cadence_arch.go index 28e331ea2..04a530af7 100644 --- a/services/requester/remote_cadence_arch.go +++ b/services/requester/remote_cadence_arch.go @@ -17,6 +17,8 @@ import ( "github.com/onflow/go-ethereum/crypto" ) +const cadenceArchCallGasLimit = 155_000 + var ( //go:embed cadence/dry_run.cdc dryRunScript []byte @@ -79,7 +81,7 @@ func (rca *RemoteCadenceArch) runCall(input []byte) (*evmTypes.ResultSummary, er Nonce: 0, To: &cadenceArchAddress, Value: big.NewInt(0), - Gas: 155_000, + Gas: cadenceArchCallGasLimit, GasPrice: big.NewInt(0), Data: input, }, From fb097d63d6e4f64b0a2af54d2deb97f69ead9083 Mon Sep 17 00:00:00 2001 From: Ardit Marku Date: Tue, 12 Nov 2024 16:10:13 +0200 Subject: [PATCH 057/174] Better handling of errors for stored default call traces --- api/debug.go | 18 ++++++++++++++++-- 1 file changed, 16 insertions(+), 2 deletions(-) diff --git a/api/debug.go b/api/debug.go index ca34c3bc4..9d68cefae 100644 --- a/api/debug.go +++ b/api/debug.go @@ -2,6 +2,7 @@ package api import ( "context" + "errors" "fmt" "math/big" "slices" @@ -18,6 +19,7 @@ import ( "github.com/onflow/flow-evm-gateway/config" "github.com/onflow/flow-evm-gateway/metrics" "github.com/onflow/flow-evm-gateway/models" + errs "github.com/onflow/flow-evm-gateway/models/errors" "github.com/onflow/flow-evm-gateway/services/evm" "github.com/onflow/flow-evm-gateway/services/replayer" "github.com/onflow/flow-evm-gateway/services/requester" @@ -85,9 +87,21 @@ func (d *DebugAPI) TraceTransaction( // from the Traces DB. if isDefaultCallTracer(config) { trace, err := d.tracer.GetTransaction(hash) + // If there is no error, we return the trace result from the DB. if err == nil { return trace, nil } + + // If we got an error of `ErrEntityNotFound`, for whatever reason, + // we simply re-compute the trace below. If we got any other error, + // we return it. + if !errors.Is(err, errs.ErrEntityNotFound) { + d.logger.Error().Err(err).Msgf( + "failed to retrieve default call trace for tx: %s", + hash, + ) + return nil, err + } } receipt, err := d.receipts.GetByTransactionID(hash) @@ -154,7 +168,7 @@ func (d *DebugAPI) TraceTransaction( } func (d *DebugAPI) TraceBlockByNumber( - _ context.Context, + ctx context.Context, number rpc.BlockNumber, config *tracers.TraceConfig, ) ([]*txTraceResult, error) { @@ -170,7 +184,7 @@ func (d *DebugAPI) TraceBlockByNumber( // from the Traces DB. if isDefaultCallTracer(config) { for i, hash := range block.TransactionHashes { - trace, err := d.tracer.GetTransaction(hash) + trace, err := d.TraceTransaction(ctx, hash, config) if err != nil { results[i] = &txTraceResult{TxHash: hash, Error: err.Error()} From dfcfdb0713d1ff3ab0eadcc5c95f67027cec5cf9 Mon Sep 17 00:00:00 2001 From: Janez Podhostnik Date: Thu, 7 Nov 2024 22:02:04 +0100 Subject: [PATCH 058/174] Storage fixes --- Makefile | 1 - api/api.go | 16 - bootstrap/bootstrap.go | 24 +- services/ingestion/engine.go | 30 +- services/ingestion/engine_test.go | 30 -- services/replayer/blocks_provider_test.go | 34 +- storage/index.go | 15 - storage/{index_testsuite.go => index_test.go} | 312 ++++++++++++------ storage/mocks/AccountIndexer.go | 109 ------ storage/pebble/accounts.go | 125 ------- storage/pebble/keys.go | 34 +- storage/pebble/ledger.go | 114 ------- storage/pebble/receipts.go | 29 +- storage/pebble/register_storage.go | 18 +- storage/pebble/storage.go | 40 +-- storage/pebble/storage_test.go | 81 +---- storage/register_delta.go | 53 ++- 17 files changed, 384 insertions(+), 681 deletions(-) rename storage/{index_testsuite.go => index_test.go} (71%) delete mode 100644 storage/mocks/AccountIndexer.go delete mode 100644 storage/pebble/accounts.go delete mode 100644 storage/pebble/ledger.go diff --git a/Makefile b/Makefile index 9205c2706..5d53182cc 100644 --- a/Makefile +++ b/Makefile @@ -33,7 +33,6 @@ generate: mockery --dir=storage --name=BlockIndexer --output=storage/mocks mockery --dir=storage --name=ReceiptIndexer --output=storage/mocks mockery --dir=storage --name=TransactionIndexer --output=storage/mocks - mockery --dir=storage --name=AccountIndexer --output=storage/mocks mockery --dir=storage --name=TraceIndexer --output=storage/mocks mockery --all --dir=services/ingestion --output=services/ingestion/mocks mockery --dir=models --name=Engine --output=models/mocks diff --git a/api/api.go b/api/api.go index 1b24bf30e..87d3fa628 100644 --- a/api/api.go +++ b/api/api.go @@ -154,7 +154,6 @@ type BlockChainAPI struct { blocks storage.BlockIndexer transactions storage.TransactionIndexer receipts storage.ReceiptIndexer - accounts storage.AccountIndexer indexingResumedHeight uint64 limiter limiter.Store collector metrics.Collector @@ -167,7 +166,6 @@ func NewBlockChainAPI( blocks storage.BlockIndexer, transactions storage.TransactionIndexer, receipts storage.ReceiptIndexer, - accounts storage.AccountIndexer, ratelimiter limiter.Store, collector metrics.Collector, ) (*BlockChainAPI, error) { @@ -184,7 +182,6 @@ func NewBlockChainAPI( blocks: blocks, transactions: transactions, receipts: receipts, - accounts: accounts, indexingResumedHeight: indexingResumedHeight, limiter: ratelimiter, collector: collector, @@ -762,19 +759,6 @@ func (b *BlockChainAPI) GetTransactionCount( return handleError[*hexutil.Uint64](err, l, b.collector) } - nonce, err := b.accounts.GetNonce(address) - if err != nil { - return handleError[*hexutil.Uint64](errs.ErrInternal, l, b.collector) - } - - // compare both until we gain confidence in db nonce tracking working correctly - if nonce != networkNonce { - l.Error(). - Uint64("network-nonce", networkNonce). - Uint64("db-nonce", nonce). - Msg("network nonce does not equal db nonce") - } - return (*hexutil.Uint64)(&networkNonce), nil } diff --git a/bootstrap/bootstrap.go b/bootstrap/bootstrap.go index 6ea08b35b..34467fdf8 100644 --- a/bootstrap/bootstrap.go +++ b/bootstrap/bootstrap.go @@ -38,7 +38,6 @@ type Storages struct { Blocks storage.BlockIndexer Transactions storage.TransactionIndexer Receipts storage.ReceiptIndexer - Accounts storage.AccountIndexer Traces storage.TraceIndexer } @@ -156,7 +155,6 @@ func (b *Bootstrap) StartEventIngestion(ctx context.Context) error { b.storages.Blocks, b.storages.Receipts, b.storages.Transactions, - b.storages.Accounts, b.storages.Traces, b.publishers.Block, b.publishers.Logs, @@ -251,7 +249,6 @@ func (b *Bootstrap) StartAPIServer(ctx context.Context) error { b.storages.Blocks, b.storages.Transactions, b.storages.Receipts, - b.storages.Accounts, ratelimiter, b.collector, ) @@ -388,6 +385,16 @@ func (b *Bootstrap) StopProfilerServer() { } } +func (b *Bootstrap) StopDB() { + if b.storages == nil || b.storages.Storage == nil { + return + } + err := b.storages.Storage.Close() + if err != nil { + b.logger.Err(err).Msg("PebbleDB graceful shutdown failed") + } +} + // StartEngine starts provided engine and panics if there are startup errors. func StartEngine( ctx context.Context, @@ -488,12 +495,13 @@ func setupStorage( }(batch) cadenceHeight := config.InitCadenceHeight + evmBlokcHeight := uint64(0) cadenceBlock, err := client.GetBlockHeaderByHeight(context.Background(), cadenceHeight) if err != nil { return nil, fmt.Errorf("could not fetch provided cadence height, make sure it's correct: %w", err) } - snapshot, err := registerStore.GetSnapshotAt(0) + snapshot, err := registerStore.GetSnapshotAt(evmBlokcHeight) if err != nil { return nil, fmt.Errorf("could not get register snapshot at block height %d: %w", 0, err) } @@ -509,7 +517,7 @@ func setupStorage( return nil, fmt.Errorf("could not set account status: %w", err) } - err = registerStore.Store(delta.GetUpdates(), cadenceHeight, batch) + err = registerStore.Store(delta.GetUpdates(), evmBlokcHeight, batch) if err != nil { return nil, fmt.Errorf("could not store register updates: %w", err) } @@ -528,7 +536,9 @@ func setupStorage( return nil, fmt.Errorf("could not commit register updates: %w", err) } - logger.Info().Msgf("database initialized with cadence height: %d", cadenceHeight) + logger.Info(). + Stringer("fvm_address_for_evm_storage_account", storageAddress). + Msgf("database initialized with cadence height: %d", cadenceHeight) } //else { // // TODO(JanezP): verify storage account owner is correct @@ -540,7 +550,6 @@ func setupStorage( Registers: registerStore, Transactions: pebble.NewTransactions(store), Receipts: pebble.NewReceipts(store), - Accounts: pebble.NewAccounts(store), Traces: pebble.NewTraces(store), }, nil } @@ -580,6 +589,7 @@ func Run(ctx context.Context, cfg *config.Config, ready chan struct{}) error { boot.StopEventIngestion() boot.StopMetricsServer() boot.StopAPIServer() + boot.StopDB() return nil } diff --git a/services/ingestion/engine.go b/services/ingestion/engine.go index 80594c1e3..4486bdf81 100644 --- a/services/ingestion/engine.go +++ b/services/ingestion/engine.go @@ -3,7 +3,6 @@ package ingestion import ( "context" "fmt" - flowGo "github.com/onflow/flow-go/model/flow" pebbleDB "github.com/cockroachdb/pebble" @@ -46,7 +45,6 @@ type Engine struct { blocks storage.BlockIndexer receipts storage.ReceiptIndexer transactions storage.TransactionIndexer - accounts storage.AccountIndexer traces storage.TraceIndexer log zerolog.Logger evmLastHeight *models.SequentialHeight @@ -64,7 +62,6 @@ func NewEventIngestionEngine( blocks storage.BlockIndexer, receipts storage.ReceiptIndexer, transactions storage.TransactionIndexer, - accounts storage.AccountIndexer, traces storage.TraceIndexer, blocksPublisher *models.Publisher[*models.Block], logsPublisher *models.Publisher[[]*gethTypes.Log], @@ -84,7 +81,6 @@ func NewEventIngestionEngine( blocks: blocks, receipts: receipts, transactions: transactions, - accounts: accounts, traces: traces, log: log, blocksPublisher: blocksPublisher, @@ -120,6 +116,13 @@ func (e *Engine) Run(ctx context.Context) error { e.MarkReady() for events := range e.subscriber.Subscribe(ctx) { + select { + case <-ctx.Done(): + // stop the engine + return nil + default: + } + if events.Err != nil { return fmt.Errorf( "failure in event subscription with: %w", @@ -127,7 +130,7 @@ func (e *Engine) Run(ctx context.Context) error { ) } - err := e.processEvents(events.Events) + err := e.processEvents(ctx, events.Events) if err != nil { e.log.Error().Err(err).Msg("failed to process EVM events") return err @@ -149,7 +152,7 @@ func (e *Engine) Run(ctx context.Context) error { // https://github.com/onflow/flow-go/blob/master/fvm/evm/types/events.go // // Any error is unexpected and fatal. -func (e *Engine) processEvents(events *models.CadenceEvents) error { +func (e *Engine) processEvents(ctx context.Context, events *models.CadenceEvents) error { e.log.Info(). Uint64("cadence-height", events.CadenceHeight()). Int("cadence-event-length", events.Length()). @@ -168,8 +171,7 @@ func (e *Engine) processEvents(events *models.CadenceEvents) error { return nil // nothing else to do this was heartbeat event with not event payloads } - // TODO(JanezP): accounts need an indexed batch. Investigate why and try to switch to non-indexed batch - batch := e.store.NewIndexedBatch() + batch := e.store.NewBatch() defer func(batch *pebbleDB.Batch) { err := batch.Close() if err != nil { @@ -255,6 +257,14 @@ func (e *Engine) processEvents(events *models.CadenceEvents) error { } } + select { + case <-ctx.Done(): + // Temporary solution to avoid committing the batch when the DB is closed + // TODO(JanezP): handle this better + return nil + default: + } + if err := batch.Commit(pebbleDB.Sync); err != nil { return fmt.Errorf("failed to commit indexed data for Cadence block %d: %w", events.CadenceHeight(), err) } @@ -327,10 +337,6 @@ func (e *Engine) indexTransaction( return fmt.Errorf("failed to store tx: %s, with: %w", tx.Hash(), err) } - if err := e.accounts.Update(tx, receipt, batch); err != nil { - return fmt.Errorf("failed to update accounts for tx: %s, with: %w", tx.Hash(), err) - } - return nil } diff --git a/services/ingestion/engine_test.go b/services/ingestion/engine_test.go index 41f61c412..81ab88d68 100644 --- a/services/ingestion/engine_test.go +++ b/services/ingestion/engine_test.go @@ -53,11 +53,6 @@ func TestSerialBlockIngestion(t *testing.T) { }). Once() // make sure this isn't called multiple times - accounts := &storageMock.AccountIndexer{} - accounts. - On("Update"). - Return(func() error { return nil }) - traces := &storageMock.TraceIndexer{} eventsChan := make(chan models.BlockEvents) @@ -77,7 +72,6 @@ func TestSerialBlockIngestion(t *testing.T) { blocks, receipts, transactions, - accounts, traces, models.NewPublisher[*models.Block](), models.NewPublisher[[]*gethTypes.Log](), @@ -139,11 +133,6 @@ func TestSerialBlockIngestion(t *testing.T) { }). Once() // make sure this isn't called multiple times - accounts := &storageMock.AccountIndexer{} - accounts. - On("Update", mock.Anything, mock.Anything). - Return(func(t models.TransactionCall, r *gethTypes.Receipt) error { return nil }) - traces := &storageMock.TraceIndexer{} eventsChan := make(chan models.BlockEvents) @@ -162,7 +151,6 @@ func TestSerialBlockIngestion(t *testing.T) { blocks, receipts, transactions, - accounts, traces, models.NewPublisher[*models.Block](), models.NewPublisher[[]*gethTypes.Log](), @@ -255,11 +243,6 @@ func TestBlockAndTransactionIngestion(t *testing.T) { return nil }) - accounts := &storageMock.AccountIndexer{} - accounts. - On("Update", mock.AnythingOfType("models.TransactionCall"), mock.AnythingOfType("*models.Receipt"), mock.Anything). - Return(func(tx models.Transaction, receipt *models.Receipt, _ *pebbleDB.Batch) error { return nil }) - eventsChan := make(chan models.BlockEvents) subscriber := &mocks.EventSubscriber{} subscriber. @@ -289,7 +272,6 @@ func TestBlockAndTransactionIngestion(t *testing.T) { blocks, receipts, transactions, - accounts, traces, models.NewPublisher[*models.Block](), models.NewPublisher[[]*gethTypes.Log](), @@ -369,11 +351,6 @@ func TestBlockAndTransactionIngestion(t *testing.T) { On("SetLatestCadenceHeight", mock.AnythingOfType("uint64")). Return(func(h uint64) error { return nil }) - accounts := &storageMock.AccountIndexer{} - accounts. - On("Update", mock.AnythingOfType("models.TransactionCall"), mock.AnythingOfType("*models.Receipt"), mock.Anything). - Return(func(tx models.Transaction, receipt *models.Receipt, _ *pebbleDB.Batch) error { return nil }) - eventsChan := make(chan models.BlockEvents) subscriber := &mocks.EventSubscriber{} subscriber. @@ -403,7 +380,6 @@ func TestBlockAndTransactionIngestion(t *testing.T) { blocks, receipts, transactions, - accounts, traces, models.NewPublisher[*models.Block](), models.NewPublisher[[]*gethTypes.Log](), @@ -479,11 +455,6 @@ func TestBlockAndTransactionIngestion(t *testing.T) { }). Once() // make sure this isn't called multiple times - accounts := &storageMock.AccountIndexer{} - accounts. - On("Update", mock.Anything, mock.AnythingOfType("*models.Receipt"), mock.Anything). - Return(func(t models.Transaction, r *models.Receipt, _ *pebbleDB.Batch) error { return nil }) - traces := &storageMock.TraceIndexer{} eventsChan := make(chan models.BlockEvents) @@ -503,7 +474,6 @@ func TestBlockAndTransactionIngestion(t *testing.T) { blocks, receipts, transactions, - accounts, traces, models.NewPublisher[*models.Block](), models.NewPublisher[[]*gethTypes.Log](), diff --git a/services/replayer/blocks_provider_test.go b/services/replayer/blocks_provider_test.go index 57eeb0f1f..d318ae3d6 100644 --- a/services/replayer/blocks_provider_test.go +++ b/services/replayer/blocks_provider_test.go @@ -28,7 +28,8 @@ import ( func TestOnBlockReceived(t *testing.T) { t.Run("without latest block", func(t *testing.T) { - blocks := setupBlocksDB(t) + _, blocks := setupBlocksDB(t) + blocksProvider := NewBlocksProvider(blocks, flowGo.Emulator, nil) block := mocks.NewBlock(1) @@ -37,7 +38,7 @@ func TestOnBlockReceived(t *testing.T) { }) t.Run("with new block non-sequential to latest block", func(t *testing.T) { - blocks := setupBlocksDB(t) + _, blocks := setupBlocksDB(t) blocksProvider := NewBlocksProvider(blocks, flowGo.Emulator, nil) block1 := mocks.NewBlock(1) @@ -55,7 +56,7 @@ func TestOnBlockReceived(t *testing.T) { }) t.Run("with new block non-sequential to latest block", func(t *testing.T) { - blocks := setupBlocksDB(t) + _, blocks := setupBlocksDB(t) blocksProvider := NewBlocksProvider(blocks, flowGo.Emulator, nil) block1 := mocks.NewBlock(10) @@ -71,7 +72,7 @@ func TestOnBlockReceived(t *testing.T) { func TestBlockContext(t *testing.T) { t.Run("for latest block", func(t *testing.T) { - blocks := setupBlocksDB(t) + _, blocks := setupBlocksDB(t) tracer := newCallTracer(t) blocksProvider := NewBlocksProvider(blocks, flowGo.Emulator, tracer) @@ -99,7 +100,7 @@ func TestBlockContext(t *testing.T) { } func TestGetHashFunc(t *testing.T) { - blocks := setupBlocksDB(t) + db, blocks := setupBlocksDB(t) missingHeight := uint64(100) blockMapping := make(map[uint64]*models.Block, 0) @@ -110,8 +111,13 @@ func TestGetHashFunc(t *testing.T) { } block := mocks.NewBlock(i) - err := blocks.Store(i, flow.Identifier{0x1}, block, nil) + batch := db.NewBatch() + err := blocks.Store(i, flow.Identifier{0x1}, block, batch) + require.NoError(t, err) + + err = batch.Commit(pebble2.Sync) require.NoError(t, err) + blockMapping[i] = block } @@ -202,7 +208,7 @@ func TestGetHashFunc(t *testing.T) { func TestGetSnapshotAt(t *testing.T) { t.Run("for latest block", func(t *testing.T) { - blocks := setupBlocksDB(t) + _, blocks := setupBlocksDB(t) tracer := newCallTracer(t) blocksProvider := NewBlocksProvider(blocks, flowGo.Emulator, tracer) @@ -222,12 +228,16 @@ func TestGetSnapshotAt(t *testing.T) { }) t.Run("for historic block", func(t *testing.T) { - blocks := setupBlocksDB(t) + db, blocks := setupBlocksDB(t) tracer := newCallTracer(t) blocksProvider := NewBlocksProvider(blocks, flowGo.Emulator, tracer) block1 := mocks.NewBlock(1) - err := blocks.Store(1, flow.Identifier{0x1}, block1, nil) + batch := db.NewBatch() + err := blocks.Store(1, flow.Identifier{0x1}, block1, batch) + require.NoError(t, err) + + err = batch.Commit(pebble2.Sync) require.NoError(t, err) block2 := mocks.NewBlock(2) @@ -246,7 +256,7 @@ func TestGetSnapshotAt(t *testing.T) { }) t.Run("for missing historic block", func(t *testing.T) { - blocks := setupBlocksDB(t) + _, blocks := setupBlocksDB(t) tracer := newCallTracer(t) blocksProvider := NewBlocksProvider(blocks, flowGo.Emulator, tracer) @@ -267,7 +277,7 @@ func TestGetSnapshotAt(t *testing.T) { }) } -func setupBlocksDB(t *testing.T) storage.BlockIndexer { +func setupBlocksDB(t *testing.T) (*pebble.Storage, storage.BlockIndexer) { dir := t.TempDir() db, err := pebble.New(dir, zerolog.Nop()) require.NoError(t, err) @@ -282,7 +292,7 @@ func setupBlocksDB(t *testing.T) storage.BlockIndexer { err = batch.Commit(pebble2.Sync) require.NoError(t, err) - return blocks + return db, blocks } func newCallTracer(t *testing.T) *tracers.Tracer { diff --git a/storage/index.go b/storage/index.go index 38cebeb73..b83e80b82 100644 --- a/storage/index.go +++ b/storage/index.go @@ -1,8 +1,6 @@ package storage import ( - "math/big" - "github.com/cockroachdb/pebble" "github.com/goccy/go-json" "github.com/onflow/flow-go-sdk" @@ -97,19 +95,6 @@ type TransactionIndexer interface { Get(ID common.Hash) (models.Transaction, error) } -type AccountIndexer interface { - // Update account with executed transactions. - // Batch is required to batch multiple indexer operations, skipped if nil. - Update(tx models.Transaction, receipt *models.Receipt, batch *pebble.Batch) error - - // GetNonce gets an account nonce. If no nonce was indexed it returns 0. - // todo add getting nonce at provided block height / hash - GetNonce(address common.Address) (uint64, error) - - // GetBalance gets an account balance. If no balance was indexed it returns 0. - GetBalance(address common.Address) (*big.Int, error) -} - type TraceIndexer interface { // StoreTransaction will index transaction trace by the transaction ID. // Batch is required to batch multiple indexer operations, skipped if nil. diff --git a/storage/index_testsuite.go b/storage/index_test.go similarity index 71% rename from storage/index_testsuite.go rename to storage/index_test.go index bdd608f27..207873d9a 100644 --- a/storage/index_testsuite.go +++ b/storage/index_test.go @@ -1,14 +1,20 @@ -package storage +package storage_test import ( "fmt" + "testing" + + pebble2 "github.com/cockroachdb/pebble" + "github.com/onflow/flow-evm-gateway/config" + "github.com/onflow/flow-evm-gateway/storage" + "github.com/onflow/flow-evm-gateway/storage/pebble" + flowGo "github.com/onflow/flow-go/model/flow" + "github.com/stretchr/testify/require" "github.com/goccy/go-json" "github.com/onflow/flow-go-sdk" - evmEmulator "github.com/onflow/flow-go/fvm/evm/emulator" "github.com/onflow/go-ethereum/common" "github.com/onflow/go-ethereum/core/types" - "github.com/onflow/go-ethereum/crypto" "github.com/stretchr/testify/suite" "github.com/onflow/flow-evm-gateway/models" @@ -16,9 +22,70 @@ import ( "github.com/onflow/flow-evm-gateway/storage/mocks" ) +// tests that make sure the implementation conform to the interface expected behaviour +func TestBlocks(t *testing.T) { + runDB("blocks", t, func(t *testing.T, db *pebble.Storage) { + bl := pebble.NewBlocks(db, flowGo.Emulator) + batch := db.NewBatch() + + err := bl.InitHeights(config.EmulatorInitCadenceHeight, flow.Identifier{0x1}, batch) + require.NoError(t, err) + + err = batch.Commit(pebble2.Sync) + require.NoError(t, err) + + suite.Run(t, &BlockTestSuite{ + Blocks: bl, + DB: db, + }) + }) +} + +func TestReceipts(t *testing.T) { + runDB("receipts", t, func(t *testing.T, db *pebble.Storage) { + // prepare the blocks database since they track heights which are used in receipts as well + bl := pebble.NewBlocks(db, flowGo.Emulator) + batch := db.NewBatch() + + err := bl.InitHeights(config.EmulatorInitCadenceHeight, flow.Identifier{0x1}, batch) + require.NoError(t, err) + err = bl.Store(30, flow.Identifier{0x1}, mocks.NewBlock(10), batch) // update first and latest height + require.NoError(t, err) + err = bl.Store(30, flow.Identifier{0x1}, mocks.NewBlock(300), batch) // update latest + require.NoError(t, err) + + err = batch.Commit(pebble2.Sync) + require.NoError(t, err) + + suite.Run(t, &ReceiptTestSuite{ + ReceiptIndexer: pebble.NewReceipts(db), + DB: db, + }) + }) +} + +func TestTransactions(t *testing.T) { + runDB("transactions", t, func(t *testing.T, db *pebble.Storage) { + suite.Run(t, &TransactionTestSuite{ + TransactionIndexer: pebble.NewTransactions(db), + DB: db, + }) + }) +} + +func TestTraces(t *testing.T) { + runDB("traces", t, func(t *testing.T, db *pebble.Storage) { + suite.Run(t, &TraceTestSuite{ + TraceIndexer: pebble.NewTraces(db), + DB: db, + }) + }) +} + type BlockTestSuite struct { suite.Suite - Blocks BlockIndexer + Blocks storage.BlockIndexer + DB *pebble.Storage } func (b *BlockTestSuite) TestGet() { @@ -26,7 +93,12 @@ func (b *BlockTestSuite) TestGet() { height := uint64(1) flowID := flow.Identifier{0x01} block := mocks.NewBlock(height) - err := b.Blocks.Store(height+1, flowID, block, nil) + batch := b.DB.NewBatch() + + err := b.Blocks.Store(height+1, flowID, block, batch) + b.Require().NoError(err) + + err = batch.Commit(pebble2.Sync) b.Require().NoError(err) ID, err := block.Hash() @@ -59,17 +131,33 @@ func (b *BlockTestSuite) TestStore() { b.Run("success", func() { flowID := flow.Identifier{0x01} - err := b.Blocks.Store(2, flowID, block, nil) + batch := b.DB.NewBatch() + + err := b.Blocks.Store(2, flowID, block, batch) b.Require().NoError(err) + err = batch.Commit(pebble2.Sync) + b.Require().NoError(err) + + batch = b.DB.NewBatch() + // we allow overwriting blocks to make the actions idempotent - err = b.Blocks.Store(2, flowID, block, nil) + err = b.Blocks.Store(2, flowID, block, batch) + b.Require().NoError(err) + + err = batch.Commit(pebble2.Sync) b.Require().NoError(err) }) b.Run("store multiple blocks, and get one", func() { + for i := 0; i < 10; i++ { - err := b.Blocks.Store(uint64(i+5), flow.Identifier{byte(i)}, mocks.NewBlock(uint64(10+i)), nil) + batch := b.DB.NewBatch() + + err := b.Blocks.Store(uint64(i+5), flow.Identifier{byte(i)}, mocks.NewBlock(uint64(10+i)), batch) + b.Require().NoError(err) + + err = batch.Commit(pebble2.Sync) b.Require().NoError(err) } @@ -89,7 +177,12 @@ func (b *BlockTestSuite) TestHeights() { b.Run("last EVM height", func() { for i := 0; i < 5; i++ { lastHeight := uint64(100 + i) - err := b.Blocks.Store(lastHeight+10, flow.Identifier{byte(i)}, mocks.NewBlock(lastHeight), nil) + batch := b.DB.NewBatch() + + err := b.Blocks.Store(lastHeight+10, flow.Identifier{byte(i)}, mocks.NewBlock(lastHeight), batch) + b.Require().NoError(err) + + err = batch.Commit(pebble2.Sync) b.Require().NoError(err) last, err := b.Blocks.LatestEVMHeight() @@ -109,7 +202,12 @@ func (b *BlockTestSuite) TestHeights() { for i, evmHeight := range evmHeights { blocks[i] = mocks.NewBlock(evmHeight) - err := b.Blocks.Store(uint64(i), cadenceIDs[i], blocks[i], nil) + batch := b.DB.NewBatch() + + err := b.Blocks.Store(uint64(i), cadenceIDs[i], blocks[i], batch) + b.Require().NoError(err) + + err = batch.Commit(pebble2.Sync) b.Require().NoError(err) } @@ -125,7 +223,11 @@ func (b *BlockTestSuite) TestHeights() { b.Run("last Cadence height", func() { for i := 0; i < 5; i++ { lastHeight := uint64(100 + i) - err := b.Blocks.Store(lastHeight, flow.Identifier{byte(i)}, mocks.NewBlock(lastHeight-10), nil) + batch := b.DB.NewBatch() + err := b.Blocks.Store(lastHeight, flow.Identifier{byte(i)}, mocks.NewBlock(lastHeight-10), batch) + b.Require().NoError(err) + + err = batch.Commit(pebble2.Sync) b.Require().NoError(err) last, err := b.Blocks.LatestCadenceHeight() @@ -138,7 +240,11 @@ func (b *BlockTestSuite) TestHeights() { evmHeights := []uint64{10, 11, 12, 13} cadenceHeights := []uint64{20, 24, 26, 27} for i, evmHeight := range evmHeights { - err := b.Blocks.Store(cadenceHeights[i], flow.Identifier{byte(i)}, mocks.NewBlock(evmHeight), nil) + batch := b.DB.NewBatch() + err := b.Blocks.Store(cadenceHeights[i], flow.Identifier{byte(i)}, mocks.NewBlock(evmHeight), batch) + b.Require().NoError(err) + + err = batch.Commit(pebble2.Sync) b.Require().NoError(err) } @@ -153,7 +259,11 @@ func (b *BlockTestSuite) TestHeights() { evmHeights := []uint64{10, 11, 12, 13} cadenceIDs := []flow.Identifier{{0x01}, {0x02}, {0x03}, {0x04}} for i, evmHeight := range evmHeights { - err := b.Blocks.Store(uint64(i), cadenceIDs[i], mocks.NewBlock(evmHeight), nil) + batch := b.DB.NewBatch() + err := b.Blocks.Store(uint64(i), cadenceIDs[i], mocks.NewBlock(evmHeight), batch) + b.Require().NoError(err) + + err = batch.Commit(pebble2.Sync) b.Require().NoError(err) } @@ -167,14 +277,19 @@ func (b *BlockTestSuite) TestHeights() { type ReceiptTestSuite struct { suite.Suite - ReceiptIndexer ReceiptIndexer + ReceiptIndexer storage.ReceiptIndexer + DB *pebble.Storage } func (s *ReceiptTestSuite) TestStoreReceipt() { s.Run("store receipt successfully", func() { receipt := mocks.NewReceipt(1, common.HexToHash("0xf1")) - err := s.ReceiptIndexer.Store([]*models.Receipt{receipt}, nil) + batch := s.DB.NewBatch() + err := s.ReceiptIndexer.Store([]*models.Receipt{receipt}, batch) + s.Require().NoError(err) + + err = batch.Commit(pebble2.Sync) s.Require().NoError(err) }) @@ -194,7 +309,11 @@ func (s *ReceiptTestSuite) TestStoreReceipt() { } } - err := s.ReceiptIndexer.Store(receipts, nil) + batch := s.DB.NewBatch() + err := s.ReceiptIndexer.Store(receipts, batch) + s.Require().NoError(err) + + err = batch.Commit(pebble2.Sync) s.Require().NoError(err) storeReceipts, err := s.ReceiptIndexer.GetByBlockHeight(height) @@ -211,7 +330,8 @@ func (s *ReceiptTestSuite) TestStoreReceipt() { mocks.NewReceipt(2, common.HexToHash("0x2")), } - err := s.ReceiptIndexer.Store(receipts, nil) + batch := s.DB.NewBatch() + err := s.ReceiptIndexer.Store(receipts, batch) s.Require().EqualError(err, "can't store receipts for multiple heights") }) } @@ -219,7 +339,11 @@ func (s *ReceiptTestSuite) TestStoreReceipt() { func (s *ReceiptTestSuite) TestGetReceiptByTransactionID() { s.Run("existing transaction ID", func() { receipt := mocks.NewReceipt(2, common.HexToHash("0xf2")) - err := s.ReceiptIndexer.Store([]*models.Receipt{receipt}, nil) + batch := s.DB.NewBatch() + err := s.ReceiptIndexer.Store([]*models.Receipt{receipt}, batch) + s.Require().NoError(err) + + err = batch.Commit(pebble2.Sync) s.Require().NoError(err) retReceipt, err := s.ReceiptIndexer.GetByTransactionID(receipt.TxHash) @@ -238,11 +362,21 @@ func (s *ReceiptTestSuite) TestGetReceiptByTransactionID() { func (s *ReceiptTestSuite) TestGetReceiptByBlockHeight() { s.Run("existing block height", func() { receipt := mocks.NewReceipt(3, common.HexToHash("0x1")) - err := s.ReceiptIndexer.Store([]*models.Receipt{receipt}, nil) + batch := s.DB.NewBatch() + err := s.ReceiptIndexer.Store([]*models.Receipt{receipt}, batch) + s.Require().NoError(err) + + err = batch.Commit(pebble2.Sync) s.Require().NoError(err) + + batch = s.DB.NewBatch() + // add one more receipt that shouldn't be retrieved r := mocks.NewReceipt(4, common.HexToHash("0x2")) - s.Require().NoError(s.ReceiptIndexer.Store([]*models.Receipt{r}, nil)) + s.Require().NoError(s.ReceiptIndexer.Store([]*models.Receipt{r}, batch)) + + err = batch.Commit(pebble2.Sync) + s.Require().NoError(err) retReceipts, err := s.ReceiptIndexer.GetByBlockHeight(receipt.BlockNumber.Uint64()) s.Require().NoError(err) @@ -268,7 +402,11 @@ func (s *ReceiptTestSuite) TestBloomsForBlockRange() { r := mocks.NewReceipt(i, common.HexToHash(fmt.Sprintf("0xf1%d", i))) testBlooms = append(testBlooms, &r.Bloom) testHeights = append(testHeights, i) - err := s.ReceiptIndexer.Store([]*models.Receipt{r}, nil) + batch := s.DB.NewBatch() + err := s.ReceiptIndexer.Store([]*models.Receipt{r}, batch) + s.Require().NoError(err) + + err = batch.Commit(pebble2.Sync) s.Require().NoError(err) } @@ -307,7 +445,10 @@ func (s *ReceiptTestSuite) TestBloomsForBlockRange() { r2 := mocks.NewReceipt(i, common.HexToHash(fmt.Sprintf("0x%d", i))) receipts := []*models.Receipt{r1, r2} - s.Require().NoError(s.ReceiptIndexer.Store(receipts, nil)) + batch := s.DB.NewBatch() + s.Require().NoError(s.ReceiptIndexer.Store(receipts, batch)) + err := batch.Commit(pebble2.Sync) + s.Require().NoError(err) testBlooms = append(testBlooms, &r1.Bloom, &r2.Bloom) testHeights = append(testHeights, i) @@ -355,7 +496,12 @@ func (s *ReceiptTestSuite) TestBloomsForBlockRange() { for i := start; i < end; i++ { r1 := mocks.NewReceipt(i, common.HexToHash(fmt.Sprintf("0x%d", i))) receipts := []*models.Receipt{r1} - s.Require().NoError(s.ReceiptIndexer.Store(receipts, nil)) + + batch := s.DB.NewBatch() + s.Require().NoError(s.ReceiptIndexer.Store(receipts, batch)) + + err := batch.Commit(pebble2.Sync) + s.Require().NoError(err) if i == specific { expectedBloom = &r1.Bloom @@ -430,14 +576,20 @@ func (s *ReceiptTestSuite) compareReceipts(expected *models.Receipt, actual *mod type TransactionTestSuite struct { suite.Suite - TransactionIndexer TransactionIndexer + TransactionIndexer storage.TransactionIndexer + DB *pebble.Storage } func (s *TransactionTestSuite) TestStoreTransaction() { tx := mocks.NewTransaction(0) s.Run("store transaction successfully", func() { - err := s.TransactionIndexer.Store(tx, nil) + batch := s.DB.NewBatch() + + err := s.TransactionIndexer.Store(tx, batch) + s.Require().NoError(err) + + err = batch.Commit(pebble2.Sync) s.Require().NoError(err) }) } @@ -445,7 +597,11 @@ func (s *TransactionTestSuite) TestStoreTransaction() { func (s *TransactionTestSuite) TestGetTransaction() { s.Run("existing transaction", func() { tx := mocks.NewTransaction(1) - err := s.TransactionIndexer.Store(tx, nil) + batch := s.DB.NewBatch() + err := s.TransactionIndexer.Store(tx, batch) + s.Require().NoError(err) + + err = batch.Commit(pebble2.Sync) s.Require().NoError(err) txHash := tx.Hash() @@ -456,15 +612,23 @@ func (s *TransactionTestSuite) TestGetTransaction() { retTxHash := retTx.Hash() s.Require().Equal(txHash, retTxHash) // if hashes are equal the data must be equal + batch = s.DB.NewBatch() // allow same transaction overwrites - s.Require().NoError(s.TransactionIndexer.Store(retTx, nil)) + s.Require().NoError(s.TransactionIndexer.Store(retTx, batch)) + + err = batch.Commit(pebble2.Sync) + s.Require().NoError(err) }) s.Run("store multiple transactions and get single", func() { var tx models.Transaction for i := 0; i < 10; i++ { tx = mocks.NewTransaction(uint64(10 + i)) - err := s.TransactionIndexer.Store(tx, nil) + batch := s.DB.NewBatch() + err := s.TransactionIndexer.Store(tx, batch) + s.Require().NoError(err) + + err = batch.Commit(pebble2.Sync) s.Require().NoError(err) } @@ -485,82 +649,23 @@ func (s *TransactionTestSuite) TestGetTransaction() { }) } -type AccountTestSuite struct { - suite.Suite - AccountIndexer AccountIndexer -} - -func (a *AccountTestSuite) TestNonce() { - - a.Run("update account and increase nonce", func() { - // todo add multiple accounts test - from := common.HexToAddress("FACF71692421039876a5BB4F10EF7A439D8ef61E") - rawKey := "f6d5333177711e562cabf1f311916196ee6ffc2a07966d9d4628094073bd5442" - key, err := crypto.HexToECDSA(rawKey) - a.Require().NoError(err) - - nonce, err := a.AccountIndexer.GetNonce(from) - a.Require().NoError(err) - a.Require().Equal(uint64(0), nonce) - - for i := 1; i < 5; i++ { - tx := mocks.NewTransaction(0) - - txCall, ok := tx.(models.TransactionCall) - a.Require().True(ok) - - txHash := tx.Hash() - - rcp := mocks.NewReceipt(uint64(i+5), txHash) - gethTx, err := types.SignTx(txCall.Transaction, evmEmulator.GetDefaultSigner(), key) - a.Require().NoError(err) - - tx = models.TransactionCall{Transaction: gethTx} - - err = a.AccountIndexer.Update(tx, rcp, nil) - a.Require().NoError(err) - - nonce, err = a.AccountIndexer.GetNonce(from) - a.Require().NoError(err) - a.Require().Equal(uint64(i), nonce) - } - - // if run second time we should still see same nonce values, since they won't be incremented - // because we track nonce with evm height, and if same height is used twice we don't update - for i := 1; i < 5; i++ { - tx := mocks.NewTransaction(0) - - txCall, ok := tx.(models.TransactionCall) - a.Require().True(ok) - - txHash := tx.Hash() - - rcp := mocks.NewReceipt(uint64(i+5), txHash) - gethTx, err := types.SignTx(txCall.Transaction, evmEmulator.GetDefaultSigner(), key) - a.Require().NoError(err) - - tx = models.TransactionCall{Transaction: gethTx} - - err = a.AccountIndexer.Update(tx, rcp, nil) - a.Require().NoError(err) - - nonce, err = a.AccountIndexer.GetNonce(from) - a.Require().NoError(err) - a.Require().Equal(uint64(4), nonce) // always equal to latest nonce - } - }) -} - type TraceTestSuite struct { suite.Suite - TraceIndexer TraceIndexer + TraceIndexer storage.TraceIndexer + DB *pebble.Storage } func (s *TraceTestSuite) TestStore() { s.Run("store new trace", func() { id := common.Hash{0x01} trace := json.RawMessage(`{ "test": "foo" }`) - err := s.TraceIndexer.StoreTransaction(id, trace, nil) + + batch := s.DB.NewBatch() + + err := s.TraceIndexer.StoreTransaction(id, trace, batch) + s.Require().NoError(err) + + err = batch.Commit(pebble2.Sync) s.Require().NoError(err) }) @@ -568,7 +673,13 @@ func (s *TraceTestSuite) TestStore() { for i := 0; i < 2; i++ { id := common.Hash{0x01} trace := json.RawMessage(`{ "test": "foo" }`) - err := s.TraceIndexer.StoreTransaction(id, trace, nil) + + batch := s.DB.NewBatch() + + err := s.TraceIndexer.StoreTransaction(id, trace, batch) + s.Require().NoError(err) + + err = batch.Commit(pebble2.Sync) s.Require().NoError(err) } }) @@ -579,7 +690,12 @@ func (s *TraceTestSuite) TestGet() { id := common.Hash{0x01} trace := json.RawMessage(`{ "test": "foo" }`) - err := s.TraceIndexer.StoreTransaction(id, trace, nil) + batch := s.DB.NewBatch() + + err := s.TraceIndexer.StoreTransaction(id, trace, batch) + s.Require().NoError(err) + + err = batch.Commit(pebble2.Sync) s.Require().NoError(err) val, err := s.TraceIndexer.GetTransaction(id) diff --git a/storage/mocks/AccountIndexer.go b/storage/mocks/AccountIndexer.go deleted file mode 100644 index a010784fc..000000000 --- a/storage/mocks/AccountIndexer.go +++ /dev/null @@ -1,109 +0,0 @@ -// Code generated by mockery v2.43.2. DO NOT EDIT. - -package mocks - -import ( - big "math/big" - - common "github.com/onflow/go-ethereum/common" - mock "github.com/stretchr/testify/mock" - - models "github.com/onflow/flow-evm-gateway/models" - - pebble "github.com/cockroachdb/pebble" -) - -// AccountIndexer is an autogenerated mock type for the AccountIndexer type -type AccountIndexer struct { - mock.Mock -} - -// GetBalance provides a mock function with given fields: address -func (_m *AccountIndexer) GetBalance(address common.Address) (*big.Int, error) { - ret := _m.Called(address) - - if len(ret) == 0 { - panic("no return value specified for GetBalance") - } - - var r0 *big.Int - var r1 error - if rf, ok := ret.Get(0).(func(common.Address) (*big.Int, error)); ok { - return rf(address) - } - if rf, ok := ret.Get(0).(func(common.Address) *big.Int); ok { - r0 = rf(address) - } else { - if ret.Get(0) != nil { - r0 = ret.Get(0).(*big.Int) - } - } - - if rf, ok := ret.Get(1).(func(common.Address) error); ok { - r1 = rf(address) - } else { - r1 = ret.Error(1) - } - - return r0, r1 -} - -// GetNonce provides a mock function with given fields: address -func (_m *AccountIndexer) GetNonce(address common.Address) (uint64, error) { - ret := _m.Called(address) - - if len(ret) == 0 { - panic("no return value specified for GetNonce") - } - - var r0 uint64 - var r1 error - if rf, ok := ret.Get(0).(func(common.Address) (uint64, error)); ok { - return rf(address) - } - if rf, ok := ret.Get(0).(func(common.Address) uint64); ok { - r0 = rf(address) - } else { - r0 = ret.Get(0).(uint64) - } - - if rf, ok := ret.Get(1).(func(common.Address) error); ok { - r1 = rf(address) - } else { - r1 = ret.Error(1) - } - - return r0, r1 -} - -// Update provides a mock function with given fields: tx, receipt, batch -func (_m *AccountIndexer) Update(tx models.Transaction, receipt *models.Receipt, batch *pebble.Batch) error { - ret := _m.Called(tx, receipt, batch) - - if len(ret) == 0 { - panic("no return value specified for Update") - } - - var r0 error - if rf, ok := ret.Get(0).(func(models.Transaction, *models.Receipt, *pebble.Batch) error); ok { - r0 = rf(tx, receipt, batch) - } else { - r0 = ret.Error(0) - } - - return r0 -} - -// NewAccountIndexer creates a new instance of AccountIndexer. It also registers a testing interface on the mock and a cleanup function to assert the mocks expectations. -// The first argument is typically a *testing.T value. -func NewAccountIndexer(t interface { - mock.TestingT - Cleanup(func()) -}) *AccountIndexer { - mock := &AccountIndexer{} - mock.Mock.Test(t) - - t.Cleanup(func() { mock.AssertExpectations(t) }) - - return mock -} diff --git a/storage/pebble/accounts.go b/storage/pebble/accounts.go deleted file mode 100644 index 3b64862b9..000000000 --- a/storage/pebble/accounts.go +++ /dev/null @@ -1,125 +0,0 @@ -package pebble - -import ( - "encoding/binary" - "errors" - "fmt" - "math/big" - "sync" - - "github.com/cockroachdb/pebble" - "github.com/onflow/go-ethereum/common" - - "github.com/onflow/flow-evm-gateway/models" - errs "github.com/onflow/flow-evm-gateway/models/errors" - "github.com/onflow/flow-evm-gateway/storage" -) - -var _ storage.AccountIndexer = &Accounts{} - -type Accounts struct { - store *Storage - mux sync.RWMutex -} - -func NewAccounts(db *Storage) *Accounts { - return &Accounts{ - store: db, - mux: sync.RWMutex{}, - } -} - -func (a *Accounts) Update( - tx models.Transaction, - receipt *models.Receipt, - batch *pebble.Batch, -) error { - a.mux.Lock() - defer a.mux.Unlock() - - from, err := tx.From() - if err != nil { - return err - } - - nonce, height, err := a.getNonce(from, batch) - if err != nil { - return err - } - - // make sure the transaction height is bigger than the height we already - // recorded for the nonce. this makes the operation idempotent and safer. - txHeight := receipt.BlockNumber.Uint64() - if txHeight <= height { - return nil - } - - nonce += 1 - - data := encodeNonce(nonce, txHeight) - return a.store.set(accountNonceKey, from.Bytes(), data, batch) -} - -func (a *Accounts) getNonce(address common.Address, batch *pebble.Batch) (uint64, uint64, error) { - var val []byte - var err error - if batch != nil { - val, err = a.store.batchGet(batch, accountNonceKey, address.Bytes()) - } else { - val, err = a.store.get(accountNonceKey, address.Bytes()) - } - if err != nil { - // if no nonce was yet saved for the account the nonce is 0 - if errors.Is(err, errs.ErrEntityNotFound) { - return 0, 0, nil - } - - return 0, 0, err - } - - nonce, height, err := decodeNonce(val) - if err != nil { - return 0, 0, err - } - - return nonce, height, nil -} - -func (a *Accounts) GetNonce(address common.Address) (uint64, error) { - a.mux.RLock() - defer a.mux.RUnlock() - nonce, _, err := a.getNonce(address, nil) - if err != nil { - return 0, fmt.Errorf("failed to get nonce of address: %s, with: %w", address, err) - } - - return nonce, nil -} - -func (a *Accounts) GetBalance(address common.Address) (*big.Int, error) { - panic("not supported") -} - -// decodeNonce converts nonce data into nonce and height -func decodeNonce(data []byte) (uint64, uint64, error) { - if len(data) != 16 { - return 0, 0, fmt.Errorf("invalid nonce data, expected length: %d, got: %d", 16, len(data)) - } - nonce := binary.BigEndian.Uint64(data[:8]) - height := binary.BigEndian.Uint64(data[8:]) - - return nonce, height, nil -} - -// encodeNonce converts nonce and height into nonce data -func encodeNonce(nonce uint64, height uint64) []byte { - payload := make([]byte, 16) - for i, b := range uint64Bytes(nonce) { - payload[i] = b - } - for i, b := range uint64Bytes(height) { - payload[i+8] = b - } - - return payload -} diff --git a/storage/pebble/keys.go b/storage/pebble/keys.go index 77411c7a4..f52eb0c4d 100644 --- a/storage/pebble/keys.go +++ b/storage/pebble/keys.go @@ -1,6 +1,10 @@ package pebble -import "encoding/binary" +import ( + "encoding/binary" + + "github.com/cockroachdb/pebble" +) const ( // block keys @@ -17,16 +21,11 @@ const ( receiptHeightKey = byte(21) bloomHeightKey = byte(22) - // account keys - accountNonceKey = byte(30) - accountBalanceKey = byte(31) - // traces keys traceTxIDKey = byte(40) - // ledger value - ledgerValue = byte(50) - ledgerSlabIndex = byte(51) + // registers + registerKeyMarker = byte(50) // special keys latestEVMHeightKey = byte(100) @@ -60,3 +59,22 @@ func uint64Bytes(height uint64) []byte { binary.BigEndian.PutUint64(b, height) return b } + +func NewMVCCComparer() *pebble.Comparer { + comparer := *pebble.DefaultComparer + comparer.Split = func(a []byte) int { + if len(a) == 0 { + // edge case. Not sure if this is possible, but just in case + return 0 + } + if a[0] != registerKeyMarker { + // default comparer + return len(a) + } + // special case for registers + return len(a) - 8 + } + comparer.Name = "flow.MVCCComparer" + + return &comparer +} diff --git a/storage/pebble/ledger.go b/storage/pebble/ledger.go deleted file mode 100644 index 6669570ec..000000000 --- a/storage/pebble/ledger.go +++ /dev/null @@ -1,114 +0,0 @@ -package pebble - -import ( - "errors" - "fmt" - "sync" - - "github.com/onflow/atree" - - errs "github.com/onflow/flow-evm-gateway/models/errors" -) - -var _ atree.Ledger = &Ledger{} - -// todo we need to support historic data, -// we likely need to create ledger with the context of block height -// and then prepend all keys with that height - -type Ledger struct { - store *Storage - mux sync.RWMutex -} - -func NewLedger(store *Storage) *Ledger { - return &Ledger{ - store: store, - mux: sync.RWMutex{}, - } -} - -func (l *Ledger) GetValue(owner, key []byte) ([]byte, error) { - l.mux.RLock() - defer l.mux.RUnlock() - - id := append(owner, key...) - val, err := l.store.get(ledgerValue, id) - if err != nil { - // as per interface expectation we need to remove nil if not found - if errors.Is(err, errs.ErrEntityNotFound) { - return nil, nil - } - - return nil, fmt.Errorf( - "failed to get ledger value at owner %x and key %x: %w", - owner, - key, - err, - ) - } - - return val, nil -} - -func (l *Ledger) SetValue(owner, key, value []byte) error { - l.mux.Lock() - defer l.mux.Unlock() - - id := append(owner, key...) - if err := l.store.set(ledgerValue, id, value, nil); err != nil { - return fmt.Errorf( - "failed to store ledger value for owner %x and key %x: %w", - owner, - key, - err, - ) - } - - return nil -} - -func (l *Ledger) ValueExists(owner, key []byte) (bool, error) { - val, err := l.GetValue(owner, key) - if err != nil { - return false, err - } - - return val != nil, nil -} - -func (l *Ledger) AllocateSlabIndex(owner []byte) (atree.SlabIndex, error) { - l.mux.Lock() - defer l.mux.Unlock() - - var index atree.SlabIndex - - val, err := l.store.get(ledgerSlabIndex, owner) - if err != nil { - if !errors.Is(err, errs.ErrEntityNotFound) { - return atree.SlabIndexUndefined, err - } - } - - if val != nil { - if len(val) != len(index) { - return atree.SlabIndexUndefined, fmt.Errorf( - "slab index was not stored in correct format for owner %x", - owner, - ) - } - - copy(index[:], val) - } - - 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", - owner, - err, - ) - } - - return index, nil -} diff --git a/storage/pebble/receipts.go b/storage/pebble/receipts.go index be45a32e1..5a3ffec68 100644 --- a/storage/pebble/receipts.go +++ b/storage/pebble/receipts.go @@ -3,7 +3,6 @@ package pebble import ( "encoding/binary" "fmt" - "sync" "github.com/cockroachdb/pebble" "github.com/onflow/go-ethereum/common" @@ -19,13 +18,11 @@ var _ storage.ReceiptIndexer = &Receipts{} type Receipts struct { store *Storage - mux sync.RWMutex } func NewReceipts(store *Storage) *Receipts { return &Receipts{ store: store, - mux: sync.RWMutex{}, } } @@ -37,9 +34,6 @@ func NewReceipts(store *Storage) *Receipts { // - receipt block height => list of encoded receipts (1+ per block) // - receipt block height => list of bloom filters (1+ per block) func (r *Receipts) Store(receipts []*models.Receipt, batch *pebble.Batch) error { - r.mux.Lock() - defer r.mux.Unlock() - var blooms []*gethTypes.Bloom var height uint64 @@ -91,15 +85,12 @@ func (r *Receipts) Store(receipts []*models.Receipt, batch *pebble.Batch) error } func (r *Receipts) GetByTransactionID(ID common.Hash) (*models.Receipt, error) { - r.mux.RLock() - defer r.mux.RUnlock() - height, err := r.store.get(receiptTxIDToHeightKey, ID.Bytes()) if err != nil { return nil, fmt.Errorf("failed to get receipt by tx ID: %s, with: %w", ID, err) } - receipts, err := r.getByBlockHeight(height, nil) + receipts, err := r.getByBlockHeight(height) if err != nil { return nil, fmt.Errorf( "failed to get receipt by height: %d, with: %w", @@ -118,21 +109,12 @@ func (r *Receipts) GetByTransactionID(ID common.Hash) (*models.Receipt, error) { } func (r *Receipts) GetByBlockHeight(height uint64) ([]*models.Receipt, error) { - r.mux.RLock() - defer r.mux.RUnlock() - - return r.getByBlockHeight(uint64Bytes(height), nil) + return r.getByBlockHeight(uint64Bytes(height)) } -func (r *Receipts) getByBlockHeight(height []byte, batch *pebble.Batch) ([]*models.Receipt, error) { - var val []byte - var err error +func (r *Receipts) getByBlockHeight(height []byte) ([]*models.Receipt, error) { - if batch != nil { - val, err = r.store.batchGet(batch, receiptHeightKey, height) - } else { - val, err = r.store.get(receiptHeightKey, height) - } + val, err := r.store.get(receiptHeightKey, height) if err != nil { return nil, err } @@ -161,9 +143,6 @@ func (r *Receipts) getByBlockHeight(height []byte, batch *pebble.Batch) ([]*mode } func (r *Receipts) BloomsForBlockRange(start, end uint64) ([]*models.BloomsHeight, error) { - r.mux.RLock() - defer r.mux.RUnlock() - if start > end { return nil, fmt.Errorf( "%w: start value %d is bigger than end value %d", diff --git a/storage/pebble/register_storage.go b/storage/pebble/register_storage.go index 64436f516..c3dc2b4cb 100644 --- a/storage/pebble/register_storage.go +++ b/storage/pebble/register_storage.go @@ -15,15 +15,16 @@ var ( // MinLookupKeyLen defines the minimum length for a valid lookup key // // Lookup keys use the following format: - // [key] / [height] + // [marker] [key] / [height] // Where: + // - marker: 1 byte marking that this is a register key // - key: optional variable length field // - height: 8 bytes representing the block height (uint64) // - separator: '/' is used to separate variable length field // - // Therefore the minimum key would be 1 byte + # of bytes for height - // / [height] - MinLookupKeyLen = 1 + registers.HeightSuffixLen + // Therefore the minimum key would be 2 bytes + # of bytes for height + // [marker] / [height] + MinLookupKeyLen = 2 + registers.HeightSuffixLen ) type RegisterStorage struct { @@ -60,7 +61,13 @@ func NewRegisterStorage( // Get returns the register value for the given register ID at the given height. // Get will check that the owner is the same as the one used to create the index. -func (r *RegisterStorage) Get(id flow.RegisterID, height uint64) (flow.RegisterValue, error) { +func (r *RegisterStorage) Get(id flow.RegisterID, height uint64) (value flow.RegisterValue, err error) { + defer func() { + if r := recover(); r != nil { + err = fmt.Errorf("panic: %v", r) + } + }() + owner := flow.BytesToAddress([]byte(id.Owner)) if r.owner != flow.BytesToAddress([]byte(id.Owner)) { return nil, registerOwnerMismatch(r.owner, owner) @@ -152,6 +159,7 @@ func newLookupKey(height uint64, key []byte) *lookupKey { // The "" part is the register lookupKey, which is used as a prefix to filter and iterate // through updated values at different heights, and find the most recent updated value at or below // a certain height. + lookupKey.encoded = append(lookupKey.encoded, registerKeyMarker) lookupKey.encoded = append(lookupKey.encoded, key...) lookupKey.encoded = append(lookupKey.encoded, '/') diff --git a/storage/pebble/storage.go b/storage/pebble/storage.go index 10731f0b2..eb33ad97e 100644 --- a/storage/pebble/storage.go +++ b/storage/pebble/storage.go @@ -5,7 +5,7 @@ import ( "fmt" "io" - "github.com/onflow/flow-go/storage/pebble/registers" + "github.com/cockroachdb/pebble/bloom" "github.com/cockroachdb/pebble" "github.com/rs/zerolog" @@ -28,6 +28,7 @@ func New(dir string, log zerolog.Logger) (*Storage, error) { // currently pebble is only used for registers opts := &pebble.Options{ Cache: cache, + Comparer: NewMVCCComparer(), FormatMajorVersion: pebble.FormatNewest, L0CompactionThreshold: 2, L0StopWritesThreshold: 1000, @@ -40,7 +41,6 @@ func New(dir string, log zerolog.Logger) (*Storage, error) { MemTableStopWritesThreshold: 4, // The default is 1. MaxConcurrentCompactions: func() int { return 4 }, - Comparer: registers.NewMVCCComparer(), } for i := 0; i < len(opts.Levels); i++ { @@ -49,6 +49,12 @@ func New(dir string, log zerolog.Logger) (*Storage, error) { // for good performance (esp. on stripped storage). l.BlockSize = 32 << 10 // 32 KB l.IndexBlockSize = 256 << 10 // 256 KB + + // The bloom filter speedsup our SeekPrefixGE by skipping + // sstables that do not contain the prefix + l.FilterPolicy = bloom.FilterPolicy(MinLookupKeyLen) + l.FilterType = pebble.TableFilter + if i > 0 { // L0 starts at 2MiB, each level is 2x the previous. l.TargetFileSize = opts.Levels[i-1].TargetFileSize * 2 @@ -79,12 +85,7 @@ func New(dir string, log zerolog.Logger) (*Storage, error) { func (s *Storage) set(keyCode byte, key []byte, value []byte, batch *pebble.Batch) error { prefixedKey := makePrefix(keyCode, key) - if batch != nil { - // set the value on batch and return - return batch.Set(prefixedKey, value, nil) - } - - return s.db.Set(prefixedKey, value, nil) + return batch.Set(prefixedKey, value, nil) } func (s *Storage) get(keyCode byte, key ...[]byte) ([]byte, error) { @@ -112,25 +113,10 @@ func (s *Storage) get(keyCode byte, key ...[]byte) ([]byte, error) { return cp, nil } -// batchGet loads the value from an indexed batch if data is found, else it loads the value from the storage. -func (s *Storage) batchGet(batch *pebble.Batch, keyCode byte, key ...[]byte) ([]byte, error) { - if batch == nil || !batch.Indexed() { - return nil, fmt.Errorf("batch must not be nil and it must be indexed") - } - - data, closer, err := batch.Get(makePrefix(keyCode, key...)) - if err == nil { - _ = closer.Close() - return data, nil - } - - return s.get(keyCode, key...) -} - -func (s *Storage) NewIndexedBatch() *pebble.Batch { - return s.db.NewIndexedBatch() -} - func (s *Storage) NewBatch() *pebble.Batch { return s.db.NewBatch() } + +func (s *Storage) Close() error { + return s.db.Close() +} diff --git a/storage/pebble/storage_test.go b/storage/pebble/storage_test.go index 9c3ebbe3d..5e80dd90c 100644 --- a/storage/pebble/storage_test.go +++ b/storage/pebble/storage_test.go @@ -5,74 +5,17 @@ import ( "github.com/cockroachdb/pebble" "github.com/goccy/go-json" + "github.com/onflow/flow-evm-gateway/config" + "github.com/onflow/flow-evm-gateway/models/errors" + "github.com/onflow/flow-evm-gateway/storage/mocks" "github.com/onflow/flow-go-sdk" flowGo "github.com/onflow/flow-go/model/flow" "github.com/onflow/go-ethereum/common" "github.com/rs/zerolog" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" - "github.com/stretchr/testify/suite" - - "github.com/onflow/flow-evm-gateway/config" - "github.com/onflow/flow-evm-gateway/models/errors" - "github.com/onflow/flow-evm-gateway/storage" - "github.com/onflow/flow-evm-gateway/storage/mocks" ) -// tests that make sure the implementation conform to the interface expected behaviour -func TestBlocks(t *testing.T) { - runDB("blocks", t, func(t *testing.T, db *Storage) { - bl := NewBlocks(db, flowGo.Emulator) - batch := db.NewBatch() - - err := bl.InitHeights(config.EmulatorInitCadenceHeight, flow.Identifier{0x1}, batch) - require.NoError(t, err) - - err = batch.Commit(pebble.Sync) - require.NoError(t, err) - - suite.Run(t, &storage.BlockTestSuite{Blocks: bl}) - }) -} - -func TestReceipts(t *testing.T) { - runDB("receipts", t, func(t *testing.T, db *Storage) { - // prepare the blocks database since they track heights which are used in receipts as well - bl := NewBlocks(db, flowGo.Emulator) - batch := db.NewBatch() - - err := bl.InitHeights(config.EmulatorInitCadenceHeight, flow.Identifier{0x1}, batch) - require.NoError(t, err) - err = bl.Store(30, flow.Identifier{0x1}, mocks.NewBlock(10), batch) // update first and latest height - require.NoError(t, err) - err = bl.Store(30, flow.Identifier{0x1}, mocks.NewBlock(300), batch) // update latest - require.NoError(t, err) - - err = batch.Commit(pebble.Sync) - require.NoError(t, err) - - suite.Run(t, &storage.ReceiptTestSuite{ReceiptIndexer: NewReceipts(db)}) - }) -} - -func TestTransactions(t *testing.T) { - runDB("transactions", t, func(t *testing.T, db *Storage) { - suite.Run(t, &storage.TransactionTestSuite{TransactionIndexer: NewTransactions(db)}) - }) -} - -func TestAccounts(t *testing.T) { - runDB("accounts", t, func(t *testing.T, db *Storage) { - suite.Run(t, &storage.AccountTestSuite{AccountIndexer: NewAccounts(db)}) - }) -} - -func TestTraces(t *testing.T) { - runDB("traces", t, func(t *testing.T, db *Storage) { - suite.Run(t, &storage.TraceTestSuite{TraceIndexer: NewTraces(db)}) - }) -} - func TestBlock(t *testing.T) { runDB("store block", t, func(t *testing.T, db *Storage) { @@ -149,24 +92,12 @@ func TestBlock(t *testing.T) { }) } -func TestAccount(t *testing.T) { - t.Run("encoding decoding nonce data", func(t *testing.T) { - nonce := uint64(10) - height := uint64(20) - raw := encodeNonce(10, 20) - decNonce, decHeight, err := decodeNonce(raw) - require.NoError(t, err) - assert.Equal(t, nonce, decNonce) - assert.Equal(t, height, decHeight) - }) -} - func TestBatch(t *testing.T) { runDB("batch successfully stores", t, func(t *testing.T, db *Storage) { blocks := NewBlocks(db, flowGo.Emulator) trace := NewTraces(db) - batch := db.NewIndexedBatch() + batch := db.NewBatch() defer func() { require.NoError(t, batch.Close()) }() @@ -194,7 +125,7 @@ func TestBatch(t *testing.T) { runDB("should not contain data without committing", t, func(t *testing.T, db *Storage) { blocks := NewBlocks(db, flowGo.Emulator) - batch := db.NewIndexedBatch() + batch := db.NewBatch() defer func() { require.NoError(t, batch.Close()) }() @@ -215,7 +146,7 @@ func TestBatch(t *testing.T) { evmHeight := uint64(10 + i) bl := mocks.NewBlock(evmHeight) - batch := db.NewIndexedBatch() + batch := db.NewBatch() err := blocks.Store(cadenceHeight, flow.HexToID("0x1"), bl, batch) require.NoError(t, err) diff --git a/storage/register_delta.go b/storage/register_delta.go index aec8d0ac0..4aba9fc37 100644 --- a/storage/register_delta.go +++ b/storage/register_delta.go @@ -1,7 +1,11 @@ package storage import ( + "fmt" + "github.com/onflow/atree" + "github.com/onflow/flow-go/fvm/environment" + "github.com/onflow/flow-go/fvm/errors" "github.com/onflow/flow-go/fvm/evm/types" "github.com/onflow/flow-go/model/flow" ) @@ -68,6 +72,51 @@ func (r *RegisterDelta) GetUpdates() flow.RegisterEntries { return entries } -func (r *RegisterDelta) AllocateSlabIndex(_ []byte) (atree.SlabIndex, error) { - return atree.SlabIndex{}, nil +func (r *RegisterDelta) AllocateSlabIndex(owner []byte) (atree.SlabIndex, error) { + // get status + address := flow.BytesToAddress(owner) + id := flow.AccountStatusRegisterID(address) + statusBytes, err := r.GetValue(owner, []byte(id.Key)) + if err != nil { + return atree.SlabIndex{}, fmt.Errorf( + "failed to load account status for the account (%s): %w", + address.String(), + err) + } + if len(statusBytes) == 0 { + return atree.SlabIndex{}, errors.NewAccountNotFoundError(address) + } + status, err := environment.AccountStatusFromBytes(statusBytes) + if err != nil { + return atree.SlabIndex{}, err + } + + // get and increment the index + index := status.SlabIndex() + newIndexBytes := index.Next() + + // store nil so that the setValue for new allocated slabs would be faster + // and won't do ledger getValue for every new slabs (currently happening to + // compute storage size changes) + // this way the getValue would load this value from deltas + key := atree.SlabIndexToLedgerKey(index) + err = r.SetValue(owner, key, []byte{}) + if err != nil { + return atree.SlabIndex{}, fmt.Errorf( + "failed to allocate an storage index: %w", + err) + } + + // update the storageIndex bytes + status.SetStorageIndex(newIndexBytes) + + err = r.SetValue(owner, []byte(id.Key), status.ToBytes()) + if err != nil { + return atree.SlabIndex{}, fmt.Errorf( + "failed to store the account status for account (%s): %w", + address.String(), + err) + } + return index, nil + } From 4bc7382bb8e189da7a089f62be3b249bc0fda85e Mon Sep 17 00:00:00 2001 From: Janez Podhostnik Date: Fri, 8 Nov 2024 15:38:01 +0100 Subject: [PATCH 059/174] remove additional locks --- services/ingestion/engine.go | 1 + storage/pebble/blocks.go | 29 ----------------------------- storage/pebble/traces.go | 9 --------- 3 files changed, 1 insertion(+), 38 deletions(-) diff --git a/services/ingestion/engine.go b/services/ingestion/engine.go index 4486bdf81..ab7a01c78 100644 --- a/services/ingestion/engine.go +++ b/services/ingestion/engine.go @@ -3,6 +3,7 @@ package ingestion import ( "context" "fmt" + flowGo "github.com/onflow/flow-go/model/flow" pebbleDB "github.com/cockroachdb/pebble" diff --git a/storage/pebble/blocks.go b/storage/pebble/blocks.go index 5643ba70b..42099b1d4 100644 --- a/storage/pebble/blocks.go +++ b/storage/pebble/blocks.go @@ -5,7 +5,6 @@ import ( "errors" "fmt" "slices" - "sync" "github.com/cockroachdb/pebble" "github.com/onflow/flow-go-sdk" @@ -27,7 +26,6 @@ var _ storage.BlockIndexer = &Blocks{} type Blocks struct { store *Storage - mux sync.RWMutex chainID flowGo.ChainID } @@ -35,7 +33,6 @@ func NewBlocks(store *Storage, chainID flowGo.ChainID) *Blocks { return &Blocks{ store: store, chainID: chainID, - mux: sync.RWMutex{}, } } @@ -45,8 +42,6 @@ func (b *Blocks) Store( block *models.Block, batch *pebble.Batch, ) error { - b.mux.Lock() - defer b.mux.Unlock() // dev note: please be careful if any store reads are added here, // store.batchGet must be used instead and batch must be used @@ -122,9 +117,6 @@ func (b *Blocks) Store( } func (b *Blocks) GetByHeight(height uint64) (*models.Block, error) { - b.mux.RLock() - defer b.mux.RUnlock() - last, err := b.latestEVMHeight() if err != nil { return nil, err @@ -144,9 +136,6 @@ func (b *Blocks) GetByHeight(height uint64) (*models.Block, error) { } func (b *Blocks) GetByID(ID common.Hash) (*models.Block, error) { - b.mux.RLock() - defer b.mux.RUnlock() - height, err := b.store.get(blockIDToHeightKey, ID.Bytes()) if err != nil { return nil, fmt.Errorf("failed to get EVM block by ID: %s, with: %w", ID, err) @@ -165,9 +154,6 @@ func (b *Blocks) GetByID(ID common.Hash) (*models.Block, error) { } func (b *Blocks) GetHeightByID(ID common.Hash) (uint64, error) { - b.mux.RLock() - defer b.mux.RUnlock() - height, err := b.store.get(blockIDToHeightKey, ID.Bytes()) if err != nil { return 0, fmt.Errorf("failed to get EVM block by ID: %s, with: %w", ID, err) @@ -177,9 +163,6 @@ func (b *Blocks) GetHeightByID(ID common.Hash) (uint64, error) { } func (b *Blocks) LatestEVMHeight() (uint64, error) { - b.mux.RLock() - defer b.mux.RUnlock() - return b.latestEVMHeight() } @@ -196,9 +179,6 @@ func (b *Blocks) latestEVMHeight() (uint64, error) { } func (b *Blocks) LatestCadenceHeight() (uint64, error) { - b.mux.RLock() - defer b.mux.RUnlock() - val, err := b.store.get(latestCadenceHeightKey) if err != nil { if errors.Is(err, errs.ErrEntityNotFound) { @@ -211,9 +191,6 @@ func (b *Blocks) LatestCadenceHeight() (uint64, error) { } func (b *Blocks) SetLatestCadenceHeight(height uint64, batch *pebble.Batch) error { - b.mux.Lock() - defer b.mux.Unlock() - if err := b.store.set(latestCadenceHeightKey, nil, uint64Bytes(height), batch); err != nil { return fmt.Errorf("failed to store latest Cadence height: %d, with: %w", height, err) } @@ -247,9 +224,6 @@ func (b *Blocks) InitHeights(cadenceHeight uint64, cadenceID flow.Identifier, ba } func (b *Blocks) GetCadenceHeight(evmHeight uint64) (uint64, error) { - b.mux.RLock() - defer b.mux.RUnlock() - val, err := b.store.get(evmHeightToCadenceHeightKey, uint64Bytes(evmHeight)) if err != nil { return 0, err @@ -259,9 +233,6 @@ func (b *Blocks) GetCadenceHeight(evmHeight uint64) (uint64, error) { } func (b *Blocks) GetCadenceID(evmHeight uint64) (flow.Identifier, error) { - b.mux.RLock() - defer b.mux.RUnlock() - val, err := b.store.get(evmHeightToCadenceIDKey, uint64Bytes(evmHeight)) if err != nil { return flow.Identifier{}, err diff --git a/storage/pebble/traces.go b/storage/pebble/traces.go index b3174d04c..7de1784d8 100644 --- a/storage/pebble/traces.go +++ b/storage/pebble/traces.go @@ -2,7 +2,6 @@ package pebble import ( "fmt" - "sync" "github.com/cockroachdb/pebble" "github.com/goccy/go-json" @@ -15,20 +14,15 @@ var _ storage.TraceIndexer = &Traces{} type Traces struct { store *Storage - mux sync.RWMutex } func NewTraces(store *Storage) *Traces { return &Traces{ store: store, - mux: sync.RWMutex{}, } } func (t *Traces) StoreTransaction(ID common.Hash, trace json.RawMessage, batch *pebble.Batch) error { - t.mux.Lock() - defer t.mux.Unlock() - if err := t.store.set(traceTxIDKey, ID.Bytes(), trace, batch); err != nil { return fmt.Errorf("failed to store trace for transaction ID %s: %w", ID.String(), err) } @@ -37,9 +31,6 @@ func (t *Traces) StoreTransaction(ID common.Hash, trace json.RawMessage, batch * } func (t *Traces) GetTransaction(ID common.Hash) (json.RawMessage, error) { - t.mux.RLock() - defer t.mux.RUnlock() - val, err := t.store.get(traceTxIDKey, ID.Bytes()) if err != nil { return nil, fmt.Errorf("failed to get trace for transaction ID %s: %w", ID.String(), err) From 5d29b0b4a188446d25de3034b4f04db24ed24f10 Mon Sep 17 00:00:00 2001 From: Janez Podhostnik Date: Tue, 12 Nov 2024 18:05:24 +0100 Subject: [PATCH 060/174] address review comments --- services/ingestion/engine.go | 52 +++++++++++++++++------------------- storage/pebble/keys.go | 10 +++---- storage/register_delta.go | 13 ++++++--- 3 files changed, 39 insertions(+), 36 deletions(-) diff --git a/services/ingestion/engine.go b/services/ingestion/engine.go index ab7a01c78..479970395 100644 --- a/services/ingestion/engine.go +++ b/services/ingestion/engine.go @@ -93,7 +93,8 @@ func NewEventIngestionEngine( // Stop the engine. func (e *Engine) Stop() { - // todo + e.MarkDone() + <-e.Stopped() } // Run the Cadence event ingestion engine. @@ -115,30 +116,33 @@ func (e *Engine) Run(ctx context.Context) error { e.log.Info().Msg("starting ingestion") e.MarkReady() + defer e.MarkStopped() - for events := range e.subscriber.Subscribe(ctx) { + events := e.subscriber.Subscribe(ctx) + + for { select { - case <-ctx.Done(): + case <-e.Done(): // stop the engine return nil - default: - } - - if events.Err != nil { - return fmt.Errorf( - "failure in event subscription with: %w", - events.Err, - ) - } - - err := e.processEvents(ctx, events.Events) - if err != nil { - e.log.Error().Err(err).Msg("failed to process EVM events") - return err + case events, ok := <-events: + if !ok { + return nil + } + if events.Err != nil { + return fmt.Errorf( + "failure in event subscription with: %w", + events.Err, + ) + } + + err := e.processEvents(events.Events) + if err != nil { + e.log.Error().Err(err).Msg("failed to process EVM events") + return err + } } } - - return nil } // processEvents converts the events to block and transactions and indexes them. @@ -153,7 +157,7 @@ func (e *Engine) Run(ctx context.Context) error { // https://github.com/onflow/flow-go/blob/master/fvm/evm/types/events.go // // Any error is unexpected and fatal. -func (e *Engine) processEvents(ctx context.Context, events *models.CadenceEvents) error { +func (e *Engine) processEvents(events *models.CadenceEvents) error { e.log.Info(). Uint64("cadence-height", events.CadenceHeight()). Int("cadence-event-length", events.Length()). @@ -258,14 +262,6 @@ func (e *Engine) processEvents(ctx context.Context, events *models.CadenceEvents } } - select { - case <-ctx.Done(): - // Temporary solution to avoid committing the batch when the DB is closed - // TODO(JanezP): handle this better - return nil - default: - } - if err := batch.Commit(pebbleDB.Sync); err != nil { return fmt.Errorf("failed to commit indexed data for Cadence block %d: %w", events.CadenceHeight(), err) } diff --git a/storage/pebble/keys.go b/storage/pebble/keys.go index f52eb0c4d..aa46b61a3 100644 --- a/storage/pebble/keys.go +++ b/storage/pebble/keys.go @@ -67,12 +67,12 @@ func NewMVCCComparer() *pebble.Comparer { // edge case. Not sure if this is possible, but just in case return 0 } - if a[0] != registerKeyMarker { - // default comparer - return len(a) + if a[0] == registerKeyMarker { + // special case for registers + return len(a) - 8 } - // special case for registers - return len(a) - 8 + // default comparer + return len(a) } comparer.Name = "flow.MVCCComparer" diff --git a/storage/register_delta.go b/storage/register_delta.go index 4aba9fc37..e7c5b85ed 100644 --- a/storage/register_delta.go +++ b/storage/register_delta.go @@ -73,10 +73,17 @@ func (r *RegisterDelta) GetUpdates() flow.RegisterEntries { } func (r *RegisterDelta) AllocateSlabIndex(owner []byte) (atree.SlabIndex, error) { + return allocateSlabIndex(owner, r) + +} + +// allocateSlabIndex allocates a new slab index for the given owner and key. +// this method only uses the storage get/set methods. +func allocateSlabIndex(owner []byte, storage types.BackendStorage) (atree.SlabIndex, error) { // get status address := flow.BytesToAddress(owner) id := flow.AccountStatusRegisterID(address) - statusBytes, err := r.GetValue(owner, []byte(id.Key)) + statusBytes, err := storage.GetValue(owner, []byte(id.Key)) if err != nil { return atree.SlabIndex{}, fmt.Errorf( "failed to load account status for the account (%s): %w", @@ -100,7 +107,7 @@ func (r *RegisterDelta) AllocateSlabIndex(owner []byte) (atree.SlabIndex, error) // compute storage size changes) // this way the getValue would load this value from deltas key := atree.SlabIndexToLedgerKey(index) - err = r.SetValue(owner, key, []byte{}) + err = storage.SetValue(owner, key, []byte{}) if err != nil { return atree.SlabIndex{}, fmt.Errorf( "failed to allocate an storage index: %w", @@ -110,7 +117,7 @@ func (r *RegisterDelta) AllocateSlabIndex(owner []byte) (atree.SlabIndex, error) // update the storageIndex bytes status.SetStorageIndex(newIndexBytes) - err = r.SetValue(owner, []byte(id.Key), status.ToBytes()) + err = storage.SetValue(owner, []byte(id.Key), status.ToBytes()) if err != nil { return atree.SlabIndex{}, fmt.Errorf( "failed to store the account status for account (%s): %w", From 4be38cd4dc87ed2ca327b2121954a73ed819aba0 Mon Sep 17 00:00:00 2001 From: Ardit Marku Date: Wed, 13 Nov 2024 13:28:12 +0200 Subject: [PATCH 061/174] Move eth related types to their own package to avoid import cycle errors --- api/api.go | 79 ++++++++++--------- api/debug.go | 3 +- api/pull.go | 5 +- api/stream.go | 5 +- api/utils.go | 3 +- api/wallet.go | 9 ++- api/models.go => eth/types/types.go | 2 +- api/models_test.go => eth/types/types_test.go | 2 +- 8 files changed, 57 insertions(+), 51 deletions(-) rename api/models.go => eth/types/types.go (99%) rename api/models_test.go => eth/types/types_test.go (99%) diff --git a/api/api.go b/api/api.go index 87d3fa628..4796a1947 100644 --- a/api/api.go +++ b/api/api.go @@ -18,6 +18,7 @@ import ( evmTypes "github.com/onflow/flow-go/fvm/evm/types" "github.com/onflow/flow-evm-gateway/config" + ethTypes "github.com/onflow/flow-evm-gateway/eth/types" "github.com/onflow/flow-evm-gateway/metrics" "github.com/onflow/flow-evm-gateway/models" errs "github.com/onflow/flow-evm-gateway/models/errors" @@ -227,7 +228,7 @@ func (b *BlockChainAPI) Syncing(ctx context.Context) (interface{}, error) { return false, nil } - return SyncStatus{ + return ethTypes.SyncStatus{ StartingBlock: hexutil.Uint64(b.indexingResumedHeight), CurrentBlock: hexutil.Uint64(currentBlock), HighestBlock: hexutil.Uint64(highestBlock), @@ -295,7 +296,7 @@ func (b *BlockChainAPI) GetBalance( func (b *BlockChainAPI) GetTransactionByHash( ctx context.Context, hash common.Hash, -) (*Transaction, error) { +) (*ethTypes.Transaction, error) { l := b.logger.With(). Str("endpoint", "getTransactionByHash"). Str("hash", hash.String()). @@ -307,15 +308,15 @@ func (b *BlockChainAPI) GetTransactionByHash( tx, err := b.transactions.Get(hash) if err != nil { - return handleError[*Transaction](err, l, b.collector) + return handleError[*ethTypes.Transaction](err, l, b.collector) } rcp, err := b.receipts.GetByTransactionID(hash) if err != nil { - return handleError[*Transaction](err, l, b.collector) + return handleError[*ethTypes.Transaction](err, l, b.collector) } - return NewTransactionResult(tx, *rcp, b.config.EVMNetworkID) + return ethTypes.NewTransactionResult(tx, *rcp, b.config.EVMNetworkID) } // GetTransactionByBlockHashAndIndex returns the transaction for the given block hash and index. @@ -323,7 +324,7 @@ func (b *BlockChainAPI) GetTransactionByBlockHashAndIndex( ctx context.Context, blockHash common.Hash, index hexutil.Uint, -) (*Transaction, error) { +) (*ethTypes.Transaction, error) { l := b.logger.With(). Str("endpoint", "getTransactionByBlockHashAndIndex"). Str("hash", blockHash.String()). @@ -336,7 +337,7 @@ func (b *BlockChainAPI) GetTransactionByBlockHashAndIndex( block, err := b.blocks.GetByID(blockHash) if err != nil { - return handleError[*Transaction](err, l, b.collector) + return handleError[*ethTypes.Transaction](err, l, b.collector) } if int(index) >= len(block.TransactionHashes) { @@ -346,7 +347,7 @@ func (b *BlockChainAPI) GetTransactionByBlockHashAndIndex( txHash := block.TransactionHashes[index] tx, err := b.prepareTransactionResponse(txHash) if err != nil { - return handleError[*Transaction](err, l, b.collector) + return handleError[*ethTypes.Transaction](err, l, b.collector) } return tx, nil @@ -358,7 +359,7 @@ func (b *BlockChainAPI) GetTransactionByBlockNumberAndIndex( ctx context.Context, blockNumber rpc.BlockNumber, index hexutil.Uint, -) (*Transaction, error) { +) (*ethTypes.Transaction, error) { l := b.logger.With(). Str("endpoint", "getTransactionByBlockNumberAndIndex"). Str("number", blockNumber.String()). @@ -372,14 +373,14 @@ func (b *BlockChainAPI) GetTransactionByBlockNumberAndIndex( if blockNumber < rpc.EarliestBlockNumber { latestBlockNumber, err := b.blocks.LatestEVMHeight() if err != nil { - return handleError[*Transaction](err, l, b.collector) + return handleError[*ethTypes.Transaction](err, l, b.collector) } blockNumber = rpc.BlockNumber(latestBlockNumber) } block, err := b.blocks.GetByHeight(uint64(blockNumber)) if err != nil { - return handleError[*Transaction](err, l, b.collector) + return handleError[*ethTypes.Transaction](err, l, b.collector) } if int(index) >= len(block.TransactionHashes) { @@ -389,7 +390,7 @@ func (b *BlockChainAPI) GetTransactionByBlockNumberAndIndex( txHash := block.TransactionHashes[index] tx, err := b.prepareTransactionResponse(txHash) if err != nil { - return handleError[*Transaction](err, l, b.collector) + return handleError[*ethTypes.Transaction](err, l, b.collector) } return tx, nil @@ -419,7 +420,7 @@ func (b *BlockChainAPI) GetTransactionReceipt( return handleError[map[string]interface{}](err, l, b.collector) } - txReceipt, err := MarshalReceipt(receipt, tx) + txReceipt, err := ethTypes.MarshalReceipt(receipt, tx) if err != nil { return handleError[map[string]interface{}](err, l, b.collector) } @@ -433,7 +434,7 @@ func (b *BlockChainAPI) GetBlockByHash( ctx context.Context, hash common.Hash, fullTx bool, -) (*Block, error) { +) (*ethTypes.Block, error) { l := b.logger.With(). Str("endpoint", "getBlockByHash"). Str("hash", hash.String()). @@ -445,12 +446,12 @@ func (b *BlockChainAPI) GetBlockByHash( block, err := b.blocks.GetByID(hash) if err != nil { - return handleError[*Block](err, l, b.collector) + return handleError[*ethTypes.Block](err, l, b.collector) } apiBlock, err := b.prepareBlockResponse(block, fullTx) if err != nil { - return handleError[*Block](err, l, b.collector) + return handleError[*ethTypes.Block](err, l, b.collector) } return apiBlock, nil @@ -467,7 +468,7 @@ func (b *BlockChainAPI) GetBlockByNumber( ctx context.Context, blockNumber rpc.BlockNumber, fullTx bool, -) (*Block, error) { +) (*ethTypes.Block, error) { l := b.logger.With(). Str("endpoint", "getBlockByNumber"). Str("blockNumber", blockNumber.String()). @@ -482,19 +483,19 @@ func (b *BlockChainAPI) GetBlockByNumber( if blockNumber < 0 { height, err = b.blocks.LatestEVMHeight() if err != nil { - return handleError[*Block](err, l, b.collector) + return handleError[*ethTypes.Block](err, l, b.collector) } } block, err := b.blocks.GetByHeight(height) if err != nil { - return handleError[*Block](err, l, b.collector) + return handleError[*ethTypes.Block](err, l, b.collector) } apiBlock, err := b.prepareBlockResponse(block, fullTx) if err != nil { - return handleError[*Block](err, l, b.collector) + return handleError[*ethTypes.Block](err, l, b.collector) } return apiBlock, nil @@ -536,7 +537,7 @@ func (b *BlockChainAPI) GetBlockReceipts( return handleError[[]map[string]interface{}](err, l, b.collector) } - receipts[i], err = MarshalReceipt(receipt, tx) + receipts[i], err = ethTypes.MarshalReceipt(receipt, tx) if err != nil { return handleError[[]map[string]interface{}](err, l, b.collector) } @@ -607,10 +608,10 @@ func (b *BlockChainAPI) GetBlockTransactionCountByNumber( // useful to execute and retrieve values. func (b *BlockChainAPI) Call( ctx context.Context, - args TransactionArgs, + args ethTypes.TransactionArgs, blockNumberOrHash *rpc.BlockNumberOrHash, - overrides *StateOverride, - blockOverrides *BlockOverrides, + stateOverrides *ethTypes.StateOverride, + _ *ethTypes.BlockOverrides, ) (hexutil.Bytes, error) { l := b.logger.With(). Str("endpoint", "call"). @@ -769,9 +770,9 @@ func (b *BlockChainAPI) GetTransactionCount( // configuration (if non-zero). func (b *BlockChainAPI) EstimateGas( ctx context.Context, - args TransactionArgs, + args ethTypes.TransactionArgs, blockNumberOrHash *rpc.BlockNumberOrHash, - overrides *StateOverride, + overrides *ethTypes.StateOverride, ) (hexutil.Uint64, error) { l := b.logger.With(). Str("endpoint", "estimateGas"). @@ -858,14 +859,14 @@ func (b *BlockChainAPI) FeeHistory( blockCount math.HexOrDecimal64, lastBlock rpc.BlockNumber, rewardPercentiles []float64, -) (*FeeHistoryResult, error) { +) (*ethTypes.FeeHistoryResult, error) { l := b.logger.With(). Str("endpoint", "feeHistory"). Str("block", lastBlock.String()). Logger() if blockCount > maxFeeHistoryBlockCount { - return handleError[*FeeHistoryResult]( + return handleError[*ethTypes.FeeHistoryResult]( fmt.Errorf("block count has to be between 1 and %d, got: %d", maxFeeHistoryBlockCount, blockCount), l, b.collector, @@ -878,7 +879,7 @@ func (b *BlockChainAPI) FeeHistory( // From the special block tags, we only support "latest". lastBlockNumber, err = b.blocks.LatestEVMHeight() if err != nil { - return handleError[*FeeHistoryResult](err, l, b.collector) + return handleError[*ethTypes.FeeHistoryResult](err, l, b.collector) } } @@ -921,7 +922,7 @@ func (b *BlockChainAPI) FeeHistory( gasUsedRatios = append(gasUsedRatios, gasUsedRatio) } - return &FeeHistoryResult{ + return ðTypes.FeeHistoryResult{ OldestBlock: oldestBlock, Reward: rewards, BaseFee: baseFees, @@ -971,8 +972,8 @@ func (b *BlockChainAPI) GetStorageAt( func (b *BlockChainAPI) fetchBlockTransactions( block *models.Block, -) ([]*Transaction, error) { - transactions := make([]*Transaction, 0) +) ([]*ethTypes.Transaction, error) { + transactions := make([]*ethTypes.Transaction, 0) for _, txHash := range block.TransactionHashes { transaction, err := b.prepareTransactionResponse(txHash) if err != nil { @@ -994,7 +995,7 @@ func (b *BlockChainAPI) fetchBlockTransactions( func (b *BlockChainAPI) prepareTransactionResponse( txHash common.Hash, -) (*Transaction, error) { +) (*ethTypes.Transaction, error) { tx, err := b.transactions.Get(txHash) if err != nil { return nil, err @@ -1005,20 +1006,20 @@ func (b *BlockChainAPI) prepareTransactionResponse( return nil, err } - return NewTransactionResult(tx, *receipt, b.config.EVMNetworkID) + return ethTypes.NewTransactionResult(tx, *receipt, b.config.EVMNetworkID) } func (b *BlockChainAPI) prepareBlockResponse( block *models.Block, fullTx bool, -) (*Block, error) { +) (*ethTypes.Block, error) { h, err := block.Hash() if err != nil { b.logger.Error().Err(err).Msg("failed to calculate hash for block by number") return nil, errs.ErrInternal } - blockResponse := &Block{ + blockResponse := ðTypes.Block{ Hash: h, Number: hexutil.Uint64(block.Height), ParentHash: block.ParentBlockHash, @@ -1168,7 +1169,7 @@ func (b *BlockChainAPI) GetProof( address common.Address, storageKeys []string, blockNumberOrHash rpc.BlockNumberOrHash, -) (*AccountResult, error) { +) (*ethTypes.AccountResult, error) { return nil, errs.NewEndpointNotSupportedError("eth_getProof") } @@ -1176,8 +1177,8 @@ func (b *BlockChainAPI) GetProof( // Reexec and blockNumberOrHash can be specified to create the accessList on top of a certain state. func (b *BlockChainAPI) CreateAccessList( ctx context.Context, - args TransactionArgs, + args ethTypes.TransactionArgs, blockNumberOrHash *rpc.BlockNumberOrHash, -) (*AccessListResult, error) { +) (*ethTypes.AccessListResult, error) { return nil, errs.NewEndpointNotSupportedError("eth_createAccessList") } diff --git a/api/debug.go b/api/debug.go index 9d68cefae..75a79d63b 100644 --- a/api/debug.go +++ b/api/debug.go @@ -17,6 +17,7 @@ import ( "github.com/rs/zerolog" "github.com/onflow/flow-evm-gateway/config" + ethTypes "github.com/onflow/flow-evm-gateway/eth/types" "github.com/onflow/flow-evm-gateway/metrics" "github.com/onflow/flow-evm-gateway/models" errs "github.com/onflow/flow-evm-gateway/models/errors" @@ -251,7 +252,7 @@ func (d *DebugAPI) TraceBlockByHash( func (d *DebugAPI) TraceCall( _ context.Context, - args TransactionArgs, + args ethTypes.TransactionArgs, blockNrOrHash rpc.BlockNumberOrHash, config *tracers.TraceCallConfig, ) (interface{}, error) { diff --git a/api/pull.go b/api/pull.go index bf5eb91a2..bd493ee5d 100644 --- a/api/pull.go +++ b/api/pull.go @@ -15,6 +15,7 @@ import ( "github.com/sethvargo/go-limiter" "github.com/onflow/flow-evm-gateway/config" + ethTypes "github.com/onflow/flow-evm-gateway/eth/types" errs "github.com/onflow/flow-evm-gateway/models/errors" "github.com/onflow/flow-evm-gateway/services/logs" "github.com/onflow/flow-evm-gateway/storage" @@ -435,7 +436,7 @@ func (api *PullAPI) getBlocks(latestHeight uint64, filter *blocksFilter) ([]comm } func (api *PullAPI) getTransactions(latestHeight uint64, filter *transactionsFilter) (any, error) { - txs := make([]*Transaction, 0) + txs := make([]*ethTypes.Transaction, 0) hashes := make([]common.Hash, 0) nextHeight := filter.next() @@ -464,7 +465,7 @@ func (api *PullAPI) getTransactions(latestHeight uint64, filter *transactionsFil if err != nil { return nil, err } - txResult, err := NewTransactionResult(tx, *receipt, api.config.EVMNetworkID) + txResult, err := ethTypes.NewTransactionResult(tx, *receipt, api.config.EVMNetworkID) if err != nil { return nil, err } diff --git a/api/stream.go b/api/stream.go index 57a979de1..bfc3b0fe0 100644 --- a/api/stream.go +++ b/api/stream.go @@ -12,6 +12,7 @@ import ( "github.com/rs/zerolog" "github.com/onflow/flow-evm-gateway/config" + ethTypes "github.com/onflow/flow-evm-gateway/eth/types" "github.com/onflow/flow-evm-gateway/models" errs "github.com/onflow/flow-evm-gateway/models/errors" "github.com/onflow/flow-evm-gateway/services/logs" @@ -123,14 +124,14 @@ func (s *StreamAPI) Logs(ctx context.Context, criteria filters.FilterCriteria) ( func (s *StreamAPI) prepareBlockHeader( block *models.Block, -) (*BlockHeader, error) { +) (*ethTypes.BlockHeader, error) { h, err := block.Hash() if err != nil { s.logger.Error().Err(err).Msg("failed to calculate hash for block by number") return nil, errs.ErrInternal } - blockHeader := &BlockHeader{ + blockHeader := ðTypes.BlockHeader{ Number: hexutil.Uint64(block.Height), Hash: h, ParentHash: block.ParentBlockHash, diff --git a/api/utils.go b/api/utils.go index 7e056e758..9f39d10f0 100644 --- a/api/utils.go +++ b/api/utils.go @@ -7,6 +7,7 @@ import ( "math/big" "strings" + ethTypes "github.com/onflow/flow-evm-gateway/eth/types" "github.com/onflow/flow-evm-gateway/metrics" errs "github.com/onflow/flow-evm-gateway/models/errors" "github.com/onflow/flow-evm-gateway/storage" @@ -135,7 +136,7 @@ func handleError[T any](err error, log zerolog.Logger, collector metrics.Collect // `EVM.dryRun` inside Cadence scripts, meaning that no state change // will occur. // This is only useful for `eth_estimateGas` and `eth_call` endpoints. -func encodeTxFromArgs(args TransactionArgs) (*types.LegacyTx, error) { +func encodeTxFromArgs(args ethTypes.TransactionArgs) (*types.LegacyTx, error) { var data []byte if args.Data != nil { data = *args.Data diff --git a/api/wallet.go b/api/wallet.go index 65cd3efe8..3629af113 100644 --- a/api/wallet.go +++ b/api/wallet.go @@ -14,6 +14,7 @@ import ( "github.com/onflow/go-ethereum/rpc" "github.com/onflow/flow-evm-gateway/config" + ethTypes "github.com/onflow/flow-evm-gateway/eth/types" ) type WalletAPI struct { @@ -66,8 +67,8 @@ func (w *WalletAPI) Sign( // the given from address and it needs to be unlocked. func (w *WalletAPI) SignTransaction( ctx context.Context, - args TransactionArgs, -) (*SignTransactionResult, error) { + args ethTypes.TransactionArgs, +) (*ethTypes.SignTransactionResult, error) { if args.Gas == nil { return nil, errors.New("gas not specified") } @@ -117,7 +118,7 @@ func (w *WalletAPI) SignTransaction( return nil, err } - return &SignTransactionResult{ + return ðTypes.SignTransactionResult{ Raw: raw, Tx: tx, }, nil @@ -127,7 +128,7 @@ func (w *WalletAPI) SignTransaction( // and submit it to the transaction pool. func (w *WalletAPI) SendTransaction( ctx context.Context, - args TransactionArgs, + args ethTypes.TransactionArgs, ) (common.Hash, error) { signed, err := w.SignTransaction(ctx, args) if err != nil { diff --git a/api/models.go b/eth/types/types.go similarity index 99% rename from api/models.go rename to eth/types/types.go index 98b5ba7d8..125c54629 100644 --- a/api/models.go +++ b/eth/types/types.go @@ -1,4 +1,4 @@ -package api +package types import ( "bytes" diff --git a/api/models_test.go b/eth/types/types_test.go similarity index 99% rename from api/models_test.go rename to eth/types/types_test.go index 2c4b024fc..e85383a50 100644 --- a/api/models_test.go +++ b/eth/types/types_test.go @@ -1,4 +1,4 @@ -package api +package types import ( "encoding/hex" From 9c069d79c79c4f1bbb52be08b61c02a1028e9b1e Mon Sep 17 00:00:00 2001 From: Ardit Marku Date: Wed, 13 Nov 2024 14:32:41 +0200 Subject: [PATCH 062/174] Support state overrides for eth_call & eth_estimateGas --- api/api.go | 6 +- api/debug.go | 3 + services/requester/requester.go | 79 ++++++++++++++++++- .../eth_deploy_contract_and_interact_test.js | 61 ++++++++++++++ 4 files changed, 142 insertions(+), 7 deletions(-) diff --git a/api/api.go b/api/api.go index 4796a1947..2360698fe 100644 --- a/api/api.go +++ b/api/api.go @@ -648,7 +648,7 @@ func (b *BlockChainAPI) Call( from = *args.From } - res, err := b.evm.Call(tx, from, height) + res, err := b.evm.Call(tx, from, height, stateOverrides) if err != nil { return handleError[hexutil.Bytes](err, l, b.collector) } @@ -772,7 +772,7 @@ func (b *BlockChainAPI) EstimateGas( ctx context.Context, args ethTypes.TransactionArgs, blockNumberOrHash *rpc.BlockNumberOrHash, - overrides *ethTypes.StateOverride, + stateOverrides *ethTypes.StateOverride, ) (hexutil.Uint64, error) { l := b.logger.With(). Str("endpoint", "estimateGas"). @@ -808,7 +808,7 @@ func (b *BlockChainAPI) EstimateGas( return handleError[hexutil.Uint64](err, l, b.collector) } - estimatedGas, err := b.evm.EstimateGas(tx, from, height) + estimatedGas, err := b.evm.EstimateGas(tx, from, height, stateOverrides) if err != nil { return handleError[hexutil.Uint64](err, l, b.collector) } diff --git a/api/debug.go b/api/debug.go index 75a79d63b..4c02cd0f4 100644 --- a/api/debug.go +++ b/api/debug.go @@ -332,6 +332,9 @@ func (d *DebugAPI) TraceCall( if account.Balance != nil { opts = append(opts, query.WithStateOverrideBalance(addr, (*big.Int)(*account.Balance))) } + if account.State != nil && account.StateDiff != nil { + return nil, fmt.Errorf("account %s has both 'state' and 'stateDiff'", addr.Hex()) + } // Replace entire state if caller requires. if account.State != nil { opts = append(opts, query.WithStateOverrideState(addr, *account.State)) diff --git a/services/requester/requester.go b/services/requester/requester.go index a703e81cf..b5f0e9df6 100644 --- a/services/requester/requester.go +++ b/services/requester/requester.go @@ -23,6 +23,7 @@ import ( "golang.org/x/sync/errgroup" "github.com/onflow/flow-evm-gateway/config" + ethTypes "github.com/onflow/flow-evm-gateway/eth/types" "github.com/onflow/flow-evm-gateway/metrics" "github.com/onflow/flow-evm-gateway/models" errs "github.com/onflow/flow-evm-gateway/models/errors" @@ -60,12 +61,22 @@ type Requester interface { // Call executes the given signed transaction data on the state for the given EVM block height. // Note, this function doesn't make and changes in the state/blockchain and is // useful to execute and retrieve values. - Call(tx *types.LegacyTx, from common.Address, height uint64) ([]byte, error) + Call( + tx *types.LegacyTx, + from common.Address, + height uint64, + stateOverrides *ethTypes.StateOverride, + ) ([]byte, error) // EstimateGas executes the given signed transaction data on the state for the given EVM block height. // Note, this function doesn't make any changes in the state/blockchain and is // useful to executed and retrieve the gas consumption and possible failures. - EstimateGas(tx *types.LegacyTx, from common.Address, height uint64) (uint64, error) + EstimateGas( + tx *types.LegacyTx, + from common.Address, + height uint64, + stateOverrides *ethTypes.StateOverride, + ) (uint64, error) // GetNonce gets nonce from the network at the given EVM block height. GetNonce(address common.Address, height uint64) (uint64, error) @@ -340,6 +351,7 @@ func (e *EVM) Call( tx *types.LegacyTx, from common.Address, height uint64, + stateOverrides *ethTypes.StateOverride, ) ([]byte, error) { view, err := e.getBlockView(height) if err != nil { @@ -355,13 +367,42 @@ func (e *EVM) Call( return nil, err } rca := NewRemoteCadenceArch(cdcHeight, e.client, e.config.FlowNetworkID) + opts := []query.DryCallOption{} + opts = append(opts, query.WithExtraPrecompiledContracts([]evmTypes.PrecompiledContract{rca})) + if stateOverrides != nil { + for addr, account := range *stateOverrides { + // Override account nonce. + if account.Nonce != nil { + opts = append(opts, query.WithStateOverrideNonce(addr, uint64(*account.Nonce))) + } + // Override account(contract) code. + if account.Code != nil { + opts = append(opts, query.WithStateOverrideCode(addr, *account.Code)) + } + // Override account balance. + if account.Balance != nil { + opts = append(opts, query.WithStateOverrideBalance(addr, (*big.Int)(*account.Balance))) + } + if account.State != nil && account.StateDiff != nil { + return nil, fmt.Errorf("account %s has both 'state' and 'stateDiff'", addr.Hex()) + } + // Replace entire state if caller requires. + if account.State != nil { + opts = append(opts, query.WithStateOverrideState(addr, *account.State)) + } + // Apply state diff into specified accounts. + if account.StateDiff != nil { + opts = append(opts, query.WithStateOverrideStateDiff(addr, *account.StateDiff)) + } + } + } result, err := view.DryCall( from, to, tx.Data, tx.Value, tx.Gas, - query.WithExtraPrecompiledContracts([]evmTypes.PrecompiledContract{rca}), + opts..., ) resultSummary := result.ResultSummary() @@ -379,6 +420,7 @@ func (e *EVM) EstimateGas( tx *types.LegacyTx, from common.Address, height uint64, + stateOverrides *ethTypes.StateOverride, ) (uint64, error) { view, err := e.getBlockView(height) if err != nil { @@ -394,13 +436,42 @@ func (e *EVM) EstimateGas( return 0, err } rca := NewRemoteCadenceArch(cdcHeight, e.client, e.config.FlowNetworkID) + opts := []query.DryCallOption{} + opts = append(opts, query.WithExtraPrecompiledContracts([]evmTypes.PrecompiledContract{rca})) + if stateOverrides != nil { + for addr, account := range *stateOverrides { + // Override account nonce. + if account.Nonce != nil { + opts = append(opts, query.WithStateOverrideNonce(addr, uint64(*account.Nonce))) + } + // Override account(contract) code. + if account.Code != nil { + opts = append(opts, query.WithStateOverrideCode(addr, *account.Code)) + } + // Override account balance. + if account.Balance != nil { + opts = append(opts, query.WithStateOverrideBalance(addr, (*big.Int)(*account.Balance))) + } + if account.State != nil && account.StateDiff != nil { + return 0, fmt.Errorf("account %s has both 'state' and 'stateDiff'", addr.Hex()) + } + // Replace entire state if caller requires. + if account.State != nil { + opts = append(opts, query.WithStateOverrideState(addr, *account.State)) + } + // Apply state diff into specified accounts. + if account.StateDiff != nil { + opts = append(opts, query.WithStateOverrideStateDiff(addr, *account.StateDiff)) + } + } + } result, err := view.DryCall( from, to, tx.Data, tx.Value, tx.Gas, - query.WithExtraPrecompiledContracts([]evmTypes.PrecompiledContract{rca}), + opts..., ) if err != nil { return 0, err diff --git a/tests/web3js/eth_deploy_contract_and_interact_test.js b/tests/web3js/eth_deploy_contract_and_interact_test.js index 4fcbfcb11..2e029aa19 100644 --- a/tests/web3js/eth_deploy_contract_and_interact_test.js +++ b/tests/web3js/eth_deploy_contract_and_interact_test.js @@ -235,4 +235,65 @@ it('deploy contract and interact', async () => { ) assert.equal(gasEstimate, 27398n) + // check that `eth_call` can handle state overrides + let stateOverrides = { + [contractAddress]: { + stateDiff: { + '0x0000000000000000000000000000000000000000000000000000000000000000': '0x00000000000000000000000000000000000000000000000000000000000003e8' + } + } + } + let response = await helpers.callRPCMethod( + 'eth_call', + [{ to: contractAddress, data: callRetrieve }, 'latest', stateOverrides] + ) + assert.equal(response.status, 200) + assert.isDefined(response.body) + + result = response.body.result + assert.equal( + result, + '0x00000000000000000000000000000000000000000000000000000000000003e8' + ) + + // check that `eth_estimateGas` can handle state overrides + stateOverrides = { + [contractAddress]: { + stateDiff: { + '0x0000000000000000000000000000000000000000000000000000000000000000': '0x00000000000000000000000000000000000000000000000000000000000003e8' + } + } + } + + updateData = deployed.contract.methods.store(100n).encodeABI() + response = await helpers.callRPCMethod( + 'eth_estimateGas', + [{ to: contractAddress, data: updateData }, 'latest', stateOverrides] + ) + assert.equal(response.status, 200) + assert.isDefined(response.body) + + result = response.body.result + assert.equal(result, '0x72c3') + + stateOverrides = { + [contractAddress]: { + stateDiff: { + '0x0000000000000000000000000000000000000000000000000000000000000000': '0x0000000000000000000000000000000000000000000000000000000000000000' + } + } + } + + updateData = deployed.contract.methods.store(100n).encodeABI() + response = await helpers.callRPCMethod( + 'eth_estimateGas', + [{ to: contractAddress, data: updateData }, 'latest', stateOverrides] + ) + assert.equal(response.status, 200) + assert.isDefined(response.body) + + // setting a storage slot from a zero-value, to a non-zero value has an + // increase of about 20,000 gas. Which is quite different to `0x72c3`. + result = response.body.result + assert.equal(result, '0xb69a') }) From c33b37265cb5afb75d5339f83b9652f5f098bcb1 Mon Sep 17 00:00:00 2001 From: Ardit Marku Date: Wed, 13 Nov 2024 17:46:37 +0200 Subject: [PATCH 063/174] Extract the common logic from Call & EstimateGas in dryRunTx helper method --- services/requester/requester.go | 189 +++++++++++++------------------- 1 file changed, 74 insertions(+), 115 deletions(-) diff --git a/services/requester/requester.go b/services/requester/requester.go index b5f0e9df6..413daf270 100644 --- a/services/requester/requester.go +++ b/services/requester/requester.go @@ -353,66 +353,11 @@ func (e *EVM) Call( height uint64, stateOverrides *ethTypes.StateOverride, ) ([]byte, error) { - view, err := e.getBlockView(height) + result, err := e.dryRunTx(tx, from, height, stateOverrides) if err != nil { return nil, err } - to := common.Address{} - if tx.To != nil { - to = *tx.To - } - cdcHeight, err := e.evmToCadenceHeight(height) - if err != nil { - return nil, err - } - rca := NewRemoteCadenceArch(cdcHeight, e.client, e.config.FlowNetworkID) - opts := []query.DryCallOption{} - opts = append(opts, query.WithExtraPrecompiledContracts([]evmTypes.PrecompiledContract{rca})) - if stateOverrides != nil { - for addr, account := range *stateOverrides { - // Override account nonce. - if account.Nonce != nil { - opts = append(opts, query.WithStateOverrideNonce(addr, uint64(*account.Nonce))) - } - // Override account(contract) code. - if account.Code != nil { - opts = append(opts, query.WithStateOverrideCode(addr, *account.Code)) - } - // Override account balance. - if account.Balance != nil { - opts = append(opts, query.WithStateOverrideBalance(addr, (*big.Int)(*account.Balance))) - } - if account.State != nil && account.StateDiff != nil { - return nil, fmt.Errorf("account %s has both 'state' and 'stateDiff'", addr.Hex()) - } - // Replace entire state if caller requires. - if account.State != nil { - opts = append(opts, query.WithStateOverrideState(addr, *account.State)) - } - // Apply state diff into specified accounts. - if account.StateDiff != nil { - opts = append(opts, query.WithStateOverrideStateDiff(addr, *account.StateDiff)) - } - } - } - result, err := view.DryCall( - from, - to, - tx.Data, - tx.Value, - tx.Gas, - opts..., - ) - - resultSummary := result.ResultSummary() - if resultSummary.ErrorCode != 0 { - if resultSummary.ErrorCode == evmTypes.ExecutionErrCodeExecutionReverted { - return nil, errs.NewRevertError(resultSummary.ReturnedData) - } - return nil, errs.NewFailedTransactionError(resultSummary.ErrorMessage) - } - return result.ReturnedData, err } @@ -422,69 +367,11 @@ func (e *EVM) EstimateGas( height uint64, stateOverrides *ethTypes.StateOverride, ) (uint64, error) { - view, err := e.getBlockView(height) + result, err := e.dryRunTx(tx, from, height, stateOverrides) if err != nil { return 0, err } - to := common.Address{} - if tx.To != nil { - to = *tx.To - } - cdcHeight, err := e.evmToCadenceHeight(height) - if err != nil { - return 0, err - } - rca := NewRemoteCadenceArch(cdcHeight, e.client, e.config.FlowNetworkID) - opts := []query.DryCallOption{} - opts = append(opts, query.WithExtraPrecompiledContracts([]evmTypes.PrecompiledContract{rca})) - if stateOverrides != nil { - for addr, account := range *stateOverrides { - // Override account nonce. - if account.Nonce != nil { - opts = append(opts, query.WithStateOverrideNonce(addr, uint64(*account.Nonce))) - } - // Override account(contract) code. - if account.Code != nil { - opts = append(opts, query.WithStateOverrideCode(addr, *account.Code)) - } - // Override account balance. - if account.Balance != nil { - opts = append(opts, query.WithStateOverrideBalance(addr, (*big.Int)(*account.Balance))) - } - if account.State != nil && account.StateDiff != nil { - return 0, fmt.Errorf("account %s has both 'state' and 'stateDiff'", addr.Hex()) - } - // Replace entire state if caller requires. - if account.State != nil { - opts = append(opts, query.WithStateOverrideState(addr, *account.State)) - } - // Apply state diff into specified accounts. - if account.StateDiff != nil { - opts = append(opts, query.WithStateOverrideStateDiff(addr, *account.StateDiff)) - } - } - } - result, err := view.DryCall( - from, - to, - tx.Data, - tx.Value, - tx.Gas, - opts..., - ) - if err != nil { - return 0, err - } - - resultSummary := result.ResultSummary() - if resultSummary.ErrorCode != 0 { - if resultSummary.ErrorCode == evmTypes.ExecutionErrCodeExecutionReverted { - return 0, errs.NewRevertError(resultSummary.ReturnedData) - } - return 0, errs.NewFailedTransactionError(resultSummary.ErrorMessage) - } - if result.Successful() { // As mentioned in https://github.com/ethereum/EIPs/blob/master/EIPS/eip-150.md#specification // Define "all but one 64th" of N as N - floor(N / 64). @@ -597,6 +484,78 @@ func (e *EVM) evmToCadenceHeight(height uint64) (uint64, error) { return cadenceHeight, nil } +func (e *EVM) dryRunTx( + tx *types.LegacyTx, + from common.Address, + height uint64, + stateOverrides *ethTypes.StateOverride, +) (*evmTypes.Result, error) { + view, err := e.getBlockView(height) + if err != nil { + return nil, err + } + + to := common.Address{} + if tx.To != nil { + to = *tx.To + } + cdcHeight, err := e.evmToCadenceHeight(height) + if err != nil { + return nil, err + } + rca := NewRemoteCadenceArch(cdcHeight, e.client, e.config.FlowNetworkID) + opts := []query.DryCallOption{} + opts = append(opts, query.WithExtraPrecompiledContracts([]evmTypes.PrecompiledContract{rca})) + if stateOverrides != nil { + for addr, account := range *stateOverrides { + // Override account nonce. + if account.Nonce != nil { + opts = append(opts, query.WithStateOverrideNonce(addr, uint64(*account.Nonce))) + } + // Override account(contract) code. + if account.Code != nil { + opts = append(opts, query.WithStateOverrideCode(addr, *account.Code)) + } + // Override account balance. + if account.Balance != nil { + opts = append(opts, query.WithStateOverrideBalance(addr, (*big.Int)(*account.Balance))) + } + if account.State != nil && account.StateDiff != nil { + return nil, fmt.Errorf("account %s has both 'state' and 'stateDiff'", addr.Hex()) + } + // Replace entire state if caller requires. + if account.State != nil { + opts = append(opts, query.WithStateOverrideState(addr, *account.State)) + } + // Apply state diff into specified accounts. + if account.StateDiff != nil { + opts = append(opts, query.WithStateOverrideStateDiff(addr, *account.StateDiff)) + } + } + } + result, err := view.DryCall( + from, + to, + tx.Data, + tx.Value, + tx.Gas, + opts..., + ) + if err != nil { + return nil, err + } + + resultSummary := result.ResultSummary() + if resultSummary.ErrorCode != 0 { + if resultSummary.ErrorCode == evmTypes.ExecutionErrCodeExecutionReverted { + return nil, errs.NewRevertError(resultSummary.ReturnedData) + } + return nil, errs.NewFailedTransactionError(resultSummary.ErrorMessage) + } + + return result, nil +} + func AddOne64th(n uint64) uint64 { // NOTE: Go's integer division floors, but that is desirable here return n + (n / 64) From 5c38a0fe316a00289d9669669f9cb94bd19ba7d3 Mon Sep 17 00:00:00 2001 From: Janez Podhostnik Date: Fri, 8 Nov 2024 17:37:06 +0100 Subject: [PATCH 064/174] batch get events when backfilling --- cmd/run/cmd.go | 9 +++ services/ingestion/event_subscriber.go | 83 ++++++++++++++++++++---- services/requester/cross-spork_client.go | 11 ++++ 3 files changed, 92 insertions(+), 11 deletions(-) diff --git a/cmd/run/cmd.go b/cmd/run/cmd.go index 3b93e45a1..1f1ec07fa 100644 --- a/cmd/run/cmd.go +++ b/cmd/run/cmd.go @@ -7,6 +7,7 @@ import ( "math/big" "os" "os/signal" + "runtime/pprof" "strings" "syscall" "time" @@ -29,6 +30,14 @@ var Cmd = &cobra.Command{ Use: "run", Short: "Runs the EVM Gateway Node", Run: func(*cobra.Command, []string) { + + f, err := os.Create("cpu.pprof") + if err != nil { + log.Fatal().Err(err).Msg("could not create cpu profile") + } + pprof.StartCPUProfile(f) + defer pprof.StopCPUProfile() + // create multi-key account if _, exists := os.LookupEnv("MULTIKEY_MODE"); exists { bootstrap.RunCreateMultiKeyAccount() diff --git a/services/ingestion/event_subscriber.go b/services/ingestion/event_subscriber.go index 47da89723..a04d2e001 100644 --- a/services/ingestion/event_subscriber.go +++ b/services/ingestion/event_subscriber.go @@ -4,6 +4,8 @@ import ( "context" "errors" "fmt" + "sort" + "time" "github.com/onflow/cadence/common" "github.com/onflow/flow-go/fvm/evm/events" @@ -62,7 +64,8 @@ func NewRPCEventSubscriber( // // If error is encountered during backfill the subscription will end and the response chanel will be closed. func (r *RPCEventSubscriber) Subscribe(ctx context.Context) <-chan models.BlockEvents { - eventsChan := make(chan models.BlockEvents) + // buffered channel so that the decoding of the events can happen in parallel to other operations + eventsChan := make(chan models.BlockEvents, 1000) go func() { defer func() { @@ -196,6 +199,7 @@ func (r *RPCEventSubscriber) subscribe(ctx context.Context, height uint64) <-cha // the height by one (next height), and check if we are still in previous sporks, if so repeat everything, // otherwise return. func (r *RPCEventSubscriber) backfill(ctx context.Context, height uint64) <-chan models.BlockEvents { + // TODO(JanezP): if we are backfilling, its more efficient to request events in a batch eventsChan := make(chan models.BlockEvents) go func() { @@ -224,25 +228,82 @@ func (r *RPCEventSubscriber) backfill(ctx context.Context, height uint64) <-chan Uint64("last-spork-height", latestHeight). Msg("backfilling spork") - for ev := range r.subscribe(ctx, height) { - eventsChan <- ev + ticker := time.NewTicker(time.Millisecond * 10) - if ev.Err != nil { + maxRange := uint64(249) + for height < latestHeight { + + // TODO: do rate limiting better + <-ticker.C + + startHeight := height + r.logger.Debug().Msg(fmt.Sprintf("backfilling [%d / %d]...", startHeight, latestHeight)) + endHeight := height + maxRange + if endHeight > latestHeight { + endHeight = latestHeight + } + + evmAddress := common.Address(systemcontracts.SystemContractsForChain(r.chain).EVMContract.Address) + blockExecutedEvent := common.NewAddressLocation( + nil, + evmAddress, + string(events.EventTypeBlockExecuted), + ).ID() + + transactionExecutedEvent := common.NewAddressLocation( + nil, + evmAddress, + string(events.EventTypeTransactionExecuted), + ).ID() + + // + blocks, err := r.client.GetEventsForHeightRange(ctx, blockExecutedEvent, startHeight, endHeight) + if err != nil { + r.logger.Error().Err(err).Msg("failed to get block events") + eventsChan <- models.NewBlockEventsError(err) + return + } + sort.Slice(blocks, func(i, j int) bool { + return blocks[i].Height < blocks[j].Height + }) + + transactions, err := r.client.GetEventsForHeightRange(ctx, transactionExecutedEvent, startHeight, endHeight) + if err != nil { + r.logger.Error().Err(err).Msg("failed to get block events") + eventsChan <- models.NewBlockEventsError(err) return } - r.logger.Debug().Msg(fmt.Sprintf("backfilling [%d / %d]...", ev.Events.CadenceHeight(), latestHeight)) + sort.Slice(transactions, func(i, j int) bool { + return transactions[i].Height < transactions[j].Height + }) + + if len(transactions) != len(blocks) { + r.logger.Error().Msg("transactions and blocks have different length") + eventsChan <- models.NewBlockEventsError(err) + return + } - if ev.Events != nil && ev.Events.CadenceHeight() == latestHeight { - height = ev.Events.CadenceHeight() + 1 // go to next height in the next spork + for i := range transactions { + if transactions[i].Height != blocks[i].Height { + r.logger.Error().Msg("transactions and blocks have different height") + eventsChan <- models.NewBlockEventsError(err) + return + } + // append the transaction events to the block events + blocks[i].Events = append(blocks[i].Events, transactions[i].Events...) - r.logger.Info(). - Uint64("next-height", height). - Msg("reached the end of spork, checking next spork") + evmEvents := models.NewBlockEvents(blocks[i]) + height = evmEvents.Events.CadenceHeight() + 1 - break } + } + ticker.Stop() + + r.logger.Info(). + Uint64("next-height", height). + Msg("reached the end of spork, checking next spork") } }() diff --git a/services/requester/cross-spork_client.go b/services/requester/cross-spork_client.go index a9df208ee..91cc76341 100644 --- a/services/requester/cross-spork_client.go +++ b/services/requester/cross-spork_client.go @@ -214,3 +214,14 @@ func (c *CrossSporkClient) SubscribeEventsByBlockHeight( } return client.SubscribeEventsByBlockHeight(ctx, startHeight, filter, opts...) } + +func (c *CrossSporkClient) GetEventsForHeightRange( + ctx context.Context, eventType string, startHeight uint64, endHeight uint64, +) ([]flow.BlockEvents, error) { + // TODO: also make sure the endHeight is not too high + client, err := c.getClientForHeight(startHeight) + if err != nil { + return nil, err + } + return client.GetEventsForHeightRange(ctx, eventType, startHeight, endHeight) +} From 78abedbc53e7fbc3b3387743f107b5f7d43052cb Mon Sep 17 00:00:00 2001 From: Janez Podhostnik Date: Wed, 13 Nov 2024 15:37:11 +0100 Subject: [PATCH 065/174] cleanup --- cmd/run/cmd.go | 29 ++--- go.mod | 2 + go.sum | 4 + services/ingestion/engine.go | 18 +-- services/ingestion/event_subscriber.go | 150 +++++++++++------------ services/requester/cross-spork_client.go | 30 ++++- services/testutils/mock_client.go | 14 +++ 7 files changed, 143 insertions(+), 104 deletions(-) diff --git a/cmd/run/cmd.go b/cmd/run/cmd.go index 1f1ec07fa..6b87e04ce 100644 --- a/cmd/run/cmd.go +++ b/cmd/run/cmd.go @@ -7,7 +7,6 @@ import ( "math/big" "os" "os/signal" - "runtime/pprof" "strings" "syscall" "time" @@ -29,15 +28,7 @@ import ( var Cmd = &cobra.Command{ Use: "run", Short: "Runs the EVM Gateway Node", - Run: func(*cobra.Command, []string) { - - f, err := os.Create("cpu.pprof") - if err != nil { - log.Fatal().Err(err).Msg("could not create cpu profile") - } - pprof.StartCPUProfile(f) - defer pprof.StopCPUProfile() - + Run: func(command *cobra.Command, _ []string) { // create multi-key account if _, exists := os.LookupEnv("MULTIKEY_MODE"); exists { bootstrap.RunCreateMultiKeyAccount() @@ -49,13 +40,15 @@ var Cmd = &cobra.Command{ os.Exit(1) } - ctx, cancel := context.WithCancel(context.Background()) + ctx, cancel := context.WithCancel(command.Context()) + done := make(chan struct{}) ready := make(chan struct{}) go func() { - if err := bootstrap.Run(ctx, cfg, ready); err != nil { + defer close(done) + err := bootstrap.Run(ctx, cfg, ready) + if err != nil { log.Err(err).Msg("failed to run bootstrap") cancel() - os.Exit(1) } }() @@ -64,7 +57,15 @@ var Cmd = &cobra.Command{ osSig := make(chan os.Signal, 1) signal.Notify(osSig, syscall.SIGINT, syscall.SIGTERM) - <-osSig + select { + case <-osSig: + log.Info().Msg("OS Signal to shutdown received, shutting down") + cancel() + case <-done: + log.Info().Msg("done, shutting down") + cancel() + } + log.Info().Msg("OS Signal to shutdown received, shutting down") cancel() }, diff --git a/go.mod b/go.mod index 335ec24ef..5fcd558ff 100644 --- a/go.mod +++ b/go.mod @@ -18,6 +18,7 @@ require ( github.com/sethvargo/go-retry v0.2.3 github.com/spf13/cobra v1.8.1 github.com/stretchr/testify v1.9.0 + go.uber.org/ratelimit v0.3.1 golang.org/x/exp v0.0.0-20240119083558-1b970713d09a golang.org/x/sync v0.8.0 google.golang.org/grpc v1.63.2 @@ -35,6 +36,7 @@ require ( github.com/SaveTheRbtz/mph v0.1.1-0.20240117162131-4166ec7869bc // indirect github.com/StackExchange/wmi v1.2.1 // indirect github.com/VictoriaMetrics/fastcache v1.12.2 // indirect + github.com/benbjohnson/clock v1.3.5 // indirect github.com/beorn7/perks v1.0.1 // indirect github.com/bits-and-blooms/bitset v1.10.0 // indirect github.com/btcsuite/btcd/btcec/v2 v2.3.4 // indirect diff --git a/go.sum b/go.sum index 6fe1a45ae..98d1b96be 100644 --- a/go.sum +++ b/go.sum @@ -69,6 +69,8 @@ github.com/allegro/bigcache v1.2.1-0.20190218064605-e24eb225f156 h1:eMwmnE/GDgah github.com/allegro/bigcache v1.2.1-0.20190218064605-e24eb225f156/go.mod h1:Cb/ax3seSYIx7SuZdm2G2xzfwmv3TPSk2ucNfQESPXM= github.com/armon/consul-api v0.0.0-20180202201655-eb2c6b5be1b6/go.mod h1:grANhF5doyWs3UAsr3K4I6qtAmlQcZDesFNEHPZAzj8= github.com/benbjohnson/clock v1.1.0/go.mod h1:J11/hYXuz8f4ySSvYwY0FKfm+ezbsZBKZxNJlLklBHA= +github.com/benbjohnson/clock v1.3.5 h1:VvXlSJBzZpA/zum6Sj74hxwYI2DIxRWuNIoXAzHZz5o= +github.com/benbjohnson/clock v1.3.5/go.mod h1:J11/hYXuz8f4ySSvYwY0FKfm+ezbsZBKZxNJlLklBHA= github.com/beorn7/perks v0.0.0-20180321164747-3a771d992973/go.mod h1:Dwedo/Wpr24TaqPxmxbtue+5NUziq4I4S80YR8gNf3Q= github.com/beorn7/perks v1.0.0/go.mod h1:KWe93zE9D1o94FZ5RNwFwVgaQK1VOXiVxmqh+CedLV8= github.com/beorn7/perks v1.0.1 h1:VlbKKnNfV8bJzeqoa4cOKqO6bYr3WgKZxO8Z16+hsOM= @@ -762,6 +764,8 @@ go.uber.org/multierr v1.1.0/go.mod h1:wR5kodmAFQ0UK8QlbwjlSNy0Z68gJhDJUG5sjR94q/ go.uber.org/multierr v1.6.0/go.mod h1:cdWPpRnG4AhwMwsgIHip0KRBQjJy5kYEpYjJxpXp9iU= go.uber.org/multierr v1.11.0 h1:blXXJkSxSSfBVBlC76pxqeO+LN3aDfLQo+309xJstO0= go.uber.org/multierr v1.11.0/go.mod h1:20+QtiLqy0Nd6FdQB9TLXag12DsQkrbs3htMFfDN80Y= +go.uber.org/ratelimit v0.3.1 h1:K4qVE+byfv/B3tC+4nYWP7v/6SimcO7HzHekoMNBma0= +go.uber.org/ratelimit v0.3.1/go.mod h1:6euWsTB6U/Nb3X++xEUXA8ciPJvr19Q/0h1+oDcJhRk= go.uber.org/zap v1.10.0/go.mod h1:vwi/ZaCAaUcBkycHslxD9B2zi4UTXhF60s6SWpuDF0Q= go.uber.org/zap v1.19.1/go.mod h1:j3DNczoxDZroyBnOT1L/Q79cfUMGZxlv/9dzN7SM1rI= go.uber.org/zap v1.26.0 h1:sI7k6L95XOKS281NhVKOFCUNIvv9e0w4BF8N3u+tCRo= diff --git a/services/ingestion/engine.go b/services/ingestion/engine.go index 479970395..cb4c1494e 100644 --- a/services/ingestion/engine.go +++ b/services/ingestion/engine.go @@ -163,9 +163,17 @@ func (e *Engine) processEvents(events *models.CadenceEvents) error { Int("cadence-event-length", events.Length()). Msg("received new cadence evm events") + batch := e.store.NewBatch() + defer func(batch *pebbleDB.Batch) { + err := batch.Close() + if err != nil { + e.log.Fatal().Err(err).Msg("failed to close batch") + } + }(batch) + // if heartbeat interval with no data still update the cadence height if events.Empty() { - if err := e.blocks.SetLatestCadenceHeight(events.CadenceHeight(), nil); err != nil { + if err := e.blocks.SetLatestCadenceHeight(events.CadenceHeight(), batch); err != nil { return fmt.Errorf( "failed to update to latest cadence height: %d, during events ingestion: %w", events.CadenceHeight(), @@ -176,14 +184,6 @@ func (e *Engine) processEvents(events *models.CadenceEvents) error { return nil // nothing else to do this was heartbeat event with not event payloads } - batch := e.store.NewBatch() - defer func(batch *pebbleDB.Batch) { - err := batch.Close() - if err != nil { - e.log.Fatal().Err(err).Msg("failed to close batch") - } - }(batch) - // Step 1: Re-execute all transactions on the latest EVM block // Step 1.1: Notify the `BlocksProvider` of the newly received EVM block diff --git a/services/ingestion/event_subscriber.go b/services/ingestion/event_subscriber.go index a04d2e001..ad919af8a 100644 --- a/services/ingestion/event_subscriber.go +++ b/services/ingestion/event_subscriber.go @@ -5,7 +5,6 @@ import ( "errors" "fmt" "sort" - "time" "github.com/onflow/cadence/common" "github.com/onflow/flow-go/fvm/evm/events" @@ -198,8 +197,7 @@ func (r *RPCEventSubscriber) subscribe(ctx context.Context, height uint64) <-cha // and check for each event it receives whether we reached the end, if we reach the end it will increase // the height by one (next height), and check if we are still in previous sporks, if so repeat everything, // otherwise return. -func (r *RPCEventSubscriber) backfill(ctx context.Context, height uint64) <-chan models.BlockEvents { - // TODO(JanezP): if we are backfilling, its more efficient to request events in a batch +func (r *RPCEventSubscriber) backfill(ctx context.Context, currentHeight uint64) <-chan models.BlockEvents { eventsChan := make(chan models.BlockEvents) go func() { @@ -208,106 +206,108 @@ func (r *RPCEventSubscriber) backfill(ctx context.Context, height uint64) <-chan }() for { - // check if the current height is still in past sporks, and if not return since we are done with backfilling - if !r.client.IsPastSpork(height) { + // check if the current currentHeight is still in past sporks, and if not return since we are done with backfilling + if !r.client.IsPastSpork(currentHeight) { r.logger.Info(). - Uint64("height", height). + Uint64("height", currentHeight). Msg("completed backfilling") return } - latestHeight, err := r.client.GetLatestHeightForSpork(ctx, height) + currentHeight, err := r.backfillSpork(ctx, currentHeight, eventsChan) if err != nil { + r.logger.Error().Err(err).Msg("error backfilling spork") eventsChan <- models.NewBlockEventsError(err) return } r.logger.Info(). - Uint64("start-height", height). - Uint64("last-spork-height", latestHeight). - Msg("backfilling spork") + Uint64("next-height", currentHeight). + Msg("reached the end of spork, checking next spork") + } + }() - ticker := time.NewTicker(time.Millisecond * 10) + return eventsChan +} - maxRange := uint64(249) - for height < latestHeight { +// maxRangeForGetEvents is the maximum range of blocks that can be fetched using the GetEventsForHeightRange method. +const maxRangeForGetEvents = uint64(249) - // TODO: do rate limiting better - <-ticker.C +func (r *RPCEventSubscriber) backfillSpork(ctx context.Context, fromHeight uint64, eventsChan chan<- models.BlockEvents) (uint64, error) { + evmAddress := common.Address(systemcontracts.SystemContractsForChain(r.chain).EVMContract.Address) - startHeight := height - r.logger.Debug().Msg(fmt.Sprintf("backfilling [%d / %d]...", startHeight, latestHeight)) - endHeight := height + maxRange - if endHeight > latestHeight { - endHeight = latestHeight - } + lastHeight, err := r.client.GetLatestHeightForSpork(ctx, fromHeight) + if err != nil { + eventsChan <- models.NewBlockEventsError(err) + return 0, err + } - evmAddress := common.Address(systemcontracts.SystemContractsForChain(r.chain).EVMContract.Address) - blockExecutedEvent := common.NewAddressLocation( - nil, - evmAddress, - string(events.EventTypeBlockExecuted), - ).ID() - - transactionExecutedEvent := common.NewAddressLocation( - nil, - evmAddress, - string(events.EventTypeTransactionExecuted), - ).ID() - - // - blocks, err := r.client.GetEventsForHeightRange(ctx, blockExecutedEvent, startHeight, endHeight) - if err != nil { - r.logger.Error().Err(err).Msg("failed to get block events") - eventsChan <- models.NewBlockEventsError(err) - return - } - sort.Slice(blocks, func(i, j int) bool { - return blocks[i].Height < blocks[j].Height - }) + r.logger.Info(). + Uint64("start-height", fromHeight). + Uint64("last-spork-height", lastHeight). + Msg("backfilling spork") - transactions, err := r.client.GetEventsForHeightRange(ctx, transactionExecutedEvent, startHeight, endHeight) - if err != nil { - r.logger.Error().Err(err).Msg("failed to get block events") - eventsChan <- models.NewBlockEventsError(err) - return - } + for fromHeight < lastHeight { + r.logger.Debug().Msg(fmt.Sprintf("backfilling [%d / %d] ...", fromHeight, lastHeight)) - sort.Slice(transactions, func(i, j int) bool { - return transactions[i].Height < transactions[j].Height - }) + startHeight := fromHeight + endHeight := fromHeight + maxRangeForGetEvents + if endHeight > lastHeight { + endHeight = lastHeight + } - if len(transactions) != len(blocks) { - r.logger.Error().Msg("transactions and blocks have different length") - eventsChan <- models.NewBlockEventsError(err) - return - } + blockExecutedEvent := common.NewAddressLocation( + nil, + evmAddress, + string(events.EventTypeBlockExecuted), + ).ID() - for i := range transactions { - if transactions[i].Height != blocks[i].Height { - r.logger.Error().Msg("transactions and blocks have different height") - eventsChan <- models.NewBlockEventsError(err) - return - } - // append the transaction events to the block events - blocks[i].Events = append(blocks[i].Events, transactions[i].Events...) + transactionExecutedEvent := common.NewAddressLocation( + nil, + evmAddress, + string(events.EventTypeTransactionExecuted), + ).ID() - evmEvents := models.NewBlockEvents(blocks[i]) - height = evmEvents.Events.CadenceHeight() + 1 + blocks, err := r.client.GetEventsForHeightRange(ctx, blockExecutedEvent, startHeight, endHeight) + if err != nil { + return 0, fmt.Errorf("failed to get block events: %w", err) + } - } + transactions, err := r.client.GetEventsForHeightRange(ctx, transactionExecutedEvent, startHeight, endHeight) + if err != nil { + return 0, fmt.Errorf("failed to get block events: %w", err) + } + // sort both, just in case + sort.Slice(blocks, func(i, j int) bool { + return blocks[i].Height < blocks[j].Height + }) + sort.Slice(transactions, func(i, j int) bool { + return transactions[i].Height < transactions[j].Height + }) + + if len(transactions) != len(blocks) { + return 0, fmt.Errorf("transactions and blocks have different length") + } + + for i := range transactions { + if transactions[i].Height != blocks[i].Height { + return 0, fmt.Errorf("transactions and blocks have different height") } - ticker.Stop() - r.logger.Info(). - Uint64("next-height", height). - Msg("reached the end of spork, checking next spork") + // append the transaction events to the block events + blocks[i].Events = append(blocks[i].Events, transactions[i].Events...) + + evmEvents := models.NewBlockEvents(blocks[i]) + eventsChan <- evmEvents + + // advance the height + fromHeight = evmEvents.Events.CadenceHeight() + 1 } - }() - return eventsChan + } + return fromHeight, nil } // fetchMissingData is used as a backup mechanism for fetching EVM-related diff --git a/services/requester/cross-spork_client.go b/services/requester/cross-spork_client.go index 91cc76341..4025aa3c0 100644 --- a/services/requester/cross-spork_client.go +++ b/services/requester/cross-spork_client.go @@ -5,19 +5,20 @@ import ( "fmt" "github.com/onflow/cadence" + errs "github.com/onflow/flow-evm-gateway/models/errors" "github.com/onflow/flow-go-sdk" "github.com/onflow/flow-go-sdk/access" flowGo "github.com/onflow/flow-go/model/flow" "github.com/rs/zerolog" + "go.uber.org/ratelimit" "golang.org/x/exp/slices" - - errs "github.com/onflow/flow-evm-gateway/models/errors" ) type sporkClient struct { - firstHeight uint64 - lastHeight uint64 - client access.Client + firstHeight uint64 + lastHeight uint64 + client access.Client + getEventsForHeightRangeLimiter ratelimit.Limiter } // contains checks if the provided height is withing the range of available heights @@ -25,6 +26,14 @@ func (s *sporkClient) contains(height uint64) bool { return height >= s.firstHeight && height <= s.lastHeight } +func (s *sporkClient) GetEventsForHeightRange( + ctx context.Context, eventType string, startHeight uint64, endHeight uint64, +) ([]flow.BlockEvents, error) { + s.getEventsForHeightRangeLimiter.Take() + + return s.client.GetEventsForHeightRange(ctx, eventType, startHeight, endHeight) +} + type sporkClients []*sporkClient // addSpork will add a new spork host defined by the first and last height boundary in that spork. @@ -48,6 +57,8 @@ func (s *sporkClients) add(logger zerolog.Logger, client access.Client) error { firstHeight: info.NodeRootBlockHeight, lastHeight: header.Height, client: client, + // TODO (JanezP): Make this configurable + getEventsForHeightRangeLimiter: ratelimit.New(100, ratelimit.WithoutSlack), }) // make sure clients are always sorted @@ -218,10 +229,17 @@ func (c *CrossSporkClient) SubscribeEventsByBlockHeight( func (c *CrossSporkClient) GetEventsForHeightRange( ctx context.Context, eventType string, startHeight uint64, endHeight uint64, ) ([]flow.BlockEvents, error) { - // TODO: also make sure the endHeight is not too high client, err := c.getClientForHeight(startHeight) if err != nil { return nil, err } + endClient, err := c.getClientForHeight(endHeight) + if err != nil { + return nil, err + } + // there is one client reference per spork, so we can compare the clients + if endClient != client { + return nil, fmt.Errorf("invalid height range, end height %d is not in the same spork as start height %d", endHeight, startHeight) + } return client.GetEventsForHeightRange(ctx, eventType, startHeight, endHeight) } diff --git a/services/testutils/mock_client.go b/services/testutils/mock_client.go index 3e4c7fafe..5d8332525 100644 --- a/services/testutils/mock_client.go +++ b/services/testutils/mock_client.go @@ -15,6 +15,9 @@ type MockClient struct { GetBlockHeaderByHeightFunc func(context.Context, uint64) (*flow.BlockHeader, error) SubscribeEventsByBlockHeightFunc func(context.Context, uint64, flow.EventFilter, ...access.SubscribeOption) (<-chan flow.BlockEvents, <-chan error, error) GetNodeVersionInfoFunc func(ctx context.Context) (*flow.NodeVersionInfo, error) + GetEventsForHeightRangeFunc func( + ctx context.Context, eventType string, startHeight uint64, endHeight uint64, + ) ([]flow.BlockEvents, error) } func (c *MockClient) GetBlockHeaderByHeight(ctx context.Context, height uint64) (*flow.BlockHeader, error) { @@ -38,6 +41,12 @@ func (c *MockClient) SubscribeEventsByBlockHeight( return c.SubscribeEventsByBlockHeightFunc(ctx, startHeight, filter, opts...) } +func (c *MockClient) GetEventsForHeightRange( + ctx context.Context, eventType string, startHeight uint64, endHeight uint64, +) ([]flow.BlockEvents, error) { + return c.GetEventsForHeightRangeFunc(ctx, eventType, startHeight, endHeight) +} + func SetupClientForRange(startHeight uint64, endHeight uint64) *MockClient { client, events := SetupClient(startHeight, endHeight) go func() { @@ -85,5 +94,10 @@ func SetupClient(startHeight uint64, endHeight uint64) (*MockClient, chan flow.B ) (<-chan flow.BlockEvents, <-chan error, error) { return events, make(chan error), nil }, + GetEventsForHeightRangeFunc: func( + ctx context.Context, eventType string, startHeight uint64, endHeight uint64, + ) ([]flow.BlockEvents, error) { + return []flow.BlockEvents{}, nil + }, }, events } From 019fdd2465570a3c920da1a8e6e175281262ab73 Mon Sep 17 00:00:00 2001 From: Janez Podhostnik Date: Wed, 13 Nov 2024 18:46:03 +0100 Subject: [PATCH 066/174] fix test and bug --- services/ingestion/event_subscriber.go | 3 ++- services/testutils/mock_client.go | 18 ++++++++++++++++-- 2 files changed, 18 insertions(+), 3 deletions(-) diff --git a/services/ingestion/event_subscriber.go b/services/ingestion/event_subscriber.go index ad919af8a..489de7f41 100644 --- a/services/ingestion/event_subscriber.go +++ b/services/ingestion/event_subscriber.go @@ -215,7 +215,8 @@ func (r *RPCEventSubscriber) backfill(ctx context.Context, currentHeight uint64) return } - currentHeight, err := r.backfillSpork(ctx, currentHeight, eventsChan) + var err error + currentHeight, err = r.backfillSpork(ctx, currentHeight, eventsChan) if err != nil { r.logger.Error().Err(err).Msg("error backfilling spork") eventsChan <- models.NewBlockEventsError(err) diff --git a/services/testutils/mock_client.go b/services/testutils/mock_client.go index 5d8332525..c0a0c01de 100644 --- a/services/testutils/mock_client.go +++ b/services/testutils/mock_client.go @@ -95,9 +95,23 @@ func SetupClient(startHeight uint64, endHeight uint64) (*MockClient, chan flow.B return events, make(chan error), nil }, GetEventsForHeightRangeFunc: func( - ctx context.Context, eventType string, startHeight uint64, endHeight uint64, + ctx context.Context, eventType string, sh uint64, eh uint64, ) ([]flow.BlockEvents, error) { - return []flow.BlockEvents{}, nil + if sh < startHeight || sh > endHeight { + return nil, storage.ErrNotFound + } + if eh < startHeight || eh > endHeight { + return nil, storage.ErrNotFound + } + + evts := make([]flow.BlockEvents, 0, eh-sh+1) + for i := uint64(0); i <= eh-sh; i++ { + evts = append(evts, flow.BlockEvents{ + Height: sh + i, + }) + } + + return evts, nil }, }, events } From 2e41206c48ccb3e0729cd26c4291e1c52547fb71 Mon Sep 17 00:00:00 2001 From: Janez Podhostnik Date: Wed, 13 Nov 2024 20:34:47 +0100 Subject: [PATCH 067/174] temp skip test --- services/ingestion/event_subscriber_test.go | 3 +++ 1 file changed, 3 insertions(+) diff --git a/services/ingestion/event_subscriber_test.go b/services/ingestion/event_subscriber_test.go index 04626af21..dfe7458f2 100644 --- a/services/ingestion/event_subscriber_test.go +++ b/services/ingestion/event_subscriber_test.go @@ -159,6 +159,7 @@ func Test_MissingBlockEvent(t *testing.T) { // This scenario tests the happy path, when the back-up fetching of // EVM events through the gRPC API, returns the correct data. func Test_SubscribingWithRetryOnError(t *testing.T) { + t.Skip("TODO fix this test") endHeight := uint64(10) var sporkClients []access.Client currentClient := testutils.SetupClientForRange(1, endHeight) @@ -222,6 +223,7 @@ func Test_SubscribingWithRetryOnError(t *testing.T) { // This scenario tests the unhappy path, when the back-up fetching // of EVM events through the gRPC API, returns duplicate EVM blocks. func Test_SubscribingWithRetryOnErrorMultipleBlocks(t *testing.T) { + t.Skip("TODO fix this test") endHeight := uint64(10) var sporkClients []access.Client currentClient := testutils.SetupClientForRange(1, endHeight) @@ -285,6 +287,7 @@ func Test_SubscribingWithRetryOnErrorMultipleBlocks(t *testing.T) { // This scenario tests the unhappy path, when the back-up fetching // of EVM events through the gRPC API, returns no EVM blocks. func Test_SubscribingWithRetryOnErrorEmptyBlocks(t *testing.T) { + t.Skip("TODO fix this test") endHeight := uint64(10) var sporkClients []access.Client currentClient := testutils.SetupClientForRange(1, endHeight) From 3bfbe7b225fcf1aa1db01f6d885bbcf45d4bc3c1 Mon Sep 17 00:00:00 2001 From: Janez Podhostnik Date: Wed, 13 Nov 2024 21:58:46 +0100 Subject: [PATCH 068/174] address review comments --- services/ingestion/event_subscriber.go | 47 +++++++++++++------------- tests/go.mod | 2 ++ tests/go.sum | 2 ++ 3 files changed, 27 insertions(+), 24 deletions(-) diff --git a/services/ingestion/event_subscriber.go b/services/ingestion/event_subscriber.go index 489de7f41..3138269c1 100644 --- a/services/ingestion/event_subscriber.go +++ b/services/ingestion/event_subscriber.go @@ -192,12 +192,9 @@ func (r *RPCEventSubscriber) subscribe(ctx context.Context, height uint64) <-cha return eventsChan } -// backfill will use the provided height and with the client for the provided spork will start backfilling -// events. Before subscribing, it will check what is the latest block in the current spork (defined by height) -// and check for each event it receives whether we reached the end, if we reach the end it will increase -// the height by one (next height), and check if we are still in previous sporks, if so repeat everything, -// otherwise return. -func (r *RPCEventSubscriber) backfill(ctx context.Context, currentHeight uint64) <-chan models.BlockEvents { +// backfill returns a channel that is filled with block events from the provided fromCadenceHeight up to the first +// height in the current spork. +func (r *RPCEventSubscriber) backfill(ctx context.Context, fromCadenceHeight uint64) <-chan models.BlockEvents { eventsChan := make(chan models.BlockEvents) go func() { @@ -206,17 +203,17 @@ func (r *RPCEventSubscriber) backfill(ctx context.Context, currentHeight uint64) }() for { - // check if the current currentHeight is still in past sporks, and if not return since we are done with backfilling - if !r.client.IsPastSpork(currentHeight) { + // check if the current fromCadenceHeight is still in past sporks, and if not return since we are done with backfilling + if !r.client.IsPastSpork(fromCadenceHeight) { r.logger.Info(). - Uint64("height", currentHeight). + Uint64("height", fromCadenceHeight). Msg("completed backfilling") return } var err error - currentHeight, err = r.backfillSpork(ctx, currentHeight, eventsChan) + fromCadenceHeight, err = r.backfillSporkFromHeight(ctx, fromCadenceHeight, eventsChan) if err != nil { r.logger.Error().Err(err).Msg("error backfilling spork") eventsChan <- models.NewBlockEventsError(err) @@ -224,7 +221,7 @@ func (r *RPCEventSubscriber) backfill(ctx context.Context, currentHeight uint64) } r.logger.Info(). - Uint64("next-height", currentHeight). + Uint64("next-cadence-height", fromCadenceHeight). Msg("reached the end of spork, checking next spork") } }() @@ -235,25 +232,27 @@ func (r *RPCEventSubscriber) backfill(ctx context.Context, currentHeight uint64) // maxRangeForGetEvents is the maximum range of blocks that can be fetched using the GetEventsForHeightRange method. const maxRangeForGetEvents = uint64(249) -func (r *RPCEventSubscriber) backfillSpork(ctx context.Context, fromHeight uint64, eventsChan chan<- models.BlockEvents) (uint64, error) { +// / backfillSporkFromHeight will fill the eventsChan with block events from the provided fromHeight up to the first height in the spork that comes +// after the spork of the provided fromHeight. +func (r *RPCEventSubscriber) backfillSporkFromHeight(ctx context.Context, fromCadenceHeight uint64, eventsChan chan<- models.BlockEvents) (uint64, error) { evmAddress := common.Address(systemcontracts.SystemContractsForChain(r.chain).EVMContract.Address) - lastHeight, err := r.client.GetLatestHeightForSpork(ctx, fromHeight) + lastHeight, err := r.client.GetLatestHeightForSpork(ctx, fromCadenceHeight) if err != nil { eventsChan <- models.NewBlockEventsError(err) return 0, err } r.logger.Info(). - Uint64("start-height", fromHeight). + Uint64("start-height", fromCadenceHeight). Uint64("last-spork-height", lastHeight). Msg("backfilling spork") - for fromHeight < lastHeight { - r.logger.Debug().Msg(fmt.Sprintf("backfilling [%d / %d] ...", fromHeight, lastHeight)) + for fromCadenceHeight < lastHeight { + r.logger.Debug().Msg(fmt.Sprintf("backfilling [%d / %d] ...", fromCadenceHeight, lastHeight)) - startHeight := fromHeight - endHeight := fromHeight + maxRangeForGetEvents + startHeight := fromCadenceHeight + endHeight := fromCadenceHeight + maxRangeForGetEvents if endHeight > lastHeight { endHeight = lastHeight } @@ -280,6 +279,10 @@ func (r *RPCEventSubscriber) backfillSpork(ctx context.Context, fromHeight uint6 return 0, fmt.Errorf("failed to get block events: %w", err) } + if len(transactions) != len(blocks) { + return 0, fmt.Errorf("transactions and blocks have different length") + } + // sort both, just in case sort.Slice(blocks, func(i, j int) bool { return blocks[i].Height < blocks[j].Height @@ -288,10 +291,6 @@ func (r *RPCEventSubscriber) backfillSpork(ctx context.Context, fromHeight uint6 return transactions[i].Height < transactions[j].Height }) - if len(transactions) != len(blocks) { - return 0, fmt.Errorf("transactions and blocks have different length") - } - for i := range transactions { if transactions[i].Height != blocks[i].Height { return 0, fmt.Errorf("transactions and blocks have different height") @@ -304,11 +303,11 @@ func (r *RPCEventSubscriber) backfillSpork(ctx context.Context, fromHeight uint6 eventsChan <- evmEvents // advance the height - fromHeight = evmEvents.Events.CadenceHeight() + 1 + fromCadenceHeight = evmEvents.Events.CadenceHeight() + 1 } } - return fromHeight, nil + return fromCadenceHeight, nil } // fetchMissingData is used as a backup mechanism for fetching EVM-related diff --git a/tests/go.mod b/tests/go.mod index c61c7ed2d..c42584d06 100644 --- a/tests/go.mod +++ b/tests/go.mod @@ -28,6 +28,7 @@ require ( github.com/StackExchange/wmi v1.2.1 // indirect github.com/VictoriaMetrics/fastcache v1.12.2 // indirect github.com/allegro/bigcache v1.2.1 // indirect + github.com/benbjohnson/clock v1.3.5 // indirect github.com/beorn7/perks v1.0.1 // indirect github.com/bits-and-blooms/bitset v1.10.0 // indirect github.com/btcsuite/btcd v0.21.0-beta // indirect @@ -217,6 +218,7 @@ require ( go.opentelemetry.io/proto/otlp v1.0.0 // indirect go.uber.org/atomic v1.11.0 // indirect go.uber.org/multierr v1.11.0 // indirect + go.uber.org/ratelimit v0.3.1 // indirect go.uber.org/zap v1.26.0 // indirect golang.org/x/crypto v0.26.0 // indirect golang.org/x/exp v0.0.0-20240119083558-1b970713d09a // indirect diff --git a/tests/go.sum b/tests/go.sum index 1311f1f08..997f8b268 100644 --- a/tests/go.sum +++ b/tests/go.sum @@ -1124,6 +1124,8 @@ go.uber.org/multierr v1.3.0/go.mod h1:VgVr7evmIr6uPjLBxg28wmKNXyqE9akIJ5XnfpiKl+ go.uber.org/multierr v1.6.0/go.mod h1:cdWPpRnG4AhwMwsgIHip0KRBQjJy5kYEpYjJxpXp9iU= go.uber.org/multierr v1.11.0 h1:blXXJkSxSSfBVBlC76pxqeO+LN3aDfLQo+309xJstO0= go.uber.org/multierr v1.11.0/go.mod h1:20+QtiLqy0Nd6FdQB9TLXag12DsQkrbs3htMFfDN80Y= +go.uber.org/ratelimit v0.3.1 h1:K4qVE+byfv/B3tC+4nYWP7v/6SimcO7HzHekoMNBma0= +go.uber.org/ratelimit v0.3.1/go.mod h1:6euWsTB6U/Nb3X++xEUXA8ciPJvr19Q/0h1+oDcJhRk= go.uber.org/tools v0.0.0-20190618225709-2cfd321de3ee/go.mod h1:vJERXedbb3MVM5f9Ejo0C68/HhF8uaILCdgjnY+goOA= go.uber.org/zap v1.10.0/go.mod h1:vwi/ZaCAaUcBkycHslxD9B2zi4UTXhF60s6SWpuDF0Q= go.uber.org/zap v1.13.0/go.mod h1:zwrFLgMcdUuIBviXEYEH1YKNaOBnKXsx2IPda5bBwHM= From 99c8faa8b378832cf106ffbbf315e33ae3f8cf3b Mon Sep 17 00:00:00 2001 From: Janez Podhostnik Date: Thu, 14 Nov 2024 12:46:42 +0100 Subject: [PATCH 069/174] unskip tests --- services/ingestion/event_subscriber_test.go | 9 ++++----- services/testutils/mock_client.go | 5 ++++- 2 files changed, 8 insertions(+), 6 deletions(-) diff --git a/services/ingestion/event_subscriber_test.go b/services/ingestion/event_subscriber_test.go index dfe7458f2..7505533a8 100644 --- a/services/ingestion/event_subscriber_test.go +++ b/services/ingestion/event_subscriber_test.go @@ -159,7 +159,6 @@ func Test_MissingBlockEvent(t *testing.T) { // This scenario tests the happy path, when the back-up fetching of // EVM events through the gRPC API, returns the correct data. func Test_SubscribingWithRetryOnError(t *testing.T) { - t.Skip("TODO fix this test") endHeight := uint64(10) var sporkClients []access.Client currentClient := testutils.SetupClientForRange(1, endHeight) @@ -206,10 +205,10 @@ func Test_SubscribingWithRetryOnError(t *testing.T) { prevHeight = eventHeight if eventHeight == cadenceHeight { - assert.Equal(t, evmBlock, ev.Events.Block()) + require.Equal(t, evmBlock, ev.Events.Block()) for i := 0; i < len(txHashes); i++ { tx := ev.Events.Transactions()[i] - assert.Equal(t, txHashes[i], tx.Hash()) + require.Equal(t, txHashes[i], tx.Hash()) } } } @@ -223,7 +222,6 @@ func Test_SubscribingWithRetryOnError(t *testing.T) { // This scenario tests the unhappy path, when the back-up fetching // of EVM events through the gRPC API, returns duplicate EVM blocks. func Test_SubscribingWithRetryOnErrorMultipleBlocks(t *testing.T) { - t.Skip("TODO fix this test") endHeight := uint64(10) var sporkClients []access.Client currentClient := testutils.SetupClientForRange(1, endHeight) @@ -287,7 +285,6 @@ func Test_SubscribingWithRetryOnErrorMultipleBlocks(t *testing.T) { // This scenario tests the unhappy path, when the back-up fetching // of EVM events through the gRPC API, returns no EVM blocks. func Test_SubscribingWithRetryOnErrorEmptyBlocks(t *testing.T) { - t.Skip("TODO fix this test") endHeight := uint64(10) var sporkClients []access.Client currentClient := testutils.SetupClientForRange(1, endHeight) @@ -420,6 +417,8 @@ func setupClientForBackupEventFetching( cadenceHeight, ).Return([]flow.BlockEvents{evmTxEvents}, nil).Once() + client.GetEventsForHeightRangeFunc = nil + client.SubscribeEventsByBlockHeightFunc = func( ctx context.Context, startHeight uint64, diff --git a/services/testutils/mock_client.go b/services/testutils/mock_client.go index c0a0c01de..2f65021a9 100644 --- a/services/testutils/mock_client.go +++ b/services/testutils/mock_client.go @@ -44,7 +44,10 @@ func (c *MockClient) SubscribeEventsByBlockHeight( func (c *MockClient) GetEventsForHeightRange( ctx context.Context, eventType string, startHeight uint64, endHeight uint64, ) ([]flow.BlockEvents, error) { - return c.GetEventsForHeightRangeFunc(ctx, eventType, startHeight, endHeight) + if c.GetEventsForHeightRangeFunc != nil { + return c.GetEventsForHeightRangeFunc(ctx, eventType, startHeight, endHeight) + } + return c.Client.GetEventsForHeightRange(ctx, eventType, startHeight, endHeight) } func SetupClientForRange(startHeight uint64, endHeight uint64) *MockClient { From a9fe319851d33db4c31321fdd45c752e7edc318a Mon Sep 17 00:00:00 2001 From: Ardit Marku Date: Tue, 19 Nov 2024 20:24:16 +0200 Subject: [PATCH 070/174] Fix wrong block hash value for the 1st EVM block with PrevRandao --- storage/pebble/blocks.go | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/storage/pebble/blocks.go b/storage/pebble/blocks.go index 42099b1d4..42846f02d 100644 --- a/storage/pebble/blocks.go +++ b/storage/pebble/blocks.go @@ -253,6 +253,15 @@ func (b *Blocks) getBlock(keyCode byte, key []byte) (*models.Block, error) { } if b.chainID == flowGo.Testnet && slices.Contains(testnetBrokenParentHashBlockHeights, block.Height) { + // Since we are going to modify the `block.ParentBlockHash` field, + // we need to set the `block.FixedHash` field. If we don't do so, + // `block.Hash()` will return a different hash. + blockHash, err := block.Hash() + if err != nil { + return nil, err + } + block.FixedHash = blockHash + parentBlock, err := b.getBlock(blockHeightKey, uint64Bytes(block.Height-1)) if err != nil { return nil, err From 1949a0a72a315e02297338538e65fb9783095e5d Mon Sep 17 00:00:00 2001 From: Janez Podhostnik Date: Thu, 14 Nov 2024 13:10:42 +0100 Subject: [PATCH 071/174] Refactor re-execution --- services/ingestion/engine.go | 74 +++++++++++++++++++++---------- services/ingestion/engine_test.go | 2 +- storage/index.go | 1 - 3 files changed, 52 insertions(+), 25 deletions(-) diff --git a/services/ingestion/engine.go b/services/ingestion/engine.go index cb4c1494e..2145e6497 100644 --- a/services/ingestion/engine.go +++ b/services/ingestion/engine.go @@ -145,6 +145,29 @@ func (e *Engine) Run(ctx context.Context) error { } } +// withBatch will execute the provided function with a new batch, and commit the batch +// afterwards if no error is returned. +func (e *Engine) withBatch(f func(batch *pebbleDB.Batch) error) error { + batch := e.store.NewBatch() + defer func(batch *pebbleDB.Batch) { + err := batch.Close() + if err != nil { + e.log.Fatal().Err(err).Msg("failed to close batch") + } + }(batch) + + err := f(batch) + if err != nil { + return err + } + + if err := batch.Commit(pebbleDB.Sync); err != nil { + return fmt.Errorf("failed to commit batch: %w", err) + } + + return nil +} + // processEvents converts the events to block and transactions and indexes them. // // BlockEvents are received by the access node API and contain Cadence height (always a single Flow block), @@ -163,14 +186,36 @@ func (e *Engine) processEvents(events *models.CadenceEvents) error { Int("cadence-event-length", events.Length()). Msg("received new cadence evm events") - batch := e.store.NewBatch() - defer func(batch *pebbleDB.Batch) { - err := batch.Close() - if err != nil { - e.log.Fatal().Err(err).Msg("failed to close batch") + err := e.withBatch( + func(batch *pebbleDB.Batch) error { + return e.indexEvents(events, batch) + }, + ) + if err != nil { + return fmt.Errorf("failed to index events for cadence block %d: %w", events.CadenceHeight(), err) + } + + e.collector.CadenceHeightIndexed(events.CadenceHeight()) + + if events.Empty() { + return nil // nothing else to do this was heartbeat event with not event payloads + } + + // emit block event and logs, only after we successfully commit the data + e.blocksPublisher.Publish(events.Block()) + for _, r := range events.Receipts() { + if len(r.Logs) > 0 { + e.logsPublisher.Publish(r.Logs) } - }(batch) + } + + e.collector.EVMTransactionIndexed(len(events.Transactions())) + e.collector.EVMHeightIndexed(events.Block().Height) + return nil +} +// indexEvents will replay the evm transactions using the block events and index all results. +func (e *Engine) indexEvents(events *models.CadenceEvents, batch *pebbleDB.Batch) error { // if heartbeat interval with no data still update the cadence height if events.Empty() { if err := e.blocks.SetLatestCadenceHeight(events.CadenceHeight(), batch); err != nil { @@ -180,7 +225,6 @@ func (e *Engine) processEvents(events *models.CadenceEvents) error { err, ) } - e.collector.CadenceHeightIndexed(events.CadenceHeight()) return nil // nothing else to do this was heartbeat event with not event payloads } @@ -262,22 +306,6 @@ func (e *Engine) processEvents(events *models.CadenceEvents) error { } } - if err := batch.Commit(pebbleDB.Sync); err != nil { - return fmt.Errorf("failed to commit indexed data for Cadence block %d: %w", events.CadenceHeight(), err) - } - - // emit block event and logs, only after we successfully commit the data - e.blocksPublisher.Publish(events.Block()) - - for _, r := range events.Receipts() { - if len(r.Logs) > 0 { - e.logsPublisher.Publish(r.Logs) - } - } - - e.collector.EVMTransactionIndexed(len(events.Transactions())) - e.collector.EVMHeightIndexed(events.Block().Height) - e.collector.CadenceHeightIndexed(events.CadenceHeight()) return nil } diff --git a/services/ingestion/engine_test.go b/services/ingestion/engine_test.go index 81ab88d68..338491aad 100644 --- a/services/ingestion/engine_test.go +++ b/services/ingestion/engine_test.go @@ -164,7 +164,7 @@ func TestSerialBlockIngestion(t *testing.T) { go func() { err := engine.Run(context.Background()) assert.ErrorIs(t, err, models.ErrInvalidHeight) - assert.EqualError(t, err, "invalid height: received new block: 20, non-sequential of latest block: 11") + assert.ErrorContains(t, err, "invalid height: received new block: 20, non-sequential of latest block: 11") close(waitErr) }() diff --git a/storage/index.go b/storage/index.go index b83e80b82..9cd5c4a5b 100644 --- a/storage/index.go +++ b/storage/index.go @@ -42,7 +42,6 @@ type BlockIndexer interface { LatestCadenceHeight() (uint64, error) // SetLatestCadenceHeight sets the latest Cadence height. - // Batch is required to batch multiple indexer operations, skipped if nil. SetLatestCadenceHeight(cadenceHeight uint64, batch *pebble.Batch) error // GetCadenceHeight returns the Cadence height that matches the From 7627b4e67f658938a01edfcb5dbd6b2f2b49422a Mon Sep 17 00:00:00 2001 From: Janez Podhostnik Date: Wed, 20 Nov 2024 16:34:15 +0100 Subject: [PATCH 072/174] Remove transactions mutex --- storage/pebble/transactions.go | 10 ---------- 1 file changed, 10 deletions(-) diff --git a/storage/pebble/transactions.go b/storage/pebble/transactions.go index 09ee16930..446f6de6b 100644 --- a/storage/pebble/transactions.go +++ b/storage/pebble/transactions.go @@ -1,8 +1,6 @@ package pebble import ( - "sync" - "github.com/cockroachdb/pebble" "github.com/onflow/go-ethereum/common" @@ -14,20 +12,15 @@ var _ storage.TransactionIndexer = &Transactions{} type Transactions struct { store *Storage - mux sync.RWMutex } func NewTransactions(store *Storage) *Transactions { return &Transactions{ store: store, - mux: sync.RWMutex{}, } } func (t *Transactions) Store(tx models.Transaction, batch *pebble.Batch) error { - t.mux.Lock() - defer t.mux.Unlock() - val, err := tx.MarshalBinary() if err != nil { return err @@ -39,9 +32,6 @@ func (t *Transactions) Store(tx models.Transaction, batch *pebble.Batch) error { } func (t *Transactions) Get(ID common.Hash) (models.Transaction, error) { - t.mux.RLock() - defer t.mux.RUnlock() - val, err := t.store.get(txIDKey, ID.Bytes()) if err != nil { return nil, err From 646c16b21ea15ffddc418f1cf17ef33f78973ef1 Mon Sep 17 00:00:00 2001 From: Janez Podhostnik Date: Wed, 20 Nov 2024 17:31:33 +0100 Subject: [PATCH 073/174] Add debug_flowHeight API endpoint --- api/debug.go | 18 ++++++++++++++++++ tests/e2e_web3js_test.go | 4 ++++ tests/web3js/debug_util_test.js | 14 ++++++++++++++ 3 files changed, 36 insertions(+) create mode 100644 tests/web3js/debug_util_test.js diff --git a/api/debug.go b/api/debug.go index 4c02cd0f4..188eeaa93 100644 --- a/api/debug.go +++ b/api/debug.go @@ -361,6 +361,24 @@ func (d *DebugAPI) TraceCall( return tracer.GetResult() } +// FlowHeight returns the Flow height for the given EVM block. +func (d *DebugAPI) FlowHeight( + _ context.Context, + blockNrOrHash rpc.BlockNumberOrHash, +) (uint64, error) { + height, err := resolveBlockTag(&blockNrOrHash, d.blocks, d.logger) + if err != nil { + return 0, err + } + + cdcHeight, err := d.blocks.GetCadenceHeight(height) + if err != nil { + return 0, err + } + + return cdcHeight, nil +} + func (d *DebugAPI) executorAtBlock(block *models.Block) (*evm.BlockExecutor, error) { snapshot, err := d.registerStore.GetSnapshotAt(block.Height) if err != nil { diff --git a/tests/e2e_web3js_test.go b/tests/e2e_web3js_test.go index 03c8b3860..5fb1b8b25 100644 --- a/tests/e2e_web3js_test.go +++ b/tests/e2e_web3js_test.go @@ -36,6 +36,10 @@ func TestWeb3_E2E(t *testing.T) { runWeb3Test(t, "debug_traces_test") }) + t.Run("test transaction traces", func(t *testing.T) { + runWeb3Test(t, "debug_util_test") + }) + t.Run("test setup sanity check", func(t *testing.T) { runWeb3Test(t, "setup_test") }) diff --git a/tests/web3js/debug_util_test.js b/tests/web3js/debug_util_test.js new file mode 100644 index 000000000..e4e764a2f --- /dev/null +++ b/tests/web3js/debug_util_test.js @@ -0,0 +1,14 @@ +const { assert } = require('chai') +const helpers = require('./helpers') + +it('should retrieve flow height', async () => { + response = await helpers.callRPCMethod( + 'debug_flowHeight', + ['latest'] + ) + assert.equal(response.status, 200) + assert.isDefined(response.body) + + let height = response.body.result + assert.isTrue(height > 0) +}) From 575017bddd7bc2f63891800bb83eb880671995df Mon Sep 17 00:00:00 2001 From: Janez Podhostnik Date: Wed, 20 Nov 2024 17:53:58 +0100 Subject: [PATCH 074/174] update readme --- README.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/README.md b/README.md index 8cc4d35e3..7d40eafa9 100644 --- a/README.md +++ b/README.md @@ -267,6 +267,8 @@ The EVM Gateway implements APIs according to the Ethereum specification: https:/ * debug_traceBlockByNumber * debug_traceBlockByHash +- debug_flowBlock - returns the flow block height for the given EVM block (id or height) + **Unsupported APIs** - Wallet APIs: we don't officially support wallet APIs (eth_accounts, eth_sign, eth_signTransaction, eth_sendTransaction) due to security concerns that come with managing the keys on production environments, however, it is possible to configure the gateway to allow these From e61ac1c33c8c49a9a6c84d727107ebd64bfa8742 Mon Sep 17 00:00:00 2001 From: Janez Podhostnik Date: Wed, 20 Nov 2024 19:58:28 +0100 Subject: [PATCH 075/174] apply review comments --- api/debug.go | 3 ++- tests/e2e_web3js_test.go | 2 +- tests/web3js/debug_util_test.js | 5 +++-- 3 files changed, 6 insertions(+), 4 deletions(-) diff --git a/api/debug.go b/api/debug.go index 188eeaa93..d55f51326 100644 --- a/api/debug.go +++ b/api/debug.go @@ -361,7 +361,8 @@ func (d *DebugAPI) TraceCall( return tracer.GetResult() } -// FlowHeight returns the Flow height for the given EVM block. +// FlowHeight returns the Flow height for the given EVM block specified either by EVM +// block height or EVM block hash. func (d *DebugAPI) FlowHeight( _ context.Context, blockNrOrHash rpc.BlockNumberOrHash, diff --git a/tests/e2e_web3js_test.go b/tests/e2e_web3js_test.go index 5fb1b8b25..881c76abd 100644 --- a/tests/e2e_web3js_test.go +++ b/tests/e2e_web3js_test.go @@ -36,7 +36,7 @@ func TestWeb3_E2E(t *testing.T) { runWeb3Test(t, "debug_traces_test") }) - t.Run("test transaction traces", func(t *testing.T) { + t.Run("test debug utils", func(t *testing.T) { runWeb3Test(t, "debug_util_test") }) diff --git a/tests/web3js/debug_util_test.js b/tests/web3js/debug_util_test.js index e4e764a2f..371967312 100644 --- a/tests/web3js/debug_util_test.js +++ b/tests/web3js/debug_util_test.js @@ -1,8 +1,9 @@ const { assert } = require('chai') const helpers = require('./helpers') +const conf = require('./config') it('should retrieve flow height', async () => { - response = await helpers.callRPCMethod( + let response = await helpers.callRPCMethod( 'debug_flowHeight', ['latest'] ) @@ -10,5 +11,5 @@ it('should retrieve flow height', async () => { assert.isDefined(response.body) let height = response.body.result - assert.isTrue(height > 0) + assert.equal(height, conf.startBlockHeight) }) From ca18d3309783e470888647d7c4827d23db64677a Mon Sep 17 00:00:00 2001 From: Ardit Marku Date: Tue, 19 Nov 2024 12:45:42 +0200 Subject: [PATCH 076/174] Handle ingestion of missing EVM.BlockExecuted event in backfill process --- models/events.go | 45 +++++--- models/events_test.go | 5 +- services/ingestion/engine_test.go | 8 +- services/ingestion/event_subscriber.go | 139 ++++++++++++++++++++++--- 4 files changed, 162 insertions(+), 35 deletions(-) diff --git a/models/events.go b/models/events.go index b8ad5a539..2d199f9f1 100644 --- a/models/events.go +++ b/models/events.go @@ -46,15 +46,6 @@ type CadenceEvents struct { // NewCadenceEvents decodes the events into evm types. func NewCadenceEvents(events flow.BlockEvents) (*CadenceEvents, error) { - // first we sort all the events in the block, by their TransactionIndex, - // and then we also sort events in the same transaction, by their EventIndex. - sort.Slice(events.Events, func(i, j int) bool { - if events.Events[i].TransactionIndex != events.Events[j].TransactionIndex { - return events.Events[i].TransactionIndex < events.Events[j].TransactionIndex - } - return events.Events[i].EventIndex < events.Events[j].EventIndex - }) - e, err := decodeCadenceEvents(events) if err != nil { return nil, err @@ -219,11 +210,41 @@ type BlockEvents struct { Err error } -func NewBlockEvents(events flow.BlockEvents) BlockEvents { - blockEvents, err := NewCadenceEvents(events) +// NewMultiBlockEvents will decode any possible `EVM.TransactionExecuted` & +// `EVM.BlockExecuted` events and populate the resulting `Block`, `Transaction` & +// `Receipt` values. +// The `EVM.TransactionExecuted` events are expected to be properly sorted by +// the caller. +// Use this method when dealing with `flow.BlockEvents` from multiple Flow blocks. +// The `EVM.TransactionExecuted` events could be produced at a Flow block, that +// comes prior to the Flow block that produced the `EVM.BlockExecuted` event. +func NewMultiBlockEvents(events flow.BlockEvents) BlockEvents { + cdcEvents, err := NewCadenceEvents(events) + return BlockEvents{ + Events: cdcEvents, + Err: err, + } +} + +// NewSingleBlockEvents will decode any possible `EVM.TransactionExecuted` & +// `EVM.BlockExecuted` events and populate the resulting `Block`, `Transaction` & +// `Receipt` values. +// The `EVM.TransactionExecuted` events will be sorted by `TransactionIndex` & +// `EventIndex`, prior to decoding. +// Use this method when dealing with `flow.BlockEvents` from a single Flow block. +func NewSingleBlockEvents(events flow.BlockEvents) BlockEvents { + // first we sort all the events in the block, by their TransactionIndex, + // and then we also sort events in the same transaction, by their EventIndex. + sort.Slice(events.Events, func(i, j int) bool { + if events.Events[i].TransactionIndex != events.Events[j].TransactionIndex { + return events.Events[i].TransactionIndex < events.Events[j].TransactionIndex + } + return events.Events[i].EventIndex < events.Events[j].EventIndex + }) + cdcEvents, err := NewCadenceEvents(events) return BlockEvents{ - Events: blockEvents, + Events: cdcEvents, Err: err, } } diff --git a/models/events_test.go b/models/events_test.go index e79f03d33..1f564bc6e 100644 --- a/models/events_test.go +++ b/models/events_test.go @@ -201,8 +201,9 @@ func TestCadenceEvents_Block(t *testing.T) { blockEvents.Events = append(blockEvents.Events, blockEvent) // parse the EventStreaming API response - cdcEvents, err := NewCadenceEvents(blockEvents) - require.NoError(t, err) + blkEvents := NewSingleBlockEvents(blockEvents) + require.NoError(t, blkEvents.Err) + cdcEvents := blkEvents.Events // assert that Flow events are sorted by their TransactionIndex and EventIndex fields assert.Equal( diff --git a/services/ingestion/engine_test.go b/services/ingestion/engine_test.go index 338491aad..bb00e5484 100644 --- a/services/ingestion/engine_test.go +++ b/services/ingestion/engine_test.go @@ -104,7 +104,7 @@ func TestSerialBlockIngestion(t *testing.T) { }). Once() - eventsChan <- models.NewBlockEvents(flow.BlockEvents{ + eventsChan <- models.NewSingleBlockEvents(flow.BlockEvents{ Events: []flow.Event{{ Type: string(blockEvent.Etype), Value: blockCdc, @@ -318,7 +318,7 @@ func TestBlockAndTransactionIngestion(t *testing.T) { }). Once() - eventsChan <- models.NewBlockEvents(flow.BlockEvents{ + eventsChan <- models.NewSingleBlockEvents(flow.BlockEvents{ Events: []flow.Event{{ Type: string(blockEvent.Etype), Value: blockCdc, @@ -420,7 +420,7 @@ func TestBlockAndTransactionIngestion(t *testing.T) { }). Once() - eventsChan <- models.NewBlockEvents(flow.BlockEvents{ + eventsChan <- models.NewSingleBlockEvents(flow.BlockEvents{ Events: []flow.Event{ // first transaction { @@ -557,7 +557,7 @@ func TestBlockAndTransactionIngestion(t *testing.T) { // and it will make the first block be swapped with second block out-of-order events[1], events[2] = events[2], events[1] - eventsChan <- models.NewBlockEvents(flow.BlockEvents{ + eventsChan <- models.NewSingleBlockEvents(flow.BlockEvents{ Events: events, Height: latestCadenceHeight + 1, }) diff --git a/services/ingestion/event_subscriber.go b/services/ingestion/event_subscriber.go index 3138269c1..a3136a073 100644 --- a/services/ingestion/event_subscriber.go +++ b/services/ingestion/event_subscriber.go @@ -160,7 +160,7 @@ func (r *RPCEventSubscriber) subscribe(ctx context.Context, height uint64) <-cha return } - evmEvents := models.NewBlockEvents(blockEvents) + evmEvents := models.NewSingleBlockEvents(blockEvents) // if events contain an error, or we are in a recovery mode if evmEvents.Err != nil || r.recovery { evmEvents = r.recover(ctx, blockEvents, evmEvents.Err) @@ -237,6 +237,18 @@ const maxRangeForGetEvents = uint64(249) func (r *RPCEventSubscriber) backfillSporkFromHeight(ctx context.Context, fromCadenceHeight uint64, eventsChan chan<- models.BlockEvents) (uint64, error) { evmAddress := common.Address(systemcontracts.SystemContractsForChain(r.chain).EVMContract.Address) + blockExecutedEvent := common.NewAddressLocation( + nil, + evmAddress, + string(events.EventTypeBlockExecuted), + ).ID() + + transactionExecutedEvent := common.NewAddressLocation( + nil, + evmAddress, + string(events.EventTypeTransactionExecuted), + ).ID() + lastHeight, err := r.client.GetLatestHeightForSpork(ctx, fromCadenceHeight) if err != nil { eventsChan <- models.NewBlockEventsError(err) @@ -257,18 +269,6 @@ func (r *RPCEventSubscriber) backfillSporkFromHeight(ctx context.Context, fromCa endHeight = lastHeight } - blockExecutedEvent := common.NewAddressLocation( - nil, - evmAddress, - string(events.EventTypeBlockExecuted), - ).ID() - - transactionExecutedEvent := common.NewAddressLocation( - nil, - evmAddress, - string(events.EventTypeTransactionExecuted), - ).ID() - blocks, err := r.client.GetEventsForHeightRange(ctx, blockExecutedEvent, startHeight, endHeight) if err != nil { return 0, fmt.Errorf("failed to get block events: %w", err) @@ -299,7 +299,22 @@ func (r *RPCEventSubscriber) backfillSporkFromHeight(ctx context.Context, fromCa // append the transaction events to the block events blocks[i].Events = append(blocks[i].Events, transactions[i].Events...) - evmEvents := models.NewBlockEvents(blocks[i]) + evmEvents := models.NewSingleBlockEvents(blocks[i]) + if evmEvents.Err != nil && errors.Is(evmEvents.Err, errs.ErrMissingBlock) { + evmEvents, err = r.accumulateBlockEvents( + ctx, + blocks[i], + blockExecutedEvent, + transactionExecutedEvent, + ) + if err != nil { + return 0, err + } + eventsChan <- evmEvents + // advance the height + fromCadenceHeight = evmEvents.Events.CadenceHeight() + 1 + break + } eventsChan <- evmEvents // advance the height @@ -310,6 +325,86 @@ func (r *RPCEventSubscriber) backfillSporkFromHeight(ctx context.Context, fromCa return fromCadenceHeight, nil } +// accumulateBlockEvents will keep fetching `EVM.TransactionExecuted` events +// until it finds their `EVM.BlockExecuted` event. +// At that point it will return the valid models.BlockEvents. +func (r *RPCEventSubscriber) accumulateBlockEvents( + ctx context.Context, + block flow.BlockEvents, + blockExecutedEventType string, + txExecutedEventType string, +) (models.BlockEvents, error) { + evmEvents := models.NewSingleBlockEvents(block) + currentHeight := block.Height + transactionEvents := make([]flow.Event, 0) + + for evmEvents.Err != nil && errors.Is(evmEvents.Err, errs.ErrMissingBlock) { + blocks, err := r.client.GetEventsForHeightRange( + ctx, + blockExecutedEventType, + currentHeight, + currentHeight+maxRangeForGetEvents, + ) + if err != nil { + return models.BlockEvents{}, fmt.Errorf("failed to get block events: %w", err) + } + + transactions, err := r.client.GetEventsForHeightRange( + ctx, + txExecutedEventType, + currentHeight, + currentHeight+maxRangeForGetEvents, + ) + if err != nil { + return models.BlockEvents{}, fmt.Errorf("failed to get block events: %w", err) + } + + if len(transactions) != len(blocks) { + return models.BlockEvents{}, fmt.Errorf("transactions and blocks have different length") + } + + // sort both, just in case + sort.Slice(blocks, func(i, j int) bool { + return blocks[i].Height < blocks[j].Height + }) + sort.Slice(transactions, func(i, j int) bool { + return transactions[i].Height < transactions[j].Height + }) + + for i := range blocks { + if transactions[i].Height != blocks[i].Height { + return models.BlockEvents{}, fmt.Errorf("transactions and blocks have different height") + } + + // If no EVM.BlockExecuted event found, keep accumulating the incoming + // EVM.TransactionExecuted events, until we find the EVM.BlockExecuted + // event that includes them. + if len(blocks[i].Events) == 0 { + txEvents := transactions[i].Events + // Sort `EVM.TransactionExecuted` events + sort.Slice(txEvents, func(i, j int) bool { + if txEvents[i].TransactionIndex != txEvents[j].TransactionIndex { + return txEvents[i].TransactionIndex < txEvents[j].TransactionIndex + } + return txEvents[i].EventIndex < txEvents[j].EventIndex + }) + transactionEvents = append(transactionEvents, txEvents...) + } else { + blocks[i].Events = append(blocks[i].Events, transactionEvents...) + // We use `models.NewMultiBlockEvents`, as the `transactionEvents` + // are coming from different Flow blocks. + evmEvents = models.NewMultiBlockEvents(blocks[i]) + if evmEvents.Err == nil { + return evmEvents, nil + } + } + + currentHeight = blocks[i].Height + 1 + } + } + return evmEvents, nil +} + // fetchMissingData is used as a backup mechanism for fetching EVM-related // events, when the event streaming API returns an inconsistent response. // An inconsistent response could be an EVM block that references EVM @@ -346,17 +441,27 @@ func (r *RPCEventSubscriber) fetchMissingData( blockEvents.Events = append(blockEvents.Events, recoveredEvents[0].Events...) } - return models.NewBlockEvents(blockEvents) + return models.NewSingleBlockEvents(blockEvents) } // accumulateEventsMissingBlock will keep receiving transaction events until it can produce a valid // EVM block event containing a block and transactions. At that point it will reset the recovery mode // and return the valid block events. func (r *RPCEventSubscriber) accumulateEventsMissingBlock(events flow.BlockEvents) models.BlockEvents { - r.recoveredEvents = append(r.recoveredEvents, events.Events...) + txEvents := events.Events + // Sort `EVM.TransactionExecuted` events + sort.Slice(txEvents, func(i, j int) bool { + if txEvents[i].TransactionIndex != txEvents[j].TransactionIndex { + return txEvents[i].TransactionIndex < txEvents[j].TransactionIndex + } + return txEvents[i].EventIndex < txEvents[j].EventIndex + }) + r.recoveredEvents = append(r.recoveredEvents, txEvents...) events.Events = r.recoveredEvents - recovered := models.NewBlockEvents(events) + // We use `models.NewMultiBlockEvents`, as the `transactionEvents` + // are coming from different Flow blocks. + recovered := models.NewMultiBlockEvents(events) r.recovery = recovered.Err != nil if !r.recovery { From a22b951ec57d58875a1ab3d6d4bc755d8e0c33a6 Mon Sep 17 00:00:00 2001 From: Ardit Marku Date: Wed, 20 Nov 2024 13:56:19 +0200 Subject: [PATCH 077/174] Add more unit tests for NewSingleBlockEvents & NewMultiBlockEvents methods --- models/events_test.go | 290 +++++++++++++++++++++++++++++++++++++++--- 1 file changed, 272 insertions(+), 18 deletions(-) diff --git a/models/events_test.go b/models/events_test.go index 1f564bc6e..1cbb0be23 100644 --- a/models/events_test.go +++ b/models/events_test.go @@ -15,7 +15,7 @@ import ( "github.com/stretchr/testify/require" ) -func TestCadenceEvents_Block(t *testing.T) { +func TestNewSingleBlockEvents(t *testing.T) { invalid := cadence.String("invalid") b0, e0, err := newBlock(0, nil) @@ -47,17 +47,18 @@ func TestCadenceEvents_Block(t *testing.T) { for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { - e, err := NewCadenceEvents(tt.events) - require.NoError(t, err) + evmEvents := NewSingleBlockEvents(tt.events) + require.NoError(t, evmEvents.Err) + cdcEvents := evmEvents.Events if tt.block != nil { ttHash, err := tt.block.Hash() require.NoError(t, err) - hash, err := e.Block().Hash() + hash, err := cdcEvents.Block().Hash() require.NoError(t, err) assert.Equal(t, ttHash, hash) } else { - assert.Nil(t, e.Block()) + assert.Nil(t, cdcEvents.Block()) } }) } @@ -75,6 +76,26 @@ func TestCadenceEvents_Block(t *testing.T) { events = append(events, txEvent) } + t.Run("missing block with transactions", func(t *testing.T) { + // generate single block + _, _, err := newBlock(cadenceHeight, hashes) + require.NoError(t, err) + + blockEvents := flow.BlockEvents{ + BlockID: flow.Identifier{0x1}, + Height: cadenceHeight, + Events: events, + } + + evmEvents := NewSingleBlockEvents(blockEvents) + require.Error(t, evmEvents.Err) + assert.ErrorContains( + t, + evmEvents.Err, + "missing block EVM block nil at flow block: 1", + ) + }) + t.Run("block with less transaction hashes", func(t *testing.T) { // generate single block _, blockEvent, err := newBlock(cadenceHeight, hashes[:txCount-2]) @@ -88,11 +109,11 @@ func TestCadenceEvents_Block(t *testing.T) { blockEvents.Events = append(blockEvents.Events, blockEvent) - _, err = NewCadenceEvents(blockEvents) - require.Error(t, err) + evmEvents := NewSingleBlockEvents(blockEvents) + require.Error(t, evmEvents.Err) assert.ErrorContains( t, - err, + evmEvents.Err, "block 1 references missing transaction/s", ) }) @@ -110,8 +131,8 @@ func TestCadenceEvents_Block(t *testing.T) { blockEvents.Events = append(blockEvents.Events, blockEvent) - _, err = NewCadenceEvents(blockEvents) - require.NoError(t, err) + evmEvents := NewSingleBlockEvents(blockEvents) + require.NoError(t, evmEvents.Err) }) t.Run("block with empty transaction hashes", func(t *testing.T) { @@ -126,8 +147,8 @@ func TestCadenceEvents_Block(t *testing.T) { blockEvents.Events = append(blockEvents.Events, blockEvent) - _, err = NewCadenceEvents(blockEvents) - require.NoError(t, err) + evmEvents := NewSingleBlockEvents(blockEvents) + require.NoError(t, evmEvents.Err) }) t.Run("block with more transaction hashes", func(t *testing.T) { @@ -145,11 +166,11 @@ func TestCadenceEvents_Block(t *testing.T) { blockEvents.Events = append(blockEvents.Events, blockEvent) - _, err = NewCadenceEvents(blockEvents) - require.Error(t, err) + evmEvents := NewSingleBlockEvents(blockEvents) + require.Error(t, evmEvents.Err) assert.ErrorContains( t, - err, + evmEvents.Err, "block 1 references missing transaction/s", ) }) @@ -201,9 +222,9 @@ func TestCadenceEvents_Block(t *testing.T) { blockEvents.Events = append(blockEvents.Events, blockEvent) // parse the EventStreaming API response - blkEvents := NewSingleBlockEvents(blockEvents) - require.NoError(t, blkEvents.Err) - cdcEvents := blkEvents.Events + evmEvents := NewSingleBlockEvents(blockEvents) + require.NoError(t, evmEvents.Err) + cdcEvents := evmEvents.Events // assert that Flow events are sorted by their TransactionIndex and EventIndex fields assert.Equal( @@ -240,6 +261,239 @@ func TestCadenceEvents_Block(t *testing.T) { }) } +func TestNewMultiBlockEvents(t *testing.T) { + invalid := cadence.String("invalid") + + b0, e0, err := newBlock(0, nil) + require.NoError(t, err) + + tests := []struct { + name string + events flow.BlockEvents + block *Block + err error + }{ + { + name: "BlockExecutedEventExists", + events: flow.BlockEvents{Events: []flow.Event{e0}}, + block: b0, + }, { + name: "BlockExecutedEventEmpty", + events: flow.BlockEvents{Events: []flow.Event{}}, + block: nil, + }, { + name: "BlockExecutedNotFound", + events: flow.BlockEvents{Events: []flow.Event{{ + Type: e0.Type, + Value: cadence.NewEvent([]cadence.Value{invalid}), + }}}, + block: nil, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + evmEvents := NewMultiBlockEvents(tt.events) + require.NoError(t, evmEvents.Err) + + cdcEvents := evmEvents.Events + if tt.block != nil { + ttHash, err := tt.block.Hash() + require.NoError(t, err) + hash, err := cdcEvents.Block().Hash() + require.NoError(t, err) + assert.Equal(t, ttHash, hash) + } else { + assert.Nil(t, cdcEvents.Block()) + } + }) + } + + cadenceHeight := uint64(15) + txCount := 10 + hashes := make([]gethCommon.Hash, txCount) + evmTxEvents := make([]flow.BlockEvents, txCount) + + // generate txs + for i := 0; i < txCount; i++ { + tx, _, txEvent, err := newTransaction(uint64(i), uint16(i)) + require.NoError(t, err) + hashes[i] = tx.Hash() + evmTxEvents[i] = flow.BlockEvents{ + BlockID: flow.BytesToID([]byte{uint8(i + 1)}), + Height: uint64(i + 1), + Events: []flow.Event{txEvent}, + } + } + + t.Run("missing block with transactions", func(t *testing.T) { + // generate single block + _, _, err := newBlock(cadenceHeight, hashes) + require.NoError(t, err) + + blockEvents := flow.BlockEvents{ + BlockID: flow.Identifier{0x1}, + Height: cadenceHeight, + } + + // Below we add all the EVM transaction events, but we have omitted + // the EVM.BlockExecuted event. + for i := 0; i < txCount; i++ { + blockEvents.Events = append(blockEvents.Events, evmTxEvents[i].Events...) + } + + evmEvents := NewSingleBlockEvents(blockEvents) + require.Error(t, evmEvents.Err) + assert.ErrorContains( + t, + evmEvents.Err, + "missing block EVM block nil at flow block: 1", + ) + }) + + t.Run("block with less transaction hashes", func(t *testing.T) { + // generate single block + _, blockEvent, err := newBlock(cadenceHeight, hashes) + require.NoError(t, err) + + blockEvents := flow.BlockEvents{ + BlockID: flow.Identifier{0x1}, + Height: cadenceHeight, + } + + // Below we omit 2 EVM transactions from the events + for i := 0; i < txCount-2; i++ { + blockEvents.Events = append(blockEvents.Events, evmTxEvents[i].Events...) + } + + blockEvents.Events = append(blockEvents.Events, blockEvent) + + evmEvents := NewMultiBlockEvents(blockEvents) + require.Error(t, evmEvents.Err) + assert.ErrorContains( + t, + evmEvents.Err, + "block 15 references missing transaction/s", + ) + }) + + t.Run("block with equal transaction hashes", func(t *testing.T) { + // generate single block + _, blockEvent, err := newBlock(cadenceHeight, hashes) + require.NoError(t, err) + + blockEvents := flow.BlockEvents{ + BlockID: flow.Identifier{0x1}, + Height: cadenceHeight, + } + + // Below we add all the EVM transaction events + for i := 0; i < txCount; i++ { + blockEvents.Events = append(blockEvents.Events, evmTxEvents[i].Events...) + } + + blockEvents.Events = append(blockEvents.Events, blockEvent) + + evmEvents := NewMultiBlockEvents(blockEvents) + require.NoError(t, evmEvents.Err) + }) + + t.Run("block with empty transaction hashes", func(t *testing.T) { + // generate single block + _, blockEvent, err := newBlock(cadenceHeight, []gethCommon.Hash{}) + require.NoError(t, err) + + blockEvents := flow.BlockEvents{ + BlockID: flow.Identifier{0x1}, + Height: cadenceHeight, + } + + blockEvents.Events = append(blockEvents.Events, blockEvent) + + evmEvents := NewMultiBlockEvents(blockEvents) + require.NoError(t, evmEvents.Err) + }) + + t.Run("block with more transaction hashes", func(t *testing.T) { + tx, _, _, err := newTransaction(1, 0) + require.NoError(t, err) + + // generate single block + _, blockEvent, err := newBlock(cadenceHeight, []gethCommon.Hash{tx.Hash()}) + require.NoError(t, err) + + blockEvents := flow.BlockEvents{ + BlockID: flow.Identifier{0x1}, + Height: cadenceHeight, + } + + blockEvents.Events = append(blockEvents.Events, blockEvent) + + evmEvents := NewMultiBlockEvents(blockEvents) + require.Error(t, evmEvents.Err) + assert.ErrorContains( + t, + evmEvents.Err, + "block 15 references missing transaction/s", + ) + }) + + t.Run("EVM.TransactionExecuted events should be properly ordered", func(t *testing.T) { + blockEvents := flow.BlockEvents{ + BlockID: flow.Identifier{0x1}, + Height: cadenceHeight, + } + + // tx1 and tx2 are EVM transactions executed on a single Flow transaction. + tx1, _, txEvent1, err := newTransaction(0, 0) + require.NoError(t, err) + txEvent1.TransactionIndex = 0 + txEvent1.EventIndex = 2 + + tx2, _, txEvent2, err := newTransaction(1, 1) + require.NoError(t, err) + txEvent2.TransactionIndex = 0 + txEvent2.EventIndex = 5 + + // tx3 is a Flow transaction with a single EVM transaction on EventIndex=1 + tx3, _, txEvent3, err := newTransaction(2, 0) + require.NoError(t, err) + txEvent3.TransactionIndex = 2 + txEvent3.EventIndex = 1 + + // needed for computing the `TransactionHashRoot` field on + // EVM.BlockExecuted event payload. the order is sensitive. + hashes = []gethCommon.Hash{ + tx1.Hash(), + tx2.Hash(), + tx3.Hash(), + } + + // add the tx events in a shuffled order + blockEvents.Events = []flow.Event{ + txEvent3, + txEvent1, + txEvent2, + } + + // generate single block + _, blockEvent, err := newBlock(cadenceHeight, hashes) + require.NoError(t, err) + blockEvent.TransactionIndex = 4 + blockEvent.EventIndex = 0 + blockEvents.Events = append(blockEvents.Events, blockEvent) + + // parse the EventStreaming API response + evmEvents := NewMultiBlockEvents(blockEvents) + require.Error(t, evmEvents.Err) + assert.ErrorContains( + t, + evmEvents.Err, + "block 15 references missing transaction/s", + ) + }) +} + func Test_EventDecoding(t *testing.T) { cadenceHeight := uint64(1) txCount := 10 From 0fff9b186919d609df6299700d9df85228b5e55c Mon Sep 17 00:00:00 2001 From: Janez Podhostnik Date: Thu, 21 Nov 2024 12:12:42 +0100 Subject: [PATCH 078/174] renamed method --- README.md | 2 +- api/api.go | 1 + api/debug.go | 4 ++-- tests/web3js/debug_util_test.js | 2 +- 4 files changed, 5 insertions(+), 4 deletions(-) diff --git a/README.md b/README.md index 7d40eafa9..e2e5c84a9 100644 --- a/README.md +++ b/README.md @@ -267,7 +267,7 @@ The EVM Gateway implements APIs according to the Ethereum specification: https:/ * debug_traceBlockByNumber * debug_traceBlockByHash -- debug_flowBlock - returns the flow block height for the given EVM block (id or height) +- debug_flowHeightByBlock - returns the flow block height for the given EVM block (id or height) **Unsupported APIs** - Wallet APIs: we don't officially support wallet APIs (eth_accounts, eth_sign, eth_signTransaction, eth_sendTransaction) due to security diff --git a/api/api.go b/api/api.go index 2360698fe..a8c96cc91 100644 --- a/api/api.go +++ b/api/api.go @@ -76,6 +76,7 @@ var validMethods = map[string]struct{}{ "debug_traceBlockByNumber": {}, "debug_traceBlockByHash": {}, "debug_traceCall": {}, + "debug_flowHeightByBlock": {}, // web3 namespace "web3_clientVersion": {}, diff --git a/api/debug.go b/api/debug.go index d55f51326..4501a1e76 100644 --- a/api/debug.go +++ b/api/debug.go @@ -361,9 +361,9 @@ func (d *DebugAPI) TraceCall( return tracer.GetResult() } -// FlowHeight returns the Flow height for the given EVM block specified either by EVM +// FlowHeightByBlock returns the Flow height for the given EVM block specified either by EVM // block height or EVM block hash. -func (d *DebugAPI) FlowHeight( +func (d *DebugAPI) FlowHeightByBlock( _ context.Context, blockNrOrHash rpc.BlockNumberOrHash, ) (uint64, error) { diff --git a/tests/web3js/debug_util_test.js b/tests/web3js/debug_util_test.js index 371967312..9e5b7291a 100644 --- a/tests/web3js/debug_util_test.js +++ b/tests/web3js/debug_util_test.js @@ -4,7 +4,7 @@ const conf = require('./config') it('should retrieve flow height', async () => { let response = await helpers.callRPCMethod( - 'debug_flowHeight', + 'debug_flowHeightByBlock', ['latest'] ) assert.equal(response.status, 200) From b6d5e555aa5f5f59f5013d8e958644d8474b0236 Mon Sep 17 00:00:00 2001 From: Janez Podhostnik Date: Thu, 21 Nov 2024 14:43:31 +0100 Subject: [PATCH 079/174] Use offchain package to create block context --- go.mod | 12 +++++------ go.sum | 28 ++++++++++++------------- services/replayer/blocks_provider.go | 31 ++++++++-------------------- tests/go.mod | 14 ++++++------- tests/go.sum | 8 +++++++ 5 files changed, 44 insertions(+), 49 deletions(-) diff --git a/go.mod b/go.mod index 5fcd558ff..7392e26eb 100644 --- a/go.mod +++ b/go.mod @@ -6,9 +6,9 @@ require ( github.com/cockroachdb/pebble v1.1.1 github.com/goccy/go-json v0.10.2 github.com/onflow/atree v0.8.0 - github.com/onflow/cadence v1.2.1 - github.com/onflow/flow-go v0.38.0-preview.0.0.20241022154145-6a254edbec23 - github.com/onflow/flow-go-sdk v1.2.2 + github.com/onflow/cadence v1.2.2 + github.com/onflow/flow-go v0.38.0-preview.0.0.20241121133145-f880fe7b2090 + github.com/onflow/flow-go-sdk v1.2.3 github.com/onflow/go-ethereum v1.14.7 github.com/prometheus/client_golang v1.18.0 github.com/rs/cors v1.8.0 @@ -195,11 +195,11 @@ require ( go.uber.org/atomic v1.11.0 // indirect go.uber.org/multierr v1.11.0 // indirect go.uber.org/zap v1.26.0 // indirect - golang.org/x/crypto v0.26.0 // indirect + golang.org/x/crypto v0.28.0 // indirect golang.org/x/net v0.25.0 // indirect golang.org/x/oauth2 v0.17.0 // indirect - golang.org/x/sys v0.23.0 // indirect - golang.org/x/text v0.17.0 // indirect + golang.org/x/sys v0.26.0 // indirect + golang.org/x/text v0.19.0 // indirect golang.org/x/time v0.5.0 // indirect golang.org/x/xerrors v0.0.0-20231012003039-104605ab7028 // indirect gonum.org/v1/gonum v0.14.0 // indirect diff --git a/go.sum b/go.sum index 98d1b96be..1b60e94a1 100644 --- a/go.sum +++ b/go.sum @@ -523,8 +523,8 @@ github.com/olekukonko/tablewriter v0.0.5 h1:P2Ga83D34wi1o9J6Wh1mRuqd4mF/x/lgBS7N github.com/olekukonko/tablewriter v0.0.5/go.mod h1:hPp6KlRPjbx+hW8ykQs1w3UBbZlj6HuIJcUGPhkA7kY= github.com/onflow/atree v0.8.0 h1:qg5c6J1gVDNObughpEeWm8oxqhPGdEyGrda121GM4u0= github.com/onflow/atree v0.8.0/go.mod h1:yccR+LR7xc1Jdic0mrjocbHvUD7lnVvg8/Ct1AA5zBo= -github.com/onflow/cadence v1.2.1 h1:hmSsgX3rTsp2E5qTSl1JXINt8qepdRrHTwDSYqN5Nxs= -github.com/onflow/cadence v1.2.1/go.mod h1:fJxxOAp1wnWDfOHT8GOc1ypsU0RR5E3z51AhG8Yf5jg= +github.com/onflow/cadence v1.2.2 h1:LwigF/2lPiXlwX5rFn71KeMpmW5Iu/f/JtsPLLULBCc= +github.com/onflow/cadence v1.2.2/go.mod h1:PYX1xLejqswtDsQzN93x/VpfSKNyjUk6hrkc/mpv7xs= github.com/onflow/crypto v0.25.2 h1:GjHunqVt+vPcdqhxxhAXiMIF3YiLX7gTuTR5O+VG2ns= github.com/onflow/crypto v0.25.2/go.mod h1:fY7eLqUdMKV8EGOw301unP8h7PvLVy8/6gVR++/g0BY= github.com/onflow/flow-core-contracts/lib/go/contracts v1.4.0 h1:R86HaOuk6vpuECZnriEUE7bw9inC2AtdSn8lL/iwQLQ= @@ -535,10 +535,10 @@ github.com/onflow/flow-ft/lib/go/contracts v1.0.1 h1:Ts5ob+CoCY2EjEd0W6vdLJ7hLL3 github.com/onflow/flow-ft/lib/go/contracts v1.0.1/go.mod h1:PwsL8fC81cjnUnTfmyL/HOIyHnyaw/JA474Wfj2tl6A= github.com/onflow/flow-ft/lib/go/templates v1.0.1 h1:FDYKAiGowABtoMNusLuRCILIZDtVqJ/5tYI4VkF5zfM= github.com/onflow/flow-ft/lib/go/templates v1.0.1/go.mod h1:uQ8XFqmMK2jxyBSVrmyuwdWjTEb+6zGjRYotfDJ5pAE= -github.com/onflow/flow-go v0.38.0-preview.0.0.20241022154145-6a254edbec23 h1:spF44tXZ341oVDTuXzzKTQ0W6rwZFV9r2/SRVVaMReo= -github.com/onflow/flow-go v0.38.0-preview.0.0.20241022154145-6a254edbec23/go.mod h1:6f7CTcguVOBINmraaWMiij5e2zu7W2mKsOmXAfvCZ2g= -github.com/onflow/flow-go-sdk v1.2.2 h1:F78Sq/VaExgtaQv739k06gnx2aIyLF5wVE0XwxFpmsc= -github.com/onflow/flow-go-sdk v1.2.2/go.mod h1:yhQ5+Sp2xWoCQ1fuRDswawTDQ0ng0z5nTkFVH82xL7E= +github.com/onflow/flow-go v0.38.0-preview.0.0.20241121133145-f880fe7b2090 h1:MjYx6rR1L3qf0q5yHZIdExxThiPM0/BJ/63EG/QoTcA= +github.com/onflow/flow-go v0.38.0-preview.0.0.20241121133145-f880fe7b2090/go.mod h1:c4ubAQ2WIMYY/TOaBvbajROEFWv2HwhKeGOsEdLPIM0= +github.com/onflow/flow-go-sdk v1.2.3 h1:jb+0dIXBO12Zt8x3c2xDXYPv6k3sRTUvhe59M+EcXTI= +github.com/onflow/flow-go-sdk v1.2.3/go.mod h1:jMaffBTlAIdutx+pBhRIigLZFIBYSDDST0Uax1rW2qo= github.com/onflow/flow-nft/lib/go/contracts v1.2.2 h1:XFERNVUDGbZ4ViZjt7P1cGD80mO1PzUJYPfdhXFsGbQ= github.com/onflow/flow-nft/lib/go/contracts v1.2.2/go.mod h1:eZ9VMMNfCq0ho6kV25xJn1kXeCfxnkhj3MwF3ed08gY= github.com/onflow/flow-nft/lib/go/templates v1.2.1 h1:SAALMZPDw9Eb9p5kSLnmnFxjyig1MLiT4JUlLp0/bSE= @@ -780,8 +780,8 @@ golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPh golang.org/x/crypto v0.0.0-20210421170649-83a5a9bb288b/go.mod h1:T9bdIzuCu7OtxOm1hfPfRQxPLYneinmdGuTeoZ9dtd4= golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc= golang.org/x/crypto v0.0.0-20220722155217-630584e8d5aa/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4= -golang.org/x/crypto v0.26.0 h1:RrRspgV4mU+YwB4FYnuBoKsUapNIL5cohGAmSH3azsw= -golang.org/x/crypto v0.26.0/go.mod h1:GY7jblb9wI+FOo5y8/S2oY4zWP07AkOJ4+jxCqdqn54= +golang.org/x/crypto v0.28.0 h1:GBDwsMXVQi34v5CCYUm2jkJvu4cbtru2U4TN2PSyQnw= +golang.org/x/crypto v0.28.0/go.mod h1:rmgy+3RHxRZMyY0jjAJShp2zgEdOqj2AO7U0pYmeQ7U= golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= golang.org/x/exp v0.0.0-20190306152737-a1d7652674e8/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= golang.org/x/exp v0.0.0-20190510132918-efd6b22b2522/go.mod h1:ZjyILWgesfNpC6sMxTJOJm9Kp84zZh5NQWvqDGG3Qr8= @@ -955,12 +955,12 @@ golang.org/x/sys v0.8.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.11.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.12.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.14.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= -golang.org/x/sys v0.23.0 h1:YfKFowiIMvtgl1UERQoTPPToxltDeZfbj4H7dVUCwmM= -golang.org/x/sys v0.23.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= +golang.org/x/sys v0.26.0 h1:KHjCJyddX0LoSTb3J+vWpupP9p0oznkqVk/IfjymZbo= +golang.org/x/sys v0.26.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8= -golang.org/x/term v0.23.0 h1:F6D4vR+EHoL9/sWAWgAR1H2DcHr4PareCbAaCo1RpuU= -golang.org/x/term v0.23.0/go.mod h1:DgV24QBUrK6jhZXl+20l6UWznPlwAHm1Q1mGHtydmSk= +golang.org/x/term v0.25.0 h1:WtHI/ltw4NvSUig5KARz9h521QvRC8RmF/cuYqifU24= +golang.org/x/term v0.25.0/go.mod h1:RPyXicDX+6vLxogjjRxjgD2TKtmAO6NZBsBRfrOLu7M= golang.org/x/text v0.0.0-20170915032832-14c0d48ead0c/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= golang.org/x/text v0.3.1-0.20180807135948-17ff2d5776d2/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= @@ -970,8 +970,8 @@ golang.org/x/text v0.3.4/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ= golang.org/x/text v0.3.8/go.mod h1:E6s5w1FMmriuDzIBO73fBruAKo1PCIq6d2Q6DHfQ8WQ= -golang.org/x/text v0.17.0 h1:XtiM5bkSOt+ewxlOE/aE/AKEHibwj/6gvWMl9Rsh0Qc= -golang.org/x/text v0.17.0/go.mod h1:BuEKDfySbSR4drPmRPG/7iBdf8hvFMuRexcpahXilzY= +golang.org/x/text v0.19.0 h1:kTxAhCbGbxhK0IwgSKiMO5awPoDQ0RpfiVYBfK860YM= +golang.org/x/text v0.19.0/go.mod h1:BuEKDfySbSR4drPmRPG/7iBdf8hvFMuRexcpahXilzY= golang.org/x/time v0.0.0-20181108054448-85acf8d2951c/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= golang.org/x/time v0.0.0-20190308202827-9d24e82272b4/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= golang.org/x/time v0.0.0-20191024005414-555d28b269f0/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= diff --git a/services/replayer/blocks_provider.go b/services/replayer/blocks_provider.go index d43749a0c..9cb2dfbe1 100644 --- a/services/replayer/blocks_provider.go +++ b/services/replayer/blocks_provider.go @@ -5,6 +5,7 @@ import ( "github.com/onflow/flow-evm-gateway/models" "github.com/onflow/flow-evm-gateway/storage" + "github.com/onflow/flow-go/fvm/evm/offchain/blocks" evmTypes "github.com/onflow/flow-go/fvm/evm/types" flowGo "github.com/onflow/flow-go/model/flow" gethCommon "github.com/onflow/go-ethereum/common" @@ -19,25 +20,11 @@ type blockSnapshot struct { var _ evmTypes.BlockSnapshot = (*blockSnapshot)(nil) func (bs *blockSnapshot) BlockContext() (evmTypes.BlockContext, error) { - return evmTypes.BlockContext{ - ChainID: evmTypes.EVMChainIDFromFlowChainID(bs.chainID), - BlockNumber: bs.block.Height, - BlockTimestamp: bs.block.Timestamp, - DirectCallBaseGasUsage: evmTypes.DefaultDirectCallBaseGasUsage, - DirectCallGasPrice: evmTypes.DefaultDirectCallGasPrice, - GasFeeCollector: evmTypes.CoinbaseAddress, - GetHashFunc: func(n uint64) gethCommon.Hash { - // For block heights greater than or equal to the current, - // return an empty block hash. - if n >= bs.block.Height { - return gethCommon.Hash{} - } - // If the given block height, is more than 256 blocks - // in the past, return an empty block hash. - if bs.block.Height-n > 256 { - return gethCommon.Hash{} - } - + return blocks.NewBlockContext( + bs.chainID, + bs.block.Height, + bs.block.Timestamp, + func(n uint64) gethCommon.Hash { block, err := bs.blocks.GetByHeight(n) if err != nil { return gethCommon.Hash{} @@ -49,9 +36,9 @@ func (bs *blockSnapshot) BlockContext() (evmTypes.BlockContext, error) { return blockHash }, - Random: bs.block.PrevRandao, - Tracer: bs.tracer, - }, nil + bs.block.PrevRandao, + bs.tracer, + ) } type BlocksProvider struct { diff --git a/tests/go.mod b/tests/go.mod index c42584d06..a8d081e27 100644 --- a/tests/go.mod +++ b/tests/go.mod @@ -4,12 +4,12 @@ go 1.22 require ( github.com/goccy/go-json v0.10.2 - github.com/onflow/cadence v1.2.1 + github.com/onflow/cadence v1.2.2 github.com/onflow/crypto v0.25.2 github.com/onflow/flow-emulator v1.1.0 github.com/onflow/flow-evm-gateway v0.0.0-20240201154855-4d4d3d3f19c7 - github.com/onflow/flow-go v0.38.0-preview.0.0.20241022154145-6a254edbec23 - github.com/onflow/flow-go-sdk v1.2.2 + github.com/onflow/flow-go v0.38.0-preview.0.0.20241121133145-f880fe7b2090 + github.com/onflow/flow-go-sdk v1.2.3 github.com/onflow/go-ethereum v1.14.7 github.com/rs/zerolog v1.33.0 github.com/stretchr/testify v1.9.0 @@ -220,14 +220,14 @@ require ( go.uber.org/multierr v1.11.0 // indirect go.uber.org/ratelimit v0.3.1 // indirect go.uber.org/zap v1.26.0 // indirect - golang.org/x/crypto v0.26.0 // indirect + golang.org/x/crypto v0.28.0 // indirect golang.org/x/exp v0.0.0-20240119083558-1b970713d09a // indirect golang.org/x/net v0.25.0 // indirect golang.org/x/oauth2 v0.17.0 // indirect golang.org/x/sync v0.8.0 // indirect - golang.org/x/sys v0.23.0 // indirect - golang.org/x/term v0.23.0 // indirect - golang.org/x/text v0.17.0 // indirect + golang.org/x/sys v0.26.0 // indirect + golang.org/x/term v0.25.0 // indirect + golang.org/x/text v0.19.0 // indirect golang.org/x/time v0.5.0 // indirect golang.org/x/xerrors v0.0.0-20231012003039-104605ab7028 // indirect gonum.org/v1/gonum v0.14.0 // indirect diff --git a/tests/go.sum b/tests/go.sum index 997f8b268..61207bed6 100644 --- a/tests/go.sum +++ b/tests/go.sum @@ -787,6 +787,7 @@ github.com/onflow/atree v0.8.0 h1:qg5c6J1gVDNObughpEeWm8oxqhPGdEyGrda121GM4u0= github.com/onflow/atree v0.8.0/go.mod h1:yccR+LR7xc1Jdic0mrjocbHvUD7lnVvg8/Ct1AA5zBo= github.com/onflow/cadence v1.2.1 h1:hmSsgX3rTsp2E5qTSl1JXINt8qepdRrHTwDSYqN5Nxs= github.com/onflow/cadence v1.2.1/go.mod h1:fJxxOAp1wnWDfOHT8GOc1ypsU0RR5E3z51AhG8Yf5jg= +github.com/onflow/cadence v1.2.2/go.mod h1:PYX1xLejqswtDsQzN93x/VpfSKNyjUk6hrkc/mpv7xs= github.com/onflow/crypto v0.25.2 h1:GjHunqVt+vPcdqhxxhAXiMIF3YiLX7gTuTR5O+VG2ns= github.com/onflow/crypto v0.25.2/go.mod h1:fY7eLqUdMKV8EGOw301unP8h7PvLVy8/6gVR++/g0BY= github.com/onflow/flow-core-contracts/lib/go/contracts v1.4.0 h1:R86HaOuk6vpuECZnriEUE7bw9inC2AtdSn8lL/iwQLQ= @@ -801,8 +802,11 @@ github.com/onflow/flow-ft/lib/go/templates v1.0.1 h1:FDYKAiGowABtoMNusLuRCILIZDt github.com/onflow/flow-ft/lib/go/templates v1.0.1/go.mod h1:uQ8XFqmMK2jxyBSVrmyuwdWjTEb+6zGjRYotfDJ5pAE= github.com/onflow/flow-go v0.38.0-preview.0.0.20241022154145-6a254edbec23 h1:spF44tXZ341oVDTuXzzKTQ0W6rwZFV9r2/SRVVaMReo= github.com/onflow/flow-go v0.38.0-preview.0.0.20241022154145-6a254edbec23/go.mod h1:6f7CTcguVOBINmraaWMiij5e2zu7W2mKsOmXAfvCZ2g= +github.com/onflow/flow-go v0.38.0-preview.0.0.20241121133145-f880fe7b2090 h1:MjYx6rR1L3qf0q5yHZIdExxThiPM0/BJ/63EG/QoTcA= +github.com/onflow/flow-go v0.38.0-preview.0.0.20241121133145-f880fe7b2090/go.mod h1:c4ubAQ2WIMYY/TOaBvbajROEFWv2HwhKeGOsEdLPIM0= github.com/onflow/flow-go-sdk v1.2.2 h1:F78Sq/VaExgtaQv739k06gnx2aIyLF5wVE0XwxFpmsc= github.com/onflow/flow-go-sdk v1.2.2/go.mod h1:yhQ5+Sp2xWoCQ1fuRDswawTDQ0ng0z5nTkFVH82xL7E= +github.com/onflow/flow-go-sdk v1.2.3/go.mod h1:jMaffBTlAIdutx+pBhRIigLZFIBYSDDST0Uax1rW2qo= github.com/onflow/flow-nft/lib/go/contracts v1.2.2 h1:XFERNVUDGbZ4ViZjt7P1cGD80mO1PzUJYPfdhXFsGbQ= github.com/onflow/flow-nft/lib/go/contracts v1.2.2/go.mod h1:eZ9VMMNfCq0ho6kV25xJn1kXeCfxnkhj3MwF3ed08gY= github.com/onflow/flow-nft/lib/go/templates v1.2.1 h1:SAALMZPDw9Eb9p5kSLnmnFxjyig1MLiT4JUlLp0/bSE= @@ -1149,6 +1153,7 @@ golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5y golang.org/x/crypto v0.0.0-20220722155217-630584e8d5aa/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4= golang.org/x/crypto v0.26.0 h1:RrRspgV4mU+YwB4FYnuBoKsUapNIL5cohGAmSH3azsw= golang.org/x/crypto v0.26.0/go.mod h1:GY7jblb9wI+FOo5y8/S2oY4zWP07AkOJ4+jxCqdqn54= +golang.org/x/crypto v0.28.0/go.mod h1:rmgy+3RHxRZMyY0jjAJShp2zgEdOqj2AO7U0pYmeQ7U= golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= golang.org/x/exp v0.0.0-20190306152737-a1d7652674e8/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= golang.org/x/exp v0.0.0-20190510132918-efd6b22b2522/go.mod h1:ZjyILWgesfNpC6sMxTJOJm9Kp84zZh5NQWvqDGG3Qr8= @@ -1343,11 +1348,13 @@ golang.org/x/sys v0.12.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.14.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= golang.org/x/sys v0.23.0 h1:YfKFowiIMvtgl1UERQoTPPToxltDeZfbj4H7dVUCwmM= golang.org/x/sys v0.23.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= +golang.org/x/sys v0.26.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8= golang.org/x/term v0.6.0/go.mod h1:m6U89DPEgQRMq3DNkDClhWw02AUbt2daBVO4cn4Hv9U= golang.org/x/term v0.23.0 h1:F6D4vR+EHoL9/sWAWgAR1H2DcHr4PareCbAaCo1RpuU= golang.org/x/term v0.23.0/go.mod h1:DgV24QBUrK6jhZXl+20l6UWznPlwAHm1Q1mGHtydmSk= +golang.org/x/term v0.25.0/go.mod h1:RPyXicDX+6vLxogjjRxjgD2TKtmAO6NZBsBRfrOLu7M= golang.org/x/text v0.0.0-20170915032832-14c0d48ead0c/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= golang.org/x/text v0.3.1-0.20180807135948-17ff2d5776d2/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= @@ -1359,6 +1366,7 @@ golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ= golang.org/x/text v0.3.8/go.mod h1:E6s5w1FMmriuDzIBO73fBruAKo1PCIq6d2Q6DHfQ8WQ= golang.org/x/text v0.17.0 h1:XtiM5bkSOt+ewxlOE/aE/AKEHibwj/6gvWMl9Rsh0Qc= golang.org/x/text v0.17.0/go.mod h1:BuEKDfySbSR4drPmRPG/7iBdf8hvFMuRexcpahXilzY= +golang.org/x/text v0.19.0/go.mod h1:BuEKDfySbSR4drPmRPG/7iBdf8hvFMuRexcpahXilzY= golang.org/x/time v0.0.0-20180412165947-fbb02b2291d2/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= golang.org/x/time v0.0.0-20181108054448-85acf8d2951c/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= golang.org/x/time v0.0.0-20190308202827-9d24e82272b4/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= From 3c3d9e99d842978d2db15a2f9929239cd0bcbc8a Mon Sep 17 00:00:00 2001 From: Janez Podhostnik Date: Thu, 21 Nov 2024 15:59:41 +0100 Subject: [PATCH 080/174] update emulator --- go.mod | 2 +- go.sum | 4 ++-- tests/go.mod | 4 ++-- tests/go.sum | 30 ++++++++++++------------------ 4 files changed, 17 insertions(+), 23 deletions(-) diff --git a/go.mod b/go.mod index 7392e26eb..6cce077ae 100644 --- a/go.mod +++ b/go.mod @@ -7,7 +7,7 @@ require ( github.com/goccy/go-json v0.10.2 github.com/onflow/atree v0.8.0 github.com/onflow/cadence v1.2.2 - github.com/onflow/flow-go v0.38.0-preview.0.0.20241121133145-f880fe7b2090 + github.com/onflow/flow-go v0.38.0-preview.0.0.20241121162719-f4654c80fa17 github.com/onflow/flow-go-sdk v1.2.3 github.com/onflow/go-ethereum v1.14.7 github.com/prometheus/client_golang v1.18.0 diff --git a/go.sum b/go.sum index 1b60e94a1..04bb19e8d 100644 --- a/go.sum +++ b/go.sum @@ -535,8 +535,8 @@ github.com/onflow/flow-ft/lib/go/contracts v1.0.1 h1:Ts5ob+CoCY2EjEd0W6vdLJ7hLL3 github.com/onflow/flow-ft/lib/go/contracts v1.0.1/go.mod h1:PwsL8fC81cjnUnTfmyL/HOIyHnyaw/JA474Wfj2tl6A= github.com/onflow/flow-ft/lib/go/templates v1.0.1 h1:FDYKAiGowABtoMNusLuRCILIZDtVqJ/5tYI4VkF5zfM= github.com/onflow/flow-ft/lib/go/templates v1.0.1/go.mod h1:uQ8XFqmMK2jxyBSVrmyuwdWjTEb+6zGjRYotfDJ5pAE= -github.com/onflow/flow-go v0.38.0-preview.0.0.20241121133145-f880fe7b2090 h1:MjYx6rR1L3qf0q5yHZIdExxThiPM0/BJ/63EG/QoTcA= -github.com/onflow/flow-go v0.38.0-preview.0.0.20241121133145-f880fe7b2090/go.mod h1:c4ubAQ2WIMYY/TOaBvbajROEFWv2HwhKeGOsEdLPIM0= +github.com/onflow/flow-go v0.38.0-preview.0.0.20241121162719-f4654c80fa17 h1:dQTw1hbB7OMmbVRIGwW2q0RC0dqMTxtZ7QV516cdq1c= +github.com/onflow/flow-go v0.38.0-preview.0.0.20241121162719-f4654c80fa17/go.mod h1:c4ubAQ2WIMYY/TOaBvbajROEFWv2HwhKeGOsEdLPIM0= github.com/onflow/flow-go-sdk v1.2.3 h1:jb+0dIXBO12Zt8x3c2xDXYPv6k3sRTUvhe59M+EcXTI= github.com/onflow/flow-go-sdk v1.2.3/go.mod h1:jMaffBTlAIdutx+pBhRIigLZFIBYSDDST0Uax1rW2qo= github.com/onflow/flow-nft/lib/go/contracts v1.2.2 h1:XFERNVUDGbZ4ViZjt7P1cGD80mO1PzUJYPfdhXFsGbQ= diff --git a/tests/go.mod b/tests/go.mod index a8d081e27..1becb75d3 100644 --- a/tests/go.mod +++ b/tests/go.mod @@ -6,9 +6,9 @@ require ( github.com/goccy/go-json v0.10.2 github.com/onflow/cadence v1.2.2 github.com/onflow/crypto v0.25.2 - github.com/onflow/flow-emulator v1.1.0 + github.com/onflow/flow-emulator v1.1.1-0.20241121151453-79e056b866c1 github.com/onflow/flow-evm-gateway v0.0.0-20240201154855-4d4d3d3f19c7 - github.com/onflow/flow-go v0.38.0-preview.0.0.20241121133145-f880fe7b2090 + github.com/onflow/flow-go v0.38.0-preview.0.0.20241121162719-f4654c80fa17 github.com/onflow/flow-go-sdk v1.2.3 github.com/onflow/go-ethereum v1.14.7 github.com/rs/zerolog v1.33.0 diff --git a/tests/go.sum b/tests/go.sum index 61207bed6..c263ccb2d 100644 --- a/tests/go.sum +++ b/tests/go.sum @@ -785,8 +785,7 @@ github.com/olekukonko/tablewriter v0.0.5 h1:P2Ga83D34wi1o9J6Wh1mRuqd4mF/x/lgBS7N github.com/olekukonko/tablewriter v0.0.5/go.mod h1:hPp6KlRPjbx+hW8ykQs1w3UBbZlj6HuIJcUGPhkA7kY= github.com/onflow/atree v0.8.0 h1:qg5c6J1gVDNObughpEeWm8oxqhPGdEyGrda121GM4u0= github.com/onflow/atree v0.8.0/go.mod h1:yccR+LR7xc1Jdic0mrjocbHvUD7lnVvg8/Ct1AA5zBo= -github.com/onflow/cadence v1.2.1 h1:hmSsgX3rTsp2E5qTSl1JXINt8qepdRrHTwDSYqN5Nxs= -github.com/onflow/cadence v1.2.1/go.mod h1:fJxxOAp1wnWDfOHT8GOc1ypsU0RR5E3z51AhG8Yf5jg= +github.com/onflow/cadence v1.2.2 h1:LwigF/2lPiXlwX5rFn71KeMpmW5Iu/f/JtsPLLULBCc= github.com/onflow/cadence v1.2.2/go.mod h1:PYX1xLejqswtDsQzN93x/VpfSKNyjUk6hrkc/mpv7xs= github.com/onflow/crypto v0.25.2 h1:GjHunqVt+vPcdqhxxhAXiMIF3YiLX7gTuTR5O+VG2ns= github.com/onflow/crypto v0.25.2/go.mod h1:fY7eLqUdMKV8EGOw301unP8h7PvLVy8/6gVR++/g0BY= @@ -794,18 +793,15 @@ github.com/onflow/flow-core-contracts/lib/go/contracts v1.4.0 h1:R86HaOuk6vpuECZ github.com/onflow/flow-core-contracts/lib/go/contracts v1.4.0/go.mod h1:9asTBnB6Tw2UlVVtQKyS/egYv3xr4zVlJnJ75z1dfac= github.com/onflow/flow-core-contracts/lib/go/templates v1.4.0 h1:u2DAG8pk0xFH7TwS70t1gSZ/FtIIZWMSNyiu4SeXBYg= github.com/onflow/flow-core-contracts/lib/go/templates v1.4.0/go.mod h1:pN768Al/wLRlf3bwugv9TyxniqJxMu4sxnX9eQJam64= -github.com/onflow/flow-emulator v1.1.0 h1:ocF5Pv/zrY5jb0iMxQ15NOM6EuS9Y/N+SUzREi+lKcg= -github.com/onflow/flow-emulator v1.1.0/go.mod h1:wuSY3boFgO+ZRW8PGeWcs+iwcicqkO58G1iPHqRQYDY= +github.com/onflow/flow-emulator v1.1.1-0.20241121151453-79e056b866c1 h1:+YJtWk9zNkGT9EVpnSjhK9MrC+ntIkkpTAmFrWigbio= +github.com/onflow/flow-emulator v1.1.1-0.20241121151453-79e056b866c1/go.mod h1:CzFBN1kCsmXGe3AkqNJIvYiV0t688268tORPQPvF4a4= github.com/onflow/flow-ft/lib/go/contracts v1.0.1 h1:Ts5ob+CoCY2EjEd0W6vdLJ7hLL3SsEftzXG2JlmSe24= github.com/onflow/flow-ft/lib/go/contracts v1.0.1/go.mod h1:PwsL8fC81cjnUnTfmyL/HOIyHnyaw/JA474Wfj2tl6A= github.com/onflow/flow-ft/lib/go/templates v1.0.1 h1:FDYKAiGowABtoMNusLuRCILIZDtVqJ/5tYI4VkF5zfM= github.com/onflow/flow-ft/lib/go/templates v1.0.1/go.mod h1:uQ8XFqmMK2jxyBSVrmyuwdWjTEb+6zGjRYotfDJ5pAE= -github.com/onflow/flow-go v0.38.0-preview.0.0.20241022154145-6a254edbec23 h1:spF44tXZ341oVDTuXzzKTQ0W6rwZFV9r2/SRVVaMReo= -github.com/onflow/flow-go v0.38.0-preview.0.0.20241022154145-6a254edbec23/go.mod h1:6f7CTcguVOBINmraaWMiij5e2zu7W2mKsOmXAfvCZ2g= -github.com/onflow/flow-go v0.38.0-preview.0.0.20241121133145-f880fe7b2090 h1:MjYx6rR1L3qf0q5yHZIdExxThiPM0/BJ/63EG/QoTcA= -github.com/onflow/flow-go v0.38.0-preview.0.0.20241121133145-f880fe7b2090/go.mod h1:c4ubAQ2WIMYY/TOaBvbajROEFWv2HwhKeGOsEdLPIM0= -github.com/onflow/flow-go-sdk v1.2.2 h1:F78Sq/VaExgtaQv739k06gnx2aIyLF5wVE0XwxFpmsc= -github.com/onflow/flow-go-sdk v1.2.2/go.mod h1:yhQ5+Sp2xWoCQ1fuRDswawTDQ0ng0z5nTkFVH82xL7E= +github.com/onflow/flow-go v0.38.0-preview.0.0.20241121162719-f4654c80fa17 h1:dQTw1hbB7OMmbVRIGwW2q0RC0dqMTxtZ7QV516cdq1c= +github.com/onflow/flow-go v0.38.0-preview.0.0.20241121162719-f4654c80fa17/go.mod h1:c4ubAQ2WIMYY/TOaBvbajROEFWv2HwhKeGOsEdLPIM0= +github.com/onflow/flow-go-sdk v1.2.3 h1:jb+0dIXBO12Zt8x3c2xDXYPv6k3sRTUvhe59M+EcXTI= github.com/onflow/flow-go-sdk v1.2.3/go.mod h1:jMaffBTlAIdutx+pBhRIigLZFIBYSDDST0Uax1rW2qo= github.com/onflow/flow-nft/lib/go/contracts v1.2.2 h1:XFERNVUDGbZ4ViZjt7P1cGD80mO1PzUJYPfdhXFsGbQ= github.com/onflow/flow-nft/lib/go/contracts v1.2.2/go.mod h1:eZ9VMMNfCq0ho6kV25xJn1kXeCfxnkhj3MwF3ed08gY= @@ -975,6 +971,8 @@ github.com/smartystreets/assertions v0.0.0-20180927180507-b2de0cb4f26d/go.mod h1 github.com/smartystreets/goconvey v1.6.4/go.mod h1:syvi0/a8iFYH4r/RixwvyeAJjdLS9QV7WQ/tjFTllLA= github.com/soheilhy/cmux v0.1.4/go.mod h1:IM3LyeVVIOuxMH7sFAkER9+bJ4dT7Ms6E4xg4kGIyLM= github.com/sony/gobreaker v0.4.1/go.mod h1:ZKptC7FHNvhBz7dN2LGjPVBz2sZJmc0/PkyDJOjmxWY= +github.com/sony/gobreaker v0.5.0 h1:dRCvqm0P490vZPmy7ppEk2qCnCieBooFJ+YoXGYB+yg= +github.com/sony/gobreaker v0.5.0/go.mod h1:ZKptC7FHNvhBz7dN2LGjPVBz2sZJmc0/PkyDJOjmxWY= github.com/spaolacci/murmur3 v0.0.0-20180118202830-f09979ecbc72/go.mod h1:JwIasOWyU6f++ZhiEuf87xNszmSA2myDM2Kzu9HwQUA= github.com/spaolacci/murmur3 v1.1.0 h1:7c1g84S4BPRrfL5Xrdp6fOJ206sU9y293DDHaoy0bLI= github.com/spaolacci/murmur3 v1.1.0/go.mod h1:JwIasOWyU6f++ZhiEuf87xNszmSA2myDM2Kzu9HwQUA= @@ -1151,8 +1149,7 @@ golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPh golang.org/x/crypto v0.0.0-20210421170649-83a5a9bb288b/go.mod h1:T9bdIzuCu7OtxOm1hfPfRQxPLYneinmdGuTeoZ9dtd4= golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc= golang.org/x/crypto v0.0.0-20220722155217-630584e8d5aa/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4= -golang.org/x/crypto v0.26.0 h1:RrRspgV4mU+YwB4FYnuBoKsUapNIL5cohGAmSH3azsw= -golang.org/x/crypto v0.26.0/go.mod h1:GY7jblb9wI+FOo5y8/S2oY4zWP07AkOJ4+jxCqdqn54= +golang.org/x/crypto v0.28.0 h1:GBDwsMXVQi34v5CCYUm2jkJvu4cbtru2U4TN2PSyQnw= golang.org/x/crypto v0.28.0/go.mod h1:rmgy+3RHxRZMyY0jjAJShp2zgEdOqj2AO7U0pYmeQ7U= golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= golang.org/x/exp v0.0.0-20190306152737-a1d7652674e8/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= @@ -1346,14 +1343,12 @@ golang.org/x/sys v0.8.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.11.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.12.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.14.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= -golang.org/x/sys v0.23.0 h1:YfKFowiIMvtgl1UERQoTPPToxltDeZfbj4H7dVUCwmM= -golang.org/x/sys v0.23.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= +golang.org/x/sys v0.26.0 h1:KHjCJyddX0LoSTb3J+vWpupP9p0oznkqVk/IfjymZbo= golang.org/x/sys v0.26.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8= golang.org/x/term v0.6.0/go.mod h1:m6U89DPEgQRMq3DNkDClhWw02AUbt2daBVO4cn4Hv9U= -golang.org/x/term v0.23.0 h1:F6D4vR+EHoL9/sWAWgAR1H2DcHr4PareCbAaCo1RpuU= -golang.org/x/term v0.23.0/go.mod h1:DgV24QBUrK6jhZXl+20l6UWznPlwAHm1Q1mGHtydmSk= +golang.org/x/term v0.25.0 h1:WtHI/ltw4NvSUig5KARz9h521QvRC8RmF/cuYqifU24= golang.org/x/term v0.25.0/go.mod h1:RPyXicDX+6vLxogjjRxjgD2TKtmAO6NZBsBRfrOLu7M= golang.org/x/text v0.0.0-20170915032832-14c0d48ead0c/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= @@ -1364,8 +1359,7 @@ golang.org/x/text v0.3.4/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ= golang.org/x/text v0.3.8/go.mod h1:E6s5w1FMmriuDzIBO73fBruAKo1PCIq6d2Q6DHfQ8WQ= -golang.org/x/text v0.17.0 h1:XtiM5bkSOt+ewxlOE/aE/AKEHibwj/6gvWMl9Rsh0Qc= -golang.org/x/text v0.17.0/go.mod h1:BuEKDfySbSR4drPmRPG/7iBdf8hvFMuRexcpahXilzY= +golang.org/x/text v0.19.0 h1:kTxAhCbGbxhK0IwgSKiMO5awPoDQ0RpfiVYBfK860YM= golang.org/x/text v0.19.0/go.mod h1:BuEKDfySbSR4drPmRPG/7iBdf8hvFMuRexcpahXilzY= golang.org/x/time v0.0.0-20180412165947-fbb02b2291d2/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= golang.org/x/time v0.0.0-20181108054448-85acf8d2951c/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= From 747a4ad5656ed11049475b68c5d2069db22beb56 Mon Sep 17 00:00:00 2001 From: Janez Podhostnik Date: Fri, 22 Nov 2024 21:35:52 +0100 Subject: [PATCH 081/174] update dependencies --- go.mod | 2 +- go.sum | 4 ++-- tests/go.mod | 4 ++-- tests/go.sum | 8 ++++---- 4 files changed, 9 insertions(+), 9 deletions(-) diff --git a/go.mod b/go.mod index 6cce077ae..c465fc877 100644 --- a/go.mod +++ b/go.mod @@ -7,7 +7,7 @@ require ( github.com/goccy/go-json v0.10.2 github.com/onflow/atree v0.8.0 github.com/onflow/cadence v1.2.2 - github.com/onflow/flow-go v0.38.0-preview.0.0.20241121162719-f4654c80fa17 + github.com/onflow/flow-go v0.38.0-preview.0.0.20241122154633-e670c37438eb github.com/onflow/flow-go-sdk v1.2.3 github.com/onflow/go-ethereum v1.14.7 github.com/prometheus/client_golang v1.18.0 diff --git a/go.sum b/go.sum index 04bb19e8d..a8620db78 100644 --- a/go.sum +++ b/go.sum @@ -535,8 +535,8 @@ github.com/onflow/flow-ft/lib/go/contracts v1.0.1 h1:Ts5ob+CoCY2EjEd0W6vdLJ7hLL3 github.com/onflow/flow-ft/lib/go/contracts v1.0.1/go.mod h1:PwsL8fC81cjnUnTfmyL/HOIyHnyaw/JA474Wfj2tl6A= github.com/onflow/flow-ft/lib/go/templates v1.0.1 h1:FDYKAiGowABtoMNusLuRCILIZDtVqJ/5tYI4VkF5zfM= github.com/onflow/flow-ft/lib/go/templates v1.0.1/go.mod h1:uQ8XFqmMK2jxyBSVrmyuwdWjTEb+6zGjRYotfDJ5pAE= -github.com/onflow/flow-go v0.38.0-preview.0.0.20241121162719-f4654c80fa17 h1:dQTw1hbB7OMmbVRIGwW2q0RC0dqMTxtZ7QV516cdq1c= -github.com/onflow/flow-go v0.38.0-preview.0.0.20241121162719-f4654c80fa17/go.mod h1:c4ubAQ2WIMYY/TOaBvbajROEFWv2HwhKeGOsEdLPIM0= +github.com/onflow/flow-go v0.38.0-preview.0.0.20241122154633-e670c37438eb h1:6yHWxaDM1uR1GJBGoqnBOIwRkTfKQ5ezII+EDvucSgk= +github.com/onflow/flow-go v0.38.0-preview.0.0.20241122154633-e670c37438eb/go.mod h1:c4ubAQ2WIMYY/TOaBvbajROEFWv2HwhKeGOsEdLPIM0= github.com/onflow/flow-go-sdk v1.2.3 h1:jb+0dIXBO12Zt8x3c2xDXYPv6k3sRTUvhe59M+EcXTI= github.com/onflow/flow-go-sdk v1.2.3/go.mod h1:jMaffBTlAIdutx+pBhRIigLZFIBYSDDST0Uax1rW2qo= github.com/onflow/flow-nft/lib/go/contracts v1.2.2 h1:XFERNVUDGbZ4ViZjt7P1cGD80mO1PzUJYPfdhXFsGbQ= diff --git a/tests/go.mod b/tests/go.mod index 1becb75d3..3f5035a0c 100644 --- a/tests/go.mod +++ b/tests/go.mod @@ -6,9 +6,9 @@ require ( github.com/goccy/go-json v0.10.2 github.com/onflow/cadence v1.2.2 github.com/onflow/crypto v0.25.2 - github.com/onflow/flow-emulator v1.1.1-0.20241121151453-79e056b866c1 + github.com/onflow/flow-emulator v1.1.1-0.20241122203445-eb64e55b4f3e github.com/onflow/flow-evm-gateway v0.0.0-20240201154855-4d4d3d3f19c7 - github.com/onflow/flow-go v0.38.0-preview.0.0.20241121162719-f4654c80fa17 + github.com/onflow/flow-go v0.38.0-preview.0.0.20241122154633-e670c37438eb github.com/onflow/flow-go-sdk v1.2.3 github.com/onflow/go-ethereum v1.14.7 github.com/rs/zerolog v1.33.0 diff --git a/tests/go.sum b/tests/go.sum index c263ccb2d..cc35fa7f8 100644 --- a/tests/go.sum +++ b/tests/go.sum @@ -793,14 +793,14 @@ github.com/onflow/flow-core-contracts/lib/go/contracts v1.4.0 h1:R86HaOuk6vpuECZ github.com/onflow/flow-core-contracts/lib/go/contracts v1.4.0/go.mod h1:9asTBnB6Tw2UlVVtQKyS/egYv3xr4zVlJnJ75z1dfac= github.com/onflow/flow-core-contracts/lib/go/templates v1.4.0 h1:u2DAG8pk0xFH7TwS70t1gSZ/FtIIZWMSNyiu4SeXBYg= github.com/onflow/flow-core-contracts/lib/go/templates v1.4.0/go.mod h1:pN768Al/wLRlf3bwugv9TyxniqJxMu4sxnX9eQJam64= -github.com/onflow/flow-emulator v1.1.1-0.20241121151453-79e056b866c1 h1:+YJtWk9zNkGT9EVpnSjhK9MrC+ntIkkpTAmFrWigbio= -github.com/onflow/flow-emulator v1.1.1-0.20241121151453-79e056b866c1/go.mod h1:CzFBN1kCsmXGe3AkqNJIvYiV0t688268tORPQPvF4a4= +github.com/onflow/flow-emulator v1.1.1-0.20241122203445-eb64e55b4f3e h1:6rhCfhZTRZqxbwuflv7esZ1KwH5Wctf7XnRxogcRhvg= +github.com/onflow/flow-emulator v1.1.1-0.20241122203445-eb64e55b4f3e/go.mod h1:q13GELf+VfwlYssOgj3rMn7zo60y+N+Hy5s9zl+Zmng= github.com/onflow/flow-ft/lib/go/contracts v1.0.1 h1:Ts5ob+CoCY2EjEd0W6vdLJ7hLL3SsEftzXG2JlmSe24= github.com/onflow/flow-ft/lib/go/contracts v1.0.1/go.mod h1:PwsL8fC81cjnUnTfmyL/HOIyHnyaw/JA474Wfj2tl6A= github.com/onflow/flow-ft/lib/go/templates v1.0.1 h1:FDYKAiGowABtoMNusLuRCILIZDtVqJ/5tYI4VkF5zfM= github.com/onflow/flow-ft/lib/go/templates v1.0.1/go.mod h1:uQ8XFqmMK2jxyBSVrmyuwdWjTEb+6zGjRYotfDJ5pAE= -github.com/onflow/flow-go v0.38.0-preview.0.0.20241121162719-f4654c80fa17 h1:dQTw1hbB7OMmbVRIGwW2q0RC0dqMTxtZ7QV516cdq1c= -github.com/onflow/flow-go v0.38.0-preview.0.0.20241121162719-f4654c80fa17/go.mod h1:c4ubAQ2WIMYY/TOaBvbajROEFWv2HwhKeGOsEdLPIM0= +github.com/onflow/flow-go v0.38.0-preview.0.0.20241122154633-e670c37438eb h1:6yHWxaDM1uR1GJBGoqnBOIwRkTfKQ5ezII+EDvucSgk= +github.com/onflow/flow-go v0.38.0-preview.0.0.20241122154633-e670c37438eb/go.mod h1:c4ubAQ2WIMYY/TOaBvbajROEFWv2HwhKeGOsEdLPIM0= github.com/onflow/flow-go-sdk v1.2.3 h1:jb+0dIXBO12Zt8x3c2xDXYPv6k3sRTUvhe59M+EcXTI= github.com/onflow/flow-go-sdk v1.2.3/go.mod h1:jMaffBTlAIdutx+pBhRIigLZFIBYSDDST0Uax1rW2qo= github.com/onflow/flow-nft/lib/go/contracts v1.2.2 h1:XFERNVUDGbZ4ViZjt7P1cGD80mO1PzUJYPfdhXFsGbQ= From 7d1c55d1d0d2dc168cc42172760a1ab5c8d8cdda Mon Sep 17 00:00:00 2001 From: Janez Podhostnik Date: Mon, 25 Nov 2024 20:34:56 +0100 Subject: [PATCH 082/174] update flow-go --- go.mod | 2 +- go.sum | 4 ++-- tests/go.mod | 2 +- tests/go.sum | 4 ++-- 4 files changed, 6 insertions(+), 6 deletions(-) diff --git a/go.mod b/go.mod index c465fc877..d03175466 100644 --- a/go.mod +++ b/go.mod @@ -7,7 +7,7 @@ require ( github.com/goccy/go-json v0.10.2 github.com/onflow/atree v0.8.0 github.com/onflow/cadence v1.2.2 - github.com/onflow/flow-go v0.38.0-preview.0.0.20241122154633-e670c37438eb + github.com/onflow/flow-go v0.38.0-preview.0.0.20241125190444-25a8af57bea1 github.com/onflow/flow-go-sdk v1.2.3 github.com/onflow/go-ethereum v1.14.7 github.com/prometheus/client_golang v1.18.0 diff --git a/go.sum b/go.sum index a8620db78..50db1a3a0 100644 --- a/go.sum +++ b/go.sum @@ -535,8 +535,8 @@ github.com/onflow/flow-ft/lib/go/contracts v1.0.1 h1:Ts5ob+CoCY2EjEd0W6vdLJ7hLL3 github.com/onflow/flow-ft/lib/go/contracts v1.0.1/go.mod h1:PwsL8fC81cjnUnTfmyL/HOIyHnyaw/JA474Wfj2tl6A= github.com/onflow/flow-ft/lib/go/templates v1.0.1 h1:FDYKAiGowABtoMNusLuRCILIZDtVqJ/5tYI4VkF5zfM= github.com/onflow/flow-ft/lib/go/templates v1.0.1/go.mod h1:uQ8XFqmMK2jxyBSVrmyuwdWjTEb+6zGjRYotfDJ5pAE= -github.com/onflow/flow-go v0.38.0-preview.0.0.20241122154633-e670c37438eb h1:6yHWxaDM1uR1GJBGoqnBOIwRkTfKQ5ezII+EDvucSgk= -github.com/onflow/flow-go v0.38.0-preview.0.0.20241122154633-e670c37438eb/go.mod h1:c4ubAQ2WIMYY/TOaBvbajROEFWv2HwhKeGOsEdLPIM0= +github.com/onflow/flow-go v0.38.0-preview.0.0.20241125190444-25a8af57bea1 h1:tE21Kgx2Aqll9ywbiRDfc2BVIz5g6zKdrIom9U9eTE4= +github.com/onflow/flow-go v0.38.0-preview.0.0.20241125190444-25a8af57bea1/go.mod h1:c4ubAQ2WIMYY/TOaBvbajROEFWv2HwhKeGOsEdLPIM0= github.com/onflow/flow-go-sdk v1.2.3 h1:jb+0dIXBO12Zt8x3c2xDXYPv6k3sRTUvhe59M+EcXTI= github.com/onflow/flow-go-sdk v1.2.3/go.mod h1:jMaffBTlAIdutx+pBhRIigLZFIBYSDDST0Uax1rW2qo= github.com/onflow/flow-nft/lib/go/contracts v1.2.2 h1:XFERNVUDGbZ4ViZjt7P1cGD80mO1PzUJYPfdhXFsGbQ= diff --git a/tests/go.mod b/tests/go.mod index 3f5035a0c..a40825797 100644 --- a/tests/go.mod +++ b/tests/go.mod @@ -8,7 +8,7 @@ require ( github.com/onflow/crypto v0.25.2 github.com/onflow/flow-emulator v1.1.1-0.20241122203445-eb64e55b4f3e github.com/onflow/flow-evm-gateway v0.0.0-20240201154855-4d4d3d3f19c7 - github.com/onflow/flow-go v0.38.0-preview.0.0.20241122154633-e670c37438eb + github.com/onflow/flow-go v0.38.0-preview.0.0.20241125190444-25a8af57bea1 github.com/onflow/flow-go-sdk v1.2.3 github.com/onflow/go-ethereum v1.14.7 github.com/rs/zerolog v1.33.0 diff --git a/tests/go.sum b/tests/go.sum index cc35fa7f8..ecab04e78 100644 --- a/tests/go.sum +++ b/tests/go.sum @@ -799,8 +799,8 @@ github.com/onflow/flow-ft/lib/go/contracts v1.0.1 h1:Ts5ob+CoCY2EjEd0W6vdLJ7hLL3 github.com/onflow/flow-ft/lib/go/contracts v1.0.1/go.mod h1:PwsL8fC81cjnUnTfmyL/HOIyHnyaw/JA474Wfj2tl6A= github.com/onflow/flow-ft/lib/go/templates v1.0.1 h1:FDYKAiGowABtoMNusLuRCILIZDtVqJ/5tYI4VkF5zfM= github.com/onflow/flow-ft/lib/go/templates v1.0.1/go.mod h1:uQ8XFqmMK2jxyBSVrmyuwdWjTEb+6zGjRYotfDJ5pAE= -github.com/onflow/flow-go v0.38.0-preview.0.0.20241122154633-e670c37438eb h1:6yHWxaDM1uR1GJBGoqnBOIwRkTfKQ5ezII+EDvucSgk= -github.com/onflow/flow-go v0.38.0-preview.0.0.20241122154633-e670c37438eb/go.mod h1:c4ubAQ2WIMYY/TOaBvbajROEFWv2HwhKeGOsEdLPIM0= +github.com/onflow/flow-go v0.38.0-preview.0.0.20241125190444-25a8af57bea1 h1:tE21Kgx2Aqll9ywbiRDfc2BVIz5g6zKdrIom9U9eTE4= +github.com/onflow/flow-go v0.38.0-preview.0.0.20241125190444-25a8af57bea1/go.mod h1:c4ubAQ2WIMYY/TOaBvbajROEFWv2HwhKeGOsEdLPIM0= github.com/onflow/flow-go-sdk v1.2.3 h1:jb+0dIXBO12Zt8x3c2xDXYPv6k3sRTUvhe59M+EcXTI= github.com/onflow/flow-go-sdk v1.2.3/go.mod h1:jMaffBTlAIdutx+pBhRIigLZFIBYSDDST0Uax1rW2qo= github.com/onflow/flow-nft/lib/go/contracts v1.2.2 h1:XFERNVUDGbZ4ViZjt7P1cGD80mO1PzUJYPfdhXFsGbQ= From 10dbeca8e740e3ac2e715b71aec7a17987242afa Mon Sep 17 00:00:00 2001 From: Janez Podhostnik Date: Mon, 25 Nov 2024 20:55:06 +0100 Subject: [PATCH 083/174] update emulator --- tests/go.mod | 2 +- tests/go.sum | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/tests/go.mod b/tests/go.mod index a40825797..742ebb543 100644 --- a/tests/go.mod +++ b/tests/go.mod @@ -6,7 +6,7 @@ require ( github.com/goccy/go-json v0.10.2 github.com/onflow/cadence v1.2.2 github.com/onflow/crypto v0.25.2 - github.com/onflow/flow-emulator v1.1.1-0.20241122203445-eb64e55b4f3e + github.com/onflow/flow-emulator v1.1.1-0.20241125195348-4e121ffb12af github.com/onflow/flow-evm-gateway v0.0.0-20240201154855-4d4d3d3f19c7 github.com/onflow/flow-go v0.38.0-preview.0.0.20241125190444-25a8af57bea1 github.com/onflow/flow-go-sdk v1.2.3 diff --git a/tests/go.sum b/tests/go.sum index ecab04e78..e41b13eeb 100644 --- a/tests/go.sum +++ b/tests/go.sum @@ -793,8 +793,8 @@ github.com/onflow/flow-core-contracts/lib/go/contracts v1.4.0 h1:R86HaOuk6vpuECZ github.com/onflow/flow-core-contracts/lib/go/contracts v1.4.0/go.mod h1:9asTBnB6Tw2UlVVtQKyS/egYv3xr4zVlJnJ75z1dfac= github.com/onflow/flow-core-contracts/lib/go/templates v1.4.0 h1:u2DAG8pk0xFH7TwS70t1gSZ/FtIIZWMSNyiu4SeXBYg= github.com/onflow/flow-core-contracts/lib/go/templates v1.4.0/go.mod h1:pN768Al/wLRlf3bwugv9TyxniqJxMu4sxnX9eQJam64= -github.com/onflow/flow-emulator v1.1.1-0.20241122203445-eb64e55b4f3e h1:6rhCfhZTRZqxbwuflv7esZ1KwH5Wctf7XnRxogcRhvg= -github.com/onflow/flow-emulator v1.1.1-0.20241122203445-eb64e55b4f3e/go.mod h1:q13GELf+VfwlYssOgj3rMn7zo60y+N+Hy5s9zl+Zmng= +github.com/onflow/flow-emulator v1.1.1-0.20241125195348-4e121ffb12af h1:VO9GygdHSeO6UpN8PHHOIXJ5vVG/1LZd2KqrH6elPL8= +github.com/onflow/flow-emulator v1.1.1-0.20241125195348-4e121ffb12af/go.mod h1:8cKIt/iHkdsYmOETXe2D28UC3DvOSC/zpX6bi6DcNyE= github.com/onflow/flow-ft/lib/go/contracts v1.0.1 h1:Ts5ob+CoCY2EjEd0W6vdLJ7hLL3SsEftzXG2JlmSe24= github.com/onflow/flow-ft/lib/go/contracts v1.0.1/go.mod h1:PwsL8fC81cjnUnTfmyL/HOIyHnyaw/JA474Wfj2tl6A= github.com/onflow/flow-ft/lib/go/templates v1.0.1 h1:FDYKAiGowABtoMNusLuRCILIZDtVqJ/5tYI4VkF5zfM= From 39812e54bf107701640c59eb3cfcb6d40aaf6997 Mon Sep 17 00:00:00 2001 From: Ardit Marku Date: Mon, 25 Nov 2024 19:57:48 +0200 Subject: [PATCH 084/174] Use a constant backoff retry strategy for retrieving the Flow transaction result --- services/requester/pool.go | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/services/requester/pool.go b/services/requester/pool.go index bb19551a1..9b0074efd 100644 --- a/services/requester/pool.go +++ b/services/requester/pool.go @@ -65,8 +65,7 @@ func (t *TxPool) Send( t.pool.Store(evmTx.Hash(), evmTx) defer t.pool.Delete(evmTx.Hash()) - backoff := retry.WithMaxDuration(time.Minute*3, retry.NewFibonacci(time.Millisecond*100)) - + backoff := retry.WithMaxDuration(time.Minute*1, retry.NewConstant(time.Second*1)) return retry.Do(ctx, backoff, func(ctx context.Context) error { res, err := t.client.GetTransactionResult(ctx, flowTx.ID()) if err != nil { From 537379c7dbb263ab5ead06958b890b923cce1794 Mon Sep 17 00:00:00 2001 From: Janez Podhostnik Date: Tue, 26 Nov 2024 15:52:35 +0100 Subject: [PATCH 085/174] Cleanup cmd --- bootstrap/bootstrap.go | 6 ++++-- cmd/run/cmd.go | 38 ++++++++++++++++++++++++++------------ tests/helpers.go | 4 +++- tests/integration_test.go | 12 +++++++++--- 4 files changed, 42 insertions(+), 18 deletions(-) diff --git a/bootstrap/bootstrap.go b/bootstrap/bootstrap.go index 34467fdf8..7f0c6c0bb 100644 --- a/bootstrap/bootstrap.go +++ b/bootstrap/bootstrap.go @@ -7,6 +7,8 @@ import ( "math" "time" + "github.com/onflow/flow-go/module/component" + pebbleDB "github.com/cockroachdb/pebble" "github.com/onflow/flow-go-sdk/access" @@ -557,7 +559,7 @@ func setupStorage( // Run will run complete bootstrap of the EVM gateway with all the engines. // Run is a blocking call, but it does signal readiness of the service // through a channel provided as an argument. -func Run(ctx context.Context, cfg *config.Config, ready chan struct{}) error { +func Run(ctx context.Context, cfg *config.Config, ready component.ReadyFunc) error { boot, err := New(cfg) if err != nil { return err @@ -580,7 +582,7 @@ func Run(ctx context.Context, cfg *config.Config, ready chan struct{}) error { } // mark ready - close(ready) + ready() // if context is canceled start shutdown <-ctx.Done() diff --git a/cmd/run/cmd.go b/cmd/run/cmd.go index 6b87e04ce..e8cab5b8e 100644 --- a/cmd/run/cmd.go +++ b/cmd/run/cmd.go @@ -3,6 +3,7 @@ package run import ( "context" "encoding/json" + "errors" "fmt" "math/big" "os" @@ -28,27 +29,38 @@ import ( var Cmd = &cobra.Command{ Use: "run", Short: "Runs the EVM Gateway Node", - Run: func(command *cobra.Command, _ []string) { + RunE: func(command *cobra.Command, _ []string) error { + + ctx, cancel := context.WithCancel(command.Context()) + defer cancel() + // create multi-key account + // TODO(JanezP): move to separate command if _, exists := os.LookupEnv("MULTIKEY_MODE"); exists { bootstrap.RunCreateMultiKeyAccount() - return + return nil } if err := parseConfigFromFlags(); err != nil { - log.Err(err).Msg("failed to parse flags") - os.Exit(1) + return fmt.Errorf("failed to parse flags: %w", err) } - ctx, cancel := context.WithCancel(command.Context()) done := make(chan struct{}) ready := make(chan struct{}) go func() { defer close(done) - err := bootstrap.Run(ctx, cfg, ready) - if err != nil { - log.Err(err).Msg("failed to run bootstrap") - cancel() + // In case an error happens before ready is called we need to close the ready channel + defer close(ready) + + err := bootstrap.Run( + ctx, + cfg, + func() { + close(ready) + }, + ) + if err != nil && !errors.Is(err, context.Canceled) { + log.Err(err).Msg("Gateway runtime error") } }() @@ -57,17 +69,19 @@ var Cmd = &cobra.Command{ osSig := make(chan os.Signal, 1) signal.Notify(osSig, syscall.SIGINT, syscall.SIGTERM) + // wait for gateway to exit or for a shutdown signal select { case <-osSig: log.Info().Msg("OS Signal to shutdown received, shutting down") cancel() case <-done: log.Info().Msg("done, shutting down") - cancel() } - log.Info().Msg("OS Signal to shutdown received, shutting down") - cancel() + // Wait for the gateway to completely stop + <-done + + return nil }, } diff --git a/tests/helpers.go b/tests/helpers.go index 600922f8d..6378f5c05 100644 --- a/tests/helpers.go +++ b/tests/helpers.go @@ -161,7 +161,9 @@ func servicesSetup(t *testing.T) (emulator.Emulator, func()) { bootstrapDone := make(chan struct{}) go func() { - err = bootstrap.Run(ctx, cfg, bootstrapDone) + err = bootstrap.Run(ctx, cfg, func() { + close(bootstrapDone) + }) require.NoError(t, err) }() diff --git a/tests/integration_test.go b/tests/integration_test.go index 14fa40305..af834bf1d 100644 --- a/tests/integration_test.go +++ b/tests/integration_test.go @@ -83,7 +83,9 @@ func Test_ConcurrentTransactionSubmission(t *testing.T) { ready := make(chan struct{}) go func() { - err := bootstrap.Run(ctx, cfg, ready) + err := bootstrap.Run(ctx, cfg, func() { + close(ready) + }) require.NoError(t, err) }() @@ -181,7 +183,9 @@ func Test_EthClientTest(t *testing.T) { ready := make(chan struct{}) go func() { - err := bootstrap.Run(ctx, cfg, ready) + err := bootstrap.Run(ctx, cfg, func() { + close(ready) + }) require.NoError(t, err) }() @@ -288,7 +292,9 @@ func Test_CloudKMSConcurrentTransactionSubmission(t *testing.T) { ready := make(chan struct{}) go func() { - err := bootstrap.Run(ctx, cfg, ready) + err := bootstrap.Run(ctx, cfg, func() { + close(ready) + }) require.NoError(t, err) }() From e7377420f5f2f2842e6b674a321456b0ec948e93 Mon Sep 17 00:00:00 2001 From: Ardit Marku Date: Thu, 21 Nov 2024 15:44:28 +0200 Subject: [PATCH 086/174] Rework gas estimation logic to find the lower gas limit at which a transaction executes successfully --- api/utils.go | 23 ++-- services/requester/requester.go | 127 +++++++++++++----- tests/web3js/build_evm_state_test.js | 4 +- tests/web3js/debug_traces_test.js | 16 +-- .../eth_deploy_contract_and_interact_test.js | 8 +- 5 files changed, 124 insertions(+), 54 deletions(-) diff --git a/api/utils.go b/api/utils.go index 9f39d10f0..49524dc78 100644 --- a/api/utils.go +++ b/api/utils.go @@ -136,7 +136,7 @@ func handleError[T any](err error, log zerolog.Logger, collector metrics.Collect // `EVM.dryRun` inside Cadence scripts, meaning that no state change // will occur. // This is only useful for `eth_estimateGas` and `eth_call` endpoints. -func encodeTxFromArgs(args ethTypes.TransactionArgs) (*types.LegacyTx, error) { +func encodeTxFromArgs(args ethTypes.TransactionArgs) (*types.DynamicFeeTx, error) { var data []byte if args.Data != nil { data = *args.Data @@ -156,12 +156,19 @@ func encodeTxFromArgs(args ethTypes.TransactionArgs) (*types.LegacyTx, error) { value = args.Value.ToInt() } - return &types.LegacyTx{ - Nonce: 0, - To: args.To, - Value: value, - Gas: gasLimit, - GasPrice: big.NewInt(0), - Data: data, + accessList := types.AccessList{} + if args.AccessList != nil { + accessList = *args.AccessList + } + + return &types.DynamicFeeTx{ + Nonce: 0, + To: args.To, + Value: value, + Gas: gasLimit, + Data: data, + GasTipCap: (*big.Int)(args.MaxPriorityFeePerGas), + GasFeeCap: (*big.Int)(args.MaxFeePerGas), + AccessList: accessList, }, nil } diff --git a/services/requester/requester.go b/services/requester/requester.go index 413daf270..7395f7ad2 100644 --- a/services/requester/requester.go +++ b/services/requester/requester.go @@ -49,6 +49,10 @@ const minFlowBalance = 2 const coaFundingBalance = minFlowBalance - 1 const blockGasLimit = 120_000_000 +// estimateGasErrorRatio is the amount of overestimation eth_estimateGas +// is allowed to produce in order to speed up calculations. +const estimateGasErrorRatio = 0.015 + type Requester interface { // SendRawTransaction will submit signed transaction data to the network. // The submitted EVM transaction hash is returned. @@ -62,7 +66,7 @@ type Requester interface { // Note, this function doesn't make and changes in the state/blockchain and is // useful to execute and retrieve values. Call( - tx *types.LegacyTx, + tx *types.DynamicFeeTx, from common.Address, height uint64, stateOverrides *ethTypes.StateOverride, @@ -72,7 +76,7 @@ type Requester interface { // Note, this function doesn't make any changes in the state/blockchain and is // useful to executed and retrieve the gas consumption and possible failures. EstimateGas( - tx *types.LegacyTx, + tx *types.DynamicFeeTx, from common.Address, height uint64, stateOverrides *ethTypes.StateOverride, @@ -348,7 +352,7 @@ func (e *EVM) GetStorageAt( } func (e *EVM) Call( - tx *types.LegacyTx, + tx *types.DynamicFeeTx, from common.Address, height uint64, stateOverrides *ethTypes.StateOverride, @@ -358,42 +362,109 @@ func (e *EVM) Call( return nil, err } - return result.ReturnedData, err + resultSummary := result.ResultSummary() + if resultSummary.ErrorCode != 0 { + if resultSummary.ErrorCode == evmTypes.ExecutionErrCodeExecutionReverted { + return nil, errs.NewRevertError(resultSummary.ReturnedData) + } + return nil, errs.NewFailedTransactionError(resultSummary.ErrorMessage) + } + + return result.ReturnedData, nil } func (e *EVM) EstimateGas( - tx *types.LegacyTx, + tx *types.DynamicFeeTx, from common.Address, height uint64, stateOverrides *ethTypes.StateOverride, ) (uint64, error) { + // Binary search the gas limit, as it may need to be higher than the amount used + var ( + failingGasLimit uint64 // lowest-known gas limit where tx execution fails + passingGasLimit uint64 // lowest-known gas limit where tx execution succeeds + ) + // Determine the highest gas limit that can be used during the estimation. + passingGasLimit = blockGasLimit + if tx.Gas >= gethParams.TxGas { + passingGasLimit = tx.Gas + } + tx.Gas = passingGasLimit + // We first execute the transaction at the highest allowable gas limit, + // since if this fails we can return error immediately. result, err := e.dryRunTx(tx, from, height, stateOverrides) if err != nil { return 0, err } + resultSummary := result.ResultSummary() + if resultSummary.ErrorCode != 0 { + if resultSummary.ErrorCode == evmTypes.ExecutionErrCodeExecutionReverted { + return 0, errs.NewRevertError(resultSummary.ReturnedData) + } + } - if result.Successful() { - // As mentioned in https://github.com/ethereum/EIPs/blob/master/EIPS/eip-150.md#specification - // Define "all but one 64th" of N as N - floor(N / 64). - // If a call asks for more gas than the maximum allowed amount - // (i.e. the total amount of gas remaining in the parent after subtracting - // the gas cost of the call and memory expansion), do not return an OOG error; - // instead, if a call asks for more gas than all but one 64th of the maximum - // allowed amount, call with all but one 64th of the maximum allowed amount of - // gas (this is equivalent to a version of EIP-901 plus EIP-1142). - // CREATE only provides all but one 64th of the parent gas to the child call. - result.GasConsumed = AddOne64th(result.GasConsumed) + // For almost any transaction, the gas consumed by the unconstrained execution + // above lower-bounds the gas limit required for it to succeed. One exception + // is those that explicitly check gas remaining in order to execute within a + // given limit, but we probably don't want to return the lowest possible gas + // limit for these cases anyway. + failingGasLimit = result.GasConsumed - 1 + + // There's a fairly high chance for the transaction to execute successfully + // with gasLimit set to the first execution's GasConsumed + GasRefund. + // Explicitly check that gas amount and use as a limit for the binary search. + optimisticGasLimit := (result.GasConsumed + result.GasRefund + gethParams.CallStipend) * 64 / 63 + if optimisticGasLimit < passingGasLimit { + tx.Gas = optimisticGasLimit + result, err = e.dryRunTx(tx, from, height, stateOverrides) + if err != nil { + // This should not happen under normal conditions since if we make it this far the + // transaction had run without error at least once before. + return 0, err + } + resultSummary := result.ResultSummary() + if resultSummary.ErrorCode == evmTypes.ExecutionErrCodeOutOfGas { + failingGasLimit = optimisticGasLimit + } else { + passingGasLimit = optimisticGasLimit + } + } - // Adding `gethParams.SstoreSentryGasEIP2200` is needed for this condition: - // https://github.com/onflow/go-ethereum/blob/master/core/vm/operations_acl.go#L29-L32 - result.GasConsumed += gethParams.SstoreSentryGasEIP2200 + // Binary search for the smallest gas limit that allows the tx to execute successfully. + for failingGasLimit+1 < passingGasLimit { + // It is a bit pointless to return a perfect estimation, as changing + // network conditions require the caller to bump it up anyway. Since + // wallets tend to use 20-25% bump, allowing a small approximation + // error is fine (as long as it's upwards). + if float64(passingGasLimit-failingGasLimit)/float64(passingGasLimit) < estimateGasErrorRatio { + break + } + mid := (passingGasLimit + failingGasLimit) / 2 + if mid > failingGasLimit*2 { + // Most txs don't need much higher gas limit than their gas used, and most txs don't + // require near the full block limit of gas, so the selection of where to bisect the + // range here is skewed to favor the low side. + mid = failingGasLimit * 2 + } + tx.Gas = mid + result, err = e.dryRunTx(tx, from, height, stateOverrides) + if err != nil { + return 0, err + } + resultSummary := result.ResultSummary() + if resultSummary.ErrorCode == evmTypes.ExecutionErrCodeOutOfGas { + failingGasLimit = mid + } else { + passingGasLimit = mid + } + } - // Take into account any gas refunds, which are calculated only after - // transaction execution. - result.GasConsumed += result.GasRefund + if tx.AccessList != nil { + passingGasLimit += uint64(len(tx.AccessList)) * gethParams.TxAccessListAddressGas + passingGasLimit += uint64(tx.AccessList.StorageKeys()) * gethParams.TxAccessListStorageKeyGas } - return result.GasConsumed, err + return passingGasLimit, nil } func (e *EVM) GetCode( @@ -485,7 +556,7 @@ func (e *EVM) evmToCadenceHeight(height uint64) (uint64, error) { } func (e *EVM) dryRunTx( - tx *types.LegacyTx, + tx *types.DynamicFeeTx, from common.Address, height uint64, stateOverrides *ethTypes.StateOverride, @@ -545,14 +616,6 @@ func (e *EVM) dryRunTx( return nil, err } - resultSummary := result.ResultSummary() - if resultSummary.ErrorCode != 0 { - if resultSummary.ErrorCode == evmTypes.ExecutionErrCodeExecutionReverted { - return nil, errs.NewRevertError(resultSummary.ReturnedData) - } - return nil, errs.NewFailedTransactionError(resultSummary.ErrorMessage) - } - return result, nil } diff --git a/tests/web3js/build_evm_state_test.js b/tests/web3js/build_evm_state_test.js index a9490492c..16ca3bd56 100644 --- a/tests/web3js/build_evm_state_test.js +++ b/tests/web3js/build_evm_state_test.js @@ -156,7 +156,7 @@ it('should handle a large number of EVM interactions', async () => { gas: 55_000, gasPrice: conf.minGasPrice }, 82n) - assert.equal(estimatedGas, 23823n) + assert.equal(estimatedGas, 21358n) estimatedGas = await web3.eth.estimateGas({ from: conf.eoa.address, @@ -165,7 +165,7 @@ it('should handle a large number of EVM interactions', async () => { gas: 55_000, gasPrice: conf.minGasPrice }, latest) - assert.equal(estimatedGas, 29292n) + assert.equal(estimatedGas, 26811n) // Add calls to verify correctness of eth_getCode on historical heights let code = await web3.eth.getCode(contractAddress, 82n) diff --git a/tests/web3js/debug_traces_test.js b/tests/web3js/debug_traces_test.js index da20c5a71..bbf6cca9f 100644 --- a/tests/web3js/debug_traces_test.js +++ b/tests/web3js/debug_traces_test.js @@ -35,7 +35,7 @@ it('should retrieve transaction traces', async () => { // Assert proper response for `callTracer` let txTrace = response.body.result assert.equal(txTrace.from, '0xfacf71692421039876a5bb4f10ef7a439d8ef61e') - assert.equal(txTrace.gas, '0x118e0c') + assert.equal(txTrace.gas, '0x1167ac') assert.equal(txTrace.gasUsed, '0x114010') assert.equal(txTrace.to, '0x99a64c993965f8d69f985b5171bc20065cc32fab') assert.lengthOf(txTrace.input, 9856n) @@ -92,7 +92,7 @@ it('should retrieve transaction traces', async () => { // Assert proper response for `callTracer` txTrace = response.body.result assert.equal(txTrace.from, '0xfacf71692421039876a5bb4f10ef7a439d8ef61e') - assert.equal(txTrace.gas, '0x72c3') + assert.equal(txTrace.gas, '0x697f') assert.equal(txTrace.gasUsed, '0x6827') assert.equal(txTrace.to, '0x99a64c993965f8d69f985b5171bc20065cc32fab') assert.equal( @@ -161,10 +161,10 @@ it('should retrieve transaction traces', async () => { txTraces, [ { - txHash: '0x87449feedc004c75c0e8b12d01656f2e28366c7d73b1b5336beae20aaa5033dd', + txHash: '0xc34f49f9c6b56ebd88095054e2ad42d6854ba818a9657caf3f8500161a5e4ef7', result: { from: '0xfacf71692421039876a5bb4f10ef7a439d8ef61e', - gas: '0x72c3', + gas: '0x697f', gasUsed: '0x6827', to: '0x99a64c993965f8d69f985b5171bc20065cc32fab', input: '0x6057361d0000000000000000000000000000000000000000000000000000000000000064', @@ -200,10 +200,10 @@ it('should retrieve transaction traces', async () => { txTraces, [ { - txHash: '0x87449feedc004c75c0e8b12d01656f2e28366c7d73b1b5336beae20aaa5033dd', + txHash: '0xc34f49f9c6b56ebd88095054e2ad42d6854ba818a9657caf3f8500161a5e4ef7', result: { from: '0xfacf71692421039876a5bb4f10ef7a439d8ef61e', - gas: '0x72c3', + gas: '0x697f', gasUsed: '0x6827', to: '0x99a64c993965f8d69f985b5171bc20065cc32fab', input: '0x6057361d0000000000000000000000000000000000000000000000000000000000000064', @@ -257,7 +257,7 @@ it('should retrieve transaction traces', async () => { txTrace, { from: conf.eoa.address.toLowerCase(), - gas: '0xc9c7', + gas: '0xbf57', gasUsed: '0x6147', to: contractAddress.toLowerCase(), input: '0xc550f90f', @@ -265,7 +265,7 @@ it('should retrieve transaction traces', async () => { calls: [ { from: contractAddress.toLowerCase(), - gas: '0x6948', + gas: '0x5f01', gasUsed: '0x2', to: '0x0000000000000000000000010000000000000001', input: '0x53e87d66', diff --git a/tests/web3js/eth_deploy_contract_and_interact_test.js b/tests/web3js/eth_deploy_contract_and_interact_test.js index 2e029aa19..f6f1969b0 100644 --- a/tests/web3js/eth_deploy_contract_and_interact_test.js +++ b/tests/web3js/eth_deploy_contract_and_interact_test.js @@ -221,7 +221,7 @@ it('deploy contract and interact', async () => { }, '0x1' ) - assert.equal(gasEstimate, 23977n) + assert.equal(gasEstimate, 21510n) gasEstimate = await web3.eth.estimateGas( { @@ -233,7 +233,7 @@ it('deploy contract and interact', async () => { }, 'latest' ) - assert.equal(gasEstimate, 27398n) + assert.equal(gasEstimate, 25052n) // check that `eth_call` can handle state overrides let stateOverrides = { @@ -274,7 +274,7 @@ it('deploy contract and interact', async () => { assert.isDefined(response.body) result = response.body.result - assert.equal(result, '0x72c3') + assert.equal(result, '0x697f') stateOverrides = { [contractAddress]: { @@ -295,5 +295,5 @@ it('deploy contract and interact', async () => { // setting a storage slot from a zero-value, to a non-zero value has an // increase of about 20,000 gas. Which is quite different to `0x72c3`. result = response.body.result - assert.equal(result, '0xb69a') + assert.equal(result, '0xac6d') }) From 512533d1b20ca160b33d66ca9fe92eb8f3072857 Mon Sep 17 00:00:00 2001 From: Ardit Marku Date: Tue, 26 Nov 2024 11:44:18 +0200 Subject: [PATCH 087/174] Add note for binary search algorithm used for EstimateGas --- services/requester/requester.go | 3 +++ 1 file changed, 3 insertions(+) diff --git a/services/requester/requester.go b/services/requester/requester.go index 7395f7ad2..0a0e1a486 100644 --- a/services/requester/requester.go +++ b/services/requester/requester.go @@ -379,6 +379,9 @@ func (e *EVM) EstimateGas( height uint64, stateOverrides *ethTypes.StateOverride, ) (uint64, error) { + // Note: The following algorithm, is largely inspired from + // https://github.com/onflow/go-ethereum/blob/master/eth/gasestimator/gasestimator.go#L49-L192, + // and adapted to fit our use-case. // Binary search the gas limit, as it may need to be higher than the amount used var ( failingGasLimit uint64 // lowest-known gas limit where tx execution fails From 227f57ea17b3f3075c9f65716cbc64d03ea047d5 Mon Sep 17 00:00:00 2001 From: Ardit Marku Date: Tue, 26 Nov 2024 16:20:09 +0200 Subject: [PATCH 088/174] Always return error from dry-run with highest allowed gas limit --- services/requester/requester.go | 1 + 1 file changed, 1 insertion(+) diff --git a/services/requester/requester.go b/services/requester/requester.go index 0a0e1a486..252c43c33 100644 --- a/services/requester/requester.go +++ b/services/requester/requester.go @@ -404,6 +404,7 @@ func (e *EVM) EstimateGas( if resultSummary.ErrorCode == evmTypes.ExecutionErrCodeExecutionReverted { return 0, errs.NewRevertError(resultSummary.ReturnedData) } + return 0, errs.NewFailedTransactionError(resultSummary.ErrorMessage) } // For almost any transaction, the gas consumed by the unconstrained execution From 31ecd72708d13d2d35b2a99b145752cc5b1d3edf Mon Sep 17 00:00:00 2001 From: Ardit Marku Date: Tue, 26 Nov 2024 20:23:26 +0200 Subject: [PATCH 089/174] Remove completely the create_coa.cdc transaction as it is not necessary --- Makefile | 2 - README.md | 3 - cmd/run/cmd.go | 1 - config/config.go | 3 - dev/run.sh | 5 +- services/requester/cadence/create_coa.cdc | 31 ---------- services/requester/requester.go | 24 -------- tests/helpers.go | 37 ++++++----- tests/integration_test.go | 75 +++++++++++------------ 9 files changed, 56 insertions(+), 125 deletions(-) delete mode 100644 services/requester/cadence/create_coa.cdc diff --git a/Makefile b/Makefile index 5d53182cc..14badd505 100644 --- a/Makefile +++ b/Makefile @@ -54,7 +54,6 @@ start-local: --coa-address=f8d6e0586b0a20c7 \ --coa-key=2619878f0e2ff438d17835c2a4561cb87b4d24d72d12ec34569acd0dd4af7c21 \ --wallet-api-key=2619878f0e2ff438d17835c2a4561cb87b4d24d72d12ec34569acd0dd4af7c21 \ - --coa-resource-create=true \ --gas-price=0 \ --log-writer=console \ --profiler-enabled=true \ @@ -71,7 +70,6 @@ start-local-bin: --coa-address=f8d6e0586b0a20c7 \ --coa-key=2619878f0e2ff438d17835c2a4561cb87b4d24d72d12ec34569acd0dd4af7c21 \ --wallet-api-key=2619878f0e2ff438d17835c2a4561cb87b4d24d72d12ec34569acd0dd4af7c21 \ - --coa-resource-create=true \ --gas-price=0 \ --log-writer=console \ --profiler-enabled=true \ diff --git a/README.md b/README.md index e2e5c84a9..2e1d55716 100644 --- a/README.md +++ b/README.md @@ -85,7 +85,6 @@ This will be improved soon. _In the example above we set `coa-address` value to the service account of the emulator, the same as `coa-key`. This account will by default be funded with Flow which is a requirement. For `coinbase` we can use whichever valid EVM address. It's not really useful when running locally besides collecting fees. We also allow for the -`coa-resource-create` to auto-create resources needed on start-up on the `coa` account in order to operate the gateway. `gas-price` is set at 0 so we don't have to fund EOA accounts. We can set it higher but keep in mind you will then need funded accounts for interacting with EVM._ @@ -132,7 +131,6 @@ Below is an example configuration for running against testnet, with an already c --flow-network-id=flow-testnet \ --init-cadence-height=211176670 \ --ws-enabled=true \ ---coa-resource-create=false \ --coinbase=FACF71692421039876a5BB4F10EF7A439D8ef61E \ --coa-address=0x62631c28c9fc5a91 \ --coa-key=2892fba444f1d5787739708874e3b01160671924610411ac787ac1379d420f49 \ @@ -212,7 +210,6 @@ The application can be configured using the following flags at runtime: | `coa-address` | `""` | Flow address holding COA account for submitting transactions | | `coa-key` | `""` | Private key for the COA address used for transactions | | `coa-key-file` | `""` | Path to a JSON file of COA keys for key-rotation (exclusive with `coa-key` flag) | -| `coa-resource-create` | `false` | Auto-create the COA resource if it doesn't exist in the Flow COA account | | `coa-cloud-kms-project-id` | `""` | Project ID for KMS keys (e.g. `flow-evm-gateway`) | | `coa-cloud-kms-location-id` | `""` | Location ID for KMS key ring (e.g. 'global') | | `coa-cloud-kms-key-ring-id` | `""` | Key ring ID for KMS keys (e.g. 'tx-signing') | diff --git a/cmd/run/cmd.go b/cmd/run/cmd.go index e8cab5b8e..57e9fa85c 100644 --- a/cmd/run/cmd.go +++ b/cmd/run/cmd.go @@ -280,7 +280,6 @@ func init() { Cmd.Flags().StringVar(&key, "coa-key", "", "Private key value for the COA address used for submitting transactions") Cmd.Flags().StringVar(&keyAlg, "coa-key-alg", "ECDSA_P256", "Private key algorithm for the COA private key, only effective if coa-key/coa-key-file is present. Available values (ECDSA_P256 / ECDSA_secp256k1 / BLS_BLS12_381), defaults to ECDSA_P256.") Cmd.Flags().StringVar(&keysPath, "coa-key-file", "", "File path that contains JSON array of COA keys used in key-rotation mechanism, this is exclusive with coa-key flag.") - Cmd.Flags().BoolVar(&cfg.CreateCOAResource, "coa-resource-create", false, "Auto-create the COA resource in the Flow COA account provided if one doesn't exist") Cmd.Flags().StringVar(&logLevel, "log-level", "debug", "Define verbosity of the log output ('debug', 'info', 'warn', 'error', 'fatal', 'panic')") Cmd.Flags().StringVar(&logWriter, "log-writer", "stderr", "Log writer used for output ('stderr', 'console')") Cmd.Flags().Float64Var(&cfg.StreamLimit, "stream-limit", 10, "Rate-limits the events sent to the client within one second") diff --git a/config/config.go b/config/config.go index ee3d37ac4..65304317e 100644 --- a/config/config.go +++ b/config/config.go @@ -51,9 +51,6 @@ type Config struct { COAKeys []crypto.PrivateKey // COACloudKMSKeys is a slice of all the keys and their versions that will be used in Cloud KMS key-rotation mechanism. COACloudKMSKeys []flowGoKMS.Key - // CreateCOAResource indicates if the COA resource should be auto-created on - // startup if one doesn't exist in the COA Flow address account - CreateCOAResource bool // GasPrice is a fixed gas price that will be used when submitting transactions. GasPrice *big.Int // InitCadenceHeight is used for initializing the database on a local emulator or a live network. diff --git a/dev/run.sh b/dev/run.sh index 1b61eb8cf..4b079296e 100755 --- a/dev/run.sh +++ b/dev/run.sh @@ -7,10 +7,9 @@ FLOW_NETWORK_ID=flow-emulator COINBASE=FACF71692421039876a5BB4F10EF7A439D8ef61E COA_ADDRESS=f8d6e0586b0a20c7 COA_KEY=2619878f0e2ff438d17835c2a4561cb87b4d24d72d12ec34569acd0dd4af7c21 -COA_RESOURCE_CREATE=true GAS_PRICE=0 RPC_HOST=0.0.0.0 RPC_PORT=8545 -/app/flow-evm-gateway/evm-gateway --flow-network-id=$FLOW_NETWORK_ID --coinbase=$COINBASE --coa-address=$COA_ADDRESS --coa-key=$COA_KEY --coa-resource-create=$COA_RESOURCE_CREATE --gas-price=$GAS_PRICE --rpc-host=$RPC_HOST --rpc-port=$RPC_PORT -sleep 5 \ No newline at end of file +/app/flow-evm-gateway/evm-gateway --flow-network-id=$FLOW_NETWORK_ID --coinbase=$COINBASE --coa-address=$COA_ADDRESS --coa-key=$COA_KEY --gas-price=$GAS_PRICE --rpc-host=$RPC_HOST --rpc-port=$RPC_PORT +sleep 5 diff --git a/services/requester/cadence/create_coa.cdc b/services/requester/cadence/create_coa.cdc deleted file mode 100644 index 443cb836b..000000000 --- a/services/requester/cadence/create_coa.cdc +++ /dev/null @@ -1,31 +0,0 @@ -import EVM -import FungibleToken -import FlowToken - -transaction(amount: UFix64) { - let auth: auth(Storage) &Account - - prepare(signer: auth(Storage) &Account) { - self.auth = signer - } - - execute { - // If the COA is already created & saved, there's nothing to do, just return. - if let coa = self.auth.storage.borrow<&EVM.CadenceOwnedAccount>(from: /storage/evm) { - return - } - - let vaultRef = self.auth.storage.borrow( - from: /storage/flowTokenVault - ) ?? panic("Could not borrow reference to the owner's Vault!") - let vault <- vaultRef.withdraw(amount: amount) as! @FlowToken.Vault - - let account <- EVM.createCadenceOwnedAccount() - account.deposit(from: <-vault) - - self.auth.storage.save<@EVM.CadenceOwnedAccount>( - <-account, - to: /storage/evm - ) - } -} diff --git a/services/requester/requester.go b/services/requester/requester.go index 413daf270..07f4e069b 100644 --- a/services/requester/requester.go +++ b/services/requester/requester.go @@ -38,15 +38,11 @@ var ( //go:embed cadence/run.cdc runTxScript []byte - //go:embed cadence/create_coa.cdc - createCOAScript []byte - //go:embed cadence/get_latest_evm_height.cdc getLatestEVMHeight []byte ) const minFlowBalance = 2 -const coaFundingBalance = minFlowBalance - 1 const blockGasLimit = 120_000_000 type Requester interface { @@ -124,9 +120,6 @@ func NewEVM( collector metrics.Collector, ) (*EVM, error) { logger = logger.With().Str("component", "requester").Logger() - // check that the address stores already created COA resource in the "evm" storage path. - // if it doesn't check if the auto-creation boolean is true and if so create it - // otherwise fail. COA resource is required by the EVM requester to be able to submit transactions. address := config.COAAddress acc, err := client.GetAccount(context.Background(), address) if err != nil { @@ -182,23 +175,6 @@ func NewEVM( collector: collector, } - // create COA on the account - if config.CreateCOAResource { - tx, err := evm.buildTransaction( - context.Background(), - replaceAddresses(createCOAScript, config.FlowNetworkID), - cadence.UFix64(coaFundingBalance), - ) - if err != nil { - logger.Warn().Err(err).Msg("COA resource auto-creation failure") - return nil, fmt.Errorf("COA resource auto-creation failure: %w", err) - } - if err := evm.client.SendTransaction(context.Background(), *tx); err != nil { - logger.Warn().Err(err).Msg("failed to send COA resource auto-creation transaction") - return nil, fmt.Errorf("failed to send COA resource auto-creation transaction: %w", err) - } - } - return evm, nil } diff --git a/tests/helpers.go b/tests/helpers.go index 6378f5c05..fca84085d 100644 --- a/tests/helpers.go +++ b/tests/helpers.go @@ -138,25 +138,24 @@ func servicesSetup(t *testing.T) (emulator.Emulator, func()) { // default config cfg := &config.Config{ - DatabaseDir: t.TempDir(), - AccessNodeHost: "localhost:3569", // emulator - RPCPort: 8545, - RPCHost: "127.0.0.1", - FlowNetworkID: "flow-emulator", - EVMNetworkID: evmTypes.FlowEVMPreviewNetChainID, - Coinbase: common.HexToAddress(coinbaseAddress), - COAAddress: service.Address, - COAKey: service.PrivateKey, - CreateCOAResource: false, - GasPrice: new(big.Int).SetUint64(150), - LogLevel: zerolog.DebugLevel, - LogWriter: testLogWriter(), - StreamTimeout: time.Second * 30, - StreamLimit: 10, - RateLimit: 500, - WSEnabled: true, - MetricsPort: 8443, - FilterExpiry: time.Second * 5, + DatabaseDir: t.TempDir(), + AccessNodeHost: "localhost:3569", // emulator + RPCPort: 8545, + RPCHost: "127.0.0.1", + FlowNetworkID: "flow-emulator", + EVMNetworkID: evmTypes.FlowEVMPreviewNetChainID, + Coinbase: common.HexToAddress(coinbaseAddress), + COAAddress: service.Address, + COAKey: service.PrivateKey, + GasPrice: new(big.Int).SetUint64(150), + LogLevel: zerolog.DebugLevel, + LogWriter: testLogWriter(), + StreamTimeout: time.Second * 30, + StreamLimit: 10, + RateLimit: 500, + WSEnabled: true, + MetricsPort: 8443, + FilterExpiry: time.Second * 5, } bootstrapDone := make(chan struct{}) diff --git a/tests/integration_test.go b/tests/integration_test.go index af834bf1d..3d5636940 100644 --- a/tests/integration_test.go +++ b/tests/integration_test.go @@ -60,19 +60,18 @@ func Test_ConcurrentTransactionSubmission(t *testing.T) { require.NoError(t, err) cfg := &config.Config{ - DatabaseDir: t.TempDir(), - AccessNodeHost: grpcHost, - RPCPort: 8545, - RPCHost: "127.0.0.1", - FlowNetworkID: "flow-emulator", - EVMNetworkID: types.FlowEVMPreviewNetChainID, - Coinbase: eoaTestAccount, - COAAddress: *createdAddr, - COAKeys: keys, - CreateCOAResource: true, - GasPrice: new(big.Int).SetUint64(0), - LogLevel: zerolog.DebugLevel, - LogWriter: testLogWriter(), + DatabaseDir: t.TempDir(), + AccessNodeHost: grpcHost, + RPCPort: 8545, + RPCHost: "127.0.0.1", + FlowNetworkID: "flow-emulator", + EVMNetworkID: types.FlowEVMPreviewNetChainID, + Coinbase: eoaTestAccount, + COAAddress: *createdAddr, + COAKeys: keys, + GasPrice: new(big.Int).SetUint64(0), + LogLevel: zerolog.DebugLevel, + LogWriter: testLogWriter(), } // todo change this test to use ingestion and emulator directly so we can completely remove @@ -166,19 +165,18 @@ func Test_EthClientTest(t *testing.T) { require.NoError(t, err) cfg := &config.Config{ - DatabaseDir: t.TempDir(), - AccessNodeHost: grpcHost, - RPCPort: 8545, - RPCHost: "127.0.0.1", - FlowNetworkID: "flow-emulator", - EVMNetworkID: types.FlowEVMPreviewNetChainID, - Coinbase: eoaTestAccount, - COAAddress: *createdAddr, - COAKeys: keys, - CreateCOAResource: true, - GasPrice: new(big.Int).SetUint64(150), - LogLevel: zerolog.DebugLevel, - LogWriter: testLogWriter(), + DatabaseDir: t.TempDir(), + AccessNodeHost: grpcHost, + RPCPort: 8545, + RPCHost: "127.0.0.1", + FlowNetworkID: "flow-emulator", + EVMNetworkID: types.FlowEVMPreviewNetChainID, + Coinbase: eoaTestAccount, + COAAddress: *createdAddr, + COAKeys: keys, + GasPrice: new(big.Int).SetUint64(150), + LogLevel: zerolog.DebugLevel, + LogWriter: testLogWriter(), } ready := make(chan struct{}) @@ -269,19 +267,18 @@ func Test_CloudKMSConcurrentTransactionSubmission(t *testing.T) { require.NoError(t, err) cfg := &config.Config{ - DatabaseDir: t.TempDir(), - AccessNodeHost: grpcHost, - RPCPort: 8545, - RPCHost: "127.0.0.1", - FlowNetworkID: "flow-emulator", - EVMNetworkID: types.FlowEVMPreviewNetChainID, - Coinbase: eoaTestAccount, - COAAddress: *createdAddr, - COACloudKMSKeys: kmsKeys, - CreateCOAResource: true, - GasPrice: new(big.Int).SetUint64(0), - LogLevel: zerolog.DebugLevel, - LogWriter: testLogWriter(), + DatabaseDir: t.TempDir(), + AccessNodeHost: grpcHost, + RPCPort: 8545, + RPCHost: "127.0.0.1", + FlowNetworkID: "flow-emulator", + EVMNetworkID: types.FlowEVMPreviewNetChainID, + Coinbase: eoaTestAccount, + COAAddress: *createdAddr, + COACloudKMSKeys: kmsKeys, + GasPrice: new(big.Int).SetUint64(0), + LogLevel: zerolog.DebugLevel, + LogWriter: testLogWriter(), } // todo change this test to use ingestion and emulator directly so we can completely remove From d8fa7a2f352e867c33a15aa70bca863f825adf86 Mon Sep 17 00:00:00 2001 From: Ardit Marku Date: Wed, 27 Nov 2024 12:35:44 +0200 Subject: [PATCH 090/174] Fix race for startingBlock field of eth_syncing endpoint --- api/api.go | 11 +++-------- bootstrap/bootstrap.go | 23 +++++++++++++++-------- 2 files changed, 18 insertions(+), 16 deletions(-) diff --git a/api/api.go b/api/api.go index a8c96cc91..43972c445 100644 --- a/api/api.go +++ b/api/api.go @@ -170,13 +170,8 @@ func NewBlockChainAPI( receipts storage.ReceiptIndexer, ratelimiter limiter.Store, collector metrics.Collector, -) (*BlockChainAPI, error) { - // get the height from which the indexing resumed since the last restart, this is needed for syncing status. - indexingResumedHeight, err := blocks.LatestEVMHeight() - if err != nil { - return nil, fmt.Errorf("failed to retrieve the indexing resumed height: %w", err) - } - + indexingResumedHeight uint64, +) *BlockChainAPI { return &BlockChainAPI{ logger: logger, config: config, @@ -187,7 +182,7 @@ func NewBlockChainAPI( indexingResumedHeight: indexingResumedHeight, limiter: ratelimiter, collector: collector, - }, nil + } } // BlockNumber returns the block number of the chain head. diff --git a/bootstrap/bootstrap.go b/bootstrap/bootstrap.go index 7f0c6c0bb..7ea78f8c7 100644 --- a/bootstrap/bootstrap.go +++ b/bootstrap/bootstrap.go @@ -244,7 +244,14 @@ func (b *Bootstrap) StartAPIServer(ctx context.Context) error { return fmt.Errorf("failed to create rate limiter: %w", err) } - blockchainAPI, err := api.NewBlockChainAPI( + // get the height from which the indexing resumed since the last restart, + // this is needed for the `eth_syncing` endpoint. + indexingResumedHeight, err := b.storages.Blocks.LatestEVMHeight() + if err != nil { + return fmt.Errorf("failed to retrieve the indexing resumed height: %w", err) + } + + blockchainAPI := api.NewBlockChainAPI( b.logger, b.config, evm, @@ -253,10 +260,8 @@ func (b *Bootstrap) StartAPIServer(ctx context.Context) error { b.storages.Receipts, ratelimiter, b.collector, + indexingResumedHeight, ) - if err != nil { - return err - } streamAPI := api.NewStreamAPI( b.logger, @@ -565,14 +570,16 @@ func Run(ctx context.Context, cfg *config.Config, ready component.ReadyFunc) err return err } - if err := boot.StartEventIngestion(ctx); err != nil { - return fmt.Errorf("failed to start event ingestion engine: %w", err) - } - + // Start the API Server first, to avoid any races with incoming + // EVM events, that might affect the starting state. if err := boot.StartAPIServer(ctx); err != nil { return fmt.Errorf("failed to start API server: %w", err) } + if err := boot.StartEventIngestion(ctx); err != nil { + return fmt.Errorf("failed to start event ingestion engine: %w", err) + } + if err := boot.StartMetricsServer(ctx); err != nil { return fmt.Errorf("failed to start metrics server: %w", err) } From 70e7f9e4d4af549f0a1584efa1364375e1988be1 Mon Sep 17 00:00:00 2001 From: Janez Podhostnik Date: Wed, 27 Nov 2024 14:40:33 +0100 Subject: [PATCH 091/174] Fix closing channel twice --- cmd/run/cmd.go | 13 +++++++++---- 1 file changed, 9 insertions(+), 4 deletions(-) diff --git a/cmd/run/cmd.go b/cmd/run/cmd.go index e8cab5b8e..f0dca36cc 100644 --- a/cmd/run/cmd.go +++ b/cmd/run/cmd.go @@ -9,6 +9,7 @@ import ( "os" "os/signal" "strings" + "sync" "syscall" "time" @@ -47,17 +48,21 @@ var Cmd = &cobra.Command{ done := make(chan struct{}) ready := make(chan struct{}) + once := sync.Once{} + closeReady := func() { + once.Do(func() { + close(ready) + }) + } go func() { defer close(done) // In case an error happens before ready is called we need to close the ready channel - defer close(ready) + defer closeReady() err := bootstrap.Run( ctx, cfg, - func() { - close(ready) - }, + closeReady, ) if err != nil && !errors.Is(err, context.Canceled) { log.Err(err).Msg("Gateway runtime error") From 565e34476fdd26b572a18cbdd53b714867d018f6 Mon Sep 17 00:00:00 2001 From: Janez Podhostnik Date: Tue, 26 Nov 2024 18:26:45 +0100 Subject: [PATCH 092/174] switch to flow-go metrics server --- bootstrap/bootstrap.go | 19 ++++++------ metrics/server.go | 68 ------------------------------------------ 2 files changed, 9 insertions(+), 78 deletions(-) delete mode 100644 metrics/server.go diff --git a/bootstrap/bootstrap.go b/bootstrap/bootstrap.go index 7f0c6c0bb..e39f5d879 100644 --- a/bootstrap/bootstrap.go +++ b/bootstrap/bootstrap.go @@ -7,16 +7,17 @@ import ( "math" "time" - "github.com/onflow/flow-go/module/component" - pebbleDB "github.com/cockroachdb/pebble" - + "github.com/onflow/flow-evm-gateway/metrics" "github.com/onflow/flow-go-sdk/access" "github.com/onflow/flow-go-sdk/access/grpc" "github.com/onflow/flow-go-sdk/crypto" "github.com/onflow/flow-go/fvm/environment" "github.com/onflow/flow-go/fvm/evm" flowGo "github.com/onflow/flow-go/model/flow" + "github.com/onflow/flow-go/module/component" + metrics2 "github.com/onflow/flow-go/module/metrics" + "github.com/onflow/flow-go/module/util" gethTypes "github.com/onflow/go-ethereum/core/types" "github.com/rs/zerolog" "github.com/sethvargo/go-limiter/memorystore" @@ -24,7 +25,6 @@ import ( "github.com/onflow/flow-evm-gateway/api" "github.com/onflow/flow-evm-gateway/config" - "github.com/onflow/flow-evm-gateway/metrics" "github.com/onflow/flow-evm-gateway/models" errs "github.com/onflow/flow-evm-gateway/models/errors" "github.com/onflow/flow-evm-gateway/services/ingestion" @@ -57,7 +57,7 @@ type Bootstrap struct { publishers *Publishers collector metrics.Collector server *api.Server - metrics *metrics.Server + metrics *metrics2.Server events *ingestion.Engine profiler *api.ProfileServer } @@ -334,15 +334,14 @@ func (b *Bootstrap) StopAPIServer() { b.server.Stop() } -func (b *Bootstrap) StartMetricsServer(_ context.Context) error { +func (b *Bootstrap) StartMetricsServer(ctx context.Context) error { b.logger.Info().Msg("bootstrap starting metrics server") - b.metrics = metrics.NewServer(b.logger, b.config.MetricsPort) - started, err := b.metrics.Start() + b.metrics = metrics2.NewServer(b.logger, uint(b.config.MetricsPort)) + err := util.WaitClosed(ctx, b.metrics.Ready()) if err != nil { return fmt.Errorf("failed to start metrics server: %w", err) } - <-started return nil } @@ -352,7 +351,7 @@ func (b *Bootstrap) StopMetricsServer() { return } b.logger.Warn().Msg("shutting down metrics server") - b.metrics.Stop() + <-b.metrics.Done() } func (b *Bootstrap) StartProfilerServer(_ context.Context) error { diff --git a/metrics/server.go b/metrics/server.go deleted file mode 100644 index d2042f8d9..000000000 --- a/metrics/server.go +++ /dev/null @@ -1,68 +0,0 @@ -package metrics - -import ( - "context" - "errors" - "fmt" - "net" - "net/http" - "time" - - "github.com/prometheus/client_golang/prometheus/promhttp" - "github.com/rs/zerolog" -) - -// endpoint where metrics are available for scraping -const endpoint = "/metrics" - -// Server is the http server that will be serving metrics requests -type Server struct { - server *http.Server - log zerolog.Logger -} - -// NewServer creates a new server that will start on the specified port, -// and responds to only the `/metrics` endpoint -func NewServer(log zerolog.Logger, port int) *Server { - log = log.With().Str("component", "metrics-server").Logger() - addr := fmt.Sprintf(":%d", port) - - mux := http.NewServeMux() - mux.Handle(endpoint, promhttp.Handler()) - - return &Server{ - server: &http.Server{Addr: addr, Handler: mux}, - log: log, - } -} - -// Start starts the server and returns a channel which is closed -// when the server is ready to serve requests. -func (s *Server) Start() (<-chan struct{}, error) { - ready := make(chan struct{}) - defer close(ready) - - listener, err := net.Listen("tcp", s.server.Addr) - if err != nil { - s.log.Err(err).Msg("error listening on address") - return nil, err - } - - go func() { - err := s.server.Serve(listener) - if err != nil && !errors.Is(err, http.ErrServerClosed) { - s.log.Err(err).Msg("error serving metrics server") - } - }() - - return ready, nil -} - -func (s *Server) Stop() { - ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second) - defer cancel() - - if err := s.server.Shutdown(ctx); err != nil { - s.log.Err(err).Msg("error shutting down metrics server") - } -} From 5499cc2df2ddcaff7df4572919877f03f215bc4b Mon Sep 17 00:00:00 2001 From: Janez Podhostnik Date: Tue, 26 Nov 2024 18:41:04 +0100 Subject: [PATCH 093/174] Seperate creating and closing pebbleDB from storage --- bootstrap/bootstrap.go | 31 ++++++----- services/ingestion/engine_test.go | 3 +- services/replayer/blocks_provider_test.go | 3 +- storage/pebble/db.go | 62 +++++++++++++++++++++ storage/pebble/storage.go | 65 ++--------------------- storage/pebble/storage_test.go | 3 +- storage/register_delta_test.go | 3 +- 7 files changed, 90 insertions(+), 80 deletions(-) create mode 100644 storage/pebble/db.go diff --git a/bootstrap/bootstrap.go b/bootstrap/bootstrap.go index e39f5d879..11795bbb6 100644 --- a/bootstrap/bootstrap.go +++ b/bootstrap/bootstrap.go @@ -60,6 +60,7 @@ type Bootstrap struct { metrics *metrics2.Server events *ingestion.Engine profiler *api.ProfileServer + db *pebbleDB.DB } func New(config *config.Config) (*Bootstrap, error) { @@ -72,7 +73,7 @@ func New(config *config.Config) (*Bootstrap, error) { return nil, err } - storages, err := setupStorage(config, client, logger) + db, storages, err := setupStorage(config, client, logger) if err != nil { return nil, err } @@ -83,6 +84,7 @@ func New(config *config.Config) (*Bootstrap, error) { Transaction: models.NewPublisher[*gethTypes.Transaction](), Logs: models.NewPublisher[[]*gethTypes.Log](), }, + db: db, storages: storages, logger: logger, config: config, @@ -387,10 +389,10 @@ func (b *Bootstrap) StopProfilerServer() { } func (b *Bootstrap) StopDB() { - if b.storages == nil || b.storages.Storage == nil { + if b.db == nil { return } - err := b.storages.Storage.Close() + err := b.db.Close() if err != nil { b.logger.Err(err).Msg("PebbleDB graceful shutdown failed") } @@ -465,12 +467,13 @@ func setupStorage( config *config.Config, client *requester.CrossSporkClient, logger zerolog.Logger, -) (*Storages, error) { +) (*pebbleDB.DB, *Storages, error) { // create pebble storage from the provided database root directory - store, err := pebble.New(config.DatabaseDir, logger) + db, err := pebble.OpenDB(config.DatabaseDir) if err != nil { - return nil, err + return nil, nil, err } + store := pebble.New(db, logger) blocks := pebble.NewBlocks(store, config.FlowNetworkID) storageAddress := evm.StorageAccountAddress(config.FlowNetworkID) @@ -480,7 +483,7 @@ func setupStorage( if config.ForceStartCadenceHeight != 0 { logger.Warn().Uint64("height", config.ForceStartCadenceHeight).Msg("force setting starting Cadence height!!!") if err := blocks.SetLatestCadenceHeight(config.ForceStartCadenceHeight, nil); err != nil { - return nil, err + return nil, nil, err } } @@ -499,12 +502,12 @@ func setupStorage( evmBlokcHeight := uint64(0) cadenceBlock, err := client.GetBlockHeaderByHeight(context.Background(), cadenceHeight) if err != nil { - return nil, fmt.Errorf("could not fetch provided cadence height, make sure it's correct: %w", err) + return nil, nil, fmt.Errorf("could not fetch provided cadence height, make sure it's correct: %w", err) } snapshot, err := registerStore.GetSnapshotAt(evmBlokcHeight) if err != nil { - return nil, fmt.Errorf("could not get register snapshot at block height %d: %w", 0, err) + return nil, nil, fmt.Errorf("could not get register snapshot at block height %d: %w", 0, err) } delta := storage.NewRegisterDelta(snapshot) @@ -515,16 +518,16 @@ func setupStorage( accountStatus.ToBytes(), ) if err != nil { - return nil, fmt.Errorf("could not set account status: %w", err) + return nil, nil, fmt.Errorf("could not set account status: %w", err) } err = registerStore.Store(delta.GetUpdates(), evmBlokcHeight, batch) if err != nil { - return nil, fmt.Errorf("could not store register updates: %w", err) + return nil, nil, fmt.Errorf("could not store register updates: %w", err) } if err := blocks.InitHeights(cadenceHeight, cadenceBlock.ID, batch); err != nil { - return nil, fmt.Errorf( + return nil, nil, fmt.Errorf( "failed to init the database for block height: %d and ID: %s, with : %w", cadenceHeight, cadenceBlock.ID, @@ -534,7 +537,7 @@ func setupStorage( err = batch.Commit(pebbleDB.Sync) if err != nil { - return nil, fmt.Errorf("could not commit register updates: %w", err) + return nil, nil, fmt.Errorf("could not commit register updates: %w", err) } logger.Info(). @@ -545,7 +548,7 @@ func setupStorage( // // TODO(JanezP): verify storage account owner is correct //} - return &Storages{ + return db, &Storages{ Storage: store, Blocks: blocks, Registers: registerStore, diff --git a/services/ingestion/engine_test.go b/services/ingestion/engine_test.go index bb00e5484..384f0da8b 100644 --- a/services/ingestion/engine_test.go +++ b/services/ingestion/engine_test.go @@ -636,8 +636,9 @@ func defaultReplayerConfig() replayer.Config { } func setupStore(t *testing.T) (*pebble.Storage, *pebble.RegisterStorage) { - store, err := pebble.New(t.TempDir(), zerolog.Nop()) + db, err := pebble.OpenDB(t.TempDir()) require.NoError(t, err) + store := pebble.New(db, zerolog.Nop()) storageAddress := evm.StorageAccountAddress(flowGo.Emulator) registerStore := pebble.NewRegisterStorage(store, storageAddress) diff --git a/services/replayer/blocks_provider_test.go b/services/replayer/blocks_provider_test.go index d318ae3d6..2fe091356 100644 --- a/services/replayer/blocks_provider_test.go +++ b/services/replayer/blocks_provider_test.go @@ -279,8 +279,9 @@ func TestGetSnapshotAt(t *testing.T) { func setupBlocksDB(t *testing.T) (*pebble.Storage, storage.BlockIndexer) { dir := t.TempDir() - db, err := pebble.New(dir, zerolog.Nop()) + pebbleDB, err := pebble.OpenDB(dir) require.NoError(t, err) + db := pebble.New(pebbleDB, zerolog.Nop()) batch := db.NewBatch() chainID := flowGo.Emulator diff --git a/storage/pebble/db.go b/storage/pebble/db.go new file mode 100644 index 000000000..5e3c0f89c --- /dev/null +++ b/storage/pebble/db.go @@ -0,0 +1,62 @@ +package pebble + +import ( + "fmt" + + "github.com/cockroachdb/pebble" + "github.com/cockroachdb/pebble/bloom" +) + +// OpenDB opens a new pebble database at the provided directory. +func OpenDB(dir string) (*pebble.DB, error) { + cache := pebble.NewCache(1 << 20) + defer cache.Unref() + + // currently pebble is only used for registers + opts := &pebble.Options{ + Cache: cache, + Comparer: NewMVCCComparer(), + FormatMajorVersion: pebble.FormatNewest, + L0CompactionThreshold: 2, + L0StopWritesThreshold: 1000, + // When the maximum number of bytes for a level is exceeded, compaction is requested. + LBaseMaxBytes: 64 << 20, // 64 MB + Levels: make([]pebble.LevelOptions, 7), + MaxOpenFiles: 16384, + // Writes are stopped when the sum of the queued memtable sizes exceeds MemTableStopWritesThreshold*MemTableSize. + MemTableSize: 64 << 20, + MemTableStopWritesThreshold: 4, + // The default is 1. + MaxConcurrentCompactions: func() int { return 4 }, + } + + for i := 0; i < len(opts.Levels); i++ { + l := &opts.Levels[i] + // The default is 4KiB (uncompressed), which is too small + // for good performance (esp. on stripped storage). + l.BlockSize = 32 << 10 // 32 KB + l.IndexBlockSize = 256 << 10 // 256 KB + + // The bloom filter speedsup our SeekPrefixGE by skipping + // sstables that do not contain the prefix + l.FilterPolicy = bloom.FilterPolicy(MinLookupKeyLen) + l.FilterType = pebble.TableFilter + + if i > 0 { + // L0 starts at 2MiB, each level is 2x the previous. + l.TargetFileSize = opts.Levels[i-1].TargetFileSize * 2 + } + l.EnsureDefaults() + } + + // Splitting sstables during flush allows increased compaction flexibility and concurrency when those + // tables are compacted to lower levels. + opts.FlushSplitBytes = opts.Levels[0].TargetFileSize + opts.EnsureDefaults() + + db, err := pebble.Open(dir, opts) + if err != nil { + return nil, fmt.Errorf("failed to open db for dir: %s, with: %w", dir, err) + } + return db, nil +} diff --git a/storage/pebble/storage.go b/storage/pebble/storage.go index eb33ad97e..94c420cf1 100644 --- a/storage/pebble/storage.go +++ b/storage/pebble/storage.go @@ -2,11 +2,8 @@ package pebble import ( "errors" - "fmt" "io" - "github.com/cockroachdb/pebble/bloom" - "github.com/cockroachdb/pebble" "github.com/rs/zerolog" @@ -18,64 +15,12 @@ type Storage struct { log zerolog.Logger } -// New creates a new storage instance using the provided dir location as the storage directory. -func New(dir string, log zerolog.Logger) (*Storage, error) { - cache := pebble.NewCache(1 << 20) - defer cache.Unref() - - log = log.With().Str("component", "storage").Logger() - - // currently pebble is only used for registers - opts := &pebble.Options{ - Cache: cache, - Comparer: NewMVCCComparer(), - FormatMajorVersion: pebble.FormatNewest, - L0CompactionThreshold: 2, - L0StopWritesThreshold: 1000, - // When the maximum number of bytes for a level is exceeded, compaction is requested. - LBaseMaxBytes: 64 << 20, // 64 MB - Levels: make([]pebble.LevelOptions, 7), - MaxOpenFiles: 16384, - // Writes are stopped when the sum of the queued memtable sizes exceeds MemTableStopWritesThreshold*MemTableSize. - MemTableSize: 64 << 20, - MemTableStopWritesThreshold: 4, - // The default is 1. - MaxConcurrentCompactions: func() int { return 4 }, - } - - for i := 0; i < len(opts.Levels); i++ { - l := &opts.Levels[i] - // The default is 4KiB (uncompressed), which is too small - // for good performance (esp. on stripped storage). - l.BlockSize = 32 << 10 // 32 KB - l.IndexBlockSize = 256 << 10 // 256 KB - - // The bloom filter speedsup our SeekPrefixGE by skipping - // sstables that do not contain the prefix - l.FilterPolicy = bloom.FilterPolicy(MinLookupKeyLen) - l.FilterType = pebble.TableFilter - - if i > 0 { - // L0 starts at 2MiB, each level is 2x the previous. - l.TargetFileSize = opts.Levels[i-1].TargetFileSize * 2 - } - l.EnsureDefaults() - } - - // Splitting sstables during flush allows increased compaction flexibility and concurrency when those - // tables are compacted to lower levels. - opts.FlushSplitBytes = opts.Levels[0].TargetFileSize - opts.EnsureDefaults() - - db, err := pebble.Open(dir, opts) - if err != nil { - return nil, fmt.Errorf("failed to open db for dir: %s, with: %w", dir, err) - } - +// New creates a new storage instance using the provided db. +func New(db *pebble.DB, log zerolog.Logger) *Storage { return &Storage{ db: db, log: log, - }, nil + } } // set key-value pair identified by key code (which act as an entity identifier). @@ -116,7 +61,3 @@ func (s *Storage) get(keyCode byte, key ...[]byte) ([]byte, error) { func (s *Storage) NewBatch() *pebble.Batch { return s.db.NewBatch() } - -func (s *Storage) Close() error { - return s.db.Close() -} diff --git a/storage/pebble/storage_test.go b/storage/pebble/storage_test.go index 5e80dd90c..a5aada0c5 100644 --- a/storage/pebble/storage_test.go +++ b/storage/pebble/storage_test.go @@ -172,8 +172,9 @@ func TestBatch(t *testing.T) { func runDB(name string, t *testing.T, f func(t *testing.T, db *Storage)) { dir := t.TempDir() - db, err := New(dir, zerolog.New(zerolog.NewTestWriter(t))) + pebbleDB, err := OpenDB(dir) require.NoError(t, err) + db := New(pebbleDB, zerolog.New(zerolog.NewTestWriter(t))) t.Run(name, func(t *testing.T) { f(t, db) diff --git a/storage/register_delta_test.go b/storage/register_delta_test.go index 10c2e501e..165d7410c 100644 --- a/storage/register_delta_test.go +++ b/storage/register_delta_test.go @@ -215,8 +215,9 @@ func Test_RegisterDeltaWithStorage(t *testing.T) { func runDB(name string, t *testing.T, f func(t *testing.T, db *pebbleStorage.Storage)) { dir := t.TempDir() - db, err := pebbleStorage.New(dir, zerolog.New(zerolog.NewTestWriter(t))) + pebbleDB, err := pebbleStorage.OpenDB(dir) require.NoError(t, err) + db := pebbleStorage.New(pebbleDB, zerolog.New(zerolog.NewTestWriter(t))) t.Run(name, func(t *testing.T) { f(t, db) From 7286060c796e920391a2fa9aa97017c34d545af9 Mon Sep 17 00:00:00 2001 From: Janez Podhostnik Date: Tue, 26 Nov 2024 19:14:09 +0100 Subject: [PATCH 094/174] Close the AN clients --- bootstrap/bootstrap.go | 11 +++++++++++ services/requester/cross-spork_client.go | 25 ++++++++++++++++++++++-- 2 files changed, 34 insertions(+), 2 deletions(-) diff --git a/bootstrap/bootstrap.go b/bootstrap/bootstrap.go index 11795bbb6..2db57e918 100644 --- a/bootstrap/bootstrap.go +++ b/bootstrap/bootstrap.go @@ -398,6 +398,16 @@ func (b *Bootstrap) StopDB() { } } +func (b *Bootstrap) StopClient() { + if b.client == nil { + return + } + err := b.client.Close() + if err != nil { + b.logger.Err(err).Msg("CrossSporkClient graceful shutdown failed") + } +} + // StartEngine starts provided engine and panics if there are startup errors. func StartEngine( ctx context.Context, @@ -593,6 +603,7 @@ func Run(ctx context.Context, cfg *config.Config, ready component.ReadyFunc) err boot.StopEventIngestion() boot.StopMetricsServer() boot.StopAPIServer() + boot.StopClient() boot.StopDB() return nil diff --git a/services/requester/cross-spork_client.go b/services/requester/cross-spork_client.go index 4025aa3c0..cddfc9297 100644 --- a/services/requester/cross-spork_client.go +++ b/services/requester/cross-spork_client.go @@ -4,6 +4,7 @@ import ( "context" "fmt" + "github.com/hashicorp/go-multierror" "github.com/onflow/cadence" errs "github.com/onflow/flow-evm-gateway/models/errors" "github.com/onflow/flow-go-sdk" @@ -34,6 +35,10 @@ func (s *sporkClient) GetEventsForHeightRange( return s.client.GetEventsForHeightRange(ctx, eventType, startHeight, endHeight) } +func (s *sporkClient) Close() error { + return s.client.Close() +} + type sporkClients []*sporkClient // addSpork will add a new spork host defined by the first and last height boundary in that spork. @@ -102,7 +107,7 @@ func (s *sporkClients) continuous() bool { // that shadows the original access Client function. type CrossSporkClient struct { logger zerolog.Logger - sporkClients *sporkClients + sporkClients sporkClients currentSporkFirstHeight uint64 access.Client } @@ -127,7 +132,7 @@ func NewCrossSporkClient( nodeRootBlockHeight = info.NodeRootBlockHeight } - clients := &sporkClients{} + clients := sporkClients{} for _, c := range pastSporks { if err := clients.add(logger, c); err != nil { return nil, err @@ -243,3 +248,19 @@ func (c *CrossSporkClient) GetEventsForHeightRange( } return client.GetEventsForHeightRange(ctx, eventType, startHeight, endHeight) } + +func (c *CrossSporkClient) Close() error { + var merr *multierror.Error + + for _, client := range c.sporkClients { + if err := client.Close(); err != nil { + merr = multierror.Append(merr, err) + } + } + err := c.Client.Close() + if err != nil { + merr = multierror.Append(merr, err) + } + + return merr.ErrorOrNil() +} From df1750c588f81e76a34b75458b9996d0749bc40a Mon Sep 17 00:00:00 2001 From: Janez Podhostnik Date: Wed, 27 Nov 2024 15:08:44 +0100 Subject: [PATCH 095/174] fix tidy --- go.mod | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/go.mod b/go.mod index d03175466..42016a609 100644 --- a/go.mod +++ b/go.mod @@ -5,6 +5,7 @@ go 1.22 require ( github.com/cockroachdb/pebble v1.1.1 github.com/goccy/go-json v0.10.2 + github.com/hashicorp/go-multierror v1.1.1 github.com/onflow/atree v0.8.0 github.com/onflow/cadence v1.2.2 github.com/onflow/flow-go v0.38.0-preview.0.0.20241125190444-25a8af57bea1 @@ -91,7 +92,6 @@ require ( github.com/gorilla/websocket v1.5.0 // indirect github.com/grpc-ecosystem/grpc-gateway/v2 v2.19.0 // indirect github.com/hashicorp/errwrap v1.1.0 // indirect - github.com/hashicorp/go-multierror v1.1.1 // indirect github.com/hashicorp/golang-lru v1.0.2 // indirect github.com/hashicorp/golang-lru/v2 v2.0.7 // indirect github.com/hashicorp/hcl v1.0.0 // indirect From 5b77d7b052d13786383ff53d8cf85fef68d8c8c6 Mon Sep 17 00:00:00 2001 From: Janez Podhostnik Date: Wed, 27 Nov 2024 15:10:25 +0100 Subject: [PATCH 096/174] Change import name --- bootstrap/bootstrap.go | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/bootstrap/bootstrap.go b/bootstrap/bootstrap.go index 2db57e918..ce468ef9f 100644 --- a/bootstrap/bootstrap.go +++ b/bootstrap/bootstrap.go @@ -16,7 +16,7 @@ import ( "github.com/onflow/flow-go/fvm/evm" flowGo "github.com/onflow/flow-go/model/flow" "github.com/onflow/flow-go/module/component" - metrics2 "github.com/onflow/flow-go/module/metrics" + flowMetrics "github.com/onflow/flow-go/module/metrics" "github.com/onflow/flow-go/module/util" gethTypes "github.com/onflow/go-ethereum/core/types" "github.com/rs/zerolog" @@ -57,7 +57,7 @@ type Bootstrap struct { publishers *Publishers collector metrics.Collector server *api.Server - metrics *metrics2.Server + metrics *flowMetrics.Server events *ingestion.Engine profiler *api.ProfileServer db *pebbleDB.DB @@ -339,7 +339,7 @@ func (b *Bootstrap) StopAPIServer() { func (b *Bootstrap) StartMetricsServer(ctx context.Context) error { b.logger.Info().Msg("bootstrap starting metrics server") - b.metrics = metrics2.NewServer(b.logger, uint(b.config.MetricsPort)) + b.metrics = flowMetrics.NewServer(b.logger, uint(b.config.MetricsPort)) err := util.WaitClosed(ctx, b.metrics.Ready()) if err != nil { return fmt.Errorf("failed to start metrics server: %w", err) From 15cbe7057cb80735165a31eed8186b5ffd24ddc5 Mon Sep 17 00:00:00 2001 From: Peter Argue <89119817+peterargue@users.noreply.github.com> Date: Thu, 28 Nov 2024 12:18:03 -0800 Subject: [PATCH 097/174] Revert "Improve gas estimation logic for `eth_estimateGas`" --- api/utils.go | 23 ++- services/requester/requester.go | 133 +++++------------- tests/web3js/build_evm_state_test.js | 4 +- tests/web3js/debug_traces_test.js | 16 +-- .../eth_deploy_contract_and_interact_test.js | 8 +- 5 files changed, 55 insertions(+), 129 deletions(-) diff --git a/api/utils.go b/api/utils.go index 49524dc78..9f39d10f0 100644 --- a/api/utils.go +++ b/api/utils.go @@ -136,7 +136,7 @@ func handleError[T any](err error, log zerolog.Logger, collector metrics.Collect // `EVM.dryRun` inside Cadence scripts, meaning that no state change // will occur. // This is only useful for `eth_estimateGas` and `eth_call` endpoints. -func encodeTxFromArgs(args ethTypes.TransactionArgs) (*types.DynamicFeeTx, error) { +func encodeTxFromArgs(args ethTypes.TransactionArgs) (*types.LegacyTx, error) { var data []byte if args.Data != nil { data = *args.Data @@ -156,19 +156,12 @@ func encodeTxFromArgs(args ethTypes.TransactionArgs) (*types.DynamicFeeTx, error value = args.Value.ToInt() } - accessList := types.AccessList{} - if args.AccessList != nil { - accessList = *args.AccessList - } - - return &types.DynamicFeeTx{ - Nonce: 0, - To: args.To, - Value: value, - Gas: gasLimit, - Data: data, - GasTipCap: (*big.Int)(args.MaxPriorityFeePerGas), - GasFeeCap: (*big.Int)(args.MaxFeePerGas), - AccessList: accessList, + return &types.LegacyTx{ + Nonce: 0, + To: args.To, + Value: value, + Gas: gasLimit, + GasPrice: big.NewInt(0), + Data: data, }, nil } diff --git a/services/requester/requester.go b/services/requester/requester.go index 7fe22a5bc..07f4e069b 100644 --- a/services/requester/requester.go +++ b/services/requester/requester.go @@ -45,10 +45,6 @@ var ( const minFlowBalance = 2 const blockGasLimit = 120_000_000 -// estimateGasErrorRatio is the amount of overestimation eth_estimateGas -// is allowed to produce in order to speed up calculations. -const estimateGasErrorRatio = 0.015 - type Requester interface { // SendRawTransaction will submit signed transaction data to the network. // The submitted EVM transaction hash is returned. @@ -62,7 +58,7 @@ type Requester interface { // Note, this function doesn't make and changes in the state/blockchain and is // useful to execute and retrieve values. Call( - tx *types.DynamicFeeTx, + tx *types.LegacyTx, from common.Address, height uint64, stateOverrides *ethTypes.StateOverride, @@ -72,7 +68,7 @@ type Requester interface { // Note, this function doesn't make any changes in the state/blockchain and is // useful to executed and retrieve the gas consumption and possible failures. EstimateGas( - tx *types.DynamicFeeTx, + tx *types.LegacyTx, from common.Address, height uint64, stateOverrides *ethTypes.StateOverride, @@ -328,7 +324,7 @@ func (e *EVM) GetStorageAt( } func (e *EVM) Call( - tx *types.DynamicFeeTx, + tx *types.LegacyTx, from common.Address, height uint64, stateOverrides *ethTypes.StateOverride, @@ -338,113 +334,42 @@ func (e *EVM) Call( return nil, err } - resultSummary := result.ResultSummary() - if resultSummary.ErrorCode != 0 { - if resultSummary.ErrorCode == evmTypes.ExecutionErrCodeExecutionReverted { - return nil, errs.NewRevertError(resultSummary.ReturnedData) - } - return nil, errs.NewFailedTransactionError(resultSummary.ErrorMessage) - } - - return result.ReturnedData, nil + return result.ReturnedData, err } func (e *EVM) EstimateGas( - tx *types.DynamicFeeTx, + tx *types.LegacyTx, from common.Address, height uint64, stateOverrides *ethTypes.StateOverride, ) (uint64, error) { - // Note: The following algorithm, is largely inspired from - // https://github.com/onflow/go-ethereum/blob/master/eth/gasestimator/gasestimator.go#L49-L192, - // and adapted to fit our use-case. - // Binary search the gas limit, as it may need to be higher than the amount used - var ( - failingGasLimit uint64 // lowest-known gas limit where tx execution fails - passingGasLimit uint64 // lowest-known gas limit where tx execution succeeds - ) - // Determine the highest gas limit that can be used during the estimation. - passingGasLimit = blockGasLimit - if tx.Gas >= gethParams.TxGas { - passingGasLimit = tx.Gas - } - tx.Gas = passingGasLimit - // We first execute the transaction at the highest allowable gas limit, - // since if this fails we can return error immediately. result, err := e.dryRunTx(tx, from, height, stateOverrides) if err != nil { return 0, err } - resultSummary := result.ResultSummary() - if resultSummary.ErrorCode != 0 { - if resultSummary.ErrorCode == evmTypes.ExecutionErrCodeExecutionReverted { - return 0, errs.NewRevertError(resultSummary.ReturnedData) - } - return 0, errs.NewFailedTransactionError(resultSummary.ErrorMessage) - } - - // For almost any transaction, the gas consumed by the unconstrained execution - // above lower-bounds the gas limit required for it to succeed. One exception - // is those that explicitly check gas remaining in order to execute within a - // given limit, but we probably don't want to return the lowest possible gas - // limit for these cases anyway. - failingGasLimit = result.GasConsumed - 1 - - // There's a fairly high chance for the transaction to execute successfully - // with gasLimit set to the first execution's GasConsumed + GasRefund. - // Explicitly check that gas amount and use as a limit for the binary search. - optimisticGasLimit := (result.GasConsumed + result.GasRefund + gethParams.CallStipend) * 64 / 63 - if optimisticGasLimit < passingGasLimit { - tx.Gas = optimisticGasLimit - result, err = e.dryRunTx(tx, from, height, stateOverrides) - if err != nil { - // This should not happen under normal conditions since if we make it this far the - // transaction had run without error at least once before. - return 0, err - } - resultSummary := result.ResultSummary() - if resultSummary.ErrorCode == evmTypes.ExecutionErrCodeOutOfGas { - failingGasLimit = optimisticGasLimit - } else { - passingGasLimit = optimisticGasLimit - } - } - // Binary search for the smallest gas limit that allows the tx to execute successfully. - for failingGasLimit+1 < passingGasLimit { - // It is a bit pointless to return a perfect estimation, as changing - // network conditions require the caller to bump it up anyway. Since - // wallets tend to use 20-25% bump, allowing a small approximation - // error is fine (as long as it's upwards). - if float64(passingGasLimit-failingGasLimit)/float64(passingGasLimit) < estimateGasErrorRatio { - break - } - mid := (passingGasLimit + failingGasLimit) / 2 - if mid > failingGasLimit*2 { - // Most txs don't need much higher gas limit than their gas used, and most txs don't - // require near the full block limit of gas, so the selection of where to bisect the - // range here is skewed to favor the low side. - mid = failingGasLimit * 2 - } - tx.Gas = mid - result, err = e.dryRunTx(tx, from, height, stateOverrides) - if err != nil { - return 0, err - } - resultSummary := result.ResultSummary() - if resultSummary.ErrorCode == evmTypes.ExecutionErrCodeOutOfGas { - failingGasLimit = mid - } else { - passingGasLimit = mid - } - } + if result.Successful() { + // As mentioned in https://github.com/ethereum/EIPs/blob/master/EIPS/eip-150.md#specification + // Define "all but one 64th" of N as N - floor(N / 64). + // If a call asks for more gas than the maximum allowed amount + // (i.e. the total amount of gas remaining in the parent after subtracting + // the gas cost of the call and memory expansion), do not return an OOG error; + // instead, if a call asks for more gas than all but one 64th of the maximum + // allowed amount, call with all but one 64th of the maximum allowed amount of + // gas (this is equivalent to a version of EIP-901 plus EIP-1142). + // CREATE only provides all but one 64th of the parent gas to the child call. + result.GasConsumed = AddOne64th(result.GasConsumed) + + // Adding `gethParams.SstoreSentryGasEIP2200` is needed for this condition: + // https://github.com/onflow/go-ethereum/blob/master/core/vm/operations_acl.go#L29-L32 + result.GasConsumed += gethParams.SstoreSentryGasEIP2200 - if tx.AccessList != nil { - passingGasLimit += uint64(len(tx.AccessList)) * gethParams.TxAccessListAddressGas - passingGasLimit += uint64(tx.AccessList.StorageKeys()) * gethParams.TxAccessListStorageKeyGas + // Take into account any gas refunds, which are calculated only after + // transaction execution. + result.GasConsumed += result.GasRefund } - return passingGasLimit, nil + return result.GasConsumed, err } func (e *EVM) GetCode( @@ -536,7 +461,7 @@ func (e *EVM) evmToCadenceHeight(height uint64) (uint64, error) { } func (e *EVM) dryRunTx( - tx *types.DynamicFeeTx, + tx *types.LegacyTx, from common.Address, height uint64, stateOverrides *ethTypes.StateOverride, @@ -596,6 +521,14 @@ func (e *EVM) dryRunTx( return nil, err } + resultSummary := result.ResultSummary() + if resultSummary.ErrorCode != 0 { + if resultSummary.ErrorCode == evmTypes.ExecutionErrCodeExecutionReverted { + return nil, errs.NewRevertError(resultSummary.ReturnedData) + } + return nil, errs.NewFailedTransactionError(resultSummary.ErrorMessage) + } + return result, nil } diff --git a/tests/web3js/build_evm_state_test.js b/tests/web3js/build_evm_state_test.js index 16ca3bd56..a9490492c 100644 --- a/tests/web3js/build_evm_state_test.js +++ b/tests/web3js/build_evm_state_test.js @@ -156,7 +156,7 @@ it('should handle a large number of EVM interactions', async () => { gas: 55_000, gasPrice: conf.minGasPrice }, 82n) - assert.equal(estimatedGas, 21358n) + assert.equal(estimatedGas, 23823n) estimatedGas = await web3.eth.estimateGas({ from: conf.eoa.address, @@ -165,7 +165,7 @@ it('should handle a large number of EVM interactions', async () => { gas: 55_000, gasPrice: conf.minGasPrice }, latest) - assert.equal(estimatedGas, 26811n) + assert.equal(estimatedGas, 29292n) // Add calls to verify correctness of eth_getCode on historical heights let code = await web3.eth.getCode(contractAddress, 82n) diff --git a/tests/web3js/debug_traces_test.js b/tests/web3js/debug_traces_test.js index bbf6cca9f..da20c5a71 100644 --- a/tests/web3js/debug_traces_test.js +++ b/tests/web3js/debug_traces_test.js @@ -35,7 +35,7 @@ it('should retrieve transaction traces', async () => { // Assert proper response for `callTracer` let txTrace = response.body.result assert.equal(txTrace.from, '0xfacf71692421039876a5bb4f10ef7a439d8ef61e') - assert.equal(txTrace.gas, '0x1167ac') + assert.equal(txTrace.gas, '0x118e0c') assert.equal(txTrace.gasUsed, '0x114010') assert.equal(txTrace.to, '0x99a64c993965f8d69f985b5171bc20065cc32fab') assert.lengthOf(txTrace.input, 9856n) @@ -92,7 +92,7 @@ it('should retrieve transaction traces', async () => { // Assert proper response for `callTracer` txTrace = response.body.result assert.equal(txTrace.from, '0xfacf71692421039876a5bb4f10ef7a439d8ef61e') - assert.equal(txTrace.gas, '0x697f') + assert.equal(txTrace.gas, '0x72c3') assert.equal(txTrace.gasUsed, '0x6827') assert.equal(txTrace.to, '0x99a64c993965f8d69f985b5171bc20065cc32fab') assert.equal( @@ -161,10 +161,10 @@ it('should retrieve transaction traces', async () => { txTraces, [ { - txHash: '0xc34f49f9c6b56ebd88095054e2ad42d6854ba818a9657caf3f8500161a5e4ef7', + txHash: '0x87449feedc004c75c0e8b12d01656f2e28366c7d73b1b5336beae20aaa5033dd', result: { from: '0xfacf71692421039876a5bb4f10ef7a439d8ef61e', - gas: '0x697f', + gas: '0x72c3', gasUsed: '0x6827', to: '0x99a64c993965f8d69f985b5171bc20065cc32fab', input: '0x6057361d0000000000000000000000000000000000000000000000000000000000000064', @@ -200,10 +200,10 @@ it('should retrieve transaction traces', async () => { txTraces, [ { - txHash: '0xc34f49f9c6b56ebd88095054e2ad42d6854ba818a9657caf3f8500161a5e4ef7', + txHash: '0x87449feedc004c75c0e8b12d01656f2e28366c7d73b1b5336beae20aaa5033dd', result: { from: '0xfacf71692421039876a5bb4f10ef7a439d8ef61e', - gas: '0x697f', + gas: '0x72c3', gasUsed: '0x6827', to: '0x99a64c993965f8d69f985b5171bc20065cc32fab', input: '0x6057361d0000000000000000000000000000000000000000000000000000000000000064', @@ -257,7 +257,7 @@ it('should retrieve transaction traces', async () => { txTrace, { from: conf.eoa.address.toLowerCase(), - gas: '0xbf57', + gas: '0xc9c7', gasUsed: '0x6147', to: contractAddress.toLowerCase(), input: '0xc550f90f', @@ -265,7 +265,7 @@ it('should retrieve transaction traces', async () => { calls: [ { from: contractAddress.toLowerCase(), - gas: '0x5f01', + gas: '0x6948', gasUsed: '0x2', to: '0x0000000000000000000000010000000000000001', input: '0x53e87d66', diff --git a/tests/web3js/eth_deploy_contract_and_interact_test.js b/tests/web3js/eth_deploy_contract_and_interact_test.js index f6f1969b0..2e029aa19 100644 --- a/tests/web3js/eth_deploy_contract_and_interact_test.js +++ b/tests/web3js/eth_deploy_contract_and_interact_test.js @@ -221,7 +221,7 @@ it('deploy contract and interact', async () => { }, '0x1' ) - assert.equal(gasEstimate, 21510n) + assert.equal(gasEstimate, 23977n) gasEstimate = await web3.eth.estimateGas( { @@ -233,7 +233,7 @@ it('deploy contract and interact', async () => { }, 'latest' ) - assert.equal(gasEstimate, 25052n) + assert.equal(gasEstimate, 27398n) // check that `eth_call` can handle state overrides let stateOverrides = { @@ -274,7 +274,7 @@ it('deploy contract and interact', async () => { assert.isDefined(response.body) result = response.body.result - assert.equal(result, '0x697f') + assert.equal(result, '0x72c3') stateOverrides = { [contractAddress]: { @@ -295,5 +295,5 @@ it('deploy contract and interact', async () => { // setting a storage slot from a zero-value, to a non-zero value has an // increase of about 20,000 gas. Which is quite different to `0x72c3`. result = response.body.result - assert.equal(result, '0xac6d') + assert.equal(result, '0xb69a') }) From 3e669d14912aef69a78b0ee28d72e18483dbfb4f Mon Sep 17 00:00:00 2001 From: Janez Podhostnik Date: Fri, 29 Nov 2024 15:09:14 +0100 Subject: [PATCH 098/174] Switch from config reference to instance --- api/api.go | 6 +++--- api/debug.go | 4 ++-- api/net.go | 4 ++-- api/pull.go | 4 ++-- api/pull_test.go | 2 +- api/server.go | 4 ++-- api/stream.go | 4 ++-- api/wallet.go | 4 ++-- bootstrap/bootstrap.go | 10 +++++----- cmd/run/cmd.go | 2 +- services/requester/requester.go | 4 ++-- tests/helpers.go | 2 +- tests/integration_test.go | 6 +++--- 13 files changed, 28 insertions(+), 28 deletions(-) diff --git a/api/api.go b/api/api.go index 43972c445..ddac3371e 100644 --- a/api/api.go +++ b/api/api.go @@ -109,7 +109,7 @@ func SupportedAPIs( pullAPI *PullAPI, debugAPI *DebugAPI, walletAPI *WalletAPI, - config *config.Config, + config config.Config, ) []rpc.API { apis := []rpc.API{{ Namespace: "eth", @@ -151,7 +151,7 @@ func SupportedAPIs( type BlockChainAPI struct { logger zerolog.Logger - config *config.Config + config config.Config evm requester.Requester blocks storage.BlockIndexer transactions storage.TransactionIndexer @@ -163,7 +163,7 @@ type BlockChainAPI struct { func NewBlockChainAPI( logger zerolog.Logger, - config *config.Config, + config config.Config, evm requester.Requester, blocks storage.BlockIndexer, transactions storage.TransactionIndexer, diff --git a/api/debug.go b/api/debug.go index 4501a1e76..962804d49 100644 --- a/api/debug.go +++ b/api/debug.go @@ -49,7 +49,7 @@ type DebugAPI struct { transactions storage.TransactionIndexer receipts storage.ReceiptIndexer client *requester.CrossSporkClient - config *config.Config + config config.Config collector metrics.Collector } @@ -60,7 +60,7 @@ func NewDebugAPI( transactions storage.TransactionIndexer, receipts storage.ReceiptIndexer, client *requester.CrossSporkClient, - config *config.Config, + config config.Config, logger zerolog.Logger, collector metrics.Collector, ) *DebugAPI { diff --git a/api/net.go b/api/net.go index 7f5023a17..a72a02ce4 100644 --- a/api/net.go +++ b/api/net.go @@ -9,10 +9,10 @@ import ( // NetAPI offers network related RPC methods type NetAPI struct { - config *config.Config + config config.Config } -func NewNetAPI(config *config.Config) *NetAPI { +func NewNetAPI(config config.Config) *NetAPI { return &NetAPI{ config: config, } diff --git a/api/pull.go b/api/pull.go index bd493ee5d..8158b5267 100644 --- a/api/pull.go +++ b/api/pull.go @@ -129,7 +129,7 @@ func newLogsFilter( type PullAPI struct { logger zerolog.Logger - config *config.Config + config config.Config blocks storage.BlockIndexer transactions storage.TransactionIndexer receipts storage.ReceiptIndexer @@ -140,7 +140,7 @@ type PullAPI struct { func NewPullAPI( logger zerolog.Logger, - config *config.Config, + config config.Config, blocks storage.BlockIndexer, transactions storage.TransactionIndexer, receipts storage.ReceiptIndexer, diff --git a/api/pull_test.go b/api/pull_test.go index a7c1c164f..ef72aea59 100644 --- a/api/pull_test.go +++ b/api/pull_test.go @@ -60,7 +60,7 @@ func TestFilterExpiryChecker(t *testing.T) { t.Run(tc.name, func(t *testing.T) { api := &PullAPI{ filters: make(map[rpc.ID]filter), - config: &config.Config{FilterExpiry: time.Millisecond * 5}, + config: config.Config{FilterExpiry: time.Millisecond * 5}, } tc.setup(api) diff --git a/api/server.go b/api/server.go index 2bfb9aa30..e1a9dc5ec 100644 --- a/api/server.go +++ b/api/server.go @@ -53,7 +53,7 @@ type Server struct { host string port int - config *config.Config + config config.Config collector metrics.Collector } @@ -66,7 +66,7 @@ const ( func NewServer( logger zerolog.Logger, collector metrics.Collector, - cfg *config.Config, + cfg config.Config, ) *Server { logger = logger.With().Str("component", "API").Logger() diff --git a/api/stream.go b/api/stream.go index bfc3b0fe0..98a8f4fba 100644 --- a/api/stream.go +++ b/api/stream.go @@ -21,7 +21,7 @@ import ( type StreamAPI struct { logger zerolog.Logger - config *config.Config + config config.Config blocks storage.BlockIndexer transactions storage.TransactionIndexer receipts storage.ReceiptIndexer @@ -32,7 +32,7 @@ type StreamAPI struct { func NewStreamAPI( logger zerolog.Logger, - config *config.Config, + config config.Config, blocks storage.BlockIndexer, transactions storage.TransactionIndexer, receipts storage.ReceiptIndexer, diff --git a/api/wallet.go b/api/wallet.go index 3629af113..8bb9901e7 100644 --- a/api/wallet.go +++ b/api/wallet.go @@ -19,10 +19,10 @@ import ( type WalletAPI struct { net *BlockChainAPI - config *config.Config + config config.Config } -func NewWalletAPI(config *config.Config, net *BlockChainAPI) *WalletAPI { +func NewWalletAPI(config config.Config, net *BlockChainAPI) *WalletAPI { return &WalletAPI{ net: net, config: config, diff --git a/bootstrap/bootstrap.go b/bootstrap/bootstrap.go index 057a622ca..f21f41af2 100644 --- a/bootstrap/bootstrap.go +++ b/bootstrap/bootstrap.go @@ -51,7 +51,7 @@ type Publishers struct { type Bootstrap struct { logger zerolog.Logger - config *config.Config + config config.Config client *requester.CrossSporkClient storages *Storages publishers *Publishers @@ -63,7 +63,7 @@ type Bootstrap struct { db *pebbleDB.DB } -func New(config *config.Config) (*Bootstrap, error) { +func New(config config.Config) (*Bootstrap, error) { logger := zerolog.New(config.LogWriter). With().Timestamp().Str("version", api.Version). Logger().Level(config.LogLevel) @@ -437,7 +437,7 @@ func StartEngine( } // setupCrossSporkClient sets up a cross-spork AN client. -func setupCrossSporkClient(config *config.Config, logger zerolog.Logger) (*requester.CrossSporkClient, error) { +func setupCrossSporkClient(config config.Config, logger zerolog.Logger) (*requester.CrossSporkClient, error) { // create access client with cross-spork capabilities currentSporkClient, err := grpc.NewClient( config.AccessNodeHost, @@ -479,7 +479,7 @@ func setupCrossSporkClient(config *config.Config, logger zerolog.Logger) (*reque // setupStorage creates storage and initializes it with configured starting cadence height // in case such a height doesn't already exist in the database. func setupStorage( - config *config.Config, + config config.Config, client *requester.CrossSporkClient, logger zerolog.Logger, ) (*pebbleDB.DB, *Storages, error) { @@ -576,7 +576,7 @@ func setupStorage( // Run will run complete bootstrap of the EVM gateway with all the engines. // Run is a blocking call, but it does signal readiness of the service // through a channel provided as an argument. -func Run(ctx context.Context, cfg *config.Config, ready component.ReadyFunc) error { +func Run(ctx context.Context, cfg config.Config, ready component.ReadyFunc) error { boot, err := New(cfg) if err != nil { return err diff --git a/cmd/run/cmd.go b/cmd/run/cmd.go index 45951a6a9..6c6819770 100644 --- a/cmd/run/cmd.go +++ b/cmd/run/cmd.go @@ -244,7 +244,7 @@ func parseConfigFromFlags() error { return nil } -var cfg = &config.Config{} +var cfg = config.Config{} var ( coinbase, gas, diff --git a/services/requester/requester.go b/services/requester/requester.go index 07f4e069b..9daa27e3e 100644 --- a/services/requester/requester.go +++ b/services/requester/requester.go @@ -94,7 +94,7 @@ type EVM struct { registerStore *pebble.RegisterStorage blocksProvider *replayer.BlocksProvider client *CrossSporkClient - config *config.Config + config config.Config signer crypto.Signer txPool *TxPool logger zerolog.Logger @@ -112,7 +112,7 @@ func NewEVM( registerStore *pebble.RegisterStorage, blocksProvider *replayer.BlocksProvider, client *CrossSporkClient, - config *config.Config, + config config.Config, signer crypto.Signer, logger zerolog.Logger, blocks storage.BlockIndexer, diff --git a/tests/helpers.go b/tests/helpers.go index fca84085d..6d2869376 100644 --- a/tests/helpers.go +++ b/tests/helpers.go @@ -137,7 +137,7 @@ func servicesSetup(t *testing.T) (emulator.Emulator, func()) { service := emu.ServiceKey() // default config - cfg := &config.Config{ + cfg := config.Config{ DatabaseDir: t.TempDir(), AccessNodeHost: "localhost:3569", // emulator RPCPort: 8545, diff --git a/tests/integration_test.go b/tests/integration_test.go index 3d5636940..f0c39158d 100644 --- a/tests/integration_test.go +++ b/tests/integration_test.go @@ -59,7 +59,7 @@ func Test_ConcurrentTransactionSubmission(t *testing.T) { ) require.NoError(t, err) - cfg := &config.Config{ + cfg := config.Config{ DatabaseDir: t.TempDir(), AccessNodeHost: grpcHost, RPCPort: 8545, @@ -164,7 +164,7 @@ func Test_EthClientTest(t *testing.T) { ) require.NoError(t, err) - cfg := &config.Config{ + cfg := config.Config{ DatabaseDir: t.TempDir(), AccessNodeHost: grpcHost, RPCPort: 8545, @@ -266,7 +266,7 @@ func Test_CloudKMSConcurrentTransactionSubmission(t *testing.T) { ) require.NoError(t, err) - cfg := &config.Config{ + cfg := config.Config{ DatabaseDir: t.TempDir(), AccessNodeHost: grpcHost, RPCPort: 8545, From 386044fa31177cbdb93be9afd41ec70b841bfdc3 Mon Sep 17 00:00:00 2001 From: Ardit Marku Date: Sun, 1 Dec 2024 11:53:35 +0200 Subject: [PATCH 099/174] Revert "Merge pull request #687 from onflow/revert-671-mpeter/improve-gas-estimation-logic" This reverts commit 2825b58d7cbe188a3759025dd14b1b3084d98377, reversing changes made to 6d11b340973182ed82b5c71df5ff4867568da6bd. --- api/utils.go | 23 +-- services/requester/requester.go | 133 +++++++++++++----- tests/web3js/build_evm_state_test.js | 4 +- tests/web3js/debug_traces_test.js | 16 +-- .../eth_deploy_contract_and_interact_test.js | 8 +- 5 files changed, 129 insertions(+), 55 deletions(-) diff --git a/api/utils.go b/api/utils.go index 9f39d10f0..49524dc78 100644 --- a/api/utils.go +++ b/api/utils.go @@ -136,7 +136,7 @@ func handleError[T any](err error, log zerolog.Logger, collector metrics.Collect // `EVM.dryRun` inside Cadence scripts, meaning that no state change // will occur. // This is only useful for `eth_estimateGas` and `eth_call` endpoints. -func encodeTxFromArgs(args ethTypes.TransactionArgs) (*types.LegacyTx, error) { +func encodeTxFromArgs(args ethTypes.TransactionArgs) (*types.DynamicFeeTx, error) { var data []byte if args.Data != nil { data = *args.Data @@ -156,12 +156,19 @@ func encodeTxFromArgs(args ethTypes.TransactionArgs) (*types.LegacyTx, error) { value = args.Value.ToInt() } - return &types.LegacyTx{ - Nonce: 0, - To: args.To, - Value: value, - Gas: gasLimit, - GasPrice: big.NewInt(0), - Data: data, + accessList := types.AccessList{} + if args.AccessList != nil { + accessList = *args.AccessList + } + + return &types.DynamicFeeTx{ + Nonce: 0, + To: args.To, + Value: value, + Gas: gasLimit, + Data: data, + GasTipCap: (*big.Int)(args.MaxPriorityFeePerGas), + GasFeeCap: (*big.Int)(args.MaxFeePerGas), + AccessList: accessList, }, nil } diff --git a/services/requester/requester.go b/services/requester/requester.go index 9daa27e3e..0d7f50ff8 100644 --- a/services/requester/requester.go +++ b/services/requester/requester.go @@ -45,6 +45,10 @@ var ( const minFlowBalance = 2 const blockGasLimit = 120_000_000 +// estimateGasErrorRatio is the amount of overestimation eth_estimateGas +// is allowed to produce in order to speed up calculations. +const estimateGasErrorRatio = 0.015 + type Requester interface { // SendRawTransaction will submit signed transaction data to the network. // The submitted EVM transaction hash is returned. @@ -58,7 +62,7 @@ type Requester interface { // Note, this function doesn't make and changes in the state/blockchain and is // useful to execute and retrieve values. Call( - tx *types.LegacyTx, + tx *types.DynamicFeeTx, from common.Address, height uint64, stateOverrides *ethTypes.StateOverride, @@ -68,7 +72,7 @@ type Requester interface { // Note, this function doesn't make any changes in the state/blockchain and is // useful to executed and retrieve the gas consumption and possible failures. EstimateGas( - tx *types.LegacyTx, + tx *types.DynamicFeeTx, from common.Address, height uint64, stateOverrides *ethTypes.StateOverride, @@ -324,7 +328,7 @@ func (e *EVM) GetStorageAt( } func (e *EVM) Call( - tx *types.LegacyTx, + tx *types.DynamicFeeTx, from common.Address, height uint64, stateOverrides *ethTypes.StateOverride, @@ -334,42 +338,113 @@ func (e *EVM) Call( return nil, err } - return result.ReturnedData, err + resultSummary := result.ResultSummary() + if resultSummary.ErrorCode != 0 { + if resultSummary.ErrorCode == evmTypes.ExecutionErrCodeExecutionReverted { + return nil, errs.NewRevertError(resultSummary.ReturnedData) + } + return nil, errs.NewFailedTransactionError(resultSummary.ErrorMessage) + } + + return result.ReturnedData, nil } func (e *EVM) EstimateGas( - tx *types.LegacyTx, + tx *types.DynamicFeeTx, from common.Address, height uint64, stateOverrides *ethTypes.StateOverride, ) (uint64, error) { + // Note: The following algorithm, is largely inspired from + // https://github.com/onflow/go-ethereum/blob/master/eth/gasestimator/gasestimator.go#L49-L192, + // and adapted to fit our use-case. + // Binary search the gas limit, as it may need to be higher than the amount used + var ( + failingGasLimit uint64 // lowest-known gas limit where tx execution fails + passingGasLimit uint64 // lowest-known gas limit where tx execution succeeds + ) + // Determine the highest gas limit that can be used during the estimation. + passingGasLimit = blockGasLimit + if tx.Gas >= gethParams.TxGas { + passingGasLimit = tx.Gas + } + tx.Gas = passingGasLimit + // We first execute the transaction at the highest allowable gas limit, + // since if this fails we can return error immediately. result, err := e.dryRunTx(tx, from, height, stateOverrides) if err != nil { return 0, err } + resultSummary := result.ResultSummary() + if resultSummary.ErrorCode != 0 { + if resultSummary.ErrorCode == evmTypes.ExecutionErrCodeExecutionReverted { + return 0, errs.NewRevertError(resultSummary.ReturnedData) + } + return 0, errs.NewFailedTransactionError(resultSummary.ErrorMessage) + } + + // For almost any transaction, the gas consumed by the unconstrained execution + // above lower-bounds the gas limit required for it to succeed. One exception + // is those that explicitly check gas remaining in order to execute within a + // given limit, but we probably don't want to return the lowest possible gas + // limit for these cases anyway. + failingGasLimit = result.GasConsumed - 1 + + // There's a fairly high chance for the transaction to execute successfully + // with gasLimit set to the first execution's GasConsumed + GasRefund. + // Explicitly check that gas amount and use as a limit for the binary search. + optimisticGasLimit := (result.GasConsumed + result.GasRefund + gethParams.CallStipend) * 64 / 63 + if optimisticGasLimit < passingGasLimit { + tx.Gas = optimisticGasLimit + result, err = e.dryRunTx(tx, from, height, stateOverrides) + if err != nil { + // This should not happen under normal conditions since if we make it this far the + // transaction had run without error at least once before. + return 0, err + } + resultSummary := result.ResultSummary() + if resultSummary.ErrorCode == evmTypes.ExecutionErrCodeOutOfGas { + failingGasLimit = optimisticGasLimit + } else { + passingGasLimit = optimisticGasLimit + } + } - if result.Successful() { - // As mentioned in https://github.com/ethereum/EIPs/blob/master/EIPS/eip-150.md#specification - // Define "all but one 64th" of N as N - floor(N / 64). - // If a call asks for more gas than the maximum allowed amount - // (i.e. the total amount of gas remaining in the parent after subtracting - // the gas cost of the call and memory expansion), do not return an OOG error; - // instead, if a call asks for more gas than all but one 64th of the maximum - // allowed amount, call with all but one 64th of the maximum allowed amount of - // gas (this is equivalent to a version of EIP-901 plus EIP-1142). - // CREATE only provides all but one 64th of the parent gas to the child call. - result.GasConsumed = AddOne64th(result.GasConsumed) - - // Adding `gethParams.SstoreSentryGasEIP2200` is needed for this condition: - // https://github.com/onflow/go-ethereum/blob/master/core/vm/operations_acl.go#L29-L32 - result.GasConsumed += gethParams.SstoreSentryGasEIP2200 + // Binary search for the smallest gas limit that allows the tx to execute successfully. + for failingGasLimit+1 < passingGasLimit { + // It is a bit pointless to return a perfect estimation, as changing + // network conditions require the caller to bump it up anyway. Since + // wallets tend to use 20-25% bump, allowing a small approximation + // error is fine (as long as it's upwards). + if float64(passingGasLimit-failingGasLimit)/float64(passingGasLimit) < estimateGasErrorRatio { + break + } + mid := (passingGasLimit + failingGasLimit) / 2 + if mid > failingGasLimit*2 { + // Most txs don't need much higher gas limit than their gas used, and most txs don't + // require near the full block limit of gas, so the selection of where to bisect the + // range here is skewed to favor the low side. + mid = failingGasLimit * 2 + } + tx.Gas = mid + result, err = e.dryRunTx(tx, from, height, stateOverrides) + if err != nil { + return 0, err + } + resultSummary := result.ResultSummary() + if resultSummary.ErrorCode == evmTypes.ExecutionErrCodeOutOfGas { + failingGasLimit = mid + } else { + passingGasLimit = mid + } + } - // Take into account any gas refunds, which are calculated only after - // transaction execution. - result.GasConsumed += result.GasRefund + if tx.AccessList != nil { + passingGasLimit += uint64(len(tx.AccessList)) * gethParams.TxAccessListAddressGas + passingGasLimit += uint64(tx.AccessList.StorageKeys()) * gethParams.TxAccessListStorageKeyGas } - return result.GasConsumed, err + return passingGasLimit, nil } func (e *EVM) GetCode( @@ -461,7 +536,7 @@ func (e *EVM) evmToCadenceHeight(height uint64) (uint64, error) { } func (e *EVM) dryRunTx( - tx *types.LegacyTx, + tx *types.DynamicFeeTx, from common.Address, height uint64, stateOverrides *ethTypes.StateOverride, @@ -521,14 +596,6 @@ func (e *EVM) dryRunTx( return nil, err } - resultSummary := result.ResultSummary() - if resultSummary.ErrorCode != 0 { - if resultSummary.ErrorCode == evmTypes.ExecutionErrCodeExecutionReverted { - return nil, errs.NewRevertError(resultSummary.ReturnedData) - } - return nil, errs.NewFailedTransactionError(resultSummary.ErrorMessage) - } - return result, nil } diff --git a/tests/web3js/build_evm_state_test.js b/tests/web3js/build_evm_state_test.js index a9490492c..16ca3bd56 100644 --- a/tests/web3js/build_evm_state_test.js +++ b/tests/web3js/build_evm_state_test.js @@ -156,7 +156,7 @@ it('should handle a large number of EVM interactions', async () => { gas: 55_000, gasPrice: conf.minGasPrice }, 82n) - assert.equal(estimatedGas, 23823n) + assert.equal(estimatedGas, 21358n) estimatedGas = await web3.eth.estimateGas({ from: conf.eoa.address, @@ -165,7 +165,7 @@ it('should handle a large number of EVM interactions', async () => { gas: 55_000, gasPrice: conf.minGasPrice }, latest) - assert.equal(estimatedGas, 29292n) + assert.equal(estimatedGas, 26811n) // Add calls to verify correctness of eth_getCode on historical heights let code = await web3.eth.getCode(contractAddress, 82n) diff --git a/tests/web3js/debug_traces_test.js b/tests/web3js/debug_traces_test.js index da20c5a71..bbf6cca9f 100644 --- a/tests/web3js/debug_traces_test.js +++ b/tests/web3js/debug_traces_test.js @@ -35,7 +35,7 @@ it('should retrieve transaction traces', async () => { // Assert proper response for `callTracer` let txTrace = response.body.result assert.equal(txTrace.from, '0xfacf71692421039876a5bb4f10ef7a439d8ef61e') - assert.equal(txTrace.gas, '0x118e0c') + assert.equal(txTrace.gas, '0x1167ac') assert.equal(txTrace.gasUsed, '0x114010') assert.equal(txTrace.to, '0x99a64c993965f8d69f985b5171bc20065cc32fab') assert.lengthOf(txTrace.input, 9856n) @@ -92,7 +92,7 @@ it('should retrieve transaction traces', async () => { // Assert proper response for `callTracer` txTrace = response.body.result assert.equal(txTrace.from, '0xfacf71692421039876a5bb4f10ef7a439d8ef61e') - assert.equal(txTrace.gas, '0x72c3') + assert.equal(txTrace.gas, '0x697f') assert.equal(txTrace.gasUsed, '0x6827') assert.equal(txTrace.to, '0x99a64c993965f8d69f985b5171bc20065cc32fab') assert.equal( @@ -161,10 +161,10 @@ it('should retrieve transaction traces', async () => { txTraces, [ { - txHash: '0x87449feedc004c75c0e8b12d01656f2e28366c7d73b1b5336beae20aaa5033dd', + txHash: '0xc34f49f9c6b56ebd88095054e2ad42d6854ba818a9657caf3f8500161a5e4ef7', result: { from: '0xfacf71692421039876a5bb4f10ef7a439d8ef61e', - gas: '0x72c3', + gas: '0x697f', gasUsed: '0x6827', to: '0x99a64c993965f8d69f985b5171bc20065cc32fab', input: '0x6057361d0000000000000000000000000000000000000000000000000000000000000064', @@ -200,10 +200,10 @@ it('should retrieve transaction traces', async () => { txTraces, [ { - txHash: '0x87449feedc004c75c0e8b12d01656f2e28366c7d73b1b5336beae20aaa5033dd', + txHash: '0xc34f49f9c6b56ebd88095054e2ad42d6854ba818a9657caf3f8500161a5e4ef7', result: { from: '0xfacf71692421039876a5bb4f10ef7a439d8ef61e', - gas: '0x72c3', + gas: '0x697f', gasUsed: '0x6827', to: '0x99a64c993965f8d69f985b5171bc20065cc32fab', input: '0x6057361d0000000000000000000000000000000000000000000000000000000000000064', @@ -257,7 +257,7 @@ it('should retrieve transaction traces', async () => { txTrace, { from: conf.eoa.address.toLowerCase(), - gas: '0xc9c7', + gas: '0xbf57', gasUsed: '0x6147', to: contractAddress.toLowerCase(), input: '0xc550f90f', @@ -265,7 +265,7 @@ it('should retrieve transaction traces', async () => { calls: [ { from: contractAddress.toLowerCase(), - gas: '0x6948', + gas: '0x5f01', gasUsed: '0x2', to: '0x0000000000000000000000010000000000000001', input: '0x53e87d66', diff --git a/tests/web3js/eth_deploy_contract_and_interact_test.js b/tests/web3js/eth_deploy_contract_and_interact_test.js index 2e029aa19..f6f1969b0 100644 --- a/tests/web3js/eth_deploy_contract_and_interact_test.js +++ b/tests/web3js/eth_deploy_contract_and_interact_test.js @@ -221,7 +221,7 @@ it('deploy contract and interact', async () => { }, '0x1' ) - assert.equal(gasEstimate, 23977n) + assert.equal(gasEstimate, 21510n) gasEstimate = await web3.eth.estimateGas( { @@ -233,7 +233,7 @@ it('deploy contract and interact', async () => { }, 'latest' ) - assert.equal(gasEstimate, 27398n) + assert.equal(gasEstimate, 25052n) // check that `eth_call` can handle state overrides let stateOverrides = { @@ -274,7 +274,7 @@ it('deploy contract and interact', async () => { assert.isDefined(response.body) result = response.body.result - assert.equal(result, '0x72c3') + assert.equal(result, '0x697f') stateOverrides = { [contractAddress]: { @@ -295,5 +295,5 @@ it('deploy contract and interact', async () => { // setting a storage slot from a zero-value, to a non-zero value has an // increase of about 20,000 gas. Which is quite different to `0x72c3`. result = response.body.result - assert.equal(result, '0xb69a') + assert.equal(result, '0xac6d') }) From 0e107bf0672642f5c600e3e43a559089660e6f4a Mon Sep 17 00:00:00 2001 From: Ardit Marku Date: Sun, 1 Dec 2024 11:54:05 +0200 Subject: [PATCH 100/174] Fix logical error in eth_estimateGas endpoint --- services/requester/requester.go | 8 +++----- 1 file changed, 3 insertions(+), 5 deletions(-) diff --git a/services/requester/requester.go b/services/requester/requester.go index 0d7f50ff8..a2da7d38a 100644 --- a/services/requester/requester.go +++ b/services/requester/requester.go @@ -370,7 +370,7 @@ func (e *EVM) EstimateGas( } tx.Gas = passingGasLimit // We first execute the transaction at the highest allowable gas limit, - // since if this fails we can return error immediately. + // since if this fails we can return the error immediately. result, err := e.dryRunTx(tx, from, height, stateOverrides) if err != nil { return 0, err @@ -402,8 +402,7 @@ func (e *EVM) EstimateGas( // transaction had run without error at least once before. return 0, err } - resultSummary := result.ResultSummary() - if resultSummary.ErrorCode == evmTypes.ExecutionErrCodeOutOfGas { + if result.Failed() { failingGasLimit = optimisticGasLimit } else { passingGasLimit = optimisticGasLimit @@ -431,8 +430,7 @@ func (e *EVM) EstimateGas( if err != nil { return 0, err } - resultSummary := result.ResultSummary() - if resultSummary.ErrorCode == evmTypes.ExecutionErrCodeOutOfGas { + if result.Failed() { failingGasLimit = mid } else { passingGasLimit = mid From c503c785d24211466a5ce17b73db7bb38806dd2a Mon Sep 17 00:00:00 2001 From: "Leo Zhang (zhangchiqing)" Date: Tue, 26 Nov 2024 14:21:00 -0800 Subject: [PATCH 101/174] add util to extract evm state --- cmd/util/main.go | 71 ++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 71 insertions(+) create mode 100644 cmd/util/main.go diff --git a/cmd/util/main.go b/cmd/util/main.go new file mode 100644 index 000000000..f777e3875 --- /dev/null +++ b/cmd/util/main.go @@ -0,0 +1,71 @@ +package main + +import ( + "flag" + "os" + + "github.com/rs/zerolog/log" + + "github.com/onflow/flow-evm-gateway/storage/pebble" + "github.com/onflow/flow-go/fvm/evm" + "github.com/onflow/flow-go/fvm/evm/emulator/state" + "github.com/onflow/flow-go/fvm/evm/offchain/storage" + flowGo "github.com/onflow/flow-go/model/flow" +) + +func main() { + var ( + height uint64 + outputDir string + registerStoreDir string + ) + + flag.Uint64Var(&height, "height", 0, "Block height for EVM state export") + flag.StringVar(&outputDir, "output", "", "Output directory for exported EVM state") + flag.StringVar(®isterStoreDir, "register-store", "", "Directory of the register store") + + flag.Parse() + + if height == 0 || outputDir == "" || registerStoreDir == "" { + log.Error().Msg("All flags (height, output, register-store) must be provided") + flag.Usage() + os.Exit(1) + } + + chainID := flowGo.Testnet + + log.Info().Msgf("exporting EVM state for height %v from registerStoreDir %v, outputDir: %v", height, registerStoreDir, outputDir) + err := ExportEVMStateForHeight(height, outputDir, registerStoreDir, chainID) + if err != nil { + log.Fatal().Err(err).Msgf("fail to export") + } + + log.Info().Msgf("successfully exported EVM state to %v", outputDir) +} + +func ExportEVMStateForHeight(height uint64, outputDir string, registerStoreDir string, chainID flowGo.ChainID) error { + store, err := pebble.New(registerStoreDir, log.Logger) + if err != nil { + return err + } + + storageAddress := evm.StorageAccountAddress(chainID) + registerStore := pebble.NewRegisterStorage(store, storageAddress) + snapshot, err := registerStore.GetSnapshotAt(height) + if err != nil { + return err + } + + ledger := storage.NewReadOnlyStorage(snapshot) + exporter, err := state.NewExporter(ledger, storageAddress) + if err != nil { + return err + } + + err = exporter.ExportGob(outputDir) + if err != nil { + return err + } + + return nil +} From e18dc70eb76357a132b164554c11725eac25bb2c Mon Sep 17 00:00:00 2001 From: "Leo Zhang (zhangchiqing)" Date: Tue, 26 Nov 2024 14:25:18 -0800 Subject: [PATCH 102/174] updat flow-go --- go.mod | 2 +- go.sum | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/go.mod b/go.mod index 42016a609..85e58586d 100644 --- a/go.mod +++ b/go.mod @@ -8,7 +8,7 @@ require ( github.com/hashicorp/go-multierror v1.1.1 github.com/onflow/atree v0.8.0 github.com/onflow/cadence v1.2.2 - github.com/onflow/flow-go v0.38.0-preview.0.0.20241125190444-25a8af57bea1 + github.com/onflow/flow-go v0.38.0-preview.0.0.4 github.com/onflow/flow-go-sdk v1.2.3 github.com/onflow/go-ethereum v1.14.7 github.com/prometheus/client_golang v1.18.0 diff --git a/go.sum b/go.sum index 50db1a3a0..8f802fc43 100644 --- a/go.sum +++ b/go.sum @@ -535,8 +535,8 @@ github.com/onflow/flow-ft/lib/go/contracts v1.0.1 h1:Ts5ob+CoCY2EjEd0W6vdLJ7hLL3 github.com/onflow/flow-ft/lib/go/contracts v1.0.1/go.mod h1:PwsL8fC81cjnUnTfmyL/HOIyHnyaw/JA474Wfj2tl6A= github.com/onflow/flow-ft/lib/go/templates v1.0.1 h1:FDYKAiGowABtoMNusLuRCILIZDtVqJ/5tYI4VkF5zfM= github.com/onflow/flow-ft/lib/go/templates v1.0.1/go.mod h1:uQ8XFqmMK2jxyBSVrmyuwdWjTEb+6zGjRYotfDJ5pAE= -github.com/onflow/flow-go v0.38.0-preview.0.0.20241125190444-25a8af57bea1 h1:tE21Kgx2Aqll9ywbiRDfc2BVIz5g6zKdrIom9U9eTE4= -github.com/onflow/flow-go v0.38.0-preview.0.0.20241125190444-25a8af57bea1/go.mod h1:c4ubAQ2WIMYY/TOaBvbajROEFWv2HwhKeGOsEdLPIM0= +github.com/onflow/flow-go v0.38.0-preview.0.0.4 h1:oVCnex8RjD4vUsCgXWFpN5fILckQV7y1FJs+WEMEPLY= +github.com/onflow/flow-go v0.38.0-preview.0.0.4/go.mod h1:c4ubAQ2WIMYY/TOaBvbajROEFWv2HwhKeGOsEdLPIM0= github.com/onflow/flow-go-sdk v1.2.3 h1:jb+0dIXBO12Zt8x3c2xDXYPv6k3sRTUvhe59M+EcXTI= github.com/onflow/flow-go-sdk v1.2.3/go.mod h1:jMaffBTlAIdutx+pBhRIigLZFIBYSDDST0Uax1rW2qo= github.com/onflow/flow-nft/lib/go/contracts v1.2.2 h1:XFERNVUDGbZ4ViZjt7P1cGD80mO1PzUJYPfdhXFsGbQ= From 9d7717c1f2bee847c8394320db403205b59c392e Mon Sep 17 00:00:00 2001 From: "Leo Zhang (zhangchiqing)" Date: Tue, 26 Nov 2024 19:33:16 -0800 Subject: [PATCH 103/174] update go.mod --- go.mod | 4 +++- go.sum | 4 ++-- 2 files changed, 5 insertions(+), 3 deletions(-) diff --git a/go.mod b/go.mod index 85e58586d..b4e14c14c 100644 --- a/go.mod +++ b/go.mod @@ -8,7 +8,7 @@ require ( github.com/hashicorp/go-multierror v1.1.1 github.com/onflow/atree v0.8.0 github.com/onflow/cadence v1.2.2 - github.com/onflow/flow-go v0.38.0-preview.0.0.4 + github.com/onflow/flow-go v0.38.0-preview.0.0.5 github.com/onflow/flow-go-sdk v1.2.3 github.com/onflow/go-ethereum v1.14.7 github.com/prometheus/client_golang v1.18.0 @@ -84,6 +84,7 @@ require ( github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da // indirect github.com/golang/protobuf v1.5.4 // indirect github.com/golang/snappy v0.0.5-0.20220116011046-fa5810519dcb // indirect + github.com/google/go-cmp v0.6.0 // indirect github.com/google/pprof v0.0.0-20231229205709-960ae82b1e42 // indirect github.com/google/s2a-go v0.1.7 // indirect github.com/google/uuid v1.6.0 // indirect @@ -211,6 +212,7 @@ require ( google.golang.org/protobuf v1.33.0 // indirect gopkg.in/ini.v1 v1.67.0 // indirect gopkg.in/yaml.v3 v3.0.1 // indirect + gotest.tools v2.2.0+incompatible // indirect lukechampine.com/blake3 v1.3.0 // indirect rsc.io/tmplfunc v0.0.3 // indirect ) diff --git a/go.sum b/go.sum index 8f802fc43..dbefec495 100644 --- a/go.sum +++ b/go.sum @@ -535,8 +535,8 @@ github.com/onflow/flow-ft/lib/go/contracts v1.0.1 h1:Ts5ob+CoCY2EjEd0W6vdLJ7hLL3 github.com/onflow/flow-ft/lib/go/contracts v1.0.1/go.mod h1:PwsL8fC81cjnUnTfmyL/HOIyHnyaw/JA474Wfj2tl6A= github.com/onflow/flow-ft/lib/go/templates v1.0.1 h1:FDYKAiGowABtoMNusLuRCILIZDtVqJ/5tYI4VkF5zfM= github.com/onflow/flow-ft/lib/go/templates v1.0.1/go.mod h1:uQ8XFqmMK2jxyBSVrmyuwdWjTEb+6zGjRYotfDJ5pAE= -github.com/onflow/flow-go v0.38.0-preview.0.0.4 h1:oVCnex8RjD4vUsCgXWFpN5fILckQV7y1FJs+WEMEPLY= -github.com/onflow/flow-go v0.38.0-preview.0.0.4/go.mod h1:c4ubAQ2WIMYY/TOaBvbajROEFWv2HwhKeGOsEdLPIM0= +github.com/onflow/flow-go v0.38.0-preview.0.0.5 h1:rc9lIlR32bi1Z9ojKHyG4WbC2pl7drkva6hI5zVQNAQ= +github.com/onflow/flow-go v0.38.0-preview.0.0.5/go.mod h1:c4ubAQ2WIMYY/TOaBvbajROEFWv2HwhKeGOsEdLPIM0= github.com/onflow/flow-go-sdk v1.2.3 h1:jb+0dIXBO12Zt8x3c2xDXYPv6k3sRTUvhe59M+EcXTI= github.com/onflow/flow-go-sdk v1.2.3/go.mod h1:jMaffBTlAIdutx+pBhRIigLZFIBYSDDST0Uax1rW2qo= github.com/onflow/flow-nft/lib/go/contracts v1.2.2 h1:XFERNVUDGbZ4ViZjt7P1cGD80mO1PzUJYPfdhXFsGbQ= From d3c813a8e5507f6f9bad8457a8efd6ff734fd818 Mon Sep 17 00:00:00 2001 From: "Leo Zhang (zhangchiqing)" Date: Tue, 26 Nov 2024 20:36:54 -0800 Subject: [PATCH 104/174] add extract EVM state tests --- cmd/util/main_test.go | 89 +++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 89 insertions(+) create mode 100644 cmd/util/main_test.go diff --git a/cmd/util/main_test.go b/cmd/util/main_test.go new file mode 100644 index 000000000..b424131a8 --- /dev/null +++ b/cmd/util/main_test.go @@ -0,0 +1,89 @@ +package main_test + +import ( + "fmt" + "path/filepath" + "testing" + + "github.com/onflow/flow-evm-gateway/storage/pebble" + "github.com/onflow/flow-go-sdk" + + "github.com/onflow/flow-go/fvm/evm" + "github.com/onflow/flow-go/fvm/evm/emulator/state" + "github.com/onflow/flow-go/fvm/evm/offchain/storage" + "github.com/onflow/flow-go/fvm/evm/testutils" + flowGo "github.com/onflow/flow-go/model/flow" + "github.com/rs/zerolog/log" + "github.com/stretchr/testify/require" +) + +func TestStateDiff(t *testing.T) { + + state1 := ExtractEVMState(t, flow.Testnet, + "/var/flow/gw/data/db", uint64(218215348)) + state2 := state.EVMStateFromReplayGobDir(t, + "/var/flow2/evm-state-from-gobs-218215348/", uint64(218215348)) + // state2 := state.EVMStateFromCheckpointExtract(t, "/var/flow2/evm-state-from-checkpoint-218215348/") + + differences := state.Diff(state1, state2) + + for i, diff := range differences { + fmt.Printf("Difference %d: %v\n", i, diff) + } + + require.Len(t, differences, 0) +} + +func ExtractEVMState( + t *testing.T, chainID flowGo.ChainID, + registerStoreDir string, flowHeight uint64) *state.EVMState { + + store, err := pebble.New(registerStoreDir, log.Logger) + require.NoError(t, err) + + storageRoot := evm.StorageAccountAddress(chainID) + registerStore := pebble.NewRegisterStorage(store, storageRoot) + snapshot, err := registerStore.GetSnapshotAt(flowHeight) + require.NoError(t, err) + + ledger := storage.NewReadOnlyStorage(snapshot) + bv, err := state.NewBaseView(ledger, storageRoot) + require.NoError(t, err) + + evmState, err := state.Extract(storageRoot, bv) + require.NoError(t, err) + return evmState +} + +func EVMStateFromCheckpointExtract(t *testing.T, dir string) *state.EVMState { + enState, err := state.ImportEVMStateFromGob("/var/flow2/evm-state-from-gobs-218215348/") + require.NoError(t, err) + return enState +} + +func EVMStateFromReplayGobDir(t *testing.T, gobDir string, flowHeight uint64) *state.EVMState { + valueFileName, allocatorFileName := evmStateGobFileNamesByEndHeight(gobDir, flowHeight) + chainID := flow.Testnet + + allocatorGobs, err := testutils.DeserializeAllocator(allocatorFileName) + require.NoError(t, err) + + storageRoot := evm.StorageAccountAddress(chainID) + valuesGob, err := testutils.DeserializeState(valueFileName) + require.NoError(t, err) + + store := testutils.GetSimpleValueStorePopulated(valuesGob, allocatorGobs) + + bv, err := state.NewBaseView(store, storageRoot) + require.NoError(t, err) + + evmState, err := state.Extract(storageRoot, bv) + require.NoError(t, err) + return evmState +} + +func evmStateGobFileNamesByEndHeight(evmStateGobDir string, endHeight uint64) (string, string) { + valueFileName := filepath.Join(evmStateGobDir, fmt.Sprintf("values-%d.gob", endHeight)) + allocatorFileName := filepath.Join(evmStateGobDir, fmt.Sprintf("allocators-%d.gob", endHeight)) + return valueFileName, allocatorFileName +} From 3d9f862978abd389dcd7a16a3f1be2f14a2c1d9c Mon Sep 17 00:00:00 2001 From: "Leo Zhang (zhangchiqing)" Date: Tue, 26 Nov 2024 20:38:22 -0800 Subject: [PATCH 105/174] fix replay --- services/ingestion/engine.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/services/ingestion/engine.go b/services/ingestion/engine.go index 2145e6497..52249366d 100644 --- a/services/ingestion/engine.go +++ b/services/ingestion/engine.go @@ -248,7 +248,7 @@ func (e *Engine) indexEvents(events *models.CadenceEvents, batch *pebbleDB.Batch // Step 1.2: Replay all block transactions // If `ReplayBlock` returns any error, we abort the EVM events processing blockEvents := events.BlockEventPayload() - res, err := replayer.ReplayBlock(events.TxEventPayloads(), blockEvents) + res, _, err := replayer.ReplayBlock(events.TxEventPayloads(), blockEvents) if err != nil { return fmt.Errorf("failed to replay block on height: %d, with: %w", events.Block().Height, err) } From e919eb8bdf5a1b19f89cb257be2250943faead37 Mon Sep 17 00:00:00 2001 From: "Leo Zhang (zhangchiqing)" Date: Tue, 26 Nov 2024 20:39:56 -0800 Subject: [PATCH 106/174] fix lint --- cmd/util/main_test.go | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/cmd/util/main_test.go b/cmd/util/main_test.go index b424131a8..6cc3d593e 100644 --- a/cmd/util/main_test.go +++ b/cmd/util/main_test.go @@ -6,7 +6,6 @@ import ( "testing" "github.com/onflow/flow-evm-gateway/storage/pebble" - "github.com/onflow/flow-go-sdk" "github.com/onflow/flow-go/fvm/evm" "github.com/onflow/flow-go/fvm/evm/emulator/state" @@ -19,9 +18,9 @@ import ( func TestStateDiff(t *testing.T) { - state1 := ExtractEVMState(t, flow.Testnet, + state1 := ExtractEVMState(t, flowGo.Testnet, "/var/flow/gw/data/db", uint64(218215348)) - state2 := state.EVMStateFromReplayGobDir(t, + state2 := EVMStateFromReplayGobDir(t, "/var/flow2/evm-state-from-gobs-218215348/", uint64(218215348)) // state2 := state.EVMStateFromCheckpointExtract(t, "/var/flow2/evm-state-from-checkpoint-218215348/") @@ -63,7 +62,7 @@ func EVMStateFromCheckpointExtract(t *testing.T, dir string) *state.EVMState { func EVMStateFromReplayGobDir(t *testing.T, gobDir string, flowHeight uint64) *state.EVMState { valueFileName, allocatorFileName := evmStateGobFileNamesByEndHeight(gobDir, flowHeight) - chainID := flow.Testnet + chainID := flowGo.Testnet allocatorGobs, err := testutils.DeserializeAllocator(allocatorFileName) require.NoError(t, err) From 6d387175ce02e9f121c3dd981124082144fdf707 Mon Sep 17 00:00:00 2001 From: "Leo Zhang (zhangchiqing)" Date: Wed, 27 Nov 2024 06:58:04 -0800 Subject: [PATCH 107/174] update tests --- cmd/util/main_test.go | 24 ++++++++++++++++++++++++ 1 file changed, 24 insertions(+) diff --git a/cmd/util/main_test.go b/cmd/util/main_test.go index 6cc3d593e..ead92a30c 100644 --- a/cmd/util/main_test.go +++ b/cmd/util/main_test.go @@ -86,3 +86,27 @@ func evmStateGobFileNamesByEndHeight(evmStateGobDir string, endHeight uint64) (s allocatorFileName := filepath.Join(evmStateGobDir, fmt.Sprintf("allocators-%d.gob", endHeight)) return valueFileName, allocatorFileName } + +func TestReadPebbleRootAccountStorageKeyID(t *testing.T) { + chainID := flowGo.Testnet + registerStoreDir := "/var/flow/gw/data/db" + flowHeight := uint64(218215348) + + store, err := pebble.New(registerStoreDir, log.Logger) + require.NoError(t, err) + + storageRoot := evm.StorageAccountAddress(chainID) + registerStore := pebble.NewRegisterStorage(store, storageRoot) + regID := flowGo.RegisterID{ + Owner: string(storageRoot.Bytes()), + Key: state.AccountsStorageIDKey, + } + regVal, err := registerStore.Get(regID, flowHeight) + require.NoError(t, err) + + require.NotNil(t, regVal) + fmt.Printf("Register value: %x\n", regVal) + + _, err = registerStore.GetSnapshotAt(flowHeight) + require.NoError(t, err) +} From dabec6107ddd5d288cfe2989aa3366481c084ff4 Mon Sep 17 00:00:00 2001 From: "Leo Zhang (zhangchiqing)" Date: Wed, 27 Nov 2024 09:39:14 -0800 Subject: [PATCH 108/174] update version --- go.mod | 2 +- go.sum | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/go.mod b/go.mod index b4e14c14c..179448e86 100644 --- a/go.mod +++ b/go.mod @@ -8,7 +8,7 @@ require ( github.com/hashicorp/go-multierror v1.1.1 github.com/onflow/atree v0.8.0 github.com/onflow/cadence v1.2.2 - github.com/onflow/flow-go v0.38.0-preview.0.0.5 + github.com/onflow/flow-go v0.38.0-preview.0.0.7 github.com/onflow/flow-go-sdk v1.2.3 github.com/onflow/go-ethereum v1.14.7 github.com/prometheus/client_golang v1.18.0 diff --git a/go.sum b/go.sum index dbefec495..0cd26354e 100644 --- a/go.sum +++ b/go.sum @@ -535,8 +535,8 @@ github.com/onflow/flow-ft/lib/go/contracts v1.0.1 h1:Ts5ob+CoCY2EjEd0W6vdLJ7hLL3 github.com/onflow/flow-ft/lib/go/contracts v1.0.1/go.mod h1:PwsL8fC81cjnUnTfmyL/HOIyHnyaw/JA474Wfj2tl6A= github.com/onflow/flow-ft/lib/go/templates v1.0.1 h1:FDYKAiGowABtoMNusLuRCILIZDtVqJ/5tYI4VkF5zfM= github.com/onflow/flow-ft/lib/go/templates v1.0.1/go.mod h1:uQ8XFqmMK2jxyBSVrmyuwdWjTEb+6zGjRYotfDJ5pAE= -github.com/onflow/flow-go v0.38.0-preview.0.0.5 h1:rc9lIlR32bi1Z9ojKHyG4WbC2pl7drkva6hI5zVQNAQ= -github.com/onflow/flow-go v0.38.0-preview.0.0.5/go.mod h1:c4ubAQ2WIMYY/TOaBvbajROEFWv2HwhKeGOsEdLPIM0= +github.com/onflow/flow-go v0.38.0-preview.0.0.7 h1:T3jRN8cettIccxNhfMwZgPPp08446dW8fXXHEUMQKwg= +github.com/onflow/flow-go v0.38.0-preview.0.0.7/go.mod h1:c4ubAQ2WIMYY/TOaBvbajROEFWv2HwhKeGOsEdLPIM0= github.com/onflow/flow-go-sdk v1.2.3 h1:jb+0dIXBO12Zt8x3c2xDXYPv6k3sRTUvhe59M+EcXTI= github.com/onflow/flow-go-sdk v1.2.3/go.mod h1:jMaffBTlAIdutx+pBhRIigLZFIBYSDDST0Uax1rW2qo= github.com/onflow/flow-nft/lib/go/contracts v1.2.2 h1:XFERNVUDGbZ4ViZjt7P1cGD80mO1PzUJYPfdhXFsGbQ= From 51d8b63ae3b94ea7dd29b0b4b179fbd3f45b01f8 Mon Sep 17 00:00:00 2001 From: "Leo Zhang (zhangchiqing)" Date: Wed, 27 Nov 2024 09:42:44 -0800 Subject: [PATCH 109/174] update version --- go.mod | 2 +- go.sum | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/go.mod b/go.mod index 179448e86..c3023f04f 100644 --- a/go.mod +++ b/go.mod @@ -8,7 +8,7 @@ require ( github.com/hashicorp/go-multierror v1.1.1 github.com/onflow/atree v0.8.0 github.com/onflow/cadence v1.2.2 - github.com/onflow/flow-go v0.38.0-preview.0.0.7 + github.com/onflow/flow-go v0.38.0-preview.0.0.8 github.com/onflow/flow-go-sdk v1.2.3 github.com/onflow/go-ethereum v1.14.7 github.com/prometheus/client_golang v1.18.0 diff --git a/go.sum b/go.sum index 0cd26354e..d1bac113a 100644 --- a/go.sum +++ b/go.sum @@ -535,8 +535,8 @@ github.com/onflow/flow-ft/lib/go/contracts v1.0.1 h1:Ts5ob+CoCY2EjEd0W6vdLJ7hLL3 github.com/onflow/flow-ft/lib/go/contracts v1.0.1/go.mod h1:PwsL8fC81cjnUnTfmyL/HOIyHnyaw/JA474Wfj2tl6A= github.com/onflow/flow-ft/lib/go/templates v1.0.1 h1:FDYKAiGowABtoMNusLuRCILIZDtVqJ/5tYI4VkF5zfM= github.com/onflow/flow-ft/lib/go/templates v1.0.1/go.mod h1:uQ8XFqmMK2jxyBSVrmyuwdWjTEb+6zGjRYotfDJ5pAE= -github.com/onflow/flow-go v0.38.0-preview.0.0.7 h1:T3jRN8cettIccxNhfMwZgPPp08446dW8fXXHEUMQKwg= -github.com/onflow/flow-go v0.38.0-preview.0.0.7/go.mod h1:c4ubAQ2WIMYY/TOaBvbajROEFWv2HwhKeGOsEdLPIM0= +github.com/onflow/flow-go v0.38.0-preview.0.0.8 h1:LBlYsBYmf0lTSfQnbAicyfwDh6m5Uw0cwcHZbNq6UcE= +github.com/onflow/flow-go v0.38.0-preview.0.0.8/go.mod h1:c4ubAQ2WIMYY/TOaBvbajROEFWv2HwhKeGOsEdLPIM0= github.com/onflow/flow-go-sdk v1.2.3 h1:jb+0dIXBO12Zt8x3c2xDXYPv6k3sRTUvhe59M+EcXTI= github.com/onflow/flow-go-sdk v1.2.3/go.mod h1:jMaffBTlAIdutx+pBhRIigLZFIBYSDDST0Uax1rW2qo= github.com/onflow/flow-nft/lib/go/contracts v1.2.2 h1:XFERNVUDGbZ4ViZjt7P1cGD80mO1PzUJYPfdhXFsGbQ= From 88f6aaa3dd87d023a6784b49fde7bb7313a6fa02 Mon Sep 17 00:00:00 2001 From: "Leo Zhang (zhangchiqing)" Date: Wed, 27 Nov 2024 09:55:54 -0800 Subject: [PATCH 110/174] update version --- go.mod | 2 +- go.sum | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/go.mod b/go.mod index c3023f04f..53e72c097 100644 --- a/go.mod +++ b/go.mod @@ -8,7 +8,7 @@ require ( github.com/hashicorp/go-multierror v1.1.1 github.com/onflow/atree v0.8.0 github.com/onflow/cadence v1.2.2 - github.com/onflow/flow-go v0.38.0-preview.0.0.8 + github.com/onflow/flow-go v0.38.0-preview.0.0.9 github.com/onflow/flow-go-sdk v1.2.3 github.com/onflow/go-ethereum v1.14.7 github.com/prometheus/client_golang v1.18.0 diff --git a/go.sum b/go.sum index d1bac113a..b5a97c175 100644 --- a/go.sum +++ b/go.sum @@ -535,8 +535,8 @@ github.com/onflow/flow-ft/lib/go/contracts v1.0.1 h1:Ts5ob+CoCY2EjEd0W6vdLJ7hLL3 github.com/onflow/flow-ft/lib/go/contracts v1.0.1/go.mod h1:PwsL8fC81cjnUnTfmyL/HOIyHnyaw/JA474Wfj2tl6A= github.com/onflow/flow-ft/lib/go/templates v1.0.1 h1:FDYKAiGowABtoMNusLuRCILIZDtVqJ/5tYI4VkF5zfM= github.com/onflow/flow-ft/lib/go/templates v1.0.1/go.mod h1:uQ8XFqmMK2jxyBSVrmyuwdWjTEb+6zGjRYotfDJ5pAE= -github.com/onflow/flow-go v0.38.0-preview.0.0.8 h1:LBlYsBYmf0lTSfQnbAicyfwDh6m5Uw0cwcHZbNq6UcE= -github.com/onflow/flow-go v0.38.0-preview.0.0.8/go.mod h1:c4ubAQ2WIMYY/TOaBvbajROEFWv2HwhKeGOsEdLPIM0= +github.com/onflow/flow-go v0.38.0-preview.0.0.9 h1:k8l2cWVDcXZceyMEG2F+V9IvXXm85r6Q2IFDnhS0A00= +github.com/onflow/flow-go v0.38.0-preview.0.0.9/go.mod h1:c4ubAQ2WIMYY/TOaBvbajROEFWv2HwhKeGOsEdLPIM0= github.com/onflow/flow-go-sdk v1.2.3 h1:jb+0dIXBO12Zt8x3c2xDXYPv6k3sRTUvhe59M+EcXTI= github.com/onflow/flow-go-sdk v1.2.3/go.mod h1:jMaffBTlAIdutx+pBhRIigLZFIBYSDDST0Uax1rW2qo= github.com/onflow/flow-nft/lib/go/contracts v1.2.2 h1:XFERNVUDGbZ4ViZjt7P1cGD80mO1PzUJYPfdhXFsGbQ= From efd5b1d3b05a3db11ab5bd67fa793d067c45469c Mon Sep 17 00:00:00 2001 From: "Leo Zhang (zhangchiqing)" Date: Wed, 27 Nov 2024 10:01:08 -0800 Subject: [PATCH 111/174] update version --- go.mod | 2 +- go.sum | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/go.mod b/go.mod index 53e72c097..b5ad070ee 100644 --- a/go.mod +++ b/go.mod @@ -8,7 +8,7 @@ require ( github.com/hashicorp/go-multierror v1.1.1 github.com/onflow/atree v0.8.0 github.com/onflow/cadence v1.2.2 - github.com/onflow/flow-go v0.38.0-preview.0.0.9 + github.com/onflow/flow-go v0.38.0-preview.0.0.10 github.com/onflow/flow-go-sdk v1.2.3 github.com/onflow/go-ethereum v1.14.7 github.com/prometheus/client_golang v1.18.0 diff --git a/go.sum b/go.sum index b5a97c175..c2f8185bc 100644 --- a/go.sum +++ b/go.sum @@ -535,8 +535,8 @@ github.com/onflow/flow-ft/lib/go/contracts v1.0.1 h1:Ts5ob+CoCY2EjEd0W6vdLJ7hLL3 github.com/onflow/flow-ft/lib/go/contracts v1.0.1/go.mod h1:PwsL8fC81cjnUnTfmyL/HOIyHnyaw/JA474Wfj2tl6A= github.com/onflow/flow-ft/lib/go/templates v1.0.1 h1:FDYKAiGowABtoMNusLuRCILIZDtVqJ/5tYI4VkF5zfM= github.com/onflow/flow-ft/lib/go/templates v1.0.1/go.mod h1:uQ8XFqmMK2jxyBSVrmyuwdWjTEb+6zGjRYotfDJ5pAE= -github.com/onflow/flow-go v0.38.0-preview.0.0.9 h1:k8l2cWVDcXZceyMEG2F+V9IvXXm85r6Q2IFDnhS0A00= -github.com/onflow/flow-go v0.38.0-preview.0.0.9/go.mod h1:c4ubAQ2WIMYY/TOaBvbajROEFWv2HwhKeGOsEdLPIM0= +github.com/onflow/flow-go v0.38.0-preview.0.0.10 h1:0hUwj9VAH2O2UHSmmt2tJW0P9p1PRpgIPToH3YLyPzM= +github.com/onflow/flow-go v0.38.0-preview.0.0.10/go.mod h1:c4ubAQ2WIMYY/TOaBvbajROEFWv2HwhKeGOsEdLPIM0= github.com/onflow/flow-go-sdk v1.2.3 h1:jb+0dIXBO12Zt8x3c2xDXYPv6k3sRTUvhe59M+EcXTI= github.com/onflow/flow-go-sdk v1.2.3/go.mod h1:jMaffBTlAIdutx+pBhRIigLZFIBYSDDST0Uax1rW2qo= github.com/onflow/flow-nft/lib/go/contracts v1.2.2 h1:XFERNVUDGbZ4ViZjt7P1cGD80mO1PzUJYPfdhXFsGbQ= From 5248ae48f1d00ed741e82e092d57fb3ba7dfed2c Mon Sep 17 00:00:00 2001 From: "Leo Zhang (zhangchiqing)" Date: Wed, 27 Nov 2024 10:19:14 -0800 Subject: [PATCH 112/174] fix extraction --- cmd/util/main_test.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/cmd/util/main_test.go b/cmd/util/main_test.go index ead92a30c..473240ec3 100644 --- a/cmd/util/main_test.go +++ b/cmd/util/main_test.go @@ -45,7 +45,7 @@ func ExtractEVMState( snapshot, err := registerStore.GetSnapshotAt(flowHeight) require.NoError(t, err) - ledger := storage.NewReadOnlyStorage(snapshot) + ledger := storage.NewEphemeralStorage(storage.NewReadOnlyStorage(snapshot)) bv, err := state.NewBaseView(ledger, storageRoot) require.NoError(t, err) From 1f74dd818223d516f8026090f357bf44e0df36df Mon Sep 17 00:00:00 2001 From: "Leo Zhang (zhangchiqing)" Date: Wed, 27 Nov 2024 13:34:17 -0800 Subject: [PATCH 113/174] get account balance --- cmd/util/main_test.go | 49 ++++++++++++++++++++++++++++++++++++------- 1 file changed, 41 insertions(+), 8 deletions(-) diff --git a/cmd/util/main_test.go b/cmd/util/main_test.go index 473240ec3..8897a79bc 100644 --- a/cmd/util/main_test.go +++ b/cmd/util/main_test.go @@ -6,6 +6,7 @@ import ( "testing" "github.com/onflow/flow-evm-gateway/storage/pebble" + gethCommon "github.com/onflow/go-ethereum/common" "github.com/onflow/flow-go/fvm/evm" "github.com/onflow/flow-go/fvm/evm/emulator/state" @@ -18,11 +19,16 @@ import ( func TestStateDiff(t *testing.T) { - state1 := ExtractEVMState(t, flowGo.Testnet, - "/var/flow/gw/data/db", uint64(218215348)) - state2 := EVMStateFromReplayGobDir(t, - "/var/flow2/evm-state-from-gobs-218215348/", uint64(218215348)) - // state2 := state.EVMStateFromCheckpointExtract(t, "/var/flow2/evm-state-from-checkpoint-218215348/") + height := uint64(218215348) + + // state1 := ExtractEVMState(t, flowGo.Testnet, "/var/flow52/evm/data/db", height) + + state1 := EVMStateFromReplayGobDir(t, "/var/flow/nov26_testnet_evm_state_gob", height) + state2 := EVMStateFromCheckpointExtract(t, "/var/flow52/evm-state-from-checkpoint-218215348-migrated/") + + // state1 := ExtractEVMState(t, flowGo.Testnet, + // "/var/flow52/evm/data/db", uint64(228901661)) + // state2 := EVMStateFromCheckpointExtract(t, "/var/flow52/evm-state-from-checkpoint-228901661") differences := state.Diff(state1, state2) @@ -55,7 +61,7 @@ func ExtractEVMState( } func EVMStateFromCheckpointExtract(t *testing.T, dir string) *state.EVMState { - enState, err := state.ImportEVMStateFromGob("/var/flow2/evm-state-from-gobs-218215348/") + enState, err := state.ImportEVMStateFromGob(dir) require.NoError(t, err) return enState } @@ -89,8 +95,8 @@ func evmStateGobFileNamesByEndHeight(evmStateGobDir string, endHeight uint64) (s func TestReadPebbleRootAccountStorageKeyID(t *testing.T) { chainID := flowGo.Testnet - registerStoreDir := "/var/flow/gw/data/db" - flowHeight := uint64(218215348) + registerStoreDir := "/var/flow52/evm/data/db" + flowHeight := uint64(228901661) store, err := pebble.New(registerStoreDir, log.Logger) require.NoError(t, err) @@ -110,3 +116,30 @@ func TestReadPebbleRootAccountStorageKeyID(t *testing.T) { _, err = registerStore.GetSnapshotAt(flowHeight) require.NoError(t, err) } + +func TestReadAccount(t *testing.T) { + + chainID := flowGo.Testnet + registerStoreDir := "/var/flow52/evm/data/db" + flowHeight := uint64(228901661) + accountStr := "17DDB8C5aA1382Fb04d057e3ec42B65a43A5fF49" + + // Convert string to common.Address + account := gethCommon.HexToAddress(accountStr) + + store, err := pebble.New(registerStoreDir, log.Logger) + require.NoError(t, err) + + storageRoot := evm.StorageAccountAddress(chainID) + registerStore := pebble.NewRegisterStorage(store, storageRoot) + + snapshot, err := registerStore.GetSnapshotAt(flowHeight) + require.NoError(t, err) + ledger := storage.NewEphemeralStorage(storage.NewReadOnlyStorage(snapshot)) + + stateDB, err := state.NewStateDB(ledger, storageRoot) + require.NoError(t, err) + + balance := stateDB.GetBalance(account) + fmt.Println("balance: ", balance) +} From d0d687f75546e7fccccda4721000e21ed35fd55c Mon Sep 17 00:00:00 2001 From: "Leo Zhang (zhangchiqing)" Date: Wed, 27 Nov 2024 13:47:12 -0800 Subject: [PATCH 114/174] print account balance from base view --- cmd/util/main_test.go | 14 +++++++++++--- 1 file changed, 11 insertions(+), 3 deletions(-) diff --git a/cmd/util/main_test.go b/cmd/util/main_test.go index 8897a79bc..94933998f 100644 --- a/cmd/util/main_test.go +++ b/cmd/util/main_test.go @@ -51,7 +51,7 @@ func ExtractEVMState( snapshot, err := registerStore.GetSnapshotAt(flowHeight) require.NoError(t, err) - ledger := storage.NewEphemeralStorage(storage.NewReadOnlyStorage(snapshot)) + ledger := storage.NewReadOnlyStorage(snapshot) bv, err := state.NewBaseView(ledger, storageRoot) require.NoError(t, err) @@ -135,11 +135,19 @@ func TestReadAccount(t *testing.T) { snapshot, err := registerStore.GetSnapshotAt(flowHeight) require.NoError(t, err) - ledger := storage.NewEphemeralStorage(storage.NewReadOnlyStorage(snapshot)) + ledger := storage.NewReadOnlyStorage(snapshot) + bv, err := state.NewBaseView(ledger, storageRoot) + require.NoError(t, err) stateDB, err := state.NewStateDB(ledger, storageRoot) require.NoError(t, err) balance := stateDB.GetBalance(account) - fmt.Println("balance: ", balance) + + fmt.Println("balance from stateDB: ", balance) + + balanceFromBaseView, err := bv.GetBalance(account) + require.NoError(t, err) + fmt.Println("balance from base view: ", balanceFromBaseView) + } From e31bb06492c44e8bbb6a8d178f533d198f12c748 Mon Sep 17 00:00:00 2001 From: "Leo Zhang (zhangchiqing)" Date: Wed, 27 Nov 2024 16:03:38 -0800 Subject: [PATCH 115/174] update evm state extraction --- cmd/util/main.go | 2 +- cmd/util/main_test.go | 153 ----------------------------------- services/evm/extract.go | 36 +++++++++ services/evm/extract_test.go | 45 +++++++++++ 4 files changed, 82 insertions(+), 154 deletions(-) delete mode 100644 cmd/util/main_test.go create mode 100644 services/evm/extract.go create mode 100644 services/evm/extract_test.go diff --git a/cmd/util/main.go b/cmd/util/main.go index f777e3875..a88022dff 100644 --- a/cmd/util/main.go +++ b/cmd/util/main.go @@ -20,7 +20,7 @@ func main() { registerStoreDir string ) - flag.Uint64Var(&height, "height", 0, "Block height for EVM state export") + flag.Uint64Var(&height, "evm-height", 0, "EVM Block height for EVM state export") flag.StringVar(&outputDir, "output", "", "Output directory for exported EVM state") flag.StringVar(®isterStoreDir, "register-store", "", "Directory of the register store") diff --git a/cmd/util/main_test.go b/cmd/util/main_test.go deleted file mode 100644 index 94933998f..000000000 --- a/cmd/util/main_test.go +++ /dev/null @@ -1,153 +0,0 @@ -package main_test - -import ( - "fmt" - "path/filepath" - "testing" - - "github.com/onflow/flow-evm-gateway/storage/pebble" - gethCommon "github.com/onflow/go-ethereum/common" - - "github.com/onflow/flow-go/fvm/evm" - "github.com/onflow/flow-go/fvm/evm/emulator/state" - "github.com/onflow/flow-go/fvm/evm/offchain/storage" - "github.com/onflow/flow-go/fvm/evm/testutils" - flowGo "github.com/onflow/flow-go/model/flow" - "github.com/rs/zerolog/log" - "github.com/stretchr/testify/require" -) - -func TestStateDiff(t *testing.T) { - - height := uint64(218215348) - - // state1 := ExtractEVMState(t, flowGo.Testnet, "/var/flow52/evm/data/db", height) - - state1 := EVMStateFromReplayGobDir(t, "/var/flow/nov26_testnet_evm_state_gob", height) - state2 := EVMStateFromCheckpointExtract(t, "/var/flow52/evm-state-from-checkpoint-218215348-migrated/") - - // state1 := ExtractEVMState(t, flowGo.Testnet, - // "/var/flow52/evm/data/db", uint64(228901661)) - // state2 := EVMStateFromCheckpointExtract(t, "/var/flow52/evm-state-from-checkpoint-228901661") - - differences := state.Diff(state1, state2) - - for i, diff := range differences { - fmt.Printf("Difference %d: %v\n", i, diff) - } - - require.Len(t, differences, 0) -} - -func ExtractEVMState( - t *testing.T, chainID flowGo.ChainID, - registerStoreDir string, flowHeight uint64) *state.EVMState { - - store, err := pebble.New(registerStoreDir, log.Logger) - require.NoError(t, err) - - storageRoot := evm.StorageAccountAddress(chainID) - registerStore := pebble.NewRegisterStorage(store, storageRoot) - snapshot, err := registerStore.GetSnapshotAt(flowHeight) - require.NoError(t, err) - - ledger := storage.NewReadOnlyStorage(snapshot) - bv, err := state.NewBaseView(ledger, storageRoot) - require.NoError(t, err) - - evmState, err := state.Extract(storageRoot, bv) - require.NoError(t, err) - return evmState -} - -func EVMStateFromCheckpointExtract(t *testing.T, dir string) *state.EVMState { - enState, err := state.ImportEVMStateFromGob(dir) - require.NoError(t, err) - return enState -} - -func EVMStateFromReplayGobDir(t *testing.T, gobDir string, flowHeight uint64) *state.EVMState { - valueFileName, allocatorFileName := evmStateGobFileNamesByEndHeight(gobDir, flowHeight) - chainID := flowGo.Testnet - - allocatorGobs, err := testutils.DeserializeAllocator(allocatorFileName) - require.NoError(t, err) - - storageRoot := evm.StorageAccountAddress(chainID) - valuesGob, err := testutils.DeserializeState(valueFileName) - require.NoError(t, err) - - store := testutils.GetSimpleValueStorePopulated(valuesGob, allocatorGobs) - - bv, err := state.NewBaseView(store, storageRoot) - require.NoError(t, err) - - evmState, err := state.Extract(storageRoot, bv) - require.NoError(t, err) - return evmState -} - -func evmStateGobFileNamesByEndHeight(evmStateGobDir string, endHeight uint64) (string, string) { - valueFileName := filepath.Join(evmStateGobDir, fmt.Sprintf("values-%d.gob", endHeight)) - allocatorFileName := filepath.Join(evmStateGobDir, fmt.Sprintf("allocators-%d.gob", endHeight)) - return valueFileName, allocatorFileName -} - -func TestReadPebbleRootAccountStorageKeyID(t *testing.T) { - chainID := flowGo.Testnet - registerStoreDir := "/var/flow52/evm/data/db" - flowHeight := uint64(228901661) - - store, err := pebble.New(registerStoreDir, log.Logger) - require.NoError(t, err) - - storageRoot := evm.StorageAccountAddress(chainID) - registerStore := pebble.NewRegisterStorage(store, storageRoot) - regID := flowGo.RegisterID{ - Owner: string(storageRoot.Bytes()), - Key: state.AccountsStorageIDKey, - } - regVal, err := registerStore.Get(regID, flowHeight) - require.NoError(t, err) - - require.NotNil(t, regVal) - fmt.Printf("Register value: %x\n", regVal) - - _, err = registerStore.GetSnapshotAt(flowHeight) - require.NoError(t, err) -} - -func TestReadAccount(t *testing.T) { - - chainID := flowGo.Testnet - registerStoreDir := "/var/flow52/evm/data/db" - flowHeight := uint64(228901661) - accountStr := "17DDB8C5aA1382Fb04d057e3ec42B65a43A5fF49" - - // Convert string to common.Address - account := gethCommon.HexToAddress(accountStr) - - store, err := pebble.New(registerStoreDir, log.Logger) - require.NoError(t, err) - - storageRoot := evm.StorageAccountAddress(chainID) - registerStore := pebble.NewRegisterStorage(store, storageRoot) - - snapshot, err := registerStore.GetSnapshotAt(flowHeight) - require.NoError(t, err) - ledger := storage.NewReadOnlyStorage(snapshot) - bv, err := state.NewBaseView(ledger, storageRoot) - require.NoError(t, err) - - stateDB, err := state.NewStateDB(ledger, storageRoot) - require.NoError(t, err) - - balance := stateDB.GetBalance(account) - - fmt.Println("balance from stateDB: ", balance) - - balanceFromBaseView, err := bv.GetBalance(account) - require.NoError(t, err) - fmt.Println("balance from base view: ", balanceFromBaseView) - -} diff --git a/services/evm/extract.go b/services/evm/extract.go new file mode 100644 index 000000000..e37cb1f89 --- /dev/null +++ b/services/evm/extract.go @@ -0,0 +1,36 @@ +package evm + +import ( + "fmt" + + "github.com/onflow/flow-evm-gateway/storage/pebble" + "github.com/onflow/flow-go/fvm/evm" + "github.com/onflow/flow-go/fvm/evm/emulator/state" + "github.com/onflow/flow-go/fvm/evm/offchain/storage" + flowGo "github.com/onflow/flow-go/model/flow" +) + +func ExtractEVMState( + chainID flowGo.ChainID, + evmHeight uint64, + store *pebble.Storage, +) (*state.EVMState, error) { + storageRoot := evm.StorageAccountAddress(chainID) + registerStore := pebble.NewRegisterStorage(store, storageRoot) + snapshot, err := registerStore.GetSnapshotAt(evmHeight) + if err != nil { + return nil, fmt.Errorf("failed to get snapshot at evm height %d: %w", evmHeight, err) + } + + ledger := storage.NewReadOnlyStorage(snapshot) + bv, err := state.NewBaseView(ledger, storageRoot) + if err != nil { + return nil, fmt.Errorf("failed to create base view: %w", err) + } + + evmState, err := state.Extract(storageRoot, bv) + if err != nil { + return nil, err + } + return evmState, nil +} diff --git a/services/evm/extract_test.go b/services/evm/extract_test.go new file mode 100644 index 000000000..dc7e08de7 --- /dev/null +++ b/services/evm/extract_test.go @@ -0,0 +1,45 @@ +package evm_test + +import ( + "fmt" + "testing" + + "github.com/onflow/flow-evm-gateway/storage/pebble" + "github.com/onflow/flow-go/fvm/evm/emulator/state" + flowGo "github.com/onflow/flow-go/model/flow" + "github.com/rs/zerolog/log" + "github.com/stretchr/testify/require" + + evmState "github.com/onflow/flow-evm-gateway/services/evm" +) + +func StateDiff(t *testing.T) { + state1 := extractEVMState(t, flowGo.Testnet, "/var/flow52/evm/data/db", uint64(17724990)) + state2 := evmStateFromCheckpointExtract(t, "/var/flow52/evm-state-from-checkpoint-228901661") + + differences := state.Diff(state1, state2) + + for i, diff := range differences { + fmt.Printf("Difference %d: %v\n", i, diff) + } + + require.Len(t, differences, 0) +} + +func extractEVMState( + t *testing.T, chainID flowGo.ChainID, + registerStoreDir string, evmHeight uint64) *state.EVMState { + + store, err := pebble.New(registerStoreDir, log.Logger) + require.NoError(t, err) + + evmState, err := evmState.ExtractEVMState(chainID, evmHeight, store) + require.NoError(t, err) + return evmState +} + +func evmStateFromCheckpointExtract(t *testing.T, dir string) *state.EVMState { + enState, err := state.ImportEVMStateFromGob(dir) + require.NoError(t, err) + return enState +} From 3a1265845fb595e679c5c4ce347d075014d5830b Mon Sep 17 00:00:00 2001 From: "Leo Zhang (zhangchiqing)" Date: Mon, 2 Dec 2024 14:15:15 -0800 Subject: [PATCH 116/174] tidy --- go.mod | 2 -- 1 file changed, 2 deletions(-) diff --git a/go.mod b/go.mod index b5ad070ee..424f026da 100644 --- a/go.mod +++ b/go.mod @@ -84,7 +84,6 @@ require ( github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da // indirect github.com/golang/protobuf v1.5.4 // indirect github.com/golang/snappy v0.0.5-0.20220116011046-fa5810519dcb // indirect - github.com/google/go-cmp v0.6.0 // indirect github.com/google/pprof v0.0.0-20231229205709-960ae82b1e42 // indirect github.com/google/s2a-go v0.1.7 // indirect github.com/google/uuid v1.6.0 // indirect @@ -212,7 +211,6 @@ require ( google.golang.org/protobuf v1.33.0 // indirect gopkg.in/ini.v1 v1.67.0 // indirect gopkg.in/yaml.v3 v3.0.1 // indirect - gotest.tools v2.2.0+incompatible // indirect lukechampine.com/blake3 v1.3.0 // indirect rsc.io/tmplfunc v0.0.3 // indirect ) From be1f5c1dad69e75121d13aeffaad4f6d6e19f5fe Mon Sep 17 00:00:00 2001 From: "Leo Zhang (zhangchiqing)" Date: Mon, 2 Dec 2024 16:01:27 -0800 Subject: [PATCH 117/174] update version --- go.mod | 2 +- go.sum | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/go.mod b/go.mod index 424f026da..64ebf2aba 100644 --- a/go.mod +++ b/go.mod @@ -8,7 +8,7 @@ require ( github.com/hashicorp/go-multierror v1.1.1 github.com/onflow/atree v0.8.0 github.com/onflow/cadence v1.2.2 - github.com/onflow/flow-go v0.38.0-preview.0.0.10 + github.com/onflow/flow-go v0.38.0-preview.0.3 github.com/onflow/flow-go-sdk v1.2.3 github.com/onflow/go-ethereum v1.14.7 github.com/prometheus/client_golang v1.18.0 diff --git a/go.sum b/go.sum index c2f8185bc..431e750a0 100644 --- a/go.sum +++ b/go.sum @@ -535,8 +535,8 @@ github.com/onflow/flow-ft/lib/go/contracts v1.0.1 h1:Ts5ob+CoCY2EjEd0W6vdLJ7hLL3 github.com/onflow/flow-ft/lib/go/contracts v1.0.1/go.mod h1:PwsL8fC81cjnUnTfmyL/HOIyHnyaw/JA474Wfj2tl6A= github.com/onflow/flow-ft/lib/go/templates v1.0.1 h1:FDYKAiGowABtoMNusLuRCILIZDtVqJ/5tYI4VkF5zfM= github.com/onflow/flow-ft/lib/go/templates v1.0.1/go.mod h1:uQ8XFqmMK2jxyBSVrmyuwdWjTEb+6zGjRYotfDJ5pAE= -github.com/onflow/flow-go v0.38.0-preview.0.0.10 h1:0hUwj9VAH2O2UHSmmt2tJW0P9p1PRpgIPToH3YLyPzM= -github.com/onflow/flow-go v0.38.0-preview.0.0.10/go.mod h1:c4ubAQ2WIMYY/TOaBvbajROEFWv2HwhKeGOsEdLPIM0= +github.com/onflow/flow-go v0.38.0-preview.0.3 h1:/e8+a0KKvAAE+DvuIYbPAJfBctX8zpoqF4caQrupBes= +github.com/onflow/flow-go v0.38.0-preview.0.3/go.mod h1:c4ubAQ2WIMYY/TOaBvbajROEFWv2HwhKeGOsEdLPIM0= github.com/onflow/flow-go-sdk v1.2.3 h1:jb+0dIXBO12Zt8x3c2xDXYPv6k3sRTUvhe59M+EcXTI= github.com/onflow/flow-go-sdk v1.2.3/go.mod h1:jMaffBTlAIdutx+pBhRIigLZFIBYSDDST0Uax1rW2qo= github.com/onflow/flow-nft/lib/go/contracts v1.2.2 h1:XFERNVUDGbZ4ViZjt7P1cGD80mO1PzUJYPfdhXFsGbQ= From c9eae967fb936c281b7f3f6c128a9d8050702c13 Mon Sep 17 00:00:00 2001 From: "Leo Zhang (zhangchiqing)" Date: Thu, 5 Dec 2024 09:25:29 -0800 Subject: [PATCH 118/174] update version --- go.mod | 2 +- go.sum | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/go.mod b/go.mod index 64ebf2aba..af84aab9f 100644 --- a/go.mod +++ b/go.mod @@ -8,7 +8,7 @@ require ( github.com/hashicorp/go-multierror v1.1.1 github.com/onflow/atree v0.8.0 github.com/onflow/cadence v1.2.2 - github.com/onflow/flow-go v0.38.0-preview.0.3 + github.com/onflow/flow-go v0.38.0-preview.0.4 github.com/onflow/flow-go-sdk v1.2.3 github.com/onflow/go-ethereum v1.14.7 github.com/prometheus/client_golang v1.18.0 diff --git a/go.sum b/go.sum index 431e750a0..f4226b2e0 100644 --- a/go.sum +++ b/go.sum @@ -535,8 +535,8 @@ github.com/onflow/flow-ft/lib/go/contracts v1.0.1 h1:Ts5ob+CoCY2EjEd0W6vdLJ7hLL3 github.com/onflow/flow-ft/lib/go/contracts v1.0.1/go.mod h1:PwsL8fC81cjnUnTfmyL/HOIyHnyaw/JA474Wfj2tl6A= github.com/onflow/flow-ft/lib/go/templates v1.0.1 h1:FDYKAiGowABtoMNusLuRCILIZDtVqJ/5tYI4VkF5zfM= github.com/onflow/flow-ft/lib/go/templates v1.0.1/go.mod h1:uQ8XFqmMK2jxyBSVrmyuwdWjTEb+6zGjRYotfDJ5pAE= -github.com/onflow/flow-go v0.38.0-preview.0.3 h1:/e8+a0KKvAAE+DvuIYbPAJfBctX8zpoqF4caQrupBes= -github.com/onflow/flow-go v0.38.0-preview.0.3/go.mod h1:c4ubAQ2WIMYY/TOaBvbajROEFWv2HwhKeGOsEdLPIM0= +github.com/onflow/flow-go v0.38.0-preview.0.4 h1:vjnp6btehu3X/aYjsXYlA3r/GGYeB05so0d7ICtXbmg= +github.com/onflow/flow-go v0.38.0-preview.0.4/go.mod h1:c4ubAQ2WIMYY/TOaBvbajROEFWv2HwhKeGOsEdLPIM0= github.com/onflow/flow-go-sdk v1.2.3 h1:jb+0dIXBO12Zt8x3c2xDXYPv6k3sRTUvhe59M+EcXTI= github.com/onflow/flow-go-sdk v1.2.3/go.mod h1:jMaffBTlAIdutx+pBhRIigLZFIBYSDDST0Uax1rW2qo= github.com/onflow/flow-nft/lib/go/contracts v1.2.2 h1:XFERNVUDGbZ4ViZjt7P1cGD80mO1PzUJYPfdhXFsGbQ= From 467051a288185566fc3f510e628477abb7acb6d6 Mon Sep 17 00:00:00 2001 From: "Leo Zhang (zhangchiqing)" Date: Thu, 5 Dec 2024 09:26:13 -0800 Subject: [PATCH 119/174] revert changes to ingestion engine --- services/ingestion/engine.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/services/ingestion/engine.go b/services/ingestion/engine.go index 52249366d..2145e6497 100644 --- a/services/ingestion/engine.go +++ b/services/ingestion/engine.go @@ -248,7 +248,7 @@ func (e *Engine) indexEvents(events *models.CadenceEvents, batch *pebbleDB.Batch // Step 1.2: Replay all block transactions // If `ReplayBlock` returns any error, we abort the EVM events processing blockEvents := events.BlockEventPayload() - res, _, err := replayer.ReplayBlock(events.TxEventPayloads(), blockEvents) + res, err := replayer.ReplayBlock(events.TxEventPayloads(), blockEvents) if err != nil { return fmt.Errorf("failed to replay block on height: %d, with: %w", events.Block().Height, err) } From 96c4472df22dfdc72f4a31ecfc9ee485eb6e9a72 Mon Sep 17 00:00:00 2001 From: "Leo Zhang (zhangchiqing)" Date: Thu, 5 Dec 2024 09:39:18 -0800 Subject: [PATCH 120/174] go mod tidy --- tests/go.mod | 2 +- tests/go.sum | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/tests/go.mod b/tests/go.mod index 742ebb543..65b9c20e4 100644 --- a/tests/go.mod +++ b/tests/go.mod @@ -8,7 +8,7 @@ require ( github.com/onflow/crypto v0.25.2 github.com/onflow/flow-emulator v1.1.1-0.20241125195348-4e121ffb12af github.com/onflow/flow-evm-gateway v0.0.0-20240201154855-4d4d3d3f19c7 - github.com/onflow/flow-go v0.38.0-preview.0.0.20241125190444-25a8af57bea1 + github.com/onflow/flow-go v0.38.0-preview.0.4 github.com/onflow/flow-go-sdk v1.2.3 github.com/onflow/go-ethereum v1.14.7 github.com/rs/zerolog v1.33.0 diff --git a/tests/go.sum b/tests/go.sum index e41b13eeb..fc082fd7b 100644 --- a/tests/go.sum +++ b/tests/go.sum @@ -799,8 +799,8 @@ github.com/onflow/flow-ft/lib/go/contracts v1.0.1 h1:Ts5ob+CoCY2EjEd0W6vdLJ7hLL3 github.com/onflow/flow-ft/lib/go/contracts v1.0.1/go.mod h1:PwsL8fC81cjnUnTfmyL/HOIyHnyaw/JA474Wfj2tl6A= github.com/onflow/flow-ft/lib/go/templates v1.0.1 h1:FDYKAiGowABtoMNusLuRCILIZDtVqJ/5tYI4VkF5zfM= github.com/onflow/flow-ft/lib/go/templates v1.0.1/go.mod h1:uQ8XFqmMK2jxyBSVrmyuwdWjTEb+6zGjRYotfDJ5pAE= -github.com/onflow/flow-go v0.38.0-preview.0.0.20241125190444-25a8af57bea1 h1:tE21Kgx2Aqll9ywbiRDfc2BVIz5g6zKdrIom9U9eTE4= -github.com/onflow/flow-go v0.38.0-preview.0.0.20241125190444-25a8af57bea1/go.mod h1:c4ubAQ2WIMYY/TOaBvbajROEFWv2HwhKeGOsEdLPIM0= +github.com/onflow/flow-go v0.38.0-preview.0.4 h1:vjnp6btehu3X/aYjsXYlA3r/GGYeB05so0d7ICtXbmg= +github.com/onflow/flow-go v0.38.0-preview.0.4/go.mod h1:c4ubAQ2WIMYY/TOaBvbajROEFWv2HwhKeGOsEdLPIM0= github.com/onflow/flow-go-sdk v1.2.3 h1:jb+0dIXBO12Zt8x3c2xDXYPv6k3sRTUvhe59M+EcXTI= github.com/onflow/flow-go-sdk v1.2.3/go.mod h1:jMaffBTlAIdutx+pBhRIigLZFIBYSDDST0Uax1rW2qo= github.com/onflow/flow-nft/lib/go/contracts v1.2.2 h1:XFERNVUDGbZ4ViZjt7P1cGD80mO1PzUJYPfdhXFsGbQ= From 54951134b584c740254b371b254e18a21ffc2810 Mon Sep 17 00:00:00 2001 From: "Leo Zhang (zhangchiqing)" Date: Thu, 5 Dec 2024 11:13:23 -0800 Subject: [PATCH 121/174] fix lint add export evm state to cmd --- cmd/main.go | 2 + cmd/util/main.go | 71 ------------------------------------ services/evm/extract_test.go | 3 +- 3 files changed, 4 insertions(+), 72 deletions(-) delete mode 100644 cmd/util/main.go diff --git a/cmd/main.go b/cmd/main.go index 68c22a0b0..7f359f79f 100644 --- a/cmd/main.go +++ b/cmd/main.go @@ -3,6 +3,7 @@ package main import ( "os" + "github.com/onflow/flow-evm-gateway/cmd/export-evm-state" "github.com/onflow/flow-evm-gateway/cmd/run" "github.com/onflow/flow-evm-gateway/cmd/version" "github.com/rs/zerolog/log" @@ -23,6 +24,7 @@ func Execute() { func main() { rootCmd.AddCommand(version.Cmd) + rootCmd.AddCommand(export.Cmd) rootCmd.AddCommand(run.Cmd) Execute() diff --git a/cmd/util/main.go b/cmd/util/main.go deleted file mode 100644 index a88022dff..000000000 --- a/cmd/util/main.go +++ /dev/null @@ -1,71 +0,0 @@ -package main - -import ( - "flag" - "os" - - "github.com/rs/zerolog/log" - - "github.com/onflow/flow-evm-gateway/storage/pebble" - "github.com/onflow/flow-go/fvm/evm" - "github.com/onflow/flow-go/fvm/evm/emulator/state" - "github.com/onflow/flow-go/fvm/evm/offchain/storage" - flowGo "github.com/onflow/flow-go/model/flow" -) - -func main() { - var ( - height uint64 - outputDir string - registerStoreDir string - ) - - flag.Uint64Var(&height, "evm-height", 0, "EVM Block height for EVM state export") - flag.StringVar(&outputDir, "output", "", "Output directory for exported EVM state") - flag.StringVar(®isterStoreDir, "register-store", "", "Directory of the register store") - - flag.Parse() - - if height == 0 || outputDir == "" || registerStoreDir == "" { - log.Error().Msg("All flags (height, output, register-store) must be provided") - flag.Usage() - os.Exit(1) - } - - chainID := flowGo.Testnet - - log.Info().Msgf("exporting EVM state for height %v from registerStoreDir %v, outputDir: %v", height, registerStoreDir, outputDir) - err := ExportEVMStateForHeight(height, outputDir, registerStoreDir, chainID) - if err != nil { - log.Fatal().Err(err).Msgf("fail to export") - } - - log.Info().Msgf("successfully exported EVM state to %v", outputDir) -} - -func ExportEVMStateForHeight(height uint64, outputDir string, registerStoreDir string, chainID flowGo.ChainID) error { - store, err := pebble.New(registerStoreDir, log.Logger) - if err != nil { - return err - } - - storageAddress := evm.StorageAccountAddress(chainID) - registerStore := pebble.NewRegisterStorage(store, storageAddress) - snapshot, err := registerStore.GetSnapshotAt(height) - if err != nil { - return err - } - - ledger := storage.NewReadOnlyStorage(snapshot) - exporter, err := state.NewExporter(ledger, storageAddress) - if err != nil { - return err - } - - err = exporter.ExportGob(outputDir) - if err != nil { - return err - } - - return nil -} diff --git a/services/evm/extract_test.go b/services/evm/extract_test.go index dc7e08de7..f4bca3197 100644 --- a/services/evm/extract_test.go +++ b/services/evm/extract_test.go @@ -30,8 +30,9 @@ func extractEVMState( t *testing.T, chainID flowGo.ChainID, registerStoreDir string, evmHeight uint64) *state.EVMState { - store, err := pebble.New(registerStoreDir, log.Logger) + pebbleDB, err := pebble.OpenDB(registerStoreDir) require.NoError(t, err) + store := pebble.New(pebbleDB, log.Logger) evmState, err := evmState.ExtractEVMState(chainID, evmHeight, store) require.NoError(t, err) From bf84136721017d892ff93661a6876aef181660c0 Mon Sep 17 00:00:00 2001 From: "Leo Zhang (zhangchiqing)" Date: Thu, 5 Dec 2024 13:20:00 -0800 Subject: [PATCH 122/174] fix lint --- cmd/export/cmd.go | 80 +++++++++++++++++++++++++++++++++++++++++++++++ cmd/main.go | 2 +- 2 files changed, 81 insertions(+), 1 deletion(-) create mode 100644 cmd/export/cmd.go diff --git a/cmd/export/cmd.go b/cmd/export/cmd.go new file mode 100644 index 000000000..3a56128d2 --- /dev/null +++ b/cmd/export/cmd.go @@ -0,0 +1,80 @@ +package export + +import ( + "fmt" + + "github.com/rs/zerolog/log" + "github.com/spf13/cobra" + + "github.com/onflow/flow-evm-gateway/storage/pebble" + "github.com/onflow/flow-go/fvm/evm" + "github.com/onflow/flow-go/fvm/evm/emulator/state" + "github.com/onflow/flow-go/fvm/evm/offchain/storage" + flowGo "github.com/onflow/flow-go/model/flow" +) + +var Cmd = &cobra.Command{ + Use: "export-evm-state", + Short: "Export EVM state at a specific height", + RunE: func(*cobra.Command, []string) error { + if height == 0 || outputDir == "" || registerStoreDir == "" { + return fmt.Errorf("all flags (height, output, register-store) must be provided") + } + + log.Info().Msgf("exporting EVM state for height %v from registerStoreDir %v, outputDir: %v, chain: %v", height, registerStoreDir, outputDir, chain) + + chainID := flowGo.ChainID(chain) + + err := ExportEVMStateForHeight(height, outputDir, registerStoreDir, chainID) + if err != nil { + return fmt.Errorf("fail to export: %w", err) + } + + log.Info().Msgf("successfully exported EVM state to %v", outputDir) + + return nil + }, +} + +var ( + height uint64 + outputDir string + chain string + registerStoreDir string +) + +func init() { + Cmd.Flags().Uint64Var(&height, "evm-height", 0, "EVM Block height for EVM state export") + Cmd.Flags().StringVar(&outputDir, "output", "", "Output directory for exported EVM state") + Cmd.Flags().StringVar(&chain, "chain-id", "testnet", "Chain ID for the EVM state") + Cmd.Flags().StringVar(®isterStoreDir, "register-store", "", "Directory of the register store") +} + +func ExportEVMStateForHeight(height uint64, outputDir string, registerStoreDir string, chainID flowGo.ChainID) error { + storageAddress := evm.StorageAccountAddress(chainID) + + pebbleDB, err := pebble.OpenDB(registerStoreDir) + if err != nil { + return fmt.Errorf("failed to open pebble db: %w", err) + } + + store := pebble.New(pebbleDB, log.Logger) + registerStore := pebble.NewRegisterStorage(store, storageAddress) + snapshot, err := registerStore.GetSnapshotAt(height) + if err != nil { + return err + } + + ledger := storage.NewReadOnlyStorage(snapshot) + exporter, err := state.NewExporter(ledger, storageAddress) + if err != nil { + return err + } + + err = exporter.ExportGob(outputDir) + if err != nil { + return err + } + + return nil +} diff --git a/cmd/main.go b/cmd/main.go index 7f359f79f..7a90a575c 100644 --- a/cmd/main.go +++ b/cmd/main.go @@ -3,7 +3,7 @@ package main import ( "os" - "github.com/onflow/flow-evm-gateway/cmd/export-evm-state" + "github.com/onflow/flow-evm-gateway/cmd/export" "github.com/onflow/flow-evm-gateway/cmd/run" "github.com/onflow/flow-evm-gateway/cmd/version" "github.com/rs/zerolog/log" From c0bf02476a8e931c2ae4bfe3a65d0c734ac32002 Mon Sep 17 00:00:00 2001 From: Jerome P Date: Thu, 5 Dec 2024 13:52:23 -0800 Subject: [PATCH 123/174] Changes to simplify first time setup and docker usage --- Dockerfile | 4 +--- Makefile | 39 +++++++++++++++++++++++++++++++++++++++ 2 files changed, 40 insertions(+), 3 deletions(-) diff --git a/Dockerfile b/Dockerfile index 34dacc435..29b2d43d4 100644 --- a/Dockerfile +++ b/Dockerfile @@ -2,8 +2,6 @@ FROM golang:1.22.0 as app-builder -ARG GATEWAY_VERSION="v0.1.0" - # Build the app binary in /app WORKDIR /app @@ -15,7 +13,7 @@ RUN go mod download RUN go mod verify # Build binary -RUN CGO_ENABLED=1 go build -o bin -ldflags="-X github.com/onflow/flow-evm-gateway/api.Version=${GATEWAY_VERSION}" cmd/main.go +RUN CGO_ENABLED=1 go build -o bin cmd/main.go RUN chmod a+x bin # RUN APP diff --git a/Makefile b/Makefile index 026a75590..d71a56669 100644 --- a/Makefile +++ b/Makefile @@ -1,3 +1,24 @@ +# The short Git commit hash +SHORT_COMMIT := $(shell git rev-parse --short HEAD) +BRANCH_NAME:=$(shell git rev-parse --abbrev-ref HEAD | tr '/' '-') +# The Git commit hash +COMMIT := $(shell git rev-parse HEAD) +# The tag of the current commit, otherwise empty +VERSION := $(shell git describe --tags --abbrev=2 --match "v*" --match "secure-cadence*" 2>/dev/null) + +# Image tag: if image tag is not set, set it with version (or short commit if empty) +ifeq (${IMAGE_TAG},) +IMAGE_TAG := ${VERSION} +endif + +ifeq (${IMAGE_TAG},) +IMAGE_TAG := ${SHORT_COMMIT} +endif + +# docker container registry +export CONTAINER_REGISTRY := gcr.io/flow-container-registry +export DOCKER_BUILDKIT := 1 + .PHONY: test test: # test all packages @@ -78,3 +99,21 @@ start-local-bin: --log-writer=console \ --profiler-enabled=true \ --profiler-port=6060 + +# Build docker image +.PHONY: docker-build +docker-build: + git fetch --tags + git checkout ${IMAGE_TAG} + docker build -f Dockerfile -t "$(CONTAINER_REGISTRY)/evm-gateway:${IMAGE_TAG}" -t "$(CONTAINER_REGISTRY)/evm-gateway:$(IMAGE_TAG)" \ + --label "git_commit=${COMMIT}" --label "git_tag=${IMAGE_TAG}" . + +# Run GW image +# ----- +# Add/change required configs as needed +# https://github.com/onflow/flow-evm-gateway?tab=readme-ov-file#configuration-flags +.PHONY: docker-run +docker-run: + docker run --access-node-grpc-host=$ACCESS_NODE_GRPC_HOST --flow-network-id=$FLOW_NETWORK_ID \ + --init-cadence-height=$INIT_CADENCE_HEIGHT --ws-enabled=true --coinbase=$COINBASE --coa-address=$COA_ADDRESS \ + --coa-key=$COA_KEY --rate-limit=9999999 --rpc-host=0.0.0.0 \ No newline at end of file From bfab77ee0ee7fc9d15eca88698b4ba2b67a7ab4f Mon Sep 17 00:00:00 2001 From: Jerome P Date: Thu, 5 Dec 2024 13:56:26 -0800 Subject: [PATCH 124/174] Not needed --- Makefile | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Makefile b/Makefile index d71a56669..ac1cdc885 100644 --- a/Makefile +++ b/Makefile @@ -4,7 +4,7 @@ BRANCH_NAME:=$(shell git rev-parse --abbrev-ref HEAD | tr '/' '-') # The Git commit hash COMMIT := $(shell git rev-parse HEAD) # The tag of the current commit, otherwise empty -VERSION := $(shell git describe --tags --abbrev=2 --match "v*" --match "secure-cadence*" 2>/dev/null) +VERSION := $(shell git describe --tags --abbrev=2 2>/dev/null) # Image tag: if image tag is not set, set it with version (or short commit if empty) ifeq (${IMAGE_TAG},) From 3b517a046ec646192a38c7e1f4598c6c529d5fe6 Mon Sep 17 00:00:00 2001 From: Jerome P Date: Thu, 5 Dec 2024 14:04:18 -0800 Subject: [PATCH 125/174] Enable override --- Makefile | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/Makefile b/Makefile index ac1cdc885..9963cf589 100644 --- a/Makefile +++ b/Makefile @@ -4,7 +4,7 @@ BRANCH_NAME:=$(shell git rev-parse --abbrev-ref HEAD | tr '/' '-') # The Git commit hash COMMIT := $(shell git rev-parse HEAD) # The tag of the current commit, otherwise empty -VERSION := $(shell git describe --tags --abbrev=2 2>/dev/null) +GIT_VERSION := $(shell git describe --tags --abbrev=2 2>/dev/null) # Image tag: if image tag is not set, set it with version (or short commit if empty) ifeq (${IMAGE_TAG},) @@ -15,6 +15,12 @@ ifeq (${IMAGE_TAG},) IMAGE_TAG := ${SHORT_COMMIT} endif +VERSION ?= ${IMAGE_TAG} + +ifeq ($(origin VERSION),command line) + VERSION = $(VERSION) +endif + # docker container registry export CONTAINER_REGISTRY := gcr.io/flow-container-registry export DOCKER_BUILDKIT := 1 From d82dd34be03d5dbdc37c4aaff146d56b75868689 Mon Sep 17 00:00:00 2001 From: Jerome P Date: Thu, 5 Dec 2024 14:20:10 -0800 Subject: [PATCH 126/174] Variable use consistency Ensure ldlags VERSION pass through --- Dockerfile | 4 +++- Makefile | 14 +++++++------- 2 files changed, 10 insertions(+), 8 deletions(-) diff --git a/Dockerfile b/Dockerfile index 29b2d43d4..3f8759a0a 100644 --- a/Dockerfile +++ b/Dockerfile @@ -12,8 +12,10 @@ COPY . ./ RUN go mod download RUN go mod verify +ARG VERSION + # Build binary -RUN CGO_ENABLED=1 go build -o bin cmd/main.go +RUN CGO_ENABLED=1 go build -o bin -ldflags="-X github.com/onflow/flow-evm-gateway/api.Version=$VERSION" cmd/main.go RUN chmod a+x bin # RUN APP diff --git a/Makefile b/Makefile index 9963cf589..e725b2fa5 100644 --- a/Makefile +++ b/Makefile @@ -47,7 +47,7 @@ check-tidy: .PHONY: build build: - CGO_ENABLED=1 go build -o flow-evm-gateway -ldflags="-X github.com/onflow/flow-evm-gateway/api.Version=$(shell git describe --tags --abbrev=0 2>/dev/null || echo 'unknown')" cmd/main.go + CGO_ENABLED=1 go build -o flow-evm-gateway -ldflags="-X github.com/onflow/flow-evm-gateway/api.Version=$(IMAGE_TAG)" cmd/main.go chmod a+x flow-evm-gateway .PHONY: fix-lint @@ -110,9 +110,9 @@ start-local-bin: .PHONY: docker-build docker-build: git fetch --tags - git checkout ${IMAGE_TAG} - docker build -f Dockerfile -t "$(CONTAINER_REGISTRY)/evm-gateway:${IMAGE_TAG}" -t "$(CONTAINER_REGISTRY)/evm-gateway:$(IMAGE_TAG)" \ - --label "git_commit=${COMMIT}" --label "git_tag=${IMAGE_TAG}" . + git checkout $(IMAGE_TAG) + docker build -f Dockerfile -t "$(CONTAINER_REGISTRY)/evm-gateway:$(IMAGE_TAG)" \ + --label "git_commit=$(COMMIT)" --label "git_tag=$(IMAGE_TAG)" --build-arg VERSION="$(VERSION)". # Run GW image # ----- @@ -120,6 +120,6 @@ docker-build: # https://github.com/onflow/flow-evm-gateway?tab=readme-ov-file#configuration-flags .PHONY: docker-run docker-run: - docker run --access-node-grpc-host=$ACCESS_NODE_GRPC_HOST --flow-network-id=$FLOW_NETWORK_ID \ - --init-cadence-height=$INIT_CADENCE_HEIGHT --ws-enabled=true --coinbase=$COINBASE --coa-address=$COA_ADDRESS \ - --coa-key=$COA_KEY --rate-limit=9999999 --rpc-host=0.0.0.0 \ No newline at end of file + docker run --access-node-grpc-host=$(ACCESS_NODE_GRPC_HOST) --flow-network-id=$(FLOW_NETWORK_ID) \ + --init-cadence-height=$(INIT_CADENCE_HEIGHT) --ws-enabled=true --coinbase=$(COINBASE) --coa-address=$(COA_ADDRESS) \ + --coa-key=$(COA_KEY) --rate-limit=9999999 --rpc-host=0.0.0.0 \ No newline at end of file From 8d63c9f80fbd9d965955aa2804e9bcfe211e3c9a Mon Sep 17 00:00:00 2001 From: Jerome P Date: Thu, 5 Dec 2024 14:30:40 -0800 Subject: [PATCH 127/174] nit --- Makefile | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/Makefile b/Makefile index e725b2fa5..d6e590f64 100644 --- a/Makefile +++ b/Makefile @@ -109,10 +109,8 @@ start-local-bin: # Build docker image .PHONY: docker-build docker-build: - git fetch --tags - git checkout $(IMAGE_TAG) - docker build -f Dockerfile -t "$(CONTAINER_REGISTRY)/evm-gateway:$(IMAGE_TAG)" \ - --label "git_commit=$(COMMIT)" --label "git_tag=$(IMAGE_TAG)" --build-arg VERSION="$(VERSION)". + docker build --build-arg VERSION="$(VERSION)" -f Dockerfile -t "$(CONTAINER_REGISTRY)/evm-gateway:$(IMAGE_TAG)" \ + --label "git_commit=$(COMMIT)" --label "git_tag=$(IMAGE_TAG)" . # Run GW image # ----- From 968f54ab80b60c5f1f1bb7c0bc1e49415ffa01d1 Mon Sep 17 00:00:00 2001 From: Jerome P Date: Thu, 5 Dec 2024 15:31:29 -0800 Subject: [PATCH 128/174] Correct image name --- Makefile | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/Makefile b/Makefile index d6e590f64..7a6e4388a 100644 --- a/Makefile +++ b/Makefile @@ -113,11 +113,11 @@ docker-build: --label "git_commit=$(COMMIT)" --label "git_tag=$(IMAGE_TAG)" . # Run GW image -# ----- -# Add/change required configs as needed # https://github.com/onflow/flow-evm-gateway?tab=readme-ov-file#configuration-flags +# +# Uses ENV variables to configure below .PHONY: docker-run docker-run: - docker run --access-node-grpc-host=$(ACCESS_NODE_GRPC_HOST) --flow-network-id=$(FLOW_NETWORK_ID) \ + docker run "$(CONTAINER_REGISTRY)/evm-gateway:$(IMAGE_TAG)" --access-node-grpc-host=$(ACCESS_NODE_GRPC_HOST) --flow-network-id=$(FLOW_NETWORK_ID) \ --init-cadence-height=$(INIT_CADENCE_HEIGHT) --ws-enabled=true --coinbase=$(COINBASE) --coa-address=$(COA_ADDRESS) \ --coa-key=$(COA_KEY) --rate-limit=9999999 --rpc-host=0.0.0.0 \ No newline at end of file From 1eeb2bfd4734c5ad8a7aacd8debe0dc0304aa50b Mon Sep 17 00:00:00 2001 From: Jerome P Date: Thu, 5 Dec 2024 15:47:08 -0800 Subject: [PATCH 129/174] Detach container by default --- Makefile | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Makefile b/Makefile index 7a6e4388a..55539417f 100644 --- a/Makefile +++ b/Makefile @@ -118,6 +118,6 @@ docker-build: # Uses ENV variables to configure below .PHONY: docker-run docker-run: - docker run "$(CONTAINER_REGISTRY)/evm-gateway:$(IMAGE_TAG)" --access-node-grpc-host=$(ACCESS_NODE_GRPC_HOST) --flow-network-id=$(FLOW_NETWORK_ID) \ + docker run -d "$(CONTAINER_REGISTRY)/evm-gateway:$(IMAGE_TAG)" --access-node-grpc-host=$(ACCESS_NODE_GRPC_HOST) --flow-network-id=$(FLOW_NETWORK_ID) \ --init-cadence-height=$(INIT_CADENCE_HEIGHT) --ws-enabled=true --coinbase=$(COINBASE) --coa-address=$(COA_ADDRESS) \ --coa-key=$(COA_KEY) --rate-limit=9999999 --rpc-host=0.0.0.0 \ No newline at end of file From dfb48e377e1f7788511262db62c0d37658913cec Mon Sep 17 00:00:00 2001 From: Jerome P Date: Thu, 5 Dec 2024 16:00:19 -0800 Subject: [PATCH 130/174] Update README Add previous spork host --- Makefile | 2 +- README.md | 15 +++++++++++---- 2 files changed, 12 insertions(+), 5 deletions(-) diff --git a/Makefile b/Makefile index 55539417f..ea582d29f 100644 --- a/Makefile +++ b/Makefile @@ -120,4 +120,4 @@ docker-build: docker-run: docker run -d "$(CONTAINER_REGISTRY)/evm-gateway:$(IMAGE_TAG)" --access-node-grpc-host=$(ACCESS_NODE_GRPC_HOST) --flow-network-id=$(FLOW_NETWORK_ID) \ --init-cadence-height=$(INIT_CADENCE_HEIGHT) --ws-enabled=true --coinbase=$(COINBASE) --coa-address=$(COA_ADDRESS) \ - --coa-key=$(COA_KEY) --rate-limit=9999999 --rpc-host=0.0.0.0 \ No newline at end of file + --coa-key=$(COA_KEY) --rate-limit=9999999 --rpc-host=0.0.0.0 --access-node-spork-hosts=access-001.devnet51.nodes.onflow.org:9000 \ No newline at end of file diff --git a/README.md b/README.md index 393dd6396..53f92726d 100644 --- a/README.md +++ b/README.md @@ -91,14 +91,21 @@ need funded accounts for interacting with EVM._ **With Docker** -Run the following commands: +Use the Makefile targets below with the provided ENV variables to build and run a docker image ```bash -cd dev +FLOW_NETWORK_ID=[flow-testnet | flow-mainnet] +ACCESS_NODE_GRPC_HOST= +INIT_CADENCE_HEIGHT=211176670 +COA_ADDRESS= +COA_KEY= +COINBASE= +``` -docker build -t onflow/flow-evm-gateway . +```bash +make docker-build -docker run -d -p 127.0.0.1:8545:8545 onflow/flow-evm-gateway +make docker-run ``` **Verify** From 21ab6827d253b98a1bd850fc325d69e4fe4771b6 Mon Sep 17 00:00:00 2001 From: Jerome P Date: Thu, 5 Dec 2024 16:04:48 -0800 Subject: [PATCH 131/174] Split guidance into two lines --- README.md | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/README.md b/README.md index 53f92726d..51bf69551 100644 --- a/README.md +++ b/README.md @@ -91,7 +91,7 @@ need funded accounts for interacting with EVM._ **With Docker** -Use the Makefile targets below with the provided ENV variables to build and run a docker image +Set the following ENV variables ```bash FLOW_NETWORK_ID=[flow-testnet | flow-mainnet] @@ -102,6 +102,7 @@ COA_KEY= COINBASE= ``` +Use the Makefile targets below ```bash make docker-build From c93e055a1b71d0809c82cb43690d0b254afe94fe Mon Sep 17 00:00:00 2001 From: Ardit Marku Date: Fri, 6 Dec 2024 12:09:50 +0200 Subject: [PATCH 132/174] Return empty Logs array when block is empty --- api/api.go | 11 ++- storage/index_test.go | 4 +- storage/pebble/receipts.go | 8 ++- tests/e2e_web3js_test.go | 18 ++++- tests/web3js/eth_logs_filtering_test.js | 90 ++++++++++++++++++++++++- 5 files changed, 124 insertions(+), 7 deletions(-) diff --git a/api/api.go b/api/api.go index ddac3371e..a80f6164a 100644 --- a/api/api.go +++ b/api/api.go @@ -673,6 +673,16 @@ func (b *BlockChainAPI) GetLogs( // if filter provided specific block ID if criteria.BlockHash != nil { + // Check if the block exists, and return an error if not. + block, err := b.blocks.GetByID(*criteria.BlockHash) + if err != nil { + return nil, err + } + // If the block has no transactions, we can simply return an empty Logs array. + if len(block.TransactionHashes) == 0 { + return []*types.Log{}, nil + } + f, err := logs.NewIDFilter(*criteria.BlockHash, filter, b.blocks, b.receipts) if err != nil { return handleError[[]*types.Log](err, l, b.collector) @@ -687,7 +697,6 @@ func (b *BlockChainAPI) GetLogs( } // otherwise we use the block range as the filter - // assign default values to latest block number, unless provided from := models.LatestBlockNumber if criteria.FromBlock != nil { diff --git a/storage/index_test.go b/storage/index_test.go index 207873d9a..fdf7f881b 100644 --- a/storage/index_test.go +++ b/storage/index_test.go @@ -385,8 +385,8 @@ func (s *ReceiptTestSuite) TestGetReceiptByBlockHeight() { s.Run("non-existing block height", func() { retReceipt, err := s.ReceiptIndexer.GetByBlockHeight(1337) - s.Require().Nil(retReceipt) - s.Require().ErrorIs(err, errors.ErrEntityNotFound) + s.Require().NoError(err) + s.Require().Len(retReceipt, 0) }) } diff --git a/storage/pebble/receipts.go b/storage/pebble/receipts.go index 5a3ffec68..e1bc56fb1 100644 --- a/storage/pebble/receipts.go +++ b/storage/pebble/receipts.go @@ -2,6 +2,7 @@ package pebble import ( "encoding/binary" + "errors" "fmt" "github.com/cockroachdb/pebble" @@ -113,9 +114,14 @@ func (r *Receipts) GetByBlockHeight(height uint64) ([]*models.Receipt, error) { } func (r *Receipts) getByBlockHeight(height []byte) ([]*models.Receipt, error) { - val, err := r.store.get(receiptHeightKey, height) if err != nil { + // For empty blocks, we do not store transactions & receipts. So when + // we encounter an `ErrEntityNotFound`, we should return an empty + // Receipts array, instead of an error. + if errors.Is(err, errs.ErrEntityNotFound) { + return []*models.Receipt{}, nil + } return nil, err } diff --git a/tests/e2e_web3js_test.go b/tests/e2e_web3js_test.go index 881c76abd..96ba5d9e9 100644 --- a/tests/e2e_web3js_test.go +++ b/tests/e2e_web3js_test.go @@ -89,7 +89,23 @@ func TestWeb3_E2E(t *testing.T) { }) t.Run("logs emitting and filtering", func(t *testing.T) { - runWeb3Test(t, "eth_logs_filtering_test") + runWeb3TestWithSetup(t, "eth_logs_filtering_test", func(emu emulator.Emulator) { + // Run an arbitrary transaction, to form an empty EVM block + // through the system chunk transaction. This is needed + // to emulate the `eth_getLogs` by passing the block hash + // of a block without EVM transactions/receipts. + res, err := flowSendTransaction( + emu, + `transaction() { + prepare(signer: auth(Storage) &Account) { + let currentBlock = getCurrentBlock() + assert(currentBlock.height > 0, message: "current block is zero") + } + }`, + ) + require.NoError(t, err) + require.NoError(t, res.Error) + }) }) t.Run("test filter-related endpoints", func(t *testing.T) { diff --git a/tests/web3js/eth_logs_filtering_test.js b/tests/web3js/eth_logs_filtering_test.js index 7e96e3f4e..672fec1f7 100644 --- a/tests/web3js/eth_logs_filtering_test.js +++ b/tests/web3js/eth_logs_filtering_test.js @@ -1,11 +1,43 @@ const { assert } = require('chai') const conf = require('./config') const helpers = require('./helpers') +const web3 = conf.web3 it('emit logs and retrieve them using different filters', async () => { - setTimeout(() => process.exit(1), 19 * 1000) // hack if the ws connection is not closed + let latestBlockNumber = await web3.eth.getBlockNumber() + let latestBlock = await web3.eth.getBlock(latestBlockNumber) - let deployed = await helpers.deployContract("storage") + let blockHashFilter = { + blockHash: latestBlock.hash, + address: ['0x0000000071727de22e5e9d8baf0edac6f37da032'], + topics: ['0x2da466a7b24304f47e87fa2e1e5a81b9831ce54fec19055ce277ca2f39ba42c4'] + } + let response = await helpers.callRPCMethod('eth_getLogs', [blockHashFilter]) + assert.equal(response.status, 200) + assert.isDefined(response.body) + assert.deepEqual(response.body.result, []) + + blockHashFilter.blockHash = '0x048641726d25605a990c439b75fcfaa5f6b1691eaa718b72dd71e02a2264f5da' + response = await helpers.callRPCMethod('eth_getLogs', [blockHashFilter]) + assert.equal(response.status, 200) + assert.isDefined(response.body.error) + assert.equal( + response.body.error.message, + 'failed to get EVM block by ID: ' + blockHashFilter.blockHash + ', with: entity not found' + ) + + let blockRangeFilter = { + fromBlock: '0x1', + toBlock: 'latest', + address: ['0x0000000071727de22e5e9d8baf0edac6f37da032'], + topics: ['0x2da466a7b24304f47e87fa2e1e5a81b9831ce54fec19055ce277ca2f39ba42c4'] + } + response = await helpers.callRPCMethod('eth_getLogs', [blockRangeFilter]) + assert.equal(response.status, 200) + assert.isDefined(response.body) + assert.deepEqual(response.body.result, []) + + let deployed = await helpers.deployContract('storage') let contractAddress = deployed.receipt.contractAddress let repeatA = 10 @@ -81,4 +113,58 @@ it('emit logs and retrieve them using different filters', async () => { assert.lengthOf(events, 2) assert.equal(events[0].returnValues.numB, 2) assert.equal(events[1].returnValues.numB, -2) + + latestBlockNumber = await web3.eth.getBlockNumber() + latestBlock = await web3.eth.getBlock(latestBlockNumber) + + filterCriteria = { + blockHash: latestBlock.hash, + address: [contractAddress], + topics: [ + '0x76efea95e5da1fa661f235b2921ae1d89b99e457ec73fb88e34a1d150f95c64b', + '0x000000000000000000000000facf71692421039876a5bb4f10ef7a439d8ef61e', + '0x000000000000000000000000000000000000000000000000000000000000000a', + '0x0000000000000000000000000000000000000000000000000000000000000190' + ] + } + response = await helpers.callRPCMethod('eth_getLogs', [filterCriteria]) + assert.equal(response.status, 200) + assert.isDefined(response.body) + assert.deepEqual( + response.body.result, + [ + { + address: '0x99a64c993965f8d69f985b5171bc20065cc32fab', + topics: [ + '0x76efea95e5da1fa661f235b2921ae1d89b99e457ec73fb88e34a1d150f95c64b', + '0x000000000000000000000000facf71692421039876a5bb4f10ef7a439d8ef61e', + '0x000000000000000000000000000000000000000000000000000000000000000a', + '0x0000000000000000000000000000000000000000000000000000000000000190' + ], + data: '0x000000000000000000000000000000000000000000000000000000000000019a', + blockNumber: '0xa', + transactionHash: '0x0c2b2477ab81c9132c5c4fd4f50935bc5807fbf4cf3bf3b69173491b68d2ca8b', + transactionIndex: '0x0', + blockHash: latestBlock.hash, + logIndex: '0x0', + removed: false + } + ] + ) + + blockRangeCriteria = { + fromBlock: web3.utils.numberToHex(latestBlock.number), + toBlock: web3.utils.numberToHex(latestBlock.number), + address: [contractAddress], + topics: [ + '0x76efea95e5da1fa661f235b2921ae1d89b99e457ec73fb88e34a1d150f95c64b', + '0x000000000000000000000000facf71692421039876a5bb4f10ef7a439d8ef61e', + '0x000000000000000000000000000000000000000000000000000000000000000a', + '0x0000000000000000000000000000000000000000000000000000000000000190' + ] + } + response = await helpers.callRPCMethod('eth_getLogs', [blockRangeFilter]) + assert.equal(response.status, 200) + assert.isDefined(response.body) + assert.deepEqual(response.body.result, []) }) From 52e0b17bdac5de879714e442b96e55754fbd18a1 Mon Sep 17 00:00:00 2001 From: Ardit Marku Date: Fri, 6 Dec 2024 21:16:51 +0200 Subject: [PATCH 133/174] Use proper variable in E2E test file for logs filtering --- tests/web3js/eth_logs_filtering_test.js | 28 +++++++++++++++++++++---- 1 file changed, 24 insertions(+), 4 deletions(-) diff --git a/tests/web3js/eth_logs_filtering_test.js b/tests/web3js/eth_logs_filtering_test.js index 672fec1f7..7b798b2d7 100644 --- a/tests/web3js/eth_logs_filtering_test.js +++ b/tests/web3js/eth_logs_filtering_test.js @@ -117,7 +117,7 @@ it('emit logs and retrieve them using different filters', async () => { latestBlockNumber = await web3.eth.getBlockNumber() latestBlock = await web3.eth.getBlock(latestBlockNumber) - filterCriteria = { + blockHashFilter = { blockHash: latestBlock.hash, address: [contractAddress], topics: [ @@ -127,7 +127,7 @@ it('emit logs and retrieve them using different filters', async () => { '0x0000000000000000000000000000000000000000000000000000000000000190' ] } - response = await helpers.callRPCMethod('eth_getLogs', [filterCriteria]) + response = await helpers.callRPCMethod('eth_getLogs', [blockHashFilter]) assert.equal(response.status, 200) assert.isDefined(response.body) assert.deepEqual( @@ -152,7 +152,7 @@ it('emit logs and retrieve them using different filters', async () => { ] ) - blockRangeCriteria = { + blockRangeFilter = { fromBlock: web3.utils.numberToHex(latestBlock.number), toBlock: web3.utils.numberToHex(latestBlock.number), address: [contractAddress], @@ -166,5 +166,25 @@ it('emit logs and retrieve them using different filters', async () => { response = await helpers.callRPCMethod('eth_getLogs', [blockRangeFilter]) assert.equal(response.status, 200) assert.isDefined(response.body) - assert.deepEqual(response.body.result, []) + assert.deepEqual( + response.body.result, + [ + { + address: '0x99a64c993965f8d69f985b5171bc20065cc32fab', + topics: [ + '0x76efea95e5da1fa661f235b2921ae1d89b99e457ec73fb88e34a1d150f95c64b', + '0x000000000000000000000000facf71692421039876a5bb4f10ef7a439d8ef61e', + '0x000000000000000000000000000000000000000000000000000000000000000a', + '0x0000000000000000000000000000000000000000000000000000000000000190' + ], + data: '0x000000000000000000000000000000000000000000000000000000000000019a', + blockNumber: '0xa', + transactionHash: '0x0c2b2477ab81c9132c5c4fd4f50935bc5807fbf4cf3bf3b69173491b68d2ca8b', + transactionIndex: '0x0', + blockHash: latestBlock.hash, + logIndex: '0x0', + removed: false + } + ] + ) }) From 334a45f270fee47aa1950e906e51e88f4719a15d Mon Sep 17 00:00:00 2001 From: Jerome P Date: Fri, 6 Dec 2024 16:02:16 -0800 Subject: [PATCH 134/174] Account for change made in Dockerfile to add $VERSION --- .github/workflows/build.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 677b42f99..c291290c0 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -40,5 +40,5 @@ jobs: - name: Docker Auth run: |- gcloud auth configure-docker ${{ vars.GAR_LOCATION }}-docker.pkg.dev - docker build -t ${{ env.DOCKER_IMAGE_URL }}:${{ steps.set_version.outputs.GATEWAY_VERSION }} --build-arg GATEWAY_VERSION=${{ steps.set_version.outputs.GATEWAY_VERSION }} --file Dockerfile . + docker build --build-arg VERSION="${{ steps.set_version.outputs.GATEWAY_VERSION }}" -t ${{ env.DOCKER_IMAGE_URL }}:${{ steps.set_version.outputs.GATEWAY_VERSION }} --build-arg GATEWAY_VERSION=${{ steps.set_version.outputs.GATEWAY_VERSION }} --file Dockerfile . docker push ${{ env.DOCKER_IMAGE_URL }}:${{ steps.set_version.outputs.GATEWAY_VERSION }} From 0679ffc7628e1b918d98a16588366b158769dd9c Mon Sep 17 00:00:00 2001 From: Jerome P Date: Fri, 6 Dec 2024 17:44:16 -0800 Subject: [PATCH 135/174] Improve usability and code --- Makefile | 55 +++++++++++++++++++++++++++++++++++++++++++++++++------ 1 file changed, 49 insertions(+), 6 deletions(-) diff --git a/Makefile b/Makefile index ea582d29f..1a0e38ed7 100644 --- a/Makefile +++ b/Makefile @@ -5,6 +5,20 @@ BRANCH_NAME:=$(shell git rev-parse --abbrev-ref HEAD | tr '/' '-') COMMIT := $(shell git rev-parse HEAD) # The tag of the current commit, otherwise empty GIT_VERSION := $(shell git describe --tags --abbrev=2 2>/dev/null) +CMD_ARGS := +# ACCESS_NODE_SPORK_HOSTS are space separated +ACCESS_NODE_SPORK_HOSTS := access-001.devnet51.nodes.onflow.org:9000 + +# Function to check and append required arguments +define check_and_append +$(if $($(2)),\ + $(eval CMD_ARGS += --$(1)=$($(2))),\ + $(error ERROR: $(2) ENV variable is required)) +endef + +define append_spork_hosts +$(foreach host,$(ACCESS_NODE_SPORK_HOSTS),$(eval CMD_ARGS += --access-node-spork-hosts=$(host))) +endef # Image tag: if image tag is not set, set it with version (or short commit if empty) ifeq (${IMAGE_TAG},) @@ -18,11 +32,11 @@ endif VERSION ?= ${IMAGE_TAG} ifeq ($(origin VERSION),command line) - VERSION = $(VERSION) +VERSION = $(VERSION) endif # docker container registry -export CONTAINER_REGISTRY := gcr.io/flow-container-registry +export CONTAINER_REGISTRY := us-west1-docker.pkg.dev/dl-flow-devex-production/development export DOCKER_BUILDKIT := 1 .PHONY: test @@ -115,9 +129,38 @@ docker-build: # Run GW image # https://github.com/onflow/flow-evm-gateway?tab=readme-ov-file#configuration-flags # -# Uses ENV variables to configure below +# IMPORTANT: Ensure these environment variables are properly secured in production +# Never commit actual values to version control .PHONY: docker-run docker-run: - docker run -d "$(CONTAINER_REGISTRY)/evm-gateway:$(IMAGE_TAG)" --access-node-grpc-host=$(ACCESS_NODE_GRPC_HOST) --flow-network-id=$(FLOW_NETWORK_ID) \ - --init-cadence-height=$(INIT_CADENCE_HEIGHT) --ws-enabled=true --coinbase=$(COINBASE) --coa-address=$(COA_ADDRESS) \ - --coa-key=$(COA_KEY) --rate-limit=9999999 --rpc-host=0.0.0.0 --access-node-spork-hosts=access-001.devnet51.nodes.onflow.org:9000 \ No newline at end of file + MODE=-it + $(eval CMD_ARGS :=) +ifdef DOCKER_RUN_DETACHED + $(eval MODE=-d) +else + $(eval MODE=-it) +endif + + $(call check_and_append,access-node-grpc-host,ACCESS_NODE_GRPC_HOST) + $(call check_and_append,flow-network-id,FLOW_NETWORK_ID) + $(call check_and_append,init-cadence-height,INIT_CADENCE_HEIGHT) + $(call check_and_append,coinbase,COINBASE) + $(call check_and_append,coa-address,COA_ADDRESS) + $(call check_and_append,coa-key,COA_KEY) + $(call append_spork_hosts) + + $(eval CMD_ARGS += --ws-enabled=true --rate-limit=9999999 --rpc-host=0.0.0.0) + + $(call append_spork_hosts) + + @if [ -z "$(DOCKER_MOUNT)" ]; then \ + echo "Hello" \ + else \ + MOUNT_ARG="-v $(DOCKER_MOUNT):/flow-evm-gateway"; \ + fi + +ifdef MOUNT_ARG + $(eval CMD_ARGS += $(MOUNT_ARG)) +endif + + docker run $(MODE) "$(CONTAINER_REGISTRY)/evm-gateway:$(IMAGE_TAG)" $(CMD_ARGS) $(MOUNT_ARG) \ No newline at end of file From 3d0973f6a199a63177c32ba402b60ee165b278a2 Mon Sep 17 00:00:00 2001 From: Jerome P Date: Fri, 6 Dec 2024 18:24:17 -0800 Subject: [PATCH 136/174] Tidyup --- Makefile | 5 ----- 1 file changed, 5 deletions(-) diff --git a/Makefile b/Makefile index 1a0e38ed7..894384a0e 100644 --- a/Makefile +++ b/Makefile @@ -129,16 +129,12 @@ docker-build: # Run GW image # https://github.com/onflow/flow-evm-gateway?tab=readme-ov-file#configuration-flags # -# IMPORTANT: Ensure these environment variables are properly secured in production -# Never commit actual values to version control .PHONY: docker-run docker-run: MODE=-it $(eval CMD_ARGS :=) ifdef DOCKER_RUN_DETACHED $(eval MODE=-d) -else - $(eval MODE=-it) endif $(call check_and_append,access-node-grpc-host,ACCESS_NODE_GRPC_HOST) @@ -154,7 +150,6 @@ endif $(call append_spork_hosts) @if [ -z "$(DOCKER_MOUNT)" ]; then \ - echo "Hello" \ else \ MOUNT_ARG="-v $(DOCKER_MOUNT):/flow-evm-gateway"; \ fi From 40b24a1158f7900cec1cd2f521a4c85c15f8d5d1 Mon Sep 17 00:00:00 2001 From: Jerome P Date: Fri, 6 Dec 2024 18:35:03 -0800 Subject: [PATCH 137/174] Fix mount issue --- Makefile | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Makefile b/Makefile index 894384a0e..fdedb15f7 100644 --- a/Makefile +++ b/Makefile @@ -131,7 +131,6 @@ docker-build: # .PHONY: docker-run docker-run: - MODE=-it $(eval CMD_ARGS :=) ifdef DOCKER_RUN_DETACHED $(eval MODE=-d) @@ -150,6 +149,7 @@ endif $(call append_spork_hosts) @if [ -z "$(DOCKER_MOUNT)" ]; then \ + echo \ else \ MOUNT_ARG="-v $(DOCKER_MOUNT):/flow-evm-gateway"; \ fi From aa76d88f27f5d888e31011fe273b1cc1e43f6a12 Mon Sep 17 00:00:00 2001 From: Jerome P Date: Fri, 6 Dec 2024 21:29:37 -0800 Subject: [PATCH 138/174] Fix mount issue --- Makefile | 16 +++++++--------- 1 file changed, 7 insertions(+), 9 deletions(-) diff --git a/Makefile b/Makefile index fdedb15f7..fb3b04c1b 100644 --- a/Makefile +++ b/Makefile @@ -148,14 +148,12 @@ endif $(call append_spork_hosts) - @if [ -z "$(DOCKER_MOUNT)" ]; then \ - echo \ - else \ - MOUNT_ARG="-v $(DOCKER_MOUNT):/flow-evm-gateway"; \ - fi - -ifdef MOUNT_ARG - $(eval CMD_ARGS += $(MOUNT_ARG)) +ifdef DOCKER_MOUNT + $(eval MOUNT=--mount type=bind,src="$(DOCKER_MOUNT)",target=/flow-evm-gateway) endif - docker run $(MODE) "$(CONTAINER_REGISTRY)/evm-gateway:$(IMAGE_TAG)" $(CMD_ARGS) $(MOUNT_ARG) \ No newline at end of file +ifdef MOUNT + $(eval CMD_ARGS += $(MOUNT)) +endif + + docker run $(MODE) $(MOUNT) "$(CONTAINER_REGISTRY)/evm-gateway:$(IMAGE_TAG)" $(CMD_ARGS) \ No newline at end of file From 46bbe75d6f2b0f9b3be8dc47212a321aafb05dae Mon Sep 17 00:00:00 2001 From: Jerome P Date: Fri, 6 Dec 2024 21:37:17 -0800 Subject: [PATCH 139/174] Fix mount issue --- Makefile | 6 +----- 1 file changed, 1 insertion(+), 5 deletions(-) diff --git a/Makefile b/Makefile index fb3b04c1b..adfad09f8 100644 --- a/Makefile +++ b/Makefile @@ -152,8 +152,4 @@ ifdef DOCKER_MOUNT $(eval MOUNT=--mount type=bind,src="$(DOCKER_MOUNT)",target=/flow-evm-gateway) endif -ifdef MOUNT - $(eval CMD_ARGS += $(MOUNT)) -endif - - docker run $(MODE) $(MOUNT) "$(CONTAINER_REGISTRY)/evm-gateway:$(IMAGE_TAG)" $(CMD_ARGS) \ No newline at end of file + docker run $(MODE) -p 127.0.0.1:8545:8545 -w /flow-evm-gateway $(MOUNT) "$(CONTAINER_REGISTRY)/evm-gateway:$(IMAGE_TAG)" $(CMD_ARGS) \ No newline at end of file From 61b09773e37586a1c5a5f832a17ed88ba12d32a6 Mon Sep 17 00:00:00 2001 From: Jerome P Date: Fri, 6 Dec 2024 21:54:15 -0800 Subject: [PATCH 140/174] More troubleshooting mount --- Makefile | 14 +++++++++----- 1 file changed, 9 insertions(+), 5 deletions(-) diff --git a/Makefile b/Makefile index adfad09f8..5ac9fb7e7 100644 --- a/Makefile +++ b/Makefile @@ -135,6 +135,14 @@ docker-run: ifdef DOCKER_RUN_DETACHED $(eval MODE=-d) endif +ifdef DOCKER_MOUNT + $(eval MOUNT=--mount type=bind,src="$(DOCKER_MOUNT)",target=/flow-evm-gateway) +endif +ifdef DOCKER_HOST_PORT + $(eval HOST_PORT=$(DOCKER_HOST_PORT)) +else + $(eval HOST_PORT=8545) +endif $(call check_and_append,access-node-grpc-host,ACCESS_NODE_GRPC_HOST) $(call check_and_append,flow-network-id,FLOW_NETWORK_ID) @@ -148,8 +156,4 @@ endif $(call append_spork_hosts) -ifdef DOCKER_MOUNT - $(eval MOUNT=--mount type=bind,src="$(DOCKER_MOUNT)",target=/flow-evm-gateway) -endif - - docker run $(MODE) -p 127.0.0.1:8545:8545 -w /flow-evm-gateway $(MOUNT) "$(CONTAINER_REGISTRY)/evm-gateway:$(IMAGE_TAG)" $(CMD_ARGS) \ No newline at end of file + docker run $(MODE) -p 127.0.0.1:$(HOST_PORT):8545 $(MOUNT) "$(CONTAINER_REGISTRY)/evm-gateway:$(IMAGE_TAG)" $(CMD_ARGS) \ No newline at end of file From a2d17924c8cd79de024342c26dc3d8a6fa798c4c Mon Sep 17 00:00:00 2001 From: Jerome P Date: Fri, 6 Dec 2024 22:01:33 -0800 Subject: [PATCH 141/174] More troubleshooting mount --- Makefile | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Makefile b/Makefile index 5ac9fb7e7..6e9c33568 100644 --- a/Makefile +++ b/Makefile @@ -136,7 +136,7 @@ ifdef DOCKER_RUN_DETACHED $(eval MODE=-d) endif ifdef DOCKER_MOUNT - $(eval MOUNT=--mount type=bind,src="$(DOCKER_MOUNT)",target=/flow-evm-gateway) + $(eval MOUNT=--mount type=bind,src="$(DOCKER_MOUNT)",target=/) endif ifdef DOCKER_HOST_PORT $(eval HOST_PORT=$(DOCKER_HOST_PORT)) From ea3e5cdde8994fd5a236444c674267ef986cbff9 Mon Sep 17 00:00:00 2001 From: Jerome P Date: Fri, 6 Dec 2024 22:13:04 -0800 Subject: [PATCH 142/174] Fix for mount --- Makefile | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/Makefile b/Makefile index 6e9c33568..e142072a2 100644 --- a/Makefile +++ b/Makefile @@ -135,14 +135,15 @@ docker-run: ifdef DOCKER_RUN_DETACHED $(eval MODE=-d) endif -ifdef DOCKER_MOUNT - $(eval MOUNT=--mount type=bind,src="$(DOCKER_MOUNT)",target=/) -endif ifdef DOCKER_HOST_PORT $(eval HOST_PORT=$(DOCKER_HOST_PORT)) else $(eval HOST_PORT=8545) endif +ifdef DOCKER_MOUNT + $(eval MOUNT=--mount type=bind,src="$(DOCKER_MOUNT)",target=/data) + $(call check_and_append,database-dir,DOCKER_MOUNT) +endif $(call check_and_append,access-node-grpc-host,ACCESS_NODE_GRPC_HOST) $(call check_and_append,flow-network-id,FLOW_NETWORK_ID) From dc9daf28864e0960bf9cdac2a09a11a1316ea641 Mon Sep 17 00:00:00 2001 From: Jerome P Date: Fri, 6 Dec 2024 22:15:40 -0800 Subject: [PATCH 143/174] Fix for mount --- Makefile | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Makefile b/Makefile index e142072a2..eae34367d 100644 --- a/Makefile +++ b/Makefile @@ -142,7 +142,7 @@ else endif ifdef DOCKER_MOUNT $(eval MOUNT=--mount type=bind,src="$(DOCKER_MOUNT)",target=/data) - $(call check_and_append,database-dir,DOCKER_MOUNT) + $(call check_and_append,database-dir,/data) endif $(call check_and_append,access-node-grpc-host,ACCESS_NODE_GRPC_HOST) From 7082eec35d5e69a283c63331ce8fc0598ec6d7a5 Mon Sep 17 00:00:00 2001 From: Jerome P Date: Fri, 6 Dec 2024 22:43:33 -0800 Subject: [PATCH 144/174] Resolved mount issue --- Makefile | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/Makefile b/Makefile index eae34367d..dd8954221 100644 --- a/Makefile +++ b/Makefile @@ -38,6 +38,7 @@ endif # docker container registry export CONTAINER_REGISTRY := us-west1-docker.pkg.dev/dl-flow-devex-production/development export DOCKER_BUILDKIT := 1 +export DATADIR := /data .PHONY: test test: @@ -141,8 +142,8 @@ else $(eval HOST_PORT=8545) endif ifdef DOCKER_MOUNT - $(eval MOUNT=--mount type=bind,src="$(DOCKER_MOUNT)",target=/data) - $(call check_and_append,database-dir,/data) + $(eval MOUNT=--mount type=bind,src="$(DOCKER_MOUNT)",target=$(datadir)) + $(call check_and_append,database-dir,DATADIR) endif $(call check_and_append,access-node-grpc-host,ACCESS_NODE_GRPC_HOST) From fdad063a9ed9a813581d13332c3d59e9169b9a8b Mon Sep 17 00:00:00 2001 From: Jerome P Date: Fri, 6 Dec 2024 22:45:47 -0800 Subject: [PATCH 145/174] Resolved mount issue --- Makefile | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Makefile b/Makefile index dd8954221..92742ccf3 100644 --- a/Makefile +++ b/Makefile @@ -142,7 +142,7 @@ else $(eval HOST_PORT=8545) endif ifdef DOCKER_MOUNT - $(eval MOUNT=--mount type=bind,src="$(DOCKER_MOUNT)",target=$(datadir)) + $(eval MOUNT=--mount type=bind,src="$(DOCKER_MOUNT)",target=$(DATADIR)) $(call check_and_append,database-dir,DATADIR) endif From b0207f138f790d0890a093a98281a6da93ba23d0 Mon Sep 17 00:00:00 2001 From: Jerome P Date: Sat, 7 Dec 2024 08:33:03 -0800 Subject: [PATCH 146/174] Fix port access from other sources --- Makefile | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Makefile b/Makefile index 92742ccf3..64aa4d4b9 100644 --- a/Makefile +++ b/Makefile @@ -158,4 +158,4 @@ endif $(call append_spork_hosts) - docker run $(MODE) -p 127.0.0.1:$(HOST_PORT):8545 $(MOUNT) "$(CONTAINER_REGISTRY)/evm-gateway:$(IMAGE_TAG)" $(CMD_ARGS) \ No newline at end of file + docker run $(MODE) -p $(HOST_PORT):8545 $(MOUNT) "$(CONTAINER_REGISTRY)/evm-gateway:$(IMAGE_TAG)" $(CMD_ARGS) \ No newline at end of file From 7e6c63e05782cad44c0ed4c4f7b76c98425e23b9 Mon Sep 17 00:00:00 2001 From: Jerome P Date: Sat, 7 Dec 2024 12:49:44 -0800 Subject: [PATCH 147/174] Minimize heavy logging --- Makefile | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Makefile b/Makefile index 64aa4d4b9..56f2d9a41 100644 --- a/Makefile +++ b/Makefile @@ -154,7 +154,7 @@ endif $(call check_and_append,coa-key,COA_KEY) $(call append_spork_hosts) - $(eval CMD_ARGS += --ws-enabled=true --rate-limit=9999999 --rpc-host=0.0.0.0) + $(eval CMD_ARGS += --ws-enabled=true --rate-limit=9999999 --rpc-host=0.0.0.0 --log-level=info) $(call append_spork_hosts) From a8fbddea3627df98cfe653be6043505d9230885d Mon Sep 17 00:00:00 2001 From: Jerome P Date: Sun, 8 Dec 2024 11:22:41 -0800 Subject: [PATCH 148/174] WIP updates to README Sort out local docker targets --- Makefile | 58 +++++++++++++++++++++++++++++++++++++------- README.md | 32 ++++++++++++++---------- dev/Dockerfile | 6 ++--- dev/run.sh | 16 ------------ evm-gateway-arch.svg | 10 ++++++++ 5 files changed, 80 insertions(+), 42 deletions(-) delete mode 100755 dev/run.sh create mode 100644 evm-gateway-arch.svg diff --git a/Makefile b/Makefile index 56f2d9a41..0649b5313 100644 --- a/Makefile +++ b/Makefile @@ -8,6 +8,9 @@ GIT_VERSION := $(shell git describe --tags --abbrev=2 2>/dev/null) CMD_ARGS := # ACCESS_NODE_SPORK_HOSTS are space separated ACCESS_NODE_SPORK_HOSTS := access-001.devnet51.nodes.onflow.org:9000 +EMULATOR_COINBASE := FACF71692421039876a5BB4F10EF7A439D8ef61E +EMULATOR_COA_ADDRESS := f8d6e0586b0a20c7 +EMULATOR_COA_KEY := 2619878f0e2ff438d17835c2a4561cb87b4d24d72d12ec34569acd0dd4af7c21 # Function to check and append required arguments define check_and_append @@ -94,9 +97,9 @@ start-local: rm -rf metrics/data/ go run cmd/main.go run \ --flow-network-id=flow-emulator \ - --coinbase=FACF71692421039876a5BB4F10EF7A439D8ef61E \ - --coa-address=f8d6e0586b0a20c7 \ - --coa-key=2619878f0e2ff438d17835c2a4561cb87b4d24d72d12ec34569acd0dd4af7c21 \ + --coinbase=$(EMULATOR_COINBASE) \ + --coa-address=$(EMULATOR_COA_ADDRESS) \ + --coa-key=$(EMULATOR_COA_KEYE) \ --wallet-api-key=2619878f0e2ff438d17835c2a4561cb87b4d24d72d12ec34569acd0dd4af7c21 \ --coa-resource-create=true \ --gas-price=0 \ @@ -111,9 +114,9 @@ start-local-bin: rm -rf metrics/data/ ./flow-evm-gateway run \ --flow-network-id=flow-emulator \ - --coinbase=FACF71692421039876a5BB4F10EF7A439D8ef61E \ - --coa-address=f8d6e0586b0a20c7 \ - --coa-key=2619878f0e2ff438d17835c2a4561cb87b4d24d72d12ec34569acd0dd4af7c21 \ + --coinbase=$(EMULATOR_COINBASE) \ + --coa-address=$(EMULATOR_COA_ADDRESS) \ + --coa-key=$(EMULATOR_COA_KEYE) \ --wallet-api-key=2619878f0e2ff438d17835c2a4561cb87b4d24d72d12ec34569acd0dd4af7c21 \ --coa-resource-create=true \ --gas-price=0 \ @@ -121,15 +124,52 @@ start-local-bin: --profiler-enabled=true \ --profiler-port=6060 -# Build docker image +# Build docker image from local sources +.PHONY: docker-local-build +docker-local-build: + docker build --build-arg VERSION="$(VERSION)" -f dev/Dockerfile -t "$(CONTAINER_REGISTRY)/evm-gateway:$(IMAGE_TAG)" \ + --label "git_commit=$(COMMIT)" --label "git_tag=$(IMAGE_TAG)" . + +# Docker run for local development +.PHONY: docker-run-local +docker-run-local: + flow emulator + sleep 5 + + $(call check_and_append,coinbase,EMULATOR_COINBASE) + $(call check_and_append,coa-address,EMULATOR_COA_ADDRESS) + $(call check_and_append,coa-key,EMULATOR_COA_KEY) + + $(eval CMD_ARGS += --flow-network-id=flow-emulator --log-level=debug --coa-resource-create=true --gas-price=0 --log-writer=console --rpc-host=0.0.0.0 --profiler-enabled=true) + + docker run -p $(HOST_PORT):8545 "$(CONTAINER_REGISTRY)/evm-gateway:$(IMAGE_TAG)" $(CMD_ARGS) + + +# Build docker image for release .PHONY: docker-build docker-build: docker build --build-arg VERSION="$(VERSION)" -f Dockerfile -t "$(CONTAINER_REGISTRY)/evm-gateway:$(IMAGE_TAG)" \ --label "git_commit=$(COMMIT)" --label "git_tag=$(IMAGE_TAG)" . +# Install image version from container registry +.PHONY: docker-pull-version +docker-pull-version: + + # Run GW image # https://github.com/onflow/flow-evm-gateway?tab=readme-ov-file#configuration-flags +# Requires the following ENV variables: +# - ACCESS_NODE_GRPC_HOST: [access.devnet.nodes.onflow.org:9000 | access.mainnet.nodes.onflow.org:9000] +# - FLOW_NETWORK_ID: [flow-testnet, flow-mainnet] +# - INIT_CADENCE_HEIGHT: [testnet: 211176670, mainnet: 88226267] +# - COINBASE: To be set by the operator. This is an EVM EOA or COA address which is set as the receiver of GW transaction fees (remove 0x prefix) +# - COA_ADDRESS: To be set by the operator. This is a Cadence address which funds gateway operations (remove 0x prefix) +# - COA_KEY: A full weight, private key belonging to operator COA_ADDRESS (remove 0x prefix). NB: For development use only. We recommend using cloud KMS configuration on mainnet # +# Optional make arguments: +# - DOCKER_RUN_DETACHED: Runs container in detached mode when true +# - DOCKER_HOST_PORT: Sets the exposed container port for the gateway JSON-RPC +# - DOCKER_MOUNT: Sets the host mount point for the EVM data dir .PHONY: docker-run docker-run: $(eval CMD_ARGS :=) @@ -152,10 +192,10 @@ endif $(call check_and_append,coinbase,COINBASE) $(call check_and_append,coa-address,COA_ADDRESS) $(call check_and_append,coa-key,COA_KEY) - $(call append_spork_hosts) $(eval CMD_ARGS += --ws-enabled=true --rate-limit=9999999 --rpc-host=0.0.0.0 --log-level=info) $(call append_spork_hosts) - docker run $(MODE) -p $(HOST_PORT):8545 $(MOUNT) "$(CONTAINER_REGISTRY)/evm-gateway:$(IMAGE_TAG)" $(CMD_ARGS) \ No newline at end of file + docker run $(MODE) -p $(HOST_PORT):8545 $(MOUNT) "$(CONTAINER_REGISTRY)/evm-gateway:$(IMAGE_TAG)" $(CMD_ARGS) + diff --git a/README.md b/README.md index 51bf69551..a6fd584d0 100644 --- a/README.md +++ b/README.md @@ -6,33 +6,33 @@ EVM Gateway implements the Ethereum JSON-RPC API for [EVM on Flow](https://developers.flow.com/evm/about) which conforms to the Ethereum [JSON-RPC specification](https://ethereum.github.io/execution-apis/api-documentation/). The EVM Gateway is tailored for integration with the EVM environment on the Flow blockchain. Rather than implementing the full `geth` stack, the JSON-RPC API available in EVM Gateway is a lightweight implementation that uses Flow's underlying consensus and smart contract language, [Cadence](https://cadence-lang.org/docs/), to handle calls received by the EVM Gateway. For those interested in the underlying implementation details, please refer to the [FLIP #243](https://github.com/onflow/flips/issues/243) (EVM Gateway) and [FLIP #223](https://github.com/onflow/flips/issues/223) (EVM on Flow Core) improvement proposals. -EVM Gateway is compatible with the majority of standard Ethereum JSON-RPC APIs allowing seamless integration with existing Ethereum-compatible web3 tools via HTTP. EVM Gateway honors Ethereum's JSON-RPC namespace system, grouping RPC methods into categories based on their specific purpose. Each method name is constructed using the namespace, an underscore, and the specific method name in that namespace. For example, the `eth_call` method is located within the `eth` namespace. See below for details on methods currently supported or planned. +EVM Gateway is compatible with the majority of standard Ethereum JSON-RPC APIs allowing seamless integration with existing Ethereum-compatible web3 tools via HTTP. EVM Gateway honors Ethereum's JSON-RPC namespace system, grouping RPC methods into categories based on their specific purpose. Each method name is constructed using the namespace, an underscore, and the specific method name in that namespace. For example, the `eth_call` method is located within the `eth` namespace. More details on Ethereum JSON-RPC compatibility are available in our [Using EVM](https://developers.flow.com/evm/using#json-rpc-methods) docs. + +No stake is required to run an EVM Gateway and since they do not participate in consensus they have a lightweight resource footprint. They are recommended as a scaling solution in place of centralized middleware JSON-RPC providers. ### Design -![design ](https://github.com/onflow/flow-evm-gateway/assets/75445744/3fd65313-4041-46d1-b263-b848640d019f) +![design ](./evm-gateway-arch.svg) -The basic design of the EVM Gateway consists of a couple of components: +The basic design of the EVM Gateway is as follows: -- Event Ingestion Engine: this component listens to all Cadence events that are emitted by the EVM core, which can be identified by the special event type ID `evm.TransactionExecuted` and `evm.BlockExecuted` and decodes and index the data they contain in the payloads. -- Flow Requester: this component knows how to submit transactions to Flow AN to change the EVM state. What happens behind the scenes is that EVM gateway will receive an EVM transaction payload, which will get wrapped in a Cadence transaction that calls EVM contract with that payload and then the EVM core will execute the transaction and change the state. -- JSON RPC API: this is the client API component that implements all the API according to the JSON RPC API specification. +- Event Ingestion Engine: consumes all Cadence events emitted by the EVM core, filtering for special event type IDs `evm.TransactionExecuted` and `evm.BlockExecuted`. These payloads are decoded and indexed locally. The local index serves all read-only requests to the JSON-RPC including `debug_traceXxxx()` requests. +- Flow Requester: submits Cadence transactions to a Flow Access Node to change the EVM state. EVM transaction payloads received by the JSON-RPC are wrapped in a Cadence transaction. The Cadence transaction execution unwraps the EVM transaction payload and is provided to the EVM core to execute and change state. +- JSON-RPC: the client API component that implements functions according to the Ethereum JSON-RPC specification. # Building -**Manual Build** - -We recommend using Docker to run the EVM Gateway, as detailed in the subsequent section. Alternatively, if you decide to build the binary manually, you can do so by running: +**Build from source** ```bash # Make sure you pull the latest changes before running `make build` +cd flow-evm-gateway git pull origin main git fetch origin --tags make build ``` - To view the binary version: ```bash @@ -51,7 +51,7 @@ The binary can then be run by passing the necessary flags: ./flow-evm-gateway run {flags} ``` -To run a local version for development, with the necessary flags set: +To run an emulator based local version for development, with the necessary flags set: ```bash make start-local-bin @@ -60,16 +60,22 @@ make start-local-bin # Running Operating an EVM Gateway is straightforward. It can either be deployed locally alongside the Flow emulator or configured to connect with any active Flow networks supporting EVM. Given that the EVM Gateway depends solely on [Access Node APIs](https://developers.flow.com/networks/node-ops/access-onchain-data/access-nodes/accessing-data/access-api), it is compatible with any networks offering this API access. +### Key concepts + + ### Running Locally + +For local development, first install [Flow CLI](https://developers.flow.com/tools/flow-cli/install). + **Start Emulator** -To run the gateway locally you need to start the Flow Emulator: +Before running the gateway locally you need to start the Flow Emulator: ```bash flow emulator ``` -_Make sure flow.json has the emulator account configured to address and private key we will use for starting gateway bellow._ +_Make sure flow.json has the emulator account configured to address and private key we will use for starting gateway below. Use `flow init` in a new folder for example config._ Please refer to the configuration section and read through all the configuration flags before proceeding. diff --git a/dev/Dockerfile b/dev/Dockerfile index dd0b884e7..fd5565740 100644 --- a/dev/Dockerfile +++ b/dev/Dockerfile @@ -13,7 +13,7 @@ RUN curl -LO https://go.dev/dl/go1.22.2.linux-amd64.tar.gz && \ ENV PATH="/usr/local/go/bin:${PATH}" # Install Flow CLI using the installation script -RUN sh -ci "$(curl -fsSL https://raw.githubusercontent.com/onflow/flow-cli/master/install.sh)" -- v1.17.0-cadence-v1.0.0-preview.18 +RUN sh -ci "$(curl -fsSL https://raw.githubusercontent.com/onflow/flow-cli/master/install.sh)" # Set the PATH environment variable to include the directory where Flow CLI is installed ENV PATH="/root/.local/bin:${PATH}" @@ -28,11 +28,9 @@ RUN chmod a+x /app/flow-evm-gateway/evm-gateway # Copy the flow.json file to the directory where the gateway expects to find it COPY flow.json /app/flow-evm-gateway/ -COPY run.sh /app/flow-evm-gateway/ -RUN chmod a+x /app/flow-evm-gateway/run.sh WORKDIR /app/flow-evm-gateway EXPOSE 8545 -CMD ./run.sh \ No newline at end of file +ENTRYPOINT ["/flow-evm-gateway/app", "run"] \ No newline at end of file diff --git a/dev/run.sh b/dev/run.sh deleted file mode 100755 index 1b61eb8cf..000000000 --- a/dev/run.sh +++ /dev/null @@ -1,16 +0,0 @@ -#!/bin/bash - -flow emulator & -sleep 5 - -FLOW_NETWORK_ID=flow-emulator -COINBASE=FACF71692421039876a5BB4F10EF7A439D8ef61E -COA_ADDRESS=f8d6e0586b0a20c7 -COA_KEY=2619878f0e2ff438d17835c2a4561cb87b4d24d72d12ec34569acd0dd4af7c21 -COA_RESOURCE_CREATE=true -GAS_PRICE=0 -RPC_HOST=0.0.0.0 -RPC_PORT=8545 - -/app/flow-evm-gateway/evm-gateway --flow-network-id=$FLOW_NETWORK_ID --coinbase=$COINBASE --coa-address=$COA_ADDRESS --coa-key=$COA_KEY --coa-resource-create=$COA_RESOURCE_CREATE --gas-price=$GAS_PRICE --rpc-host=$RPC_HOST --rpc-port=$RPC_PORT -sleep 5 \ No newline at end of file diff --git a/evm-gateway-arch.svg b/evm-gateway-arch.svg new file mode 100644 index 000000000..a9506916a --- /dev/null +++ b/evm-gateway-arch.svg @@ -0,0 +1,10 @@ + + + + + + + + Cadence TXFlowEVM GatewayAccess NodeEVM ContractEVM Runtime CoreFlow RequesterEvent Ingestion EngineLocal IndexBlock Executed EventTx Executed BlockEVMTX JSON-RPCtxnsread onlyMetamaskEVM Txprotocol state sync+++LEGENDFLOW/CADENCE COMPATIBLEEVM COMPATIBLEDATA FLOWS \ No newline at end of file From c52d8745a0c4c7fa5d4e3052bd49f4095ac0125a Mon Sep 17 00:00:00 2001 From: Jerome P Date: Sun, 8 Dec 2024 11:36:14 -0800 Subject: [PATCH 149/174] Additions for local docker build docker pull version --- Makefile | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/Makefile b/Makefile index 0649b5313..5f1395a38 100644 --- a/Makefile +++ b/Makefile @@ -127,8 +127,7 @@ start-local-bin: # Build docker image from local sources .PHONY: docker-local-build docker-local-build: - docker build --build-arg VERSION="$(VERSION)" -f dev/Dockerfile -t "$(CONTAINER_REGISTRY)/evm-gateway:$(IMAGE_TAG)" \ - --label "git_commit=$(COMMIT)" --label "git_tag=$(IMAGE_TAG)" . + docker build -f dev/Dockerfile . # Docker run for local development .PHONY: docker-run-local @@ -154,7 +153,7 @@ docker-build: # Install image version from container registry .PHONY: docker-pull-version docker-pull-version: - + docker pull "$(CONTAINER_REGISTRY)/evm-gateway:$(IMAGE_TAG)" # Run GW image # https://github.com/onflow/flow-evm-gateway?tab=readme-ov-file#configuration-flags From 03718df1bc9a301560f85855cf404aaa95a321d5 Mon Sep 17 00:00:00 2001 From: Jerome P Date: Sun, 8 Dec 2024 11:42:51 -0800 Subject: [PATCH 150/174] No caching when local build --- Makefile | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Makefile b/Makefile index 5f1395a38..1ebed279a 100644 --- a/Makefile +++ b/Makefile @@ -127,7 +127,7 @@ start-local-bin: # Build docker image from local sources .PHONY: docker-local-build docker-local-build: - docker build -f dev/Dockerfile . + docker build --no-cache -f dev/Dockerfile -t "$(CONTAINER_REGISTRY)/evm-gateway:$(IMAGE_TAG)" . # Docker run for local development .PHONY: docker-run-local From 29bebe359c1d23a0ddeda16d3370caf05144cda1 Mon Sep 17 00:00:00 2001 From: Jerome P Date: Sun, 8 Dec 2024 11:54:40 -0800 Subject: [PATCH 151/174] Couple of changes to test local build --- Makefile | 2 +- dev/Dockerfile | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/Makefile b/Makefile index 1ebed279a..e51e56699 100644 --- a/Makefile +++ b/Makefile @@ -127,7 +127,7 @@ start-local-bin: # Build docker image from local sources .PHONY: docker-local-build docker-local-build: - docker build --no-cache -f dev/Dockerfile -t "$(CONTAINER_REGISTRY)/evm-gateway:$(IMAGE_TAG)" . + docker build --no-cache -f dev/Dockerfile -t "$(CONTAINER_REGISTRY)/evm-gateway:$(COMMIT)" . # Docker run for local development .PHONY: docker-run-local diff --git a/dev/Dockerfile b/dev/Dockerfile index fd5565740..44b536839 100644 --- a/dev/Dockerfile +++ b/dev/Dockerfile @@ -23,7 +23,7 @@ RUN git clone https://github.com/onflow/flow-evm-gateway.git /app/flow-evm-gatew RUN cd /app/flow-evm-gateway && go mod download RUN cd /app/flow-evm-gateway && go mod verify -RUN cd /app/flow-evm-gateway && CGO_ENABLED=1 go build -o /app/flow-evm-gateway/evm-gateway /app/flow-evm-gateway/cmd/main/main.go +RUN cd /app/flow-evm-gateway && CGO_ENABLED=1 go build -o /app/flow-evm-gateway/evm-gateway -ldflags="-X github.com/onflow/flow-evm-gateway/api.Version=LOCAL_BUILD" /app/flow-evm-gateway/cmd/main/main.go RUN chmod a+x /app/flow-evm-gateway/evm-gateway # Copy the flow.json file to the directory where the gateway expects to find it From 1cc6775fb2f7f876821756e1c7db9cb1e9d04212 Mon Sep 17 00:00:00 2001 From: Jerome P Date: Sun, 8 Dec 2024 13:44:40 -0800 Subject: [PATCH 152/174] Last tweaks and add redirect to main docs --- Makefile | 18 ++++----- README.md | 116 +++++++++++++++++++++++++++++------------------------- 2 files changed, 69 insertions(+), 65 deletions(-) diff --git a/Makefile b/Makefile index e51e56699..53919d2ea 100644 --- a/Makefile +++ b/Makefile @@ -6,7 +6,7 @@ COMMIT := $(shell git rev-parse HEAD) # The tag of the current commit, otherwise empty GIT_VERSION := $(shell git describe --tags --abbrev=2 2>/dev/null) CMD_ARGS := -# ACCESS_NODE_SPORK_HOSTS are space separated +# ACCESS_NODE_SPORK_HOSTS are comma separated ACCESS_NODE_SPORK_HOSTS := access-001.devnet51.nodes.onflow.org:9000 EMULATOR_COINBASE := FACF71692421039876a5BB4F10EF7A439D8ef61E EMULATOR_COA_ADDRESS := f8d6e0586b0a20c7 @@ -19,10 +19,7 @@ $(if $($(2)),\ $(error ERROR: $(2) ENV variable is required)) endef -define append_spork_hosts -$(foreach host,$(ACCESS_NODE_SPORK_HOSTS),$(eval CMD_ARGS += --access-node-spork-hosts=$(host))) -endef - +# Image tag: if image tag is not set, set it with version (or short commit if empty) # Image tag: if image tag is not set, set it with version (or short commit if empty) ifeq (${IMAGE_TAG},) IMAGE_TAG := ${VERSION} @@ -125,8 +122,8 @@ start-local-bin: --profiler-port=6060 # Build docker image from local sources -.PHONY: docker-local-build -docker-local-build: +.PHONY: docker-build-local +docker-build-local: docker build --no-cache -f dev/Dockerfile -t "$(CONTAINER_REGISTRY)/evm-gateway:$(COMMIT)" . # Docker run for local development @@ -141,7 +138,7 @@ docker-run-local: $(eval CMD_ARGS += --flow-network-id=flow-emulator --log-level=debug --coa-resource-create=true --gas-price=0 --log-writer=console --rpc-host=0.0.0.0 --profiler-enabled=true) - docker run -p $(HOST_PORT):8545 "$(CONTAINER_REGISTRY)/evm-gateway:$(IMAGE_TAG)" $(CMD_ARGS) + docker run -p $(HOST_PORT):8545 "$(CONTAINER_REGISTRY)/evm-gateway:$(COMMIT)" $(CMD_ARGS) # Build docker image for release @@ -153,7 +150,7 @@ docker-build: # Install image version from container registry .PHONY: docker-pull-version docker-pull-version: - docker pull "$(CONTAINER_REGISTRY)/evm-gateway:$(IMAGE_TAG)" + docker pull "$(CONTAINER_REGISTRY)/evm-gateway:$(IMAGE_VERSION)" # Run GW image # https://github.com/onflow/flow-evm-gateway?tab=readme-ov-file#configuration-flags @@ -193,8 +190,7 @@ endif $(call check_and_append,coa-key,COA_KEY) $(eval CMD_ARGS += --ws-enabled=true --rate-limit=9999999 --rpc-host=0.0.0.0 --log-level=info) - - $(call append_spork_hosts) + $(call check_and_append,access-node-spork-hosts,ACCESS_NODE_SPORK_HOSTS) docker run $(MODE) -p $(HOST_PORT):8545 $(MOUNT) "$(CONTAINER_REGISTRY)/evm-gateway:$(IMAGE_TAG)" $(CMD_ARGS) diff --git a/README.md b/README.md index a6fd584d0..786270cf3 100644 --- a/README.md +++ b/README.md @@ -60,15 +60,21 @@ make start-local-bin # Running Operating an EVM Gateway is straightforward. It can either be deployed locally alongside the Flow emulator or configured to connect with any active Flow networks supporting EVM. Given that the EVM Gateway depends solely on [Access Node APIs](https://developers.flow.com/networks/node-ops/access-onchain-data/access-nodes/accessing-data/access-api), it is compatible with any networks offering this API access. -### Key concepts +## Key concepts +The EVM Gateway's role in mediating EVM transactions over to Cadence means it can accrue fees from handling client transactions. Since +the gateway submits Cadence transactions wrapping EVM transaction payloads to the Flow Access Node the transaction fee for that must +be paid by the EVM Gateway. +The account used for funding gateway Cadence transactions must be a COA, not an EOA. `--coa-address` is configured with the Cadence address +of the COA account and the `--coa-key` must belong to the same account. The `--coinbase` account accrues EVM Gateway fees from EVM client +transactions and can be either an EVM EOA or COA address. ### Running Locally -For local development, first install [Flow CLI](https://developers.flow.com/tools/flow-cli/install). +For local development, first install [Flow CLI](https://developers.flow.com/tools/flow-cli/install). The examples below require no configuration and are intended for local development. -**Start Emulator** +**Run from CLI** Before running the gateway locally you need to start the Flow Emulator: @@ -88,31 +94,16 @@ make start-local Note that the gateway will be starting from the latest emulator block, so if the emulator is run before any transactions happen in the meantime, the gateway will not fetch those historical blocks & transactions. This will be improved soon. -_In the example above we set `coa-address` value to the service account of the emulator, the same as `coa-key`. -This account will by default be funded with Flow which is a requirement. For `coinbase` we can -use whichever valid EVM address. It's not really useful when running locally besides collecting fees. We also allow for the -`coa-resource-create` to auto-create resources needed on start-up on the `coa` account in order to operate the gateway. -`gas-price` is set at 0 so we don't have to fund EOA accounts. We can set it higher but keep in mind you will then -need funded accounts for interacting with EVM._ +**Run with Docker** -**With Docker** - -Set the following ENV variables +Using Docker for local development is also supported. The following target builds the current source directory into a docker image ```bash -FLOW_NETWORK_ID=[flow-testnet | flow-mainnet] -ACCESS_NODE_GRPC_HOST= -INIT_CADENCE_HEIGHT=211176670 -COA_ADDRESS= -COA_KEY= -COINBASE= +make docker-build-local ``` - -Use the Makefile targets below +This target starts the flow emulator and then runs the EVM Gateway using the image built by the above `make` target ```bash -make docker-build - -make docker-run +make docker-run-local ``` **Verify** @@ -137,7 +128,31 @@ it should return: Running against the testnet with a local build can be done by pointing the gateway to the testnet ANs and providing the correct configuration. Please refer to the configuration section and read through all the configuration flags before proceeding. -Below is an example configuration for running against testnet, with an already created testnet account. +**Create Flow account to use for COA** + +If you don't already have a Flow account you will need to create one to have a COA using the following command. + +```bash +flow keys generate +``` + +This will output something similar to: + +```bash +🔴️ Store private key safely and don't share with anyone! +Private Key 3cf8334d.....95c3c54a28e4ad1 +Public Key 33a13ade6....85f1b49a197747 +Mnemonic often scare peanut ... boil corn change +Derivation Path m/44'/539'/0'/0/0 +Signature Algorithm ECDSA_P256 +``` + +Visit https://faucet.flow.com/, and use the generated `Public Key`, to create and fund your Flow testnet account. +Make sure to use the Flow address and the `Private Key` for the `--coa-address` & `--coa-key` flags. + +**Run local EVM Gateway connected to Testnet** + +Below is an example configuration for running against testnet, with a preconfigured testnet account. ```bash ./flow-evm-gateway run \ @@ -148,43 +163,20 @@ Below is an example configuration for running against testnet, with an already c --ws-enabled=true \ --coa-resource-create=false \ --coinbase=FACF71692421039876a5BB4F10EF7A439D8ef61E \ ---coa-address=0x62631c28c9fc5a91 \ +--coa-address=62631c28c9fc5a91 \ --coa-key=2892fba444f1d5787739708874e3b01160671924610411ac787ac1379d420f49 \ --gas-price=100 ``` -The `--init-cadence-height` is the Flow block height to start indexing from. To index the full EVM state, from its beginning, the proper value for this flag is `211176670`. This is the height where the `EVM` contract was first deployed on testnet, and this is where the EVM state starts from. +The `--init-cadence-height` is the Flow block height to start indexing from. To index the full EVM state, from its beginning, the proper value for this flag for testnet is `211176670`. This is the height where the `EVM` contract was first deployed on testnet, and this is where the EVM state starts from. -If you wish to test this out with your own Access Node, simply set `--access-node-grpc-host` to the location where it is hosted. -**Note:** You need to make sure that your Access Node has indexed at least up to Flow block height `211176670`. +If you wish to test this out with your own Access Node, simply set `--access-node-grpc-host` to the DNS or IP where it is hosted. +**Note:** You need to make sure that the testnet Access Node which the gateway is connected to has indexed at least up to Flow block height `211176670`. For the `--gas-price`, feel free to experiment with different values. The `--coinbase` can be any EOA address. -To generate your own `--coa-key` and `--coa-address`, run: - -```bash -# Install Flow CLI, if you do not already have it installed -sh -ci "$(curl -fsSL https://raw.githubusercontent.com/onflow/flow-cli/master/install.sh)" - -flow-c1 keys generate -``` - -This will output something similar to: - -```bash -🔴️ Store private key safely and don't share with anyone! -Private Key 3cf8334d.....95c3c54a28e4ad1 -Public Key 33a13ade6....85f1b49a197747 -Mnemonic often scare peanut ... boil corn change -Derivation Path m/44'/539'/0'/0/0 -Signature Algorithm ECDSA_P256 -``` - -Visit https://faucet.flow.com/, and use the generated `Public Key`, to create and fund your Flow account. -Make sure to use the Flow address and the `Private Key` for the `--coa-address` & `--coa-key` flags. - Once the EVM Gateway is up and running, verify that indexing works with: ```bash @@ -201,11 +193,27 @@ Should return a response similar to: } ``` -### Running on Mainnet +**Run local EVM GW docker container connected to Testnet** + +To use the `make` target to connect a container based gateway instance to testnet requires the following environment variables to be set. + +* `ACCESS_NODE_GRPC_HOST`: access.devnet.nodes.onflow.org:9000 +* `FLOW_NETWORK_ID`: flow-testnet +* `INIT_CADENCE_HEIGHT`: 211176670 +* `COINBASE`: FACF71692421039876a5BB4F10EF7A439D8ef61E +* `COA_ADDRESS`: 62631c28c9fc5a91 +* `COA_KEY`: 2892fba444f1d5787739708874e3b01160671924610411ac787ac1379d420f49 +* `VERSION`: [_repo commit hash or tag version used when building with docker_] + +Once set, this target starts the EVM Gateway for the specified image version and connects it to testnet +```bash +make docker-run +``` -Running the EVM gateway for mainnet requires additional security and stability measures which are described in this document: https://flowfoundation.notion.site/EVM-Gateway-Deployment-3c41da6710af40acbaf971e22ce0a9fd?pvs=74 +### Mainnet and Node Operations -For mainnet, the Flow block height where the EVM state starts from is `85981134`. To index the full EVM state, use this value for the `--init-cadence-height` flag. +Guidance for EVM Gateway node operations including considerations for mainnet, hardware specs, profiler or monitoring setup +and troubleshooting can be found in the EVM Gateway [node operations docs](https://developers.flow.com/networks/node-ops/access-onchain-data/evm-gateway/evm-gateway-setup). ## Configuration Flags From eb68bfafc133416a46d02178c66260466053ab47 Mon Sep 17 00:00:00 2001 From: Jerome P Date: Mon, 9 Dec 2024 09:58:46 -0800 Subject: [PATCH 153/174] Nits and remove traces-gcp-bucket config from options --- README.md | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/README.md b/README.md index 786270cf3..abf69e04f 100644 --- a/README.md +++ b/README.md @@ -212,8 +212,8 @@ make docker-run ### Mainnet and Node Operations -Guidance for EVM Gateway node operations including considerations for mainnet, hardware specs, profiler or monitoring setup -and troubleshooting can be found in the EVM Gateway [node operations docs](https://developers.flow.com/networks/node-ops/access-onchain-data/evm-gateway/evm-gateway-setup). +Guidance for EVM Gateway node operations including considerations for mainnet, hardware specs, monitoring setup and troubleshooting +can be found in the EVM Gateway [node operations docs](https://developers.flow.com/networks/node-ops/access-onchain-data/evm-gateway/evm-gateway-setup). ## Configuration Flags @@ -249,7 +249,6 @@ The application can be configured using the following flags at runtime: | `force-start-height` | `0` | Force-set starting Cadence height (local/testing use only) | | `wallet-api-key` | `""` | ECDSA private key for wallet APIs (local/testing use only) | | `filter-expiry` | `5m` | Expiry time for idle filters | -| `traces-gcp-bucket` | `""` | GCP bucket name for transaction traces | | `traces-backfill-start-height` | `0` | Start height for backfilling transaction traces | | `traces-backfill-end-height` | `0` | End height for backfilling transaction traces | | `index-only` | `false` | Run in index-only mode, allowing state queries and indexing but no transaction sending | @@ -285,7 +284,7 @@ EVM Gateway has public RPC endpoints available for the following environments: # JSON-RPC API -The EVM Gateway implements APIs according to the Ethereum specification: https://ethereum.org/en/developers/docs/apis/json-rpc/#json-rpc-methods +The EVM Gateway implements APIs according to the Ethereum specification: https://ethereum.org/en/developers/docs/apis/json-rpc/#json-rpc-methods. **Additional APIs** - Tracing APIs allows you to fetch execution traces @@ -302,6 +301,8 @@ The EVM Gateway implements APIs according to the Ethereum specification: https:/ - Access Lists: we don't yet support creating access lists as they don't affect the fees we charge. We might support this in the future to optimize fees, but it currently is not part of our priorities. +A full list of supported methods is available in the [Using EVM](https://developers.flow.com/evm/using#json-rpc-methods) docs. + # Debugging ## Profiler From 839e5d34a4dfd4ae2f51b89fc997a54e1fedb426 Mon Sep 17 00:00:00 2001 From: Jerome P Date: Mon, 9 Dec 2024 10:06:58 -0800 Subject: [PATCH 154/174] Added missing metrics-port config --- README.md | 73 ++++++++++++++++++++++++++++--------------------------- 1 file changed, 37 insertions(+), 36 deletions(-) diff --git a/README.md b/README.md index abf69e04f..08d406384 100644 --- a/README.md +++ b/README.md @@ -219,42 +219,43 @@ can be found in the EVM Gateway [node operations docs](https://developers.flow.c The application can be configured using the following flags at runtime: -| Flag | Default Value | Description | -|--------------------------------|-------------------------------|------------------------------------------------------------------------------------------| -| `database-dir` | `./db` | Path to the directory for the database | -| `rpc-host` | `""` | Host for the RPC API server | -| `rpc-port` | `8545` | Port for the RPC API server | -| `ws-enabled` | `false` | Enable websocket connections | -| `access-node-grpc-host` | `localhost:3569` | Host to the flow access node gRPC API | -| `access-node-spork-hosts` | `""` | Previous spork AN hosts, defined as a comma-separated list (e.g. `"host-1.com,host2.com"`) | -| `flow-network-id` | `flow-emulator` | Flow network ID (options: `flow-emulator`, `flow-testnet`, `flow-mainnet`) | -| `coinbase` | `""` | Coinbase address to use for fee collection | -| `init-cadence-height` | `0` | Cadence block height to start indexing; avoid using on a new network | -| `gas-price` | `1` | Static gas price for EVM transactions | -| `coa-address` | `""` | Flow address holding COA account for submitting transactions | -| `coa-key` | `""` | Private key for the COA address used for transactions | -| `coa-key-file` | `""` | Path to a JSON file of COA keys for key-rotation (exclusive with `coa-key` flag) | -| `coa-resource-create` | `false` | Auto-create the COA resource if it doesn't exist in the Flow COA account | -| `coa-cloud-kms-project-id` | `""` | Project ID for KMS keys (e.g. `flow-evm-gateway`) | -| `coa-cloud-kms-location-id` | `""` | Location ID for KMS key ring (e.g. 'global') | -| `coa-cloud-kms-key-ring-id` | `""` | Key ring ID for KMS keys (e.g. 'tx-signing') | -| `coa-cloud-kms-keys` | `""` | KMS keys and versions, comma-separated (e.g. `"gw-key-6@1,gw-key-7@1"`) | -| `log-level` | `debug` | Log verbosity level (`debug`, `info`, `warn`, `error`, `fatal`, `panic`) | -| `log-writer` | `stderr` | Output method for logs (`stderr`, `console`) | -| `stream-limit` | `10` | Rate-limit for client events sent per second | -| `rate-limit` | `50` | Requests per second limit for clients over any protocol (ws/http) | -| `address-header` | `""` | Header for client IP when server is behind a proxy | -| `heartbeat-interval` | `100` | Interval for AN event subscription heartbeats | -| `stream-timeout` | `3` | Timeout in seconds for sending events to clients | -| `force-start-height` | `0` | Force-set starting Cadence height (local/testing use only) | -| `wallet-api-key` | `""` | ECDSA private key for wallet APIs (local/testing use only) | -| `filter-expiry` | `5m` | Expiry time for idle filters | -| `traces-backfill-start-height` | `0` | Start height for backfilling transaction traces | -| `traces-backfill-end-height` | `0` | End height for backfilling transaction traces | -| `index-only` | `false` | Run in index-only mode, allowing state queries and indexing but no transaction sending | -| `profiler-enabled` | `false` | Enable the pprof profiler server | -| `profiler-host` | `localhost` | Host for the pprof profiler | -| `profiler-port` | `6060` | Port for the pprof profiler | +| Flag | Default Value | Description | +|--------------------------------|------------------|--------------------------------------------------------------------------------------------| +| `database-dir` | `./db` | Path to the directory for the database | +| `rpc-host` | `""` | Host for the RPC API server | +| `rpc-port` | `8545` | Port for the RPC API server | +| `ws-enabled` | `false` | Enable websocket connections | +| `access-node-grpc-host` | `localhost:3569` | Host to the flow access node gRPC API | +| `access-node-spork-hosts` | `""` | Previous spork AN hosts, defined as a comma-separated list (e.g. `"host-1.com,host2.com"`) | +| `flow-network-id` | `flow-emulator` | Flow network ID (options: `flow-emulator`, `flow-testnet`, `flow-mainnet`) | +| `coinbase` | `""` | Coinbase address to use for fee collection | +| `init-cadence-height` | `0` | Cadence block height to start indexing; avoid using on a new network | +| `gas-price` | `1` | Static gas price for EVM transactions | +| `coa-address` | `""` | Flow address holding COA account for submitting transactions | +| `coa-key` | `""` | Private key for the COA address used for transactions | +| `coa-key-file` | `""` | Path to a JSON file of COA keys for key-rotation (exclusive with `coa-key` flag) | +| `coa-resource-create` | `false` | Auto-create the COA resource if it doesn't exist in the Flow COA account | +| `coa-cloud-kms-project-id` | `""` | Project ID for KMS keys (e.g. `flow-evm-gateway`) | +| `coa-cloud-kms-location-id` | `""` | Location ID for KMS key ring (e.g. 'global') | +| `coa-cloud-kms-key-ring-id` | `""` | Key ring ID for KMS keys (e.g. 'tx-signing') | +| `coa-cloud-kms-keys` | `""` | KMS keys and versions, comma-separated (e.g. `"gw-key-6@1,gw-key-7@1"`) | +| `log-level` | `debug` | Log verbosity level (`debug`, `info`, `warn`, `error`, `fatal`, `panic`) | +| `log-writer` | `stderr` | Output method for logs (`stderr`, `console`) | +| `stream-limit` | `10` | Rate-limit for client events sent per second | +| `rate-limit` | `50` | Requests per second limit for clients over any protocol (ws/http) | +| `address-header` | `""` | Header for client IP when server is behind a proxy | +| `heartbeat-interval` | `100` | Interval for AN event subscription heartbeats | +| `stream-timeout` | `3` | Timeout in seconds for sending events to clients | +| `force-start-height` | `0` | Force-set starting Cadence height (local/testing use only) | +| `wallet-api-key` | `""` | ECDSA private key for wallet APIs (local/testing use only) | +| `filter-expiry` | `5m` | Expiry time for idle filters | +| `traces-backfill-start-height` | `0` | Start height for backfilling transaction traces | +| `traces-backfill-end-height` | `0` | End height for backfilling transaction traces | +| `index-only` | `false` | Run in index-only mode, allowing state queries and indexing but no transaction sending | +| `metrics-port` | `8080` | Port for Prometheus metrics | +| `profiler-enabled` | `false` | Enable the pprof profiler server | +| `profiler-host` | `localhost` | Host for the pprof profiler | +| `profiler-port` | `6060` | Port for the pprof profiler | # Deploying Deploying the EVM Gateway node comes with some prerequisites as well as expectations and they are best explained in the WIP document: https://flowfoundation.notion.site/EVM-Gateway-Deployment-3c41da6710af40acbaf971e22ce0a9fd From d82f71ef2f44c25e41fdf1d86c6176ebc391440d Mon Sep 17 00:00:00 2001 From: Jerome P Date: Mon, 9 Dec 2024 10:07:37 -0800 Subject: [PATCH 155/174] remove dupe --- Makefile | 1 - 1 file changed, 1 deletion(-) diff --git a/Makefile b/Makefile index 53919d2ea..fdd40a319 100644 --- a/Makefile +++ b/Makefile @@ -19,7 +19,6 @@ $(if $($(2)),\ $(error ERROR: $(2) ENV variable is required)) endef -# Image tag: if image tag is not set, set it with version (or short commit if empty) # Image tag: if image tag is not set, set it with version (or short commit if empty) ifeq (${IMAGE_TAG},) IMAGE_TAG := ${VERSION} From d5b4c75bc38ee0e0af36bfb39bb7532ffbcd98c1 Mon Sep 17 00:00:00 2001 From: Jerome P Date: Mon, 9 Dec 2024 14:13:48 -0800 Subject: [PATCH 156/174] Fix docker-build-local and docker-run-local targets --- .gitignore | 1 - Makefile | 8 ++++---- dev/Dockerfile | 9 +++++---- dev/emulator-account.pkey | 1 + dev/flow.json | 17 +++++++++++++++++ 5 files changed, 27 insertions(+), 9 deletions(-) create mode 100644 dev/emulator-account.pkey create mode 100644 dev/flow.json diff --git a/.gitignore b/.gitignore index f11538de2..5dede032c 100644 --- a/.gitignore +++ b/.gitignore @@ -5,6 +5,5 @@ tests/e2e-network/node_modules tests/e2e-network/package-lock.json db flow.json -flow*.json .idea metrics/data/ \ No newline at end of file diff --git a/Makefile b/Makefile index fdd40a319..5164aca29 100644 --- a/Makefile +++ b/Makefile @@ -128,16 +128,16 @@ docker-build-local: # Docker run for local development .PHONY: docker-run-local docker-run-local: - flow emulator - sleep 5 + flow emulator & + sleep 2 $(call check_and_append,coinbase,EMULATOR_COINBASE) $(call check_and_append,coa-address,EMULATOR_COA_ADDRESS) $(call check_and_append,coa-key,EMULATOR_COA_KEY) - $(eval CMD_ARGS += --flow-network-id=flow-emulator --log-level=debug --coa-resource-create=true --gas-price=0 --log-writer=console --rpc-host=0.0.0.0 --profiler-enabled=true) + $(eval CMD_ARGS += --flow-network-id=flow-emulator --log-level=debug --coa-resource-create=true --gas-price=0 --log-writer=console --profiler-enabled=true) - docker run -p $(HOST_PORT):8545 "$(CONTAINER_REGISTRY)/evm-gateway:$(COMMIT)" $(CMD_ARGS) + docker run -p 8545:8545 "$(CONTAINER_REGISTRY)/evm-gateway:$(COMMIT)" $(CMD_ARGS) # Build docker image for release diff --git a/dev/Dockerfile b/dev/Dockerfile index 44b536839..8a4785eca 100644 --- a/dev/Dockerfile +++ b/dev/Dockerfile @@ -23,14 +23,15 @@ RUN git clone https://github.com/onflow/flow-evm-gateway.git /app/flow-evm-gatew RUN cd /app/flow-evm-gateway && go mod download RUN cd /app/flow-evm-gateway && go mod verify -RUN cd /app/flow-evm-gateway && CGO_ENABLED=1 go build -o /app/flow-evm-gateway/evm-gateway -ldflags="-X github.com/onflow/flow-evm-gateway/api.Version=LOCAL_BUILD" /app/flow-evm-gateway/cmd/main/main.go -RUN chmod a+x /app/flow-evm-gateway/evm-gateway +RUN cd /app/flow-evm-gateway && CGO_ENABLED=1 go build -o /app/flow-evm-gateway/app /app/flow-evm-gateway/cmd/main.go +RUN chmod a+x /app/flow-evm-gateway/app # Copy the flow.json file to the directory where the gateway expects to find it -COPY flow.json /app/flow-evm-gateway/ +COPY dev/flow.json /app/flow-evm-gateway/ +COPY dev/emulator-account.pkey /app/flow-evm-gateway/ WORKDIR /app/flow-evm-gateway EXPOSE 8545 -ENTRYPOINT ["/flow-evm-gateway/app", "run"] \ No newline at end of file +ENTRYPOINT ["/app/flow-evm-gateway/app", "run"] \ No newline at end of file diff --git a/dev/emulator-account.pkey b/dev/emulator-account.pkey new file mode 100644 index 000000000..881b32654 --- /dev/null +++ b/dev/emulator-account.pkey @@ -0,0 +1 @@ +0xb96ce56f16ef8dc0badfcd2ecc9c4ee972e1f987015974e1f07f39c3f26d5982 \ No newline at end of file diff --git a/dev/flow.json b/dev/flow.json new file mode 100644 index 000000000..b725e472b --- /dev/null +++ b/dev/flow.json @@ -0,0 +1,17 @@ +{ + "networks": { + "emulator": "127.0.0.1:3569", + "mainnet": "access.mainnet.nodes.onflow.org:9000", + "testing": "127.0.0.1:3569", + "testnet": "access.devnet.nodes.onflow.org:9000" + }, + "accounts": { + "emulator-account": { + "address": "f8d6e0586b0a20c7", + "key": { + "type": "file", + "location": "emulator-account.pkey" + } + } + } +} \ No newline at end of file From 756a31b924028e7d2911ce882fee0bd7fa7c2eed Mon Sep 17 00:00:00 2001 From: Jerome P Date: Mon, 9 Dec 2024 14:44:46 -0800 Subject: [PATCH 157/174] Handle missing GOARCH for macs --- Dockerfile | 1 + dev/Dockerfile | 2 ++ 2 files changed, 3 insertions(+) diff --git a/Dockerfile b/Dockerfile index 3f8759a0a..cd27dace5 100644 --- a/Dockerfile +++ b/Dockerfile @@ -13,6 +13,7 @@ RUN go mod download RUN go mod verify ARG VERSION +ARG GOARCH=amd64 # Build binary RUN CGO_ENABLED=1 go build -o bin -ldflags="-X github.com/onflow/flow-evm-gateway/api.Version=$VERSION" cmd/main.go diff --git a/dev/Dockerfile b/dev/Dockerfile index 8a4785eca..7db76ddc7 100644 --- a/dev/Dockerfile +++ b/dev/Dockerfile @@ -1,5 +1,7 @@ FROM ubuntu:latest +ARG GOARCH=amd64 + # Install curl, Git, and other required dependencies RUN apt-get update && apt-get install -y curl git gcc From 6188ee13067982ed9564df9401906a07a0b042b1 Mon Sep 17 00:00:00 2001 From: Jerome P Date: Mon, 9 Dec 2024 15:57:55 -0800 Subject: [PATCH 158/174] Add OS specific flags to go build --- Dockerfile | 4 ++-- Makefile | 19 +++++++++++++++++-- README.md | 8 +++++--- dev/Dockerfile | 4 ++-- 4 files changed, 26 insertions(+), 9 deletions(-) diff --git a/Dockerfile b/Dockerfile index cd27dace5..b067c4087 100644 --- a/Dockerfile +++ b/Dockerfile @@ -13,10 +13,10 @@ RUN go mod download RUN go mod verify ARG VERSION -ARG GOARCH=amd64 +ARG ARCH # Build binary -RUN CGO_ENABLED=1 go build -o bin -ldflags="-X github.com/onflow/flow-evm-gateway/api.Version=$VERSION" cmd/main.go +RUN CGO_ENABLED=1 GOOS=linux GOARCH=$ARCH build -o bin -ldflags="-X github.com/onflow/flow-evm-gateway/api.Version=$VERSION" cmd/main.go RUN chmod a+x bin # RUN APP diff --git a/Makefile b/Makefile index 5164aca29..0abd55c67 100644 --- a/Makefile +++ b/Makefile @@ -11,6 +11,10 @@ ACCESS_NODE_SPORK_HOSTS := access-001.devnet51.nodes.onflow.org:9000 EMULATOR_COINBASE := FACF71692421039876a5BB4F10EF7A439D8ef61E EMULATOR_COA_ADDRESS := f8d6e0586b0a20c7 EMULATOR_COA_KEY := 2619878f0e2ff438d17835c2a4561cb87b4d24d72d12ec34569acd0dd4af7c21 +UNAME_S := $(shell uname -s) +# Set default values +ARCH := +OS := # Function to check and append required arguments define check_and_append @@ -34,6 +38,17 @@ ifeq ($(origin VERSION),command line) VERSION = $(VERSION) endif +# Determine OS and set ARCH +ifeq ($(UNAME_S),Darwin) + OS := macos + ARCH := arm64 +else ifeq ($(UNAME_S),Linux) + OS := linux + ARCH := amd64 +else + $(error Unsupported operating system: $(UNAME_S)) +endif + # docker container registry export CONTAINER_REGISTRY := us-west1-docker.pkg.dev/dl-flow-devex-production/development export DOCKER_BUILDKIT := 1 @@ -123,7 +138,7 @@ start-local-bin: # Build docker image from local sources .PHONY: docker-build-local docker-build-local: - docker build --no-cache -f dev/Dockerfile -t "$(CONTAINER_REGISTRY)/evm-gateway:$(COMMIT)" . + docker build --build-arg ARCH=$(ARCH) --no-cache -f dev/Dockerfile -t "$(CONTAINER_REGISTRY)/evm-gateway:$(COMMIT)" . # Docker run for local development .PHONY: docker-run-local @@ -143,7 +158,7 @@ docker-run-local: # Build docker image for release .PHONY: docker-build docker-build: - docker build --build-arg VERSION="$(VERSION)" -f Dockerfile -t "$(CONTAINER_REGISTRY)/evm-gateway:$(IMAGE_TAG)" \ + docker build --build-arg VERSION="$(VERSION)" --build-arg ARCH=$(ARCH) -f Dockerfile -t "$(CONTAINER_REGISTRY)/evm-gateway:$(IMAGE_TAG)" \ --label "git_commit=$(COMMIT)" --label "git_tag=$(IMAGE_TAG)" . # Install image version from container registry diff --git a/README.md b/README.md index 08d406384..050459a47 100644 --- a/README.md +++ b/README.md @@ -62,7 +62,7 @@ Operating an EVM Gateway is straightforward. It can either be deployed locally a ## Key concepts -The EVM Gateway's role in mediating EVM transactions over to Cadence means it can accrue fees from handling client transactions. Since +The EVM Gateway's role in mediating EVM transactions over to Cadence is how it accrues fees from handling client transactions. Since the gateway submits Cadence transactions wrapping EVM transaction payloads to the Flow Access Node the transaction fee for that must be paid by the EVM Gateway. @@ -70,6 +70,8 @@ The account used for funding gateway Cadence transactions must be a COA, not an of the COA account and the `--coa-key` must belong to the same account. The `--coinbase` account accrues EVM Gateway fees from EVM client transactions and can be either an EVM EOA or COA address. +It is acceptable to create a single Cadence account for the COA and use the EVM address associated with that for the COINBASE address. + ### Running Locally For local development, first install [Flow CLI](https://developers.flow.com/tools/flow-cli/install). The examples below require no configuration and are intended for local development. @@ -130,7 +132,7 @@ Please refer to the configuration section and read through all the configuration **Create Flow account to use for COA** -If you don't already have a Flow account you will need to create one to have a COA using the following command. +If you don't already have a Flow account you will need to create account keys using the following command. ```bash flow keys generate @@ -147,7 +149,7 @@ Derivation Path m/44'/539'/0'/0/0 Signature Algorithm ECDSA_P256 ``` -Visit https://faucet.flow.com/, and use the generated `Public Key`, to create and fund your Flow testnet account. +Then visit https://faucet.flow.com/, and use the generated `Public Key`, to create and fund your Flow testnet account. Make sure to use the Flow address and the `Private Key` for the `--coa-address` & `--coa-key` flags. **Run local EVM Gateway connected to Testnet** diff --git a/dev/Dockerfile b/dev/Dockerfile index 7db76ddc7..1bd9250ff 100644 --- a/dev/Dockerfile +++ b/dev/Dockerfile @@ -1,6 +1,6 @@ FROM ubuntu:latest -ARG GOARCH=amd64 +ARG ARCH # Install curl, Git, and other required dependencies RUN apt-get update && apt-get install -y curl git gcc @@ -25,7 +25,7 @@ RUN git clone https://github.com/onflow/flow-evm-gateway.git /app/flow-evm-gatew RUN cd /app/flow-evm-gateway && go mod download RUN cd /app/flow-evm-gateway && go mod verify -RUN cd /app/flow-evm-gateway && CGO_ENABLED=1 go build -o /app/flow-evm-gateway/app /app/flow-evm-gateway/cmd/main.go +RUN cd /app/flow-evm-gateway && CGO_ENABLED=1 GOOS=linux GOARCH=$ARCH go build -o /app/flow-evm-gateway/app /app/flow-evm-gateway/cmd/main.go RUN chmod a+x /app/flow-evm-gateway/app # Copy the flow.json file to the directory where the gateway expects to find it From 76989f8efeeea978177e5869d5df89c0f11e066a Mon Sep 17 00:00:00 2001 From: Jerome P Date: Mon, 9 Dec 2024 16:33:59 -0800 Subject: [PATCH 159/174] Enable GOARCH override if ENV var set --- Makefile | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/Makefile b/Makefile index 0abd55c67..058665851 100644 --- a/Makefile +++ b/Makefile @@ -138,6 +138,9 @@ start-local-bin: # Build docker image from local sources .PHONY: docker-build-local docker-build-local: +ifdef GOARCH + $(eval ARCH=$(GOARCH)) +endif docker build --build-arg ARCH=$(ARCH) --no-cache -f dev/Dockerfile -t "$(CONTAINER_REGISTRY)/evm-gateway:$(COMMIT)" . # Docker run for local development @@ -158,6 +161,9 @@ docker-run-local: # Build docker image for release .PHONY: docker-build docker-build: +ifdef GOARCH + $(eval ARCH=$(GOARCH)) +endif docker build --build-arg VERSION="$(VERSION)" --build-arg ARCH=$(ARCH) -f Dockerfile -t "$(CONTAINER_REGISTRY)/evm-gateway:$(IMAGE_TAG)" \ --label "git_commit=$(COMMIT)" --label "git_tag=$(IMAGE_TAG)" . From 3ed2b216a048c1d3240e418e0c9361971f08bfcd Mon Sep 17 00:00:00 2001 From: Jerome P Date: Mon, 9 Dec 2024 17:13:05 -0800 Subject: [PATCH 160/174] Support GAS_PRICE configuration and default amount --- Makefile | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/Makefile b/Makefile index 058665851..832eed58f 100644 --- a/Makefile +++ b/Makefile @@ -182,6 +182,9 @@ docker-pull-version: # - COA_ADDRESS: To be set by the operator. This is a Cadence address which funds gateway operations (remove 0x prefix) # - COA_KEY: A full weight, private key belonging to operator COA_ADDRESS (remove 0x prefix). NB: For development use only. We recommend using cloud KMS configuration on mainnet # +# Optional +# - GAS_PRICE: the attoFlow amount +# # Optional make arguments: # - DOCKER_RUN_DETACHED: Runs container in detached mode when true # - DOCKER_HOST_PORT: Sets the exposed container port for the gateway JSON-RPC @@ -197,6 +200,9 @@ ifdef DOCKER_HOST_PORT else $(eval HOST_PORT=8545) endif +ifndef GAS_PRICE + $(eval GAS_PRICE=100) +endif ifdef DOCKER_MOUNT $(eval MOUNT=--mount type=bind,src="$(DOCKER_MOUNT)",target=$(DATADIR)) $(call check_and_append,database-dir,DATADIR) @@ -208,6 +214,7 @@ endif $(call check_and_append,coinbase,COINBASE) $(call check_and_append,coa-address,COA_ADDRESS) $(call check_and_append,coa-key,COA_KEY) + $(call check_and_append,gas-price,GAS_PRICE) $(eval CMD_ARGS += --ws-enabled=true --rate-limit=9999999 --rpc-host=0.0.0.0 --log-level=info) $(call check_and_append,access-node-spork-hosts,ACCESS_NODE_SPORK_HOSTS) From 36afe8291b161c1ad55632d702dfb0a367eca0ab Mon Sep 17 00:00:00 2001 From: Jerome P Date: Mon, 9 Dec 2024 17:58:03 -0800 Subject: [PATCH 161/174] Fixed to make docker-run-local work --- Makefile | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/Makefile b/Makefile index 832eed58f..da7a45ebd 100644 --- a/Makefile +++ b/Makefile @@ -147,13 +147,13 @@ endif .PHONY: docker-run-local docker-run-local: flow emulator & - sleep 2 + sleep 3 $(call check_and_append,coinbase,EMULATOR_COINBASE) $(call check_and_append,coa-address,EMULATOR_COA_ADDRESS) $(call check_and_append,coa-key,EMULATOR_COA_KEY) - $(eval CMD_ARGS += --flow-network-id=flow-emulator --log-level=debug --coa-resource-create=true --gas-price=0 --log-writer=console --profiler-enabled=true) + $(eval CMD_ARGS += --flow-network-id=flow-emulator --log-level=debug --gas-price=0 --log-writer=console --profiler-enabled=true) docker run -p 8545:8545 "$(CONTAINER_REGISTRY)/evm-gateway:$(COMMIT)" $(CMD_ARGS) @@ -219,5 +219,5 @@ endif $(eval CMD_ARGS += --ws-enabled=true --rate-limit=9999999 --rpc-host=0.0.0.0 --log-level=info) $(call check_and_append,access-node-spork-hosts,ACCESS_NODE_SPORK_HOSTS) - docker run $(MODE) -p $(HOST_PORT):8545 $(MOUNT) "$(CONTAINER_REGISTRY)/evm-gateway:$(IMAGE_TAG)" $(CMD_ARGS) + docker run $(MODE) -p $(HOST_PORT):8545 -p 8080:8080 $(MOUNT) "$(CONTAINER_REGISTRY)/evm-gateway:$(IMAGE_TAG)" $(CMD_ARGS) From 9ec01a807ee13deb46c1f70aa2b4ea99a52ede45 Mon Sep 17 00:00:00 2001 From: Jerome P Date: Tue, 10 Dec 2024 10:18:08 -0800 Subject: [PATCH 162/174] Open access to the right port for emulator --- Makefile | 2 +- dev/Dockerfile | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/Makefile b/Makefile index da7a45ebd..d8ac564eb 100644 --- a/Makefile +++ b/Makefile @@ -155,7 +155,7 @@ docker-run-local: $(eval CMD_ARGS += --flow-network-id=flow-emulator --log-level=debug --gas-price=0 --log-writer=console --profiler-enabled=true) - docker run -p 8545:8545 "$(CONTAINER_REGISTRY)/evm-gateway:$(COMMIT)" $(CMD_ARGS) + docker run -p 3569:3569 "$(CONTAINER_REGISTRY)/evm-gateway:$(COMMIT)" $(CMD_ARGS) # Build docker image for release diff --git a/dev/Dockerfile b/dev/Dockerfile index 1bd9250ff..387d78a97 100644 --- a/dev/Dockerfile +++ b/dev/Dockerfile @@ -34,6 +34,6 @@ COPY dev/emulator-account.pkey /app/flow-evm-gateway/ WORKDIR /app/flow-evm-gateway -EXPOSE 8545 +EXPOSE 3569 ENTRYPOINT ["/app/flow-evm-gateway/app", "run"] \ No newline at end of file From a428439568c297856ac1c558a42a500e20b277a8 Mon Sep 17 00:00:00 2001 From: Jerome P Date: Tue, 10 Dec 2024 10:29:13 -0800 Subject: [PATCH 163/174] Revert incorrect config --- Makefile | 2 +- dev/Dockerfile | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/Makefile b/Makefile index d8ac564eb..da7a45ebd 100644 --- a/Makefile +++ b/Makefile @@ -155,7 +155,7 @@ docker-run-local: $(eval CMD_ARGS += --flow-network-id=flow-emulator --log-level=debug --gas-price=0 --log-writer=console --profiler-enabled=true) - docker run -p 3569:3569 "$(CONTAINER_REGISTRY)/evm-gateway:$(COMMIT)" $(CMD_ARGS) + docker run -p 8545:8545 "$(CONTAINER_REGISTRY)/evm-gateway:$(COMMIT)" $(CMD_ARGS) # Build docker image for release diff --git a/dev/Dockerfile b/dev/Dockerfile index 387d78a97..1bd9250ff 100644 --- a/dev/Dockerfile +++ b/dev/Dockerfile @@ -34,6 +34,6 @@ COPY dev/emulator-account.pkey /app/flow-evm-gateway/ WORKDIR /app/flow-evm-gateway -EXPOSE 3569 +EXPOSE 8545 ENTRYPOINT ["/app/flow-evm-gateway/app", "run"] \ No newline at end of file From 8a515a35f10098aec0e0f386cdae6ffc65cb65f9 Mon Sep 17 00:00:00 2001 From: j pimmel Date: Tue, 10 Dec 2024 10:32:52 -0800 Subject: [PATCH 164/174] Simplify variable assignment Co-authored-by: coderabbitai[bot] <136622811+coderabbitai[bot]@users.noreply.github.com> --- Makefile | 17 ++++------------- 1 file changed, 4 insertions(+), 13 deletions(-) diff --git a/Makefile b/Makefile index da7a45ebd..937f82f29 100644 --- a/Makefile +++ b/Makefile @@ -23,20 +23,11 @@ $(if $($(2)),\ $(error ERROR: $(2) ENV variable is required)) endef -# Image tag: if image tag is not set, set it with version (or short commit if empty) -ifeq (${IMAGE_TAG},) -IMAGE_TAG := ${VERSION} -endif - -ifeq (${IMAGE_TAG},) -IMAGE_TAG := ${SHORT_COMMIT} -endif +# Set VERSION from command line, environment, or default to SHORT_COMMIT +VERSION ?= ${SHORT_COMMIT} -VERSION ?= ${IMAGE_TAG} - -ifeq ($(origin VERSION),command line) -VERSION = $(VERSION) -endif +# Set IMAGE_TAG from VERSION if not explicitly set +IMAGE_TAG ?= ${VERSION} # Determine OS and set ARCH ifeq ($(UNAME_S),Darwin) From 9df4d7d9afc5b8cdac7b7b1ae30cc3550e6f2ad5 Mon Sep 17 00:00:00 2001 From: Jerome P Date: Tue, 10 Dec 2024 11:10:47 -0800 Subject: [PATCH 165/174] Fix broken make targets for local compilation Tidy up compiler args and account for Mac specifics Fix docker-run-local with host node docker access --- Dockerfile | 2 +- Makefile | 63 ++++++++++++++++++-------------------------------- dev/Dockerfile | 2 +- 3 files changed, 25 insertions(+), 42 deletions(-) diff --git a/Dockerfile b/Dockerfile index b067c4087..a53af923e 100644 --- a/Dockerfile +++ b/Dockerfile @@ -16,7 +16,7 @@ ARG VERSION ARG ARCH # Build binary -RUN CGO_ENABLED=1 GOOS=linux GOARCH=$ARCH build -o bin -ldflags="-X github.com/onflow/flow-evm-gateway/api.Version=$VERSION" cmd/main.go +RUN CGO_ENABLED=1 GOOS=linux GOARCH=$ARCH build -o bin -ldflags="-X github.com/onflow/flow-evm-gateway/api.Version=$VERSION" -trimpath cmd/main.go RUN chmod a+x bin # RUN APP diff --git a/Makefile b/Makefile index da7a45ebd..2d02af998 100644 --- a/Makefile +++ b/Makefile @@ -15,6 +15,23 @@ UNAME_S := $(shell uname -s) # Set default values ARCH := OS := +COMPILER_FLAGS := CGO_ENABLED=1 + +EMULATOR_ARGS := --flow-network-id=flow-emulator \ + --coinbase=$(EMULATOR_COINBASE) \ + --coa-address=$(EMULATOR_COA_ADDRESS) \ + --coa-key=$(EMULATOR_COA_KEY) \ + --wallet-api-key=2619878f0e2ff438d17835c2a4561cb87b4d24d72d12ec34569acd0dd4af7c21 \ + --gas-price=0 \ + --log-writer=console \ + --profiler-enabled=true \ + --profiler-port=6060 + +# Set VERSION from command line, environment, or default to SHORT_COMMIT +VERSION ?= ${SHORT_COMMIT} + +# Set IMAGE_TAG from VERSION if not explicitly set +IMAGE_TAG ?= ${VERSION} # Function to check and append required arguments define check_and_append @@ -23,25 +40,11 @@ $(if $($(2)),\ $(error ERROR: $(2) ENV variable is required)) endef -# Image tag: if image tag is not set, set it with version (or short commit if empty) -ifeq (${IMAGE_TAG},) -IMAGE_TAG := ${VERSION} -endif - -ifeq (${IMAGE_TAG},) -IMAGE_TAG := ${SHORT_COMMIT} -endif - -VERSION ?= ${IMAGE_TAG} - -ifeq ($(origin VERSION),command line) -VERSION = $(VERSION) -endif - # Determine OS and set ARCH ifeq ($(UNAME_S),Darwin) OS := macos ARCH := arm64 + $(eval COMPILER_FLAGS += CGO_CFLAGS="-O2 -D__BLST_PORTABLE__") else ifeq ($(UNAME_S),Linux) OS := linux ARCH := amd64 @@ -76,7 +79,7 @@ check-tidy: .PHONY: build build: - CGO_ENABLED=1 go build -o flow-evm-gateway -ldflags="-X github.com/onflow/flow-evm-gateway/api.Version=$(IMAGE_TAG)" cmd/main.go + $(COMPILER_FLAGS) go build -o flow-evm-gateway -ldflags="-X github.com/onflow/flow-evm-gateway/api.Version=$(IMAGE_TAG)" cmd/main.go chmod a+x flow-evm-gateway .PHONY: fix-lint @@ -100,40 +103,20 @@ ci: check-tidy test e2e-test .PHONY: start start: - go run ./cmd/server/main.go + $(COMPILER_FLAGS) go run ./cmd/main.go .PHONY: start-local start-local: rm -rf db/ rm -rf metrics/data/ - go run cmd/main.go run \ - --flow-network-id=flow-emulator \ - --coinbase=$(EMULATOR_COINBASE) \ - --coa-address=$(EMULATOR_COA_ADDRESS) \ - --coa-key=$(EMULATOR_COA_KEYE) \ - --wallet-api-key=2619878f0e2ff438d17835c2a4561cb87b4d24d72d12ec34569acd0dd4af7c21 \ - --coa-resource-create=true \ - --gas-price=0 \ - --log-writer=console \ - --profiler-enabled=true \ - --profiler-port=6060 + $(COMPILER_FLAGS) go run cmd/main.go run $(EMULATOR_ARGS) # Use this after running `make build`, to test out the binary .PHONY: start-local-bin start-local-bin: rm -rf db/ rm -rf metrics/data/ - ./flow-evm-gateway run \ - --flow-network-id=flow-emulator \ - --coinbase=$(EMULATOR_COINBASE) \ - --coa-address=$(EMULATOR_COA_ADDRESS) \ - --coa-key=$(EMULATOR_COA_KEYE) \ - --wallet-api-key=2619878f0e2ff438d17835c2a4561cb87b4d24d72d12ec34569acd0dd4af7c21 \ - --coa-resource-create=true \ - --gas-price=0 \ - --log-writer=console \ - --profiler-enabled=true \ - --profiler-port=6060 + $(COMPILER_FLAGS) go run cmd/main.go run $(EMULATOR_ARGS) # Build docker image from local sources .PHONY: docker-build-local @@ -155,7 +138,7 @@ docker-run-local: $(eval CMD_ARGS += --flow-network-id=flow-emulator --log-level=debug --gas-price=0 --log-writer=console --profiler-enabled=true) - docker run -p 8545:8545 "$(CONTAINER_REGISTRY)/evm-gateway:$(COMMIT)" $(CMD_ARGS) + docker run -p 8545:8545 --add-host=host.docker.internal:host-gateway "$(CONTAINER_REGISTRY)/evm-gateway:$(COMMIT)" $(CMD_ARGS) # Build docker image for release diff --git a/dev/Dockerfile b/dev/Dockerfile index 1bd9250ff..0da38833d 100644 --- a/dev/Dockerfile +++ b/dev/Dockerfile @@ -25,7 +25,7 @@ RUN git clone https://github.com/onflow/flow-evm-gateway.git /app/flow-evm-gatew RUN cd /app/flow-evm-gateway && go mod download RUN cd /app/flow-evm-gateway && go mod verify -RUN cd /app/flow-evm-gateway && CGO_ENABLED=1 GOOS=linux GOARCH=$ARCH go build -o /app/flow-evm-gateway/app /app/flow-evm-gateway/cmd/main.go +RUN cd /app/flow-evm-gateway && CGO_ENABLED=1 GOOS=linux GOARCH=$ARCH go build -o /app/flow-evm-gateway/app -ldflags="-s -w -X github.com/onflow/flow-evm-gateway/api.Version=LOCALBUILD" -trimpath /app/flow-evm-gateway/cmd/main.go RUN chmod a+x /app/flow-evm-gateway/app # Copy the flow.json file to the directory where the gateway expects to find it From ade4766ed9ce1a99c963278fbb56ea309eb27b85 Mon Sep 17 00:00:00 2001 From: Jerome P Date: Tue, 10 Dec 2024 11:16:49 -0800 Subject: [PATCH 166/174] Fix missing compiler arg for Mac --- Makefile | 1 + 1 file changed, 1 insertion(+) diff --git a/Makefile b/Makefile index 292120419..424fdd185 100644 --- a/Makefile +++ b/Makefile @@ -50,6 +50,7 @@ IMAGE_TAG ?= ${VERSION} ifeq ($(UNAME_S),Darwin) OS := macos ARCH := arm64 + $(eval COMPILER_FLAGS += CGO_CFLAGS="-O2 -D__BLST_PORTABLE__") else ifeq ($(UNAME_S),Linux) OS := linux ARCH := amd64 From 2425298be5a9def7322af16decdd25c600331401 Mon Sep 17 00:00:00 2001 From: Jerome P Date: Tue, 10 Dec 2024 12:11:07 -0800 Subject: [PATCH 167/174] Remove duplicate variable setting and add ARCH flag --- .github/workflows/build.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index c291290c0..f0b63eac6 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -40,5 +40,5 @@ jobs: - name: Docker Auth run: |- gcloud auth configure-docker ${{ vars.GAR_LOCATION }}-docker.pkg.dev - docker build --build-arg VERSION="${{ steps.set_version.outputs.GATEWAY_VERSION }}" -t ${{ env.DOCKER_IMAGE_URL }}:${{ steps.set_version.outputs.GATEWAY_VERSION }} --build-arg GATEWAY_VERSION=${{ steps.set_version.outputs.GATEWAY_VERSION }} --file Dockerfile . + docker build --build-arg VERSION="${{ steps.set_version.outputs.GATEWAY_VERSION }}" --build-arg ARCH=amd64 -t ${{ env.DOCKER_IMAGE_URL }}:${{ steps.set_version.outputs.GATEWAY_VERSION }} --file Dockerfile . docker push ${{ env.DOCKER_IMAGE_URL }}:${{ steps.set_version.outputs.GATEWAY_VERSION }} From a5a688d6a688410d607711eca5914273d92e4f88 Mon Sep 17 00:00:00 2001 From: Jerome P Date: Tue, 10 Dec 2024 13:58:15 -0800 Subject: [PATCH 168/174] Remove duplicate entries --- Makefile | 6 ------ 1 file changed, 6 deletions(-) diff --git a/Makefile b/Makefile index 424fdd185..2d02af998 100644 --- a/Makefile +++ b/Makefile @@ -41,12 +41,6 @@ $(if $($(2)),\ endef # Determine OS and set ARCH -# Set VERSION from command line, environment, or default to SHORT_COMMIT -VERSION ?= ${SHORT_COMMIT} - -# Set IMAGE_TAG from VERSION if not explicitly set -IMAGE_TAG ?= ${VERSION} - ifeq ($(UNAME_S),Darwin) OS := macos ARCH := arm64 From b5d6462102fc8c663dadcb46ebbb5bbe4a64321d Mon Sep 17 00:00:00 2001 From: Jerome P Date: Tue, 10 Dec 2024 15:50:45 -0800 Subject: [PATCH 169/174] Specify host.docker.internal:3569 explicitly for the docker-run-local container tp stop it looking on the container local loopback IP --- Makefile | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Makefile b/Makefile index 2d02af998..7f0000156 100644 --- a/Makefile +++ b/Makefile @@ -136,7 +136,7 @@ docker-run-local: $(call check_and_append,coa-address,EMULATOR_COA_ADDRESS) $(call check_and_append,coa-key,EMULATOR_COA_KEY) - $(eval CMD_ARGS += --flow-network-id=flow-emulator --log-level=debug --gas-price=0 --log-writer=console --profiler-enabled=true) + $(eval CMD_ARGS += --flow-network-id=flow-emulator --log-level=debug --gas-price=0 --log-writer=console --profiler-enabled=true --access-node-grpc-host=host.docker.internal:3569) docker run -p 8545:8545 --add-host=host.docker.internal:host-gateway "$(CONTAINER_REGISTRY)/evm-gateway:$(COMMIT)" $(CMD_ARGS) From 1b2d9a1b7ab649d8c7e98c880c77c7a6ad4d4102 Mon Sep 17 00:00:00 2001 From: Jerome P Date: Tue, 10 Dec 2024 15:57:02 -0800 Subject: [PATCH 170/174] Fix emulator startup warning message --- Makefile | 2 +- dev/emulator-account.pkey | 1 - dev/flow.json | 5 +---- 3 files changed, 2 insertions(+), 6 deletions(-) delete mode 100644 dev/emulator-account.pkey diff --git a/Makefile b/Makefile index 7f0000156..260c00083 100644 --- a/Makefile +++ b/Makefile @@ -129,7 +129,7 @@ endif # Docker run for local development .PHONY: docker-run-local docker-run-local: - flow emulator & + flow emulator -f dev/flow.json & sleep 3 $(call check_and_append,coinbase,EMULATOR_COINBASE) diff --git a/dev/emulator-account.pkey b/dev/emulator-account.pkey deleted file mode 100644 index 881b32654..000000000 --- a/dev/emulator-account.pkey +++ /dev/null @@ -1 +0,0 @@ -0xb96ce56f16ef8dc0badfcd2ecc9c4ee972e1f987015974e1f07f39c3f26d5982 \ No newline at end of file diff --git a/dev/flow.json b/dev/flow.json index b725e472b..4d9c12dcb 100644 --- a/dev/flow.json +++ b/dev/flow.json @@ -8,10 +8,7 @@ "accounts": { "emulator-account": { "address": "f8d6e0586b0a20c7", - "key": { - "type": "file", - "location": "emulator-account.pkey" - } + "key": "b96ce56f16ef8dc0badfcd2ecc9c4ee972e1f987015974e1f07f39c3f26d5982" } } } \ No newline at end of file From 272ef11c4ad7dfce1247c4d21c6813904b674a3b Mon Sep 17 00:00:00 2001 From: Jerome P Date: Tue, 10 Dec 2024 16:00:59 -0800 Subject: [PATCH 171/174] Not needed --- dev/Dockerfile | 1 - 1 file changed, 1 deletion(-) diff --git a/dev/Dockerfile b/dev/Dockerfile index 0da38833d..6e426b5c5 100644 --- a/dev/Dockerfile +++ b/dev/Dockerfile @@ -30,7 +30,6 @@ RUN chmod a+x /app/flow-evm-gateway/app # Copy the flow.json file to the directory where the gateway expects to find it COPY dev/flow.json /app/flow-evm-gateway/ -COPY dev/emulator-account.pkey /app/flow-evm-gateway/ WORKDIR /app/flow-evm-gateway From 56867b89e8ea89b6ff65521deafd55815d4c5f3e Mon Sep 17 00:00:00 2001 From: Jerome P Date: Tue, 10 Dec 2024 16:20:38 -0800 Subject: [PATCH 172/174] Image update to highlight cadence transaction arrow --- evm-gateway-arch.svg | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/evm-gateway-arch.svg b/evm-gateway-arch.svg index a9506916a..a54615d45 100644 --- a/evm-gateway-arch.svg +++ b/evm-gateway-arch.svg @@ -7,4 +7,4 @@ - Cadence TXFlowEVM GatewayAccess NodeEVM ContractEVM Runtime CoreFlow RequesterEvent Ingestion EngineLocal IndexBlock Executed EventTx Executed BlockEVMTX JSON-RPCtxnsread onlyMetamaskEVM Txprotocol state sync+++LEGENDFLOW/CADENCE COMPATIBLEEVM COMPATIBLEDATA FLOWS \ No newline at end of file + Cadence TXFlowEVM GatewayAccess NodeEVM ContractEVM Runtime CoreFlow RequesterEvent Ingestion EngineLocal IndexBlock Executed EventTx Executed BlockEVMTX JSON-RPCtxnsread onlyMetamaskEVM Txprotocol state sync+++LEGENDFLOW/CADENCE COMPATIBLEEVM COMPATIBLEDATA FLOWS \ No newline at end of file From 69770c9b68f88065b4581d7d9545095a6e3d345c Mon Sep 17 00:00:00 2001 From: j pimmel Date: Wed, 11 Dec 2024 09:10:41 -0800 Subject: [PATCH 173/174] Kill background emulator process before restart Co-authored-by: coderabbitai[bot] <136622811+coderabbitai[bot]@users.noreply.github.com> --- Makefile | 1 + 1 file changed, 1 insertion(+) diff --git a/Makefile b/Makefile index 260c00083..b91afb626 100644 --- a/Makefile +++ b/Makefile @@ -129,6 +129,7 @@ endif # Docker run for local development .PHONY: docker-run-local docker-run-local: + @trap 'kill $$(jobs -p)' EXIT flow emulator -f dev/flow.json & sleep 3 From 874be8ad386af5485ac8951126a08ec10111a59d Mon Sep 17 00:00:00 2001 From: Jerome P Date: Wed, 11 Dec 2024 10:16:01 -0800 Subject: [PATCH 174/174] Merged changes from feature/local-tx-reexecution branch --- README.md | 12 ++++-------- 1 file changed, 4 insertions(+), 8 deletions(-) diff --git a/README.md b/README.md index 050459a47..4994770a3 100644 --- a/README.md +++ b/README.md @@ -72,7 +72,7 @@ transactions and can be either an EVM EOA or COA address. It is acceptable to create a single Cadence account for the COA and use the EVM address associated with that for the COINBASE address. -### Running Locally +## Running Locally For local development, first install [Flow CLI](https://developers.flow.com/tools/flow-cli/install). The examples below require no configuration and are intended for local development. @@ -125,7 +125,7 @@ it should return: } ``` -### Running on Testnet +## Running on Testnet Running against the testnet with a local build can be done by pointing the gateway to the testnet ANs and providing the correct configuration. Please refer to the configuration section and read through all the configuration flags before proceeding. @@ -163,7 +163,6 @@ Below is an example configuration for running against testnet, with a preconfigu --flow-network-id=flow-testnet \ --init-cadence-height=211176670 \ --ws-enabled=true \ ---coa-resource-create=false \ --coinbase=FACF71692421039876a5BB4F10EF7A439D8ef61E \ --coa-address=62631c28c9fc5a91 \ --coa-key=2892fba444f1d5787739708874e3b01160671924610411ac787ac1379d420f49 \ @@ -212,7 +211,7 @@ Once set, this target starts the EVM Gateway for the specified image version and make docker-run ``` -### Mainnet and Node Operations +## Mainnet and Node Operations Guidance for EVM Gateway node operations including considerations for mainnet, hardware specs, monitoring setup and troubleshooting can be found in the EVM Gateway [node operations docs](https://developers.flow.com/networks/node-ops/access-onchain-data/evm-gateway/evm-gateway-setup). @@ -236,7 +235,6 @@ The application can be configured using the following flags at runtime: | `coa-address` | `""` | Flow address holding COA account for submitting transactions | | `coa-key` | `""` | Private key for the COA address used for transactions | | `coa-key-file` | `""` | Path to a JSON file of COA keys for key-rotation (exclusive with `coa-key` flag) | -| `coa-resource-create` | `false` | Auto-create the COA resource if it doesn't exist in the Flow COA account | | `coa-cloud-kms-project-id` | `""` | Project ID for KMS keys (e.g. `flow-evm-gateway`) | | `coa-cloud-kms-location-id` | `""` | Location ID for KMS key ring (e.g. 'global') | | `coa-cloud-kms-key-ring-id` | `""` | Key ring ID for KMS keys (e.g. 'tx-signing') | @@ -259,9 +257,6 @@ The application can be configured using the following flags at runtime: | `profiler-host` | `localhost` | Host for the pprof profiler | | `profiler-port` | `6060` | Port for the pprof profiler | -# Deploying -Deploying the EVM Gateway node comes with some prerequisites as well as expectations and they are best explained in the WIP document: https://flowfoundation.notion.site/EVM-Gateway-Deployment-3c41da6710af40acbaf971e22ce0a9fd - # EVM Gateway Endpoints @@ -294,6 +289,7 @@ The EVM Gateway implements APIs according to the Ethereum specification: https:/ * debug_traceTransaction * debug_traceBlockByNumber * debug_traceBlockByHash +- debug_flowHeightByBlock - returns the flow block height for the given EVM block (id or height) **Unsupported APIs** - Wallet APIs: we don't officially support wallet APIs (eth_accounts, eth_sign, eth_signTransaction, eth_sendTransaction) due to security