feat(feynman): add new compression-aware rollup fee formula (#1196)

* Change l1DataFee to rollupFee for Feynman

* Update CommitScalarSlot to ExecScalarSlot

* Rename l1DataFee to rollupFee

* Scale compression ratio to match scalars precision

* Use l1DataFee and commitScalar instead of rollupFee and execScalar

* Use blocktime not blocknumber

* Add unit test

* Clarify test comment

* feat(feynman): upgrade gas oracle predeploy

* address comments

* revert renaming CalculateL1DataFee to CalculateRollupFee

* simplify EstimateL1DataFeeForMessage

* address comments

* add a comment based on pr reviews

* update formula

* tweaks

* tweak comments

* Estimate compression ratio at rollupFee for Feynman  (#1197)

* Estimate compression ratio

* Update rollup/fees/rollup_fee.go

Co-authored-by: Péter Garamvölgyi <peter@scroll.io>

* Check compressed size smaller and remove unnecessary checks/changes

* goimports locally run

* rollback format changes

* update da-codec's zstd function

* apply zstd

* tweaks

* tweak unit test

* make sure compression ratio >= 1

* nit

---------

Co-authored-by: Péter Garamvölgyi <peter@scroll.io>
Co-authored-by: colinlyguo <colinlyguo@scroll.io>
Co-authored-by: colin <102356659+colinlyguo@users.noreply.github.com>

* refactoring

* remove incorrect merge artifact

* error handling

* update da-codec commit

* update da-codec commit

---------

Co-authored-by: Péter Garamvölgyi <peter@scroll.io>
Co-authored-by: colin <102356659+colinlyguo@users.noreply.github.com>
Co-authored-by: colinlyguo <colinlyguo@scroll.io>
This commit is contained in:
Alejandro Ranchal-Pedrosa 2025-06-26 12:08:40 +02:00 committed by GitHub
parent a67863c655
commit c0d89415da
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
23 changed files with 282 additions and 67 deletions

View file

@ -640,7 +640,7 @@ func (b *SimulatedBackend) callContract(ctx context.Context, call ethereum.CallM
vmEnv := vm.NewEVM(evmContext, txContext, stateDB, b.config, vm.Config{NoBaseFee: true})
gasPool := new(core.GasPool).AddGas(math.MaxUint64)
signer := types.MakeSigner(b.blockchain.Config(), head.Number, head.Time)
l1DataFee, err := fees.EstimateL1DataFeeForMessage(msg, head.BaseFee, b.blockchain.Config(), signer, stateDB, head.Number)
l1DataFee, err := fees.EstimateL1DataFeeForMessage(msg, head.BaseFee, b.blockchain.Config(), signer, stateDB, head.Number, head.Time)
if err != nil {
return nil, err
}

View file

@ -184,7 +184,7 @@ func (pre *Prestate) Apply(vmConfig vm.Config, chainConfig *params.ChainConfig,
snapshot := statedb.Snapshot()
evm := vm.NewEVM(vmContext, txContext, statedb, chainConfig, vmConfig)
l1DataFee, err := fees.CalculateL1DataFee(tx, statedb, chainConfig, new(big.Int).SetUint64(pre.Env.Number))
l1DataFee, err := fees.CalculateL1DataFee(tx, statedb, chainConfig, new(big.Int).SetUint64(pre.Env.Number), pre.Env.Timestamp)
if err != nil {
log.Info("rejected tx due to fees.CalculateL1DataFee", "index", i, "hash", tx.Hash(), "from", msg.From(), "error", err)
rejectedTxs = append(rejectedTxs, &rejectedTx{i, err.Error()})

View file

@ -71,7 +71,7 @@ func (p *statePrefetcher) Prefetch(block *types.Block, statedb *state.StateDB, c
}
statedb.SetTxContext(tx.Hash(), i)
l1DataFee, err := fees.CalculateL1DataFee(tx, statedb, p.config, block.Number())
l1DataFee, err := fees.CalculateL1DataFee(tx, statedb, p.config, block.Number(), block.Time())
if err != nil {
return
}

View file

@ -80,6 +80,7 @@ func (p *StateProcessor) Process(block *types.Block, statedb *state.StateDB, cfg
header = block.Header()
blockHash = block.Hash()
blockNumber = block.Number()
blockTime = block.Time()
allLogs []*types.Log
gp = new(GasPool).AddGas(block.GasLimit())
)
@ -110,7 +111,7 @@ func (p *StateProcessor) Process(block *types.Block, statedb *state.StateDB, cfg
return nil, nil, 0, fmt.Errorf("could not apply tx %d [%v]: %w", i, tx.Hash().Hex(), err)
}
statedb.SetTxContext(tx.Hash(), i)
receipt, err := applyTransaction(msg, p.config, p.bc, nil, gp, statedb, blockNumber, blockHash, tx, usedGas, vmenv)
receipt, err := applyTransaction(msg, p.config, p.bc, nil, gp, statedb, blockNumber, blockTime, blockHash, tx, usedGas, vmenv)
if err != nil {
return nil, nil, 0, fmt.Errorf("could not apply tx %d [%v]: %w", i, tx.Hash().Hex(), err)
}
@ -125,7 +126,7 @@ func (p *StateProcessor) Process(block *types.Block, statedb *state.StateDB, cfg
return receipts, allLogs, *usedGas, nil
}
func applyTransaction(msg types.Message, config *params.ChainConfig, bc ChainContext, author *common.Address, gp *GasPool, statedb *state.StateDB, blockNumber *big.Int, blockHash common.Hash, tx *types.Transaction, usedGas *uint64, evm *vm.EVM) (*types.Receipt, error) {
func applyTransaction(msg types.Message, config *params.ChainConfig, bc ChainContext, author *common.Address, gp *GasPool, statedb *state.StateDB, blockNumber *big.Int, blockTime uint64, blockHash common.Hash, tx *types.Transaction, usedGas *uint64, evm *vm.EVM) (*types.Receipt, error) {
defer func(t0 time.Time) {
applyTransactionTimer.Update(time.Since(t0))
}(time.Now())
@ -134,7 +135,7 @@ func applyTransaction(msg types.Message, config *params.ChainConfig, bc ChainCon
txContext := NewEVMTxContext(msg)
evm.Reset(txContext, statedb)
l1DataFee, err := fees.CalculateL1DataFee(tx, statedb, config, blockNumber)
l1DataFee, err := fees.CalculateL1DataFee(tx, statedb, config, blockNumber, blockTime)
if err != nil {
return nil, err
}
@ -206,7 +207,7 @@ func ApplyTransaction(config *params.ChainConfig, bc ChainContext, author *commo
// Create a new context to be used in the EVM environment
blockContext := NewEVMBlockContext(header, bc, config, author)
vmenv := vm.NewEVM(blockContext, vm.TxContext{}, statedb, config, cfg)
return applyTransaction(msg, config, bc, author, gp, statedb, header.Number, header.Hash(), tx, usedGas, vmenv)
return applyTransaction(msg, config, bc, author, gp, statedb, header.Number, header.Time, header.Hash(), tx, usedGas, vmenv)
}
// ProcessParentBlockHash stores the parent block hash in the history storage contract

View file

@ -282,7 +282,7 @@ func (l *txList) Overlaps(tx *types.Transaction) bool {
//
// If the new transaction is accepted into the list, the lists' cost and gas
// thresholds are also potentially updated.
func (l *txList) Add(tx *types.Transaction, state *state.StateDB, priceBump uint64, chainconfig *params.ChainConfig, blockNumber *big.Int) (bool, *types.Transaction) {
func (l *txList) Add(tx *types.Transaction, state *state.StateDB, priceBump uint64, chainconfig *params.ChainConfig, blockNumber *big.Int, blockTime uint64) (bool, *types.Transaction) {
// If there's an older better transaction, abort
old := l.txs.Get(tx.Nonce())
if old != nil {
@ -310,7 +310,7 @@ func (l *txList) Add(tx *types.Transaction, state *state.StateDB, priceBump uint
l1DataFee := big.NewInt(0)
if state != nil && chainconfig != nil {
var err error
l1DataFee, err = fees.CalculateL1DataFee(tx, state, chainconfig, blockNumber)
l1DataFee, err = fees.CalculateL1DataFee(tx, state, chainconfig, blockNumber, blockTime)
if err != nil {
log.Error("Failed to calculate L1 data fee", "err", err, "tx", tx)
return false, nil

View file

@ -38,7 +38,7 @@ func TestStrictTxListAdd(t *testing.T) {
// Insert the transactions in a random order
list := newTxList(true)
for _, v := range rand.Perm(len(txs)) {
list.Add(txs[v], nil, DefaultTxPoolConfig.PriceBump, nil, nil)
list.Add(txs[v], nil, DefaultTxPoolConfig.PriceBump, nil, nil, 0)
}
// Verify internal state
if len(list.txs.items) != len(txs) {
@ -65,7 +65,7 @@ func BenchmarkTxListAdd(b *testing.B) {
for i := 0; i < b.N; i++ {
list := newTxList(true)
for _, v := range rand.Perm(len(txs)) {
list.Add(txs[v], nil, DefaultTxPoolConfig.PriceBump, nil, nil)
list.Add(txs[v], nil, DefaultTxPoolConfig.PriceBump, nil, nil, 0)
list.Filter(priceLimit, DefaultTxPoolConfig.PriceBump)
}
}

View file

@ -301,6 +301,7 @@ type TxPool struct {
currentState *state.StateDB // Current state in the blockchain head
currentHead *big.Int // Current blockchain head
currentTime uint64 // Current blockchain head time
pendingNonces *txNoncer // Pending state tracking virtual nonces
currentMaxGas uint64 // Current gas limit for transaction caps
@ -838,7 +839,7 @@ func (pool *TxPool) validateTx(tx *types.Transaction, local bool) error {
// 2. If FeeVault is enabled, perform an additional check for L1 data fees.
if pool.chainconfig.Scroll.FeeVaultEnabled() {
// Get L1 data fee in current state
l1DataFee, err := fees.CalculateL1DataFee(tx, pool.currentState, pool.chainconfig, pool.currentHead)
l1DataFee, err := fees.CalculateL1DataFee(tx, pool.currentState, pool.chainconfig, pool.currentHead, pool.currentTime)
if err != nil {
return fmt.Errorf("failed to calculate L1 data fee, err: %w", err)
}
@ -947,7 +948,7 @@ func (pool *TxPool) add(tx *types.Transaction, local bool) (replaced bool, err e
from, _ := types.Sender(pool.signer, tx) // already validated
if list := pool.pending[from]; list != nil && list.Overlaps(tx) {
// Nonce already pending, check if required price bump is met
inserted, old := list.Add(tx, pool.currentState, pool.config.PriceBump, pool.chainconfig, pool.currentHead)
inserted, old := list.Add(tx, pool.currentState, pool.config.PriceBump, pool.chainconfig, pool.currentHead, pool.currentTime)
if !inserted {
log.Debug("Discarding underpriced pending transaction", "hash", hash.Hex(), "gasTipCap", tx.GasTipCap(), "gasFeeCap", tx.GasFeeCap())
pendingDiscardMeter.Mark(1)
@ -1001,7 +1002,7 @@ func (pool *TxPool) enqueueTx(hash common.Hash, tx *types.Transaction, local boo
pool.queue[from] = newTxList(false)
}
inserted, old := pool.queue[from].Add(tx, pool.currentState, pool.config.PriceBump, pool.chainconfig, pool.currentHead)
inserted, old := pool.queue[from].Add(tx, pool.currentState, pool.config.PriceBump, pool.chainconfig, pool.currentHead, pool.currentTime)
if !inserted {
// An older transaction was better, discard this
log.Debug("Discarding underpriced queued transaction", "hash", hash.Hex(), "gasTipCap", tx.GasTipCap(), "gasFeeCap", tx.GasFeeCap())
@ -1068,7 +1069,7 @@ func (pool *TxPool) promoteTx(addr common.Address, hash common.Hash, tx *types.T
return false
}
inserted, old := list.Add(tx, pool.currentState, pool.config.PriceBump, pool.chainconfig, pool.currentHead)
inserted, old := list.Add(tx, pool.currentState, pool.config.PriceBump, pool.chainconfig, pool.currentHead, pool.currentTime)
if !inserted {
// An older transaction was better, discard this
log.Debug("Discarding underpriced pending transaction", "hash", hash.Hex(), "gasTipCap", tx.GasTipCap(), "gasFeeCap", tx.GasFeeCap())
@ -1603,6 +1604,7 @@ func (pool *TxPool) reset(oldHead, newHead *types.Header) {
// Update current head
pool.currentHead = next
pool.currentTime = newHead.Time
}
// promoteExecutables moves transactions that have become processable from the
@ -1690,7 +1692,7 @@ func (pool *TxPool) executableTxFilter(costLimit *big.Int) func(tx *types.Transa
if pool.chainconfig.Scroll.FeeVaultEnabled() {
// recheck L1 data fee, as the oracle price may have changed
l1DataFee, err := fees.CalculateL1DataFee(tx, pool.currentState, pool.chainconfig, pool.currentHead)
l1DataFee, err := fees.CalculateL1DataFee(tx, pool.currentState, pool.chainconfig, pool.currentHead, pool.currentTime)
if err != nil {
log.Error("Failed to calculate L1 data fee", "err", err, "tx", tx)
return false

View file

@ -198,7 +198,7 @@ func (eth *Ethereum) stateAtTransaction(block *types.Block, txIndex int, reexec
// Not yet the searched for transaction, execute on top of the current state
vmenv := vm.NewEVM(context, txContext, statedb, eth.blockchain.Config(), vm.Config{})
statedb.SetTxContext(tx.Hash(), idx)
l1DataFee, err := fees.CalculateL1DataFee(tx, statedb, eth.blockchain.Config(), block.Number())
l1DataFee, err := fees.CalculateL1DataFee(tx, statedb, eth.blockchain.Config(), block.Number(), block.Time())
if err != nil {
return nil, vm.BlockContext{}, nil, err
}

View file

@ -292,7 +292,7 @@ func (api *API) traceChain(ctx context.Context, start, end *types.Block, config
TxHash: tx.Hash(),
}
l1DataFee, err := fees.CalculateL1DataFee(tx, task.statedb, api.backend.ChainConfig(), task.block.Number())
l1DataFee, err := fees.CalculateL1DataFee(tx, task.statedb, api.backend.ChainConfig(), task.block.Number(), task.block.Time())
if err != nil {
// though it's not a "tracing error", we still need to put it here
task.results[i] = &txTraceResult{Error: err.Error()}
@ -557,7 +557,7 @@ func (api *API) IntermediateRoots(ctx context.Context, hash common.Hash, config
)
statedb.SetTxContext(tx.Hash(), i)
l1DataFee, err := fees.CalculateL1DataFee(tx, statedb, api.backend.ChainConfig(), block.Number())
l1DataFee, err := fees.CalculateL1DataFee(tx, statedb, api.backend.ChainConfig(), block.Number(), block.Time())
if err != nil {
log.Warn("Tracing intermediate roots did not complete due to fees.CalculateL1DataFee", "txindex", i, "txhash", tx.Hash(), "err", err)
return nil, err
@ -645,7 +645,7 @@ func (api *API) traceBlock(ctx context.Context, block *types.Block, config *Trac
TxHash: txs[task.index].Hash(),
}
l1DataFee, err := fees.CalculateL1DataFee(txs[task.index], task.statedb, api.backend.ChainConfig(), block.Number())
l1DataFee, err := fees.CalculateL1DataFee(txs[task.index], task.statedb, api.backend.ChainConfig(), block.Number(), block.Time())
if err != nil {
// though it's not a "tracing error", we still need to put it here
results[task.index] = &txTraceResult{
@ -679,7 +679,7 @@ func (api *API) traceBlock(ctx context.Context, block *types.Block, config *Trac
msg, _ := tx.AsMessage(signer, block.BaseFee())
statedb.SetTxContext(tx.Hash(), i)
vmenv := vm.NewEVM(blockCtx, core.NewEVMTxContext(msg), statedb, api.backend.ChainConfig(), vm.Config{})
l1DataFee, err := fees.CalculateL1DataFee(tx, statedb, api.backend.ChainConfig(), block.Number())
l1DataFee, err := fees.CalculateL1DataFee(tx, statedb, api.backend.ChainConfig(), block.Number(), block.Time())
if err != nil {
failed = err
break
@ -804,7 +804,7 @@ func (api *API) standardTraceBlockToFile(ctx context.Context, block *types.Block
// Execute the transaction and flush any traces to disk
vmenv := vm.NewEVM(vmctx, txContext, statedb, chainConfig, vmConf)
statedb.SetTxContext(tx.Hash(), i)
l1DataFee, err := fees.CalculateL1DataFee(tx, statedb, api.backend.ChainConfig(), block.Number())
l1DataFee, err := fees.CalculateL1DataFee(tx, statedb, api.backend.ChainConfig(), block.Number(), block.Time())
if err == nil {
_, err = core.ApplyMessage(vmenv, msg, new(core.GasPool).AddGas(msg.Gas()), l1DataFee)
}
@ -869,7 +869,7 @@ func (api *API) TraceTransaction(ctx context.Context, hash common.Hash, config *
TxIndex: int(index),
TxHash: hash,
}
l1DataFee, err := fees.CalculateL1DataFee(tx, statedb, api.backend.ChainConfig(), block.Number())
l1DataFee, err := fees.CalculateL1DataFee(tx, statedb, api.backend.ChainConfig(), block.Number(), block.Time())
if err != nil {
return nil, err
}
@ -929,7 +929,7 @@ func (api *API) TraceCall(ctx context.Context, args ethapi.TransactionArgs, bloc
}
signer := types.MakeSigner(api.backend.ChainConfig(), block.Number(), block.Time())
l1DataFee, err := fees.EstimateL1DataFeeForMessage(msg, block.BaseFee(), api.backend.ChainConfig(), signer, statedb, block.Number())
l1DataFee, err := fees.EstimateL1DataFeeForMessage(msg, block.BaseFee(), api.backend.ChainConfig(), signer, statedb, block.Number(), block.Time())
if err != nil {
return nil, err
}

View file

@ -173,7 +173,7 @@ func (b *testBackend) StateAtTransaction(ctx context.Context, block *types.Block
return msg, context, statedb, nil
}
vmenv := vm.NewEVM(context, txContext, statedb, b.chainConfig, vm.Config{})
l1DataFee, err := fees.CalculateL1DataFee(tx, statedb, b.chainConfig, block.Number())
l1DataFee, err := fees.CalculateL1DataFee(tx, statedb, b.chainConfig, block.Number(), block.Time())
if err != nil {
return nil, vm.BlockContext{}, nil, err
}

View file

@ -193,7 +193,7 @@ func testCallTracer(tracerName string, dirPath string, t *testing.T) {
if err != nil {
t.Fatalf("failed to prepare transaction for tracing: %v", err)
}
l1DataFee, err := fees.CalculateL1DataFee(tx, statedb, test.Genesis.Config, context.BlockNumber)
l1DataFee, err := fees.CalculateL1DataFee(tx, statedb, test.Genesis.Config, context.BlockNumber, context.Time.Uint64())
if err != nil {
t.Fatalf("failed to calculate l1DataFee: %v", err)
}
@ -308,7 +308,7 @@ func benchTracer(tracerName string, test *callTracerTest, b *testing.B) {
}
evm := vm.NewEVM(context, txContext, statedb, test.Genesis.Config, vm.Config{Debug: true, Tracer: tracer})
snap := statedb.Snapshot()
l1DataFee, err := fees.CalculateL1DataFee(tx, statedb, test.Genesis.Config, context.BlockNumber)
l1DataFee, err := fees.CalculateL1DataFee(tx, statedb, test.Genesis.Config, context.BlockNumber, context.Time.Uint64())
if err != nil {
b.Fatalf("failed to calculate l1DataFee: %v", err)
}
@ -381,7 +381,7 @@ func TestZeroValueToNotExitCall(t *testing.T) {
if err != nil {
t.Fatalf("failed to prepare transaction for tracing: %v", err)
}
l1DataFee, err := fees.CalculateL1DataFee(tx, statedb, params.MainnetChainConfig, context.BlockNumber)
l1DataFee, err := fees.CalculateL1DataFee(tx, statedb, params.MainnetChainConfig, context.BlockNumber, context.Time.Uint64())
if err != nil {
t.Fatalf("failed to calculate l1DataFee: %v", err)
}

View file

@ -112,7 +112,7 @@ func BenchmarkTransactionTrace(b *testing.B) {
for i := 0; i < b.N; i++ {
snap := statedb.Snapshot()
l1DataFee, err := fees.CalculateL1DataFee(tx, statedb, params.AllEthashProtocolChanges, context.BlockNumber)
l1DataFee, err := fees.CalculateL1DataFee(tx, statedb, params.AllEthashProtocolChanges, context.BlockNumber, context.Time.Uint64())
if err != nil {
b.Fatal(err)
}

2
go.mod
View file

@ -51,7 +51,7 @@ require (
github.com/prometheus/tsdb v0.7.1
github.com/rjeczalik/notify v0.9.1
github.com/rs/cors v1.7.0
github.com/scroll-tech/da-codec v0.1.3-0.20250519114140-bfa7133d4ad1
github.com/scroll-tech/da-codec v0.1.3-0.20250626091118-58b899494da6
github.com/scroll-tech/zktrie v0.8.4
github.com/shirou/gopsutil v3.21.11+incompatible
github.com/sourcegraph/conc v0.3.0

4
go.sum
View file

@ -396,8 +396,8 @@ github.com/rogpeppe/go-internal v1.10.0/go.mod h1:UQnix2H7Ngw/k4C5ijL5+65zddjncj
github.com/rs/cors v1.7.0 h1:+88SsELBHx5r+hZ8TCkggzSstaWNbDvThkVK8H6f9ik=
github.com/rs/cors v1.7.0/go.mod h1:gFx+x8UowdsKA9AchylcLynDq+nNFfI8FkUZdN/jGCU=
github.com/russross/blackfriday/v2 v2.0.1/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM=
github.com/scroll-tech/da-codec v0.1.3-0.20250519114140-bfa7133d4ad1 h1:6aKqJSal+QVdB5HMWMs0JTbAIZ6/iAHJx9qizz0w9dU=
github.com/scroll-tech/da-codec v0.1.3-0.20250519114140-bfa7133d4ad1/go.mod h1:yhTS9OVC0xQGhg7DN5iV5KZJvnSIlFWAxDdp+6jxQtY=
github.com/scroll-tech/da-codec v0.1.3-0.20250626091118-58b899494da6 h1:vb2XLvQwCf+F/ifP6P/lfeiQrHY6+Yb/E3R4KHXLqSE=
github.com/scroll-tech/da-codec v0.1.3-0.20250626091118-58b899494da6/go.mod h1:Z6kN5u2khPhiqHyk172kGB7o38bH/nj7Ilrb/46wZGg=
github.com/scroll-tech/zktrie v0.8.4 h1:UagmnZ4Z3ITCk+aUq9NQZJNAwnWl4gSxsLb2Nl7IgRE=
github.com/scroll-tech/zktrie v0.8.4/go.mod h1:XvNo7vAk8yxNyTjBDj5WIiFzYW4bx/gJ78+NK6Zn6Uk=
github.com/segmentio/kafka-go v0.1.0/go.mod h1:X6itGqS9L4jDletMsxZ7Dz+JFWxM6JHfPOCvTvk+EJo=

View file

@ -999,7 +999,7 @@ func EstimateL1MsgFee(ctx context.Context, b Backend, args TransactionArgs, bloc
}()
signer := types.MakeSigner(config, header.Number, header.Time)
return fees.EstimateL1DataFeeForMessage(msg, header.BaseFee, config, signer, evm.StateDB, header.Number)
return fees.EstimateL1DataFeeForMessage(msg, header.BaseFee, config, signer, evm.StateDB, header.Number, header.Time)
}
func DoCall(ctx context.Context, b Backend, args TransactionArgs, blockNrOrHash rpc.BlockNumberOrHash, overrides *StateOverride, timeout time.Duration, globalGasCap uint64) (*core.ExecutionResult, error) {
@ -1595,7 +1595,7 @@ func AccessList(ctx context.Context, b Backend, blockNrOrHash rpc.BlockNumberOrH
return nil, 0, nil, err
}
signer := types.MakeSigner(b.ChainConfig(), header.Number, header.Time)
l1DataFee, err := fees.EstimateL1DataFeeForMessage(msg, header.BaseFee, b.ChainConfig(), signer, statedb, header.Number)
l1DataFee, err := fees.EstimateL1DataFeeForMessage(msg, header.BaseFee, b.ChainConfig(), signer, statedb, header.Number, header.Time)
if err != nil {
return nil, 0, nil, fmt.Errorf("failed to apply transaction: %v err: %v", args.toTransaction().Hash(), err)
}

View file

@ -145,7 +145,7 @@ func odrContractCall(ctx context.Context, db ethdb.Database, config *params.Chai
//vmenv := core.NewEnv(statedb, config, bc, msg, header, vm.Config{})
gp := new(core.GasPool).AddGas(math.MaxUint64)
signer := types.MakeSigner(config, header.Number, header.Time)
l1DataFee, _ := fees.EstimateL1DataFeeForMessage(msg, header.BaseFee, config, signer, statedb, header.Number)
l1DataFee, _ := fees.EstimateL1DataFeeForMessage(msg, header.BaseFee, config, signer, statedb, header.Number, header.Time)
result, _ := core.ApplyMessage(vmenv, msg, gp, l1DataFee)
res = append(res, result.Return()...)
}
@ -159,7 +159,7 @@ func odrContractCall(ctx context.Context, db ethdb.Database, config *params.Chai
vmenv := vm.NewEVM(context, txContext, state, config, vm.Config{NoBaseFee: true})
gp := new(core.GasPool).AddGas(math.MaxUint64)
signer := types.MakeSigner(config, header.Number, header.Time)
l1DataFee, _ := fees.EstimateL1DataFeeForMessage(msg, header.BaseFee, config, signer, state, header.Number)
l1DataFee, _ := fees.EstimateL1DataFeeForMessage(msg, header.BaseFee, config, signer, state, header.Number, header.Time)
result, _ := core.ApplyMessage(vmenv, msg, gp, l1DataFee)
if state.Error() == nil {
res = append(res, result.Return()...)

View file

@ -65,7 +65,7 @@ func (leth *LightEthereum) stateAtTransaction(ctx context.Context, block *types.
}
// Not yet the searched for transaction, execute on top of the current state
vmenv := vm.NewEVM(context, txContext, statedb, leth.blockchain.Config(), vm.Config{})
l1DataFee, err := fees.CalculateL1DataFee(tx, statedb, leth.blockchain.Config(), block.Number())
l1DataFee, err := fees.CalculateL1DataFee(tx, statedb, leth.blockchain.Config(), block.Number(), block.Time())
if err != nil {
return nil, vm.BlockContext{}, nil, fmt.Errorf("transaction %#x failed: %v", tx.Hash(), err)
}

View file

@ -201,7 +201,7 @@ func odrContractCall(ctx context.Context, db ethdb.Database, bc *core.BlockChain
vmenv := vm.NewEVM(context, txContext, st, config, vm.Config{NoBaseFee: true})
gp := new(core.GasPool).AddGas(math.MaxUint64)
signer := types.MakeSigner(config, header.Number, header.Time)
l1DataFee, _ := fees.EstimateL1DataFeeForMessage(msg, header.BaseFee, config, signer, st, header.Number)
l1DataFee, _ := fees.EstimateL1DataFeeForMessage(msg, header.BaseFee, config, signer, st, header.Number, header.Time)
result, _ := core.ApplyMessage(vmenv, msg, gp, l1DataFee)
res = append(res, result.Return()...)
if st.Error() != nil {

View file

@ -74,6 +74,7 @@ type TxPool struct {
shanghai bool // Fork indicator whether we are in the shanghai stage.
currentHead *big.Int // Current blockchain head
currentTime uint64 // Current blockchain head time
}
// TxRelayBackend provides an interface to the mechanism that forwards transacions
@ -326,6 +327,7 @@ func (pool *TxPool) setNewHead(head *types.Header) {
pool.shanghai = pool.config.IsShanghai(next)
pool.currentHead = next
pool.currentTime = head.Time
}
// Stop stops the light transaction pool
@ -408,7 +410,7 @@ func (pool *TxPool) validateTx(ctx context.Context, tx *types.Transaction) error
// 2. If FeeVault is enabled, perform an additional check for L1 data fees.
if pool.config.Scroll.FeeVaultEnabled() {
// Get L1 data fee in current state
l1DataFee, err := fees.CalculateL1DataFee(tx, currentState, pool.config, pool.currentHead)
l1DataFee, err := fees.CalculateL1DataFee(tx, currentState, pool.config, pool.currentHead, pool.currentTime)
if err != nil {
return fmt.Errorf("failed to calculate L1 data fee, err: %w", err)
}

View file

@ -2,14 +2,17 @@ package fees
import (
"bytes"
"fmt"
"math"
"math/big"
"github.com/holiman/uint256"
"github.com/scroll-tech/da-codec/encoding"
"github.com/scroll-tech/go-ethereum/common"
"github.com/scroll-tech/go-ethereum/core/types"
"github.com/scroll-tech/go-ethereum/crypto"
"github.com/scroll-tech/go-ethereum/log"
"github.com/scroll-tech/go-ethereum/params"
"github.com/scroll-tech/go-ethereum/rollup/rcfg"
)
@ -60,9 +63,11 @@ type gpoState struct {
l1BlobBaseFee *big.Int
commitScalar *big.Int
blobScalar *big.Int
penaltyThreshold *big.Int
penaltyFactor *big.Int
}
func EstimateL1DataFeeForMessage(msg Message, baseFee *big.Int, config *params.ChainConfig, signer types.Signer, state StateDB, blockNumber *big.Int) (*big.Int, error) {
func EstimateL1DataFeeForMessage(msg Message, baseFee *big.Int, config *params.ChainConfig, signer types.Signer, state StateDB, blockNumber *big.Int, blockTime uint64) (*big.Int, error) {
if msg.IsL1MessageTx() {
return big.NewInt(0), nil
}
@ -74,22 +79,7 @@ func EstimateL1DataFeeForMessage(msg Message, baseFee *big.Int, config *params.C
return nil, err
}
raw, err := tx.MarshalBinary()
if err != nil {
return nil, err
}
gpoState := readGPOStorageSlots(rcfg.L1GasPriceOracleAddress, state)
var l1DataFee *big.Int
if !config.IsCurie(blockNumber) {
l1DataFee = calculateEncodedL1DataFee(raw, gpoState.overhead, gpoState.l1BaseFee, gpoState.scalar)
} else {
l1DataFee = calculateEncodedL1DataFeeCurie(raw, gpoState.l1BaseFee, gpoState.l1BlobBaseFee, gpoState.commitScalar, gpoState.blobScalar)
}
return l1DataFee, nil
return CalculateL1DataFee(tx, state, config, blockNumber, blockTime)
}
// asUnsignedTx turns a Message into a types.Transaction
@ -173,9 +163,58 @@ func readGPOStorageSlots(addr common.Address, state StateDB) gpoState {
gpoState.l1BlobBaseFee = state.GetState(addr, rcfg.L1BlobBaseFeeSlot).Big()
gpoState.commitScalar = state.GetState(addr, rcfg.CommitScalarSlot).Big()
gpoState.blobScalar = state.GetState(addr, rcfg.BlobScalarSlot).Big()
gpoState.penaltyThreshold = state.GetState(addr, rcfg.PenaltyThresholdSlot).Big()
gpoState.penaltyFactor = state.GetState(addr, rcfg.PenaltyFactorSlot).Big()
return gpoState
}
// estimateTxCompressionRatio estimates the compression ratio for transaction data using da-codec
// compression_ratio(tx) = size(tx) * PRECISION / size(zstd(tx))
func estimateTxCompressionRatio(data []byte, blockNumber uint64, blockTime uint64, config *params.ChainConfig) (*big.Int, error) {
if len(data) == 0 {
return nil, fmt.Errorf("raw data is empty")
}
// Compress data using da-codec
compressed, err := encoding.CompressScrollBatchBytes(data, blockNumber, blockTime, config)
if err != nil {
log.Error("Batch compression failed, using 1.0 compression ratio", "error", err, "data size", len(data), "data", common.Bytes2Hex(data))
return nil, fmt.Errorf("batch compression failed: %w", err)
}
if len(compressed) == 0 {
log.Error("Compressed data is empty, using 1.0 compression ratio", "data size", len(data), "data", common.Bytes2Hex(data))
return nil, fmt.Errorf("compressed data is empty")
}
// compression_ratio = size(tx) * PRECISION / size(zstd(tx))
originalSize := new(big.Int).SetUint64(uint64(len(data)))
compressedSize := new(big.Int).SetUint64(uint64(len(compressed)))
// Make sure compression ratio >= 1 by checking if compressed data is bigger or equal to original data
// This behavior is consistent with DA Batch compression in codecv7 and later versions
if len(compressed) >= len(data) {
log.Debug("Compressed data is bigger or equal to the original data, using 1.0 compression ratio", "original size", len(data), "compressed size", len(compressed))
return rcfg.Precision, nil
}
ratio := new(big.Int).Mul(originalSize, rcfg.Precision)
ratio.Div(ratio, compressedSize)
return ratio, nil
}
// calculatePenalty computes the penalty multiplier based on compression ratio
// penalty(tx) = compression_ratio(tx) >= penalty_threshold ? 1 * PRECISION : penalty_factor
func calculatePenalty(compressionRatio, penaltyThreshold, penaltyFactor *big.Int) *big.Int {
if compressionRatio.Cmp(penaltyThreshold) >= 0 {
// No penalty
return rcfg.Precision
}
// Apply penalty
return penaltyFactor
}
// calculateEncodedL1DataFee computes the L1 fee for an RLP-encoded tx
func calculateEncodedL1DataFee(data []byte, overhead, l1BaseFee *big.Int, scalar *big.Int) *big.Int {
l1GasUsed := calculateL1GasUsed(data, overhead)
@ -200,6 +239,51 @@ func calculateEncodedL1DataFeeCurie(data []byte, l1BaseFee *big.Int, l1BlobBaseF
return l1DataFee
}
// calculateEncodedL1DataFeeFeynman computes the L1 fee for an RLP-encoded tx, post Feynman
//
// Post Feynman formula:
// rollup_fee(tx) = (execScalar * l1BaseFee + blobScalar * l1BlobBaseFee) * size(tx) * penalty(tx) / PRECISION / PRECISION
//
// Where:
// penalty(tx) = compression_ratio(tx) >= penalty_threshold ? 1 * PRECISION : penalty_factor
//
// compression_ratio(tx) = size(tx) * PRECISION / size(zstd(tx))
// exec_scalar = compression_scalar * (commit_scalar + verification_scalar)
// blob_scalar = compression_scalar * blob_scalar
func calculateEncodedL1DataFeeFeynman(
data []byte,
l1BaseFee *big.Int,
l1BlobBaseFee *big.Int,
execScalar *big.Int,
blobScalar *big.Int,
penaltyThreshold *big.Int,
penaltyFactor *big.Int,
compressionRatio *big.Int,
) *big.Int {
// Calculate penalty multiplier
penalty := calculatePenalty(compressionRatio, penaltyThreshold, penaltyFactor)
// Transaction size (RLP-encoded)
txSize := big.NewInt(int64(len(data)))
// Compute gas components
execGas := new(big.Int).Mul(execScalar, l1BaseFee)
blobGas := new(big.Int).Mul(blobScalar, l1BlobBaseFee)
// fee per byte = execGas + blobGas
feePerByte := new(big.Int).Add(execGas, blobGas)
// l1DataFee = feePerByte * txSize * penalty
l1DataFee := new(big.Int).Mul(feePerByte, txSize)
l1DataFee.Mul(l1DataFee, penalty)
// Divide by rcfg.Precision (once for scalars, once for penalty)
l1DataFee.Div(l1DataFee, rcfg.Precision) // account for scalars
l1DataFee.Div(l1DataFee, rcfg.Precision) // accounts for penalty
return l1DataFee
}
// calculateL1GasUsed computes the L1 gas used based on the calldata and
// constant sized overhead. The overhead can be decreased as the cost of the
// batch submission goes down via contract optimizations. This will not overflow
@ -233,7 +317,7 @@ func mulAndScale(x *big.Int, y *big.Int, precision *big.Int) *big.Int {
return new(big.Int).Quo(z, precision)
}
func CalculateL1DataFee(tx *types.Transaction, state StateDB, config *params.ChainConfig, blockNumber *big.Int) (*big.Int, error) {
func CalculateL1DataFee(tx *types.Transaction, state StateDB, config *params.ChainConfig, blockNumber *big.Int, blockTime uint64) (*big.Int, error) {
if tx.IsL1MessageTx() {
return big.NewInt(0), nil
}
@ -249,8 +333,26 @@ func CalculateL1DataFee(tx *types.Transaction, state StateDB, config *params.Cha
if !config.IsCurie(blockNumber) {
l1DataFee = calculateEncodedL1DataFee(raw, gpoState.overhead, gpoState.l1BaseFee, gpoState.scalar)
} else {
} else if !config.IsFeynman(blockTime) {
l1DataFee = calculateEncodedL1DataFeeCurie(raw, gpoState.l1BaseFee, gpoState.l1BlobBaseFee, gpoState.commitScalar, gpoState.blobScalar)
} else {
// Calculate compression ratio for Feynman
compressionRatio, err := estimateTxCompressionRatio(raw, blockNumber.Uint64(), blockTime, config)
if err != nil {
return nil, fmt.Errorf("failed to estimate compression ratio: tx hash=%s: %w", tx.Hash().Hex(), err)
}
// The contract slot for commitScalar is changed to execScalar in Feynman
l1DataFee = calculateEncodedL1DataFeeFeynman(
raw,
gpoState.l1BaseFee,
gpoState.l1BlobBaseFee,
gpoState.commitScalar, // now represents execScalar
gpoState.blobScalar,
gpoState.penaltyThreshold,
gpoState.penaltyFactor,
compressionRatio,
)
}
// ensure l1DataFee fits into uint64 for circuit compatibility

View file

@ -5,6 +5,8 @@ import (
"testing"
"github.com/stretchr/testify/assert"
"github.com/scroll-tech/go-ethereum/params"
)
func TestL1DataFeeBeforeCurie(t *testing.T) {
@ -31,3 +33,109 @@ func TestL1DataFeeAfterCurie(t *testing.T) {
actual := calculateEncodedL1DataFeeCurie(data, l1BaseFee, l1BlobBaseFee, commitScalar, blobScalar)
assert.Equal(t, expected, actual)
}
func TestL1DataFeeFeynman(t *testing.T) {
l1BaseFee := new(big.Int).SetInt64(1_000_000_000)
l1BlobBaseFee := new(big.Int).SetInt64(1_000_000_000)
execScalar := new(big.Int).SetInt64(10)
blobScalar := new(big.Int).SetInt64(20)
penaltyThreshold := new(big.Int).SetInt64(6_000_000_000) // 6 * PRECISION
penaltyFactor := new(big.Int).SetInt64(2_000_000_000) // 2 * PRECISION (200% penalty)
// Test case 1: No penalty (compression ratio >= threshold)
t.Run("no penalty case", func(t *testing.T) {
data := make([]byte, 100) // txSize = 100
compressionRatio := new(big.Int).SetInt64(6_000_000_000) // exactly at threshold
// Since compression ratio >= penaltyThreshold, penalty = 1 * PRECISION
// feePerByte = execScalar * l1BaseFee + blobScalar * l1BlobBaseFee = 10 * 1_000_000_000 + 20 * 1_000_000_000 = 30_000_000_000
// l1DataFee = feePerByte * txSize * penalty / PRECISION / PRECISION
// = 30_000_000_000 * 100 * 1_000_000_000 / 1_000_000_000 / 1_000_000_000 = 3000
expected := new(big.Int).SetInt64(3000)
actual := calculateEncodedL1DataFeeFeynman(
data,
l1BaseFee,
l1BlobBaseFee,
execScalar,
blobScalar,
penaltyThreshold,
penaltyFactor,
compressionRatio,
)
assert.Equal(t, expected, actual)
})
// Test case 2: With penalty (compression ratio < threshold)
t.Run("with penalty case", func(t *testing.T) {
data := make([]byte, 100) // txSize = 100
compressionRatio := new(big.Int).SetInt64(5_000_000_000) // below threshold
// Since compression ratio < penaltyThreshold, penalty = penaltyFactor
// feePerByte = execScalar * l1BaseFee + blobScalar * l1BlobBaseFee = 30_000_000_000
// l1DataFee = feePerByte * txSize * penaltyFactor / PRECISION / PRECISION
// = 30_000_000_000 * 100 * 2_000_000_000 / 1_000_000_000 / 1_000_000_000 = 6000
expected := new(big.Int).SetInt64(6000)
actual := calculateEncodedL1DataFeeFeynman(
data,
l1BaseFee,
l1BlobBaseFee,
execScalar,
blobScalar,
penaltyThreshold,
penaltyFactor,
compressionRatio,
)
assert.Equal(t, expected, actual)
})
}
func TestEstimateTxCompressionRatio(t *testing.T) {
// Mock config that would select a specific codec version
// Note: You'll need to adjust this based on your actual params.ChainConfig structure
t.Run("empty data", func(t *testing.T) {
data := []byte{}
// Should return 1.0 ratio (PRECISION)
ratio, err := estimateTxCompressionRatio(data, 1000000, 1700000000, params.TestChainConfig)
assert.Error(t, err, "raw data is empty")
assert.Nil(t, ratio)
// The exact value depends on rcfg.Precision, but should be the "1.0" equivalent
})
t.Run("non-empty data", func(t *testing.T) {
// Create some compressible data
data := make([]byte, 1000)
for i := range data {
data[i] = byte(i % 10) // Create patterns for better compression
}
ratio, err := estimateTxCompressionRatio(data, 1000000, 1700000000, params.TestChainConfig)
assert.NoError(t, err)
assert.NotNil(t, ratio)
// Should return a ratio > 1.0 (since compressed size < original size)
})
}
func TestCalculatePenalty(t *testing.T) {
precision := new(big.Int).SetInt64(1_000_000_000) // PRECISION
penaltyThreshold := new(big.Int).SetInt64(6_000_000_000) // 6 * PRECISION
penaltyFactor := new(big.Int).SetInt64(2_000_000_000) // 2 * PRECISION
t.Run("no penalty when ratio >= threshold", func(t *testing.T) {
compressionRatio := new(big.Int).SetInt64(6_000_000_000) // exactly at threshold
penalty := calculatePenalty(compressionRatio, penaltyThreshold, penaltyFactor)
assert.Equal(t, precision, penalty)
})
t.Run("penalty when ratio < threshold", func(t *testing.T) {
compressionRatio := new(big.Int).SetInt64(5_000_000_000) // below threshold
penalty := calculatePenalty(compressionRatio, penaltyThreshold, penaltyFactor)
assert.Equal(t, penaltyFactor, penalty)
})
}

View file

@ -239,7 +239,7 @@ func (env *TraceEnv) GetBlockTrace(block *types.Block) (*types.BlockTrace, error
msg, _ := tx.AsMessage(env.signer, block.BaseFee())
env.state.SetTxContext(tx.Hash(), i)
vmenv := vm.NewEVM(env.blockCtx, core.NewEVMTxContext(msg), env.state, env.chainConfig, vm.Config{})
l1DataFee, err := fees.CalculateL1DataFee(tx, env.state, env.chainConfig, block.Number())
l1DataFee, err := fees.CalculateL1DataFee(tx, env.state, env.chainConfig, block.Number(), block.Time())
if err != nil {
failed = err
break
@ -342,7 +342,7 @@ func (env *TraceEnv) getTxResult(state *state.StateDB, index int, block *types.B
state.SetTxContext(txctx.TxHash, txctx.TxIndex)
// Computes the new state by applying the given message.
l1DataFee, err := fees.CalculateL1DataFee(tx, state, env.chainConfig, block.Number())
l1DataFee, err := fees.CalculateL1DataFee(tx, state, env.chainConfig, block.Number(), block.Time())
if err != nil {
return err
}

View file

@ -251,7 +251,7 @@ func (t *StateTest) RunNoVerify(subtest StateSubtest, vmconfig vm.Config, snapsh
gaspool := new(core.GasPool)
gaspool.AddGas(block.GasLimit())
l1DataFee, err := fees.CalculateL1DataFee(&ttx, statedb, config, block.Number())
l1DataFee, err := fees.CalculateL1DataFee(&ttx, statedb, config, block.Number(), block.Time())
if err != nil {
return nil, nil, common.Hash{}, err
}