-
Notifications
You must be signed in to change notification settings - Fork 887
fix: harden mempool proposal gas accounting #3892
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -66,6 +66,9 @@ func (wtx *WrappedTx) check(c TxConstraints) error { | |
| if c.MaxGas >= 0 && wtx.gasWanted > c.MaxGas { | ||
| return fmt.Errorf("gas wanted exceeds max gas: gas wanted %d is greater than max gas %d", wtx.gasWanted, c.MaxGas) | ||
| } | ||
| if c.MaxGasWanted > 0 && wtx.gasWanted > c.MaxGasWanted { | ||
|
seidroid[bot] marked this conversation as resolved.
|
||
| return fmt.Errorf("gas wanted exceeds max gas wanted: gas wanted %d is greater than max gas wanted %d", wtx.gasWanted, c.MaxGasWanted) | ||
| } | ||
| return nil | ||
| } | ||
|
|
||
|
|
@@ -550,12 +553,12 @@ func (s *txStore) compact(inner *txStoreInner, clearAccounts bool) { | |
| } | ||
|
|
||
| type updateSpec struct { | ||
| Now time.Time | ||
| Height int64 | ||
| TxResults map[types.TxHash]bool // true - success, false - failed, missing - not executed | ||
| Constraints TxConstraints | ||
| NewPriorities map[types.TxHash]int64 | ||
| InvalidTxs map[types.TxHash]bool | ||
| Now time.Time | ||
| Height int64 | ||
| TxResults map[types.TxHash]bool // true - success, false - failed, missing - not executed | ||
| Constraints TxConstraints | ||
| RecheckedTxs map[types.TxHash]checkedTxMetadata | ||
| InvalidTxs map[types.TxHash]bool | ||
| } | ||
|
|
||
| func (s *txStore) Update(spec updateSpec) { | ||
|
|
@@ -592,6 +595,11 @@ func (s *txStore) Update(spec updateSpec) { | |
| if expired { | ||
| Global.ExpiredTxsAt().Add(1) | ||
| } | ||
| if metadata, ok := spec.RecheckedTxs[wtx.Hash()]; ok { | ||
| wtx.priority = metadata.priority | ||
| wtx.gasWanted = metadata.gasWanted | ||
| wtx.estimatedGas = metadata.estimatedGas | ||
| } | ||
| invalid := spec.InvalidTxs[wtx.Hash()] || wtx.check(spec.Constraints) != nil | ||
| _, executed := spec.TxResults[wtx.Hash()] | ||
| remove := invalid || executed || (expired && (s.config.RemoveExpiredTxsFromQueue || !inner.isReady(wtx))) | ||
|
|
@@ -606,8 +614,6 @@ func (s *txStore) Update(spec updateSpec) { | |
| if el, ok := wtx.readyEl.Get(); ok { | ||
| s.readyTxs.Remove(el) | ||
| } | ||
| } else if newPriority, ok := spec.NewPriorities[wtx.Hash()]; ok { | ||
| wtx.priority = newPriority | ||
| } | ||
| } | ||
| start := time.Now() | ||
|
|
@@ -624,6 +630,14 @@ type ReapLimits struct { | |
| MaxGasEstimated utils.Option[int64] | ||
| } | ||
|
|
||
| // fitsReapCapacity reports whether a transaction fits every remaining proposal budget. | ||
| // The budgets are independent, so a candidate must fit all three to be selectable. | ||
| func (wtx *WrappedTx) fitsReapCapacity(remainingBytes, remainingGasWanted, remainingGasEstimated int64) bool { | ||
| return wtx.protoSize <= remainingBytes && | ||
| wtx.gasWanted <= remainingGasWanted && | ||
| wtx.estimatedGas <= remainingGasEstimated | ||
| } | ||
|
|
||
| // Reap returns a list of transactions within the provided tx, | ||
| // byte, and gas constraints together with the total estimated gas for the | ||
| // returned transactions. Reaped txs are removed iff remove == true. | ||
|
|
@@ -649,18 +663,32 @@ func (s *txStore) Reap(l ReapLimits, remove bool) (types.Txs, int64) { | |
| var wtxs []*WrappedTx | ||
| for inner := range s.inner.Lock() { | ||
| if uint64(inner.state.Load().ready.count) >= s.config.TxNotifyThreshold { //nolint:gosec // count is non-negative | ||
| blockedEVMAccounts := map[common.Address]struct{}{} | ||
| for _, wtx := range inner.inInclusionOrder() { | ||
| if uint64(len(wtxs)) >= maxTxs || !inner.isReady(wtx) { | ||
| if uint64(len(wtxs)) >= maxTxs { | ||
| break | ||
| } | ||
| if maxBytes-totalSize < wtx.protoSize { | ||
| if !inner.isReady(wtx) { | ||
| break | ||
| } | ||
| if maxGasWanted-totalGasWanted < wtx.gasWanted { | ||
| break | ||
| evm, isEVM := wtx.evm.Get() | ||
| // Non-EVM ordering assumes transactions come from different accounts. If that | ||
|
seidroid[bot] marked this conversation as resolved.
seidroid[bot] marked this conversation as resolved.
seidroid[bot] marked this conversation as resolved.
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. [suggestion] Consolidating the same point raised across several earlier rounds, since it survives the rebase (Codex flags it too). The EVM half of this is correct — I checked that The Cosmos half has no equivalent guard, and this comment documents the hazard instead of closing it: a signer with sequences N and N+1 in the mempool can now have N skipped for capacity and N+1 selected, which fails The blocker to fixing it properly is that |
||
| // assumption is violated, skipping sequence N can select N+1 and fail delivery. | ||
| if isEVM { | ||
| if _, blocked := blockedEVMAccounts[evm.address]; blocked { | ||
| continue | ||
| } | ||
| } | ||
| if maxGasEstimated-totalGasEstimated < wtx.estimatedGas { | ||
| break | ||
| if !wtx.fitsReapCapacity( | ||
| maxBytes-totalSize, | ||
| maxGasWanted-totalGasWanted, | ||
| maxGasEstimated-totalGasEstimated, | ||
| ) { | ||
| if isEVM { | ||
| // A later nonce cannot be selected when this account's current nonce is skipped. | ||
| blockedEVMAccounts[evm.address] = struct{}{} | ||
|
seidroid[bot] marked this conversation as resolved.
|
||
| } | ||
| continue | ||
|
seidroid[bot] marked this conversation as resolved.
|
||
| } | ||
| // include tx and update totals | ||
| totalSize += wtx.protoSize | ||
|
|
||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
[nit] The
> 0guard makesMaxGasWanted == 0mean "unchecked", while theMaxGascheck directly above uses>= 0soMaxGas == 0rejects everything. The asymmetry is intentional and documented intypes.go, but the resulting state is a trap: at zero, admission accepts every transaction whileReapproduces only empty proposals (exactly whatTestTxMempool_ZeroProposalGasLimitDoesNotRejectAtAdmissionpins), so the mempool fills with transactions that can never be proposed. A one-time warning log or a metric when a node observesMaxGasWanted == 0would make that misconfiguration diagnosable instead of presenting as unexplained empty blocks.Separately: rejection here calls
txmp.txStore.MarkInvalid, so a transaction rejected under the current limit stays cache-blacklisted even ifMaxGasWantedis later raised via consensus params, until LRU eviction. Minor, and consistent with howMaxGasalready behaves.