Skip to content

Commit

Permalink
Enable transaction validation with local state index
Browse files Browse the repository at this point in the history
  • Loading branch information
m-Peter committed Dec 5, 2024
1 parent 4b32ea8 commit a043332
Show file tree
Hide file tree
Showing 11 changed files with 173 additions and 73 deletions.
6 changes: 4 additions & 2 deletions Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -57,7 +57,8 @@ start-local:
--gas-price=0 \
--log-writer=console \
--profiler-enabled=true \
--profiler-port=6060
--profiler-port=6060 \
--tx-state-validation=local-index

# Use this after running `make build`, to test out the binary
.PHONY: start-local-bin
Expand All @@ -73,4 +74,5 @@ start-local-bin:
--gas-price=0 \
--log-writer=console \
--profiler-enabled=true \
--profiler-port=6060
--profiler-port=6060 \
--tx-state-validation=local-index
27 changes: 20 additions & 7 deletions api/server.go
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ import (
"strings"
"time"

"github.com/onflow/go-ethereum/core"
gethVM "github.com/onflow/go-ethereum/core/vm"
gethLog "github.com/onflow/go-ethereum/log"
"github.com/onflow/go-ethereum/rpc"
Expand Down Expand Up @@ -427,6 +428,17 @@ type responseHandler struct {
metrics metrics.Collector
}

var knownErrors = []error{
errs.ErrRateLimit,
errs.ErrInvalid,
errs.ErrFailedTransaction,
errs.ErrEndpointNotSupported,
gethVM.ErrExecutionReverted,
core.ErrNonceTooLow,
core.ErrNonceTooHigh,
core.ErrInsufficientFunds,
}

const errMethodNotFound = -32601
const errCodePanic = -32603

Expand Down Expand Up @@ -471,11 +483,7 @@ func (w *responseHandler) Write(data []byte) (int, error) {
}

// don't error log known handled errors
if !errorIs(errMsg, errs.ErrRateLimit) &&
!errorIs(errMsg, errs.ErrInvalid) &&
!errorIs(errMsg, errs.ErrFailedTransaction) &&
!errorIs(errMsg, errs.ErrEndpointNotSupported) &&
!errorIs(errMsg, gethVM.ErrExecutionReverted) {
if !isKnownError(errMsg) {
// log the response error as a warning
l.Warn().Err(errors.New(errMsg)).Msg("API response")
}
Expand Down Expand Up @@ -505,6 +513,11 @@ func (w *responseHandler) WriteHeader(statusCode int) {
w.ResponseWriter.WriteHeader(statusCode)
}

func errorIs(msg string, err error) bool {
return strings.Contains(msg, err.Error())
func isKnownError(errMsg string) bool {
for _, err := range knownErrors {
if strings.Contains(errMsg, err.Error()) {
return true
}
}
return false
}
7 changes: 7 additions & 0 deletions api/utils.go
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ import (
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"
"github.com/onflow/go-ethereum/core/types"
"github.com/onflow/go-ethereum/rpc"
"github.com/rs/zerolog"
Expand Down Expand Up @@ -124,6 +125,12 @@ func handleError[T any](err error, log zerolog.Logger, collector metrics.Collect
return zero, err
case errors.As(err, &revertedErr):
return zero, revertedErr
case errors.Is(err, core.ErrNonceTooLow):
return zero, err
case errors.Is(err, core.ErrNonceTooHigh):
return zero, err
case errors.Is(err, core.ErrInsufficientFunds):
return zero, err
default:
collector.ApiErrorOccurred()
log.Error().Err(err).Msg("api error")
Expand Down
1 change: 1 addition & 0 deletions bootstrap/bootstrap.go
Original file line number Diff line number Diff line change
Expand Up @@ -211,6 +211,7 @@ func (b *Bootstrap) StartAPIServer(ctx context.Context) error {
b.client,
b.publishers.Transaction,
b.logger,
b.config,
)

blocksProvider := replayer.NewBlocksProvider(
Expand Down
1 change: 1 addition & 0 deletions cmd/run/cmd.go
Original file line number Diff line number Diff line change
Expand Up @@ -303,4 +303,5 @@ func init() {
Cmd.Flags().BoolVar(&cfg.ProfilerEnabled, "profiler-enabled", false, "Run the profiler server to capture pprof data.")
Cmd.Flags().StringVar(&cfg.ProfilerHost, "profiler-host", "localhost", "Host for the Profiler server")
Cmd.Flags().IntVar(&cfg.ProfilerPort, "profiler-port", 6060, "Port for the Profiler server")
Cmd.Flags().StringVar(&cfg.TxStateValidation, "tx-state-validation", "local", "")
}
2 changes: 2 additions & 0 deletions config/config.go
Original file line number Diff line number Diff line change
Expand Up @@ -85,4 +85,6 @@ type Config struct {
ProfilerHost string
// ProfilerPort is the port for the profiler server
ProfilerPort int
// TxStateValidation
TxStateValidation string
}
62 changes: 35 additions & 27 deletions services/requester/pool.go
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ import (
"github.com/rs/zerolog"
"github.com/sethvargo/go-retry"

"github.com/onflow/flow-evm-gateway/config"
"github.com/onflow/flow-evm-gateway/models"
errs "github.com/onflow/flow-evm-gateway/models/errors"
)
Expand All @@ -30,19 +31,22 @@ type TxPool struct {
client *CrossSporkClient
pool *sync.Map
txPublisher *models.Publisher[*gethTypes.Transaction]
config config.Config
// todo add methods to inspect transaction pool state
}

func NewTxPool(
client *CrossSporkClient,
transactionsPublisher *models.Publisher[*gethTypes.Transaction],
logger zerolog.Logger,
config config.Config,
) *TxPool {
return &TxPool{
logger: logger.With().Str("component", "tx-pool").Logger(),
client: client,
txPublisher: transactionsPublisher,
pool: &sync.Map{},
config: config,
}
}

Expand All @@ -61,37 +65,41 @@ func (t *TxPool) Send(
return err
}

// add to pool and delete after transaction is sealed or errored out
t.pool.Store(evmTx.Hash(), evmTx)
defer t.pool.Delete(evmTx.Hash())

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 {
return fmt.Errorf("failed to retrieve flow transaction result %s: %w", flowTx.ID(), err)
}
// retry until transaction is sealed
if res.Status < flow.TransactionStatusSealed {
return retry.RetryableError(fmt.Errorf("transaction %s not sealed", flowTx.ID()))
}

if res.Error != nil {
if err, ok := parseInvalidError(res.Error); ok {
return err
if t.config.TxStateValidation == "sealed-result" {
// add to pool and delete after transaction is sealed or errored out
t.pool.Store(evmTx.Hash(), evmTx)
defer t.pool.Delete(evmTx.Hash())

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 {
return fmt.Errorf("failed to retrieve flow transaction result %s: %w", flowTx.ID(), err)
}
// retry until transaction is sealed
if res.Status < flow.TransactionStatusSealed {
return retry.RetryableError(fmt.Errorf("transaction %s not sealed", flowTx.ID()))
}

t.logger.Error().Err(res.Error).
Str("flow-id", flowTx.ID().String()).
Str("evm-id", evmTx.Hash().Hex()).
Msg("flow transaction error")
if res.Error != nil {
if err, ok := parseInvalidError(res.Error); ok {
return err
}

t.logger.Error().Err(res.Error).
Str("flow-id", flowTx.ID().String()).
Str("evm-id", evmTx.Hash().Hex()).
Msg("flow transaction error")

// hide specific cause since it's an implementation issue
return fmt.Errorf("failed to submit flow evm transaction %s", evmTx.Hash())
}
// hide specific cause since it's an implementation issue
return fmt.Errorf("failed to submit flow evm transaction %s", evmTx.Hash())
}

return nil
})
}

return nil
})
return nil
}

// this will extract the evm specific error from the Flow transaction error message
Expand Down
64 changes: 64 additions & 0 deletions services/requester/requester.go
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ import (
"github.com/onflow/flow-go/fvm/evm/offchain/query"
evmTypes "github.com/onflow/flow-go/fvm/evm/types"
"github.com/onflow/go-ethereum/common"
gethCore "github.com/onflow/go-ethereum/core"
"github.com/onflow/go-ethereum/core/txpool"
"github.com/onflow/go-ethereum/core/types"
"github.com/rs/zerolog"
Expand Down Expand Up @@ -182,6 +183,63 @@ func NewEVM(
return evm, nil
}

func (e *EVM) validateTransactionWithState(
tx *types.Transaction,
from common.Address,
) error {
height, err := e.blocks.LatestEVMHeight()
if err != nil {
return err
}
view, err := e.getBlockView(height)
if err != nil {
return err
}

nonce, err := view.GetNonce(from)
if err != nil {
return err
}

// Ensure the transaction adheres to nonce ordering
if tx.Nonce() < nonce {
return fmt.Errorf(
"%w: next nonce %v, tx nonce %v",
gethCore.ErrNonceTooLow,
nonce,
tx.Nonce(),
)
}

if tx.Nonce() > nonce {
return fmt.Errorf(
"%w: tx nonce %v, next nonce %v",
gethCore.ErrNonceTooHigh,
tx.Nonce(),
nonce,
)
}

// Ensure the transactor has enough funds to cover the transaction costs
cost := tx.Cost()
balance, err := view.GetBalance(from)
if err != nil {
return err
}

if balance.Cmp(cost) < 0 {
return fmt.Errorf(
"%w: balance %v, tx cost %v, overshot %v",
gethCore.ErrInsufficientFunds,
balance,
cost,
new(big.Int).Sub(cost, balance),
)
}

return nil
}

func (e *EVM) SendRawTransaction(ctx context.Context, data []byte) (common.Hash, error) {
tx := &types.Transaction{}
if err := tx.UnmarshalBinary(data); err != nil {
Expand All @@ -201,6 +259,12 @@ func (e *EVM) SendRawTransaction(ctx context.Context, data []byte) (common.Hash,
return common.Hash{}, errs.NewTxGasPriceTooLowError(e.config.GasPrice)
}

if e.config.TxStateValidation == "local-index" {
if err := e.validateTransactionWithState(tx, from); err != nil {
return common.Hash{}, err
}
}

txData := hex.EncodeToString(data)
hexEncodedTx, err := cadence.NewString(txData)
if err != nil {
Expand Down
37 changes: 19 additions & 18 deletions tests/helpers.go
Original file line number Diff line number Diff line change
Expand Up @@ -138,24 +138,25 @@ 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,
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,
TxStateValidation: "local-index",
}

bootstrapDone := make(chan struct{})
Expand Down
25 changes: 13 additions & 12 deletions tests/integration_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -60,18 +60,19 @@ 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,
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(),
TxStateValidation: "sealed-result",
}

// todo change this test to use ingestion and emulator directly so we can completely remove
Expand Down
Loading

0 comments on commit a043332

Please sign in to comment.