diff --git a/rollup/fees/rollup_fee.go b/rollup/fees/rollup_fee.go new file mode 100644 index 0000000000..ab03e481f5 --- /dev/null +++ b/rollup/fees/rollup_fee.go @@ -0,0 +1,225 @@ +package fees + +import ( + "bytes" + "errors" + "fmt" + "math/big" + + "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/params" + "github.com/scroll-tech/go-ethereum/rollup/rcfg" +) + +var ( + // txExtraDataBytes is the number of bytes that we commit to L1 in addition + // to the RLP-encoded signed transaction. Note that these are all assumed + // to be non-zero. + // - tx length prefix: 4 bytes + txExtraDataBytes = uint64(4) +) + +// Message represents the interface of a message. +// It should be a subset of the methods found on +// types.Message +type Message interface { + From() common.Address + To() *common.Address + GasPrice() *big.Int + Gas() uint64 + GasFeeCap() *big.Int + GasTipCap() *big.Int + Value() *big.Int + Nonce() uint64 + Data() []byte + AccessList() types.AccessList + IsL1MessageTx() bool +} + +// StateDB represents the StateDB interface +// required to compute the L1 fee +type StateDB interface { + GetState(common.Address, common.Hash) common.Hash + GetBalance(addr common.Address) *big.Int +} + +func EstimateL1DataFeeForMessage(msg Message, baseFee, chainID *big.Int, signer types.Signer, state StateDB) (*big.Int, error) { + if msg.IsL1MessageTx() { + return big.NewInt(0), nil + } + + unsigned := asUnsignedTx(msg, baseFee, chainID) + // with v=1 + tx, err := unsigned.WithSignature(signer, append(bytes.Repeat([]byte{0xff}, crypto.SignatureLength-1), 0x01)) + if err != nil { + return nil, err + } + + raw, err := rlpEncode(tx) + if err != nil { + return nil, err + } + + l1BaseFee, overhead, scalar := readGPOStorageSlots(rcfg.L1GasPriceOracleAddress, state) + l1DataFee := calculateEncodedL1DataFee(raw, overhead, l1BaseFee, scalar) + return l1DataFee, nil +} + +// asUnsignedTx turns a Message into a types.Transaction +func asUnsignedTx(msg Message, baseFee, chainID *big.Int) *types.Transaction { + if baseFee == nil { + if msg.AccessList() == nil { + return asUnsignedLegacyTx(msg) + } + + return asUnsignedAccessListTx(msg, chainID) + } + + return asUnsignedDynamicTx(msg, chainID) +} + +func asUnsignedLegacyTx(msg Message) *types.Transaction { + return types.NewTx(&types.LegacyTx{ + Nonce: msg.Nonce(), + To: msg.To(), + Value: msg.Value(), + Gas: msg.Gas(), + GasPrice: msg.GasPrice(), + Data: msg.Data(), + }) +} + +func asUnsignedAccessListTx(msg Message, chainID *big.Int) *types.Transaction { + return types.NewTx(&types.AccessListTx{ + Nonce: msg.Nonce(), + To: msg.To(), + Value: msg.Value(), + Gas: msg.Gas(), + GasPrice: msg.GasPrice(), + Data: msg.Data(), + AccessList: msg.AccessList(), + ChainID: chainID, + }) +} + +func asUnsignedDynamicTx(msg Message, chainID *big.Int) *types.Transaction { + return types.NewTx(&types.DynamicFeeTx{ + Nonce: msg.Nonce(), + To: msg.To(), + Value: msg.Value(), + Gas: msg.Gas(), + GasFeeCap: msg.GasFeeCap(), + GasTipCap: msg.GasTipCap(), + Data: msg.Data(), + AccessList: msg.AccessList(), + ChainID: chainID, + }) +} + +// rlpEncode RLP encodes the transaction into bytes +func rlpEncode(tx *types.Transaction) ([]byte, error) { + raw := new(bytes.Buffer) + if err := tx.EncodeRLP(raw); err != nil { + return nil, err + } + + return raw.Bytes(), nil +} + +func readGPOStorageSlots(addr common.Address, state StateDB) (*big.Int, *big.Int, *big.Int) { + l1BaseFee := state.GetState(addr, rcfg.L1BaseFeeSlot) + overhead := state.GetState(addr, rcfg.OverheadSlot) + scalar := state.GetState(addr, rcfg.ScalarSlot) + return l1BaseFee.Big(), overhead.Big(), scalar.Big() +} + +// calculateEncodedL1DataFee computes the L1 fee for an RLP-encoded tx +func calculateEncodedL1DataFee(data []byte, overhead, l1GasPrice *big.Int, scalar *big.Int) *big.Int { + l1GasUsed := CalculateL1GasUsed(data, overhead) + l1DataFee := new(big.Int).Mul(l1GasUsed, l1GasPrice) + return mulAndScale(l1DataFee, scalar, rcfg.Precision) +} + +// 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 +// under standard network conditions. +func CalculateL1GasUsed(data []byte, overhead *big.Int) *big.Int { + zeroes, ones := zeroesAndOnes(data) + zeroesGas := zeroes * params.TxDataZeroGas + onesGas := (ones + txExtraDataBytes) * params.TxDataNonZeroGasEIP2028 + l1Gas := new(big.Int).SetUint64(zeroesGas + onesGas) + return new(big.Int).Add(l1Gas, overhead) +} + +// zeroesAndOnes counts the number of 0 bytes and non 0 bytes in a byte slice +func zeroesAndOnes(data []byte) (uint64, uint64) { + var zeroes uint64 + var ones uint64 + for _, byt := range data { + if byt == 0 { + zeroes++ + } else { + ones++ + } + } + return zeroes, ones +} + +// mulAndScale multiplies a big.Int by a big.Int and then scale it by precision, +// rounded towards zero +func mulAndScale(x *big.Int, y *big.Int, precision *big.Int) *big.Int { + z := new(big.Int).Mul(x, y) + return new(big.Int).Quo(z, precision) +} + +func CalculateL1DataFee(tx *types.Transaction, state StateDB) (*big.Int, error) { + if tx.IsL1MessageTx() { + return big.NewInt(0), nil + } + + raw, err := rlpEncode(tx) + if err != nil { + return nil, err + } + + l1BaseFee, overhead, scalar := readGPOStorageSlots(rcfg.L1GasPriceOracleAddress, state) + l1DataFee := calculateEncodedL1DataFee(raw, overhead, l1BaseFee, scalar) + return l1DataFee, nil +} + +func calculateL2Fee(tx *types.Transaction) *big.Int { + l2GasLimit := new(big.Int).SetUint64(tx.Gas()) + return new(big.Int).Mul(tx.GasPrice(), l2GasLimit) +} + +func VerifyFee(signer types.Signer, tx *types.Transaction, state StateDB) error { + from, err := types.Sender(signer, tx) + if err != nil { + return errors.New("invalid transaction: invalid sender") + } + + balance := state.GetBalance(from) + l2Fee := calculateL2Fee(tx) + l1DataFee, err := CalculateL1DataFee(tx, state) + if err != nil { + return fmt.Errorf("invalid transaction: %w", err) + } + + cost := tx.Value() + cost = cost.Add(cost, l2Fee) + if balance.Cmp(cost) < 0 { + return errors.New("invalid transaction: insufficient funds for gas * price + value") + } + + cost = cost.Add(cost, l1DataFee) + if balance.Cmp(cost) < 0 { + return errors.New("invalid transaction: insufficient funds for l1fee + gas * price + value") + } + + // TODO: check GasPrice is in an expected range + + return nil +} diff --git a/rollup/fees/rollup_fee_test.go b/rollup/fees/rollup_fee_test.go new file mode 100644 index 0000000000..3e8b65045b --- /dev/null +++ b/rollup/fees/rollup_fee_test.go @@ -0,0 +1,20 @@ +package fees + +import ( + "math/big" + "testing" + + "github.com/stretchr/testify/assert" +) + +func TestCalculateEncodedL1DataFee(t *testing.T) { + l1BaseFee := new(big.Int).SetUint64(15000000) + + data := []byte{0, 10, 1, 0} + overhead := new(big.Int).SetUint64(100) + scalar := new(big.Int).SetUint64(10) + + expected := new(big.Int).SetUint64(184) // 184.2 + actual := calculateEncodedL1DataFee(data, overhead, l1BaseFee, scalar) + assert.Equal(t, expected, actual) +}