Skip to content

Commit 890f0e2

Browse files
KolbyMLgligneultsahee
authored
Multi-fragment activation: charge gas per fragment (#4312)
* resolve small PR review comments for stylus merge on activate * Create kolbyml-nit-4406.md * bump go-ethereum pin * fix error from git merge * update go ethereum pin * Multi-fragment activation: charge gas per fragment * Add changelog * Attempt to resolve PR concerns * Resolve concerns * pushed file that wasn't saved * initialize cost properly * fix lint * remove evmMemory gas * update test * fix lint * try to make test more robust in ci * Update programs.go * Add BurnMultiGas to Burner interface Close NIT-4455 * feedback from Tsahi --------- Co-authored-by: Gabriel de Quadros Ligneul <gligneul@offchainlabs.com> Co-authored-by: Tsahi Zidenberg <65945052+tsahee@users.noreply.github.com>
1 parent 1a9d3ac commit 890f0e2

File tree

10 files changed

+183
-11
lines changed

10 files changed

+183
-11
lines changed

arbos/burn/burn.go

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@ import (
1414

1515
type Burner interface {
1616
Burn(kind multigas.ResourceKind, amount uint64) error
17+
BurnMultiGas(amount multigas.MultiGas) error
1718
Burned() uint64
1819
GasLeft() uint64 // `SystemBurner`s panic (no notion of GasLeft)
1920
BurnOut() error
@@ -41,6 +42,11 @@ func (burner *SystemBurner) Burn(kind multigas.ResourceKind, amount uint64) erro
4142
return nil
4243
}
4344

45+
func (burner *SystemBurner) BurnMultiGas(amount multigas.MultiGas) error {
46+
burner.gasBurnt.SaturatingAddInto(amount)
47+
return nil
48+
}
49+
4450
func (burner *SystemBurner) Burned() uint64 {
4551
return burner.gasBurnt.SingleGas()
4652
}

arbos/programs/native.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -277,7 +277,7 @@ func getCompiledProgram(statedb vm.StateDB, moduleHash common.Hash, addressForLo
277277
}
278278

279279
// addressForLogging may be empty or may not correspond to the code, so we need to be careful to use the code passed in separately
280-
wasm, err := getWasmFromContractCode(statedb, code, params, false)
280+
wasm, err := getWasmFromContractCode(statedb, code, params, nil)
281281
if err != nil {
282282
log.Error("Failed to reactivate program: getWasm", "address", addressForLogging, "expected moduleHash", moduleHash, "err", err)
283283
return nil, fmt.Errorf("failed to reactivate program address: %v err: %w", addressForLogging, err)

arbos/programs/programs.go

Lines changed: 56 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -6,10 +6,12 @@ package programs
66
import (
77
"errors"
88
"fmt"
9+
"math"
910
"math/big"
1011

1112
"github.com/ethereum/go-ethereum/arbitrum/multigas"
1213
"github.com/ethereum/go-ethereum/common"
14+
gethMath "github.com/ethereum/go-ethereum/common/math"
1315
"github.com/ethereum/go-ethereum/core"
1416
"github.com/ethereum/go-ethereum/core/state"
1517
"github.com/ethereum/go-ethereum/core/vm"
@@ -19,6 +21,7 @@ import (
1921

2022
"github.com/offchainlabs/nitro/arbcompress"
2123
"github.com/offchainlabs/nitro/arbos/addressSet"
24+
"github.com/offchainlabs/nitro/arbos/burn"
2225
"github.com/offchainlabs/nitro/arbos/storage"
2326
"github.com/offchainlabs/nitro/arbos/util"
2427
"github.com/offchainlabs/nitro/arbutil"
@@ -113,7 +116,7 @@ func (p Programs) ActivateProgram(evm *vm.EVM, address common.Address, runCtx *c
113116
// already activated and up to date
114117
return 0, codeHash, common.Hash{}, nil, false, ProgramUpToDateError()
115118
}
116-
wasm, err := getWasm(statedb, address, params)
119+
wasm, err := getWasm(statedb, address, params, burner)
117120
if err != nil {
118121
return 0, codeHash, common.Hash{}, nil, false, err
119122
}
@@ -290,20 +293,31 @@ func attributeWasmComputation(contract *vm.Contract, startingGas uint64) {
290293
}
291294
}
292295

296+
// toWordSize returns the ceiled word size required for memory expansion.
297+
func ToWordSize(size uint64) uint64 {
298+
if size > math.MaxUint64-31 {
299+
return math.MaxUint64/32 + 1
300+
}
301+
302+
return (size + 31) / 32
303+
}
304+
293305
func evmMemoryCost(size uint64) uint64 {
294306
// It would take 100GB to overflow this calculation, so no need to worry about that
295-
words := (size + 31) / 32
307+
words := ToWordSize(size)
296308
linearCost := words * gethParams.MemoryGas
297309
squareCost := (words * words) / gethParams.QuadCoeffDiv
298310
return linearCost + squareCost
299311
}
300312

301-
func getWasm(statedb vm.StateDB, program common.Address, params *StylusParams) ([]byte, error) {
313+
func getWasm(statedb vm.StateDB, program common.Address, params *StylusParams, burner burn.Burner) ([]byte, error) {
302314
prefixedWasm := statedb.GetCode(program)
303-
return getWasmFromContractCode(statedb, prefixedWasm, params, true)
315+
return getWasmFromContractCode(statedb, prefixedWasm, params, burner)
304316
}
305317

306-
func getWasmFromContractCode(statedb vm.StateDB, prefixedWasm []byte, params *StylusParams, isActivation bool) ([]byte, error) {
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) {
307321
if len(prefixedWasm) == 0 {
308322
return nil, ProgramNotWasmError()
309323
}
@@ -314,7 +328,7 @@ func getWasmFromContractCode(statedb vm.StateDB, prefixedWasm []byte, params *St
314328

315329
if params.arbosVersion >= gethParams.ArbosVersion_StylusContractLimit {
316330
if state.IsStylusRootProgramPrefix(prefixedWasm) {
317-
return getWasmFromRootStylus(statedb, prefixedWasm, params.MaxWasmSize, params.MaxFragmentCount, isActivation)
331+
return getWasmFromRootStylus(statedb, prefixedWasm, params.MaxWasmSize, params.MaxFragmentCount, burner)
318332
}
319333

320334
if state.IsStylusFragmentPrefix(prefixedWasm) {
@@ -339,13 +353,15 @@ func getWasmFromClassicStylus(data []byte, maxSize uint32) ([]byte, error) {
339353
return arbcompress.DecompressWithDictionary(wasm, int(maxSize), dict)
340354
}
341355

342-
func getWasmFromRootStylus(statedb vm.StateDB, data []byte, maxSize uint32, maxFragments uint8, isActivation bool) ([]byte, error) {
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) {
343359
root, err := state.NewStylusRoot(data)
344360
if err != nil {
345361
return nil, err
346362
}
347363

348-
if isActivation {
364+
if burner != nil {
349365
if root.DecompressedLength > maxSize {
350366
return nil, fmt.Errorf("invalid wasm: decompressedLength %d is greater then MaxWasmSize %d", root.DecompressedLength, maxSize)
351367
}
@@ -361,6 +377,11 @@ func getWasmFromRootStylus(statedb vm.StateDB, data []byte, maxSize uint32, maxF
361377
var compressedWasm []byte
362378
for _, addr := range root.Addresses {
363379
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+
}
384+
}
364385

365386
payload, err := state.StripStylusFragmentPrefix(fragCode)
366387
if err != nil {
@@ -387,6 +408,33 @@ func getWasmFromRootStylus(statedb vm.StateDB, data []byte, maxSize uint32, maxF
387408
return wasm, nil
388409
}
389410

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
414+
var cost multigas.MultiGas
415+
if statedb.AddressInAccessList(addr) {
416+
cost = multigas.ComputationGas(gethParams.WarmStorageReadCostEIP2929)
417+
} else {
418+
statedb.AddAddressToAccessList(addr)
419+
cost = multigas.StorageAccessGas(gethParams.ColdAccountAccessCostEIP2929)
420+
}
421+
// charge copy gas
422+
words := ToWordSize(codeSize)
423+
copyGas, overflow := gethMath.SafeMul(words, gethParams.CopyGas)
424+
if overflow {
425+
log.Trace("fragment copy gas overflow", "address", addr, "codeSize", codeSize, "words", words, "copyGas", gethParams.CopyGas)
426+
return vm.ErrGasUintOverflow
427+
}
428+
if cost, overflow = cost.SafeIncrement(multigas.ResourceKindStorageAccess, 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
434+
}
435+
return nil
436+
}
437+
390438
func getStylusCompressionDict(id byte) (arbcompress.Dictionary, error) {
391439
switch id {
392440
case 0:

arbos/programs/wasm.go

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -161,7 +161,9 @@ func handleProgramPrepare(statedb vm.StateDB, moduleHash common.Hash, addressFor
161161
debugInt = 1
162162
}
163163

164-
wasm, err := getWasmFromContractCode(statedb, code, params, true)
164+
// Not an activation path here, so fragment read gas shouldn't be charged.
165+
// Passing nil avoids charging gas through a storage-backed burner here.
166+
wasm, err := getWasmFromContractCode(statedb, code, params, nil)
165167
if err != nil {
166168
panic(fmt.Sprintf("failed to get wasm for program, program address: %v, err: %v", addressForLogging.Hex(), err))
167169
}

arbos/programs/wasmstorehelper.go

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -51,7 +51,9 @@ func (p Programs) SaveActiveProgramToWasmStore(statedb *state.StateDB, codeHash
5151
return nil
5252
}
5353

54-
wasm, err := getWasmFromContractCode(statedb, code, progParams, false)
54+
// Not an activation path, so fragment read gas shouldn't be charged.
55+
// Passing nil avoids charging gas through a storage-backed burner here.
56+
wasm, err := getWasmFromContractCode(statedb, code, progParams, nil)
5557
if err != nil {
5658
log.Error("Failed to reactivate program while rebuilding wasm store: getWasmFromContractCode", "expected moduleHash", moduleHash, "err", err)
5759
return fmt.Errorf("failed to reactivate program while rebuilding wasm store: %w", err)

changelog/gligneul-nit-4455.md

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
### Internal
2+
- Add BurnMultiGas to Burner interface

changelog/kolbyml-nit-4405.md

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
### Changed
2+
- Charge EXTCODECOPY-style gas per fragment during multi-fragment Stylus activation.

precompiles/context.go

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -49,6 +49,17 @@ func (c *Context) Burn(kind multigas.ResourceKind, amount uint64) error {
4949
return nil
5050
}
5151

52+
func (c *Context) BurnMultiGas(amount multigas.MultiGas) error {
53+
if c.free {
54+
return nil
55+
}
56+
if c.GasLeft() < amount.SingleGas() {
57+
return c.BurnOut()
58+
}
59+
c.gasUsed.SaturatingAddInto(amount)
60+
return nil
61+
}
62+
5263
//nolint:unused
5364
func (c *Context) Burned() uint64 {
5465
return c.gasUsed.SingleGas()

precompiles/context_test.go

Lines changed: 47 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -68,3 +68,50 @@ func TestContextBurn(t *testing.T) {
6868
t.Errorf("wrong computation: got %v, want %v", got, want)
6969
}
7070
}
71+
72+
func TestContextBurnMultiGas(t *testing.T) {
73+
ctx := Context{
74+
gasSupplied: 1_000,
75+
gasUsed: multigas.ZeroGas(),
76+
}
77+
if got, want := ctx.GasLeft(), uint64(1000); got != want {
78+
t.Errorf("wrong gas left: got %v, want %v", got, want)
79+
}
80+
if got, want := ctx.Burned(), uint64(0); got != want {
81+
t.Errorf("wrong gas burned: got %v, want %v", got, want)
82+
}
83+
84+
gasToBurn := multigas.MultiGasFromPairs(
85+
multigas.Pair{Kind: multigas.ResourceKindStorageAccess, Amount: 400},
86+
multigas.Pair{Kind: multigas.ResourceKindStorageGrowth, Amount: 200},
87+
)
88+
if err := ctx.BurnMultiGas(gasToBurn); err != nil {
89+
t.Errorf("unexpected error from burn: %v", err)
90+
}
91+
if got, want := ctx.GasLeft(), uint64(400); got != want {
92+
t.Errorf("wrong gas left: got %v, want %v", got, want)
93+
}
94+
if got, want := ctx.Burned(), uint64(600); got != want {
95+
t.Errorf("wrong gas burned: got %v, want %v", got, want)
96+
}
97+
98+
if err := ctx.BurnMultiGas(gasToBurn); !errors.Is(err, vm.ErrOutOfGas) {
99+
t.Errorf("wrong erro from burn: got %v, want %v", err, vm.ErrOutOfGas)
100+
}
101+
if got, want := ctx.GasLeft(), uint64(0); got != want {
102+
t.Errorf("wrong gas left: got %v, want %v", got, want)
103+
}
104+
if got, want := ctx.Burned(), uint64(1000); got != want {
105+
t.Errorf("wrong gas burned: got %v, want %v", got, want)
106+
}
107+
108+
if got, want := ctx.gasUsed.Get(multigas.ResourceKindStorageAccess), uint64(400); got != want {
109+
t.Errorf("wrong storage access: got %v, want %v", got, want)
110+
}
111+
if got, want := ctx.gasUsed.Get(multigas.ResourceKindStorageGrowth), uint64(200); got != want {
112+
t.Errorf("wrong storage growth: got %v, want %v", got, want)
113+
}
114+
if got, want := ctx.gasUsed.Get(multigas.ResourceKindComputation), uint64(400); got != want {
115+
t.Errorf("wrong computation: got %v, want %v", got, want)
116+
}
117+
}

system_tests/stylus_contract_limit_increase_test.go

Lines changed: 52 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -212,6 +212,39 @@ func TestFragmentedContractValidation(t *testing.T) {
212212
}
213213
}
214214

215+
func TestFragmentActivationChargesPerFragmentCodeRead(t *testing.T) {
216+
builder, auth, cleanup := setupProgramTest(t, true, func(b *NodeBuilder) {
217+
b.WithExtraArchs(allWasmTargets)
218+
b.WithArbOSVersion(params.ArbosVersion_StylusContractLimit)
219+
})
220+
defer cleanup()
221+
222+
file := rustFile("storage")
223+
fragmentsOne, _, _ := readFragmentedContractFile(t, file, 1)
224+
fragmentsTwo, _, _ := readFragmentedContractFile(t, file, 2)
225+
require.Len(t, fragmentsOne, 1)
226+
require.Len(t, fragmentsTwo, 2)
227+
228+
minDelta := fragmentReadCostWarmOnly(uint64(len(fragmentsTwo[0]))) + fragmentReadCostWarmOnly(uint64(len(fragmentsTwo[1]))) - fragmentReadCostWarmOnly(uint64(len(fragmentsOne[0])))
229+
maxDelta := fragmentReadCost(uint64(len(fragmentsTwo[0]))) + fragmentReadCost(uint64(len(fragmentsTwo[1]))) - fragmentReadCost(uint64(len(fragmentsOne[0])))
230+
copyDelta := fragmentCopyCost(uint64(len(fragmentsTwo[0]))) + fragmentCopyCost(uint64(len(fragmentsTwo[1]))) - fragmentCopyCost(uint64(len(fragmentsOne[0])))
231+
minDelta += copyDelta
232+
maxDelta += copyDelta
233+
234+
_, _, receiptOne := deployAndActivateFragmentedContract(t, builder.ctx, auth, builder.L2.Client, deployConfig{
235+
fragmentCount: 1,
236+
expectActivation: true,
237+
})
238+
_, _, receiptTwo := deployAndActivateFragmentedContract(t, builder.ctx, auth, builder.L2.Client, deployConfig{
239+
fragmentCount: 2,
240+
expectActivation: true,
241+
})
242+
243+
absDelta := absDiffUint64(receiptTwo.GasUsed, receiptOne.GasUsed)
244+
require.GreaterOrEqual(t, absDelta, minDelta)
245+
require.LessOrEqual(t, absDelta, maxDelta)
246+
}
247+
215248
// Specific Edge Case Tests
216249

217250
func TestThatWeCantActivateStylusFragmentContract(t *testing.T) {
@@ -636,6 +669,25 @@ func TestArbOwnerSetMaxFragmentCountFailsOnArbOS50(t *testing.T) {
636669

637670
// Utils
638671

672+
func fragmentReadCost(codeSize uint64) uint64 {
673+
return params.WarmStorageReadCostEIP2929 + params.ColdAccountAccessCostEIP2929
674+
}
675+
676+
func fragmentReadCostWarmOnly(codeSize uint64) uint64 {
677+
return params.WarmStorageReadCostEIP2929
678+
}
679+
680+
func fragmentCopyCost(codeSize uint64) uint64 {
681+
return programs.ToWordSize(codeSize) * params.CopyGas
682+
}
683+
684+
func absDiffUint64(left uint64, right uint64) uint64 {
685+
if left >= right {
686+
return left - right
687+
}
688+
return right - left
689+
}
690+
639691
// readFragmentedContractFile reads, compiles, compresses, and fragments a contract.
640692
func readFragmentedContractFile(t *testing.T, file string, fragmentCount uint16) ([][]byte, []byte, arbcompress.Dictionary) {
641693
t.Helper()

0 commit comments

Comments
 (0)