Skip to content

Commit 8424424

Browse files
Impleent new pricing model
1 parent d0f44ac commit 8424424

File tree

2 files changed

+182
-0
lines changed

2 files changed

+182
-0
lines changed

arbos/constraints/model.go

Lines changed: 55 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,55 @@
1+
// Copyright 2025, Offchain Labs, Inc.
2+
// For license information, see https://github.com/OffchainLabs/nitro/blob/master/LICENSE.md
3+
4+
// The constraints package tracks the multi-dimensional gas usage to apply constraint-based pricing.
5+
package constraints
6+
7+
import (
8+
"math"
9+
"math/big"
10+
11+
"github.com/offchainlabs/nitro/util/arbmath"
12+
)
13+
14+
const (
15+
PricingInertiaFactor = 30
16+
)
17+
18+
type PricingState struct {
19+
constraints ResourceConstraints
20+
}
21+
22+
// UpdatePricingModel adjusts the basefee according to simplified constraint-based pricing.
23+
// Formula: basefee = F_min * exp( max_i ( B_i / (30 * T_i * sqrt(Δ_i)) ) )
24+
func (ps *PricingState) UpdatePricingModel(minBaseFee *big.Int, timePassed uint64) *big.Int {
25+
var maxExponentBips arbmath.Bips
26+
27+
for c := range ps.constraints.All() {
28+
// Decay per-constraint backlog by T_i * timePassed
29+
c.RemoveFromBacklog(timePassed)
30+
31+
if c.Backlog == 0 || c.TargetPerSec == 0 || c.Period == 0 {
32+
continue
33+
}
34+
35+
// Compute inertia = 30 * sqrt(Δ_i)
36+
inertia := PricingInertiaFactor * uint64(math.Floor(math.Sqrt(float64(c.Period))))
37+
38+
// Compute denominator = inertia * T_i
39+
denominator := arbmath.SaturatingUMul(inertia, c.TargetPerSec)
40+
41+
// Normalized backlog = B_i / (inertia * T_i)
42+
expBips := arbmath.NaturalToBips(arbmath.SaturatingCast[int64](c.Backlog)) / arbmath.SaturatingCast[arbmath.Bips](denominator)
43+
44+
// Pick the maximum exponent across all constraints
45+
if expBips > maxExponentBips {
46+
maxExponentBips = expBips
47+
}
48+
}
49+
50+
// Apply the maximum exponent
51+
if maxExponentBips == 0 {
52+
return new(big.Int).Set(minBaseFee)
53+
}
54+
return arbmath.BigMulByBips(minBaseFee, arbmath.ApproxExpBasisPoints(maxExponentBips, 4))
55+
}

arbos/constraints/model_test.go

Lines changed: 127 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,127 @@
1+
// Copyright 2025, Offchain Labs, Inc.
2+
// For license information, see https://github.com/OffchainLabs/nitro/blob/master/LICENSE.md
3+
4+
package constraints
5+
6+
import (
7+
"math/big"
8+
"testing"
9+
10+
"github.com/stretchr/testify/require"
11+
12+
"github.com/ethereum/go-ethereum/arbitrum/multigas"
13+
14+
"github.com/offchainlabs/nitro/arbos/burn"
15+
"github.com/offchainlabs/nitro/arbos/l2pricing"
16+
"github.com/offchainlabs/nitro/arbos/storage"
17+
)
18+
19+
func TestConstraintsModelTwoResources(t *testing.T) {
20+
// Params: target = 5M/sec, Δ = 10s
21+
var target uint64 = 5_000_000
22+
const periodSecs = PeriodSecs(10)
23+
const iterations = 30
24+
25+
// Setup new constraint-based pricing model with 2 resources
26+
constraints := NewResourceConstraints()
27+
resources := EmptyResourceSet().
28+
WithResources(
29+
multigas.ResourceKindComputation,
30+
multigas.ResourceKindStorageAccess,
31+
)
32+
constraints.Set(resources, periodSecs, target)
33+
34+
model := PricingState{constraints: *constraints}
35+
36+
// Base fee floor
37+
baseFee := big.NewInt(100_000_000)
38+
39+
// Phase 1: exceed target to force fee increase
40+
for i := 0; i < iterations; i++ {
41+
mg := multigas.MultiGasFromPairs(
42+
multigas.Pair{Kind: multigas.ResourceKindComputation, Amount: 5},
43+
multigas.Pair{Kind: multigas.ResourceKindStorageAccess, Amount: 2},
44+
)
45+
model.constraints.Get(resources, periodSecs).AddToBacklog(mg)
46+
47+
newFee := model.UpdatePricingModel(baseFee, 1)
48+
49+
// Fee should never fall below baseFee during surge
50+
require.GreaterOrEqualf(t, newFee.Cmp(baseFee), 0,
51+
"fee dropped below base fee at iter %d", i)
52+
}
53+
54+
// Phase 2: no usage, backlog drains and fee should decay back
55+
for i := 0; i < iterations*2; i++ {
56+
newFee := model.UpdatePricingModel(baseFee, 1)
57+
58+
// Fee must eventually reach the floor
59+
if i == iterations*2-1 {
60+
require.Equal(t, 0, newFee.Cmp(baseFee),
61+
"fee should decay back to base fee")
62+
}
63+
}
64+
}
65+
66+
func TestConstraintsModelVersusLegacy(t *testing.T) {
67+
// Test parameters
68+
var gasUsedPerSecond int64 = 8_000_000 // >7M target to accumulate backlog
69+
var iterations int = 50
70+
var periodSecs = PeriodSecs(12)
71+
72+
// Initialize L2PricingState with legacy pricing model
73+
burner := burn.NewSystemBurner(nil, false)
74+
storage := storage.NewMemoryBacked(burner)
75+
require.NoError(t, l2pricing.InitializeL2PricingState(storage))
76+
l2PricingState := l2pricing.OpenL2PricingState(storage)
77+
78+
// Match new model
79+
_ = l2PricingState.SetBacklogTolerance(0) // no tolerance
80+
require.NoError(t, l2PricingState.SetSpeedLimitPerSecond(l2pricing.InitialSpeedLimitPerSecondV6))
81+
82+
// Setup constraint-based pricing model with a single gas constraint
83+
constraints := NewResourceConstraints()
84+
resources := EmptyResourceSet().
85+
WithResources(
86+
multigas.ResourceKindComputation,
87+
multigas.ResourceKindStorageAccess,
88+
multigas.ResourceKindStorageGrowth,
89+
multigas.ResourceKindHistoryGrowth,
90+
multigas.ResourceKindWasmComputation,
91+
)
92+
constraints.Set(resources, periodSecs, l2pricing.InitialSpeedLimitPerSecondV6)
93+
model := PricingState{
94+
constraints: *constraints,
95+
}
96+
97+
minBaseFee, _ := l2PricingState.MinBaseFeeWei()
98+
99+
for i := 1; i < iterations+1; i++ {
100+
// L2PricingState model update
101+
baseFeeLegacy, _ := l2PricingState.BaseFeeWei()
102+
burner.Restrict(l2PricingState.AddToGasPool(-gasUsedPerSecond)) // negative = gas consumed
103+
l2PricingState.UpdatePricingModel(baseFeeLegacy, 1, false)
104+
legacyFee, _ := l2PricingState.BaseFeeWei()
105+
106+
// Constraint-based model update
107+
// #nosec G115 -- gasUsedPerSecond is a fixed positive constant for testing
108+
mg := multigas.ComputationGas(uint64(gasUsedPerSecond))
109+
model.constraints.Get(resources, periodSecs).AddToBacklog(mg)
110+
newFee := model.UpdatePricingModel(minBaseFee, 1)
111+
112+
// Expect new fee to be slightly lower (slower growth) than legacy,
113+
// within about 7% after 50 iterations due to inertia differences.
114+
diff := new(big.Float).Quo(
115+
new(big.Float).SetInt(legacyFee),
116+
new(big.Float).SetInt(newFee),
117+
)
118+
val, _ := diff.Float64()
119+
120+
require.InEpsilonf(t, 1.0, val, 0.015, // within 1.5% tolerance
121+
"fees differ too much at iteration %d: legacy=%s new=%s",
122+
i, legacyFee.String(), newFee.String())
123+
124+
// Uncomment for debug output
125+
// fmt.Printf("%-4d %-15s %-15s %-10.4f\n", i, legacyFee.String(), newFee.String(), val)
126+
}
127+
}

0 commit comments

Comments
 (0)