Skip to content

Commit 37be9e0

Browse files
authored
core/txpool: fix rollup cost accounting (#567)
Unlike the regular transaction cost, the rollup cost can change from block to block. So we need to cache transaction costs in the pool's lists when adding them, so we later subtract the exact same cost.
1 parent cd1571b commit 37be9e0

File tree

2 files changed

+45
-28
lines changed

2 files changed

+45
-28
lines changed

core/txpool/legacypool/legacypool_opstack_test.go

Lines changed: 32 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -26,18 +26,19 @@ import (
2626
"github.com/ethereum/go-ethereum/core/txpool"
2727
"github.com/ethereum/go-ethereum/core/types"
2828
"github.com/ethereum/go-ethereum/params"
29+
"github.com/holiman/uint256"
2930
"github.com/stretchr/testify/require"
3031
)
3132

3233
func setupOPStackPool() (*LegacyPool, *ecdsa.PrivateKey) {
3334
return setupPoolWithConfig(params.OptimismTestConfig)
3435
}
3536

36-
func setupTestL1FeeParams(t *testing.T, pool *LegacyPool) {
37-
l1FeeScalars := common.Hash{19: 1} // smallest possible base fee scalar
37+
func setupTestL1FeeParams(t *testing.T, pool *LegacyPool, l1FeeScalar byte) {
38+
l1FeeScalars := common.Hash{19: l1FeeScalar}
3839
// sanity check
3940
l1BaseFeeScalar, l1BlobBaseFeeScalar := types.ExtractEcotoneFeeParams(l1FeeScalars[:])
40-
require.EqualValues(t, 1, l1BaseFeeScalar.Uint64())
41+
require.EqualValues(t, l1FeeScalar, l1BaseFeeScalar.Uint64())
4142
require.Zero(t, l1BlobBaseFeeScalar.Sign())
4243
pool.currentState.SetState(types.L1BlockAddr, types.L1FeeScalarsSlot, l1FeeScalars)
4344
l1BaseFee := big.NewInt(1e6) // to account for division by 1e12 in L1 cost
@@ -46,15 +47,13 @@ func setupTestL1FeeParams(t *testing.T, pool *LegacyPool) {
4647
require.Equal(t, l1BaseFee, pool.currentState.GetState(types.L1BlockAddr, types.L1BaseFeeSlot).Big())
4748
}
4849

49-
func setupTestOperatorFeeParams(opFeeConst byte) func(t *testing.T, pool *LegacyPool) {
50-
return func(t *testing.T, pool *LegacyPool) {
51-
opFeeParams := common.Hash{31: opFeeConst} // 0 scalar
52-
// sanity check
53-
s, c := types.ExtractOperatorFeeParams(opFeeParams)
54-
require.Zero(t, s.Sign())
55-
require.EqualValues(t, opFeeConst, c.Uint64())
56-
pool.currentState.SetState(types.L1BlockAddr, types.OperatorFeeParamsSlot, opFeeParams)
57-
}
50+
func setupTestOperatorFeeParams(t *testing.T, pool *LegacyPool, opFeeConst byte) {
51+
opFeeParams := common.Hash{31: opFeeConst} // 0 scalar
52+
// sanity check
53+
s, c := types.ExtractOperatorFeeParams(opFeeParams)
54+
require.Zero(t, s.Sign())
55+
require.EqualValues(t, opFeeConst, c.Uint64())
56+
pool.currentState.SetState(types.L1BlockAddr, types.OperatorFeeParamsSlot, opFeeParams)
5857
}
5958

6059
func TestInvalidRollupTransactions(t *testing.T) {
@@ -67,11 +66,11 @@ func TestInvalidRollupTransactions(t *testing.T) {
6766
})
6867

6968
t.Run("operator-cost", func(t *testing.T) {
70-
testInvalidRollupTransactions(t, setupTestOperatorFeeParams(1))
69+
testInvalidRollupTransactions(t, setupTestOperatorFeeParams)
7170
})
7271
}
7372

74-
func testInvalidRollupTransactions(t *testing.T, stateMod func(t *testing.T, pool *LegacyPool)) {
73+
func testInvalidRollupTransactions(t *testing.T, stateMod func(t *testing.T, pool *LegacyPool, x byte)) {
7574
t.Parallel()
7675

7776
pool, key := setupOPStackPool()
@@ -90,7 +89,7 @@ func testInvalidRollupTransactions(t *testing.T, stateMod func(t *testing.T, poo
9089
}
9190

9291
// Now we cause insufficient funds error due to rollup cost
93-
stateMod(t, pool)
92+
stateMod(t, pool, 1)
9493

9594
rcost := pool.rollupCostFn(tx)
9695
require.Equal(t, 1, rcost.Sign(), "rollup cost must be >0")
@@ -108,11 +107,11 @@ func TestRollupTransactionCostAccounting(t *testing.T) {
108107
})
109108

110109
t.Run("operator-cost", func(t *testing.T) {
111-
testRollupTransactionCostAccounting(t, setupTestOperatorFeeParams(1))
110+
testRollupTransactionCostAccounting(t, setupTestOperatorFeeParams)
112111
})
113112
}
114113

115-
func testRollupTransactionCostAccounting(t *testing.T, stateMod func(t *testing.T, pool *LegacyPool)) {
114+
func testRollupTransactionCostAccounting(t *testing.T, stateMod func(t *testing.T, pool *LegacyPool, x byte)) {
116115
t.Parallel()
117116

118117
pool, key := setupOPStackPool()
@@ -127,28 +126,37 @@ func testRollupTransactionCostAccounting(t *testing.T, stateMod func(t *testing.
127126
require.NotNil(t, pool.rollupCostFn)
128127

129128
if stateMod != nil {
130-
stateMod(t, pool)
129+
stateMod(t, pool, 1)
131130
}
132131

133132
cost0, of := txpool.TotalTxCost(tx0, pool.rollupCostFn)
134133
require.False(t, of)
135-
cost1, of := txpool.TotalTxCost(tx1, pool.rollupCostFn)
136-
require.False(t, of)
137134

138135
if stateMod != nil {
139136
require.Greater(t, cost0.Uint64(), tx0.Cost().Uint64(), "tx0 total cost should be greater than regular cost")
140137
}
141138

142139
// we add the initial tx to the pool
143-
testAddBalance(pool, from, cost1.ToBig()) // already give enough funds for tx1
140+
testAddBalance(pool, from, cost0.ToBig())
144141
require.NoError(t, pool.addRemoteSync(tx0))
145142
_, ok := pool.queue[from]
146143
require.False(t, ok, "tx0 should not be in queue, but pending")
147144
pending, ok := pool.pending[from]
148145
require.True(t, ok, "tx0 should be pending")
149146
require.Equal(t, cost0, pending.totalcost, "tx0 total pending cost should match")
150147

151-
// now we add a replacement and check the accounting
148+
pool.reset(nil, nil) // reset the rollup cost function, simulates a head change
149+
if stateMod != nil {
150+
stateMod(t, pool, 2) // change rollup params to cause higher cost
151+
cost0r, of := txpool.TotalTxCost(tx0, pool.rollupCostFn)
152+
require.False(t, of)
153+
require.Greater(t, cost0r.Uint64(), cost0.Uint64(), "new tx0 cost should be larger")
154+
}
155+
cost1, of := txpool.TotalTxCost(tx1, pool.rollupCostFn)
156+
require.False(t, of)
157+
// add just enough for the replacement tx
158+
testAddBalance(pool, from, new(uint256.Int).Sub(cost1, cost0).ToBig())
159+
// now we add the replacement and check the accounting
152160
require.NoError(t, pool.addRemoteSync(tx1))
153161
_, ok = pool.queue[from]
154162
require.False(t, ok, "tx1 should not be in queue, but pending")
@@ -173,7 +181,7 @@ func TestRollupCostFuncChange(t *testing.T) {
173181

174182
require.NotNil(t, pool.rollupCostFn)
175183

176-
setupTestOperatorFeeParams(10)(t, pool)
184+
setupTestOperatorFeeParams(t, pool, 10)
177185

178186
cost0, of := txpool.TotalTxCost(tx0, pool.rollupCostFn)
179187
require.False(t, of)
@@ -186,7 +194,7 @@ func TestRollupCostFuncChange(t *testing.T) {
186194
// so adding 2nd tx should fail with 10 missing.
187195
testAddBalance(pool, from, cost0.ToBig())
188196
pool.reset(nil, nil) // reset the rollup cost function, simulates a head change
189-
setupTestOperatorFeeParams(20)(t, pool)
197+
setupTestOperatorFeeParams(t, pool, 20)
190198
require.ErrorContains(t, pool.addRemoteSync(tx1), "overshot 10")
191199

192200
// 3rd now add missing 10, adding tx1 should succeed

core/txpool/legacypool/list.go

Lines changed: 13 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -292,6 +292,10 @@ type list struct {
292292
// always passing the rollup cost function as an argument to every function.
293293
// It should not be accessed directly, but through the rollupCostFn method.
294294
rollupCostFnPrv rollupCostFuncProvider
295+
296+
// OP-Stack needs to cache the total cost of transactions because the rollup costs
297+
// can vary from block to block.
298+
txCosts map[common.Hash]*uint256.Int
295299
}
296300

297301
// newList creates a new transaction list for maintaining nonce-indexable fast,
@@ -302,6 +306,9 @@ func newList(strict bool) *list {
302306
txs: NewSortedMap(),
303307
costcap: new(uint256.Int),
304308
totalcost: new(uint256.Int),
309+
310+
// OP-Stack addition
311+
txCosts: make(map[common.Hash]*uint256.Int),
305312
}
306313
}
307314

@@ -368,6 +375,7 @@ func (l *list) Add(tx *types.Transaction, priceBump uint64) (bool, *types.Transa
368375
if overflow {
369376
return false, nil
370377
}
378+
l.txCosts[tx.Hash()] = cost // OP-Stack addition
371379
l.totalcost.Add(l.totalcost, cost)
372380
// Otherwise overwrite the old transaction with the current one
373381
l.txs.Put(tx)
@@ -503,14 +511,15 @@ func (l *list) LastElement() *types.Transaction {
503511
// total cost of all transactions.
504512
func (l *list) subTotalCost(txs []*types.Transaction) {
505513
for _, tx := range txs {
506-
cost, overflow := txpool.TotalTxCost(tx, l.rollupCostFn())
507-
if overflow {
508-
panic("tx total cost overflow")
509-
}
514+
h := tx.Hash()
515+
// OP-Stack diff: read cached cost
516+
// Note that subTotalCost is always called after Add, so the cached cost must be present.
517+
cost := l.txCosts[h]
510518
_, underflow := l.totalcost.SubOverflow(l.totalcost, cost)
511519
if underflow {
512520
panic("totalcost underflow")
513521
}
522+
delete(l.txCosts, h) // OP-Stack addition: sanitize cache
514523
}
515524
}
516525

0 commit comments

Comments
 (0)