diff --git a/.github/workflows/casper-node.yml b/.github/workflows/casper-node.yml index 87015d34ab..67811db628 100644 --- a/.github/workflows/casper-node.yml +++ b/.github/workflows/casper-node.yml @@ -187,7 +187,7 @@ jobs: run: make build-test-artifacts CARGO_FLAGS=--release - name: archive smart contracts - run: tar -czf smart-contracts.tar.gz resources/local/chainspec.toml target/wasm32v1-none/release/*.wasm + run: tar -czf smart-contracts.tar.gz resources/local/chainspec.toml resources/local/preinstalls target/wasm32v1-none/release/*.wasm - name: upload smart contracts uses: actions/upload-artifact@v4 diff --git a/.gitignore b/.gitignore index f99cd150ab..45965d7aca 100644 --- a/.gitignore +++ b/.gitignore @@ -134,6 +134,7 @@ dist resources/production/*.wasm resources/node-storage/* resources/local/chainspec.toml +resources/local/preinstalls/ execution_engine_testing/test_support/resources/chainspec.toml diff --git a/Cargo.lock b/Cargo.lock index 8d76379060..2b669d0f54 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -8336,9 +8336,9 @@ dependencies = [ [[package]] name = "toml" -version = "1.1.2+spec-1.1.0" +version = "1.1.6+spec-1.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "81f3d15e84cbcd896376e6730314d59fb5a87f31e4b038454184435cd57defee" +checksum = "920602543f0911ab71da12c50d59701da54c196d1a2bf5cb4b75667f137a406a" dependencies = [ "indexmap 2.14.0", "serde_core", @@ -8419,9 +8419,9 @@ dependencies = [ [[package]] name = "toml_parser" -version = "1.1.2+spec-1.1.0" +version = "1.1.3+spec-1.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a2abe9b86193656635d2411dc43050282ca48aa31c2451210f4202550afb7526" +checksum = "1d38ac1cf9b95face32296c0a3ede1fdc270627c9d9c02a7274dd6d960dc4d56" dependencies = [ "winnow 1.0.3", ] @@ -8434,9 +8434,9 @@ checksum = "5d99f8c9a7727884afe522e9bd5edbfc91a3312b36a77b5fb8926e4c31a41801" [[package]] name = "toml_writer" -version = "1.1.1+spec-1.1.0" +version = "1.1.2+spec-1.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "756daf9b1013ebe47a8776667b466417e2d4c5679d441c26230efd9ef78692db" +checksum = "7d56353a2a665ad0f41a421187180aab746c8c325620617ad883a99a1cbe66d2" [[package]] name = "tower" @@ -8707,7 +8707,7 @@ dependencies = [ "serde_json", "target-triple", "termcolor", - "toml 1.1.2+spec-1.1.0", + "toml 1.1.6+spec-1.1.0", ] [[package]] diff --git a/EVM.md b/EVM.md index 379c878fa6..8b32b3637b 100644 --- a/EVM.md +++ b/EVM.md @@ -66,6 +66,8 @@ Implemented in this workspace: history or pre-block state updates. - [EIP-2935][eip-2935] block-hash history predeploy and native direct-call lookup backed by indexed Casper block headers. +- [Canonical utility preinstalls](#preinstalls) at their standard addresses at + genesis and protocol upgrade commit when EVM execution is enabled. Implemented in the sidecar workspace for validation: @@ -207,6 +209,131 @@ decisions for [EIP-7002][eip-7002] and [EIP-7251][eip-7251], and explicit Prague conformance coverage for [EIP-2537][eip-2537], [EIP-7623][eip-7623], and [EIP-7702][eip-7702]. +## Preinstalls + +Preinstalls are utility contracts whose runtime bytecode is installed directly +at their canonical EVM addresses. When `[evm].enabled = true`, the node upserts +them at genesis and during protocol upgrade commit, immediately after the EVM +predeploys. No deployment transaction is required. + +Installation is idempotent: matching code is retained, missing code records are +restored, and conflicting code is rejected. Existing account metadata, +balances, nonces, and storage are preserved. The chainspec supplies a list of +installation address and runtime Keccak-256 checksum pairs; runtime bytes are +separate protocol assets: + +```toml +[evm] +preinstalls = [ + # Multicall3; the second value is a 32-byte runtime checksum. + ["0xcA11bde05977b3631167028862bE2a173976CA11", "0xd5c15df687b16f2ff992fc8d767b4216323184a2bbc6ee2f9c398c318e770891"], +] +``` + +An omitted or empty list installs no utility contracts. The node has no +compiled preinstall registry. Addresses must contain exactly 20 bytes and +checksums exactly 32 bytes; when EVM is enabled, duplicate addresses, including +differences in hex case, are rejected during chainspec validation and upgrade +discovery. The list is serialized in the configured order and included in the +chainspec hash. All validators must receive the same finalized list and assets +before activation. Only runtime code is installed; constructors are not executed. +Removing an entry in a later chainspec does not delete existing on-chain code. + +The selected protocol directory is the directory containing `config.toml` and +`chainspec.toml`. Runtime lookup is always `preinstalls/.bin` in that +directory, using lowercase hash digits without `0x`. Each file contains raw +runtime bytes, not hexadecimal text or creation bytecode. There is no additional +node config setting, network download, or search through other protocol versions. + +When EVM is enabled, node startup validates every listed file. The upgrade +watcher validates the nearest staged package's assets when that version enables +EVM; missing, unreadable, empty or mismatched files are reported and withhold +that upgrade. The current node continues running and retries validation on +subsequent scans. +Genesis and upgrade commit reread and verify the files when EVM is enabled; +upgrade loading is covered by the existing commit timeout. The storage installer +also verifies separately supplied bytecode against the configured checksums. +With EVM disabled, preinstall key validation and file loading are skipped. + +`ChainspecRawBytes` contains the manifest and its existing companion files, not +these runtimes. A recipient needs the matching protocol assets to perform +activation. Protocol config archives and external launcher/staging tooling must +copy the complete `preinstalls/` directory beside the selected chainspec. The +canonical repository assets live in `resources/evm/preinstalls`; local chainspec +generation copies them into `resources/local/preinstalls`. + +For the initial EVM rollout, the protocol upgrade that first enables EVM +should contain a non-empty `[evm].preinstalls` list with the expected contracts +below. EntryPoint and SenderCreator must be included together. This is a +rollout requirement: the generic installer accepts an empty list and does +not populate the expected list automatically. + +The [local chainspec template](resources/local/chainspec.toml.in) configures no +utility preinstalls. The [integration tests](executor/evm/tests/preinstalls.rs) +pin the initial rollout list below, each contract under its upstream license: + +| Preinstall | License | Canonical address | Ethereum mainnet reference | +| --- | --- | --- | --- | +| Multicall3 | MIT | `0xcA11bde05977b3631167028862bE2a173976CA11` | [Etherscan: Read Contract](https://etherscan.io/address/0xcA11bde05977b3631167028862bE2a173976CA11#readContract) | +| Arachnid CREATE2 deployer | Unlicense | `0x4e59b44847b379578588920cA78FbF26c0B4956C` | [Etherscan: Code](https://etherscan.io/address/0x4e59b44847b379578588920cA78FbF26c0B4956C#code) | +| Safe Singleton Factory | MIT | `0x914d7Fec6aaC8cd542e72Bca78B30650d45643d7` | [Etherscan: Code](https://etherscan.io/address/0x914d7Fec6aaC8cd542e72Bca78B30650d45643d7#code) | +| ERC-2470 Singleton Factory | CC0-1.0 | `0xce0042B868300000d44A59004Da54A005ffdcf9f` | [Etherscan: Code](https://etherscan.io/address/0xce0042B868300000d44A59004Da54A005ffdcf9f#code) | +| Permit2 | MIT | `0x000000000022D473030F116dDEE9F6B43aC78BA3` | [Etherscan: Read Contract](https://etherscan.io/address/0x000000000022D473030F116dDEE9F6B43aC78BA3#readContract) | +| SenderCreator v0.8 | GPL-3.0 | `0x449ED7C3e6Fee6a97311d4b55475DF59C44AdD33` | [Etherscan: Read Contract](https://etherscan.io/address/0x449ED7C3e6Fee6a97311d4b55475DF59C44AdD33#readContract) | +| EntryPoint v0.8 | GPL-3.0; OpenZeppelin dependencies: MIT | `0x4337084D9E255Ff0702461CF8895CE9E3b5Ff108` | [Etherscan: Read Contract](https://etherscan.io/address/0x4337084D9E255Ff0702461CF8895CE9E3b5Ff108#readContract) | + +The Etherscan references open the same addresses on Ethereum mainnet for +comparing bytecode, interfaces, and read results. Multicall3 aggregates +contract calls in one EVM execution and exposes block and chain information. +Getters such as `getChainId()`, `getBlockNumber()`, and `getBasefee()` return +values for the network on which they execute. + +The Arachnid deployer is Foundry's default CREATE2 factory. Calls contain a +32-byte salt followed by creation bytecode, and return the created address as +20 bytes. This permits deterministic deployments using ordinary transactions +at the configured gas price, without the factory's presigned bootstrap +transaction. See the [upstream usage guide](https://github.com/Arachnid/deterministic-deployment-proxy). + +Safe Singleton Factory exposes the same raw salt/creation-code interface at +the address used by Safe's deployment tooling. Preinstalling the factory +provides deterministic deployment infrastructure; Safe wallet implementations +and proxy factories are deployed separately. See the [Safe factory guide](https://github.com/safe-fndn/safe-singleton-factory). + +The [ERC-2470 Singleton Factory](https://eips.ethereum.org/EIPS/eip-2470) +provides the ABI method `deploy(bytes initCode, bytes32 salt)`. It deploys via +CREATE2 with zero value and returns an ABI-encoded address, or the zero +address on failure. Its calldata and failure behavior differ from the two +raw factories above. + +[Permit2](https://docs.uniswap.org/contracts/permit2/overview) provides shared +ERC-20 allowance management and signature-based transfers. Tokens still need +to authorize Permit2 through their own `approve` method. Its EIP-712 domain +uses the executing network's chain ID and canonical Permit2 address; the +mainnet runtime's cached domain is recomputed on other chain IDs. Allowances +and signature nonces begin empty and are preserved through protocol upgrades. + +[EntryPoint v0.8](https://github.com/eth-infinitism/account-abstraction/tree/4cbc06072cdc19fd60f285c5997f4f7f57a588de) +processes ERC-4337 UserOperations. It and SenderCreator must be installed +together: their mainnet runtimes already contain the immutable addresses that +link the pair, and SenderCreator authorizes only that EntryPoint. The installed +EntryPoint recomputes its EIP-712 domain on other chain IDs and uses the +Prague-supported transient reentrancy guard. Deposits, stakes, and nonce +mappings begin empty and are preserved through upgrades. + +Bundlers, account factories, and wallet integrations require configuration for +the network, and bundled transactions pay the configured fees. + +Preinstallation leaves the base fee and transaction admission rules unchanged. +Runtime bytecode and attribution are retained in +[`resources/evm/preinstalls`](resources/evm/preinstalls/README.md). The README +records pinned sources, addresses, runtime hashes, lengths, and constructor +immutables; complete upstream license notices accompany the binaries. + +Distributors must preserve the notices and provide the exact corresponding +source for GPL runtimes while distributing them. Later network releases can +extend the table with additional canonical tools; existing matching contracts +are retained by the idempotent upsert. + ## Transaction Shape `Transaction::Evm` stores `casper_types::evm::Transaction` directly, like the diff --git a/Makefile b/Makefile index 8c36061685..5fe1b4e7d4 100644 --- a/Makefile +++ b/Makefile @@ -164,6 +164,9 @@ lint-smart-contracts: cd smart_contracts/contract && $(CARGO) clippy --all-targets -- -A renamed_and_removed_lints .PHONY: audit-rs +# RUSTSEC-2026-0344: Wasmer 6.1 uses object::write; object's default +# features pull in ruzstd, but no zstd decompression APIs are called. +# Remove this exception when Wasmer drops the affected ruzstd dependency. audit-rs: $(CARGO) audit \ --ignore RUSTSEC-2025-0055 \ @@ -172,7 +175,8 @@ audit-rs: --ignore RUSTSEC-2026-0233 \ --ignore RUSTSEC-2026-0234 \ --ignore RUSTSEC-2026-0235 \ - --ignore RUSTSEC-2026-0258 + --ignore RUSTSEC-2026-0258 \ + --ignore RUSTSEC-2026-0344 .PHONY: audit audit: audit-rs diff --git a/ci/build_update_package.sh b/ci/build_update_package.sh index 1531b5469d..27f45f7bd3 100755 --- a/ci/build_update_package.sh +++ b/ci/build_update_package.sh @@ -70,6 +70,7 @@ mkdir -p "$CONFIG_DIR" cp "$GENESIS_FILES_DIR/chainspec.toml" "$CONFIG_DIR" cp "$GENESIS_FILES_DIR/config-example.toml" "$CONFIG_DIR" cp "$GENESIS_FILES_DIR/accounts.toml" "$CONFIG_DIR" +cp -R "$ROOT_DIR/resources/evm/preinstalls" "$CONFIG_DIR/" # To get no path in tar, need to cd in. cd "$CONFIG_DIR" || exit tar -czvf "../config.tar.gz" . diff --git a/ci/nctl_upgrade_stage.sh b/ci/nctl_upgrade_stage.sh index 5c8c66e2ea..4531173f8b 100755 --- a/ci/nctl_upgrade_stage.sh +++ b/ci/nctl_upgrade_stage.sh @@ -70,6 +70,9 @@ for i in "${BIN_ARRAY[@]}"; do echo "" done +# Protocol runtime assets and their attribution must travel with the configs. +cp -R "$ROOT_DIR/resources/evm/preinstalls" "$TEMP_STAGE_DIR/" + # Copy wasm to staging dir for i in "${WASM_ARRAY[@]}"; do if [ -f "$WASM_BUILD_DIR/$i" ]; then diff --git a/executor/evm/tests/executor.rs b/executor/evm/tests/executor.rs index 3e5ebe2a90..bdc68120bf 100644 --- a/executor/evm/tests/executor.rs +++ b/executor/evm/tests/executor.rs @@ -124,6 +124,7 @@ fn executor_with_base_fee(spec: EvmSpec, base_fee: u64) -> EvmExecutor { base_fee, wei_per_mote: DEFAULT_WEI_PER_MOTE, transaction_lanes: Vec::new(), + preinstalls: Default::default(), }) } @@ -1925,6 +1926,7 @@ fn whole_mote_value_executes_in_wei_and_persists_without_dust() { base_fee: 0, wei_per_mote: DEFAULT_WEI_PER_MOTE, transaction_lanes: Vec::new(), + preinstalls: Default::default(), }); let sender = evm::Address::new([0x31; 20]); let recipient = evm::Address::new([0x32; 20]); @@ -3249,6 +3251,7 @@ fn unchecked_call_with_calldata_does_not_underflow_unfunded_sender() { base_fee: 1_000_000, wei_per_mote: DEFAULT_WEI_PER_MOTE, transaction_lanes: Vec::new(), + preinstalls: Default::default(), }; let gas_price = evm_config.base_fee_wei(); let executor = EvmExecutor::new(evm_config); @@ -3435,6 +3438,7 @@ fn signed_transactions_require_configured_chain_id() { base_fee: 0, wei_per_mote: DEFAULT_WEI_PER_MOTE, transaction_lanes: Vec::new(), + preinstalls: Default::default(), }); let transaction = legacy_transaction(Some(7)); let request = ExecuteRequest { diff --git a/executor/evm/tests/preinstalls.rs b/executor/evm/tests/preinstalls.rs new file mode 100644 index 0000000000..cf81e1e648 --- /dev/null +++ b/executor/evm/tests/preinstalls.rs @@ -0,0 +1,986 @@ +use std::{collections::BTreeMap, convert::TryInto}; + +use alloy_primitives::{hex, keccak256}; +use casper_executor_evm::{ + BlockContext, CallRequest, CallValidation, EvmExecutor, ExecuteKind, ExecuteRequest, + ExecutionOutcome, ExecutionStatus, +}; +use casper_storage::{ + block_store::lmdb::LmdbBlockStore, + data_access_layer::{ + DataAccessLayer, GenesisRequest, GenesisResult, ProtocolUpgradeRequest, + ProtocolUpgradeResult, + }, + eip2935, eip4788, + global_state::state::{ + lmdb::{make_temporary_global_state, LmdbGlobalState}, + CommitProvider, StateProvider, + }, + system::protocol_upgrade::ProtocolUpgradeError, +}; +use casper_types::{ + bytesrepr::Bytes, evm, execution::TransformKindV2, ByteCode, ByteCodeKind, CLValue, + ChainspecRegistry, Digest, EraId, EvmAddr, EvmConfig, FeeHandling, GenesisAccount, + GenesisConfig, HoldBalanceHandling, Key, Motes, ProtocolUpgradeConfig, ProtocolVersion, + PublicKey, RewardsHandling, SecretKey, StorageCosts, StoredValue, SystemConfig, WasmConfig, + U256, U512, +}; + +#[derive(Clone, Copy)] +struct TestPreinstall { + name: &'static str, + address: evm::Address, + code_hash: evm::Hash, +} + +impl TestPreinstall { + fn load(self) -> (evm::Address, Vec) { + let code = std::fs::read( + std::path::Path::new(env!("CARGO_MANIFEST_DIR")) + .join("../../resources/evm/preinstalls") + .join(format!("{}.bin", self.code_hash.to_hex_string())), + ) + .unwrap(); + (self.address, code) + } +} + +const MULTICALL3: TestPreinstall = TestPreinstall { + name: "Multicall3", + address: evm::Address::new(hex!("0xcA11bde05977b3631167028862bE2a173976CA11")), + code_hash: evm::Hash::new(hex!( + "d5c15df687b16f2ff992fc8d767b4216323184a2bbc6ee2f9c398c318e770891" + )), +}; + +const CREATE2_DEPLOYER: TestPreinstall = TestPreinstall { + name: "Arachnid CREATE2 deployer", + address: evm::Address::new(hex!("0x4e59b44847b379578588920cA78FbF26c0B4956C")), + code_hash: evm::Hash::new(hex!( + "2fa86add0aed31f33a762c9d88e807c475bd51d0f52bd0955754b2608f7e4989" + )), +}; + +const SAFE_SINGLETON_FACTORY: TestPreinstall = TestPreinstall { + name: "Safe Singleton Factory", + address: evm::Address::new(hex!("0x914d7Fec6aaC8cd542e72Bca78B30650d45643d7")), + code_hash: CREATE2_DEPLOYER.code_hash, +}; + +const ERC2470_SINGLETON_FACTORY: TestPreinstall = TestPreinstall { + name: "ERC-2470 Singleton Factory", + address: evm::Address::new(hex!("0xce0042B868300000d44A59004Da54A005ffdcf9f")), + code_hash: evm::Hash::new(hex!( + "c4d5542b53a8b779595a20a8ddd60e58a6c49d3c3decc2df83ced1c69c8ca807" + )), +}; + +const PERMIT2: TestPreinstall = TestPreinstall { + name: "Permit2", + address: evm::Address::new(hex!("0x000000000022D473030F116dDEE9F6B43aC78BA3")), + code_hash: evm::Hash::new(hex!( + "c67d1657868aa5146eaf24fb879fb1fdec3d2d493b3683a61c9c2f4fb2851131" + )), +}; + +const SENDER_CREATOR_V08: TestPreinstall = TestPreinstall { + name: "SenderCreator v0.8", + address: evm::Address::new(hex!("0x449ED7C3e6Fee6a97311d4b55475DF59C44AdD33")), + code_hash: evm::Hash::new(hex!( + "c69a1b3a000d570bc86eb096ee63a9014a17951ad616d720882ec61432b00fcf" + )), +}; + +const ENTRYPOINT_V08: TestPreinstall = TestPreinstall { + name: "EntryPoint v0.8", + address: evm::Address::new(hex!("0x4337084D9E255Ff0702461CF8895CE9E3b5Ff108")), + code_hash: evm::Hash::new(hex!( + "44e632a24c6f2600cbd5b5b8b4c2d372359112c8b5774297f5fd0a9e64f11f86" + )), +}; + +const TEST_PREINSTALL: evm::Address = evm::Address::new([0x11; 20]); +const OTHER_PREINSTALL: evm::Address = evm::Address::new([0x22; 20]); +// Calls return the word 42. +const TEST_RUNTIME: &[u8] = &hex!("602a60005260206000f3"); + +fn test_preinstalls() -> [(evm::Address, Vec); 2] { + [TEST_PREINSTALL, OTHER_PREINSTALL].map(|address| (address, TEST_RUNTIME.to_vec())) +} + +fn evm_config(enabled: bool, preinstalls: &[(evm::Address, Vec)]) -> EvmConfig { + EvmConfig { + enabled, + chain_id: 7, + base_fee: 5_000, + preinstalls: preinstalls + .iter() + .map(|(address, code)| (*address, evm::Hash::new(keccak256(code).0))) + .collect(), + ..Default::default() + } +} + +fn preinstall_bytecodes(preinstalls: &[(evm::Address, Vec)]) -> Vec<(evm::Hash, Bytes)> { + preinstalls + .iter() + .map(|(_, code)| (evm::Hash::new(keccak256(code).0), code.clone().into())) + .collect() +} + +fn genesis( + enabled: bool, + enable_entity: bool, + preinstalls: &[(evm::Address, Vec)], +) -> (LmdbGlobalState, Digest, impl Send) { + let (state, _, tempdir) = make_temporary_global_state([]); + let secret = SecretKey::ed25519_from_bytes([1; 32]).unwrap(); + let config = GenesisConfig::new( + vec![GenesisAccount::Account { + public_key: PublicKey::from(&secret), + balance: Motes::new(U512::from(1_000_000_000_000u64)), + validator: None, + }], + evm_config(enabled, preinstalls), + WasmConfig::default(), + SystemConfig::default(), + 10, + 10, + 0, + Default::default(), + 14, + 0, + HoldBalanceHandling::Accrued, + 0, + enable_entity, + None, + StorageCosts::default(), + 0, + ) + .with_evm_preinstall_bytecodes(preinstall_bytecodes(preinstalls)); + let result = state.genesis(GenesisRequest::new( + Digest::hash("preinstall-test-genesis"), + ProtocolVersion::V2_0_0, + config, + ChainspecRegistry::new_with_genesis(b"chainspec", b"accounts"), + )); + let GenesisResult::Success { + post_state_hash, .. + } = result + else { + panic!("genesis failed: {result:?}"); + }; + (state, post_state_hash, tempdir) +} + +fn upgrade_request( + root: Digest, + current_patch: u32, + enabled: bool, + enable_entity: bool, + updates: BTreeMap, + preinstalls: &[(evm::Address, Vec)], +) -> ProtocolUpgradeRequest { + let config = ProtocolUpgradeConfig::new( + root, + ProtocolVersion::from_parts(2, 0, current_patch), + ProtocolVersion::from_parts(2, 0, current_patch + 1), + Some(EraId::new(1)), + None, + None, + None, + None, + None, + None, + None, + updates, + ChainspecRegistry::new_with_optional_global_state(b"upgrade", None), + evm_config(enabled, preinstalls), + FeeHandling::NoFee, + 0, + u64::MAX, + 0, + enable_entity, + RewardsHandling::Standard, + None, + ) + .with_evm_preinstall_bytecodes(preinstall_bytecodes(preinstalls)); + ProtocolUpgradeRequest::new(config) +} + +fn code_hash_key(address: evm::Address) -> Key { + Key::Evm(EvmAddr::CodeHash(address)) +} + +fn bytecode_key(code: &[u8]) -> Key { + Key::Evm(EvmAddr::ByteCode(evm::Hash::new(keccak256(code).0))) +} + +fn read(state: &LmdbGlobalState, root: Digest, key: Key) -> Option { + state + .tracking_copy(root) + .unwrap() + .unwrap() + .read(&key) + .unwrap() +} + +fn assert_preinstalls( + state: &LmdbGlobalState, + root: Digest, + preinstalls: &[(evm::Address, Vec)], +) { + for (address, code) in preinstalls { + assert_eq!( + read(state, root, code_hash_key(*address)), + Some(StoredValue::CLValue( + CLValue::from_t(evm::Hash::new(keccak256(code).0)).unwrap(), + )), + "{address} code hash", + ); + assert_eq!( + read(state, root, bytecode_key(code)), + Some(StoredValue::ByteCode(ByteCode::new( + ByteCodeKind::EvmPrague, + code.clone(), + ))), + "{address} runtime", + ); + } +} + +fn call_contract( + state: LmdbGlobalState, + root: Digest, + address: evm::Address, + input: Vec, +) -> ExecutionOutcome { + call_sequence(state, root, &[(address, input)]).remove(0) +} + +fn call_sequence( + state: LmdbGlobalState, + root: Digest, + calls: &[(evm::Address, Vec)], +) -> Vec { + call_sequence_with_config(state, root, calls, evm_config(true, &[])) +} + +fn call_sequence_with_config( + state: LmdbGlobalState, + root: Digest, + calls: &[(evm::Address, Vec)], + config: EvmConfig, +) -> Vec { + let data_access_layer = DataAccessLayer { + state, + block_store: LmdbBlockStore::new_temporary(64 * 1024 * 1024).unwrap(), + max_query_depth: 5, + enable_addressable_entity: false, + }; + let mut tracking_copy = data_access_layer + .state + .tracking_copy(root) + .unwrap() + .unwrap(); + let executor = EvmExecutor::new(config); + calls + .iter() + .enumerate() + .map(|(index, (address, input))| { + executor + .execute( + &data_access_layer, + &mut tracking_copy, + ExecuteRequest { + block: BlockContext { + number: 42, + timestamp: 1_714_000_000, + beneficiary: evm::Address::ZERO, + gas_limit: None, + base_fee: None, + prevrandao: evm::Hash::new([0x42; 32]), + }, + kind: ExecuteKind::Call(CallRequest { + from: evm::Address::new([2; 20]), + to: Some(*address), + value: U256::zero(), + input: input.clone(), + gas_limit: 1_000_000, + gas_price: 0, + nonce: index as u64, + validation: CallValidation::UncheckedSimulation, + }), + }, + ) + .expect("preinstalled contract execution should succeed") + }) + .collect() +} + +fn selector(signature: &str) -> Vec { + keccak256(signature.as_bytes())[..4].to_vec() +} + +fn word(value: u64) -> [u8; 32] { + let mut result = [0; 32]; + result[24..].copy_from_slice(&value.to_be_bytes()); + result +} + +fn address_word(address: evm::Address) -> [u8; 32] { + let mut result = [0; 32]; + result[12..].copy_from_slice(address.as_bytes()); + result +} + +fn bytes_call(signature: &str, data: &[u8]) -> Vec { + let mut input = selector(signature); + input.extend_from_slice(&word(32)); + input.extend_from_slice(&word(data.len() as u64)); + input.extend_from_slice(data); + input.resize(4 + 64 + data.len().div_ceil(32) * 32, 0); + input +} + +// Creation code returning a ten-byte runtime whose calls return the word 42. +const TEST_INIT_CODE: &[u8] = &hex!("600a600c600039600a6000f3602a60005260206000f3"); + +fn create2_address(factory: evm::Address, salt: [u8; 32]) -> evm::Address { + let mut input = vec![0xff]; + input.extend_from_slice(factory.as_bytes()); + input.extend_from_slice(&salt); + input.extend_from_slice(keccak256(TEST_INIT_CODE).as_slice()); + evm::Address::new(keccak256(input)[12..].try_into().unwrap()) +} + +fn singleton_factory_input(salt: [u8; 32]) -> Vec { + let mut input = selector("deploy(bytes,bytes32)"); + input.extend_from_slice(&word(64)); + input.extend_from_slice(&salt); + input.extend_from_slice(&word(TEST_INIT_CODE.len() as u64)); + input.extend_from_slice(TEST_INIT_CODE); + input.resize(4 + 128, 0); + input +} + +fn assert_raw_create2_deployment(factory: TestPreinstall) { + let salt = [0x42; 32]; + let address = create2_address(factory.address, salt); + let mut input = salt.to_vec(); + input.extend_from_slice(TEST_INIT_CODE); + let (state, root, _tempdir) = genesis(true, false, &[factory.load()]); + let outcomes = call_sequence( + state, + root, + &[ + (factory.address, input.clone()), + (address, Vec::new()), + (factory.address, input), + ], + ); + assert_eq!(outcomes[0].status, ExecutionStatus::Success); + assert_eq!(outcomes[0].output, address.as_bytes()); + assert_eq!(outcomes[1].status, ExecutionStatus::Success); + assert_eq!(outcomes[1].output, word(42)); + assert_eq!(outcomes[2].status, ExecutionStatus::Revert); +} + +fn aggregate3_input(calls: &[(&str, bool)]) -> Vec { + let mut input = selector("aggregate3((address,bool,bytes)[])"); + input.extend_from_slice(&word(32)); + input.extend_from_slice(&word(calls.len() as u64)); + // Array offsets are relative to the first element-offset word. Every + // tuple contains a 96-byte header and a 64-byte encoded function selector. + for index in 0..calls.len() { + input.extend_from_slice(&word((calls.len() * 32 + index * 160) as u64)); + } + for (signature, allow_failure) in calls { + input.extend_from_slice(&[0; 12]); + input.extend_from_slice(MULTICALL3.address.as_bytes()); + input.extend_from_slice(&word(u64::from(*allow_failure))); + input.extend_from_slice(&word(96)); + input.extend_from_slice(&word(4)); + input.extend_from_slice(&selector(signature)); + input.extend_from_slice(&[0; 28]); + } + input +} + +#[test] +fn canonical_runtimes_have_the_expected_lengths_and_hashes() { + for (contract, length) in [ + (MULTICALL3, 3_808), + (CREATE2_DEPLOYER, 69), + (SAFE_SINGLETON_FACTORY, 69), + (ERC2470_SINGLETON_FACTORY, 308), + (PERMIT2, 9_152), + (SENDER_CREATOR_V08, 1_217), + (ENTRYPOINT_V08, 21_738), + ] { + let (_, code) = contract.load(); + assert_eq!(code.len(), length, "{} length", contract.name); + assert_eq!( + evm::Hash::new(keccak256(&code).0), + contract.code_hash, + "{} hash", + contract.name + ); + } +} + +#[test] +fn enabled_evm_with_empty_preinstalls_installs_only_predeploys() { + for enable_entity in [false, true] { + let (state, root, _tempdir) = genesis(true, enable_entity, &[]); + for address in [ + eip4788::BEACON_ROOTS_ADDRESS, + eip2935::BLOCK_HASH_HISTORY_ADDRESS, + ] { + assert!(read(&state, root, code_hash_key(address)).is_some()); + } + for address in [TEST_PREINSTALL, OTHER_PREINSTALL] { + assert!(read(&state, root, code_hash_key(address)).is_none()); + } + let result = state.protocol_upgrade(upgrade_request( + root, + 0, + true, + enable_entity, + BTreeMap::new(), + &[], + )); + let ProtocolUpgradeResult::Success { + post_state_hash, .. + } = result + else { + panic!("empty-map upgrade failed: {result:?}"); + }; + for address in [TEST_PREINSTALL, OTHER_PREINSTALL] { + assert!(read(&state, post_state_hash, code_hash_key(address)).is_none()); + } + } +} + +#[test] +fn chainspec_can_install_arbitrary_runtime_and_extend_it_on_upgrade() { + let preinstalls = test_preinstalls(); + for enable_entity in [false, true] { + let (state, root, _tempdir) = genesis(true, enable_entity, &preinstalls[..1]); + assert_preinstalls(&state, root, &preinstalls[..1]); + assert!(read(&state, root, code_hash_key(OTHER_PREINSTALL)).is_none()); + let result = state.protocol_upgrade(upgrade_request( + root, + 0, + true, + enable_entity, + BTreeMap::new(), + &preinstalls[1..], + )); + let ProtocolUpgradeResult::Success { + post_state_hash, .. + } = result + else { + panic!("custom-map upgrade failed: {result:?}"); + }; + assert_preinstalls(&state, post_state_hash, &preinstalls); + let outcomes = call_sequence( + state, + post_state_hash, + &[ + (TEST_PREINSTALL, Vec::new()), + (OTHER_PREINSTALL, Vec::new()), + ], + ); + for outcome in outcomes { + assert_eq!(outcome.status, ExecutionStatus::Success); + assert_eq!(outcome.output, word(42)); + } + } +} + +#[test] +fn genesis_and_upgrade_install_preinstalls_idempotently() { + let preinstalls = test_preinstalls(); + for at_genesis in [false, true] { + for enable_entity in [false, true] { + let (state, before, _tempdir) = genesis(at_genesis, enable_entity, &preinstalls); + let (root, current_patch) = if at_genesis { + (before, 0) + } else { + let result = state.protocol_upgrade(upgrade_request( + before, + 0, + true, + enable_entity, + BTreeMap::new(), + &preinstalls, + )); + let ProtocolUpgradeResult::Success { + post_state_hash, .. + } = result + else { + panic!("upgrade failed: {result:?}"); + }; + assert!(read(&state, before, code_hash_key(TEST_PREINSTALL)).is_none()); + (post_state_hash, 1) + }; + assert_preinstalls(&state, root, &preinstalls); + for address in [ + eip4788::BEACON_ROOTS_ADDRESS, + eip2935::BLOCK_HASH_HISTORY_ADDRESS, + ] { + assert!(read(&state, root, code_hash_key(address)).is_some()); + } + assert!(read(&state, root, Key::Evm(EvmAddr::Nonce(TEST_PREINSTALL))).is_none()); + assert!(read(&state, root, Key::Evm(EvmAddr::Account(TEST_PREINSTALL))).is_none()); + + let result = state.protocol_upgrade(upgrade_request( + root, + current_patch, + true, + enable_entity, + BTreeMap::new(), + &preinstalls, + )); + let ProtocolUpgradeResult::Success { + post_state_hash, + effects, + } = result + else { + panic!("repeated upgrade failed: {result:?}"); + }; + assert_preinstalls(&state, post_state_hash, &preinstalls); + for transform in effects.transforms() { + if preinstalls.iter().any(|(address, code)| { + *transform.key() == code_hash_key(*address) + || *transform.key() == bytecode_key(code) + }) { + assert_eq!(*transform.kind(), TransformKindV2::Identity); + } + } + + let outcome = call_contract(state, post_state_hash, TEST_PREINSTALL, Vec::new()); + assert_eq!(outcome.status, ExecutionStatus::Success); + assert_eq!(outcome.output, word(42)); + } + } +} + +#[test] +fn disabled_genesis_and_upgrade_skip_preinstalls() { + let preinstalls = test_preinstalls(); + for enable_entity in [false, true] { + let (state, root, _tempdir) = genesis(false, enable_entity, &preinstalls); + for (address, code) in &preinstalls { + assert!(read(&state, root, code_hash_key(*address)).is_none()); + assert!(read(&state, root, bytecode_key(code)).is_none()); + } + for address in [ + eip4788::BEACON_ROOTS_ADDRESS, + eip2935::BLOCK_HASH_HISTORY_ADDRESS, + ] { + assert!(read(&state, root, code_hash_key(address)).is_none()); + } + + let result = state.protocol_upgrade(upgrade_request( + root, + 0, + false, + enable_entity, + BTreeMap::new(), + &preinstalls, + )); + let ProtocolUpgradeResult::Success { + post_state_hash, .. + } = result + else { + panic!("disabled EVM upgrade failed: {result:?}"); + }; + for (address, code) in &preinstalls { + assert!(read(&state, post_state_hash, code_hash_key(*address)).is_none()); + assert!(read(&state, post_state_hash, bytecode_key(code)).is_none()); + } + } +} + +#[test] +fn upgrade_rejects_preinstall_conflicts_after_predeploys() { + let preinstalls = test_preinstalls(); + let (state, root, _tempdir) = genesis(false, false, &preinstalls); + let conflicting_code = StoredValue::CLValue(CLValue::from_t(evm::Hash::new([1; 32])).unwrap()); + let mut updates = BTreeMap::new(); + updates.insert(code_hash_key(TEST_PREINSTALL), conflicting_code.clone()); + + let result = state.protocol_upgrade(upgrade_request( + root, + 0, + true, + false, + updates.clone(), + &preinstalls, + )); + assert!(matches!( + result, + ProtocolUpgradeResult::Failure(ProtocolUpgradeError::EvmPreinstall(_)) + )); + assert!(read(&state, root, code_hash_key(TEST_PREINSTALL)).is_none()); + assert!(read(&state, root, bytecode_key(TEST_RUNTIME)).is_none()); + assert!(read( + &state, + root, + code_hash_key(eip2935::BLOCK_HASH_HISTORY_ADDRESS) + ) + .is_none()); + + updates.insert( + code_hash_key(eip2935::BLOCK_HASH_HISTORY_ADDRESS), + conflicting_code, + ); + let result = + state.protocol_upgrade(upgrade_request(root, 0, true, false, updates, &preinstalls)); + assert!(matches!( + result, + ProtocolUpgradeResult::Failure(ProtocolUpgradeError::EvmPredeploy(_)) + )); +} + +#[test] +fn upgrade_repairs_missing_preinstall_bytecode() { + let preinstalls = test_preinstalls(); + let (state, root, _tempdir) = genesis(false, false, &preinstalls); + let root = state + .commit_values( + root, + vec![( + code_hash_key(TEST_PREINSTALL), + StoredValue::CLValue( + CLValue::from_t(evm::Hash::new(keccak256(TEST_RUNTIME).0)).unwrap(), + ), + )], + Default::default(), + ) + .unwrap(); + let result = state.protocol_upgrade(upgrade_request( + root, + 0, + true, + false, + BTreeMap::new(), + &preinstalls, + )); + let ProtocolUpgradeResult::Success { + post_state_hash, .. + } = result + else { + panic!("repair upgrade failed: {result:?}"); + }; + assert_preinstalls(&state, post_state_hash, &preinstalls); +} + +#[test] +fn preinstalled_multicall3_aggregates_calls_and_honors_allow_failure() { + let preinstalls = [MULTICALL3.load()]; + for allow_failure in [true, false] { + let (state, root, _tempdir) = genesis(false, false, &[]); + let result = state.protocol_upgrade(upgrade_request( + root, + 0, + true, + false, + BTreeMap::new(), + &preinstalls, + )); + let ProtocolUpgradeResult::Success { + post_state_hash, .. + } = result + else { + panic!("upgrade failed: {result:?}"); + }; + let outcome = call_contract( + state, + post_state_hash, + MULTICALL3.address, + aggregate3_input(&[ + ("getBlockNumber()", false), + ("getChainId()", false), + ("missing()", allow_failure), + ]), + ); + if !allow_failure { + assert_eq!(outcome.status, ExecutionStatus::Revert); + continue; + } + assert_eq!(outcome.status, ExecutionStatus::Success); + assert_eq!(&outcome.output[..32], &word(32)); + assert_eq!(&outcome.output[32..64], &word(3)); + for (index, expected) in [42, 7].into_iter().enumerate() { + let offset_word = &outcome.output[64 + index * 32..96 + index * 32]; + let offset = u64::from_be_bytes(offset_word[24..].try_into().unwrap()) as usize; + let tuple = &outcome.output[64 + offset..]; + assert_eq!(&tuple[..32], &word(1)); + assert_eq!(&tuple[32..64], &word(64)); + assert_eq!(&tuple[64..96], &word(32)); + assert_eq!(&tuple[96..128], &word(expected)); + } + let offset = u64::from_be_bytes(outcome.output[152..160].try_into().unwrap()) as usize; + let failed_tuple = &outcome.output[64 + offset..]; + assert_eq!(&failed_tuple[..32], &word(0)); + assert_eq!(&failed_tuple[64..96], &word(0)); + } +} + +#[test] +fn preinstalled_create2_deployer_deploys_at_the_deterministic_address() { + assert_raw_create2_deployment(CREATE2_DEPLOYER); +} + +#[test] +fn preinstalled_safe_singleton_factory_deploys_at_the_deterministic_address() { + assert_raw_create2_deployment(SAFE_SINGLETON_FACTORY); +} + +#[test] +fn preinstalled_erc2470_factory_deploys_and_returns_zero_on_collision() { + let salt = [0x42; 32]; + let address = create2_address(ERC2470_SINGLETON_FACTORY.address, salt); + let input = singleton_factory_input(salt); + let (state, root, _tempdir) = genesis(true, false, &[ERC2470_SINGLETON_FACTORY.load()]); + let outcomes = call_sequence( + state, + root, + &[ + (ERC2470_SINGLETON_FACTORY.address, input.clone()), + (address, Vec::new()), + (ERC2470_SINGLETON_FACTORY.address, input), + ], + ); + assert_eq!(outcomes[0].status, ExecutionStatus::Success); + assert_eq!(outcomes[0].output, address_word(address)); + assert_eq!(outcomes[1].status, ExecutionStatus::Success); + assert_eq!(outcomes[1].output, word(42)); + assert_eq!(outcomes[2].status, ExecutionStatus::Success); + assert_eq!(outcomes[2].output, word(0)); +} + +fn permit2_domain_separator(chain_id: u64) -> [u8; 32] { + let mut domain = Vec::new(); + domain.extend_from_slice( + keccak256("EIP712Domain(string name,uint256 chainId,address verifyingContract)").as_slice(), + ); + domain.extend_from_slice(keccak256("Permit2").as_slice()); + domain.extend_from_slice(&word(chain_id)); + domain.extend_from_slice(&address_word(PERMIT2.address)); + keccak256(domain).0 +} + +#[test] +fn preinstalled_permit2_domain_separator_uses_the_current_chain_id() { + for chain_id in [1, 7, 31_337] { + let (state, root, _tempdir) = genesis(true, false, &[PERMIT2.load()]); + let config = EvmConfig { + chain_id, + ..evm_config(true, &[]) + }; + let outcomes = call_sequence_with_config( + state, + root, + &[(PERMIT2.address, selector("DOMAIN_SEPARATOR()"))], + config, + ); + assert_eq!(outcomes[0].status, ExecutionStatus::Success); + assert_eq!(outcomes[0].output, permit2_domain_separator(chain_id)); + } +} + +#[test] +fn preinstalled_permit2_accepts_a_signed_allowance_and_rejects_replay() { + let owner = evm::Address::new(hex!("7E5F4552091A69125d5DfCb7b8C2659029395Bdf")); + let token = evm::Address::new([3; 20]); + let spender = evm::Address::new([4; 20]); + // EIP-712 PermitSingle signed with the public test private key 1, for chain 7, + // this Permit2 address, token/spender below, amount 123, expiry/deadline + // 1_800_000_000, and nonce 0. Generated independently with `cast wallet sign`. + let signature = hex!("056bc518f3598cd1cf57a812964edec4272b771dc0a99ebf3a15266a55c6e0e838337fe89ad91837b517eee46473376525f0aaa96936228551484bbb98d3efac1b"); + let mut permit = + selector("permit(address,((address,uint160,uint48,uint48),address,uint256),bytes)"); + permit.extend_from_slice(&address_word(owner)); + permit.extend_from_slice(&address_word(token)); + permit.extend_from_slice(&word(123)); + permit.extend_from_slice(&word(1_800_000_000)); + permit.extend_from_slice(&word(0)); + permit.extend_from_slice(&address_word(spender)); + permit.extend_from_slice(&word(1_800_000_000)); + permit.extend_from_slice(&word(256)); + permit.extend_from_slice(&word(signature.len() as u64)); + permit.extend_from_slice(&signature); + permit.resize(4 + 256 + 128, 0); + + let mut allowance = selector("allowance(address,address,address)"); + allowance.extend_from_slice(&address_word(owner)); + allowance.extend_from_slice(&address_word(token)); + allowance.extend_from_slice(&address_word(spender)); + let (state, root, _tempdir) = genesis(true, false, &[PERMIT2.load()]); + let outcomes = call_sequence( + state, + root, + &[ + (PERMIT2.address, permit.clone()), + (PERMIT2.address, allowance), + (PERMIT2.address, permit), + ], + ); + assert_eq!(outcomes[0].status, ExecutionStatus::Success); + assert_eq!(outcomes[1].status, ExecutionStatus::Success); + assert_eq!( + outcomes[1].output, + [word(123), word(1_800_000_000), word(1)].concat() + ); + assert_eq!(outcomes[2].status, ExecutionStatus::Revert); + assert_eq!(outcomes[2].output, selector("InvalidNonce()")); +} + +#[test] +fn preinstalled_sender_creator_is_linked_and_rejects_unauthorized_creation() { + let (state, root, _tempdir) = genesis(true, false, &[SENDER_CREATOR_V08.load()]); + let outcomes = call_sequence( + state, + root, + &[ + (SENDER_CREATOR_V08.address, selector("entryPoint()")), + ( + SENDER_CREATOR_V08.address, + bytes_call("createSender(bytes)", &[0; 20]), + ), + ], + ); + assert_eq!(outcomes[0].status, ExecutionStatus::Success); + assert_eq!(outcomes[0].output, address_word(ENTRYPOINT_V08.address)); + assert_eq!(outcomes[1].status, ExecutionStatus::Revert); + assert_eq!( + outcomes[1].output, + bytes_call("Error(string)", b"AA97 should call from EntryPoint") + ); +} + +#[test] +fn preinstalled_entrypoint_uses_its_matching_helper_for_sender_creation() { + let preinstalls = [ + ENTRYPOINT_V08.load(), + SENDER_CREATOR_V08.load(), + ERC2470_SINGLETON_FACTORY.load(), + ]; + for at_genesis in [false, true] { + let (state, root, _tempdir) = genesis(at_genesis, false, &preinstalls); + let root = if at_genesis { + root + } else { + let result = state.protocol_upgrade(upgrade_request( + root, + 0, + true, + false, + BTreeMap::new(), + &preinstalls, + )); + let ProtocolUpgradeResult::Success { + post_state_hash, .. + } = result + else { + panic!("upgrade failed: {result:?}"); + }; + post_state_hash + }; + let salt = [0x42; 32]; + let address = create2_address(ERC2470_SINGLETON_FACTORY.address, salt); + let mut init_code = ERC2470_SINGLETON_FACTORY.address.as_bytes().to_vec(); + init_code.extend_from_slice(&singleton_factory_input(salt)); + let outcomes = call_sequence( + state, + root, + &[ + (ENTRYPOINT_V08.address, selector("senderCreator()")), + ( + ENTRYPOINT_V08.address, + bytes_call("getSenderAddress(bytes)", &init_code), + ), + ], + ); + assert_eq!(outcomes[0].status, ExecutionStatus::Success); + assert_eq!(outcomes[0].output, address_word(SENDER_CREATOR_V08.address)); + // getSenderAddress deliberately reverts with the computed sender after + // EntryPoint -> SenderCreator -> factory successfully creates it. + let mut expected = selector("SenderAddressResult(address)"); + expected.extend_from_slice(&address_word(address)); + assert_eq!(outcomes[1].status, ExecutionStatus::Revert); + assert_eq!(outcomes[1].output, expected); + } +} + +#[test] +fn preinstalled_entrypoint_domain_separator_uses_the_current_chain_id() { + for chain_id in [1, 7, 31_337] { + let (state, root, _tempdir) = genesis(true, false, &[ENTRYPOINT_V08.load()]); + let config = EvmConfig { + chain_id, + ..evm_config(true, &[]) + }; + let outcomes = call_sequence_with_config( + state, + root, + &[(ENTRYPOINT_V08.address, selector("getDomainSeparatorV4()"))], + config, + ); + let mut domain = Vec::new(); + domain.extend_from_slice( + keccak256("EIP712Domain(string name,string version,uint256 chainId,address verifyingContract)").as_slice(), + ); + domain.extend_from_slice(keccak256("ERC4337").as_slice()); + domain.extend_from_slice(keccak256("1").as_slice()); + domain.extend_from_slice(&word(chain_id)); + domain.extend_from_slice(&address_word(ENTRYPOINT_V08.address)); + assert_eq!(outcomes[0].status, ExecutionStatus::Success); + assert_eq!(outcomes[0].output, keccak256(domain).as_slice()); + } +} + +#[test] +fn preinstalled_entrypoint_starts_empty_and_executes_bundles_with_transient_guard() { + let account = evm::Address::new([3; 20]); + let mut nonce = selector("getNonce(address,uint192)"); + nonce.extend_from_slice(&address_word(account)); + nonce.extend_from_slice(&word(0)); + let mut balance = selector("balanceOf(address)"); + balance.extend_from_slice(&address_word(account)); + let mut bundle = selector( + "handleOps((address,uint256,bytes,bytes,bytes32,uint256,bytes32,bytes,bytes)[],address)", + ); + bundle.extend_from_slice(&word(64)); + bundle.extend_from_slice(&address_word(evm::Address::new([2; 20]))); + bundle.extend_from_slice(&word(0)); + let (state, root, _tempdir) = genesis(true, false, &[ENTRYPOINT_V08.load()]); + let outcomes = call_sequence( + state, + root, + &[ + (ENTRYPOINT_V08.address, nonce), + (ENTRYPOINT_V08.address, balance), + (ENTRYPOINT_V08.address, bundle.clone()), + (ENTRYPOINT_V08.address, bundle), + ], + ); + for outcome in &outcomes[..2] { + assert_eq!(outcome.status, ExecutionStatus::Success); + assert_eq!(outcome.output, word(0)); + } + for outcome in &outcomes[2..] { + assert_eq!(outcome.status, ExecutionStatus::Success); + assert!(outcome.output.is_empty()); + assert_eq!(outcome.logs.len(), 1); + assert_eq!(outcome.logs[0].address, ENTRYPOINT_V08.address); + assert_eq!( + outcome.logs[0].topics, + vec![evm::Topic::new(keccak256("BeforeExecution()").0)] + ); + } +} diff --git a/generate-chainspec.sh b/generate-chainspec.sh index 115bee4cc1..5a4bdfcfcf 100755 --- a/generate-chainspec.sh +++ b/generate-chainspec.sh @@ -26,6 +26,9 @@ generate_chainspec() { echo "Generating chainspec..." envsubst < ${SOURCE} > ${TARGET} + + # Keep the local protocol package self-contained, including runtime attribution. + cp -R "${BASEDIR}/resources/evm/preinstalls" "${BASEDIR}/resources/local/" } main() { diff --git a/node/Cargo.toml b/node/Cargo.toml index f947647f64..25c518d438 100644 --- a/node/Cargo.toml +++ b/node/Cargo.toml @@ -99,6 +99,7 @@ casper-executor-wasm-interface = { version = "0.1.3", path = "../executor/wasm_i fs_extra = "1.3.0" casper-executor-evm = { version = "0.1.0", path = "../executor/evm" } parking_lot = "0.12.5" +alloy-primitives = { version = "=1.5.7", default-features = false, features = ["sha3-keccak"] } [dev-dependencies] casper-binary-port = { version = "1.1.1", path = "../binary_port", features = ["testing"] } diff --git a/node/src/cli.rs b/node/src/cli.rs index b00585ca5b..d77c21c079 100644 --- a/node/src/cli.rs +++ b/node/src/cli.rs @@ -29,8 +29,8 @@ use crate::{ setup_signal_hooks, types::ExitCode, utils::{ - chain_specification::validate_chainspec, config_specification::validate_config, Loadable, - WithDir, + chain_specification::validate_chainspec, config_specification::validate_config, + evm_preinstalls::load_preinstall_bytecodes, Loadable, WithDir, }, }; @@ -182,6 +182,12 @@ impl Cli { bail!("invalid chainspec"); } + load_preinstall_bytecodes( + reactor_config.dir(), + chainspec.evm_config.enabled, + &chainspec.evm_config.preinstalls, + )?; + if !validate_config(reactor_config.value()) { bail!("invalid config"); } diff --git a/node/src/components/contract_runtime.rs b/node/src/components/contract_runtime.rs index 9aa002be97..89604b88d8 100644 --- a/node/src/components/contract_runtime.rs +++ b/node/src/components/contract_runtime.rs @@ -16,7 +16,7 @@ use std::{ cmp::Ordering, convert::TryInto, fmt::{self, Debug, Formatter}, - path::Path, + path::{Path, PathBuf}, sync::{Arc, Mutex}, time::Instant, }; @@ -102,6 +102,8 @@ pub(crate) struct ContractRuntime { #[data_size(skip)] data_access_layer: Arc>, current_gas_price: EraPrice, + /// Selected protocol package directory, separate from the global-state database. + protocol_dir: PathBuf, } impl Debug for ContractRuntime { @@ -113,6 +115,7 @@ impl Debug for ContractRuntime { impl ContractRuntime { pub(crate) fn new( storage_dir: &Path, + protocol_dir: &Path, block_store: LmdbBlockStore, contract_runtime_config: &Config, chainspec: Arc, @@ -183,6 +186,7 @@ impl ContractRuntime { let metrics = Arc::new(Metrics::new(registry)?); Ok(ContractRuntime { + protocol_dir: protocol_dir.to_path_buf(), state: ComponentState::Initialized, execution_pre_state, execution_engine_v1, @@ -266,7 +270,18 @@ impl ContractRuntime { let start = Instant::now(); let protocol_version = chainspec.protocol_config.version; let chainspec_hash = chainspec.hash(); - let genesis_config = chainspec.into(); + let bytecodes = match crate::utils::evm_preinstalls::load_preinstall_bytecodes( + &self.protocol_dir, + chainspec.evm_config.enabled, + &chainspec.evm_config.preinstalls, + ) { + Ok(bytecodes) => bytecodes, + Err(error) => { + return GenesisResult::Failure(GenesisError::EvmPreinstall(error.to_string())) + } + }; + let genesis_config = + casper_types::GenesisConfig::from(chainspec).with_evm_preinstall_bytecodes(bytecodes); let account_bytes = match chainspec_raw_bytes.maybe_genesis_accounts_bytes() { Some(bytes) => bytes, None => { @@ -315,11 +330,20 @@ impl ContractRuntime { ) -> Result { debug!(?upgrade_config, "upgrade"); let start = Instant::now(); - let upgrade_request = ProtocolUpgradeRequest::new(upgrade_config); + let protocol_dir = self.protocol_dir.clone(); let data_access_layer = Arc::clone(&self.data_access_layer); let metrics = Arc::clone(&self.metrics); run_intensive_task(move || { + let bytecodes = crate::utils::evm_preinstalls::load_preinstall_bytecodes( + &protocol_dir, + upgrade_config.evm_config().enabled, + &upgrade_config.evm_config().preinstalls, + ) + .map_err(|error| error.to_string())?; + let upgrade_request = ProtocolUpgradeRequest::new( + upgrade_config.with_evm_preinstall_bytecodes(bytecodes), + ); let result = data_access_layer.protocol_upgrade(upgrade_request); if result.is_success() { info!("committed upgrade"); diff --git a/node/src/components/contract_runtime/operations.rs b/node/src/components/contract_runtime/operations.rs index aef57437c1..fa482c3db7 100644 --- a/node/src/components/contract_runtime/operations.rs +++ b/node/src/components/contract_runtime/operations.rs @@ -2588,6 +2588,7 @@ mod tests { base_fee: 3, wei_per_mote: DEFAULT_WEI_PER_MOTE, transaction_lanes: Vec::new(), + preinstalls: Default::default(), }, ..Default::default() }; diff --git a/node/src/components/contract_runtime/tests.rs b/node/src/components/contract_runtime/tests.rs index 5d5796b58f..b714807fa4 100644 --- a/node/src/components/contract_runtime/tests.rs +++ b/node/src/components/contract_runtime/tests.rs @@ -156,6 +156,7 @@ impl reactor::Reactor for Reactor { let contract_runtime = ContractRuntime::new( storage.root_path(), + &RESOURCES_PATH.join("local"), contract_runtime_block_store, &config.config, chainspec, @@ -1228,6 +1229,7 @@ mod test_mod { }; let block_store = LmdbBlockStore::new(temp_dir.path(), 64 * 1024 * 1024).unwrap(); let contract_runtime = ContractRuntime::new( + temp_dir.path(), temp_dir.path(), block_store, &ContractRuntimeConfig::default(), diff --git a/node/src/components/upgrade_watcher.rs b/node/src/components/upgrade_watcher.rs index e2fba63c4f..56bd8fcfc1 100644 --- a/node/src/components/upgrade_watcher.rs +++ b/node/src/components/upgrade_watcher.rs @@ -21,10 +21,7 @@ use thiserror::Error; use tokio::task; use tracing::{debug, error, info, trace, warn}; -use casper_types::{ - file_utils::{self, ReadFileError}, - Chainspec, EraId, NextUpgrade, ProtocolConfig, ProtocolVersion, TimeDiff, -}; +use casper_types::{evm, Chainspec, EraId, NextUpgrade, ProtocolConfig, ProtocolVersion, TimeDiff}; use crate::{ components::{Component, ComponentState, InitializedComponent}, @@ -33,7 +30,10 @@ use crate::{ EffectExt, Effects, }, reactor::main_reactor::MainEvent, - utils::chain_specification::parse_toml::CHAINSPEC_FILENAME, + utils::{ + chain_specification::{parse_toml::CHAINSPEC_FILENAME, validate_evm_preinstalls}, + evm_preinstalls::load_preinstall_bytecodes, + }, NodeRng, }; @@ -101,7 +101,7 @@ pub(crate) enum Error { /// Error loading the upgrade point. #[error("could not load upgrade point: {0}")] - LoadUpgradePoint(ReadFileError), + LoadUpgradePoint(io::Error), /// Failed to read the given directory. #[error("failed to read dir {}: {error}", dir.display())] @@ -325,14 +325,24 @@ where struct UpgradePoint { #[serde(rename = "protocol")] pub(crate) protocol_config: ProtocolConfig, + #[serde(default)] + evm: UpgradeEvmConfig, +} + +/// Reads the activation flag and asset manifest, allowing unrelated future EVM fields. +#[derive(Default, Deserialize)] +#[serde(default)] +struct UpgradeEvmConfig { + enabled: bool, + preinstalls: Vec<(evm::Address, evm::Hash)>, } impl UpgradePoint { /// Parses a chainspec file at the given path as an `UpgradePoint`. fn from_chainspec_path + fmt::Debug>(path: P) -> Result { - let bytes = file_utils::read_file(path.as_ref().join(CHAINSPEC_FILENAME)) + let contents = fs::read_to_string(path.as_ref().join(CHAINSPEC_FILENAME)) .map_err(Error::LoadUpgradePoint)?; - Ok(toml::from_str(std::str::from_utf8(&bytes).unwrap())?) + Ok(toml::from_str(&contents)?) } } @@ -418,7 +428,7 @@ fn next_upgrade(dir: PathBuf, current_version: ProtocolVersion) -> Option upgrade_point, Err(error) => { - debug!(subdir=%subdir.display(), %error, "failed to load upgrade point"); + warn!(subdir=%subdir.display(), %error, "failed to load upgrade point"); return None; } }; @@ -432,6 +442,22 @@ fn next_upgrade(dir: PathBuf, current_version: ProtocolVersion) -> Option PathBuf { + let dir = root.join("1_0_0"); + fs::create_dir_all(dir.join("preinstalls")).unwrap(); + let address = evm::Address::new([0xaa; 20]); + let hash = evm::Hash::new(alloy_primitives::keccak256([0x00]).0); + // Intentionally use a future spec and unknown fields: only protocol and hashes matter. + let text = format!( + r#" +[protocol] +version = "1.0.0" +hard_reset = false +activation_point = 10 +[evm] +enabled = {enabled} +spec = "future_fork" +future_feature = true +preinstalls = [["{address}", "{hash}"]] +[future_subsystem] +setting = "unknown" +"# + ); + fs::write(dir.join(CHAINSPEC_FILENAME), text).unwrap(); + dir.join("preinstalls") + .join(format!("{}.bin", hash.to_hex_string())) + } + + #[test] + fn preinstall_assets_are_validated_only_when_evm_is_enabled() { + let mut rng = crate::new_rng(); + for enabled in [false, true] { + let dir = tempfile::tempdir().unwrap(); + let path = stage_preinstall(dir.path(), enabled); + // An invalid nearest package must not cause the watcher to skip to a later version. + install_chainspec(&mut rng, dir.path(), V1_0_3); + let mut current = Chainspec::default(); + current.protocol_config.version = V0_9_9; + let mut watcher = + UpgradeWatcher::new(¤t, Config::default(), dir.path().join("0_9_9")).unwrap(); + let expected = Some(NextUpgrade::new( + ActivationPoint::EraId(EraId::new(10)), + V1_0_0, + )); + if !enabled { + assert_eq!(watcher.next_upgrade, expected); + continue; + } + assert!(watcher.next_upgrade.is_none()); + let mut scan = || { + let _ = + watcher.handle_got_next_upgrade(next_upgrade(dir.path().to_path_buf(), V0_9_9)); + watcher.next_upgrade + }; + fs::write(&path, [0x00]).unwrap(); + assert_eq!(scan(), expected); + for invalid in [vec![], vec![0x01]] { + fs::write(&path, invalid).unwrap(); + assert!(scan().is_none()); + fs::write(&path, [0x00]).unwrap(); + assert_eq!(scan(), expected); + } + fs::remove_file(&path).unwrap(); + assert!(scan().is_none()); + } + } + + #[test] + fn staged_preinstall_manifest_rejects_duplicate_addresses_and_invalid_hashes() { + let dir = tempfile::tempdir().unwrap(); + let bytecode_path = stage_preinstall(dir.path(), true); + fs::write(bytecode_path, [0x00]).unwrap(); + let path = dir.path().join("1_0_0").join(CHAINSPEC_FILENAME); + let text = fs::read_to_string(&path).unwrap(); + let hash = evm::Hash::new(alloy_primitives::keccak256([0x00]).0); + assert!(next_upgrade(dir.path().to_path_buf(), V0_9_9).is_some()); + for address in ["aa".repeat(20), "AA".repeat(20)] { + let duplicate = text.replace("\"]]", &format!("\"], [\"0x{address}\", \"{hash}\"]]")); + fs::write(&path, duplicate).unwrap(); + assert!(next_upgrade(dir.path().to_path_buf(), V0_9_9).is_none()); + } + fs::write(&path, text.replace(&hash.to_string(), "0x00")).unwrap(); + assert!(UpgradePoint::from_chainspec_path(dir.path().join("1_0_0")).is_err()); + fs::write(path, [0xff]).unwrap(); + assert!(next_upgrade(dir.path().to_path_buf(), V0_9_9).is_none()); + } + #[test] fn should_get_next_installed_version() { let tempdir = tempfile::tempdir().expect("should create temp dir"); diff --git a/node/src/reactor/main_reactor.rs b/node/src/reactor/main_reactor.rs index 64e95fb1a9..3b2ac03fc4 100644 --- a/node/src/reactor/main_reactor.rs +++ b/node/src/reactor/main_reactor.rs @@ -1143,6 +1143,7 @@ impl reactor::Reactor for MainReactor { let contract_runtime = ContractRuntime::new( &storage_root, + &root_dir, block_store.clone(), &config.contract_runtime, chainspec.clone(), diff --git a/node/src/reactor/main_reactor/tests/transactions.rs b/node/src/reactor/main_reactor/tests/transactions.rs index 22ccfc850e..e01162cbf9 100644 --- a/node/src/reactor/main_reactor/tests/transactions.rs +++ b/node/src/reactor/main_reactor/tests/transactions.rs @@ -1334,6 +1334,7 @@ fn evm_test_config(chain_id: u64) -> EvmConfig { enabled: true, chain_id, spec: EvmSpec::Prague, + preinstalls: Default::default(), block_gas_limit: 30_000_000, base_fee: 1, wei_per_mote: DEFAULT_WEI_PER_MOTE, @@ -1861,6 +1862,7 @@ async fn should_execute_evm_transaction_and_store_receipt() { u64::MAX, 100, )], + preinstalls: Default::default(), }; let config = SingleTransactionTestCase::default_test_config() .with_evm_config(evm_config.clone()) @@ -1989,6 +1991,7 @@ async fn should_assign_evm_transactions_to_correctly_sized_lanes() { ), TransactionLaneDefinition::new(LARGE_LANE_ID, u64::MAX, u64::MAX, u64::MAX, 50), ], + preinstalls: Default::default(), }; let config = SingleTransactionTestCase::default_test_config() .with_evm_config(evm_config.clone()) @@ -2059,6 +2062,7 @@ async fn should_reduce_total_supply_for_evm_burns_and_rounding_without_debiting_ u64::MAX, 100, )], + preinstalls: Default::default(), }; let config = SingleTransactionTestCase::default_test_config() .with_evm_config(evm_config.clone()) @@ -2180,6 +2184,7 @@ async fn should_prelink_ed25519_proposer_coinbase_for_evm_execution() { u64::MAX, 100, )], + preinstalls: Default::default(), }; let config = SingleTransactionTestCase::default_test_config() .with_evm_config(evm_config.clone()) @@ -2278,6 +2283,7 @@ async fn should_apply_casper_fee_and_refund_handling_to_evm_transaction() { u64::MAX, 100, )], + preinstalls: Default::default(), }; let config = SingleTransactionTestCase::default_test_config() .with_evm_config(evm_config.clone()) @@ -2347,6 +2353,7 @@ async fn should_apply_no_refund_to_eip1559_max_fee_headroom() { u64::MAX, 100, )], + preinstalls: Default::default(), }; let config = SingleTransactionTestCase::default_test_config() .with_evm_config(evm_config.clone()) @@ -2412,6 +2419,7 @@ async fn should_require_balance_for_eip1559_signed_maximum() { u64::MAX, 100, )], + preinstalls: Default::default(), }; let config = SingleTransactionTestCase::default_test_config() .with_evm_config(evm_config.clone()) @@ -2495,6 +2503,7 @@ async fn should_apply_refund_policy_to_evm_revert_and_halt() { u64::MAX, 100, )], + preinstalls: Default::default(), }; let config = SingleTransactionTestCase::default_test_config() .with_evm_config(evm_config.clone()) @@ -2611,6 +2620,7 @@ async fn should_reject_evm_transaction_when_value_and_fee_exceed_balance() { u64::MAX, 100, )], + preinstalls: Default::default(), }; let config = SingleTransactionTestCase::default_test_config() .with_evm_config(evm_config.clone()) @@ -2689,6 +2699,7 @@ async fn should_not_seed_evm_accounts_at_genesis() { u64::MAX, 100, )], + preinstalls: Default::default(), }; let config = SingleTransactionTestCase::default_test_config().with_evm_config(evm_config.clone()); @@ -2748,6 +2759,7 @@ async fn should_transfer_to_evm_address_with_native_transfer() { u64::MAX, 100, )], + preinstalls: Default::default(), }; let config = SingleTransactionTestCase::default_test_config() .with_evm_config(evm_config.clone()) @@ -2828,6 +2840,7 @@ async fn should_reject_native_transfer_to_evm_contract_address() { u64::MAX, 100, )], + preinstalls: Default::default(), }; let config = SingleTransactionTestCase::default_test_config() .with_evm_config(evm_config.clone()) diff --git a/node/src/utils.rs b/node/src/utils.rs index 2a2f8f641c..7b9bd321df 100644 --- a/node/src/utils.rs +++ b/node/src/utils.rs @@ -6,6 +6,7 @@ pub(crate) mod chain_specification; pub(crate) mod config_specification; mod display_error; pub(crate) mod ds; +pub(crate) mod evm_preinstalls; mod external; pub(crate) mod fmt_limit; pub(crate) mod opt_display; diff --git a/node/src/utils/chain_specification.rs b/node/src/utils/chain_specification.rs index db791e772b..f0d0caef86 100644 --- a/node/src/utils/chain_specification.rs +++ b/node/src/utils/chain_specification.rs @@ -8,9 +8,9 @@ use once_cell::sync::Lazy; use tracing::{error, info, warn}; use casper_types::{ - system::auction::VESTING_SCHEDULE_LENGTH_MILLIS, Chainspec, ConsensusProtocolName, CoreConfig, - EvmConfig, ProtocolConfig, TimeDiff, TransactionConfig, TransactionV1Config, AUCTION_LANE_ID, - INSTALL_UPGRADE_LANE_ID, MINIMUM_WEI_PER_MOTE, MINT_LANE_ID, + evm, system::auction::VESTING_SCHEDULE_LENGTH_MILLIS, Chainspec, ConsensusProtocolName, + CoreConfig, EvmConfig, ProtocolConfig, TimeDiff, TransactionConfig, TransactionV1Config, + AUCTION_LANE_ID, INSTALL_UPGRADE_LANE_ID, MINIMUM_WEI_PER_MOTE, MINT_LANE_ID, }; use crate::components::network; @@ -23,6 +23,11 @@ static RESERVED_LANE_IDS: Lazy> = pub fn validate_chainspec(chainspec: &Chainspec) -> bool { info!("begin chainspec validation"); + if chainspec.evm_config.enabled && !validate_evm_preinstalls(&chainspec.evm_config.preinstalls) + { + return false; + } + if chainspec.core_config.unbonding_delay <= chainspec.core_config.auction_delay { warn!( "unbonding delay is set to {} but it should be greater than the auction delay (currently set to {})", @@ -99,6 +104,18 @@ pub fn validate_chainspec(chainspec: &Chainspec) -> bool { ) } +/// Checks that every preinstall address occurs once. +pub(crate) fn validate_evm_preinstalls(preinstalls: &[(evm::Address, evm::Hash)]) -> bool { + let mut addresses = HashSet::new(); + for (address, _) in preinstalls { + if !addresses.insert(address) { + error!(%address, "duplicate EVM preinstall address"); + return false; + } + } + true +} + /// Checks whether the values set in the config make sense and returns `false` if they don't. pub(crate) fn validate_protocol_config(_protocol_config: &ProtocolConfig) -> bool { true @@ -892,6 +909,27 @@ mod tests { assert!(!validate_chainspec(&chainspec)); } + #[test] + fn should_validate_evm_preinstall_addresses_only_when_enabled() { + let (mut chainspec, _) = <(Chainspec, ChainspecRawBytes)>::from_resources("local"); + let address = evm::Address::new([0xaa; 20]); + let hash = evm::Hash::new([1; 32]); + for enabled in [false, true] { + chainspec.evm_config.enabled = enabled; + chainspec.evm_config.preinstalls = + vec![(address, hash), (evm::Address::new([0xbb; 20]), hash)]; + assert!(validate_chainspec(&chainspec)); + for duplicate_hash in [hash, evm::Hash::new([2; 32])] { + chainspec + .evm_config + .preinstalls + .push((address, duplicate_hash)); + assert_eq!(validate_chainspec(&chainspec), !enabled); + chainspec.evm_config.preinstalls.pop(); + } + } + } + fn fail_validation_with_lane_id(lane_id: u8) { let mut v1_config = TransactionV1Config::default(); let definition_1 = TransactionLaneDefinition::new(lane_id, 100, 100, 100, 10); diff --git a/node/src/utils/evm_preinstalls.rs b/node/src/utils/evm_preinstalls.rs new file mode 100644 index 0000000000..002693082c --- /dev/null +++ b/node/src/utils/evm_preinstalls.rs @@ -0,0 +1,131 @@ +//! Loads content-addressed EVM runtimes from a selected protocol package. + +use std::{fs, path::Path}; + +use alloy_primitives::keccak256; +use anyhow::{anyhow, ensure}; +use casper_types::{bytesrepr::Bytes, evm}; + +/// Reads and validates listed runtime files when EVM is enabled. +pub(crate) fn load_preinstall_bytecodes( + protocol_dir: &Path, + evm_enabled: bool, + preinstalls: &[(evm::Address, evm::Hash)], +) -> anyhow::Result> { + let mut bytecodes = Vec::new(); + if !evm_enabled { + return Ok(bytecodes); + } + for &(address, expected) in preinstalls { + if bytecodes.iter().any(|(hash, _)| *hash == expected) { + continue; + } + let path = protocol_dir + .join("preinstalls") + .join(format!("{}.bin", expected.to_hex_string())); + let code = fs::read(&path).map_err(|error| { + anyhow!("EVM preinstall at {address}: cannot read {path:?}, expected runtime hash {expected}: {error}") + })?; + ensure!( + !code.is_empty(), + "EVM preinstall at {address}: empty runtime bytecode in {path:?}, expected {expected}" + ); + let actual = evm::Hash::new(keccak256(&code).0); + ensure!( + actual == expected, + "EVM preinstall at {address}: runtime hash mismatch in {path:?}: expected {expected}, actual {actual}" + ); + bytecodes.push((expected, code.into())); + } + Ok(bytecodes) +} + +#[cfg(test)] +mod tests { + use std::path::PathBuf; + + use super::*; + + const CODE: &[u8] = &[0x00]; + + fn fixture() -> (tempfile::TempDir, Vec<(evm::Address, evm::Hash)>, PathBuf) { + let dir = tempfile::tempdir().unwrap(); + let expected = evm::Hash::new(keccak256(CODE).0); + let preinstalls = vec![ + (evm::Address::new([1; 20]), expected), + (evm::Address::new([2; 20]), expected), + ]; + fs::create_dir(dir.path().join("preinstalls")).unwrap(); + let path = dir + .path() + .join("preinstalls") + .join(format!("{}.bin", expected.to_hex_string())); + (dir, preinstalls, path) + } + + #[test] + fn loads_shared_bytecode_independently_of_protocol_directory() { + for _ in 0..2 { + let (dir, preinstalls, path) = fixture(); + fs::write(path, CODE).unwrap(); + let bytecodes = load_preinstall_bytecodes(dir.path(), true, &preinstalls).unwrap(); + assert_eq!(bytecodes, vec![(preinstalls[0].1, Bytes::from(CODE))]); + } + } + + #[test] + fn missing_and_unreadable_files_report_the_address_path_and_hash() { + let (dir, preinstalls, path) = fixture(); + for unreadable in [false, true] { + if unreadable { + // A directory cannot be read as a bytecode file, even when run as root. + fs::create_dir(&path).unwrap(); + } + let message = load_preinstall_bytecodes(dir.path(), true, &preinstalls) + .unwrap_err() + .to_string(); + assert!(message.contains("cannot read")); + assert!(message.contains(&preinstalls[0].0.to_string())); + assert!(message.contains(&path.display().to_string())); + assert!(message.contains(&preinstalls[0].1.to_string())); + } + } + + #[test] + fn rejects_empty_and_mismatched_bytecode_and_revalidates_changes() { + let (dir, preinstalls, path) = fixture(); + fs::write(&path, CODE).unwrap(); + assert!(load_preinstall_bytecodes(dir.path(), true, &preinstalls).is_ok()); + for (invalid, diagnostic) in [ + (&[][..], "empty runtime bytecode"), + (&[0x01][..], "runtime hash mismatch"), + ] { + fs::write(&path, invalid).unwrap(); + let message = load_preinstall_bytecodes(dir.path(), true, &preinstalls) + .unwrap_err() + .to_string(); + assert!(message.contains(diagnostic)); + assert!(message.contains(&path.display().to_string())); + assert!(message.contains(&preinstalls[0].0.to_string())); + assert!(message.contains(&preinstalls[0].1.to_string())); + if !invalid.is_empty() { + assert!(message.contains(&evm::Hash::new(keccak256(invalid).0).to_string())); + } + fs::write(&path, CODE).unwrap(); + assert!(load_preinstall_bytecodes(dir.path(), true, &preinstalls).is_ok()); + } + fs::remove_file(&path).unwrap(); + assert!(load_preinstall_bytecodes(dir.path(), true, &preinstalls).is_err()); + } + + #[test] + fn disabled_evm_or_empty_list_requires_no_asset_directory() { + let dir = tempfile::tempdir().unwrap(); + let preinstalls = [(evm::Address::new([1; 20]), evm::Hash::new([2; 32]))]; + for (enabled, entries) in [(false, preinstalls.as_slice()), (true, &[])] { + assert!(load_preinstall_bytecodes(dir.path(), enabled, entries) + .unwrap() + .is_empty()); + } + } +} diff --git a/resources/evm/preinstalls/2fa86add0aed31f33a762c9d88e807c475bd51d0f52bd0955754b2608f7e4989.bin b/resources/evm/preinstalls/2fa86add0aed31f33a762c9d88e807c475bd51d0f52bd0955754b2608f7e4989.bin new file mode 100644 index 0000000000..7dbf91a2a9 Binary files /dev/null and b/resources/evm/preinstalls/2fa86add0aed31f33a762c9d88e807c475bd51d0f52bd0955754b2608f7e4989.bin differ diff --git a/resources/evm/preinstalls/44e632a24c6f2600cbd5b5b8b4c2d372359112c8b5774297f5fd0a9e64f11f86.bin b/resources/evm/preinstalls/44e632a24c6f2600cbd5b5b8b4c2d372359112c8b5774297f5fd0a9e64f11f86.bin new file mode 100644 index 0000000000..f3f3bd1c1b Binary files /dev/null and b/resources/evm/preinstalls/44e632a24c6f2600cbd5b5b8b4c2d372359112c8b5774297f5fd0a9e64f11f86.bin differ diff --git a/resources/evm/preinstalls/LICENSE.account-abstraction-v08 b/resources/evm/preinstalls/LICENSE.account-abstraction-v08 new file mode 100644 index 0000000000..f288702d2f --- /dev/null +++ b/resources/evm/preinstalls/LICENSE.account-abstraction-v08 @@ -0,0 +1,674 @@ + GNU GENERAL PUBLIC LICENSE + Version 3, 29 June 2007 + + Copyright (C) 2007 Free Software Foundation, Inc. + Everyone is permitted to copy and distribute verbatim copies + of this license document, but changing it is not allowed. + + Preamble + + The GNU General Public License is a free, copyleft license for +software and other kinds of works. + + The licenses for most software and other practical works are designed +to take away your freedom to share and change the works. By contrast, +the GNU General Public License is intended to guarantee your freedom to +share and change all versions of a program--to make sure it remains free +software for all its users. We, the Free Software Foundation, use the +GNU General Public License for most of our software; it applies also to +any other work released this way by its authors. You can apply it to +your programs, too. + + When we speak of free software, we are referring to freedom, not +price. Our General Public Licenses are designed to make sure that you +have the freedom to distribute copies of free software (and charge for +them if you wish), that you receive source code or can get it if you +want it, that you can change the software or use pieces of it in new +free programs, and that you know you can do these things. + + To protect your rights, we need to prevent others from denying you +these rights or asking you to surrender the rights. Therefore, you have +certain responsibilities if you distribute copies of the software, or if +you modify it: responsibilities to respect the freedom of others. + + For example, if you distribute copies of such a program, whether +gratis or for a fee, you must pass on to the recipients the same +freedoms that you received. You must make sure that they, too, receive +or can get the source code. And you must show them these terms so they +know their rights. + + Developers that use the GNU GPL protect your rights with two steps: +(1) assert copyright on the software, and (2) offer you this License +giving you legal permission to copy, distribute and/or modify it. + + For the developers' and authors' protection, the GPL clearly explains +that there is no warranty for this free software. For both users' and +authors' sake, the GPL requires that modified versions be marked as +changed, so that their problems will not be attributed erroneously to +authors of previous versions. + + Some devices are designed to deny users access to install or run +modified versions of the software inside them, although the manufacturer +can do so. This is fundamentally incompatible with the aim of +protecting users' freedom to change the software. The systematic +pattern of such abuse occurs in the area of products for individuals to +use, which is precisely where it is most unacceptable. Therefore, we +have designed this version of the GPL to prohibit the practice for those +products. If such problems arise substantially in other domains, we +stand ready to extend this provision to those domains in future versions +of the GPL, as needed to protect the freedom of users. + + Finally, every program is threatened constantly by software patents. +States should not allow patents to restrict development and use of +software on general-purpose computers, but in those that do, we wish to +avoid the special danger that patents applied to a free program could +make it effectively proprietary. To prevent this, the GPL assures that +patents cannot be used to render the program non-free. + + The precise terms and conditions for copying, distribution and +modification follow. + + TERMS AND CONDITIONS + + 0. Definitions. + + "This License" refers to version 3 of the GNU General Public License. + + "Copyright" also means copyright-like laws that apply to other kinds of +works, such as semiconductor masks. + + "The Program" refers to any copyrightable work licensed under this +License. Each licensee is addressed as "you". "Licensees" and +"recipients" may be individuals or organizations. + + To "modify" a work means to copy from or adapt all or part of the work +in a fashion requiring copyright permission, other than the making of an +exact copy. The resulting work is called a "modified version" of the +earlier work or a work "based on" the earlier work. + + A "covered work" means either the unmodified Program or a work based +on the Program. + + To "propagate" a work means to do anything with it that, without +permission, would make you directly or secondarily liable for +infringement under applicable copyright law, except executing it on a +computer or modifying a private copy. Propagation includes copying, +distribution (with or without modification), making available to the +public, and in some countries other activities as well. + + To "convey" a work means any kind of propagation that enables other +parties to make or receive copies. Mere interaction with a user through +a computer network, with no transfer of a copy, is not conveying. + + An interactive user interface displays "Appropriate Legal Notices" +to the extent that it includes a convenient and prominently visible +feature that (1) displays an appropriate copyright notice, and (2) +tells the user that there is no warranty for the work (except to the +extent that warranties are provided), that licensees may convey the +work under this License, and how to view a copy of this License. If +the interface presents a list of user commands or options, such as a +menu, a prominent item in the list meets this criterion. + + 1. Source Code. + + The "source code" for a work means the preferred form of the work +for making modifications to it. "Object code" means any non-source +form of a work. + + A "Standard Interface" means an interface that either is an official +standard defined by a recognized standards body, or, in the case of +interfaces specified for a particular programming language, one that +is widely used among developers working in that language. + + The "System Libraries" of an executable work include anything, other +than the work as a whole, that (a) is included in the normal form of +packaging a Major Component, but which is not part of that Major +Component, and (b) serves only to enable use of the work with that +Major Component, or to implement a Standard Interface for which an +implementation is available to the public in source code form. A +"Major Component", in this context, means a major essential component +(kernel, window system, and so on) of the specific operating system +(if any) on which the executable work runs, or a compiler used to +produce the work, or an object code interpreter used to run it. + + The "Corresponding Source" for a work in object code form means all +the source code needed to generate, install, and (for an executable +work) run the object code and to modify the work, including scripts to +control those activities. However, it does not include the work's +System Libraries, or general-purpose tools or generally available free +programs which are used unmodified in performing those activities but +which are not part of the work. For example, Corresponding Source +includes interface definition files associated with source files for +the work, and the source code for shared libraries and dynamically +linked subprograms that the work is specifically designed to require, +such as by intimate data communication or control flow between those +subprograms and other parts of the work. + + The Corresponding Source need not include anything that users +can regenerate automatically from other parts of the Corresponding +Source. + + The Corresponding Source for a work in source code form is that +same work. + + 2. Basic Permissions. + + All rights granted under this License are granted for the term of +copyright on the Program, and are irrevocable provided the stated +conditions are met. This License explicitly affirms your unlimited +permission to run the unmodified Program. The output from running a +covered work is covered by this License only if the output, given its +content, constitutes a covered work. This License acknowledges your +rights of fair use or other equivalent, as provided by copyright law. + + You may make, run and propagate covered works that you do not +convey, without conditions so long as your license otherwise remains +in force. You may convey covered works to others for the sole purpose +of having them make modifications exclusively for you, or provide you +with facilities for running those works, provided that you comply with +the terms of this License in conveying all material for which you do +not control copyright. Those thus making or running the covered works +for you must do so exclusively on your behalf, under your direction +and control, on terms that prohibit them from making any copies of +your copyrighted material outside their relationship with you. + + Conveying under any other circumstances is permitted solely under +the conditions stated below. Sublicensing is not allowed; section 10 +makes it unnecessary. + + 3. Protecting Users' Legal Rights From Anti-Circumvention Law. + + No covered work shall be deemed part of an effective technological +measure under any applicable law fulfilling obligations under article +11 of the WIPO copyright treaty adopted on 20 December 1996, or +similar laws prohibiting or restricting circumvention of such +measures. + + When you convey a covered work, you waive any legal power to forbid +circumvention of technological measures to the extent such circumvention +is effected by exercising rights under this License with respect to +the covered work, and you disclaim any intention to limit operation or +modification of the work as a means of enforcing, against the work's +users, your or third parties' legal rights to forbid circumvention of +technological measures. + + 4. Conveying Verbatim Copies. + + You may convey verbatim copies of the Program's source code as you +receive it, in any medium, provided that you conspicuously and +appropriately publish on each copy an appropriate copyright notice; +keep intact all notices stating that this License and any +non-permissive terms added in accord with section 7 apply to the code; +keep intact all notices of the absence of any warranty; and give all +recipients a copy of this License along with the Program. + + You may charge any price or no price for each copy that you convey, +and you may offer support or warranty protection for a fee. + + 5. Conveying Modified Source Versions. + + You may convey a work based on the Program, or the modifications to +produce it from the Program, in the form of source code under the +terms of section 4, provided that you also meet all of these conditions: + + a) The work must carry prominent notices stating that you modified + it, and giving a relevant date. + + b) The work must carry prominent notices stating that it is + released under this License and any conditions added under section + 7. This requirement modifies the requirement in section 4 to + "keep intact all notices". + + c) You must license the entire work, as a whole, under this + License to anyone who comes into possession of a copy. This + License will therefore apply, along with any applicable section 7 + additional terms, to the whole of the work, and all its parts, + regardless of how they are packaged. This License gives no + permission to license the work in any other way, but it does not + invalidate such permission if you have separately received it. + + d) If the work has interactive user interfaces, each must display + Appropriate Legal Notices; however, if the Program has interactive + interfaces that do not display Appropriate Legal Notices, your + work need not make them do so. + + A compilation of a covered work with other separate and independent +works, which are not by their nature extensions of the covered work, +and which are not combined with it such as to form a larger program, +in or on a volume of a storage or distribution medium, is called an +"aggregate" if the compilation and its resulting copyright are not +used to limit the access or legal rights of the compilation's users +beyond what the individual works permit. Inclusion of a covered work +in an aggregate does not cause this License to apply to the other +parts of the aggregate. + + 6. Conveying Non-Source Forms. + + You may convey a covered work in object code form under the terms +of sections 4 and 5, provided that you also convey the +machine-readable Corresponding Source under the terms of this License, +in one of these ways: + + a) Convey the object code in, or embodied in, a physical product + (including a physical distribution medium), accompanied by the + Corresponding Source fixed on a durable physical medium + customarily used for software interchange. + + b) Convey the object code in, or embodied in, a physical product + (including a physical distribution medium), accompanied by a + written offer, valid for at least three years and valid for as + long as you offer spare parts or customer support for that product + model, to give anyone who possesses the object code either (1) a + copy of the Corresponding Source for all the software in the + product that is covered by this License, on a durable physical + medium customarily used for software interchange, for a price no + more than your reasonable cost of physically performing this + conveying of source, or (2) access to copy the + Corresponding Source from a network server at no charge. + + c) Convey individual copies of the object code with a copy of the + written offer to provide the Corresponding Source. This + alternative is allowed only occasionally and noncommercially, and + only if you received the object code with such an offer, in accord + with subsection 6b. + + d) Convey the object code by offering access from a designated + place (gratis or for a charge), and offer equivalent access to the + Corresponding Source in the same way through the same place at no + further charge. You need not require recipients to copy the + Corresponding Source along with the object code. If the place to + copy the object code is a network server, the Corresponding Source + may be on a different server (operated by you or a third party) + that supports equivalent copying facilities, provided you maintain + clear directions next to the object code saying where to find the + Corresponding Source. Regardless of what server hosts the + Corresponding Source, you remain obligated to ensure that it is + available for as long as needed to satisfy these requirements. + + e) Convey the object code using peer-to-peer transmission, provided + you inform other peers where the object code and Corresponding + Source of the work are being offered to the general public at no + charge under subsection 6d. + + A separable portion of the object code, whose source code is excluded +from the Corresponding Source as a System Library, need not be +included in conveying the object code work. + + A "User Product" is either (1) a "consumer product", which means any +tangible personal property which is normally used for personal, family, +or household purposes, or (2) anything designed or sold for incorporation +into a dwelling. In determining whether a product is a consumer product, +doubtful cases shall be resolved in favor of coverage. For a particular +product received by a particular user, "normally used" refers to a +typical or common use of that class of product, regardless of the status +of the particular user or of the way in which the particular user +actually uses, or expects or is expected to use, the product. A product +is a consumer product regardless of whether the product has substantial +commercial, industrial or non-consumer uses, unless such uses represent +the only significant mode of use of the product. + + "Installation Information" for a User Product means any methods, +procedures, authorization keys, or other information required to install +and execute modified versions of a covered work in that User Product from +a modified version of its Corresponding Source. The information must +suffice to ensure that the continued functioning of the modified object +code is in no case prevented or interfered with solely because +modification has been made. + + If you convey an object code work under this section in, or with, or +specifically for use in, a User Product, and the conveying occurs as +part of a transaction in which the right of possession and use of the +User Product is transferred to the recipient in perpetuity or for a +fixed term (regardless of how the transaction is characterized), the +Corresponding Source conveyed under this section must be accompanied +by the Installation Information. But this requirement does not apply +if neither you nor any third party retains the ability to install +modified object code on the User Product (for example, the work has +been installed in ROM). + + The requirement to provide Installation Information does not include a +requirement to continue to provide support service, warranty, or updates +for a work that has been modified or installed by the recipient, or for +the User Product in which it has been modified or installed. Access to a +network may be denied when the modification itself materially and +adversely affects the operation of the network or violates the rules and +protocols for communication across the network. + + Corresponding Source conveyed, and Installation Information provided, +in accord with this section must be in a format that is publicly +documented (and with an implementation available to the public in +source code form), and must require no special password or key for +unpacking, reading or copying. + + 7. Additional Terms. + + "Additional permissions" are terms that supplement the terms of this +License by making exceptions from one or more of its conditions. +Additional permissions that are applicable to the entire Program shall +be treated as though they were included in this License, to the extent +that they are valid under applicable law. If additional permissions +apply only to part of the Program, that part may be used separately +under those permissions, but the entire Program remains governed by +this License without regard to the additional permissions. + + When you convey a copy of a covered work, you may at your option +remove any additional permissions from that copy, or from any part of +it. (Additional permissions may be written to require their own +removal in certain cases when you modify the work.) You may place +additional permissions on material, added by you to a covered work, +for which you have or can give appropriate copyright permission. + + Notwithstanding any other provision of this License, for material you +add to a covered work, you may (if authorized by the copyright holders of +that material) supplement the terms of this License with terms: + + a) Disclaiming warranty or limiting liability differently from the + terms of sections 15 and 16 of this License; or + + b) Requiring preservation of specified reasonable legal notices or + author attributions in that material or in the Appropriate Legal + Notices displayed by works containing it; or + + c) Prohibiting misrepresentation of the origin of that material, or + requiring that modified versions of such material be marked in + reasonable ways as different from the original version; or + + d) Limiting the use for publicity purposes of names of licensors or + authors of the material; or + + e) Declining to grant rights under trademark law for use of some + trade names, trademarks, or service marks; or + + f) Requiring indemnification of licensors and authors of that + material by anyone who conveys the material (or modified versions of + it) with contractual assumptions of liability to the recipient, for + any liability that these contractual assumptions directly impose on + those licensors and authors. + + All other non-permissive additional terms are considered "further +restrictions" within the meaning of section 10. If the Program as you +received it, or any part of it, contains a notice stating that it is +governed by this License along with a term that is a further +restriction, you may remove that term. If a license document contains +a further restriction but permits relicensing or conveying under this +License, you may add to a covered work material governed by the terms +of that license document, provided that the further restriction does +not survive such relicensing or conveying. + + If you add terms to a covered work in accord with this section, you +must place, in the relevant source files, a statement of the +additional terms that apply to those files, or a notice indicating +where to find the applicable terms. + + Additional terms, permissive or non-permissive, may be stated in the +form of a separately written license, or stated as exceptions; +the above requirements apply either way. + + 8. Termination. + + You may not propagate or modify a covered work except as expressly +provided under this License. Any attempt otherwise to propagate or +modify it is void, and will automatically terminate your rights under +this License (including any patent licenses granted under the third +paragraph of section 11). + + However, if you cease all violation of this License, then your +license from a particular copyright holder is reinstated (a) +provisionally, unless and until the copyright holder explicitly and +finally terminates your license, and (b) permanently, if the copyright +holder fails to notify you of the violation by some reasonable means +prior to 60 days after the cessation. + + Moreover, your license from a particular copyright holder is +reinstated permanently if the copyright holder notifies you of the +violation by some reasonable means, this is the first time you have +received notice of violation of this License (for any work) from that +copyright holder, and you cure the violation prior to 30 days after +your receipt of the notice. + + Termination of your rights under this section does not terminate the +licenses of parties who have received copies or rights from you under +this License. If your rights have been terminated and not permanently +reinstated, you do not qualify to receive new licenses for the same +material under section 10. + + 9. Acceptance Not Required for Having Copies. + + You are not required to accept this License in order to receive or +run a copy of the Program. Ancillary propagation of a covered work +occurring solely as a consequence of using peer-to-peer transmission +to receive a copy likewise does not require acceptance. However, +nothing other than this License grants you permission to propagate or +modify any covered work. These actions infringe copyright if you do +not accept this License. Therefore, by modifying or propagating a +covered work, you indicate your acceptance of this License to do so. + + 10. Automatic Licensing of Downstream Recipients. + + Each time you convey a covered work, the recipient automatically +receives a license from the original licensors, to run, modify and +propagate that work, subject to this License. You are not responsible +for enforcing compliance by third parties with this License. + + An "entity transaction" is a transaction transferring control of an +organization, or substantially all assets of one, or subdividing an +organization, or merging organizations. If propagation of a covered +work results from an entity transaction, each party to that +transaction who receives a copy of the work also receives whatever +licenses to the work the party's predecessor in interest had or could +give under the previous paragraph, plus a right to possession of the +Corresponding Source of the work from the predecessor in interest, if +the predecessor has it or can get it with reasonable efforts. + + You may not impose any further restrictions on the exercise of the +rights granted or affirmed under this License. For example, you may +not impose a license fee, royalty, or other charge for exercise of +rights granted under this License, and you may not initiate litigation +(including a cross-claim or counterclaim in a lawsuit) alleging that +any patent claim is infringed by making, using, selling, offering for +sale, or importing the Program or any portion of it. + + 11. Patents. + + A "contributor" is a copyright holder who authorizes use under this +License of the Program or a work on which the Program is based. The +work thus licensed is called the contributor's "contributor version". + + A contributor's "essential patent claims" are all patent claims +owned or controlled by the contributor, whether already acquired or +hereafter acquired, that would be infringed by some manner, permitted +by this License, of making, using, or selling its contributor version, +but do not include claims that would be infringed only as a +consequence of further modification of the contributor version. For +purposes of this definition, "control" includes the right to grant +patent sublicenses in a manner consistent with the requirements of +this License. + + Each contributor grants you a non-exclusive, worldwide, royalty-free +patent license under the contributor's essential patent claims, to +make, use, sell, offer for sale, import and otherwise run, modify and +propagate the contents of its contributor version. + + In the following three paragraphs, a "patent license" is any express +agreement or commitment, however denominated, not to enforce a patent +(such as an express permission to practice a patent or covenant not to +sue for patent infringement). To "grant" such a patent license to a +party means to make such an agreement or commitment not to enforce a +patent against the party. + + If you convey a covered work, knowingly relying on a patent license, +and the Corresponding Source of the work is not available for anyone +to copy, free of charge and under the terms of this License, through a +publicly available network server or other readily accessible means, +then you must either (1) cause the Corresponding Source to be so +available, or (2) arrange to deprive yourself of the benefit of the +patent license for this particular work, or (3) arrange, in a manner +consistent with the requirements of this License, to extend the patent +license to downstream recipients. "Knowingly relying" means you have +actual knowledge that, but for the patent license, your conveying the +covered work in a country, or your recipient's use of the covered work +in a country, would infringe one or more identifiable patents in that +country that you have reason to believe are valid. + + If, pursuant to or in connection with a single transaction or +arrangement, you convey, or propagate by procuring conveyance of, a +covered work, and grant a patent license to some of the parties +receiving the covered work authorizing them to use, propagate, modify +or convey a specific copy of the covered work, then the patent license +you grant is automatically extended to all recipients of the covered +work and works based on it. + + A patent license is "discriminatory" if it does not include within +the scope of its coverage, prohibits the exercise of, or is +conditioned on the non-exercise of one or more of the rights that are +specifically granted under this License. You may not convey a covered +work if you are a party to an arrangement with a third party that is +in the business of distributing software, under which you make payment +to the third party based on the extent of your activity of conveying +the work, and under which the third party grants, to any of the +parties who would receive the covered work from you, a discriminatory +patent license (a) in connection with copies of the covered work +conveyed by you (or copies made from those copies), or (b) primarily +for and in connection with specific products or compilations that +contain the covered work, unless you entered into that arrangement, +or that patent license was granted, prior to 28 March 2007. + + Nothing in this License shall be construed as excluding or limiting +any implied license or other defenses to infringement that may +otherwise be available to you under applicable patent law. + + 12. No Surrender of Others' Freedom. + + If conditions are imposed on you (whether by court order, agreement or +otherwise) that contradict the conditions of this License, they do not +excuse you from the conditions of this License. If you cannot convey a +covered work so as to satisfy simultaneously your obligations under this +License and any other pertinent obligations, then as a consequence you may +not convey it at all. For example, if you agree to terms that obligate you +to collect a royalty for further conveying from those to whom you convey +the Program, the only way you could satisfy both those terms and this +License would be to refrain entirely from conveying the Program. + + 13. Use with the GNU Affero General Public License. + + Notwithstanding any other provision of this License, you have +permission to link or combine any covered work with a work licensed +under version 3 of the GNU Affero General Public License into a single +combined work, and to convey the resulting work. The terms of this +License will continue to apply to the part which is the covered work, +but the special requirements of the GNU Affero General Public License, +section 13, concerning interaction through a network will apply to the +combination as such. + + 14. Revised Versions of this License. + + The Free Software Foundation may publish revised and/or new versions of +the GNU General Public License from time to time. Such new versions will +be similar in spirit to the present version, but may differ in detail to +address new problems or concerns. + + Each version is given a distinguishing version number. If the +Program specifies that a certain numbered version of the GNU General +Public License "or any later version" applies to it, you have the +option of following the terms and conditions either of that numbered +version or of any later version published by the Free Software +Foundation. If the Program does not specify a version number of the +GNU General Public License, you may choose any version ever published +by the Free Software Foundation. + + If the Program specifies that a proxy can decide which future +versions of the GNU General Public License can be used, that proxy's +public statement of acceptance of a version permanently authorizes you +to choose that version for the Program. + + Later license versions may give you additional or different +permissions. However, no additional obligations are imposed on any +author or copyright holder as a result of your choosing to follow a +later version. + + 15. Disclaimer of Warranty. + + THERE IS NO WARRANTY FOR THE PROGRAM, TO THE EXTENT PERMITTED BY +APPLICABLE LAW. EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT +HOLDERS AND/OR OTHER PARTIES PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY +OF ANY KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, +THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR +PURPOSE. THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE PROGRAM +IS WITH YOU. SHOULD THE PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF +ALL NECESSARY SERVICING, REPAIR OR CORRECTION. + + 16. Limitation of Liability. + + IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING +WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MODIFIES AND/OR CONVEYS +THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, INCLUDING ANY +GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING OUT OF THE +USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED TO LOSS OF +DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD +PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER PROGRAMS), +EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF +SUCH DAMAGES. + + 17. Interpretation of Sections 15 and 16. + + If the disclaimer of warranty and limitation of liability provided +above cannot be given local legal effect according to their terms, +reviewing courts shall apply local law that most closely approximates +an absolute waiver of all civil liability in connection with the +Program, unless a warranty or assumption of liability accompanies a +copy of the Program in return for a fee. + + END OF TERMS AND CONDITIONS + + How to Apply These Terms to Your New Programs + + If you develop a new program, and you want it to be of the greatest +possible use to the public, the best way to achieve this is to make it +free software which everyone can redistribute and change under these terms. + + To do so, attach the following notices to the program. It is safest +to attach them to the start of each source file to most effectively +state the exclusion of warranty; and each file should have at least +the "copyright" line and a pointer to where the full notice is found. + + + Copyright (C) + + This program is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with this program. If not, see . + +Also add information on how to contact you by electronic and paper mail. + + If the program does terminal interaction, make it output a short +notice like this when it starts in an interactive mode: + + Copyright (C) + This program comes with ABSOLUTELY NO WARRANTY; for details type `show w'. + This is free software, and you are welcome to redistribute it + under certain conditions; type `show c' for details. + +The hypothetical commands `show w' and `show c' should show the appropriate +parts of the General Public License. Of course, your program's commands +might be different; for a GUI interface, you would use an "about box". + + You should also get your employer (if you work as a programmer) or school, +if any, to sign a "copyright disclaimer" for the program, if necessary. +For more information on this, and how to apply and follow the GNU GPL, see +. + + The GNU General Public License does not permit incorporating your program +into proprietary programs. If your program is a subroutine library, you +may consider it more useful to permit linking proprietary applications with +the library. If this is what you want to do, use the GNU Lesser General +Public License instead of this License. But first, please read +. diff --git a/resources/evm/preinstalls/LICENSE.create2-deployer b/resources/evm/preinstalls/LICENSE.create2-deployer new file mode 100644 index 0000000000..cf1ab25da0 --- /dev/null +++ b/resources/evm/preinstalls/LICENSE.create2-deployer @@ -0,0 +1,24 @@ +This is free and unencumbered software released into the public domain. + +Anyone is free to copy, modify, publish, use, compile, sell, or +distribute this software, either in source code form or as a compiled +binary, for any purpose, commercial or non-commercial, and by any +means. + +In jurisdictions that recognize copyright laws, the author or authors +of this software dedicate any and all copyright interest in the +software to the public domain. We make this dedication for the benefit +of the public at large and to the detriment of our heirs and +successors. We intend this dedication to be an overt act of +relinquishment in perpetuity of all present and future rights to this +software under copyright law. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, +EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF +MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. +IN NO EVENT SHALL THE AUTHORS BE LIABLE FOR ANY CLAIM, DAMAGES OR +OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, +ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR +OTHER DEALINGS IN THE SOFTWARE. + +For more information, please refer to diff --git a/resources/evm/preinstalls/LICENSE.erc2470-singleton-factory b/resources/evm/preinstalls/LICENSE.erc2470-singleton-factory new file mode 100644 index 0000000000..0e259d42c9 --- /dev/null +++ b/resources/evm/preinstalls/LICENSE.erc2470-singleton-factory @@ -0,0 +1,121 @@ +Creative Commons Legal Code + +CC0 1.0 Universal + + CREATIVE COMMONS CORPORATION IS NOT A LAW FIRM AND DOES NOT PROVIDE + LEGAL SERVICES. DISTRIBUTION OF THIS DOCUMENT DOES NOT CREATE AN + ATTORNEY-CLIENT RELATIONSHIP. CREATIVE COMMONS PROVIDES THIS + INFORMATION ON AN "AS-IS" BASIS. CREATIVE COMMONS MAKES NO WARRANTIES + REGARDING THE USE OF THIS DOCUMENT OR THE INFORMATION OR WORKS + PROVIDED HEREUNDER, AND DISCLAIMS LIABILITY FOR DAMAGES RESULTING FROM + THE USE OF THIS DOCUMENT OR THE INFORMATION OR WORKS PROVIDED + HEREUNDER. + +Statement of Purpose + +The laws of most jurisdictions throughout the world automatically confer +exclusive Copyright and Related Rights (defined below) upon the creator +and subsequent owner(s) (each and all, an "owner") of an original work of +authorship and/or a database (each, a "Work"). + +Certain owners wish to permanently relinquish those rights to a Work for +the purpose of contributing to a commons of creative, cultural and +scientific works ("Commons") that the public can reliably and without fear +of later claims of infringement build upon, modify, incorporate in other +works, reuse and redistribute as freely as possible in any form whatsoever +and for any purposes, including without limitation commercial purposes. +These owners may contribute to the Commons to promote the ideal of a free +culture and the further production of creative, cultural and scientific +works, or to gain reputation or greater distribution for their Work in +part through the use and efforts of others. + +For these and/or other purposes and motivations, and without any +expectation of additional consideration or compensation, the person +associating CC0 with a Work (the "Affirmer"), to the extent that he or she +is an owner of Copyright and Related Rights in the Work, voluntarily +elects to apply CC0 to the Work and publicly distribute the Work under its +terms, with knowledge of his or her Copyright and Related Rights in the +Work and the meaning and intended legal effect of CC0 on those rights. + +1. Copyright and Related Rights. A Work made available under CC0 may be +protected by copyright and related or neighboring rights ("Copyright and +Related Rights"). Copyright and Related Rights include, but are not +limited to, the following: + + i. the right to reproduce, adapt, distribute, perform, display, + communicate, and translate a Work; + ii. moral rights retained by the original author(s) and/or performer(s); +iii. publicity and privacy rights pertaining to a person's image or + likeness depicted in a Work; + iv. rights protecting against unfair competition in regards to a Work, + subject to the limitations in paragraph 4(a), below; + v. rights protecting the extraction, dissemination, use and reuse of data + in a Work; + vi. database rights (such as those arising under Directive 96/9/EC of the + European Parliament and of the Council of 11 March 1996 on the legal + protection of databases, and under any national implementation + thereof, including any amended or successor version of such + directive); and +vii. other similar, equivalent or corresponding rights throughout the + world based on applicable law or treaty, and any national + implementations thereof. + +2. Waiver. To the greatest extent permitted by, but not in contravention +of, applicable law, Affirmer hereby overtly, fully, permanently, +irrevocably and unconditionally waives, abandons, and surrenders all of +Affirmer's Copyright and Related Rights and associated claims and causes +of action, whether now known or unknown (including existing as well as +future claims and causes of action), in the Work (i) in all territories +worldwide, (ii) for the maximum duration provided by applicable law or +treaty (including future time extensions), (iii) in any current or future +medium and for any number of copies, and (iv) for any purpose whatsoever, +including without limitation commercial, advertising or promotional +purposes (the "Waiver"). Affirmer makes the Waiver for the benefit of each +member of the public at large and to the detriment of Affirmer's heirs and +successors, fully intending that such Waiver shall not be subject to +revocation, rescission, cancellation, termination, or any other legal or +equitable action to disrupt the quiet enjoyment of the Work by the public +as contemplated by Affirmer's express Statement of Purpose. + +3. Public License Fallback. Should any part of the Waiver for any reason +be judged legally invalid or ineffective under applicable law, then the +Waiver shall be preserved to the maximum extent permitted taking into +account Affirmer's express Statement of Purpose. In addition, to the +extent the Waiver is so judged Affirmer hereby grants to each affected +person a royalty-free, non transferable, non sublicensable, non exclusive, +irrevocable and unconditional license to exercise Affirmer's Copyright and +Related Rights in the Work (i) in all territories worldwide, (ii) for the +maximum duration provided by applicable law or treaty (including future +time extensions), (iii) in any current or future medium and for any number +of copies, and (iv) for any purpose whatsoever, including without +limitation commercial, advertising or promotional purposes (the +"License"). The License shall be deemed effective as of the date CC0 was +applied by Affirmer to the Work. Should any part of the License for any +reason be judged legally invalid or ineffective under applicable law, such +partial invalidity or ineffectiveness shall not invalidate the remainder +of the License, and in such case Affirmer hereby affirms that he or she +will not (i) exercise any of his or her remaining Copyright and Related +Rights in the Work or (ii) assert any associated claims and causes of +action with respect to the Work, in either case contrary to Affirmer's +express Statement of Purpose. + +4. Limitations and Disclaimers. + + a. No trademark or patent rights held by Affirmer are waived, abandoned, + surrendered, licensed or otherwise affected by this document. + b. Affirmer offers the Work as-is and makes no representations or + warranties of any kind concerning the Work, express, implied, + statutory or otherwise, including without limitation warranties of + title, merchantability, fitness for a particular purpose, non + infringement, or the absence of latent or other defects, accuracy, or + the present or absence of errors, whether or not discoverable, all to + the greatest extent permissible under applicable law. + c. Affirmer disclaims responsibility for clearing rights of other persons + that may apply to the Work or any use thereof, including without + limitation any person's Copyright and Related Rights in the Work. + Further, Affirmer disclaims responsibility for obtaining any necessary + consents, permissions or other rights required for any use of the + Work. + d. Affirmer understands and acknowledges that Creative Commons is not a + party to this document and has no duty or obligation with respect to + this CC0 or use of the Work. diff --git a/resources/evm/preinstalls/LICENSE.multicall3 b/resources/evm/preinstalls/LICENSE.multicall3 new file mode 100644 index 0000000000..2cc7ab2d68 --- /dev/null +++ b/resources/evm/preinstalls/LICENSE.multicall3 @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2023 Matt Solomon + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/resources/evm/preinstalls/LICENSE.openzeppelin-v510 b/resources/evm/preinstalls/LICENSE.openzeppelin-v510 new file mode 100644 index 0000000000..b2fee8f211 --- /dev/null +++ b/resources/evm/preinstalls/LICENSE.openzeppelin-v510 @@ -0,0 +1,22 @@ +The MIT License (MIT) + +Copyright (c) 2016-2024 Zeppelin Group Ltd + +Permission is hereby granted, free of charge, to any person obtaining +a copy of this software and associated documentation files (the +"Software"), to deal in the Software without restriction, including +without limitation the rights to use, copy, modify, merge, publish, +distribute, sublicense, and/or sell copies of the Software, and to +permit persons to whom the Software is furnished to do so, subject to +the following conditions: + +The above copyright notice and this permission notice shall be included +in all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS +OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF +MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. +IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY +CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, +TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE +SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. diff --git a/resources/evm/preinstalls/LICENSE.permit2 b/resources/evm/preinstalls/LICENSE.permit2 new file mode 100644 index 0000000000..b55441cdb0 --- /dev/null +++ b/resources/evm/preinstalls/LICENSE.permit2 @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2022 Uniswap Labs + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/resources/evm/preinstalls/LICENSE.safe-singleton-factory b/resources/evm/preinstalls/LICENSE.safe-singleton-factory new file mode 100644 index 0000000000..c9d273f6ea --- /dev/null +++ b/resources/evm/preinstalls/LICENSE.safe-singleton-factory @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2021 Safe Ecosystem Foundation + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/resources/evm/preinstalls/README.md b/resources/evm/preinstalls/README.md new file mode 100644 index 0000000000..25316d1dcf --- /dev/null +++ b/resources/evm/preinstalls/README.md @@ -0,0 +1,178 @@ +# EVM preinstall protocol assets + +These are the unchanged deployed runtime bytes formerly embedded in the local +chainspec. Filenames are lowercase runtime Keccak-256 hashes without `0x`. +The chainspec lists installation address and hash pairs in `[evm].preinstalls`. + +| Contract | Address | File / runtime Keccak-256 | Bytes | +| --- | --- | --- | --- | +| Multicall3 | `0xcA11bde05977b3631167028862bE2a173976CA11` | [d5c15df687b16f2ff992fc8d767b4216323184a2bbc6ee2f9c398c318e770891.bin](d5c15df687b16f2ff992fc8d767b4216323184a2bbc6ee2f9c398c318e770891.bin) | 3808 | +| Arachnid CREATE2 deployer | `0x4e59b44847b379578588920cA78FbF26c0B4956C` | [2fa86add0aed31f33a762c9d88e807c475bd51d0f52bd0955754b2608f7e4989.bin](2fa86add0aed31f33a762c9d88e807c475bd51d0f52bd0955754b2608f7e4989.bin) | 69 | +| Safe Singleton Factory | `0x914d7Fec6aaC8cd542e72Bca78B30650d45643d7` | [2fa86add0aed31f33a762c9d88e807c475bd51d0f52bd0955754b2608f7e4989.bin](2fa86add0aed31f33a762c9d88e807c475bd51d0f52bd0955754b2608f7e4989.bin) | 69 | +| ERC-2470 Singleton Factory | `0xce0042B868300000d44A59004Da54A005ffdcf9f` | [c4d5542b53a8b779595a20a8ddd60e58a6c49d3c3decc2df83ced1c69c8ca807.bin](c4d5542b53a8b779595a20a8ddd60e58a6c49d3c3decc2df83ced1c69c8ca807.bin) | 308 | +| Permit2 | `0x000000000022D473030F116dDEE9F6B43aC78BA3` | [c67d1657868aa5146eaf24fb879fb1fdec3d2d493b3683a61c9c2f4fb2851131.bin](c67d1657868aa5146eaf24fb879fb1fdec3d2d493b3683a61c9c2f4fb2851131.bin) | 9152 | +| SenderCreator v0.8 | `0x449ED7C3e6Fee6a97311d4b55475DF59C44AdD33` | [c69a1b3a000d570bc86eb096ee63a9014a17951ad616d720882ec61432b00fcf.bin](c69a1b3a000d570bc86eb096ee63a9014a17951ad616d720882ec61432b00fcf.bin) | 1217 | +| EntryPoint v0.8 | `0x4337084D9E255Ff0702461CF8895CE9E3b5Ff108` | [44e632a24c6f2600cbd5b5b8b4c2d372359112c8b5774297f5fd0a9e64f11f86.bin](44e632a24c6f2600cbd5b5b8b4c2d372359112c8b5774297f5fd0a9e64f11f86.bin) | 21738 | + +Copy this entire directory, including attribution and license notices, to +`preinstalls/` beside each protocol package's `chainspec.toml`. Startup and +upgrade discovery validate every listed runtime when EVM is enabled. +Activation reads and verifies the files again when enabled. No download or lookup +in other protocol directories is performed. + +## Runtime provenance + +Genesis and protocol upgrade commit upsert the configured contracts after the +EVM predeploys when EVM execution is enabled. Both genesis storage formats use +the same upsert logic. Installation preserves existing account metadata, +balances, and storage, and rejects conflicting code. + +Each `.bin` file contains raw runtime bytecode, including compiler metadata. +It must not contain hexadecimal text or creation bytecode. Runtime assets are +loaded from the selected protocol package at activation; they are not embedded +in the node executable. + +## Multicall3 + +- Address: `0xcA11bde05977b3631167028862bE2a173976CA11`. +- Upstream commit: `b667d67ecfa5361a81e8f110234ce242613b0012`. +- Source: [official signed deployment transaction](https://github.com/mds1/multicall3/blob/b667d67ecfa5361a81e8f110234ce242613b0012/README.md#new-deployments). +- Deployment transaction hash: `0x07471adfe8f4ec553c1199f495be97fc8be8e0626ae307281c22534460184ed1`. +- Runtime length: 3,808 bytes. +- Runtime Keccak-256: `0xd5c15df687b16f2ff992fc8d767b4216323184a2bbc6ee2f9c398c318e770891`. +- License: MIT; the upstream notice is retained in `LICENSE.multicall3`. + +The deployment's 3,840-byte input begins with the 32-byte creation prefix +`608060405234801561001057600080fd5b50610ee0806100206000396000f3fe`. +That prefix copies and returns the remaining 3,808 bytes as runtime code. +`d5c15df687b16f2ff992fc8d767b4216323184a2bbc6ee2f9c398c318e770891.bin` contains those remaining bytes unchanged. Its address, length, +and hash are pinned by integration tests; no network access or Solidity compiler is +needed during compilation, genesis, or protocol upgrade. + +## Arachnid CREATE2 deployer + +- Address: `0x4e59b44847b379578588920cA78FbF26c0B4956C`. +- Upstream commit: `be3c5974db5028d502537209329ff2e730ed336c`. +- Source: [official signed deployment transaction](https://github.com/Arachnid/deterministic-deployment-proxy/blob/be3c5974db5028d502537209329ff2e730ed336c/README.md#deployment-transaction). +- Deployment transaction hash: `0xeddf9e61fb9d8f5111840daef55e5fde0041f5702856532cdbb5a02998033d26`. +- Runtime length: 69 bytes. +- Runtime Keccak-256: `0x2fa86add0aed31f33a762c9d88e807c475bd51d0f52bd0955754b2608f7e4989`. +- License: Unlicense; retained in `LICENSE.create2-deployer`. + +The deployment's 83-byte input starts with the 14-byte creation prefix +`604580600e600039806000f350fe`. The remaining 69 bytes are returned unchanged +as runtime code and stored in `2fa86add0aed31f33a762c9d88e807c475bd51d0f52bd0955754b2608f7e4989.bin`. They match `eth_getCode` +on Ethereum mainnet at block 26,104,339. The factory has no constructor storage +or runtime immutables to initialize. + +## Safe Singleton Factory + +- Address: `0x914d7Fec6aaC8cd542e72Bca78B30650d45643d7`. +- Upstream commit: `e550557e891dc0418c46daf785198dc05df6645c`. +- Source: [official Ethereum deployment transaction](https://github.com/safe-fndn/safe-singleton-factory/blob/e550557e891dc0418c46daf785198dc05df6645c/artifacts/1/deployment.json). +- Deployment transaction hash: `0x69c275b5304db980105b7a6d731f9e1157a3fe29e7ff6ff95235297df53e9928`. +- Runtime length: 69 bytes. +- Runtime Keccak-256: `0x2fa86add0aed31f33a762c9d88e807c475bd51d0f52bd0955754b2608f7e4989`. +- License: MIT; the upstream notice is retained in `LICENSE.safe-singleton-factory`. + +The deployment has the same 14-byte creation prefix and 69-byte runtime as +the Arachnid proxy. `2fa86add0aed31f33a762c9d88e807c475bd51d0f52bd0955754b2608f7e4989.bin` contains that runtime unchanged; +it matches `eth_getCode` on Ethereum mainnet at block 26,104,339. Its distinct +canonical address comes from Safe's deployer account, not a bytecode change. +No constructor storage or runtime immutables need initialization. Both +factories share a content-addressed bytecode record in global state. + +## ERC-2470 Singleton Factory + +- Address: `0xce0042B868300000d44A59004Da54A005ffdcf9f`. +- Specification commit: `8dd085d159cb123f545c272c0d871a5339550e79`. +- Source: [the specification's signed deployment transaction](https://github.com/ethereum/ercs/blob/8dd085d159cb123f545c272c0d871a5339550e79/ERCS/erc-2470.md#deployment-transaction). +- Deployment transaction hash: `0x803351deb6d745e91545a6a3e1c0ea3e9a6a02a1a4193b70edfcd2f40f71a01c`. +- Runtime length: 308 bytes. +- Runtime Keccak-256: `0xc4d5542b53a8b779595a20a8ddd60e58a6c49d3c3decc2df83ced1c69c8ca807`. +- License: CC0-1.0; retained in `LICENSE.erc2470-singleton-factory`. + +The deployment's 340-byte input starts with the 32-byte creation prefix +`608060405234801561001057600080fd5b50610134806100206000396000f3fe`. +The remaining 308 bytes are returned unchanged as runtime code and stored in +`c4d5542b53a8b779595a20a8ddd60e58a6c49d3c3decc2df83ced1c69c8ca807.bin`. They match `eth_getCode` on Ethereum mainnet +at block 26,104,339. The factory needs no constructor storage or runtime +immutable initialization. + +## Permit2 + +- Address: `0x000000000022D473030F116dDEE9F6B43aC78BA3`. +- Upstream commit: `cc56ad0f3439c502c246fc5cfcc3db92bb8b7219`. +- Source: Ethereum mainnet runtime, fetched with `eth_getCode` at block 26,104,339 + through `https://ethereum-rpc.publicnode.com`. +- Upstream reference: [precompiled runtime utility](https://github.com/Uniswap/permit2/blob/cc56ad0f3439c502c246fc5cfcc3db92bb8b7219/test/utils/DeployPermit2.sol). +- Runtime length: 9,152 bytes. +- Runtime Keccak-256: `0xc67d1657868aa5146eaf24fb879fb1fdec3d2d493b3683a61c9c2f4fb2851131`. +- License: MIT; the upstream notice is retained in `LICENSE.permit2`. + +`c67d1657868aa5146eaf24fb879fb1fdec3d2d493b3683a61c9c2f4fb2851131.bin` contains the deployed runtime unchanged, including constructor +immutables. It matches the pinned upstream utility's runtime except for the +constructor-derived cached chain ID and domain separator. The mainnet cached +chain ID is 1 and its separator is +`0x866a5aba21966af95d6c7ab78eb2b2fc913915c28be3b9aa07cc04ff903e3f28`. +The [EIP-712 implementation](https://github.com/Uniswap/permit2/blob/cc56ad0f3439c502c246fc5cfcc3db92bb8b7219/src/EIP712.sol) +recomputes the separator when `block.chainid` differs from the cache. Integration +tests exercise cached and recomputed domains, accept a signed allowance on +chain 7, and reject replay. No constructor storage initialization is required. + +## SenderCreator v0.8 + +- Address: `0x449ED7C3e6Fee6a97311d4b55475DF59C44AdD33`. +- Release: `eth-infinitism/account-abstraction` v0.8.0. +- Upstream commit: `4cbc06072cdc19fd60f285c5997f4f7f57a588de`. +- Source: Ethereum mainnet runtime, fetched with `eth_getCode` at block 26,104,339 + through `https://ethereum-rpc.publicnode.com`. +- Creator: EntryPoint v0.8 at `0x4337084D9E255Ff0702461CF8895CE9E3b5Ff108`, + using CREATE nonce 1 during construction. +- Upstream reference: [canonical EntryPoint deployment artifact](https://github.com/eth-infinitism/account-abstraction/blob/4cbc06072cdc19fd60f285c5997f4f7f57a588de/deployments/ethereum/EntryPoint.json). +- Runtime length: 1,217 bytes. +- Runtime Keccak-256: `0xc69a1b3a000d570bc86eb096ee63a9014a17951ad616d720882ec61432b00fcf`. +- License: GPL-3.0 (upstream SPDX identifier); retained in `LICENSE.account-abstraction-v08`. + +`c69a1b3a000d570bc86eb096ee63a9014a17951ad616d720882ec61432b00fcf.bin` contains the deployed runtime unchanged. Its immutable +authorized EntryPoint address occurs at three call/getter sites. Replacing +that address with the compiler's zero placeholder reproduces the 1,217-byte +runtime template embedded in the pinned EntryPoint creation bytecode at +offset 22,958. The helper has no constructor storage to initialize. + +The matching EntryPoint and SenderCreator runtimes must be activated together +at their canonical addresses. Direct runtime installation executes neither +constructor. The corresponding [Solidity sources and compiler settings](https://github.com/eth-infinitism/account-abstraction/blob/4cbc06072cdc19fd60f285c5997f4f7f57a588de/deployments/ethereum/solcInputs/594e0595d5f1f4861d3b32a5f38cc32f.json) +are pinned in the upstream deployment's complete compiler input. + +## EntryPoint v0.8 + +- Address: `0x4337084D9E255Ff0702461CF8895CE9E3b5Ff108`. +- Release: `eth-infinitism/account-abstraction` v0.8.0. +- Upstream commit: `4cbc06072cdc19fd60f285c5997f4f7f57a588de`. +- Source: Ethereum mainnet runtime, fetched with `eth_getCode` at block 26,104,339 + through `https://ethereum-rpc.publicnode.com`. +- Upstream reference: [canonical deployment artifact](https://github.com/eth-infinitism/account-abstraction/blob/4cbc06072cdc19fd60f285c5997f4f7f57a588de/deployments/ethereum/EntryPoint.json). +- Deployment transaction hash: `0xae4eafd1fd17e03b5200f6e2ecad4f669b41d9ba3a4fa0018e92cde985b33129`. +- Runtime length: 21,738 bytes. +- Runtime Keccak-256: `0x44e632a24c6f2600cbd5b5b8b4c2d372359112c8b5774297f5fd0a9e64f11f86`. +- License: GPL-3.0 (upstream SPDX identifier), retained in `LICENSE.account-abstraction-v08`; + OpenZeppelin Contracts v5.1.0's MIT notice is retained in `LICENSE.openzeppelin-v510`. + +`44e632a24c6f2600cbd5b5b8b4c2d372359112c8b5774297f5fd0a9e64f11f86.bin` contains the deployed runtime unchanged. The artifact's +`deployedBytecode` is a compiler template with zero immutable placeholders, +and must not be installed as-is. The mainnet runtime fills those placeholders +with the matching SenderCreator address and the constructor's EIP-712 values: +the EntryPoint address, cached chain ID 1 and domain separator, hashed name and +version, and short-string encodings of `ERC4337` and `1`. The template is +embedded in the pinned creation bytecode at offset 1,168; all differences +between it and the installed runtime are these constructor immutables. + +The complete compiler input linked above records Solidity 0.8.28, Cancun, +via-IR compilation, and optimizer runs 1,000,000. No compiler is needed at node +build or installation time. EntryPoint v0.8 uses a transient reentrancy guard; +the Prague EVM supports its opcodes. Deposits, stakes, and nonce mappings start +at zero, and the short EIP-712 name/version need no fallback string storage. +Existing state remains intact during upserts. Integration tests verify paired +account creation at genesis and upgrades, domain selection across chain IDs, +and repeated empty bundles through the transient guard. diff --git a/resources/evm/preinstalls/c4d5542b53a8b779595a20a8ddd60e58a6c49d3c3decc2df83ced1c69c8ca807.bin b/resources/evm/preinstalls/c4d5542b53a8b779595a20a8ddd60e58a6c49d3c3decc2df83ced1c69c8ca807.bin new file mode 100644 index 0000000000..a70fd873ee Binary files /dev/null and b/resources/evm/preinstalls/c4d5542b53a8b779595a20a8ddd60e58a6c49d3c3decc2df83ced1c69c8ca807.bin differ diff --git a/resources/evm/preinstalls/c67d1657868aa5146eaf24fb879fb1fdec3d2d493b3683a61c9c2f4fb2851131.bin b/resources/evm/preinstalls/c67d1657868aa5146eaf24fb879fb1fdec3d2d493b3683a61c9c2f4fb2851131.bin new file mode 100644 index 0000000000..7ab1aadc48 Binary files /dev/null and b/resources/evm/preinstalls/c67d1657868aa5146eaf24fb879fb1fdec3d2d493b3683a61c9c2f4fb2851131.bin differ diff --git a/resources/evm/preinstalls/c69a1b3a000d570bc86eb096ee63a9014a17951ad616d720882ec61432b00fcf.bin b/resources/evm/preinstalls/c69a1b3a000d570bc86eb096ee63a9014a17951ad616d720882ec61432b00fcf.bin new file mode 100644 index 0000000000..0a8c61fb52 Binary files /dev/null and b/resources/evm/preinstalls/c69a1b3a000d570bc86eb096ee63a9014a17951ad616d720882ec61432b00fcf.bin differ diff --git a/resources/evm/preinstalls/d5c15df687b16f2ff992fc8d767b4216323184a2bbc6ee2f9c398c318e770891.bin b/resources/evm/preinstalls/d5c15df687b16f2ff992fc8d767b4216323184a2bbc6ee2f9c398c318e770891.bin new file mode 100644 index 0000000000..30e2fdcbe6 Binary files /dev/null and b/resources/evm/preinstalls/d5c15df687b16f2ff992fc8d767b4216323184a2bbc6ee2f9c398c318e770891.bin differ diff --git a/storage/src/system/evm.rs b/storage/src/system/evm.rs index 784fa51db7..357fb46897 100644 --- a/storage/src/system/evm.rs +++ b/storage/src/system/evm.rs @@ -1,8 +1,9 @@ //! EVM system contract support. +use alloy_primitives::keccak256; use casper_types::{ - evm, ByteCode, ByteCodeKind, CLValue, CLValueError, EvmAddr, EvmConfig, EvmSpec, Key, - StoredValue, + bytesrepr::Bytes, evm, ByteCode, ByteCodeKind, CLValue, CLValueError, EvmAddr, EvmConfig, + EvmSpec, Key, StoredValue, }; use thiserror::Error; @@ -12,30 +13,62 @@ use crate::{ tracking_copy::{TrackingCopy, TrackingCopyError}, }; -/// Error returned while installing or validating an EVM predeploy. +/// Error returned while installing or validating an EVM contract's code. #[derive(Debug, Error)] -pub(crate) enum EvmPredeployError { +pub(crate) enum EvmContractError { + /// A configured preinstall has no runtime bytecode. + #[error("EVM preinstall at {address} has empty runtime bytecode")] + EmptyPreinstall { + /// Configured address. + address: evm::Address, + }, + /// Runtime bytecode was not supplied for a configured checksum. + #[error("EVM preinstall at {address} is missing runtime bytecode for {expected}")] + MissingPreinstall { + /// Configured address. + address: evm::Address, + /// Configured checksum. + expected: evm::Hash, + }, + /// Supplied runtime bytecode does not match the configured checksum. + #[error("EVM preinstall at {address} has runtime hash {actual}, expected {expected}")] + PreinstallHashMismatch { + /// Configured address. + address: evm::Address, + /// Configured checksum. + expected: evm::Hash, + /// Computed checksum. + actual: evm::Hash, + }, /// Failed to read from the tracking copy. #[error(transparent)] TrackingCopy(#[from] TrackingCopyError), /// Failed to build or decode a CLValue. #[error("CLValue error: {0}")] CLValue(String), - /// The reserved predeploy address already has non-empty code. - #[error("reserved EVM predeploy address has conflicting code hash {actual}")] + /// The reserved contract address already has different non-empty code. + #[error( + "reserved {name} address {address} has conflicting code hash {actual}, expected {expected}" + )] ConflictingCodeHash { + /// Contract being installed. + name: &'static str, + /// Reserved address. + address: evm::Address, + /// Expected code hash. + expected: evm::Hash, /// Actual code hash found under the reserved address. actual: evm::Hash, }, /// The expected bytecode key contains incompatible data. - #[error("EVM predeploy bytecode conflict at {key}: {details}")] + #[error("EVM contract bytecode conflict at {key}: {details}")] ConflictingByteCode { /// Global-state key where the conflict was found. key: Box, /// Conflict details. details: String, }, - /// An EVM predeploy key contains an unexpected stored value. + /// An EVM code key contains an unexpected stored value. #[error("unexpected stored value at {key}: expected {expected}, found {found}")] UnexpectedStoredValue { /// Global-state key that was read. @@ -47,21 +80,30 @@ pub(crate) enum EvmPredeployError { }, } -impl From for EvmPredeployError { +impl From for EvmContractError { fn from(error: CLValueError) -> Self { - EvmPredeployError::CLValue(error.to_string()) + EvmContractError::CLValue(error.to_string()) } } #[derive(Clone, Copy)] -struct EvmPredeploy { +struct EvmContract<'a> { name: &'static str, address: evm::Address, - code: &'static [u8], + code: &'a [u8], code_hash: evm::Hash, } -impl EvmPredeploy { +impl<'a> EvmContract<'a> { + fn preinstall(address: evm::Address, code: &'a [u8]) -> Self { + Self { + name: "EVM preinstall", + address, + code, + code_hash: evm::Hash::new(keccak256(code).0), + } + } + fn eip2935() -> Self { Self { name: "EIP-2935", @@ -88,7 +130,7 @@ impl EvmPredeploy { Key::Evm(EvmAddr::ByteCode(self.code_hash)) } - fn code_hash_value(self) -> Result { + fn code_hash_value(self) -> Result { Ok(StoredValue::CLValue(CLValue::from_t(self.code_hash)?)) } @@ -97,8 +139,8 @@ impl EvmPredeploy { } } -fn prague_predeploys() -> [EvmPredeploy; 2] { - [EvmPredeploy::eip4788(), EvmPredeploy::eip2935()] +fn prague_predeploys() -> [EvmContract<'static>; 2] { + [EvmContract::eip4788(), EvmContract::eip2935()] } /// Returns whether Prague EVM predeploys should be installed for the supplied EVM config. @@ -109,21 +151,69 @@ pub(crate) fn should_upsert_prague_predeploys(config: &EvmConfig) -> bool { /// Idempotently installs the Prague EVM predeploys. pub(crate) fn upsert_prague_predeploys( tracking_copy: &mut TrackingCopy, -) -> Result<(), EvmPredeployError> +) -> Result<(), EvmContractError> where R: StateReader, { for predeploy in prague_predeploys() { - upsert_code_hash(tracking_copy, predeploy)?; - upsert_byte_code(tracking_copy, predeploy)?; + upsert_contract(tracking_copy, predeploy)?; + } + Ok(()) +} + +/// Verifies separately supplied runtime bytecode and idempotently installs preinstalls. +pub(crate) fn upsert_preinstalls( + tracking_copy: &mut TrackingCopy, + preinstalls: &[(evm::Address, evm::Hash)], + bytecodes: &[(evm::Hash, Bytes)], +) -> Result<(), EvmContractError> +where + R: StateReader, +{ + // Validate the entire payload before writing any preinstall state. + let mut contracts = Vec::with_capacity(preinstalls.len()); + for (address, expected) in preinstalls { + let code = bytecodes + .iter() + .find_map(|(hash, code)| (hash == expected).then_some(code)) + .ok_or(EvmContractError::MissingPreinstall { + address: *address, + expected: *expected, + })?; + if code.is_empty() { + return Err(EvmContractError::EmptyPreinstall { address: *address }); + } + let contract = EvmContract::preinstall(*address, code); + if contract.code_hash != *expected { + return Err(EvmContractError::PreinstallHashMismatch { + address: *address, + expected: *expected, + actual: contract.code_hash, + }); + } + contracts.push(contract); + } + for contract in contracts { + upsert_contract(tracking_copy, contract)?; } Ok(()) } +fn upsert_contract( + tracking_copy: &mut TrackingCopy, + contract: EvmContract<'_>, +) -> Result<(), EvmContractError> +where + R: StateReader, +{ + upsert_code_hash(tracking_copy, contract)?; + upsert_byte_code(tracking_copy, contract) +} + #[cfg(test)] fn predeploy_entries( - predeploy: EvmPredeploy, -) -> Result, EvmPredeployError> { + predeploy: EvmContract<'_>, +) -> Result, EvmContractError> { Ok(vec![ (predeploy.code_hash_key(), predeploy.code_hash_value()?), (predeploy.byte_code_key(), predeploy.byte_code_value()), @@ -131,7 +221,7 @@ fn predeploy_entries( } #[cfg(test)] -fn prague_predeploy_entries() -> Result, EvmPredeployError> { +fn prague_predeploy_entries() -> Result, EvmContractError> { prague_predeploys() .iter() .copied() @@ -142,8 +232,8 @@ fn prague_predeploy_entries() -> Result, EvmPredeployErr fn upsert_code_hash( tracking_copy: &mut TrackingCopy, - predeploy: EvmPredeploy, -) -> Result<(), EvmPredeployError> + predeploy: EvmContract<'_>, +) -> Result<(), EvmContractError> where R: StateReader, { @@ -162,10 +252,15 @@ where tracking_copy.write(key, predeploy.code_hash_value()?); return Ok(()); } - return Err(EvmPredeployError::ConflictingCodeHash { actual }); + return Err(EvmContractError::ConflictingCodeHash { + name: predeploy.name, + address: predeploy.address, + expected, + actual, + }); } Some(stored_value) => { - return Err(EvmPredeployError::UnexpectedStoredValue { + return Err(EvmContractError::UnexpectedStoredValue { key: Box::new(key), expected: "StoredValue::CLValue(evm::Hash)", found: stored_value.type_name(), @@ -177,8 +272,8 @@ where fn upsert_byte_code( tracking_copy: &mut TrackingCopy, - predeploy: EvmPredeploy, -) -> Result<(), EvmPredeployError> + predeploy: EvmContract<'_>, +) -> Result<(), EvmContractError> where R: StateReader, { @@ -191,7 +286,7 @@ where if byte_code.kind() == ByteCodeKind::EvmPrague && byte_code.bytes() == predeploy.code { return Ok(()); } - return Err(EvmPredeployError::ConflictingByteCode { + return Err(EvmContractError::ConflictingByteCode { key: Box::new(key), details: format!( "expected Prague {} bytecode, found kind {} with {} bytes", @@ -202,7 +297,7 @@ where }); } Some(stored_value) => { - return Err(EvmPredeployError::UnexpectedStoredValue { + return Err(EvmContractError::UnexpectedStoredValue { key: Box::new(key), expected: "StoredValue::ByteCode(EvmPrague)", found: stored_value.type_name(), @@ -219,6 +314,29 @@ mod tests { use super::*; use crate::global_state::state::{self, lmdb::LmdbGlobalStateView, StateProvider as _}; + fn test_contracts() -> [EvmContract<'static>; 2] { + // Two addresses share a tiny, project-authored runtime returning 42. + let code = &[0x60, 0x2a, 0x60, 0, 0x52, 0x60, 0x20, 0x60, 0, 0xf3]; + [ + EvmContract::preinstall(evm::Address::new([1; 20]), code), + EvmContract::preinstall(evm::Address::new([2; 20]), code), + ] + } + + fn test_preinstalls() -> Vec<(evm::Address, evm::Hash)> { + test_contracts() + .iter() + .map(|contract| (contract.address, contract.code_hash)) + .collect() + } + + fn test_bytecodes() -> Vec<(evm::Hash, Bytes)> { + test_contracts() + .iter() + .map(|contract| (contract.code_hash, Bytes::from(contract.code))) + .collect() + } + fn tracking_copy( initial_data: impl IntoIterator, ) -> (TrackingCopy, impl Send) { @@ -240,7 +358,7 @@ mod tests { fn assert_predeploy_present( tracking_copy: &mut TrackingCopy, - predeploy: EvmPredeploy, + predeploy: EvmContract<'_>, ) { assert_eq!( read(tracking_copy, &predeploy.code_hash_key()), @@ -271,13 +389,13 @@ mod tests { upsert_prague_predeploys(&mut tracking_copy).expect("upsert should succeed"); - assert_predeploy_present(&mut tracking_copy, EvmPredeploy::eip4788()); - assert_predeploy_present(&mut tracking_copy, EvmPredeploy::eip2935()); + assert_predeploy_present(&mut tracking_copy, EvmContract::eip4788()); + assert_predeploy_present(&mut tracking_copy, EvmContract::eip2935()); } #[test] fn upsert_repairs_missing_eip2935_bytecode() { - let predeploy = EvmPredeploy::eip2935(); + let predeploy = EvmContract::eip2935(); let (mut tracking_copy, _tempdir) = tracking_copy([( predeploy.code_hash_key(), predeploy @@ -300,7 +418,7 @@ mod tests { #[test] fn upsert_rejects_conflicting_eip2935_non_empty_code_hash() { - let predeploy = EvmPredeploy::eip2935(); + let predeploy = EvmContract::eip2935(); let conflicting_hash = evm::Hash::new([1; evm::HASH_LENGTH]); let (mut tracking_copy, _tempdir) = tracking_copy([( predeploy.code_hash_key(), @@ -312,15 +430,15 @@ mod tests { assert!(matches!( error, - EvmPredeployError::ConflictingCodeHash { - actual + EvmContractError::ConflictingCodeHash { + actual, .. } if actual == conflicting_hash )); } #[test] fn upsert_rejects_conflicting_eip2935_bytecode() { - let predeploy = EvmPredeploy::eip2935(); + let predeploy = EvmContract::eip2935(); let (mut tracking_copy, _tempdir) = tracking_copy([ ( predeploy.code_hash_key(), @@ -339,13 +457,13 @@ mod tests { assert!(matches!( error, - EvmPredeployError::ConflictingByteCode { .. } + EvmContractError::ConflictingByteCode { .. } )); } #[test] fn upsert_repairs_empty_eip2935_code_hash() { - let predeploy = EvmPredeploy::eip2935(); + let predeploy = EvmContract::eip2935(); let (mut tracking_copy, _tempdir) = tracking_copy([( predeploy.code_hash_key(), StoredValue::CLValue( @@ -360,7 +478,7 @@ mod tests { #[test] fn upsert_rejects_unexpected_eip2935_code_hash_value() { - let predeploy = EvmPredeploy::eip2935(); + let predeploy = EvmContract::eip2935(); let (mut tracking_copy, _tempdir) = tracking_copy([( predeploy.code_hash_key(), StoredValue::CLValue( @@ -374,6 +492,232 @@ mod tests { let error = upsert_prague_predeploys(&mut tracking_copy) .expect_err("invalid code hash value should fail"); - assert!(matches!(error, EvmPredeployError::CLValue(_))); + assert!(matches!(error, EvmContractError::CLValue(_))); + } + + #[test] + fn empty_preinstall_list_does_not_write_state() { + let (mut tracking_copy, _tempdir) = tracking_copy([]); + upsert_preinstalls(&mut tracking_copy, &[], &[]).unwrap(); + let (writes, prunes, _) = tracking_copy.destructure(); + assert!(writes.is_empty()); + assert!(prunes.is_empty()); + } + + #[test] + fn empty_runtime_bytecode_is_rejected() { + let address = evm::Address::new([1; 20]); + let expected = evm::Hash::new(keccak256([]).0); + let preinstalls = vec![(address, expected)]; + let bytecodes = vec![(expected, Bytes::new())]; + let (mut tracking_copy, _tempdir) = tracking_copy([]); + let error = upsert_preinstalls(&mut tracking_copy, &preinstalls, &bytecodes).unwrap_err(); + assert!( + matches!(error, EvmContractError::EmptyPreinstall { address: actual } if actual == address) + ); + let (writes, prunes, _) = tracking_copy.destructure(); + assert!(writes.is_empty()); + assert!(prunes.is_empty()); + } + + #[test] + fn invalid_preinstall_payload_is_rejected_before_writing_state() { + let contracts = test_contracts(); + let missing = evm::Hash::new([9; 32]); + let preinstalls = vec![ + (contracts[0].address, contracts[0].code_hash), + (contracts[1].address, missing), + ]; + for bytecodes in [test_bytecodes(), { + let mut bytecodes = test_bytecodes(); + bytecodes.push((missing, vec![0x00].into())); + bytecodes + }] { + let (mut tracking_copy, _tempdir) = tracking_copy([]); + let error = + upsert_preinstalls(&mut tracking_copy, &preinstalls, &bytecodes).unwrap_err(); + assert!(matches!(error, + EvmContractError::MissingPreinstall { address, expected } + | EvmContractError::PreinstallHashMismatch { address, expected, .. } + if address == contracts[1].address && expected == missing + )); + let (writes, prunes, _) = tracking_copy.destructure(); + assert!(writes.is_empty()); + assert!(prunes.is_empty()); + } + } + + #[test] + fn upsert_creates_missing_preinstalls_without_predeploys() { + let (mut tracking_copy, _tempdir) = tracking_copy([]); + + upsert_preinstalls(&mut tracking_copy, &test_preinstalls(), &test_bytecodes()) + .expect("preinstall upsert should succeed"); + + for preinstall in test_contracts() { + assert_predeploy_present(&mut tracking_copy, preinstall); + } + for predeploy in prague_predeploys() { + assert!(tracking_copy + .read(&predeploy.code_hash_key()) + .unwrap() + .is_none()); + } + } + + #[test] + fn upsert_preinstalls_repairs_empty_hash_and_missing_bytecode() { + let contract = test_contracts()[0]; + for hash in [ + evm::EMPTY_CODE_HASH, + evm::Hash::new([0; 32]), + contract.code_hash, + ] { + let (mut tracking_copy, _tempdir) = tracking_copy([( + contract.code_hash_key(), + StoredValue::CLValue(CLValue::from_t(hash).unwrap()), + )]); + + upsert_preinstalls(&mut tracking_copy, &test_preinstalls(), &test_bytecodes()) + .expect("preinstall repair should succeed"); + + assert_predeploy_present(&mut tracking_copy, contract); + } + } + + #[test] + fn upsert_preinstalls_noops_when_canonical_code_is_present() { + let entries = test_contracts() + .iter() + .flat_map(|preinstall| predeploy_entries(*preinstall).unwrap()) + .collect::>(); + let (mut tracking_copy, _tempdir) = tracking_copy(entries); + + upsert_preinstalls(&mut tracking_copy, &test_preinstalls(), &test_bytecodes()) + .expect("preinstall upsert should succeed"); + + let (writes, prunes, _) = tracking_copy.destructure(); + assert!(writes.is_empty()); + assert!(prunes.is_empty()); + } + + #[test] + fn upsert_preinstalls_preserves_account_metadata_balance_and_storage() { + let preserved = test_contracts() + .iter() + .flat_map(|preinstall| { + let contract = *preinstall; + let purse = evm::deterministic_purse(contract.address); + vec![ + ( + Key::Evm(EvmAddr::Account(contract.address)), + StoredValue::CLValue(CLValue::from_t(Key::URef(purse)).unwrap()), + ), + ( + Key::Evm(EvmAddr::Nonce(contract.address)), + StoredValue::CLValue(CLValue::from_t(9u64).unwrap()), + ), + ( + Key::Balance(purse.addr()), + StoredValue::CLValue( + CLValue::from_t(casper_types::U512::from(123)).unwrap(), + ), + ), + ( + Key::Evm(EvmAddr::Storage(evm::StorageAddr::new( + contract.address, + casper_types::U256::from(7), + ))), + StoredValue::CLValue( + CLValue::from_t(casper_types::U256::from(42)).unwrap(), + ), + ), + ] + }) + .collect::>(); + let (mut tracking_copy, _tempdir) = tracking_copy(preserved.clone()); + + upsert_preinstalls(&mut tracking_copy, &test_preinstalls(), &test_bytecodes()) + .expect("preinstall upsert should succeed"); + + for preinstall in test_contracts() { + assert_predeploy_present(&mut tracking_copy, preinstall); + } + for (key, value) in preserved { + assert_eq!(read(&mut tracking_copy, &key), value); + } + let (writes, prunes, _) = tracking_copy.destructure(); + let expected = test_contracts() + .iter() + .flat_map(|preinstall| predeploy_entries(*preinstall).unwrap()) + .collect::>(); + assert_eq!( + writes + .into_iter() + .collect::>(), + expected + ); + assert!(prunes.is_empty()); + } + + #[test] + fn upsert_preinstalls_rejects_conflicting_code_hash() { + let contract = test_contracts()[0]; + let (mut tracking_copy, _tempdir) = tracking_copy([( + contract.code_hash_key(), + StoredValue::CLValue(CLValue::from_t(evm::Hash::new([1; 32])).unwrap()), + )]); + + let error = upsert_preinstalls(&mut tracking_copy, &test_preinstalls(), &test_bytecodes()) + .expect_err("conflict should fail"); + + assert!( + matches!(error, EvmContractError::ConflictingCodeHash { address, name, .. } + if address == contract.address && name == "EVM preinstall") + ); + let (writes, prunes, _) = tracking_copy.destructure(); + assert!(writes.is_empty()); + assert!(prunes.is_empty()); + } + + #[test] + fn upsert_preinstalls_rejects_conflicting_bytecode_or_kind() { + let contract = test_contracts()[0]; + for bytecode in [ + ByteCode::new(ByteCodeKind::EvmPrague, vec![0xfe]), + ByteCode::new(ByteCodeKind::V1CasperWasm, contract.code.to_vec()), + ] { + let (mut tracking_copy, _tempdir) = tracking_copy([ + ( + contract.code_hash_key(), + contract.code_hash_value().unwrap(), + ), + (contract.byte_code_key(), StoredValue::ByteCode(bytecode)), + ]); + + let error = + upsert_preinstalls(&mut tracking_copy, &test_preinstalls(), &test_bytecodes()) + .expect_err("conflict should fail"); + + assert!(matches!( + error, + EvmContractError::ConflictingByteCode { .. } + )); + let (writes, prunes, _) = tracking_copy.destructure(); + assert!(writes.is_empty()); + assert!(prunes.is_empty()); + } + } + + #[test] + fn upsert_preinstalls_rejects_malformed_code_records() { + let contract = test_contracts()[0]; + for key in [contract.code_hash_key(), contract.byte_code_key()] { + let (mut tracking_copy, _tempdir) = + tracking_copy([(key, StoredValue::CLValue(CLValue::from_t(42u64).unwrap()))]); + + upsert_preinstalls(&mut tracking_copy, &test_preinstalls(), &test_bytecodes()) + .expect_err("malformed record should fail"); + } } } diff --git a/storage/src/system/genesis.rs b/storage/src/system/genesis.rs index d117849d2c..88be6ca62a 100644 --- a/storage/src/system/genesis.rs +++ b/storage/src/system/genesis.rs @@ -137,6 +137,8 @@ pub enum GenesisError { TrackingCopy(TrackingCopyError), /// Failed to install an EVM predeploy. EvmPredeploy(String), + /// Failed to install an EVM preinstall. + EvmPreinstall(String), } impl fmt::Display for GenesisError { diff --git a/storage/src/system/genesis/account_contract_installer.rs b/storage/src/system/genesis/account_contract_installer.rs index 4ecd8cece2..dbc67e92b7 100644 --- a/storage/src/system/genesis/account_contract_installer.rs +++ b/storage/src/system/genesis/account_contract_installer.rs @@ -11,7 +11,7 @@ use std::{ use crate::{ global_state::state::StateProvider, system::{ - evm::{should_upsert_prague_predeploys, upsert_prague_predeploys}, + evm::{should_upsert_prague_predeploys, upsert_prague_predeploys, upsert_preinstalls}, genesis::{GenesisError, DEFAULT_ADDRESS, NO_WASM}, protocol_upgrade::ProtocolUpgradeError, }, @@ -806,6 +806,18 @@ where Ok(()) } + fn create_evm_preinstalls(&self) -> Result<(), Box> { + if self.config.evm_config().enabled { + upsert_preinstalls( + &mut self.tracking_copy.borrow_mut(), + &self.config.evm_config().preinstalls, + self.config.evm_preinstall_bytecodes(), + ) + .map_err(|error| GenesisError::EvmPreinstall(error.to_string()))?; + } + Ok(()) + } + pub(crate) fn handle_sustain_purse( &mut self, sustain_purse: Option, @@ -865,6 +877,9 @@ where // Create EVM predeploys. self.create_evm_predeploys()?; + // Create EVM preinstalls. + self.create_evm_preinstalls()?; + // Write block time to global state self.store_block_time()?; diff --git a/storage/src/system/genesis/entity_installer.rs b/storage/src/system/genesis/entity_installer.rs index 6236175726..fbb8d93f5b 100644 --- a/storage/src/system/genesis/entity_installer.rs +++ b/storage/src/system/genesis/entity_installer.rs @@ -11,7 +11,7 @@ use std::{ use crate::{ global_state::state::StateProvider, system::{ - evm::{should_upsert_prague_predeploys, upsert_prague_predeploys}, + evm::{should_upsert_prague_predeploys, upsert_prague_predeploys, upsert_preinstalls}, genesis::{GenesisError, DEFAULT_ADDRESS, NO_WASM}, }, AddressGenerator, TrackingCopy, @@ -875,6 +875,18 @@ where Ok(()) } + fn create_evm_preinstalls(&self) -> Result<(), Box> { + if self.config.evm_config().enabled { + upsert_preinstalls( + &mut self.tracking_copy.borrow_mut(), + &self.config.evm_config().preinstalls, + self.config.evm_preinstall_bytecodes(), + ) + .map_err(|error| GenesisError::EvmPreinstall(error.to_string()))?; + } + Ok(()) + } + /// Performs a complete system installation. pub fn install( &mut self, @@ -901,6 +913,9 @@ where // Create EVM predeploys. self.create_evm_predeploys()?; + // Create EVM preinstalls. + self.create_evm_preinstalls()?; + // Write block time to global state self.store_block_time()?; diff --git a/storage/src/system/protocol_upgrade.rs b/storage/src/system/protocol_upgrade.rs index 66ef25bcdd..1372743347 100644 --- a/storage/src/system/protocol_upgrade.rs +++ b/storage/src/system/protocol_upgrade.rs @@ -11,7 +11,7 @@ use tracing::{debug, error, info, warn}; use crate::{ global_state::state::StateProvider, - system::evm::{should_upsert_prague_predeploys, upsert_prague_predeploys}, + system::evm::{should_upsert_prague_predeploys, upsert_prague_predeploys, upsert_preinstalls}, tracking_copy::{AddResult, TrackingCopy, TrackingCopyEntityExt, TrackingCopyExt}, AddressGenerator, }; @@ -94,6 +94,9 @@ pub enum ProtocolUpgradeError { /// Failed to install an EVM predeploy. #[error("EVM predeploy error: {0}")] EvmPredeploy(String), + /// Failed to install an EVM preinstall. + #[error("EVM preinstall error: {0}")] + EvmPreinstall(String), } impl From for ProtocolUpgradeError { @@ -188,6 +191,7 @@ where self.check_next_protocol_version_validity()?; self.handle_global_state_updates(); self.handle_evm_predeploys()?; + self.handle_evm_preinstalls()?; let system_entity_addresses = self.handle_system_hashes()?; self.read_only_system_purse(system_entity_addresses.mint)?; @@ -1712,6 +1716,19 @@ where Ok(()) } + /// Upsert EVM utility contract preinstalls after predeploy setup. + pub fn handle_evm_preinstalls(&mut self) -> Result<(), ProtocolUpgradeError> { + if self.config.evm_config().enabled { + upsert_preinstalls( + &mut self.tracking_copy, + &self.config.evm_config().preinstalls, + self.config.evm_preinstall_bytecodes(), + ) + .map_err(|error| ProtocolUpgradeError::EvmPreinstall(error.to_string()))?; + } + Ok(()) + } + /// Handle setting up minimum_delegation_rate pub fn handle_minimum_delegation_rate( &mut self, diff --git a/types/src/chainspec/evm.rs b/types/src/chainspec/evm.rs index b62de896fa..1edd01b80a 100644 --- a/types/src/chainspec/evm.rs +++ b/types/src/chainspec/evm.rs @@ -97,6 +97,12 @@ pub struct EvmConfig { deserialize_with = "vec_to_transaction_lane_definitions" )] pub transaction_lanes: Vec, + /// Keccak-256 hashes of runtime bytecode installed at genesis and upgrades. + /// + /// Each entry is an address and code hash, both 0x-prefixed hexadecimal + /// strings in TOML. Addresses must be unique. An omitted list is empty. + #[serde(default)] + pub preinstalls: Vec<(crate::evm::Address, crate::evm::Hash)>, } impl Default for EvmConfig { @@ -109,6 +115,7 @@ impl Default for EvmConfig { base_fee: 0, wei_per_mote: DEFAULT_WEI_PER_MOTE, transaction_lanes: Vec::new(), + preinstalls: Vec::new(), } } } @@ -315,7 +322,7 @@ impl ToBytes for EvmConfig { .collect(); base + transaction_lanes_as_vecs.serialized_length() }; - base + base + self.preinstalls.serialized_length() } fn write_bytes(&self, writer: &mut Vec) -> Result<(), bytesrepr::Error> { @@ -331,7 +338,7 @@ impl ToBytes for EvmConfig { .map(transaction_lane_definition_to_vec) .collect(); transaction_lanes_as_vecs.write_bytes(writer)?; - Ok(()) + self.preinstalls.write_bytes(writer) } } @@ -349,6 +356,8 @@ impl FromBytes for EvmConfig { .into_iter() .map(TransactionLaneDefinition::try_from) .collect(); + let (preinstalls, remainder) = + Vec::<(crate::evm::Address, crate::evm::Hash)>::from_bytes(remainder)?; Ok(( EvmConfig { enabled, @@ -358,6 +367,7 @@ impl FromBytes for EvmConfig { base_fee, wei_per_mote, transaction_lanes: transaction_lanes.map_err(|_| bytesrepr::Error::Formatting)?, + preinstalls, }, remainder, )) @@ -366,8 +376,133 @@ impl FromBytes for EvmConfig { #[cfg(test)] mod tests { + use alloc::string::String; + + use serde_json::{json, Value}; + use super::*; + fn config_json(preinstalls: Vec<(String, String)>) -> Value { + let mut value = serde_json::to_value(EvmConfig::default()).unwrap(); + value["preinstalls"] = json!(preinstalls); + value + } + + #[test] + fn omitted_preinstalls_default_to_empty() { + let mut value = serde_json::to_value(EvmConfig::default()).unwrap(); + value.as_object_mut().unwrap().remove("preinstalls"); + let config: EvmConfig = serde_json::from_value(value).unwrap(); + assert!(config.preinstalls.is_empty()); + } + + #[test] + fn preinstalls_roundtrip_as_prefixed_base16_and_binary() { + let value = config_json(vec![( + "0xAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA".into(), + format!("0x{}", "AB".repeat(32)), + )]); + let config: EvmConfig = serde_json::from_value(value).unwrap(); + let address = crate::evm::Address::new([0xaa; 20]); + assert_eq!( + config.preinstalls, + vec![(address, crate::evm::Hash::new([0xab; 32]))] + ); + let value = serde_json::to_value(&config).unwrap(); + assert_eq!( + value["preinstalls"][0], + json!([address.to_string(), format!("0x{}", "ab".repeat(32))]) + ); + assert_eq!(serde_json::from_value::(value).unwrap(), config); + let serialized = config.to_bytes().unwrap(); + assert_eq!(serialized.len(), config.serialized_length()); + let (decoded, remainder) = EvmConfig::from_bytes(&serialized).unwrap(); + assert_eq!(decoded, config); + assert!(remainder.is_empty()); + } + + #[test] + fn preinstalls_reject_invalid_addresses_and_hashes() { + for (address, code) in [ + ("aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa", "0x00"), + ("0x01", "0x00"), + ("0xaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa", "00"), + ("0xaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa", "0x0"), + ("0xaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa", "0xgg"), + ("0xaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa", "0x"), + ] { + let value = config_json(vec![(address.into(), code.into())]); + assert!(serde_json::from_value::(value).is_err()); + } + + for length in [31, 33] { + let value = config_json(vec![( + "0xaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa".into(), + format!("0x{}", "ab".repeat(length)), + )]); + assert!(serde_json::from_value::(value).is_err()); + } + + let mut value = config_json(Vec::new()); + value["preinstalls"] = json!([["0xaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa", [0]]]); + assert!(serde_json::from_value::(value).is_err()); + } + + #[test] + fn preinstalls_are_included_in_the_chainspec_hash() { + let mut chainspec = crate::Chainspec::default(); + let empty_hash = chainspec.hash(); + let address = crate::evm::Address::new([1; 20]); + chainspec + .evm_config + .preinstalls + .push((address, crate::evm::Hash::new([0; 32]))); + let installed_hash = chainspec.hash(); + assert_ne!(empty_hash, installed_hash); + chainspec.evm_config.preinstalls[0].1 = crate::evm::Hash::new([1; 32]); + assert_ne!(installed_hash, chainspec.hash()); + chainspec.evm_config.preinstalls[0] = ( + crate::evm::Address::new([2; 20]), + crate::evm::Hash::new([0; 32]), + ); + assert_ne!(installed_hash, chainspec.hash()); + } + + #[test] + fn preinstall_serialization_preserves_input_order() { + let mut chainspec = crate::Chainspec::default(); + chainspec.evm_config.preinstalls = vec![ + ( + crate::evm::Address::new([1; 20]), + crate::evm::Hash::new([0; 32]), + ), + ( + crate::evm::Address::new([2; 20]), + crate::evm::Hash::new([1; 32]), + ), + ]; + let first = chainspec.evm_config.to_bytes().unwrap(); + let hash = chainspec.hash(); + chainspec.evm_config.preinstalls.reverse(); + let value = serde_json::to_value(&chainspec.evm_config).unwrap(); + let decoded: EvmConfig = serde_json::from_value(value).unwrap(); + assert_eq!(decoded, chainspec.evm_config); + assert_ne!(first, decoded.to_bytes().unwrap()); + assert_ne!(hash, chainspec.hash()); + } + + #[test] + fn binary_preinstalls_reject_truncated_hash() { + let mut config = EvmConfig::default(); + config.preinstalls.push(( + crate::evm::Address::new([1; 20]), + crate::evm::Hash::new([2; 32]), + )); + let mut bytes = config.to_bytes().unwrap(); + bytes.pop(); + assert!(EvmConfig::from_bytes(&bytes).is_err()); + } + #[test] fn should_scale_base_fee_to_wei() { let config = EvmConfig { diff --git a/types/src/chainspec/genesis_config.rs b/types/src/chainspec/genesis_config.rs index 5676f05935..31bb8c6318 100644 --- a/types/src/chainspec/genesis_config.rs +++ b/types/src/chainspec/genesis_config.rs @@ -12,9 +12,9 @@ use rand::{ use serde::{Deserialize, Serialize}; use crate::{ - system::auction::DelegationRate, AdministratorAccount, Chainspec, EvmConfig, GenesisAccount, - GenesisValidator, HoldBalanceHandling, Motes, PublicKey, RewardsHandling, SystemConfig, - WasmConfig, + bytesrepr::Bytes, evm, system::auction::DelegationRate, AdministratorAccount, Chainspec, + EvmConfig, GenesisAccount, GenesisValidator, HoldBalanceHandling, Motes, PublicKey, + RewardsHandling, SystemConfig, WasmConfig, }; use super::StorageCosts; @@ -24,6 +24,8 @@ use super::StorageCosts; pub struct GenesisConfig { accounts: Vec, evm_config: EvmConfig, + #[serde(default)] + evm_preinstall_bytecodes: Vec<(evm::Hash, Bytes)>, wasm_config: WasmConfig, system_config: SystemConfig, validator_slots: u32, @@ -64,6 +66,7 @@ impl GenesisConfig { GenesisConfig { accounts, evm_config, + evm_preinstall_bytecodes: Vec::new(), wasm_config, system_config, validator_slots, @@ -86,6 +89,17 @@ impl GenesisConfig { &self.evm_config } + /// Returns runtime bytecode supplied separately from the chainspec. + pub fn evm_preinstall_bytecodes(&self) -> &[(evm::Hash, Bytes)] { + &self.evm_preinstall_bytecodes + } + + /// Supplies runtime bytecode for the configured preinstall hashes. + pub fn with_evm_preinstall_bytecodes(mut self, bytecodes: Vec<(evm::Hash, Bytes)>) -> Self { + self.evm_preinstall_bytecodes = bytecodes; + self + } + /// Returns WASM config. pub fn wasm_config(&self) -> &WasmConfig { &self.wasm_config @@ -245,6 +259,7 @@ impl Distribution for Standard { GenesisConfig { accounts, evm_config, + evm_preinstall_bytecodes: Vec::new(), wasm_config, system_config, validator_slots, @@ -280,6 +295,7 @@ impl From<&Chainspec> for GenesisConfig { GenesisConfig { accounts: chainspec.network_config.accounts_config.clone().into(), evm_config: chainspec.evm_config.clone(), + evm_preinstall_bytecodes: Vec::new(), wasm_config: chainspec.wasm_config, system_config: chainspec.system_costs_config, validator_slots: chainspec.core_config.validator_slots, diff --git a/types/src/chainspec/upgrade_config.rs b/types/src/chainspec/upgrade_config.rs index a028cac47b..a386649db8 100644 --- a/types/src/chainspec/upgrade_config.rs +++ b/types/src/chainspec/upgrade_config.rs @@ -3,8 +3,9 @@ use serde::Serialize; use std::collections::BTreeMap; use crate::{ - system::auction::DelegationRate, ChainspecRegistry, Digest, EraId, EvmConfig, FeeHandling, - HoldBalanceHandling, Key, ProtocolVersion, RewardsHandling, StoredValue, + bytesrepr::Bytes, evm, system::auction::DelegationRate, ChainspecRegistry, Digest, EraId, + EvmConfig, FeeHandling, HoldBalanceHandling, Key, ProtocolVersion, RewardsHandling, + StoredValue, }; /// Represents the configuration of a protocol upgrade. @@ -24,6 +25,7 @@ pub struct ProtocolUpgradeConfig { global_state_update: BTreeMap, chainspec_registry: ChainspecRegistry, evm_config: EvmConfig, + evm_preinstall_bytecodes: Vec<(evm::Hash, Bytes)>, fee_handling: FeeHandling, validator_minimum_bid_amount: u64, maximum_delegation_amount: u64, @@ -74,6 +76,7 @@ impl ProtocolUpgradeConfig { global_state_update, chainspec_registry, evm_config, + evm_preinstall_bytecodes: Vec::new(), fee_handling, validator_minimum_bid_amount, maximum_delegation_amount, @@ -154,6 +157,17 @@ impl ProtocolUpgradeConfig { &self.evm_config } + /// Returns runtime bytecode supplied separately from the chainspec. + pub fn evm_preinstall_bytecodes(&self) -> &[(evm::Hash, Bytes)] { + &self.evm_preinstall_bytecodes + } + + /// Supplies runtime bytecode for the configured preinstall hashes. + pub fn with_evm_preinstall_bytecodes(mut self, bytecodes: Vec<(evm::Hash, Bytes)>) -> Self { + self.evm_preinstall_bytecodes = bytecodes; + self + } + /// Sets new pre state hash. pub fn with_pre_state_hash(&mut self, pre_state_hash: Digest) { self.pre_state_hash = pre_state_hash;