Skip to content

fix(txpool): restore cumulative affordability enforcement with fee-delegation accounting#116

Open
colinkim wants to merge 1 commit into
devfrom
fix/txpool-cumulative-balance-validation
Open

fix(txpool): restore cumulative affordability enforcement with fee-delegation accounting#116
colinkim wants to merge 1 commit into
devfrom
fix/txpool-cumulative-balance-validation

Conversation

@colinkim

@colinkim colinkim commented Jul 22, 2026

Copy link
Copy Markdown
Contributor

Summary

Fee-delegated transactions (type 0x16) split their financial pending expenditure:

  • the sender pays tx.Value()
  • the fee payer pays tx.FeeCost()

The upstream cumulative overdraft check introduced by ethereum/go-ethereum#27429 assumed that the sender pays the entire transaction cost. Because that assumption caused false rejections for fee-delegated transactions, the check had been disabled globally.

Disabling it allowed individually affordable but collectively unfundable transactions to occupy txpool capacity:

  1. A sender could submit sequential transactions whose cumulative cost exceeded its balance.
  2. Multiple senders could charge gas to the same fee payer until the aggregate pending expenditure exceeded the fee payer's balance.

This change restores cumulative overdraft protection while accounting for sender and fee-payer pending expenditure separately, preserving StableNet's fee model.

Changes

Cumulative accounting

  • Keep list.totalcost for sender-side pending expenditure:
    • normal transaction: tx.Cost()
    • fee-delegated transaction: tx.Value()
  • Add pool-wide pendingGas, indexed by fee payer, to aggregate delegated gas pending expenditure across sender lists.
  • Make pending lists update pendingGas through paired add/remove callbacks. Queued transactions remain outside pending expenditure accounting.
  • Ignore zero gas pending expenditure symmetrically and detect accounting underflows as internal invariant failures.

Stateful validation

  • Restore cumulative overdraft validation for normal transactions.
  • Check fee-delegated sender value and fee-payer gas against their respective balances.
  • Add ExistingTx so replacement validation can subtract the previous transaction's value and gas from the correct accounts, including fee-payer changes.

Demotion

  • Disable the list filter short circuit while fee-delegated transactions are present, ensuring a transaction is removed from pending if its fee payer later becomes unable to cover the gas pending expenditure.

Tests

  • Add coverage for:
    • cumulative delegated gas across multiple senders
    • zero-value pendingGas accounting
    • fee-delegated list-counter consistency
    • filter behavior after fee-payer insolvency
    • restore blob-pool cumulative insufficient-funds expectations.

Verified with:

  • go test ./core/txpool/legacypool ./core/txpool/blobpool
  • go test -race ./core/txpool/legacypool

Port go-wbft cumulative overdraft validation onto stableNet's fee model.
Track sender-side pending cost via list.totalcost and fee-payer gas via pool-wide pendingGas, and include both in ExistingExpenditure so shared fee-payer expenditure are checked cumulatively.
Fee-payer gas is reserved at gasFeeCap (tx.FeeCost()) as a conservative upper bound, matching upstream mempool semantics.
@colinkim colinkim self-assigned this Jul 22, 2026
@colinkim colinkim added the bug Something isn't working label Jul 22, 2026
@colinkim colinkim changed the title fix(txpool): re-enable cumulative fee-delegation accounting fix(txpool): restore cumulative affordability enforcement with fee-delegation accounting Jul 24, 2026

@hominlee-wemade hominlee-wemade left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

LGTM

@0xmhha 0xmhha left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

The description in this PR uses "pending expenditure" in the Demotion section:

▎ "ensuring a transaction is removed from pending if its fee payer later becomes unable to cover the gas pending expenditure"

However, the original wording in go-wbft#197 uses "obligation":

▎ "unable to cover the gas obligation"

The word "pending expenditure" implies the demotion check uses the cumulative pendingGas index, but the actual Filter implementation does a per-tx spot check against the on-chain balance (tx.FeeCost() > stateDB.GetBalance(feePayer)). This is correct and intentional behavior — the cumulative guard runs at Add time, not during demotion — but the rephrased description could mislead readers into expecting a cumulative check at demotion.

Suggest reverting the wording in the Demotion section to match go-wbft#197 for consistency.

@0xmhha 0xmhha left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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


File: core/txpool/legacypool/legacypool_test.go
Location: After the existing fee-delegation test group (e.g., after TestFeeDelegationFilterShortCircuitGate)


The existing tests cover the case where sender != feePayer, but there is no test for a fee-delegated transaction where sender == feePayer (i.e., an account sponsors its own gas via the fee-delegation path).

This case exercises a non-obvious accounting path: in addCost, the sender-as-feePayer splits its own Cost() across two buckets — totalcost receives tx.Value() and
pendingGas[sender] receives tx.FeeCost(). In ExistingExpenditure, both are summed back together, which must equal tx.Cost(). In validation, the from == gasPayer branch then treats the combined total as a single balance check.

A test should verify:

  1. totalcost[sender] == tx.Value() (not tx.Cost())
  2. pendingGas[sender] == tx.FeeCost()
  3. ExistingExpenditure(sender) == tx.Cost() (the sum of both)
  4. A second FD tx from the same sender-as-feePayer is correctly rejected when the cumulative total would exceed the balance

Without this coverage, a regression that accidentally routes FD self-pay through the else branch (fee-delegation split path) — charging only tx.Value() to the sender and leaving tx.FeeCost() unchecked — would go undetected.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

bug Something isn't working

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants