Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
31 commits
Select commit Hold shift + click to select a range
de58a9e
feat(requester): add GetBlockView to NonceProvider
vishalchangrani Jun 25, 2026
9cce00c
refactor(requester): replace hasInFlight with an explicit nonceTracker
vishalchangrani Jun 25, 2026
24a6b3f
feat(requester): reject out-of-range nonces in the mempool Add path
vishalchangrani Jun 25, 2026
b535dc1
perf(requester): cache the block view by indexed height
vishalchangrani Jun 26, 2026
54fd7d3
refactor(requester): rename optionalNonce -> nonceWrapper
vishalchangrani Jun 26, 2026
9c6f6c5
refactor(requester): document coherent zero-value queue init (step 4)
vishalchangrani Jun 26, 2026
4019174
feat(requester): log every submission outcome, no silent drops (step 5)
vishalchangrani Jun 26, 2026
328ccf4
refactor(requester): defer heldTx allocation past cheap rejections (s…
vishalchangrani Jun 26, 2026
d1ab394
docs(requester): top-of-file behavior spec + dedupe struct doc (step 7)
vishalchangrani Jun 26, 2026
d1db5d8
docs(requester): audit and tighten tx_mempool comments
vishalchangrani Jun 26, 2026
8a9053c
test(requester): inject a clock to test timing behavior through the r…
vishalchangrani Jun 26, 2026
c705722
docs/test(requester): correct burst example, test the real duplicate …
vishalchangrani Jun 26, 2026
ad71012
fix(requester): bound the fast-path submit to protect the pool lock
vishalchangrani Jun 26, 2026
7303ab9
refactor(requester): mechanical cleanups (helpers, prealloc, wrapping…
vishalchangrani Jun 26, 2026
b04c8ed
refactor(requester): decompose collectDueBatches into focused helpers
vishalchangrani Jun 26, 2026
82306fe
refactor(requester): unify the per-batch drop/submit log fields
vishalchangrani Jun 26, 2026
8cbdfce
docs(requester): document the no-graceful-drain-on-shutdown caveat
vishalchangrani Jun 26, 2026
46cf137
refactor(requester): rename batchFields -> batchLogFields
vishalchangrani Jun 26, 2026
ec791ad
fix(requester): overflow-safe nonce-gap check; tighten timeout test
vishalchangrani Jun 26, 2026
f001141
docs(requester): document the submission-spacing collection-time skew
vishalchangrani Jun 26, 2026
8b3b742
Apply suggestion from @zhangchiqing
vishalchangrani Jun 26, 2026
fb426a5
docs(requester): clarify GetNonce returns the next-to-use account nonce
vishalchangrani Jun 27, 2026
968afb1
refactor(requester): normalize maxNonceGap so classify drops the gap>…
vishalchangrani Jun 27, 2026
ac59fc4
refactor(requester): make Add nonce-verdict handling exhaustive
vishalchangrani Jun 27, 2026
7123a4e
refactor(requester): handle keep verdicts inside the Add switch
vishalchangrani Jun 27, 2026
59383db
fix(requester): guard markSubmitted against a stale success ack
vishalchangrani Jun 27, 2026
17417b0
refactor(requester): extract clearSubmittingIf shared by mark/rollback
vishalchangrani Jun 27, 2026
32a1f15
refactor(requester): rename flushWork.inFlight -> needsReconcile
vishalchangrani Jun 28, 2026
ab7fa95
Update services/requester/tx_mempool.go
vishalchangrani Jun 29, 2026
294c631
Update services/requester/tx_mempool.go
vishalchangrani Jun 29, 2026
60b0bb6
refactor(requester): rename indexed-nonce naming to next-nonce (Leo #…
vishalchangrani Jun 30, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions cmd/run/cmd.go
Original file line number Diff line number Diff line change
Expand Up @@ -323,6 +323,7 @@ func init() {
Cmd.Flags().DurationVar(&cfg.TxSubmissionSpacing, "tx-submission-spacing", 1200*time.Millisecond, "Minimum gap between consecutive Cadence submissions for the same EOA in the transaction mempool; also serves as the flush deadline for a continuously-fed collection window. Recommended ~1.5x the block production rate.")
Cmd.Flags().DurationVar(&cfg.TxPoolTTL, "tx-pool-ttl", 30*time.Second, "How long the transaction mempool holds an out-of-order transaction waiting for its nonce gap to fill, before submitting it anyway.")
Cmd.Flags().IntVar(&cfg.TxMaxBatchSize, "tx-max-batch-size", 5, "Maximum number of EVM transactions per EVM.batchRun Cadence transaction in the transaction mempool.")
Cmd.Flags().Uint64Var(&cfg.TxMaxNonceGap, "tx-max-nonce-gap", 500, "How far ahead of an EOA's on-chain nonce the transaction mempool accepts a nonce; nonces beyond indexedNonce+gap are rejected as nonce-too-high. 0 means no upper bound. A nonce below the indexed nonce is always rejected as nonce-too-low regardless of this setting.")

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

this change is to address @zhangchiqing suggestion that we should reject nonce that are ridiculously higher than the current nonce. e.g. 1M nonce in the future.

Cmd.Flags().DurationVar(&cfg.RpcRequestTimeout, "rpc-request-timeout", time.Second*120, "Sets the maximum duration at which JSON-RPC requests should generate a response, before they timeout. The default is 120 seconds.")

err := Cmd.Flags().MarkDeprecated("init-cadence-height", "This flag is no longer necessary and will be removed in future version. The initial Cadence height is known for testnet/mainnet and this was only required for fresh deployments of EVM Gateway. Once the DB has been initialized, the latest index Cadence height will be used upon start-up.")
Expand Down
8 changes: 8 additions & 0 deletions config/config.go
Original file line number Diff line number Diff line change
Expand Up @@ -147,6 +147,14 @@ type Config struct {
// single EVM.batchRun Cadence transaction by the transaction mempool,
// bounded by the Cadence transaction computation limit.
TxMaxBatchSize int
// TxMaxNonceGap is how far ahead of an EOA's on-chain nonce the transaction
// mempool will accept a nonce. A transaction whose nonce exceeds
// indexedNonce + TxMaxNonceGap is rejected up front with ErrNonceTooHigh,
// giving the client immediate feedback instead of holding an unexecutable tx
// until TTL. 0 means no upper bound (any future nonce is accepted). This
// bounds only the upper end: a nonce below the indexed nonce is always
// rejected with ErrNonceTooLow regardless of this setting.
TxMaxNonceGap uint64
// RpcRequestTimeout is the maximum duration at which JSON-RPC requests should generate
// a response, before they timeout.
RpcRequestTimeout time.Duration
Expand Down
7 changes: 7 additions & 0 deletions models/errors/errors.go
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,13 @@ var (
// already been submitted to the network and is awaiting execution. Letting
// it through would burn Flow fees on a guaranteed nonce-mismatch failure.
ErrInFlightNonce = fmt.Errorf("%w: %s", ErrInvalid, "transaction with the same nonce already submitted")
// ErrNonceTooLow is returned when a transaction's nonce is below the EOA's
// current on-chain nonce: it has already been used and can never execute.
ErrNonceTooLow = fmt.Errorf("%w: %s", ErrInvalid, "nonce too low")
// ErrNonceTooHigh is returned when a transaction's nonce is more than the
// configured maximum gap ahead of the EOA's on-chain nonce. Such a tx cannot
// execute until the gap fills, so it is rejected up front for fast feedback.
ErrNonceTooHigh = fmt.Errorf("%w: %s", ErrInvalid, "nonce too high")

// Storage errors

Expand Down
65 changes: 58 additions & 7 deletions services/requester/nonce_provider.go
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
package requester

import (
"sync"

gethCommon "github.com/ethereum/go-ethereum/common"
"github.com/onflow/flow-go/fvm/evm"
"github.com/onflow/flow-go/fvm/evm/offchain/query"
Expand All @@ -10,25 +12,49 @@ import (
"github.com/onflow/flow-evm-gateway/storage/pebble"
)

// NonceProvider returns the current nonce of the given EOA address.
// The transaction mempool uses it to determine the expected next nonce.
// NonceView reads EOA nonces at a single, fixed EVM state (one built block

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

These are changes to incorporate @m-Peter comment in the earlier PR about reusing the block view to find the last nonce for an EOA. Caching the view for a given block height ensures we are not performing the expensive operation of reading the full block view each time when figuring out the latest on-chain nonce for an EOA.

// view). The mempool reads many EOAs' nonces from one view per flush tick
// rather than rebuilding the (expensive) view per address. It is an interface
// so tests can fake it without constructing a real query.View.
type NonceView interface {
// GetNonce returns the EOA's account nonce (the next nonce to use) at this
// view's state. Named GetNonce — not GetNextNonce — because this interface is
// satisfied directly by flow-go's query.View, whose method is GetNonce.
GetNonce(address gethCommon.Address) (uint64, error)
}

// NonceProvider returns the next nonce of the given EOA address. The transaction
// mempool uses it to determine the expected next nonce.
type NonceProvider interface {
// GetNonce returns the current nonce of the given EOA address.
// GetNextNonce returns the account nonce of the given EOA — its transaction
// count, i.e. the next nonce the EOA should use (matches eth_getTransactionCount).
//
// A non-nil error represents an EXCEPTION, not an expected condition:
// the underlying read is a local state-index lookup that should not
// fail under normal operation. Callers must therefore treat an error
// as a hard failure (reject the transaction / abort the operation)
// rather than a routine, recoverable condition to swallow.
GetNonce(address gethCommon.Address) (uint64, error)
GetNextNonce(address gethCommon.Address) (uint64, error)

// GetBlockView returns a NonceView over the latest indexed EVM state. A
// non-nil error is an EXCEPTION, same contract as GetNextNonce.
GetBlockView() (NonceView, error)
}

// LocalNonceProvider reads the EOA nonce from the latest height of the
// local state index.
// local state index. It caches the built block view and reuses it while the
// indexed height is unchanged (see GetBlockView).
type LocalNonceProvider struct {
chainID flowGo.ChainID
registerStore *pebble.RegisterStorage
blocks storage.BlockIndexer

// mu guards the cached view below. The cached view is shared across reads;
// callers that read it concurrently must serialize (the mempool does, via
// its queueMux).
mu sync.Mutex
cachedView NonceView
cachedHeight uint64
}

var _ NonceProvider = &LocalNonceProvider{}
Expand All @@ -45,10 +71,23 @@ func NewLocalNonceProvider(
}
}

func (p *LocalNonceProvider) GetNonce(address gethCommon.Address) (uint64, error) {
// GetBlockView returns a NonceView over the latest indexed EVM height. The view
// is cached and reused while the indexed height is unchanged, so a burst of
// reads within one block — many Add calls, or a collectDueBatches pass — builds
// the (expensive) view only once. It is rebuilt when a new block is indexed.
// Reuse is safe because an EOA's on-chain nonce cannot change without a new
// block being indexed.
func (p *LocalNonceProvider) GetBlockView() (NonceView, error) {
height, err := p.blocks.LatestEVMHeight()
if err != nil {
return 0, err
return nil, err
}

p.mu.Lock()
defer p.mu.Unlock()

if p.cachedView != nil && p.cachedHeight == height {
return p.cachedView, nil
}

viewProvider := query.NewViewProvider(
Expand All @@ -60,6 +99,18 @@ func (p *LocalNonceProvider) GetNonce(address gethCommon.Address) (uint64, error
)

view, err := viewProvider.GetBlockView(height)
if err != nil {
return nil, err
}

p.cachedView = view
p.cachedHeight = height

return view, nil
}

func (p *LocalNonceProvider) GetNextNonce(address gethCommon.Address) (uint64, error) {
view, err := p.GetBlockView()
if err != nil {
return 0, err
}
Expand Down
Loading
Loading