diff --git a/arbitrator/prover/src/programs/config.rs b/arbitrator/prover/src/programs/config.rs index 1a37294b04..0353589358 100644 --- a/arbitrator/prover/src/programs/config.rs +++ b/arbitrator/prover/src/programs/config.rs @@ -17,7 +17,7 @@ use { meter::Meter, start::StartMover, MiddlewareWrapper, }, std::sync::Arc, - wasmer::{Cranelift, CraneliftOptLevel, Engine, Store}, + wasmer::{Cranelift, CraneliftOptLevel, Engine, Store, Target}, wasmer_compiler_singlepass::Singlepass, }; @@ -180,17 +180,19 @@ impl CompileConfig { } #[cfg(feature = "native")] - pub fn store(&self) -> Store { - let mut compiler: Box = match self.debug.cranelift { + pub fn engine(&self, target: Target) -> Engine { + use wasmer::sys::EngineBuilder; + + let mut wasmer_config: Box = match self.debug.cranelift { true => { - let mut compiler = Cranelift::new(); - compiler.opt_level(CraneliftOptLevel::Speed); - Box::new(compiler) + let mut wasmer_config = Cranelift::new(); + wasmer_config.opt_level(CraneliftOptLevel::Speed); + Box::new(wasmer_config) } false => Box::new(Singlepass::new()), }; - compiler.canonicalize_nans(true); - compiler.enable_verifier(); + wasmer_config.canonicalize_nans(true); + wasmer_config.enable_verifier(); let start = MiddlewareWrapper::new(StartMover::new(self.debug.debug_info)); let meter = MiddlewareWrapper::new(Meter::new(&self.pricing)); @@ -200,22 +202,24 @@ impl CompileConfig { // add the instrumentation in the order of application // note: this must be consistent with the prover - compiler.push_middleware(Arc::new(start)); - compiler.push_middleware(Arc::new(meter)); - compiler.push_middleware(Arc::new(dygas)); - compiler.push_middleware(Arc::new(depth)); - compiler.push_middleware(Arc::new(bound)); + wasmer_config.push_middleware(Arc::new(start)); + wasmer_config.push_middleware(Arc::new(meter)); + wasmer_config.push_middleware(Arc::new(dygas)); + wasmer_config.push_middleware(Arc::new(depth)); + wasmer_config.push_middleware(Arc::new(bound)); if self.debug.count_ops { let counter = Counter::new(); - compiler.push_middleware(Arc::new(MiddlewareWrapper::new(counter))); + wasmer_config.push_middleware(Arc::new(MiddlewareWrapper::new(counter))); } - Store::new(compiler) + EngineBuilder::new(wasmer_config) + .set_target(Some(target)) + .into() } #[cfg(feature = "native")] - pub fn engine(&self) -> Engine { - self.store().engine().clone() + pub fn store(&self, target: Target) -> Store { + Store::new(self.engine(target)) } } diff --git a/arbitrator/stylus/src/cache.rs b/arbitrator/stylus/src/cache.rs index 06739f2219..fa38d45419 100644 --- a/arbitrator/stylus/src/cache.rs +++ b/arbitrator/stylus/src/cache.rs @@ -10,6 +10,8 @@ use prover::programs::config::CompileConfig; use std::{collections::HashMap, num::NonZeroUsize}; use wasmer::{Engine, Module, Store}; +use crate::target_cache::target_native; + lazy_static! { static ref INIT_CACHE: Mutex = Mutex::new(InitCache::new(256)); } @@ -120,7 +122,7 @@ impl InitCache { } drop(cache); - let engine = CompileConfig::version(version, debug).engine(); + let engine = CompileConfig::version(version, debug).engine(target_native()); let module = unsafe { Module::deserialize_unchecked(&engine, module)? }; let item = CacheItem::new(module, engine); diff --git a/arbitrator/stylus/src/lib.rs b/arbitrator/stylus/src/lib.rs index 3c53359f8b..a252b60a01 100644 --- a/arbitrator/stylus/src/lib.rs +++ b/arbitrator/stylus/src/lib.rs @@ -18,6 +18,7 @@ use native::NativeInstance; use prover::programs::{prelude::*, StylusData}; use run::RunProgram; use std::{marker::PhantomData, mem, ptr}; +use target_cache::{target_cache_get, target_cache_set}; pub use brotli; pub use prover; @@ -29,6 +30,7 @@ pub mod run; mod cache; mod evm_api; +mod target_cache; mod util; #[cfg(test)] @@ -122,9 +124,9 @@ impl RustBytes { } } -/// Instruments and "activates" a user wasm. +/// "activates" a user wasm. /// -/// The `output` is either the serialized asm & module pair or an error string. +/// The `output` is either the module or an error string. /// Returns consensus info such as the module hash and footprint on success. /// /// Note that this operation costs gas and is limited by the amount supplied via the `gas` pointer. @@ -140,7 +142,6 @@ pub unsafe extern "C" fn stylus_activate( version: u16, debug: bool, output: *mut RustBytes, - asm_len: *mut usize, codehash: *const Bytes32, module_hash: *mut Bytes32, stylus_data: *mut StylusData, @@ -152,18 +153,97 @@ pub unsafe extern "C" fn stylus_activate( let codehash = &*codehash; let gas = &mut *gas; - let (asm, module, info) = - match native::activate(wasm, codehash, version, page_limit, debug, gas) { - Ok(val) => val, - Err(err) => return output.write_err(err), - }; - *asm_len = asm.len(); + let (module, info) = match native::activate(wasm, codehash, version, page_limit, debug, gas) { + Ok(val) => val, + Err(err) => return output.write_err(err), + }; + *module_hash = module.hash(); *stylus_data = info; - let mut data = asm; - data.extend(&*module.into_bytes()); - output.write(data); + output.write(module.into_bytes()); + UserOutcomeKind::Success +} + +/// "compiles" a user wasm. +/// +/// The `output` is either the asm or an error string. +/// Returns consensus info such as the module hash and footprint on success. +/// +/// # Safety +/// +/// `output` must not be null. +#[no_mangle] +pub unsafe extern "C" fn stylus_compile( + wasm: GoSliceData, + version: u16, + debug: bool, + name: GoSliceData, + output: *mut RustBytes, +) -> UserOutcomeKind { + let wasm = wasm.slice(); + let output = &mut *output; + let name = match String::from_utf8(name.slice().to_vec()) { + Ok(val) => val, + Err(err) => return output.write_err(err.into()), + }; + let target = match target_cache_get(&name) { + Ok(val) => val, + Err(err) => return output.write_err(err), + }; + + let asm = match native::compile(wasm, version, debug, target) { + Ok(val) => val, + Err(err) => return output.write_err(err), + }; + + output.write(asm); + UserOutcomeKind::Success +} + +#[no_mangle] +/// # Safety +/// +/// `output` must not be null. +pub unsafe extern "C" fn wat_to_wasm(wat: GoSliceData, output: *mut RustBytes) -> UserOutcomeKind { + let output = &mut *output; + let wasm = match wasmer::wat2wasm(wat.slice()) { + Ok(val) => val, + Err(err) => return output.write_err(err.into()), + }; + output.write(wasm.into_owned()); + UserOutcomeKind::Success +} + +/// sets target index to a string +/// +/// String format is: Triple+CpuFeature+CpuFeature.. +/// +/// # Safety +/// +/// `output` must not be null. +#[no_mangle] +pub unsafe extern "C" fn stylus_target_set( + name: GoSliceData, + description: GoSliceData, + output: *mut RustBytes, + native: bool, +) -> UserOutcomeKind { + let output = &mut *output; + let name = match String::from_utf8(name.slice().to_vec()) { + Ok(val) => val, + Err(err) => return output.write_err(err.into()), + }; + + let desc_str = match String::from_utf8(description.slice().to_vec()) { + Ok(val) => val, + Err(err) => return output.write_err(err.into()), + }; + + if let Err(err) = target_cache_set(name, desc_str, native) { + return output.write_err(err); + }; + UserOutcomeKind::Success } diff --git a/arbitrator/stylus/src/native.rs b/arbitrator/stylus/src/native.rs index a7b996edf0..cc1d191fe2 100644 --- a/arbitrator/stylus/src/native.rs +++ b/arbitrator/stylus/src/native.rs @@ -4,7 +4,7 @@ use crate::{ cache::InitCache, env::{MeterData, WasmEnv}, - host, util, + host, }; use arbutil::{ evm::{ @@ -33,11 +33,13 @@ use std::{ ops::{Deref, DerefMut}, }; use wasmer::{ - imports, AsStoreMut, Function, FunctionEnv, Instance, Memory, Module, Pages, Store, + imports, AsStoreMut, Function, FunctionEnv, Instance, Memory, Module, Pages, Store, Target, TypedFunction, Value, WasmTypeList, }; use wasmer_vm::VMExtern; +use crate::target_cache::target_native; + #[derive(Debug)] pub struct NativeInstance> { pub instance: Instance, @@ -98,7 +100,7 @@ impl> NativeInstance { evm_data: EvmData, ) -> Result { let env = WasmEnv::new(compile, None, evm, evm_data); - let store = env.compile.store(); + let store = env.compile.store(target_native()); let module = unsafe { Module::deserialize_unchecked(&store, module)? }; Self::from_module(module, store, env) } @@ -137,9 +139,10 @@ impl> NativeInstance { evm_data: EvmData, compile: &CompileConfig, config: StylusConfig, + target: Target, ) -> Result { let env = WasmEnv::new(compile.clone(), Some(config), evm_api, evm_data); - let store = env.compile.store(); + let store = env.compile.store(target); let wat_or_wasm = std::fs::read(path)?; let module = Module::new(&store, wat_or_wasm)?; Self::from_module(module, store, env) @@ -347,8 +350,8 @@ impl> StartlessMachine for NativeInstance { } } -pub fn module(wasm: &[u8], compile: CompileConfig) -> Result> { - let mut store = compile.store(); +pub fn module(wasm: &[u8], compile: CompileConfig, target: Target) -> Result> { + let mut store = compile.store(target); let module = Module::new(&store, wasm)?; macro_rules! stub { (u8 <- $($types:tt)+) => { @@ -428,7 +431,6 @@ pub fn module(wasm: &[u8], compile: CompileConfig) -> Result> { imports.define("console", "tee_f64", stub!(f64 <- |_: f64|)); imports.define("debug", "null_host", stub!(||)); } - Instance::new(&mut store, &module, &imports)?; let module = module.serialize()?; Ok(module.to_vec()) @@ -441,14 +443,14 @@ pub fn activate( page_limit: u16, debug: bool, gas: &mut u64, -) -> Result<(Vec, ProverModule, StylusData)> { - let compile = CompileConfig::version(version, debug); +) -> Result<(ProverModule, StylusData)> { let (module, stylus_data) = ProverModule::activate(wasm, codehash, version, page_limit, debug, gas)?; - let asm = match self::module(wasm, compile) { - Ok(asm) => asm, - Err(err) => util::panic_with_wasm(wasm, err), - }; - Ok((asm, module, stylus_data)) + Ok((module, stylus_data)) +} + +pub fn compile(wasm: &[u8], version: u16, debug: bool, target: Target) -> Result> { + let compile = CompileConfig::version(version, debug); + self::module(wasm, compile, target) } diff --git a/arbitrator/stylus/src/target_cache.rs b/arbitrator/stylus/src/target_cache.rs new file mode 100644 index 0000000000..a1d63829d6 --- /dev/null +++ b/arbitrator/stylus/src/target_cache.rs @@ -0,0 +1,81 @@ +// Copyright 2022-2024, Offchain Labs, Inc. +// For license information, see https://github.com/OffchainLabs/nitro/blob/master/LICENSE + +use eyre::{eyre, OptionExt, Result}; +use lazy_static::lazy_static; +use parking_lot::RwLock; +use std::{collections::HashMap, str::FromStr}; +use wasmer_types::{CpuFeature, Target, Triple}; + +lazy_static! { + static ref TARGET_CACHE: RwLock> = RwLock::new(HashMap::new()); + static ref TARGET_NATIVE: RwLock = RwLock::new(Target::default()); +} + +fn target_from_string(input: String) -> Result { + if input.is_empty() { + return Ok(Target::default()); + } + let mut parts = input.split('+'); + + let Some(triple_string) = parts.next() else { + return Err(eyre!("no architecture")); + }; + + let triple = match Triple::from_str(triple_string) { + Ok(val) => val, + Err(e) => return Err(eyre!(e)), + }; + + let mut features = CpuFeature::set(); + for flag in parts { + features.insert(CpuFeature::from_str(flag)?); + } + + Ok(Target::new(triple, features)) +} + +/// Populates `TARGET_CACHE` inserting target specified by `description` under `name` key. +/// Additionally, if `native` is set it sets `TARGET_NATIVE` to the specified target. +pub fn target_cache_set(name: String, description: String, native: bool) -> Result<()> { + let target = target_from_string(description)?; + + if native { + if !target.is_native() { + return Err(eyre!("arch not native")); + } + let flags_not_supported = Target::default() + .cpu_features() + .complement() + .intersection(*target.cpu_features()); + if !flags_not_supported.is_empty() { + let mut err_message = String::new(); + err_message.push_str("cpu flags not supported on local cpu for: "); + for item in flags_not_supported.iter() { + err_message.push('+'); + err_message.push_str(&item.to_string()); + } + return Err(eyre!(err_message)); + } + *TARGET_NATIVE.write() = target.clone(); + } + + TARGET_CACHE.write().insert(name, target); + + Ok(()) +} + +pub fn target_native() -> Target { + TARGET_NATIVE.read().clone() +} + +pub fn target_cache_get(name: &str) -> Result { + if name.is_empty() { + return Ok(TARGET_NATIVE.read().clone()); + } + TARGET_CACHE + .read() + .get(name) + .cloned() + .ok_or_eyre("arch not set") +} diff --git a/arbitrator/stylus/src/test/api.rs b/arbitrator/stylus/src/test/api.rs index 92d7317918..5d9f625e5e 100644 --- a/arbitrator/stylus/src/test/api.rs +++ b/arbitrator/stylus/src/test/api.rs @@ -14,6 +14,7 @@ use eyre::Result; use parking_lot::Mutex; use prover::programs::{memory::MemoryModel, prelude::*}; use std::{collections::HashMap, sync::Arc}; +use wasmer::Target; use super::TestInstance; @@ -53,7 +54,7 @@ impl TestEvmApi { pub fn deploy(&mut self, address: Bytes20, config: StylusConfig, name: &str) -> Result<()> { let file = format!("tests/{name}/target/wasm32-unknown-unknown/release/{name}.wasm"); let wasm = std::fs::read(file)?; - let module = native::module(&wasm, self.compile.clone())?; + let module = native::module(&wasm, self.compile.clone(), Target::default())?; self.contracts.lock().insert(address, module); self.configs.lock().insert(address, config); Ok(()) diff --git a/arbitrator/stylus/src/test/misc.rs b/arbitrator/stylus/src/test/misc.rs index ae44a885f0..92c4394ae3 100644 --- a/arbitrator/stylus/src/test/misc.rs +++ b/arbitrator/stylus/src/test/misc.rs @@ -9,12 +9,12 @@ use crate::{ }; use eyre::Result; use prover::programs::{prelude::*, start::StartMover}; -use wasmer::{imports, Function}; +use wasmer::{imports, Function, Target}; #[test] fn test_bulk_memory() -> Result<()> { let (compile, config, ink) = test_configs(); - let mut store = compile.store(); + let mut store = compile.store(Target::default()); let filename = "../prover/test-cases/bulk-memory.wat"; let imports = imports! { "env" => { diff --git a/arbitrator/stylus/src/test/mod.rs b/arbitrator/stylus/src/test/mod.rs index 236e5639e6..00c9c62ae4 100644 --- a/arbitrator/stylus/src/test/mod.rs +++ b/arbitrator/stylus/src/test/mod.rs @@ -16,7 +16,7 @@ use rand::prelude::*; use std::{collections::HashMap, path::Path, sync::Arc}; use wasmer::{ imports, wasmparser::Operator, CompilerConfig, Function, FunctionEnv, Imports, Instance, - Module, Store, + Module, Store, Target, }; use wasmer_compiler_singlepass::Singlepass; @@ -33,7 +33,7 @@ type TestInstance = NativeInstance; impl TestInstance { fn new_test(path: &str, compile: CompileConfig) -> Result { - let mut store = compile.store(); + let mut store = compile.store(Target::default()); let imports = imports! { "test" => { "noop" => Function::new_typed(&mut store, || {}), @@ -86,7 +86,14 @@ impl TestInstance { config: StylusConfig, ) -> Result<(Self, TestEvmApi)> { let (mut evm, evm_data) = TestEvmApi::new(compile.clone()); - let native = Self::from_path(path, evm.clone(), evm_data, compile, config)?; + let native = Self::from_path( + path, + evm.clone(), + evm_data, + compile, + config, + Target::default(), + )?; let footprint = native.memory().ty(&native.store).minimum.0 as u16; evm.set_pages(footprint); Ok((native, evm)) diff --git a/arbnode/inbox_test.go b/arbnode/inbox_test.go index 70392598d6..1c46c593b9 100644 --- a/arbnode/inbox_test.go +++ b/arbnode/inbox_test.go @@ -72,7 +72,9 @@ func NewTransactionStreamerForTest(t *testing.T, ownerAddress common.Address) (* if err != nil { Fail(t, err) } - execEngine.Initialize(gethexec.DefaultCachingConfig.StylusLRUCache) + if err := execEngine.Initialize(gethexec.DefaultCachingConfig.StylusLRUCache, &gethexec.DefaultStylusTargetConfig); err != nil { + Fail(t, err) + } execSeq := &execClientWrapper{execEngine, t} inbox, err := NewTransactionStreamer(arbDb, bc.Config(), execSeq, nil, make(chan error, 1), transactionStreamerConfigFetcher, &DefaultSnapSyncConfig) if err != nil { diff --git a/arbos/programs/cgo_test.go b/arbos/programs/cgo_test.go new file mode 100644 index 0000000000..c0e146d98d --- /dev/null +++ b/arbos/programs/cgo_test.go @@ -0,0 +1,44 @@ +// Copyright 2024, Offchain Labs, Inc. +// For license information, see https://github.com/OffchainLabs/nitro/blob/master/LICENSE + +//go:build !wasm +// +build !wasm + +package programs + +import ( + "fmt" + "os" + "strings" + "testing" +) + +func TestConstants(t *testing.T) { + err := testConstants() + if err != nil { + t.Fatal(err) + } +} + +// normal test will not write anything to disk +// to test cross-compilation: +// * run test with TEST_COMPILE=STORE on one machine +// * copy target/testdata to the other machine +// * run test with TEST_COMPILE=LOAD on the other machine +func TestCompileArch(t *testing.T) { + compile_env := os.Getenv("TEST_COMPILE") + if compile_env == "" { + fmt.Print("use TEST_COMPILE=[STORE|LOAD] to allow store/load in compile test") + } + store := strings.Contains(compile_env, "STORE") + err := testCompileArch(store) + if err != nil { + t.Fatal(err) + } + if store || strings.Contains(compile_env, "LOAD") { + err = testCompileLoad() + if err != nil { + t.Fatal(err) + } + } +} diff --git a/arbos/programs/constant_test.go b/arbos/programs/constant_test.go deleted file mode 100644 index fe29bcf3d9..0000000000 --- a/arbos/programs/constant_test.go +++ /dev/null @@ -1,13 +0,0 @@ -// Copyright 2024, Offchain Labs, Inc. -// For license information, see https://github.com/OffchainLabs/nitro/blob/master/LICENSE - -package programs - -import "testing" - -func TestConstants(t *testing.T) { - err := testConstants() - if err != nil { - t.Fatal(err) - } -} diff --git a/arbos/programs/native.go b/arbos/programs/native.go index a0976afb2f..fd3dec25a0 100644 --- a/arbos/programs/native.go +++ b/arbos/programs/native.go @@ -54,11 +54,11 @@ func activateProgram( debug bool, burner burn.Burner, ) (*activationInfo, error) { - info, asm, module, err := activateProgramInternal(db, program, codehash, wasm, page_limit, version, debug, burner.GasLeft()) + info, asmMap, err := activateProgramInternal(db, program, codehash, wasm, page_limit, version, debug, burner.GasLeft()) if err != nil { return nil, err } - db.ActivateWasm(info.moduleHash, asm, module) + db.ActivateWasm(info.moduleHash, asmMap) return info, nil } @@ -71,41 +71,52 @@ func activateProgramInternal( version uint16, debug bool, gasLeft *uint64, -) (*activationInfo, []byte, []byte, error) { +) (*activationInfo, map[rawdb.Target][]byte, error) { output := &rustBytes{} - asmLen := usize(0) moduleHash := &bytes32{} stylusData := &C.StylusData{} codeHash := hashToBytes32(codehash) - status := userStatus(C.stylus_activate( + status_mod := userStatus(C.stylus_activate( goSlice(wasm), u16(page_limit), u16(version), cbool(debug), output, - &asmLen, &codeHash, moduleHash, stylusData, (*u64)(gasLeft), )) - data, msg, err := status.toResult(output.intoBytes(), debug) + module, msg, err := status_mod.toResult(output.intoBytes(), debug) if err != nil { if debug { log.Warn("activation failed", "err", err, "msg", msg, "program", addressForLogging) } if errors.Is(err, vm.ErrExecutionReverted) { - return nil, nil, nil, fmt.Errorf("%w: %s", ErrProgramActivation, msg) + return nil, nil, fmt.Errorf("%w: %s", ErrProgramActivation, msg) } - return nil, nil, nil, err + return nil, nil, err + } + target := rawdb.LocalTarget() + status_asm := C.stylus_compile( + goSlice(wasm), + u16(version), + cbool(debug), + goSlice([]byte(target)), + output, + ) + asm := output.intoBytes() + if status_asm != 0 { + return nil, nil, fmt.Errorf("%w: %s", ErrProgramActivation, string(asm)) + } + asmMap := map[rawdb.Target][]byte{ + rawdb.TargetWavm: module, + target: asm, } hash := moduleHash.toHash() - split := int(asmLen) - asm := data[:split] - module := data[split:] info := &activationInfo{ moduleHash: hash, @@ -114,11 +125,12 @@ func activateProgramInternal( asmEstimate: uint32(stylusData.asm_estimate), footprint: uint16(stylusData.footprint), } - return info, asm, module, err + return info, asmMap, err } func getLocalAsm(statedb vm.StateDB, moduleHash common.Hash, addressForLogging common.Address, code []byte, codeHash common.Hash, pagelimit uint16, time uint64, debugMode bool, program Program) ([]byte, error) { - localAsm, err := statedb.TryGetActivatedAsm(moduleHash) + localTarget := rawdb.LocalTarget() + localAsm, err := statedb.TryGetActivatedAsm(localTarget, moduleHash) if err == nil && len(localAsm) > 0 { return localAsm, nil } @@ -132,7 +144,7 @@ func getLocalAsm(statedb vm.StateDB, moduleHash common.Hash, addressForLogging c unlimitedGas := uint64(0xffffffffffff) // we know program is activated, so it must be in correct version and not use too much memory - info, asm, module, err := activateProgramInternal(statedb, addressForLogging, codeHash, wasm, pagelimit, program.version, debugMode, &unlimitedGas) + info, asmMap, err := activateProgramInternal(statedb, addressForLogging, codeHash, wasm, pagelimit, program.version, debugMode, &unlimitedGas) 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) @@ -148,14 +160,23 @@ func getLocalAsm(statedb vm.StateDB, moduleHash common.Hash, addressForLogging c // stylus program is active on-chain, and was activated in the past // so we store it directly to database batch := statedb.Database().WasmStore().NewBatch() - rawdb.WriteActivation(batch, moduleHash, asm, module) + rawdb.WriteActivation(batch, moduleHash, asmMap) if err := batch.Write(); err != nil { log.Error("failed writing re-activation to state", "address", addressForLogging, "err", err) } } else { // program activated recently, possibly in this eth_call // store it to statedb. It will be stored to database if statedb is commited - statedb.ActivateWasm(info.moduleHash, asm, module) + statedb.ActivateWasm(info.moduleHash, asmMap) + } + asm, exists := asmMap[localTarget] + if !exists { + var availableTargets []rawdb.Target + for target := range asmMap { + availableTargets = append(availableTargets, target) + } + log.Error("failed to reactivate program - missing asm for local target", "address", addressForLogging, "local target", localTarget, "available targets", availableTargets) + return nil, fmt.Errorf("failed to reactivate program - missing asm for local target, address: %v, local target: %v, available targets: %v", addressForLogging, localTarget, availableTargets) } return asm, nil } @@ -182,7 +203,11 @@ func callProgram( } if db, ok := db.(*state.StateDB); ok { - db.RecordProgram(moduleHash) + targets := []rawdb.Target{ + rawdb.TargetWavm, + rawdb.LocalTarget(), + } + db.RecordProgram(targets, moduleHash) } evmApi := newApi(interpreter, tracingInfo, scope, memoryModel) @@ -263,6 +288,25 @@ func ResizeWasmLruCache(size uint32) { C.stylus_cache_lru_resize(u32(size)) } +const DefaultTargetDescriptionArm = "arm64-linux-unknown+neon" +const DefaultTargetDescriptionX86 = "x86_64-linux-unknown+sse4.2" + +func SetTarget(name rawdb.Target, description string, native bool) error { + output := &rustBytes{} + status := userStatus(C.stylus_target_set( + goSlice([]byte(name)), + goSlice([]byte(description)), + output, + cbool(native), + )) + if status != userSuccess { + msg := arbutil.ToStringOrHex(output.intoBytes()) + log.Error("failed to set stylus compilation target", "status", status, "msg", msg) + return fmt.Errorf("failed to set stylus compilation target, status %v: %v", status, msg) + } + return nil +} + func (value bytes32) toHash() common.Hash { hash := common.Hash{} for index, b := range value.bytes { diff --git a/arbos/programs/testcompile.go b/arbos/programs/testcompile.go new file mode 100644 index 0000000000..a16bae52c0 --- /dev/null +++ b/arbos/programs/testcompile.go @@ -0,0 +1,246 @@ +// Copyright 2024, Offchain Labs, Inc. +// For license information, see https://github.com/OffchainLabs/nitro/blob/master/LICENSE + +//go:build !wasm +// +build !wasm + +package programs + +// This file exists because cgo isn't allowed in tests + +/* +#cgo CFLAGS: -g -Wall -I../../target/include/ +#include "arbitrator.h" + +typedef uint16_t u16; +typedef uint32_t u32; +typedef uint64_t u64; +typedef size_t usize; + +void handleReqWrap(usize api, u32 req_type, RustSlice *data, u64 *out_cost, GoSliceData *out_result, GoSliceData *out_raw_data); +*/ +import "C" +import ( + "fmt" + "os" + "runtime" + + "github.com/ethereum/go-ethereum/core/rawdb" +) + +func Wat2Wasm(wat []byte) ([]byte, error) { + output := &rustBytes{} + + status := C.wat_to_wasm(goSlice(wat), output) + + if status != 0 { + return nil, fmt.Errorf("failed reading wat file: %v", string(output.intoBytes())) + } + + return output.intoBytes(), nil +} + +func testCompileArch(store bool) error { + + localTarget := rawdb.LocalTarget() + nativeArm64 := localTarget == rawdb.TargetArm64 + nativeAmd64 := localTarget == rawdb.TargetAmd64 + + arm64CompileName := []byte(rawdb.TargetArm64) + amd64CompileName := []byte(rawdb.TargetAmd64) + + arm64TargetString := []byte(DefaultTargetDescriptionArm) + amd64TargetString := []byte(DefaultTargetDescriptionX86) + + output := &rustBytes{} + + _, err := fmt.Print("starting test.. native arm? ", nativeArm64, " amd? ", nativeAmd64, " GOARCH/GOOS: ", runtime.GOARCH+"/"+runtime.GOOS, "\n") + if err != nil { + return err + } + + status := C.stylus_target_set(goSlice(arm64CompileName), + goSlice(arm64TargetString), + output, + cbool(nativeArm64)) + + if status != 0 { + return fmt.Errorf("failed setting compilation target arm: %v", string(output.intoBytes())) + } + + status = C.stylus_target_set(goSlice(amd64CompileName), + goSlice(amd64TargetString), + output, + cbool(nativeAmd64)) + + if status != 0 { + return fmt.Errorf("failed setting compilation target amd: %v", string(output.intoBytes())) + } + + source, err := os.ReadFile("../../arbitrator/stylus/tests/add.wat") + if err != nil { + return fmt.Errorf("failed reading stylus contract: %w", err) + } + + wasm, err := Wat2Wasm(source) + if err != nil { + return err + } + + if store { + _, err := fmt.Print("storing compiled files to ../../target/testdata/\n") + if err != nil { + return err + } + err = os.MkdirAll("../../target/testdata", 0755) + if err != nil { + return err + } + } + + status = C.stylus_compile( + goSlice(wasm), + u16(1), + cbool(true), + goSlice([]byte("booga")), + output, + ) + if status == 0 { + return fmt.Errorf("succeeded compiling non-existent arch: %v", string(output.intoBytes())) + } + + status = C.stylus_compile( + goSlice(wasm), + u16(1), + cbool(true), + goSlice([]byte{}), + output, + ) + if status != 0 { + return fmt.Errorf("failed compiling native: %v", string(output.intoBytes())) + } + if store && !nativeAmd64 && !nativeArm64 { + _, err := fmt.Printf("writing host file\n") + if err != nil { + return err + } + + err = os.WriteFile("../../target/testdata/host.bin", output.intoBytes(), 0644) + if err != nil { + return err + } + } + + status = C.stylus_compile( + goSlice(wasm), + u16(1), + cbool(true), + goSlice(arm64CompileName), + output, + ) + if status != 0 { + return fmt.Errorf("failed compiling arm: %v", string(output.intoBytes())) + } + if store { + _, err := fmt.Printf("writing arm file\n") + if err != nil { + return err + } + + err = os.WriteFile("../../target/testdata/arm64.bin", output.intoBytes(), 0644) + if err != nil { + return err + } + } + + status = C.stylus_compile( + goSlice(wasm), + u16(1), + cbool(true), + goSlice(amd64CompileName), + output, + ) + if status != 0 { + return fmt.Errorf("failed compiling amd: %v", string(output.intoBytes())) + } + if store { + _, err := fmt.Printf("writing amd64 file\n") + if err != nil { + return err + } + + err = os.WriteFile("../../target/testdata/amd64.bin", output.intoBytes(), 0644) + if err != nil { + return err + } + } + + return nil +} + +func testCompileLoad() error { + filePath := "../../target/testdata/host.bin" + localTarget := rawdb.LocalTarget() + if localTarget == rawdb.TargetArm64 { + filePath = "../../target/testdata/arm64.bin" + } + if localTarget == rawdb.TargetAmd64 { + filePath = "../../target/testdata/amd64.bin" + } + + _, err := fmt.Print("starting load test. FilePath: ", filePath, " GOARCH/GOOS: ", runtime.GOARCH+"/"+runtime.GOOS, "\n") + if err != nil { + return err + } + + localAsm, err := os.ReadFile(filePath) + if err != nil { + return err + } + + calldata := []byte{} + + evmData := EvmData{} + progParams := ProgParams{ + MaxDepth: 10000, + InkPrice: 1, + DebugMode: true, + } + reqHandler := C.NativeRequestHandler{ + handle_request_fptr: (*[0]byte)(C.handleReqWrap), + id: 0, + } + + inifiniteGas := u64(0xfffffffffffffff) + + output := &rustBytes{} + + _, err = fmt.Print("launching program..\n") + if err != nil { + return err + } + + status := userStatus(C.stylus_call( + goSlice(localAsm), + goSlice(calldata), + progParams.encode(), + reqHandler, + evmData.encode(), + cbool(true), + output, + &inifiniteGas, + u32(0), + )) + + _, err = fmt.Print("returned: ", status, "\n") + if err != nil { + return err + } + + _, msg, err := status.toResult(output.intoBytes(), true) + if status == userFailure { + err = fmt.Errorf("%w: %v", err, msg) + } + + return err +} diff --git a/arbos/programs/wasmstorehelper.go b/arbos/programs/wasmstorehelper.go index 9e69178694..4f82d80282 100644 --- a/arbos/programs/wasmstorehelper.go +++ b/arbos/programs/wasmstorehelper.go @@ -44,8 +44,8 @@ func (p Programs) SaveActiveProgramToWasmStore(statedb *state.StateDB, codeHash } // If already in wasm store then return early - localAsm, err := statedb.TryGetActivatedAsm(moduleHash) - if err == nil && len(localAsm) > 0 { + _, err = statedb.TryGetActivatedAsmMap([]rawdb.Target{rawdb.TargetWavm, rawdb.LocalTarget()}, moduleHash) + if err == nil { return nil } @@ -58,7 +58,7 @@ func (p Programs) SaveActiveProgramToWasmStore(statedb *state.StateDB, codeHash unlimitedGas := uint64(0xffffffffffff) // We know program is activated, so it must be in correct version and not use too much memory // Empty program address is supplied because we dont have access to this during rebuilding of wasm store - info, asm, module, err := activateProgramInternal(statedb, common.Address{}, codeHash, wasm, params.PageLimit, program.version, debugMode, &unlimitedGas) + info, asmMap, err := activateProgramInternal(statedb, common.Address{}, codeHash, wasm, params.PageLimit, program.version, debugMode, &unlimitedGas) if err != nil { log.Error("failed to reactivate program while rebuilding wasm store", "expected moduleHash", moduleHash, "err", err) return fmt.Errorf("failed to reactivate program while rebuilding wasm store: %w", err) @@ -70,7 +70,7 @@ func (p Programs) SaveActiveProgramToWasmStore(statedb *state.StateDB, codeHash } batch := statedb.Database().WasmStore().NewBatch() - rawdb.WriteActivation(batch, moduleHash, asm, module) + rawdb.WriteActivation(batch, moduleHash, asmMap) if err := batch.Write(); err != nil { log.Error("failed writing re-activation to state while rebuilding wasm store", "err", err) return err diff --git a/cmd/nitro/init.go b/cmd/nitro/init.go index c364da5932..f578348b04 100644 --- a/cmd/nitro/init.go +++ b/cmd/nitro/init.go @@ -397,6 +397,82 @@ func checkEmptyDatabaseDir(dir string, force bool) error { return nil } +func databaseIsEmpty(db ethdb.Database) bool { + it := db.NewIterator(nil, nil) + defer it.Release() + return !it.Next() +} + +// removes all entries with keys prefixed with prefixes and of length used in initial version of wasm store schema +func purgeVersion0WasmStoreEntries(db ethdb.Database) error { + prefixes, keyLength := rawdb.DeprecatedPrefixesV0() + batch := db.NewBatch() + notMatchingLengthKeyLogged := false + for _, prefix := range prefixes { + it := db.NewIterator(prefix, nil) + defer it.Release() + for it.Next() { + key := it.Key() + if len(key) != keyLength { + if !notMatchingLengthKeyLogged { + log.Warn("Found key with deprecated prefix but not matching length, skipping removal. (this warning is logged only once)", "key", key) + notMatchingLengthKeyLogged = true + } + continue + } + if err := batch.Delete(key); err != nil { + return fmt.Errorf("Failed to remove key %v : %w", key, err) + } + + // Recreate the iterator after every batch commit in order + // to allow the underlying compactor to delete the entries. + if batch.ValueSize() >= ethdb.IdealBatchSize { + if err := batch.Write(); err != nil { + return fmt.Errorf("Failed to write batch: %w", err) + } + batch.Reset() + it.Release() + it = db.NewIterator(prefix, key) + } + } + } + if batch.ValueSize() > 0 { + if err := batch.Write(); err != nil { + return fmt.Errorf("Failed to write batch: %w", err) + } + batch.Reset() + } + return nil +} + +// if db is not empty, validates if wasm database schema version matches current version +// otherwise persists current version +func validateOrUpgradeWasmStoreSchemaVersion(db ethdb.Database) error { + if !databaseIsEmpty(db) { + version, err := rawdb.ReadWasmSchemaVersion(db) + if err != nil { + if dbutil.IsErrNotFound(err) { + version = []byte{0} + } else { + return fmt.Errorf("Failed to retrieve wasm schema version: %w", err) + } + } + if len(version) != 1 || version[0] > rawdb.WasmSchemaVersion { + return fmt.Errorf("Unsupported wasm database schema version, current version: %v, read from wasm database: %v", rawdb.WasmSchemaVersion, version) + } + // special step for upgrading from version 0 - remove all entries added in version 0 + if version[0] == 0 { + log.Warn("Detected wasm store schema version 0 - removing all old wasm store entries") + if err := purgeVersion0WasmStoreEntries(db); err != nil { + return fmt.Errorf("Failed to purge wasm store version 0 entries: %w", err) + } + log.Info("Wasm store schama version 0 entries successfully removed.") + } + } + rawdb.WriteWasmSchemaVersion(db) + return nil +} + func openInitializeChainDb(ctx context.Context, stack *node.Node, config *NodeConfig, chainId *big.Int, cacheConfig *core.CacheConfig, persistentConfig *conf.PersistentConfig, l1Client arbutil.L1Interface, rollupAddrs chaininfo.RollupAddresses) (ethdb.Database, *core.BlockChain, error) { if !config.Init.Force { if readOnlyDb, err := stack.OpenDatabaseWithFreezerWithExtraOptions("l2chaindata", 0, 0, config.Persistent.Ancient, "l2chaindata/", true, persistentConfig.Pebble.ExtraOptions("l2chaindata")); err == nil { @@ -416,6 +492,9 @@ func openInitializeChainDb(ctx context.Context, stack *node.Node, config *NodeCo if err != nil { return nil, nil, err } + if err := validateOrUpgradeWasmStoreSchemaVersion(wasmDb); err != nil { + return nil, nil, err + } if err := dbutil.UnfinishedConversionCheck(wasmDb); err != nil { return nil, nil, fmt.Errorf("wasm unfinished database conversion check error: %w", err) } @@ -533,6 +612,9 @@ func openInitializeChainDb(ctx context.Context, stack *node.Node, config *NodeCo if err != nil { return nil, nil, err } + if err := validateOrUpgradeWasmStoreSchemaVersion(wasmDb); err != nil { + return nil, nil, err + } chainDb := rawdb.WrapDatabaseWithWasm(chainData, wasmDb, 1) _, err = rawdb.ParseStateScheme(cacheConfig.StateScheme, chainDb) if err != nil { diff --git a/cmd/nitro/init_test.go b/cmd/nitro/init_test.go index 95a4b208d4..6171f631cc 100644 --- a/cmd/nitro/init_test.go +++ b/cmd/nitro/init_test.go @@ -22,6 +22,7 @@ import ( "github.com/ethereum/go-ethereum/core/rawdb" "github.com/ethereum/go-ethereum/ethclient" + "github.com/ethereum/go-ethereum/ethdb" "github.com/ethereum/go-ethereum/node" "github.com/offchainlabs/nitro/arbnode" "github.com/offchainlabs/nitro/cmd/chaininfo" @@ -404,3 +405,104 @@ func TestOpenInitializeChainDbIncompatibleStateScheme(t *testing.T) { t.Fatalf("Failed to detect incompatible state scheme") } } + +func writeKeys(t *testing.T, db ethdb.Database, keys [][]byte) { + t.Helper() + batch := db.NewBatch() + for _, key := range keys { + err := batch.Put(key, []byte("some data")) + if err != nil { + t.Fatal("Internal test error - failed to insert key:", err) + } + } + err := batch.Write() + if err != nil { + t.Fatal("Internal test error - failed to write batch:", err) + } + batch.Reset() +} + +func checkKeys(t *testing.T, db ethdb.Database, keys [][]byte, shouldExist bool) { + t.Helper() + for _, key := range keys { + has, err := db.Has(key) + if err != nil { + t.Fatal("Failed to check key existence, key: ", key) + } + if shouldExist && !has { + t.Fatal("Key not found:", key) + } + if !shouldExist && has { + t.Fatal("Key found:", key, "k3:", string(key[:3]), "len", len(key)) + } + } +} + +func TestPurgeVersion0WasmStoreEntries(t *testing.T) { + stackConf := node.DefaultConfig + stackConf.DataDir = t.TempDir() + stack, err := node.New(&stackConf) + if err != nil { + t.Fatalf("Failed to create test stack: %v", err) + } + defer stack.Close() + db, err := stack.OpenDatabaseWithExtraOptions("wasm", NodeConfigDefault.Execution.Caching.DatabaseCache, NodeConfigDefault.Persistent.Handles, "wasm/", false, nil) + if err != nil { + t.Fatalf("Failed to open test db: %v", err) + } + var version0Keys [][]byte + for i := 0; i < 20; i++ { + version0Keys = append(version0Keys, + append([]byte{0x00, 'w', 'a'}, testhelpers.RandomSlice(32)...)) + version0Keys = append(version0Keys, + append([]byte{0x00, 'w', 'm'}, testhelpers.RandomSlice(32)...)) + } + var collidedKeys [][]byte + for i := 0; i < 5; i++ { + collidedKeys = append(collidedKeys, + append([]byte{0x00, 'w', 'a'}, testhelpers.RandomSlice(31)...)) + collidedKeys = append(collidedKeys, + append([]byte{0x00, 'w', 'm'}, testhelpers.RandomSlice(31)...)) + collidedKeys = append(collidedKeys, + append([]byte{0x00, 'w', 'a'}, testhelpers.RandomSlice(33)...)) + collidedKeys = append(collidedKeys, + append([]byte{0x00, 'w', 'm'}, testhelpers.RandomSlice(33)...)) + } + var otherKeys [][]byte + for i := 0x00; i <= 0xff; i++ { + if byte(i) == 'a' || byte(i) == 'm' { + continue + } + otherKeys = append(otherKeys, + append([]byte{0x00, 'w', byte(i)}, testhelpers.RandomSlice(32)...)) + otherKeys = append(otherKeys, + append([]byte{0x00, 'w', byte(i)}, testhelpers.RandomSlice(32)...)) + } + for i := 0; i < 10; i++ { + var randomSlice []byte + var j int + for j = 0; j < 10; j++ { + randomSlice = testhelpers.RandomSlice(testhelpers.RandomUint64(1, 40)) + if len(randomSlice) >= 3 && !bytes.Equal(randomSlice[:3], []byte{0x00, 'w', 'm'}) && !bytes.Equal(randomSlice[:3], []byte{0x00, 'w', 'm'}) { + break + } + } + if j == 10 { + t.Fatal("Internal test error - failed to generate random key") + } + otherKeys = append(otherKeys, randomSlice) + } + writeKeys(t, db, version0Keys) + writeKeys(t, db, collidedKeys) + writeKeys(t, db, otherKeys) + checkKeys(t, db, version0Keys, true) + checkKeys(t, db, collidedKeys, true) + checkKeys(t, db, otherKeys, true) + err = purgeVersion0WasmStoreEntries(db) + if err != nil { + t.Fatal("Failed to purge version 0 keys, err:", err) + } + checkKeys(t, db, version0Keys, false) + checkKeys(t, db, collidedKeys, true) + checkKeys(t, db, otherKeys, true) +} diff --git a/execution/gethexec/executionengine.go b/execution/gethexec/executionengine.go index d8a592736c..806355b2c6 100644 --- a/execution/gethexec/executionengine.go +++ b/execution/gethexec/executionengine.go @@ -27,6 +27,7 @@ import ( "time" "github.com/ethereum/go-ethereum/core" + "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/log" @@ -149,10 +150,25 @@ func (s *ExecutionEngine) MarkFeedStart(to arbutil.MessageIndex) { } } -func (s *ExecutionEngine) Initialize(rustCacheSize uint32) { +func (s *ExecutionEngine) Initialize(rustCacheSize uint32, targetConfig *StylusTargetConfig) error { if rustCacheSize != 0 { programs.ResizeWasmLruCache(rustCacheSize) } + var effectiveStylusTarget string + target := rawdb.LocalTarget() + switch target { + case rawdb.TargetArm64: + effectiveStylusTarget = targetConfig.Arm64 + case rawdb.TargetAmd64: + effectiveStylusTarget = targetConfig.Amd64 + case rawdb.TargetHost: + effectiveStylusTarget = targetConfig.Host + } + err := programs.SetTarget(target, effectiveStylusTarget, true) + if err != nil { + return fmt.Errorf("Failed to set stylus target: %w", err) + } + return nil } func (s *ExecutionEngine) SetRecorder(recorder *BlockRecorder) { diff --git a/execution/gethexec/node.go b/execution/gethexec/node.go index af40b4b3f7..b864332e83 100644 --- a/execution/gethexec/node.go +++ b/execution/gethexec/node.go @@ -19,6 +19,7 @@ import ( "github.com/ethereum/go-ethereum/node" "github.com/ethereum/go-ethereum/rpc" "github.com/offchainlabs/nitro/arbos/arbostypes" + "github.com/offchainlabs/nitro/arbos/programs" "github.com/offchainlabs/nitro/arbutil" "github.com/offchainlabs/nitro/execution" "github.com/offchainlabs/nitro/solgen/go/precompilesgen" @@ -27,6 +28,24 @@ import ( flag "github.com/spf13/pflag" ) +type StylusTargetConfig struct { + Arm64 string `koanf:"arm64"` + Amd64 string `koanf:"amd64"` + Host string `koanf:"host"` +} + +var DefaultStylusTargetConfig = StylusTargetConfig{ + Arm64: programs.DefaultTargetDescriptionArm, + Amd64: programs.DefaultTargetDescriptionX86, + Host: "", +} + +func StylusTargetConfigAddOptions(prefix string, f *flag.FlagSet) { + f.String(prefix+".arm64", DefaultStylusTargetConfig.Arm64, "stylus programs compilation target for arm64 linux") + f.String(prefix+".amd64", DefaultStylusTargetConfig.Amd64, "stylus programs compilation target for amd64 linux") + f.String(prefix+".host", DefaultStylusTargetConfig.Host, "stylus programs compilation target for system other than 64-bit ARM or 64-bit x86") +} + type Config struct { ParentChainReader headerreader.Config `koanf:"parent-chain-reader" reload:"hot"` Sequencer SequencerConfig `koanf:"sequencer" reload:"hot"` @@ -40,6 +59,7 @@ type Config struct { TxLookupLimit uint64 `koanf:"tx-lookup-limit"` EnablePrefetchBlock bool `koanf:"enable-prefetch-block"` SyncMonitor SyncMonitorConfig `koanf:"sync-monitor"` + StylusTarget StylusTargetConfig `koanf:"stylus-target"` forwardingTarget string } @@ -78,6 +98,7 @@ func ConfigAddOptions(prefix string, f *flag.FlagSet) { SyncMonitorConfigAddOptions(prefix+".sync-monitor", f) f.Uint64(prefix+".tx-lookup-limit", ConfigDefault.TxLookupLimit, "retain the ability to lookup transactions by hash for the past N blocks (0 = all blocks)") f.Bool(prefix+".enable-prefetch-block", ConfigDefault.EnablePrefetchBlock, "enable prefetching of blocks") + StylusTargetConfigAddOptions(prefix+".stylus-target", f) } var ConfigDefault = Config{ @@ -92,6 +113,7 @@ var ConfigDefault = Config{ Caching: DefaultCachingConfig, Forwarder: DefaultNodeForwarderConfig, EnablePrefetchBlock: true, + StylusTarget: DefaultStylusTargetConfig, } type ConfigFetcher func() *Config @@ -251,9 +273,13 @@ func (n *ExecutionNode) MarkFeedStart(to arbutil.MessageIndex) { } func (n *ExecutionNode) Initialize(ctx context.Context) error { - n.ExecEngine.Initialize(n.ConfigFetcher().Caching.StylusLRUCache) + config := n.ConfigFetcher() + err := n.ExecEngine.Initialize(config.Caching.StylusLRUCache, &config.StylusTarget) + if err != nil { + return fmt.Errorf("error initializing execution engine: %w", err) + } n.ArbInterface.Initialize(n) - err := n.Backend.Start() + err = n.Backend.Start() if err != nil { return fmt.Errorf("error starting geth backend: %w", err) } diff --git a/go-ethereum b/go-ethereum index a1fc200e5b..575062fad7 160000 --- a/go-ethereum +++ b/go-ethereum @@ -1 +1 @@ -Subproject commit a1fc200e5b85a7737a9834ec28fb768fb7bde7bd +Subproject commit 575062fad7ff4db9d7c235f49472f658be29e2fe diff --git a/go.mod b/go.mod index 6649973725..da49b0d8b9 100644 --- a/go.mod +++ b/go.mod @@ -41,7 +41,6 @@ require ( github.com/rivo/tview v0.0.0-20240307173318-e804876934a1 github.com/spf13/pflag v1.0.5 github.com/syndtr/goleveldb v1.0.1-0.20210819022825-2ae1ddf74ef7 - github.com/wasmerio/wasmer-go v1.0.4 github.com/wealdtech/go-merkletree v1.0.0 golang.org/x/crypto v0.21.0 golang.org/x/exp v0.0.0-20231110203233-9a3e6036ecaa diff --git a/go.sum b/go.sum index 8529b2497d..c0193be769 100644 --- a/go.sum +++ b/go.sum @@ -704,8 +704,6 @@ github.com/vmihailenco/msgpack/v5 v5.3.5 h1:5gO0H1iULLWGhs2H5tbAHIZTV8/cYafcFOr9 github.com/vmihailenco/msgpack/v5 v5.3.5/go.mod h1:7xyJ9e+0+9SaZT0Wt1RGleJXzli6Q/V5KbhBonMG9jc= github.com/vmihailenco/tagparser/v2 v2.0.0 h1:y09buUbR+b5aycVFQs/g70pqKVZNBmxwAhO7/IwNM9g= github.com/vmihailenco/tagparser/v2 v2.0.0/go.mod h1:Wri+At7QHww0WTrCBeu4J6bNtoV6mEfg5OIWRZA9qds= -github.com/wasmerio/wasmer-go v1.0.4 h1:MnqHoOGfiQ8MMq2RF6wyCeebKOe84G88h5yv+vmxJgs= -github.com/wasmerio/wasmer-go v1.0.4/go.mod h1:0gzVdSfg6pysA6QVp6iVRPTagC6Wq9pOE8J86WKb2Fk= github.com/wealdtech/go-merkletree v1.0.0 h1:DsF1xMzj5rK3pSQM6mPv8jlyJyHXhFxpnA2bwEjMMBY= github.com/wealdtech/go-merkletree v1.0.0/go.mod h1:cdil512d/8ZC7Kx3bfrDvGMQXB25NTKbsm0rFrmDax4= github.com/xeipuuv/gojsonpointer v0.0.0-20180127040702-4e3ac2762d5f/go.mod h1:N2zxlSyiKSe5eX1tZViRH5QA0qijqEDrYZiPEAiq3wU= diff --git a/staker/block_validator.go b/staker/block_validator.go index df465cc31f..8f5724beac 100644 --- a/staker/block_validator.go +++ b/staker/block_validator.go @@ -17,6 +17,7 @@ import ( "time" "github.com/ethereum/go-ethereum/common" + "github.com/ethereum/go-ethereum/core/rawdb" "github.com/ethereum/go-ethereum/ethdb" "github.com/ethereum/go-ethereum/log" "github.com/ethereum/go-ethereum/metrics" @@ -126,6 +127,9 @@ func (c *BlockValidatorConfig) Validate() error { } c.memoryFreeLimit = limit } + if err := c.RedisValidationClientConfig.Validate(); err != nil { + return fmt.Errorf("failed to validate redis validation client config: %w", err) + } streamsEnabled := c.RedisValidationClientConfig.Enabled() if len(c.ValidationServerConfigs) == 0 { c.ValidationServerConfigs = []rpcclient.ClientConfig{c.ValidationServer} @@ -495,7 +499,7 @@ func (v *BlockValidator) sendRecord(s *validationStatus) error { //nolint:gosec func (v *BlockValidator) writeToFile(validationEntry *validationEntry, moduleRoot common.Hash) error { - input, err := validationEntry.ToInput([]string{"wavm"}) + input, err := validationEntry.ToInput([]rawdb.Target{rawdb.TargetWavm}) if err != nil { return err } diff --git a/staker/challenge_manager.go b/staker/challenge_manager.go index 80cafccced..b1421d7e41 100644 --- a/staker/challenge_manager.go +++ b/staker/challenge_manager.go @@ -14,6 +14,7 @@ import ( "github.com/ethereum/go-ethereum/accounts/abi/bind" "github.com/ethereum/go-ethereum/accounts/abi/bind/backends" "github.com/ethereum/go-ethereum/common" + "github.com/ethereum/go-ethereum/core/rawdb" "github.com/ethereum/go-ethereum/core/types" "github.com/ethereum/go-ethereum/log" "github.com/ethereum/go-ethereum/rpc" @@ -467,7 +468,7 @@ func (m *ChallengeManager) createExecutionBackend(ctx context.Context, step uint if err != nil { return fmt.Errorf("error creating validation entry for challenge %v msg %v for execution challenge: %w", m.challengeIndex, initialCount, err) } - input, err := entry.ToInput([]string{"wavm"}) + input, err := entry.ToInput([]rawdb.Target{rawdb.TargetWavm}) if err != nil { return fmt.Errorf("error getting validation entry input of challenge %v msg %v: %w", m.challengeIndex, initialCount, err) } diff --git a/staker/stateless_block_validator.go b/staker/stateless_block_validator.go index e8232264fe..d5eeb8eb69 100644 --- a/staker/stateless_block_validator.go +++ b/staker/stateless_block_validator.go @@ -7,12 +7,12 @@ import ( "context" "errors" "fmt" - "runtime" "testing" "github.com/offchainlabs/nitro/arbstate/daprovider" "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/ethdb" "github.com/ethereum/go-ethereum/log" @@ -134,7 +134,7 @@ type validationEntry struct { DelayedMsg []byte } -func (e *validationEntry) ToInput(stylusArchs []string) (*validator.ValidationInput, error) { +func (e *validationEntry) ToInput(stylusArchs []rawdb.Target) (*validator.ValidationInput, error) { if e.Stage != Ready { return nil, errors.New("cannot create input from non-ready entry") } @@ -143,21 +143,22 @@ func (e *validationEntry) ToInput(stylusArchs []string) (*validator.ValidationIn HasDelayedMsg: e.HasDelayedMsg, DelayedMsgNr: e.DelayedMsgNr, Preimages: e.Preimages, - UserWasms: make(map[string]map[common.Hash][]byte, len(e.UserWasms)), + UserWasms: make(map[rawdb.Target]map[common.Hash][]byte, len(e.UserWasms)), BatchInfo: e.BatchInfo, DelayedMsg: e.DelayedMsg, StartState: e.Start, DebugChain: e.ChainConfig.DebugMode(), } + if len(stylusArchs) == 0 && len(e.UserWasms) > 0 { + return nil, fmt.Errorf("stylus support is required") + } for _, stylusArch := range stylusArchs { res.UserWasms[stylusArch] = make(map[common.Hash][]byte) } - for hash, info := range e.UserWasms { + for hash, asmMap := range e.UserWasms { for _, stylusArch := range stylusArchs { - if stylusArch == "wavm" { - res.UserWasms[stylusArch][hash] = info.Module - } else if stylusArch == runtime.GOARCH { - res.UserWasms[stylusArch][hash] = info.Asm + if asm, exists := asmMap[stylusArch]; exists { + res.UserWasms[stylusArch][hash] = asm } else { return nil, fmt.Errorf("stylusArch not supported by block validator: %v", stylusArch) } diff --git a/system_tests/program_test.go b/system_tests/program_test.go index ae34c6c5bb..e171f2a444 100644 --- a/system_tests/program_test.go +++ b/system_tests/program_test.go @@ -41,7 +41,6 @@ import ( "github.com/offchainlabs/nitro/util/colors" "github.com/offchainlabs/nitro/util/testhelpers" "github.com/offchainlabs/nitro/validator/valnode" - "github.com/wasmerio/wasmer-go/wasmer" ) var oneEth = arbmath.UintToBig(1e18) @@ -1530,7 +1529,7 @@ func readWasmFile(t *testing.T, file string) ([]byte, []byte) { // chose a random dictionary for testing, but keep the same files consistent randDict := arbcompress.Dictionary((len(file) + len(t.Name())) % 2) - wasmSource, err := wasmer.Wat2Wasm(string(source)) + wasmSource, err := programs.Wat2Wasm(source) Require(t, err) wasm, err := arbcompress.Compress(wasmSource, arbcompress.LEVEL_WELL, randDict) Require(t, err) diff --git a/system_tests/validation_mock_test.go b/system_tests/validation_mock_test.go index 2c6321d009..88421e4c4b 100644 --- a/system_tests/validation_mock_test.go +++ b/system_tests/validation_mock_test.go @@ -8,6 +8,7 @@ import ( "time" "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/crypto" "github.com/ethereum/go-ethereum/node" @@ -60,8 +61,8 @@ func (s *mockSpawner) WasmModuleRoots() ([]common.Hash, error) { return mockWasmModuleRoots, nil } -func (s *mockSpawner) StylusArchs() []string { - return []string{"mock"} +func (s *mockSpawner) StylusArchs() []rawdb.Target { + return []rawdb.Target{"mock"} } func (s *mockSpawner) Launch(entry *validator.ValidationInput, moduleRoot common.Hash) validator.ValidationRun { diff --git a/validator/client/redis/producer.go b/validator/client/redis/producer.go index b3ad0f8839..f98c246d0e 100644 --- a/validator/client/redis/producer.go +++ b/validator/client/redis/producer.go @@ -6,6 +6,7 @@ import ( "sync/atomic" "github.com/ethereum/go-ethereum/common" + "github.com/ethereum/go-ethereum/core/rawdb" "github.com/ethereum/go-ethereum/log" "github.com/go-redis/redis/v8" "github.com/offchainlabs/nitro/pubsub" @@ -32,11 +33,20 @@ func (c ValidationClientConfig) Enabled() bool { return c.RedisURL != "" } +func (c ValidationClientConfig) Validate() error { + for _, arch := range c.StylusArchs { + if !rawdb.Target(arch).IsValid() { + return fmt.Errorf("Invalid stylus arch: %v", arch) + } + } + return nil +} + var DefaultValidationClientConfig = ValidationClientConfig{ Name: "redis validation client", Room: 2, RedisURL: "", - StylusArchs: []string{"wavm"}, + StylusArchs: []string{string(rawdb.TargetWavm)}, ProducerConfig: pubsub.DefaultProducerConfig, CreateStreams: true, } @@ -46,7 +56,7 @@ var TestValidationClientConfig = ValidationClientConfig{ Room: 2, RedisURL: "", StreamPrefix: "test-", - StylusArchs: []string{"wavm"}, + StylusArchs: []string{string(rawdb.TargetWavm)}, ProducerConfig: pubsub.TestProducerConfig, CreateStreams: false, } @@ -152,8 +162,12 @@ func (c *ValidationClient) Name() string { return c.config.Name } -func (c *ValidationClient) StylusArchs() []string { - return c.config.StylusArchs +func (c *ValidationClient) StylusArchs() []rawdb.Target { + stylusArchs := make([]rawdb.Target, 0, len(c.config.StylusArchs)) + for _, arch := range c.config.StylusArchs { + stylusArchs = append(stylusArchs, rawdb.Target(arch)) + } + return stylusArchs } func (c *ValidationClient) Room() int { diff --git a/validator/client/validation_client.go b/validator/client/validation_client.go index 05d947db3d..80cff66675 100644 --- a/validator/client/validation_client.go +++ b/validator/client/validation_client.go @@ -8,7 +8,6 @@ import ( "encoding/base64" "errors" "fmt" - "runtime" "sync/atomic" "time" @@ -22,6 +21,7 @@ import ( "github.com/offchainlabs/nitro/validator/server_common" "github.com/ethereum/go-ethereum/common" + "github.com/ethereum/go-ethereum/core/rawdb" "github.com/ethereum/go-ethereum/log" "github.com/ethereum/go-ethereum/node" "github.com/ethereum/go-ethereum/rpc" @@ -31,7 +31,7 @@ type ValidationClient struct { stopwaiter.StopWaiter client *rpcclient.RpcClient name string - stylusArchs []string + stylusArchs []rawdb.Target room atomic.Int32 wasmModuleRoots []common.Hash } @@ -40,7 +40,7 @@ func NewValidationClient(config rpcclient.ClientConfigFetcher, stack *node.Node) return &ValidationClient{ client: rpcclient.NewRpcClient(config, stack), name: "not started", - stylusArchs: []string{"not started"}, + stylusArchs: []rawdb.Target{"not started"}, } } @@ -67,20 +67,20 @@ func (c *ValidationClient) Start(ctx context.Context) error { if len(name) == 0 { return errors.New("couldn't read name from server") } - var stylusArchs []string + var stylusArchs []rawdb.Target if err := c.client.CallContext(ctx, &stylusArchs, server_api.Namespace+"_stylusArchs"); err != nil { var rpcError rpc.Error ok := errors.As(err, &rpcError) if !ok || rpcError.ErrorCode() != -32601 { return fmt.Errorf("could not read stylus arch from server: %w", err) } - stylusArchs = []string{"pre-stylus"} // validation does not support stylus + stylusArchs = []rawdb.Target{rawdb.Target("pre-stylus")} // invalid, will fail if trying to validate block with stylus } else { if len(stylusArchs) == 0 { return fmt.Errorf("could not read stylus archs from validation server") } for _, stylusArch := range stylusArchs { - if stylusArch != "wavm" && stylusArch != runtime.GOARCH && stylusArch != "mock" { + if stylusArch != rawdb.TargetWavm && stylusArch != rawdb.LocalTarget() && stylusArch != "mock" { return fmt.Errorf("unsupported stylus architecture: %v", stylusArch) } } @@ -117,11 +117,11 @@ func (c *ValidationClient) WasmModuleRoots() ([]common.Hash, error) { return nil, errors.New("not started") } -func (c *ValidationClient) StylusArchs() []string { +func (c *ValidationClient) StylusArchs() []rawdb.Target { if c.Started() { return c.stylusArchs } - return []string{"not started"} + return []rawdb.Target{"not started"} } func (c *ValidationClient) Stop() { diff --git a/validator/interface.go b/validator/interface.go index 80aa2c1fcc..81b40ae5cf 100644 --- a/validator/interface.go +++ b/validator/interface.go @@ -4,6 +4,7 @@ import ( "context" "github.com/ethereum/go-ethereum/common" + "github.com/ethereum/go-ethereum/core/rawdb" "github.com/offchainlabs/nitro/util/containers" ) @@ -13,7 +14,7 @@ type ValidationSpawner interface { Start(context.Context) error Stop() Name() string - StylusArchs() []string + StylusArchs() []rawdb.Target Room() int } diff --git a/validator/server_api/json.go b/validator/server_api/json.go index 90746e4c57..dbe2bb1fee 100644 --- a/validator/server_api/json.go +++ b/validator/server_api/json.go @@ -11,6 +11,7 @@ import ( "os" "github.com/ethereum/go-ethereum/common" + "github.com/ethereum/go-ethereum/core/rawdb" "github.com/offchainlabs/nitro/arbcompress" "github.com/offchainlabs/nitro/arbutil" @@ -63,7 +64,7 @@ type InputJSON struct { BatchInfo []BatchInfoJson DelayedMsgB64 string StartState validator.GoGlobalState - UserWasms map[string]map[common.Hash]string + UserWasms map[rawdb.Target]map[common.Hash]string DebugChain bool } @@ -95,14 +96,14 @@ func ValidationInputToJson(entry *validator.ValidationInput) *InputJSON { DelayedMsgB64: base64.StdEncoding.EncodeToString(entry.DelayedMsg), StartState: entry.StartState, PreimagesB64: jsonPreimagesMap, - UserWasms: make(map[string]map[common.Hash]string), + UserWasms: make(map[rawdb.Target]map[common.Hash]string), DebugChain: entry.DebugChain, } for _, binfo := range entry.BatchInfo { encData := base64.StdEncoding.EncodeToString(binfo.Data) res.BatchInfo = append(res.BatchInfo, BatchInfoJson{Number: binfo.Number, DataB64: encData}) } - for arch, wasms := range entry.UserWasms { + for target, wasms := range entry.UserWasms { archWasms := make(map[common.Hash]string) for moduleHash, data := range wasms { compressed, err := arbcompress.CompressLevel(data, 1) @@ -111,7 +112,7 @@ func ValidationInputToJson(entry *validator.ValidationInput) *InputJSON { } archWasms[moduleHash] = base64.StdEncoding.EncodeToString(compressed) } - res.UserWasms[arch] = archWasms + res.UserWasms[target] = archWasms } return res } @@ -127,7 +128,7 @@ func ValidationInputFromJson(entry *InputJSON) (*validator.ValidationInput, erro DelayedMsgNr: entry.DelayedMsgNr, StartState: entry.StartState, Preimages: preimages, - UserWasms: make(map[string]map[common.Hash][]byte), + UserWasms: make(map[rawdb.Target]map[common.Hash][]byte), DebugChain: entry.DebugChain, } delayed, err := base64.StdEncoding.DecodeString(entry.DelayedMsgB64) @@ -146,7 +147,7 @@ func ValidationInputFromJson(entry *InputJSON) (*validator.ValidationInput, erro } valInput.BatchInfo = append(valInput.BatchInfo, decInfo) } - for arch, wasms := range entry.UserWasms { + for target, wasms := range entry.UserWasms { archWasms := make(map[common.Hash][]byte) for moduleHash, encoded := range wasms { decoded, err := base64.StdEncoding.DecodeString(encoded) @@ -171,7 +172,7 @@ func ValidationInputFromJson(entry *InputJSON) (*validator.ValidationInput, erro } archWasms[moduleHash] = uncompressed } - valInput.UserWasms[arch] = archWasms + valInput.UserWasms[target] = archWasms } return valInput, nil } diff --git a/validator/server_arb/validator_spawner.go b/validator/server_arb/validator_spawner.go index 1d4126dc7c..844a988d28 100644 --- a/validator/server_arb/validator_spawner.go +++ b/validator/server_arb/validator_spawner.go @@ -21,6 +21,7 @@ import ( "github.com/offchainlabs/nitro/validator/valnode/redis" "github.com/ethereum/go-ethereum/common" + "github.com/ethereum/go-ethereum/core/rawdb" "github.com/ethereum/go-ethereum/log" "github.com/ethereum/go-ethereum/metrics" ) @@ -88,8 +89,8 @@ func (s *ArbitratorSpawner) WasmModuleRoots() ([]common.Hash, error) { return s.locator.ModuleRoots(), nil } -func (s *ArbitratorSpawner) StylusArchs() []string { - return []string{"wavm"} +func (s *ArbitratorSpawner) StylusArchs() []rawdb.Target { + return []rawdb.Target{rawdb.TargetWavm} } func (s *ArbitratorSpawner) Name() string { @@ -122,14 +123,14 @@ func (v *ArbitratorSpawner) loadEntryToMachine(ctx context.Context, entry *valid return fmt.Errorf("error while trying to add sequencer msg for proving: %w", err) } } - if len(entry.UserWasms["wavm"]) == 0 { + if len(entry.UserWasms[rawdb.TargetWavm]) == 0 { for stylusArch, wasms := range entry.UserWasms { if len(wasms) > 0 { return fmt.Errorf("bad stylus arch loaded to machine. Expected wavm. Got: %s", stylusArch) } } } - for moduleHash, module := range entry.UserWasms["wavm"] { + for moduleHash, module := range entry.UserWasms[rawdb.TargetWavm] { err = mach.AddUserWasm(moduleHash, module) if err != nil { log.Error( diff --git a/validator/server_jit/jit_machine.go b/validator/server_jit/jit_machine.go index e4fb840cbb..23a75bba83 100644 --- a/validator/server_jit/jit_machine.go +++ b/validator/server_jit/jit_machine.go @@ -12,10 +12,10 @@ import ( "net" "os" "os/exec" - "runtime" "time" "github.com/ethereum/go-ethereum/common" + "github.com/ethereum/go-ethereum/core/rawdb" "github.com/ethereum/go-ethereum/log" "github.com/ethereum/go-ethereum/metrics" "github.com/offchainlabs/nitro/util/arbmath" @@ -212,13 +212,14 @@ func (machine *JitMachine) prove( } } - userWasms := entry.UserWasms[runtime.GOARCH] + localTarget := rawdb.LocalTarget() + userWasms := entry.UserWasms[localTarget] // if there are user wasms, but only for wrong architecture - error if len(userWasms) == 0 { for arch, userWasms := range entry.UserWasms { if len(userWasms) != 0 { - return state, fmt.Errorf("bad stylus arch for validation input. got: %v, expected: %v", arch, runtime.GOARCH) + return state, fmt.Errorf("bad stylus arch for validation input. got: %v, expected: %v", arch, localTarget) } } } diff --git a/validator/server_jit/spawner.go b/validator/server_jit/spawner.go index 5ba3664109..92b50b17cb 100644 --- a/validator/server_jit/spawner.go +++ b/validator/server_jit/spawner.go @@ -9,6 +9,7 @@ import ( flag "github.com/spf13/pflag" "github.com/ethereum/go-ethereum/common" + "github.com/ethereum/go-ethereum/core/rawdb" "github.com/offchainlabs/nitro/util/stopwaiter" "github.com/offchainlabs/nitro/validator" @@ -71,8 +72,8 @@ func (v *JitSpawner) WasmModuleRoots() ([]common.Hash, error) { return v.locator.ModuleRoots(), nil } -func (v *JitSpawner) StylusArchs() []string { - return []string{runtime.GOARCH} +func (v *JitSpawner) StylusArchs() []rawdb.Target { + return []rawdb.Target{rawdb.LocalTarget()} } func (v *JitSpawner) execute( diff --git a/validator/validation_entry.go b/validator/validation_entry.go index 133a67a8a8..2c357659ad 100644 --- a/validator/validation_entry.go +++ b/validator/validation_entry.go @@ -2,6 +2,7 @@ package validator import ( "github.com/ethereum/go-ethereum/common" + "github.com/ethereum/go-ethereum/core/rawdb" "github.com/offchainlabs/nitro/arbutil" ) @@ -16,7 +17,7 @@ type ValidationInput struct { HasDelayedMsg bool DelayedMsgNr uint64 Preimages map[arbutil.PreimageType]map[common.Hash][]byte - UserWasms map[string]map[common.Hash][]byte + UserWasms map[rawdb.Target]map[common.Hash][]byte BatchInfo []BatchInfo DelayedMsg []byte StartState GoGlobalState diff --git a/validator/valnode/validation_api.go b/validator/valnode/validation_api.go index 6245ffc5e3..a79ac7fa55 100644 --- a/validator/valnode/validation_api.go +++ b/validator/valnode/validation_api.go @@ -12,6 +12,7 @@ import ( "time" "github.com/ethereum/go-ethereum/common" + "github.com/ethereum/go-ethereum/core/rawdb" "github.com/offchainlabs/nitro/util/stopwaiter" "github.com/offchainlabs/nitro/validator" @@ -44,7 +45,7 @@ func (a *ValidationServerAPI) WasmModuleRoots() ([]common.Hash, error) { return a.spawner.WasmModuleRoots() } -func (a *ValidationServerAPI) StylusArchs() ([]string, error) { +func (a *ValidationServerAPI) StylusArchs() ([]rawdb.Target, error) { return a.spawner.StylusArchs(), nil }