diff --git a/.github/buildspec.yml b/.github/buildspec.yml new file mode 100644 index 0000000000..9b6503bb5d --- /dev/null +++ b/.github/buildspec.yml @@ -0,0 +1,36 @@ +version: 0.2 + +phases: + pre_build: + commands: + - git submodule update --init + - echo Logging in to Dockerhub.... + - docker login --username $DOCKERHUB_USERNAME --password $DOCKERHUB_PASSWORD + - aws ecr get-login-password --region us-west-2 | docker login --username AWS --password-stdin $REPOSITORY_URI + - COMMIT_HASH=$(git rev-parse --short=7 HEAD || echo "latest") + - VERSION_TAG=$(git tag --points-at HEAD | sed '/-/!s/$/_/' | sort -rV | sed 's/_$//' | head -n 1 | grep ^ || git show -s --pretty=%D | sed 's/, /\n/g' | grep -v '^origin/' |grep -v '^grafted\|HEAD\|master\|main$' || echo "dev") + - NITRO_VERSION=${VERSION_TAG}-${COMMIT_HASH} + - IMAGE_TAG=${NITRO_VERSION} + - NITRO_DATETIME=$(git show -s --date=iso-strict --format=%cd) + - NITRO_MODIFIED="false" + - echo ${NITRO_VERSION} > ./.nitro-tag.txt + build: + commands: + - echo Build started on `date` + - echo Building the Docker image ${NITRO_VERSION}... + - DOCKER_BUILDKIT=1 docker build . -t nitro-node-slim --target nitro-node-slim --build-arg version=$NITRO_VERSION --build-arg datetime=$NITRO_DATETIME --build-arg modified=$NITRO_MODIFIED + - DOCKER_BUILDKIT=1 docker build . -t nitro-node --target nitro-node --build-arg version=$NITRO_VERSION --build-arg datetime=$NITRO_DATETIME --build-arg modified=$NITRO_MODIFIED + - DOCKER_BUILDKIT=1 docker build . -t nitro-node-dev --target nitro-node-dev --build-arg version=$NITRO_VERSION --build-arg datetime=$NITRO_DATETIME --build-arg modified=$NITRO_MODIFIED + - DOCKER_BUILDKIT=1 docker build . -t nitro-node-validator --target nitro-node-validator --build-arg version=$NITRO_VERSION --build-arg datetime=$NITRO_DATETIME --build-arg modified=$NITRO_MODIFIED + - docker tag nitro-node:latest $REPOSITORY_URI:$IMAGE_TAG-$ARCH_TAG + - docker tag nitro-node-slim:latest $REPOSITORY_URI:$IMAGE_TAG-slim-$ARCH_TAG + - docker tag nitro-node-dev:latest $REPOSITORY_URI:$IMAGE_TAG-dev-$ARCH_TAG + - docker tag nitro-node-validator:latest $REPOSITORY_URI:$IMAGE_TAG-validator-$ARCH_TAG + post_build: + commands: + - echo Build completed on `date` + - echo pushing to repo + - docker push $REPOSITORY_URI:$IMAGE_TAG-$ARCH_TAG + - docker push $REPOSITORY_URI:$IMAGE_TAG-slim-$ARCH_TAG + - docker push $REPOSITORY_URI:$IMAGE_TAG-dev-$ARCH_TAG + - docker push $REPOSITORY_URI:$IMAGE_TAG-validator-$ARCH_TAG diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index a944f08f40..8ed49634ad 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -169,6 +169,26 @@ jobs: run: | echo "Running redis tests" >> full.log TEST_REDIS=redis://localhost:6379/0 gotestsum --format short-verbose -- -p 1 -run TestRedis ./arbnode/... ./system_tests/... -coverprofile=coverage-redis.txt -covermode=atomic -coverpkg=./... + + - name: create block input json file + if: matrix.test-mode == 'defaults' + run: | + gotestsum --format short-verbose -- -run TestProgramStorage$ ./system_tests/... --count 1 --recordBlockInputs.WithBaseDir="${{ github.workspace }}/target" --recordBlockInputs.WithTimestampDirEnabled=false --recordBlockInputs.WithBlockIdInFileNameEnabled=false + + - name: run arbitrator prover on block input json + if: matrix.test-mode == 'defaults' + run: | + make build-prover-bin + target/bin/prover target/machines/latest/machine.wavm.br -b --json-inputs="${{ github.workspace }}/target/TestProgramStorage/block_inputs.json" + + - name: run jit prover on block input json + if: matrix.test-mode == 'defaults' + run: | + make build-jit + if [ -n "$(target/bin/jit --binary target/machines/latest/replay.wasm --cranelift --json-inputs='${{ github.workspace }}/target/TestProgramStorage/block_inputs.json')" ]; then + echo "Error: Command produced output." + exit 1 + fi - name: run challenge tests if: matrix.test-mode == 'challenge' diff --git a/Makefile b/Makefile index 88bbd8dabe..12dfb07cf8 100644 --- a/Makefile +++ b/Makefile @@ -155,6 +155,8 @@ stylus_test_hostio-test_src = $(call get_stylus_test_rust,hostio-test) stylus_test_wasms = $(stylus_test_keccak_wasm) $(stylus_test_keccak-100_wasm) $(stylus_test_fallible_wasm) $(stylus_test_storage_wasm) $(stylus_test_multicall_wasm) $(stylus_test_log_wasm) $(stylus_test_create_wasm) $(stylus_test_math_wasm) $(stylus_test_sdk-storage_wasm) $(stylus_test_erc20_wasm) $(stylus_test_read-return-data_wasm) $(stylus_test_evm-data_wasm) $(stylus_test_hostio-test_wasm) $(stylus_test_bfs:.b=.wasm) stylus_benchmarks = $(wildcard $(stylus_dir)/*.toml $(stylus_dir)/src/*.rs) $(stylus_test_wasms) +CBROTLI_WASM_BUILD_ARGS ?=-d + # user targets .PHONY: push @@ -579,9 +581,9 @@ contracts/test/prover/proofs/%.json: $(arbitrator_cases)/%.wasm $(prover_bin) @touch $@ .make/cbrotli-wasm: $(DEP_PREDICATE) $(ORDER_ONLY_PREDICATE) .make - test -f target/lib-wasm/libbrotlicommon-static.a || ./scripts/build-brotli.sh -w -d - test -f target/lib-wasm/libbrotlienc-static.a || ./scripts/build-brotli.sh -w -d - test -f target/lib-wasm/libbrotlidec-static.a || ./scripts/build-brotli.sh -w -d + test -f target/lib-wasm/libbrotlicommon-static.a || ./scripts/build-brotli.sh -w $(CBROTLI_WASM_BUILD_ARGS) + test -f target/lib-wasm/libbrotlienc-static.a || ./scripts/build-brotli.sh -w $(CBROTLI_WASM_BUILD_ARGS) + test -f target/lib-wasm/libbrotlidec-static.a || ./scripts/build-brotli.sh -w $(CBROTLI_WASM_BUILD_ARGS) @touch $@ .make/wasm-lib: $(DEP_PREDICATE) arbitrator/wasm-libraries/soft-float/SoftFloat/build/Wasm-Clang/softfloat.a $(ORDER_ONLY_PREDICATE) .make diff --git a/arbitrator/arbutil/src/evm/api.rs b/arbitrator/arbutil/src/evm/api.rs index 9d4c78c0de..0a603a3bb2 100644 --- a/arbitrator/arbutil/src/evm/api.rs +++ b/arbitrator/arbutil/src/evm/api.rs @@ -73,19 +73,103 @@ impl DataReader for VecReader { } } +macro_rules! derive_math { + ($t:ident) => { + impl std::ops::Add for $t { + type Output = Self; + + fn add(self, rhs: Self) -> Self { + Self(self.0 + rhs.0) + } + } + + impl std::ops::AddAssign for $t { + fn add_assign(&mut self, rhs: Self) { + self.0 += rhs.0; + } + } + + impl std::ops::Sub for $t { + type Output = Self; + + fn sub(self, rhs: Self) -> Self { + Self(self.0 - rhs.0) + } + } + + impl std::ops::SubAssign for $t { + fn sub_assign(&mut self, rhs: Self) { + self.0 -= rhs.0; + } + } + + impl std::ops::Mul for $t { + type Output = Self; + + fn mul(self, rhs: u64) -> Self { + Self(self.0 * rhs) + } + } + + impl std::ops::Mul<$t> for u64 { + type Output = $t; + + fn mul(self, rhs: $t) -> $t { + $t(self * rhs.0) + } + } + + impl $t { + /// Equivalent to the Add trait, but const. + pub const fn add(self, rhs: Self) -> Self { + Self(self.0 + rhs.0) + } + + /// Equivalent to the Sub trait, but const. + pub const fn sub(self, rhs: Self) -> Self { + Self(self.0 - rhs.0) + } + + pub const fn saturating_add(self, rhs: Self) -> Self { + Self(self.0.saturating_add(rhs.0)) + } + + pub const fn saturating_sub(self, rhs: Self) -> Self { + Self(self.0.saturating_sub(rhs.0)) + } + + pub fn to_be_bytes(self) -> [u8; 8] { + self.0.to_be_bytes() + } + } + }; +} + +#[derive(Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash, Debug, Default)] +#[must_use] +pub struct Gas(pub u64); + +derive_math!(Gas); + +#[derive(Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash, Debug, Default)] +#[must_use] +pub struct Ink(pub u64); + +derive_math!(Ink); + pub trait EvmApi: Send + 'static { /// Reads the 32-byte value in the EVM state trie at offset `key`. /// Returns the value and the access cost in gas. /// Analogous to `vm.SLOAD`. - fn get_bytes32(&mut self, key: Bytes32, evm_api_gas_to_use: u64) -> (Bytes32, u64); + fn get_bytes32(&mut self, key: Bytes32, evm_api_gas_to_use: Gas) -> (Bytes32, Gas); /// Stores the given value at the given key in Stylus VM's cache of the EVM state trie. /// Note that the actual values only get written after calls to `set_trie_slots`. - fn cache_bytes32(&mut self, key: Bytes32, value: Bytes32) -> u64; + fn cache_bytes32(&mut self, key: Bytes32, value: Bytes32) -> Gas; /// Persists any dirty values in the storage cache to the EVM state trie, dropping the cache entirely if requested. /// Analogous to repeated invocations of `vm.SSTORE`. - fn flush_storage_cache(&mut self, clear: bool, gas_left: u64) -> Result; + fn flush_storage_cache(&mut self, clear: bool, gas_left: Gas) -> Result; /// Reads the 32-byte value in the EVM's transient state trie at offset `key`. /// Analogous to `vm.TLOAD`. @@ -102,10 +186,10 @@ pub trait EvmApi: Send + 'static { &mut self, contract: Bytes20, calldata: &[u8], - gas_left: u64, - gas_req: u64, + gas_left: Gas, + gas_req: Gas, value: Bytes32, - ) -> (u32, u64, UserOutcomeKind); + ) -> (u32, Gas, UserOutcomeKind); /// Delegate-calls the contract at the given address. /// Returns the EVM return data's length, the gas cost, and whether the call succeeded. @@ -114,9 +198,9 @@ pub trait EvmApi: Send + 'static { &mut self, contract: Bytes20, calldata: &[u8], - gas_left: u64, - gas_req: u64, - ) -> (u32, u64, UserOutcomeKind); + gas_left: Gas, + gas_req: Gas, + ) -> (u32, Gas, UserOutcomeKind); /// Static-calls the contract at the given address. /// Returns the EVM return data's length, the gas cost, and whether the call succeeded. @@ -125,9 +209,9 @@ pub trait EvmApi: Send + 'static { &mut self, contract: Bytes20, calldata: &[u8], - gas_left: u64, - gas_req: u64, - ) -> (u32, u64, UserOutcomeKind); + gas_left: Gas, + gas_req: Gas, + ) -> (u32, Gas, UserOutcomeKind); /// Deploys a new contract using the init code provided. /// Returns the new contract's address on success, or the error reason on failure. @@ -137,8 +221,8 @@ pub trait EvmApi: Send + 'static { &mut self, code: Vec, endowment: Bytes32, - gas: u64, - ) -> (eyre::Result, u32, u64); + gas: Gas, + ) -> (eyre::Result, u32, Gas); /// Deploys a new contract using the init code provided, with an address determined in part by the `salt`. /// Returns the new contract's address on success, or the error reason on failure. @@ -149,8 +233,8 @@ pub trait EvmApi: Send + 'static { code: Vec, endowment: Bytes32, salt: Bytes32, - gas: u64, - ) -> (eyre::Result, u32, u64); + gas: Gas, + ) -> (eyre::Result, u32, Gas); /// Returns the EVM return data. /// Analogous to `vm.RETURNDATACOPY`. @@ -164,21 +248,21 @@ pub trait EvmApi: Send + 'static { /// Gets the balance of the given account. /// Returns the balance and the access cost in gas. /// Analogous to `vm.BALANCE`. - fn account_balance(&mut self, address: Bytes20) -> (Bytes32, u64); + fn account_balance(&mut self, address: Bytes20) -> (Bytes32, Gas); /// Returns the code and the access cost in gas. /// Analogous to `vm.EXTCODECOPY`. - fn account_code(&mut self, address: Bytes20, gas_left: u64) -> (D, u64); + fn account_code(&mut self, address: Bytes20, gas_left: Gas) -> (D, Gas); /// Gets the hash of the given address's code. /// Returns the hash and the access cost in gas. /// Analogous to `vm.EXTCODEHASH`. - fn account_codehash(&mut self, address: Bytes20) -> (Bytes32, u64); + fn account_codehash(&mut self, address: Bytes20) -> (Bytes32, Gas); /// Determines the cost in gas of allocating additional wasm pages. /// Note: has the side effect of updating Geth's memory usage tracker. /// Not analogous to any EVM opcode. - fn add_pages(&mut self, pages: u16) -> u64; + fn add_pages(&mut self, pages: u16) -> Gas; /// Captures tracing information for hostio invocations during native execution. fn capture_hostio( @@ -186,7 +270,7 @@ pub trait EvmApi: Send + 'static { name: &str, args: &[u8], outs: &[u8], - start_ink: u64, - end_ink: u64, + start_ink: Ink, + end_ink: Ink, ); } diff --git a/arbitrator/arbutil/src/evm/mod.rs b/arbitrator/arbutil/src/evm/mod.rs index 36dadd906a..063194b0c6 100644 --- a/arbitrator/arbutil/src/evm/mod.rs +++ b/arbitrator/arbutil/src/evm/mod.rs @@ -2,6 +2,7 @@ // For license information, see https://github.com/OffchainLabs/nitro/blob/master/LICENSE use crate::{Bytes20, Bytes32}; +use api::Gas; pub mod api; pub mod req; @@ -9,70 +10,70 @@ pub mod storage; pub mod user; // params.SstoreSentryGasEIP2200 -pub const SSTORE_SENTRY_GAS: u64 = 2300; +pub const SSTORE_SENTRY_GAS: Gas = Gas(2300); // params.ColdAccountAccessCostEIP2929 -pub const COLD_ACCOUNT_GAS: u64 = 2600; +pub const COLD_ACCOUNT_GAS: Gas = Gas(2600); // params.ColdSloadCostEIP2929 -pub const COLD_SLOAD_GAS: u64 = 2100; +pub const COLD_SLOAD_GAS: Gas = Gas(2100); // params.WarmStorageReadCostEIP2929 -pub const WARM_SLOAD_GAS: u64 = 100; +pub const WARM_SLOAD_GAS: Gas = Gas(100); // params.WarmStorageReadCostEIP2929 (see enable1153 in jump_table.go) -pub const TLOAD_GAS: u64 = WARM_SLOAD_GAS; -pub const TSTORE_GAS: u64 = WARM_SLOAD_GAS; +pub const TLOAD_GAS: Gas = WARM_SLOAD_GAS; +pub const TSTORE_GAS: Gas = WARM_SLOAD_GAS; // params.LogGas and params.LogDataGas -pub const LOG_TOPIC_GAS: u64 = 375; -pub const LOG_DATA_GAS: u64 = 8; +pub const LOG_TOPIC_GAS: Gas = Gas(375); +pub const LOG_DATA_GAS: Gas = Gas(8); // params.CopyGas -pub const COPY_WORD_GAS: u64 = 3; +pub const COPY_WORD_GAS: Gas = Gas(3); // params.Keccak256Gas -pub const KECCAK_256_GAS: u64 = 30; -pub const KECCAK_WORD_GAS: u64 = 6; +pub const KECCAK_256_GAS: Gas = Gas(30); +pub const KECCAK_WORD_GAS: Gas = Gas(6); // vm.GasQuickStep (see gas.go) -pub const GAS_QUICK_STEP: u64 = 2; +pub const GAS_QUICK_STEP: Gas = Gas(2); // vm.GasQuickStep (see jump_table.go) -pub const ADDRESS_GAS: u64 = GAS_QUICK_STEP; +pub const ADDRESS_GAS: Gas = GAS_QUICK_STEP; // vm.GasQuickStep (see eips.go) -pub const BASEFEE_GAS: u64 = GAS_QUICK_STEP; +pub const BASEFEE_GAS: Gas = GAS_QUICK_STEP; // vm.GasQuickStep (see eips.go) -pub const CHAINID_GAS: u64 = GAS_QUICK_STEP; +pub const CHAINID_GAS: Gas = GAS_QUICK_STEP; // vm.GasQuickStep (see jump_table.go) -pub const COINBASE_GAS: u64 = GAS_QUICK_STEP; +pub const COINBASE_GAS: Gas = GAS_QUICK_STEP; // vm.GasQuickStep (see jump_table.go) -pub const GASLIMIT_GAS: u64 = GAS_QUICK_STEP; +pub const GASLIMIT_GAS: Gas = GAS_QUICK_STEP; // vm.GasQuickStep (see jump_table.go) -pub const NUMBER_GAS: u64 = GAS_QUICK_STEP; +pub const NUMBER_GAS: Gas = GAS_QUICK_STEP; // vm.GasQuickStep (see jump_table.go) -pub const TIMESTAMP_GAS: u64 = GAS_QUICK_STEP; +pub const TIMESTAMP_GAS: Gas = GAS_QUICK_STEP; // vm.GasQuickStep (see jump_table.go) -pub const GASLEFT_GAS: u64 = GAS_QUICK_STEP; +pub const GASLEFT_GAS: Gas = GAS_QUICK_STEP; // vm.GasQuickStep (see jump_table.go) -pub const CALLER_GAS: u64 = GAS_QUICK_STEP; +pub const CALLER_GAS: Gas = GAS_QUICK_STEP; // vm.GasQuickStep (see jump_table.go) -pub const CALLVALUE_GAS: u64 = GAS_QUICK_STEP; +pub const CALLVALUE_GAS: Gas = GAS_QUICK_STEP; // vm.GasQuickStep (see jump_table.go) -pub const GASPRICE_GAS: u64 = GAS_QUICK_STEP; +pub const GASPRICE_GAS: Gas = GAS_QUICK_STEP; // vm.GasQuickStep (see jump_table.go) -pub const ORIGIN_GAS: u64 = GAS_QUICK_STEP; +pub const ORIGIN_GAS: Gas = GAS_QUICK_STEP; pub const ARBOS_VERSION_STYLUS_CHARGING_FIXES: u64 = 32; diff --git a/arbitrator/arbutil/src/evm/req.rs b/arbitrator/arbutil/src/evm/req.rs index 0304f2d378..621f41e951 100644 --- a/arbitrator/arbutil/src/evm/req.rs +++ b/arbitrator/arbutil/src/evm/req.rs @@ -12,8 +12,10 @@ use crate::{ use eyre::{bail, eyre, Result}; use std::collections::hash_map::Entry; +use super::api::{Gas, Ink}; + pub trait RequestHandler: Send + 'static { - fn request(&mut self, req_type: EvmApiMethod, req_data: impl AsRef<[u8]>) -> (Vec, D, u64); + fn request(&mut self, req_type: EvmApiMethod, req_data: impl AsRef<[u8]>) -> (Vec, D, Gas); } pub struct EvmApiRequestor> { @@ -33,7 +35,7 @@ impl> EvmApiRequestor { } } - fn request(&mut self, req_type: EvmApiMethod, req_data: impl AsRef<[u8]>) -> (Vec, D, u64) { + fn request(&mut self, req_type: EvmApiMethod, req_data: impl AsRef<[u8]>) -> (Vec, D, Gas) { self.handler.request(req_type, req_data) } @@ -43,10 +45,10 @@ impl> EvmApiRequestor { call_type: EvmApiMethod, contract: Bytes20, input: &[u8], - gas_left: u64, - gas_req: u64, + gas_left: Gas, + gas_req: Gas, value: Bytes32, - ) -> (u32, u64, UserOutcomeKind) { + ) -> (u32, Gas, UserOutcomeKind) { let mut request = Vec::with_capacity(20 + 32 + 8 + 8 + input.len()); request.extend(contract); request.extend(value); @@ -71,8 +73,8 @@ impl> EvmApiRequestor { code: Vec, endowment: Bytes32, salt: Option, - gas: u64, - ) -> (Result, u32, u64) { + gas: Gas, + ) -> (Result, u32, Gas) { let mut request = Vec::with_capacity(8 + 2 * 32 + code.len()); request.extend(gas.to_be_bytes()); request.extend(endowment); @@ -98,7 +100,7 @@ impl> EvmApiRequestor { } impl> EvmApi for EvmApiRequestor { - fn get_bytes32(&mut self, key: Bytes32, evm_api_gas_to_use: u64) -> (Bytes32, u64) { + fn get_bytes32(&mut self, key: Bytes32, evm_api_gas_to_use: Gas) -> (Bytes32, Gas) { let cache = &mut self.storage_cache; let mut cost = cache.read_gas(); @@ -110,7 +112,7 @@ impl> EvmApi for EvmApiRequestor { (value.value, cost) } - fn cache_bytes32(&mut self, key: Bytes32, value: Bytes32) -> u64 { + fn cache_bytes32(&mut self, key: Bytes32, value: Bytes32) -> Gas { let cost = self.storage_cache.write_gas(); match self.storage_cache.entry(key) { Entry::Occupied(mut key) => key.get_mut().value = value, @@ -119,7 +121,7 @@ impl> EvmApi for EvmApiRequestor { cost } - fn flush_storage_cache(&mut self, clear: bool, gas_left: u64) -> Result { + fn flush_storage_cache(&mut self, clear: bool, gas_left: Gas) -> Result { let mut data = Vec::with_capacity(64 * self.storage_cache.len() + 8); data.extend(gas_left.to_be_bytes()); @@ -134,7 +136,7 @@ impl> EvmApi for EvmApiRequestor { self.storage_cache.clear(); } if data.len() == 8 { - return Ok(0); // no need to make request + return Ok(Gas(0)); // no need to make request } let (res, _, cost) = self.request(EvmApiMethod::SetTrieSlots, data); @@ -174,10 +176,10 @@ impl> EvmApi for EvmApiRequestor { &mut self, contract: Bytes20, input: &[u8], - gas_left: u64, - gas_req: u64, + gas_left: Gas, + gas_req: Gas, value: Bytes32, - ) -> (u32, u64, UserOutcomeKind) { + ) -> (u32, Gas, UserOutcomeKind) { self.call_request( EvmApiMethod::ContractCall, contract, @@ -192,9 +194,9 @@ impl> EvmApi for EvmApiRequestor { &mut self, contract: Bytes20, input: &[u8], - gas_left: u64, - gas_req: u64, - ) -> (u32, u64, UserOutcomeKind) { + gas_left: Gas, + gas_req: Gas, + ) -> (u32, Gas, UserOutcomeKind) { self.call_request( EvmApiMethod::DelegateCall, contract, @@ -209,9 +211,9 @@ impl> EvmApi for EvmApiRequestor { &mut self, contract: Bytes20, input: &[u8], - gas_left: u64, - gas_req: u64, - ) -> (u32, u64, UserOutcomeKind) { + gas_left: Gas, + gas_req: Gas, + ) -> (u32, Gas, UserOutcomeKind) { self.call_request( EvmApiMethod::StaticCall, contract, @@ -226,8 +228,8 @@ impl> EvmApi for EvmApiRequestor { &mut self, code: Vec, endowment: Bytes32, - gas: u64, - ) -> (Result, u32, u64) { + gas: Gas, + ) -> (Result, u32, Gas) { self.create_request(EvmApiMethod::Create1, code, endowment, None, gas) } @@ -236,8 +238,8 @@ impl> EvmApi for EvmApiRequestor { code: Vec, endowment: Bytes32, salt: Bytes32, - gas: u64, - ) -> (Result, u32, u64) { + gas: Gas, + ) -> (Result, u32, Gas) { self.create_request(EvmApiMethod::Create2, code, endowment, Some(salt), gas) } @@ -258,15 +260,15 @@ impl> EvmApi for EvmApiRequestor { Ok(()) } - fn account_balance(&mut self, address: Bytes20) -> (Bytes32, u64) { + fn account_balance(&mut self, address: Bytes20) -> (Bytes32, Gas) { let (res, _, cost) = self.request(EvmApiMethod::AccountBalance, address); (res.try_into().unwrap(), cost) } - fn account_code(&mut self, address: Bytes20, gas_left: u64) -> (D, u64) { + fn account_code(&mut self, address: Bytes20, gas_left: Gas) -> (D, Gas) { if let Some((stored_address, data)) = self.last_code.as_ref() { if address == *stored_address { - return (data.clone(), 0); + return (data.clone(), Gas(0)); } } let mut req = Vec::with_capacity(20 + 8); @@ -278,12 +280,12 @@ impl> EvmApi for EvmApiRequestor { (data, cost) } - fn account_codehash(&mut self, address: Bytes20) -> (Bytes32, u64) { + fn account_codehash(&mut self, address: Bytes20) -> (Bytes32, Gas) { let (res, _, cost) = self.request(EvmApiMethod::AccountCodeHash, address); (res.try_into().unwrap(), cost) } - fn add_pages(&mut self, pages: u16) -> u64 { + fn add_pages(&mut self, pages: u16) -> Gas { self.request(EvmApiMethod::AddPages, pages.to_be_bytes()).2 } @@ -292,8 +294,8 @@ impl> EvmApi for EvmApiRequestor { name: &str, args: &[u8], outs: &[u8], - start_ink: u64, - end_ink: u64, + start_ink: Ink, + end_ink: Ink, ) { let mut request = Vec::with_capacity(2 * 8 + 3 * 2 + name.len() + args.len() + outs.len()); request.extend(start_ink.to_be_bytes()); @@ -305,6 +307,7 @@ impl> EvmApi for EvmApiRequestor { request.extend(name.as_bytes()); request.extend(args); request.extend(outs); - self.request(EvmApiMethod::CaptureHostIO, request); + // ignore response (including gas) as we're just tracing + _ = self.request(EvmApiMethod::CaptureHostIO, request); } } diff --git a/arbitrator/arbutil/src/evm/storage.rs b/arbitrator/arbutil/src/evm/storage.rs index 32b60dd21b..5f688364d7 100644 --- a/arbitrator/arbutil/src/evm/storage.rs +++ b/arbitrator/arbutil/src/evm/storage.rs @@ -5,6 +5,8 @@ use crate::Bytes32; use fnv::FnvHashMap as HashMap; use std::ops::{Deref, DerefMut}; +use super::api::Gas; + /// Represents the EVM word at a given key. #[derive(Debug)] pub struct StorageWord { @@ -37,23 +39,23 @@ pub struct StorageCache { } impl StorageCache { - pub const REQUIRED_ACCESS_GAS: u64 = 10; + pub const REQUIRED_ACCESS_GAS: Gas = Gas(10); - pub fn read_gas(&mut self) -> u64 { + pub fn read_gas(&mut self) -> Gas { self.reads += 1; match self.reads { - 0..=32 => 0, - 33..=128 => 2, - _ => 10, + 0..=32 => Gas(0), + 33..=128 => Gas(2), + _ => Gas(10), } } - pub fn write_gas(&mut self) -> u64 { + pub fn write_gas(&mut self) -> Gas { self.writes += 1; match self.writes { - 0..=8 => 0, - 9..=64 => 7, - _ => 10, + 0..=8 => Gas(0), + 9..=64 => Gas(7), + _ => Gas(10), } } } diff --git a/arbitrator/arbutil/src/pricing.rs b/arbitrator/arbutil/src/pricing.rs index 4614b02a2a..4d6bf827be 100644 --- a/arbitrator/arbutil/src/pricing.rs +++ b/arbitrator/arbutil/src/pricing.rs @@ -1,20 +1,22 @@ // Copyright 2023, Offchain Labs, Inc. // For license information, see https://github.com/OffchainLabs/nitro/blob/master/LICENSE +use crate::evm::api::Ink; + /// For hostios that may return something. -pub const HOSTIO_INK: u64 = 8400; +pub const HOSTIO_INK: Ink = Ink(8400); /// For hostios that include pointers. -pub const PTR_INK: u64 = 13440 - HOSTIO_INK; +pub const PTR_INK: Ink = Ink(13440).sub(HOSTIO_INK); /// For hostios that involve an API cost. -pub const EVM_API_INK: u64 = 59673; +pub const EVM_API_INK: Ink = Ink(59673); /// For hostios that involve a div or mod. -pub const DIV_INK: u64 = 20000; +pub const DIV_INK: Ink = Ink(20000); /// For hostios that involve a mulmod. -pub const MUL_MOD_INK: u64 = 24100; +pub const MUL_MOD_INK: Ink = Ink(24100); /// For hostios that involve an addmod. -pub const ADD_MOD_INK: u64 = 21000; +pub const ADD_MOD_INK: Ink = Ink(21000); diff --git a/arbitrator/jit/src/machine.rs b/arbitrator/jit/src/machine.rs index 02523f740a..0d74c74ef6 100644 --- a/arbitrator/jit/src/machine.rs +++ b/arbitrator/jit/src/machine.rs @@ -2,8 +2,8 @@ // For license information, see https://github.com/nitro/blob/master/LICENSE use crate::{ - arbcompress, caller_env::GoRuntimeState, program, socket, stylus_backend::CothreadHandler, - wasip1_stub, wavmio, Opts, + arbcompress, caller_env::GoRuntimeState, prepare::prepare_env, program, socket, + stylus_backend::CothreadHandler, wasip1_stub, wavmio, Opts, }; use arbutil::{Bytes32, Color, PreimageType}; use eyre::{bail, ErrReport, Result, WrapErr}; @@ -215,72 +215,76 @@ pub struct WasmEnv { impl WasmEnv { pub fn cli(opts: &Opts) -> Result { - let mut env = WasmEnv::default(); - env.process.forks = opts.forks; - env.process.debug = opts.debug; + if let Some(json_inputs) = opts.json_inputs.clone() { + prepare_env(json_inputs, opts.debug) + } else { + let mut env = WasmEnv::default(); + env.process.forks = opts.forks; + env.process.debug = opts.debug; - let mut inbox_position = opts.inbox_position; - let mut delayed_position = opts.delayed_inbox_position; + let mut inbox_position = opts.inbox_position; + let mut delayed_position = opts.delayed_inbox_position; - for path in &opts.inbox { - let mut msg = vec![]; - File::open(path)?.read_to_end(&mut msg)?; - env.sequencer_messages.insert(inbox_position, msg); - inbox_position += 1; - } - for path in &opts.delayed_inbox { - let mut msg = vec![]; - File::open(path)?.read_to_end(&mut msg)?; - env.delayed_messages.insert(delayed_position, msg); - delayed_position += 1; - } + for path in &opts.inbox { + let mut msg = vec![]; + File::open(path)?.read_to_end(&mut msg)?; + env.sequencer_messages.insert(inbox_position, msg); + inbox_position += 1; + } + for path in &opts.delayed_inbox { + let mut msg = vec![]; + File::open(path)?.read_to_end(&mut msg)?; + env.delayed_messages.insert(delayed_position, msg); + delayed_position += 1; + } - if let Some(path) = &opts.preimages { - let mut file = BufReader::new(File::open(path)?); - let mut preimages = Vec::new(); - let filename = path.to_string_lossy(); - loop { - let mut size_buf = [0u8; 8]; - match file.read_exact(&mut size_buf) { - Ok(()) => {} - Err(err) if err.kind() == ErrorKind::UnexpectedEof => break, - Err(err) => bail!("Failed to parse {filename}: {}", err), + if let Some(path) = &opts.preimages { + let mut file = BufReader::new(File::open(path)?); + let mut preimages = Vec::new(); + let filename = path.to_string_lossy(); + loop { + let mut size_buf = [0u8; 8]; + match file.read_exact(&mut size_buf) { + Ok(()) => {} + Err(err) if err.kind() == ErrorKind::UnexpectedEof => break, + Err(err) => bail!("Failed to parse {filename}: {}", err), + } + let size = u64::from_le_bytes(size_buf) as usize; + let mut buf = vec![0u8; size]; + file.read_exact(&mut buf)?; + preimages.push(buf); + } + let keccak_preimages = env.preimages.entry(PreimageType::Keccak256).or_default(); + for preimage in preimages { + let mut hasher = Keccak256::new(); + hasher.update(&preimage); + let hash = hasher.finalize().into(); + keccak_preimages.insert(hash, preimage); } - let size = u64::from_le_bytes(size_buf) as usize; - let mut buf = vec![0u8; size]; - file.read_exact(&mut buf)?; - preimages.push(buf); - } - let keccak_preimages = env.preimages.entry(PreimageType::Keccak256).or_default(); - for preimage in preimages { - let mut hasher = Keccak256::new(); - hasher.update(&preimage); - let hash = hasher.finalize().into(); - keccak_preimages.insert(hash, preimage); } - } - fn parse_hex(arg: &Option, name: &str) -> Result { - match arg { - Some(arg) => { - let mut arg = arg.as_str(); - if arg.starts_with("0x") { - arg = &arg[2..]; + fn parse_hex(arg: &Option, name: &str) -> Result { + match arg { + Some(arg) => { + let mut arg = arg.as_str(); + if arg.starts_with("0x") { + arg = &arg[2..]; + } + let mut bytes32 = [0u8; 32]; + hex::decode_to_slice(arg, &mut bytes32) + .wrap_err_with(|| format!("failed to parse {} contents", name))?; + Ok(bytes32.into()) } - let mut bytes32 = [0u8; 32]; - hex::decode_to_slice(arg, &mut bytes32) - .wrap_err_with(|| format!("failed to parse {} contents", name))?; - Ok(bytes32.into()) + None => Ok(Bytes32::default()), } - None => Ok(Bytes32::default()), } - } - let last_block_hash = parse_hex(&opts.last_block_hash, "--last-block-hash")?; - let last_send_root = parse_hex(&opts.last_send_root, "--last-send-root")?; - env.small_globals = [opts.inbox_position, opts.position_within_message]; - env.large_globals = [last_block_hash, last_send_root]; - Ok(env) + let last_block_hash = parse_hex(&opts.last_block_hash, "--last-block-hash")?; + let last_send_root = parse_hex(&opts.last_send_root, "--last-send-root")?; + env.small_globals = [opts.inbox_position, opts.position_within_message]; + env.large_globals = [last_block_hash, last_send_root]; + Ok(env) + } } pub fn send_results(&mut self, error: Option, memory_used: Pages) { diff --git a/arbitrator/jit/src/main.rs b/arbitrator/jit/src/main.rs index e432dc215c..6e44500215 100644 --- a/arbitrator/jit/src/main.rs +++ b/arbitrator/jit/src/main.rs @@ -10,6 +10,7 @@ use structopt::StructOpt; mod arbcompress; mod caller_env; mod machine; +mod prepare; mod program; mod socket; mod stylus_backend; @@ -46,6 +47,10 @@ pub struct Opts { debug: bool, #[structopt(long)] require_success: bool, + // JSON inputs supercede any of the command-line inputs which could + // be specified in the JSON file. + #[structopt(long)] + json_inputs: Option, } fn main() -> Result<()> { diff --git a/arbitrator/jit/src/prepare.rs b/arbitrator/jit/src/prepare.rs new file mode 100644 index 0000000000..e7a7ba0f4d --- /dev/null +++ b/arbitrator/jit/src/prepare.rs @@ -0,0 +1,73 @@ +// Copyright 2022-2024, Offchain Labs, Inc. +// For license information, see https://github.com/nitro/blob/master/LICENSE + +use crate::WasmEnv; +use arbutil::{Bytes32, PreimageType}; +use eyre::Ok; +use prover::parse_input::FileData; +use std::env; +use std::fs::File; +use std::io::BufReader; +use std::path::PathBuf; + +// local_target matches rawdb.LocalTarget() on the go side. +// While generating json_inputs file, one should make sure user_wasms map +// has entry for the system's arch that jit validation is being run on +pub fn local_target() -> String { + if env::consts::OS == "linux" { + match env::consts::ARCH { + "aarch64" => "arm64".to_string(), + "x86_64" => "amd64".to_string(), + _ => "host".to_string(), + } + } else { + "host".to_string() + } +} + +pub fn prepare_env(json_inputs: PathBuf, debug: bool) -> eyre::Result { + let file = File::open(json_inputs)?; + let reader = BufReader::new(file); + + let data = FileData::from_reader(reader)?; + + let mut env = WasmEnv::default(); + env.process.forks = false; // Should be set to false when using json_inputs + env.process.debug = debug; + + let block_hash: [u8; 32] = data.start_state.block_hash.try_into().unwrap(); + let block_hash: Bytes32 = block_hash.into(); + let send_root: [u8; 32] = data.start_state.send_root.try_into().unwrap(); + let send_root: Bytes32 = send_root.into(); + let bytes32_vals: [Bytes32; 2] = [block_hash, send_root]; + let u64_vals: [u64; 2] = [data.start_state.batch, data.start_state.pos_in_batch]; + env.small_globals = u64_vals; + env.large_globals = bytes32_vals; + + for batch_info in data.batch_info.iter() { + env.sequencer_messages + .insert(batch_info.number, batch_info.data_b64.clone()); + } + + if data.delayed_msg_nr != 0 && !data.delayed_msg_b64.is_empty() { + env.delayed_messages + .insert(data.delayed_msg_nr, data.delayed_msg_b64.clone()); + } + + for (ty, inner_map) in data.preimages_b64 { + let preimage_ty = PreimageType::try_from(ty as u8)?; + let map = env.preimages.entry(preimage_ty).or_default(); + for (hash, preimage) in inner_map { + map.insert(hash, preimage); + } + } + + if let Some(user_wasms) = data.user_wasms.get(&local_target()) { + for (module_hash, module_asm) in user_wasms.iter() { + env.module_asms + .insert(*module_hash, module_asm.as_vec().into()); + } + } + + Ok(env) +} diff --git a/arbitrator/jit/src/program.rs b/arbitrator/jit/src/program.rs index 084afe96bc..f10a059748 100644 --- a/arbitrator/jit/src/program.rs +++ b/arbitrator/jit/src/program.rs @@ -6,6 +6,7 @@ use crate::caller_env::JitEnv; use crate::machine::{Escape, MaybeEscape, WasmEnvMut}; use crate::stylus_backend::exec_wasm; +use arbutil::evm::api::Gas; use arbutil::Bytes32; use arbutil::{evm::EvmData, format::DebugBytes, heapify}; use caller_env::{GuestPtr, MemAccess}; @@ -131,7 +132,7 @@ pub fn new_program( // buy ink let pricing = config.stylus.pricing; - let ink = pricing.gas_to_ink(gas); + let ink = pricing.gas_to_ink(Gas(gas)); let Some(module) = exec.module_asms.get(&compiled_hash).cloned() else { return Err(Escape::Failure(format!( @@ -217,7 +218,7 @@ pub fn set_response( let raw_data = mem.read_slice(raw_data_ptr, raw_data_len as usize); let thread = exec.threads.last_mut().unwrap(); - thread.set_response(id, result, raw_data, gas) + thread.set_response(id, result, raw_data, Gas(gas)) } /// sends previos response diff --git a/arbitrator/jit/src/stylus_backend.rs b/arbitrator/jit/src/stylus_backend.rs index 61dbf258d4..0d8c477c6c 100644 --- a/arbitrator/jit/src/stylus_backend.rs +++ b/arbitrator/jit/src/stylus_backend.rs @@ -4,7 +4,7 @@ #![allow(clippy::too_many_arguments)] use crate::machine::{Escape, MaybeEscape}; -use arbutil::evm::api::VecReader; +use arbutil::evm::api::{Gas, Ink, VecReader}; use arbutil::evm::{ api::{EvmApiMethod, EVM_API_METHOD_REQ_OFFSET}, req::EvmApiRequestor, @@ -28,7 +28,7 @@ use stylus::{native::NativeInstance, run::RunProgram}; struct MessageToCothread { result: Vec, raw_data: Vec, - cost: u64, + cost: Gas, } #[derive(Clone)] @@ -47,7 +47,7 @@ impl RequestHandler for CothreadRequestor { &mut self, req_type: EvmApiMethod, req_data: impl AsRef<[u8]>, - ) -> (Vec, VecReader, u64) { + ) -> (Vec, VecReader, Gas) { let msg = MessageFromCothread { req_type: req_type as u32 + EVM_API_METHOD_REQ_OFFSET, req_data: req_data.as_ref().to_vec(), @@ -104,7 +104,7 @@ impl CothreadHandler { id: u32, result: Vec, raw_data: Vec, - cost: u64, + cost: Gas, ) -> MaybeEscape { let Some(msg) = self.last_request.clone() else { return Escape::hostio("trying to set response but no message pending"); @@ -131,7 +131,7 @@ pub fn exec_wasm( compile: CompileConfig, config: StylusConfig, evm_data: EvmData, - ink: u64, + ink: Ink, ) -> Result { let (tothread_tx, tothread_rx) = mpsc::sync_channel::(0); let (fromthread_tx, fromthread_rx) = mpsc::sync_channel::(0); @@ -150,7 +150,7 @@ pub fn exec_wasm( let outcome = instance.run_main(&calldata, config, ink); let ink_left = match outcome.as_ref() { - Ok(UserOutcome::OutOfStack) => 0, // take all ink when out of stack + Ok(UserOutcome::OutOfStack) => Ink(0), // take all ink when out of stack _ => instance.ink_left().into(), }; diff --git a/arbitrator/prover/src/machine.rs b/arbitrator/prover/src/machine.rs index 4ece1f7bf2..dec355ac7c 100644 --- a/arbitrator/prover/src/machine.rs +++ b/arbitrator/prover/src/machine.rs @@ -1816,7 +1816,12 @@ impl Machine { } #[cfg(feature = "native")] - pub fn call_user_func(&mut self, func: &str, args: Vec, ink: u64) -> Result> { + pub fn call_user_func( + &mut self, + func: &str, + args: Vec, + ink: arbutil::evm::api::Ink, + ) -> Result> { self.set_ink(ink); self.call_function("user", func, args) } diff --git a/arbitrator/prover/src/merkle.rs b/arbitrator/prover/src/merkle.rs index 4a1278b4cb..fbd704dfc6 100644 --- a/arbitrator/prover/src/merkle.rs +++ b/arbitrator/prover/src/merkle.rs @@ -549,8 +549,7 @@ mod test { let mut empty_node = Bytes32([ 57, 29, 211, 154, 252, 227, 18, 99, 65, 126, 203, 166, 252, 232, 32, 3, 98, 194, 254, 186, 118, 14, 139, 192, 101, 156, 55, 194, 101, 11, 11, 168, - ]) - .clone(); + ]); for _ in 0..64 { print!("Bytes32(["); for i in 0..32 { @@ -607,7 +606,7 @@ mod test { for layer in 0..64 { // empty_hash_at is just a lookup, but empty_hash is calculated iteratively. assert_eq!(empty_hash_at(ty, layer), &empty_hash); - empty_hash = hash_node(ty, &empty_hash, &empty_hash); + empty_hash = hash_node(ty, empty_hash, empty_hash); } } } diff --git a/arbitrator/prover/src/programs/config.rs b/arbitrator/prover/src/programs/config.rs index 0353589358..bd6fb3a843 100644 --- a/arbitrator/prover/src/programs/config.rs +++ b/arbitrator/prover/src/programs/config.rs @@ -4,6 +4,7 @@ #![allow(clippy::field_reassign_with_default)] use crate::{programs::meter, value::FunctionType}; +use arbutil::evm::api::{Gas, Ink}; use derivative::Derivative; use fnv::FnvHashMap as HashMap; use std::fmt::Debug; @@ -72,12 +73,12 @@ impl PricingParams { Self { ink_price } } - pub fn gas_to_ink(&self, gas: u64) -> u64 { - gas.saturating_mul(self.ink_price.into()) + pub fn gas_to_ink(&self, gas: Gas) -> Ink { + Ink(gas.0.saturating_mul(self.ink_price.into())) } - pub fn ink_to_gas(&self, ink: u64) -> u64 { - ink / self.ink_price as u64 // never 0 + pub fn ink_to_gas(&self, ink: Ink) -> Gas { + Gas(ink.0 / self.ink_price as u64) // ink_price is never 0 } } diff --git a/arbitrator/prover/src/programs/memory.rs b/arbitrator/prover/src/programs/memory.rs index 7253b59dc4..82c4d4469d 100644 --- a/arbitrator/prover/src/programs/memory.rs +++ b/arbitrator/prover/src/programs/memory.rs @@ -1,6 +1,8 @@ // Copyright 2023, Offchain Labs, Inc. // For license information, see https://github.com/OffchainLabs/nitro/blob/master/LICENSE +use arbutil::evm::api::Gas; + #[derive(Clone, Copy, Debug)] #[repr(C)] pub struct MemoryModel { @@ -28,20 +30,20 @@ impl MemoryModel { } /// Determines the gas cost of allocating `new` pages given `open` are active and `ever` have ever been. - pub fn gas_cost(&self, new: u16, open: u16, ever: u16) -> u64 { + pub fn gas_cost(&self, new: u16, open: u16, ever: u16) -> Gas { let new_open = open.saturating_add(new); let new_ever = ever.max(new_open); // free until expansion beyond the first few if new_ever <= self.free_pages { - return 0; + return Gas(0); } let credit = |pages: u16| pages.saturating_sub(self.free_pages); let adding = credit(new_open).saturating_sub(credit(open)) as u64; let linear = adding.saturating_mul(self.page_gas.into()); let expand = Self::exp(new_ever) - Self::exp(ever); - linear.saturating_add(expand) + Gas(linear.saturating_add(expand)) } fn exp(pages: u16) -> u64 { @@ -81,14 +83,14 @@ fn test_model() { let model = MemoryModel::new(2, 1000); for jump in 1..=128 { - let mut total = 0; + let mut total = Gas(0); let mut pages = 0; while pages < 128 { let jump = jump.min(128 - pages); total += model.gas_cost(jump, pages, pages); pages += jump; } - assert_eq!(total, 31999998); + assert_eq!(total, Gas(31999998)); } for jump in 1..=128 { @@ -98,7 +100,7 @@ fn test_model() { let mut adds = 0; while ever < 128 { let jump = jump.min(128 - open); - total += model.gas_cost(jump, open, ever); + total += model.gas_cost(jump, open, ever).0; open += jump; ever = ever.max(open); @@ -114,12 +116,12 @@ fn test_model() { } // check saturation - assert_eq!(u64::MAX, model.gas_cost(129, 0, 0)); - assert_eq!(u64::MAX, model.gas_cost(u16::MAX, 0, 0)); + assert_eq!(Gas(u64::MAX), model.gas_cost(129, 0, 0)); + assert_eq!(Gas(u64::MAX), model.gas_cost(u16::MAX, 0, 0)); // check free pages let model = MemoryModel::new(128, 1000); - assert_eq!(0, model.gas_cost(128, 0, 0)); - assert_eq!(0, model.gas_cost(128, 0, 128)); - assert_eq!(u64::MAX, model.gas_cost(129, 0, 0)); + assert_eq!(Gas(0), model.gas_cost(128, 0, 0)); + assert_eq!(Gas(0), model.gas_cost(128, 0, 128)); + assert_eq!(Gas(u64::MAX), model.gas_cost(129, 0, 0)); } diff --git a/arbitrator/prover/src/programs/meter.rs b/arbitrator/prover/src/programs/meter.rs index ab069fd911..0d7b3151d7 100644 --- a/arbitrator/prover/src/programs/meter.rs +++ b/arbitrator/prover/src/programs/meter.rs @@ -9,7 +9,14 @@ use crate::{ value::FunctionType, Machine, }; -use arbutil::{evm, operator::OperatorInfo, Bytes32}; +use arbutil::{ + evm::{ + self, + api::{Gas, Ink}, + }, + operator::OperatorInfo, + Bytes32, +}; use derivative::Derivative; use eyre::Result; use fnv::FnvHashMap as HashMap; @@ -188,15 +195,15 @@ impl<'a, F: OpcodePricer> FuncMiddleware<'a> for FuncMeter<'a, F> { #[derive(Clone, Copy, Debug, PartialEq, Eq)] pub enum MachineMeter { - Ready(u64), + Ready(Ink), Exhausted, } impl MachineMeter { - pub fn ink(self) -> u64 { + pub fn ink(self) -> Ink { match self { Self::Ready(ink) => ink, - Self::Exhausted => 0, + Self::Exhausted => Ink(0), } } @@ -210,8 +217,8 @@ impl MachineMeter { /// We don't implement `From` since it's unclear what 0 would map to #[allow(clippy::from_over_into)] -impl Into for MachineMeter { - fn into(self) -> u64 { +impl Into for MachineMeter { + fn into(self) -> Ink { self.ink() } } @@ -219,7 +226,7 @@ impl Into for MachineMeter { impl Display for MachineMeter { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { match self { - Self::Ready(ink) => write!(f, "{ink} ink"), + Self::Ready(ink) => write!(f, "{} ink", ink.0), Self::Exhausted => write!(f, "exhausted"), } } @@ -241,7 +248,7 @@ pub trait MeteredMachine { fn ink_left(&self) -> MachineMeter; fn set_meter(&mut self, meter: MachineMeter); - fn set_ink(&mut self, ink: u64) { + fn set_ink(&mut self, ink: Ink) { self.set_meter(MachineMeter::Ready(ink)); } @@ -250,14 +257,14 @@ pub trait MeteredMachine { Err(OutOfInkError) } - fn ink_ready(&mut self) -> Result { + fn ink_ready(&mut self) -> Result { let MachineMeter::Ready(ink_left) = self.ink_left() else { return self.out_of_ink(); }; Ok(ink_left) } - fn buy_ink(&mut self, ink: u64) -> Result<(), OutOfInkError> { + fn buy_ink(&mut self, ink: Ink) -> Result<(), OutOfInkError> { let ink_left = self.ink_ready()?; if ink_left < ink { return self.out_of_ink(); @@ -267,7 +274,7 @@ pub trait MeteredMachine { } /// Checks if the user has enough ink, but doesn't burn any - fn require_ink(&mut self, ink: u64) -> Result<(), OutOfInkError> { + fn require_ink(&mut self, ink: Ink) -> Result<(), OutOfInkError> { let ink_left = self.ink_ready()?; if ink_left < ink { return self.out_of_ink(); @@ -277,18 +284,18 @@ pub trait MeteredMachine { /// Pays for a write into the client. fn pay_for_write(&mut self, bytes: u32) -> Result<(), OutOfInkError> { - self.buy_ink(sat_add_mul(5040, 30, bytes.saturating_sub(32))) + self.buy_ink(Ink(sat_add_mul(5040, 30, bytes.saturating_sub(32)))) } /// Pays for a read into the host. fn pay_for_read(&mut self, bytes: u32) -> Result<(), OutOfInkError> { - self.buy_ink(sat_add_mul(16381, 55, bytes.saturating_sub(32))) + self.buy_ink(Ink(sat_add_mul(16381, 55, bytes.saturating_sub(32)))) } /// Pays for both I/O and keccak. fn pay_for_keccak(&mut self, bytes: u32) -> Result<(), OutOfInkError> { let words = evm::evm_words(bytes).saturating_sub(2); - self.buy_ink(sat_add_mul(121800, 21000, words)) + self.buy_ink(Ink(sat_add_mul(121800, 21000, words))) } /// Pays for copying bytes from geth. @@ -305,14 +312,14 @@ pub trait MeteredMachine { false => break, } } - self.buy_ink(3000 + exp * 17500) + self.buy_ink(Ink(3000 + exp * 17500)) } } pub trait GasMeteredMachine: MeteredMachine { fn pricing(&self) -> PricingParams; - fn gas_left(&self) -> Result { + fn gas_left(&self) -> Result { let pricing = self.pricing(); match self.ink_left() { MachineMeter::Ready(ink) => Ok(pricing.ink_to_gas(ink)), @@ -320,13 +327,13 @@ pub trait GasMeteredMachine: MeteredMachine { } } - fn buy_gas(&mut self, gas: u64) -> Result<(), OutOfInkError> { + fn buy_gas(&mut self, gas: Gas) -> Result<(), OutOfInkError> { let pricing = self.pricing(); self.buy_ink(pricing.gas_to_ink(gas)) } /// Checks if the user has enough gas, but doesn't burn any - fn require_gas(&mut self, gas: u64) -> Result<(), OutOfInkError> { + fn require_gas(&mut self, gas: Gas) -> Result<(), OutOfInkError> { let pricing = self.pricing(); self.require_ink(pricing.gas_to_ink(gas)) } @@ -350,7 +357,7 @@ impl MeteredMachine for Machine { }}; } - let ink = || convert!(self.get_global(STYLUS_INK_LEFT)); + let ink = || Ink(convert!(self.get_global(STYLUS_INK_LEFT))); let status: u32 = convert!(self.get_global(STYLUS_INK_STATUS)); match status { @@ -362,7 +369,7 @@ impl MeteredMachine for Machine { fn set_meter(&mut self, meter: MachineMeter) { let ink = meter.ink(); let status = meter.status(); - self.set_global(STYLUS_INK_LEFT, ink.into()).unwrap(); + self.set_global(STYLUS_INK_LEFT, ink.0.into()).unwrap(); self.set_global(STYLUS_INK_STATUS, status.into()).unwrap(); } } diff --git a/arbitrator/prover/src/test.rs b/arbitrator/prover/src/test.rs index 97170441ff..4fd739342e 100644 --- a/arbitrator/prover/src/test.rs +++ b/arbitrator/prover/src/test.rs @@ -1,8 +1,6 @@ // Copyright 2022-2024, Offchain Labs, Inc. // For license information, see https://github.com/nitro/blob/master/LICENSE -#![cfg(test)] - use crate::binary; use brotli::Dictionary; use eyre::Result; @@ -64,7 +62,7 @@ pub fn test_compress() -> Result<()> { let deflate = brotli::compress(data, 11, 22, dict).unwrap(); let inflate = brotli::decompress(&deflate, dict).unwrap(); assert_eq!(hex::encode(inflate), hex::encode(data)); - assert!(&deflate != &last); + assert!(deflate != last); last = deflate; } Ok(()) diff --git a/arbitrator/stylus/src/cache.rs b/arbitrator/stylus/src/cache.rs index c1fdaaccee..9b788a45db 100644 --- a/arbitrator/stylus/src/cache.rs +++ b/arbitrator/stylus/src/cache.rs @@ -29,8 +29,16 @@ pub struct LruCounters { pub does_not_fit: u32, } +pub struct LongTermCounters { + pub hits: u32, + pub misses: u32, +} + pub struct InitCache { long_term: HashMap, + long_term_size_bytes: usize, + long_term_counters: LongTermCounters, + lru: CLruCache, lru_counters: LruCounters, } @@ -91,6 +99,20 @@ pub struct LruCacheMetrics { pub does_not_fit: u32, } +#[repr(C)] +pub struct LongTermCacheMetrics { + pub size_bytes: u64, + pub count: u32, + pub hits: u32, + pub misses: u32, +} + +#[repr(C)] +pub struct CacheMetrics { + pub lru: LruCacheMetrics, + pub long_term: LongTermCacheMetrics, +} + pub fn deserialize_module( module: &[u8], version: u16, @@ -117,6 +139,9 @@ impl InitCache { fn new(size_bytes: usize) -> Self { Self { long_term: HashMap::new(), + long_term_size_bytes: 0, + long_term_counters: LongTermCounters { hits: 0, misses: 0 }, + lru: CLruCache::with_config( CLruCacheConfig::new(NonZeroUsize::new(size_bytes).unwrap()) .with_scale(CustomWeightScale), @@ -136,22 +161,41 @@ impl InitCache { } /// Retrieves a cached value, updating items as necessary. - pub fn get(module_hash: Bytes32, version: u16, debug: bool) -> Option<(Module, Store)> { - let mut cache = cache!(); + /// If long_term_tag is 1 and the item is only in LRU will insert to long term cache. + pub fn get( + module_hash: Bytes32, + version: u16, + long_term_tag: u32, + debug: bool, + ) -> Option<(Module, Store)> { let key = CacheKey::new(module_hash, version, debug); + let mut cache = cache!(); // See if the item is in the long term cache if let Some(item) = cache.long_term.get(&key) { - return Some(item.data()); + let data = item.data(); + cache.long_term_counters.hits += 1; + return Some(data); + } + if long_term_tag == Self::ARBOS_TAG { + // only count misses only when we can expect to find the item in long term cache + cache.long_term_counters.misses += 1; } // See if the item is in the LRU cache, promoting if so - if let Some(item) = cache.lru.get(&key) { - let data = item.data(); + if let Some(item) = cache.lru.peek(&key).cloned() { cache.lru_counters.hits += 1; - return Some(data); + if long_term_tag == Self::ARBOS_TAG { + cache.long_term_size_bytes += item.entry_size_estimate_bytes; + cache.long_term.insert(key, item.clone()); + } else { + // only calls get to move the key to the head of the LRU list + cache.lru.get(&key); + } + return Some((item.module, Store::new(item.engine))); } cache.lru_counters.misses += 1; + None } @@ -174,6 +218,7 @@ impl InitCache { if let Some(item) = cache.lru.peek(&key).cloned() { if long_term_tag == Self::ARBOS_TAG { cache.long_term.insert(key, item.clone()); + cache.long_term_size_bytes += item.entry_size_estimate_bytes; } else { // only calls get to move the key to the head of the LRU list cache.lru.get(&key); @@ -195,6 +240,7 @@ impl InitCache { }; } else { cache.long_term.insert(key, item); + cache.long_term_size_bytes += entry_size_estimate_bytes; } Ok(data) } @@ -207,6 +253,7 @@ impl InitCache { let key = CacheKey::new(module_hash, version, debug); let mut cache = cache!(); if let Some(item) = cache.long_term.remove(&key) { + cache.long_term_size_bytes -= item.entry_size_estimate_bytes; if cache.lru.put_with_weight(key, item).is_err() { eprintln!("{}", Self::DOES_NOT_FIT_MSG); } @@ -225,22 +272,24 @@ impl InitCache { eprintln!("{}", Self::DOES_NOT_FIT_MSG); } } + cache.long_term_size_bytes = 0; } - pub fn get_lru_metrics() -> LruCacheMetrics { + pub fn get_metrics(output: &mut CacheMetrics) { let mut cache = cache!(); - let count = cache.lru.len(); - let metrics = LruCacheMetrics { - // add 1 to each entry to account that we subtracted 1 in the weight calculation - size_bytes: (cache.lru.weight() + count).try_into().unwrap(), - - count: count.try_into().unwrap(), + let lru_count = cache.lru.len(); + // adds 1 to each entry to account that we subtracted 1 in the weight calculation + output.lru.size_bytes = (cache.lru.weight() + lru_count).try_into().unwrap(); + output.lru.count = lru_count.try_into().unwrap(); + output.lru.hits = cache.lru_counters.hits; + output.lru.misses = cache.lru_counters.misses; + output.lru.does_not_fit = cache.lru_counters.does_not_fit; - hits: cache.lru_counters.hits, - misses: cache.lru_counters.misses, - does_not_fit: cache.lru_counters.does_not_fit, - }; + output.long_term.size_bytes = cache.long_term_size_bytes.try_into().unwrap(); + output.long_term.count = cache.long_term.len().try_into().unwrap(); + output.long_term.hits = cache.long_term_counters.hits; + output.long_term.misses = cache.long_term_counters.misses; // Empty counters. // go side, which is the only consumer of this function besides tests, @@ -250,8 +299,7 @@ impl InitCache { misses: 0, does_not_fit: 0, }; - - metrics + cache.long_term_counters = LongTermCounters { hits: 0, misses: 0 }; } // only used for testing diff --git a/arbitrator/stylus/src/env.rs b/arbitrator/stylus/src/env.rs index 69d542070d..a153fb5bf1 100644 --- a/arbitrator/stylus/src/env.rs +++ b/arbitrator/stylus/src/env.rs @@ -3,7 +3,7 @@ use arbutil::{ evm::{ - api::{DataReader, EvmApi}, + api::{DataReader, EvmApi, Ink}, EvmData, }, pricing, @@ -74,7 +74,7 @@ impl> WasmEnv { pub fn start<'a>( env: &'a mut WasmEnvMut<'_, D, E>, - ink: u64, + ink: Ink, ) -> Result, Escape> { let mut info = Self::program(env)?; info.buy_ink(pricing::HOSTIO_INK + ink)?; @@ -88,7 +88,7 @@ impl> WasmEnv { env, memory, store, - start_ink: 0, + start_ink: Ink(0), }; if info.env.evm_data.tracing { info.start_ink = info.ink_ready()?; @@ -114,16 +114,16 @@ pub struct MeterData { } impl MeterData { - pub fn ink(&self) -> u64 { - unsafe { self.ink_left.as_ref().val.u64 } + pub fn ink(&self) -> Ink { + Ink(unsafe { self.ink_left.as_ref().val.u64 }) } pub fn status(&self) -> u32 { unsafe { self.ink_status.as_ref().val.u32 } } - pub fn set_ink(&mut self, ink: u64) { - unsafe { self.ink_left.as_mut().val = RawValue { u64: ink } } + pub fn set_ink(&mut self, ink: Ink) { + unsafe { self.ink_left.as_mut().val = RawValue { u64: ink.0 } } } pub fn set_status(&mut self, status: u32) { @@ -140,7 +140,7 @@ pub struct HostioInfo<'a, D: DataReader, E: EvmApi> { pub env: &'a mut WasmEnv, pub memory: Memory, pub store: StoreMut<'a>, - pub start_ink: u64, + pub start_ink: Ink, } impl<'a, D: DataReader, E: EvmApi> HostioInfo<'a, D, E> { diff --git a/arbitrator/stylus/src/evm_api.rs b/arbitrator/stylus/src/evm_api.rs index d267372827..0dd27e3f8c 100644 --- a/arbitrator/stylus/src/evm_api.rs +++ b/arbitrator/stylus/src/evm_api.rs @@ -3,7 +3,7 @@ use crate::{GoSliceData, RustSlice}; use arbutil::evm::{ - api::{EvmApiMethod, EVM_API_METHOD_REQ_OFFSET}, + api::{EvmApiMethod, Gas, EVM_API_METHOD_REQ_OFFSET}, req::RequestHandler, }; @@ -31,7 +31,7 @@ impl RequestHandler for NativeRequestHandler { &mut self, req_type: EvmApiMethod, req_data: impl AsRef<[u8]>, - ) -> (Vec, GoSliceData, u64) { + ) -> (Vec, GoSliceData, Gas) { let mut result = GoSliceData::null(); let mut raw_data = GoSliceData::null(); let mut cost = 0; @@ -45,6 +45,6 @@ impl RequestHandler for NativeRequestHandler { ptr!(raw_data), ) }; - (result.slice().to_vec(), raw_data, cost) + (result.slice().to_vec(), raw_data, Gas(cost)) } } diff --git a/arbitrator/stylus/src/host.rs b/arbitrator/stylus/src/host.rs index 1afc1b4e51..c72cafc316 100644 --- a/arbitrator/stylus/src/host.rs +++ b/arbitrator/stylus/src/host.rs @@ -6,7 +6,7 @@ use crate::env::{Escape, HostioInfo, MaybeEscape, WasmEnv, WasmEnvMut}; use arbutil::{ evm::{ - api::{DataReader, EvmApi}, + api::{DataReader, EvmApi, Gas, Ink}, EvmData, }, Color, @@ -82,7 +82,7 @@ where println!("{} {text}", "Stylus says:".yellow()); } - fn trace(&mut self, name: &str, args: &[u8], outs: &[u8], end_ink: u64) { + fn trace(&mut self, name: &str, args: &[u8], outs: &[u8], end_ink: Ink) { let start_ink = self.start_ink; self.evm_api .capture_hostio(name, args, outs, start_ink, end_ink); @@ -168,7 +168,7 @@ pub(crate) fn call_contract>( ) -> Result { hostio!( env, - call_contract(contract, data, data_len, value, gas, ret_len) + call_contract(contract, data, data_len, value, Gas(gas), ret_len) ) } @@ -182,7 +182,7 @@ pub(crate) fn delegate_call_contract>( ) -> Result { hostio!( env, - delegate_call_contract(contract, data, data_len, gas, ret_len) + delegate_call_contract(contract, data, data_len, Gas(gas), ret_len) ) } @@ -196,7 +196,7 @@ pub(crate) fn static_call_contract>( ) -> Result { hostio!( env, - static_call_contract(contract, data, data_len, gas, ret_len) + static_call_contract(contract, data, data_len, Gas(gas), ret_len) ) } @@ -334,13 +334,13 @@ pub(crate) fn contract_address>( pub(crate) fn evm_gas_left>( mut env: WasmEnvMut, ) -> Result { - hostio!(env, evm_gas_left()) + hostio!(env, evm_gas_left()).map(|g| g.0) } pub(crate) fn evm_ink_left>( mut env: WasmEnvMut, ) -> Result { - hostio!(env, evm_ink_left()) + hostio!(env, evm_ink_left()).map(|i| i.0) } pub(crate) fn math_div>( diff --git a/arbitrator/stylus/src/lib.rs b/arbitrator/stylus/src/lib.rs index abea428167..e7f10c2400 100644 --- a/arbitrator/stylus/src/lib.rs +++ b/arbitrator/stylus/src/lib.rs @@ -3,7 +3,7 @@ use arbutil::{ evm::{ - api::DataReader, + api::{DataReader, Gas, Ink}, req::EvmApiRequestor, user::{UserOutcome, UserOutcomeKind}, EvmData, @@ -11,7 +11,7 @@ use arbutil::{ format::DebugBytes, Bytes32, }; -use cache::{deserialize_module, InitCache, LruCacheMetrics}; +use cache::{deserialize_module, CacheMetrics, InitCache}; use evm_api::NativeRequestHandler; use eyre::ErrReport; use native::NativeInstance; @@ -279,7 +279,7 @@ pub unsafe extern "C" fn stylus_call( let evm_api = EvmApiRequestor::new(req_handler); let pricing = config.pricing; let output = &mut *output; - let ink = pricing.gas_to_ink(*gas); + let ink = pricing.gas_to_ink(Gas(*gas)); // Safety: module came from compile_user_wasm and we've paid for memory expansion let instance = unsafe { @@ -302,10 +302,10 @@ pub unsafe extern "C" fn stylus_call( Ok(outcome) => output.write_outcome(outcome), }; let ink_left = match status { - UserOutcomeKind::OutOfStack => 0, // take all gas when out of stack + UserOutcomeKind::OutOfStack => Ink(0), // take all gas when out of stack _ => instance.ink_left().into(), }; - *gas = pricing.ink_to_gas(ink_left); + *gas = pricing.ink_to_gas(ink_left).0; status } @@ -364,10 +364,15 @@ pub unsafe extern "C" fn stylus_drop_vec(vec: RustBytes) { } } -/// Gets lru cache metrics. +/// Gets cache metrics. +/// +/// # Safety +/// +/// `output` must not be null. #[no_mangle] -pub extern "C" fn stylus_get_lru_cache_metrics() -> LruCacheMetrics { - InitCache::get_lru_metrics() +pub unsafe extern "C" fn stylus_get_cache_metrics(output: *mut CacheMetrics) { + let output = &mut *output; + InitCache::get_metrics(output); } /// Clears lru cache. @@ -377,18 +382,23 @@ pub extern "C" fn stylus_clear_lru_cache() { InitCache::clear_lru_cache() } -/// Gets lru entry size in bytes. +/// Clears long term cache (for arbos_tag = 1) /// Only used for testing purposes. #[no_mangle] -pub extern "C" fn stylus_get_lru_entry_size_estimate_bytes( +pub extern "C" fn stylus_clear_long_term_cache() { + InitCache::clear_long_term(1); +} + +/// Gets entry size in bytes. +/// Only used for testing purposes. +#[no_mangle] +pub extern "C" fn stylus_get_entry_size_estimate_bytes( module: GoSliceData, version: u16, debug: bool, ) -> u64 { match deserialize_module(module.slice(), version, debug) { Err(error) => panic!("tried to get invalid asm!: {error}"), - Ok((_, _, lru_entry_size_estimate_bytes)) => { - lru_entry_size_estimate_bytes.try_into().unwrap() - } + Ok((_, _, entry_size_estimate_bytes)) => entry_size_estimate_bytes.try_into().unwrap(), } } diff --git a/arbitrator/stylus/src/native.rs b/arbitrator/stylus/src/native.rs index 516c6602e7..0fbdb342f3 100644 --- a/arbitrator/stylus/src/native.rs +++ b/arbitrator/stylus/src/native.rs @@ -8,7 +8,7 @@ use crate::{ }; use arbutil::{ evm::{ - api::{DataReader, EvmApi}, + api::{DataReader, EvmApi, Ink}, EvmData, }, operator::OperatorCode, @@ -121,13 +121,12 @@ impl> NativeInstance { let compile = CompileConfig::version(version, debug); let env = WasmEnv::new(compile, None, evm, evm_data); let module_hash = env.evm_data.module_hash; - - if let Some((module, store)) = InitCache::get(module_hash, version, debug) { - return Self::from_module(module, store, env); - } if !env.evm_data.cached { long_term_tag = 0; } + if let Some((module, store)) = InitCache::get(module_hash, version, long_term_tag, debug) { + return Self::from_module(module, store, env); + } let (module, store) = InitCache::insert(module_hash, module, version, long_term_tag, debug)?; Self::from_module(module, store, env) @@ -271,7 +270,7 @@ impl> NativeInstance { global.set(store, value.into()).map_err(ErrReport::msg) } - pub fn call_func(&mut self, func: TypedFunction<(), R>, ink: u64) -> Result + pub fn call_func(&mut self, func: TypedFunction<(), R>, ink: Ink) -> Result where R: WasmTypeList, { diff --git a/arbitrator/stylus/src/run.rs b/arbitrator/stylus/src/run.rs index 8e673a25e5..6cbb0cfb42 100644 --- a/arbitrator/stylus/src/run.rs +++ b/arbitrator/stylus/src/run.rs @@ -4,18 +4,18 @@ #![allow(clippy::redundant_closure_call)] use crate::{env::Escape, native::NativeInstance}; -use arbutil::evm::api::{DataReader, EvmApi}; +use arbutil::evm::api::{DataReader, EvmApi, Ink}; use arbutil::evm::user::UserOutcome; use eyre::{eyre, Result}; use prover::machine::Machine; use prover::programs::{prelude::*, STYLUS_ENTRY_POINT}; pub trait RunProgram { - fn run_main(&mut self, args: &[u8], config: StylusConfig, ink: u64) -> Result; + fn run_main(&mut self, args: &[u8], config: StylusConfig, ink: Ink) -> Result; } impl RunProgram for Machine { - fn run_main(&mut self, args: &[u8], config: StylusConfig, ink: u64) -> Result { + fn run_main(&mut self, args: &[u8], config: StylusConfig, ink: Ink) -> Result { macro_rules! call { ($module:expr, $func:expr, $args:expr) => { call!($module, $func, $args, |error| UserOutcome::Failure(error)) @@ -65,7 +65,7 @@ impl RunProgram for Machine { } impl> RunProgram for NativeInstance { - fn run_main(&mut self, args: &[u8], config: StylusConfig, ink: u64) -> Result { + fn run_main(&mut self, args: &[u8], config: StylusConfig, ink: Ink) -> Result { use UserOutcome::*; self.set_ink(ink); diff --git a/arbitrator/stylus/src/test/api.rs b/arbitrator/stylus/src/test/api.rs index 66d600a6f7..7a5af6f89f 100644 --- a/arbitrator/stylus/src/test/api.rs +++ b/arbitrator/stylus/src/test/api.rs @@ -4,7 +4,7 @@ use crate::{native, run::RunProgram}; use arbutil::{ evm::{ - api::{EvmApi, VecReader}, + api::{EvmApi, Gas, Ink, VecReader}, user::UserOutcomeKind, EvmData, }, @@ -68,24 +68,24 @@ impl TestEvmApi { } impl EvmApi for TestEvmApi { - fn get_bytes32(&mut self, key: Bytes32, _evm_api_gas_to_use: u64) -> (Bytes32, u64) { + fn get_bytes32(&mut self, key: Bytes32, _evm_api_gas_to_use: Gas) -> (Bytes32, Gas) { let storage = &mut self.storage.lock(); let storage = storage.get_mut(&self.program).unwrap(); let value = storage.get(&key).cloned().unwrap_or_default(); - (value, 2100) // pretend worst case + (value, Gas(2100)) // pretend worst case } - fn cache_bytes32(&mut self, key: Bytes32, value: Bytes32) -> u64 { + fn cache_bytes32(&mut self, key: Bytes32, value: Bytes32) -> Gas { let storage = &mut self.storage.lock(); let storage = storage.get_mut(&self.program).unwrap(); storage.insert(key, value); - 0 + Gas(0) } - fn flush_storage_cache(&mut self, _clear: bool, _gas_left: u64) -> Result { + fn flush_storage_cache(&mut self, _clear: bool, _gas_left: Gas) -> Result { let storage = &mut self.storage.lock(); let storage = storage.get_mut(&self.program).unwrap(); - Ok(22100 * storage.len() as u64) // pretend worst case + Ok(Gas(22100) * storage.len() as u64) // pretend worst case } fn get_transient_bytes32(&mut self, _key: Bytes32) -> Bytes32 { @@ -102,10 +102,10 @@ impl EvmApi for TestEvmApi { &mut self, contract: Bytes20, calldata: &[u8], - _gas_left: u64, - gas_req: u64, + _gas_left: Gas, + gas_req: Gas, _value: Bytes32, - ) -> (u32, u64, UserOutcomeKind) { + ) -> (u32, Gas, UserOutcomeKind) { let compile = self.compile.clone(); let evm_data = self.evm_data; let config = *self.configs.lock().get(&contract).unwrap(); @@ -122,7 +122,7 @@ impl EvmApi for TestEvmApi { let (status, outs) = outcome.into_data(); let outs_len = outs.len() as u32; - let ink_left: u64 = native.ink_left().into(); + let ink_left: Ink = native.ink_left().into(); let gas_left = config.pricing.ink_to_gas(ink_left); *self.write_result.lock() = outs; (outs_len, gas - gas_left, status) @@ -132,9 +132,9 @@ impl EvmApi for TestEvmApi { &mut self, _contract: Bytes20, _calldata: &[u8], - _gas_left: u64, - _gas_req: u64, - ) -> (u32, u64, UserOutcomeKind) { + _gas_left: Gas, + _gas_req: Gas, + ) -> (u32, Gas, UserOutcomeKind) { todo!("delegate call not yet supported") } @@ -142,9 +142,9 @@ impl EvmApi for TestEvmApi { &mut self, contract: Bytes20, calldata: &[u8], - gas_left: u64, - gas_req: u64, - ) -> (u32, u64, UserOutcomeKind) { + gas_left: Gas, + gas_req: Gas, + ) -> (u32, Gas, UserOutcomeKind) { println!("note: overriding static call with call"); self.contract_call(contract, calldata, gas_left, gas_req, Bytes32::default()) } @@ -153,8 +153,8 @@ impl EvmApi for TestEvmApi { &mut self, _code: Vec, _endowment: Bytes32, - _gas: u64, - ) -> (Result, u32, u64) { + _gas: Gas, + ) -> (Result, u32, Gas) { unimplemented!("create1 not supported") } @@ -163,8 +163,8 @@ impl EvmApi for TestEvmApi { _code: Vec, _endowment: Bytes32, _salt: Bytes32, - _gas: u64, - ) -> (Result, u32, u64) { + _gas: Gas, + ) -> (Result, u32, Gas) { unimplemented!("create2 not supported") } @@ -176,19 +176,19 @@ impl EvmApi for TestEvmApi { Ok(()) // pretend a log was emitted } - fn account_balance(&mut self, _address: Bytes20) -> (Bytes32, u64) { + fn account_balance(&mut self, _address: Bytes20) -> (Bytes32, Gas) { unimplemented!() } - fn account_code(&mut self, _address: Bytes20, _gas_left: u64) -> (VecReader, u64) { + fn account_code(&mut self, _address: Bytes20, _gas_left: Gas) -> (VecReader, Gas) { unimplemented!() } - fn account_codehash(&mut self, _address: Bytes20) -> (Bytes32, u64) { + fn account_codehash(&mut self, _address: Bytes20) -> (Bytes32, Gas) { unimplemented!() } - fn add_pages(&mut self, new: u16) -> u64 { + fn add_pages(&mut self, new: u16) -> Gas { let model = MemoryModel::new(2, 1000); let (open, ever) = *self.pages.lock(); @@ -203,8 +203,8 @@ impl EvmApi for TestEvmApi { _name: &str, _args: &[u8], _outs: &[u8], - _start_ink: u64, - _end_ink: u64, + _start_ink: Ink, + _end_ink: Ink, ) { unimplemented!() } diff --git a/arbitrator/stylus/src/test/mod.rs b/arbitrator/stylus/src/test/mod.rs index 00c9c62ae4..3fd0faede8 100644 --- a/arbitrator/stylus/src/test/mod.rs +++ b/arbitrator/stylus/src/test/mod.rs @@ -3,7 +3,10 @@ use crate::{env::WasmEnv, native::NativeInstance, run::RunProgram, test::api::TestEvmApi}; use arbutil::{ - evm::{api::VecReader, user::UserOutcome}, + evm::{ + api::{Ink, VecReader}, + user::UserOutcome, + }, Bytes20, Bytes32, Color, }; use eyre::{bail, Result}; @@ -41,7 +44,7 @@ impl TestInstance { }; let mut native = Self::new_from_store(path, store, imports)?; native.set_meter_data(); - native.set_ink(u64::MAX); + native.set_ink(Ink(u64::MAX)); native.set_stack(u32::MAX); Ok(native) } @@ -107,8 +110,8 @@ fn expensive_add(op: &Operator, _tys: &SigMap) -> u64 { } } -pub fn random_ink(min: u64) -> u64 { - rand::thread_rng().gen_range(min..=u64::MAX) +pub fn random_ink(min: u64) -> Ink { + Ink(rand::thread_rng().gen_range(min..=u64::MAX)) } pub fn random_bytes20() -> Bytes20 { @@ -135,7 +138,7 @@ fn uniform_cost_config() -> StylusConfig { stylus_config } -fn test_configs() -> (CompileConfig, StylusConfig, u64) { +fn test_configs() -> (CompileConfig, StylusConfig, Ink) { ( test_compile_config(), uniform_cost_config(), @@ -165,12 +168,12 @@ fn new_test_machine(path: &str, compile: &CompileConfig) -> Result { Arc::new(|_, _, _| panic!("tried to read preimage")), Some(stylus_data), )?; - mach.set_ink(u64::MAX); + mach.set_ink(Ink(u64::MAX)); mach.set_stack(u32::MAX); Ok(mach) } -fn run_native(native: &mut TestInstance, args: &[u8], ink: u64) -> Result> { +fn run_native(native: &mut TestInstance, args: &[u8], ink: Ink) -> Result> { let config = native.env().config.expect("no config"); match native.run_main(args, config, ink)? { UserOutcome::Success(output) => Ok(output), @@ -182,7 +185,7 @@ fn run_machine( machine: &mut Machine, args: &[u8], config: StylusConfig, - ink: u64, + ink: Ink, ) -> Result> { match machine.run_main(args, config, ink)? { UserOutcome::Success(output) => Ok(output), diff --git a/arbitrator/stylus/src/test/native.rs b/arbitrator/stylus/src/test/native.rs index 9669932a03..672bdd179c 100644 --- a/arbitrator/stylus/src/test/native.rs +++ b/arbitrator/stylus/src/test/native.rs @@ -16,7 +16,7 @@ use crate::{ use arbutil::{ crypto, evm::{ - api::EvmApi, + api::{EvmApi, Gas, Ink}, user::{UserOutcome, UserOutcomeKind}, }, format, Bytes20, Bytes32, Color, @@ -48,8 +48,8 @@ fn test_ink() -> Result<()> { macro_rules! exhaust { ($ink:expr) => { - native.set_ink($ink); - assert_eq!(native.ink_left(), MachineMeter::Ready($ink)); + native.set_ink(Ink($ink)); + assert_eq!(native.ink_left(), MachineMeter::Ready(Ink($ink))); assert!(add_one.call(&mut native.store, 32).is_err()); assert_eq!(native.ink_left(), MachineMeter::Exhausted); }; @@ -59,12 +59,12 @@ fn test_ink() -> Result<()> { exhaust!(50); exhaust!(99); - let mut ink_left = 500; + let mut ink_left = Ink(500); native.set_ink(ink_left); - while ink_left > 0 { + while ink_left > Ink(0) { assert_eq!(native.ink_left(), MachineMeter::Ready(ink_left)); assert_eq!(add_one.call(&mut native.store, 64)?, 65); - ink_left -= 100; + ink_left -= Ink(100); } assert!(add_one.call(&mut native.store, 32).is_err()); assert_eq!(native.ink_left(), MachineMeter::Exhausted); @@ -198,7 +198,7 @@ fn test_import_export_safety() -> Result<()> { let mut bin = bin?; assert!(bin.clone().instrument(&compile, codehash).is_err()); compile.debug.debug_info = false; - assert!(bin.instrument(&compile, &codehash).is_err()); + assert!(bin.instrument(&compile, codehash).is_err()); if both { assert!(TestInstance::new_test(file, compile).is_err()); @@ -268,7 +268,7 @@ fn test_heap() -> Result<()> { assert_eq!(pages, 128); let used = config.pricing.ink_to_gas(ink - native.ink_ready()?); - ensure!((used as i64 - 32_000_000).abs() < 3_000, "wrong ink"); + ensure!((used.0 as i64 - 32_000_000).abs() < 3_000, "wrong ink"); assert_eq!(native.memory_size(), Pages(128)); if step == extra { @@ -283,7 +283,7 @@ fn test_heap() -> Result<()> { // the cost should exceed a maximum u32, consuming more gas than can ever be bought let (mut native, _) = TestInstance::new_with_evm("tests/memory2.wat", &compile, config)?; - let outcome = native.run_main(&[], config, config.pricing.ink_to_gas(u32::MAX.into()))?; + let outcome = native.run_main(&[], config, config.pricing.gas_to_ink(Gas(u32::MAX.into())))?; assert_eq!(outcome.kind(), UserOutcomeKind::OutOfInk); // ensure we reject programs with excessive footprints @@ -381,7 +381,7 @@ fn test_storage() -> Result<()> { let (mut native, mut evm) = TestInstance::new_with_evm(filename, &compile, config)?; run_native(&mut native, &store_args, ink)?; - assert_eq!(evm.get_bytes32(key.into(), 0).0, Bytes32(value)); + assert_eq!(evm.get_bytes32(key.into(), Gas(0)).0, Bytes32(value)); assert_eq!(run_native(&mut native, &load_args, ink)?, value); let mut machine = Machine::from_user_path(Path::new(filename), &compile)?; @@ -465,7 +465,7 @@ fn test_calls() -> Result<()> { run_native(&mut native, &args, ink)?; for (key, value) in slots { - assert_eq!(evm.get_bytes32(key, 0).0, value); + assert_eq!(evm.get_bytes32(key, Gas(0)).0, value); } Ok(()) } diff --git a/arbitrator/stylus/src/test/wavm.rs b/arbitrator/stylus/src/test/wavm.rs index e707cf490a..729cfebf2f 100644 --- a/arbitrator/stylus/src/test/wavm.rs +++ b/arbitrator/stylus/src/test/wavm.rs @@ -2,6 +2,7 @@ // For license information, see https://github.com/nitro/blob/master/LICENSE use crate::test::{new_test_machine, test_compile_config}; +use arbutil::evm::api::Ink; use eyre::Result; use prover::{programs::prelude::*, Machine}; @@ -15,8 +16,8 @@ fn test_ink() -> Result<()> { macro_rules! exhaust { ($ink:expr) => { - machine.set_ink($ink); - assert_eq!(machine.ink_left(), MachineMeter::Ready($ink)); + machine.set_ink(Ink($ink)); + assert_eq!(machine.ink_left(), MachineMeter::Ready(Ink($ink))); assert!(call(machine, 32).is_err()); assert_eq!(machine.ink_left(), MachineMeter::Exhausted); }; @@ -26,12 +27,12 @@ fn test_ink() -> Result<()> { exhaust!(50); exhaust!(99); - let mut ink_left = 500; + let mut ink_left = Ink(500); machine.set_ink(ink_left); - while ink_left > 0 { + while ink_left > Ink(0) { assert_eq!(machine.ink_left(), MachineMeter::Ready(ink_left)); assert_eq!(call(machine, 64)?, vec![65_u32.into()]); - ink_left -= 100; + ink_left -= Ink(100); } assert!(call(machine, 32).is_err()); assert_eq!(machine.ink_left(), MachineMeter::Exhausted); diff --git a/arbitrator/wasm-libraries/user-host-trait/src/lib.rs b/arbitrator/wasm-libraries/user-host-trait/src/lib.rs index 12a6bdbed2..35a4a31347 100644 --- a/arbitrator/wasm-libraries/user-host-trait/src/lib.rs +++ b/arbitrator/wasm-libraries/user-host-trait/src/lib.rs @@ -5,7 +5,7 @@ use arbutil::{ crypto, evm::{ self, - api::{DataReader, EvmApi}, + api::{DataReader, EvmApi, Gas, Ink}, storage::StorageCache, user::UserOutcomeKind, EvmData, ARBOS_VERSION_STYLUS_CHARGING_FIXES, @@ -88,7 +88,7 @@ pub trait UserHost: GasMeteredMachine { } fn say(&self, text: D); - fn trace(&mut self, name: &str, args: &[u8], outs: &[u8], end_ink: u64); + fn trace(&mut self, name: &str, args: &[u8], outs: &[u8], end_ink: Ink); fn write_bytes20(&self, ptr: GuestPtr, src: Bytes20) -> Result<(), Self::MemoryErr> { self.write_slice(ptr, &src.0) @@ -147,7 +147,7 @@ pub trait UserHost: GasMeteredMachine { // require for cache-miss case, preserve wrong behavior for old arbos let evm_api_gas_to_use = if arbos_version < ARBOS_VERSION_STYLUS_CHARGING_FIXES { - EVM_API_INK + Gas(EVM_API_INK.0) } else { self.pricing().ink_to_gas(EVM_API_INK) }; @@ -253,7 +253,7 @@ pub trait UserHost: GasMeteredMachine { data: GuestPtr, data_len: u32, value: GuestPtr, - gas: u64, + gas: Gas, ret_len: GuestPtr, ) -> Result { let value = Some(value); @@ -282,7 +282,7 @@ pub trait UserHost: GasMeteredMachine { contract: GuestPtr, data: GuestPtr, data_len: u32, - gas: u64, + gas: Gas, ret_len: GuestPtr, ) -> Result { let call = |api: &mut Self::A, contract, data: &_, left, req, _| { @@ -312,7 +312,7 @@ pub trait UserHost: GasMeteredMachine { contract: GuestPtr, data: GuestPtr, data_len: u32, - gas: u64, + gas: Gas, ret_len: GuestPtr, ) -> Result { let call = |api: &mut Self::A, contract, data: &_, left, req, _| { @@ -329,7 +329,7 @@ pub trait UserHost: GasMeteredMachine { calldata: GuestPtr, calldata_len: u32, value: Option, - gas: u64, + gas: Gas, return_data_len: GuestPtr, call: F, name: &str, @@ -339,10 +339,10 @@ pub trait UserHost: GasMeteredMachine { &mut Self::A, Address, &[u8], - u64, - u64, + Gas, + Gas, Option, - ) -> (u32, u64, UserOutcomeKind), + ) -> (u32, Gas, UserOutcomeKind), { self.buy_ink(HOSTIO_INK + 3 * PTR_INK + EVM_API_INK)?; self.pay_for_read(calldata_len)?; @@ -465,12 +465,12 @@ pub trait UserHost: GasMeteredMachine { salt: Option, contract: GuestPtr, revert_data_len: GuestPtr, - cost: u64, + cost: Ink, call: F, name: &str, ) -> Result<(), Self::Err> where - F: FnOnce(&mut Self::A, Vec, Bytes32, Option, u64) -> (Result
, u32, u64), + F: FnOnce(&mut Self::A, Vec, Bytes32, Option, Gas) -> (Result
, u32, Gas), { self.buy_ink(HOSTIO_INK + cost)?; self.pay_for_read(code_len)?; @@ -745,7 +745,7 @@ pub trait UserHost: GasMeteredMachine { /// equivalent to that of the EVM's [`GAS`] opcode. /// /// [`GAS`]: https://www.evm.codes/#5a - fn evm_gas_left(&mut self) -> Result { + fn evm_gas_left(&mut self) -> Result { self.buy_ink(HOSTIO_INK)?; let gas = self.gas_left()?; trace!("evm_gas_left", self, &[], be!(gas), gas) @@ -757,7 +757,7 @@ pub trait UserHost: GasMeteredMachine { /// /// [`GAS`]: https://www.evm.codes/#5a /// [`Ink and Gas`]: https://developer.arbitrum.io/TODO - fn evm_ink_left(&mut self) -> Result { + fn evm_ink_left(&mut self) -> Result { self.buy_ink(HOSTIO_INK)?; let ink = self.ink_ready()?; trace!("evm_ink_left", self, &[], be!(ink), ink) diff --git a/arbitrator/wasm-libraries/user-host/src/host.rs b/arbitrator/wasm-libraries/user-host/src/host.rs index abe55b8c12..5ec2ece2c3 100644 --- a/arbitrator/wasm-libraries/user-host/src/host.rs +++ b/arbitrator/wasm-libraries/user-host/src/host.rs @@ -2,7 +2,7 @@ // For license information, see https://github.com/nitro/blob/master/LICENSE use crate::program::Program; -use arbutil::evm::user::UserOutcomeKind; +use arbutil::evm::{api::Gas, user::UserOutcomeKind}; use caller_env::GuestPtr; use user_host_trait::UserHost; @@ -77,7 +77,14 @@ pub unsafe extern "C" fn user_host__call_contract( gas: u64, ret_len: GuestPtr, ) -> u8 { - hostio!(call_contract(contract, data, data_len, value, gas, ret_len)) + hostio!(call_contract( + contract, + data, + data_len, + value, + Gas(gas), + ret_len + )) } #[no_mangle] @@ -89,7 +96,11 @@ pub unsafe extern "C" fn user_host__delegate_call_contract( ret_len: GuestPtr, ) -> u8 { hostio!(delegate_call_contract( - contract, data, data_len, gas, ret_len + contract, + data, + data_len, + Gas(gas), + ret_len )) } @@ -101,7 +112,13 @@ pub unsafe extern "C" fn user_host__static_call_contract( gas: u64, ret_len: GuestPtr, ) -> u8 { - hostio!(static_call_contract(contract, data, data_len, gas, ret_len)) + hostio!(static_call_contract( + contract, + data, + data_len, + Gas(gas), + ret_len + )) } #[no_mangle] @@ -207,12 +224,12 @@ pub unsafe extern "C" fn user_host__contract_address(ptr: GuestPtr) { #[no_mangle] pub unsafe extern "C" fn user_host__evm_gas_left() -> u64 { - hostio!(evm_gas_left()) + hostio!(evm_gas_left()).0 } #[no_mangle] pub unsafe extern "C" fn user_host__evm_ink_left() -> u64 { - hostio!(evm_ink_left()) + hostio!(evm_ink_left()).0 } #[no_mangle] diff --git a/arbitrator/wasm-libraries/user-host/src/ink.rs b/arbitrator/wasm-libraries/user-host/src/ink.rs index e01e616e07..bde7cfc1c0 100644 --- a/arbitrator/wasm-libraries/user-host/src/ink.rs +++ b/arbitrator/wasm-libraries/user-host/src/ink.rs @@ -2,6 +2,7 @@ // For license information, see https://github.com/OffchainLabs/nitro/blob/master/LICENSE use crate::program::Program; +use arbutil::evm::api::Ink; use prover::programs::{ config::PricingParams, prelude::{GasMeteredMachine, MachineMeter, MeteredMachine}, @@ -18,7 +19,7 @@ impl MeteredMachine for Program { fn ink_left(&self) -> MachineMeter { unsafe { match user_ink_status() { - 0 => MachineMeter::Ready(user_ink_left()), + 0 => MachineMeter::Ready(Ink(user_ink_left())), _ => MachineMeter::Exhausted, } } @@ -26,7 +27,7 @@ impl MeteredMachine for Program { fn set_meter(&mut self, meter: MachineMeter) { unsafe { - user_set_ink(meter.ink(), meter.status()); + user_set_ink(meter.ink().0, meter.status()); } } } diff --git a/arbitrator/wasm-libraries/user-host/src/link.rs b/arbitrator/wasm-libraries/user-host/src/link.rs index f4c402fd97..cb9f046cdb 100644 --- a/arbitrator/wasm-libraries/user-host/src/link.rs +++ b/arbitrator/wasm-libraries/user-host/src/link.rs @@ -3,7 +3,11 @@ use crate::program::Program; use arbutil::{ - evm::{user::UserOutcomeKind, EvmData}, + evm::{ + api::{Gas, Ink}, + user::UserOutcomeKind, + EvmData, + }, format::DebugBytes, heapify, Bytes20, Bytes32, }; @@ -120,11 +124,11 @@ pub unsafe extern "C" fn programs__new_program( // buy ink let pricing = config.pricing; - let ink = pricing.gas_to_ink(gas); + let ink = pricing.gas_to_ink(Gas(gas)); // link the program and ready its instrumentation let module = wavm_link_module(&MemoryLeaf(*module_hash)); - program_set_ink(module, ink); + program_set_ink(module, ink.0); program_set_stack(module, config.max_depth); // provide arguments @@ -175,7 +179,7 @@ pub unsafe extern "C" fn programs__set_response( id, STATIC_MEM.read_slice(result_ptr, result_len), STATIC_MEM.read_slice(raw_data_ptr, raw_data_len), - gas, + Gas(gas), ); } @@ -207,7 +211,7 @@ pub unsafe extern "C" fn program_internal__set_done(mut status: UserOutcomeKind) let program = Program::current(); let module = program.module; let mut outs = program.outs.as_slice(); - let mut ink_left = program_ink_left(module); + let mut ink_left = Ink(program_ink_left(module)); // apply any early exit codes if let Some(early) = program.early_exit { @@ -218,12 +222,12 @@ pub unsafe extern "C" fn program_internal__set_done(mut status: UserOutcomeKind) if program_ink_status(module) != 0 { status = OutOfInk; outs = &[]; - ink_left = 0; + ink_left = Ink(0); } if program_stack_left(module) == 0 { status = OutOfStack; outs = &[]; - ink_left = 0; + ink_left = Ink(0); } let gas_left = program.config.pricing.ink_to_gas(ink_left); diff --git a/arbitrator/wasm-libraries/user-host/src/program.rs b/arbitrator/wasm-libraries/user-host/src/program.rs index 4199a691f7..7b3782b2e5 100644 --- a/arbitrator/wasm-libraries/user-host/src/program.rs +++ b/arbitrator/wasm-libraries/user-host/src/program.rs @@ -3,7 +3,7 @@ use arbutil::{ evm::{ - api::{EvmApiMethod, VecReader, EVM_API_METHOD_REQ_OFFSET}, + api::{EvmApiMethod, Gas, Ink, VecReader, EVM_API_METHOD_REQ_OFFSET}, req::{EvmApiRequestor, RequestHandler}, user::UserOutcomeKind, EvmData, @@ -49,7 +49,7 @@ static mut LAST_REQUEST_ID: u32 = 0x10000; #[derive(Clone)] pub(crate) struct UserHostRequester { data: Option>, - answer: Option<(Vec, VecReader, u64)>, + answer: Option<(Vec, VecReader, Gas)>, req_type: u32, id: u32, } @@ -95,7 +95,7 @@ impl UserHostRequester { req_id: u32, result: Vec, raw_data: Vec, - gas: u64, + gas: Gas, ) { self.answer = Some((result, VecReader::new(raw_data), gas)); if req_id != self.id { @@ -130,7 +130,7 @@ impl UserHostRequester { } #[no_mangle] - unsafe fn send_request(&mut self, req_type: u32, data: Vec) -> (Vec, VecReader, u64) { + unsafe fn send_request(&mut self, req_type: u32, data: Vec) -> (Vec, VecReader, Gas) { let req_id = self.set_request(req_type, &data); compiler_fence(Ordering::SeqCst); @@ -149,7 +149,7 @@ impl RequestHandler for UserHostRequester { &mut self, req_type: EvmApiMethod, req_data: impl AsRef<[u8]>, - ) -> (Vec, VecReader, u64) { + ) -> (Vec, VecReader, Gas) { unsafe { self.send_request( req_type as u32 + EVM_API_METHOD_REQ_OFFSET, @@ -265,7 +265,7 @@ impl UserHost for Program { println!("{} {text}", "Stylus says:".yellow()); } - fn trace(&mut self, name: &str, args: &[u8], outs: &[u8], _end_ink: u64) { + fn trace(&mut self, name: &str, args: &[u8], outs: &[u8], _end_ink: Ink) { let args = hex::encode(args); let outs = hex::encode(outs); println!("Error: unexpected hostio tracing info for {name} while proving: {args}, {outs}"); diff --git a/arbitrator/wasm-libraries/user-test/src/host.rs b/arbitrator/wasm-libraries/user-test/src/host.rs index f2912eaae3..f1b4506414 100644 --- a/arbitrator/wasm-libraries/user-test/src/host.rs +++ b/arbitrator/wasm-libraries/user-test/src/host.rs @@ -2,6 +2,7 @@ // For license information, see https://github.com/nitro/blob/master/LICENSE use crate::program::Program; +use arbutil::evm::api::Gas; use caller_env::GuestPtr; use user_host_trait::UserHost; @@ -63,7 +64,14 @@ pub unsafe extern "C" fn vm_hooks__call_contract( gas: u64, ret_len: GuestPtr, ) -> u8 { - hostio!(call_contract(contract, data, data_len, value, gas, ret_len)) + hostio!(call_contract( + contract, + data, + data_len, + value, + Gas(gas), + ret_len + )) } #[no_mangle] @@ -75,7 +83,11 @@ pub unsafe extern "C" fn vm_hooks__delegate_call_contract( ret_len: GuestPtr, ) -> u8 { hostio!(delegate_call_contract( - contract, data, data_len, gas, ret_len + contract, + data, + data_len, + Gas(gas), + ret_len )) } @@ -87,7 +99,13 @@ pub unsafe extern "C" fn vm_hooks__static_call_contract( gas: u64, ret_len: GuestPtr, ) -> u8 { - hostio!(static_call_contract(contract, data, data_len, gas, ret_len)) + hostio!(static_call_contract( + contract, + data, + data_len, + Gas(gas), + ret_len + )) } #[no_mangle] @@ -189,12 +207,12 @@ pub unsafe extern "C" fn vm_hooks__contract_address(ptr: GuestPtr) { #[no_mangle] pub unsafe extern "C" fn vm_hooks__evm_gas_left() -> u64 { - hostio!(evm_gas_left()) + hostio!(evm_gas_left()).0 } #[no_mangle] pub unsafe extern "C" fn vm_hooks__evm_ink_left() -> u64 { - hostio!(evm_ink_left()) + hostio!(evm_ink_left()).0 } #[no_mangle] diff --git a/arbitrator/wasm-libraries/user-test/src/ink.rs b/arbitrator/wasm-libraries/user-test/src/ink.rs index fca658e59b..72ecfadd96 100644 --- a/arbitrator/wasm-libraries/user-test/src/ink.rs +++ b/arbitrator/wasm-libraries/user-test/src/ink.rs @@ -2,6 +2,7 @@ // For license information, see https://github.com/OffchainLabs/nitro/blob/master/LICENSE use crate::{program::Program, CONFIG}; +use arbutil::evm::api::Ink; use prover::programs::{ config::PricingParams, prelude::{GasMeteredMachine, MachineMeter, MeteredMachine}, @@ -18,7 +19,7 @@ impl MeteredMachine for Program { fn ink_left(&self) -> MachineMeter { unsafe { match user_ink_status() { - 0 => MachineMeter::Ready(user_ink_left()), + 0 => MachineMeter::Ready(Ink(user_ink_left())), _ => MachineMeter::Exhausted, } } @@ -26,7 +27,7 @@ impl MeteredMachine for Program { fn set_meter(&mut self, meter: MachineMeter) { unsafe { - user_set_ink(meter.ink(), meter.status()); + user_set_ink(meter.ink().0, meter.status()); } } } diff --git a/arbitrator/wasm-libraries/user-test/src/program.rs b/arbitrator/wasm-libraries/user-test/src/program.rs index 85b522ee74..299fca08c3 100644 --- a/arbitrator/wasm-libraries/user-test/src/program.rs +++ b/arbitrator/wasm-libraries/user-test/src/program.rs @@ -4,7 +4,7 @@ use crate::{ARGS, EVER_PAGES, EVM_DATA, KEYS, LOGS, OPEN_PAGES, OUTS}; use arbutil::{ evm::{ - api::{EvmApi, VecReader}, + api::{EvmApi, Gas, Ink, VecReader}, user::UserOutcomeKind, EvmData, }, @@ -80,7 +80,7 @@ impl UserHost for Program { println!("{} {text}", "Stylus says:".yellow()); } - fn trace(&mut self, name: &str, args: &[u8], outs: &[u8], _end_ink: u64) { + fn trace(&mut self, name: &str, args: &[u8], outs: &[u8], _end_ink: Ink) { let args = hex::encode(args); let outs = hex::encode(outs); println!("Error: unexpected hostio tracing info for {name} while proving: {args}, {outs}"); @@ -102,18 +102,18 @@ impl Program { pub struct MockEvmApi; impl EvmApi for MockEvmApi { - fn get_bytes32(&mut self, key: Bytes32, _evm_api_gas_to_use: u64) -> (Bytes32, u64) { + fn get_bytes32(&mut self, key: Bytes32, _evm_api_gas_to_use: Gas) -> (Bytes32, Gas) { let value = KEYS.lock().get(&key).cloned().unwrap_or_default(); - (value, 2100) // pretend worst case + (value, Gas(2100)) // pretend worst case } - fn cache_bytes32(&mut self, key: Bytes32, value: Bytes32) -> u64 { + fn cache_bytes32(&mut self, key: Bytes32, value: Bytes32) -> Gas { KEYS.lock().insert(key, value); - 0 + Gas(0) } - fn flush_storage_cache(&mut self, _clear: bool, _gas_left: u64) -> Result { - Ok(22100 * KEYS.lock().len() as u64) // pretend worst case + fn flush_storage_cache(&mut self, _clear: bool, _gas_left: Gas) -> Result { + Ok(Gas(22100) * KEYS.lock().len() as u64) // pretend worst case } fn get_transient_bytes32(&mut self, _key: Bytes32) -> Bytes32 { @@ -130,10 +130,10 @@ impl EvmApi for MockEvmApi { &mut self, _contract: Bytes20, _calldata: &[u8], - _gas_left: u64, - _gas_req: u64, + _gas_left: Gas, + _gas_req: Gas, _value: Bytes32, - ) -> (u32, u64, UserOutcomeKind) { + ) -> (u32, Gas, UserOutcomeKind) { unimplemented!() } @@ -141,9 +141,9 @@ impl EvmApi for MockEvmApi { &mut self, _contract: Bytes20, _calldata: &[u8], - _gas_left: u64, - _gas_req: u64, - ) -> (u32, u64, UserOutcomeKind) { + _gas_left: Gas, + _gas_req: Gas, + ) -> (u32, Gas, UserOutcomeKind) { unimplemented!() } @@ -151,9 +151,9 @@ impl EvmApi for MockEvmApi { &mut self, _contract: Bytes20, _calldata: &[u8], - _gas_left: u64, - _gas_req: u64, - ) -> (u32, u64, UserOutcomeKind) { + _gas_left: Gas, + _gas_req: Gas, + ) -> (u32, Gas, UserOutcomeKind) { unimplemented!() } @@ -161,8 +161,8 @@ impl EvmApi for MockEvmApi { &mut self, _code: Vec, _endowment: Bytes32, - _gas: u64, - ) -> (Result, u32, u64) { + _gas: Gas, + ) -> (Result, u32, Gas) { unimplemented!() } @@ -171,8 +171,8 @@ impl EvmApi for MockEvmApi { _code: Vec, _endowment: Bytes32, _salt: Bytes32, - _gas: u64, - ) -> (Result, u32, u64) { + _gas: Gas, + ) -> (Result, u32, Gas) { unimplemented!() } @@ -185,19 +185,19 @@ impl EvmApi for MockEvmApi { Ok(()) } - fn account_balance(&mut self, _address: Bytes20) -> (Bytes32, u64) { + fn account_balance(&mut self, _address: Bytes20) -> (Bytes32, Gas) { unimplemented!() } - fn account_code(&mut self, _address: Bytes20, _gas_left: u64) -> (VecReader, u64) { + fn account_code(&mut self, _address: Bytes20, _gas_left: Gas) -> (VecReader, Gas) { unimplemented!() } - fn account_codehash(&mut self, _address: Bytes20) -> (Bytes32, u64) { + fn account_codehash(&mut self, _address: Bytes20) -> (Bytes32, Gas) { unimplemented!() } - fn add_pages(&mut self, pages: u16) -> u64 { + fn add_pages(&mut self, pages: u16) -> Gas { let model = MemoryModel::new(2, 1000); unsafe { let (open, ever) = (OPEN_PAGES, EVER_PAGES); @@ -212,8 +212,8 @@ impl EvmApi for MockEvmApi { _name: &str, _args: &[u8], _outs: &[u8], - _start_ink: u64, - _end_ink: u64, + _start_ink: Ink, + _end_ink: Ink, ) { unimplemented!() } diff --git a/arbnode/batch_poster.go b/arbnode/batch_poster.go index 44b360e76e..46a0160b71 100644 --- a/arbnode/batch_poster.go +++ b/arbnode/batch_poster.go @@ -98,7 +98,6 @@ type BatchPoster struct { arbOSVersionGetter execution.FullExecutionClient config BatchPosterConfigFetcher seqInbox *bridgegen.SequencerInbox - bridge *bridgegen.Bridge syncMonitor *SyncMonitor seqInboxABI *abi.ABI seqInboxAddr common.Address @@ -172,6 +171,7 @@ type BatchPosterConfig struct { Dangerous BatchPosterDangerousConfig `koanf:"dangerous"` ReorgResistanceMargin time.Duration `koanf:"reorg-resistance-margin" reload:"hot"` CheckBatchCorrectness bool `koanf:"check-batch-correctness"` + MaxEmptyBatchDelay time.Duration `koanf:"max-empty-batch-delay"` gasRefunder common.Address l1BlockBound l1BlockBound @@ -225,6 +225,7 @@ func BatchPosterConfigAddOptions(prefix string, f *pflag.FlagSet) { f.Uint64(prefix+".gas-estimate-base-fee-multiple-bips", uint64(DefaultBatchPosterConfig.GasEstimateBaseFeeMultipleBips), "for gas estimation, use this multiple of the basefee (measured in basis points) as the max fee per gas") f.Duration(prefix+".reorg-resistance-margin", DefaultBatchPosterConfig.ReorgResistanceMargin, "do not post batch if its within this duration from layer 1 minimum bounds. Requires l1-block-bound option not be set to \"ignore\"") f.Bool(prefix+".check-batch-correctness", DefaultBatchPosterConfig.CheckBatchCorrectness, "setting this to true will run the batch against an inbox multiplexer and verifies that it produces the correct set of messages") + f.Duration(prefix+".max-empty-batch-delay", DefaultBatchPosterConfig.MaxEmptyBatchDelay, "maximum empty batch posting delay, batch poster will only be able to post an empty batch if this time period building a batch has passed") redislock.AddConfigOptions(prefix+".redis-lock", f) dataposter.DataPosterConfigAddOptions(prefix+".data-poster", f, dataposter.DefaultDataPosterConfig) genericconf.WalletConfigAddOptions(prefix+".parent-chain-wallet", f, DefaultBatchPosterConfig.ParentChainWallet.Pathname) @@ -256,6 +257,7 @@ var DefaultBatchPosterConfig = BatchPosterConfig{ GasEstimateBaseFeeMultipleBips: arbmath.OneInUBips * 3 / 2, ReorgResistanceMargin: 10 * time.Minute, CheckBatchCorrectness: true, + MaxEmptyBatchDelay: 3 * 24 * time.Hour, } var DefaultBatchPosterL1WalletConfig = genericconf.WalletConfig{ @@ -309,10 +311,7 @@ func NewBatchPoster(ctx context.Context, opts *BatchPosterOpts) (*BatchPoster, e if err != nil { return nil, err } - bridge, err := bridgegen.NewBridge(opts.DeployInfo.Bridge, opts.L1Reader.Client()) - if err != nil { - return nil, err - } + if err = opts.Config().Validate(); err != nil { return nil, err } @@ -340,7 +339,6 @@ func NewBatchPoster(ctx context.Context, opts *BatchPosterOpts) (*BatchPoster, e arbOSVersionGetter: opts.VersionGetter, syncMonitor: opts.SyncMonitor, config: opts.Config, - bridge: bridge, seqInbox: seqInbox, seqInboxABI: seqInboxABI, seqInboxAddr: opts.DeployInfo.SequencerInbox, @@ -716,12 +714,14 @@ type batchSegments struct { } type buildingBatch struct { - segments *batchSegments - startMsgCount arbutil.MessageIndex - msgCount arbutil.MessageIndex - haveUsefulMessage bool - use4844 bool - muxBackend *simulatedMuxBackend + segments *batchSegments + startMsgCount arbutil.MessageIndex + msgCount arbutil.MessageIndex + haveUsefulMessage bool + use4844 bool + muxBackend *simulatedMuxBackend + firstNonDelayedMsg *arbostypes.MessageWithMetadata + firstUsefulMsg *arbostypes.MessageWithMetadata } func newBatchSegments(firstDelayed uint64, config *BatchPosterConfig, backlog uint64, use4844 bool) *batchSegments { @@ -1176,12 +1176,6 @@ func (b *BatchPoster) maybePostSequencerBatch(ctx context.Context) (bool, error) // There's nothing after the newest batch, therefore batch posting was not required return false, nil } - firstMsg, err := b.streamer.GetMessage(batchPosition.MessageCount) - if err != nil { - return false, err - } - // #nosec G115 - firstMsgTime := time.Unix(int64(firstMsg.Message.Header.Timestamp), 0) lastPotentialMsg, err := b.streamer.GetMessage(msgCount - 1) if err != nil { @@ -1189,7 +1183,7 @@ func (b *BatchPoster) maybePostSequencerBatch(ctx context.Context) (bool, error) } config := b.config() - forcePostBatch := config.MaxDelay <= 0 || time.Since(firstMsgTime) >= config.MaxDelay + forcePostBatch := config.MaxDelay <= 0 var l1BoundMaxBlockNumber uint64 = math.MaxUint64 var l1BoundMaxTimestamp uint64 = math.MaxUint64 @@ -1301,6 +1295,9 @@ func (b *BatchPoster) maybePostSequencerBatch(ctx context.Context) (bool, error) forcePostBatch = true } b.building.haveUsefulMessage = true + if b.building.firstUsefulMsg == nil { + b.building.firstUsefulMsg = msg + } break } if config.CheckBatchCorrectness { @@ -1309,15 +1306,32 @@ func (b *BatchPoster) maybePostSequencerBatch(ctx context.Context) (bool, error) b.building.muxBackend.delayedInbox = append(b.building.muxBackend.delayedInbox, msg) } } - if msg.Message.Header.Kind != arbostypes.L1MessageType_BatchPostingReport { + // #nosec G115 + timeSinceMsg := time.Since(time.Unix(int64(msg.Message.Header.Timestamp), 0)) + if (msg.Message.Header.Kind != arbostypes.L1MessageType_BatchPostingReport) || (timeSinceMsg >= config.MaxEmptyBatchDelay) { b.building.haveUsefulMessage = true + if b.building.firstUsefulMsg == nil { + b.building.firstUsefulMsg = msg + } + } + if !isDelayed && b.building.firstNonDelayedMsg == nil { + b.building.firstNonDelayedMsg = msg } b.building.msgCount++ } - if hasL1Bound && config.ReorgResistanceMargin > 0 { - firstMsgBlockNumber := firstMsg.Message.Header.BlockNumber - firstMsgTimeStamp := firstMsg.Message.Header.Timestamp + firstUsefulMsgTime := time.Now() + if b.building.firstUsefulMsg != nil { + // #nosec G115 + firstUsefulMsgTime = time.Unix(int64(b.building.firstUsefulMsg.Message.Header.Timestamp), 0) + if time.Since(firstUsefulMsgTime) >= config.MaxDelay { + forcePostBatch = true + } + } + + if b.building.firstNonDelayedMsg != nil && hasL1Bound && config.ReorgResistanceMargin > 0 { + firstMsgBlockNumber := b.building.firstNonDelayedMsg.Message.Header.BlockNumber + firstMsgTimeStamp := b.building.firstNonDelayedMsg.Message.Header.Timestamp // #nosec G115 batchNearL1BoundMinBlockNumber := firstMsgBlockNumber <= arbmath.SaturatingUAdd(l1BoundMinBlockNumber, uint64(config.ReorgResistanceMargin/ethPosBlockTime)) // #nosec G115 @@ -1468,7 +1482,7 @@ func (b *BatchPoster) maybePostSequencerBatch(ctx context.Context) (bool, error) } tx, err := b.dataPoster.PostTransaction(ctx, - firstMsgTime, + firstUsefulMsgTime, nonce, newMeta, b.seqInboxAddr, diff --git a/arbnode/dataposter/data_poster.go b/arbnode/dataposter/data_poster.go index acbf9c4cc8..373d247696 100644 --- a/arbnode/dataposter/data_poster.go +++ b/arbnode/dataposter/data_poster.go @@ -34,7 +34,6 @@ import ( "github.com/ethereum/go-ethereum/rlp" "github.com/ethereum/go-ethereum/rpc" "github.com/ethereum/go-ethereum/signer/core/apitypes" - "github.com/go-redis/redis/v8" "github.com/holiman/uint256" "github.com/offchainlabs/nitro/arbnode/dataposter/dbstorage" "github.com/offchainlabs/nitro/arbnode/dataposter/noop" @@ -46,6 +45,7 @@ import ( "github.com/offchainlabs/nitro/util/rpcclient" "github.com/offchainlabs/nitro/util/signature" "github.com/offchainlabs/nitro/util/stopwaiter" + "github.com/redis/go-redis/v9" "github.com/spf13/pflag" redisstorage "github.com/offchainlabs/nitro/arbnode/dataposter/redis" diff --git a/arbnode/dataposter/redis/redisstorage.go b/arbnode/dataposter/redis/redisstorage.go index 8b6dcf65ac..b54abf618b 100644 --- a/arbnode/dataposter/redis/redisstorage.go +++ b/arbnode/dataposter/redis/redisstorage.go @@ -9,9 +9,9 @@ import ( "errors" "fmt" - "github.com/go-redis/redis/v8" "github.com/offchainlabs/nitro/arbnode/dataposter/storage" "github.com/offchainlabs/nitro/util/signature" + "github.com/redis/go-redis/v9" ) // Storage implements redis sorted set backed storage. It does not support @@ -196,7 +196,7 @@ func (s *Storage) Put(ctx context.Context, index uint64, prev, new *storage.Queu if err != nil { return err } - if err := pipe.ZAdd(ctx, s.key, &redis.Z{ + if err := pipe.ZAdd(ctx, s.key, redis.Z{ Score: float64(index), Member: string(signedItem), }).Err(); err != nil { diff --git a/arbnode/dataposter/testdata/client.crt b/arbnode/dataposter/testdata/client.crt index 3d494be820..9171094ba5 100644 --- a/arbnode/dataposter/testdata/client.crt +++ b/arbnode/dataposter/testdata/client.crt @@ -1,28 +1,25 @@ -----BEGIN CERTIFICATE----- -MIIE0jCCA7qgAwIBAgIUPaBB3/hHMpZfGB3VOw1+mHG4LnUwDQYJKoZIhvcNAQEL +MIIEIjCCAwqgAwIBAgIUV1axsouzA9h1Vgr2cPv17AvUrKswDQYJKoZIhvcNAQEL BQAwgYMxCzAJBgNVBAYTAkNIMQswCQYDVQQIDAJaSDEPMA0GA1UEBwwGWnVyaWNo MRYwFAYDVQQKDA1PZmZjaGFpbiBMYWJzMRIwEAYDVQQDDAlsb2NhbGhvc3QxKjAo -BgkqhkiG9w0BCQEWG25vdGFiaWdkZWFsQG9mZmNoYWlubGFicy5jaDAeFw0yMzEw -MTYxNDU2MjhaFw0yNDEwMTUxNDU2MjhaMIGDMQswCQYDVQQGEwJDSDELMAkGA1UE -CAwCWkgxDzANBgNVBAcMBlp1cmljaDEWMBQGA1UECgwNT2ZmY2hhaW4gTGFiczES -MBAGA1UEAwwJbG9jYWxob3N0MSowKAYJKoZIhvcNAQkBFhtub3RhYmlnZGVhbEBv -ZmZjaGFpbmxhYnMuY2gwggEiMA0GCSqGSIb3DQEBAQUAA4IBDwAwggEKAoIBAQC1 -1asfUzv07QTVwlM4o3g51ilIFEApPkpdQej/GIItLEVRQW+GI9jYuEM07wdwMhSH -JPFNbZB3dmBuqDLx13hY03ufyeY+nab0/sO6x13kXChvIqgPRyJtkEAoYkMM3W0D -S6HeL/6DFoTQ2xAlZb/7i/9deuUwDL3MNVSjPCm9PjFzSOFgAQQud2uUT7aENGuG -Whw3oXz9gU/8gv3keLzcIa2PHyEW5M7jeGSYMjfW3wr0d+Z5mSNRc/U6kncKi06c -QrMKrgFfF7a5kHgxUL7bRCGgCMemXe7VfrW6oKT11JcLWDKhe+uo6bNXUptek55H -HfQi6x8cbM46/h3riZA3AgMBAAGjggE6MIIBNjAdBgNVHQ4EFgQUQD2BOems0+JQ -br234cW5noMmXRIwga0GA1UdIwSBpTCBoqGBiaSBhjCBgzELMAkGA1UEBhMCQ0gx -CzAJBgNVBAgMAlpIMQ8wDQYDVQQHDAZadXJpY2gxFjAUBgNVBAoMDU9mZmNoYWlu -IExhYnMxEjAQBgNVBAMMCWxvY2FsaG9zdDEqMCgGCSqGSIb3DQEJARYbbm90YWJp -Z2RlYWxAb2ZmY2hhaW5sYWJzLmNoghQ9oEHf+Ecyll8YHdU7DX6YcbgudTAJBgNV -HRMEAjAAMAsGA1UdDwQEAwIFoDAfBgNVHREEGDAWgglsb2NhbGhvc3SCCTEyNy4w -LjAuMTAsBglghkgBhvhCAQ0EHxYdT3BlblNTTCBHZW5lcmF0ZWQgQ2VydGlmaWNh -dGUwDQYJKoZIhvcNAQELBQADggEBAF4EVkOZZeMIvv0JViP7NsmIl2ke/935x6Hd -hQiLUw13XHYXzMa5/8Y5fnKjttBODpFoQlwjgI18vzuYzItYMBc2cabQJcpfG+Wq -M3m/wl1TC2XOuHj1E4RA/nU3tslntahtXG+vkks9RN+f9irHUhDRR6AGSnSB2Gi/ -B2OGmXn7S4Qge8+fGHAjN+tlu+tOoEWP6R3if/a9UIe5EGM8QTe4zw6lr+iPrOhC -M94pK5IEWn5IIGhr3zJIYkm/Dp+rFqhV1sqPOjjFLVCA7KJ3jVVVHlcm4Xa/+fyk -CIm7/VAmnbeUNlMbkXNOfQMeku8Iwsu80pvf3kjhU/PgO/5oojk= +BgkqhkiG9w0BCQEWG25vdGFiaWdkZWFsQG9mZmNoYWlubGFicy5jaDAgFw0yNDEw +MTYwMzI1NDdaGA8yMTI0MDkyMjAzMjU0N1owgYMxCzAJBgNVBAYTAkNIMQswCQYD +VQQIDAJaSDEPMA0GA1UEBwwGWnVyaWNoMRYwFAYDVQQKDA1PZmZjaGFpbiBMYWJz +MRIwEAYDVQQDDAlsb2NhbGhvc3QxKjAoBgkqhkiG9w0BCQEWG25vdGFiaWdkZWFs +QG9mZmNoYWlubGFicy5jaDCCASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoCggEB +AKU5q5iwYV4/gPeWcyys561pTGV4pk+sRY2q0znsZFOYcxrjaXEjj2HGNkvH1rKy +8Cv1ZoFW+1ejQZeLtd0qL9v5fDkdLsmCZaIYI5Bvo2CfY6KLUZ5c1q2K2GZgQk8i +eSbqBXq+F/EwziDfheXkhDoAE05hOg684titb21eJ0ZK7f7Koam7cmbQI0lqUCrt +MLp0cJzWnfW0SpCzahnCZ5h31BZeZIRLOxsTvg5N1wOivrdWLXGVbprNCGGhVg0E +ZxhwI00pU/E/K4mcKjtPy/5fqe71jH7/iLYNmhRp6PrA78GilxTT79rro8ooantD +GyQbm+Qkk2tMHHum3GOcjuECAwEAAaOBiTCBhjAdBgNVHQ4EFgQUIUFF6jA0NkRA +1kZhJKH0W/9zJWIwCQYDVR0TBAIwADALBgNVHQ8EBAMCBaAwHwYDVR0RBBgwFoIJ +bG9jYWxob3N0ggkxMjcuMC4wLjEwLAYJYIZIAYb4QgENBB8WHU9wZW5TU0wgR2Vu +ZXJhdGVkIENlcnRpZmljYXRlMA0GCSqGSIb3DQEBCwUAA4IBAQCkfknujeFa0yf4 +YX/3ltP9itq4hLtAYnQF7M/uC86QdyDPsrNqhvj54qC0BnR5wGeZP3c144J2mAUr +4j4Y/ztgFVBR4rLyatHgm0/tL/fy/UgjeSmpY4UOr1QnpNP3fIzL7hxacS4uO8v4 +wcc5KlG/xjHRcrzJaaWLldCogBMb8vlModcbeKrkvQ4hUF+zf138RtpRfcRf1X5c +EaAtUZk+BxVYS79qL7YyESRD8YYMhIImLuiyPt2V3HQRhrjqa3mzODBLhUbNRWPX +8/CH2UZ6TD9Hy4FVX0VZzLoDZjfi4KCTgXI3WGrDoL4FF26cSiK8HVx0qJzAWw4a +tkkj5jtd -----END CERTIFICATE----- diff --git a/arbnode/dataposter/testdata/client.key b/arbnode/dataposter/testdata/client.key index b14941dd9f..4313d0f124 100644 --- a/arbnode/dataposter/testdata/client.key +++ b/arbnode/dataposter/testdata/client.key @@ -1,28 +1,28 @@ -----BEGIN PRIVATE KEY----- -MIIEvwIBADANBgkqhkiG9w0BAQEFAASCBKkwggSlAgEAAoIBAQC11asfUzv07QTV -wlM4o3g51ilIFEApPkpdQej/GIItLEVRQW+GI9jYuEM07wdwMhSHJPFNbZB3dmBu -qDLx13hY03ufyeY+nab0/sO6x13kXChvIqgPRyJtkEAoYkMM3W0DS6HeL/6DFoTQ -2xAlZb/7i/9deuUwDL3MNVSjPCm9PjFzSOFgAQQud2uUT7aENGuGWhw3oXz9gU/8 -gv3keLzcIa2PHyEW5M7jeGSYMjfW3wr0d+Z5mSNRc/U6kncKi06cQrMKrgFfF7a5 -kHgxUL7bRCGgCMemXe7VfrW6oKT11JcLWDKhe+uo6bNXUptek55HHfQi6x8cbM46 -/h3riZA3AgMBAAECggEADUboCYMCpm+LqIhzNCtqswQD6QsiSwCmqs8nuKZGk9ue -+hmZj5IpgMJZLrgvWY4s+PGfgiRR/28QCBrVXkETiZ5zirQFN4tvLlKcSK4xZf29 -FBRUCiPxck36NhiqrBNOi1Mn8BKedl4cESkvSu1cvcmeOh100HPcHfLDVqHx3qsl -D/5yMkT2+zdhtLa+X3nkAa+3aibOvgtyfkV679e20CG6h89N9GBKkTXO8ioLZZVm -84ksnd4FcpTo7ebJJxElEB+ZA4akPHbF6ArUmcpqtGso5GtwqqO2ZlguSn2XQT0d -jqvOG4DwfSXk6SpE/dpWvU92fmxWAxZvGrZNgDyJ2QKBgQDyQ8NN4b80Yza/YXar -LWx8A6B0eMc1dXgt9m3UUI+titt45jEcaXhCX01FRFTznWGmWFtJmcWBoaQVPVel -IcDYQSxEuBUrCeI75ocv/IQtENaiX3TK7Nlz5RHfpQpfDVJq45lpiD38CGkYkAif -9pSzC8aup4W3WR0JJZ1AOHUZaQKBgQDAJNJnaSNzB+eDWTKCIN5V9X3QMkmjsuir -Nf2lBXHYARnlYWAbtYFG12wLJQMTNX5ewVQQrWtsdPkGPpCnPLelUTxMssrsXjej -JlLzYUfzRBqEXMI3AA9bVdiauxId2RTcp2F81SM1keCMcuHYxrzVkBSOC9u3wCnb -Whb6+feInwKBgQCbzgC5AcoaQwReqKvNAvWV/C8hONvFAbs8tBOGTBlbHsZvRnun -Lh1tciUbuwp3cmvuszxiZUakS/RexIitZrvDWIbD2y+h8kVRCL1Am0HWSdH/syxF -pXVkF5obHuVApCyxGZb8S+axRCdy6I7jcY3IaHZqtMpGVEVcMJilSKnmoQKBgQCC -tEmgaMfhhx34nqOaG4vDA4T7LEolnh1h4g9RwztnCZC5FZ1QHA79xqrLhfjqhzgY -cwChe6aYl5WSptq1uLrgLTuMnQ8m7QyB4h8JSkKse8ZiBctjqJnJssLutpSjUzk6 -xG2vgjk6RqpuP/PcB40K5cDlw7FJ9OFEQqthPMsi1wKBgQC0/vv5bY3DQ+wV6gUy -nFoSa/XNHaa8y7jmmlCnWJqs6DAAQQ3VW0tPX03GYL/NDcI+PwzYDHDkSB6Qa/o8 -VzVGK1/kr/+bveNvqmi0vNb54fMFLveGgsY4Cu1cffiw8m6nYJ/V4eCsHfpF1B5L -5HDnt5rFKt1Mi9WsUSRtxipxBA== +MIIEvAIBADANBgkqhkiG9w0BAQEFAASCBKYwggSiAgEAAoIBAQClOauYsGFeP4D3 +lnMsrOetaUxleKZPrEWNqtM57GRTmHMa42lxI49hxjZLx9aysvAr9WaBVvtXo0GX +i7XdKi/b+Xw5HS7JgmWiGCOQb6Ngn2Oii1GeXNatithmYEJPInkm6gV6vhfxMM4g +34Xl5IQ6ABNOYToOvOLYrW9tXidGSu3+yqGpu3Jm0CNJalAq7TC6dHCc1p31tEqQ +s2oZwmeYd9QWXmSESzsbE74OTdcDor63Vi1xlW6azQhhoVYNBGcYcCNNKVPxPyuJ +nCo7T8v+X6nu9Yx+/4i2DZoUaej6wO/BopcU0+/a66PKKGp7QxskG5vkJJNrTBx7 +ptxjnI7hAgMBAAECggEAAagHWVuDTl+SmmjOtMby96ETm/zOpgPTGq14up7tDo17 +sexPtUum91L2XmIde+MhVz95jJhjoqhHUw6afyIaIrlojmYFfw2omSxmxt7no2NV +q158Lfs+R7UZoEUcxRBSaJp1/ZoEQW2800WKYRieXrp7dxCwdU9dctCiSlVkTWcU +w+f9xr084dUIKgtICbLWRdGDvGFmr99MBZXzHg5+x8MiAVtpiNcggRfQKIg2QYQv +xdUtfxrKHuRcbeo4QOgSR6fb772F5eO6hPfwgl0AqmSX9XyRaPox/vKcq531S95S +JvGeAdS47Qo8Elh9rIlC/pxVdJ/Gz4sbmlYCfcXDRQKBgQDbSFUrEaOnH5ITEDy/ +SDTCbdQ3bP1FmHVoLdCRoBohb0xHZrJoOn6cmxyyHWR6dwpv+rvi1bJCScDdphJM +zV8W5sG94PaM/dwCws4CFAwaAlMakrsNUXtMgIeub27mzX5OMTss8vjKdKbVDyAv +XCT4idJY1EOdLA3R+JTLSszxJwKBgQDA5CSKLn4HpZI6qmR5g7HRZ5g249BbX/q8 +oszAUIFfY0ME5aujWYRmTfdWno0Y2yG9x4g9QDVNK9fUH3Ii6pNRFGc/yF3HkbsP +kT6UW6rw9CeyyYPKjrFx7M+2kBWJ16+5noVvzWhLScMCt7IcVCKaqiJFapOkS75t +zBYH1IX0twKBgCtFBqlM/cIIlMZ2OcZ09RQ4n9ugAgotn11DTRivQvi+AYtFVIcE +o987LFppOl6ABus5ysFj8Zzq+MfD8XB+Rfk655gUQBJqNXPGBOicFBc9xjBEK+zg +2zepVRyymGuquPWs+URRXY51nkYEihFOWW1BpOQqXn0xKDj6mEHVLMOZAoGAc7Ol +k1ll8ZJIT3ZLxHPRYqmALVSjc1v0G9iPdsATijMRTUuyk84rU+5qcYOzYPh4mcyp +FQyBrGOjF7MxFG6epSDW+fRnBEGO8jyOTBFcTSI2+dBUhFjpaUvCIGD2+nLtDitf +IPwWFisNlYC4jrOM+jcZTYgrPX7NoDCt+k5pd6sCgYB7NMYEC9XjoXV8S3n2yni5 +y0oGLox39hh31LJ90yQ/l+oFJWn1BPnzTI6xeJTTzJjQf7padyvfw45b+3BywZHM +TI5LWcIaA6L+tiBlgBjYa7gE3CCxh0AdV+pUa8L/R6OWAK6+lg2zNt1/ommZ2sKg +LcbNAqMiMWH1a1el7idoBA== -----END PRIVATE KEY----- diff --git a/arbnode/dataposter/testdata/localhost.crt b/arbnode/dataposter/testdata/localhost.crt index ca33dfc8cc..8b7fb02c9a 100644 --- a/arbnode/dataposter/testdata/localhost.crt +++ b/arbnode/dataposter/testdata/localhost.crt @@ -1,28 +1,24 @@ -----BEGIN CERTIFICATE----- -MIIEwzCCA6ugAwIBAgIUHx3SdpCP5jXZE7USUqX5uRNFKPIwDQYJKoZIhvcNAQEL -BQAwfzELMAkGA1UEBhMCQ0gxCzAJBgNVBAgMAlpIMQ8wDQYDVQQHDAZadXJpY2gx -FjAUBgNVBAoMDU9mZmNoYWluIExhYnMxEjAQBgNVBAMMCWxvY2FsaG9zdDEmMCQG -CSqGSIb3DQEJARYXYmlnZGVhbEBvZmZjaGFpbmxhYnMuY2gwHhcNMjMxMDE2MTQ0 -MDA1WhcNMjQxMDE1MTQ0MDA1WjB/MQswCQYDVQQGEwJDSDELMAkGA1UECAwCWkgx -DzANBgNVBAcMBlp1cmljaDEWMBQGA1UECgwNT2ZmY2hhaW4gTGFiczESMBAGA1UE -AwwJbG9jYWxob3N0MSYwJAYJKoZIhvcNAQkBFhdiaWdkZWFsQG9mZmNoYWlubGFi -cy5jaDCCASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoCggEBALg7XwaIh4l2Fp8a -MfNMdTQSMPMR0zpnicVTn/eiozWsqlAKaxmQM3PxJ0oVWW3iJ89p4rv5m+UjK6Dr -vsUQOzl8isgyGCTMnkLtxFlyallDNRDawRcuTPuNI9NkdJm+Zz7HooLzFeBDeS13 -iRPEXr1T/4af9MjOxqFvbw5xBY9k4tc2hPp6q00948gPWKIB9Mz4thoB2Hl2rQBY -X/WhjSnre9o9qoyBO0XAsG0mssBs1vPa9/aEp7C5cDY0HCuM1RIjhXnRpb8lC9VQ -aC+FozDffmm23EGVpLmyPs590UOtVJdTUd6Q0TAT6d7fjCRUJ12DendQf2uMFV90 -u6Yj0zUCAwEAAaOCATUwggExMB0GA1UdDgQWBBT2B3FTGFQ49JyBgDGLoZREOIGD -DTCBqAYDVR0jBIGgMIGdoYGEpIGBMH8xCzAJBgNVBAYTAkNIMQswCQYDVQQIDAJa -SDEPMA0GA1UEBwwGWnVyaWNoMRYwFAYDVQQKDA1PZmZjaGFpbiBMYWJzMRIwEAYD -VQQDDAlsb2NhbGhvc3QxJjAkBgkqhkiG9w0BCQEWF2JpZ2RlYWxAb2ZmY2hhaW5s -YWJzLmNoghQfHdJ2kI/mNdkTtRJSpfm5E0Uo8jAJBgNVHRMEAjAAMAsGA1UdDwQE -AwIFoDAfBgNVHREEGDAWgglsb2NhbGhvc3SCCTEyNy4wLjAuMTAsBglghkgBhvhC -AQ0EHxYdT3BlblNTTCBHZW5lcmF0ZWQgQ2VydGlmaWNhdGUwDQYJKoZIhvcNAQEL -BQADggEBAIkhBcnLeeNwUwb+sSG4Qm8JdeplHPMeViNfFIflUfIIYS00JA2q9w8W -+6Nh8s6Dn20lQETUnesYj97BdqzLjFuJYAlblhE+zP8g/3Mkpu+wZAGvQjUIRyGT -C17BEtQQgAnv5pD22jr9hpLl2KowN6Oo1gzilCA+AtMkNZFIGDOxzuIv2u8rSD89 -R/V6UEDMCgusFJnZ/GzKkUNbsrAfNUezNUal+KzMhHGHBwg4jfCNhnAAB43eRtJA -0pSRMMLcUEQnVotXDXYC3DhJmkYp1uXOH/tWs6z9xForOkWFxNMVj+zUWBi7n3Jw -N2BXlb64D96uor13U0dmvQJ72ooJc+A= +MIIEFzCCAv+gAwIBAgITSiI3ITH8yVNHC4Bh412fjdSR2TANBgkqhkiG9w0BAQsF +ADB/MQswCQYDVQQGEwJDSDELMAkGA1UECAwCWkgxDzANBgNVBAcMBlp1cmljaDEW +MBQGA1UECgwNT2ZmY2hhaW4gTGFiczESMBAGA1UEAwwJbG9jYWxob3N0MSYwJAYJ +KoZIhvcNAQkBFhdiaWdkZWFsQG9mZmNoYWlubGFicy5jaDAgFw0yNDEwMTYwMzI1 +NDdaGA8yMTI0MDkyMjAzMjU0N1owfzELMAkGA1UEBhMCQ0gxCzAJBgNVBAgMAlpI +MQ8wDQYDVQQHDAZadXJpY2gxFjAUBgNVBAoMDU9mZmNoYWluIExhYnMxEjAQBgNV +BAMMCWxvY2FsaG9zdDEmMCQGCSqGSIb3DQEJARYXYmlnZGVhbEBvZmZjaGFpbmxh +YnMuY2gwggEiMA0GCSqGSIb3DQEBAQUAA4IBDwAwggEKAoIBAQDK4BRVZ0nU98/f +l+QC4fF60oNtlMPCC/2R6GZoWz7VyhrrXBuol+F9vboAePxLgxeIr/pwHAbFWlW2 +ueAsN9dorC2waf5PDhfOE0gI6w7LysTkO5n7oMFf1KYPSpPJ15WxlobZR8qWeroR +we7z44tQ2F+es+HaqBrrk7jm0GS9AqaledN/ay9SP4CBu029F6nWDnK+VpNWuoN4 +A/pnwGFWrxDf0ftN7BxnxzzdsWs64+kYfz91Mojce2UKGuDTuk/oqOnHhX34bFDc +/e9KGAQqP1I+RuCJmQXW5b55+3WgpvT3u3Mp7478C+AK8GthPjja7go48nHp3uby +drNpTw+bAgMBAAGjgYkwgYYwHQYDVR0OBBYEFG09BO7OJcjB3fRFhPCsjQ6ICb2E +MAkGA1UdEwQCMAAwCwYDVR0PBAQDAgWgMB8GA1UdEQQYMBaCCWxvY2FsaG9zdIIJ +MTI3LjAuMC4xMCwGCWCGSAGG+EIBDQQfFh1PcGVuU1NMIEdlbmVyYXRlZCBDZXJ0 +aWZpY2F0ZTANBgkqhkiG9w0BAQsFAAOCAQEAJHG/5WOmpO7wg3BtTt4b0DoqDJjh +eQb9Woq5xbvliZ1RCp8E6m6BmOr66i4qu5r+31DEeAeQ2M9pG2nJKoayCVi2ygaQ +RAulxIH7o5+JUcZtX6FbRBsS7Go+SLmtkkJ89YVSIF40+2CAQs7loqQjHNeo9/iO +rVKt1Fa6rQhXmv4ItVOwRaMBvXRVw4gc3ObmH0ZBYZrvsE7uQkKX5f6sVKXOX3mm +ofyB+22QMYmx3XvEEQm8ELnjIr5Q8LxqQxHqjLFjyrcrXYVi4+3/PfjIdRr5+qes +H8JWJlAbF/SNncdXRb1jtkdxit56Qo7/Mz/c4Yuh1WLiYcQGJeBpr53dmg== -----END CERTIFICATE----- diff --git a/arbnode/dataposter/testdata/localhost.key b/arbnode/dataposter/testdata/localhost.key index aad9b40b3d..f56aef1e77 100644 --- a/arbnode/dataposter/testdata/localhost.key +++ b/arbnode/dataposter/testdata/localhost.key @@ -1,28 +1,28 @@ -----BEGIN PRIVATE KEY----- -MIIEvQIBADANBgkqhkiG9w0BAQEFAASCBKcwggSjAgEAAoIBAQC4O18GiIeJdhaf -GjHzTHU0EjDzEdM6Z4nFU5/3oqM1rKpQCmsZkDNz8SdKFVlt4ifPaeK7+ZvlIyug -677FEDs5fIrIMhgkzJ5C7cRZcmpZQzUQ2sEXLkz7jSPTZHSZvmc+x6KC8xXgQ3kt -d4kTxF69U/+Gn/TIzsahb28OcQWPZOLXNoT6eqtNPePID1iiAfTM+LYaAdh5dq0A -WF/1oY0p63vaPaqMgTtFwLBtJrLAbNbz2vf2hKewuXA2NBwrjNUSI4V50aW/JQvV -UGgvhaMw335pttxBlaS5sj7OfdFDrVSXU1HekNEwE+ne34wkVCddg3p3UH9rjBVf -dLumI9M1AgMBAAECggEAHuc8oyKrQ5xmooUZHGP2pAeqJNfYXAtqoYpLwtUJ9hKy -1e7NdNIKw3fP/J4UrHk7btAm65us8hSCeMGatEErAhNZT0gR4zhcksMCBPQLkVIT -+HINYjdOzAJqoEbRRUnaVT5VDQy8HmyLCtyqhoGR18XbjshNnhKLYKCJ2z0Lrvf2 -3rU7bbt7/rvLitVhxVL8SIe2jWSfIgcEmEAZMigB9WAnUyQ/tAfbPy1I764LLfzD -nLXn7E2OH7GrxkLjOsH9kfERlur7V7IhC9NE/wI0q+rnILRa7Q3+ifRu8qla3bo1 -iyHl1ZmsYJ8Jnzbu9exzZaQmk42OoFPcMFm0mRe+2QKBgQDvRv0Q5JhBuVurkU98 -lzATwEO0uYmeWDMnHzrFSWAKr/x4LNQ9ytSCfe1aLxgOkZq6dQ3TyZiCYzpmwGz9 -K7/gghxmsVDKeCqiGVZOgFAWy7AhQyF6zM60oqqwSvJHhmGTsA/B5LPUiYe9lITW -ZSLVYkOzha7Coa++U8vPzI5VaQKBgQDFG4reFT79j8RKEm9jie6PdRdYMzOSDWty -Gjj5N9Jnlp1k/6RzCxjmp7w7yIorq/7fWZsQtt0UqgayOn25+I8dZeGC0BradUSB -tZbGElxPsF8Jg00ZvvK3G5mpZYDrJCud8Q05EaUZPXv9GuZhozEsTQgylVecVzsN -wyEK8VuZ7QKBgQChx9adUGIdtgzkILiknbh08j8U94mz1SCo5/WdpLHaKAlE29KZ -AQXUQP51Rng2iX4bab9yndCPADZheON3/debHX3EdUkRzFPPC+CN7TW5Y/jvVGtT -kxyDh6Ru1A2iDJr290iAKXjpUB/GL5/tMa5upiTuQYnasOWZgyC/nCf0WQKBgEwn -pRLDMLA1IMjhsInL3BEvU1KvjahLaQ0P1p1rlO6TAcLpBrewPPG5MwACLmhLLtFK -xJ/Dl02Jl8a61KLKxzi7iVLKZuWq00ouR8/FfkcHxOBfC6X74bkff9I0NogjVHrU -jKBVEe3blJEpGIP20mPka1tn2g68oUNi9dxNfm/NAoGAWj/Q0pgnNq0MQ8Lj6m99 -1baaXSo8biks3E3A3cqhHQm/j3SRnkf0lueQW8+r9yR9IWdYFXz5Waq13qK+lopE -KDmww0xr8dyMUYTP1vde7np2XKa/OX3iejDzbI3RcZN/DEV+dCBY8pqHHfaAaESu -fwBWvfD8wtwCZzB3lOZEi80= +MIIEvQIBADANBgkqhkiG9w0BAQEFAASCBKcwggSjAgEAAoIBAQDK4BRVZ0nU98/f +l+QC4fF60oNtlMPCC/2R6GZoWz7VyhrrXBuol+F9vboAePxLgxeIr/pwHAbFWlW2 +ueAsN9dorC2waf5PDhfOE0gI6w7LysTkO5n7oMFf1KYPSpPJ15WxlobZR8qWeroR +we7z44tQ2F+es+HaqBrrk7jm0GS9AqaledN/ay9SP4CBu029F6nWDnK+VpNWuoN4 +A/pnwGFWrxDf0ftN7BxnxzzdsWs64+kYfz91Mojce2UKGuDTuk/oqOnHhX34bFDc +/e9KGAQqP1I+RuCJmQXW5b55+3WgpvT3u3Mp7478C+AK8GthPjja7go48nHp3uby +drNpTw+bAgMBAAECggEANE2Q8HOwlTdOYFbIcfXOS9v6BkZUMbLlrLg9rqnXiUaR +qhwVBWIiwEgpq/WFFfK2HodACacwF7EyZ+mD4eKDpni9Tr4E0lzPxlEyQRpYtjGQ +kUbMbBMFx68LIOYZM/Bgp2gnW90mXaVGU02sTTRctnsSK9g0Yir0xcdP5DHVxuRy +cReMIDjHu7/PN5X12oHpBxT1w7TKYZk3M2ZbmRcGfRSv1UIg06hXjxY4Tonqxv5b +Yv/R/r+RYsHE6cO3hLV7FE/ypaRFGt/qcHhqwmHKlpc+8/FNEpb5truXCbf5LhjM +YD2eMOpiBaiyufs+2BpUy/iDKToYz+fdusT5N3FgAQKBgQDuemOauY/ytbETUabB +Vt11fCvXvsx4L+45vqtVcRWl6jcy73rMnyA80D/ndzyxkuw2NRAllIUvsdVG26vM +8nWAxrsY7rrZ4kdRAAKAPYmTtT2O4mvhvJOYHA/ueEUamrKqZMgyLik1Wl1y2mt2 +Iak0zNB0GWoHsgDF20TTbxKTkwKBgQDZyAa/r3qhokKfvg/7LhwXH7vdiJd35zp1 +K1KZVeZf97FeReL4DLfTHZ92yFyVhWepp97Icd2WtQZ8jVVcesTP9C+mVoFEGXZf +nWx5y2WD92dBP/kKYNayXuBFQnRfS4AC7ALK8fKrU/Fzc7OctnkvHHWZfaKQ2sQ5 +xydj6Rso2QKBgBpgvT2zAsIU6MY7RNej1REWr/7IIvO0UYRfm7HytTNJ6dsfdBTI +ERfI7RicLsFxf+ErE2Mkv2qcH/wbdjBQLUEWOkGyvkY1ajACcURgCiSlam6wisBI +TIcJq5V0BijALbz9MsuiIXq+SRHYKQTDCmVFtlTxLrI1NTKtYzqD0akzAoGAB5fW +zGYk42/R3Nn2mq5f4lqD5VR224JfYmhxR9Fb5+qt73iGUlm3KxA0WCLiP4BYPe0R +cnGt5SxInp0a5c+N/yYnZyhK94Hfw7OsbY6u6mv82KSPXVJFChEOxrtrbUsnmnJ6 +InNPH7QcjgbxszwVe5QFcaWUvnIyN0V/VRdyj/kCgYEA0kPCIPR7t7OFEnQsw50W +AclrTlGRB0TzmCYZN4DTKC+1ZAV80XQUR4umJpHeyArdRZpFv+HBLF7fHsBnkmiU +ixBdbWgW4mxjAhyZkLMjcCkoiLmRPDUvELyFs4xpM+IEKoAk60PPbGi4CeIMHb5k +E47NZaw1bdG1tv2ya1FlQ/k= -----END PRIVATE KEY----- diff --git a/arbnode/dataposter/testdata/regenerate-certs.sh b/arbnode/dataposter/testdata/regenerate-certs.sh new file mode 100755 index 0000000000..6bcbd27b8e --- /dev/null +++ b/arbnode/dataposter/testdata/regenerate-certs.sh @@ -0,0 +1,8 @@ +#!/bin/bash +set -eu +cd "$(dirname "$0")" +for name in localhost client; do + openssl genrsa -out "$name.key" 2048 + csr="$(openssl req -new -key "$name.key" -config "$name.cnf" -batch)" + openssl x509 -req -signkey "$name.key" -out "$name.crt" -days 36500 -extensions req_ext -extfile "$name.cnf" <<< "$csr" +done diff --git a/arbnode/maintenance.go b/arbnode/maintenance.go index 53d038a0f9..7397229c2e 100644 --- a/arbnode/maintenance.go +++ b/arbnode/maintenance.go @@ -101,7 +101,7 @@ func NewMaintenanceRunner(config MaintenanceConfigFetcher, seqCoordinator *SeqCo if seqCoordinator != nil { c := func() *redislock.SimpleCfg { return &cfg.Lock } r := func() bool { return true } // always ready to lock - rl, err := redislock.NewSimple(seqCoordinator.Client, c, r) + rl, err := redislock.NewSimple(seqCoordinator.RedisCoordinator().Client, c, r) if err != nil { return nil, fmt.Errorf("creating new simple redis lock: %w", err) } diff --git a/arbnode/redislock/redis.go b/arbnode/redislock/redis.go index 7e26010cae..de9508323a 100644 --- a/arbnode/redislock/redis.go +++ b/arbnode/redislock/redis.go @@ -12,8 +12,8 @@ import ( "time" "github.com/ethereum/go-ethereum/log" - "github.com/go-redis/redis/v8" "github.com/offchainlabs/nitro/util/stopwaiter" + "github.com/redis/go-redis/v9" flag "github.com/spf13/pflag" ) diff --git a/arbnode/seq_coordinator.go b/arbnode/seq_coordinator.go index 1fb09c5ca8..fc2f3c9cf6 100644 --- a/arbnode/seq_coordinator.go +++ b/arbnode/seq_coordinator.go @@ -14,7 +14,7 @@ import ( "sync/atomic" "time" - "github.com/go-redis/redis/v8" + "github.com/redis/go-redis/v9" flag "github.com/spf13/pflag" "github.com/ethereum/go-ethereum/log" @@ -37,7 +37,10 @@ var ( type SeqCoordinator struct { stopwaiter.StopWaiter - redisutil.RedisCoordinator + redisCoordinatorMutex sync.RWMutex + redisCoordinator redisutil.RedisCoordinator + prevRedisCoordinator *redisutil.RedisCoordinator + prevRedisMessageCount arbutil.MessageIndex sync *SyncMonitor streamer *TransactionStreamer @@ -61,6 +64,7 @@ type SeqCoordinatorConfig struct { Enable bool `koanf:"enable"` ChosenHealthcheckAddr string `koanf:"chosen-healthcheck-addr"` RedisUrl string `koanf:"redis-url"` + NewRedisUrl string `koanf:"new-redis-url"` LockoutDuration time.Duration `koanf:"lockout-duration"` LockoutSpare time.Duration `koanf:"lockout-spare"` SeqNumDuration time.Duration `koanf:"seq-num-duration"` @@ -86,6 +90,7 @@ func (c *SeqCoordinatorConfig) Url() string { func SeqCoordinatorConfigAddOptions(prefix string, f *flag.FlagSet) { f.Bool(prefix+".enable", DefaultSeqCoordinatorConfig.Enable, "enable sequence coordinator") f.String(prefix+".redis-url", DefaultSeqCoordinatorConfig.RedisUrl, "the Redis URL to coordinate via") + f.String(prefix+".new-redis-url", DefaultSeqCoordinatorConfig.NewRedisUrl, "switch to the new Redis URL to coordinate via") f.String(prefix+".chosen-healthcheck-addr", DefaultSeqCoordinatorConfig.ChosenHealthcheckAddr, "if non-empty, launch an HTTP service binding to this address that returns status code 200 when chosen and 503 otherwise") f.Duration(prefix+".lockout-duration", DefaultSeqCoordinatorConfig.LockoutDuration, "") f.Duration(prefix+".lockout-spare", DefaultSeqCoordinatorConfig.LockoutSpare, "") @@ -105,6 +110,7 @@ var DefaultSeqCoordinatorConfig = SeqCoordinatorConfig{ Enable: false, ChosenHealthcheckAddr: "", RedisUrl: "", + NewRedisUrl: "", LockoutDuration: time.Minute, LockoutSpare: 30 * time.Second, SeqNumDuration: 10 * 24 * time.Hour, @@ -122,6 +128,7 @@ var DefaultSeqCoordinatorConfig = SeqCoordinatorConfig{ var TestSeqCoordinatorConfig = SeqCoordinatorConfig{ Enable: false, RedisUrl: "", + NewRedisUrl: "", LockoutDuration: time.Second * 2, LockoutSpare: time.Millisecond * 10, SeqNumDuration: time.Minute * 10, @@ -153,7 +160,7 @@ func NewSeqCoordinator( return nil, err } coordinator := &SeqCoordinator{ - RedisCoordinator: *redisCoordinator, + redisCoordinator: *redisCoordinator, sync: sync, streamer: streamer, sequencer: sequencer, @@ -174,6 +181,19 @@ func (c *SeqCoordinator) SetDelayedSequencer(delayedSequencer *DelayedSequencer) c.delayedSequencer = delayedSequencer } +func (c *SeqCoordinator) RedisCoordinator() *redisutil.RedisCoordinator { + c.redisCoordinatorMutex.RLock() + defer c.redisCoordinatorMutex.RUnlock() + return &c.redisCoordinator +} + +func (c *SeqCoordinator) setRedisCoordinator(redisCoordinator *redisutil.RedisCoordinator) { + c.redisCoordinatorMutex.Lock() + defer c.redisCoordinatorMutex.Unlock() + c.prevRedisCoordinator = &c.redisCoordinator + c.redisCoordinator = *redisCoordinator +} + func StandaloneSeqCoordinatorInvalidateMsgIndex(ctx context.Context, redisClient redis.UniversalClient, keyConfig string, msgIndex arbutil.MessageIndex) error { signerConfig := signature.EmptySimpleHmacConfig if keyConfig == "" { @@ -276,7 +296,7 @@ func (c *SeqCoordinator) acquireLockoutAndWriteMessage(ctx context.Context, msgC defer c.wantsLockoutMutex.Unlock() setWantsLockout := c.avoidLockout <= 0 lockoutUntil := time.Now().Add(c.config.LockoutDuration) - err = c.Client.Watch(ctx, func(tx *redis.Tx) error { + err = c.RedisCoordinator().Client.Watch(ctx, func(tx *redis.Tx) error { current, err := tx.Get(ctx, redisutil.CHOSENSEQ_KEY).Result() var wasEmpty bool if errors.Is(err, redis.Nil) { @@ -345,7 +365,7 @@ func (c *SeqCoordinator) acquireLockoutAndWriteMessage(ctx context.Context, msgC } func (c *SeqCoordinator) getRemoteFinalizedMsgCount(ctx context.Context) (arbutil.MessageIndex, error) { - resStr, err := c.Client.Get(ctx, redisutil.FINALIZED_MSG_COUNT_KEY).Result() + resStr, err := c.RedisCoordinator().Client.Get(ctx, redisutil.FINALIZED_MSG_COUNT_KEY).Result() if err != nil { return 0, err } @@ -364,23 +384,23 @@ func (c *SeqCoordinator) getRemoteMsgCountImpl(ctx context.Context, r redis.Cmda } func (c *SeqCoordinator) GetRemoteMsgCount() (arbutil.MessageIndex, error) { - return c.getRemoteMsgCountImpl(c.GetContext(), c.Client) + return c.getRemoteMsgCountImpl(c.GetContext(), c.RedisCoordinator().Client) } -func (c *SeqCoordinator) wantsLockoutUpdate(ctx context.Context) error { +func (c *SeqCoordinator) wantsLockoutUpdate(ctx context.Context, client redis.UniversalClient) error { c.wantsLockoutMutex.Lock() defer c.wantsLockoutMutex.Unlock() - return c.wantsLockoutUpdateWithMutex(ctx) + return c.wantsLockoutUpdateWithMutex(ctx, client) } // Requires the caller hold the wantsLockoutMutex -func (c *SeqCoordinator) wantsLockoutUpdateWithMutex(ctx context.Context) error { +func (c *SeqCoordinator) wantsLockoutUpdateWithMutex(ctx context.Context, client redis.UniversalClient) error { if c.avoidLockout > 0 { return nil } myWantsLockoutKey := redisutil.WantsLockoutKeyFor(c.config.Url()) wantsLockoutUntil := time.Now().Add(c.config.LockoutDuration) - pipe := c.Client.TxPipeline() + pipe := client.TxPipeline() initialDuration := c.config.LockoutDuration if initialDuration < 2*time.Second { initialDuration = 2 * time.Second @@ -398,7 +418,7 @@ func (c *SeqCoordinator) wantsLockoutUpdateWithMutex(ctx context.Context) error func (c *SeqCoordinator) chosenOneRelease(ctx context.Context) error { atomicTimeWrite(&c.lockoutUntil, time.Time{}) isActiveSequencer.Update(0) - releaseErr := c.Client.Watch(ctx, func(tx *redis.Tx) error { + releaseErr := c.RedisCoordinator().Client.Watch(ctx, func(tx *redis.Tx) error { current, err := tx.Get(ctx, redisutil.CHOSENSEQ_KEY).Result() if errors.Is(err, redis.Nil) { return nil @@ -421,7 +441,7 @@ func (c *SeqCoordinator) chosenOneRelease(ctx context.Context) error { return nil } // got error - was it still released? - current, readErr := c.Client.Get(ctx, redisutil.CHOSENSEQ_KEY).Result() + current, readErr := c.RedisCoordinator().Client.Get(ctx, redisutil.CHOSENSEQ_KEY).Result() if errors.Is(readErr, redis.Nil) { return nil } @@ -438,10 +458,10 @@ func (c *SeqCoordinator) wantsLockoutRelease(ctx context.Context) error { return nil } myWantsLockoutKey := redisutil.WantsLockoutKeyFor(c.config.Url()) - releaseErr := c.Client.Del(ctx, myWantsLockoutKey).Err() + releaseErr := c.RedisCoordinator().Client.Del(ctx, myWantsLockoutKey).Err() if releaseErr != nil { // got error - was it still deleted? - readErr := c.Client.Get(ctx, myWantsLockoutKey).Err() + readErr := c.RedisCoordinator().Client.Get(ctx, myWantsLockoutKey).Err() if !errors.Is(readErr, redis.Nil) { return releaseErr } @@ -525,7 +545,7 @@ func (c *SeqCoordinator) deleteFinalizedMsgsFromRedis(ctx context.Context, final // In non-init cases it doesn't matter how we delete as we always try to delete from prevFinalized to finalized batchDeleteCount := 1000 for i := len(keys); i > 0; i -= batchDeleteCount { - if err := c.Client.Del(ctx, keys[max(0, i-batchDeleteCount):i]...).Err(); err != nil { + if err := c.RedisCoordinator().Client.Del(ctx, keys[max(0, i-batchDeleteCount):i]...).Err(); err != nil { return fmt.Errorf("error deleting finalized messages and their signatures from redis: %w", err) } } @@ -534,7 +554,7 @@ func (c *SeqCoordinator) deleteFinalizedMsgsFromRedis(ctx context.Context, final if err != nil { return err } - if err = c.Client.Set(ctx, redisutil.FINALIZED_MSG_COUNT_KEY, finalizedBytes, c.config.SeqNumDuration).Err(); err != nil { + if err = c.RedisCoordinator().Client.Set(ctx, redisutil.FINALIZED_MSG_COUNT_KEY, finalizedBytes, c.config.SeqNumDuration).Err(); err != nil { return fmt.Errorf("couldn't set %s key to current finalizedMsgCount in redis: %w", redisutil.FINALIZED_MSG_COUNT_KEY, err) } return nil @@ -543,7 +563,7 @@ func (c *SeqCoordinator) deleteFinalizedMsgsFromRedis(ctx context.Context, final if errors.Is(err, redis.Nil) { var keys []string for msg := finalized - 1; msg > 0; msg-- { - exists, err := c.Client.Exists(ctx, redisutil.MessageKeyFor(msg), redisutil.MessageSigKeyFor(msg)).Result() + exists, err := c.RedisCoordinator().Client.Exists(ctx, redisutil.MessageKeyFor(msg), redisutil.MessageSigKeyFor(msg)).Result() if err != nil { // If there is an error deleting finalized messages during init, we retry later either from this sequencer or from another return err @@ -558,7 +578,7 @@ func (c *SeqCoordinator) deleteFinalizedMsgsFromRedis(ctx context.Context, final } else if err != nil { return fmt.Errorf("error getting finalizedMsgCount value from redis: %w", err) } - remoteMsgCount, err := c.getRemoteMsgCountImpl(ctx, c.Client) + remoteMsgCount, err := c.getRemoteMsgCountImpl(ctx, c.RedisCoordinator().Client) if err != nil { return fmt.Errorf("cannot get remote message count: %w", err) } @@ -574,7 +594,7 @@ func (c *SeqCoordinator) deleteFinalizedMsgsFromRedis(ctx context.Context, final } func (c *SeqCoordinator) update(ctx context.Context) time.Duration { - chosenSeq, err := c.RecommendSequencerWantingLockout(ctx) + chosenSeq, err := c.RedisCoordinator().RecommendSequencerWantingLockout(ctx) if err != nil { log.Warn("coordinator failed finding sequencer wanting lockout", "err", err) return c.retryAfterRedisError() @@ -603,6 +623,15 @@ func (c *SeqCoordinator) update(ctx context.Context) time.Duration { log.Error("cannot read message count", "err", err) return c.config.UpdateInterval } + // Cache the previous redis coordinator's message count + if c.prevRedisCoordinator != nil && c.prevRedisMessageCount == 0 { + prevRemoteMsgCount, err := c.getRemoteMsgCountImpl(ctx, c.prevRedisCoordinator.Client) + if err != nil { + log.Warn("cannot get remote message count", "err", err) + return c.retryAfterRedisError() + } + c.prevRedisMessageCount = prevRemoteMsgCount + } remoteFinalizedMsgCount, err := c.getRemoteFinalizedMsgCount(ctx) if err != nil { loglevel := log.Error @@ -617,12 +646,22 @@ func (c *SeqCoordinator) update(ctx context.Context) time.Duration { return c.retryAfterRedisError() } readUntil := min(localMsgCount+c.config.MsgPerPoll, remoteMsgCount) + client := c.RedisCoordinator().Client + // If we have a previous redis coordinator, + // we can read from it until the local message count catches up to the prev coordinator's message count + if c.prevRedisMessageCount > localMsgCount { + readUntil = min(readUntil, c.prevRedisMessageCount) + client = c.prevRedisCoordinator.Client + } + if c.prevRedisMessageCount != 0 && localMsgCount >= c.prevRedisMessageCount { + log.Info("coordinator caught up to prev redis coordinator", "msgcount", localMsgCount, "prevMsgCount", c.prevRedisMessageCount) + } var messages []arbostypes.MessageWithMetadata msgToRead := localMsgCount var msgReadErr error for msgToRead < readUntil && localMsgCount >= remoteFinalizedMsgCount { var resString string - resString, msgReadErr = c.Client.Get(ctx, redisutil.MessageKeyFor(msgToRead)).Result() + resString, msgReadErr = client.Get(ctx, redisutil.MessageKeyFor(msgToRead)).Result() if msgReadErr != nil && c.sequencer.Synced() { log.Warn("coordinator failed reading message", "pos", msgToRead, "err", msgReadErr) break @@ -631,7 +670,7 @@ func (c *SeqCoordinator) update(ctx context.Context) time.Duration { var sigString string var sigBytes []byte sigSeparateKey := true - sigString, msgReadErr = c.Client.Get(ctx, redisutil.MessageSigKeyFor(msgToRead)).Result() + sigString, msgReadErr = client.Get(ctx, redisutil.MessageSigKeyFor(msgToRead)).Result() if errors.Is(msgReadErr, redis.Nil) { // no separate signature. Try reading old-style sig if len(rsBytes) < 32 { @@ -722,7 +761,7 @@ func (c *SeqCoordinator) update(ctx context.Context) time.Duration { // this could be just new messages we didn't get yet - even then, we should retry soon log.Info("sequencer failed to become chosen", "err", err, "msgcount", localMsgCount) // make sure we're marked as wanting the lockout - if err := c.wantsLockoutUpdate(ctx); err != nil { + if err := c.wantsLockoutUpdate(ctx, c.RedisCoordinator().Client); err != nil { log.Warn("failed to update wants lockout key", "err", err) } c.prevChosenSequencer = "" @@ -750,7 +789,7 @@ func (c *SeqCoordinator) update(ctx context.Context) time.Duration { // update wanting the lockout var wantsLockoutErr error if synced && !c.AvoidingLockout() { - wantsLockoutErr = c.wantsLockoutUpdate(ctx) + wantsLockoutErr = c.wantsLockoutUpdate(ctx, c.RedisCoordinator().Client) } else { wantsLockoutErr = c.wantsLockoutRelease(ctx) } @@ -817,12 +856,59 @@ func (c *SeqCoordinator) launchHealthcheckServer(ctx context.Context) { func (c *SeqCoordinator) Start(ctxIn context.Context) { c.StopWaiter.Start(ctxIn, c) - c.CallIteratively(c.update) + var newRedisCoordinator *redisutil.RedisCoordinator + if c.config.NewRedisUrl != "" { + var err error + newRedisCoordinator, err = redisutil.NewRedisCoordinator(c.config.NewRedisUrl) + if err != nil { + log.Warn("failed to create new redis coordinator", "err", + err, "newRedisUrl", c.config.NewRedisUrl) + } + } + c.CallIteratively(func(ctx context.Context) time.Duration { return c.chooseRedisAndUpdate(ctx, newRedisCoordinator) }) if c.config.ChosenHealthcheckAddr != "" { c.StopWaiter.LaunchThread(c.launchHealthcheckServer) } } +func (c *SeqCoordinator) chooseRedisAndUpdate(ctx context.Context, newRedisCoordinator *redisutil.RedisCoordinator) time.Duration { + // If we have a new redis coordinator, and we haven't switched to it yet, try to switch. + if c.config.NewRedisUrl != "" && c.prevRedisCoordinator == nil { + // If we fail to try to switch, we'll retry soon. + if err := c.trySwitchingRedis(ctx, newRedisCoordinator); err != nil { + log.Warn("error while trying to switch redis coordinator", "err", err) + return c.retryAfterRedisError() + } + } + return c.update(ctx) +} + +func (c *SeqCoordinator) trySwitchingRedis(ctx context.Context, newRedisCoordinator *redisutil.RedisCoordinator) error { + err := c.wantsLockoutUpdate(ctx, newRedisCoordinator.Client) + if err != nil { + return err + } + current, err := c.RedisCoordinator().Client.Get(ctx, redisutil.CHOSENSEQ_KEY).Result() + var wasEmpty bool + if errors.Is(err, redis.Nil) { + wasEmpty = true + err = nil + } + if err != nil { + log.Warn("failed to get current chosen sequencer", "err", err) + return err + } + // If the chosen key is set to switch, we need to switch to the new redis coordinator. + if !wasEmpty && (current == redisutil.SWITCHED_REDIS) { + err = c.wantsLockoutUpdate(ctx, c.RedisCoordinator().Client) + if err != nil { + return err + } + c.setRedisCoordinator(newRedisCoordinator) + } + return nil +} + // Calls check() every c.config.RetryInterval until it returns true, or the context times out. func (c *SeqCoordinator) waitFor(ctx context.Context, check func() bool) bool { for { @@ -872,7 +958,7 @@ func (c *SeqCoordinator) StopAndWait() { time.Sleep(c.retryAfterRedisError()) } } - _ = c.Client.Close() + _ = c.RedisCoordinator().Client.Close() } func (c *SeqCoordinator) CurrentlyChosen() bool { @@ -914,7 +1000,7 @@ func (c *SeqCoordinator) TryToHandoffChosenOne(ctx context.Context) bool { return !c.CurrentlyChosen() }) if success { - wantsLockout, err := c.RecommendSequencerWantingLockout(ctx) + wantsLockout, err := c.RedisCoordinator().RecommendSequencerWantingLockout(ctx) if err == nil { log.Info("released chosen one status; a new sequencer hopefully wants to acquire it", "delay", c.config.SafeShutdownDelay, "wantsLockout", wantsLockout) } else { @@ -936,7 +1022,7 @@ func (c *SeqCoordinator) SeekLockout(ctx context.Context) { log.Info("seeking lockout", "myUrl", c.config.Url()) if c.sequencer.Synced() { // Even if this errors we still internally marked ourselves as wanting the lockout - err := c.wantsLockoutUpdateWithMutex(ctx) + err := c.wantsLockoutUpdateWithMutex(ctx, c.RedisCoordinator().Client) if err != nil { log.Warn("failed to set wants lockout key in redis after seeking lockout again", "err", err) } diff --git a/arbnode/seq_coordinator_test.go b/arbnode/seq_coordinator_test.go index 6498543f3a..3f35011c20 100644 --- a/arbnode/seq_coordinator_test.go +++ b/arbnode/seq_coordinator_test.go @@ -125,7 +125,7 @@ func TestRedisSeqCoordinatorAtomic(t *testing.T) { redisCoordinator, err := redisutil.NewRedisCoordinator(config.RedisUrl) Require(t, err) coordinator := &SeqCoordinator{ - RedisCoordinator: *redisCoordinator, + redisCoordinator: *redisCoordinator, config: config, signer: nullSigner, } @@ -181,7 +181,7 @@ func TestSeqCoordinatorDeletesFinalizedMessages(t *testing.T) { redisCoordinator, err := redisutil.NewRedisCoordinator(config.RedisUrl) Require(t, err) coordinator := &SeqCoordinator{ - RedisCoordinator: *redisCoordinator, + redisCoordinator: *redisCoordinator, config: config, signer: nullSigner, } @@ -191,18 +191,18 @@ func TestSeqCoordinatorDeletesFinalizedMessages(t *testing.T) { msgBytes, err := coordinator.msgCountToSignedBytes(0) Require(t, err) for i := arbutil.MessageIndex(1); i <= 10; i++ { - err = coordinator.Client.Set(ctx, redisutil.MessageKeyFor(i), msgBytes, time.Hour).Err() + err = coordinator.RedisCoordinator().Client.Set(ctx, redisutil.MessageKeyFor(i), msgBytes, time.Hour).Err() Require(t, err) - err = coordinator.Client.Set(ctx, redisutil.MessageSigKeyFor(i), msgBytes, time.Hour).Err() + err = coordinator.RedisCoordinator().Client.Set(ctx, redisutil.MessageSigKeyFor(i), msgBytes, time.Hour).Err() Require(t, err) keys = append(keys, redisutil.MessageKeyFor(i), redisutil.MessageSigKeyFor(i)) } // Set msgCount key msgCountBytes, err := coordinator.msgCountToSignedBytes(11) Require(t, err) - err = coordinator.Client.Set(ctx, redisutil.MSG_COUNT_KEY, msgCountBytes, time.Hour).Err() + err = coordinator.RedisCoordinator().Client.Set(ctx, redisutil.MSG_COUNT_KEY, msgCountBytes, time.Hour).Err() Require(t, err) - exists, err := coordinator.Client.Exists(ctx, keys...).Result() + exists, err := coordinator.RedisCoordinator().Client.Exists(ctx, keys...).Result() Require(t, err) if exists != 20 { t.Fatal("couldn't find all messages and signatures in redis") @@ -213,7 +213,7 @@ func TestSeqCoordinatorDeletesFinalizedMessages(t *testing.T) { Require(t, err) // Check if messages and signatures were deleted successfully - exists, err = coordinator.Client.Exists(ctx, keys[:8]...).Result() + exists, err = coordinator.RedisCoordinator().Client.Exists(ctx, keys[:8]...).Result() Require(t, err) if exists != 0 { t.Fatal("finalized messages and signatures in range 1 to 4 were not deleted") @@ -229,7 +229,7 @@ func TestSeqCoordinatorDeletesFinalizedMessages(t *testing.T) { // Try deleting finalized messages when theres already a finalizedMsgCount err = coordinator.deleteFinalizedMsgsFromRedis(ctx, 7) Require(t, err) - exists, err = coordinator.Client.Exists(ctx, keys[8:12]...).Result() + exists, err = coordinator.RedisCoordinator().Client.Exists(ctx, keys[8:12]...).Result() Require(t, err) if exists != 0 { t.Fatal("finalized messages and signatures in range 5 to 6 were not deleted") @@ -241,7 +241,7 @@ func TestSeqCoordinatorDeletesFinalizedMessages(t *testing.T) { } // Check that non-finalized messages are still available in redis - exists, err = coordinator.Client.Exists(ctx, keys[12:]...).Result() + exists, err = coordinator.RedisCoordinator().Client.Exists(ctx, keys[12:]...).Result() Require(t, err) if exists != 8 { t.Fatal("non-finalized messages and signatures in range 7 to 10 are not fully available") diff --git a/arbos/block_processor.go b/arbos/block_processor.go index b180405c43..19fc36b351 100644 --- a/arbos/block_processor.go +++ b/arbos/block_processor.go @@ -144,6 +144,7 @@ func ProduceBlock( chainContext core.ChainContext, chainConfig *params.ChainConfig, isMsgForPrefetch bool, + runMode core.MessageRunMode, ) (*types.Block, types.Receipts, error) { txes, err := ParseL2Transactions(message, chainConfig.ChainID) if err != nil { @@ -153,7 +154,7 @@ func ProduceBlock( hooks := NoopSequencingHooks() return ProduceBlockAdvanced( - message.Header, txes, delayedMessagesRead, lastBlockHeader, statedb, chainContext, chainConfig, hooks, isMsgForPrefetch, + message.Header, txes, delayedMessagesRead, lastBlockHeader, statedb, chainContext, chainConfig, hooks, isMsgForPrefetch, runMode, ) } @@ -168,6 +169,7 @@ func ProduceBlockAdvanced( chainConfig *params.ChainConfig, sequencingHooks *SequencingHooks, isMsgForPrefetch bool, + runMode core.MessageRunMode, ) (*types.Block, types.Receipts, error) { state, err := arbosState.OpenSystemArbosState(statedb, nil, true) @@ -318,6 +320,7 @@ func ProduceBlockAdvanced( tx, &header.GasUsed, vm.Config{}, + runMode, func(result *core.ExecutionResult) error { return hooks.PostTxFilter(header, state, tx, sender, dataGas, result) }, diff --git a/arbos/programs/native.go b/arbos/programs/native.go index 5fbc512211..725b302ac0 100644 --- a/arbos/programs/native.go +++ b/arbos/programs/native.go @@ -47,11 +47,16 @@ type rustBytes = C.RustBytes type rustSlice = C.RustSlice var ( - stylusLRUCacheSizeBytesGauge = metrics.NewRegisteredGauge("arb/arbos/stylus/cache/lru/size_bytes", nil) - stylusLRUCacheSizeCountGauge = metrics.NewRegisteredGauge("arb/arbos/stylus/cache/lru/count", nil) - stylusLRUCacheSizeHitsCounter = metrics.NewRegisteredCounter("arb/arbos/stylus/cache/lru/hits", nil) - stylusLRUCacheSizeMissesCounter = metrics.NewRegisteredCounter("arb/arbos/stylus/cache/lru/misses", nil) - stylusLRUCacheSizeDoesNotFitCounter = metrics.NewRegisteredCounter("arb/arbos/stylus/cache/lru/does_not_fit", nil) + stylusLRUCacheSizeBytesGauge = metrics.NewRegisteredGauge("arb/arbos/stylus/cache/lru/size_bytes", nil) + stylusLRUCacheCountGauge = metrics.NewRegisteredGauge("arb/arbos/stylus/cache/lru/count", nil) + stylusLRUCacheHitsCounter = metrics.NewRegisteredCounter("arb/arbos/stylus/cache/lru/hits", nil) + stylusLRUCacheMissesCounter = metrics.NewRegisteredCounter("arb/arbos/stylus/cache/lru/misses", nil) + stylusLRUCacheDoesNotFitCounter = metrics.NewRegisteredCounter("arb/arbos/stylus/cache/lru/does_not_fit", nil) + + stylusLongTermCacheSizeBytesGauge = metrics.NewRegisteredGauge("arb/arbos/stylus/cache/long_term/size_bytes", nil) + stylusLongTermCacheCountGauge = metrics.NewRegisteredGauge("arb/arbos/stylus/cache/long_term/count", nil) + stylusLongTermCacheHitsCounter = metrics.NewRegisteredCounter("arb/arbos/stylus/cache/long_term/hits", nil) + stylusLongTermCacheMissesCounter = metrics.NewRegisteredCounter("arb/arbos/stylus/cache/long_term/misses", nil) ) func activateProgram( @@ -333,24 +338,54 @@ func SetWasmLruCacheCapacity(capacityBytes uint64) { C.stylus_set_cache_lru_capacity(u64(capacityBytes)) } -// exported for testing +func UpdateWasmCacheMetrics() { + metrics := &C.CacheMetrics{} + C.stylus_get_cache_metrics(metrics) + + stylusLRUCacheSizeBytesGauge.Update(int64(metrics.lru.size_bytes)) + stylusLRUCacheCountGauge.Update(int64(metrics.lru.count)) + stylusLRUCacheHitsCounter.Inc(int64(metrics.lru.hits)) + stylusLRUCacheMissesCounter.Inc(int64(metrics.lru.misses)) + stylusLRUCacheDoesNotFitCounter.Inc(int64(metrics.lru.does_not_fit)) + + stylusLongTermCacheSizeBytesGauge.Update(int64(metrics.long_term.size_bytes)) + stylusLongTermCacheCountGauge.Update(int64(metrics.long_term.count)) + stylusLongTermCacheHitsCounter.Inc(int64(metrics.long_term.hits)) + stylusLongTermCacheMissesCounter.Inc(int64(metrics.long_term.misses)) +} + +// Used for testing type WasmLruCacheMetrics struct { SizeBytes uint64 Count uint32 } -func GetWasmLruCacheMetrics() *WasmLruCacheMetrics { - metrics := C.stylus_get_lru_cache_metrics() +// Used for testing +type WasmLongTermCacheMetrics struct { + SizeBytes uint64 + Count uint32 +} - stylusLRUCacheSizeBytesGauge.Update(int64(metrics.size_bytes)) - stylusLRUCacheSizeCountGauge.Update(int64(metrics.count)) - stylusLRUCacheSizeHitsCounter.Inc(int64(metrics.hits)) - stylusLRUCacheSizeMissesCounter.Inc(int64(metrics.misses)) - stylusLRUCacheSizeDoesNotFitCounter.Inc(int64(metrics.does_not_fit)) +// Used for testing +type WasmCacheMetrics struct { + Lru WasmLruCacheMetrics + LongTerm WasmLongTermCacheMetrics +} - return &WasmLruCacheMetrics{ - SizeBytes: uint64(metrics.size_bytes), - Count: uint32(metrics.count), +// Used for testing +func GetWasmCacheMetrics() *WasmCacheMetrics { + metrics := &C.CacheMetrics{} + C.stylus_get_cache_metrics(metrics) + + return &WasmCacheMetrics{ + Lru: WasmLruCacheMetrics{ + SizeBytes: uint64(metrics.lru.size_bytes), + Count: uint32(metrics.lru.count), + }, + LongTerm: WasmLongTermCacheMetrics{ + SizeBytes: uint64(metrics.long_term.size_bytes), + Count: uint32(metrics.long_term.count), + }, } } @@ -360,8 +395,13 @@ func ClearWasmLruCache() { } // Used for testing -func GetLruEntrySizeEstimateBytes(module []byte, version uint16, debug bool) uint64 { - return uint64(C.stylus_get_lru_entry_size_estimate_bytes(goSlice(module), u16(version), cbool(debug))) +func ClearWasmLongTermCache() { + C.stylus_clear_long_term_cache() +} + +// Used for testing +func GetEntrySizeEstimateBytes(module []byte, version uint16, debug bool) uint64 { + return uint64(C.stylus_get_entry_size_estimate_bytes(goSlice(module), u16(version), cbool(debug))) } const DefaultTargetDescriptionArm = "arm64-linux-unknown+neon" diff --git a/cmd/conf/chain.go b/cmd/conf/chain.go index b85f7727b1..28b06aad2b 100644 --- a/cmd/conf/chain.go +++ b/cmd/conf/chain.go @@ -52,23 +52,19 @@ func (c *ParentChainConfig) Validate() error { } type L2Config struct { - ID uint64 `koanf:"id"` - Name string `koanf:"name"` - InfoFiles []string `koanf:"info-files"` - InfoJson string `koanf:"info-json"` - DevWallet genericconf.WalletConfig `koanf:"dev-wallet"` - InfoIpfsUrl string `koanf:"info-ipfs-url"` - InfoIpfsDownloadPath string `koanf:"info-ipfs-download-path"` + ID uint64 `koanf:"id"` + Name string `koanf:"name"` + InfoFiles []string `koanf:"info-files"` + InfoJson string `koanf:"info-json"` + DevWallet genericconf.WalletConfig `koanf:"dev-wallet"` } var L2ConfigDefault = L2Config{ - ID: 0, - Name: "", - InfoFiles: []string{}, // Default file used is chaininfo/arbitrum_chain_info.json, stored in DefaultChainInfo in chain_info.go - InfoJson: "", - DevWallet: genericconf.WalletConfigDefault, - InfoIpfsUrl: "", - InfoIpfsDownloadPath: "/tmp/", + ID: 0, + Name: "", + InfoFiles: []string{}, // Default file used is chaininfo/arbitrum_chain_info.json, stored in DefaultChainInfo in chain_info.go + InfoJson: "", + DevWallet: genericconf.WalletConfigDefault, } func L2ConfigAddOptions(prefix string, f *flag.FlagSet) { @@ -79,9 +75,6 @@ func L2ConfigAddOptions(prefix string, f *flag.FlagSet) { // Dev wallet does not exist unless specified genericconf.WalletConfigAddOptions(prefix+".dev-wallet", f, "") - f.String(prefix+".info-ipfs-url", L2ConfigDefault.InfoIpfsUrl, "url to download chain info file") - f.String(prefix+".info-ipfs-download-path", L2ConfigDefault.InfoIpfsDownloadPath, "path to save temp downloaded file") - } func (c *L2Config) ResolveDirectoryNames(chain string) { diff --git a/cmd/ipfshelper/ipfshelper.bkup_go b/cmd/ipfshelper/ipfshelper.bkup_go deleted file mode 100644 index ccde492ca6..0000000000 --- a/cmd/ipfshelper/ipfshelper.bkup_go +++ /dev/null @@ -1,281 +0,0 @@ -//go:build ipfs -// +build ipfs - -package ipfshelper - -import ( - "context" - "fmt" - "io" - "math/rand" - "os" - "path/filepath" - "strings" - "sync" - - "github.com/ethereum/go-ethereum/log" - "github.com/ipfs/go-libipfs/files" - coreiface "github.com/ipfs/interface-go-ipfs-core" - "github.com/ipfs/interface-go-ipfs-core/options" - "github.com/ipfs/interface-go-ipfs-core/path" - "github.com/ipfs/kubo/config" - "github.com/ipfs/kubo/core" - "github.com/ipfs/kubo/core/coreapi" - "github.com/ipfs/kubo/core/node/libp2p" - "github.com/ipfs/kubo/plugin/loader" - "github.com/ipfs/kubo/repo" - "github.com/ipfs/kubo/repo/fsrepo" - "github.com/libp2p/go-libp2p/core/host" - "github.com/libp2p/go-libp2p/core/peer" - ma "github.com/multiformats/go-multiaddr" -) - -const DefaultIpfsProfiles = "" - -type IpfsHelper struct { - api coreiface.CoreAPI - node *core.IpfsNode - cfg *config.Config - repoPath string - repo repo.Repo -} - -func (h *IpfsHelper) createRepo(downloadPath string, profiles string) error { - fileInfo, err := os.Stat(downloadPath) - if err != nil { - return fmt.Errorf("failed to stat ipfs repo directory: %w", err) - } - if !fileInfo.IsDir() { - return fmt.Errorf("%s is not a directory", downloadPath) - } - h.repoPath = filepath.Join(downloadPath, "ipfs-repo") - // Create a config with default options and a 2048 bit key - h.cfg, err = config.Init(io.Discard, 2048) - if err != nil { - return err - } - if len(profiles) > 0 { - for _, profile := range strings.Split(profiles, ",") { - transformer, ok := config.Profiles[profile] - if !ok { - return fmt.Errorf("invalid ipfs configuration profile: %s", profile) - } - - if err := transformer.Transform(h.cfg); err != nil { - return err - } - } - } - // Create the repo with the config - // fsrepo.Init initializes new repo only if it's not initialized yet - err = fsrepo.Init(h.repoPath, h.cfg) - if err != nil { - return fmt.Errorf("failed to init ipfs repo: %w", err) - } - h.repo, err = fsrepo.Open(h.repoPath) - if err != nil { - return fmt.Errorf("failed to open ipfs repo: %w", err) - } - return nil -} - -func (h *IpfsHelper) createNode(ctx context.Context, clientOnly bool) error { - var routing libp2p.RoutingOption - if clientOnly { - routing = libp2p.DHTClientOption - } else { - routing = libp2p.DHTOption - } - nodeOptions := &core.BuildCfg{ - Online: true, - Routing: routing, - Repo: h.repo, - } - var err error - h.node, err = core.NewNode(ctx, nodeOptions) - if err != nil { - return err - } - h.api, err = coreapi.NewCoreAPI(h.node) - return err -} - -func (h *IpfsHelper) connectToPeers(ctx context.Context, peers []string) error { - peerInfos := make(map[peer.ID]*peer.AddrInfo, len(peers)) - for _, addressString := range peers { - address, err := ma.NewMultiaddr(addressString) - if err != nil { - return err - } - addressInfo, err := peer.AddrInfoFromP2pAddr(address) - if err != nil { - return err - } - peerInfo, ok := peerInfos[addressInfo.ID] - if !ok { - peerInfo = &peer.AddrInfo{ID: addressInfo.ID} - peerInfos[peerInfo.ID] = peerInfo - } - peerInfo.Addrs = append(peerInfo.Addrs, addressInfo.Addrs...) - } - var wg sync.WaitGroup - wg.Add(len(peerInfos)) - for _, peerInfo := range peerInfos { - go func(peerInfo *peer.AddrInfo) { - defer wg.Done() - err := h.api.Swarm().Connect(ctx, *peerInfo) - if err != nil { - log.Warn("failed to connect to peer", "peerId", peerInfo.ID, "err", err) - return - } - }(peerInfo) - } - wg.Wait() - return nil -} - -func (h *IpfsHelper) GetPeerHostAddresses() ([]string, error) { - addresses, err := peer.AddrInfoToP2pAddrs(host.InfoFromHost(h.node.PeerHost)) - if err != nil { - return []string{}, err - } - addressesStrings := make([]string, len(addresses)) - for i, a := range addresses { - addressesStrings[i] = a.String() - } - return addressesStrings, nil -} - -func normalizeCidString(cidString string) string { - if strings.HasPrefix(cidString, "ipfs://") { - return "/ipfs/" + cidString[7:] - } - if strings.HasPrefix(cidString, "ipns://") { - return "/ipns/" + cidString[7:] - } - return cidString -} - -func (h *IpfsHelper) DownloadFile(ctx context.Context, cidString string, destinationDir string) (string, error) { - cidString = normalizeCidString(cidString) - cidPath := path.New(cidString) - resolvedPath, err := h.api.ResolvePath(ctx, cidPath) - if err != nil { - return "", fmt.Errorf("failed to resolve path: %w", err) - } - // first pin the root node, then all its children nodes in random order to improve sharing with peers started at the same time - if err := h.api.Pin().Add(ctx, resolvedPath, options.Pin.Recursive(false)); err != nil { - return "", fmt.Errorf("failed to pin root path: %w", err) - } - links, err := h.api.Object().Links(ctx, resolvedPath) - if err != nil { - return "", fmt.Errorf("failed to get root links: %w", err) - } - log.Info("Pinning ipfs subtrees...") - printProgress := func(done int, all int) { - if all == 0 { - all = 1 // avoid division by 0 - done = 1 - } - fmt.Printf("\033[2K\rPinned %d / %d subtrees (%.2f%%)", done, all, float32(done)/float32(all)*100) - } - permutation := rand.Perm(len(links)) - printProgress(0, len(links)) - for i, j := range permutation { - link := links[j] - if err := h.api.Pin().Add(ctx, path.IpfsPath(link.Cid), options.Pin.Recursive(true)); err != nil { - return "", fmt.Errorf("failed to pin child path: %w", err) - } - printProgress(i+1, len(links)) - } - fmt.Printf("\n") - rootNodeDirectory, err := h.api.Unixfs().Get(ctx, cidPath) - if err != nil { - return "", fmt.Errorf("could not get file with CID: %w", err) - } - log.Info("Writing file...") - outputFilePath := filepath.Join(destinationDir, resolvedPath.Cid().String()) - _ = os.Remove(outputFilePath) - err = files.WriteTo(rootNodeDirectory, outputFilePath) - if err != nil { - return "", fmt.Errorf("could not write out the fetched CID: %w", err) - } - log.Info("Download done.") - return outputFilePath, nil -} - -func (h *IpfsHelper) AddFile(ctx context.Context, filePath string, includeHidden bool) (path.Resolved, error) { - fileInfo, err := os.Stat(filePath) - if err != nil { - return nil, err - } - fileNode, err := files.NewSerialFile(filePath, includeHidden, fileInfo) - if err != nil { - return nil, err - } - return h.api.Unixfs().Add(ctx, fileNode) -} - -func CreateIpfsHelper(ctx context.Context, downloadPath string, clientOnly bool, peerList []string, profiles string) (*IpfsHelper, error) { - return createIpfsHelperImpl(ctx, downloadPath, clientOnly, peerList, profiles) -} - -func (h *IpfsHelper) Close() error { - return h.node.Close() -} - -func setupPlugins() error { - plugins, err := loader.NewPluginLoader("") - if err != nil { - return fmt.Errorf("error loading plugins: %w", err) - } - // Load preloaded and external plugins - if err := plugins.Initialize(); err != nil { - return fmt.Errorf("error initializing plugins: %w", err) - } - if err := plugins.Inject(); err != nil { - return fmt.Errorf("error initializing plugins: %w", err) - } - return nil -} - -var loadPluginsOnce sync.Once - -func createIpfsHelperImpl(ctx context.Context, downloadPath string, clientOnly bool, peerList []string, profiles string) (*IpfsHelper, error) { - var onceErr error - loadPluginsOnce.Do(func() { - onceErr = setupPlugins() - }) - if onceErr != nil { - return nil, onceErr - } - client := IpfsHelper{} - err := client.createRepo(downloadPath, profiles) - if err != nil { - return nil, err - } - err = client.createNode(ctx, clientOnly) - if err != nil { - return nil, err - } - err = client.connectToPeers(ctx, peerList) - if err != nil { - return nil, err - } - return &client, nil -} - -func CanBeIpfsPath(pathString string) bool { - path := path.New(pathString) - return path.IsValid() == nil || - strings.HasPrefix(pathString, "/ipfs/") || - strings.HasPrefix(pathString, "/ipld/") || - strings.HasPrefix(pathString, "/ipns/") || - strings.HasPrefix(pathString, "ipfs://") || - strings.HasPrefix(pathString, "ipns://") -} - -// TODO break abstraction for now til we figure out what fns are needed -func (h *IpfsHelper) GetAPI() coreiface.CoreAPI { - return h.api -} diff --git a/cmd/ipfshelper/ipfshelper_stub.go b/cmd/ipfshelper/ipfshelper_stub.go deleted file mode 100644 index fa6a451927..0000000000 --- a/cmd/ipfshelper/ipfshelper_stub.go +++ /dev/null @@ -1,31 +0,0 @@ -//go:build !ipfs -// +build !ipfs - -package ipfshelper - -import ( - "context" - "errors" -) - -type IpfsHelper struct{} - -var ErrIpfsNotSupported = errors.New("ipfs not supported") - -var DefaultIpfsProfiles = "default ipfs profiles stub" - -func CanBeIpfsPath(pathString string) bool { - return false -} - -func CreateIpfsHelper(ctx context.Context, downloadPath string, clientOnly bool, peerList []string, profiles string) (*IpfsHelper, error) { - return nil, ErrIpfsNotSupported -} - -func (h *IpfsHelper) DownloadFile(ctx context.Context, cidString string, destinationDir string) (string, error) { - return "", ErrIpfsNotSupported -} - -func (h *IpfsHelper) Close() error { - return ErrIpfsNotSupported -} diff --git a/cmd/ipfshelper/ipfshelper_test.go b/cmd/ipfshelper/ipfshelper_test.go deleted file mode 100644 index 80f10c21f6..0000000000 --- a/cmd/ipfshelper/ipfshelper_test.go +++ /dev/null @@ -1,123 +0,0 @@ -//go:build ipfs -// +build ipfs - -package ipfshelper - -import ( - "bytes" - "context" - "math/rand" - "os" - "path/filepath" - "testing" - "time" - - "github.com/offchainlabs/nitro/util/testhelpers" -) - -func getTempFileWithData(t *testing.T, data []byte) string { - path := filepath.Join(t.TempDir(), "config.json") - err := os.WriteFile(path, []byte(data), 0600) - testhelpers.RequireImpl(t, err) - return path -} - -func fileDataEqual(t *testing.T, path string, expected []byte) bool { - data, err := os.ReadFile(path) - testhelpers.RequireImpl(t, err) - return bytes.Equal(data, expected) -} - -func TestIpfsHelper(t *testing.T) { - ctx := context.Background() - ipfsA, err := createIpfsHelperImpl(ctx, t.TempDir(), false, []string{}, "test") - testhelpers.RequireImpl(t, err) - // add a test file to node A - testData := make([]byte, 1024*1024) - _, err = rand.Read(testData) - testhelpers.RequireImpl(t, err) - testFile := getTempFileWithData(t, testData) - ipfsTestFilePath, err := ipfsA.AddFile(ctx, testFile, false) - testhelpers.RequireImpl(t, err) - testFileCid := ipfsTestFilePath.Cid().String() - addrsA, err := ipfsA.GetPeerHostAddresses() - testhelpers.RequireImpl(t, err) - // create node B connected to node A - ipfsB, err := createIpfsHelperImpl(ctx, t.TempDir(), false, addrsA, "test") - testhelpers.RequireImpl(t, err) - // download the test file with node B - downloadedFile, err := ipfsB.DownloadFile(ctx, testFileCid, t.TempDir()) - testhelpers.RequireImpl(t, err) - if !fileDataEqual(t, downloadedFile, testData) { - testhelpers.FailImpl(t, "Downloaded file does not contain expected data") - } - // clean up node A and test downloading the file from yet another node C - err = ipfsA.Close() - os.RemoveAll(ipfsA.repoPath) - testhelpers.RequireImpl(t, err) - addrsB, err := ipfsB.GetPeerHostAddresses() - testhelpers.RequireImpl(t, err) - ipfsC, err := createIpfsHelperImpl(ctx, t.TempDir(), false, addrsB, "test") - testhelpers.RequireImpl(t, err) - downloadedFile, err = ipfsC.DownloadFile(ctx, testFileCid, t.TempDir()) - testhelpers.RequireImpl(t, err) - if !fileDataEqual(t, downloadedFile, testData) { - testhelpers.FailImpl(t, "Downloaded file does not contain expected data") - } - // make sure closing B and C nodes (A already closed) will make it impossible to download the test file from new node D - ipfsD, err := createIpfsHelperImpl(ctx, t.TempDir(), false, addrsB, "test") - testhelpers.RequireImpl(t, err) - err = ipfsB.Close() - testhelpers.RequireImpl(t, err) - err = ipfsC.Close() - testhelpers.RequireImpl(t, err) - testTimeout := 300 * time.Millisecond - timeoutCtx, cancel := context.WithTimeout(ctx, testTimeout) - defer cancel() - _, err = ipfsD.DownloadFile(timeoutCtx, testFileCid, t.TempDir()) - if err == nil { - testhelpers.FailImpl(t, "Download attempt did not fail as expected") - } - err = ipfsD.Close() - testhelpers.RequireImpl(t, err) -} - -func TestNormalizeCidString(t *testing.T) { - for _, test := range []struct { - input string - expected string - }{ - {"ipfs://QmaCpDMGvV2BGHeYERUEnRQAwe3N8SzbUtfsmvsqQLuvuJ", "/ipfs/QmaCpDMGvV2BGHeYERUEnRQAwe3N8SzbUtfsmvsqQLuvuJ"}, - {"ipns://k51qzi5uqu5dlvj2baxnqndepeb86cbk3ng7n3i46uzyxzyqj2xjonzllnv0v8", "/ipns/k51qzi5uqu5dlvj2baxnqndepeb86cbk3ng7n3i46uzyxzyqj2xjonzllnv0v8"}, - {"ipns://docs.ipfs.tech/introduction/", "/ipns/docs.ipfs.tech/introduction/"}, - {"/ipfs/QmaCpDMGvV2BGHeYERUEnRQAwe3N8SzbUtfsmvsqQLuvuJ", "/ipfs/QmaCpDMGvV2BGHeYERUEnRQAwe3N8SzbUtfsmvsqQLuvuJ"}, - {"/ipns/k51qzi5uqu5dlvj2baxnqndepeb86cbk3ng7n3i46uzyxzyqj2xjonzllnv0v8", "/ipns/k51qzi5uqu5dlvj2baxnqndepeb86cbk3ng7n3i46uzyxzyqj2xjonzllnv0v8"}, - {"QmaCpDMGvV2BGHeYERUEnRQAwe3N8SzbUtfsmvsqQLuvuJ", "QmaCpDMGvV2BGHeYERUEnRQAwe3N8SzbUtfsmvsqQLuvuJ"}, - } { - if res := normalizeCidString(test.input); res != test.expected { - testhelpers.FailImpl(t, "Failed to normalize cid string, input: ", test.input, " got: ", res, " expected: ", test.expected) - } - } -} - -func TestCanBeIpfsPath(t *testing.T) { - correctPaths := []string{ - "QmaCpDMGvV2BGHeYERUEnRQAwe3N8SzbUtfsmvsqQLuvuJ", - "/ipfs/QmaCpDMGvV2BGHeYERUEnRQAwe3N8SzbUtfsmvsqQLuvuJ", - "/ipns/k51qzi5uqu5dlvj2baxnqndepeb86cbk3ng7n3i46uzyxzyqj2xjonzllnv0v8", - "/ipns/docs.ipfs.tech/introduction/", - "ipfs://QmaCpDMGvV2BGHeYERUEnRQAwe3N8SzbUtfsmvsqQLuvuJ", - "ipns://k51qzi5uqu5dlvj2baxnqndepeb86cbk3ng7n3i46uzyxzyqj2xjonzllnv0v8", - } - for _, path := range correctPaths { - if !CanBeIpfsPath(path) { - testhelpers.FailImpl(t, "false negative result for path:", path) - } - } - incorrectPaths := []string{"www.ipfs.tech", "https://www.ipfs.tech", "QmIncorrect"} - for _, path := range incorrectPaths { - if CanBeIpfsPath(path) { - testhelpers.FailImpl(t, "false positive result for path:", path) - } - } -} diff --git a/cmd/nitro/init.go b/cmd/nitro/init.go index 9e3ecec747..f0b303817c 100644 --- a/cmd/nitro/init.go +++ b/cmd/nitro/init.go @@ -40,7 +40,6 @@ import ( "github.com/offchainlabs/nitro/arbos/arbostypes" "github.com/offchainlabs/nitro/cmd/chaininfo" "github.com/offchainlabs/nitro/cmd/conf" - "github.com/offchainlabs/nitro/cmd/ipfshelper" "github.com/offchainlabs/nitro/cmd/pruning" "github.com/offchainlabs/nitro/cmd/staterecovery" "github.com/offchainlabs/nitro/execution/gethexec" @@ -58,25 +57,6 @@ func downloadInit(ctx context.Context, initConfig *conf.InitConfig) (string, err if strings.HasPrefix(initConfig.Url, "file:") { return initConfig.Url[5:], nil } - if ipfshelper.CanBeIpfsPath(initConfig.Url) { - ipfsNode, err := ipfshelper.CreateIpfsHelper(ctx, initConfig.DownloadPath, false, []string{}, ipfshelper.DefaultIpfsProfiles) - if err != nil { - return "", err - } - log.Info("Downloading initial database via IPFS", "url", initConfig.Url) - initFile, downloadErr := ipfsNode.DownloadFile(ctx, initConfig.Url, initConfig.DownloadPath) - closeErr := ipfsNode.Close() - if downloadErr != nil { - if closeErr != nil { - log.Error("Failed to close IPFS node after download error", "err", closeErr) - } - return "", fmt.Errorf("Failed to download file from IPFS: %w", downloadErr) - } - if closeErr != nil { - return "", fmt.Errorf("Failed to close IPFS node: %w", err) - } - return initFile, nil - } log.Info("Downloading initial database", "url", initConfig.Url) if !initConfig.ValidateChecksum { file, err := downloadFile(ctx, initConfig, initConfig.Url, nil) @@ -732,8 +712,7 @@ func openInitializeChainDb(ctx context.Context, stack *node.Node, config *NodeCo if err != nil { return chainDb, nil, err } - combinedL2ChainInfoFiles := aggregateL2ChainInfoFiles(ctx, config.Chain.InfoFiles, config.Chain.InfoIpfsUrl, config.Chain.InfoIpfsDownloadPath) - chainConfig, err = chaininfo.GetChainConfig(new(big.Int).SetUint64(config.Chain.ID), config.Chain.Name, genesisBlockNr, combinedL2ChainInfoFiles, config.Chain.InfoJson) + chainConfig, err = chaininfo.GetChainConfig(new(big.Int).SetUint64(config.Chain.ID), config.Chain.Name, genesisBlockNr, config.Chain.InfoFiles, config.Chain.InfoJson) if err != nil { return chainDb, nil, err } diff --git a/cmd/nitro/nitro.go b/cmd/nitro/nitro.go index bc2155a475..076ac66ec4 100644 --- a/cmd/nitro/nitro.go +++ b/cmd/nitro/nitro.go @@ -285,8 +285,6 @@ func mainImpl() int { } } - combinedL2ChainInfoFile := aggregateL2ChainInfoFiles(ctx, nodeConfig.Chain.InfoFiles, nodeConfig.Chain.InfoIpfsUrl, nodeConfig.Chain.InfoIpfsDownloadPath) - if nodeConfig.Node.Staker.Enable { if !nodeConfig.Node.ParentChainReader.Enable { flag.Usage() @@ -335,7 +333,7 @@ func mainImpl() int { log.Info("connected to l1 chain", "l1url", nodeConfig.ParentChain.Connection.URL, "l1chainid", nodeConfig.ParentChain.ID) - rollupAddrs, err = chaininfo.GetRollupAddressesConfig(nodeConfig.Chain.ID, nodeConfig.Chain.Name, combinedL2ChainInfoFile, nodeConfig.Chain.InfoJson) + rollupAddrs, err = chaininfo.GetRollupAddressesConfig(nodeConfig.Chain.ID, nodeConfig.Chain.Name, nodeConfig.Chain.InfoFiles, nodeConfig.Chain.InfoJson) if err != nil { log.Crit("error getting rollup addresses", "err", err) } @@ -367,7 +365,7 @@ func mainImpl() int { log.Crit("--node.validator.only-create-wallet-contract conflicts with --node.dangerous.no-l1-listener") } // Just create validator smart wallet if needed then exit - deployInfo, err := chaininfo.GetRollupAddressesConfig(nodeConfig.Chain.ID, nodeConfig.Chain.Name, combinedL2ChainInfoFile, nodeConfig.Chain.InfoJson) + deployInfo, err := chaininfo.GetRollupAddressesConfig(nodeConfig.Chain.ID, nodeConfig.Chain.Name, nodeConfig.Chain.InfoFiles, nodeConfig.Chain.InfoJson) if err != nil { log.Crit("error getting rollup addresses config", "err", err) } @@ -493,7 +491,7 @@ func mainImpl() int { return 0 } - chainInfo, err := chaininfo.ProcessChainInfo(nodeConfig.Chain.ID, nodeConfig.Chain.Name, combinedL2ChainInfoFile, nodeConfig.Chain.InfoJson) + chainInfo, err := chaininfo.ProcessChainInfo(nodeConfig.Chain.ID, nodeConfig.Chain.Name, nodeConfig.Chain.InfoFiles, nodeConfig.Chain.InfoJson) if err != nil { log.Error("error processing l2 chain info", "err", err) return 1 @@ -840,12 +838,10 @@ func ParseNode(ctx context.Context, args []string) (*NodeConfig, *genericconf.Wa l2ChainId := k.Int64("chain.id") l2ChainName := k.String("chain.name") - l2ChainInfoIpfsUrl := k.String("chain.info-ipfs-url") - l2ChainInfoIpfsDownloadPath := k.String("chain.info-ipfs-download-path") l2ChainInfoFiles := k.Strings("chain.info-files") l2ChainInfoJson := k.String("chain.info-json") // #nosec G115 - err = applyChainParameters(ctx, k, uint64(l2ChainId), l2ChainName, l2ChainInfoFiles, l2ChainInfoJson, l2ChainInfoIpfsUrl, l2ChainInfoIpfsDownloadPath) + err = applyChainParameters(k, uint64(l2ChainId), l2ChainName, l2ChainInfoFiles, l2ChainInfoJson) if err != nil { return nil, nil, err } @@ -908,20 +904,8 @@ func ParseNode(ctx context.Context, args []string) (*NodeConfig, *genericconf.Wa return &nodeConfig, &l2DevWallet, nil } -func aggregateL2ChainInfoFiles(ctx context.Context, l2ChainInfoFiles []string, l2ChainInfoIpfsUrl string, l2ChainInfoIpfsDownloadPath string) []string { - if l2ChainInfoIpfsUrl != "" { - l2ChainInfoIpfsFile, err := util.GetL2ChainInfoIpfsFile(ctx, l2ChainInfoIpfsUrl, l2ChainInfoIpfsDownloadPath) - if err != nil { - log.Error("error getting l2 chain info file from ipfs", "err", err) - } - l2ChainInfoFiles = append(l2ChainInfoFiles, l2ChainInfoIpfsFile) - } - return l2ChainInfoFiles -} - -func applyChainParameters(ctx context.Context, k *koanf.Koanf, chainId uint64, chainName string, l2ChainInfoFiles []string, l2ChainInfoJson string, l2ChainInfoIpfsUrl string, l2ChainInfoIpfsDownloadPath string) error { - combinedL2ChainInfoFiles := aggregateL2ChainInfoFiles(ctx, l2ChainInfoFiles, l2ChainInfoIpfsUrl, l2ChainInfoIpfsDownloadPath) - chainInfo, err := chaininfo.ProcessChainInfo(chainId, chainName, combinedL2ChainInfoFiles, l2ChainInfoJson) +func applyChainParameters(k *koanf.Koanf, chainId uint64, chainName string, l2ChainInfoFiles []string, l2ChainInfoJson string) error { + chainInfo, err := chaininfo.ProcessChainInfo(chainId, chainName, l2ChainInfoFiles, l2ChainInfoJson) if err != nil { return err } @@ -930,7 +914,7 @@ func applyChainParameters(ctx context.Context, k *koanf.Koanf, chainId uint64, c parentChainIsArbitrum = *chainInfo.ParentChainIsArbitrum } else { log.Warn("Chain info field parent-chain-is-arbitrum is missing, in the future this will be required", "chainId", chainInfo.ChainConfig.ChainID, "parentChainId", chainInfo.ParentChainId) - _, err := chaininfo.ProcessChainInfo(chainInfo.ParentChainId, "", combinedL2ChainInfoFiles, "") + _, err := chaininfo.ProcessChainInfo(chainInfo.ParentChainId, "", l2ChainInfoFiles, "") if err == nil { parentChainIsArbitrum = true } diff --git a/cmd/replay/main.go b/cmd/replay/main.go index 0fe56eb4c9..661040ea10 100644 --- a/cmd/replay/main.go +++ b/cmd/replay/main.go @@ -14,6 +14,7 @@ import ( "github.com/ethereum/go-ethereum/common" "github.com/ethereum/go-ethereum/consensus" + "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" @@ -291,7 +292,7 @@ func main() { message := readMessage(chainConfig.ArbitrumChainParams.DataAvailabilityCommittee) chainContext := WavmChainContext{} - newBlock, _, err = arbos.ProduceBlock(message.Message, message.DelayedMessagesRead, lastBlockHeader, statedb, chainContext, chainConfig, false) + newBlock, _, err = arbos.ProduceBlock(message.Message, message.DelayedMessagesRead, lastBlockHeader, statedb, chainContext, chainConfig, false, core.MessageReplayMode) if err != nil { panic(err) } diff --git a/cmd/seq-coordinator-manager/rediscoordinator/redis_coordinator.go b/cmd/seq-coordinator-manager/rediscoordinator/redis_coordinator.go index e963c0e96c..b897b23252 100644 --- a/cmd/seq-coordinator-manager/rediscoordinator/redis_coordinator.go +++ b/cmd/seq-coordinator-manager/rediscoordinator/redis_coordinator.go @@ -5,8 +5,8 @@ import ( "errors" "strings" - "github.com/go-redis/redis/v8" "github.com/offchainlabs/nitro/util/redisutil" + "github.com/redis/go-redis/v9" ) // RedisCoordinator builds upon RedisCoordinator of redisutil with additional functionality diff --git a/cmd/util/chaininfoutil.go b/cmd/util/chaininfoutil.go deleted file mode 100644 index 906aa234ed..0000000000 --- a/cmd/util/chaininfoutil.go +++ /dev/null @@ -1,29 +0,0 @@ -package util - -import ( - "context" - "fmt" - - "github.com/ethereum/go-ethereum/log" - "github.com/offchainlabs/nitro/cmd/ipfshelper" -) - -func GetL2ChainInfoIpfsFile(ctx context.Context, l2ChainInfoIpfsUrl string, l2ChainInfoIpfsDownloadPath string) (string, error) { - ipfsNode, err := ipfshelper.CreateIpfsHelper(ctx, l2ChainInfoIpfsDownloadPath, false, []string{}, ipfshelper.DefaultIpfsProfiles) - if err != nil { - return "", err - } - log.Info("Downloading l2 info file via IPFS", "url", l2ChainInfoIpfsDownloadPath) - l2ChainInfoFile, downloadErr := ipfsNode.DownloadFile(ctx, l2ChainInfoIpfsUrl, l2ChainInfoIpfsDownloadPath) - closeErr := ipfsNode.Close() - if downloadErr != nil { - if closeErr != nil { - log.Error("Failed to close IPFS node after download error", "err", closeErr) - } - return "", fmt.Errorf("failed to download file from IPFS: %w", downloadErr) - } - if closeErr != nil { - return "", fmt.Errorf("failed to close IPFS node: %w", err) - } - return l2ChainInfoFile, nil -} diff --git a/das/dasRpcClient.go b/das/dasRpcClient.go index 241f2196b1..d6e2c389c9 100644 --- a/das/dasRpcClient.go +++ b/das/dasRpcClient.go @@ -101,6 +101,7 @@ func (c *DASRPCClient) Store(ctx context.Context, message []byte, timeout uint64 var startChunkedStoreResult StartChunkedStoreResult if err := c.clnt.CallContext(ctx, &startChunkedStoreResult, "das_startChunkedStore", hexutil.Uint64(timestamp), hexutil.Uint64(nChunks), hexutil.Uint64(c.chunkSize), hexutil.Uint64(totalSize), hexutil.Uint64(timeout), hexutil.Bytes(startReqSig)); err != nil { if strings.Contains(err.Error(), "the method das_startChunkedStore does not exist") { + log.Info("Legacy store is used by the DAS client", "url", c.url) return c.legacyStore(ctx, message, timeout) } return nil, err diff --git a/das/google_cloud_storage_service.go b/das/google_cloud_storage_service.go index 2c490f346c..829f4b5265 100644 --- a/das/google_cloud_storage_service.go +++ b/das/google_cloud_storage_service.go @@ -1,21 +1,24 @@ package das import ( - googlestorage "cloud.google.com/go/storage" "context" "fmt" + "io" + "math" + "sort" + "time" + + googlestorage "cloud.google.com/go/storage" + "github.com/google/go-cmp/cmp" + flag "github.com/spf13/pflag" + "google.golang.org/api/option" + "github.com/ethereum/go-ethereum/common" "github.com/ethereum/go-ethereum/log" - "github.com/google/go-cmp/cmp" + "github.com/offchainlabs/nitro/arbstate/daprovider" "github.com/offchainlabs/nitro/das/dastree" "github.com/offchainlabs/nitro/util/pretty" - flag "github.com/spf13/pflag" - "google.golang.org/api/option" - "io" - "math" - "sort" - "time" ) type GoogleCloudStorageOperator interface { @@ -69,7 +72,7 @@ type GoogleCloudStorageServiceConfig struct { var DefaultGoogleCloudStorageServiceConfig = GoogleCloudStorageServiceConfig{} func GoogleCloudConfigAddOptions(prefix string, f *flag.FlagSet) { - f.Bool(prefix+".enable", DefaultGoogleCloudStorageServiceConfig.Enable, "enable storage/retrieval of sequencer batch data from an Google Cloud Storage bucket") + f.Bool(prefix+".enable", DefaultGoogleCloudStorageServiceConfig.Enable, "EXPERIMENTAL/unsupported - enable storage/retrieval of sequencer batch data from an Google Cloud Storage bucket") f.String(prefix+".access-token", DefaultGoogleCloudStorageServiceConfig.AccessToken, "Google Cloud Storage access token") f.String(prefix+".bucket", DefaultGoogleCloudStorageServiceConfig.Bucket, "Google Cloud Storage bucket") f.String(prefix+".object-prefix", DefaultGoogleCloudStorageServiceConfig.ObjectPrefix, "prefix to add to Google Cloud Storage objects") diff --git a/das/redis_storage_service.go b/das/redis_storage_service.go index 210d5cb2d4..e57240992c 100644 --- a/das/redis_storage_service.go +++ b/das/redis_storage_service.go @@ -12,11 +12,11 @@ import ( "golang.org/x/crypto/sha3" - "github.com/go-redis/redis/v8" "github.com/offchainlabs/nitro/arbstate/daprovider" "github.com/offchainlabs/nitro/das/dastree" "github.com/offchainlabs/nitro/util/pretty" "github.com/offchainlabs/nitro/util/redisutil" + "github.com/redis/go-redis/v9" flag "github.com/spf13/pflag" "github.com/ethereum/go-ethereum/common" diff --git a/execution/gethexec/block_recorder.go b/execution/gethexec/block_recorder.go index a31b6b3736..a3af7876a8 100644 --- a/execution/gethexec/block_recorder.go +++ b/execution/gethexec/block_recorder.go @@ -8,6 +8,7 @@ import ( "github.com/ethereum/go-ethereum/arbitrum" "github.com/ethereum/go-ethereum/common" + "github.com/ethereum/go-ethereum/core" "github.com/ethereum/go-ethereum/core/types" "github.com/ethereum/go-ethereum/ethdb" "github.com/ethereum/go-ethereum/log" @@ -154,6 +155,7 @@ func (r *BlockRecorder) RecordBlockCreation( chaincontext, chainConfig, false, + core.MessageReplayMode, ) if err != nil { return nil, err diff --git a/execution/gethexec/blockchain.go b/execution/gethexec/blockchain.go index 9b0c1a6f2f..fda8f49093 100644 --- a/execution/gethexec/blockchain.go +++ b/execution/gethexec/blockchain.go @@ -26,20 +26,21 @@ import ( ) type CachingConfig struct { - Archive bool `koanf:"archive"` - BlockCount uint64 `koanf:"block-count"` - BlockAge time.Duration `koanf:"block-age"` - TrieTimeLimit time.Duration `koanf:"trie-time-limit"` - TrieDirtyCache int `koanf:"trie-dirty-cache"` - TrieCleanCache int `koanf:"trie-clean-cache"` - SnapshotCache int `koanf:"snapshot-cache"` - DatabaseCache int `koanf:"database-cache"` - SnapshotRestoreGasLimit uint64 `koanf:"snapshot-restore-gas-limit"` - MaxNumberOfBlocksToSkipStateSaving uint32 `koanf:"max-number-of-blocks-to-skip-state-saving"` - MaxAmountOfGasToSkipStateSaving uint64 `koanf:"max-amount-of-gas-to-skip-state-saving"` - StylusLRUCacheCapacity uint32 `koanf:"stylus-lru-cache-capacity"` - StateScheme string `koanf:"state-scheme"` - StateHistory uint64 `koanf:"state-history"` + Archive bool `koanf:"archive"` + BlockCount uint64 `koanf:"block-count"` + BlockAge time.Duration `koanf:"block-age"` + TrieTimeLimit time.Duration `koanf:"trie-time-limit"` + TrieDirtyCache int `koanf:"trie-dirty-cache"` + TrieCleanCache int `koanf:"trie-clean-cache"` + SnapshotCache int `koanf:"snapshot-cache"` + DatabaseCache int `koanf:"database-cache"` + SnapshotRestoreGasLimit uint64 `koanf:"snapshot-restore-gas-limit"` + MaxNumberOfBlocksToSkipStateSaving uint32 `koanf:"max-number-of-blocks-to-skip-state-saving"` + MaxAmountOfGasToSkipStateSaving uint64 `koanf:"max-amount-of-gas-to-skip-state-saving"` + StylusLRUCacheCapacity uint32 `koanf:"stylus-lru-cache-capacity"` + DisableStylusCacheMetricsCollection bool `koanf:"disable-stylus-cache-metrics-collection"` + StateScheme string `koanf:"state-scheme"` + StateHistory uint64 `koanf:"state-history"` } func CachingConfigAddOptions(prefix string, f *flag.FlagSet) { @@ -55,6 +56,7 @@ func CachingConfigAddOptions(prefix string, f *flag.FlagSet) { f.Uint32(prefix+".max-number-of-blocks-to-skip-state-saving", DefaultCachingConfig.MaxNumberOfBlocksToSkipStateSaving, "maximum number of blocks to skip state saving to persistent storage (archive node only) -- warning: this option seems to cause issues") f.Uint64(prefix+".max-amount-of-gas-to-skip-state-saving", DefaultCachingConfig.MaxAmountOfGasToSkipStateSaving, "maximum amount of gas in blocks to skip saving state to Persistent storage (archive node only) -- warning: this option seems to cause issues") f.Uint32(prefix+".stylus-lru-cache-capacity", DefaultCachingConfig.StylusLRUCacheCapacity, "capacity, in megabytes, of the LRU cache that keeps initialized stylus programs") + f.Bool(prefix+".disable-stylus-cache-metrics-collection", DefaultCachingConfig.DisableStylusCacheMetricsCollection, "disable metrics collection for the stylus cache") f.String(prefix+".state-scheme", DefaultCachingConfig.StateScheme, "scheme to use for state trie storage (hash, path)") f.Uint64(prefix+".state-history", DefaultCachingConfig.StateHistory, "number of recent blocks to retain state history for (path state-scheme only)") } diff --git a/execution/gethexec/executionengine.go b/execution/gethexec/executionengine.go index a0f3a2f59a..6571672b71 100644 --- a/execution/gethexec/executionengine.go +++ b/execution/gethexec/executionengine.go @@ -87,6 +87,8 @@ type ExecutionEngine struct { reorgSequencing bool + disableStylusCacheMetricsCollection bool + prefetchBlock bool cachedL1PriceData *L1PriceData @@ -212,6 +214,16 @@ func (s *ExecutionEngine) EnableReorgSequencing() { s.reorgSequencing = true } +func (s *ExecutionEngine) DisableStylusCacheMetricsCollection() { + if s.Started() { + panic("trying to disable stylus cache metrics collection after start") + } + if s.disableStylusCacheMetricsCollection { + panic("trying to disable stylus cache metrics collection when already set") + } + s.disableStylusCacheMetricsCollection = true +} + func (s *ExecutionEngine) EnablePrefetchBlock() { if s.Started() { panic("trying to enable prefetch block after start") @@ -505,6 +517,7 @@ func (s *ExecutionEngine) sequenceTransactionsWithBlockMutex(header *arbostypes. s.bc.Config(), hooks, false, + core.MessageCommitMode, ) if err != nil { return nil, err @@ -661,6 +674,10 @@ func (s *ExecutionEngine) createBlockFromNextMessage(msg *arbostypes.MessageWith statedb.StartPrefetcher("TransactionStreamer") defer statedb.StopPrefetcher() + runMode := core.MessageCommitMode + if isMsgForPrefetch { + runMode = core.MessageReplayMode + } block, receipts, err := arbos.ProduceBlock( msg.Message, msg.DelayedMessagesRead, @@ -669,6 +686,7 @@ func (s *ExecutionEngine) createBlockFromNextMessage(msg *arbostypes.MessageWith s.bc, s.bc.Config(), isMsgForPrefetch, + runMode, ) return block, statedb, receipts, err @@ -963,15 +981,17 @@ func (s *ExecutionEngine) Start(ctx_in context.Context) { } } }) - // periodically update stylus lru cache metrics - s.LaunchThread(func(ctx context.Context) { - for { - select { - case <-ctx.Done(): - return - case <-time.After(time.Minute): - programs.GetWasmLruCacheMetrics() + if !s.disableStylusCacheMetricsCollection { + // periodically update stylus cache metrics + s.LaunchThread(func(ctx context.Context) { + for { + select { + case <-ctx.Done(): + return + case <-time.After(time.Minute): + programs.UpdateWasmCacheMetrics() + } } - } - }) + }) + } } diff --git a/execution/gethexec/node.go b/execution/gethexec/node.go index cb06a58e74..499a13164e 100644 --- a/execution/gethexec/node.go +++ b/execution/gethexec/node.go @@ -188,6 +188,9 @@ func CreateExecutionNode( if config.EnablePrefetchBlock { execEngine.EnablePrefetchBlock() } + if config.Caching.DisableStylusCacheMetricsCollection { + execEngine.DisableStylusCacheMetricsCollection() + } if err != nil { return nil, err } diff --git a/go-ethereum b/go-ethereum index b068464bf5..b1075d3786 160000 --- a/go-ethereum +++ b/go-ethereum @@ -1 +1 @@ -Subproject commit b068464bf59ab5414f72c2d4aba855b8af5edc17 +Subproject commit b1075d3786b28a6a3a06fe0e0ab8d1cdecc72f55 diff --git a/go.mod b/go.mod index 18e3a8b02a..488d455f44 100644 --- a/go.mod +++ b/go.mod @@ -25,7 +25,6 @@ require ( github.com/ethereum/go-ethereum v1.10.26 github.com/fatih/structtag v1.2.0 github.com/gdamore/tcell/v2 v2.7.1 - github.com/go-redis/redis/v8 v8.11.5 github.com/gobwas/httphead v0.1.0 github.com/gobwas/ws v1.2.1 github.com/gobwas/ws-examples v0.0.0-20190625122829-a9e8908d9484 @@ -39,6 +38,7 @@ require ( github.com/mitchellh/mapstructure v1.4.1 github.com/pkg/errors v0.9.1 github.com/r3labs/diff/v3 v3.0.1 + github.com/redis/go-redis/v9 v9.6.1 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 @@ -99,7 +99,7 @@ require ( github.com/beorn7/perks v1.0.1 // indirect github.com/bits-and-blooms/bitset v1.10.0 // indirect github.com/btcsuite/btcd/btcec/v2 v2.2.0 // indirect - github.com/cespare/xxhash/v2 v2.2.0 // indirect + github.com/cespare/xxhash/v2 v2.3.0 // indirect github.com/cockroachdb/errors v1.9.1 // indirect github.com/cockroachdb/logtags v0.0.0-20211118104740-dabe8e521a4f // indirect github.com/cockroachdb/redact v1.1.3 // indirect diff --git a/go.sum b/go.sum index f848c2aa2d..d11610724e 100644 --- a/go.sum +++ b/go.sum @@ -139,6 +139,10 @@ github.com/beorn7/perks v1.0.1/go.mod h1:G2ZrVWU2WbWT9wwq4/hrbKbnv/1ERSJQ0ibhJ6r github.com/bgentry/speakeasy v0.1.0/go.mod h1:+zsyZBPWlz7T6j88CTgSN5bM796AkVf0kBD4zp0CCIs= github.com/bits-and-blooms/bitset v1.10.0 h1:ePXTeiPEazB5+opbv5fr8umg2R/1NlzgDsyepwsSr88= github.com/bits-and-blooms/bitset v1.10.0/go.mod h1:7hO7Gc7Pp1vODcmWvKMRA9BNmbv6a/7QIWpPxHddWR8= +github.com/bsm/ginkgo/v2 v2.12.0 h1:Ny8MWAHyOepLGlLKYmXG4IEkioBysk6GpaRTLC8zwWs= +github.com/bsm/ginkgo/v2 v2.12.0/go.mod h1:SwYbGRRDovPVboqFv0tPTcG1sN61LM1Z4ARdbAV9g4c= +github.com/bsm/gomega v1.27.10 h1:yeMWxP2pV2fG3FgAODIY8EiRE3dy0aeFYt4l7wh6yKA= +github.com/bsm/gomega v1.27.10/go.mod h1:JyEr/xRbxbtgWNi8tIEVPUYZ5Dzef52k01W3YH0H+O0= github.com/btcsuite/btcd/btcec/v2 v2.2.0 h1:fzn1qaOt32TuLjFlkzYSsBC35Q3KUjT1SwPxiMSCF5k= github.com/btcsuite/btcd/btcec/v2 v2.2.0/go.mod h1:U7MHm051Al6XmscBQ0BoNydpOTsFAn707034b5nY8zU= github.com/btcsuite/btcd/chaincfg/chainhash v1.0.1 h1:q0rUy8C/TYNBQS1+CGKw68tLOFYSNEs0TFnxxnS9+4U= @@ -150,8 +154,8 @@ github.com/cespare/cp v0.1.0 h1:SE+dxFebS7Iik5LK0tsi1k9ZCxEaFX4AjQmoyA+1dJk= github.com/cespare/cp v0.1.0/go.mod h1:SOGHArjBr4JWaSDEVpWpo/hNg6RoKrls6Oh40hiwW+s= github.com/cespare/xxhash/v2 v2.1.1/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs= github.com/cespare/xxhash/v2 v2.1.2/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs= -github.com/cespare/xxhash/v2 v2.2.0 h1:DC2CZ1Ep5Y4k3ZQ899DldepgrayRUGE6BBZ/cd9Cj44= -github.com/cespare/xxhash/v2 v2.2.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs= +github.com/cespare/xxhash/v2 v2.3.0 h1:UL815xU9SqsFlibzuggzjXhog7bL6oX9BbNZnL2UFvs= +github.com/cespare/xxhash/v2 v2.3.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs= github.com/chzyer/logex v1.1.10/go.mod h1:+Ywpsq7O8HXn0nuIou7OrIPyXbp3wmkHB+jjWRnGsAI= github.com/chzyer/logex v1.2.0/go.mod h1:9+9sk7u7pGNWYMkh0hdiL++6OeibzJccyQU4p4MedaY= github.com/chzyer/readline v0.0.0-20180603132655-2972be24d48e/go.mod h1:nSuG5e5PlCu98SY8svDHJxuZscDgtXS6KTTbou5AhLI= @@ -281,8 +285,6 @@ github.com/go-martini/martini v0.0.0-20170121215854-22fa46961aab/go.mod h1:/P9AE github.com/go-ole/go-ole v1.2.5/go.mod h1:pprOEPIfldk/42T2oK7lQ4v4JSDwmV0As9GaiUsvbm0= github.com/go-ole/go-ole v1.3.0 h1:Dt6ye7+vXGIKZ7Xtk4s6/xVdGDQynvom7xCFEdWr6uE= github.com/go-ole/go-ole v1.3.0/go.mod h1:5LS6F96DhAwUc7C+1HLexzMXY1xGRSryjyPPKW6zv78= -github.com/go-redis/redis/v8 v8.11.5 h1:AcZZR7igkdvfVmQTPnu9WE37LRrO/YrBH5zWyjDC0oI= -github.com/go-redis/redis/v8 v8.11.5/go.mod h1:gREzHqY1hg6oD9ngVRbLStwAWKhA0FEgq8Jd4h5lpwo= github.com/go-sourcemap/sourcemap v2.1.3+incompatible h1:W1iEw64niKVGogNgBN3ePyLFfuisuzeidWPMPWmECqU= github.com/go-sourcemap/sourcemap v2.1.3+incompatible/go.mod h1:F8jJfvm2KbVjc5NqelyYJmf/v5J0dwNLS2mL4sNA1Jg= github.com/go-stack/stack v1.8.0/go.mod h1:v0f6uXyyMGvRgIKkXu+yp6POWl0qKG85gN/melR3HDY= @@ -582,22 +584,19 @@ github.com/nats-io/nkeys v0.1.0/go.mod h1:xpnFELMwJABBLVhffcfd1MZx6VsNRFpEugbxzi github.com/nats-io/nuid v1.0.1/go.mod h1:19wcPz3Ph3q0Jbyiqsd0kePYG7A95tJPxeL+1OSON2c= github.com/niemeyer/pretty v0.0.0-20200227124842-a10e7caefd8e/go.mod h1:zD1mROLANZcx1PVRCS0qkT7pwLkGfwJo4zjcN/Tysno= github.com/npillmayer/nestext v0.1.3/go.mod h1:h2lrijH8jpicr25dFY+oAJLyzlya6jhnuG+zWp9L0Uk= +github.com/nxadm/tail v1.4.4 h1:DQuhQpB1tVlglWS2hLQ5OV6B5r8aGxSrPc5Qo6uTN78= github.com/nxadm/tail v1.4.4/go.mod h1:kenIhsEOeOJmVchQTgglprH7qJGnHDVpk1VPCcaMI8A= -github.com/nxadm/tail v1.4.8 h1:nPr65rt6Y5JFSKQO7qToXr7pePgD6Gwiw05lkbyAQTE= -github.com/nxadm/tail v1.4.8/go.mod h1:+ncqLTQzXmGhMZNUePPaPqPvBxHAIsmXswZKocGu+AU= github.com/oklog/run v1.0.0/go.mod h1:dlhp/R75TPv97u0XWUtDeV/lRKWPKSdTuV0TZvrmrQA= github.com/olekukonko/tablewriter v0.0.5 h1:P2Ga83D34wi1o9J6Wh1mRuqd4mF/x/lgBS7N7AbDhec= github.com/olekukonko/tablewriter v0.0.5/go.mod h1:hPp6KlRPjbx+hW8ykQs1w3UBbZlj6HuIJcUGPhkA7kY= github.com/onsi/ginkgo v1.6.0/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE= github.com/onsi/ginkgo v1.10.3/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE= github.com/onsi/ginkgo v1.12.1/go.mod h1:zj2OWP4+oCPe1qIXoGWkgMRwljMUYCdkwsT2108oapk= +github.com/onsi/ginkgo v1.14.0 h1:2mOpI4JVVPBN+WQRa0WKH2eXR+Ey+uK4n7Zj0aYpIQA= github.com/onsi/ginkgo v1.14.0/go.mod h1:iSB4RoI2tjJc9BBv4NKIKWKya62Rps+oPG/Lv9klQyY= -github.com/onsi/ginkgo v1.16.5 h1:8xi0RTUf59SOSfEtZMvwTvXYMzG4gV23XVHOZiXNtnE= -github.com/onsi/ginkgo v1.16.5/go.mod h1:+E8gABHa3K6zRBolWtd+ROzc/U5bkGt0FwiG042wbpU= github.com/onsi/gomega v1.7.1/go.mod h1:XdKZgCCFLUoM/7CFJVPcG8C1xQ1AJ0vpAezJrB7JYyY= +github.com/onsi/gomega v1.10.1 h1:o0+MgICZLuZ7xjH7Vx6zS/zcu93/BEp1VwkIW1mEXCE= github.com/onsi/gomega v1.10.1/go.mod h1:iN09h71vgCQne3DLsj+A5owkum+a2tYe+TOCB1ybHNo= -github.com/onsi/gomega v1.18.1 h1:M1GfJqGRrBrrGGsbxzV5dqM2U2ApXefZCQpkukxYRLE= -github.com/onsi/gomega v1.18.1/go.mod h1:0q+aL8jAiMXy9hbwj2mr5GziHiwhAIQpFmmtT5hitRs= github.com/opentracing/opentracing-go v1.1.0 h1:pWlfV3Bxv7k65HYwkikxat0+s3pV4bsqf19k25Ur8rU= github.com/opentracing/opentracing-go v1.1.0/go.mod h1:UkNAQd3GIcIGf0SeVgPpRdFStlNbqXla1AfSYxPUl2o= github.com/pascaldekloe/goe v0.1.0/go.mod h1:lzWF7FIEvWOWxwDKqyGYQf6ZUaNfKdP144TG7ZOy1lc= @@ -643,6 +642,8 @@ github.com/prometheus/procfs v0.8.0 h1:ODq8ZFEaYeCaZOJlZZdJA2AbQR98dSHSM1KW/You5 github.com/prometheus/procfs v0.8.0/go.mod h1:z7EfXMXOkbkqb9IINtpCn86r/to3BnA0uaxHdg830/4= github.com/r3labs/diff/v3 v3.0.1 h1:CBKqf3XmNRHXKmdU7mZP1w7TV0pDyVCis1AUHtA4Xtg= github.com/r3labs/diff/v3 v3.0.1/go.mod h1:f1S9bourRbiM66NskseyUdo0fTmEE0qKrikYJX63dgo= +github.com/redis/go-redis/v9 v9.6.1 h1:HHDteefn6ZkTtY5fGUE8tj8uy85AHk6zP7CpzIAM0y4= +github.com/redis/go-redis/v9 v9.6.1/go.mod h1:0C0c6ycQsdpVNQpxb1njEQIqkx5UcsM8FJCQLgE9+RA= github.com/rhnvrm/simples3 v0.6.1 h1:H0DJwybR6ryQE+Odi9eqkHuzjYAeJgtGcGtuBwOhsH8= github.com/rhnvrm/simples3 v0.6.1/go.mod h1:Y+3vYm2V7Y4VijFoJHHTrja6OgPrJ2cBti8dPGkC3sA= github.com/rivo/tview v0.0.0-20240307173318-e804876934a1 h1:bWLHTRekAy497pE7+nXSuzXwwFHI0XauRzz6roUvY+s= @@ -1042,6 +1043,7 @@ golang.org/x/tools v0.21.1-0.20240508182429-e35e4ccd0d2d/go.mod h1:aiJjzUbINMkxb golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1 h1:go1bK/D/BFZV2I8cIQd1NKEZ+0owSTG1fDTci4IqFcE= golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= google.golang.org/api v0.4.0/go.mod h1:8k5glujaEP+g9n7WNsDg8QP6cUVNI86fCNMcbazEtwE= google.golang.org/api v0.7.0/go.mod h1:WtwebWUNSVBH/HAw79HIFXZNqEvBhG+Ra+ax0hx3E3M= diff --git a/precompiles/ArbAddressTable_test.go b/precompiles/ArbAddressTable_test.go index b01a460636..62ce177480 100644 --- a/precompiles/ArbAddressTable_test.go +++ b/precompiles/ArbAddressTable_test.go @@ -47,6 +47,12 @@ func TestAddressTable1(t *testing.T) { addr := common.BytesToAddress(crypto.Keccak256([]byte{})[:20]) + exists, err := atab.AddressExists(context, evm, addr) + Require(t, err) + if exists { + t.Fatal("Address shouldn't exist") + } + // register addr slot, err := atab.Register(context, evm, addr) Require(t, err) @@ -61,6 +67,12 @@ func TestAddressTable1(t *testing.T) { t.Fatal() } + exists, err = atab.AddressExists(context, evm, addr) + Require(t, err) + if !exists { + t.Fatal("Address should exist") + } + // verify Lookup of addr returns 0 index, err := atab.Lookup(context, evm, addr) Require(t, err) diff --git a/precompiles/ArbAggregator_test.go b/precompiles/ArbAggregator_test.go index ce1cebde5d..879fc737e4 100644 --- a/precompiles/ArbAggregator_test.go +++ b/precompiles/ArbAggregator_test.go @@ -12,34 +12,6 @@ import ( "github.com/offchainlabs/nitro/arbos/l1pricing" ) -func TestArbAggregatorBatchPosters(t *testing.T) { - evm := newMockEVMForTesting() - context := testContext(common.Address{}, evm) - - addr := common.BytesToAddress(crypto.Keccak256([]byte{})[:20]) - - // initially should have one batch poster - bps, err := ArbAggregator{}.GetBatchPosters(context, evm) - Require(t, err) - if len(bps) != 1 { - Fail(t) - } - - // add addr as a batch poster - Require(t, ArbDebug{}.BecomeChainOwner(context, evm)) - Require(t, ArbAggregator{}.AddBatchPoster(context, evm, addr)) - - // there should now be two batch posters, and addr should be one of them - bps, err = ArbAggregator{}.GetBatchPosters(context, evm) - Require(t, err) - if len(bps) != 2 { - Fail(t) - } - if bps[0] != addr && bps[1] != addr { - Fail(t) - } -} - func TestFeeCollector(t *testing.T) { evm := newMockEVMForTesting() agg := ArbAggregator{} diff --git a/precompiles/ArbGasInfo_test.go b/precompiles/ArbGasInfo_test.go new file mode 100644 index 0000000000..260d7b3cef --- /dev/null +++ b/precompiles/ArbGasInfo_test.go @@ -0,0 +1,140 @@ +// Copyright 2021-2024, Offchain Labs, Inc. +// For license information, see https://github.com/nitro/blob/master/LICENSE + +package precompiles + +import ( + "math/big" + "testing" + + "github.com/ethereum/go-ethereum/common" + "github.com/ethereum/go-ethereum/core/types" + "github.com/ethereum/go-ethereum/core/vm" + "github.com/ethereum/go-ethereum/crypto" + "github.com/ethereum/go-ethereum/params" + "github.com/offchainlabs/nitro/arbos/arbosState" + "github.com/offchainlabs/nitro/arbos/burn" + "github.com/offchainlabs/nitro/arbos/storage" + "github.com/offchainlabs/nitro/arbos/util" + "github.com/offchainlabs/nitro/util/testhelpers" +) + +func setupArbGasInfo( + t *testing.T, +) ( + *vm.EVM, + *arbosState.ArbosState, + *Context, + *ArbGasInfo, +) { + evm := newMockEVMForTesting() + caller := common.BytesToAddress(crypto.Keccak256([]byte{})[:20]) + tracer := util.NewTracingInfo(evm, testhelpers.RandomAddress(), types.ArbosAddress, util.TracingDuringEVM) + state, err := arbosState.OpenArbosState(evm.StateDB, burn.NewSystemBurner(tracer, false)) + Require(t, err) + + arbGasInfo := &ArbGasInfo{} + callCtx := testContext(caller, evm) + + return evm, state, callCtx, arbGasInfo +} + +func TestGetGasBacklog(t *testing.T) { + t.Parallel() + + evm, state, callCtx, arbGasInfo := setupArbGasInfo(t) + + backlog := uint64(1000) + err := state.L2PricingState().SetGasBacklog(backlog) + Require(t, err) + retrievedBacklog, err := arbGasInfo.GetGasBacklog(callCtx, evm) + Require(t, err) + if retrievedBacklog != backlog { + t.Fatal("expected backlog to be", backlog, "but got", retrievedBacklog) + } +} + +func TestGetL1PricingUpdateTime(t *testing.T) { + t.Parallel() + + evm, state, callCtx, arbGasInfo := setupArbGasInfo(t) + + lastUpdateTime := uint64(1001) + err := state.L1PricingState().SetLastUpdateTime(lastUpdateTime) + Require(t, err) + retrievedLastUpdateTime, err := arbGasInfo.GetLastL1PricingUpdateTime(callCtx, evm) + Require(t, err) + if retrievedLastUpdateTime != lastUpdateTime { + t.Fatal("expected last update time to be", lastUpdateTime, "but got", retrievedLastUpdateTime) + } +} + +func TestGetL1PricingFundsDueForRewards(t *testing.T) { + t.Parallel() + + evm, state, callCtx, arbGasInfo := setupArbGasInfo(t) + + fundsDueForRewards := big.NewInt(1002) + err := state.L1PricingState().SetFundsDueForRewards(fundsDueForRewards) + Require(t, err) + retrievedFundsDueForRewards, err := arbGasInfo.GetL1PricingFundsDueForRewards(callCtx, evm) + Require(t, err) + if retrievedFundsDueForRewards.Cmp(fundsDueForRewards) != 0 { + t.Fatal("expected funds due for rewards to be", fundsDueForRewards, "but got", retrievedFundsDueForRewards) + } +} + +func TestGetL1PricingUnitsSinceUpdate(t *testing.T) { + t.Parallel() + + evm, state, callCtx, arbGasInfo := setupArbGasInfo(t) + + pricingUnitsSinceUpdate := uint64(1003) + err := state.L1PricingState().SetUnitsSinceUpdate(pricingUnitsSinceUpdate) + Require(t, err) + retrievedPricingUnitsSinceUpdate, err := arbGasInfo.GetL1PricingUnitsSinceUpdate(callCtx, evm) + Require(t, err) + if retrievedPricingUnitsSinceUpdate != pricingUnitsSinceUpdate { + t.Fatal("expected pricing units since update to be", pricingUnitsSinceUpdate, "but got", retrievedPricingUnitsSinceUpdate) + } +} + +func TestGetLastL1PricingSurplus(t *testing.T) { + t.Parallel() + + evm, state, callCtx, arbGasInfo := setupArbGasInfo(t) + + lastSurplus := big.NewInt(1004) + err := state.L1PricingState().SetLastSurplus(lastSurplus, params.ArbosVersion_Stylus) + Require(t, err) + retrievedLastSurplus, err := arbGasInfo.GetLastL1PricingSurplus(callCtx, evm) + Require(t, err) + if retrievedLastSurplus.Cmp(lastSurplus) != 0 { + t.Fatal("expected last surplus to be", lastSurplus, "but got", retrievedLastSurplus) + } +} + +func TestGetPricesInArbGas(t *testing.T) { + t.Parallel() + + evm := newMockEVMForTesting() + caller := common.BytesToAddress(crypto.Keccak256([]byte{})[:20]) + arbGasInfo := &ArbGasInfo{} + callCtx := testContext(caller, evm) + + evm.Context.BaseFee = big.NewInt(1005) + expectedGasPerL2Tx := big.NewInt(111442786069) + expectedGasForL1Calldata := big.NewInt(796019900) + expectedStorageArbGas := big.NewInt(int64(storage.StorageWriteCost)) + gasPerL2Tx, gasForL1Calldata, storageArbGas, err := arbGasInfo.GetPricesInArbGas(callCtx, evm) + Require(t, err) + if gasPerL2Tx.Cmp(expectedGasPerL2Tx) != 0 { + t.Fatal("expected gas per L2 tx to be", expectedGasPerL2Tx, "but got", gasPerL2Tx) + } + if gasForL1Calldata.Cmp(expectedGasForL1Calldata) != 0 { + t.Fatal("expected gas for L1 calldata to be", expectedGasForL1Calldata, "but got", gasForL1Calldata) + } + if storageArbGas.Cmp(expectedStorageArbGas) != 0 { + t.Fatal("expected storage arb gas to be", expectedStorageArbGas, "but got", storageArbGas) + } +} diff --git a/precompiles/ArbOwner_test.go b/precompiles/ArbOwner_test.go index 1f8c7ae4cd..1fc6e679cb 100644 --- a/precompiles/ArbOwner_test.go +++ b/precompiles/ArbOwner_test.go @@ -151,6 +151,23 @@ func TestArbOwner(t *testing.T) { if avail.Cmp(deposited) != 0 { Fail(t, avail, deposited) } + + err = prec.SetNetworkFeeAccount(callCtx, evm, addr1) + Require(t, err) + retrievedNetworkFeeAccount, err := prec.GetNetworkFeeAccount(callCtx, evm) + Require(t, err) + if retrievedNetworkFeeAccount.Cmp(addr1) != 0 { + Fail(t, "Expected", addr1, "got", retrievedNetworkFeeAccount) + } + + l2BaseFee := big.NewInt(123) + err = prec.SetL2BaseFee(callCtx, evm, l2BaseFee) + Require(t, err) + retrievedL2BaseFee, err := state.L2PricingState().BaseFeeWei() + Require(t, err) + if l2BaseFee.Cmp(retrievedL2BaseFee) != 0 { + Fail(t, "Expected", l2BaseFee, "got", retrievedL2BaseFee) + } } func TestArbOwnerSetChainConfig(t *testing.T) { diff --git a/precompiles/ArbRetryableTx.go b/precompiles/ArbRetryableTx.go index 93e8023603..d925499180 100644 --- a/precompiles/ArbRetryableTx.go +++ b/precompiles/ArbRetryableTx.go @@ -154,7 +154,6 @@ func (con ArbRetryableTx) GetTimeout(c ctx, evm mech, ticketId bytes32) (huge, e // Keepalive adds one lifetime period to the ticket's expiry func (con ArbRetryableTx) Keepalive(c ctx, evm mech, ticketId bytes32) (huge, error) { - // charge for the expiry update retryableState := c.State.RetryableState() nbytes, err := retryableState.RetryableSizeBytes(ticketId, evm.Context.Time) diff --git a/precompiles/ArbRetryableTx_test.go b/precompiles/ArbRetryableTx_test.go index 9ccb437abc..47450299ce 100644 --- a/precompiles/ArbRetryableTx_test.go +++ b/precompiles/ArbRetryableTx_test.go @@ -7,12 +7,37 @@ import ( "math/big" "testing" + "github.com/offchainlabs/nitro/arbos" "github.com/offchainlabs/nitro/arbos/storage" "github.com/ethereum/go-ethereum/common" + "github.com/ethereum/go-ethereum/core" + "github.com/ethereum/go-ethereum/core/vm" templates "github.com/offchainlabs/nitro/solgen/go/precompilesgen" ) +func newMockEVMForTestingWithCurrentRefundTo(currentRefundTo *common.Address) *vm.EVM { + evm := newMockEVMForTesting() + txProcessor := arbos.NewTxProcessor(evm, &core.Message{}) + txProcessor.CurrentRefundTo = currentRefundTo + evm.ProcessingHook = txProcessor + return evm +} + +func TestGetCurrentRedeemer(t *testing.T) { + currentRefundTo := common.HexToAddress("0x030405") + + evm := newMockEVMForTestingWithCurrentRefundTo(¤tRefundTo) + retryableTx := ArbRetryableTx{} + context := testContext(common.Address{}, evm) + + currentRedeemer, err := retryableTx.GetCurrentRedeemer(context, evm) + Require(t, err) + if currentRefundTo.Cmp(currentRedeemer) != 0 { + t.Fatal("Expected to be ", currentRefundTo, " but got ", currentRedeemer) + } +} + func TestRetryableRedeem(t *testing.T) { evm := newMockEVMForTesting() precompileCtx := testContext(common.Address{}, evm) diff --git a/precompiles/ArbSys.go b/precompiles/ArbSys.go index d55067a09c..689d3b18de 100644 --- a/precompiles/ArbSys.go +++ b/precompiles/ArbSys.go @@ -92,7 +92,6 @@ func (con *ArbSys) WasMyCallersAddressAliased(c ctx, evm mech) (bool, error) { // MyCallersAddressWithoutAliasing gets the caller's caller without any potential aliasing func (con *ArbSys) MyCallersAddressWithoutAliasing(c ctx, evm mech) (addr, error) { - address := addr{} if evm.Depth() > 1 { diff --git a/pubsub/common.go b/pubsub/common.go index d7f041af15..ad36b6e622 100644 --- a/pubsub/common.go +++ b/pubsub/common.go @@ -2,12 +2,15 @@ package pubsub import ( "context" + "fmt" "strings" "github.com/ethereum/go-ethereum/log" - "github.com/go-redis/redis/v8" + "github.com/redis/go-redis/v9" ) +func ResultKeyFor(streamName, id string) string { return fmt.Sprintf("%s.%s", streamName, id) } + // CreateStream tries to create stream with given name, if it already exists // does not return an error. func CreateStream(ctx context.Context, streamName string, client redis.UniversalClient) error { diff --git a/pubsub/consumer.go b/pubsub/consumer.go index bd73e729e7..391042bd7e 100644 --- a/pubsub/consumer.go +++ b/pubsub/consumer.go @@ -5,36 +5,38 @@ import ( "encoding/json" "errors" "fmt" + "math" + "math/rand" + "strconv" "time" "github.com/ethereum/go-ethereum/log" - "github.com/go-redis/redis/v8" "github.com/google/uuid" "github.com/offchainlabs/nitro/util/stopwaiter" + "github.com/redis/go-redis/v9" "github.com/spf13/pflag" ) type ConsumerConfig struct { // Timeout of result entry in Redis. ResponseEntryTimeout time.Duration `koanf:"response-entry-timeout"` - // Duration after which consumer is considered to be dead if heartbeat - // is not updated. - KeepAliveTimeout time.Duration `koanf:"keepalive-timeout"` + // Minimum idle time after which messages will be autoclaimed + IdletimeToAutoclaim time.Duration `koanf:"idletime-to-autoclaim"` } var DefaultConsumerConfig = ConsumerConfig{ ResponseEntryTimeout: time.Hour, - KeepAliveTimeout: 5 * time.Minute, + IdletimeToAutoclaim: 5 * time.Minute, } var TestConsumerConfig = ConsumerConfig{ ResponseEntryTimeout: time.Minute, - KeepAliveTimeout: 30 * time.Millisecond, + IdletimeToAutoclaim: 30 * time.Millisecond, } func ConsumerConfigAddOptions(prefix string, f *pflag.FlagSet) { f.Duration(prefix+".response-entry-timeout", DefaultConsumerConfig.ResponseEntryTimeout, "timeout for response entry") - f.Duration(prefix+".keepalive-timeout", DefaultConsumerConfig.KeepAliveTimeout, "timeout after which consumer is considered inactive if heartbeat wasn't performed") + f.Duration(prefix+".idletime-to-autoclaim", DefaultConsumerConfig.IdletimeToAutoclaim, "After a message spends this amount of time in PEL (Pending Entries List i.e claimed by another consumer but not Acknowledged) it will be allowed to be autoclaimed by other consumers") } // Consumer implements a consumer for redis stream provides heartbeat to @@ -51,6 +53,7 @@ type Consumer[Request any, Response any] struct { type Message[Request any] struct { ID string Value Request + Ack func() } func NewConsumer[Request any, Response any](client redis.UniversalClient, streamName string, cfg *ConsumerConfig) (*Consumer[Request, Response], error) { @@ -69,12 +72,6 @@ func NewConsumer[Request any, Response any](client redis.UniversalClient, stream // Start starts the consumer to iteratively perform heartbeat in configured intervals. func (c *Consumer[Request, Response]) Start(ctx context.Context) { c.StopWaiter.Start(ctx, c) - c.StopWaiter.CallIteratively( - func(ctx context.Context) time.Duration { - c.heartBeat(ctx) - return c.cfg.KeepAliveTimeout / 10 - }, - ) } func (c *Consumer[Request, Response]) Id() string { @@ -83,11 +80,6 @@ func (c *Consumer[Request, Response]) Id() string { func (c *Consumer[Request, Response]) StopAndWait() { c.StopWaiter.StopAndWait() - c.deleteHeartBeat(c.GetParentContext()) -} - -func heartBeatKey(id string) string { - return fmt.Sprintf("consumer:%s:heartbeat", id) } func (c *Consumer[Request, Response]) RedisClient() redis.UniversalClient { @@ -98,68 +90,124 @@ func (c *Consumer[Request, Response]) StreamName() string { return c.redisStream } -func (c *Consumer[Request, Response]) heartBeatKey() string { - return heartBeatKey(c.id) -} - -// deleteHeartBeat deletes the heartbeat to indicate it is being shut down. -func (c *Consumer[Request, Response]) deleteHeartBeat(ctx context.Context) { - if err := c.client.Del(ctx, c.heartBeatKey()).Err(); err != nil { - l := log.Info - if ctx.Err() != nil { - l = log.Error - } - l("Deleting heardbeat", "consumer", c.id, "error", err) +func decrementMsgIdByOne(msgId string) string { + id, err := getUintParts(msgId) + if err != nil { + log.Error("Error decrementing start of XAutoClaim by one, defaulting to 0", "err", err) + return "0" } -} - -// heartBeat updates the heartBeat key indicating aliveness. -func (c *Consumer[Request, Response]) heartBeat(ctx context.Context) { - if err := c.client.Set(ctx, c.heartBeatKey(), time.Now().UnixMilli(), 2*c.cfg.KeepAliveTimeout).Err(); err != nil { - l := log.Info - if ctx.Err() != nil { - l = log.Error - } - l("Updating heardbeat", "consumer", c.id, "error", err) + if id[1] > 0 { + return strconv.FormatUint(id[0], 10) + "-" + strconv.FormatUint(id[1]-1, 10) + } else if id[0] > 0 { + return strconv.FormatUint(id[0]-1, 10) + "-" + strconv.FormatUint(math.MaxUint64, 10) } + return "0" } // Consumer first checks it there exists pending message that is claimed by // unresponsive consumer, if not then reads from the stream. func (c *Consumer[Request, Response]) Consume(ctx context.Context) (*Message[Request], error) { - res, err := c.client.XReadGroup(ctx, &redis.XReadGroupArgs{ - Group: c.redisGroup, - Consumer: c.id, - // Receive only messages that were never delivered to any other consumer, - // that is, only new messages. - Streams: []string{c.redisStream, ">"}, - Count: 1, - Block: time.Millisecond, // 0 seems to block the read instead of immediately returning - }).Result() - if errors.Is(err, redis.Nil) { - return nil, nil - } - if err != nil { - return nil, fmt.Errorf("reading message for consumer: %q: %w", c.id, err) + // First try to XAUTOCLAIM, with start as a random messageID from PEL with MinIdle as IdletimeToAutoclaim + // this prioritizes processing PEL messages that have been waiting for more than IdletimeToAutoclaim duration + var messages []redis.XMessage + if pendingMsgs, err := c.client.XPendingExt(ctx, &redis.XPendingExtArgs{ + Stream: c.redisStream, + Group: c.redisGroup, + Start: "-", + End: "+", + Count: 50, + Idle: c.cfg.IdletimeToAutoclaim, + }).Result(); err != nil { + if !errors.Is(err, redis.Nil) { + log.Error("Error from XpendingExt in getting PEL for auto claim", "err", err, "penindlen", len(pendingMsgs)) + } + } else if len(pendingMsgs) > 0 { + idx := rand.Intn(len(pendingMsgs)) + messages, _, err = c.client.XAutoClaim(ctx, &redis.XAutoClaimArgs{ + Group: c.redisGroup, + Consumer: c.id, + MinIdle: c.cfg.IdletimeToAutoclaim, // Minimum idle time for messages to claim (in milliseconds) + Stream: c.redisStream, + Start: decrementMsgIdByOne(pendingMsgs[idx].ID), + Count: 1, + }).Result() + if err != nil { + log.Info("error from xautoclaim", "err", err) + } } - if len(res) != 1 || len(res[0].Messages) != 1 { - return nil, fmt.Errorf("redis returned entries: %+v, for querying single message", res) + if len(messages) == 0 { + // If we fail to autoclaim then we do not retry but instead fallback to reading new messages + res, err := c.client.XReadGroup(ctx, &redis.XReadGroupArgs{ + Group: c.redisGroup, + Consumer: c.id, + // Receive only messages that were never delivered to any other consumer, + // that is, only new messages. + Streams: []string{c.redisStream, ">"}, + Count: 1, + Block: time.Millisecond, // 0 seems to block the read instead of immediately returning + }).Result() + if errors.Is(err, redis.Nil) { + return nil, nil + } + if err != nil { + return nil, fmt.Errorf("reading message for consumer: %q: %w", c.id, err) + } + if len(res) != 1 || len(res[0].Messages) != 1 { + return nil, fmt.Errorf("redis returned entries: %+v, for querying single message", res) + } + messages = res[0].Messages } + var ( - value = res[0].Messages[0].Values[messageKey] + value = messages[0].Values[messageKey] data, ok = (value).(string) ) if !ok { - return nil, fmt.Errorf("casting request to string: %w", err) + return nil, errors.New("error casting request to string") } var req Request if err := json.Unmarshal([]byte(data), &req); err != nil { return nil, fmt.Errorf("unmarshaling value: %v, error: %w", value, err) } - log.Debug("Redis stream consuming", "consumer_id", c.id, "message_id", res[0].Messages[0].ID) + ackNotifier := make(chan struct{}) + c.StopWaiter.LaunchThread(func(ctx context.Context) { + for { + // Use XClaimJustID so that we would have clear difference between invalid requests that are claimed multiple times due to xautoclaim and + // valid requests that are just being claimed in regular intervals to indicate heartbeat + if ids, err := c.client.XClaimJustID(ctx, &redis.XClaimArgs{ + Stream: c.redisStream, + Group: c.redisGroup, + Consumer: c.id, + MinIdle: 0, + Messages: []string{messages[0].ID}, + }).Result(); err != nil { + log.Error("Error claiming message, it might be possible that other consumers might pick this request", "msgID", messages[0].ID) + } else if len(ids) == 0 { + log.Warn("XClaimJustID returned empty response when indicating hearbeat", "msgID", messages[0].ID) + } else if len(ids) > 1 { + log.Error("XClaimJustID returned response with more than entry", "msgIDs", ids) + } + select { + case <-ackNotifier: + return + case <-ctx.Done(): + log.Info("Context done while claiming message to indicate hearbeat", "messageID", messages[0].ID, "error", ctx.Err().Error()) + if c.StopWaiter.GetParentContext().Err() == nil { + // Proceeding to set the Idle time of message to IdletimeToAutoclaim to allow it to be picked by other consumers + if err := c.client.Do(c.StopWaiter.GetParentContext(), "XCLAIM", c.redisStream, c.redisGroup, c.id, 0, messages[0].ID, "IDLE", c.cfg.IdletimeToAutoclaim.Milliseconds()).Err(); err != nil { + log.Error("error when trying to set the idle time of currently worked on message to IdletimeToAutoclaim", "messageID", messages[0].ID, "err", err) + } + } + return + case <-time.After(c.cfg.IdletimeToAutoclaim / 10): + } + } + }) + log.Debug("Redis stream consuming", "consumer_id", c.id, "message_id", messages[0].ID) return &Message[Request]{ - ID: res[0].Messages[0].ID, + ID: messages[0].ID, Value: req, + Ack: func() { close(ackNotifier) }, }, nil } @@ -168,14 +216,18 @@ func (c *Consumer[Request, Response]) SetResult(ctx context.Context, messageID s if err != nil { return fmt.Errorf("marshaling result: %w", err) } - log.Debug("consumer: setting result", "cid", c.id, "messageId", messageID) - acquired, err := c.client.SetNX(ctx, messageID, resp, c.cfg.ResponseEntryTimeout).Result() + resultKey := ResultKeyFor(c.StreamName(), messageID) + log.Debug("consumer: setting result", "cid", c.id, "msgIdInStream", messageID, "resultKeyInRedis", resultKey) + acquired, err := c.client.SetNX(ctx, resultKey, resp, c.cfg.ResponseEntryTimeout).Result() if err != nil || !acquired { - return fmt.Errorf("setting result for message: %v, error: %w", messageID, err) + return fmt.Errorf("setting result for message with message-id in stream: %v, error: %w", messageID, err) } log.Debug("consumer: xack", "cid", c.id, "messageId", messageID) if _, err := c.client.XAck(ctx, c.redisStream, c.redisGroup, messageID).Result(); err != nil { return fmt.Errorf("acking message: %v, error: %w", messageID, err) } + if _, err := c.client.XDel(ctx, c.redisStream, messageID).Result(); err != nil { + return fmt.Errorf("deleting message: %v, error: %w", messageID, err) + } return nil } diff --git a/pubsub/producer.go b/pubsub/producer.go index 5eec3a4b52..722c145a09 100644 --- a/pubsub/producer.go +++ b/pubsub/producer.go @@ -13,17 +13,16 @@ import ( "encoding/json" "errors" "fmt" - "math" "strconv" "strings" "sync" "time" "github.com/ethereum/go-ethereum/log" - "github.com/go-redis/redis/v8" "github.com/google/uuid" "github.com/offchainlabs/nitro/util/containers" "github.com/offchainlabs/nitro/util/stopwaiter" + "github.com/redis/go-redis/v9" "github.com/spf13/pflag" ) @@ -43,50 +42,31 @@ type Producer[Request any, Response any] struct { promisesLock sync.RWMutex promises map[string]*containers.Promise[Response] - // Used for running checks for pending messages with inactive consumers - // and checking responses from consumers iteratively for the first time when - // Produce is called. + // Used for checking responses from consumers iteratively + // For the first time when Produce is called. once sync.Once } type ProducerConfig struct { - // When enabled, messages that are sent to consumers that later die before - // processing them, will be re-inserted into the stream to be proceesed by - // another consumer - EnableReproduce bool `koanf:"enable-reproduce"` - // Interval duration in which producer checks for pending messages delivered - // to the consumers that are currently inactive. - CheckPendingInterval time.Duration `koanf:"check-pending-interval"` - // Duration after which consumer is considered to be dead if heartbeat - // is not updated. - KeepAliveTimeout time.Duration `koanf:"keepalive-timeout"` // Interval duration for checking the result set by consumers. CheckResultInterval time.Duration `koanf:"check-result-interval"` - CheckPendingItems int64 `koanf:"check-pending-items"` + // RequestTimeout is a TTL for any message sent to the redis stream + RequestTimeout time.Duration `koanf:"request-timeout"` } var DefaultProducerConfig = ProducerConfig{ - EnableReproduce: true, - CheckPendingInterval: time.Second, - KeepAliveTimeout: 5 * time.Minute, - CheckResultInterval: 5 * time.Second, - CheckPendingItems: 256, + CheckResultInterval: 5 * time.Second, + RequestTimeout: 3 * time.Hour, } var TestProducerConfig = ProducerConfig{ - EnableReproduce: false, - CheckPendingInterval: 10 * time.Millisecond, - KeepAliveTimeout: 100 * time.Millisecond, - CheckResultInterval: 5 * time.Millisecond, - CheckPendingItems: 256, + CheckResultInterval: 5 * time.Millisecond, + RequestTimeout: time.Minute, } func ProducerAddConfigAddOptions(prefix string, f *pflag.FlagSet) { - f.Bool(prefix+".enable-reproduce", DefaultProducerConfig.EnableReproduce, "when enabled, messages with dead consumer will be re-inserted into the stream") - f.Duration(prefix+".check-pending-interval", DefaultProducerConfig.CheckPendingInterval, "interval in which producer checks pending messages whether consumer processing them is inactive") f.Duration(prefix+".check-result-interval", DefaultProducerConfig.CheckResultInterval, "interval in which producer checks pending messages whether consumer processing them is inactive") - f.Duration(prefix+".keepalive-timeout", DefaultProducerConfig.KeepAliveTimeout, "timeout after which consumer is considered inactive if heartbeat wasn't performed") - f.Int64(prefix+".check-pending-items", DefaultProducerConfig.CheckPendingItems, "items to screen during check-pending") + f.Duration(prefix+".request-timeout", DefaultProducerConfig.RequestTimeout, "timeout after which the message in redis stream is considered as errored, this prevents workers from working on wrong requests indefinitely") } func NewProducer[Request any, Response any](client redis.UniversalClient, streamName string, cfg *ProducerConfig) (*Producer[Request, Response], error) { @@ -106,125 +86,72 @@ func NewProducer[Request any, Response any](client redis.UniversalClient, stream }, nil } -func (p *Producer[Request, Response]) errorPromisesFor(msgIds []string) { - p.promisesLock.Lock() - defer p.promisesLock.Unlock() - for _, msg := range msgIds { - if promise, found := p.promises[msg]; found { - promise.ProduceError(fmt.Errorf("internal error, consumer died while serving the request")) - delete(p.promises, msg) - } +func getUintParts(msgId string) ([2]uint64, error) { + idParts := strings.Split(msgId, "-") + if len(idParts) != 2 { + return [2]uint64{}, fmt.Errorf("invalid i.d: %v", msgId) } -} - -// checkAndReproduce reproduce pending messages that were sent to consumers -// that are currently inactive. -func (p *Producer[Request, Response]) checkAndReproduce(ctx context.Context) time.Duration { - staleIds, err := p.checkPending(ctx) + idTimeStamp, err := strconv.ParseUint(idParts[0], 10, 64) if err != nil { - log.Error("Checking pending messages", "error", err) - return p.cfg.CheckPendingInterval - } - if len(staleIds) == 0 { - return p.cfg.CheckPendingInterval + return [2]uint64{}, fmt.Errorf("invalid i.d: %v err: %w", msgId, err) } - if p.cfg.EnableReproduce { - err = p.reproduceIds(ctx, staleIds) - if err != nil { - log.Warn("filed reproducing messages", "err", err) - } - } else { - p.errorPromisesFor(staleIds) - } - return p.cfg.CheckPendingInterval -} - -func (p *Producer[Request, Response]) reproduceIds(ctx context.Context, staleIds []string) error { - log.Info("Attempting to claim", "messages", staleIds) - claimedMsgs, err := p.client.XClaim(ctx, &redis.XClaimArgs{ - Stream: p.redisStream, - Group: p.redisGroup, - Consumer: p.id, - MinIdle: p.cfg.KeepAliveTimeout, - Messages: staleIds, - }).Result() + idSerial, err := strconv.ParseUint(idParts[1], 10, 64) if err != nil { - return fmt.Errorf("claiming ownership on messages: %v, error: %w", staleIds, err) - } - for _, msg := range claimedMsgs { - data, ok := (msg.Values[messageKey]).(string) - if !ok { - log.Error("redis producer reproduce: message not string", "id", msg.ID, "value", msg.Values[messageKey]) - continue - } - var req Request - if err := json.Unmarshal([]byte(data), &req); err != nil { - log.Error("redis producer reproduce: message not a request", "id", msg.ID, "err", err, "value", msg.Values[messageKey]) - continue - } - if _, err := p.client.XAck(ctx, p.redisStream, p.redisGroup, msg.ID).Result(); err != nil { - log.Error("redis producer reproduce: could not ACK", "id", msg.ID, "err", err) - continue - } - // Only re-insert messages that were removed the the pending list first. - if _, err := p.reproduce(ctx, req, msg.ID); err != nil { - log.Error("redis producer reproduce: error", "err", err) - } + return [2]uint64{}, fmt.Errorf("invalid i.d serial: %v err: %w", msgId, err) } - return nil + return [2]uint64{idTimeStamp, idSerial}, nil } -func setMinIdInt(min *[2]uint64, id string) error { - idParts := strings.Split(id, "-") - if len(idParts) != 2 { - return fmt.Errorf("invalid i.d: %v", id) - } - idTimeStamp, err := strconv.ParseUint(idParts[0], 10, 64) +// cmpMsgId compares two msgid's and returns (0) if equal, (-1) if msgId1 < msgId2, (1) if msgId1 > msgId2, (-2) if not comparable (or error) +func cmpMsgId(msgId1, msgId2 string) int { + id1, err := getUintParts(msgId1) if err != nil { - return fmt.Errorf("invalid i.d: %v err: %w", id, err) + log.Trace("error comparing msgIds", "msgId1", msgId1, "msgId2", msgId2) + return -2 } - if idTimeStamp > min[0] { - return nil - } - idSerial, err := strconv.ParseUint(idParts[1], 10, 64) + id2, err := getUintParts(msgId2) if err != nil { - return fmt.Errorf("invalid i.d serial: %v err: %w", id, err) + log.Trace("error comparing msgIds", "msgId1", msgId1, "msgId2", msgId2) + return -2 } - if idTimeStamp < min[0] { - min[0] = idTimeStamp - min[1] = idSerial - return nil + if id1[0] < id2[0] { + return -1 + } else if id1[0] > id2[0] { + return 1 + } else if id1[1] < id2[1] { + return -1 + } else if id1[1] > id2[1] { + return 1 } - // idTimeStamp == min[0] - if idSerial < min[1] { - min[1] = idSerial - } - return nil + return 0 } // checkResponses checks iteratively whether response for the promise is ready. func (p *Producer[Request, Response]) checkResponses(ctx context.Context) time.Duration { - minIdInt := [2]uint64{math.MaxUint64, math.MaxUint64} log.Debug("redis producer: check responses starting") p.promisesLock.Lock() defer p.promisesLock.Unlock() responded := 0 errored := 0 checked := 0 + allowedOldestID := fmt.Sprintf("%d-0", time.Now().Add(-p.cfg.RequestTimeout).UnixMilli()) for id, promise := range p.promises { if ctx.Err() != nil { return 0 } checked++ - res, err := p.client.Get(ctx, id).Result() + resultKey := ResultKeyFor(p.redisStream, id) + res, err := p.client.Get(ctx, resultKey).Result() if err != nil { - errSetId := setMinIdInt(&minIdInt, id) - if errSetId != nil { - log.Error("redis producer: error setting minId", "err", err) - return p.cfg.CheckResultInterval - } if !errors.Is(err, redis.Nil) { - log.Error("redis producer: Error reading value in redis", "key", id, "error", err) + log.Error("Error reading value in redis", "key", resultKey, "error", err) + } else if cmpMsgId(id, allowedOldestID) == -1 { + // The request this producer is waiting for has been past its TTL or is older than current PEL's lower, + // so safe to error and stop tracking this promise + promise.ProduceError(errors.New("error getting response, request has been waiting for too long")) + log.Error("error getting response, request has been waiting past its TTL") + errored++ + delete(p.promises, id) } continue } @@ -237,22 +164,51 @@ func (p *Producer[Request, Response]) checkResponses(ctx context.Context) time.D promise.Produce(resp) responded++ } - p.client.Del(ctx, id) + p.client.Del(ctx, resultKey) delete(p.promises, id) } - var trimmed int64 - var trimErr error - minId := "+" - if minIdInt[0] < math.MaxUint64 { - minId = fmt.Sprintf("%d-%d", minIdInt[0], minIdInt[1]) - trimmed, trimErr = p.client.XTrimMinID(ctx, p.redisStream, minId).Result() - } else { - trimmed, trimErr = p.client.XTrimMaxLen(ctx, p.redisStream, 0).Result() - } - log.Debug("trimming", "id", minId, "trimmed", trimmed, "responded", responded, "errored", errored, "trim-err", trimErr, "checked", checked) + log.Debug("checkResponses", "responded", responded, "errored", errored, "checked", checked) return p.cfg.CheckResultInterval } +func (p *Producer[Request, Response]) clearMessages(ctx context.Context) time.Duration { + pelData, err := p.client.XPending(ctx, p.redisStream, p.redisGroup).Result() + if err != nil { + log.Error("error getting PEL data from xpending, xtrimming is disabled", "err", err) + } + // XDEL on consumer side already deletes acked messages (mark as deleted) but doesnt claim the memory back, XTRIM helps in claiming this memory in normal conditions + // pelData might be outdated when we do the xtrim, but thats ok as the messages are also being trimmed by other producers + if pelData != nil && pelData.Lower != "" { + trimmed, trimErr := p.client.XTrimMinID(ctx, p.redisStream, pelData.Lower).Result() + log.Debug("trimming", "xTrimMinID", pelData.Lower, "trimmed", trimmed, "trim-err", trimErr) + // Check if pelData.Lower has been past its TTL and if it is then ack it to remove from PEL and delete it, once + // its taken out from PEL the producer that sent this request will handle the corresponding promise accordingly (as its past TTL) + allowedOldestID := fmt.Sprintf("%d-0", time.Now().Add(-p.cfg.RequestTimeout).UnixMilli()) + if cmpMsgId(pelData.Lower, allowedOldestID) == -1 { + if err := p.client.XClaim(ctx, &redis.XClaimArgs{ + Stream: p.redisStream, + Group: p.redisGroup, + Consumer: p.id, + MinIdle: 0, + Messages: []string{pelData.Lower}, + }).Err(); err != nil { + log.Error("error claiming PEL's lower message thats past its TTL", "msgID", pelData.Lower, "err", err) + return 5 * p.cfg.CheckResultInterval + } + if _, err := p.client.XAck(ctx, p.redisStream, p.redisGroup, pelData.Lower).Result(); err != nil { + log.Error("error acking PEL's lower message thats past its TTL", "msgID", pelData.Lower, "err", err) + return 5 * p.cfg.CheckResultInterval + } + if _, err := p.client.XDel(ctx, p.redisStream, pelData.Lower).Result(); err != nil { + log.Error("error deleting PEL's lower message thats past its TTL", "msgID", pelData.Lower, "err", err) + return 5 * p.cfg.CheckResultInterval + } + return 0 + } + } + return 5 * p.cfg.CheckResultInterval +} + func (p *Producer[Request, Response]) Start(ctx context.Context) { p.StopWaiter.Start(ctx, p) } @@ -263,101 +219,31 @@ func (p *Producer[Request, Response]) promisesLen() int { return len(p.promises) } -// reproduce is used when Producer claims ownership on the pending -// message that was sent to inactive consumer and reinserts it into the stream, -// so that seamlessly return the answer in the same promise. -func (p *Producer[Request, Response]) reproduce(ctx context.Context, value Request, oldKey string) (*containers.Promise[Response], error) { +func (p *Producer[Request, Response]) produce(ctx context.Context, value Request) (*containers.Promise[Response], error) { val, err := json.Marshal(value) if err != nil { return nil, fmt.Errorf("marshaling value: %w", err) } - // catching the promiseLock before we sendXadd makes sure promise ids will - // be always ascending + // catching the promiseLock before we sendXadd makes sure promise ids will be always ascending p.promisesLock.Lock() defer p.promisesLock.Unlock() - id, err := p.client.XAdd(ctx, &redis.XAddArgs{ + msgId, err := p.client.XAdd(ctx, &redis.XAddArgs{ Stream: p.redisStream, Values: map[string]any{messageKey: val}, }).Result() if err != nil { return nil, fmt.Errorf("adding values to redis: %w", err) } - promise := p.promises[oldKey] - if oldKey != "" && promise == nil { - // This will happen if the old consumer became inactive but then ack_d - // the message afterwards. - // don't error - log.Warn("tried reproducing a message but it wasn't found - probably got response", "oldKey", oldKey) - } - if oldKey == "" || promise == nil { - pr := containers.NewPromise[Response](nil) - promise = &pr - } - delete(p.promises, oldKey) - p.promises[id] = promise - return promise, nil + promise := containers.NewPromise[Response](nil) + p.promises[msgId] = &promise + return &promise, nil } func (p *Producer[Request, Response]) Produce(ctx context.Context, value Request) (*containers.Promise[Response], error) { log.Debug("Redis stream producing", "value", value) p.once.Do(func() { - p.StopWaiter.CallIteratively(p.checkAndReproduce) p.StopWaiter.CallIteratively(p.checkResponses) + p.StopWaiter.CallIteratively(p.clearMessages) }) - return p.reproduce(ctx, value, "") -} - -// Check if a consumer is with specified ID is alive. -func (p *Producer[Request, Response]) isConsumerAlive(ctx context.Context, consumerID string) bool { - if _, err := p.client.Get(ctx, heartBeatKey(consumerID)).Int64(); err != nil { - return false - } - return true -} - -func (p *Producer[Request, Response]) havePromiseFor(messageID string) bool { - p.promisesLock.Lock() - defer p.promisesLock.Unlock() - _, found := p.promises[messageID] - return found -} - -// returns ids of pending messages that's worker doesn't appear alive -func (p *Producer[Request, Response]) checkPending(ctx context.Context) ([]string, error) { - pendingMessages, err := p.client.XPendingExt(ctx, &redis.XPendingExtArgs{ - Stream: p.redisStream, - Group: p.redisGroup, - Start: "-", - End: "+", - Count: p.cfg.CheckPendingItems, - }).Result() - - if err != nil && !errors.Is(err, redis.Nil) { - return nil, fmt.Errorf("querying pending messages: %w", err) - } - if len(pendingMessages) == 0 { - return nil, nil - } - if len(pendingMessages) >= int(p.cfg.CheckPendingItems) { - log.Warn("redis producer: many pending items found", "stream", p.redisStream, "check-pending-items", p.cfg.CheckPendingItems) - } - // IDs of the pending messages with inactive consumers. - var ids []string - active := make(map[string]bool) - for _, msg := range pendingMessages { - // Ignore messages not produced by this producer. - if !p.havePromiseFor(msg.ID) { - continue - } - alive, found := active[msg.Consumer] - if !found { - alive = p.isConsumerAlive(ctx, msg.Consumer) - active[msg.Consumer] = alive - } - if alive { - continue - } - ids = append(ids, msg.ID) - } - return ids, nil + return p.produce(ctx, value) } diff --git a/pubsub/pubsub_test.go b/pubsub/pubsub_test.go index 9f774b6372..8bd1aed25d 100644 --- a/pubsub/pubsub_test.go +++ b/pubsub/pubsub_test.go @@ -10,11 +10,11 @@ import ( "time" "github.com/ethereum/go-ethereum/log" - "github.com/go-redis/redis/v8" "github.com/google/go-cmp/cmp" "github.com/google/uuid" "github.com/offchainlabs/nitro/util/containers" "github.com/offchainlabs/nitro/util/redisutil" + "github.com/redis/go-redis/v9" ) var ( @@ -23,7 +23,8 @@ var ( ) type testRequest struct { - Request string + Request string + IsInvalid bool } type testResponse struct { @@ -45,36 +46,21 @@ func destroyRedisGroup(ctx context.Context, t *testing.T, streamName string, cli } } -type configOpt interface { - apply(consCfg *ConsumerConfig, prodCfg *ProducerConfig) -} - -type withReproduce struct { - reproduce bool -} - -func (e *withReproduce) apply(_ *ConsumerConfig, prodCfg *ProducerConfig) { - prodCfg.EnableReproduce = e.reproduce -} - func producerCfg() *ProducerConfig { return &ProducerConfig{ - EnableReproduce: TestProducerConfig.EnableReproduce, - CheckPendingInterval: TestProducerConfig.CheckPendingInterval, - KeepAliveTimeout: TestProducerConfig.KeepAliveTimeout, - CheckResultInterval: TestProducerConfig.CheckResultInterval, - CheckPendingItems: TestProducerConfig.CheckPendingItems, + CheckResultInterval: TestProducerConfig.CheckResultInterval, + RequestTimeout: 2 * time.Second, } } func consumerCfg() *ConsumerConfig { return &ConsumerConfig{ ResponseEntryTimeout: TestConsumerConfig.ResponseEntryTimeout, - KeepAliveTimeout: TestConsumerConfig.KeepAliveTimeout, + IdletimeToAutoclaim: TestConsumerConfig.IdletimeToAutoclaim, } } -func newProducerConsumers(ctx context.Context, t *testing.T, opts ...configOpt) (redis.UniversalClient, string, *Producer[testRequest, testResponse], []*Consumer[testRequest, testResponse]) { +func newProducerConsumers(ctx context.Context, t *testing.T) (redis.UniversalClient, string, *Producer[testRequest, testResponse], []*Consumer[testRequest, testResponse]) { t.Helper() redisClient, err := redisutil.RedisClientFromURL(redisutil.CreateTestRedis(ctx, t)) if err != nil { @@ -82,9 +68,7 @@ func newProducerConsumers(ctx context.Context, t *testing.T, opts ...configOpt) } prodCfg, consCfg := producerCfg(), consumerCfg() streamName := fmt.Sprintf("stream:%s", uuid.NewString()) - for _, o := range opts { - o.apply(consCfg, prodCfg) - } + producer, err := NewProducer[testRequest, testResponse](redisClient, streamName, prodCfg) if err != nil { t.Fatalf("Error creating new producer: %v", err) @@ -102,13 +86,6 @@ func newProducerConsumers(ctx context.Context, t *testing.T, opts ...configOpt) t.Cleanup(func() { ctx := context.Background() destroyRedisGroup(ctx, t, streamName, producer.client) - var keys []string - for _, c := range consumers { - keys = append(keys, c.heartBeatKey()) - } - if _, err := producer.client.Del(ctx, keys...).Result(); err != nil { - log.Debug("Error deleting heartbeat keys", "error", err) - } }) return redisClient, streamName, producer, consumers } @@ -125,10 +102,10 @@ func msgForIndex(idx int) string { return fmt.Sprintf("msg: %d", idx) } -func wantMessages(n int) []string { +func wantMessages(n int, group string) []string { var ret []string for i := 0; i < n; i++ { - ret = append(ret, msgForIndex(i)) + ret = append(ret, group+msgForIndex(i)) } sort.Strings(ret) return ret @@ -143,10 +120,14 @@ func flatten(responses [][]string) []string { return ret } -func produceMessages(ctx context.Context, msgs []string, producer *Producer[testRequest, testResponse]) ([]*containers.Promise[testResponse], error) { +func produceMessages(ctx context.Context, msgs []string, producer *Producer[testRequest, testResponse], withInvalidEntries bool) ([]*containers.Promise[testResponse], error) { var promises []*containers.Promise[testResponse] - for i := 0; i < messagesCount; i++ { - promise, err := producer.Produce(ctx, testRequest{Request: msgs[i]}) + for i := 0; i < len(msgs); i++ { + req := testRequest{Request: msgs[i]} + if withInvalidEntries && i%50 == 0 { + req.IsInvalid = true + } + promise, err := producer.Produce(ctx, req) if err != nil { return nil, err } @@ -197,51 +178,97 @@ func consume(ctx context.Context, t *testing.T, consumers []*Consumer[testReques continue } gotMessages[idx][res.ID] = res.Value.Request - resp := fmt.Sprintf("result for: %v", res.ID) - if err := c.SetResult(ctx, res.ID, testResponse{Response: resp}); err != nil { - t.Errorf("Error setting a result: %v", err) + if !res.Value.IsInvalid { + resp := fmt.Sprintf("result for: %v", res.ID) + if err := c.SetResult(ctx, res.ID, testResponse{Response: resp}); err != nil { + t.Errorf("Error setting a result: %v", err) + } + wantResponses[idx] = append(wantResponses[idx], resp) } - wantResponses[idx] = append(wantResponses[idx], resp) + res.Ack() } }) } return wantResponses } -func TestRedisProduce(t *testing.T) { +func TestRedisProduceComplex(t *testing.T) { log.SetDefault(log.NewLogger(log.NewTerminalHandlerWithLevel(os.Stderr, log.LevelTrace, true))) t.Parallel() for _, tc := range []struct { - name string - killConsumers bool - autoRecover bool + name string + entriesCount []int + numProducers int + killConsumers bool + withInvalidEntries bool // If this is set, then every 50th entry is invalid (requests that can't be solved by any consumer) }{ { - name: "all consumers are active", - killConsumers: false, - autoRecover: false, + name: "one producer, all consumers are active", + entriesCount: []int{messagesCount}, + numProducers: 1, + }, + { + name: "two producers, all consumers are active", + entriesCount: []int{20, 20}, + numProducers: 2, }, { - name: "some consumers killed, others should take over their work", + name: "one producer, some consumers killed, others should take over their work", + entriesCount: []int{messagesCount}, + numProducers: 1, killConsumers: true, - autoRecover: true, }, + { - name: "some consumers killed, should return failure", + name: "two producers, some consumers killed, others should take over their work, unequal number of requests from producers", + entriesCount: []int{messagesCount, 2 * messagesCount}, + numProducers: 2, killConsumers: true, - autoRecover: false, + }, + { + name: "two producers, some consumers killed, others should take over their work, some invalid entries, unequal number of requests from producers", + entriesCount: []int{messagesCount, 2 * messagesCount}, + numProducers: 2, + killConsumers: true, + withInvalidEntries: true, }, } { t.Run(tc.name, func(t *testing.T) { ctx, cancel := context.WithCancel(context.Background()) defer cancel() - redisClient, streamName, producer, consumers := newProducerConsumers(ctx, t, &withReproduce{tc.autoRecover}) - producer.Start(ctx) - wantMsgs := wantMessages(messagesCount) - promises, err := produceMessages(ctx, wantMsgs, producer) - if err != nil { - t.Fatalf("Error producing messages: %v", err) + + var producers []*Producer[testRequest, testResponse] + redisClient, streamName, producer, consumers := newProducerConsumers(ctx, t) + producers = append(producers, producer) + if tc.numProducers == 2 { + producer, err := NewProducer[testRequest, testResponse](redisClient, streamName, producerCfg()) + if err != nil { + t.Fatalf("Error creating second producer: %v", err) + } + producers = append(producers, producer) + } + + for _, producer := range producers { + producer.Start(ctx) + } + + var entries [][]string + if tc.numProducers == 2 { + entries = append(entries, wantMessages(tc.entriesCount[0], "1.")) + entries = append(entries, wantMessages(tc.entriesCount[1], "2.")) + } else { + entries = append(entries, wantMessages(tc.entriesCount[0], "")) } + + var promises [][]*containers.Promise[testResponse] + for i := 0; i < tc.numProducers; i++ { + prs, err := produceMessages(ctx, entries[i], producers[i], tc.withInvalidEntries) + if err != nil { + t.Fatalf("Error producing messages from producer%d: %v", i, err) + } + promises = append(promises, prs) + } + gotMessages := messagesMaps(len(consumers)) if tc.killConsumers { // Consumer messages in every third consumer but don't ack them to check @@ -252,40 +279,79 @@ func TestRedisProduce(t *testing.T) { if err != nil { t.Errorf("Error consuming message: %v", err) } - if !tc.autoRecover { - gotMessages[i][req.ID] = req.Value.Request + if req == nil { + t.Error("Didn't consume any message") } + // Kills the actnotifier hence allowing XAUTOCLAIM consumers[i].StopAndWait() } } + time.Sleep(time.Second) wantResponses := consume(ctx, t, consumers, gotMessages) - gotResponses, errIndexes := awaitResponses(ctx, promises) - if len(errIndexes) != 0 && tc.autoRecover { - t.Fatalf("Error awaiting responses: %v", errIndexes) + + var gotResponses []string + for i := 0; i < tc.numProducers; i++ { + grs, errIndexes := awaitResponses(ctx, promises[i]) + if tc.withInvalidEntries { + if errIndexes[len(errIndexes)-1]+50 < len(entries[i]) { + t.Fatalf("Unexpected number of invalid requests while awaiting responses") + } + for j, idx := range errIndexes { + if idx != j*50 { + t.Fatalf("Invalid request' index mismatch want: %d got %d", j*50, idx) + } + } + } else if len(errIndexes) != 0 { + t.Fatalf("Error awaiting responses from promises %d: %v", i, errIndexes) + } + gotResponses = append(gotResponses, grs...) } - producer.StopAndWait() + for _, c := range consumers { c.StopAndWait() } - got, err := mergeValues(gotMessages) + + got, err := mergeValues(gotMessages, tc.withInvalidEntries) if err != nil { t.Fatalf("mergeMaps() unexpected error: %v", err) } + // Only when there are invalid entries got will have duplicates + if tc.withInvalidEntries { + got = removeDuplicates(got) + } + + var combinedEntries []string + for i := 0; i < tc.numProducers; i++ { + combinedEntries = append(combinedEntries, entries[i]...) + } + wantMsgs := combinedEntries if diff := cmp.Diff(wantMsgs, got); diff != "" { t.Errorf("Unexpected diff (-want +got):\n%s\n", diff) } - wantResp := flatten(wantResponses) + sort.Strings(gotResponses) + wantResp := flatten(wantResponses) if diff := cmp.Diff(wantResp, gotResponses); diff != "" { t.Errorf("Unexpected diff in responses:\n%s\n", diff) } - if cnt := producer.promisesLen(); cnt != 0 { - t.Errorf("Producer still has %d unfullfilled promises", cnt) + + // Check each producers all promises were responded to + for i := 0; i < tc.numProducers; i++ { + if cnt := producers[i].promisesLen(); cnt != 0 { + t.Errorf("Producer%d still has %d unfullfilled promises", i, cnt) + } } + // Trigger a trim - producer.checkResponses(ctx) + time.Sleep(time.Second) + for i := 0; i < tc.numProducers; i++ { + producers[i].checkResponses(ctx) + producers[i].StopAndWait() + } + + // Check that no messages remain in the stream msgs, err := redisClient.XRange(ctx, streamName, "-", "+").Result() if err != nil { t.Errorf("XRange failed: %v", err) @@ -297,14 +363,27 @@ func TestRedisProduce(t *testing.T) { } } +func removeDuplicates(list []string) []string { + capture := map[string]bool{} + var ret []string + for _, elem := range list { + if _, found := capture[elem]; !found { + ret = append(ret, elem) + capture[elem] = true + } + } + sort.Strings(ret) + return ret +} + // mergeValues merges maps from the slice and returns their values. // Returns and error if there exists duplicate key. -func mergeValues(messages []map[string]string) ([]string, error) { +func mergeValues(messages []map[string]string, withInvalidEntries bool) ([]string, error) { res := make(map[string]any) var ret []string for _, m := range messages { for k, v := range m { - if _, found := res[k]; found { + if _, found := res[k]; found && !withInvalidEntries { return nil, fmt.Errorf("duplicate key: %v", k) } res[k] = v diff --git a/scripts/check-build.sh b/scripts/check-build.sh new file mode 100755 index 0000000000..d654405c49 --- /dev/null +++ b/scripts/check-build.sh @@ -0,0 +1,138 @@ +#!/bin/bash +# This script checks the prerequisites for building Arbitrum Nitro locally. + +# Color codes +RED='\033[0;31m' +GREEN='\033[0;32m' +YELLOW='\033[1;33m' +BLUE='\033[0;34m' +NC='\033[0m' # No Color + +# Documentation link for installation instructions +INSTALLATION_DOCS_URL="Refer to https://docs.arbitrum.io/run-arbitrum-node/nitro/build-nitro-locally for installation." + +# Function to check if a command exists +command_exists() { + command -v "$1" >/dev/null 2>&1 +} + +EXIT_CODE=0 + +# Detect operating system +OS=$(uname -s) +echo -e "${BLUE}Detected OS: $OS${NC}" +echo -e "${BLUE}Checking prerequisites for building Nitro locally...${NC}" + +# Step 1: Check Docker Installation +if command_exists docker; then + echo -e "${GREEN}Docker is installed.${NC}" +else + echo -e "${RED}Docker is not installed.${NC}" + EXIT_CODE=1 +fi + +# Step 2: Check if Docker service is running +if [[ "$OS" == "Linux" ]] && ! sudo service docker status >/dev/null; then + echo -e "${YELLOW}Docker service is not running on Linux. Start it with: sudo service docker start${NC}" + EXIT_CODE=1 +elif [[ "$OS" == "Darwin" ]] && ! docker info >/dev/null 2>&1; then + echo -e "${YELLOW}Docker service is not running on macOS. Ensure Docker Desktop is started.${NC}" + EXIT_CODE=1 +else + echo -e "${GREEN}Docker service is running.${NC}" +fi + +# Step 3: Check the version tag +VERSION_TAG=$(git tag --points-at HEAD | sed '/-/!s/$/_/' | sort -rV | sed 's/_$//' | head -n 1 | grep ^ || git show -s --pretty=%D | sed 's/, /\n/g' | grep -v '^origin/' | grep -v '^grafted\|HEAD\|master\|main$' || echo "dev") +echo -e "${YELLOW}You are on the version tag: $VERSION_TAG${NC}" + +# Check if submodules are properly initialized and updated +if git submodule status | grep -qE '^-|\+'; then + echo -e "${YELLOW}Submodules are not properly initialized or updated. Run: git submodule update --init --recursive --force${NC}" + EXIT_CODE=1 +else + echo -e "${GREEN}All submodules are properly initialized and up to date.${NC}" +fi + +# Step 4: Check if Nitro Docker Image is built +if docker images | grep -q "nitro-node"; then + echo -e "${GREEN}Nitro Docker image is built.${NC}" +else + echo -e "${YELLOW}Nitro Docker image is not built. Build it using: docker build . --tag nitro-node${NC}" +fi + +# Step 5: Check prerequisites for building binaries +echo -e "${BLUE}Checking prerequisites for building Nitro's binaries...${NC}" +if [[ "$OS" == "Linux" ]]; then + prerequisites=(git curl make cmake npm golang clang make gotestsum wasm2wat wasm-ld python3 yarn) +else + prerequisites=(git curl make cmake npm go golangci-lint wasm2wat clang wasm-ld gotestsum yarn) +fi + +for pkg in "${prerequisites[@]}"; do + EXISTS=$(command_exists "$pkg") + [[ "$pkg" == "make" ]] && pkg="build-essential" + [[ "$pkg" == "wasm2wat" ]] && pkg="wabt" + [[ "$pkg" == "clang" ]] && pkg="llvm" + [[ "$pkg" == "wasm-ld" ]] && pkg="lld" + if $EXISTS; then + # There is no way to check for wabt / llvm directly, since they install multiple tools + # So instead, we check for wasm2wat and clang, which are part of wabt and llvm respectively + # and if they are installed, we assume wabt / llvm is installed else we ask the user to install wabt / llvm + + echo -e "${GREEN}$pkg is installed.${NC}" + else + echo -e "${RED}$pkg is not installed. Please install $pkg.${NC}" + EXIT_CODE=1 + fi +done + +# Step 6: Check Node.js version +if command_exists node && node -v | grep -q "v18"; then + echo -e "${GREEN}Node.js version 18 is installed.${NC}" +else + echo -e "${RED}Node.js version 18 not installed.${NC}" + EXIT_CODE=1 +fi + +# Step 7a: Check Rust version +if command_exists rustc && rustc --version | grep -q "1.80.1"; then + echo -e "${GREEN}Rust version 1.80.1 is installed.${NC}" +else + echo -e "${RED}Rust version 1.80.1 not installed.${NC}" + EXIT_CODE=1 +fi + +# Step 7b: Check Rust nightly toolchain +if rustup toolchain list | grep -q "nightly"; then + echo -e "${GREEN}Rust nightly toolchain is installed.${NC}" +else + echo -e "${RED}Rust nightly toolchain is not installed. Install it using: rustup toolchain install nightly${NC}" + EXIT_CODE=1 +fi + +# Step 8: Check Go version +go_version_needed=$(grep "^go " go.mod | awk '{print $2}') +if command_exists go && go version | grep -q "$go_version_needed"; then + echo -e "${GREEN}Go version $go_version_needed is installed.${NC}" +else + echo -e "${RED}Go version $go_version_needed not installed.${NC}" + EXIT_CODE=1 +fi + +# Step 9: Check Foundry installation +if command_exists foundryup; then + echo -e "${GREEN}Foundry is installed.${NC}" +else + echo -e "${RED}Foundry is not installed.${NC}" + EXIT_CODE=1 +fi + +echo -e "${BLUE}Verification complete.${NC}" + +if [ $EXIT_CODE != 0 ]; then + echo -e "${RED}One or more dependencies missing. $INSTALLATION_DOCS_URL${NC}" +fi + +exit $EXIT_CODE + diff --git a/staker/stateless_block_validator.go b/staker/stateless_block_validator.go index 9257c5582a..d9c9c5446b 100644 --- a/staker/stateless_block_validator.go +++ b/staker/stateless_block_validator.go @@ -511,12 +511,12 @@ func (v *StatelessBlockValidator) ValidateResult( return true, &entry.End, nil } -func (v *StatelessBlockValidator) ValidationInputsAt(ctx context.Context, pos arbutil.MessageIndex, target ethdb.WasmTarget) (server_api.InputJSON, error) { +func (v *StatelessBlockValidator) ValidationInputsAt(ctx context.Context, pos arbutil.MessageIndex, targets ...ethdb.WasmTarget) (server_api.InputJSON, error) { entry, err := v.CreateReadyValidationEntry(ctx, pos) if err != nil { return server_api.InputJSON{}, err } - input, err := entry.ToInput([]ethdb.WasmTarget{target}) + input, err := entry.ToInput(targets) if err != nil { return server_api.InputJSON{}, err } diff --git a/system_tests/common_test.go b/system_tests/common_test.go index 93c38b5eae..027a41d875 100644 --- a/system_tests/common_test.go +++ b/system_tests/common_test.go @@ -9,6 +9,7 @@ import ( "encoding/binary" "encoding/hex" "encoding/json" + "flag" "io" "math/big" "net" @@ -20,7 +21,6 @@ import ( "testing" "time" - "github.com/go-redis/redis/v8" "github.com/offchainlabs/nitro/arbos" "github.com/offchainlabs/nitro/arbos/arbostypes" "github.com/offchainlabs/nitro/arbos/util" @@ -42,6 +42,7 @@ import ( "github.com/offchainlabs/nitro/validator/server_common" "github.com/offchainlabs/nitro/validator/valnode" rediscons "github.com/offchainlabs/nitro/validator/valnode/redis" + "github.com/redis/go-redis/v9" "github.com/ethereum/go-ethereum" "github.com/ethereum/go-ethereum/accounts/abi" @@ -86,12 +87,13 @@ import ( type info = *BlockchainTestInfo type SecondNodeParams struct { - nodeConfig *arbnode.Config - execConfig *gethexec.Config - stackConfig *node.Config - dasConfig *das.DataAvailabilityConfig - initData *statetransfer.ArbosInitializationInfo - addresses *chaininfo.RollupAddresses + nodeConfig *arbnode.Config + execConfig *gethexec.Config + stackConfig *node.Config + dasConfig *das.DataAvailabilityConfig + initData *statetransfer.ArbosInitializationInfo + addresses *chaininfo.RollupAddresses + wasmCacheTag uint32 } type TestClient struct { @@ -155,19 +157,20 @@ func (tc *TestClient) EnsureTxSucceededWithTimeout(transaction *types.Transactio } var TestCachingConfig = gethexec.CachingConfig{ - Archive: false, - BlockCount: 128, - BlockAge: 30 * time.Minute, - TrieTimeLimit: time.Hour, - TrieDirtyCache: 1024, - TrieCleanCache: 600, - SnapshotCache: 400, - DatabaseCache: 2048, - SnapshotRestoreGasLimit: 300_000_000_000, - MaxNumberOfBlocksToSkipStateSaving: 0, - MaxAmountOfGasToSkipStateSaving: 0, - StylusLRUCacheCapacity: 0, - StateScheme: env.GetTestStateScheme(), + Archive: false, + BlockCount: 128, + BlockAge: 30 * time.Minute, + TrieTimeLimit: time.Hour, + TrieDirtyCache: 1024, + TrieCleanCache: 600, + SnapshotCache: 400, + DatabaseCache: 2048, + SnapshotRestoreGasLimit: 300_000_000_000, + MaxNumberOfBlocksToSkipStateSaving: 0, + MaxAmountOfGasToSkipStateSaving: 0, + StylusLRUCacheCapacity: 0, + DisableStylusCacheMetricsCollection: true, + StateScheme: env.GetTestStateScheme(), } var DefaultTestForwarderConfig = gethexec.ForwarderConfig{ @@ -248,6 +251,7 @@ type NodeBuilder struct { initMessage *arbostypes.ParsedInitMessage l3InitMessage *arbostypes.ParsedInitMessage withProdConfirmPeriodBlocks bool + wasmCacheTag uint32 // Created nodes L1 *TestClient @@ -351,6 +355,15 @@ func (b *NodeBuilder) WithExtraArchs(targets []string) *NodeBuilder { return b } +func (b *NodeBuilder) WithStylusLongTermCache(enabled bool) *NodeBuilder { + if enabled { + b.wasmCacheTag = 1 + } else { + b.wasmCacheTag = 0 + } + return b +} + func (b *NodeBuilder) Build(t *testing.T) func() { b.CheckConfig(t) if b.withL1 { @@ -424,6 +437,8 @@ func buildOnParentChain( initMessage *arbostypes.ParsedInitMessage, addresses *chaininfo.RollupAddresses, + + wasmCacheTag uint32, ) *TestClient { if parentChainTestClient == nil { t.Fatal("must build parent chain before building chain") @@ -435,7 +450,7 @@ func buildOnParentChain( var arbDb ethdb.Database var blockchain *core.BlockChain _, chainTestClient.Stack, chainDb, arbDb, blockchain = createNonL1BlockChainWithStackConfig( - t, chainInfo, dataDir, chainConfig, initMessage, stackConfig, execConfig) + t, chainInfo, dataDir, chainConfig, initMessage, stackConfig, execConfig, wasmCacheTag) var sequencerTxOptsPtr *bind.TransactOpts var dataSigner signature.DataSignerFunc @@ -523,6 +538,8 @@ func (b *NodeBuilder) BuildL3OnL2(t *testing.T) func() { b.l3InitMessage, b.l3Addresses, + + b.wasmCacheTag, ) return func() { @@ -551,6 +568,8 @@ func (b *NodeBuilder) BuildL2OnL1(t *testing.T) func() { b.initMessage, b.addresses, + + b.wasmCacheTag, ) return func() { @@ -572,7 +591,7 @@ func (b *NodeBuilder) BuildL2(t *testing.T) func() { var arbDb ethdb.Database var blockchain *core.BlockChain b.L2Info, b.L2.Stack, chainDb, arbDb, blockchain = createL2BlockChain( - t, b.L2Info, b.dataDir, b.chainConfig, b.execConfig) + t, b.L2Info, b.dataDir, b.chainConfig, b.execConfig, b.wasmCacheTag) Require(t, b.execConfig.Validate()) execConfig := b.execConfig @@ -623,7 +642,7 @@ func (b *NodeBuilder) RestartL2Node(t *testing.T) { } b.L2.cleanup() - l2info, stack, chainDb, arbDb, blockchain := createNonL1BlockChainWithStackConfig(t, b.L2Info, b.dataDir, b.chainConfig, b.initMessage, b.l2StackConfig, b.execConfig) + l2info, stack, chainDb, arbDb, blockchain := createNonL1BlockChainWithStackConfig(t, b.L2Info, b.dataDir, b.chainConfig, b.initMessage, b.l2StackConfig, b.execConfig, b.wasmCacheTag) execConfigFetcher := func() *gethexec.Config { return b.execConfig } execNode, err := gethexec.CreateExecutionNode(b.ctx, stack, chainDb, blockchain, nil, execConfigFetcher) @@ -700,7 +719,7 @@ func build2ndNode( testClient := NewTestClient(ctx) testClient.Client, testClient.ConsensusNode = - Create2ndNodeWithConfig(t, ctx, firstNodeTestClient.ConsensusNode, parentChainTestClient.Stack, parentChainInfo, params.initData, params.nodeConfig, params.execConfig, params.stackConfig, valnodeConfig, params.addresses, initMessage) + Create2ndNodeWithConfig(t, ctx, firstNodeTestClient.ConsensusNode, parentChainTestClient.Stack, parentChainInfo, params.initData, params.nodeConfig, params.execConfig, params.stackConfig, valnodeConfig, params.addresses, initMessage, params.wasmCacheTag) testClient.ExecNode = getExecNode(t, testClient.ConsensusNode) testClient.cleanup = func() { testClient.ConsensusNode.StopAndWait() } return testClient, func() { testClient.cleanup() } @@ -1286,13 +1305,13 @@ func deployOnParentChain( } func createL2BlockChain( - t *testing.T, l2info *BlockchainTestInfo, dataDir string, chainConfig *params.ChainConfig, execConfig *gethexec.Config, + t *testing.T, l2info *BlockchainTestInfo, dataDir string, chainConfig *params.ChainConfig, execConfig *gethexec.Config, wasmCacheTag uint32, ) (*BlockchainTestInfo, *node.Node, ethdb.Database, ethdb.Database, *core.BlockChain) { - return createNonL1BlockChainWithStackConfig(t, l2info, dataDir, chainConfig, nil, nil, execConfig) + return createNonL1BlockChainWithStackConfig(t, l2info, dataDir, chainConfig, nil, nil, execConfig, wasmCacheTag) } func createNonL1BlockChainWithStackConfig( - t *testing.T, info *BlockchainTestInfo, dataDir string, chainConfig *params.ChainConfig, initMessage *arbostypes.ParsedInitMessage, stackConfig *node.Config, execConfig *gethexec.Config, + t *testing.T, info *BlockchainTestInfo, dataDir string, chainConfig *params.ChainConfig, initMessage *arbostypes.ParsedInitMessage, stackConfig *node.Config, execConfig *gethexec.Config, wasmCacheTag uint32, ) (*BlockchainTestInfo, *node.Node, ethdb.Database, ethdb.Database, *core.BlockChain) { if info == nil { info = NewArbTestInfo(t, chainConfig.ChainID) @@ -1311,7 +1330,7 @@ func createNonL1BlockChainWithStackConfig( Require(t, err) wasmData, err := stack.OpenDatabaseWithExtraOptions("wasm", 0, 0, "wasm/", false, conf.PersistentConfigDefault.Pebble.ExtraOptions("wasm")) Require(t, err) - chainDb := rawdb.WrapDatabaseWithWasm(chainData, wasmData, 0, execConfig.StylusTarget.WasmTargets()) + chainDb := rawdb.WrapDatabaseWithWasm(chainData, wasmData, wasmCacheTag, execConfig.StylusTarget.WasmTargets()) arbDb, err := stack.OpenDatabaseWithExtraOptions("arbitrumdata", 0, 0, "arbitrumdata/", false, conf.PersistentConfigDefault.Pebble.ExtraOptions("arbitrumdata")) Require(t, err) @@ -1382,6 +1401,7 @@ func Create2ndNodeWithConfig( valnodeConfig *valnode.Config, addresses *chaininfo.RollupAddresses, initMessage *arbostypes.ParsedInitMessage, + wasmCacheTag uint32, ) (*ethclient.Client, *arbnode.Node) { if nodeConfig == nil { nodeConfig = arbnode.ConfigDefaultL1NonSequencerTest() @@ -1403,7 +1423,7 @@ func Create2ndNodeWithConfig( Require(t, err) wasmData, err := chainStack.OpenDatabaseWithExtraOptions("wasm", 0, 0, "wasm/", false, conf.PersistentConfigDefault.Pebble.ExtraOptions("wasm")) Require(t, err) - chainDb := rawdb.WrapDatabaseWithWasm(chainData, wasmData, 0, execConfig.StylusTarget.WasmTargets()) + chainDb := rawdb.WrapDatabaseWithWasm(chainData, wasmData, wasmCacheTag, execConfig.StylusTarget.WasmTargets()) arbDb, err := chainStack.OpenDatabaseWithExtraOptions("arbitrumdata", 0, 0, "arbitrumdata/", false, conf.PersistentConfigDefault.Pebble.ExtraOptions("arbitrumdata")) Require(t, err) @@ -1697,11 +1717,23 @@ func logParser[T any](t *testing.T, source string, name string) func(*types.Log) } } +var ( + recordBlockInputsEnable = flag.Bool("recordBlockInputs.enable", true, "Whether to record block inputs as a json file") + recordBlockInputsWithSlug = flag.String("recordBlockInputs.WithSlug", "", "Slug directory for validationInputsWriter") + recordBlockInputsWithBaseDir = flag.String("recordBlockInputs.WithBaseDir", "", "Base directory for validationInputsWriter") + recordBlockInputsWithTimestampDirEnabled = flag.Bool("recordBlockInputs.WithTimestampDirEnabled", true, "Whether to add timestamp directory while recording block inputs") + recordBlockInputsWithBlockIdInFileNameEnabled = flag.Bool("recordBlockInputs.WithBlockIdInFileNameEnabled", true, "Whether to record block inputs using test specific block_id") +) + // recordBlock writes a json file with all of the data needed to validate a block. // // This can be used as an input to the arbitrator prover to validate a block. -func recordBlock(t *testing.T, block uint64, builder *NodeBuilder) { +func recordBlock(t *testing.T, block uint64, builder *NodeBuilder, targets ...ethdb.WasmTarget) { t.Helper() + flag.Parse() + if !*recordBlockInputsEnable { + return + } ctx := builder.ctx inboxPos := arbutil.MessageIndex(block) for { @@ -1714,9 +1746,20 @@ func recordBlock(t *testing.T, block uint64, builder *NodeBuilder) { break } } - validationInputsWriter, err := inputs.NewWriter(inputs.WithSlug(t.Name())) + var options []inputs.WriterOption + options = append(options, inputs.WithTimestampDirEnabled(*recordBlockInputsWithTimestampDirEnabled)) + options = append(options, inputs.WithBlockIdInFileNameEnabled(*recordBlockInputsWithBlockIdInFileNameEnabled)) + if *recordBlockInputsWithBaseDir != "" { + options = append(options, inputs.WithBaseDir(*recordBlockInputsWithBaseDir)) + } + if *recordBlockInputsWithSlug != "" { + options = append(options, inputs.WithSlug(*recordBlockInputsWithSlug)) + } else { + options = append(options, inputs.WithSlug(t.Name())) + } + validationInputsWriter, err := inputs.NewWriter(options...) Require(t, err) - inputJson, err := builder.L2.ConsensusNode.StatelessBlockValidator.ValidationInputsAt(ctx, inboxPos, rawdb.TargetWavm) + inputJson, err := builder.L2.ConsensusNode.StatelessBlockValidator.ValidationInputsAt(ctx, inboxPos, targets...) if err != nil { Fatal(t, "failed to get validation inputs", block, err) } diff --git a/system_tests/debugapi_test.go b/system_tests/debugapi_test.go index 30a2bee03e..eb2bcd095d 100644 --- a/system_tests/debugapi_test.go +++ b/system_tests/debugapi_test.go @@ -43,7 +43,7 @@ func TestDebugAPI(t *testing.T) { arbSys, err := precompilesgen.NewArbSys(types.ArbSysAddress, builder.L2.Client) Require(t, err) auth := builder.L2Info.GetDefaultTransactOpts("Owner", ctx) - tx, err := arbSys.WithdrawEth(&auth, common.Address{}) + tx, err := arbSys.SendTxToL1(&auth, common.Address{}, []byte{}) Require(t, err) receipt, err := builder.L2.EnsureTxSucceeded(tx) Require(t, err) diff --git a/system_tests/fees_test.go b/system_tests/fees_test.go index ccca82e009..76de23e2cb 100644 --- a/system_tests/fees_test.go +++ b/system_tests/fees_test.go @@ -55,6 +55,12 @@ func TestSequencerFeePaid(t *testing.T) { l1Estimate, err := arbGasInfo.GetL1BaseFeeEstimate(callOpts) Require(t, err) + l1EstimateThroughGetL1GasPriceEstimate, err := arbGasInfo.GetL1GasPriceEstimate(callOpts) + Require(t, err) + if !arbmath.BigEquals(l1Estimate, l1EstimateThroughGetL1GasPriceEstimate) { + Fatal(t, "GetL1BaseFeeEstimate and GetL1GasPriceEstimate should return the same value") + } + baseFee := builder.L2.GetBaseFee(t) builder.L2Info.GasPrice = baseFee diff --git a/system_tests/forwarder_test.go b/system_tests/forwarder_test.go index 57381ca84e..6a1d1c68d8 100644 --- a/system_tests/forwarder_test.go +++ b/system_tests/forwarder_test.go @@ -170,7 +170,7 @@ func waitForSequencerLockout(ctx context.Context, node *arbnode.Node, duration t case <-time.After(duration): return fmt.Errorf("no sequencer was chosen") default: - if c, err := node.SeqCoordinator.CurrentChosenSequencer(ctx); err == nil && c != "" { + if c, err := node.SeqCoordinator.RedisCoordinator().CurrentChosenSequencer(ctx); err == nil && c != "" { return nil } time.Sleep(100 * time.Millisecond) diff --git a/system_tests/precompile_doesnt_revert_test.go b/system_tests/precompile_doesnt_revert_test.go new file mode 100644 index 0000000000..e6751d347d --- /dev/null +++ b/system_tests/precompile_doesnt_revert_test.go @@ -0,0 +1,248 @@ +// Copyright 2021-2024, Offchain Labs, Inc. +// For license information, see https://github.com/OffchainLabs/nitro/blob/master/LICENSE + +package arbtest + +import ( + "context" + "encoding/json" + "math/big" + "testing" + + "github.com/ethereum/go-ethereum/accounts/abi/bind" + "github.com/ethereum/go-ethereum/common" + "github.com/ethereum/go-ethereum/core/types" + "github.com/ethereum/go-ethereum/crypto" + "github.com/ethereum/go-ethereum/params" + "github.com/offchainlabs/nitro/arbos/l1pricing" + "github.com/offchainlabs/nitro/solgen/go/precompilesgen" +) + +// DoesntRevert tests are useful to check if precompile calls revert due to differences in the +// return types of a contract between go and solidity. +// They are not a substitute for unit tests, as they don't test the actual functionality of the precompile. + +func TestArbAddressTableDoesntRevert(t *testing.T) { + t.Parallel() + + ctx, cancel := context.WithCancel(context.Background()) + defer cancel() + + builder := NewNodeBuilder(ctx).DefaultConfig(t, false) + cleanup := builder.Build(t) + defer cleanup() + + auth := builder.L2Info.GetDefaultTransactOpts("Owner", ctx) + callOpts := &bind.CallOpts{Context: ctx} + + arbAddressTable, err := precompilesgen.NewArbAddressTable(types.ArbAddressTableAddress, builder.L2.Client) + Require(t, err) + + addr := common.BytesToAddress(crypto.Keccak256([]byte{})[:20]) + + exists, err := arbAddressTable.AddressExists(callOpts, addr) + Require(t, err) + if exists { + Fatal(t, "expected address to not exist") + } + + tx, err := arbAddressTable.Register(&auth, addr) + Require(t, err) + _, err = builder.L2.EnsureTxSucceeded(tx) + Require(t, err) + + idx, err := arbAddressTable.Lookup(callOpts, addr) + Require(t, err) + + retrievedAddr, err := arbAddressTable.LookupIndex(callOpts, idx) + Require(t, err) + if retrievedAddr.Cmp(addr) != 0 { + Fatal(t, "expected retrieved address to be", addr, "got", retrievedAddr) + } + + size, err := arbAddressTable.Size(callOpts) + Require(t, err) + if size.Cmp(big.NewInt(1)) != 0 { + Fatal(t, "expected size to be 1, got", size) + } + + tx, err = arbAddressTable.Compress(&auth, addr) + Require(t, err) + _, err = builder.L2.EnsureTxSucceeded(tx) + Require(t, err) + + res := []uint8{128} + _, _, err = arbAddressTable.Decompress(callOpts, res, big.NewInt(0)) + Require(t, err) +} + +func TestArbAggregatorDoesntRevert(t *testing.T) { + t.Parallel() + + ctx, cancel := context.WithCancel(context.Background()) + defer cancel() + + builder := NewNodeBuilder(ctx).DefaultConfig(t, false) + cleanup := builder.Build(t) + defer cleanup() + + auth := builder.L2Info.GetDefaultTransactOpts("Owner", ctx) + callOpts := &bind.CallOpts{Context: ctx} + + arbAggregator, err := precompilesgen.NewArbAggregator(types.ArbAggregatorAddress, builder.L2.Client) + Require(t, err) + + tx, err := arbAggregator.SetFeeCollector(&auth, l1pricing.BatchPosterAddress, common.Address{}) + Require(t, err) + _, err = builder.L2.EnsureTxSucceeded(tx) + Require(t, err) + + _, err = arbAggregator.GetFeeCollector(callOpts, l1pricing.BatchPosterAddress) + Require(t, err) +} + +func TestArbosTestDoesntRevert(t *testing.T) { + t.Parallel() + + ctx, cancel := context.WithCancel(context.Background()) + defer cancel() + + builder := NewNodeBuilder(ctx).DefaultConfig(t, false) + cleanup := builder.Build(t) + defer cleanup() + + callOpts := &bind.CallOpts{Context: ctx} + + arbosTest, err := precompilesgen.NewArbosTest(types.ArbosTestAddress, builder.L2.Client) + Require(t, err) + + err = arbosTest.BurnArbGas(callOpts, big.NewInt(1)) + Require(t, err) +} + +func TestArbSysDoesntRevert(t *testing.T) { + t.Parallel() + + ctx, cancel := context.WithCancel(context.Background()) + defer cancel() + + builder := NewNodeBuilder(ctx).DefaultConfig(t, false) + cleanup := builder.Build(t) + defer cleanup() + + callOpts := &bind.CallOpts{Context: ctx} + + arbSys, err := precompilesgen.NewArbSys(types.ArbSysAddress, builder.L2.Client) + Require(t, err) + + addr1 := common.BytesToAddress(crypto.Keccak256([]byte{})[:20]) + addr2 := common.BytesToAddress(crypto.Keccak256([]byte{})[:20]) + _, err = arbSys.MapL1SenderContractAddressToL2Alias(callOpts, addr1, addr2) + Require(t, err) +} + +func TestArbOwnerDoesntRevert(t *testing.T) { + t.Parallel() + + ctx, cancel := context.WithCancel(context.Background()) + defer cancel() + + builder := NewNodeBuilder(ctx).DefaultConfig(t, false) + cleanup := builder.Build(t) + defer cleanup() + + auth := builder.L2Info.GetDefaultTransactOpts("Owner", ctx) + + arbOwner, err := precompilesgen.NewArbOwner(types.ArbOwnerAddress, builder.L2.Client) + Require(t, err) + + chainConfig := params.ArbitrumDevTestChainConfig() + chainConfig.ArbitrumChainParams.MaxCodeSize = 100 + serializedChainConfig, err := json.Marshal(chainConfig) + Require(t, err) + tx, err := arbOwner.SetChainConfig(&auth, string(serializedChainConfig)) + Require(t, err) + _, err = builder.L2.EnsureTxSucceeded(tx) + Require(t, err) + + tx, err = arbOwner.SetAmortizedCostCapBips(&auth, 77734) + Require(t, err) + _, err = builder.L2.EnsureTxSucceeded(tx) + Require(t, err) + + tx, err = arbOwner.ReleaseL1PricerSurplusFunds(&auth, big.NewInt(1)) + Require(t, err) + _, err = builder.L2.EnsureTxSucceeded(tx) + Require(t, err) + + tx, err = arbOwner.SetL2BaseFee(&auth, big.NewInt(1)) + Require(t, err) + _, err = builder.L2.EnsureTxSucceeded(tx) + Require(t, err) +} + +func TestArbGasInfoDoesntRevert(t *testing.T) { + t.Parallel() + + ctx, cancel := context.WithCancel(context.Background()) + defer cancel() + + builder := NewNodeBuilder(ctx).DefaultConfig(t, false) + cleanup := builder.Build(t) + defer cleanup() + + callOpts := &bind.CallOpts{Context: ctx} + addr := common.BytesToAddress(crypto.Keccak256([]byte{})[:20]) + + arbGasInfo, err := precompilesgen.NewArbGasInfo(types.ArbGasInfoAddress, builder.L2.Client) + Require(t, err) + + _, err = arbGasInfo.GetGasBacklog(callOpts) + Require(t, err) + + _, err = arbGasInfo.GetLastL1PricingUpdateTime(callOpts) + Require(t, err) + + _, err = arbGasInfo.GetL1PricingFundsDueForRewards(callOpts) + Require(t, err) + + _, err = arbGasInfo.GetL1PricingUnitsSinceUpdate(callOpts) + Require(t, err) + + _, err = arbGasInfo.GetLastL1PricingSurplus(callOpts) + Require(t, err) + + _, _, _, err = arbGasInfo.GetPricesInArbGas(callOpts) + Require(t, err) + + _, _, _, err = arbGasInfo.GetPricesInArbGasWithAggregator(callOpts, addr) + Require(t, err) + + _, err = arbGasInfo.GetAmortizedCostCapBips(callOpts) + Require(t, err) + + _, err = arbGasInfo.GetL1FeesAvailable(callOpts) + Require(t, err) + + _, _, _, _, _, _, err = arbGasInfo.GetPricesInWeiWithAggregator(callOpts, addr) + Require(t, err) +} + +func TestArbRetryableTxDoesntRevert(t *testing.T) { + t.Parallel() + + ctx, cancel := context.WithCancel(context.Background()) + defer cancel() + + builder := NewNodeBuilder(ctx).DefaultConfig(t, false) + cleanup := builder.Build(t) + defer cleanup() + + callOpts := &bind.CallOpts{Context: ctx} + + arbRetryableTx, err := precompilesgen.NewArbRetryableTx(common.HexToAddress("6e"), builder.L2.Client) + Require(t, err) + + _, err = arbRetryableTx.GetCurrentRedeemer(callOpts) + Require(t, err) +} diff --git a/system_tests/precompile_test.go b/system_tests/precompile_test.go index 9e829124ee..9d5737c249 100644 --- a/system_tests/precompile_test.go +++ b/system_tests/precompile_test.go @@ -7,12 +7,16 @@ import ( "context" "fmt" "math/big" + "sort" "testing" "github.com/ethereum/go-ethereum/accounts/abi/bind" "github.com/ethereum/go-ethereum/common" + "github.com/ethereum/go-ethereum/core/types" + "github.com/ethereum/go-ethereum/crypto" "github.com/ethereum/go-ethereum/params" "github.com/offchainlabs/nitro/arbos" + "github.com/offchainlabs/nitro/arbos/l1pricing" "github.com/offchainlabs/nitro/solgen/go/mocksgen" "github.com/offchainlabs/nitro/solgen/go/precompilesgen" "github.com/offchainlabs/nitro/util/arbmath" @@ -22,7 +26,10 @@ func TestPurePrecompileMethodCalls(t *testing.T) { ctx, cancel := context.WithCancel(context.Background()) defer cancel() - builder := NewNodeBuilder(ctx).DefaultConfig(t, false) + arbosVersion := uint64(31) + builder := NewNodeBuilder(ctx). + DefaultConfig(t, false). + WithArbOSVersion(arbosVersion) cleanup := builder.Build(t) defer cleanup() @@ -33,6 +40,19 @@ func TestPurePrecompileMethodCalls(t *testing.T) { if chainId.Uint64() != params.ArbitrumDevTestChainConfig().ChainID.Uint64() { Fatal(t, "Wrong ChainID", chainId.Uint64()) } + + expectedArbosVersion := 55 + arbosVersion // Nitro versions start at 56 + arbSysArbosVersion, err := arbSys.ArbOSVersion(&bind.CallOpts{}) + Require(t, err) + if arbSysArbosVersion.Uint64() != expectedArbosVersion { + Fatal(t, "Expected ArbOS version", expectedArbosVersion, "got", arbSysArbosVersion) + } + + storageGasAvailable, err := arbSys.GetStorageGasAvailable(&bind.CallOpts{}) + Require(t, err) + if storageGasAvailable.Cmp(big.NewInt(0)) != 0 { + Fatal(t, "Expected 0 storage gas available, got", storageGasAvailable) + } } func TestViewLogReverts(t *testing.T) { @@ -52,7 +72,29 @@ func TestViewLogReverts(t *testing.T) { } } -func TestCustomSolidityErrors(t *testing.T) { +func TestArbDebugPanic(t *testing.T) { + ctx, cancel := context.WithCancel(context.Background()) + defer cancel() + + builder := NewNodeBuilder(ctx).DefaultConfig(t, false) + cleanup := builder.Build(t) + defer cleanup() + + auth := builder.L2Info.GetDefaultTransactOpts("Owner", ctx) + + arbDebug, err := precompilesgen.NewArbDebug(types.ArbDebugAddress, builder.L2.Client) + Require(t, err) + + _, err = arbDebug.Panic(&auth) + if err == nil { + Fatal(t, "unexpected success") + } + if err.Error() != "method handler crashed" { + Fatal(t, "expected method handler to crash") + } +} + +func TestArbDebugLegacyError(t *testing.T) { ctx, cancel := context.WithCancel(context.Background()) defer cancel() @@ -61,32 +103,97 @@ func TestCustomSolidityErrors(t *testing.T) { defer cleanup() callOpts := &bind.CallOpts{Context: ctx} + arbDebug, err := precompilesgen.NewArbDebug(common.HexToAddress("0xff"), builder.L2.Client) - Require(t, err, "could not bind ArbDebug contract") - customError := arbDebug.CustomRevert(callOpts, 1024) - if customError == nil { - Fatal(t, "customRevert call should have errored") + Require(t, err) + + err = arbDebug.LegacyError(callOpts) + if err == nil { + Fatal(t, "unexpected success") } - observedMessage := customError.Error() - expectedError := "Custom(1024, This spider family wards off bugs: /\\oo/\\ //\\(oo)//\\ /\\oo/\\, true)" - // The first error is server side. The second error is client side ABI decoding. - expectedMessage := fmt.Sprintf("execution reverted: error %v: %v", expectedError, expectedError) - if observedMessage != expectedMessage { - Fatal(t, observedMessage) +} + +func TestCustomSolidityErrors(t *testing.T) { + ctx, cancel := context.WithCancel(context.Background()) + defer cancel() + + builder := NewNodeBuilder(ctx).DefaultConfig(t, false) + cleanup := builder.Build(t) + defer cleanup() + + callOpts := &bind.CallOpts{Context: ctx} + auth := builder.L2Info.GetDefaultTransactOpts("Owner", ctx) + + ensure := func( + customError error, + expectedError string, + scenario string, + ) { + if customError == nil { + Fatal(t, "should have errored", "scenario", scenario) + } + observedMessage := customError.Error() + // The first error is server side. The second error is client side ABI decoding. + expectedMessage := fmt.Sprintf("execution reverted: error %v: %v", expectedError, expectedError) + if observedMessage != expectedMessage { + Fatal(t, observedMessage, "scenario", scenario) + } } + arbDebug, err := precompilesgen.NewArbDebug(types.ArbDebugAddress, builder.L2.Client) + Require(t, err, "could not bind ArbDebug contract") + ensure( + arbDebug.CustomRevert(callOpts, 1024), + "Custom(1024, This spider family wards off bugs: /\\oo/\\ //\\(oo)//\\ /\\oo/\\, true)", + "arbDebug.CustomRevert", + ) + arbSys, err := precompilesgen.NewArbSys(arbos.ArbSysAddress, builder.L2.Client) Require(t, err, "could not bind ArbSys contract") - _, customError = arbSys.ArbBlockHash(callOpts, big.NewInt(1e9)) - if customError == nil { - Fatal(t, "out of range ArbBlockHash call should have errored") - } - observedMessage = customError.Error() - expectedError = "InvalidBlockNumber(1000000000, 1)" - expectedMessage = fmt.Sprintf("execution reverted: error %v: %v", expectedError, expectedError) - if observedMessage != expectedMessage { - Fatal(t, observedMessage) - } + _, customError := arbSys.ArbBlockHash(callOpts, big.NewInt(1e9)) + ensure( + customError, + "InvalidBlockNumber(1000000000, 1)", + "arbSys.ArbBlockHash", + ) + + arbRetryableTx, err := precompilesgen.NewArbRetryableTx(types.ArbRetryableTxAddress, builder.L2.Client) + Require(t, err) + _, customError = arbRetryableTx.SubmitRetryable( + &auth, + [32]byte{}, + big.NewInt(0), + big.NewInt(0), + big.NewInt(0), + big.NewInt(0), + 0, + big.NewInt(0), + common.Address{}, + common.Address{}, + common.Address{}, + []byte{}, + ) + ensure( + customError, + "NotCallable()", + "arbRetryableTx.SubmitRetryable", + ) + + arbosActs, err := precompilesgen.NewArbosActs(types.ArbosAddress, builder.L2.Client) + Require(t, err) + _, customError = arbosActs.StartBlock(&auth, big.NewInt(0), 0, 0, 0) + ensure( + customError, + "CallerNotArbOS()", + "arbosActs.StartBlock", + ) + + _, customError = arbosActs.BatchPostingReport(&auth, big.NewInt(0), common.Address{}, 0, 0, big.NewInt(0)) + ensure( + customError, + "CallerNotArbOS()", + "arbosActs.BatchPostingReport", + ) } func TestPrecompileErrorGasLeft(t *testing.T) { @@ -125,6 +232,274 @@ func TestPrecompileErrorGasLeft(t *testing.T) { assertNotAllGasConsumed(common.HexToAddress("0xff"), arbDebug.Methods["legacyError"].ID) } +func setupArbOwnerAndArbGasInfo( + t *testing.T, +) ( + *NodeBuilder, + func(), + bind.TransactOpts, + *precompilesgen.ArbOwner, + *precompilesgen.ArbGasInfo, +) { + ctx, cancel := context.WithCancel(context.Background()) + + builder := NewNodeBuilder(ctx).DefaultConfig(t, false) + builderCleanup := builder.Build(t) + + cleanup := func() { + builderCleanup() + cancel() + } + + auth := builder.L2Info.GetDefaultTransactOpts("Owner", ctx) + + arbOwner, err := precompilesgen.NewArbOwner(common.HexToAddress("0x70"), builder.L2.Client) + Require(t, err) + arbGasInfo, err := precompilesgen.NewArbGasInfo(common.HexToAddress("0x6c"), builder.L2.Client) + Require(t, err) + + return builder, cleanup, auth, arbOwner, arbGasInfo +} + +func TestL1BaseFeeEstimateInertia(t *testing.T) { + t.Parallel() + + builder, cleanup, auth, arbOwner, arbGasInfo := setupArbOwnerAndArbGasInfo(t) + defer cleanup() + ctx := builder.ctx + + inertia := uint64(11) + tx, err := arbOwner.SetL1BaseFeeEstimateInertia(&auth, inertia) + Require(t, err) + _, err = builder.L2.EnsureTxSucceeded(tx) + Require(t, err) + arbGasInfoInertia, err := arbGasInfo.GetL1BaseFeeEstimateInertia(&bind.CallOpts{Context: ctx}) + Require(t, err) + if arbGasInfoInertia != inertia { + Fatal(t, "expected inertia to be", inertia, "got", arbGasInfoInertia) + } +} + +// Similar to TestL1BaseFeeEstimateInertia, but now using a different setter from ArbOwner +func TestL1PricingInertia(t *testing.T) { + t.Parallel() + + builder, cleanup, auth, arbOwner, arbGasInfo := setupArbOwnerAndArbGasInfo(t) + defer cleanup() + ctx := builder.ctx + + inertia := uint64(12) + tx, err := arbOwner.SetL1PricingInertia(&auth, inertia) + Require(t, err) + _, err = builder.L2.EnsureTxSucceeded(tx) + Require(t, err) + arbGasInfoInertia, err := arbGasInfo.GetL1BaseFeeEstimateInertia(&bind.CallOpts{Context: ctx}) + Require(t, err) + if arbGasInfoInertia != inertia { + Fatal(t, "expected inertia to be", inertia, "got", arbGasInfoInertia) + } +} + +func TestL1PricingRewardRate(t *testing.T) { + t.Parallel() + + builder, cleanup, auth, arbOwner, arbGasInfo := setupArbOwnerAndArbGasInfo(t) + defer cleanup() + ctx := builder.ctx + + perUnitReward := uint64(13) + tx, err := arbOwner.SetL1PricingRewardRate(&auth, perUnitReward) + Require(t, err) + _, err = builder.L2.EnsureTxSucceeded(tx) + Require(t, err) + arbGasInfoPerUnitReward, err := arbGasInfo.GetL1RewardRate(&bind.CallOpts{Context: ctx}) + Require(t, err) + if arbGasInfoPerUnitReward != perUnitReward { + Fatal(t, "expected per unit reward to be", perUnitReward, "got", arbGasInfoPerUnitReward) + } +} + +func TestL1PricingRewardRecipient(t *testing.T) { + t.Parallel() + + builder, cleanup, auth, arbOwner, arbGasInfo := setupArbOwnerAndArbGasInfo(t) + defer cleanup() + ctx := builder.ctx + + rewardRecipient := common.BytesToAddress(crypto.Keccak256([]byte{})[:20]) + tx, err := arbOwner.SetL1PricingRewardRecipient(&auth, rewardRecipient) + Require(t, err) + _, err = builder.L2.EnsureTxSucceeded(tx) + Require(t, err) + arbGasInfoRewardRecipient, err := arbGasInfo.GetL1RewardRecipient(&bind.CallOpts{Context: ctx}) + Require(t, err) + if arbGasInfoRewardRecipient.Cmp(rewardRecipient) != 0 { + Fatal(t, "expected reward recipient to be", rewardRecipient, "got", arbGasInfoRewardRecipient) + } +} + +func TestL2GasPricingInertia(t *testing.T) { + t.Parallel() + + builder, cleanup, auth, arbOwner, arbGasInfo := setupArbOwnerAndArbGasInfo(t) + defer cleanup() + ctx := builder.ctx + + inertia := uint64(14) + tx, err := arbOwner.SetL2GasPricingInertia(&auth, inertia) + Require(t, err) + _, err = builder.L2.EnsureTxSucceeded(tx) + Require(t, err) + arbGasInfoInertia, err := arbGasInfo.GetPricingInertia(&bind.CallOpts{Context: ctx}) + Require(t, err) + if arbGasInfoInertia != inertia { + Fatal(t, "expected inertia to be", inertia, "got", arbGasInfoInertia) + } +} + +func TestL2GasBacklogTolerance(t *testing.T) { + t.Parallel() + + builder, cleanup, auth, arbOwner, arbGasInfo := setupArbOwnerAndArbGasInfo(t) + defer cleanup() + ctx := builder.ctx + + gasTolerance := uint64(15) + tx, err := arbOwner.SetL2GasBacklogTolerance(&auth, gasTolerance) + Require(t, err) + _, err = builder.L2.EnsureTxSucceeded(tx) + Require(t, err) + arbGasInfoGasTolerance, err := arbGasInfo.GetGasBacklogTolerance(&bind.CallOpts{Context: ctx}) + Require(t, err) + if arbGasInfoGasTolerance != gasTolerance { + Fatal(t, "expected gas tolerance to be", gasTolerance, "got", arbGasInfoGasTolerance) + } +} + +func TestPerBatchGasCharge(t *testing.T) { + t.Parallel() + + builder, cleanup, auth, arbOwner, arbGasInfo := setupArbOwnerAndArbGasInfo(t) + defer cleanup() + ctx := builder.ctx + + perBatchGasCharge := int64(16) + tx, err := arbOwner.SetPerBatchGasCharge(&auth, perBatchGasCharge) + Require(t, err) + _, err = builder.L2.EnsureTxSucceeded(tx) + Require(t, err) + arbGasInfoPerBatchGasCharge, err := arbGasInfo.GetPerBatchGasCharge(&bind.CallOpts{Context: ctx}) + Require(t, err) + if arbGasInfoPerBatchGasCharge != perBatchGasCharge { + Fatal(t, "expected per batch gas charge to be", perBatchGasCharge, "got", arbGasInfoPerBatchGasCharge) + } +} + +func TestL1PricingEquilibrationUnits(t *testing.T) { + t.Parallel() + + builder, cleanup, auth, arbOwner, arbGasInfo := setupArbOwnerAndArbGasInfo(t) + defer cleanup() + ctx := builder.ctx + + equilUnits := big.NewInt(17) + tx, err := arbOwner.SetL1PricingEquilibrationUnits(&auth, equilUnits) + Require(t, err) + _, err = builder.L2.EnsureTxSucceeded(tx) + Require(t, err) + arbGasInfoEquilUnits, err := arbGasInfo.GetL1PricingEquilibrationUnits(&bind.CallOpts{Context: ctx}) + Require(t, err) + if arbGasInfoEquilUnits.Cmp(equilUnits) != 0 { + Fatal(t, "expected equilibration units to be", equilUnits, "got", arbGasInfoEquilUnits) + } +} + +func TestGasAccountingParams(t *testing.T) { + t.Parallel() + + builder, cleanup, auth, arbOwner, arbGasInfo := setupArbOwnerAndArbGasInfo(t) + defer cleanup() + ctx := builder.ctx + + speedLimit := uint64(18) + txGasLimit := uint64(19) + tx, err := arbOwner.SetSpeedLimit(&auth, speedLimit) + Require(t, err) + _, err = builder.L2.EnsureTxSucceeded(tx) + Require(t, err) + tx, err = arbOwner.SetMaxTxGasLimit(&auth, txGasLimit) + Require(t, err) + _, err = builder.L2.EnsureTxSucceeded(tx) + Require(t, err) + arbGasInfoSpeedLimit, arbGasInfoPoolSize, arbGasInfoTxGasLimit, err := arbGasInfo.GetGasAccountingParams(&bind.CallOpts{Context: ctx}) + Require(t, err) + if arbGasInfoSpeedLimit.Cmp(big.NewInt(int64(speedLimit))) != 0 { + Fatal(t, "expected speed limit to be", speedLimit, "got", arbGasInfoSpeedLimit) + } + if arbGasInfoPoolSize.Cmp(big.NewInt(int64(txGasLimit))) != 0 { + Fatal(t, "expected pool size to be", txGasLimit, "got", arbGasInfoPoolSize) + } + if arbGasInfoTxGasLimit.Cmp(big.NewInt(int64(txGasLimit))) != 0 { + Fatal(t, "expected tx gas limit to be", txGasLimit, "got", arbGasInfoTxGasLimit) + } +} + +func TestCurrentTxL1GasFees(t *testing.T) { + t.Parallel() + + ctx, cancel := context.WithCancel(context.Background()) + defer cancel() + + builder := NewNodeBuilder(ctx).DefaultConfig(t, false) + cleanup := builder.Build(t) + defer cleanup() + + arbGasInfo, err := precompilesgen.NewArbGasInfo(types.ArbGasInfoAddress, builder.L2.Client) + Require(t, err) + + currTxL1GasFees, err := arbGasInfo.GetCurrentTxL1GasFees(&bind.CallOpts{Context: ctx}) + Require(t, err) + if currTxL1GasFees == nil { + Fatal(t, "currTxL1GasFees is nil") + } + if currTxL1GasFees.Cmp(big.NewInt(0)) != 1 { + Fatal(t, "expected currTxL1GasFees to be greater than 0, got", currTxL1GasFees) + } +} + +func TestGetBrotliCompressionLevel(t *testing.T) { + ctx, cancel := context.WithCancel(context.Background()) + defer cancel() + + builder := NewNodeBuilder(ctx).DefaultConfig(t, false) + cleanup := builder.Build(t) + defer cleanup() + + auth := builder.L2Info.GetDefaultTransactOpts("Owner", ctx) + + arbOwnerPublic, err := precompilesgen.NewArbOwnerPublic(types.ArbOwnerPublicAddress, builder.L2.Client) + Require(t, err) + + arbOwner, err := precompilesgen.NewArbOwner(types.ArbOwnerAddress, builder.L2.Client) + Require(t, err) + + brotliCompressionLevel := uint64(11) + + // sets brotli compression level + tx, err := arbOwner.SetBrotliCompressionLevel(&auth, brotliCompressionLevel) + Require(t, err) + _, err = builder.L2.EnsureTxSucceeded(tx) + Require(t, err) + + // retrieves brotli compression level + callOpts := &bind.CallOpts{Context: ctx} + retrievedBrotliCompressionLevel, err := arbOwnerPublic.GetBrotliCompressionLevel(callOpts) + Require(t, err) + if retrievedBrotliCompressionLevel != brotliCompressionLevel { + Fatal(t, "expected brotli compression level to be", brotliCompressionLevel, "got", retrievedBrotliCompressionLevel) + } +} + func TestScheduleArbosUpgrade(t *testing.T) { ctx, cancel := context.WithCancel(context.Background()) defer cancel() @@ -175,3 +550,291 @@ func TestScheduleArbosUpgrade(t *testing.T) { t.Errorf("expected upgrade to be scheduled for version %v timestamp %v, got version %v timestamp %v", testVersion, testTimestamp, scheduled.ArbosVersion, scheduled.ScheduledForTimestamp) } } + +func TestArbStatistics(t *testing.T) { + t.Parallel() + + ctx, cancel := context.WithCancel(context.Background()) + defer cancel() + + builder := NewNodeBuilder(ctx).DefaultConfig(t, false) + cleanup := builder.Build(t) + defer cleanup() + + arbStatistics, err := precompilesgen.NewArbStatistics(types.ArbStatisticsAddress, builder.L2.Client) + Require(t, err) + + callOpts := &bind.CallOpts{Context: ctx} + blockNum, _, _, _, _, _, err := arbStatistics.GetStats(callOpts) + Require(t, err) + + expectedBlockNum, err := builder.L2.Client.BlockNumber(ctx) + Require(t, err) + + if blockNum.Uint64() != expectedBlockNum { + Fatal(t, "expected block number to be", expectedBlockNum, "got", blockNum) + } +} + +func TestArbFunctionTable(t *testing.T) { + t.Parallel() + + ctx, cancel := context.WithCancel(context.Background()) + defer cancel() + + builder := NewNodeBuilder(ctx).DefaultConfig(t, false) + cleanup := builder.Build(t) + defer cleanup() + + auth := builder.L2Info.GetDefaultTransactOpts("Owner", ctx) + callOpts := &bind.CallOpts{Context: ctx} + + arbFunctionTable, err := precompilesgen.NewArbFunctionTable(types.ArbFunctionTableAddress, builder.L2.Client) + Require(t, err) + + addr := common.BytesToAddress(crypto.Keccak256([]byte{})[:20]) + + // should be a noop + tx, err := arbFunctionTable.Upload(&auth, []byte{0, 0, 0, 0}) + Require(t, err) + _, err = builder.L2.EnsureTxSucceeded(tx) + Require(t, err) + + size, err := arbFunctionTable.Size(callOpts, addr) + Require(t, err) + if size.Cmp(big.NewInt(0)) != 0 { + t.Fatal("Size should be 0") + } + + _, _, _, err = arbFunctionTable.Get(callOpts, addr, big.NewInt(10)) + if err == nil { + t.Fatal("Should error") + } +} + +func TestArbAggregatorBaseFee(t *testing.T) { + t.Parallel() + + ctx, cancel := context.WithCancel(context.Background()) + defer cancel() + + builder := NewNodeBuilder(ctx).DefaultConfig(t, false) + cleanup := builder.Build(t) + defer cleanup() + + auth := builder.L2Info.GetDefaultTransactOpts("Owner", ctx) + callOpts := &bind.CallOpts{Context: ctx} + + arbAggregator, err := precompilesgen.NewArbAggregator(types.ArbAggregatorAddress, builder.L2.Client) + Require(t, err) + + tx, err := arbAggregator.SetTxBaseFee(&auth, common.Address{}, big.NewInt(1)) + Require(t, err) + _, err = builder.L2.EnsureTxSucceeded(tx) + Require(t, err) + + fee, err := arbAggregator.GetTxBaseFee(callOpts, common.Address{}) + Require(t, err) + if fee.Cmp(big.NewInt(0)) != 0 { + Fatal(t, "expected fee to be 0, got", fee) + } +} + +func TestFeeAccounts(t *testing.T) { + t.Parallel() + + ctx, cancel := context.WithCancel(context.Background()) + defer cancel() + + builder := NewNodeBuilder(ctx).DefaultConfig(t, false) + cleanup := builder.Build(t) + defer cleanup() + + auth := builder.L2Info.GetDefaultTransactOpts("Owner", ctx) + callOpts := &bind.CallOpts{Context: ctx} + + arbOwner, err := precompilesgen.NewArbOwner(types.ArbOwnerAddress, builder.L2.Client) + Require(t, err) + + builder.L2Info.GenerateAccount("User2") + addr := builder.L2Info.GetAddress("User2") + + tx, err := arbOwner.SetNetworkFeeAccount(&auth, addr) + Require(t, err) + _, err = builder.L2.EnsureTxSucceeded(tx) + Require(t, err) + + feeAccount, err := arbOwner.GetNetworkFeeAccount(callOpts) + Require(t, err) + if feeAccount.Cmp(addr) != 0 { + Fatal(t, "expected fee account to be", addr, "got", feeAccount) + } + + tx, err = arbOwner.SetInfraFeeAccount(&auth, addr) + Require(t, err) + _, err = builder.L2.EnsureTxSucceeded(tx) + Require(t, err) + + feeAccount, err = arbOwner.GetInfraFeeAccount(callOpts) + Require(t, err) + if feeAccount.Cmp(addr) != 0 { + Fatal(t, "expected fee account to be", addr, "got", feeAccount) + } +} + +func TestChainOwners(t *testing.T) { + t.Parallel() + + ctx, cancel := context.WithCancel(context.Background()) + defer cancel() + + builder := NewNodeBuilder(ctx).DefaultConfig(t, false) + cleanup := builder.Build(t) + defer cleanup() + + auth := builder.L2Info.GetDefaultTransactOpts("Owner", ctx) + callOpts := &bind.CallOpts{Context: ctx} + + arbOwnerPublic, err := precompilesgen.NewArbOwnerPublic(types.ArbOwnerPublicAddress, builder.L2.Client) + Require(t, err) + arbOwner, err := precompilesgen.NewArbOwner(types.ArbOwnerAddress, builder.L2.Client) + Require(t, err) + + builder.L2Info.GenerateAccount("Owner2") + chainOwnerAddr2 := builder.L2Info.GetAddress("Owner2") + tx, err := arbOwner.AddChainOwner(&auth, chainOwnerAddr2) + Require(t, err) + _, err = builder.L2.EnsureTxSucceeded(tx) + Require(t, err) + isChainOwner, err := arbOwnerPublic.IsChainOwner(callOpts, chainOwnerAddr2) + Require(t, err) + if !isChainOwner { + Fatal(t, "expected owner2 to be a chain owner") + } + + // check that the chain owners retrieved from arbOwnerPublic and arbOwner are the same + chainOwnersArbOwnerPublic, err := arbOwnerPublic.GetAllChainOwners(callOpts) + Require(t, err) + chainOwnersArbOwner, err := arbOwner.GetAllChainOwners(callOpts) + Require(t, err) + if len(chainOwnersArbOwnerPublic) != len(chainOwnersArbOwner) { + Fatal(t, "expected chain owners to be the same length") + } + // sort the chain owners to ensure they are in the same order + sort.Slice(chainOwnersArbOwnerPublic, func(i, j int) bool { + return chainOwnersArbOwnerPublic[i].Cmp(chainOwnersArbOwnerPublic[j]) < 0 + }) + for i := 0; i < len(chainOwnersArbOwnerPublic); i += 1 { + if chainOwnersArbOwnerPublic[i].Cmp(chainOwnersArbOwner[i]) != 0 { + Fatal(t, "expected chain owners to be the same") + } + } + chainOwnerAddr := builder.L2Info.GetAddress("Owner") + chainOwnerInChainOwners := false + for _, chainOwner := range chainOwnersArbOwner { + if chainOwner.Cmp(chainOwnerAddr) == 0 { + chainOwnerInChainOwners = true + } + } + if !chainOwnerInChainOwners { + Fatal(t, "expected owner to be in chain owners") + } + + // remove chain owner 2 + tx, err = arbOwner.RemoveChainOwner(&auth, chainOwnerAddr2) + Require(t, err) + _, err = builder.L2.EnsureTxSucceeded(tx) + Require(t, err) + isChainOwner, err = arbOwnerPublic.IsChainOwner(callOpts, chainOwnerAddr2) + Require(t, err) + if isChainOwner { + Fatal(t, "expected owner2 to not be a chain owner") + } + + _, err = arbOwnerPublic.RectifyChainOwner(&auth, chainOwnerAddr) + if (err == nil) || (err.Error() != "execution reverted") { + Fatal(t, "expected rectify chain owner to revert since it is already an owner") + } +} + +func TestArbAggregatorBatchPosters(t *testing.T) { + t.Parallel() + + ctx, cancel := context.WithCancel(context.Background()) + defer cancel() + + builder := NewNodeBuilder(ctx).DefaultConfig(t, false) + cleanup := builder.Build(t) + defer cleanup() + + auth := builder.L2Info.GetDefaultTransactOpts("Owner", ctx) + callOpts := &bind.CallOpts{Context: ctx} + + arbAggregator, err := precompilesgen.NewArbAggregator(types.ArbAggregatorAddress, builder.L2.Client) + Require(t, err) + + arbDebug, err := precompilesgen.NewArbDebug(types.ArbDebugAddress, builder.L2.Client) + Require(t, err) + + addr := common.BytesToAddress(crypto.Keccak256([]byte{})[:20]) + + // initially should have one batch poster + bps, err := arbAggregator.GetBatchPosters(callOpts) + Require(t, err) + if len(bps) != 1 { + Fatal(t, "expected one batch poster") + } + + // add addr as a batch poster + tx, err := arbDebug.BecomeChainOwner(&auth) + Require(t, err) + _, err = builder.L2.EnsureTxSucceeded(tx) + Require(t, err) + tx, err = arbAggregator.AddBatchPoster(&auth, addr) + Require(t, err) + _, err = builder.L2.EnsureTxSucceeded(tx) + Require(t, err) + + // there should now be two batch posters, and addr should be one of them + bps, err = arbAggregator.GetBatchPosters(callOpts) + Require(t, err) + if len(bps) != 2 { + Fatal(t, "expected two batch posters") + } + if bps[0] != addr && bps[1] != addr { + Fatal(t, "expected addr to be a batch poster") + } +} + +func TestArbAggregatorGetPreferredAggregator(t *testing.T) { + t.Parallel() + + ctx, cancel := context.WithCancel(context.Background()) + defer cancel() + + builder := NewNodeBuilder(ctx).DefaultConfig(t, false) + cleanup := builder.Build(t) + defer cleanup() + + callOpts := &bind.CallOpts{Context: ctx} + + arbAggregator, err := precompilesgen.NewArbAggregator(types.ArbAggregatorAddress, builder.L2.Client) + Require(t, err) + + addr := common.BytesToAddress(crypto.Keccak256([]byte{})[:20]) + + prefAgg, isDefault, err := arbAggregator.GetPreferredAggregator(callOpts, addr) + Require(t, err) + if !isDefault { + Fatal(t, "expected default preferred aggregator") + } + if prefAgg != l1pricing.BatchPosterAddress { + Fatal(t, "expected default preferred aggregator to be", l1pricing.BatchPosterAddress, "got", prefAgg) + } + + prefAgg, err = arbAggregator.GetDefaultAggregator(callOpts) + Require(t, err) + if prefAgg != l1pricing.BatchPosterAddress { + Fatal(t, "expected default preferred aggregator to be", l1pricing.BatchPosterAddress, "got", prefAgg) + } +} diff --git a/system_tests/program_test.go b/system_tests/program_test.go index cf8cd72559..ea4ccddd03 100644 --- a/system_tests/program_test.go +++ b/system_tests/program_test.go @@ -423,9 +423,9 @@ func storageTest(t *testing.T, jit bool) { validateBlocks(t, 2, jit, builder) - // Captures a block_input_.json file for the block that included the - // storage write transaction. - recordBlock(t, receipt.BlockNumber.Uint64(), builder) + // Captures a block_inputs json file for the block that included the + // storage write transaction. Include wasm targets necessary for arbitrator prover and jit binaries + recordBlock(t, receipt.BlockNumber.Uint64(), builder, rawdb.TargetWavm, rawdb.LocalTarget()) } func TestProgramTransientStorage(t *testing.T) { @@ -535,6 +535,16 @@ func testCalls(t *testing.T, jit bool) { defer cleanup() callsAddr := deployWasm(t, ctx, auth, l2client, rustFile("multicall")) + // checks that ArbInfo.GetCode works properly + codeFromFile, _ := readWasmFile(t, rustFile("multicall")) + arbInfo, err := pgen.NewArbInfo(types.ArbInfoAddress, l2client) + Require(t, err) + codeFromArbInfo, err := arbInfo.GetCode(nil, callsAddr) + Require(t, err) + if !bytes.Equal(codeFromFile, codeFromArbInfo) { + t.Fatal("ArbInfo.GetCode returned wrong code") + } + ensure := func(tx *types.Transaction, err error) *types.Receipt { t.Helper() Require(t, err) @@ -716,6 +726,13 @@ func testCalls(t *testing.T, jit bool) { Fatal(t, balance, value) } + // checks that ArbInfo.GetBalance works properly + balance, err = arbInfo.GetBalance(nil, eoa) + Require(t, err) + if !arbmath.BigEquals(balance, value) { + Fatal(t, balance, value) + } + blocks := []uint64{10} validateBlockRange(t, blocks, jit, builder) } @@ -1242,6 +1259,140 @@ func testSdkStorage(t *testing.T, jit bool) { check() } +func TestStylusPrecompileMethodsSimple(t *testing.T) { + ctx, cancel := context.WithCancel(context.Background()) + defer cancel() + + builder := NewNodeBuilder(ctx).DefaultConfig(t, true) + cleanup := builder.Build(t) + defer cleanup() + + arbOwner, err := pgen.NewArbOwner(types.ArbOwnerAddress, builder.L2.Client) + Require(t, err) + arbDebug, err := pgen.NewArbDebug(types.ArbDebugAddress, builder.L2.Client) + Require(t, err) + arbWasm, err := pgen.NewArbWasm(types.ArbWasmAddress, builder.L2.Client) + Require(t, err) + + ensure := func(tx *types.Transaction, err error) *types.Receipt { + t.Helper() + Require(t, err) + receipt, err := EnsureTxSucceeded(ctx, builder.L2.Client, tx) + Require(t, err) + return receipt + } + + ownerAuth := builder.L2Info.GetDefaultTransactOpts("Owner", ctx) + ensure(arbDebug.BecomeChainOwner(&ownerAuth)) + + wasm, _ := readWasmFile(t, rustFile("keccak")) + programAddress := deployContract(t, ctx, ownerAuth, builder.L2.Client, wasm) + + activateAuth := ownerAuth + activateAuth.Value = oneEth + ensure(arbWasm.ActivateProgram(&activateAuth, programAddress)) + + expectedExpiryDays := uint16(1) + ensure(arbOwner.SetWasmExpiryDays(&ownerAuth, expectedExpiryDays)) + ed, err := arbWasm.ExpiryDays(nil) + Require(t, err) + if ed != expectedExpiryDays { + t.Errorf("ExpiryDays from arbWasm precompile didnt match the value set by arbowner. have: %d, want: %d", ed, expectedExpiryDays) + } + ptl, err := arbWasm.ProgramTimeLeft(nil, programAddress) + Require(t, err) + expectedExpirySeconds := (uint64(expectedExpiryDays) * 24 * 3600) + // ProgramTimeLeft returns time in seconds to expiry and the current ExpiryDays is set to 1 day + // We expect the lag of 3600 seconds to exist because program.activatedAt uses hoursSinceArbitrum that + // rounds down (the current time since ArbitrumStartTime in hours)/3600 + if expectedExpirySeconds-ptl > 3600 { + t.Errorf("ProgramTimeLeft from arbWasm precompile returned value lesser than expected. %d <= want <= %d, have: %d", expectedExpirySeconds-3600, expectedExpirySeconds, ptl) + } + + ensure(arbOwner.SetWasmBlockCacheSize(&ownerAuth, 100)) + bcs, err := arbWasm.BlockCacheSize(nil) + Require(t, err) + if bcs != 100 { + t.Errorf("BlockCacheSize from arbWasm precompile didnt match the value set by arbowner. have: %d, want: %d", bcs, 100) + } + + ensure(arbOwner.SetWasmFreePages(&ownerAuth, 3)) + fp, err := arbWasm.FreePages(nil) + Require(t, err) + if fp != 3 { + t.Errorf("FreePages from arbWasm precompile didnt match the value set by arbowner. have: %d, want: %d", fp, 3) + } + + ensure(arbOwner.SetWasmInitCostScalar(&ownerAuth, uint64(4))) + ics, err := arbWasm.InitCostScalar(nil) + Require(t, err) + if ics != uint64(4) { + t.Errorf("InitCostScalar from arbWasm precompile didnt match the value set by arbowner. have: %d, want: %d", ics, 4) + } + + ensure(arbOwner.SetInkPrice(&ownerAuth, uint32(5))) + ip, err := arbWasm.InkPrice(nil) + Require(t, err) + if ip != uint32(5) { + t.Errorf("InkPrice from arbWasm precompile didnt match the value set by arbowner. have: %d, want: %d", ip, 5) + } + + ensure(arbOwner.SetWasmKeepaliveDays(&ownerAuth, 0)) + kad, err := arbWasm.KeepaliveDays(nil) + Require(t, err) + if kad != 0 { + t.Errorf("KeepaliveDays from arbWasm precompile didnt match the value set by arbowner. have: %d, want: 0", kad) + } + + ensure(arbOwner.SetWasmMaxStackDepth(&ownerAuth, uint32(6))) + msd, err := arbWasm.MaxStackDepth(nil) + Require(t, err) + if msd != uint32(6) { + t.Errorf("MaxStackDepth from arbWasm precompile didnt match the value set by arbowner. have: %d, want: %d", msd, 6) + } + + // Setting low values of gas and cached parameters ensures when MinInitGas is called on ArbWasm precompile, + // the returned values would be programs.MinInitGasUnits and programs.MinCachedGasUnits + ensure(arbOwner.SetWasmMinInitGas(&ownerAuth, 1, 1)) + mig, err := arbWasm.MinInitGas(nil) + Require(t, err) + if mig.Gas != programs.MinInitGasUnits { + t.Errorf("MinInitGas from arbWasm precompile didnt match the Gas value set by arbowner. have: %d, want: %d", mig.Gas, programs.MinInitGasUnits) + } + if mig.Cached != programs.MinCachedGasUnits { + t.Errorf("MinInitGas from arbWasm precompile didnt match the Cached value set by arbowner. have: %d, want: %d", mig.Cached, programs.MinCachedGasUnits) + } + + ensure(arbOwner.SetWasmPageGas(&ownerAuth, 7)) + pg, err := arbWasm.PageGas(nil) + Require(t, err) + if pg != 7 { + t.Errorf("PageGas from arbWasm precompile didnt match the value set by arbowner. have: %d, want: %d", pg, 7) + } + + ensure(arbOwner.SetWasmPageLimit(&ownerAuth, 8)) + pl, err := arbWasm.PageLimit(nil) + Require(t, err) + if pl != 8 { + t.Errorf("PageLimit from arbWasm precompile didnt match the value set by arbowner. have: %d, want: %d", pl, 8) + } + + // pageramp currently is initialPageRamp = 620674314 value in programs package + _, err = arbWasm.PageRamp(nil) + Require(t, err) + + codehash := crypto.Keccak256Hash(wasm) + cas, err := arbWasm.CodehashAsmSize(nil, codehash) + Require(t, err) + if cas == 0 { + t.Error("CodehashAsmSize from arbWasm precompile returned 0 value") + } + // Since ArbOwner has set wasm KeepaliveDays to 0, it enables us to do this, though this shouldn't have any effect + codehashKeepaliveAuth := ownerAuth + codehashKeepaliveAuth.Value = oneEth + ensure(arbWasm.CodehashKeepalive(&codehashKeepaliveAuth, codehash)) +} + func TestProgramActivationLogs(t *testing.T) { t.Parallel() builder, auth, cleanup := setupProgramTest(t, true) @@ -1385,7 +1536,7 @@ func TestProgramCacheManager(t *testing.T) { isManager, err := arbWasmCache.IsCacheManager(nil, manager) assert(!isManager, err) - // athorize the manager + // authorize the manager ensure(arbOwner.AddWasmCacheManager(&ownerAuth, manager)) assert(arbWasmCache.IsCacheManager(nil, manager)) all, err := arbWasmCache.AllCacheManagers(nil) @@ -2013,7 +2164,7 @@ func checkWasmStoreContent(t *testing.T, wasmDb ethdb.KeyValueStore, targets []s } } -func deployWasmAndGetLruEntrySizeEstimateBytes( +func deployWasmAndGetEntrySizeEstimateBytes( t *testing.T, builder *NodeBuilder, auth bind.TransactOpts, @@ -2044,12 +2195,12 @@ func deployWasmAndGetLruEntrySizeEstimateBytes( module, err := statedb.TryGetActivatedAsm(rawdb.LocalTarget(), log.ModuleHash) Require(t, err, ", wasmName:", wasmName) - lruEntrySizeEstimateBytes := programs.GetLruEntrySizeEstimateBytes(module, log.Version, true) + entrySizeEstimateBytes := programs.GetEntrySizeEstimateBytes(module, log.Version, true) // just a sanity check - if lruEntrySizeEstimateBytes == 0 { - Fatal(t, "lruEntrySizeEstimateBytes is 0, wasmName:", wasmName) + if entrySizeEstimateBytes == 0 { + Fatal(t, "entrySizeEstimateBytes is 0, wasmName:", wasmName) } - return programAddress, lruEntrySizeEstimateBytes + return programAddress, entrySizeEstimateBytes } func TestWasmLruCache(t *testing.T) { @@ -2059,81 +2210,356 @@ func TestWasmLruCache(t *testing.T) { l2client := builder.L2.Client defer cleanup() + ensure := func(tx *types.Transaction, err error) *types.Receipt { + t.Helper() + Require(t, err) + receipt, err := EnsureTxSucceeded(ctx, l2client, tx) + Require(t, err) + return receipt + } + auth.GasLimit = 32000000 auth.Value = oneEth - fallibleProgramAddress, fallibleLruEntrySizeEstimateBytes := deployWasmAndGetLruEntrySizeEstimateBytes(t, builder, auth, "fallible") - keccakProgramAddress, keccakLruEntrySizeEstimateBytes := deployWasmAndGetLruEntrySizeEstimateBytes(t, builder, auth, "keccak") - mathProgramAddress, mathLruEntrySizeEstimateBytes := deployWasmAndGetLruEntrySizeEstimateBytes(t, builder, auth, "math") + fallibleProgramAddress, fallibleEntrySize := deployWasmAndGetEntrySizeEstimateBytes(t, builder, auth, "fallible") + keccakProgramAddress, keccakEntrySize := deployWasmAndGetEntrySizeEstimateBytes(t, builder, auth, "keccak") + mathProgramAddress, mathEntrySize := deployWasmAndGetEntrySizeEstimateBytes(t, builder, auth, "math") t.Log( - "lruEntrySizeEstimateBytes, ", - "fallible:", fallibleLruEntrySizeEstimateBytes, - "keccak:", keccakLruEntrySizeEstimateBytes, - "math:", mathLruEntrySizeEstimateBytes, + "entrySizeEstimateBytes, ", + "fallible:", fallibleEntrySize, + "keccak:", keccakEntrySize, + "math:", mathEntrySize, ) + if fallibleEntrySize == keccakEntrySize || fallibleEntrySize == mathEntrySize || keccakEntrySize == mathEntrySize { + Fatal(t, "at least two programs have the same entry size") + } programs.ClearWasmLruCache() - lruMetrics := programs.GetWasmLruCacheMetrics() - if lruMetrics.Count != 0 { - t.Fatalf("lruMetrics.Count, expected: %v, actual: %v", 0, lruMetrics.Count) - } - if lruMetrics.SizeBytes != 0 { - t.Fatalf("lruMetrics.SizeBytes, expected: %v, actual: %v", 0, lruMetrics.SizeBytes) - } + checkLruCacheMetrics(t, programs.WasmLruCacheMetrics{ + Count: 0, + SizeBytes: 0, + }) - programs.SetWasmLruCacheCapacity(fallibleLruEntrySizeEstimateBytes - 1) + programs.SetWasmLruCacheCapacity(fallibleEntrySize - 1) // fallible wasm program will not be cached since its size is greater than lru cache capacity tx := l2info.PrepareTxTo("Owner", &fallibleProgramAddress, l2info.TransferGas, nil, []byte{0x01}) - Require(t, l2client.SendTransaction(ctx, tx)) - _, err := EnsureTxSucceeded(ctx, l2client, tx) - Require(t, err) - lruMetrics = programs.GetWasmLruCacheMetrics() - if lruMetrics.Count != 0 { - t.Fatalf("lruMetrics.Count, expected: %v, actual: %v", 0, lruMetrics.Count) - } - if lruMetrics.SizeBytes != 0 { - t.Fatalf("lruMetrics.SizeBytes, expected: %v, actual: %v", 0, lruMetrics.SizeBytes) - } + ensure(tx, l2client.SendTransaction(ctx, tx)) + checkLruCacheMetrics(t, programs.WasmLruCacheMetrics{ + Count: 0, + SizeBytes: 0, + }) programs.SetWasmLruCacheCapacity( - fallibleLruEntrySizeEstimateBytes + keccakLruEntrySizeEstimateBytes + mathLruEntrySizeEstimateBytes - 1, + fallibleEntrySize + keccakEntrySize + mathEntrySize - 1, ) // fallible wasm program will be cached tx = l2info.PrepareTxTo("Owner", &fallibleProgramAddress, l2info.TransferGas, nil, []byte{0x01}) - Require(t, l2client.SendTransaction(ctx, tx)) - _, err = EnsureTxSucceeded(ctx, l2client, tx) - Require(t, err) - lruMetrics = programs.GetWasmLruCacheMetrics() - if lruMetrics.Count != 1 { - t.Fatalf("lruMetrics.Count, expected: %v, actual: %v", 1, lruMetrics.Count) - } - if lruMetrics.SizeBytes != fallibleLruEntrySizeEstimateBytes { - t.Fatalf("lruMetrics.SizeBytes, expected: %v, actual: %v", fallibleLruEntrySizeEstimateBytes, lruMetrics.SizeBytes) - } + ensure(tx, l2client.SendTransaction(ctx, tx)) + checkLruCacheMetrics(t, programs.WasmLruCacheMetrics{ + Count: 1, + SizeBytes: fallibleEntrySize, + }) // keccak wasm program will be cached tx = l2info.PrepareTxTo("Owner", &keccakProgramAddress, l2info.TransferGas, nil, []byte{0x01}) - Require(t, l2client.SendTransaction(ctx, tx)) - _, err = EnsureTxSucceeded(ctx, l2client, tx) - Require(t, err) - lruMetrics = programs.GetWasmLruCacheMetrics() - if lruMetrics.Count != 2 { - t.Fatalf("lruMetrics.Count, expected: %v, actual: %v", 2, lruMetrics.Count) + ensure(tx, l2client.SendTransaction(ctx, tx)) + checkLruCacheMetrics(t, programs.WasmLruCacheMetrics{ + Count: 2, + SizeBytes: fallibleEntrySize + keccakEntrySize, + }) + + // math wasm program will be cached, but fallible will be evicted since (fallible + keccak + math) > lruCacheCapacity + tx = l2info.PrepareTxTo("Owner", &mathProgramAddress, l2info.TransferGas, nil, []byte{0x01}) + ensure(tx, l2client.SendTransaction(ctx, tx)) + checkLruCacheMetrics(t, programs.WasmLruCacheMetrics{ + Count: 2, + SizeBytes: keccakEntrySize + mathEntrySize, + }) +} + +func checkLongTermCacheMetrics(t *testing.T, expected programs.WasmLongTermCacheMetrics) { + t.Helper() + longTermMetrics := programs.GetWasmCacheMetrics().LongTerm + if longTermMetrics.Count != expected.Count { + t.Fatalf("longTermMetrics.Count, expected: %v, actual: %v", expected.Count, longTermMetrics.Count) } - if lruMetrics.SizeBytes != fallibleLruEntrySizeEstimateBytes+keccakLruEntrySizeEstimateBytes { - t.Fatalf("lruMetrics.SizeBytes, expected: %v, actual: %v", fallibleLruEntrySizeEstimateBytes+keccakLruEntrySizeEstimateBytes, lruMetrics.SizeBytes) + if longTermMetrics.SizeBytes != expected.SizeBytes { + t.Fatalf("longTermMetrics.SizeBytes, expected: %v, actual: %v", expected.SizeBytes, longTermMetrics.SizeBytes) } +} - // math wasm program will be cached, but fallible will be evicted since (fallible + keccak + math) > lruCacheCapacity +func checkLruCacheMetrics(t *testing.T, expected programs.WasmLruCacheMetrics) { + t.Helper() + lruMetrics := programs.GetWasmCacheMetrics().Lru + if lruMetrics.Count != expected.Count { + t.Fatalf("lruMetrics.Count, expected: %v, actual: %v", expected.Count, lruMetrics.Count) + } + if lruMetrics.SizeBytes != expected.SizeBytes { + t.Fatalf("lruMetrics.SizeBytes, expected: %v, actual: %v", expected.SizeBytes, lruMetrics.SizeBytes) + } +} + +func TestWasmLongTermCache(t *testing.T) { + builder, ownerAuth, cleanup := setupProgramTest(t, true, func(builder *NodeBuilder) { + builder.WithStylusLongTermCache(true) + }) + ctx := builder.ctx + l2info := builder.L2Info + l2client := builder.L2.Client + defer cleanup() + + ensure := func(tx *types.Transaction, err error) *types.Receipt { + t.Helper() + Require(t, err) + receipt, err := EnsureTxSucceeded(ctx, l2client, tx) + Require(t, err) + return receipt + } + + arbWasmCache, err := pgen.NewArbWasmCache(types.ArbWasmCacheAddress, builder.L2.Client) + Require(t, err) + arbOwner, err := pgen.NewArbOwner(types.ArbOwnerAddress, builder.L2.Client) + Require(t, err) + ensure(arbOwner.SetInkPrice(&ownerAuth, 10_000)) + + ownerAuth.GasLimit = 32000000 + ownerAuth.Value = oneEth + + fallibleProgramAddress, fallibleEntrySize := deployWasmAndGetEntrySizeEstimateBytes(t, builder, ownerAuth, "fallible") + keccakProgramAddress, keccakEntrySize := deployWasmAndGetEntrySizeEstimateBytes(t, builder, ownerAuth, "keccak") + mathProgramAddress, mathEntrySize := deployWasmAndGetEntrySizeEstimateBytes(t, builder, ownerAuth, "math") + t.Log( + "entrySizeEstimateBytes, ", + "fallible:", fallibleEntrySize, + "keccak:", keccakEntrySize, + "math:", mathEntrySize, + ) + if fallibleEntrySize == keccakEntrySize || fallibleEntrySize == mathEntrySize || keccakEntrySize == mathEntrySize { + Fatal(t, "at least two programs have the same entry size") + } + + ownerAuth.Value = common.Big0 + + programs.ClearWasmLongTermCache() + checkLongTermCacheMetrics(t, programs.WasmLongTermCacheMetrics{ + Count: 0, + SizeBytes: 0, + }) + + // fallible wasm program will not be cached since caching is not set for this program + tx := l2info.PrepareTxTo("Owner", &fallibleProgramAddress, l2info.TransferGas, nil, []byte{0x01}) + ensure(tx, l2client.SendTransaction(ctx, tx)) + checkLongTermCacheMetrics(t, programs.WasmLongTermCacheMetrics{ + Count: 0, + SizeBytes: 0, + }) + + ensure(arbWasmCache.CacheProgram(&ownerAuth, fallibleProgramAddress)) + // fallible wasm program will be cached + tx = l2info.PrepareTxTo("Owner", &fallibleProgramAddress, l2info.TransferGas, nil, []byte{0x01}) + ensure(tx, l2client.SendTransaction(ctx, tx)) + checkLongTermCacheMetrics(t, programs.WasmLongTermCacheMetrics{ + Count: 1, + SizeBytes: fallibleEntrySize, + }) + + // keccak wasm program will be cached + ensure(arbWasmCache.CacheProgram(&ownerAuth, keccakProgramAddress)) + tx = l2info.PrepareTxTo("Owner", &keccakProgramAddress, l2info.TransferGas, nil, []byte{0x01}) + ensure(tx, l2client.SendTransaction(ctx, tx)) + checkLongTermCacheMetrics(t, programs.WasmLongTermCacheMetrics{ + Count: 2, + SizeBytes: fallibleEntrySize + keccakEntrySize, + }) + + // math wasm program will not be cached tx = l2info.PrepareTxTo("Owner", &mathProgramAddress, l2info.TransferGas, nil, []byte{0x01}) - Require(t, l2client.SendTransaction(ctx, tx)) - _, err = EnsureTxSucceeded(ctx, l2client, tx) + ensure(tx, l2client.SendTransaction(ctx, tx)) + checkLongTermCacheMetrics(t, programs.WasmLongTermCacheMetrics{ + Count: 2, + SizeBytes: fallibleEntrySize + keccakEntrySize, + }) + + // math wasm program will be cached + ensure(arbWasmCache.CacheProgram(&ownerAuth, mathProgramAddress)) + tx = l2info.PrepareTxTo("Owner", &mathProgramAddress, l2info.TransferGas, nil, []byte{0x01}) + ensure(tx, l2client.SendTransaction(ctx, tx)) + checkLongTermCacheMetrics(t, programs.WasmLongTermCacheMetrics{ + Count: 3, + SizeBytes: fallibleEntrySize + keccakEntrySize + mathEntrySize, + }) + + statedb, err := builder.L2.ExecNode.Backend.ArbInterface().BlockChain().State() Require(t, err) - lruMetrics = programs.GetWasmLruCacheMetrics() - if lruMetrics.Count != 2 { - t.Fatalf("lruMetrics.Count, expected: %v, actual: %v", 2, lruMetrics.Count) + fallibleProgramHash := statedb.GetCodeHash(fallibleProgramAddress) + keccakProgramHash := statedb.GetCodeHash(keccakProgramAddress) + mathProgramHash := statedb.GetCodeHash(mathProgramAddress) + + ensure(arbWasmCache.EvictCodehash(&ownerAuth, keccakProgramHash)) + checkLongTermCacheMetrics(t, programs.WasmLongTermCacheMetrics{ + Count: 2, + SizeBytes: fallibleEntrySize + mathEntrySize, + }) + + // keccak wasm program will not be cached + tx = l2info.PrepareTxTo("Owner", &keccakProgramAddress, l2info.TransferGas, nil, []byte{0x01}) + ensure(tx, l2client.SendTransaction(ctx, tx)) + checkLongTermCacheMetrics(t, programs.WasmLongTermCacheMetrics{ + Count: 2, + SizeBytes: fallibleEntrySize + mathEntrySize, + }) + + // keccak wasm program will be cached + ensure(arbWasmCache.CacheProgram(&ownerAuth, keccakProgramAddress)) + tx = l2info.PrepareTxTo("Owner", &mathProgramAddress, l2info.TransferGas, nil, []byte{0x01}) + ensure(tx, l2client.SendTransaction(ctx, tx)) + checkLongTermCacheMetrics(t, programs.WasmLongTermCacheMetrics{ + Count: 3, + SizeBytes: fallibleEntrySize + keccakEntrySize + mathEntrySize, + }) + + ensure(arbWasmCache.EvictCodehash(&ownerAuth, fallibleProgramHash)) + checkLongTermCacheMetrics(t, programs.WasmLongTermCacheMetrics{ + Count: 2, + SizeBytes: keccakEntrySize + mathEntrySize, + }) + + ensure(arbWasmCache.EvictCodehash(&ownerAuth, mathProgramHash)) + checkLongTermCacheMetrics(t, programs.WasmLongTermCacheMetrics{ + Count: 1, + SizeBytes: keccakEntrySize, + }) + + ensure(arbWasmCache.EvictCodehash(&ownerAuth, keccakProgramHash)) + checkLongTermCacheMetrics(t, programs.WasmLongTermCacheMetrics{ + Count: 0, + SizeBytes: 0, + }) +} + +func TestRepopulateWasmLongTermCacheFromLru(t *testing.T) { + builder, ownerAuth, cleanup := setupProgramTest(t, true, func(builder *NodeBuilder) { + builder.WithStylusLongTermCache(true) + }) + ctx := builder.ctx + l2info := builder.L2Info + l2client := builder.L2.Client + defer cleanup() + + ensure := func(tx *types.Transaction, err error) *types.Receipt { + t.Helper() + Require(t, err) + receipt, err := EnsureTxSucceeded(ctx, l2client, tx) + Require(t, err) + return receipt } - if lruMetrics.SizeBytes != keccakLruEntrySizeEstimateBytes+mathLruEntrySizeEstimateBytes { - t.Fatalf("lruMetrics.SizeBytes, expected: %v, actual: %v", keccakLruEntrySizeEstimateBytes+mathLruEntrySizeEstimateBytes, lruMetrics.SizeBytes) + + arbWasmCache, err := pgen.NewArbWasmCache(types.ArbWasmCacheAddress, builder.L2.Client) + Require(t, err) + arbOwner, err := pgen.NewArbOwner(types.ArbOwnerAddress, builder.L2.Client) + Require(t, err) + ensure(arbOwner.SetInkPrice(&ownerAuth, 10_000)) + + ownerAuth.GasLimit = 32000000 + ownerAuth.Value = oneEth + + fallibleProgramAddress, fallibleEntrySize := deployWasmAndGetEntrySizeEstimateBytes(t, builder, ownerAuth, "fallible") + keccakProgramAddress, keccakEntrySize := deployWasmAndGetEntrySizeEstimateBytes(t, builder, ownerAuth, "keccak") + mathProgramAddress, mathEntrySize := deployWasmAndGetEntrySizeEstimateBytes(t, builder, ownerAuth, "math") + if fallibleEntrySize == keccakEntrySize || fallibleEntrySize == mathEntrySize || keccakEntrySize == mathEntrySize { + Fatal(t, "at least two programs have the same entry size") } + + ownerAuth.Value = common.Big0 + + programs.ClearWasmLongTermCache() + programs.ClearWasmLruCache() + // only 2 out of 3 programs should fit lru + programs.SetWasmLruCacheCapacity( + fallibleEntrySize + keccakEntrySize + mathEntrySize - 1, + ) + + checkLongTermCacheMetrics(t, programs.WasmLongTermCacheMetrics{ + Count: 0, + SizeBytes: 0, + }) + checkLruCacheMetrics(t, programs.WasmLruCacheMetrics{ + Count: 0, + SizeBytes: 0, + }) + + ensure(arbWasmCache.CacheProgram(&ownerAuth, fallibleProgramAddress)) + checkLruCacheMetrics(t, programs.WasmLruCacheMetrics{ + Count: 0, + SizeBytes: 0, + }) + checkLongTermCacheMetrics(t, programs.WasmLongTermCacheMetrics{ + Count: 1, + SizeBytes: fallibleEntrySize, + }) + + // clear long term cache to emulate restart + programs.ClearWasmLongTermCache() + programs.ClearWasmLruCache() + + checkLruCacheMetrics(t, programs.WasmLruCacheMetrics{ + Count: 0, + SizeBytes: 0, + }) + checkLongTermCacheMetrics(t, programs.WasmLongTermCacheMetrics{ + Count: 0, + SizeBytes: 0, + }) + + nonce := builder.L2Info.GetInfoWithPrivKey("Owner").Nonce.Load() + tx := l2info.PrepareTxTo("Owner", &fallibleProgramAddress, l2info.TransferGas, nil, []byte{0x01}) + _, err = arbutil.SendTxAsCall(ctx, l2client, tx, l2info.GetAddress("Owner"), nil, true) + Require(t, err) + // restore nonce in L2Info + builder.L2Info.GetInfoWithPrivKey("Owner").Nonce.Store(nonce) + // fallibleProgram should be added only to lru cache as the api call should be processed with wasm cache tag = 0 + checkLruCacheMetrics(t, programs.WasmLruCacheMetrics{ + Count: 1, + SizeBytes: fallibleEntrySize, + }) + checkLongTermCacheMetrics(t, programs.WasmLongTermCacheMetrics{ + Count: 0, + SizeBytes: 0, + }) + + tx = l2info.PrepareTxTo("Owner", &keccakProgramAddress, l2info.TransferGas, nil, []byte{0x01}) + ensure(tx, l2client.SendTransaction(ctx, tx)) + checkLruCacheMetrics(t, programs.WasmLruCacheMetrics{ + Count: 2, + SizeBytes: fallibleEntrySize + keccakEntrySize, + }) + checkLongTermCacheMetrics(t, programs.WasmLongTermCacheMetrics{ + Count: 0, + SizeBytes: 0, + }) + + tx = l2info.PrepareTxTo("Owner", &fallibleProgramAddress, l2info.TransferGas, nil, []byte{0x01}) + ensure(tx, l2client.SendTransaction(ctx, tx)) + checkLruCacheMetrics(t, programs.WasmLruCacheMetrics{ + Count: 2, + SizeBytes: fallibleEntrySize + keccakEntrySize, + }) + checkLongTermCacheMetrics(t, programs.WasmLongTermCacheMetrics{ + Count: 1, + SizeBytes: fallibleEntrySize, + }) + + // mathProgram should end up in lru cache and + // as result fallibleProgram should be evicted as least recently used item (tx that restores the program back to long term cache shouldn't promote the lru item); + // fallibleProgram should remain in long term cache + tx = l2info.PrepareTxTo("Owner", &mathProgramAddress, l2info.TransferGas, nil, []byte{0x01}) + ensure(tx, l2client.SendTransaction(ctx, tx)) + checkLruCacheMetrics(t, programs.WasmLruCacheMetrics{ + Count: 2, + SizeBytes: keccakEntrySize + mathEntrySize, + }) + checkLongTermCacheMetrics(t, programs.WasmLongTermCacheMetrics{ + Count: 1, + SizeBytes: fallibleEntrySize, + }) } diff --git a/system_tests/retryable_test.go b/system_tests/retryable_test.go index aa9fbfd72e..89446e3c4b 100644 --- a/system_tests/retryable_test.go +++ b/system_tests/retryable_test.go @@ -423,6 +423,118 @@ func TestSubmitRetryableFailThenRetry(t *testing.T) { } } +func TestGetLifetime(t *testing.T) { + t.Parallel() + + ctx, cancel := context.WithCancel(context.Background()) + defer cancel() + + builder := NewNodeBuilder(ctx).DefaultConfig(t, false) + cleanup := builder.Build(t) + defer cleanup() + + callOpts := &bind.CallOpts{Context: ctx} + + arbRetryableTx, err := precompilesgen.NewArbRetryableTx(common.HexToAddress("6e"), builder.L2.Client) + Require(t, err) + + lifetime, err := arbRetryableTx.GetLifetime(callOpts) + Require(t, err) + if lifetime.Cmp(big.NewInt(retryables.RetryableLifetimeSeconds)) != 0 { + t.Fatal("Expected to be ", retryables.RetryableLifetimeSeconds, " but got ", lifetime) + } +} + +func TestKeepaliveAndCancelRetryable(t *testing.T) { + t.Parallel() + builder, delayedInbox, lookupL2Tx, ctx, teardown := retryableSetup(t) + defer teardown() + + ownerTxOpts := builder.L2Info.GetDefaultTransactOpts("Owner", ctx) + usertxopts := builder.L1Info.GetDefaultTransactOpts("Faucet", ctx) + usertxopts.Value = arbmath.BigMul(big.NewInt(1e12), big.NewInt(1e12)) + + simpleAddr, _ := builder.L2.DeploySimple(t, ownerTxOpts) + simpleABI, err := mocksgen.SimpleMetaData.GetAbi() + Require(t, err) + + beneficiaryAddress := builder.L2Info.GetAddress("Beneficiary") + l1tx, err := delayedInbox.CreateRetryableTicket( + &usertxopts, + simpleAddr, + common.Big0, + big.NewInt(1e16), + beneficiaryAddress, + beneficiaryAddress, + // send enough L2 gas for intrinsic but not compute + big.NewInt(int64(params.TxGas+params.TxDataNonZeroGasEIP2028*4)), + big.NewInt(l2pricing.InitialBaseFeeWei*2), + simpleABI.Methods["incrementRedeem"].ID, + ) + Require(t, err) + + l1Receipt, err := builder.L1.EnsureTxSucceeded(l1tx) + Require(t, err) + if l1Receipt.Status != types.ReceiptStatusSuccessful { + Fatal(t, "l1Receipt indicated failure") + } + + waitForL1DelayBlocks(t, builder) + + receipt, err := builder.L2.EnsureTxSucceeded(lookupL2Tx(l1Receipt)) + Require(t, err) + if len(receipt.Logs) != 2 { + Fatal(t, len(receipt.Logs)) + } + ticketId := receipt.Logs[0].Topics[1] + firstRetryTxId := receipt.Logs[1].Topics[2] + + // make sure it failed + receipt, err = WaitForTx(ctx, builder.L2.Client, firstRetryTxId, time.Second*5) + Require(t, err) + if receipt.Status != types.ReceiptStatusFailed { + Fatal(t, receipt.GasUsed) + } + + arbRetryableTx, err := precompilesgen.NewArbRetryableTx(common.HexToAddress("6e"), builder.L2.Client) + Require(t, err) + + // checks that the ticket exists and gets current timeout + timeoutBeforeKeepalive, err := arbRetryableTx.GetTimeout(&bind.CallOpts{}, ticketId) + Require(t, err) + + // checks beneficiary + retrievedBeneficiaryAddress, err := arbRetryableTx.GetBeneficiary(&bind.CallOpts{}, ticketId) + Require(t, err) + if retrievedBeneficiaryAddress != beneficiaryAddress { + Fatal(t, "expected beneficiary to be", beneficiaryAddress, "but got", retrievedBeneficiaryAddress) + } + + // checks that keepalive increases the timeout as expected + _, err = arbRetryableTx.Keepalive(&ownerTxOpts, ticketId) + Require(t, err) + timeoutAfterKeepalive, err := arbRetryableTx.GetTimeout(&bind.CallOpts{}, ticketId) + Require(t, err) + expectedTimeoutAfterKeepAlive := timeoutBeforeKeepalive + expectedTimeoutAfterKeepAlive.Add(expectedTimeoutAfterKeepAlive, big.NewInt(retryables.RetryableLifetimeSeconds)) + if timeoutAfterKeepalive.Cmp(expectedTimeoutAfterKeepAlive) != 0 { + Fatal(t, "expected timeout after keepalive to be", expectedTimeoutAfterKeepAlive, "but got", timeoutAfterKeepalive) + } + + // cancel the ticket + beneficiaryTxOpts := builder.L2Info.GetDefaultTransactOpts("Beneficiary", ctx) + tx, err := arbRetryableTx.Cancel(&beneficiaryTxOpts, ticketId) + Require(t, err) + _, err = builder.L2.EnsureTxSucceeded(tx) + Require(t, err) + + // checks that the ticket no longer exists + _, err = arbRetryableTx.GetTimeout(&bind.CallOpts{}, ticketId) + if (err == nil) || (err.Error() != "execution reverted: error NoTicketWithID(): NoTicketWithID()") { + Fatal(t, "didn't get expected NoTicketWithID error") + } +} + func TestSubmissionGasCosts(t *testing.T) { t.Parallel() builder, delayedInbox, lookupL2Tx, ctx, teardown := retryableSetup(t) diff --git a/system_tests/seq_coordinator_test.go b/system_tests/seq_coordinator_test.go index 1b8926a1b9..e7d8bf6b39 100644 --- a/system_tests/seq_coordinator_test.go +++ b/system_tests/seq_coordinator_test.go @@ -12,7 +12,7 @@ import ( "testing" "time" - "github.com/go-redis/redis/v8" + "github.com/redis/go-redis/v9" "github.com/ethereum/go-ethereum/common" "github.com/ethereum/go-ethereum/log" @@ -373,3 +373,77 @@ func TestRedisSeqCoordinatorMessageSync(t *testing.T) { func TestRedisSeqCoordinatorWrongKeyMessageSync(t *testing.T) { testCoordinatorMessageSync(t, false) } + +func TestRedisSwitchover(t *testing.T) { + ctx, cancel := context.WithCancel(context.Background()) + defer cancel() + + builder := NewNodeBuilder(ctx).DefaultConfig(t, true) + builder.nodeConfig.SeqCoordinator.Enable = true + builder.nodeConfig.SeqCoordinator.RedisUrl = redisutil.CreateTestRedis(ctx, t) + builder.nodeConfig.SeqCoordinator.NewRedisUrl = redisutil.CreateTestRedis(ctx, t) + builder.nodeConfig.BatchPoster.Enable = false + + nodeNames := []string{"stdio://A", "stdio://B"} + initRedisForTest(t, ctx, builder.nodeConfig.SeqCoordinator.RedisUrl, nodeNames) + initRedisForTest(t, ctx, builder.nodeConfig.SeqCoordinator.NewRedisUrl, nodeNames) + builder.nodeConfig.SeqCoordinator.MyUrl = nodeNames[0] + + cleanup := builder.Build(t) + defer cleanup() + + redisClient, err := redisutil.RedisClientFromURL(builder.nodeConfig.SeqCoordinator.RedisUrl) + Require(t, err) + + // wait for sequencerA to become master + for { + err := redisClient.Get(ctx, redisutil.CHOSENSEQ_KEY).Err() + if errors.Is(err, redis.Nil) { + time.Sleep(builder.nodeConfig.SeqCoordinator.UpdateInterval) + continue + } + Require(t, err) + break + } + + builder.L2Info.GenerateAccount("User2") + + nodeConfigDup := *builder.nodeConfig + builder.nodeConfig = &nodeConfigDup + builder.nodeConfig.Feed.Output = *newBroadcasterConfigTest() + builder.nodeConfig.SeqCoordinator.MyUrl = nodeNames[1] + testClientB, cleanupB := builder.Build2ndNode(t, &SecondNodeParams{nodeConfig: builder.nodeConfig}) + defer cleanupB() + + verifyTxIsProcessed(t, ctx, builder, testClientB, 1e12) + + redisClient.Set(ctx, redisutil.CHOSENSEQ_KEY, redisutil.SWITCHED_REDIS, time.Duration(-1)) + + verifyTxIsProcessed(t, ctx, builder, testClientB, 1e12*2) + + // Wait for all messages to be processed, before closing old redisClient + time.Sleep(1 * time.Second) + err = redisClient.Close() + Require(t, err) + + verifyTxIsProcessed(t, ctx, builder, testClientB, 1e12*3) + +} + +func verifyTxIsProcessed(t *testing.T, ctx context.Context, builder *NodeBuilder, testClientB *TestClient, balance int64) { + tx := builder.L2Info.PrepareTx("Owner", "User2", builder.L2Info.TransferGas, big.NewInt(1e12), nil) + + err := builder.L2.Client.SendTransaction(ctx, tx) + Require(t, err) + + _, err = builder.L2.EnsureTxSucceeded(tx) + Require(t, err) + + _, err = WaitForTx(ctx, testClientB.Client, tx.Hash(), time.Second*5) + Require(t, err) + l2balance, err := testClientB.Client.BalanceAt(ctx, builder.L2Info.GetAddress("User2"), nil) + Require(t, err) + if l2balance.Cmp(big.NewInt(balance)) != 0 { + t.Fatal("Unexpected balance:", l2balance) + } +} diff --git a/system_tests/state_fuzz_test.go b/system_tests/state_fuzz_test.go index 24140e480d..c8312350e6 100644 --- a/system_tests/state_fuzz_test.go +++ b/system_tests/state_fuzz_test.go @@ -38,6 +38,7 @@ func BuildBlock( chainConfig *params.ChainConfig, inbox arbstate.InboxBackend, seqBatch []byte, + runMode core.MessageRunMode, ) (*types.Block, error) { var delayedMessagesRead uint64 if lastBlockHeader != nil { @@ -59,11 +60,13 @@ func BuildBlock( } err = l1Message.FillInBatchGasCost(batchFetcher) if err != nil { - return nil, err + // skip malformed batch posting report + // nolint:nilerr + return nil, nil } block, _, err := arbos.ProduceBlock( - l1Message, delayedMessagesRead, lastBlockHeader, statedb, chainContext, chainConfig, false, + l1Message, delayedMessagesRead, lastBlockHeader, statedb, chainContext, chainConfig, false, runMode, ) return block, err } @@ -127,7 +130,7 @@ func (c noopChainContext) GetHeader(common.Hash, uint64) *types.Header { } func FuzzStateTransition(f *testing.F) { - f.Fuzz(func(t *testing.T, compressSeqMsg bool, seqMsg []byte, delayedMsg []byte) { + f.Fuzz(func(t *testing.T, compressSeqMsg bool, seqMsg []byte, delayedMsg []byte, runModeSeed uint8) { if len(seqMsg) > 0 && daprovider.IsL1AuthenticatedMessageHeaderByte(seqMsg[0]) { return } @@ -201,7 +204,9 @@ func FuzzStateTransition(f *testing.F) { positionWithinMessage: 0, delayedMessages: delayedMessages, } - _, err = BuildBlock(statedb, genesis, noopChainContext{}, params.ArbitrumOneChainConfig(), inbox, seqBatch) + numberOfMessageRunModes := uint8(core.MessageReplayMode) + 1 // TODO update number of run modes when new mode is added + runMode := core.MessageRunMode(runModeSeed % numberOfMessageRunModes) + _, err = BuildBlock(statedb, genesis, noopChainContext{}, params.ArbitrumOneChainConfig(), inbox, seqBatch, runMode) if err != nil { // With the fixed header it shouldn't be possible to read a delayed message, // and no other type of error should be possible. diff --git a/util/redisutil/redis_coordinator.go b/util/redisutil/redis_coordinator.go index 2c12ffec50..39db7c8645 100644 --- a/util/redisutil/redis_coordinator.go +++ b/util/redisutil/redis_coordinator.go @@ -6,7 +6,7 @@ import ( "fmt" "strings" - "github.com/go-redis/redis/v8" + "github.com/redis/go-redis/v9" "github.com/ethereum/go-ethereum/log" @@ -21,6 +21,7 @@ const WANTS_LOCKOUT_KEY_PREFIX string = "coordinator.liveliness." // Per se const MESSAGE_KEY_PREFIX string = "coordinator.msg." // Per Message. Only written by sequencer holding CHOSEN const SIGNATURE_KEY_PREFIX string = "coordinator.msg.sig." // Per Message. Only written by sequencer holding CHOSEN const WANTS_LOCKOUT_VAL string = "OK" +const SWITCHED_REDIS string = "SWITCHED_REDIS" const INVALID_VAL string = "INVALID" const INVALID_URL string = "" diff --git a/util/redisutil/redisutil.go b/util/redisutil/redisutil.go index f89c250e9a..01ba836d5b 100644 --- a/util/redisutil/redisutil.go +++ b/util/redisutil/redisutil.go @@ -1,6 +1,6 @@ package redisutil -import "github.com/go-redis/redis/v8" +import "github.com/redis/go-redis/v9" func RedisClientFromURL(url string) (redis.UniversalClient, error) { if url == "" { diff --git a/validator/client/redis/producer.go b/validator/client/redis/producer.go index adc2f34af5..c5726ffe8b 100644 --- a/validator/client/redis/producer.go +++ b/validator/client/redis/producer.go @@ -9,7 +9,6 @@ import ( "github.com/ethereum/go-ethereum/core/rawdb" "github.com/ethereum/go-ethereum/ethdb" "github.com/ethereum/go-ethereum/log" - "github.com/go-redis/redis/v8" "github.com/offchainlabs/nitro/pubsub" "github.com/offchainlabs/nitro/util/containers" "github.com/offchainlabs/nitro/util/redisutil" @@ -17,6 +16,7 @@ import ( "github.com/offchainlabs/nitro/validator" "github.com/offchainlabs/nitro/validator/server_api" "github.com/offchainlabs/nitro/validator/server_common" + "github.com/redis/go-redis/v9" "github.com/spf13/pflag" ) diff --git a/validator/inputs/writer.go b/validator/inputs/writer.go index a45e584f52..1a476c52a3 100644 --- a/validator/inputs/writer.go +++ b/validator/inputs/writer.go @@ -17,20 +17,25 @@ import ( // // The path can be nested under a slug directory so callers can provide a // recognizable name to differentiate various contexts in which the InputJSON -// is being written. If the Writer is configured by calling SetSlug, then the +// is being written. If the Writer is configured by calling WithSlug, then the // path will be like: // // $HOME/.arbuitrum/validation-inputs///block_inputs_.json // +// The inclusion of BlockId in the file's name is on by default, however that can be disabled +// by calling WithBlockIdInFileNameEnabled(false). In which case, the path will be like: +// +// $HOME/.arbuitrum/validation-inputs///block_inputs.json +// // The inclusion of a timestamp directory is on by default to avoid conflicts which // would result in files being overwritten. However, the Writer can be configured // to not use a timestamp directory. If the Writer is configured by calling -// SetUseTimestampDir(false), then the path will be like: +// WithTimestampDirEnabled(false), then the path will be like: // // $HOME/.arbuitrum/validation-inputs//block_inputs_.json // // Finally, to give complete control to the clients, the base directory can be -// set directly with SetBaseDir. In which case, the path will be like: +// set directly with WithBaseDir. In which case, the path will be like: // // /block_inputs_.json // or @@ -38,10 +43,11 @@ import ( // or // ///block_inputs_.json type Writer struct { - clock Clock - baseDir string - slug string - useTimestampDir bool + clock Clock + baseDir string + slug string + useTimestampDir bool + useBlockIdInFileName bool } // WriterOption is a function that configures a Writer. @@ -66,10 +72,11 @@ func NewWriter(options ...WriterOption) (*Writer, error) { } baseDir := filepath.Join(homeDir, ".arbitrum", "validation-inputs") w := &Writer{ - clock: realClock{}, - baseDir: baseDir, - slug: "", - useTimestampDir: true, + clock: realClock{}, + baseDir: baseDir, + slug: "", + useTimestampDir: true, + useBlockIdInFileName: true, } for _, o := range options { o(w) @@ -114,6 +121,13 @@ func WithTimestampDirEnabled(useTimestampDir bool) WriterOption { } } +// WithBlockIdInFileNameEnabled controls the inclusion of Block Id in the input json file's name +func WithBlockIdInFileNameEnabled(useBlockIdInFileName bool) WriterOption { + return func(w *Writer) { + w.useBlockIdInFileName = useBlockIdInFileName + } +} + // Write writes the given InputJSON to a file in JSON format. func (w *Writer) Write(json *server_api.InputJSON) error { dir := w.baseDir @@ -132,9 +146,11 @@ func (w *Writer) Write(json *server_api.InputJSON) error { if err != nil { return err } - if err = os.WriteFile( - filepath.Join(dir, fmt.Sprintf("block_inputs_%d.json", json.Id)), - contents, 0600); err != nil { + fileName := "block_inputs.json" + if w.useBlockIdInFileName { + fileName = fmt.Sprintf("block_inputs_%d.json", json.Id) + } + if err = os.WriteFile(filepath.Join(dir, fileName), contents, 0600); err != nil { return err } return nil diff --git a/validator/valnode/redis/consumer.go b/validator/valnode/redis/consumer.go index e0d53ffb2e..4392a3c91e 100644 --- a/validator/valnode/redis/consumer.go +++ b/validator/valnode/redis/consumer.go @@ -161,9 +161,13 @@ func (s *ValidationServer) Start(ctx_in context.Context) { res, err := valRun.Await(ctx) if err != nil { log.Error("Error validating", "request value", work.req.Value, "error", err) + work.req.Ack() } else { log.Debug("done work", "thread", i, "workid", work.req.ID) - if err := s.consumers[work.moduleRoot].SetResult(ctx, work.req.ID, res); err != nil { + err := s.consumers[work.moduleRoot].SetResult(ctx, work.req.ID, res) + // Even in error we close ackNotifier as there's no retry mechanism here and closing it will alow other consumers to autoclaim + work.req.Ack() + if err != nil { log.Error("Error setting result for request", "id", work.req.ID, "result", res, "error", err) } log.Debug("set result", "thread", i, "workid", work.req.ID)