diff --git a/api/api.go b/api/api.go index ba12bf34e..0b9936f46 100644 --- a/api/api.go +++ b/api/api.go @@ -158,12 +158,12 @@ func (b *BlockChainAPI) GetBalance( address common.Address, blockNumberOrHash *rpc.BlockNumberOrHash, ) (*hexutil.Big, error) { - cadenceHeight, err := b.getCadenceHeight(blockNumberOrHash) + evmHeight, err := b.getBlockNumber(blockNumberOrHash) if err != nil { return handleError[*hexutil.Big](b.logger, err) } - balance, err := b.evm.GetBalance(ctx, address, cadenceHeight) + balance, err := b.evm.GetBalance(ctx, address, evmHeight) if err != nil { return handleError[*hexutil.Big](b.logger, err) } @@ -407,7 +407,7 @@ func (b *BlockChainAPI) Call( overrides *StateOverride, blockOverrides *BlockOverrides, ) (hexutil.Bytes, error) { - cadenceHeight, err := b.getCadenceHeight(blockNumberOrHash) + evmHeight, err := b.getBlockNumber(blockNumberOrHash) if err != nil { return handleError[hexutil.Bytes](b.logger, err) } @@ -424,7 +424,7 @@ func (b *BlockChainAPI) Call( from = *args.From } - res, err := b.evm.Call(ctx, tx, from, cadenceHeight) + res, err := b.evm.Call(ctx, tx, from, evmHeight) if err != nil { // we debug output this error because the execution error is related to user input b.logger.Debug().Err(err).Msg("failed to execute call") @@ -492,12 +492,12 @@ func (b *BlockChainAPI) GetTransactionCount( address common.Address, blockNumberOrHash *rpc.BlockNumberOrHash, ) (*hexutil.Uint64, error) { - cadenceHeight, err := b.getCadenceHeight(blockNumberOrHash) + evmHeight, err := b.getBlockNumber(blockNumberOrHash) if err != nil { return handleError[*hexutil.Uint64](b.logger, err) } - networkNonce, err := b.evm.GetNonce(ctx, address, cadenceHeight) + networkNonce, err := b.evm.GetNonce(ctx, address, evmHeight) if err != nil { b.logger.Error().Err(err).Msg("get nonce on network failed") return handleError[*hexutil.Uint64](b.logger, err) @@ -560,18 +560,18 @@ func (b *BlockChainAPI) GetCode( address common.Address, blockNumberOrHash *rpc.BlockNumberOrHash, ) (hexutil.Bytes, error) { - cadenceHeight, err := b.getCadenceHeight(blockNumberOrHash) + evmHeight, err := b.getBlockNumber(blockNumberOrHash) if err != nil { return handleError[hexutil.Bytes](b.logger, err) } - code, err := b.evm.GetCode(ctx, address, cadenceHeight) + code, err := b.evm.GetCode(ctx, address, evmHeight) if err != nil { b.logger.Error().Err(err).Msg("failed to retrieve account code") return handleError[hexutil.Bytes](b.logger, err) } - return hexutil.Bytes(code), nil + return code, nil } // handleError takes in an error and in case the error is of type ErrNotFound @@ -675,36 +675,18 @@ func (b *BlockChainAPI) prepareBlockResponse( return blockResponse, nil } -func (b *BlockChainAPI) getCadenceHeight( - blockNumberOrHash *rpc.BlockNumberOrHash, -) (uint64, error) { - height := requester.LatestBlockHeight +func (b *BlockChainAPI) getBlockNumber(blockNumberOrHash *rpc.BlockNumberOrHash) (int64, error) { if number, ok := blockNumberOrHash.Number(); ok { - if number < 0 { - // negative values are special values and we only support latest height - return height, nil - } - - height, err := b.blocks.GetCadenceHeight(uint64(number.Int64())) - if err != nil { - b.logger.Error().Err(err).Msg("failed to get cadence height") - return 0, err - } + return number.Int64(), nil + } - return height, nil - } else if hash, ok := blockNumberOrHash.Hash(); ok { + 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") return 0, err } - height, err = b.blocks.GetCadenceHeight(evmHeight) - if err != nil { - b.logger.Error().Err(err).Msg("failed to get cadence height") - return 0, err - } - - return height, nil + return int64(evmHeight), nil } return 0, fmt.Errorf("invalid arguments; neither block nor hash specified") diff --git a/bootstrap/bootstrap.go b/bootstrap/bootstrap.go index 421bf3818..147d5cc64 100644 --- a/bootstrap/bootstrap.go +++ b/bootstrap/bootstrap.go @@ -224,6 +224,7 @@ func startServer( cfg, signer, logger, + blocks, ) if err != nil { return fmt.Errorf("failed to create EVM requester: %w", err) diff --git a/services/requester/requester.go b/services/requester/requester.go index 0b11c4628..acfd857a7 100644 --- a/services/requester/requester.go +++ b/services/requester/requester.go @@ -26,6 +26,7 @@ import ( "github.com/onflow/flow-evm-gateway/api/errors" "github.com/onflow/flow-evm-gateway/config" + "github.com/onflow/flow-evm-gateway/storage" ) var ( @@ -62,25 +63,25 @@ type Requester interface { SendRawTransaction(ctx context.Context, data []byte) (common.Hash, error) // GetBalance returns the amount of wei for the given address in the state of the - // given block height. - GetBalance(ctx context.Context, address common.Address, height uint64) (*big.Int, error) + // given EVM block height. + GetBalance(ctx context.Context, address common.Address, evmHeight int64) (*big.Int, error) - // Call executes the given signed transaction data on the state for the given block number. + // 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, height uint64) ([]byte, error) + Call(ctx context.Context, data []byte, from common.Address, evmHeight int64) ([]byte, error) // EstimateGas executes the given signed transaction data on the state. // 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) (uint64, error) - // GetNonce gets nonce from the network at the given block height. - GetNonce(ctx context.Context, address common.Address, height 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) // GetCode returns the code stored at the given address in - // the state for the given block number. - GetCode(ctx context.Context, address common.Address, height uint64) ([]byte, error) + // the state for the given EVM block height. + GetCode(ctx context.Context, address common.Address, evmHeight int64) ([]byte, error) // GetLatestEVMHeight returns the latest EVM height of the network. GetLatestEVMHeight(ctx context.Context) (uint64, error) @@ -93,6 +94,7 @@ type EVM struct { config *config.Config signer crypto.Signer logger zerolog.Logger + blocks storage.BlockIndexer } func NewEVM( @@ -100,6 +102,7 @@ func NewEVM( config *config.Config, signer crypto.Signer, logger zerolog.Logger, + blocks storage.BlockIndexer, ) (*EVM, error) { logger = logger.With().Str("component", "requester").Logger() // check that the address stores already created COA resource in the "evm" storage path. @@ -128,6 +131,7 @@ func NewEVM( config: config, signer: signer, logger: logger, + blocks: blocks, } // create COA on the account @@ -188,104 +192,21 @@ func (e *EVM) SendRawTransaction(ctx context.Context, data []byte) (common.Hash, return tx.Hash(), nil } -// signAndSend creates a flow transaction from the provided script -// with the arguments and signs it with the configured COA account -// and then submits it to the network. -func (e *EVM) signAndSend( - ctx context.Context, - script []byte, - args ...cadence.Value, -) (flow.Identifier, error) { - var ( - g = errgroup.Group{} - err1, err2 error - latestBlock *flow.Block - index int - seqNum uint64 - ) - // execute concurrently so we can speed up all the information we need for tx - g.Go(func() error { - latestBlock, err1 = e.client.GetLatestBlock(ctx, true) - return err1 - }) - g.Go(func() error { - index, seqNum, err2 = e.getSignerNetworkInfo(ctx) - return err2 - }) - if err := g.Wait(); err != nil { - return flow.EmptyID, err - } - - address := e.config.COAAddress - flowTx := flow.NewTransaction(). - SetScript(script). - SetProposalKey(address, index, seqNum). - SetReferenceBlockID(latestBlock.ID). - SetPayer(address). - AddAuthorizer(address) - - for _, arg := range args { - if err := flowTx.AddArgument(arg); err != nil { - return flow.EmptyID, fmt.Errorf("failed to add argument: %w", err) - } - } - - if err := flowTx.SignEnvelope(address, index, e.signer); err != nil { - return flow.EmptyID, fmt.Errorf("failed to sign envelope: %w", err) - } - - if err := e.client.SendTransaction(ctx, *flowTx); err != nil { - return flow.EmptyID, fmt.Errorf("failed to send transaction: %w", err) - } - - // get transaction status after it is submitted - go func(id flow.Identifier) { - const fetchInterval = time.Millisecond * 500 - const fetchTimeout = time.Second * 60 - - fetchTicker := time.NewTicker(fetchInterval) - timeoutTicker := time.NewTicker(fetchTimeout) - - defer fetchTicker.Stop() - defer timeoutTicker.Stop() - - for { - select { - case <-fetchTicker.C: - res, err := e.client.GetTransactionResult(context.Background(), id) - if err != nil { - e.logger.Error().Str("id", id.String()).Err(err).Msg("failed to get transaction result") - return - } - if res != nil && res.Status > flow.TransactionStatusPending { - e.logger.Info(). - Str("status", res.Status.String()). - Str("id", id.String()). - Int("events", len(res.Events)). - Err(res.Error). - Msg("transaction result received") - return - } - case <-timeoutTicker.C: - e.logger.Error().Str("id", id.String()).Msg("could not get transaction result") - return - } - } - }(flowTx.ID()) - - return flowTx.ID(), nil -} - func (e *EVM) GetBalance( ctx context.Context, address common.Address, - height uint64, + evmHeight int64, ) (*big.Int, error) { hexEncodedAddress, err := addressToCadenceString(address) if err != nil { return nil, err } + height, err := e.evmToCadenceHeight(evmHeight) + if err != nil { + return nil, err + } + val, err := e.executeScriptAtHeight( ctx, getBalanceScript, @@ -312,13 +233,18 @@ func (e *EVM) GetBalance( func (e *EVM) GetNonce( ctx context.Context, address common.Address, - height uint64, + evmHeight int64, ) (uint64, error) { hexEncodedAddress, err := addressToCadenceString(address) if err != nil { return 0, err } + height, err := e.evmToCadenceHeight(evmHeight) + if err != nil { + return 0, err + } + val, err := e.executeScriptAtHeight( ctx, getNonceScript, @@ -338,14 +264,22 @@ func (e *EVM) GetNonce( e.logger.Panic().Msg(fmt.Sprintf("failed to convert balance %v to UInt64", val)) } - return uint64(val.(cadence.UInt64)), nil + 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) Call( ctx context.Context, data []byte, from common.Address, - height uint64, + evmHeight int64, ) ([]byte, error) { hexEncodedTx, err := cadence.NewString(hex.EncodeToString(data)) if err != nil { @@ -357,6 +291,11 @@ func (e *EVM) Call( return nil, err } + height, err := e.evmToCadenceHeight(evmHeight) + if err != nil { + return nil, err + } + scriptResult, err := e.executeScriptAtHeight( ctx, dryRunScript, @@ -385,6 +324,8 @@ func (e *EVM) Call( e.logger.Debug(). Str("result", hex.EncodeToString(result)). + Int64("evm-height", evmHeight). + Uint64("cadence-height", height). Msg("call executed") return result, nil @@ -395,10 +336,6 @@ func (e *EVM) EstimateGas( data []byte, from common.Address, ) (uint64, error) { - e.logger.Debug(). - Str("data", fmt.Sprintf("%x", data)). - Msg("estimate gas") - hexEncodedTx, err := cadence.NewString(hex.EncodeToString(data)) if err != nil { return 0, err @@ -433,24 +370,28 @@ func (e *EVM) EstimateGas( // fix to also apply for the EVM API, on Cadence side. gasConsumed := evmResult.GasConsumed + params.SstoreSentryGasEIP2200 + 1 + e.logger.Debug(). + Uint64("gas", gasConsumed). + Msg("gas estimation executed") + return gasConsumed, nil } func (e *EVM) GetCode( ctx context.Context, address common.Address, - height uint64, + evmHeight int64, ) ([]byte, error) { - e.logger.Debug(). - Str("address", address.Hex()). - Uint64("height", height). - Msg("get code") - hexEncodedAddress, err := addressToCadenceString(address) if err != nil { return nil, err } + height, err := e.evmToCadenceHeight(evmHeight) + if err != nil { + return nil, err + } + value, err := e.executeScriptAtHeight( ctx, getCodeScript, @@ -466,8 +407,10 @@ func (e *EVM) GetCode( return nil, err } - e.logger.Info(). + 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") @@ -491,7 +434,13 @@ func (e *EVM) GetLatestEVMHeight(ctx context.Context) (uint64, error) { e.logger.Panic().Msg(fmt.Sprintf("failed to convert height %v to UInt64", val)) } - return uint64(val.(cadence.UInt64)), nil + height := uint64(val.(cadence.UInt64)) + + e.logger.Debug(). + Uint64("evm-height", height). + Msg("get latest evm height executed") + + return height, nil } // getSignerNetworkInfo loads the signer account from network and returns key index and sequence number @@ -532,6 +481,33 @@ 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 to cadence height, getting latest evm height: %w", 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 to cadence height: %w", 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 @@ -558,6 +534,94 @@ func (e *EVM) executeScriptAtHeight( ) } +// signAndSend creates a flow transaction from the provided script +// with the arguments and signs it with the configured COA account +// and then submits it to the network. +func (e *EVM) signAndSend( + ctx context.Context, + script []byte, + args ...cadence.Value, +) (flow.Identifier, error) { + var ( + g = errgroup.Group{} + err1, err2 error + latestBlock *flow.Block + index int + seqNum uint64 + ) + // execute concurrently so we can speed up all the information we need for tx + g.Go(func() error { + latestBlock, err1 = e.client.GetLatestBlock(ctx, true) + return err1 + }) + g.Go(func() error { + index, seqNum, err2 = e.getSignerNetworkInfo(ctx) + return err2 + }) + if err := g.Wait(); err != nil { + return flow.EmptyID, err + } + + address := e.config.COAAddress + flowTx := flow.NewTransaction(). + SetScript(script). + SetProposalKey(address, index, seqNum). + SetReferenceBlockID(latestBlock.ID). + SetPayer(address). + AddAuthorizer(address) + + for _, arg := range args { + if err := flowTx.AddArgument(arg); err != nil { + return flow.EmptyID, fmt.Errorf("failed to add argument: %w", err) + } + } + + if err := flowTx.SignEnvelope(address, index, e.signer); err != nil { + return flow.EmptyID, fmt.Errorf("failed to sign envelope: %w", err) + } + + if err := e.client.SendTransaction(ctx, *flowTx); err != nil { + return flow.EmptyID, fmt.Errorf("failed to send transaction: %w", err) + } + + // get transaction status after it is submitted + go func(id flow.Identifier) { + const fetchInterval = time.Millisecond * 500 + const fetchTimeout = time.Second * 60 + + fetchTicker := time.NewTicker(fetchInterval) + timeoutTicker := time.NewTicker(fetchTimeout) + + defer fetchTicker.Stop() + defer timeoutTicker.Stop() + + for { + select { + case <-fetchTicker.C: + res, err := e.client.GetTransactionResult(context.Background(), id) + if err != nil { + e.logger.Error().Str("id", id.String()).Err(err).Msg("failed to get transaction result") + return + } + if res != nil && res.Status > flow.TransactionStatusPending { + e.logger.Info(). + Str("status", res.Status.String()). + Str("id", id.String()). + Int("events", len(res.Events)). + Err(res.Error). + Msg("transaction result received") + return + } + case <-timeoutTicker.C: + e.logger.Error().Str("id", id.String()).Msg("could not get transaction result") + return + } + } + }(flowTx.ID()) + + return flowTx.ID(), nil +} + func addressToCadenceString(address common.Address) (cadence.String, error) { return cadence.NewString( strings.TrimPrefix(address.Hex(), "0x"), diff --git a/storage/pebble/storage.go b/storage/pebble/storage.go index 37c42bede..2cce9ff88 100644 --- a/storage/pebble/storage.go +++ b/storage/pebble/storage.go @@ -4,11 +4,13 @@ import ( "encoding/hex" "errors" "fmt" + "io" + "github.com/cockroachdb/pebble" lru "github.com/hashicorp/golang-lru/v2" - errs "github.com/onflow/flow-evm-gateway/storage/errors" "github.com/rs/zerolog" - "io" + + errs "github.com/onflow/flow-evm-gateway/storage/errors" ) type Storage struct { @@ -82,19 +84,19 @@ func New(dir string, log zerolog.Logger) (*Storage, error) { // commit the batch or revert it. func (s *Storage) set(keyCode byte, key []byte, value []byte, batch *pebble.Batch) error { prefixedKey := makePrefix(keyCode, key) - - var err error + + var err error if batch != nil { - err = batch.Set(prefixedKey, value, nil) - } else { - err = s.db.Set(prefixedKey, value, nil) - } - if err != nil { - return err - } - - s.cache.Add(hex.EncodeToString(prefixedKey), value) - return nil + err = batch.Set(prefixedKey, value, nil) + } else { + err = s.db.Set(prefixedKey, value, nil) + } + if err != nil { + return err + } + + s.cache.Add(hex.EncodeToString(prefixedKey), value) + return nil } func (s *Storage) get(keyCode byte, key ...[]byte) ([]byte, error) {