diff --git a/arbitrum/recordingdb.go b/arbitrum/recordingdb.go index 30cafe5a77..6da71a497e 100644 --- a/arbitrum/recordingdb.go +++ b/arbitrum/recordingdb.go @@ -55,6 +55,12 @@ func (db *RecordingKV) Get(key []byte) ([]byte, error) { // Retrieving code copy(hash[:], key[len(rawdb.CodePrefix):]) res, err = db.diskDb.Get(key) + } else if ok, _ := rawdb.IsActivatedAsmKey(key); ok { + // Arbitrum: the asm is non-consensus + return db.diskDb.Get(key) + } else if ok, _ := rawdb.IsActivatedModuleKey(key); ok { + // Arbitrum: the module is non-consensus (only its hash is) + return db.diskDb.Get(key) } else { err = fmt.Errorf("recording KV attempted to access non-hash key %v", hex.EncodeToString(key)) } @@ -278,6 +284,7 @@ func (r *RecordingDatabase) PrepareRecording(ctx context.Context, lastBlockHeade if err != nil { return nil, nil, nil, fmt.Errorf("failed to create recordingStateDb: %w", err) } + recordingStateDb.StartRecording() var recordingChainContext *RecordingChainContext if lastBlockHeader != nil { if !lastBlockHeader.Number.IsUint64() { diff --git a/cmd/evm/internal/t8ntool/tracewriter.go b/cmd/evm/internal/t8ntool/tracewriter.go index 93fdf551eb..0dffe14b86 100644 --- a/cmd/evm/internal/t8ntool/tracewriter.go +++ b/cmd/evm/internal/t8ntool/tracewriter.go @@ -84,3 +84,5 @@ func (t *traceWriter) CaptureArbitrumTransfer(env *vm.EVM, from, to *common.Addr func (t *traceWriter) CaptureArbitrumStorageGet(key common.Hash, depth int, before bool) {} func (t *traceWriter) CaptureArbitrumStorageSet(key, value common.Hash, depth int, before bool) {} + +func (t *traceWriter) CaptureStylusHostio(name string, args, outs []byte, startInk, endInk uint64) {} diff --git a/cmd/geth/logging_test.go b/cmd/geth/logging_test.go index b5ce03f4b8..e56c679dae 100644 --- a/cmd/geth/logging_test.go +++ b/cmd/geth/logging_test.go @@ -59,6 +59,8 @@ func censor(input string, start, end int) string { } func TestLogging(t *testing.T) { + t.Skip("Skipping cmd logging test due to color stripping") + t.Parallel() testConsoleLogging(t, "terminal", 6, 24) testConsoleLogging(t, "logfmt", 2, 26) diff --git a/common/lru/basiclru_arbitrum.go b/common/lru/basiclru_arbitrum.go new file mode 100644 index 0000000000..aa8d3fdf87 --- /dev/null +++ b/common/lru/basiclru_arbitrum.go @@ -0,0 +1,22 @@ +// Copyright 2022 The go-ethereum Authors +// This file is part of the go-ethereum library. +// +// The go-ethereum library is free software: you can redistribute it and/or modify +// it under the terms of the GNU Lesser General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// The go-ethereum library is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU Lesser General Public License for more details. +// +// You should have received a copy of the GNU Lesser General Public License +// along with the go-ethereum library. If not, see . + +// Package lru implements generically-typed LRU caches. +package lru + +func (c *BasicLRU[K, V]) Capacity() int { + return c.cap +} diff --git a/common/types_arbitrum.go b/common/types_arbitrum.go new file mode 100644 index 0000000000..c63ca76158 --- /dev/null +++ b/common/types_arbitrum.go @@ -0,0 +1,64 @@ +// Copyright 2015 The go-ethereum Authors +// This file is part of the go-ethereum library. +// +// The go-ethereum library is free software: you can redistribute it and/or modify +// it under the terms of the GNU Lesser General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// The go-ethereum library is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU Lesser General Public License for more details. +// +// You should have received a copy of the GNU Lesser General Public License +// along with the go-ethereum library. If not, see . + +package common + +type Signed interface { + ~int | ~int8 | ~int16 | ~int32 | ~int64 +} + +type Unsigned interface { + ~uint | ~uint8 | ~uint16 | ~uint32 | ~uint64 | ~uintptr +} + +type Integer interface { + Signed | Unsigned +} + +type Float interface { + ~float32 | ~float64 +} + +// Ordered is anything that implements comparison operators such as `<` and `>`. +// Unfortunately, that doesn't include big ints. +type Ordered interface { + Integer | Float +} + +// MinInt the minimum of two ints +func MinInt[T Ordered](value, ceiling T) T { + if value > ceiling { + return ceiling + } + return value +} + +// MaxInt the maximum of two ints +func MaxInt[T Ordered](value, floor T) T { + if value < floor { + return floor + } + return value +} + +// SaturatingUAdd add two integers without overflow +func SaturatingUAdd[T Unsigned](a, b T) T { + sum := a + b + if sum < a || sum < b { + sum = ^T(0) + } + return sum +} diff --git a/core/rawdb/accessors_state_arbitrum.go b/core/rawdb/accessors_state_arbitrum.go new file mode 100644 index 0000000000..32ccb1c94d --- /dev/null +++ b/core/rawdb/accessors_state_arbitrum.go @@ -0,0 +1,36 @@ +// Copyright 2020 The go-ethereum Authors +// This file is part of the go-ethereum library. +// +// The go-ethereum library is free software: you can redistribute it and/or modify +// it under the terms of the GNU Lesser General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// The go-ethereum library is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU Lesser General Public License for more details. +// +// You should have received a copy of the GNU Lesser General Public License +// along with the go-ethereum library. If not, see . + +package rawdb + +import ( + "github.com/ethereum/go-ethereum/common" + "github.com/ethereum/go-ethereum/ethdb" + "github.com/ethereum/go-ethereum/log" +) + +// Stores the activated asm and module for a given codeHash +func WriteActivation(db ethdb.KeyValueWriter, moduleHash common.Hash, asm, module []byte) { + key := ActivatedAsmKey(moduleHash) + if err := db.Put(key[:], asm); err != nil { + log.Crit("Failed to store activated wasm asm", "err", err) + } + + key = ActivatedModuleKey(moduleHash) + if err := db.Put(key[:], module); err != nil { + log.Crit("Failed to store activated wasm module", "err", err) + } +} diff --git a/core/rawdb/fileutil.go b/core/rawdb/fileutil.go index 5bdec39404..50b9bfb2f0 100644 --- a/core/rawdb/fileutil.go +++ b/core/rawdb/fileutil.go @@ -14,8 +14,8 @@ // You should have received a copy of the GNU Lesser General Public License // along with the go-ethereum library. If not, see . -//go:build !js -// +build !js +//go:build !wasm +// +build !wasm package rawdb diff --git a/core/rawdb/fileutil_mock.go b/core/rawdb/fileutil_mock.go index a411f5fb0f..b8028fadbf 100644 --- a/core/rawdb/fileutil_mock.go +++ b/core/rawdb/fileutil_mock.go @@ -14,8 +14,8 @@ // You should have received a copy of the GNU Lesser General Public License // along with the go-ethereum library. If not, see . -//go:build js -// +build js +//go:build wasm +// +build wasm package rawdb diff --git a/core/rawdb/schema_arbitrum.go b/core/rawdb/schema_arbitrum.go new file mode 100644 index 0000000000..72a3f755bb --- /dev/null +++ b/core/rawdb/schema_arbitrum.go @@ -0,0 +1,66 @@ +// Copyright 2018 The go-ethereum Authors +// This file is part of the go-ethereum library. +// +// The go-ethereum library is free software: you can redistribute it and/or modify +// it under the terms of the GNU Lesser General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// The go-ethereum library is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU Lesser General Public License for more details. +// +// You should have received a copy of the GNU Lesser General Public License +// along with the go-ethereum library. If not, see . + +// Package rawdb contains a collection of low level database accessors. + +package rawdb + +import ( + "bytes" + + "github.com/ethereum/go-ethereum/common" +) + +var ( + activatedAsmPrefix = []byte{0x00, 'w', 'a'} // (prefix, moduleHash) -> stylus asm + activatedModulePrefix = []byte{0x00, 'w', 'm'} // (prefix, moduleHash) -> stylus module +) + +// WasmKeyLen = CompiledWasmCodePrefix + moduleHash +const WasmKeyLen = 3 + 32 + +type WasmKey = [WasmKeyLen]byte + +func ActivatedAsmKey(moduleHash common.Hash) WasmKey { + return newWasmKey(activatedAsmPrefix, moduleHash) +} + +func ActivatedModuleKey(moduleHash common.Hash) WasmKey { + return newWasmKey(activatedModulePrefix, moduleHash) +} + +// key = prefix + moduleHash +func newWasmKey(prefix []byte, moduleHash common.Hash) WasmKey { + var key WasmKey + copy(key[:3], prefix) + copy(key[3:], moduleHash[:]) + return key +} + +func IsActivatedAsmKey(key []byte) (bool, common.Hash) { + return extractWasmKey(activatedAsmPrefix, key) +} + +func IsActivatedModuleKey(key []byte) (bool, common.Hash) { + return extractWasmKey(activatedModulePrefix, key) +} + +func extractWasmKey(prefix, key []byte) (bool, common.Hash) { + if !bytes.HasPrefix(key, prefix) || len(key) != WasmKeyLen { + return false, common.Hash{} + } + return true, common.BytesToHash(key[len(prefix):]) +} diff --git a/core/state/database.go b/core/state/database.go index fa941611d7..48022d0e99 100644 --- a/core/state/database.go +++ b/core/state/database.go @@ -33,6 +33,9 @@ import ( ) const ( + // Arbitrum: Cache size granted for caching clean compiled wasm code. + activatedWasmCacheSize = 64 * 1024 * 1024 + // Number of codehash->size associations to keep. codeSizeCacheSize = 100000 @@ -48,6 +51,10 @@ const ( // Database wraps access to tries and contract code. type Database interface { + // Arbitrum: Read activated Stylus contracts + ActivatedAsm(moduleHash common.Hash) (asm []byte, err error) + ActivatedModule(moduleHash common.Hash) (module []byte, err error) + // OpenTrie opens the main account trie. OpenTrie(root common.Hash) (Trie, error) @@ -152,6 +159,10 @@ func NewDatabase(db ethdb.Database) Database { // large memory cache. func NewDatabaseWithConfig(db ethdb.Database, config *trie.Config) Database { cdb := &cachingDB{ + // Arbitrum only + activatedAsmCache: lru.NewSizeConstrainedCache[common.Hash, []byte](activatedWasmCacheSize), + activatedModuleCache: lru.NewSizeConstrainedCache[common.Hash, []byte](activatedWasmCacheSize), + disk: db, codeSizeCache: lru.NewCache[common.Hash, int](codeSizeCacheSize), codeCache: lru.NewSizeConstrainedCache[common.Hash, []byte](codeCacheSize), @@ -163,6 +174,10 @@ func NewDatabaseWithConfig(db ethdb.Database, config *trie.Config) Database { // NewDatabaseWithNodeDB creates a state database with an already initialized node database. func NewDatabaseWithNodeDB(db ethdb.Database, triedb *trie.Database) Database { cdb := &cachingDB{ + // Arbitrum only + activatedAsmCache: lru.NewSizeConstrainedCache[common.Hash, []byte](activatedWasmCacheSize), + activatedModuleCache: lru.NewSizeConstrainedCache[common.Hash, []byte](activatedWasmCacheSize), + disk: db, codeSizeCache: lru.NewCache[common.Hash, int](codeSizeCacheSize), codeCache: lru.NewSizeConstrainedCache[common.Hash, []byte](codeCacheSize), @@ -172,6 +187,10 @@ func NewDatabaseWithNodeDB(db ethdb.Database, triedb *trie.Database) Database { } type cachingDB struct { + // Arbitrum + activatedAsmCache *lru.SizeConstrainedCache[common.Hash, []byte] + activatedModuleCache *lru.SizeConstrainedCache[common.Hash, []byte] + disk ethdb.KeyValueStore codeSizeCache *lru.Cache[common.Hash, int] codeCache *lru.SizeConstrainedCache[common.Hash, []byte] diff --git a/core/state/database_arbitrum.go b/core/state/database_arbitrum.go new file mode 100644 index 0000000000..6e545eb0a0 --- /dev/null +++ b/core/state/database_arbitrum.go @@ -0,0 +1,40 @@ +package state + +import ( + "errors" + + "github.com/ethereum/go-ethereum/common" + "github.com/ethereum/go-ethereum/core/rawdb" +) + +func (db *cachingDB) ActivatedAsm(moduleHash common.Hash) ([]byte, error) { + if asm, _ := db.activatedAsmCache.Get(moduleHash); len(asm) > 0 { + return asm, nil + } + wasmKey := rawdb.ActivatedAsmKey(moduleHash) + asm, err := db.disk.Get(wasmKey[:]) + if err != nil { + return nil, err + } + if len(asm) > 0 { + db.activatedAsmCache.Add(moduleHash, asm) + return asm, nil + } + return nil, errors.New("not found") +} + +func (db *cachingDB) ActivatedModule(moduleHash common.Hash) ([]byte, error) { + if module, _ := db.activatedModuleCache.Get(moduleHash); len(module) > 0 { + return module, nil + } + wasmKey := rawdb.ActivatedModuleKey(moduleHash) + module, err := db.disk.Get(wasmKey[:]) + if err != nil { + return nil, err + } + if len(module) > 0 { + db.activatedModuleCache.Add(moduleHash, module) + return module, nil + } + return nil, errors.New("not found") +} diff --git a/core/state/journal_arbitrum.go b/core/state/journal_arbitrum.go new file mode 100644 index 0000000000..804a308ec2 --- /dev/null +++ b/core/state/journal_arbitrum.go @@ -0,0 +1,50 @@ +package state + +import ( + "github.com/ethereum/go-ethereum/common" +) + +type wasmActivation struct { + moduleHash common.Hash +} + +func (ch wasmActivation) revert(s *StateDB) { + delete(s.arbExtraData.activatedWasms, ch.moduleHash) +} + +func (ch wasmActivation) dirtied() *common.Address { + return nil +} + +// Updates the Rust-side recent program cache +var CacheWasmRust func(asm []byte, moduleHash common.Hash, version uint16, debug bool) = func([]byte, common.Hash, uint16, bool) {} +var EvictWasmRust func(moduleHash common.Hash, version uint16, debug bool) = func(common.Hash, uint16, bool) {} + +type CacheWasm struct { + ModuleHash common.Hash + Version uint16 + Debug bool +} + +func (ch CacheWasm) revert(s *StateDB) { + EvictWasmRust(ch.ModuleHash, ch.Version, ch.Debug) +} + +func (ch CacheWasm) dirtied() *common.Address { + return nil +} + +type EvictWasm struct { + ModuleHash common.Hash + Version uint16 + Debug bool +} + +func (ch EvictWasm) revert(s *StateDB) { + asm := s.GetActivatedAsm(ch.ModuleHash) // only happens in native mode + CacheWasmRust(asm, ch.ModuleHash, ch.Version, ch.Debug) +} + +func (ch EvictWasm) dirtied() *common.Address { + return nil +} diff --git a/core/state/snapshot/conversion.go b/core/state/snapshot/conversion.go index 681be7ebc0..4352b3321e 100644 --- a/core/state/snapshot/conversion.go +++ b/core/state/snapshot/conversion.go @@ -80,6 +80,8 @@ func GenerateTrie(snaptree *Tree, root common.Hash, src ethdb.Database, dst ethd return common.Hash{}, errors.New("failed to read contract code") } rawdb.WriteCode(dst, codeHash, code) + + // TODO: How do we migrate the compiled wasm code } // Then migrate all storage trie nodes into the tmp db. storageIt, err := snaptree.StorageIterator(root, accountHash, common.Hash{}) diff --git a/core/state/statedb.go b/core/state/statedb.go index 8acd87633e..28c6aeb2c2 100644 --- a/core/state/statedb.go +++ b/core/state/statedb.go @@ -47,6 +47,7 @@ const ( type revision struct { id int journalIndex int + // Arbitrum: track the total balance change across all accounts unexpectedBalanceDelta *big.Int } @@ -157,6 +158,10 @@ func New(root common.Hash, db Database, snaps *snapshot.Tree) (*StateDB, error) sdb := &StateDB{ arbExtraData: &ArbitrumExtraData{ unexpectedBalanceDelta: new(big.Int), + openWasmPages: 0, + everWasmPages: 0, + activatedWasms: make(map[common.Hash]*ActivatedWasm), + recentWasms: NewRecentWasms(), }, db: db, @@ -184,15 +189,6 @@ func New(root common.Hash, db Database, snaps *snapshot.Tree) (*StateDB, error) return sdb, nil } -func NewDeterministic(root common.Hash, db Database) (*StateDB, error) { - sdb, err := New(root, db, nil) - if err != nil { - return nil, err - } - sdb.deterministic = true - return sdb, nil -} - // StartPrefetcher initializes a new trie prefetcher to pull in nodes from the // state trie concurrently while the state is mutated so that when we reach the // commit phase, most of the needed data is already hot. @@ -726,6 +722,10 @@ func (s *StateDB) Copy() *StateDB { state := &StateDB{ arbExtraData: &ArbitrumExtraData{ unexpectedBalanceDelta: new(big.Int).Set(s.arbExtraData.unexpectedBalanceDelta), + activatedWasms: make(map[common.Hash]*ActivatedWasm, len(s.arbExtraData.activatedWasms)), + recentWasms: s.arbExtraData.recentWasms.Copy(), + openWasmPages: s.arbExtraData.openWasmPages, + everWasmPages: s.arbExtraData.everWasmPages, }, db: s.db, @@ -818,6 +818,18 @@ func (s *StateDB) Copy() *StateDB { state.accessList = s.accessList.Copy() state.transientStorage = s.transientStorage.Copy() + // Arbitrum: copy wasm calls and activated WASMs + if s.arbExtraData.userWasms != nil { + state.arbExtraData.userWasms = make(UserWasms, len(s.arbExtraData.userWasms)) + for call, wasm := range s.arbExtraData.userWasms { + state.arbExtraData.userWasms[call] = wasm + } + } + for moduleHash, info := range s.arbExtraData.activatedWasms { + // It's fine to skip a deep copy since activations are immutable. + state.arbExtraData.activatedWasms[moduleHash] = info + } + // If there's a prefetcher running, make an inactive copy of it that can // only access data but does not actively preload (since the user will not // know that they need to explicitly terminate an active copy). @@ -991,6 +1003,10 @@ func (s *StateDB) IntermediateRoot(deleteEmptyObjects bool) common.Hash { func (s *StateDB) SetTxContext(thash common.Hash, ti int) { s.thash = thash s.txIndex = ti + + // Arbitrum: clear memory charging state for new tx + s.arbExtraData.openWasmPages = 0 + s.arbExtraData.everWasmPages = 0 } func (s *StateDB) clearJournalAndRefund() { @@ -1267,6 +1283,15 @@ func (s *StateDB) Commit(block uint64, deleteEmptyObjects bool) (common.Hash, er storageTrieNodesDeleted += deleted } } + + // Arbitrum: write Stylus programs to disk + for moduleHash, info := range s.arbExtraData.activatedWasms { + rawdb.WriteActivation(codeWriter, moduleHash, info.Asm, info.Module) + } + if len(s.arbExtraData.activatedWasms) > 0 { + s.arbExtraData.activatedWasms = make(map[common.Hash]*ActivatedWasm) + } + if codeWriter.ValueSize() > 0 { if err := codeWriter.Write(); err != nil { log.Crit("Failed to commit dirty codes", "error", err) diff --git a/core/state/statedb_arbitrum.go b/core/state/statedb_arbitrum.go index ce4b19b7ae..10beeed3b3 100644 --- a/core/state/statedb_arbitrum.go +++ b/core/state/statedb_arbitrum.go @@ -18,18 +18,145 @@ package state import ( + "bytes" + "fmt" "math/big" + + "errors" "runtime" "github.com/ethereum/go-ethereum/common" + "github.com/ethereum/go-ethereum/common/lru" "github.com/ethereum/go-ethereum/core/types" "github.com/ethereum/go-ethereum/rlp" "github.com/ethereum/go-ethereum/trie" ) +var ( + // Defines prefix bytes for Stylus WASM program bytecode + // when deployed on-chain via a user-initiated transaction. + // These byte prefixes are meant to conflict with the L1 contract EOF + // validation rules so they can be sufficiently differentiated from EVM bytecode. + // This allows us to store WASM programs as code in the stateDB side-by-side + // with EVM contracts, but match against these prefix bytes when loading code + // to execute the WASMs through Stylus rather than the EVM. + stylusEOFMagic = byte(0xEF) + stylusEOFMagicSuffix = byte(0xF0) + stylusEOFVersion = byte(0x00) + // 4th byte specifies the Stylus dictionary used during compression + + StylusDiscriminant = []byte{stylusEOFMagic, stylusEOFMagicSuffix, stylusEOFVersion} +) + +type ActivatedWasm struct { + Asm []byte + Module []byte +} + +// checks if a valid Stylus prefix is present +func IsStylusProgram(b []byte) bool { + if len(b) < len(StylusDiscriminant)+1 { + return false + } + return bytes.Equal(b[:3], StylusDiscriminant) +} + +// strips the Stylus header from a contract, returning the dictionary used +func StripStylusPrefix(b []byte) ([]byte, byte, error) { + if !IsStylusProgram(b) { + return nil, 0, errors.New("specified bytecode is not a Stylus program") + } + return b[4:], b[3], nil +} + +// creates a new Stylus prefix from the given dictionary byte +func NewStylusPrefix(dictionary byte) []byte { + prefix := bytes.Clone(StylusDiscriminant) + return append(prefix, dictionary) +} + +func (s *StateDB) ActivateWasm(moduleHash common.Hash, asm, module []byte) { + _, exists := s.arbExtraData.activatedWasms[moduleHash] + if exists { + return + } + s.arbExtraData.activatedWasms[moduleHash] = &ActivatedWasm{ + Asm: asm, + Module: module, + } + s.journal.append(wasmActivation{ + moduleHash: moduleHash, + }) +} + +func (s *StateDB) GetActivatedAsm(moduleHash common.Hash) []byte { + info, exists := s.arbExtraData.activatedWasms[moduleHash] + if exists { + return info.Asm + } + asm, err := s.db.ActivatedAsm(moduleHash) + if err != nil { + s.setError(fmt.Errorf("failed to load asm for %x: %v", moduleHash, err)) + } + return asm +} + +func (s *StateDB) GetActivatedModule(moduleHash common.Hash) []byte { + info, exists := s.arbExtraData.activatedWasms[moduleHash] + if exists { + return info.Module + } + code, err := s.db.ActivatedModule(moduleHash) + if err != nil { + s.setError(fmt.Errorf("failed to load module for %x: %v", moduleHash, err)) + } + return code +} + +func (s *StateDB) GetStylusPages() (uint16, uint16) { + return s.arbExtraData.openWasmPages, s.arbExtraData.everWasmPages +} + +func (s *StateDB) GetStylusPagesOpen() uint16 { + return s.arbExtraData.openWasmPages +} + +func (s *StateDB) SetStylusPagesOpen(open uint16) { + s.arbExtraData.openWasmPages = open +} + +// Tracks that `new` additional pages have been opened, returning the previous counts +func (s *StateDB) AddStylusPages(new uint16) (uint16, uint16) { + open, ever := s.GetStylusPages() + s.arbExtraData.openWasmPages = common.SaturatingUAdd(open, new) + s.arbExtraData.everWasmPages = common.MaxInt(ever, s.arbExtraData.openWasmPages) + return open, ever +} + +func (s *StateDB) AddStylusPagesEver(new uint16) { + s.arbExtraData.everWasmPages = common.SaturatingUAdd(s.arbExtraData.everWasmPages, new) +} + +func NewDeterministic(root common.Hash, db Database) (*StateDB, error) { + sdb, err := New(root, db, nil) + if err != nil { + return nil, err + } + sdb.deterministic = true + return sdb, nil +} + +func (s *StateDB) Deterministic() bool { + return s.deterministic +} + type ArbitrumExtraData struct { - // track the total balance change across all accounts - unexpectedBalanceDelta *big.Int + unexpectedBalanceDelta *big.Int // total balance change across all accounts + userWasms UserWasms // user wasms encountered during execution + openWasmPages uint16 // number of pages currently open + everWasmPages uint16 // largest number of pages ever allocated during this tx's execution + activatedWasms map[common.Hash]*ActivatedWasm // newly activated WASMs + recentWasms RecentWasms } func (s *StateDB) SetArbFinalizer(f func(*ArbitrumExtraData)) { @@ -101,3 +228,72 @@ func forEachStorage(s *StateDB, addr common.Address, cb func(key, value common.H } return nil } + +// maps moduleHash to activation info +type UserWasms map[common.Hash]ActivatedWasm + +func (s *StateDB) StartRecording() { + s.arbExtraData.userWasms = make(UserWasms) +} + +func (s *StateDB) RecordProgram(moduleHash common.Hash) { + if s.arbExtraData.userWasms != nil { + s.arbExtraData.userWasms[moduleHash] = ActivatedWasm{ + Asm: s.GetActivatedAsm(moduleHash), + Module: s.GetActivatedModule(moduleHash), + } + } +} + +func (s *StateDB) UserWasms() UserWasms { + return s.arbExtraData.userWasms +} + +func (s *StateDB) RecordCacheWasm(wasm CacheWasm) { + s.journal.entries = append(s.journal.entries, wasm) +} + +func (s *StateDB) RecordEvictWasm(wasm EvictWasm) { + s.journal.entries = append(s.journal.entries, wasm) +} + +func (s *StateDB) GetRecentWasms() RecentWasms { + return s.arbExtraData.recentWasms +} + +// Type for managing recent program access. +// The cache contained is discarded at the end of each block. +type RecentWasms struct { + cache *lru.BasicLRU[common.Hash, struct{}] +} + +// Creates an un uninitialized cache +func NewRecentWasms() RecentWasms { + return RecentWasms{cache: nil} +} + +// Inserts a new item, returning true if already present. +func (p RecentWasms) Insert(item common.Hash, retain uint16) bool { + if p.cache == nil { + cache := lru.NewBasicLRU[common.Hash, struct{}](int(retain)) + p.cache = &cache + } + if _, hit := p.cache.Get(item); hit { + println("hit!") + return hit + } + p.cache.Add(item, struct{}{}) + return false +} + +// Copies all entries into a new LRU. +func (p RecentWasms) Copy() RecentWasms { + if p.cache == nil { + return NewRecentWasms() + } + cache := lru.NewBasicLRU[common.Hash, struct{}](p.cache.Capacity()) + for _, item := range p.cache.Keys() { + cache.Add(item, struct{}{}) + } + return RecentWasms{cache: &cache} +} diff --git a/core/types/arbitrum_signer.go b/core/types/arbitrum_signer.go index e9f4c5ee9e..5f2656b14b 100644 --- a/core/types/arbitrum_signer.go +++ b/core/types/arbitrum_signer.go @@ -7,11 +7,24 @@ import ( ) var ArbosAddress = common.HexToAddress("0xa4b05") +var ArbosStateAddress = common.HexToAddress("0xA4B05FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF") var ArbSysAddress = common.HexToAddress("0x64") +var ArbInfoAddress = common.HexToAddress("0x65") +var ArbAddressTableAddress = common.HexToAddress("0x66") +var ArbBLSAddress = common.HexToAddress("0x67") +var ArbFunctionTableAddress = common.HexToAddress("0x68") +var ArbosTestAddress = common.HexToAddress("0x69") var ArbGasInfoAddress = common.HexToAddress("0x6c") +var ArbOwnerPublicAddress = common.HexToAddress("0x6b") +var ArbAggregatorAddress = common.HexToAddress("0x6d") var ArbRetryableTxAddress = common.HexToAddress("0x6e") +var ArbStatisticsAddress = common.HexToAddress("0x6f") +var ArbOwnerAddress = common.HexToAddress("0x70") +var ArbWasmAddress = common.HexToAddress("0x71") +var ArbWasmCacheAddress = common.HexToAddress("0x72") var NodeInterfaceAddress = common.HexToAddress("0xc8") var NodeInterfaceDebugAddress = common.HexToAddress("0xc9") +var ArbDebugAddress = common.HexToAddress("0xff") type arbitrumSigner struct{ Signer } diff --git a/core/types/receipt_arbitrum.go b/core/types/receipt_arbitrum.go new file mode 100644 index 0000000000..9c82eac108 --- /dev/null +++ b/core/types/receipt_arbitrum.go @@ -0,0 +1,21 @@ +// Copyright 2014 The go-ethereum Authors +// This file is part of the go-ethereum library. +// +// The go-ethereum library is free software: you can redistribute it and/or modify +// it under the terms of the GNU Lesser General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// The go-ethereum library is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU Lesser General Public License for more details. +// +// You should have received a copy of the GNU Lesser General Public License +// along with the go-ethereum library. If not, see . + +package types + +func (r *Receipt) GasUsedForL2() uint64 { + return r.GasUsed - r.GasUsedForL1 +} diff --git a/core/vm/contract.go b/core/vm/contract.go index 16b669ebca..327f7caff9 100644 --- a/core/vm/contract.go +++ b/core/vm/contract.go @@ -48,6 +48,9 @@ type Contract struct { caller ContractRef self ContractRef + // Arbitrum + delegateOrCallcode bool + jumpdests map[common.Hash]bitvec // Aggregated result of JUMPDEST analysis. analysis bitvec // Locally cached result of JUMPDEST analysis diff --git a/core/vm/contract_arbitrum.go b/core/vm/contract_arbitrum.go new file mode 100644 index 0000000000..fb7bfbb88c --- /dev/null +++ b/core/vm/contract_arbitrum.go @@ -0,0 +1,30 @@ +// Copyright 2015 The go-ethereum Authors +// This file is part of the go-ethereum library. +// +// The go-ethereum library is free software: you can redistribute it and/or modify +// it under the terms of the GNU Lesser General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// The go-ethereum library is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU Lesser General Public License for more details. +// +// You should have received a copy of the GNU Lesser General Public License +// along with the go-ethereum library. If not, see . + +package vm + +func (c *Contract) BurnGas(amount uint64) error { + if c.Gas < amount { + c.Gas = 0 + return ErrOutOfGas + } + c.Gas -= amount + return nil +} + +func (c *Contract) IsDelegateOrCallcode() bool { + return c.delegateOrCallcode +} diff --git a/core/vm/contracts.go b/core/vm/contracts.go index e633970a5b..0b20cd38a3 100644 --- a/core/vm/contracts.go +++ b/core/vm/contracts.go @@ -157,10 +157,9 @@ func init() { // ActivePrecompiles returns the precompiles enabled with the current configuration. func ActivePrecompiles(rules params.Rules) []common.Address { switch { + case rules.IsStylus: + return PrecompiledAddressesArbOS30 case rules.IsArbitrum: - if rules.ArbOSVersion >= 30 { - return PrecompiledAddressesArbOS30 - } return PrecompiledAddressesArbitrum case rules.IsCancun: return PrecompiledAddressesCancun diff --git a/core/vm/contracts_arbitrum.go b/core/vm/contracts_arbitrum.go index 0609fb874e..f05defe191 100644 --- a/core/vm/contracts_arbitrum.go +++ b/core/vm/contracts_arbitrum.go @@ -1,3 +1,19 @@ +// Copyright 2024 The go-ethereum Authors +// This file is part of the go-ethereum library. +// +// The go-ethereum library is free software: you can redistribute it and/or modify +// it under the terms of the GNU Lesser General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// The go-ethereum library is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU Lesser General Public License for more details. +// +// You should have received a copy of the GNU Lesser General Public License +// along with the go-ethereum library. If not, see . + package vm import "github.com/ethereum/go-ethereum/common" diff --git a/core/vm/evm.go b/core/vm/evm.go index 82089791b4..4c0724f41e 100644 --- a/core/vm/evm.go +++ b/core/vm/evm.go @@ -23,6 +23,7 @@ import ( "github.com/holiman/uint256" "github.com/ethereum/go-ethereum/common" + "github.com/ethereum/go-ethereum/core/state" "github.com/ethereum/go-ethereum/core/types" "github.com/ethereum/go-ethereum/crypto" "github.com/ethereum/go-ethereum/params" @@ -41,11 +42,9 @@ type ( func (evm *EVM) precompile(addr common.Address) (PrecompiledContract, bool) { var precompiles map[common.Address]PrecompiledContract switch { + case evm.chainRules.IsStylus: + precompiles = PrecompiledContractsArbOS30 case evm.chainRules.IsArbitrum: - if evm.chainRules.ArbOSVersion >= 30 { - precompiles = PrecompiledContractsArbOS30 - break - } precompiles = PrecompiledContractsArbitrum case evm.chainRules.IsCancun: precompiles = PrecompiledContractsCancun @@ -324,6 +323,10 @@ func (evm *EVM) CallCode(caller ContractRef, addr common.Address, input []byte, // The contract is a scoped environment for this execution context only. contract := NewContract(caller, AccountRef(caller.Address()), value, gas) contract.SetCallCode(&addrCopy, evm.StateDB.GetCodeHash(addrCopy), evm.StateDB.GetCode(addrCopy)) + + // Arbitrum: note the callcode + contract.delegateOrCallcode = true + ret, err = evm.interpreter.Run(contract, input, false) gas = contract.Gas } @@ -377,6 +380,10 @@ func (evm *EVM) DelegateCall(caller ContractRef, addr common.Address, input []by // Initialise a new contract and make initialise the delegate values contract := NewContract(caller, AccountRef(caller.Address()), nil, gas).AsDelegate() contract.SetCallCode(&addrCopy, evm.StateDB.GetCodeHash(addrCopy), evm.StateDB.GetCode(addrCopy)) + + // Arbitrum: note the delegate call + contract.delegateOrCallcode = true + ret, err = evm.interpreter.Run(contract, input, false) gas = contract.Gas } @@ -521,6 +528,11 @@ func (evm *EVM) create(caller ContractRef, codeAndHash *codeAndHash, gas uint64, // Reject code starting with 0xEF if EIP-3541 is enabled. if err == nil && len(ret) >= 1 && ret[0] == 0xEF && evm.chainRules.IsLondon { err = ErrInvalidCode + + // Arbitrum: retain Stylus programs and instead store them in the DB alongside normal EVM bytecode. + if evm.chainRules.IsStylus && state.IsStylusProgram(ret) { + err = nil + } } // if the contract creation ran successfully and no errors were returned diff --git a/core/vm/evm_arbitrum.go b/core/vm/evm_arbitrum.go index cd85975ce1..a77f2a7aa5 100644 --- a/core/vm/evm_arbitrum.go +++ b/core/vm/evm_arbitrum.go @@ -21,6 +21,7 @@ import ( "github.com/ethereum/go-ethereum/common" "github.com/ethereum/go-ethereum/core/types" + "github.com/ethereum/go-ethereum/log" ) // Depth returns the current depth @@ -39,8 +40,8 @@ func (evm *EVM) DecrementDepth() { type TxProcessingHook interface { StartTxHook() (bool, uint64, error, []byte) // return 4-tuple rather than *struct to avoid an import cycle GasChargingHook(gasRemaining *uint64) (common.Address, error) - PushCaller(addr common.Address) - PopCaller() + PushContract(contract *Contract) + PopContract() ForceRefundGas() uint64 NonrefundableGas() uint64 DropTip() bool @@ -51,6 +52,7 @@ type TxProcessingHook interface { GasPriceOp(evm *EVM) *big.Int FillReceiptInfo(receipt *types.Receipt) MsgIsNonMutating() bool + ExecuteWASM(scope *ScopeContext, input []byte, interpreter *EVMInterpreter) ([]byte, error) } type DefaultTxProcessor struct { @@ -65,9 +67,9 @@ func (p DefaultTxProcessor) GasChargingHook(gasRemaining *uint64) (common.Addres return p.evm.Context.Coinbase, nil } -func (p DefaultTxProcessor) PushCaller(addr common.Address) {} +func (p DefaultTxProcessor) PushContract(contract *Contract) {} -func (p DefaultTxProcessor) PopCaller() {} +func (p DefaultTxProcessor) PopContract() {} func (p DefaultTxProcessor) ForceRefundGas() uint64 { return 0 } @@ -98,3 +100,8 @@ func (p DefaultTxProcessor) FillReceiptInfo(*types.Receipt) {} func (p DefaultTxProcessor) MsgIsNonMutating() bool { return false } + +func (p DefaultTxProcessor) ExecuteWASM(scope *ScopeContext, input []byte, interpreter *EVMInterpreter) ([]byte, error) { + log.Crit("tried to execute WASM with default processing hook") + return nil, nil +} diff --git a/core/vm/instructions.go b/core/vm/instructions.go index 366178ba5e..861709f5ef 100644 --- a/core/vm/instructions.go +++ b/core/vm/instructions.go @@ -20,6 +20,7 @@ import ( "math/big" "github.com/ethereum/go-ethereum/common" + "github.com/ethereum/go-ethereum/core/state" "github.com/ethereum/go-ethereum/core/types" "github.com/ethereum/go-ethereum/crypto" "github.com/ethereum/go-ethereum/params" @@ -835,6 +836,13 @@ func opSelfdestruct6780(pc *uint64, interpreter *EVMInterpreter, scope *ScopeCon if interpreter.readOnly { return nil, ErrWriteProtection } + + // Arbitrum: revert if acting account is a Stylus program + actingAddress := scope.Contract.Address() + if code := interpreter.evm.StateDB.GetCode(actingAddress); state.IsStylusProgram(code) { + return nil, ErrExecutionReverted + } + beneficiary := scope.Stack.pop() balance := interpreter.evm.StateDB.GetBalance(scope.Contract.Address()) interpreter.evm.StateDB.SubBalance(scope.Contract.Address(), balance) diff --git a/core/vm/instructions_arbitrum.go b/core/vm/instructions_arbitrum.go new file mode 100644 index 0000000000..f8927b3c80 --- /dev/null +++ b/core/vm/instructions_arbitrum.go @@ -0,0 +1,50 @@ +// Copyright 2020 The go-ethereum Authors +// This file is part of the go-ethereum library. +// +// The go-ethereum library is free software: you can redistribute it and/or modify +// it under the terms of the GNU Lesser General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// The go-ethereum library is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU Lesser General Public License for more details. +// +// You should have received a copy of the GNU Lesser General Public License +// along with the go-ethereum library. If not, see . + +package vm + +import ( + "math/big" + + "github.com/ethereum/go-ethereum/common" +) + +// adaptation of opBlockHash that doesn't require an EVM stack +func BlockHashOp(evm *EVM, block *big.Int) common.Hash { + if !block.IsUint64() { + return common.Hash{} + } + num64 := block.Uint64() + upper, err := evm.ProcessingHook.L1BlockNumber(evm.Context) + if err != nil { + return common.Hash{} + } + + var lower uint64 + if upper < 257 { + lower = 0 + } else { + lower = upper - 256 + } + if num64 >= lower && num64 < upper { + hash, err := evm.ProcessingHook.L1BlockHash(evm.Context, num64) + if err != nil { + return common.Hash{} + } + return hash + } + return common.Hash{} +} diff --git a/core/vm/interface.go b/core/vm/interface.go index 635cbeea3a..d94d9cfcec 100644 --- a/core/vm/interface.go +++ b/core/vm/interface.go @@ -20,6 +20,7 @@ import ( "math/big" "github.com/ethereum/go-ethereum/common" + "github.com/ethereum/go-ethereum/core/state" "github.com/ethereum/go-ethereum/core/types" "github.com/ethereum/go-ethereum/params" "github.com/holiman/uint256" @@ -27,6 +28,24 @@ import ( // StateDB is an EVM database for full state querying. type StateDB interface { + // Arbitrum: manage Stylus wasms + ActivateWasm(moduleHash common.Hash, asm, module []byte) + GetActivatedAsm(moduleHash common.Hash) (asm []byte) + GetActivatedModule(moduleHash common.Hash) (module []byte) + RecordCacheWasm(wasm state.CacheWasm) + RecordEvictWasm(wasm state.EvictWasm) + GetRecentWasms() state.RecentWasms + + // Arbitrum: track stylus's memory footprint + GetStylusPages() (uint16, uint16) + GetStylusPagesOpen() uint16 + SetStylusPagesOpen(open uint16) + AddStylusPages(new uint16) (uint16, uint16) + AddStylusPagesEver(new uint16) + + Deterministic() bool + Database() state.Database + CreateAccount(common.Address) SubBalance(common.Address, *uint256.Int) diff --git a/core/vm/interpreter.go b/core/vm/interpreter.go index cdd566bdf5..ca8b6db6d0 100644 --- a/core/vm/interpreter.go +++ b/core/vm/interpreter.go @@ -19,6 +19,7 @@ package vm import ( "github.com/ethereum/go-ethereum/common" "github.com/ethereum/go-ethereum/common/math" + "github.com/ethereum/go-ethereum/core/state" "github.com/ethereum/go-ethereum/crypto" "github.com/ethereum/go-ethereum/log" ) @@ -107,9 +108,9 @@ func NewEVMInterpreter(evm *EVM) *EVMInterpreter { func (in *EVMInterpreter) Run(contract *Contract, input []byte, readOnly bool) (ret []byte, err error) { // Increment the call depth which is restricted to 1024 in.evm.depth++ - in.evm.ProcessingHook.PushCaller(contract.Caller()) + in.evm.ProcessingHook.PushContract(contract) defer func() { in.evm.depth-- }() - defer func() { in.evm.ProcessingHook.PopCaller() }() + defer func() { in.evm.ProcessingHook.PopContract() }() // Make sure the readOnly is only set if we aren't in readOnly yet. // This also makes sure that the readOnly flag isn't removed for child calls. @@ -167,6 +168,13 @@ func (in *EVMInterpreter) Run(contract *Contract, input []byte, readOnly bool) ( } }() } + + // Arbitrum: handle Stylus programs + if in.evm.chainRules.IsStylus && state.IsStylusProgram(contract.Code) { + ret, err = in.evm.ProcessingHook.ExecuteWASM(callContext, input, in) + return + } + // The Interpreter main run loop (contextual). This loop runs until either an // explicit STOP, RETURN or SELFDESTRUCT is executed, an error occurred during // the execution of one of the operations or until the done flag is set by the diff --git a/core/vm/interpreter_arbitrum.go b/core/vm/interpreter_arbitrum.go new file mode 100644 index 0000000000..97a63efe48 --- /dev/null +++ b/core/vm/interpreter_arbitrum.go @@ -0,0 +1,41 @@ +// Copyright 2014 The go-ethereum Authors +// This file is part of the go-ethereum library. +// +// The go-ethereum library is free software: you can redistribute it and/or modify +// it under the terms of the GNU Lesser General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// The go-ethereum library is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU Lesser General Public License for more details. +// +// You should have received a copy of the GNU Lesser General Public License +// along with the go-ethereum library. If not, see . + +package vm + +func (in *EVMInterpreter) Config() *Config { + return &in.evm.Config +} + +func (in *EVMInterpreter) Depth() int { + return in.evm.depth +} + +func (in *EVMInterpreter) Evm() *EVM { + return in.evm +} + +func (in *EVMInterpreter) ReadOnly() bool { + return in.readOnly +} + +func (in *EVMInterpreter) GetReturnData() []byte { + return in.returnData +} + +func (in *EVMInterpreter) SetReturnData(data []byte) { + in.returnData = data +} diff --git a/core/vm/logger.go b/core/vm/logger.go index f430575671..c3fdd341f7 100644 --- a/core/vm/logger.go +++ b/core/vm/logger.go @@ -33,6 +33,9 @@ type EVMLogger interface { CaptureArbitrumStorageGet(key common.Hash, depth int, before bool) CaptureArbitrumStorageSet(key, value common.Hash, depth int, before bool) + // Stylus: capture hostio invocation + CaptureStylusHostio(name string, args, outs []byte, startInk, endInk uint64) + // Transaction level CaptureTxStart(gasLimit uint64) CaptureTxEnd(restGas uint64) diff --git a/core/vm/operations_acl_arbitrum.go b/core/vm/operations_acl_arbitrum.go new file mode 100644 index 0000000000..8e0f6b90e0 --- /dev/null +++ b/core/vm/operations_acl_arbitrum.go @@ -0,0 +1,158 @@ +// Copyright 2020 The go-ethereum Authors +// This file is part of the go-ethereum library. +// +// The go-ethereum library is free software: you can redistribute it and/or modify +// it under the terms of the GNU Lesser General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// The go-ethereum library is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU Lesser General Public License for more details. +// +// You should have received a copy of the GNU Lesser General Public License +// along with the go-ethereum library. If not, see . + +package vm + +import ( + "fmt" + + "github.com/ethereum/go-ethereum/common" + "github.com/ethereum/go-ethereum/params" + "github.com/holiman/uint256" +) + +// Computes the cost of doing a state load in wasm +// Note: the code here is adapted from gasSLoadEIP2929 +func WasmStateLoadCost(db StateDB, program common.Address, key common.Hash) uint64 { + // Check slot presence in the access list + if _, slotPresent := db.SlotInAccessList(program, key); !slotPresent { + // If the caller cannot afford the cost, this change will be rolled back + // If he does afford it, we can skip checking the same thing later on, during execution + db.AddSlotToAccessList(program, key) + return params.ColdSloadCostEIP2929 + } + return params.WarmStorageReadCostEIP2929 +} + +// Computes the cost of doing a state store in wasm +// Note: the code here is adapted from makeGasSStoreFunc with the most recent parameters as of The Merge +// Note: the sentry check must be done by the caller +func WasmStateStoreCost(db StateDB, program common.Address, key, value common.Hash) uint64 { + clearingRefund := params.SstoreClearsScheduleRefundEIP3529 + + cost := uint64(0) + current := db.GetState(program, key) + + // Check slot presence in the access list + if addrPresent, slotPresent := db.SlotInAccessList(program, key); !slotPresent { + cost = params.ColdSloadCostEIP2929 + // If the caller cannot afford the cost, this change will be rolled back + db.AddSlotToAccessList(program, key) + if !addrPresent { + panic(fmt.Sprintf("impossible case: address %v was not present in access list", program)) + } + } + + if current == value { // noop (1) + // EIP 2200 original clause: + // return params.SloadGasEIP2200, nil + return cost + params.WarmStorageReadCostEIP2929 // SLOAD_GAS + } + original := db.GetCommittedState(program, key) + if original == current { + if original == (common.Hash{}) { // create slot (2.1.1) + return cost + params.SstoreSetGasEIP2200 + } + if value == (common.Hash{}) { // delete slot (2.1.2b) + db.AddRefund(clearingRefund) + } + // EIP-2200 original clause: + // return params.SstoreResetGasEIP2200, nil // write existing slot (2.1.2) + return cost + (params.SstoreResetGasEIP2200 - params.ColdSloadCostEIP2929) // write existing slot (2.1.2) + } + if original != (common.Hash{}) { + if current == (common.Hash{}) { // recreate slot (2.2.1.1) + db.SubRefund(clearingRefund) + } else if value == (common.Hash{}) { // delete slot (2.2.1.2) + db.AddRefund(clearingRefund) + } + } + if original == value { + if original == (common.Hash{}) { // reset to original inexistent slot (2.2.2.1) + // EIP 2200 Original clause: + //evm.StateDB.AddRefund(params.SstoreSetGasEIP2200 - params.SloadGasEIP2200) + db.AddRefund(params.SstoreSetGasEIP2200 - params.WarmStorageReadCostEIP2929) + } else { // reset to original existing slot (2.2.2.2) + // EIP 2200 Original clause: + // evm.StateDB.AddRefund(params.SstoreResetGasEIP2200 - params.SloadGasEIP2200) + // - SSTORE_RESET_GAS redefined as (5000 - COLD_SLOAD_COST) + // - SLOAD_GAS redefined as WARM_STORAGE_READ_COST + // Final: (5000 - COLD_SLOAD_COST) - WARM_STORAGE_READ_COST + db.AddRefund((params.SstoreResetGasEIP2200 - params.ColdSloadCostEIP2929) - params.WarmStorageReadCostEIP2929) + } + } + // EIP-2200 original clause: + //return params.SloadGasEIP2200, nil // dirty update (2.2) + return cost + params.WarmStorageReadCostEIP2929 // dirty update (2.2) +} + +// Computes the cost of starting a call from wasm +// +// The code here is adapted from the following functions with the most recent parameters as of The Merge +// - operations_acl.go makeCallVariantGasCallEIP2929() +// - gas_table.go gasCall() +func WasmCallCost(db StateDB, contract common.Address, value *uint256.Int, budget uint64) (uint64, error) { + total := uint64(0) + apply := func(amount uint64) bool { + total += amount + return total > budget + } + + // EIP 2929: the static cost + if apply(params.WarmStorageReadCostEIP2929) { + return total, ErrOutOfGas + } + + // EIP 2929: first dynamic cost if cold (makeCallVariantGasCallEIP2929) + warmAccess := db.AddressInAccessList(contract) + coldCost := params.ColdAccountAccessCostEIP2929 - params.WarmStorageReadCostEIP2929 + if !warmAccess { + db.AddAddressToAccessList(contract) + + if apply(coldCost) { + return total, ErrOutOfGas + } + } + + // gasCall() + transfersValue := value.Sign() != 0 + if transfersValue && db.Empty(contract) { + if apply(params.CallNewAccountGas) { + return total, ErrOutOfGas + } + } + if transfersValue { + if apply(params.CallValueTransferGas) { + return total, ErrOutOfGas + } + } + return total, nil +} + +// Computes the cost of touching an account in wasm +// Note: the code here is adapted from gasEip2929AccountCheck with the most recent parameters as of The Merge +func WasmAccountTouchCost(cfg *params.ChainConfig, db StateDB, addr common.Address, withCode bool) uint64 { + cost := uint64(0) + if withCode { + cost = cfg.MaxCodeSize() / 24576 * params.ExtcodeSizeGasEIP150 + } + + if !db.AddressInAccessList(addr) { + db.AddAddressToAccessList(addr) + return cost + params.ColdAccountAccessCostEIP2929 + } + return cost + params.WarmStorageReadCostEIP2929 +} diff --git a/crypto/signature_cgo.go b/crypto/signature_cgo.go index 2339e52015..3a19489488 100644 --- a/crypto/signature_cgo.go +++ b/crypto/signature_cgo.go @@ -14,8 +14,8 @@ // You should have received a copy of the GNU Lesser General Public License // along with the go-ethereum library. If not, see . -//go:build !nacl && !js && cgo && !gofuzz -// +build !nacl,!js,cgo,!gofuzz +//go:build !nacl && !wasm && cgo && !gofuzz +// +build !nacl,!wasm,cgo,!gofuzz package crypto diff --git a/crypto/signature_nocgo.go b/crypto/signature_nocgo.go index 6d628d758d..57bd419e29 100644 --- a/crypto/signature_nocgo.go +++ b/crypto/signature_nocgo.go @@ -14,8 +14,8 @@ // You should have received a copy of the GNU Lesser General Public License // along with the go-ethereum library. If not, see . -//go:build nacl || js || !cgo || gofuzz -// +build nacl js !cgo gofuzz +//go:build nacl || wasm || !cgo || gofuzz +// +build nacl wasm !cgo gofuzz package crypto diff --git a/eth/tracers/js/tracer_arbitrum.go b/eth/tracers/js/tracer_arbitrum.go index 9ab0f641f6..093b6d3540 100644 --- a/eth/tracers/js/tracer_arbitrum.go +++ b/eth/tracers/js/tracer_arbitrum.go @@ -55,3 +55,21 @@ func (jst *jsTracer) CaptureArbitrumTransfer( func (*jsTracer) CaptureArbitrumStorageGet(key common.Hash, depth int, before bool) {} func (*jsTracer) CaptureArbitrumStorageSet(key, value common.Hash, depth int, before bool) {} + +func (jst *jsTracer) CaptureStylusHostio(name string, args, outs []byte, startInk, endInk uint64) { + hostio, ok := goja.AssertFunction(jst.obj.Get("hostio")) + if !ok { + return + } + + info := jst.vm.NewObject() + info.Set("name", name) + info.Set("args", args) + info.Set("outs", outs) + info.Set("startInk", startInk) + info.Set("endInk", endInk) + + if _, err := hostio(jst.obj, info); err != nil { + jst.err = wrapError("hostio", err) + } +} diff --git a/eth/tracers/logger/logger_arbitrum.go b/eth/tracers/logger/logger_arbitrum.go index 0ecaa7a8ec..6c48146897 100644 --- a/eth/tracers/logger/logger_arbitrum.go +++ b/eth/tracers/logger/logger_arbitrum.go @@ -41,3 +41,9 @@ func (*AccessListTracer) CaptureArbitrumStorageSet(key, value common.Hash, depth func (*JSONLogger) CaptureArbitrumStorageSet(key, value common.Hash, depth int, before bool) {} func (*StructLogger) CaptureArbitrumStorageSet(key, value common.Hash, depth int, before bool) {} func (*mdLogger) CaptureArbitrumStorageSet(key, value common.Hash, depth int, before bool) {} + +func (*AccessListTracer) CaptureStylusHostio(name string, args, outs []byte, startInk, endInk uint64) { +} +func (*JSONLogger) CaptureStylusHostio(name string, args, outs []byte, startInk, endInk uint64) {} +func (*StructLogger) CaptureStylusHostio(name string, args, outs []byte, startInk, endInk uint64) {} +func (*mdLogger) CaptureStylusHostio(name string, args, outs []byte, startInk, endInk uint64) {} diff --git a/eth/tracers/native/mux.go b/eth/tracers/native/mux.go index 0fca3dafb6..e487c5a7fe 100644 --- a/eth/tracers/native/mux.go +++ b/eth/tracers/native/mux.go @@ -131,6 +131,12 @@ func (t *muxTracer) CaptureArbitrumTransfer(env *vm.EVM, from, to *common.Addres } } +func (t *muxTracer) CaptureStylusHostio(name string, args, outs []byte, startInk, endInk uint64) { + for _, t := range t.tracers { + t.CaptureStylusHostio(name, args, outs, startInk, endInk) + } +} + // GetResult returns an empty json object. func (t *muxTracer) GetResult() (json.RawMessage, error) { resObject := make(map[string]json.RawMessage) diff --git a/eth/tracers/native/tracer_arbitrum.go b/eth/tracers/native/tracer_arbitrum.go index d79de6ebe4..8c17c436cf 100644 --- a/eth/tracers/native/tracer_arbitrum.go +++ b/eth/tracers/native/tracer_arbitrum.go @@ -93,6 +93,12 @@ func (*noopTracer) CaptureArbitrumStorageSet(key, value common.Hash, depth int, func (*prestateTracer) CaptureArbitrumStorageSet(key, value common.Hash, depth int, before bool) {} func (*flatCallTracer) CaptureArbitrumStorageSet(key, value common.Hash, depth int, before bool) {} +func (*callTracer) CaptureStylusHostio(name string, args, outs []byte, startInk, endInk uint64) {} +func (*fourByteTracer) CaptureStylusHostio(name string, args, outs []byte, startInk, endInk uint64) {} +func (*noopTracer) CaptureStylusHostio(name string, args, outs []byte, startInk, endInk uint64) {} +func (*prestateTracer) CaptureStylusHostio(name string, args, outs []byte, startInk, endInk uint64) {} +func (*flatCallTracer) CaptureStylusHostio(name string, args, outs []byte, startInk, endInk uint64) {} + func bigToHex(n *big.Int) string { if n == nil { return "" diff --git a/ethdb/leveldb/fake_leveldb.go b/ethdb/leveldb/fake_leveldb.go index 51aa406341..28f19d1ddf 100644 --- a/ethdb/leveldb/fake_leveldb.go +++ b/ethdb/leveldb/fake_leveldb.go @@ -14,8 +14,8 @@ // You should have received a copy of the GNU Lesser General Public License // along with the go-ethereum library. If not, see . -//go:build js -// +build js +//go:build wasm +// +build wasm // Package leveldb implements the key-value database layer based on LevelDB. package leveldb diff --git a/ethdb/leveldb/leveldb.go b/ethdb/leveldb/leveldb.go index e58efbddbe..a4270bfaf2 100644 --- a/ethdb/leveldb/leveldb.go +++ b/ethdb/leveldb/leveldb.go @@ -14,8 +14,8 @@ // You should have received a copy of the GNU Lesser General Public License // along with the go-ethereum library. If not, see . -//go:build !js -// +build !js +//go:build !wasm +// +build !wasm // Package leveldb implements the key-value database layer based on LevelDB. package leveldb diff --git a/internal/ethapi/transaction_args.go b/internal/ethapi/transaction_args.go index 12721c75d8..f91517d579 100644 --- a/internal/ethapi/transaction_args.go +++ b/internal/ethapi/transaction_args.go @@ -349,7 +349,7 @@ func (args *TransactionArgs) ToMessage(globalGasCap uint64, header *types.Header SkipL1Charging: skipL1Charging, } // Arbitrum: raise the gas cap to ignore L1 costs so that it's compute-only - if core.InterceptRPCGasCap != nil && state != nil { + if state != nil { // ToMessage recurses once to allow ArbOS to intercept the result for all callers // ArbOS uses this to modify globalGasCap so that the cap will ignore this tx's specific L1 data costs core.InterceptRPCGasCap(&globalGasCap, msg, header, state) diff --git a/log/format.go b/log/format.go index 6447f3c1f1..846ffe403c 100644 --- a/log/format.go +++ b/log/format.go @@ -274,6 +274,9 @@ func appendU256(dst []byte, n *uint256.Int) []byte { // appendEscapeString writes the string s to the given writer, with // escaping/quoting if needed. func appendEscapeString(dst []byte, s string) []byte { + // Arbitrum: remove console colors introduced in Arbitrator + s = Uncolor(s) + needsQuoting := false needsEscaping := false for _, r := range s { @@ -307,6 +310,9 @@ func appendEscapeString(dst []byte, s string) []byte { // to escapeString. The difference is that this method is more lenient: it allows // for spaces and linebreaks to occur without needing quoting. func escapeMessage(s string) string { + // Arbitrum: remove console colors introduced in Arbitrator + s = Uncolor(s) + needsQuoting := false for _, r := range s { // Allow CR/LF/TAB. This is to make multi-line messages work. diff --git a/log/logger_arbitrum.go b/log/logger_arbitrum.go new file mode 100644 index 0000000000..9fcdaf6b6f --- /dev/null +++ b/log/logger_arbitrum.go @@ -0,0 +1,9 @@ +package log + +import "regexp" + +var uncolor = regexp.MustCompile("\x1b\\[([0-9]+;)*[0-9]+m") + +func Uncolor(text string) string { + return uncolor.ReplaceAllString(text, "") +} diff --git a/metrics/cpu_disabled.go b/metrics/cpu_disabled.go index 025d97aeb3..67253912ca 100644 --- a/metrics/cpu_disabled.go +++ b/metrics/cpu_disabled.go @@ -14,8 +14,8 @@ // You should have received a copy of the GNU Lesser General Public License // along with the go-ethereum library. If not, see . -//go:build ios || js -// +build ios js +//go:build ios || wasm +// +build ios wasm package metrics diff --git a/metrics/cpu_enabled.go b/metrics/cpu_enabled.go index 2359028a21..b722e0bcaf 100644 --- a/metrics/cpu_enabled.go +++ b/metrics/cpu_enabled.go @@ -14,8 +14,8 @@ // You should have received a copy of the GNU Lesser General Public License // along with the go-ethereum library. If not, see . -//go:build !ios && !js -// +build !ios,!js +//go:build !ios && !wasm +// +build !ios,!wasm package metrics diff --git a/metrics/cputime_nop.go b/metrics/cputime_nop.go index 465d88c4d2..b8eafd3ffb 100644 --- a/metrics/cputime_nop.go +++ b/metrics/cputime_nop.go @@ -14,8 +14,8 @@ // You should have received a copy of the GNU Lesser General Public License // along with the go-ethereum library. If not, see . -//go:build windows || js -// +build windows js +//go:build windows || wasm +// +build windows wasm package metrics diff --git a/metrics/cputime_unix.go b/metrics/cputime_unix.go index ad4f812fd2..8e86a84087 100644 --- a/metrics/cputime_unix.go +++ b/metrics/cputime_unix.go @@ -14,8 +14,8 @@ // You should have received a copy of the GNU Lesser General Public License // along with the go-ethereum library. If not, see . -//go:build !windows && !js -// +build !windows,!js +//go:build !windows && !wasm +// +build !windows,!wasm package metrics diff --git a/params/config.go b/params/config.go index 4230b8d535..a8afa9cc54 100644 --- a/params/config.go +++ b/params/config.go @@ -915,7 +915,7 @@ func (err *ConfigCompatError) Error() string { // Rules is a one time interface meaning that it shouldn't be used in between transition // phases. type Rules struct { - IsArbitrum bool + IsArbitrum, IsStylus bool ChainID *big.Int ArbOSVersion uint64 IsHomestead, IsEIP150, IsEIP155, IsEIP158 bool @@ -933,6 +933,7 @@ func (c *ChainConfig) Rules(num *big.Int, isMerge bool, timestamp uint64, curren } return Rules{ IsArbitrum: c.IsArbitrum(), + IsStylus: c.IsArbitrum() && currentArbosVersion >= ArbosVersion_Stylus, ChainID: new(big.Int).Set(chainID), ArbOSVersion: currentArbosVersion, IsHomestead: c.IsHomestead(num), diff --git a/params/config_arbitrum.go b/params/config_arbitrum.go index a22052a028..46c460959f 100644 --- a/params/config_arbitrum.go +++ b/params/config_arbitrum.go @@ -22,6 +22,9 @@ import ( "github.com/ethereum/go-ethereum/common" ) +const ArbosVersion_FixRedeemGas = uint64(11) +const ArbosVersion_Stylus = uint64(30) + type ArbitrumChainParams struct { EnableArbOS bool AllowDebugPrecompiles bool @@ -110,7 +113,7 @@ func ArbitrumDevTestParams() ArbitrumChainParams { EnableArbOS: true, AllowDebugPrecompiles: true, DataAvailabilityCommittee: false, - InitialArbOSVersion: 20, + InitialArbOSVersion: 30, InitialChainOwner: common.Address{}, } } @@ -120,7 +123,7 @@ func ArbitrumDevTestDASParams() ArbitrumChainParams { EnableArbOS: true, AllowDebugPrecompiles: true, DataAvailabilityCommittee: true, - InitialArbOSVersion: 20, + InitialArbOSVersion: 30, InitialChainOwner: common.Address{}, } } diff --git a/rlp/safe.go b/rlp/safe.go index 3c910337b6..8da305e706 100644 --- a/rlp/safe.go +++ b/rlp/safe.go @@ -14,8 +14,8 @@ // You should have received a copy of the GNU Lesser General Public License // along with the go-ethereum library. If not, see . -//go:build nacl || js || !cgo -// +build nacl js !cgo +//go:build nacl || wasm || !cgo +// +build nacl wasm !cgo package rlp diff --git a/rlp/unsafe.go b/rlp/unsafe.go index 2152ba35fc..53ad7159de 100644 --- a/rlp/unsafe.go +++ b/rlp/unsafe.go @@ -14,8 +14,8 @@ // You should have received a copy of the GNU Lesser General Public License // along with the go-ethereum library. If not, see . -//go:build !nacl && !js && cgo -// +build !nacl,!js,cgo +//go:build !nacl && !wasm && cgo +// +build !nacl,!wasm,cgo package rlp diff --git a/rpc/ipc_js.go b/rpc/ipc_wasm.go similarity index 97% rename from rpc/ipc_js.go rename to rpc/ipc_wasm.go index 453a20bc1a..66c4eeba03 100644 --- a/rpc/ipc_js.go +++ b/rpc/ipc_wasm.go @@ -14,8 +14,8 @@ // You should have received a copy of the GNU Lesser General Public License // along with the go-ethereum library. If not, see . -//go:build js -// +build js +//go:build wasm +// +build wasm package rpc