diff --git a/arbcompress/native.go b/arbcompress/native.go index 9347abc923..5f14c0766c 100644 --- a/arbcompress/native.go +++ b/arbcompress/native.go @@ -41,7 +41,7 @@ func Compress(input []byte, level uint32, dictionary Dictionary) ([]byte, error) status := C.brotli_compress(inbuf, outbuf, C.Dictionary(dictionary), u32(level)) if status != C.BrotliStatus_Success { - return nil, fmt.Errorf("failed decompression: %d", status) + return nil, fmt.Errorf("failed compression: %d", status) } output = output[:*outbuf.len] return output, nil diff --git a/arbos/arbosState/arbosstate.go b/arbos/arbosState/arbosstate.go index ea8ec15ba4..5852c31909 100644 --- a/arbos/arbosState/arbosstate.go +++ b/arbos/arbosState/arbosstate.go @@ -462,8 +462,13 @@ func (state *ArbosState) UpgradeArbosVersion( case 52, 53, 54, 55, 56, 57, 58, 59: // these versions are left to Orbit chains for custom upgrades. - case params.ArbosVersion_TransactionFiltering: - // Once the final ArbOS version is locked in, this can be moved to that numeric version. + case params.ArbosVersion_60: + // Changes for ArbosVersion_StylusContractLimit + p, err := state.Programs().Params() + ensure(err) + ensure(p.UpgradeToArbosVersion(nextArbosVersion)) + ensure(p.Save()) + // Changes for ArbosVersion_TransactionFiltering ensure(addressSet.Initialize(state.backingStorage.OpenSubStorage(transactionFiltererSubspace))) default: diff --git a/arbos/programs/native.go b/arbos/programs/native.go index 6dea9e78c7..81c1d2f865 100644 --- a/arbos/programs/native.go +++ b/arbos/programs/native.go @@ -262,7 +262,7 @@ func activateProgramInternal( } // getCompiledProgram gets compiled wasm for all targets and recompiles missing ones. -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) { +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) { targets := runCtx.WasmTargets() // 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 asmMap, missingTargets, err := statedb.ActivatedAsmMap(targets, moduleHash) @@ -277,7 +277,7 @@ func getCompiledProgram(statedb vm.StateDB, moduleHash common.Hash, addressForLo } // addressForLogging may be empty or may not correspond to the code, so we need to be careful to use the code passed in separately - wasm, err := getWasmFromContractCode(code, maxWasmSize) + wasm, err := getWasmFromContractCode(statedb, code, params, false) if err != nil { log.Error("Failed to reactivate program: getWasm", "address", addressForLogging, "expected moduleHash", moduleHash, "err", err) 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 // we know program is activated, so it must be in correct version and not use too much memory moduleActivationMandatory := false // compile only missing targets - info, newlyBuilt, err := activateProgramInternal(addressForLogging, codehash, wasm, pagelimit, program.version, zeroArbosVersion, debugMode, &zeroGas, missingTargets, moduleActivationMandatory) + info, newlyBuilt, err := activateProgramInternal(addressForLogging, codehash, wasm, params.PageLimit, program.version, zeroArbosVersion, debugMode, &zeroGas, missingTargets, moduleActivationMandatory) if err != nil { log.Error("failed to reactivate program", "address", addressForLogging, "expected moduleHash", moduleHash, "err", err) return nil, fmt.Errorf("failed to reactivate program address: %v err: %w", addressForLogging, err) @@ -326,8 +326,8 @@ func getCompiledProgram(statedb vm.StateDB, moduleHash common.Hash, addressForLo return asmMap, nil } -func handleProgramPrepare(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) []byte { - asmMap, err := getCompiledProgram(statedb, moduleHash, addressForLogging, code, codehash, maxWasmSize, pagelimit, time, debugMode, program, runCtx) +func handleProgramPrepare(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) []byte { + asmMap, err := getCompiledProgram(statedb, moduleHash, addressForLogging, code, codehash, params, time, debugMode, program, runCtx) var ok bool var localAsm []byte if asmMap != nil { @@ -413,7 +413,7 @@ func handleReqImpl(apiId usize, req_type u32, data *rustSlice, costPtr *u64, out 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) { if runCtx.IsCommitMode() { // address is only used for logging - asmMap, err := getCompiledProgram(db, module, addressForLogging, code, codehash, params.MaxWasmSize, params.PageLimit, time, debug, program, runCtx) + asmMap, err := getCompiledProgram(db, module, addressForLogging, code, codehash, params, time, debug, program, runCtx) var ok bool var localAsm []byte if asmMap != nil { diff --git a/arbos/programs/params.go b/arbos/programs/params.go index 24316c4fbf..e7caffc291 100644 --- a/arbos/programs/params.go +++ b/arbos/programs/params.go @@ -24,13 +24,14 @@ const InitialPageGas = 1000 // linear cost per allocation. const initialPageRamp = 620674314 // targets 8MB costing 32 million gas, minus the linear term. const initialPageLimit = 128 // reject wasms with memories larger than 8MB. const initialInkPrice = 10000 // 1 evm gas buys 10k ink. -const initialMinInitGas = 72 // charge 72 * 128 = 9216 gas. -const initialMinCachedGas = 11 // charge 11 * 32 = 352 gas. -const initialInitCostScalar = 50 // scale costs 1:1 (100%) -const initialCachedCostScalar = 50 // scale costs 1:1 (100%) -const initialExpiryDays = 365 // deactivate after 1 year. -const initialKeepaliveDays = 31 // wait a month before allowing reactivation. -const initialRecentCacheSize = 32 // cache the 32 most recent programs. +const initialMaxFragmentCount = 2 +const initialMinInitGas = 72 // charge 72 * 128 = 9216 gas. +const initialMinCachedGas = 11 // charge 11 * 32 = 352 gas. +const initialInitCostScalar = 50 // scale costs 1:1 (100%) +const initialCachedCostScalar = 50 // scale costs 1:1 (100%) +const initialExpiryDays = 365 // deactivate after 1 year. +const initialKeepaliveDays = 31 // wait a month before allowing reactivation. +const initialRecentCacheSize = 32 // cache the 32 most recent programs. const v2MinInitGas = 69 // charge 69 * 128 = 8832 gas (minCachedGas will also be charged in v2). @@ -60,6 +61,7 @@ type StylusParams struct { KeepaliveDays uint16 BlockCacheSize uint16 MaxWasmSize uint32 + MaxFragmentCount uint16 } // Provides a view of the Stylus parameters. Call Save() to persist. @@ -110,6 +112,11 @@ func (p Programs) Params() (*StylusParams, error) { } else { stylusParams.MaxWasmSize = initialMaxWasmSize } + if p.ArbosVersion >= params.ArbosVersion_StylusContractLimit { + stylusParams.MaxFragmentCount = arbmath.BytesToUint16(take(2)) + } else { + stylusParams.MaxFragmentCount = 0 + } return stylusParams, nil } @@ -139,6 +146,9 @@ func (p *StylusParams) Save() error { if p.arbosVersion >= params.ArbosVersion_40 { data = append(data, arbmath.Uint32ToBytes(p.MaxWasmSize)...) } + if p.arbosVersion >= params.ArbosVersion_StylusContractLimit { + data = append(data, arbmath.Uint16ToBytes(p.MaxFragmentCount)...) + } slot := uint64(0) for len(data) != 0 { @@ -171,23 +181,24 @@ func (p *StylusParams) UpgradeToVersion(version uint16) error { } func (p *StylusParams) UpgradeToArbosVersion(newArbosVersion uint64) error { - if newArbosVersion == params.ArbosVersion_50 { - if p.arbosVersion >= params.ArbosVersion_50 { - return fmt.Errorf("unexpected arbosVersion upgrade to %d from %d", newArbosVersion, p.arbosVersion) - } + if p.arbosVersion >= newArbosVersion { + return fmt.Errorf("unexpected arbosVersion upgrade to %d from %d", newArbosVersion, p.arbosVersion) + } + + switch newArbosVersion { + case params.ArbosVersion_50: if p.MaxStackDepth > arbOS50MaxWasmSize { p.MaxStackDepth = arbOS50MaxWasmSize } - } - if newArbosVersion == params.ArbosVersion_40 { - if p.arbosVersion >= params.ArbosVersion_40 { - return fmt.Errorf("unexpected arbosVersion upgrade to %d from %d", newArbosVersion, p.arbosVersion) - } + case params.ArbosVersion_40: if p.Version != 2 { return fmt.Errorf("unexpected arbosVersion upgrade to %d while stylus version %d", newArbosVersion, p.Version) } p.MaxWasmSize = initialMaxWasmSize + case params.ArbosVersion_StylusContractLimit: + p.MaxFragmentCount = initialMaxFragmentCount } + p.arbosVersion = newArbosVersion return nil } @@ -214,5 +225,9 @@ func initStylusParams(arbosVersion uint64, sto *storage.Storage) { if arbosVersion >= params.ArbosVersion_40 { stylusParams.MaxWasmSize = initialMaxWasmSize } + if arbosVersion >= params.ArbosVersion_StylusContractLimit { + stylusParams.MaxFragmentCount = initialMaxFragmentCount + } + _ = stylusParams.Save() } diff --git a/arbos/programs/programs.go b/arbos/programs/programs.go index 0dc4840616..0eecc758e4 100644 --- a/arbos/programs/programs.go +++ b/arbos/programs/programs.go @@ -113,7 +113,7 @@ func (p Programs) ActivateProgram(evm *vm.EVM, address common.Address, runCtx *c // already activated and up to date return 0, codeHash, common.Hash{}, nil, false, ProgramUpToDateError() } - wasm, err := getWasm(statedb, address, params.MaxWasmSize) + wasm, err := getWasm(statedb, address, params) if err != nil { return 0, codeHash, common.Hash{}, nil, false, err } @@ -224,7 +224,7 @@ func (p Programs) CallProgram( statedb.AddStylusPages(program.footprint) defer statedb.SetStylusPagesOpen(open) - localAsm := handleProgramPrepare(statedb, moduleHash, contract.Address(), contract.Code, contract.CodeHash, params.MaxWasmSize, params.PageLimit, evm.Context.Time, debugMode, program, runCtx) + localAsm := handleProgramPrepare(statedb, moduleHash, contract.Address(), contract.Code, contract.CodeHash, params, evm.Context.Time, debugMode, program, runCtx) evmData := &EvmData{ arbosVersion: evm.Context.ArbOSVersion, @@ -298,30 +298,104 @@ func evmMemoryCost(size uint64) uint64 { return linearCost + squareCost } -func getWasm(statedb vm.StateDB, program common.Address, maxWasmSize uint32) ([]byte, error) { +func getWasm(statedb vm.StateDB, program common.Address, params *StylusParams) ([]byte, error) { prefixedWasm := statedb.GetCode(program) - return getWasmFromContractCode(prefixedWasm, maxWasmSize) + return getWasmFromContractCode(statedb, prefixedWasm, params, true) } -func getWasmFromContractCode(prefixedWasm []byte, maxWasmSize uint32) ([]byte, error) { - if prefixedWasm == nil { +func getWasmFromContractCode(statedb vm.StateDB, prefixedWasm []byte, params *StylusParams, isActivation bool) ([]byte, error) { + if len(prefixedWasm) == 0 { return nil, ProgramNotWasmError() } - wasm, dictByte, err := state.StripStylusPrefix(prefixedWasm) + + if state.IsStylusClassicProgramPrefix(prefixedWasm) { + return handleClassicStylus(prefixedWasm, params.MaxWasmSize) + } + + if params.arbosVersion >= gethParams.ArbosVersion_StylusContractLimit { + if state.IsStylusRootProgramPrefix(prefixedWasm) { + return handleRootStylus(statedb, prefixedWasm, params.MaxWasmSize, params.MaxFragmentCount, isActivation) + } + + if state.IsStylusFragmentPrefix(prefixedWasm) { + return nil, errors.New("fragmented stylus programs cannot be activated directly; activate the root program instead") + } + } + + return nil, ProgramNotWasmError() +} + +func handleClassicStylus(data []byte, maxSize uint32) ([]byte, error) { + wasm, dictByte, err := state.StripStylusPrefix(data) + if err != nil { + return nil, err + } + + dict, err := getStylusCompressionDict(dictByte) + if err != nil { + return nil, err + } + + return arbcompress.DecompressWithDictionary(wasm, int(maxSize), dict) +} + +func handleRootStylus(statedb vm.StateDB, data []byte, maxSize uint32, maxFragments uint16, isActivation bool) ([]byte, error) { + root, err := state.NewStylusRoot(data) + if err != nil { + return nil, err + } + + if isActivation { + if root.DecompressedLength > maxSize { + return nil, fmt.Errorf("invalid wasm: decompressedLength %d is greater then MaxWasmSize %d", root.DecompressedLength, maxSize) + } + if len(root.Addresses) > int(maxFragments) { + return nil, fmt.Errorf("invalid wasm: fragment count exceeds limit of %d", maxFragments) + } + } + + if len(root.Addresses) == 0 { + return nil, fmt.Errorf("invalid wasm: fragment count cannot be zero") + } + + var compressedWasm []byte + for _, addr := range root.Addresses { + fragCode := statedb.GetCode(addr) + + payload, err := state.StripStylusFragmentPrefix(fragCode) + if err != nil { + return nil, err + } + + compressedWasm = append(compressedWasm, payload...) + } + + dict, err := getStylusCompressionDict(root.DictionaryType) if err != nil { return nil, err } - var dict arbcompress.Dictionary - switch dictByte { + wasm, err := arbcompress.DecompressWithDictionary(compressedWasm, int(root.DecompressedLength), dict) + if err != nil { + return nil, err + } + + if len(wasm) != int(root.DecompressedLength) { + return nil, fmt.Errorf("invalid wasm: decompressed length %d does not match expected length %d", len(wasm), root.DecompressedLength) + } + + return wasm, nil +} + +func getStylusCompressionDict(id byte) (arbcompress.Dictionary, error) { + switch id { case 0: - dict = arbcompress.EmptyDictionary + return arbcompress.EmptyDictionary, nil case 1: - dict = arbcompress.StylusProgramDictionary + return arbcompress.StylusProgramDictionary, nil default: - return nil, fmt.Errorf("unsupported dictionary %v", dictByte) + return 0, fmt.Errorf("unsupported dictionary type: %d", id) } - return arbcompress.DecompressWithDictionary(wasm, int(maxWasmSize), dict) } // Gets a program entry, which may be expired or not yet activated. diff --git a/arbos/programs/wasm.go b/arbos/programs/wasm.go index 96660108a8..5cd7429cb0 100644 --- a/arbos/programs/wasm.go +++ b/arbos/programs/wasm.go @@ -156,7 +156,7 @@ func startProgram(module uint32) uint32 //go:wasmimport programs send_response func sendResponse(req_id uint32) uint32 -func handleProgramPrepare(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) []byte { +func handleProgramPrepare(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) []byte { requiresPrepare := programRequiresPrepare(unsafe.Pointer(&moduleHash[0])) if requiresPrepare != 0 { var debugInt uint32 @@ -173,8 +173,8 @@ func handleProgramPrepare(statedb vm.StateDB, moduleHash common.Hash, addressFor unsafe.Pointer(&code), codeSize, unsafe.Pointer(&codehash), - maxWasmSize, - uint32(pagelimit), + params.MaxWasmSize, + uint32(params.PageLimit), time, debugInt, unsafe.Pointer(&program), diff --git a/arbos/programs/wasmstorehelper.go b/arbos/programs/wasmstorehelper.go index 60dd9edc91..ae1086b5fb 100644 --- a/arbos/programs/wasmstorehelper.go +++ b/arbos/programs/wasmstorehelper.go @@ -51,7 +51,7 @@ func (p Programs) SaveActiveProgramToWasmStore(statedb *state.StateDB, codeHash return nil } - wasm, err := getWasmFromContractCode(code, progParams.MaxWasmSize) + wasm, err := getWasmFromContractCode(statedb, code, progParams, false) if err != nil { log.Error("Failed to reactivate program while rebuilding wasm store: getWasmFromContractCode", "expected moduleHash", moduleHash, "err", err) return fmt.Errorf("failed to reactivate program while rebuilding wasm store: %w", err) diff --git a/changelog/kolbyml-increase-stylus-smart-contract-size.md b/changelog/kolbyml-increase-stylus-smart-contract-size.md new file mode 100644 index 0000000000..02055393da --- /dev/null +++ b/changelog/kolbyml-increase-stylus-smart-contract-size.md @@ -0,0 +1,2 @@ +### Added +- Increase Stylus smart contract size limit via merge-on-activate diff --git a/contracts-local/src/precompiles b/contracts-local/src/precompiles index 0e455541b5..b0777f7a3f 160000 --- a/contracts-local/src/precompiles +++ b/contracts-local/src/precompiles @@ -1 +1 @@ -Subproject commit 0e455541b5dc9203506d995b153575f18cf71cdd +Subproject commit b0777f7a3f6382a109e4481bacbbe679560ab667 diff --git a/execution/gethexec/wasmstorerebuilder.go b/execution/gethexec/wasmstorerebuilder.go index 0df2c7bd96..144094a882 100644 --- a/execution/gethexec/wasmstorerebuilder.go +++ b/execution/gethexec/wasmstorerebuilder.go @@ -93,7 +93,7 @@ func RebuildWasmStore(ctx context.Context, wasmStore ethdb.KeyValueStore, execut codeHashBytes := bytes.TrimPrefix(iter.Key(), rawdb.CodePrefix) codeHash := common.BytesToHash(codeHashBytes) code := iter.Value() - if state.IsStylusProgram(code) { + if state.IsStylusDeployableProgramPrefix(code) { if err := programs.SaveActiveProgramToWasmStore(stateDB, codeHash, code, latestHeader.Time, l2Blockchain.Config().DebugMode(), rebuildingStartHeader.Time, targets); err != nil { return fmt.Errorf("error while rebuilding of wasm store, aborting rebuilding: %w", err) } diff --git a/go-ethereum b/go-ethereum index 527cf6d824..2b1022a64a 160000 --- a/go-ethereum +++ b/go-ethereum @@ -1 +1 @@ -Subproject commit 527cf6d8245c248df5535ab9e98aa57ee55b4698 +Subproject commit 2b1022a64a1864083fe590f331f5c7c998fcfd63 diff --git a/precompiles/ArbOwner.go b/precompiles/ArbOwner.go index dfa0a12fbd..332b3f1a40 100644 --- a/precompiles/ArbOwner.go +++ b/precompiles/ArbOwner.go @@ -603,5 +603,10 @@ func (con ArbOwner) SetMultiGasPricingConstraints( } func (con ArbOwner) SetMaxStylusContractFragments(c ctx, evm mech, maxFragments uint16) error { - return errors.New("SetMaxStylusContractFragments is not implemented yet") + params, err := c.State.Programs().Params() + if err != nil { + return err + } + params.MaxFragmentCount = maxFragments + return params.Save() } diff --git a/precompiles/ArbOwnerPublic.go b/precompiles/ArbOwnerPublic.go index 048f17ac1d..4f7bdeb032 100644 --- a/precompiles/ArbOwnerPublic.go +++ b/precompiles/ArbOwnerPublic.go @@ -4,8 +4,6 @@ package precompiles import ( - "errors" - "github.com/ethereum/go-ethereum/common" "github.com/ethereum/go-ethereum/params" ) @@ -113,5 +111,9 @@ func (con ArbOwnerPublic) GetParentGasFloorPerToken(c ctx, evm mech) (uint64, er } func (con ArbOwnerPublic) GetMaxStylusContractFragments(c ctx, evm mech) (uint16, error) { - return 0, errors.New("GetMaxStylusContractFragments is not implemented yet") + params, err := c.State.Programs().Params() + if err != nil { + return 0, err + } + return params.MaxFragmentCount, nil } diff --git a/precompiles/precompile.go b/precompiles/precompile.go index 547415b2ec..750e906458 100644 --- a/precompiles/precompile.go +++ b/precompiles/precompile.go @@ -561,7 +561,7 @@ func Precompiles() map[addr]ArbosPrecompile { ArbOwnerPublic.methodsByName["IsNativeTokenOwner"].arbosVersion = params.ArbosVersion_41 ArbOwnerPublic.methodsByName["GetAllNativeTokenOwners"].arbosVersion = params.ArbosVersion_41 ArbOwnerPublic.methodsByName["GetParentGasFloorPerToken"].arbosVersion = params.ArbosVersion_50 - ArbOwnerPublic.methodsByName["GetMaxStylusContractFragments"].arbosVersion = params.ArbosVersion_60 + ArbOwnerPublic.methodsByName["GetMaxStylusContractFragments"].arbosVersion = params.ArbosVersion_StylusContractLimit ArbWasmImpl := &ArbWasm{Address: types.ArbWasmAddress} ArbWasm := insert(MakePrecompile(precompilesgen.ArbWasmMetaData, ArbWasmImpl)) @@ -656,6 +656,7 @@ func Precompiles() map[addr]ArbosPrecompile { ArbOwner.methodsByName["GetAllNativeTokenOwners"].arbosVersion = params.ArbosVersion_41 ArbOwner.methodsByName["SetParentGasFloorPerToken"].arbosVersion = params.ArbosVersion_50 ArbOwner.methodsByName["SetMaxBlockGasLimit"].arbosVersion = params.ArbosVersion_50 + ArbOwner.methodsByName["SetMaxStylusContractFragments"].arbosVersion = params.ArbosVersion_StylusContractLimit ArbOwner.methodsByName["AddTransactionFilterer"].arbosVersion = params.ArbosVersion_TransactionFiltering ArbOwner.methodsByName["RemoveTransactionFilterer"].arbosVersion = params.ArbosVersion_TransactionFiltering diff --git a/system_tests/archival_path_scheme_test.go b/system_tests/archival_path_scheme_test.go index a0a7939062..2a483ea266 100644 --- a/system_tests/archival_path_scheme_test.go +++ b/system_tests/archival_path_scheme_test.go @@ -4,7 +4,6 @@ package arbtest import ( "context" - "fmt" "math/big" "testing" @@ -73,7 +72,6 @@ func TestAccessingPathSchemeArchivalState(t *testing.T) { // Build a node with history past the 128 block diff threshold cancelNode := buildWithHistory(t, ctx, builder, 150) - fmt.Println("bluebird 5-3", builder.execConfig.Caching.StateScheme) execNode, l2client := builder.L2.ExecNode, builder.L2.Client defer cancelNode() bc := execNode.Backend.ArbInterface().BlockChain() diff --git a/system_tests/common_test.go b/system_tests/common_test.go index 9437b2679d..9d9cef15b1 100644 --- a/system_tests/common_test.go +++ b/system_tests/common_test.go @@ -2453,10 +2453,20 @@ func deployContractInitCode(code []byte, revert bool) []byte { func deployContract( t *testing.T, ctx context.Context, auth bind.TransactOpts, client *ethclient.Client, code []byte, ) common.Address { + address, err := deployContractForwardError(t, ctx, auth, client, code) + Require(t, err) + return address +} + +func deployContractForwardError( + t *testing.T, ctx context.Context, auth bind.TransactOpts, client *ethclient.Client, code []byte, +) (common.Address, error) { deploy := deployContractInitCode(code, false) basefee := arbmath.BigMulByFrac(GetBaseFee(t, client, ctx), 6, 5) // current*1.2 nonce, err := client.NonceAt(ctx, auth.From, nil) - Require(t, err) + if err != nil { + return common.Address{}, err + } gas, err := client.EstimateGas(ctx, ethereum.CallMsg{ From: auth.From, GasPrice: basefee, @@ -2464,14 +2474,22 @@ func deployContract( Value: big.NewInt(0), Data: deploy, }) - Require(t, err) + if err != nil { + return common.Address{}, err + } tx := types.NewContractCreation(nonce, big.NewInt(0), gas, basefee, deploy) tx, err = auth.Signer(auth.From, tx) - Require(t, err) - Require(t, client.SendTransaction(ctx, tx)) + if err != nil { + return common.Address{}, err + } + if err := client.SendTransaction(ctx, tx); err != nil { + return common.Address{}, err + } _, err = EnsureTxSucceeded(ctx, client, tx) - Require(t, err) - return crypto.CreateAddress(auth.From, nonce) + if err != nil { + return common.Address{}, err + } + return crypto.CreateAddress(auth.From, nonce), nil } func sendContractCall( diff --git a/system_tests/stylus_contract_limit_increase_test.go b/system_tests/stylus_contract_limit_increase_test.go new file mode 100644 index 0000000000..fa879fc22e --- /dev/null +++ b/system_tests/stylus_contract_limit_increase_test.go @@ -0,0 +1,725 @@ +// Copyright 2025, Offchain Labs, Inc. +// For license information, see https://github.com/OffchainLabs/nitro/blob/master/LICENSE.md + +package arbtest + +import ( + "bytes" + "context" + "encoding/binary" + "fmt" + "os" + "path/filepath" + "slices" + "strings" + "testing" + + "github.com/stretchr/testify/require" + + "github.com/ethereum/go-ethereum/accounts/abi/bind" + "github.com/ethereum/go-ethereum/common" + "github.com/ethereum/go-ethereum/core/rawdb" + "github.com/ethereum/go-ethereum/core/state" + "github.com/ethereum/go-ethereum/core/types" + "github.com/ethereum/go-ethereum/core/vm" + _ "github.com/ethereum/go-ethereum/eth/tracers/js" + "github.com/ethereum/go-ethereum/ethclient" + "github.com/ethereum/go-ethereum/ethdb" + "github.com/ethereum/go-ethereum/log" + "github.com/ethereum/go-ethereum/params" + + "github.com/offchainlabs/nitro/arbcompress" + "github.com/offchainlabs/nitro/arbos/programs" + "github.com/offchainlabs/nitro/arbutil" + "github.com/offchainlabs/nitro/execution/gethexec" + "github.com/offchainlabs/nitro/solgen/go/precompilesgen" + "github.com/offchainlabs/nitro/util/colors" + "github.com/offchainlabs/nitro/util/testhelpers" +) + +// Shared Helpers + +type deployConfig struct { + fragmentCount uint16 + mutateDict func(arbcompress.Dictionary) arbcompress.Dictionary + mutateAddrs func([]common.Address) + mutateSize func(int) int + expectActivation bool + expectedErr string +} + +func defaultDeployConfig() deployConfig { + return deployConfig{ + fragmentCount: 2, + expectActivation: true, + } +} + +// deployAndActivateFragmentedContract handles the common flow of reading rust files, +// splitting them, deploying fragments, constructing the root, and activating it. +func deployAndActivateFragmentedContract( + t *testing.T, + ctx context.Context, + auth bind.TransactOpts, + l2client *ethclient.Client, + cfg deployConfig, +) (common.Address, []byte, *types.Receipt) { + file := rustFile("storage") + name := strings.TrimSuffix(filepath.Base(file), filepath.Ext(file)) + + // Read and fragment + fragments, sourceWasm, dictType := readFragmentedContractFile(t, file, cfg.fragmentCount) + require.Len(t, fragments, int(cfg.fragmentCount)) + + // Apply dictionary mutations if any + if cfg.mutateDict != nil { + dictType = cfg.mutateDict(dictType) + } + + // Deploy fragments + auth.GasLimit = 32000000 // skip gas estimation + addresses := make([]common.Address, 0, len(fragments)) + for i, fragment := range fragments { + fragmentAddress := deployContract(t, ctx, auth, l2client, fragment) + colors.PrintGrey(name, ": fragment contract", i, " deployed to ", fragmentAddress.Hex()) + addresses = append(addresses, fragmentAddress) + } + + // Apply address mutations if any + if cfg.mutateAddrs != nil { + cfg.mutateAddrs(addresses) + } + + // Calculate size + // #nosec G115 + decompressedSize := uint32(len(sourceWasm)) + if cfg.mutateSize != nil { + // #nosec G115 + decompressedSize = uint32(cfg.mutateSize(len(sourceWasm))) + } + + // Deploy root contract + rootContract := constructRootContract(t, decompressedSize, addresses, dictType) + rootAddress := deployContract(t, ctx, auth, l2client, rootContract) + colors.PrintGrey(name, ": root contract deployed to ", rootAddress.Hex()) + + // Activate + arbWasm, err := precompilesgen.NewArbWasm(types.ArbWasmAddress, l2client) + Require(t, err) + auth.Value = oneEth + tx, err := arbWasm.ActivateProgram(&auth, rootAddress) + Require(t, err) + + receipt, err := EnsureTxSucceeded(ctx, l2client, tx) + if cfg.expectActivation { + Require(t, err) + return rootAddress, sourceWasm, receipt + } + + require.Error(t, err, cfg.expectedErr) + return rootAddress, sourceWasm, nil +} + +// Validation Tests + +func TestFragmentedContractValidation(t *testing.T) { + tests := []struct { + name string + cfg deployConfig + }{ + { + name: "Valid 2 Fragments", + cfg: defaultDeployConfig(), + }, + { + name: "Valid 1 Fragment", + cfg: deployConfig{ + fragmentCount: 1, + expectActivation: true, + }, + }, + { + name: "Zero Fragments", + cfg: deployConfig{ + fragmentCount: 0, + expectActivation: false, + expectedErr: "We can't deploy fragmented contracts which have zero fragments", + }, + }, + { + name: "Too Many Fragments (3)", + cfg: deployConfig{ + fragmentCount: 3, + expectActivation: false, + expectedErr: "more fragments then the current limit", + }, + }, + { + name: "Decompression Size Too Small", + cfg: deployConfig{ + fragmentCount: 2, + expectActivation: false, + expectedErr: "smaller decompression size then the actual wasm size", + mutateSize: func(i int) int { return i - 1 }, + }, + }, + { + name: "Decompression Size Too Big", + cfg: deployConfig{ + fragmentCount: 2, + expectActivation: false, + expectedErr: "bigger decompression size then the actual wasm size", + mutateSize: func(i int) int { return i + 1 }, + }, + }, + { + name: "Incorrect Dictionary Type", + cfg: deployConfig{ + fragmentCount: 2, + expectActivation: false, + expectedErr: "incorrect dictionary type", + mutateDict: func(d arbcompress.Dictionary) arbcompress.Dictionary { + return (d + 1) % 2 + }, + }, + }, + { + name: "Invalid Address Order", + cfg: deployConfig{ + fragmentCount: 2, + expectActivation: false, + expectedErr: "fragment addresses in the wrong order", + mutateAddrs: func(addrs []common.Address) { + slices.Reverse(addrs) + }, + }, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + builder, auth, cleanup := setupProgramTest(t, true, func(b *NodeBuilder) { + b.WithExtraArchs(allWasmTargets) + b.WithArbOSVersion(params.ArbosVersion_StylusContractLimit) + }) + defer cleanup() + + // If testing 0 fragments, readFragmentedContractFile returns empty sourceWasm, + // creating an issue for constructRootContract size calculation if not handled. + // The helper handles typical cases, edge cases are covered by logic inside deployAndActivate. + deployAndActivateFragmentedContract(t, builder.ctx, auth, builder.L2.Client, tt.cfg) + }) + } +} + +// Specific Edge Case Tests + +func TestThatWeCantActivateStylusFragmentContract(t *testing.T) { + builder, auth, cleanup := setupProgramTest(t, true, func(b *NodeBuilder) { + b.WithExtraArchs(allWasmTargets) + b.WithArbOSVersion(params.ArbosVersion_StylusContractLimit) + }) + defer cleanup() + + file := rustFile("storage") + fragments, _, _ := readFragmentedContractFile(t, file, 1) + require.Len(t, fragments, 1) + auth.GasLimit = 32000000 + fragmentAddress := deployContract(t, builder.ctx, auth, builder.L2.Client, fragments[0]) + + arbWasm, err := precompilesgen.NewArbWasm(types.ArbWasmAddress, builder.L2.Client) + Require(t, err) + auth.Value = oneEth + tx, err := arbWasm.ActivateProgram(&auth, fragmentAddress) + Require(t, err) + _, err = EnsureTxSucceeded(builder.ctx, builder.L2.Client, tx) + require.Error(t, err, "We can't activate a stylus fragment contract directly") +} + +func TestDeployStylusRootContractGreaterThanMaxCodeSize(t *testing.T) { + builder, auth, cleanup := setupProgramTest(t, true, func(b *NodeBuilder) { + b.WithExtraArchs(allWasmTargets) + b.WithArbOSVersion(params.ArbosVersion_StylusContractLimit) + b.chainConfig.ArbitrumChainParams.MaxCodeSize = 4500 + }) + defer cleanup() + + // 1. Classic contract fail + file := rustFile("storage") + wasm, _ := readWasmFile(t, file) + auth.GasLimit = 32000000 + _, err := deployContractForwardError(t, builder.ctx, auth, builder.L2.Client, wasm) + require.Error(t, err, "We can't activate a classic stylus contract greater than the MaxCodeSize") + + // 2. Fragmented contract success + deployAndActivateFragmentedContract(t, builder.ctx, auth, builder.L2.Client, defaultDeployConfig()) +} + +// ArbOwner Limit Modification Tests + +func TestCantActivateRootContractBiggerThanMaxWasmSize(t *testing.T) { + builder, auth, cleanup := setupProgramTest(t, true, func(b *NodeBuilder) { + b.WithExtraArchs(allWasmTargets) + b.WithArbOSVersion(params.ArbosVersion_StylusContractLimit) + }) + defer cleanup() + + // Deploy manually to inject custom logic before activation + file := rustFile("storage") + name := strings.TrimSuffix(filepath.Base(file), filepath.Ext(file)) + fragments, sourceWasm, dictType := readFragmentedContractFile(t, file, 2) + auth.GasLimit = 32000000 + + addresses := make([]common.Address, 0, len(fragments)) + for i, fragment := range fragments { + addr := deployContract(t, builder.ctx, auth, builder.L2.Client, fragment) + addresses = append(addresses, addr) + colors.PrintGrey(name, ": fragment", i, addr.Hex()) + } + + // #nosec G115 + rootContract := constructRootContract(t, uint32(len(sourceWasm)), addresses, dictType) + rootAddress := deployContract(t, builder.ctx, auth, builder.L2.Client, rootContract) + + // Decrease limit + arbOwner, err := precompilesgen.NewArbOwner(types.ArbOwnerAddress, builder.L2.Client) + Require(t, err) + // #nosec G115 + tx, err := arbOwner.SetWasmMaxSize(&auth, uint32(len(sourceWasm)-1)) + Require(t, err) + _, err = EnsureTxSucceeded(builder.ctx, builder.L2.Client, tx) + Require(t, err) + + // Attempt activation + arbWasm, err := precompilesgen.NewArbWasm(types.ArbWasmAddress, builder.L2.Client) + Require(t, err) + auth.Value = oneEth + tx, err = arbWasm.ActivateProgram(&auth, rootAddress) + Require(t, err) + _, err = EnsureTxSucceeded(builder.ctx, builder.L2.Client, tx) + require.Error(t, err, "We can't activate a fragmented contract greater than the MaxWasmSize") +} + +func TestArbOwnerModifyingMaxFragmentCount(t *testing.T) { + builder, auth, cleanup := setupProgramTest(t, true, func(b *NodeBuilder) { + b.WithExtraArchs(allWasmTargets) + b.WithArbOSVersion(params.ArbosVersion_StylusContractLimit) + }) + defer cleanup() + + arbOwnerPublic, err := precompilesgen.NewArbOwnerPublic(types.ArbOwnerPublicAddress, builder.L2.Client) + Require(t, err) + arbOwner, err := precompilesgen.NewArbOwner(types.ArbOwnerAddress, builder.L2.Client) + Require(t, err) + callOpts := &bind.CallOpts{Context: builder.ctx} + + // Verify initial + count, err := arbOwnerPublic.GetMaxStylusContractFragments(callOpts) + Require(t, err) + require.Equal(t, uint16(2), count) + + // Change to 1 + tx, err := arbOwner.SetMaxStylusContractFragments(&auth, 1) + Require(t, err) + _, err = EnsureTxSucceeded(builder.ctx, builder.L2.Client, tx) + Require(t, err) + + count, err = arbOwnerPublic.GetMaxStylusContractFragments(callOpts) + Require(t, err) + require.Equal(t, uint16(1), count) + + // Change to 3 + tx, err = arbOwner.SetMaxStylusContractFragments(&auth, 3) + Require(t, err) + _, err = EnsureTxSucceeded(builder.ctx, builder.L2.Client, tx) + Require(t, err) + + count, err = arbOwnerPublic.GetMaxStylusContractFragments(callOpts) + Require(t, err) + require.Equal(t, uint16(3), count) +} + +func TestArbOwnerPublicReturnsCorrectMaxFragmentCount(t *testing.T) { + builder, _, cleanup := setupProgramTest(t, true, func(b *NodeBuilder) { + b.WithExtraArchs(allWasmTargets) + b.WithArbOSVersion(params.ArbosVersion_StylusContractLimit) + }) + defer cleanup() + + arbOwnerPublic, err := precompilesgen.NewArbOwnerPublic(types.ArbOwnerPublicAddress, builder.L2.Client) + Require(t, err) + + count, err := arbOwnerPublic.GetMaxStylusContractFragments(&bind.CallOpts{Context: builder.ctx}) + Require(t, err) + require.Equal(t, uint16(2), count) +} + +// Generic Runners for Limit Decrease Scenarios +// These generic functions handle the heavy lifting for Rebuild, Execute, Cache, and Deploy tests. +// They accept a `setLimitFunc` to toggle between testing MaxWasmSize and MaxFragmentCount. + +type limitSetter func(t *testing.T, ctx context.Context, auth *bind.TransactOpts, client *ethclient.Client) + +func runRebuildWasmStoreTest(t *testing.T, setLimit limitSetter) { + databaseEngine := rawdb.DBLeveldb + builder, auth, cleanup := setupProgramTest(t, true, func(b *NodeBuilder) { + b.WithExtraArchs(allWasmTargets) + b.WithArbOSVersion(params.ArbosVersion_StylusContractLimit) + b.WithDatabase(databaseEngine) + }) + ctx := builder.ctx + defer cleanup() + + rootAddress, _, _ := deployAndActivateFragmentedContract(t, ctx, auth, builder.L2.Client, defaultDeployConfig()) + + // Store value + zero := common.Hash{} + val := common.HexToHash("0x121233445566") + storeTx := builder.L2Info.PrepareTxTo("Owner", &rootAddress, builder.L2Info.TransferGas, nil, argsForStorageWrite(zero, val)) + Require(t, builder.L2.Client.SendTransaction(ctx, storeTx)) + _, err := EnsureTxSucceeded(ctx, builder.L2.Client, storeTx) + Require(t, err) + + // Decrease Limit + auth.GasLimit = 0 + auth.Value = nil + setLimit(t, ctx, &auth, builder.L2.Client) + + // Build 2nd Node + testDir := t.TempDir() + nodeBStack := testhelpers.CreateStackConfigForTest(testDir) + nodeBStack.DBEngine = databaseEngine + nodeB, cleanupB := builder.Build2ndNode(t, &SecondNodeParams{stackConfig: nodeBStack}) + + // Ensure tx succeeds on nodeB + _, err = EnsureTxSucceeded(ctx, nodeB.Client, storeTx) + Require(t, err) + + // Verify read + loadTx := builder.L2Info.PrepareTxTo("Owner", &rootAddress, builder.L2Info.TransferGas, nil, argsForStorageRead(zero)) + result, err := arbutil.SendTxAsCall(ctx, nodeB.Client, loadTx, builder.L2Info.GetAddress("Owner"), nil, true) + Require(t, err) + require.Equal(t, val, common.BytesToHash(result)) + + // Verify Wasm Store + wasmDb := nodeB.ExecNode.Backend.ArbInterface().BlockChain().StateCache().WasmStore() + checkWasmStoreContent(t, wasmDb, builder.execConfig.StylusTarget.WasmTargets(), 1) + + // Rebuild Test: Close, Delete Wasm, Reopen, Rebuild + cleanupB() + wasmPath := filepath.Join(testDir, nodeBStack.Name, "wasm") + dirContents, err := os.ReadDir(wasmPath) + Require(t, err) + require.NotEmpty(t, dirContents) + os.RemoveAll(wasmPath) + + nodeB, cleanupB = builder.Build2ndNode(t, &SecondNodeParams{stackConfig: nodeBStack}) + + // Verify empty before rebuild + wasmDbAfterDelete := nodeB.ExecNode.Backend.ArbInterface().BlockChain().StateCache().WasmStore() + storeMapAfterDelete, err := createMapFromDb(wasmDbAfterDelete) + Require(t, err) + require.Empty(t, storeMapAfterDelete) + + log.Info("starting rebuilding of wasm store") + execConfig := builder.execConfig + bc := nodeB.ExecNode.Backend.ArbInterface().BlockChain() + Require(t, gethexec.RebuildWasmStore(ctx, wasmDbAfterDelete, nodeB.ExecNode.ExecutionDB, execConfig.RPC.MaxRecreateStateDepth, &execConfig.StylusTarget, bc, common.Hash{}, bc.CurrentBlock().Hash())) + + wasmDbAfterRebuild := nodeB.ExecNode.Backend.ArbInterface().BlockChain().StateCache().WasmStore() + checkWasmStoreContent(t, wasmDbAfterRebuild, builder.execConfig.StylusTarget.WasmTargets(), 1) + cleanupB() +} + +func runExecuteWasmTest(t *testing.T, setLimit limitSetter, deleteWasm bool) { + databaseEngine := rawdb.DBLeveldb + builder, auth, cleanup := setupProgramTest(t, true, func(b *NodeBuilder) { + b.WithExtraArchs(allWasmTargets) + b.WithArbOSVersion(params.ArbosVersion_StylusContractLimit) + b.WithDatabase(databaseEngine) + }) + ctx := builder.ctx + defer cleanup() + + rootAddress, _, receipt := deployAndActivateFragmentedContract(t, ctx, auth, builder.L2.Client, defaultDeployConfig()) + + // Decrease Limit + auth.GasLimit = 0 + auth.Value = nil + setLimit(t, ctx, &auth, builder.L2.Client) + + if deleteWasm { + arbWasm, err := precompilesgen.NewArbWasm(types.ArbWasmAddress, builder.L2.Client) + Require(t, err) + l, err := arbWasm.ParseProgramActivated(*receipt.Logs[0]) + Require(t, err) + + wasmStore := builder.L2.ExecNode.Backend.ArbInterface().BlockChain().StateCache().WasmStore() + Require(t, deleteAnyKeysContainingModuleHash(wasmStore, l.ModuleHash)) + } + + // Execute Wasm + zero := common.Hash{} + val := common.HexToHash("0x121233445566") + storeTx := builder.L2Info.PrepareTxTo("Owner", &rootAddress, builder.L2Info.TransferGas, nil, argsForStorageWrite(zero, val)) + Require(t, builder.L2.Client.SendTransaction(ctx, storeTx)) + _, err := EnsureTxSucceeded(ctx, builder.L2.Client, storeTx) + Require(t, err) +} + +func runCacheProgramTest(t *testing.T, setLimit limitSetter) { + databaseEngine := rawdb.DBLeveldb + builder, auth, cleanup := setupProgramTest(t, true, func(b *NodeBuilder) { + b.WithExtraArchs(allWasmTargets) + b.WithArbOSVersion(params.ArbosVersion_StylusContractLimit) + b.WithDatabase(databaseEngine) + }) + ctx := builder.ctx + defer cleanup() + + rootAddress, _, receipt := deployAndActivateFragmentedContract(t, ctx, auth, builder.L2.Client, defaultDeployConfig()) + + // Decrease Limit + auth.GasLimit = 0 + auth.Value = nil + setLimit(t, ctx, &auth, builder.L2.Client) + + // Identify module hash and delete from store + arbWasm, err := precompilesgen.NewArbWasm(types.ArbWasmAddress, builder.L2.Client) + Require(t, err) + l, err := arbWasm.ParseProgramActivated(*receipt.Logs[0]) + Require(t, err) + wasmStore := builder.L2.ExecNode.Backend.ArbInterface().BlockChain().StateCache().WasmStore() + Require(t, deleteAnyKeysContainingModuleHash(wasmStore, l.ModuleHash)) + + // Cache + arbWasmCache, err := precompilesgen.NewArbWasmCache(types.ArbWasmCacheAddress, builder.L2.Client) + Require(t, err) + _, err = arbWasmCache.CacheProgram(&auth, rootAddress) + Require(t, err) +} + +func runDeployAfterLimitTest(t *testing.T, setLimit limitSetter) { + databaseEngine := rawdb.DBLeveldb + builder, auth, cleanup := setupProgramTest(t, true, func(b *NodeBuilder) { + b.WithExtraArchs(allWasmTargets) + b.WithArbOSVersion(params.ArbosVersion_StylusContractLimit) + b.WithDatabase(databaseEngine) + }) + ctx := builder.ctx + defer cleanup() + + // Initial Deploy + rootAddress, _, _ := deployAndActivateFragmentedContract(t, ctx, auth, builder.L2.Client, defaultDeployConfig()) + + // Write + zero := common.Hash{} + val := common.HexToHash("0x121233445566") + storeTx := builder.L2Info.PrepareTxTo("Owner", &rootAddress, builder.L2Info.TransferGas, nil, argsForStorageWrite(zero, val)) + Require(t, builder.L2.Client.SendTransaction(ctx, storeTx)) + _, err := EnsureTxSucceeded(ctx, builder.L2.Client, storeTx) + Require(t, err) + + // Decrease Limit + auth.GasLimit = 0 + auth.Value = nil + setLimit(t, ctx, &auth, builder.L2.Client) + + // Second Deploy (should fail) + failCfg := defaultDeployConfig() + failCfg.expectActivation = false + deployAndActivateFragmentedContract(t, ctx, auth, builder.L2.Client, failCfg) +} + +// Specific Implementations of Limit Tests + +// Shared Limit Setters +func setWasmLimitTo10k(t *testing.T, ctx context.Context, auth *bind.TransactOpts, client *ethclient.Client) { + arbOwner, err := precompilesgen.NewArbOwner(types.ArbOwnerAddress, client) + Require(t, err) + tx, err := arbOwner.SetWasmMaxSize(auth, 10000) + Require(t, err) + _, err = EnsureTxSucceeded(ctx, client, tx) + Require(t, err) +} + +func setFragmentLimitTo1(t *testing.T, ctx context.Context, auth *bind.TransactOpts, client *ethclient.Client) { + arbOwner, err := precompilesgen.NewArbOwner(types.ArbOwnerAddress, client) + Require(t, err) + tx, err := arbOwner.SetMaxStylusContractFragments(auth, 1) + Require(t, err) + _, err = EnsureTxSucceeded(ctx, client, tx) + Require(t, err) +} + +// Tests: Decrease Max Wasm Size +func TestRebuildWasmStoreWithDecreasedMaxWasmSize(t *testing.T) { + runRebuildWasmStoreTest(t, setWasmLimitTo10k) +} +func TestExecuteWasmWithDecreasedMaxWasmSizeWasmPresent(t *testing.T) { + runExecuteWasmTest(t, setWasmLimitTo10k, false) +} +func TestExecuteWasmWithDecreasedMaxWasmSizeRecoverWasm(t *testing.T) { + runExecuteWasmTest(t, setWasmLimitTo10k, true) +} +func TestCacheProgramWithDecreasedMaxWasmSizeRecoverWasm(t *testing.T) { + runCacheProgramTest(t, setWasmLimitTo10k) +} +func TestDeployingContractBeforeAndAfterDecreaseMaxWasmSize(t *testing.T) { + runDeployAfterLimitTest(t, setWasmLimitTo10k) +} + +// Tests: Decrease Max Fragment Count +func TestRebuildWasmStoreWithDecreasedMaxFragmentCount(t *testing.T) { + runRebuildWasmStoreTest(t, setFragmentLimitTo1) +} +func TestExecuteWasmWithDecreasedMaxFragmentCountWasmPresent(t *testing.T) { + runExecuteWasmTest(t, setFragmentLimitTo1, false) +} +func TestExecuteWasmWithDecreasedMaxFragmentCountRecoverWasm(t *testing.T) { + runExecuteWasmTest(t, setFragmentLimitTo1, true) +} +func TestCacheProgramWithDecreasedMaxFragmentCountRecoverWasm(t *testing.T) { + runCacheProgramTest(t, setFragmentLimitTo1) +} +func TestDeployingContractBeforeAndAfterDecreaseMaxFragmentCount(t *testing.T) { + runDeployAfterLimitTest(t, setFragmentLimitTo1) +} + +// Test that fragmented contracts fail on ArbOS versions before the feature is active + +func TestFragmentedContractFailsOnArbOS50(t *testing.T) { + builder, auth, cleanup := setupProgramTest(t, true, func(b *NodeBuilder) { + b.WithExtraArchs(allWasmTargets) + b.WithArbOSVersion(params.ArbosVersion_50) + }) + defer cleanup() + + fragments, _, _ := readFragmentedContractFile(t, rustFile("storage"), 2) + require.Len(t, fragments, 2) + + auth.GasLimit = 32_000_000 + _, err := deployContractForwardError(t, builder.ctx, auth, builder.L2.Client, fragments[0]) + require.ErrorContains(t, err, vm.ErrInvalidCode.Error()) +} + +func TestArbOwnerPublicGetMaxFragmentCountFailsOnArbOS50(t *testing.T) { + builder, _, cleanup := setupProgramTest(t, true, func(b *NodeBuilder) { + b.WithExtraArchs(allWasmTargets) + b.WithArbOSVersion(params.ArbosVersion_50) + }) + defer cleanup() + + arbOwnerPublic, err := precompilesgen.NewArbOwnerPublic(types.ArbOwnerPublicAddress, builder.L2.Client) + Require(t, err) + + _, err = arbOwnerPublic.GetMaxStylusContractFragments(&bind.CallOpts{Context: builder.ctx}) + require.Error(t, err, "GetMaxStylusContractFragments should fail on ArbOS 50 because the feature is not yet active") +} + +func TestArbOwnerSetMaxFragmentCountFailsOnArbOS50(t *testing.T) { + builder, auth, cleanup := setupProgramTest(t, true, func(b *NodeBuilder) { + b.WithExtraArchs(allWasmTargets) + b.WithArbOSVersion(params.ArbosVersion_50) + }) + defer cleanup() + + arbOwner, err := precompilesgen.NewArbOwner(types.ArbOwnerAddress, builder.L2.Client) + Require(t, err) + + tx, err := arbOwner.SetMaxStylusContractFragments(&auth, 10) + if err == nil { + _, err = EnsureTxSucceeded(builder.ctx, builder.L2.Client, tx) + } + require.Error(t, err, "SetMaxStylusContractFragments should fail on ArbOS 50") +} + +// Utils + +// readFragmentedContractFile reads, compiles, compresses, and fragments a contract. +func readFragmentedContractFile(t *testing.T, file string, fragmentCount uint16) ([][]byte, []byte, arbcompress.Dictionary) { + t.Helper() + name := strings.TrimSuffix(filepath.Base(file), filepath.Ext(file)) + source, err := os.ReadFile(file) + Require(t, err) + + // #nosec G115 + randDict := arbcompress.Dictionary((len(file) + len(t.Name())) % 2) + + wasmSource, err := programs.Wat2Wasm(source) + Require(t, err) + + fragments := make([][]byte, 0, fragmentCount) + if fragmentCount == 0 { + return fragments, wasmSource, randDict + } + + compressedWasm, err := arbcompress.Compress(wasmSource, arbcompress.LEVEL_WELL, randDict) + Require(t, err) + + toKb := func(data []byte) float64 { return float64(len(data)) / 1024.0 } + colors.PrintGrey(fmt.Sprintf("%v: len %.2fK vs %.2fK", name, toKb(compressedWasm), toKb(wasmSource))) + + prefix := state.NewStylusFragmentPrefix() + payloadLen := len(compressedWasm) + chunkSize := (payloadLen + int(fragmentCount) - 1) / int(fragmentCount) + + for i := 0; i < int(fragmentCount); i++ { + start := i * chunkSize + if start >= payloadLen { + break + } + end := start + chunkSize + if end > payloadLen { + end = payloadLen + } + frag := make([]byte, 0, len(prefix)+(end-start)) + frag = append(frag, prefix...) + frag = append(frag, compressedWasm[start:end]...) + fragments = append(fragments, frag) + } + + return fragments, wasmSource, randDict +} + +func constructRootContract( + t *testing.T, + dictionaryTypeUncompressedWasmSize uint32, + addresses []common.Address, + dictionaryType arbcompress.Dictionary, +) []byte { + t.Helper() + // prefix 3 bytes + dict 1 byte + length 4 bytes + len(address) * 20 bytes + contract := make([]byte, 0, 3+1+4+len(addresses)*common.AddressLength) + contract = append(contract, state.NewStylusRootPrefix(byte(dictionaryType))...) + var sizeBuf [4]byte + binary.BigEndian.PutUint32(sizeBuf[:], dictionaryTypeUncompressedWasmSize) + contract = append(contract, sizeBuf[:]...) + for _, addr := range addresses { + contract = append(contract, addr.Bytes()...) + } + return contract +} + +func deleteAnyKeysContainingModuleHash(db ethdb.KeyValueStore, moduleHash common.Hash) error { + it := db.NewIterator(nil, nil) + defer it.Release() + + batch := db.NewBatch() + mh := moduleHash.Bytes() + + for it.Next() { + k := it.Key() + if bytes.Contains(k, mh) { + kk := append([]byte(nil), k...) + err := batch.Delete(kk) + if err != nil { + return err + } + } + } + if err := it.Error(); err != nil { + return err + } + return batch.Write() +}