Skip to content

Commit 4446026

Browse files
committed
Implement Stylus Contract limit increase
always verify wasm fragment count isn't zero add tests for stylus contract limit increase
1 parent 02566fc commit 4446026

File tree

16 files changed

+836
-39
lines changed

16 files changed

+836
-39
lines changed

arbcompress/native.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -41,7 +41,7 @@ func Compress(input []byte, level uint32, dictionary Dictionary) ([]byte, error)
4141

4242
status := C.brotli_compress(inbuf, outbuf, C.Dictionary(dictionary), u32(level))
4343
if status != C.BrotliStatus_Success {
44-
return nil, fmt.Errorf("failed decompression: %d", status)
44+
return nil, fmt.Errorf("failed compression: %d", status)
4545
}
4646
output = output[:*outbuf.len]
4747
return output, nil

arbos/programs/native.go

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -262,7 +262,7 @@ func activateProgramInternal(
262262
}
263263

264264
// getCompiledProgram gets compiled wasm for all targets and recompiles missing ones.
265-
func getCompiledProgram(statedb vm.StateDB, moduleHash common.Hash, addressForLogging common.Address, code []byte, codehash common.Hash, maxWasmSize uint32, pagelimit uint16, time uint64, debugMode bool, program Program, runCtx *core.MessageRunContext) (map[rawdb.WasmTarget][]byte, error) {
265+
func getCompiledProgram(statedb vm.StateDB, moduleHash common.Hash, addressForLogging common.Address, code []byte, codehash common.Hash, params *StylusParams, time uint64, debugMode bool, program Program, runCtx *core.MessageRunContext) (map[rawdb.WasmTarget][]byte, error) {
266266
targets := runCtx.WasmTargets()
267267
// even though we need only asm for local target, make sure that all configured targets are available as they are needed during multi-target recording of a program call
268268
asmMap, missingTargets, err := statedb.ActivatedAsmMap(targets, moduleHash)
@@ -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(code, maxWasmSize)
280+
wasm, err := getWasmFromContractCode(statedb, code, params, false)
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)
@@ -290,7 +290,7 @@ func getCompiledProgram(statedb vm.StateDB, moduleHash common.Hash, addressForLo
290290
// we know program is activated, so it must be in correct version and not use too much memory
291291
moduleActivationMandatory := false
292292
// compile only missing targets
293-
info, newlyBuilt, err := activateProgramInternal(addressForLogging, codehash, wasm, pagelimit, program.version, zeroArbosVersion, debugMode, &zeroGas, missingTargets, moduleActivationMandatory)
293+
info, newlyBuilt, err := activateProgramInternal(addressForLogging, codehash, wasm, params.PageLimit, program.version, zeroArbosVersion, debugMode, &zeroGas, missingTargets, moduleActivationMandatory)
294294
if err != nil {
295295
log.Error("failed to reactivate program", "address", addressForLogging, "expected moduleHash", moduleHash, "err", err)
296296
return nil, fmt.Errorf("failed to reactivate program address: %v err: %w", addressForLogging, err)
@@ -399,7 +399,7 @@ func handleReqImpl(apiId usize, req_type u32, data *rustSlice, costPtr *u64, out
399399
func cacheProgram(db vm.StateDB, module common.Hash, program Program, addressForLogging common.Address, code []byte, codehash common.Hash, params *StylusParams, debug bool, time uint64, runCtx *core.MessageRunContext) {
400400
if runCtx.IsCommitMode() {
401401
// address is only used for logging
402-
asmMap, err := getCompiledProgram(db, module, addressForLogging, code, codehash, params.MaxWasmSize, params.PageLimit, time, debug, program, runCtx)
402+
asmMap, err := getCompiledProgram(db, module, addressForLogging, code, codehash, params, time, debug, program, runCtx)
403403
var ok bool
404404
var localAsm []byte
405405
if asmMap != nil {

arbos/programs/params.go

Lines changed: 28 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -24,13 +24,14 @@ const InitialPageGas = 1000 // linear cost per allocation.
2424
const initialPageRamp = 620674314 // targets 8MB costing 32 million gas, minus the linear term.
2525
const initialPageLimit = 128 // reject wasms with memories larger than 8MB.
2626
const initialInkPrice = 10000 // 1 evm gas buys 10k ink.
27-
const initialMinInitGas = 72 // charge 72 * 128 = 9216 gas.
28-
const initialMinCachedGas = 11 // charge 11 * 32 = 352 gas.
29-
const initialInitCostScalar = 50 // scale costs 1:1 (100%)
30-
const initialCachedCostScalar = 50 // scale costs 1:1 (100%)
31-
const initialExpiryDays = 365 // deactivate after 1 year.
32-
const initialKeepaliveDays = 31 // wait a month before allowing reactivation.
33-
const initialRecentCacheSize = 32 // cache the 32 most recent programs.
27+
const initialMaxFragmentCount = 2
28+
const initialMinInitGas = 72 // charge 72 * 128 = 9216 gas.
29+
const initialMinCachedGas = 11 // charge 11 * 32 = 352 gas.
30+
const initialInitCostScalar = 50 // scale costs 1:1 (100%)
31+
const initialCachedCostScalar = 50 // scale costs 1:1 (100%)
32+
const initialExpiryDays = 365 // deactivate after 1 year.
33+
const initialKeepaliveDays = 31 // wait a month before allowing reactivation.
34+
const initialRecentCacheSize = 32 // cache the 32 most recent programs.
3435

3536
const v2MinInitGas = 69 // charge 69 * 128 = 8832 gas (minCachedGas will also be charged in v2).
3637

@@ -60,6 +61,7 @@ type StylusParams struct {
6061
KeepaliveDays uint16
6162
BlockCacheSize uint16
6263
MaxWasmSize uint32
64+
MaxFragmentCount uint16
6365
}
6466

6567
// Provides a view of the Stylus parameters. Call Save() to persist.
@@ -110,6 +112,11 @@ func (p Programs) Params() (*StylusParams, error) {
110112
} else {
111113
stylusParams.MaxWasmSize = initialMaxWasmSize
112114
}
115+
if p.ArbosVersion >= params.ArbosVersion_StylusContractLimit {
116+
stylusParams.MaxFragmentCount = arbmath.BytesToUint16(take(2))
117+
} else {
118+
stylusParams.MaxFragmentCount = initialMaxFragmentCount
119+
}
113120
return stylusParams, nil
114121
}
115122

@@ -139,6 +146,10 @@ func (p *StylusParams) Save() error {
139146
if p.arbosVersion >= params.ArbosVersion_40 {
140147
data = append(data, arbmath.Uint32ToBytes(p.MaxWasmSize)...)
141148
}
149+
if p.arbosVersion >= params.ArbosVersion_StylusContractLimit {
150+
data = append(data, arbmath.Uint16ToBytes(p.MaxFragmentCount)...)
151+
152+
}
142153

143154
slot := uint64(0)
144155
for len(data) != 0 {
@@ -188,6 +199,12 @@ func (p *StylusParams) UpgradeToArbosVersion(newArbosVersion uint64) error {
188199
}
189200
p.MaxWasmSize = initialMaxWasmSize
190201
}
202+
if newArbosVersion == params.ArbosVersion_StylusContractLimit {
203+
if p.arbosVersion >= params.ArbosVersion_StylusContractLimit {
204+
return fmt.Errorf("unexpected arbosVersion upgrade to %d from %d", newArbosVersion, p.arbosVersion)
205+
}
206+
p.MaxFragmentCount = initialMaxFragmentCount
207+
}
191208
p.arbosVersion = newArbosVersion
192209
return nil
193210
}
@@ -214,5 +231,9 @@ func initStylusParams(arbosVersion uint64, sto *storage.Storage) {
214231
if arbosVersion >= params.ArbosVersion_40 {
215232
stylusParams.MaxWasmSize = initialMaxWasmSize
216233
}
234+
if arbosVersion >= params.ArbosVersion_StylusContractLimit {
235+
stylusParams.MaxFragmentCount = initialMaxFragmentCount
236+
}
237+
217238
_ = stylusParams.Save()
218239
}

arbos/programs/programs.go

Lines changed: 91 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -114,7 +114,7 @@ func (p Programs) ActivateProgram(evm *vm.EVM, address common.Address, runCtx *c
114114
// already activated and up to date
115115
return 0, codeHash, common.Hash{}, nil, false, ProgramUpToDateError()
116116
}
117-
wasm, err := getWasm(statedb, address, params.MaxWasmSize)
117+
wasm, err := getWasm(statedb, address, params)
118118
if err != nil {
119119
return 0, codeHash, common.Hash{}, nil, false, err
120120
}
@@ -225,7 +225,7 @@ func (p Programs) CallProgram(
225225
statedb.AddStylusPages(program.footprint)
226226
defer statedb.SetStylusPagesOpen(open)
227227

228-
asmMap, err := getCompiledProgram(statedb, moduleHash, contract.Address(), contract.Code, contract.CodeHash, params.MaxWasmSize, params.PageLimit, evm.Context.Time, debugMode, program, runCtx)
228+
asmMap, err := getCompiledProgram(statedb, moduleHash, contract.Address(), contract.Code, contract.CodeHash, params, evm.Context.Time, debugMode, program, runCtx)
229229
var ok bool
230230
var localAsm []byte
231231
if asmMap != nil {
@@ -307,30 +307,108 @@ func evmMemoryCost(size uint64) uint64 {
307307
return linearCost + squareCost
308308
}
309309

310-
func getWasm(statedb vm.StateDB, program common.Address, maxWasmSize uint32) ([]byte, error) {
310+
func getWasm(statedb vm.StateDB, program common.Address, params *StylusParams) ([]byte, error) {
311311
prefixedWasm := statedb.GetCode(program)
312-
return getWasmFromContractCode(prefixedWasm, maxWasmSize)
312+
return getWasmFromContractCode(statedb, prefixedWasm, params, true)
313313
}
314314

315-
func getWasmFromContractCode(prefixedWasm []byte, maxWasmSize uint32) ([]byte, error) {
316-
if prefixedWasm == nil {
315+
func getWasmFromContractCode(statedb vm.StateDB, prefixedWasm []byte, params *StylusParams, isActivation bool) ([]byte, error) {
316+
if len(prefixedWasm) == 0 {
317317
return nil, ProgramNotWasmError()
318318
}
319-
wasm, dictByte, err := state.StripStylusPrefix(prefixedWasm)
319+
320+
if state.IsStylusProgramClassic(prefixedWasm) {
321+
return handleClassicStylus(prefixedWasm, params.MaxWasmSize)
322+
}
323+
324+
if params.arbosVersion >= gethParams.ArbosVersion_StylusContractLimit {
325+
if state.IsStylusProgramRoot(prefixedWasm) {
326+
return handleRootStylus(statedb, prefixedWasm, params.MaxWasmSize, params.MaxFragmentCount, isActivation)
327+
}
328+
329+
if state.IsStylusProgramFragment(prefixedWasm) {
330+
return nil, errors.New("fragmented stylus programs cannot be activated directly; activate the root program instead")
331+
}
332+
}
333+
334+
return nil, ProgramNotWasmError()
335+
}
336+
337+
func handleClassicStylus(data []byte, maxSize uint32) ([]byte, error) {
338+
wasm, dictByte, err := state.StripStylusPrefix(data)
320339
if err != nil {
321340
return nil, err
322341
}
323342

324-
var dict arbcompress.Dictionary
325-
switch dictByte {
343+
dict, err := getStylusCompressionDict(dictByte)
344+
if err != nil {
345+
return nil, err
346+
}
347+
348+
return arbcompress.DecompressWithDictionary(wasm, int(maxSize), dict)
349+
}
350+
351+
func handleRootStylus(statedb vm.StateDB, data []byte, maxSize uint32, maxFragments uint16, isActivation bool) ([]byte, error) {
352+
rawAddresses, dictByte, decompressedLength, err := state.StripStylusRootPrefix(data)
353+
if err != nil {
354+
return nil, err
355+
}
356+
if isActivation && decompressedLength > maxSize {
357+
return nil, fmt.Errorf("invalid wasm: decompressedLength %d is greater then MaxWasmSize %d", decompressedLength, maxSize)
358+
}
359+
if isActivation && len(rawAddresses)/common.AddressLength > int(maxFragments) {
360+
return nil, fmt.Errorf("invalid wasm: fragment count exceeds limit of %d", maxFragments)
361+
}
362+
363+
if len(rawAddresses)/common.AddressLength == 0 {
364+
return nil, fmt.Errorf("invalid wasm: fragment count cannot be zero")
365+
}
366+
367+
var compressedWasm []byte
368+
for i := 0; i < len(rawAddresses); i += common.AddressLength {
369+
addr := common.BytesToAddress(rawAddresses[i : i+common.AddressLength])
370+
371+
fragCode := statedb.GetCode(addr)
372+
payload, err := state.StripStylusFragmentPrefix(fragCode)
373+
if err != nil {
374+
return nil, err
375+
}
376+
377+
if len(payload) == 0 {
378+
continue
379+
}
380+
381+
compressedWasm = append(compressedWasm, payload...)
382+
}
383+
384+
dict, err := getStylusCompressionDict(dictByte)
385+
if err != nil {
386+
return nil, err
387+
}
388+
389+
wasm, err := arbcompress.DecompressWithDictionary(compressedWasm, int(decompressedLength), dict)
390+
391+
if err != nil {
392+
return nil, err
393+
}
394+
395+
if len(wasm) != int(decompressedLength) {
396+
return nil, fmt.Errorf("invalid wasm: decompressed length %d does not match expected length %d", len(wasm), decompressedLength)
397+
}
398+
399+
return wasm, nil
400+
}
401+
402+
// Named return parameters allow us to return the zero-value for 'dict' implicitly on error
403+
func getStylusCompressionDict(id byte) (dict arbcompress.Dictionary, err error) {
404+
switch id {
326405
case 0:
327-
dict = arbcompress.EmptyDictionary
406+
return arbcompress.EmptyDictionary, nil
328407
case 1:
329-
dict = arbcompress.StylusProgramDictionary
408+
return arbcompress.StylusProgramDictionary, nil
330409
default:
331-
return nil, fmt.Errorf("unsupported dictionary %v", dictByte)
410+
return dict, fmt.Errorf("unsupported dictionary type: %d", id)
332411
}
333-
return arbcompress.DecompressWithDictionary(wasm, int(maxWasmSize), dict)
334412
}
335413

336414
// Gets a program entry, which may be expired or not yet activated.

arbos/programs/wasm.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -136,7 +136,7 @@ func startProgram(module uint32) uint32
136136
//go:wasmimport programs send_response
137137
func sendResponse(req_id uint32) uint32
138138

139-
func getCompiledProgram(statedb vm.StateDB, moduleHash common.Hash, addressForLogging common.Address, code []byte, codeHash common.Hash, maxWasmSize uint32, pagelimit uint16, time uint64, debugMode bool, program Program, runCtx *core.MessageRunContext) (map[rawdb.WasmTarget][]byte, error) {
139+
func getCompiledProgram(statedb vm.StateDB, moduleHash common.Hash, addressForLogging common.Address, code []byte, codeHash common.Hash, stylusParams *StylusParams, time uint64, debugMode bool, program Program, runCtx *core.MessageRunContext) (map[rawdb.WasmTarget][]byte, error) {
140140
// we need to return asm map with an entry for local target to make checks for local target work
141141
return map[rawdb.WasmTarget][]byte{rawdb.LocalTarget(): {}}, nil
142142
}

arbos/programs/wasmstorehelper.go

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

54-
wasm, err := getWasmFromContractCode(code, progParams.MaxWasmSize)
54+
wasm, err := getWasmFromContractCode(statedb, code, progParams, false)
5555
if err != nil {
5656
log.Error("Failed to reactivate program while rebuilding wasm store: getWasmFromContractCode", "expected moduleHash", moduleHash, "err", err)
5757
return fmt.Errorf("failed to reactivate program while rebuilding wasm store: %w", err)

contracts

contracts-legacy

0 commit comments

Comments
 (0)