Skip to content

Commit c2a7f22

Browse files
Merge remote-tracking branch 'origin/master' into split-resource-kind-storage-access
2 parents e184562 + b980434 commit c2a7f22

15 files changed

Lines changed: 285 additions & 57 deletions

File tree

arbnode/batch_poster.go

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -2071,9 +2071,9 @@ func (b *BatchPoster) GetBacklogEstimate() uint64 {
20712071
}
20722072

20732073
func (b *BatchPoster) Start(ctxIn context.Context) {
2074-
b.dataPoster.Start(ctxIn)
2075-
b.redisLock.Start(ctxIn)
20762074
b.StopWaiter.Start(ctxIn, b)
2075+
b.dataPoster.Start(b.GetContext())
2076+
b.redisLock.Start(b.GetContext())
20772077
b.LaunchThread(b.pollForReverts)
20782078
b.LaunchThread(b.pollForL1PriceData)
20792079
commonEphemeralErrorHandler := util.NewEphemeralErrorHandler(time.Minute, "", 0)
@@ -2157,9 +2157,9 @@ func (b *BatchPoster) Start(ctxIn context.Context) {
21572157
}
21582158

21592159
func (b *BatchPoster) StopAndWait() {
2160-
b.StopWaiter.StopAndWait()
2161-
b.dataPoster.StopAndWait()
21622160
b.redisLock.StopAndWait()
2161+
b.dataPoster.StopAndWait()
2162+
b.StopWaiter.StopAndWait()
21632163
}
21642164

21652165
type BoolRing struct {

arbos/programs/programs.go

Lines changed: 81 additions & 29 deletions
Original file line numberDiff line numberDiff line change
@@ -116,7 +116,11 @@ func (p Programs) ActivateProgram(evm *vm.EVM, address common.Address, runCtx *c
116116
// already activated and up to date
117117
return 0, codeHash, common.Hash{}, nil, false, ProgramUpToDateError()
118118
}
119-
wasm, err := getWasm(statedb, address, params, burner)
119+
charger, err := newFragmentReadCharger(burner, evm.ChainConfig().MaxCodeSize())
120+
if err != nil {
121+
return 0, codeHash, common.Hash{}, nil, false, err
122+
}
123+
wasm, err := getWasm(statedb, address, params, charger)
120124
if err != nil {
121125
return 0, codeHash, common.Hash{}, nil, false, err
122126
}
@@ -310,14 +314,65 @@ func evmMemoryCost(size uint64) uint64 {
310314
return linearCost + squareCost
311315
}
312316

313-
func getWasm(statedb vm.StateDB, program common.Address, params *StylusParams, burner burn.Burner) ([]byte, error) {
317+
type fragmentReadCharger struct {
318+
burner burn.Burner
319+
maxCodeSize uint64
320+
}
321+
322+
func newFragmentReadCharger(burner burn.Burner, maxCodeSize uint64) (*fragmentReadCharger, error) {
323+
if burner == nil {
324+
if maxCodeSize != 0 {
325+
return nil, errors.New("maxCodeSize requires fragment read burner")
326+
}
327+
return nil, nil
328+
}
329+
if maxCodeSize == 0 {
330+
return nil, errors.New("fragment read burner requires maxCodeSize")
331+
}
332+
return &fragmentReadCharger{
333+
burner: burner,
334+
maxCodeSize: maxCodeSize,
335+
}, nil
336+
}
337+
338+
func (charger *fragmentReadCharger) canReadNewFragment(statedb vm.StateDB, addr common.Address) error {
339+
if charger == nil {
340+
return nil
341+
}
342+
cost, err := fragmentReadGasCost(statedb.AddressInAccessList(addr), charger.maxCodeSize)
343+
if err != nil {
344+
return err
345+
}
346+
if charger.burner.GasLeft() < cost.SingleGas() {
347+
return charger.burner.BurnOut()
348+
}
349+
return nil
350+
}
351+
352+
func (charger *fragmentReadCharger) chargeForReadingFragment(statedb vm.StateDB, addr common.Address, codeSize uint64) error {
353+
if charger == nil {
354+
return nil
355+
}
356+
warm := statedb.AddressInAccessList(addr)
357+
if !warm {
358+
statedb.AddAddressToAccessList(addr)
359+
}
360+
cost, err := fragmentReadGasCost(warm, codeSize)
361+
if err != nil {
362+
log.Trace("fragment copy gas overflow", "address", addr, "codeSize", codeSize, "err", err)
363+
return err
364+
}
365+
return charger.burner.BurnMultiGas(cost)
366+
}
367+
368+
func getWasm(statedb vm.StateDB, program common.Address, params *StylusParams, charger *fragmentReadCharger) ([]byte, error) {
314369
prefixedWasm := statedb.GetCode(program)
315-
return getWasmFromContractCode(statedb, prefixedWasm, params, burner)
370+
return getWasmFromContractCode(statedb, prefixedWasm, params, charger)
316371
}
317372

318-
// burner is used to charge gas for reading fragments. If it is present activation is assumed, and activation checks are enforced.
319-
// Only pass a burner if activating the program.
320-
func getWasmFromContractCode(statedb vm.StateDB, prefixedWasm []byte, params *StylusParams, burner burn.Burner) ([]byte, error) {
373+
// charger is used to charge gas for reading fragments. If it is present activation is assumed, and activation checks are enforced.
374+
// Only pass a charger if activating the program.
375+
func getWasmFromContractCode(statedb vm.StateDB, prefixedWasm []byte, params *StylusParams, charger *fragmentReadCharger) ([]byte, error) {
321376
if len(prefixedWasm) == 0 {
322377
return nil, ProgramNotWasmError()
323378
}
@@ -328,7 +383,7 @@ func getWasmFromContractCode(statedb vm.StateDB, prefixedWasm []byte, params *St
328383

329384
if params.arbosVersion >= gethParams.ArbosVersion_StylusContractLimit {
330385
if state.IsStylusRootProgramPrefix(prefixedWasm) {
331-
return getWasmFromRootStylus(statedb, prefixedWasm, params.MaxWasmSize, params.MaxFragmentCount, burner)
386+
return getWasmFromRootStylus(statedb, prefixedWasm, params.MaxWasmSize, params.MaxFragmentCount, charger)
332387
}
333388

334389
if state.IsStylusFragmentPrefix(prefixedWasm) {
@@ -353,15 +408,15 @@ func getWasmFromClassicStylus(data []byte, maxSize uint32) ([]byte, error) {
353408
return arbcompress.DecompressWithDictionary(wasm, int(maxSize), dict)
354409
}
355410

356-
// burner is used to charge gas for reading fragments. If it is present activation is assumed, and activation checks are enforced.
357-
// Only pass a burner if activating the program.
358-
func getWasmFromRootStylus(statedb vm.StateDB, data []byte, maxSize uint32, maxFragments uint8, burner burn.Burner) ([]byte, error) {
411+
// charger is used to charge gas for reading fragments. If it is present activation is assumed, and activation checks are enforced.
412+
// Only pass a charger if activating the program.
413+
func getWasmFromRootStylus(statedb vm.StateDB, data []byte, maxSize uint32, maxFragments uint8, charger *fragmentReadCharger) ([]byte, error) {
359414
root, err := state.NewStylusRoot(data)
360415
if err != nil {
361416
return nil, err
362417
}
363418

364-
if burner != nil {
419+
if charger != nil {
365420
if root.DecompressedLength > maxSize {
366421
return nil, fmt.Errorf("invalid wasm: decompressedLength %d is greater then MaxWasmSize %d", root.DecompressedLength, maxSize)
367422
}
@@ -376,11 +431,15 @@ func getWasmFromRootStylus(statedb vm.StateDB, data []byte, maxSize uint32, maxF
376431

377432
var compressedWasm []byte
378433
for _, addr := range root.Addresses {
434+
// Fail before touching state unless the caller can afford a max-sized fragment
435+
// read; once the fragment length is known, charge only the actual read cost.
436+
if err := charger.canReadNewFragment(statedb, addr); err != nil {
437+
return nil, err
438+
}
439+
379440
fragCode := statedb.GetCode(addr)
380-
if burner != nil {
381-
if err := chargeFragmentReadGas(burner, statedb, addr, uint64(len(fragCode))); err != nil {
382-
return nil, err
383-
}
441+
if err := charger.chargeForReadingFragment(statedb, addr, uint64(len(fragCode))); err != nil {
442+
return nil, err
384443
}
385444

386445
payload, err := state.StripStylusFragmentPrefix(fragCode)
@@ -408,31 +467,24 @@ func getWasmFromRootStylus(statedb vm.StateDB, data []byte, maxSize uint32, maxF
408467
return wasm, nil
409468
}
410469

411-
// chargeFragmentReadGas charges EXTCODECOPY-style gas for reading fragment code.
412-
func chargeFragmentReadGas(burner burn.Burner, statedb vm.StateDB, addr common.Address, codeSize uint64) error {
413-
// charge access gas
470+
func fragmentReadGasCost(warm bool, codeSize uint64) (multigas.MultiGas, error) {
414471
var cost multigas.MultiGas
415-
if statedb.AddressInAccessList(addr) {
472+
if warm {
416473
cost = multigas.ComputationGas(gethParams.WarmStorageReadCostEIP2929)
417474
} else {
418-
statedb.AddAddressToAccessList(addr)
419475
cost = multigas.StorageAccessReadGas(gethParams.ColdAccountAccessCostEIP2929)
420476
}
421-
// charge copy gas
422477
words := ToWordSize(codeSize)
423478
copyGas, overflow := gethMath.SafeMul(words, gethParams.CopyGas)
424479
if overflow {
425-
log.Trace("fragment copy gas overflow", "address", addr, "codeSize", codeSize, "words", words, "copyGas", gethParams.CopyGas)
426-
return vm.ErrGasUintOverflow
480+
log.Trace("fragment copy gas overflow", "codeSize", codeSize, "words", words, "copyGas", gethParams.CopyGas)
481+
return multigas.ZeroGas(), vm.ErrGasUintOverflow
427482
}
428483
if cost, overflow = cost.SafeIncrement(multigas.ResourceKindStorageAccessRead, copyGas); overflow {
429-
log.Trace("fragment copy gas overflow", "address", addr, "codeSize", codeSize, "copyGas", copyGas)
430-
return vm.ErrGasUintOverflow
431-
}
432-
if err := burner.BurnMultiGas(cost); err != nil {
433-
return err
484+
log.Trace("fragment copy gas overflow", "codeSize", codeSize, "copyGas", copyGas)
485+
return multigas.ZeroGas(), vm.ErrGasUintOverflow
434486
}
435-
return nil
487+
return cost, nil
436488
}
437489

438490
func getStylusCompressionDict(id byte) (arbcompress.Dictionary, error) {
Lines changed: 164 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,164 @@
1+
// Copyright 2026, Offchain Labs, Inc.
2+
// For license information, see https://github.com/offchainlabs/nitro/blob/master/LICENSE.md
3+
4+
package programs
5+
6+
import (
7+
"encoding/binary"
8+
"testing"
9+
10+
"github.com/stretchr/testify/require"
11+
12+
"github.com/ethereum/go-ethereum/arbitrum/multigas"
13+
"github.com/ethereum/go-ethereum/common"
14+
"github.com/ethereum/go-ethereum/core/state"
15+
"github.com/ethereum/go-ethereum/core/tracing"
16+
"github.com/ethereum/go-ethereum/core/types"
17+
"github.com/ethereum/go-ethereum/core/vm"
18+
gethParams "github.com/ethereum/go-ethereum/params"
19+
20+
"github.com/offchainlabs/nitro/arbcompress"
21+
"github.com/offchainlabs/nitro/arbos/util"
22+
)
23+
24+
type testBurner struct {
25+
gasSupplied uint64
26+
gasUsed multigas.MultiGas
27+
}
28+
29+
func newTestBurner(gasSupplied uint64) *testBurner {
30+
return &testBurner{gasSupplied: gasSupplied}
31+
}
32+
33+
func (b *testBurner) Burn(kind multigas.ResourceKind, amount uint64) error {
34+
if b.GasLeft() < amount {
35+
return b.BurnOut()
36+
}
37+
b.gasUsed.SaturatingIncrementInto(kind, amount)
38+
return nil
39+
}
40+
41+
func (b *testBurner) BurnMultiGas(amount multigas.MultiGas) error {
42+
if b.GasLeft() < amount.SingleGas() {
43+
return b.BurnOut()
44+
}
45+
b.gasUsed.SaturatingAddInto(amount)
46+
return nil
47+
}
48+
49+
func (b *testBurner) Burned() uint64 {
50+
return b.gasUsed.SingleGas()
51+
}
52+
53+
func (b *testBurner) GasLeft() uint64 {
54+
return b.gasSupplied - b.gasUsed.SingleGas()
55+
}
56+
57+
func (b *testBurner) BurnOut() error {
58+
b.gasUsed.SaturatingIncrementInto(multigas.ResourceKindComputation, b.GasLeft())
59+
return vm.ErrOutOfGas
60+
}
61+
62+
func (*testBurner) Restrict(error) {}
63+
64+
func (*testBurner) HandleError(err error) error {
65+
return err
66+
}
67+
68+
func (*testBurner) ReadOnly() bool {
69+
return false
70+
}
71+
72+
func (*testBurner) TracingInfo() *util.TracingInfo {
73+
return nil
74+
}
75+
76+
func makeFragmentedRootForTest(t *testing.T) (*state.StateDB, []byte, []byte, common.Address, []byte) {
77+
t.Helper()
78+
79+
statedb, err := state.New(types.EmptyRootHash, state.NewDatabaseForTesting())
80+
require.NoError(t, err)
81+
82+
wasm := []byte("fragment gas reserve regression test")
83+
compressedWasm, err := arbcompress.Compress(wasm, arbcompress.LEVEL_WELL, arbcompress.EmptyDictionary)
84+
require.NoError(t, err)
85+
86+
fragmentCode := append(state.NewStylusFragmentPrefix(), compressedWasm...)
87+
fragmentAddr := common.Address{1}
88+
statedb.SetCode(fragmentAddr, fragmentCode, tracing.CodeChangeUnspecified)
89+
90+
rootCode := make([]byte, 0, 8+common.AddressLength)
91+
rootCode = append(rootCode, state.NewStylusRootPrefix(byte(arbcompress.EmptyDictionary))...)
92+
var decompressedLen [4]byte
93+
// #nosec G115
94+
binary.BigEndian.PutUint32(decompressedLen[:], uint32(len(wasm)))
95+
rootCode = append(rootCode, decompressedLen[:]...)
96+
rootCode = append(rootCode, fragmentAddr.Bytes()...)
97+
98+
return statedb, rootCode, wasm, fragmentAddr, fragmentCode
99+
}
100+
101+
func TestGetWasmFromRootStylusRequiresMaxFragmentReadReserve(t *testing.T) {
102+
statedb, rootCode, _, fragmentAddr, fragmentCode := makeFragmentedRootForTest(t)
103+
104+
actualCost, err := fragmentReadGasCost(false, uint64(len(fragmentCode)))
105+
require.NoError(t, err)
106+
maxCost, err := fragmentReadGasCost(false, gethParams.DefaultMaxCodeSize)
107+
require.NoError(t, err)
108+
require.Greater(t, maxCost.SingleGas(), actualCost.SingleGas())
109+
110+
burner := newTestBurner(actualCost.SingleGas())
111+
charger, err := newFragmentReadCharger(burner, gethParams.DefaultMaxCodeSize)
112+
require.NoError(t, err)
113+
114+
wasm, err := getWasmFromRootStylus(statedb, rootCode, 1<<20, 1, charger)
115+
require.Nil(t, wasm)
116+
require.ErrorIs(t, err, vm.ErrOutOfGas)
117+
require.Equal(t, actualCost.SingleGas(), burner.Burned())
118+
require.False(t, statedb.AddressInAccessList(fragmentAddr))
119+
}
120+
121+
func TestGetWasmFromRootStylusBurnsActualFragmentReadCostAfterPreflight(t *testing.T) {
122+
statedb, rootCode, expectedWasm, fragmentAddr, fragmentCode := makeFragmentedRootForTest(t)
123+
124+
actualCost, err := fragmentReadGasCost(false, uint64(len(fragmentCode)))
125+
require.NoError(t, err)
126+
maxCost, err := fragmentReadGasCost(false, gethParams.DefaultMaxCodeSize)
127+
require.NoError(t, err)
128+
require.Greater(t, maxCost.SingleGas(), actualCost.SingleGas())
129+
130+
burner := newTestBurner(maxCost.SingleGas())
131+
charger, err := newFragmentReadCharger(burner, gethParams.DefaultMaxCodeSize)
132+
require.NoError(t, err)
133+
134+
// #nosec G115
135+
wasm, err := getWasmFromRootStylus(statedb, rootCode, uint32(len(expectedWasm)), 1, charger)
136+
require.NoError(t, err)
137+
require.Equal(t, expectedWasm, wasm)
138+
require.Equal(t, actualCost.SingleGas(), burner.Burned())
139+
require.Equal(t, maxCost.SingleGas()-actualCost.SingleGas(), burner.GasLeft())
140+
require.True(t, statedb.AddressInAccessList(fragmentAddr))
141+
}
142+
143+
func TestGetWasmFromRootStylusUsesWarmFragmentReadCostWhenAddressIsWarm(t *testing.T) {
144+
statedb, rootCode, expectedWasm, fragmentAddr, fragmentCode := makeFragmentedRootForTest(t)
145+
statedb.AddAddressToAccessList(fragmentAddr)
146+
147+
actualCost, err := fragmentReadGasCost(true, uint64(len(fragmentCode)))
148+
require.NoError(t, err)
149+
maxCost, err := fragmentReadGasCost(true, gethParams.DefaultMaxCodeSize)
150+
require.NoError(t, err)
151+
require.Greater(t, maxCost.SingleGas(), actualCost.SingleGas())
152+
153+
burner := newTestBurner(maxCost.SingleGas())
154+
charger, err := newFragmentReadCharger(burner, gethParams.DefaultMaxCodeSize)
155+
require.NoError(t, err)
156+
157+
// #nosec G115
158+
wasm, err := getWasmFromRootStylus(statedb, rootCode, uint32(len(expectedWasm)), 1, charger)
159+
require.NoError(t, err)
160+
require.Equal(t, expectedWasm, wasm)
161+
require.Equal(t, actualCost.SingleGas(), burner.Burned())
162+
require.Equal(t, maxCost.SingleGas()-actualCost.SingleGas(), burner.GasLeft())
163+
require.True(t, statedb.AddressInAccessList(fragmentAddr))
164+
}

changelog/kolbyml-nit-4634.md

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
### Changed
2+
- Preflight the worst-case fragment read gas during multi-fragment Stylus activation, then charge the actual EXTCODECOPY-style cost after the fragment code is read.

changelog/pmikolajczyk-nit-4666.md

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
### Fixed
2+
- Fix StopWaiter lifecycle ordering: stop children before parent in StopAndWait, and pass managed context to children in Start

cmd/el-proxy/main.go

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -137,12 +137,12 @@ func NewExpressLaneProxy(
137137

138138
func (p *ExpressLaneProxy) Start(ctx context.Context) {
139139
p.StopWaiter.Start(ctx, p)
140-
p.expressLaneTracker.Start(ctx)
140+
p.expressLaneTracker.Start(p.GetContext())
141141
}
142142

143143
func (p *ExpressLaneProxy) StopAndWait() {
144-
p.StopWaiter.StopAndWait()
145144
p.expressLaneTracker.StopAndWait()
145+
p.StopWaiter.StopAndWait()
146146
}
147147

148148
var ErrorInternalConnectionError = errors.New("internal connection error")

execution/gethexec/addressfilter/service.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -84,7 +84,7 @@ func (s *FilterService) Start(ctx context.Context) {
8484
return s.config.PollInterval
8585
})
8686

87-
s.addressChecker.Start(ctx)
87+
s.addressChecker.Start(s.GetContext())
8888

8989
log.Info("address-filter service started",
9090
"poll_interval", s.config.PollInterval,

0 commit comments

Comments
 (0)