Skip to content

Commit

Permalink
Merge pull request bnb-chain#249 from base-org/flz-l1-cost-func
Browse files Browse the repository at this point in the history
Fjord: Add FastLZ compression into L1CostFunc
  • Loading branch information
sebastianst authored May 15, 2024
2 parents 6b2bf0f + f8b1629 commit 285e183
Show file tree
Hide file tree
Showing 3 changed files with 394 additions and 76 deletions.
2 changes: 1 addition & 1 deletion core/types/receipt.go
Original file line number Diff line number Diff line change
Expand Up @@ -87,7 +87,7 @@ type Receipt struct {
// Optimism: extend receipts with L1 fee info
L1GasPrice *big.Int `json:"l1GasPrice,omitempty"` // Present from pre-bedrock. L1 Basefee after Bedrock
L1BlobBaseFee *big.Int `json:"l1BlobBaseFee,omitempty"` // Always nil prior to the Ecotone hardfork
L1GasUsed *big.Int `json:"l1GasUsed,omitempty"` // Present from pre-bedrock
L1GasUsed *big.Int `json:"l1GasUsed,omitempty"` // Present from pre-bedrock, deprecated as of Fjord
L1Fee *big.Int `json:"l1Fee,omitempty"` // Present from pre-bedrock
FeeScalar *big.Float `json:"l1FeeScalar,omitempty"` // Present from pre-bedrock to Ecotone. Nil after Ecotone
L1BaseFeeScalar *uint64 `json:"l1BaseFeeScalar,omitempty"` // Always nil prior to the Ecotone hardfork
Expand Down
254 changes: 207 additions & 47 deletions core/types/rollup_cost.go
Original file line number Diff line number Diff line change
Expand Up @@ -32,9 +32,9 @@ const (
// sequence number and have the following Solidity offsets within the slot. Note that Solidity
// offsets correspond to the last byte of the value in the slot, counting backwards from the
// end of the slot. For example, The 8-byte sequence number has offset 0, and is therefore
// stored as big-endian format in bytes [24:32] of the slot.
BaseFeeScalarSlotOffset = 12 // bytes [16:20] of the slot
BlobBaseFeeScalarSlotOffset = 8 // bytes [20:24] of the slot
// stored as big-endian format in bytes [24:32) of the slot.
BaseFeeScalarSlotOffset = 12 // bytes [16:20) of the slot
BlobBaseFeeScalarSlotOffset = 8 // bytes [20:24) of the slot

// scalarSectionStart is the beginning of the scalar values segment in the slot
// array. baseFeeScalar is in the first four bytes of the segment, blobBaseFeeScalar the next
Expand Down Expand Up @@ -62,7 +62,7 @@ var (
OverheadSlot = common.BigToHash(big.NewInt(5))
ScalarSlot = common.BigToHash(big.NewInt(6))

// L2BlobBaseFeeSlot was added with the Ecotone upgrade and stores the blobBaseFee L1 gas
// L1BlobBaseFeeSlot was added with the Ecotone upgrade and stores the blobBaseFee L1 gas
// attribute.
L1BlobBaseFeeSlot = common.BigToHash(big.NewInt(7))
// L1FeeScalarsSlot as of the Ecotone upgrade stores the 32-bit basefeeScalar and
Expand All @@ -72,15 +72,23 @@ var (

oneMillion = big.NewInt(1_000_000)
ecotoneDivisor = big.NewInt(1_000_000 * 16)
fjordDivisor = big.NewInt(1_000_000_000_000)
sixteen = big.NewInt(16)

L1CostIntercept = big.NewInt(-42_585_600)
L1CostFastlzCoef = big.NewInt(836_500)

MinTransactionSize = big.NewInt(100)
MinTransactionSizeScaled = new(big.Int).Mul(MinTransactionSize, big.NewInt(1e6))

emptyScalars = make([]byte, 8)
)

// RollupCostData is a transaction structure that caches data for quickly computing the data
// availablility costs for the transaction.
// availability costs for the transaction.
type RollupCostData struct {
zeroes, ones uint64
Zeroes, Ones uint64
FastLzSize uint64
}

type StateGetter interface {
Expand All @@ -98,11 +106,12 @@ type l1CostFunc func(rcd RollupCostData) (fee, gasUsed *big.Int)
func NewRollupCostData(data []byte) (out RollupCostData) {
for _, b := range data {
if b == 0 {
out.zeroes++
out.Zeroes++
} else {
out.ones++
out.Ones++
}
}
out.FastLzSize = uint64(FlzCompressLen(data))
return out
}

Expand All @@ -114,6 +123,44 @@ func NewL1CostFunc(config *params.ChainConfig, statedb StateGetter) L1CostFunc {
}
forBlock := ^uint64(0)
var cachedFunc l1CostFunc
selectFunc := func(blockTime uint64) l1CostFunc {
if !config.IsOptimismEcotone(blockTime) {
return newL1CostFuncBedrock(config, statedb, blockTime)
}

// Note: the various state variables below are not initialized from the DB until this
// point to allow deposit transactions from the block to be processed first by state
// transition. This behavior is consensus critical!
l1FeeScalars := statedb.GetState(L1BlockAddr, L1FeeScalarsSlot).Bytes()
l1BlobBaseFee := statedb.GetState(L1BlockAddr, L1BlobBaseFeeSlot).Big()
l1BaseFee := statedb.GetState(L1BlockAddr, L1BaseFeeSlot).Big()

// Edge case: the very first Ecotone block requires we use the Bedrock cost
// function. We detect this scenario by checking if the Ecotone parameters are
// unset. Note here we rely on assumption that the scalar parameters are adjacent
// in the buffer and l1BaseFeeScalar comes first. We need to check this prior to
// other forks, as the first block of Fjord and Ecotone could be the same block.
firstEcotoneBlock := l1BlobBaseFee.BitLen() == 0 &&
bytes.Equal(emptyScalars, l1FeeScalars[scalarSectionStart:scalarSectionStart+8])
if firstEcotoneBlock {
log.Info("using bedrock l1 cost func for first Ecotone block", "time", blockTime)
return newL1CostFuncBedrock(config, statedb, blockTime)
}

l1BaseFeeScalar, l1BlobBaseFeeScalar := extractEcotoneFeeParams(l1FeeScalars)

if config.IsOptimismFjord(blockTime) {
return NewL1CostFuncFjord(
l1BaseFee,
l1BlobBaseFee,
l1BaseFeeScalar,
l1BlobBaseFeeScalar,
)
} else {
return newL1CostFuncEcotone(l1BaseFee, l1BlobBaseFee, l1BaseFeeScalar, l1BlobBaseFeeScalar)
}
}

return func(rollupCostData RollupCostData, blockTime uint64) *big.Int {
if rollupCostData == (RollupCostData{}) {
return nil // Do not charge if there is no rollup cost-data (e.g. RPC call or deposit).
Expand All @@ -125,31 +172,7 @@ func NewL1CostFunc(config *params.ChainConfig, statedb StateGetter) L1CostFunc {
log.Info("l1 cost func re-used for different L1 block", "oldTime", forBlock, "newTime", blockTime)
}
forBlock = blockTime
// Note: the various state variables below are not initialized from the DB until this
// point to allow deposit transactions from the block to be processed first by state
// transition. This behavior is consensus critical!
if !config.IsOptimismEcotone(blockTime) {
cachedFunc = newL1CostFuncBedrock(config, statedb, blockTime)
} else {
l1BlobBaseFee := statedb.GetState(L1BlockAddr, L1BlobBaseFeeSlot).Big()
l1FeeScalars := statedb.GetState(L1BlockAddr, L1FeeScalarsSlot).Bytes()

// Edge case: the very first Ecotone block requires we use the Bedrock cost
// function. We detect this scenario by checking if the Ecotone parameters are
// unset. Not here we rely on assumption that the scalar parameters are adjacent
// in the buffer and basefeeScalar comes first.
if l1BlobBaseFee.BitLen() == 0 &&
bytes.Equal(emptyScalars, l1FeeScalars[scalarSectionStart:scalarSectionStart+8]) {
log.Info("using bedrock l1 cost func for first Ecotone block", "time", blockTime)
cachedFunc = newL1CostFuncBedrock(config, statedb, blockTime)
} else {
l1BaseFee := statedb.GetState(L1BlockAddr, L1BaseFeeSlot).Big()
offset := scalarSectionStart
l1BaseFeeScalar := new(big.Int).SetBytes(l1FeeScalars[offset : offset+4])
l1BlobBaseFeeScalar := new(big.Int).SetBytes(l1FeeScalars[offset+4 : offset+8])
cachedFunc = newL1CostFuncEcotone(l1BaseFee, l1BlobBaseFee, l1BaseFeeScalar, l1BlobBaseFeeScalar)
}
}
cachedFunc = selectFunc(blockTime)
}
fee, _ := cachedFunc(rollupCostData)
return fee
Expand All @@ -173,11 +196,11 @@ func newL1CostFuncBedrockHelper(l1BaseFee, overhead, scalar *big.Int, isRegolith
if rollupCostData == (RollupCostData{}) {
return nil, nil // Do not charge if there is no rollup cost-data (e.g. RPC call or deposit)
}
gas := rollupCostData.zeroes * params.TxDataZeroGas
gas := rollupCostData.Zeroes * params.TxDataZeroGas
if isRegolith {
gas += rollupCostData.ones * params.TxDataNonZeroGasEIP2028
gas += rollupCostData.Ones * params.TxDataNonZeroGasEIP2028
} else {
gas += (rollupCostData.ones + 68) * params.TxDataNonZeroGasEIP2028
gas += (rollupCostData.Ones + 68) * params.TxDataNonZeroGasEIP2028
}
gasWithOverhead := new(big.Int).SetUint64(gas)
gasWithOverhead.Add(gasWithOverhead, overhead)
Expand All @@ -190,8 +213,7 @@ func newL1CostFuncBedrockHelper(l1BaseFee, overhead, scalar *big.Int, isRegolith
// very first block of the upgrade.
func newL1CostFuncEcotone(l1BaseFee, l1BlobBaseFee, l1BaseFeeScalar, l1BlobBaseFeeScalar *big.Int) l1CostFunc {
return func(costData RollupCostData) (fee, calldataGasUsed *big.Int) {
calldataGas := (costData.zeroes * params.TxDataZeroGas) + (costData.ones * params.TxDataNonZeroGasEIP2028)
calldataGasUsed = new(big.Int).SetUint64(calldataGas)
calldataGasUsed = bedrockCalldataGasUsed(costData)

// Ecotone L1 cost function:
//
Expand Down Expand Up @@ -240,8 +262,30 @@ func extractL1GasParams(config *params.ChainConfig, time uint64, data []byte) (g
// edge case: for the very first Ecotone block we still need to use the Bedrock
// function. We detect this edge case by seeing if the function selector is the old one
// If so, fall through to the pre-ecotone format
// Both Ecotone and Fjord use the same function selector
if config.IsEcotone(time) && len(data) >= 4 && !bytes.Equal(data[0:4], BedrockL1AttributesSelector) {
return extractL1GasParamsEcotone(data)
p, err := extractL1GasParamsPostEcotone(data)
if err != nil {
return gasParams{}, err
}

if config.IsFjord(time) {
p.costFunc = NewL1CostFuncFjord(
p.l1BaseFee,
p.l1BlobBaseFee,
big.NewInt(int64(*p.l1BaseFeeScalar)),
big.NewInt(int64(*p.l1BlobBaseFeeScalar)),
)
} else {
p.costFunc = newL1CostFuncEcotone(
p.l1BaseFee,
p.l1BlobBaseFee,
big.NewInt(int64(*p.l1BaseFeeScalar)),
big.NewInt(int64(*p.l1BlobBaseFeeScalar)),
)
}

return p, nil
}
return extractL1GasParamsPreEcotone(config, time, data)
}
Expand All @@ -264,35 +308,31 @@ func extractL1GasParamsPreEcotone(config *params.ChainConfig, time uint64, data
}, nil
}

// extractEcotoneL1GasParams extracts the gas parameters necessary to compute gas from L1 attribute
// extractL1GasParamsPostEcotone extracts the gas parameters necessary to compute gas from L1 attribute
// info calldata after the Ecotone upgrade, but not for the very first Ecotone block.
func extractL1GasParamsEcotone(data []byte) (gasParams, error) {
func extractL1GasParamsPostEcotone(data []byte) (gasParams, error) {
if len(data) != 164 {
return gasParams{}, fmt.Errorf("expected 164 L1 info bytes, got %d", len(data))
}
// data layout assumed for Ecotone:
// offset type varname
// 0 <selector>
// 0 <selector>
// 4 uint32 _basefeeScalar
// 8 uint32 _blobBaseFeeScalar
// 12 uint64 _sequenceNumber,
// 20 uint64 _timestamp,
// 28 uint64 _l1BlockNumber
// 36 uint256 _basefee,
// 68 uint256 _blobBaseFee,
// 100 bytes32 _hash,
// 100 bytes32 _hash,
// 132 bytes32 _batcherHash,
l1BaseFee := new(big.Int).SetBytes(data[36:68])
l1BlobBaseFee := new(big.Int).SetBytes(data[68:100])
l1BaseFeeScalar := binary.BigEndian.Uint32(data[4:8])
l1BaseFeeScalarBig := big.NewInt(int64(l1BaseFeeScalar))
l1BlobBaseFeeScalar := binary.BigEndian.Uint32(data[8:12])
l1BlobBaseFeeScalarBig := big.NewInt(int64(l1BlobBaseFeeScalar))
costFunc := newL1CostFuncEcotone(l1BaseFee, l1BlobBaseFee, l1BaseFeeScalarBig, l1BlobBaseFeeScalarBig)
return gasParams{
l1BaseFee: l1BaseFee,
l1BlobBaseFee: l1BlobBaseFee,
costFunc: costFunc,
l1BaseFeeScalar: &l1BaseFeeScalar,
l1BlobBaseFeeScalar: &l1BlobBaseFeeScalar,
}, nil
Expand All @@ -311,3 +351,123 @@ func l1CostHelper(gasWithOverhead, l1BaseFee, scalar *big.Int) *big.Int {
fee.Mul(fee, l1BaseFee).Mul(fee, scalar).Div(fee, oneMillion)
return fee
}

// NewL1CostFuncFjord returns an l1 cost function suitable for the Fjord upgrade
func NewL1CostFuncFjord(l1BaseFee, l1BlobBaseFee, baseFeeScalar, blobFeeScalar *big.Int) l1CostFunc {
return func(costData RollupCostData) (fee, calldataGasUsed *big.Int) {
// Fjord L1 cost function:
//l1FeeScaled = baseFeeScalar*l1BaseFee*16 + blobFeeScalar*l1BlobBaseFee
//estimatedSize = max(minTransactionSize, intercept + fastlzCoef*fastlzSize)
//l1Cost = estimatedSize * l1FeeScaled / 1e12

scaledL1BaseFee := new(big.Int).Mul(baseFeeScalar, l1BaseFee)
calldataCostPerByte := new(big.Int).Mul(scaledL1BaseFee, sixteen)
blobCostPerByte := new(big.Int).Mul(blobFeeScalar, l1BlobBaseFee)
l1FeeScaled := new(big.Int).Add(calldataCostPerByte, blobCostPerByte)

fastLzSize := new(big.Int).SetUint64(costData.FastLzSize)
estimatedSize := new(big.Int).Add(L1CostIntercept, new(big.Int).Mul(L1CostFastlzCoef, fastLzSize))

if estimatedSize.Cmp(MinTransactionSizeScaled) < 0 {
estimatedSize.Set(MinTransactionSizeScaled)
}

l1CostScaled := new(big.Int).Mul(estimatedSize, l1FeeScaled)
l1Cost := new(big.Int).Div(l1CostScaled, fjordDivisor)

calldataGasUsed = new(big.Int).Mul(estimatedSize, new(big.Int).SetUint64(params.TxDataNonZeroGasEIP2028))
calldataGasUsed.Div(calldataGasUsed, big.NewInt(1e6))

return l1Cost, calldataGasUsed
}
}

func extractEcotoneFeeParams(l1FeeParams []byte) (l1BaseFeeScalar, l1BlobBaseFeeScalar *big.Int) {
offset := scalarSectionStart
l1BaseFeeScalar = new(big.Int).SetBytes(l1FeeParams[offset : offset+4])
l1BlobBaseFeeScalar = new(big.Int).SetBytes(l1FeeParams[offset+4 : offset+8])
return
}

func bedrockCalldataGasUsed(costData RollupCostData) (calldataGasUsed *big.Int) {
calldataGas := (costData.Zeroes * params.TxDataZeroGas) + (costData.Ones * params.TxDataNonZeroGasEIP2028)
return new(big.Int).SetUint64(calldataGas)
}

// FlzCompressLen returns the length of the data after compression through FastLZ, based on
// https://github.com/Vectorized/solady/blob/5315d937d79b335c668896d7533ac603adac5315/js/solady.js
func FlzCompressLen(ib []byte) uint32 {
n := uint32(0)
ht := make([]uint32, 8192)
u24 := func(i uint32) uint32 {
return uint32(ib[i]) | (uint32(ib[i+1]) << 8) | (uint32(ib[i+2]) << 16)
}
cmp := func(p uint32, q uint32, e uint32) uint32 {
l := uint32(0)
for e -= q; l < e; l++ {
if ib[p+l] != ib[q+l] {
e = 0
}
}
return l
}
literals := func(r uint32) {
n += 0x21 * (r / 0x20)
r %= 0x20
if r != 0 {
n += r + 1
}
}
match := func(l uint32) {
l--
n += 3 * (l / 262)
if l%262 >= 6 {
n += 3
} else {
n += 2
}
}
hash := func(v uint32) uint32 {
return ((2654435769 * v) >> 19) & 0x1fff
}
setNextHash := func(ip uint32) uint32 {
ht[hash(u24(ip))] = ip
return ip + 1
}
a := uint32(0)
ipLimit := uint32(len(ib)) - 13
if len(ib) < 13 {
ipLimit = 0
}
for ip := a + 2; ip < ipLimit; {
r := uint32(0)
d := uint32(0)
for {
s := u24(ip)
h := hash(s)
r = ht[h]
ht[h] = ip
d = ip - r
if ip >= ipLimit {
break
}
ip++
if d <= 0x1fff && s == u24(r) {
break
}
}
if ip >= ipLimit {
break
}
ip--
if ip > a {
literals(ip - a)
}
l := cmp(r+3, ip+3, ipLimit+9)
match(l)
ip = setNextHash(setNextHash(ip + l))
a = ip
}
literals(uint32(len(ib)) - a)
return n
}
Loading

0 comments on commit 285e183

Please sign in to comment.