Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
16 commits
Select commit Hold shift + click to select a range
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions arbos/l2pricing/l2pricing.go
Original file line number Diff line number Diff line change
Expand Up @@ -48,9 +48,9 @@ var multiGasBaseFeesKey []byte = []byte{2}

const GethBlockGasLimit = 1 << 50

// TODO(NIT-4152): Number of constraints limited because of retryable redeem gas cost calculation.
// Number of single-gas constraints limited because of retryable redeem gas cost calculation.
// The limit is ignored starting from ArbOS version 60.
const GasConstraintsMaxNum = 20
const MultiGasConstraintsMaxNum = 15

// MaxPricingExponentBips caps the basefee growth: exp(8.5) ~= x5,000 min base fee.
const MaxPricingExponentBips = arbmath.Bips(85_000)
Expand Down
38 changes: 11 additions & 27 deletions arbos/l2pricing/model.go
Original file line number Diff line number Diff line change
Expand Up @@ -14,9 +14,6 @@ import (
"github.com/offchainlabs/nitro/util/arbmath"
)

const ArbosSingleGasConstraintsVersion = params.ArbosVersion_50
const ArbosMultiGasConstraintsVersion = params.ArbosVersion_60

const InitialSpeedLimitPerSecondV0 = 1000000
const InitialPerBlockGasLimitV0 uint64 = 20 * 1000000
const InitialSpeedLimitPerSecondV6 = 7000000
Expand All @@ -27,6 +24,9 @@ const InitialPricingInertia = 102
const InitialBacklogTolerance = 10
const InitialPerTxGasLimitV50 uint64 = 32 * 1000000

// Static price equivalent to the single gas backlog update cost
const MultiConstraintStaticBacklogUpdateCost = storage.StorageReadCost + storage.StorageWriteCost

type GasModel int

const (
Expand All @@ -39,7 +39,7 @@ const (
// GasModelToUse returns the active gas-pricing model based on ArbOS version
// and whether the corresponding constraints are currently configured.
func (ps *L2PricingState) GasModelToUse() (GasModel, error) {
if ps.ArbosVersion >= ArbosMultiGasConstraintsVersion {
if ps.ArbosVersion >= params.ArbosVersion_MultiGasConstraintsVersion {
constraintsLength, err := ps.MultiGasConstraintsLength()
if err != nil {
return GasModelUnknown, err
Expand All @@ -48,7 +48,7 @@ func (ps *L2PricingState) GasModelToUse() (GasModel, error) {
return GasModelMultiGasConstraints, nil
}
}
if ps.ArbosVersion >= ArbosSingleGasConstraintsVersion {
if ps.ArbosVersion >= params.ArbosVersion_SingleGasConstraintsVersion {
constraintsLength, err := ps.GasConstraintsLength()
if err != nil {
return GasModelUnknown, err
Expand Down Expand Up @@ -149,39 +149,23 @@ func applyGasDelta(op BacklogOperation, backlog uint64, delta uint64) uint64 {
}
}

// TODO(NIT-4152): eliminate manual gas calculation
// BacklogUpdateCost returns the gas cost for updating the backlog in the active pricing model.
func (ps *L2PricingState) BacklogUpdateCost() uint64 {
result := uint64(0)

// Multi-dimensional pricer overhead (ArbOS 60 and later)
if ps.ArbosVersion >= ArbosMultiGasConstraintsVersion {
// Read multi-gas constraints length (GasModelToUse)
// This overhead applies even when no constraints are configured.
result += storage.StorageReadCost

// updateMultiGasConstraintsBacklogs costs
constraintsLength, _ := ps.multiGasConstraints.Length()
if constraintsLength > 0 {
result += storage.StorageReadCost // read length to traverse

// DecrementBacklog costs for each multi-dimensional constraint
result += constraintsLength * uint64(multigas.NumResourceKind) * storage.StorageReadCost
result += constraintsLength * (storage.StorageReadCost + storage.StorageWriteCost)
return result
}
// No return here, fallthrough to single-constraint costs
// Charge static price for any pricer starting from ArbosVersion_MultiGasConstraintsVersion
if ps.ArbosVersion >= params.ArbosVersion_MultiGasConstraintsVersion {
return MultiConstraintStaticBacklogUpdateCost
}

result := uint64(0)
// Single-dimensional constraint pricer costs
// This overhead applies even when no constraints are configured.
if ps.ArbosVersion >= ArbosSingleGasConstraintsVersion {
if ps.ArbosVersion >= params.ArbosVersion_SingleGasConstraintsVersion {
// Read gas constraints length for "GasModelToUse()"
result += storage.StorageReadCost
}

if ps.ArbosVersion >= params.ArbosVersion_MultiConstraintFix {
// updateSingleGasConstraintsBacklogs costs (ArbOS 51 and later)
// updateSingleGasConstraintsBacklogs costs (ArbosVersion_MultiConstraintFix and later)
constraintsLength, _ := ps.gasConstraints.Length()
if constraintsLength > 0 {
result += storage.StorageReadCost // read length to traverse
Expand Down
4 changes: 2 additions & 2 deletions arbos/l2pricing/model_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -130,7 +130,7 @@ func TestCompareSingleGasConstraintsPricingModelWithMultiGasConstraints(t *testi

func TestCalcMultiGasConstraintsExponents(t *testing.T) {
pricing := PricingForTest(t)
pricing.ArbosVersion = ArbosMultiGasConstraintsVersion
pricing.ArbosVersion = params.ArbosVersion_MultiGasConstraintsVersion

Require(t, pricing.AddMultiGasConstraint(
100000,
Expand Down Expand Up @@ -195,7 +195,7 @@ func TestMultiDimensionalPriceForRefund(t *testing.T) {
singlePrice := minPrice.Mul(minPrice, singleGas)
Require(t, err)

pricing.ArbosVersion = ArbosMultiGasConstraintsVersion
pricing.ArbosVersion = params.ArbosVersion_MultiGasConstraintsVersion

// Initial price check
price, err := pricing.MultiDimensionalPriceForRefund(multiGas)
Expand Down
3 changes: 1 addition & 2 deletions arbos/tx_processor.go
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,6 @@ import (

"github.com/offchainlabs/nitro/arbos/arbosState"
"github.com/offchainlabs/nitro/arbos/l1pricing"
"github.com/offchainlabs/nitro/arbos/l2pricing"
"github.com/offchainlabs/nitro/arbos/retryables"
"github.com/offchainlabs/nitro/arbos/util"
"github.com/offchainlabs/nitro/util/arbmath"
Expand Down Expand Up @@ -541,7 +540,7 @@ func (p *TxProcessor) EndTxHook(gasLeft uint64, usedMultiGas multigas.MultiGas,

var multiDimensionalCost *big.Int
var err error
if p.state.L2PricingState().ArbosVersion >= l2pricing.ArbosMultiGasConstraintsVersion {
if p.state.L2PricingState().ArbosVersion >= params.ArbosVersion_MultiGasConstraintsVersion {
multiDimensionalCost, err = p.state.L2PricingState().MultiDimensionalPriceForRefund(usedMultiGas)
p.state.Restrict(err)
}
Expand Down
5 changes: 3 additions & 2 deletions arbos/tx_processor_multigas_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ import (
"github.com/ethereum/go-ethereum/core/tracing"
"github.com/ethereum/go-ethereum/core/types"
"github.com/ethereum/go-ethereum/core/vm"
"github.com/ethereum/go-ethereum/params"

"github.com/offchainlabs/nitro/arbos/arbosState"
"github.com/offchainlabs/nitro/arbos/l2pricing"
Expand Down Expand Up @@ -115,7 +116,7 @@ func TestEndTxHookMultiGasRefundNormalTx(t *testing.T) {
)

// Set up multi-gas constraints and spin model to produce different multi-dimensional cost.
txProcessor.state.L2PricingState().ArbosVersion = l2pricing.ArbosMultiGasConstraintsVersion
txProcessor.state.L2PricingState().ArbosVersion = params.ArbosVersion_MultiGasConstraintsVersion

Require(t, txProcessor.state.L2PricingState().AddMultiGasConstraint(
100000,
Expand Down Expand Up @@ -188,7 +189,7 @@ func TestEndTxHookMultiGasRefundRetryableTx(t *testing.T) {

// Set up multi-gas constraints and spin model to produce a different multi-dimensional cost.
pricing := txProcessor.state.L2PricingState()
pricing.ArbosVersion = l2pricing.ArbosMultiGasConstraintsVersion
pricing.ArbosVersion = params.ArbosVersion_MultiGasConstraintsVersion

Require(t, pricing.AddMultiGasConstraint(
100000,
Expand Down
2 changes: 2 additions & 0 deletions changelog/mrogachev-nit-4152.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
### Changed
- Remove manual gas math from ArbRetryableTx.Redeem by using static L2 pricing backlog update cost
2 changes: 1 addition & 1 deletion go-ethereum
8 changes: 2 additions & 6 deletions precompiles/ArbOwner.go
Original file line number Diff line number Diff line change
Expand Up @@ -529,7 +529,8 @@ func (con ArbOwner) SetGasPricingConstraints(c ctx, evm mech, constraints [][3]u
return fmt.Errorf("failed to clear existing constraints: %w", err)
}

if c.State.ArbOSVersion() >= params.ArbosVersion_MultiConstraintFix {
arbosVersion := c.State.ArbOSVersion()
if arbosVersion >= params.ArbosVersion_MultiConstraintFix && arbosVersion < params.ArbosVersion_MultiGasConstraintsVersion {
limit := l2pricing.GasConstraintsMaxNum
if len(constraints) > limit {
return fmt.Errorf("too many constraints. Max: %d", limit)
Expand Down Expand Up @@ -559,11 +560,6 @@ func (con ArbOwner) SetMultiGasPricingConstraints(
evm mech,
constraints []MultiGasConstraint,
) error {
limit := l2pricing.MultiGasConstraintsMaxNum
if len(constraints) > limit {
return fmt.Errorf("too many constraints. Max: %d", limit)
}

if err := c.State.L2PricingState().ClearMultiGasConstraints(); err != nil {
return fmt.Errorf("failed to clear existing multi-gas constraints: %w", err)
}
Expand Down
20 changes: 15 additions & 5 deletions precompiles/ArbRetryableTx.go
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ import (
"github.com/ethereum/go-ethereum/core/types"
"github.com/ethereum/go-ethereum/params"

"github.com/offchainlabs/nitro/arbos/l2pricing"
"github.com/offchainlabs/nitro/arbos/retryables"
"github.com/offchainlabs/nitro/arbos/util"
"github.com/offchainlabs/nitro/util/arbmath"
Expand Down Expand Up @@ -99,11 +100,11 @@ func (con ArbRetryableTx) Redeem(c ctx, evm mech, ticketId bytes32) (bytes32, er
gasCostToReturnResult := params.CopyGas

// `redeem` must prepay the gas needed by the trailing call to
// L2PricingState().AddToGasPool(). GasPoolUpdateCost(ArbOSVersion) returns
// that amount based on the storage read/write mix used by AddToGasPool().
gasPoolUpdateCost := c.State.L2PricingState().BacklogUpdateCost()
// L2PricingState().ShrinkBacklog(). BacklogUpdateCost() returns
// that amount based on the storage read/write mix used by ShrinkBacklog().
backlogUpdateCost := c.State.L2PricingState().BacklogUpdateCost()

futureGasCosts := eventCost + gasCostToReturnResult + gasPoolUpdateCost
futureGasCosts := eventCost + gasCostToReturnResult + backlogUpdateCost
if c.GasLeft() < futureGasCosts {
return hash{}, c.Burn(multigas.ResourceKindComputation, futureGasCosts) // this will error
}
Expand Down Expand Up @@ -132,7 +133,16 @@ func (con ArbRetryableTx) Redeem(c ctx, evm mech, ticketId bytes32) (bytes32, er

// Add the gasToDonate back to the gas pool: the retryable attempt will then consume it.
// This ensures that the gas pool has enough gas to run the retryable attempt.
// TODO(NIT-4120): clarify the gas dimension for gasToDonate
// Starting from ArbosVersion_MultiGasConstraintsVersion, don't charge gas for the ShrinkBacklog call.
stopChargingGas := c.State.L2PricingState().ArbosVersion >= params.ArbosVersion_MultiGasConstraintsVersion
Copy link
Contributor

Choose a reason for hiding this comment

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

where do you charge the backlog update cost?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

This is my current understanding:

ArbOS version Active pricer(s) possible BacklogUpdateCost() behaviour
< 50 Legacy StorageReadCost + StorageWriteCost
SingleGasConstraintsVersion (50) Legacy / SingleGasConstraints StorageReadCost (for model detection) + legacy fallback cost
. Buggy if constraints > 0
MultiConstraintFix (51) Legacy / SingleGasConstraints If constraints exist:
StorageReadCost (model check)
StorageReadCost (iterate)
N × (StorageReadCost + StorageWriteCost)
Else legacy cost
>= MultiGasConstraintsVersion (60) Legacy / SingleGas / MultiGas Always MultiConstraintStaticBacklogUpdateCost, independent of model

So having the single gas constraints configured on ArbOS 50 should be the only broken state (fixed by MultiConstraintFix)

Copy link
Contributor Author

Choose a reason for hiding this comment

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

So backlogUpdateCost := c.State.L2PricingState().BacklogUpdateCost() should be always the correct value

Copy link
Contributor Author

Choose a reason for hiding this comment

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

With the latest changes:

  • MultiConstraintStaticBacklogUpdateCost is the actual charged price for the redeem backlog update.
  • Fix redeem test: the extra storage.StorageWriteCost - storage.StorageWriteZeroCost is now expected only for < ArbOS 60.

if stopChargingGas {
if err := c.Burn(multigas.ResourceKindComputation, l2pricing.MultiConstraintStaticBacklogUpdateCost); err != nil {
return hash{}, err
}

c.SetUnmeteredGasAccounting(true)
defer c.SetUnmeteredGasAccounting(false)
}
return retryTxHash, c.State.L2PricingState().ShrinkBacklog(gasToDonate, multigas.ComputationGas(gasToDonate))
}

Expand Down
92 changes: 63 additions & 29 deletions precompiles/ArbRetryableTx_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -11,9 +11,9 @@ import (
"github.com/ethereum/go-ethereum/common"
"github.com/ethereum/go-ethereum/core"
"github.com/ethereum/go-ethereum/core/vm"
"github.com/ethereum/go-ethereum/params"

"github.com/offchainlabs/nitro/arbos"
"github.com/offchainlabs/nitro/arbos/burn"
"github.com/offchainlabs/nitro/arbos/l2pricing"
"github.com/offchainlabs/nitro/arbos/storage"
"github.com/offchainlabs/nitro/solgen/go/precompilesgen"
Expand Down Expand Up @@ -41,7 +41,7 @@ func TestGetCurrentRedeemer(t *testing.T) {
}
}

func testRetryableRedeem(t *testing.T, evm *vm.EVM, precompileCtx *Context) {
func testRetryableRedeem(t *testing.T, evm *vm.EVM, precompileCtx *Context, expectedLegacyGas bool) {
t.Helper()

id := common.BigToHash(big.NewInt(978645611142))
Expand Down Expand Up @@ -82,7 +82,11 @@ func testRetryableRedeem(t *testing.T, evm *vm.EVM, precompileCtx *Context) {
)
Require(t, err)

expected := storage.StorageWriteCost - storage.StorageWriteZeroCost
expected := uint64(0)
if expectedLegacyGas {
expected = storage.StorageWriteCost - storage.StorageWriteZeroCost
}

if gasLeft != expected {
// We expect to have some gas left over, because in this test we write a zero, but in other
// use cases the precompile would cause a non-zero write. So the precompile allocates enough gas
Expand All @@ -102,7 +106,7 @@ func testRetryableRedeem(t *testing.T, evm *vm.EVM, precompileCtx *Context) {
}
}

func TestRetryableRedeem(t *testing.T) {
func TestRetryableRedeemLegacy(t *testing.T) {
evm := newMockEVMForTesting()
precompileCtx := testContext(common.Address{}, evm)

Expand All @@ -113,22 +117,34 @@ func TestRetryableRedeem(t *testing.T) {
Fail(t, "should use legacy model")
}

testRetryableRedeem(t, evm, precompileCtx)
testRetryableRedeem(t, evm, precompileCtx, true)
}

func TestRetryableRedeemLegacyArbOS60(t *testing.T) {
arbosVersion := params.ArbosVersion_MultiGasConstraintsVersion
evm := newMockEVMForTestingWithVersion(&arbosVersion)
precompileCtx := testContext(common.Address{}, evm)

model, err := precompileCtx.State.L2PricingState().GasModelToUse()
Require(t, err)

if model != l2pricing.GasModelLegacy {
Fail(t, "should use legacy model")
}

testRetryableRedeem(t, evm, precompileCtx, true)
}

func TestRetryableRedeemWithSingleGasConstraints(t *testing.T) {
func TestRetryableRedeemWithGasConstraints(t *testing.T) {
evm := newMockEVMForTesting()
precompileCtx := testContext(common.Address{}, evm)

for i := range l2pricing.GasConstraintsMaxNum {
// #nosec G115
target0 := uint64((i + 1) * 1000000)
// #nosec G115
window0 := uint64((i + 1) * 10)
// #nosec G115
backlog0 := uint64((i + 1) * 500000)
for i := range uint64(l2pricing.GasConstraintsMaxNum) {
target := (i + 1) * 1000000
window := (i + 1) * 10
backlog := (i + 1) * 500000

err := precompileCtx.State.L2PricingState().AddGasConstraint(target0, window0, backlog0)
err := precompileCtx.State.L2PricingState().AddGasConstraint(target, window, backlog)
Require(t, err)
}

Expand All @@ -139,23 +155,41 @@ func TestRetryableRedeemWithSingleGasConstraints(t *testing.T) {
Fail(t, "should use single-gas constraints model")
}

testRetryableRedeem(t, evm, precompileCtx)
testRetryableRedeem(t, evm, precompileCtx, true)
}

func TestRetryableRedeemWithMultiGasConstraints(t *testing.T) {
evm := newMockEVMForTesting()
precompileCtx := testContext(common.Address{}, evm)
precompileCtx.State.L2PricingState().ArbosVersion = l2pricing.ArbosMultiGasConstraintsVersion

// Override default ArbOS varsion in the database
versionSlot := uint64(0)
version := new(big.Int).SetUint64(l2pricing.ArbosMultiGasConstraintsVersion)
burner := burn.NewSystemBurner(nil, false)
sto := storage.NewGeth(evm.StateDB, burner)
err := sto.SetByUint64(versionSlot, common.BigToHash(version))
func TestRetryableRedeemWithGasConstraintsArbOSMultiGasConstraintsVersion(t *testing.T) {
arbosVersion := params.ArbosVersion_MultiGasConstraintsVersion
evm := newMockEVMForTestingWithVersion(&arbosVersion)

precompileCtx := testContextWithVersion(common.Address{}, evm, arbosVersion)

for i := range uint64(100) {
target := (i + 1) * 1000000
window := (i + 1) * 10
backlog := (i + 1) * 500000

err := precompileCtx.State.L2PricingState().AddGasConstraint(target, window, backlog)
Require(t, err)
}

model, err := precompileCtx.State.L2PricingState().GasModelToUse()
Require(t, err)

for i := range l2pricing.MultiGasConstraintsMaxNum {
if model != l2pricing.GasModelSingleGasConstraints {
Fail(t, "should use single-gas constraints model")
}

testRetryableRedeem(t, evm, precompileCtx, false)
}

func TestRetryableRedeemWithMultiGasConstraints(t *testing.T) {
arbosVersion := params.ArbosVersion_MultiGasConstraintsVersion
evm := newMockEVMForTestingWithVersion(&arbosVersion)

precompileCtx := testContextWithVersion(common.Address{}, evm, arbosVersion)

for i := range 100 {
// #nosec G115
target := uint64((i + 1) * 1000000)
// #nosec G115
Expand All @@ -172,7 +206,7 @@ func TestRetryableRedeemWithMultiGasConstraints(t *testing.T) {
uint8(multigas.ResourceKindWasmComputation): 6,
}

err = precompileCtx.State.L2PricingState().AddMultiGasConstraint(target, window, backlog, weights)
err := precompileCtx.State.L2PricingState().AddMultiGasConstraint(target, window, backlog, weights)
Require(t, err)
}

Expand All @@ -183,5 +217,5 @@ func TestRetryableRedeemWithMultiGasConstraints(t *testing.T) {
Fail(t, "should use multi-gas constraints model")
}

testRetryableRedeem(t, evm, precompileCtx)
testRetryableRedeem(t, evm, precompileCtx, false)
}
Loading
Loading