Skip to content
2 changes: 1 addition & 1 deletion arbcompress/native.go
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
9 changes: 7 additions & 2 deletions arbos/arbosState/arbosstate.go
Original file line number Diff line number Diff line change
Expand Up @@ -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:
Expand Down
12 changes: 6 additions & 6 deletions arbos/programs/native.go
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand All @@ -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)
Expand All @@ -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)
Expand Down Expand Up @@ -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 {
Expand Down Expand Up @@ -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 {
Expand Down
47 changes: 31 additions & 16 deletions arbos/programs/params.go
Original file line number Diff line number Diff line change
Expand Up @@ -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).

Expand Down Expand Up @@ -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.
Expand Down Expand Up @@ -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))
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

let's use uint8 and only take 1.
255 fragments should be enough, and we're very close to the 32byte limit.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ok here is the PR for that #4285

} else {
stylusParams.MaxFragmentCount = 0
}
return stylusParams, nil
}

Expand Down Expand Up @@ -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 {
Expand Down Expand Up @@ -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
}
Expand All @@ -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()
}
100 changes: 87 additions & 13 deletions arbos/programs/programs.go
Original file line number Diff line number Diff line change
Expand Up @@ -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
}
Expand Down Expand Up @@ -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,
Expand Down Expand Up @@ -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) {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

That's a style question, but I don't like this function name. It's visible/theoretically callable from the entire module and "handle" doesn't mean anything for that context. Suggest something like "getWasmFromClassicStylus" or something similar.

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) {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

same

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.
Expand Down
6 changes: 3 additions & 3 deletions arbos/programs/wasm.go
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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),
Expand Down
2 changes: 1 addition & 1 deletion arbos/programs/wasmstorehelper.go
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down
2 changes: 2 additions & 0 deletions changelog/kolbyml-increase-stylus-smart-contract-size.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
### Added
- Increase Stylus smart contract size limit via merge-on-activate
2 changes: 1 addition & 1 deletion contracts-local/src/precompiles
2 changes: 1 addition & 1 deletion execution/gethexec/wasmstorerebuilder.go
Original file line number Diff line number Diff line change
Expand Up @@ -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)
}
Expand Down
7 changes: 6 additions & 1 deletion precompiles/ArbOwner.go
Original file line number Diff line number Diff line change
Expand Up @@ -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()
}
8 changes: 5 additions & 3 deletions precompiles/ArbOwnerPublic.go
Original file line number Diff line number Diff line change
Expand Up @@ -4,8 +4,6 @@
package precompiles

import (
"errors"

"github.com/ethereum/go-ethereum/common"
"github.com/ethereum/go-ethereum/params"
)
Expand Down Expand Up @@ -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
}
3 changes: 2 additions & 1 deletion precompiles/precompile.go
Original file line number Diff line number Diff line change
Expand Up @@ -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))
Expand Down Expand Up @@ -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
Expand Down
Loading
Loading