From f474849e739f659fd5cfe32b1b8b878c1f6b54ab Mon Sep 17 00:00:00 2001 From: Stelios Daveas Date: Wed, 10 Jun 2026 14:11:18 +0000 Subject: [PATCH] chore: add crates.io placeholder source for Arc crate names --- .cargo/audit.toml | 36 - .cargo/config.toml | 5 - .config/nextest.toml | 23 - .dockerignore | 82 - .foundry-version | 1 - .github/actions/cloudsmith-login/action.yml | 66 - .github/workflows/build-docker.yaml | 219 - .github/workflows/ci.yml | 288 - .github/workflows/label-external-prs.yml | 40 - .github/workflows/release-binaries.yaml | 226 - .gitignore | 123 - .gitmodules | 11 - .licenseignore | 1 - .mcp.json | 9 - .pre-commit-config.yaml | 54 - .prettierignore | 8 - .prettierrc | 8 - ACCOUNTS.md | 91 - BREAKING_CHANGES.md | 108 - CHANGELOG.md | 169 - CONTRIBUTING.md | 31 - COPYRIGHT | 15 - Cargo.lock | 16339 ---------------- Cargo.toml | 361 +- Makefile | 262 - README.md | 229 +- SECURITY.md | 4 - arcup/arcup | 841 - arcup/install | 128 - arcup/test_arcup.sh | 829 - assets/apt/apt-retry.conf | 5 - assets/apt/debian.sources | 23 - assets/artifacts/Permit2/Permit2.json | 1 - assets/artifacts/README.md | 43 - assets/artifacts/manifest.json | 36 - .../stablecoin-contracts/FiatTokenProxy.json | 128 - .../NativeFiatTokenV2_2.json | 1462 -- .../artifacts/stablecoin-contracts/README.md | 13 - .../SignatureChecker.json | 40 - assets/devnet/.gitignore | 4 - assets/devnet/config.json | 189 - assets/devnet/genesis.config.ts | 138 - assets/devnet/genesis.json | 1531 -- assets/localdev/.gitignore | 4 - assets/localdev/genesis.config.ts | 171 - assets/localdev/genesis.json | 1599 -- assets/localdev/reth.toml | 105 - assets/mainnet/.gitignore | 4 - assets/mainnet/config.json | 230 - assets/mainnet/genesis.config.ts | 177 - assets/mainnet/genesis.json | 1583 -- assets/testnet/.gitignore | 4 - assets/testnet/config.json | 179 - assets/testnet/genesis.config.ts | 128 - assets/testnet/genesis.json | 1531 -- buf.yaml | 40 - clippy.toml | 1 - contracts/.gitignore | 17 - contracts/README.md | 92 - contracts/lib/forge-std | 1 - contracts/lib/openzeppelin-contracts | 1 - .../lib/openzeppelin-contracts-upgradeable | 1 - contracts/scripts/Addresses.sol | 50 - contracts/scripts/ArtifactHelper.s.sol | 314 - contracts/scripts/DenylistManagement.s.sol | 214 - contracts/scripts/DeployGasGuzzler.s.sol | 32 - contracts/scripts/DeployMemo.s.sol | 55 - contracts/scripts/DeployMulticall3From.s.sol | 31 - .../DeployPermissionedValidatorManager.s.sol | 107 - contracts/scripts/DeployProtocolConfig.s.sol | 58 - .../scripts/ProtocolConfigManagement.s.sol | 403 - contracts/scripts/ProxyManagement.s.sol | 51 - contracts/scripts/ValidatorManagement.s.sol | 322 - contracts/src/Denylist.sol | 177 - contracts/src/Precompiles.sol | 27 - contracts/src/batch/IMulticall3From.sol | 112 - contracts/src/batch/Multicall3From.sol | 189 - contracts/src/call-from/ICallFrom.sol | 31 - contracts/src/common/roles/Pausable.sol | 181 - contracts/src/memo/IMemo.sol | 55 - contracts/src/memo/Memo.sol | 47 - contracts/src/mocks/CallHelper.sol | 192 - contracts/src/mocks/GasGuzzler.sol | 111 - contracts/src/mocks/IMulticall3.sol | 85 - contracts/src/mocks/NativeTransferHelper.sol | 72 - contracts/src/mocks/PrecompileCallCode.sol | 77 - contracts/src/mocks/PrecompileDelegater.sol | 69 - .../src/mocks/RevertingProtocolConfig.sol | 27 - contracts/src/mocks/TestImplementation.sol | 180 - contracts/src/mocks/TestToken.sol | 26 - contracts/src/pq/IPQ.sol | 31 - .../src/protocol-config/ProtocolConfig.sol | 194 - .../interfaces/IProtocolConfig.sol | 94 - .../src/protocol-config/roles/Controller.sol | 111 - contracts/src/proxy/AdminUpgradeableProxy.sol | 100 - .../PermissionedValidatorManager.sol | 165 - .../validator-manager/ValidatorRegistry.sol | 269 - .../IPermissionedValidatorManager.sol | 56 - .../interfaces/IValidatorRegistry.sol | 82 - .../validator-manager/roles/Controller.sol | 212 - .../roles/ValidatorRegisterer.sol | 148 - contracts/test/Denylist.t.sol | 274 - contracts/test/batch/Multicall3From.t.sol | 554 - contracts/test/call-from/ICallFrom.t.sol | 33 - contracts/test/memo/Memo.t.sol | 174 - contracts/test/mock/CallHelper.t.sol | 143 - .../test/protocol-config/ProtocolConfig.t.sol | 1386 -- .../protocol-config/ProtocolConfigProxy.t.sol | 350 - .../test/proxy/AdminUpgradeableProxy.t.sol | 559 - .../test/scripts/DenylistManagement.t.sol | 199 - ...eployPermissionedValidatorManagement.t.sol | 52 - .../scripts/ProtocolConfigManagement.t.sol | 127 - .../test/scripts/ValidatorManagement.t.sol | 265 - contracts/test/scripts/VerifyArtifacts.t.sol | 126 - .../PermissionedValidatorManager.t.sol | 871 - .../test/validator-manager/TestUtils.sol | 48 - .../validator-manager/ValidatorRegistry.t.sol | 985 - .../ValidatorRegistryProxy.t.sol | 431 - .../mocks/MockController.sol | 24 - .../mocks/MockValidatorRegisterer.sol | 24 - .../validator-manager/roles/Controller.t.sol | 349 - .../roles/ValidatorRegisterer.t.sol | 252 - crates/arc-eth-engine/Cargo.toml | 9 + crates/arc-eth-engine/README.md | 11 + crates/arc-eth-engine/src/lib.rs | 1 + crates/arc-evm-node/Cargo.toml | 9 + crates/arc-evm-node/README.md | 11 + crates/arc-evm-node/src/lib.rs | 1 + crates/arc-evm/Cargo.toml | 9 + crates/arc-evm/README.md | 10 + crates/arc-evm/src/lib.rs | 1 + crates/arc-execution-config/Cargo.toml | 9 + crates/arc-execution-config/README.md | 11 + crates/arc-execution-config/src/lib.rs | 1 + crates/arc-execution-payload/Cargo.toml | 9 + crates/arc-execution-payload/README.md | 11 + crates/arc-execution-payload/src/lib.rs | 1 + crates/arc-execution-txpool/Cargo.toml | 9 + crates/arc-execution-txpool/README.md | 11 + crates/arc-execution-txpool/src/lib.rs | 1 + crates/arc-execution-validation/Cargo.toml | 9 + crates/arc-execution-validation/README.md | 11 + crates/arc-execution-validation/src/lib.rs | 1 + crates/arc-node-consensus/Cargo.toml | 9 + crates/arc-node-consensus/README.md | 11 + crates/arc-node-consensus/src/lib.rs | 1 + crates/arc-node-execution/Cargo.toml | 9 + crates/arc-node-execution/README.md | 11 + crates/arc-node-execution/src/lib.rs | 1 + crates/arc-precompiles/Cargo.toml | 9 + crates/arc-precompiles/README.md | 11 + crates/arc-precompiles/src/lib.rs | 1 + crates/consensus-db/Cargo.toml | 38 - crates/consensus-db/src/decoder.rs | 657 - crates/consensus-db/src/encoder.rs | 416 - crates/consensus-db/src/invalid_payloads.rs | 128 - crates/consensus-db/src/keys.rs | 143 - crates/consensus-db/src/lib.rs | 38 - crates/consensus-db/src/metrics.rs | 211 - crates/consensus-db/src/migrations.rs | 1568 -- .../consensus-db/src/migrations/migrators.rs | 397 - .../src/repositories/certificates.rs | 57 - .../src/repositories/decided_blocks.rs | 76 - .../src/repositories/invalid_payloads.rs | 52 - crates/consensus-db/src/repositories/mod.rs | 46 - .../consensus-db/src/repositories/payloads.rs | 46 - .../src/repositories/pending_proposals.rs | 74 - .../src/repositories/undecided_blocks.rs | 125 - crates/consensus-db/src/services/mod.rs | 25 - crates/consensus-db/src/services/pruning.rs | 114 - crates/consensus-db/src/store.rs | 2916 --- crates/consensus-db/src/versions.rs | 232 - crates/engine-bench/Cargo.toml | 38 - crates/engine-bench/README.md | 201 - crates/engine-bench/src/bench/context.rs | 250 - crates/engine-bench/src/bench/fixture.rs | 542 - crates/engine-bench/src/bench/helpers.rs | 27 - crates/engine-bench/src/bench/mod.rs | 31 - .../engine-bench/src/bench/new_payload_fcu.rs | 212 - crates/engine-bench/src/bench/output.rs | 313 - .../engine-bench/src/bench/prepare_payload.rs | 164 - crates/engine-bench/src/cli.rs | 94 - crates/engine-bench/src/lib.rs | 27 - crates/engine-bench/src/main.rs | 45 - crates/eth-engine/Cargo.toml | 62 - crates/eth-engine/src/abi_utils.rs | 531 - crates/eth-engine/src/capabilities.rs | 116 - crates/eth-engine/src/constants.rs | 120 - crates/eth-engine/src/engine.rs | 875 - crates/eth-engine/src/ipc/engine_ipc.rs | 194 - crates/eth-engine/src/ipc/ethereum_ipc.rs | 847 - crates/eth-engine/src/ipc/ipc_builder.rs | 251 - crates/eth-engine/src/ipc/mod.rs | 20 - crates/eth-engine/src/json_structures.rs | 32 - crates/eth-engine/src/lib.rs | 34 - crates/eth-engine/src/persistence_meter.rs | 787 - crates/eth-engine/src/retry.rs | 60 - crates/eth-engine/src/rpc/auth.rs | 80 - crates/eth-engine/src/rpc/engine_rpc.rs | 259 - crates/eth-engine/src/rpc/errors.rs | 208 - crates/eth-engine/src/rpc/ethereum_rpc.rs | 796 - crates/eth-engine/src/rpc/json_structs.rs | 47 - crates/eth-engine/src/rpc/mod.rs | 25 - crates/eth-engine/src/rpc/request_builder.rs | 145 - crates/eth-engine/tests/integration.rs | 277 - crates/evm-node/Cargo.toml | 69 - crates/evm-node/src/engine.rs | 322 - crates/evm-node/src/lib.rs | 31 - crates/evm-node/src/node.rs | 872 - crates/evm-node/src/payload.rs | 138 - crates/evm-node/src/rebroadcast.rs | 211 - crates/evm-node/src/rpc/arc.rs | 67 - crates/evm-node/src/rpc/common.rs | 30 - crates/evm-node/src/rpc/get_certificate.rs | 288 - crates/evm-node/src/rpc/get_version.rs | 43 - crates/evm-node/src/rpc/mod.rs | 22 - crates/evm-node/src/rpc/openapi/openapi.yaml | 218 - crates/evm-node/src/rpc_middleware.rs | 1479 -- crates/evm-specs-tests/Cargo.toml | 40 - crates/evm-specs-tests/README.md | 179 - crates/evm-specs-tests/src/adapter.rs | 329 - crates/evm-specs-tests/src/cmd/mod.rs | 17 - crates/evm-specs-tests/src/cmd/statetest.rs | 58 - crates/evm-specs-tests/src/error.rs | 147 - crates/evm-specs-tests/src/exception_match.rs | 457 - .../evm-specs-tests/src/fixture_sanitizer.rs | 154 - crates/evm-specs-tests/src/lib.rs | 24 - crates/evm-specs-tests/src/main.rs | 159 - crates/evm-specs-tests/src/result.rs | 328 - crates/evm-specs-tests/src/roots.rs | 375 - crates/evm-specs-tests/src/runner.rs | 1854 -- crates/evm/Cargo.toml | 63 - crates/evm/src/assembler.rs | 121 - crates/evm/src/evm.rs | 8797 --------- crates/evm/src/executor.rs | 1239 -- crates/evm/src/frame_result.rs | 161 - crates/evm/src/handler.rs | 739 - crates/evm/src/lib.rs | 39 - crates/evm/src/log.rs | 95 - crates/evm/src/opcode.rs | 1483 -- crates/evm/src/subcall.rs | 133 - crates/evm/src/subcall_test.rs | 296 - crates/execution-config/Cargo.toml | 56 - .../src/addresses_denylist.rs | 214 - crates/execution-config/src/call_from.rs | 27 - crates/execution-config/src/chainspec.rs | 1933 -- crates/execution-config/src/defaults.rs | 131 - crates/execution-config/src/follow.rs | 107 - crates/execution-config/src/gas_fee.rs | 390 - crates/execution-config/src/hardforks.rs | 859 - crates/execution-config/src/lib.rs | 30 - .../src/native_coin_control.rs | 82 - .../execution-config/src/protocol_config.rs | 284 - crates/execution-e2e/Cargo.toml | 65 - crates/execution-e2e/src/action.rs | 43 - .../execution-e2e/src/actions/assert_named.rs | 248 - .../src/actions/assert_tx_logs.rs | 337 - .../src/actions/assert_tx_trace.rs | 201 - .../execution-e2e/src/actions/assertions.rs | 590 - .../src/actions/call_contract.rs | 222 - crates/execution-e2e/src/actions/mod.rs | 49 - .../src/actions/payload_utils.rs | 189 - .../src/actions/produce_blocks.rs | 140 - .../src/actions/produce_invalid_block.rs | 188 - .../src/actions/send_transaction.rs | 278 - .../src/actions/store_deployed_address.rs | 81 - crates/execution-e2e/src/chainspec.rs | 92 - crates/execution-e2e/src/environment.rs | 185 - crates/execution-e2e/src/lib.rs | 108 - crates/execution-e2e/src/setup.rs | 196 - crates/execution-e2e/tests/base_fee.rs | 213 - .../tests/beneficiary_blocklist.rs | 71 - .../execution-e2e/tests/block_hash_history.rs | 134 - .../execution-e2e/tests/block_production.rs | 68 - crates/execution-e2e/tests/denylist.rs | 183 - .../execution-e2e/tests/eip7708_denylist.rs | 224 - .../execution-e2e/tests/eip7708_edge_cases.rs | 356 - .../tests/eip7708_hardfork_transition.rs | 184 - .../execution-e2e/tests/eip7708_log_format.rs | 176 - .../tests/eip7708_native_transfer.rs | 855 - .../tests/eip7708_payload_validation.rs | 110 - .../execution-e2e/tests/eip7708_precompile.rs | 471 - .../tests/eip7708_zero_address.rs | 109 - .../tests/gas_limit_validation.rs | 180 - .../tests/hardfork_transition.rs | 110 - .../execution-e2e/tests/helpers/constants.rs | 37 - .../execution-e2e/tests/helpers/contracts.rs | 282 - crates/execution-e2e/tests/helpers/mod.rs | 18 - crates/execution-e2e/tests/invalid_tx_list.rs | 246 - .../tests/native_transfer_balance.rs | 140 - crates/execution-e2e/tests/p256_precompile.rs | 95 - crates/execution-e2e/tests/pq_precompile.rs | 184 - .../execution-e2e/tests/static_rpc_gas_cap.rs | 194 - crates/execution-e2e/tests/transaction.rs | 75 - crates/execution-payload/Cargo.toml | 53 - crates/execution-payload/src/builder.rs | 33 - crates/execution-payload/src/lib.rs | 23 - crates/execution-payload/src/metrics.rs | 118 - crates/execution-payload/src/payload.rs | 1231 -- crates/execution-txpool/Cargo.toml | 49 - crates/execution-txpool/src/error.rs | 77 - crates/execution-txpool/src/lib.rs | 41 - crates/execution-txpool/src/pool.rs | 246 - crates/execution-txpool/src/validator.rs | 1413 -- crates/execution-validation/Cargo.toml | 41 - crates/execution-validation/src/consensus.rs | 1098 -- crates/execution-validation/src/denylist.rs | 192 - crates/execution-validation/src/lib.rs | 21 - crates/malachite-app/Cargo.toml | 95 - crates/malachite-app/METRICS.md | 196 - crates/malachite-app/README.md | 374 - crates/malachite-app/src/app.rs | 490 - crates/malachite-app/src/block.rs | 17 - crates/malachite-app/src/config.rs | 408 - crates/malachite-app/src/env_config.rs | 331 - crates/malachite-app/src/finalize.rs | 226 - .../src/handlers/consensus_ready.rs | 1920 -- crates/malachite-app/src/handlers/decided.rs | 883 - .../malachite-app/src/handlers/finalized.rs | 150 - .../src/handlers/get_decided_values.rs | 543 - .../src/handlers/get_history_min_height.rs | 173 - .../malachite-app/src/handlers/get_value.rs | 310 - crates/malachite-app/src/handlers/mod.rs | 34 - .../src/handlers/process_synced_value.rs | 716 - .../src/handlers/received_proposal_part.rs | 466 - .../src/handlers/restream_proposal.rs | 299 - .../src/handlers/started_round.rs | 661 - .../malachite-app/src/handlers/test_utils.rs | 52 - crates/malachite-app/src/hardcoded_config.rs | 609 - crates/malachite-app/src/lib.rs | 42 - crates/malachite-app/src/main.rs | 778 - crates/malachite-app/src/metrics/app.rs | 644 - crates/malachite-app/src/metrics/db.rs | 17 - crates/malachite-app/src/metrics/mod.rs | 25 - crates/malachite-app/src/metrics/process.rs | 414 - .../src/metrics/validator_set.rs | 73 - crates/malachite-app/src/node.rs | 1011 - crates/malachite-app/src/payload.rs | 800 - crates/malachite-app/src/proposal_parts.rs | 537 - crates/malachite-app/src/request.rs | 260 - crates/malachite-app/src/rpc/handlers.rs | 255 - crates/malachite-app/src/rpc/middleware.rs | 117 - crates/malachite-app/src/rpc/mod.rs | 29 - crates/malachite-app/src/rpc/routes.rs | 936 - crates/malachite-app/src/rpc/types.rs | 1106 -- crates/malachite-app/src/rpc/version.rs | 187 - crates/malachite-app/src/rpc_sync/client.rs | 304 - crates/malachite-app/src/rpc_sync/mod.rs | 57 - crates/malachite-app/src/rpc_sync/network.rs | 408 - crates/malachite-app/src/rpc_sync/peers.rs | 252 - .../src/rpc_sync/ws_subscription.rs | 250 - crates/malachite-app/src/state.rs | 539 - crates/malachite-app/src/stats.rs | 104 - crates/malachite-app/src/streaming.rs | 2538 --- .../malachite-app/src/utils/coord_upgrade.rs | 53 - crates/malachite-app/src/utils/mod.rs | 22 - crates/malachite-app/src/utils/pretty.rs | 77 - crates/malachite-app/src/utils/sync_state.rs | 80 - crates/malachite-app/src/validator_proof.rs | 200 - crates/malachite-app/tests/cli_db_migrate.rs | 220 - crates/malachite-app/tests/cli_db_rollback.rs | 727 - crates/malachite-app/tests/common/mod.rs | 122 - crates/malachite-app/tests/rpc_integration.rs | 502 - crates/malachite-cli/Cargo.toml | 47 - crates/malachite-cli/src/args.rs | 144 - crates/malachite-cli/src/cmd/db.rs | 57 - crates/malachite-cli/src/cmd/download.rs | 227 - crates/malachite-cli/src/cmd/init.rs | 217 - crates/malachite-cli/src/cmd/key.rs | 103 - crates/malachite-cli/src/cmd/mod.rs | 21 - crates/malachite-cli/src/cmd/start.rs | 1727 -- crates/malachite-cli/src/error.rs | 63 - crates/malachite-cli/src/file.rs | 70 - crates/malachite-cli/src/lib.rs | 28 - crates/malachite-cli/src/logging.rs | 90 - crates/malachite-cli/src/metrics.rs | 52 - crates/malachite-cli/src/new.rs | 117 - crates/malachite-cli/src/runtime.rs | 36 - crates/mesh-analysis/Cargo.toml | 24 - crates/mesh-analysis/src/analyze.rs | 315 - crates/mesh-analysis/src/fetch.rs | 55 - crates/mesh-analysis/src/lib.rs | 35 - crates/mesh-analysis/src/main.rs | 164 - crates/mesh-analysis/src/parse.rs | 305 - crates/mesh-analysis/src/report.rs | 641 - crates/mesh-analysis/src/tier.rs | 236 - crates/mesh-analysis/src/types.rs | 142 - crates/node/Cargo.toml | 89 - crates/node/README.md | 258 - crates/node/src/args.rs | 151 - crates/node/src/lib.rs | 31 - crates/node/src/main.rs | 1609 -- crates/node/src/metrics.rs | 34 - crates/node/tests/common.rs | 181 - crates/node/tests/fiat_token.rs | 229 - crates/node/tests/native_transfer.rs | 961 - crates/precompiles/Cargo.toml | 58 - crates/precompiles/benches/pq.rs | 93 - .../src/bin/generate_pq_test_vectors.rs | 96 - crates/precompiles/src/call_from.rs | 285 - crates/precompiles/src/helpers.rs | 1074 - crates/precompiles/src/lib.rs | 208 - crates/precompiles/src/macros.rs | 134 - .../precompiles/src/native_coin_authority.rs | 2845 --- crates/precompiles/src/native_coin_control.rs | 1172 -- crates/precompiles/src/pq.rs | 360 - crates/precompiles/src/pq_test_vectors.rs | 80 - crates/precompiles/src/precompile_provider.rs | 166 - crates/precompiles/src/subcall.rs | 148 - crates/precompiles/src/system_accounting.rs | 1388 -- crates/quake/Cargo.toml | 78 - crates/quake/README.md | 2141 -- crates/quake/docs/web-architecture.md | 312 - crates/quake/files/entrypoint_cl.sh | 36 - crates/quake/files/entrypoint_el.sh | 40 - crates/quake/files/web_index.html | 2145 -- crates/quake/macros/Cargo.toml | 12 - crates/quake/macros/src/lib.rs | 136 - crates/quake/scenarios/db-upgrade-v0-v1.toml | 28 - crates/quake/scenarios/examples/10nodes.toml | 18 - .../scenarios/examples/10nodes_simple.toml | 10 - crates/quake/scenarios/examples/14nodes.toml | 81 - crates/quake/scenarios/examples/3nodes.toml | 7 - crates/quake/scenarios/examples/5nodes.toml | 5 - .../scenarios/examples/5nodes_upgrade.toml | 13 - crates/quake/scenarios/examples/arc-node.toml | 67 - .../scenarios/examples/multi_subnets.toml | 16 - crates/quake/scenarios/examples/rpc-sync.toml | 42 - crates/quake/scenarios/examples/sentry.toml | 96 - .../scenarios/examples/sentry_asymmetric.toml | 83 - .../examples/testnet-small-default.toml | 141 - .../scenarios/examples/testnet-small.toml | 153 - .../scenarios/localdev-remote-signer.toml | 18 - crates/quake/scenarios/localdev.toml | 25 - .../scenarios/nightly-chaos-testing.toml | 79 - crates/quake/scenarios/nightly-perf.toml | 12 - .../scenarios/nightly-tx-propagation.toml | 97 - crates/quake/scenarios/nightly-upgrade.toml | 28 - crates/quake/scenarios/public-testnet.toml | 160 - crates/quake/scripts/aws-resources.sh | 1077 - crates/quake/src/build.rs | 144 - crates/quake/src/clean.rs | 179 - crates/quake/src/cli_version.rs | 408 - crates/quake/src/genesis.rs | 146 - crates/quake/src/info.rs | 417 - crates/quake/src/infra/docker.rs | 116 - crates/quake/src/infra/export.rs | 227 - crates/quake/src/infra/local.rs | 340 - crates/quake/src/infra/mod.rs | 331 - crates/quake/src/infra/remote.rs | 785 - crates/quake/src/infra/ssm.rs | 1010 - crates/quake/src/infra/terraform.rs | 307 - crates/quake/src/latency.rs | 410 - crates/quake/src/load.rs | 483 - crates/quake/src/main.rs | 1208 -- crates/quake/src/manifest.rs | 2814 --- crates/quake/src/manifest/flags.rs | 477 - crates/quake/src/manifest/generate.rs | 1089 - crates/quake/src/manifest/raw.rs | 949 - crates/quake/src/manifest/subnets.rs | 446 - crates/quake/src/mcp.rs | 1345 -- crates/quake/src/mesh.rs | 39 - crates/quake/src/monitor.rs | 688 - crates/quake/src/node.rs | 508 - crates/quake/src/nodekey.rs | 267 - crates/quake/src/nodes.rs | 628 - crates/quake/src/perturb.rs | 775 - crates/quake/src/report.rs | 2145 -- crates/quake/src/rpc/mod.rs | 352 - crates/quake/src/rpc/valset_manager.rs | 519 - crates/quake/src/setup.rs | 2370 --- crates/quake/src/shell.rs | 227 - crates/quake/src/testnet.rs | 1355 -- crates/quake/src/tests/arc_node.rs | 628 - crates/quake/src/tests/health.rs | 183 - crates/quake/src/tests/historical_queries.rs | 95 - crates/quake/src/tests/malformed_validator.rs | 282 - crates/quake/src/tests/mempool.rs | 47 - crates/quake/src/tests/mesh.rs | 346 - crates/quake/src/tests/mev.rs | 80 - crates/quake/src/tests/mod.rs | 296 - crates/quake/src/tests/net.rs | 173 - crates/quake/src/tests/perf.rs | 112 - crates/quake/src/tests/probe.rs | 111 - crates/quake/src/tests/sanity.rs | 412 - crates/quake/src/tests/snapshot.rs | 247 - crates/quake/src/tests/sync.rs | 131 - crates/quake/src/tests/tx.rs | 361 - crates/quake/src/tests/types.rs | 297 - crates/quake/src/tests/util.rs | 153 - crates/quake/src/util.rs | 146 - crates/quake/src/valset.rs | 111 - crates/quake/src/wait.rs | 301 - crates/quake/src/web.rs | 1793 -- .../templates/local/arc_builders.yaml.hbs | 42 - .../local/compose-monitoring.yaml.hbs | 67 - crates/quake/templates/local/compose.yaml.hbs | 456 - crates/quake/templates/prometheus.yml.hbs | 31 - .../templates/remote/compose-node.yaml.hbs | 139 - crates/quake/terraform/cc-data.yaml | 251 - crates/quake/terraform/cc.tf | 268 - crates/quake/terraform/nodes-data.yaml | 164 - crates/quake/terraform/nodes.tf | 147 - crates/quake/terraform/outputs.tf | 27 - crates/quake/terraform/project.tf | 245 - crates/quake/terraform/provider.tf | 28 - .../terraform/templates/infra-data-json.tmpl | 19 - .../templates/monitoring/compose.yaml.hbs | 223 - .../templates/monitoring/prometheus-yml.tmpl | 23 - .../templates/pprof-proxy/compose.yaml.hbs | 18 - .../pprof-proxy/pprof-proxy-conf.tmpl | 54 - .../templates/rpc-proxy/compose.yaml.hbs | 26 - .../templates/rpc-proxy/rpc-proxy-conf.tmpl | 97 - crates/quake/terraform/variables.tf | 129 - crates/quake/tests/README.md | 14 - crates/quake/tests/basic.md | 182 - crates/quake/tests/subnets.md | 120 - crates/quake/tests/upgrade.md | 155 - crates/quake/tests/valset.md | 146 - crates/remote-signer/Cargo.toml | 42 - crates/remote-signer/build.rs | 42 - .../proto/arc/signer/v1/signer.proto | 39 - crates/remote-signer/src/client.rs | 423 - crates/remote-signer/src/config.rs | 206 - crates/remote-signer/src/error.rs | 47 - crates/remote-signer/src/lib.rs | 39 - crates/remote-signer/src/metrics.rs | 150 - crates/remote-signer/src/provider.rs | 653 - crates/shared/Cargo.toml | 22 - crates/shared/src/chain_ids.rs | 28 - crates/shared/src/lib.rs | 20 - crates/shared/src/metrics/denylist.rs | 41 - crates/shared/src/metrics/mod.rs | 20 - crates/shared/src/metrics/validator_set.rs | 81 - crates/signer/Cargo.toml | 39 - crates/signer/src/lib.rs | 214 - crates/signer/src/local.rs | 211 - crates/signer/src/remote.rs | 17 - crates/signer/tests/cross.rs | 79 - crates/snapshots/Cargo.toml | 39 - crates/snapshots/README.md | 44 - crates/snapshots/src/download.rs | 1425 -- crates/snapshots/src/lib.rs | 19 - crates/snapshots/src/main.rs | 286 - crates/spammer/Cargo.toml | 45 - crates/spammer/Dockerfile | 69 - crates/spammer/README.md | 610 - crates/spammer/src/accounts.rs | 190 - crates/spammer/src/cli.rs | 499 - crates/spammer/src/config.rs | 664 - crates/spammer/src/erc20.rs | 138 - crates/spammer/src/generator.rs | 1107 -- crates/spammer/src/latency/block_stream.rs | 454 - crates/spammer/src/latency/csv.rs | 56 - crates/spammer/src/latency/mod.rs | 23 - crates/spammer/src/latency/timestamp.rs | 105 - crates/spammer/src/latency/tracker.rs | 543 - crates/spammer/src/lib.rs | 38 - crates/spammer/src/main.rs | 419 - crates/spammer/src/rate_limiter.rs | 64 - crates/spammer/src/result_tracker.rs | 280 - crates/spammer/src/sender.rs | 537 - crates/spammer/src/spammer.rs | 586 - crates/spammer/src/ws.rs | 517 - crates/test/checks/Cargo.toml | 29 - crates/test/checks/src/fetch.rs | 62 - crates/test/checks/src/health.rs | 731 - crates/test/checks/src/lib.rs | 54 - crates/test/checks/src/mempool.rs | 182 - crates/test/checks/src/mesh.rs | 33 - crates/test/checks/src/metric.rs | 75 - crates/test/checks/src/mev.rs | 509 - crates/test/checks/src/perf.rs | 1398 -- crates/test/checks/src/store.rs | 245 - crates/test/checks/src/sync_speed.rs | 284 - crates/test/checks/src/types.rs | 157 - crates/test/framework/Cargo.toml | 24 - crates/test/framework/README.md | 406 - crates/test/framework/src/events.rs | 51 - crates/test/framework/src/expected.rs | 99 - crates/test/framework/src/lib.rs | 542 - crates/test/framework/src/logging.rs | 56 - crates/test/framework/src/mock.rs | 70 - crates/test/framework/src/node.rs | 234 - crates/test/framework/src/params.rs | 53 - crates/test/framework/src/scenarios.rs | 207 - crates/test/framework/tests/basic.rs | 378 - crates/test/framework/tests/errors.rs | 398 - crates/test/integration/Cargo.toml | 35 - crates/test/integration/README.md | 76 - crates/test/integration/src/bridge.rs | 170 - crates/test/integration/src/lib.rs | 36 - crates/test/integration/src/runner.rs | 790 - crates/test/integration/tests/basic.rs | 94 - crates/types/Cargo.toml | 68 - crates/types/build.rs | 39 - .../proto/arc/consensus/v1/consensus.proto | 109 - .../proto/arc/liveness/v1/liveness.proto | 62 - crates/types/proto/arc/store/v1/store.proto | 93 - crates/types/proto/arc/sync/v1/sync.proto | 80 - crates/types/src/address.rs | 325 - crates/types/src/aliases.rs | 28 - crates/types/src/block.rs | 136 - crates/types/src/certificate.rs | 65 - crates/types/src/codec/error.rs | 29 - crates/types/src/codec/mod.rs | 86 - crates/types/src/codec/network.rs | 545 - crates/types/src/codec/proto.rs | 1180 -- crates/types/src/codec/versions.rs | 340 - crates/types/src/codec/wal.rs | 346 - crates/types/src/commit_http.rs | 151 - crates/types/src/config.rs | 402 - crates/types/src/consensus_params.rs | 417 - crates/types/src/context.rs | 95 - crates/types/src/evidence.rs | 121 - crates/types/src/height.rs | 260 - crates/types/src/lib.rs | 63 - crates/types/src/proposal.rs | 172 - crates/types/src/proposal_monitor.rs | 282 - crates/types/src/proposal_part.rs | 241 - crates/types/src/proposal_parts.rs | 168 - crates/types/src/proposer.rs | 67 - crates/types/src/proto.rs | 74 - crates/types/src/rpc_sync.rs | 382 - crates/types/src/signing.rs | 48 - crates/types/src/spec.rs | 584 - crates/types/src/ssz/mod.rs | 29 - crates/types/src/ssz/v1/block.rs | 29 - crates/types/src/ssz/v1/mod.rs | 28 - crates/types/src/ssz/v1/nil_or_val.rs | 113 - crates/types/src/ssz/v1/round.rs | 106 - crates/types/src/ssz/v1/signature.rs | 46 - crates/types/src/ssz/v1/vote.rs | 132 - crates/types/src/sync.rs | 17 - crates/types/src/validator_set.rs | 202 - crates/types/src/value.rs | 137 - crates/types/src/vote.rs | 234 - .../types/tests/unit/certificates/commit.rs | 191 - crates/types/tests/unit/certificates/mod.rs | 433 - crates/types/tests/unit/certificates/polka.rs | 213 - crates/types/tests/unit/certificates/round.rs | 468 - crates/types/tests/unit/main.rs | 24 - crates/version/Cargo.toml | 18 - crates/version/build.rs | 256 - crates/version/src/lib.rs | 127 - deployments/.gitignore | 2 - deployments/Dockerfile.consensus | 128 - deployments/Dockerfile.engine-bench | 86 - deployments/Dockerfile.execution | 131 - deployments/arc_consensus.yaml | 28 - deployments/arc_execution.yaml | 68 - deployments/blockscout.yaml | 72 - deployments/certs/.gitkeep | 0 deployments/docker-compose.yml | 114 - deployments/entrypoint_arc_consensus.sh | 26 - deployments/entrypoint_arc_execution.sh | 26 - .../localstack_scripts/create-kms-keys.sh | 55 - .../create-secret-manager-key.sh | 28 - .../localstack_scripts/health-check.sh | 63 - .../localstack_scripts/install-cert.sh | 31 - deployments/monitoring.yaml | 49 - .../monitoring/config-blockscout/backend.env | 35 - .../config-blockscout/frontend/.curlrc | 2 - .../config-blockscout/frontend/frontend.env | 26 - .../proxy/default.conf.template | 30 - .../monitoring/config-grafana/grafana.ini | 6 - .../provisioning/dashboards-data/default.json | 4140 ---- .../reth-arc-payload-build.json | 186 - .../dashboards-data/reth-database.json | 683 - .../dashboards-data/reth-discovery.json | 1169 -- .../dashboards-data/reth-io-correlation.json | 861 - .../dashboards-data/reth-mempool.json | 4411 ----- .../dashboards-data/reth-state-growth.json | 1747 -- .../provisioning/dashboards-data/reth.json | 11913 ----------- .../dashboards-data/version-panels.json | 185 - .../provisioning/dashboards/default.yml | 13 - .../provisioning/datasources/prometheus.yml | 9 - .../config-prometheus/prometheus.yml | 100 - docker-bake.hcl | 71 - docs/ARCHITECTURE.md | 276 - docs/PROFILING.md | 176 - docs/adr/0001-adr-process.md | 75 - docs/adr/0002-block-dissemination-protocol.md | 260 - ...governance-configuration-and-validation.md | 115 - docs/adr/0004-base-fee-validation.md | 182 - docs/adr/README.md | 43 - docs/adr/TEMPLATE.md | 36 - docs/assets/arc-logo-dark.svg | 9 - docs/assets/arc-logo-light.svg | 16 - docs/installation.md | 215 - docs/monitoring.md | 323 - docs/running-an-arc-node.md | 858 - eslint.config.mjs | 71 - foundry.toml | 32 - hardhat.config.ts | 90 - package-lock.json | 14182 -------------- package.json | 49 - remappings.txt | 5 - rust-toolchain.toml | 3 - rustfmt.toml | 3 - scripts/arc-check.sh | 312 - scripts/build-docker.sh | 54 - scripts/common.sh | 49 - scripts/down.sh | 35 - scripts/engine-bench-report.py | 1134 -- scripts/export-container-logs.sh | 37 - scripts/genesis/AccountCreator.ts | 271 - scripts/genesis/AdminUpgradeableProxy.ts | 72 - scripts/genesis/Denylist.ts | 110 - scripts/genesis/NativeFiatToken.ts | 193 - scripts/genesis/ProtocolConfig.ts | 205 - scripts/genesis/ValidatorManager.ts | 344 - scripts/genesis/addresses.ts | 55 - scripts/genesis/context.ts | 118 - scripts/genesis/genesis.ts | 320 - scripts/genesis/index.ts | 19 - scripts/genesis/types.ts | 180 - scripts/genesis/versions.ts | 46 - scripts/hardhat/chains/config.ts | 39 - scripts/hardhat/chains/devnet.ts | 31 - scripts/hardhat/chains/localdev.ts | 33 - scripts/hardhat/chains/testnet.ts | 31 - scripts/hardhat/tasks/genesis.ts | 137 - scripts/hardhat/tasks/query-fee.ts | 115 - scripts/hardhat/tasks/query-state.ts | 480 - scripts/hardhat/viem-helper.ts | 62 - scripts/localdev.mjs | 246 - scripts/md-exec.md | 235 - scripts/md-exec.py | 650 - scripts/perf/check-end-state.sh | 124 - scripts/perf/collect-metrics.sh | 125 - scripts/perf/compare-reports.sh | 237 - scripts/perf/prom-lib.sh | 40 - scripts/register-validator.sh | 75 - scripts/release-package.sh | 38 - scripts/run-upgrade-test.sh | 125 - scripts/scenarios/README.md | 75 - scripts/scenarios/nightly-chaos-testing.sh | 298 - scripts/scenarios/nightly-perf.sh | 201 - scripts/scenarios/nightly-random-manifests.sh | 129 - scripts/scenarios/nightly-tx-propagation.sh | 396 - scripts/scenarios/nightly-upgrade.sh | 348 - scripts/test_tx_latency_report.py | 725 - scripts/tx_latency_report.py | 569 - scripts/up.sh | 45 - scripts/update-malachite-deps.sh | 312 - tests/helpers/AdminUpgradeableProxy.ts | 42 - tests/helpers/BalanceComparator.ts | 113 - tests/helpers/CallHelper.ts | 258 - tests/helpers/Denylist.ts | 50 - tests/helpers/DeterministicDeployerProxy.ts | 81 - tests/helpers/FiatToken.ts | 261 - tests/helpers/GasGuzzler.ts | 60 - tests/helpers/NativeCoinAuthority.ts | 46 - tests/helpers/NativeCoinControl.ts | 70 - tests/helpers/NativeTransferHelper.ts | 173 - tests/helpers/PQ.ts | 25 - tests/helpers/ProtocolConfig.ts | 69 - tests/helpers/ReceiptVerifier.ts | 468 - tests/helpers/RevertingProtocolConfig.ts | 37 - tests/helpers/SystemAccounting.ts | 117 - tests/helpers/TraceVerifier.ts | 132 - tests/helpers/ValidatorManager.ts | 61 - tests/helpers/arc-remote-signer-keys.json | 8 - tests/helpers/client-extension.ts | 184 - tests/helpers/forge-artifact.ts | 49 - tests/helpers/getActiveValidatorSet.ts | 38 - tests/helpers/index.ts | 32 - tests/helpers/kzg.ts | 35 - tests/helpers/matchers/Address.ts | 120 - tests/helpers/matchers/BigInt.ts | 184 - tests/helpers/matchers/Hex.ts | 58 - tests/helpers/matchers/index.test.ts | 134 - tests/helpers/matchers/index.ts | 19 - tests/helpers/matchers/plugin.ts | 33 - tests/helpers/matchers/skippable.ts | 65 - tests/helpers/matchers/types.d.ts | 35 - tests/helpers/matchers/utils.ts | 40 - tests/helpers/networks/index.ts | 58 - tests/helpers/networks/localdev.ts | 80 - tests/helpers/pq_test_vectors.json | 25 - tests/helpers/registerNewValidator.ts | 79 - tests/localdev/4844.test.ts | 51 - tests/localdev/7702.test.ts | 242 - tests/localdev/Denylist.test.ts | 80 - tests/localdev/NativeCoinAuthority.test.ts | 465 - tests/localdev/NativeCoinControl.test.ts | 220 - tests/localdev/NativeFiatToken.test.ts | 2469 --- tests/localdev/PQ.test.ts | 230 - tests/localdev/PrecompileCode.test.ts | 161 - tests/localdev/ProtocolConfig.test.ts | 681 - tests/localdev/SystemAccounting.test.ts | 136 - tests/localdev/UnprotectedTx.test.ts | 119 - tests/localdev/ValidatorManager.test.ts | 357 - tests/localdev/evm_compatibility.test.ts | 1587 -- tests/localdev/genesis.test.ts | 638 - tests/localdev/native_transfer.test.ts | 634 - tests/localdev/per_validator_fees.test.ts | 195 - tests/localdev/subcall.test.ts | 702 - tests/simulation/BlockHashHistory.test.ts | 133 - tests/simulation/DelegateCall.test.ts | 386 - tests/simulation/Denylist.test.ts | 285 - tests/simulation/NativeFiatToken.test.ts | 537 - .../NativeFiatToken.upgrade.test.ts | 138 - tests/simulation/ProtocolConfig.test.ts | 260 - .../simulation/ProtocolConfig.upgrade.test.ts | 260 - tests/simulation/ValidatorManager.test.ts | 94 - tests/simulation/native_transfer.test.ts | 206 - tests/unit/deployer-nonce.test.ts | 181 - tsconfig.json | 12 - 810 files changed, 236 insertions(+), 270402 deletions(-) delete mode 100644 .cargo/audit.toml delete mode 100644 .cargo/config.toml delete mode 100644 .config/nextest.toml delete mode 100644 .dockerignore delete mode 100644 .foundry-version delete mode 100644 .github/actions/cloudsmith-login/action.yml delete mode 100644 .github/workflows/build-docker.yaml delete mode 100644 .github/workflows/ci.yml delete mode 100644 .github/workflows/label-external-prs.yml delete mode 100644 .github/workflows/release-binaries.yaml delete mode 100644 .gitignore delete mode 100644 .gitmodules delete mode 100644 .licenseignore delete mode 100644 .mcp.json delete mode 100644 .pre-commit-config.yaml delete mode 100644 .prettierignore delete mode 100644 .prettierrc delete mode 100644 ACCOUNTS.md delete mode 100644 BREAKING_CHANGES.md delete mode 100644 CHANGELOG.md delete mode 100644 CONTRIBUTING.md delete mode 100644 COPYRIGHT delete mode 100644 Cargo.lock delete mode 100644 Makefile delete mode 100644 SECURITY.md delete mode 100755 arcup/arcup delete mode 100755 arcup/install delete mode 100755 arcup/test_arcup.sh delete mode 100644 assets/apt/apt-retry.conf delete mode 100644 assets/apt/debian.sources delete mode 100644 assets/artifacts/Permit2/Permit2.json delete mode 100644 assets/artifacts/README.md delete mode 100644 assets/artifacts/manifest.json delete mode 100644 assets/artifacts/stablecoin-contracts/FiatTokenProxy.json delete mode 100644 assets/artifacts/stablecoin-contracts/NativeFiatTokenV2_2.json delete mode 100644 assets/artifacts/stablecoin-contracts/README.md delete mode 100644 assets/artifacts/stablecoin-contracts/SignatureChecker.json delete mode 100644 assets/devnet/.gitignore delete mode 100644 assets/devnet/config.json delete mode 100644 assets/devnet/genesis.config.ts delete mode 100644 assets/devnet/genesis.json delete mode 100644 assets/localdev/.gitignore delete mode 100644 assets/localdev/genesis.config.ts delete mode 100644 assets/localdev/genesis.json delete mode 100644 assets/localdev/reth.toml delete mode 100644 assets/mainnet/.gitignore delete mode 100644 assets/mainnet/config.json delete mode 100644 assets/mainnet/genesis.config.ts delete mode 100644 assets/mainnet/genesis.json delete mode 100644 assets/testnet/.gitignore delete mode 100644 assets/testnet/config.json delete mode 100644 assets/testnet/genesis.config.ts delete mode 100644 assets/testnet/genesis.json delete mode 100644 buf.yaml delete mode 100644 clippy.toml delete mode 100644 contracts/.gitignore delete mode 100644 contracts/README.md delete mode 160000 contracts/lib/forge-std delete mode 160000 contracts/lib/openzeppelin-contracts delete mode 160000 contracts/lib/openzeppelin-contracts-upgradeable delete mode 100644 contracts/scripts/Addresses.sol delete mode 100644 contracts/scripts/ArtifactHelper.s.sol delete mode 100644 contracts/scripts/DenylistManagement.s.sol delete mode 100644 contracts/scripts/DeployGasGuzzler.s.sol delete mode 100644 contracts/scripts/DeployMemo.s.sol delete mode 100644 contracts/scripts/DeployMulticall3From.s.sol delete mode 100644 contracts/scripts/DeployPermissionedValidatorManager.s.sol delete mode 100644 contracts/scripts/DeployProtocolConfig.s.sol delete mode 100644 contracts/scripts/ProtocolConfigManagement.s.sol delete mode 100644 contracts/scripts/ProxyManagement.s.sol delete mode 100644 contracts/scripts/ValidatorManagement.s.sol delete mode 100644 contracts/src/Denylist.sol delete mode 100644 contracts/src/Precompiles.sol delete mode 100644 contracts/src/batch/IMulticall3From.sol delete mode 100644 contracts/src/batch/Multicall3From.sol delete mode 100644 contracts/src/call-from/ICallFrom.sol delete mode 100644 contracts/src/common/roles/Pausable.sol delete mode 100644 contracts/src/memo/IMemo.sol delete mode 100644 contracts/src/memo/Memo.sol delete mode 100644 contracts/src/mocks/CallHelper.sol delete mode 100644 contracts/src/mocks/GasGuzzler.sol delete mode 100644 contracts/src/mocks/IMulticall3.sol delete mode 100644 contracts/src/mocks/NativeTransferHelper.sol delete mode 100644 contracts/src/mocks/PrecompileCallCode.sol delete mode 100644 contracts/src/mocks/PrecompileDelegater.sol delete mode 100644 contracts/src/mocks/RevertingProtocolConfig.sol delete mode 100644 contracts/src/mocks/TestImplementation.sol delete mode 100644 contracts/src/mocks/TestToken.sol delete mode 100644 contracts/src/pq/IPQ.sol delete mode 100644 contracts/src/protocol-config/ProtocolConfig.sol delete mode 100644 contracts/src/protocol-config/interfaces/IProtocolConfig.sol delete mode 100644 contracts/src/protocol-config/roles/Controller.sol delete mode 100644 contracts/src/proxy/AdminUpgradeableProxy.sol delete mode 100644 contracts/src/validator-manager/PermissionedValidatorManager.sol delete mode 100644 contracts/src/validator-manager/ValidatorRegistry.sol delete mode 100644 contracts/src/validator-manager/interfaces/IPermissionedValidatorManager.sol delete mode 100644 contracts/src/validator-manager/interfaces/IValidatorRegistry.sol delete mode 100644 contracts/src/validator-manager/roles/Controller.sol delete mode 100644 contracts/src/validator-manager/roles/ValidatorRegisterer.sol delete mode 100644 contracts/test/Denylist.t.sol delete mode 100644 contracts/test/batch/Multicall3From.t.sol delete mode 100644 contracts/test/call-from/ICallFrom.t.sol delete mode 100644 contracts/test/memo/Memo.t.sol delete mode 100644 contracts/test/mock/CallHelper.t.sol delete mode 100644 contracts/test/protocol-config/ProtocolConfig.t.sol delete mode 100644 contracts/test/protocol-config/ProtocolConfigProxy.t.sol delete mode 100644 contracts/test/proxy/AdminUpgradeableProxy.t.sol delete mode 100644 contracts/test/scripts/DenylistManagement.t.sol delete mode 100644 contracts/test/scripts/DeployPermissionedValidatorManagement.t.sol delete mode 100644 contracts/test/scripts/ProtocolConfigManagement.t.sol delete mode 100644 contracts/test/scripts/ValidatorManagement.t.sol delete mode 100644 contracts/test/scripts/VerifyArtifacts.t.sol delete mode 100644 contracts/test/validator-manager/PermissionedValidatorManager.t.sol delete mode 100644 contracts/test/validator-manager/TestUtils.sol delete mode 100644 contracts/test/validator-manager/ValidatorRegistry.t.sol delete mode 100644 contracts/test/validator-manager/ValidatorRegistryProxy.t.sol delete mode 100644 contracts/test/validator-manager/mocks/MockController.sol delete mode 100644 contracts/test/validator-manager/mocks/MockValidatorRegisterer.sol delete mode 100644 contracts/test/validator-manager/roles/Controller.t.sol delete mode 100644 contracts/test/validator-manager/roles/ValidatorRegisterer.t.sol create mode 100644 crates/arc-eth-engine/Cargo.toml create mode 100644 crates/arc-eth-engine/README.md create mode 100644 crates/arc-eth-engine/src/lib.rs create mode 100644 crates/arc-evm-node/Cargo.toml create mode 100644 crates/arc-evm-node/README.md create mode 100644 crates/arc-evm-node/src/lib.rs create mode 100644 crates/arc-evm/Cargo.toml create mode 100644 crates/arc-evm/README.md create mode 100644 crates/arc-evm/src/lib.rs create mode 100644 crates/arc-execution-config/Cargo.toml create mode 100644 crates/arc-execution-config/README.md create mode 100644 crates/arc-execution-config/src/lib.rs create mode 100644 crates/arc-execution-payload/Cargo.toml create mode 100644 crates/arc-execution-payload/README.md create mode 100644 crates/arc-execution-payload/src/lib.rs create mode 100644 crates/arc-execution-txpool/Cargo.toml create mode 100644 crates/arc-execution-txpool/README.md create mode 100644 crates/arc-execution-txpool/src/lib.rs create mode 100644 crates/arc-execution-validation/Cargo.toml create mode 100644 crates/arc-execution-validation/README.md create mode 100644 crates/arc-execution-validation/src/lib.rs create mode 100644 crates/arc-node-consensus/Cargo.toml create mode 100644 crates/arc-node-consensus/README.md create mode 100644 crates/arc-node-consensus/src/lib.rs create mode 100644 crates/arc-node-execution/Cargo.toml create mode 100644 crates/arc-node-execution/README.md create mode 100644 crates/arc-node-execution/src/lib.rs create mode 100644 crates/arc-precompiles/Cargo.toml create mode 100644 crates/arc-precompiles/README.md create mode 100644 crates/arc-precompiles/src/lib.rs delete mode 100644 crates/consensus-db/Cargo.toml delete mode 100644 crates/consensus-db/src/decoder.rs delete mode 100644 crates/consensus-db/src/encoder.rs delete mode 100644 crates/consensus-db/src/invalid_payloads.rs delete mode 100644 crates/consensus-db/src/keys.rs delete mode 100644 crates/consensus-db/src/lib.rs delete mode 100644 crates/consensus-db/src/metrics.rs delete mode 100644 crates/consensus-db/src/migrations.rs delete mode 100644 crates/consensus-db/src/migrations/migrators.rs delete mode 100644 crates/consensus-db/src/repositories/certificates.rs delete mode 100644 crates/consensus-db/src/repositories/decided_blocks.rs delete mode 100644 crates/consensus-db/src/repositories/invalid_payloads.rs delete mode 100644 crates/consensus-db/src/repositories/mod.rs delete mode 100644 crates/consensus-db/src/repositories/payloads.rs delete mode 100644 crates/consensus-db/src/repositories/pending_proposals.rs delete mode 100644 crates/consensus-db/src/repositories/undecided_blocks.rs delete mode 100644 crates/consensus-db/src/services/mod.rs delete mode 100644 crates/consensus-db/src/services/pruning.rs delete mode 100644 crates/consensus-db/src/store.rs delete mode 100644 crates/consensus-db/src/versions.rs delete mode 100644 crates/engine-bench/Cargo.toml delete mode 100644 crates/engine-bench/README.md delete mode 100644 crates/engine-bench/src/bench/context.rs delete mode 100644 crates/engine-bench/src/bench/fixture.rs delete mode 100644 crates/engine-bench/src/bench/helpers.rs delete mode 100644 crates/engine-bench/src/bench/mod.rs delete mode 100644 crates/engine-bench/src/bench/new_payload_fcu.rs delete mode 100644 crates/engine-bench/src/bench/output.rs delete mode 100644 crates/engine-bench/src/bench/prepare_payload.rs delete mode 100644 crates/engine-bench/src/cli.rs delete mode 100644 crates/engine-bench/src/lib.rs delete mode 100644 crates/engine-bench/src/main.rs delete mode 100644 crates/eth-engine/Cargo.toml delete mode 100644 crates/eth-engine/src/abi_utils.rs delete mode 100644 crates/eth-engine/src/capabilities.rs delete mode 100644 crates/eth-engine/src/constants.rs delete mode 100644 crates/eth-engine/src/engine.rs delete mode 100644 crates/eth-engine/src/ipc/engine_ipc.rs delete mode 100644 crates/eth-engine/src/ipc/ethereum_ipc.rs delete mode 100644 crates/eth-engine/src/ipc/ipc_builder.rs delete mode 100644 crates/eth-engine/src/ipc/mod.rs delete mode 100644 crates/eth-engine/src/json_structures.rs delete mode 100644 crates/eth-engine/src/lib.rs delete mode 100644 crates/eth-engine/src/persistence_meter.rs delete mode 100644 crates/eth-engine/src/retry.rs delete mode 100644 crates/eth-engine/src/rpc/auth.rs delete mode 100644 crates/eth-engine/src/rpc/engine_rpc.rs delete mode 100644 crates/eth-engine/src/rpc/errors.rs delete mode 100644 crates/eth-engine/src/rpc/ethereum_rpc.rs delete mode 100644 crates/eth-engine/src/rpc/json_structs.rs delete mode 100644 crates/eth-engine/src/rpc/mod.rs delete mode 100644 crates/eth-engine/src/rpc/request_builder.rs delete mode 100644 crates/eth-engine/tests/integration.rs delete mode 100644 crates/evm-node/Cargo.toml delete mode 100644 crates/evm-node/src/engine.rs delete mode 100644 crates/evm-node/src/lib.rs delete mode 100644 crates/evm-node/src/node.rs delete mode 100644 crates/evm-node/src/payload.rs delete mode 100644 crates/evm-node/src/rebroadcast.rs delete mode 100644 crates/evm-node/src/rpc/arc.rs delete mode 100644 crates/evm-node/src/rpc/common.rs delete mode 100644 crates/evm-node/src/rpc/get_certificate.rs delete mode 100644 crates/evm-node/src/rpc/get_version.rs delete mode 100644 crates/evm-node/src/rpc/mod.rs delete mode 100644 crates/evm-node/src/rpc/openapi/openapi.yaml delete mode 100644 crates/evm-node/src/rpc_middleware.rs delete mode 100644 crates/evm-specs-tests/Cargo.toml delete mode 100644 crates/evm-specs-tests/README.md delete mode 100644 crates/evm-specs-tests/src/adapter.rs delete mode 100644 crates/evm-specs-tests/src/cmd/mod.rs delete mode 100644 crates/evm-specs-tests/src/cmd/statetest.rs delete mode 100644 crates/evm-specs-tests/src/error.rs delete mode 100644 crates/evm-specs-tests/src/exception_match.rs delete mode 100644 crates/evm-specs-tests/src/fixture_sanitizer.rs delete mode 100644 crates/evm-specs-tests/src/lib.rs delete mode 100644 crates/evm-specs-tests/src/main.rs delete mode 100644 crates/evm-specs-tests/src/result.rs delete mode 100644 crates/evm-specs-tests/src/roots.rs delete mode 100644 crates/evm-specs-tests/src/runner.rs delete mode 100644 crates/evm/Cargo.toml delete mode 100644 crates/evm/src/assembler.rs delete mode 100644 crates/evm/src/evm.rs delete mode 100644 crates/evm/src/executor.rs delete mode 100644 crates/evm/src/frame_result.rs delete mode 100644 crates/evm/src/handler.rs delete mode 100644 crates/evm/src/lib.rs delete mode 100644 crates/evm/src/log.rs delete mode 100644 crates/evm/src/opcode.rs delete mode 100644 crates/evm/src/subcall.rs delete mode 100644 crates/evm/src/subcall_test.rs delete mode 100644 crates/execution-config/Cargo.toml delete mode 100644 crates/execution-config/src/addresses_denylist.rs delete mode 100644 crates/execution-config/src/call_from.rs delete mode 100644 crates/execution-config/src/chainspec.rs delete mode 100644 crates/execution-config/src/defaults.rs delete mode 100644 crates/execution-config/src/follow.rs delete mode 100644 crates/execution-config/src/gas_fee.rs delete mode 100644 crates/execution-config/src/hardforks.rs delete mode 100644 crates/execution-config/src/lib.rs delete mode 100644 crates/execution-config/src/native_coin_control.rs delete mode 100644 crates/execution-config/src/protocol_config.rs delete mode 100644 crates/execution-e2e/Cargo.toml delete mode 100644 crates/execution-e2e/src/action.rs delete mode 100644 crates/execution-e2e/src/actions/assert_named.rs delete mode 100644 crates/execution-e2e/src/actions/assert_tx_logs.rs delete mode 100644 crates/execution-e2e/src/actions/assert_tx_trace.rs delete mode 100644 crates/execution-e2e/src/actions/assertions.rs delete mode 100644 crates/execution-e2e/src/actions/call_contract.rs delete mode 100644 crates/execution-e2e/src/actions/mod.rs delete mode 100644 crates/execution-e2e/src/actions/payload_utils.rs delete mode 100644 crates/execution-e2e/src/actions/produce_blocks.rs delete mode 100644 crates/execution-e2e/src/actions/produce_invalid_block.rs delete mode 100644 crates/execution-e2e/src/actions/send_transaction.rs delete mode 100644 crates/execution-e2e/src/actions/store_deployed_address.rs delete mode 100644 crates/execution-e2e/src/chainspec.rs delete mode 100644 crates/execution-e2e/src/environment.rs delete mode 100644 crates/execution-e2e/src/lib.rs delete mode 100644 crates/execution-e2e/src/setup.rs delete mode 100644 crates/execution-e2e/tests/base_fee.rs delete mode 100644 crates/execution-e2e/tests/beneficiary_blocklist.rs delete mode 100644 crates/execution-e2e/tests/block_hash_history.rs delete mode 100644 crates/execution-e2e/tests/block_production.rs delete mode 100644 crates/execution-e2e/tests/denylist.rs delete mode 100644 crates/execution-e2e/tests/eip7708_denylist.rs delete mode 100644 crates/execution-e2e/tests/eip7708_edge_cases.rs delete mode 100644 crates/execution-e2e/tests/eip7708_hardfork_transition.rs delete mode 100644 crates/execution-e2e/tests/eip7708_log_format.rs delete mode 100644 crates/execution-e2e/tests/eip7708_native_transfer.rs delete mode 100644 crates/execution-e2e/tests/eip7708_payload_validation.rs delete mode 100644 crates/execution-e2e/tests/eip7708_precompile.rs delete mode 100644 crates/execution-e2e/tests/eip7708_zero_address.rs delete mode 100644 crates/execution-e2e/tests/gas_limit_validation.rs delete mode 100644 crates/execution-e2e/tests/hardfork_transition.rs delete mode 100644 crates/execution-e2e/tests/helpers/constants.rs delete mode 100644 crates/execution-e2e/tests/helpers/contracts.rs delete mode 100644 crates/execution-e2e/tests/helpers/mod.rs delete mode 100644 crates/execution-e2e/tests/invalid_tx_list.rs delete mode 100644 crates/execution-e2e/tests/native_transfer_balance.rs delete mode 100644 crates/execution-e2e/tests/p256_precompile.rs delete mode 100644 crates/execution-e2e/tests/pq_precompile.rs delete mode 100644 crates/execution-e2e/tests/static_rpc_gas_cap.rs delete mode 100644 crates/execution-e2e/tests/transaction.rs delete mode 100644 crates/execution-payload/Cargo.toml delete mode 100644 crates/execution-payload/src/builder.rs delete mode 100644 crates/execution-payload/src/lib.rs delete mode 100644 crates/execution-payload/src/metrics.rs delete mode 100644 crates/execution-payload/src/payload.rs delete mode 100644 crates/execution-txpool/Cargo.toml delete mode 100644 crates/execution-txpool/src/error.rs delete mode 100644 crates/execution-txpool/src/lib.rs delete mode 100644 crates/execution-txpool/src/pool.rs delete mode 100644 crates/execution-txpool/src/validator.rs delete mode 100644 crates/execution-validation/Cargo.toml delete mode 100644 crates/execution-validation/src/consensus.rs delete mode 100644 crates/execution-validation/src/denylist.rs delete mode 100644 crates/execution-validation/src/lib.rs delete mode 100644 crates/malachite-app/Cargo.toml delete mode 100644 crates/malachite-app/METRICS.md delete mode 100644 crates/malachite-app/README.md delete mode 100644 crates/malachite-app/src/app.rs delete mode 100644 crates/malachite-app/src/block.rs delete mode 100644 crates/malachite-app/src/config.rs delete mode 100644 crates/malachite-app/src/env_config.rs delete mode 100644 crates/malachite-app/src/finalize.rs delete mode 100644 crates/malachite-app/src/handlers/consensus_ready.rs delete mode 100644 crates/malachite-app/src/handlers/decided.rs delete mode 100644 crates/malachite-app/src/handlers/finalized.rs delete mode 100644 crates/malachite-app/src/handlers/get_decided_values.rs delete mode 100644 crates/malachite-app/src/handlers/get_history_min_height.rs delete mode 100644 crates/malachite-app/src/handlers/get_value.rs delete mode 100644 crates/malachite-app/src/handlers/mod.rs delete mode 100644 crates/malachite-app/src/handlers/process_synced_value.rs delete mode 100644 crates/malachite-app/src/handlers/received_proposal_part.rs delete mode 100644 crates/malachite-app/src/handlers/restream_proposal.rs delete mode 100644 crates/malachite-app/src/handlers/started_round.rs delete mode 100644 crates/malachite-app/src/handlers/test_utils.rs delete mode 100644 crates/malachite-app/src/hardcoded_config.rs delete mode 100644 crates/malachite-app/src/lib.rs delete mode 100644 crates/malachite-app/src/main.rs delete mode 100644 crates/malachite-app/src/metrics/app.rs delete mode 100644 crates/malachite-app/src/metrics/db.rs delete mode 100644 crates/malachite-app/src/metrics/mod.rs delete mode 100644 crates/malachite-app/src/metrics/process.rs delete mode 100644 crates/malachite-app/src/metrics/validator_set.rs delete mode 100644 crates/malachite-app/src/node.rs delete mode 100644 crates/malachite-app/src/payload.rs delete mode 100644 crates/malachite-app/src/proposal_parts.rs delete mode 100644 crates/malachite-app/src/request.rs delete mode 100644 crates/malachite-app/src/rpc/handlers.rs delete mode 100644 crates/malachite-app/src/rpc/middleware.rs delete mode 100644 crates/malachite-app/src/rpc/mod.rs delete mode 100644 crates/malachite-app/src/rpc/routes.rs delete mode 100644 crates/malachite-app/src/rpc/types.rs delete mode 100644 crates/malachite-app/src/rpc/version.rs delete mode 100644 crates/malachite-app/src/rpc_sync/client.rs delete mode 100644 crates/malachite-app/src/rpc_sync/mod.rs delete mode 100644 crates/malachite-app/src/rpc_sync/network.rs delete mode 100644 crates/malachite-app/src/rpc_sync/peers.rs delete mode 100644 crates/malachite-app/src/rpc_sync/ws_subscription.rs delete mode 100644 crates/malachite-app/src/state.rs delete mode 100644 crates/malachite-app/src/stats.rs delete mode 100644 crates/malachite-app/src/streaming.rs delete mode 100644 crates/malachite-app/src/utils/coord_upgrade.rs delete mode 100644 crates/malachite-app/src/utils/mod.rs delete mode 100644 crates/malachite-app/src/utils/pretty.rs delete mode 100644 crates/malachite-app/src/utils/sync_state.rs delete mode 100644 crates/malachite-app/src/validator_proof.rs delete mode 100644 crates/malachite-app/tests/cli_db_migrate.rs delete mode 100644 crates/malachite-app/tests/cli_db_rollback.rs delete mode 100644 crates/malachite-app/tests/common/mod.rs delete mode 100644 crates/malachite-app/tests/rpc_integration.rs delete mode 100644 crates/malachite-cli/Cargo.toml delete mode 100644 crates/malachite-cli/src/args.rs delete mode 100644 crates/malachite-cli/src/cmd/db.rs delete mode 100644 crates/malachite-cli/src/cmd/download.rs delete mode 100644 crates/malachite-cli/src/cmd/init.rs delete mode 100644 crates/malachite-cli/src/cmd/key.rs delete mode 100644 crates/malachite-cli/src/cmd/mod.rs delete mode 100644 crates/malachite-cli/src/cmd/start.rs delete mode 100644 crates/malachite-cli/src/error.rs delete mode 100644 crates/malachite-cli/src/file.rs delete mode 100644 crates/malachite-cli/src/lib.rs delete mode 100644 crates/malachite-cli/src/logging.rs delete mode 100644 crates/malachite-cli/src/metrics.rs delete mode 100644 crates/malachite-cli/src/new.rs delete mode 100644 crates/malachite-cli/src/runtime.rs delete mode 100644 crates/mesh-analysis/Cargo.toml delete mode 100644 crates/mesh-analysis/src/analyze.rs delete mode 100644 crates/mesh-analysis/src/fetch.rs delete mode 100644 crates/mesh-analysis/src/lib.rs delete mode 100644 crates/mesh-analysis/src/main.rs delete mode 100644 crates/mesh-analysis/src/parse.rs delete mode 100644 crates/mesh-analysis/src/report.rs delete mode 100644 crates/mesh-analysis/src/tier.rs delete mode 100644 crates/mesh-analysis/src/types.rs delete mode 100644 crates/node/Cargo.toml delete mode 100644 crates/node/README.md delete mode 100644 crates/node/src/args.rs delete mode 100644 crates/node/src/lib.rs delete mode 100644 crates/node/src/main.rs delete mode 100644 crates/node/src/metrics.rs delete mode 100644 crates/node/tests/common.rs delete mode 100644 crates/node/tests/fiat_token.rs delete mode 100644 crates/node/tests/native_transfer.rs delete mode 100644 crates/precompiles/Cargo.toml delete mode 100644 crates/precompiles/benches/pq.rs delete mode 100644 crates/precompiles/src/bin/generate_pq_test_vectors.rs delete mode 100644 crates/precompiles/src/call_from.rs delete mode 100644 crates/precompiles/src/helpers.rs delete mode 100644 crates/precompiles/src/lib.rs delete mode 100644 crates/precompiles/src/macros.rs delete mode 100644 crates/precompiles/src/native_coin_authority.rs delete mode 100644 crates/precompiles/src/native_coin_control.rs delete mode 100644 crates/precompiles/src/pq.rs delete mode 100644 crates/precompiles/src/pq_test_vectors.rs delete mode 100644 crates/precompiles/src/precompile_provider.rs delete mode 100644 crates/precompiles/src/subcall.rs delete mode 100644 crates/precompiles/src/system_accounting.rs delete mode 100644 crates/quake/Cargo.toml delete mode 100644 crates/quake/README.md delete mode 100644 crates/quake/docs/web-architecture.md delete mode 100755 crates/quake/files/entrypoint_cl.sh delete mode 100755 crates/quake/files/entrypoint_el.sh delete mode 100644 crates/quake/files/web_index.html delete mode 100644 crates/quake/macros/Cargo.toml delete mode 100644 crates/quake/macros/src/lib.rs delete mode 100644 crates/quake/scenarios/db-upgrade-v0-v1.toml delete mode 100644 crates/quake/scenarios/examples/10nodes.toml delete mode 100644 crates/quake/scenarios/examples/10nodes_simple.toml delete mode 100644 crates/quake/scenarios/examples/14nodes.toml delete mode 100644 crates/quake/scenarios/examples/3nodes.toml delete mode 100644 crates/quake/scenarios/examples/5nodes.toml delete mode 100644 crates/quake/scenarios/examples/5nodes_upgrade.toml delete mode 100644 crates/quake/scenarios/examples/arc-node.toml delete mode 100644 crates/quake/scenarios/examples/multi_subnets.toml delete mode 100644 crates/quake/scenarios/examples/rpc-sync.toml delete mode 100644 crates/quake/scenarios/examples/sentry.toml delete mode 100644 crates/quake/scenarios/examples/sentry_asymmetric.toml delete mode 100644 crates/quake/scenarios/examples/testnet-small-default.toml delete mode 100644 crates/quake/scenarios/examples/testnet-small.toml delete mode 100644 crates/quake/scenarios/localdev-remote-signer.toml delete mode 100644 crates/quake/scenarios/localdev.toml delete mode 100644 crates/quake/scenarios/nightly-chaos-testing.toml delete mode 100644 crates/quake/scenarios/nightly-perf.toml delete mode 100644 crates/quake/scenarios/nightly-tx-propagation.toml delete mode 100644 crates/quake/scenarios/nightly-upgrade.toml delete mode 100644 crates/quake/scenarios/public-testnet.toml delete mode 100755 crates/quake/scripts/aws-resources.sh delete mode 100644 crates/quake/src/build.rs delete mode 100644 crates/quake/src/clean.rs delete mode 100644 crates/quake/src/cli_version.rs delete mode 100644 crates/quake/src/genesis.rs delete mode 100644 crates/quake/src/info.rs delete mode 100644 crates/quake/src/infra/docker.rs delete mode 100644 crates/quake/src/infra/export.rs delete mode 100644 crates/quake/src/infra/local.rs delete mode 100644 crates/quake/src/infra/mod.rs delete mode 100644 crates/quake/src/infra/remote.rs delete mode 100644 crates/quake/src/infra/ssm.rs delete mode 100644 crates/quake/src/infra/terraform.rs delete mode 100644 crates/quake/src/latency.rs delete mode 100644 crates/quake/src/load.rs delete mode 100644 crates/quake/src/main.rs delete mode 100644 crates/quake/src/manifest.rs delete mode 100644 crates/quake/src/manifest/flags.rs delete mode 100644 crates/quake/src/manifest/generate.rs delete mode 100644 crates/quake/src/manifest/raw.rs delete mode 100644 crates/quake/src/manifest/subnets.rs delete mode 100644 crates/quake/src/mcp.rs delete mode 100644 crates/quake/src/mesh.rs delete mode 100644 crates/quake/src/monitor.rs delete mode 100644 crates/quake/src/node.rs delete mode 100644 crates/quake/src/nodekey.rs delete mode 100644 crates/quake/src/nodes.rs delete mode 100644 crates/quake/src/perturb.rs delete mode 100644 crates/quake/src/report.rs delete mode 100644 crates/quake/src/rpc/mod.rs delete mode 100644 crates/quake/src/rpc/valset_manager.rs delete mode 100644 crates/quake/src/setup.rs delete mode 100644 crates/quake/src/shell.rs delete mode 100644 crates/quake/src/testnet.rs delete mode 100644 crates/quake/src/tests/arc_node.rs delete mode 100644 crates/quake/src/tests/health.rs delete mode 100644 crates/quake/src/tests/historical_queries.rs delete mode 100644 crates/quake/src/tests/malformed_validator.rs delete mode 100644 crates/quake/src/tests/mempool.rs delete mode 100644 crates/quake/src/tests/mesh.rs delete mode 100644 crates/quake/src/tests/mev.rs delete mode 100644 crates/quake/src/tests/mod.rs delete mode 100644 crates/quake/src/tests/net.rs delete mode 100644 crates/quake/src/tests/perf.rs delete mode 100644 crates/quake/src/tests/probe.rs delete mode 100644 crates/quake/src/tests/sanity.rs delete mode 100644 crates/quake/src/tests/snapshot.rs delete mode 100644 crates/quake/src/tests/sync.rs delete mode 100644 crates/quake/src/tests/tx.rs delete mode 100644 crates/quake/src/tests/types.rs delete mode 100644 crates/quake/src/tests/util.rs delete mode 100644 crates/quake/src/util.rs delete mode 100644 crates/quake/src/valset.rs delete mode 100644 crates/quake/src/wait.rs delete mode 100644 crates/quake/src/web.rs delete mode 100644 crates/quake/templates/local/arc_builders.yaml.hbs delete mode 100644 crates/quake/templates/local/compose-monitoring.yaml.hbs delete mode 100644 crates/quake/templates/local/compose.yaml.hbs delete mode 100644 crates/quake/templates/prometheus.yml.hbs delete mode 100644 crates/quake/templates/remote/compose-node.yaml.hbs delete mode 100644 crates/quake/terraform/cc-data.yaml delete mode 100644 crates/quake/terraform/cc.tf delete mode 100644 crates/quake/terraform/nodes-data.yaml delete mode 100644 crates/quake/terraform/nodes.tf delete mode 100644 crates/quake/terraform/outputs.tf delete mode 100644 crates/quake/terraform/project.tf delete mode 100644 crates/quake/terraform/provider.tf delete mode 100644 crates/quake/terraform/templates/infra-data-json.tmpl delete mode 100644 crates/quake/terraform/templates/monitoring/compose.yaml.hbs delete mode 100644 crates/quake/terraform/templates/monitoring/prometheus-yml.tmpl delete mode 100644 crates/quake/terraform/templates/pprof-proxy/compose.yaml.hbs delete mode 100644 crates/quake/terraform/templates/pprof-proxy/pprof-proxy-conf.tmpl delete mode 100644 crates/quake/terraform/templates/rpc-proxy/compose.yaml.hbs delete mode 100644 crates/quake/terraform/templates/rpc-proxy/rpc-proxy-conf.tmpl delete mode 100644 crates/quake/terraform/variables.tf delete mode 100644 crates/quake/tests/README.md delete mode 100644 crates/quake/tests/basic.md delete mode 100644 crates/quake/tests/subnets.md delete mode 100644 crates/quake/tests/upgrade.md delete mode 100644 crates/quake/tests/valset.md delete mode 100644 crates/remote-signer/Cargo.toml delete mode 100644 crates/remote-signer/build.rs delete mode 100644 crates/remote-signer/proto/arc/signer/v1/signer.proto delete mode 100644 crates/remote-signer/src/client.rs delete mode 100644 crates/remote-signer/src/config.rs delete mode 100644 crates/remote-signer/src/error.rs delete mode 100644 crates/remote-signer/src/lib.rs delete mode 100644 crates/remote-signer/src/metrics.rs delete mode 100644 crates/remote-signer/src/provider.rs delete mode 100644 crates/shared/Cargo.toml delete mode 100644 crates/shared/src/chain_ids.rs delete mode 100644 crates/shared/src/lib.rs delete mode 100644 crates/shared/src/metrics/denylist.rs delete mode 100644 crates/shared/src/metrics/mod.rs delete mode 100644 crates/shared/src/metrics/validator_set.rs delete mode 100644 crates/signer/Cargo.toml delete mode 100644 crates/signer/src/lib.rs delete mode 100644 crates/signer/src/local.rs delete mode 100644 crates/signer/src/remote.rs delete mode 100644 crates/signer/tests/cross.rs delete mode 100644 crates/snapshots/Cargo.toml delete mode 100644 crates/snapshots/README.md delete mode 100644 crates/snapshots/src/download.rs delete mode 100644 crates/snapshots/src/lib.rs delete mode 100644 crates/snapshots/src/main.rs delete mode 100644 crates/spammer/Cargo.toml delete mode 100644 crates/spammer/Dockerfile delete mode 100644 crates/spammer/README.md delete mode 100644 crates/spammer/src/accounts.rs delete mode 100644 crates/spammer/src/cli.rs delete mode 100644 crates/spammer/src/config.rs delete mode 100644 crates/spammer/src/erc20.rs delete mode 100644 crates/spammer/src/generator.rs delete mode 100644 crates/spammer/src/latency/block_stream.rs delete mode 100644 crates/spammer/src/latency/csv.rs delete mode 100644 crates/spammer/src/latency/mod.rs delete mode 100644 crates/spammer/src/latency/timestamp.rs delete mode 100644 crates/spammer/src/latency/tracker.rs delete mode 100644 crates/spammer/src/lib.rs delete mode 100644 crates/spammer/src/main.rs delete mode 100644 crates/spammer/src/rate_limiter.rs delete mode 100644 crates/spammer/src/result_tracker.rs delete mode 100644 crates/spammer/src/sender.rs delete mode 100644 crates/spammer/src/spammer.rs delete mode 100644 crates/spammer/src/ws.rs delete mode 100644 crates/test/checks/Cargo.toml delete mode 100644 crates/test/checks/src/fetch.rs delete mode 100644 crates/test/checks/src/health.rs delete mode 100644 crates/test/checks/src/lib.rs delete mode 100644 crates/test/checks/src/mempool.rs delete mode 100644 crates/test/checks/src/mesh.rs delete mode 100644 crates/test/checks/src/metric.rs delete mode 100644 crates/test/checks/src/mev.rs delete mode 100644 crates/test/checks/src/perf.rs delete mode 100644 crates/test/checks/src/store.rs delete mode 100644 crates/test/checks/src/sync_speed.rs delete mode 100644 crates/test/checks/src/types.rs delete mode 100644 crates/test/framework/Cargo.toml delete mode 100644 crates/test/framework/README.md delete mode 100644 crates/test/framework/src/events.rs delete mode 100644 crates/test/framework/src/expected.rs delete mode 100644 crates/test/framework/src/lib.rs delete mode 100644 crates/test/framework/src/logging.rs delete mode 100644 crates/test/framework/src/mock.rs delete mode 100644 crates/test/framework/src/node.rs delete mode 100644 crates/test/framework/src/params.rs delete mode 100644 crates/test/framework/src/scenarios.rs delete mode 100644 crates/test/framework/tests/basic.rs delete mode 100644 crates/test/framework/tests/errors.rs delete mode 100644 crates/test/integration/Cargo.toml delete mode 100644 crates/test/integration/README.md delete mode 100644 crates/test/integration/src/bridge.rs delete mode 100644 crates/test/integration/src/lib.rs delete mode 100644 crates/test/integration/src/runner.rs delete mode 100644 crates/test/integration/tests/basic.rs delete mode 100644 crates/types/Cargo.toml delete mode 100644 crates/types/build.rs delete mode 100644 crates/types/proto/arc/consensus/v1/consensus.proto delete mode 100644 crates/types/proto/arc/liveness/v1/liveness.proto delete mode 100644 crates/types/proto/arc/store/v1/store.proto delete mode 100644 crates/types/proto/arc/sync/v1/sync.proto delete mode 100644 crates/types/src/address.rs delete mode 100644 crates/types/src/aliases.rs delete mode 100644 crates/types/src/block.rs delete mode 100644 crates/types/src/certificate.rs delete mode 100644 crates/types/src/codec/error.rs delete mode 100644 crates/types/src/codec/mod.rs delete mode 100644 crates/types/src/codec/network.rs delete mode 100644 crates/types/src/codec/proto.rs delete mode 100644 crates/types/src/codec/versions.rs delete mode 100644 crates/types/src/codec/wal.rs delete mode 100644 crates/types/src/commit_http.rs delete mode 100644 crates/types/src/config.rs delete mode 100644 crates/types/src/consensus_params.rs delete mode 100644 crates/types/src/context.rs delete mode 100644 crates/types/src/evidence.rs delete mode 100644 crates/types/src/height.rs delete mode 100644 crates/types/src/lib.rs delete mode 100644 crates/types/src/proposal.rs delete mode 100644 crates/types/src/proposal_monitor.rs delete mode 100644 crates/types/src/proposal_part.rs delete mode 100644 crates/types/src/proposal_parts.rs delete mode 100644 crates/types/src/proposer.rs delete mode 100644 crates/types/src/proto.rs delete mode 100644 crates/types/src/rpc_sync.rs delete mode 100644 crates/types/src/signing.rs delete mode 100644 crates/types/src/spec.rs delete mode 100644 crates/types/src/ssz/mod.rs delete mode 100644 crates/types/src/ssz/v1/block.rs delete mode 100644 crates/types/src/ssz/v1/mod.rs delete mode 100644 crates/types/src/ssz/v1/nil_or_val.rs delete mode 100644 crates/types/src/ssz/v1/round.rs delete mode 100644 crates/types/src/ssz/v1/signature.rs delete mode 100644 crates/types/src/ssz/v1/vote.rs delete mode 100644 crates/types/src/sync.rs delete mode 100644 crates/types/src/validator_set.rs delete mode 100644 crates/types/src/value.rs delete mode 100644 crates/types/src/vote.rs delete mode 100644 crates/types/tests/unit/certificates/commit.rs delete mode 100644 crates/types/tests/unit/certificates/mod.rs delete mode 100644 crates/types/tests/unit/certificates/polka.rs delete mode 100644 crates/types/tests/unit/certificates/round.rs delete mode 100644 crates/types/tests/unit/main.rs delete mode 100644 crates/version/Cargo.toml delete mode 100644 crates/version/build.rs delete mode 100644 crates/version/src/lib.rs delete mode 100644 deployments/.gitignore delete mode 100644 deployments/Dockerfile.consensus delete mode 100644 deployments/Dockerfile.engine-bench delete mode 100644 deployments/Dockerfile.execution delete mode 100644 deployments/arc_consensus.yaml delete mode 100644 deployments/arc_execution.yaml delete mode 100644 deployments/blockscout.yaml delete mode 100644 deployments/certs/.gitkeep delete mode 100644 deployments/docker-compose.yml delete mode 100755 deployments/entrypoint_arc_consensus.sh delete mode 100755 deployments/entrypoint_arc_execution.sh delete mode 100755 deployments/localstack_scripts/create-kms-keys.sh delete mode 100755 deployments/localstack_scripts/create-secret-manager-key.sh delete mode 100755 deployments/localstack_scripts/health-check.sh delete mode 100755 deployments/localstack_scripts/install-cert.sh delete mode 100644 deployments/monitoring.yaml delete mode 100644 deployments/monitoring/config-blockscout/backend.env delete mode 100644 deployments/monitoring/config-blockscout/frontend/.curlrc delete mode 100644 deployments/monitoring/config-blockscout/frontend/frontend.env delete mode 100644 deployments/monitoring/config-blockscout/proxy/default.conf.template delete mode 100644 deployments/monitoring/config-grafana/grafana.ini delete mode 100644 deployments/monitoring/config-grafana/provisioning/dashboards-data/default.json delete mode 100644 deployments/monitoring/config-grafana/provisioning/dashboards-data/reth-arc-payload-build.json delete mode 100644 deployments/monitoring/config-grafana/provisioning/dashboards-data/reth-database.json delete mode 100644 deployments/monitoring/config-grafana/provisioning/dashboards-data/reth-discovery.json delete mode 100644 deployments/monitoring/config-grafana/provisioning/dashboards-data/reth-io-correlation.json delete mode 100644 deployments/monitoring/config-grafana/provisioning/dashboards-data/reth-mempool.json delete mode 100644 deployments/monitoring/config-grafana/provisioning/dashboards-data/reth-state-growth.json delete mode 100644 deployments/monitoring/config-grafana/provisioning/dashboards-data/reth.json delete mode 100644 deployments/monitoring/config-grafana/provisioning/dashboards-data/version-panels.json delete mode 100644 deployments/monitoring/config-grafana/provisioning/dashboards/default.yml delete mode 100644 deployments/monitoring/config-grafana/provisioning/datasources/prometheus.yml delete mode 100644 deployments/monitoring/config-prometheus/prometheus.yml delete mode 100644 docker-bake.hcl delete mode 100644 docs/ARCHITECTURE.md delete mode 100644 docs/PROFILING.md delete mode 100644 docs/adr/0001-adr-process.md delete mode 100644 docs/adr/0002-block-dissemination-protocol.md delete mode 100644 docs/adr/0003-governance-configuration-and-validation.md delete mode 100644 docs/adr/0004-base-fee-validation.md delete mode 100644 docs/adr/README.md delete mode 100644 docs/adr/TEMPLATE.md delete mode 100644 docs/assets/arc-logo-dark.svg delete mode 100644 docs/assets/arc-logo-light.svg delete mode 100644 docs/installation.md delete mode 100644 docs/monitoring.md delete mode 100644 docs/running-an-arc-node.md delete mode 100644 eslint.config.mjs delete mode 100644 foundry.toml delete mode 100644 hardhat.config.ts delete mode 100644 package-lock.json delete mode 100644 package.json delete mode 100644 remappings.txt delete mode 100644 rust-toolchain.toml delete mode 100644 rustfmt.toml delete mode 100755 scripts/arc-check.sh delete mode 100755 scripts/build-docker.sh delete mode 100755 scripts/common.sh delete mode 100755 scripts/down.sh delete mode 100755 scripts/engine-bench-report.py delete mode 100644 scripts/export-container-logs.sh delete mode 100644 scripts/genesis/AccountCreator.ts delete mode 100644 scripts/genesis/AdminUpgradeableProxy.ts delete mode 100644 scripts/genesis/Denylist.ts delete mode 100644 scripts/genesis/NativeFiatToken.ts delete mode 100644 scripts/genesis/ProtocolConfig.ts delete mode 100644 scripts/genesis/ValidatorManager.ts delete mode 100644 scripts/genesis/addresses.ts delete mode 100644 scripts/genesis/context.ts delete mode 100644 scripts/genesis/genesis.ts delete mode 100644 scripts/genesis/index.ts delete mode 100644 scripts/genesis/types.ts delete mode 100644 scripts/genesis/versions.ts delete mode 100644 scripts/hardhat/chains/config.ts delete mode 100644 scripts/hardhat/chains/devnet.ts delete mode 100644 scripts/hardhat/chains/localdev.ts delete mode 100644 scripts/hardhat/chains/testnet.ts delete mode 100644 scripts/hardhat/tasks/genesis.ts delete mode 100644 scripts/hardhat/tasks/query-fee.ts delete mode 100644 scripts/hardhat/tasks/query-state.ts delete mode 100644 scripts/hardhat/viem-helper.ts delete mode 100755 scripts/localdev.mjs delete mode 100644 scripts/md-exec.md delete mode 100644 scripts/md-exec.py delete mode 100755 scripts/perf/check-end-state.sh delete mode 100755 scripts/perf/collect-metrics.sh delete mode 100755 scripts/perf/compare-reports.sh delete mode 100755 scripts/perf/prom-lib.sh delete mode 100755 scripts/register-validator.sh delete mode 100755 scripts/release-package.sh delete mode 100755 scripts/run-upgrade-test.sh delete mode 100644 scripts/scenarios/README.md delete mode 100755 scripts/scenarios/nightly-chaos-testing.sh delete mode 100755 scripts/scenarios/nightly-perf.sh delete mode 100644 scripts/scenarios/nightly-random-manifests.sh delete mode 100755 scripts/scenarios/nightly-tx-propagation.sh delete mode 100755 scripts/scenarios/nightly-upgrade.sh delete mode 100644 scripts/test_tx_latency_report.py delete mode 100644 scripts/tx_latency_report.py delete mode 100755 scripts/up.sh delete mode 100755 scripts/update-malachite-deps.sh delete mode 100644 tests/helpers/AdminUpgradeableProxy.ts delete mode 100644 tests/helpers/BalanceComparator.ts delete mode 100644 tests/helpers/CallHelper.ts delete mode 100644 tests/helpers/Denylist.ts delete mode 100644 tests/helpers/DeterministicDeployerProxy.ts delete mode 100644 tests/helpers/FiatToken.ts delete mode 100644 tests/helpers/GasGuzzler.ts delete mode 100644 tests/helpers/NativeCoinAuthority.ts delete mode 100644 tests/helpers/NativeCoinControl.ts delete mode 100644 tests/helpers/NativeTransferHelper.ts delete mode 100644 tests/helpers/PQ.ts delete mode 100644 tests/helpers/ProtocolConfig.ts delete mode 100644 tests/helpers/ReceiptVerifier.ts delete mode 100644 tests/helpers/RevertingProtocolConfig.ts delete mode 100644 tests/helpers/SystemAccounting.ts delete mode 100644 tests/helpers/TraceVerifier.ts delete mode 100644 tests/helpers/ValidatorManager.ts delete mode 100644 tests/helpers/arc-remote-signer-keys.json delete mode 100644 tests/helpers/client-extension.ts delete mode 100644 tests/helpers/forge-artifact.ts delete mode 100644 tests/helpers/getActiveValidatorSet.ts delete mode 100644 tests/helpers/index.ts delete mode 100644 tests/helpers/kzg.ts delete mode 100644 tests/helpers/matchers/Address.ts delete mode 100644 tests/helpers/matchers/BigInt.ts delete mode 100644 tests/helpers/matchers/Hex.ts delete mode 100644 tests/helpers/matchers/index.test.ts delete mode 100644 tests/helpers/matchers/index.ts delete mode 100644 tests/helpers/matchers/plugin.ts delete mode 100644 tests/helpers/matchers/skippable.ts delete mode 100644 tests/helpers/matchers/types.d.ts delete mode 100644 tests/helpers/matchers/utils.ts delete mode 100644 tests/helpers/networks/index.ts delete mode 100644 tests/helpers/networks/localdev.ts delete mode 100644 tests/helpers/pq_test_vectors.json delete mode 100644 tests/helpers/registerNewValidator.ts delete mode 100644 tests/localdev/4844.test.ts delete mode 100644 tests/localdev/7702.test.ts delete mode 100644 tests/localdev/Denylist.test.ts delete mode 100644 tests/localdev/NativeCoinAuthority.test.ts delete mode 100644 tests/localdev/NativeCoinControl.test.ts delete mode 100644 tests/localdev/NativeFiatToken.test.ts delete mode 100644 tests/localdev/PQ.test.ts delete mode 100644 tests/localdev/PrecompileCode.test.ts delete mode 100644 tests/localdev/ProtocolConfig.test.ts delete mode 100644 tests/localdev/SystemAccounting.test.ts delete mode 100644 tests/localdev/UnprotectedTx.test.ts delete mode 100644 tests/localdev/ValidatorManager.test.ts delete mode 100644 tests/localdev/evm_compatibility.test.ts delete mode 100644 tests/localdev/genesis.test.ts delete mode 100644 tests/localdev/native_transfer.test.ts delete mode 100644 tests/localdev/per_validator_fees.test.ts delete mode 100644 tests/localdev/subcall.test.ts delete mode 100644 tests/simulation/BlockHashHistory.test.ts delete mode 100644 tests/simulation/DelegateCall.test.ts delete mode 100644 tests/simulation/Denylist.test.ts delete mode 100644 tests/simulation/NativeFiatToken.test.ts delete mode 100644 tests/simulation/NativeFiatToken.upgrade.test.ts delete mode 100644 tests/simulation/ProtocolConfig.test.ts delete mode 100644 tests/simulation/ProtocolConfig.upgrade.test.ts delete mode 100644 tests/simulation/ValidatorManager.test.ts delete mode 100644 tests/simulation/native_transfer.test.ts delete mode 100644 tests/unit/deployer-nonce.test.ts delete mode 100644 tsconfig.json diff --git a/.cargo/audit.toml b/.cargo/audit.toml deleted file mode 100644 index 478e9f4..0000000 --- a/.cargo/audit.toml +++ /dev/null @@ -1,36 +0,0 @@ -# Configuration for cargo audit - -[advisories] -# Ignore specific advisories by ID -ignore = [ - # ring 0.16.20 - Some AES functions may panic when overflow checking is enabled - "RUSTSEC-2025-0009", - # ring 0.16.20 is unmaintained - waiting for libp2p to update dependencies - "RUSTSEC-2025-0010", - - # Transitive dependencies from reth/alloy that we cannot update directly: - # bincode is unmaintained - used by reth-nippy-jar, reth-stages - "RUSTSEC-2025-0141", - # derivative is unmaintained - used by various reth crates - "RUSTSEC-2024-0388", - # paste is unmaintained - used by alloy-primitives - "RUSTSEC-2024-0436", - # rustls-pemfile is unmaintained - used by reqwest via reth - "RUSTSEC-2025-0134", - # lru IterMut violates Stacked Borrows - used by discv5, reth-network - "RUSTSEC-2026-0002", - - # rand 0.8.5 is unsound with a custom logger - no 0.8.x patch available; upgrading - # to 0.9+ requires a major-version API migration and transitive deps (yamux, libp2p) - # still require rand 0.8.x - "RUSTSEC-2026-0097", - - # hickory-proto 0.25.2 - NSEC3 closest-encloser proof unbounded loop; no fixed version - # available yet. Transitive dependency via reth-dns-discovery (reth v1.11.3) and - # libp2p-mdns (libp2p 0.56.0) — cannot be updated without bumping reth/libp2p. - "RUSTSEC-2026-0118", - # hickory-proto 0.25.2 - O(n²) name compression CPU exhaustion; fix requires >=0.26.1. - # Transitive dependency via reth-dns-discovery (reth v1.11.3) and libp2p-mdns - # (libp2p 0.56.0) — both pin hickory-proto to 0.25.x; cannot update without bumping reth. - "RUSTSEC-2026-0119", -] \ No newline at end of file diff --git a/.cargo/config.toml b/.cargo/config.toml deleted file mode 100644 index 725c201..0000000 --- a/.cargo/config.toml +++ /dev/null @@ -1,5 +0,0 @@ -[build] -rustflags = ["-C", "force-frame-pointers=yes"] - -[net] -git-fetch-with-cli = true diff --git a/.config/nextest.toml b/.config/nextest.toml deleted file mode 100644 index e3df569..0000000 --- a/.config/nextest.toml +++ /dev/null @@ -1,23 +0,0 @@ -# Nextest configuration for arc-node -# See: https://nexte.st/docs/configuration/ - -[profile.default] -# Default profile settings (local development) -fail-fast = true - -[profile.ci] -# CI-specific profile: run with `cargo nextest run --profile ci` -# Ensures all tests run to completion so CI gets the full picture of failures. -fail-fast = false - -# Retry flaky tests with exponential backoff. -# Tests are retried up to 2 times with increasing delay between attempts. -retries = { backoff = "exponential", count = 2, delay = "1s", max-delay = "10s" } - -# Detect tests that hang or run unreasonably long. -slow-timeout = { period = "120s", terminate-after = 2 } - -# JUnit XML output for CI reporting and test analytics. -[profile.ci.junit] -path = "target/nextest/ci/junit.xml" -report-name = "arc-node-ci" diff --git a/.dockerignore b/.dockerignore deleted file mode 100644 index 36aaf45..0000000 --- a/.dockerignore +++ /dev/null @@ -1,82 +0,0 @@ -# Rust build artifacts -target/ -rust-toolchain.toml -**/*.rs.bk - -# Quake artifacts -/quake -.quake/ - -# Terraform state -.terraform -.terraform.* -terraform.tfstate* - -# IDE and editor files -.vscode/ -.idea/ -*.swp -*.swo -*~ - -# OS generated files -.DS_Store -.DS_Store? -._* -.Spotlight-V100 -.Trashes -ehthumbs.db -Thumbs.db - -# Git -.git/ -.gitignore - -# Documentation -docs/ -*.md - -# Test files -tests/ -**/*.test.ts -**/*.test.js - -# Node.js (if any) -node_modules/ -npm-debug.log* -yarn-debug.log* -yarn-error.log* - -# Build assets -datadir/ -contracts/ - -# Docker -Dockerfile* -docker-compose* -.dockerignore -docker-bake.hcl - -# CI/CD and tooling -.github/ -.agents/ -.claude/ -.gitlab-ci.yml -.travis.yml -atlantis.yaml - -# JS tooling (not needed for Rust builds) -package.json -package-lock.json -k6/ - -# Logs -*.log - -# Temporary files -*.tmp -*.temp - -# Scripts (if not needed for build) -scripts/ -deployments/ diff --git a/.foundry-version b/.foundry-version deleted file mode 100644 index 61919cd..0000000 --- a/.foundry-version +++ /dev/null @@ -1 +0,0 @@ -v1.4.4 diff --git a/.github/actions/cloudsmith-login/action.yml b/.github/actions/cloudsmith-login/action.yml deleted file mode 100644 index 71adc29..0000000 --- a/.github/actions/cloudsmith-login/action.yml +++ /dev/null @@ -1,66 +0,0 @@ -name: Cloudsmith Docker Login -description: Exchange GitHub OIDC token for Cloudsmith credentials and log in to the Docker registry. - -inputs: - registry: - description: Cloudsmith Docker registry hostname - required: true - org: - description: Cloudsmith organization - required: false - default: circle - service-slug: - description: Cloudsmith OIDC service account slug - required: false - default: arc-publisher-gha-service - -runs: - using: composite - steps: - - name: Get Cloudsmith token - id: get-token - uses: actions/github-script@ed597411d8f924073f98dfc5c65a23a2325f34cd # v8.0.0 - env: - CLOUDSMITH_ORG: ${{ inputs.org }} - CLOUDSMITH_SERVICE_SLUG: ${{ inputs.service-slug }} - with: - script: |- - let oidc_token; - try { - oidc_token = await core.getIDToken(); - core.setSecret(oidc_token); - } catch (error) { - core.setFailed(`Failed to get OIDC token: ${error}`); - return; - } - try { - const response = await fetch(`https://api.cloudsmith.io/openid/${process.env.CLOUDSMITH_ORG}/`, { - method: 'POST', - headers: { - 'Content-Type': 'application/json', - }, - body: JSON.stringify({ - oidc_token: oidc_token, - service_slug: process.env.CLOUDSMITH_SERVICE_SLUG - }), - signal: AbortSignal.timeout(30000), - }); - if (!response.ok) { - core.setFailed(`Cloudsmith OIDC exchange failed: ${response.status} ${response.statusText}`); - return; - } - const data = await response.json(); - core.setSecret(data.token); - core.setOutput('cloudsmith_user', process.env.CLOUDSMITH_SERVICE_SLUG); - core.setOutput('cloudsmith_token', data.token); - } catch (error) { - core.setFailed(`Failed to login to Cloudsmith: ${error}`); - return; - } - - - name: Login to Cloudsmith - uses: docker/login-action@b45d80f862d83dbcd57f89517bcf500b2ab88fb2 # v4.0.0 - with: - registry: ${{ inputs.registry }} - username: ${{ steps.get-token.outputs.cloudsmith_user }} - password: ${{ steps.get-token.outputs.cloudsmith_token }} diff --git a/.github/workflows/build-docker.yaml b/.github/workflows/build-docker.yaml deleted file mode 100644 index ec92ba9..0000000 --- a/.github/workflows/build-docker.yaml +++ /dev/null @@ -1,219 +0,0 @@ -name: Build Docker - -on: - pull_request: - types: [labeled, synchronize] - branches: - - main - - release/* - push: - tags: - - 'v*' - -concurrency: - group: docker-${{ github.ref }} - cancel-in-progress: ${{ github.event_name == 'pull_request' }} - -env: - REGISTRY: docker.cloudsmith.io - REGISTRY_NAMESPACE: circle/arc-network - -jobs: - build: - if: >- - github.event_name == 'push' || - contains(github.event.pull_request.labels.*.name, 'build-docker') - name: Build ${{ matrix.image }} (${{ matrix.platform }}) - permissions: - contents: read - id-token: write - runs-on: ${{ matrix.runner }} - strategy: - fail-fast: false - matrix: - image: - - arc-execution - - arc-consensus - platform: - - linux/amd64 - - linux/arm64 - include: - - platform: linux/amd64 - arch: amd64 - runner: ubuntu-latest - - platform: linux/arm64 - arch: arm64 - runner: ubuntu-24.04-arm - steps: - - name: Checkout - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 - with: - submodules: recursive - - - name: Compute short hash - id: vars - env: - SHA: ${{ github.sha }} - run: echo "short_hash=${SHA::8}" >> "$GITHUB_OUTPUT" - - - name: Set up Docker Buildx - uses: docker/setup-buildx-action@4d04d5d9486b7bd6fa91e7baf45bbb4f8b9deedd # v4.0.0 - - - name: Build image - id: build - uses: docker/bake-action@82490499d2e5613fcead7e128237ef0b0ea210f7 # v7.0.0 - with: - source: . - files: docker-bake.hcl - targets: ${{ matrix.image }} - set: | - ${{ matrix.image }}.platform=${{ matrix.platform }} - ${{ matrix.image }}.tags= - ${{ matrix.image }}.output=type=oci,tar=false,dest=/tmp/image - env: - BUILDX_NO_DEFAULT_ATTESTATIONS: 1 - GITHUB_TOKEN: ${{ github.token }} - GIT_COMMIT_HASH: ${{ github.sha }} - GIT_VERSION: ${{ github.ref_name }} - GIT_SHORT_HASH: ${{ steps.vars.outputs.short_hash }} - - - name: Trivy vulnerability scan - uses: aquasecurity/trivy-action@57a97c7e7821a5776cebc9bb87c984fa69cba8f1 # v0.35.0 - with: - input: /tmp/image - format: sarif - output: trivy-results.sarif - severity: CRITICAL - limit-severities-for-sarif: true - exit-code: '1' - - - name: Upload Trivy scan results - if: always() - uses: actions/upload-artifact@bbbca2ddaa5d8feaa63e36b76fdaad77386f024f # v7.0.0 - with: - name: trivy-${{ matrix.image }}-${{ matrix.arch }} - path: trivy-results.sarif - - - name: Login to Cloudsmith - if: ${{ github.event_name == 'push' }} - uses: ./.github/actions/cloudsmith-login - with: - registry: ${{ env.REGISTRY }} - - - name: Push image by digest - if: ${{ github.event_name == 'push' }} - env: - IMAGE: ${{ env.REGISTRY }}/${{ env.REGISTRY_NAMESPACE }}/${{ matrix.image }} - BAKE_METADATA: ${{ steps.build.outputs.metadata }} - MATRIX_IMAGE: ${{ matrix.image }} - MATRIX_PLATFORM: ${{ matrix.platform }} - run: | - DIGEST=$(echo "${BAKE_METADATA}" | jq -r --arg img "${MATRIX_IMAGE}" '.[$img]."containerimage.digest"') - if [ -z "${DIGEST}" ] || [ "${DIGEST}" = "null" ]; then - echo "::error::Failed to extract digest from build metadata" - exit 1 - fi - skopeo copy "oci:/tmp/image" "docker://${IMAGE}@${DIGEST}" --digestfile /tmp/push-digest - rm -rf /tmp/image - REGISTRY_DIGEST=$(cat /tmp/push-digest) - if [ "${DIGEST}" != "${REGISTRY_DIGEST}" ]; then - echo "::warning::Bake metadata digest ${DIGEST} differs from registry digest ${REGISTRY_DIGEST}" - fi - PLATFORM_SLUG=$(echo "${MATRIX_PLATFORM}" | tr '/' '-') - mkdir -p "/tmp/digests/${MATRIX_IMAGE}" - echo "${REGISTRY_DIGEST}" > "/tmp/digests/${MATRIX_IMAGE}/${PLATFORM_SLUG}" - - - name: Upload digest - if: ${{ github.event_name == 'push' }} - uses: actions/upload-artifact@bbbca2ddaa5d8feaa63e36b76fdaad77386f024f # v7.0.0 - with: - name: digest-${{ matrix.image }}-${{ matrix.arch }} - path: /tmp/digests/${{ matrix.image }}/* - if-no-files-found: error - - manifest: - if: ${{ github.event_name == 'push' }} - name: Manifest ${{ matrix.image }} - permissions: - contents: read - id-token: write - attestations: write - needs: [build] - runs-on: ubuntu-latest - strategy: - fail-fast: false - matrix: - image: - - arc-execution - - arc-consensus - steps: - - name: Checkout - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 - with: - sparse-checkout: .github/actions - - - name: Set up Docker Buildx - uses: docker/setup-buildx-action@4d04d5d9486b7bd6fa91e7baf45bbb4f8b9deedd # v4.0.0 - - - name: Login to Cloudsmith - uses: ./.github/actions/cloudsmith-login - with: - registry: ${{ env.REGISTRY }} - - - name: Download digests - uses: actions/download-artifact@70fc10c6e5e1ce46ad2ea6f2b72d43f7d47b13c3 # v8.0.0 - with: - pattern: digest-${{ matrix.image }}-* - merge-multiple: true - path: /tmp/digests - - - name: Create and inspect multi-arch manifest - id: manifest - env: - IMAGE: ${{ env.REGISTRY }}/${{ env.REGISTRY_NAMESPACE }}/${{ matrix.image }} - RELEASE_TAG: ${{ github.ref_name }} - run: | - VERSION="${RELEASE_TAG#v}" - TAG="${IMAGE}:${VERSION}" - - # Build digest args from all platform artifacts - DIGEST_ARGS=() - for f in /tmp/digests/*; do - DIGEST_ARGS+=("${IMAGE}@$(cat "$f")") - done - - if [ ${#DIGEST_ARGS[@]} -eq 0 ]; then - echo "::error::No digest files found" - exit 1 - fi - - # Create and push the manifest list - docker buildx imagetools create -t "${TAG}" "${DIGEST_ARGS[@]}" - - MANIFEST_DIGEST=$(docker buildx imagetools inspect "${TAG}" --raw | sha256sum | awk '{print "sha256:"$1}') - echo "digest=${MANIFEST_DIGEST}" >> "$GITHUB_OUTPUT" - echo "image-with-digest=${IMAGE}@${MANIFEST_DIGEST}" >> "$GITHUB_OUTPUT" - - - name: Generate SBOM - continue-on-error: true - uses: anchore/sbom-action@17ae1740179002c89186b61233e0f892c3118b11 # v0.23.0 - with: - image: ${{ steps.manifest.outputs.image-with-digest }} - artifact-name: sbom-${{ matrix.image }}.spdx.json - output-file: sbom-${{ matrix.image }}.spdx.json - upload-release-assets: false - - - name: Attest build provenance - uses: actions/attest-build-provenance@a2bbfa25375fe432b6a289bc6b6cd05ecd0c4c32 # v4.1.0 - with: - subject-name: ${{ env.REGISTRY }}/${{ env.REGISTRY_NAMESPACE }}/${{ matrix.image }} - subject-digest: ${{ steps.manifest.outputs.digest }} - push-to-registry: true - - - name: Attest SBOM - uses: actions/attest-sbom@07e74fc4e78d1aad915e867f9a094073a9f71527 # v4.0.0 - with: - subject-name: ${{ env.REGISTRY }}/${{ env.REGISTRY_NAMESPACE }}/${{ matrix.image }} - subject-digest: ${{ steps.manifest.outputs.digest }} - sbom-path: sbom-${{ matrix.image }}.spdx.json - push-to-registry: true diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml deleted file mode 100644 index 31928da..0000000 --- a/.github/workflows/ci.yml +++ /dev/null @@ -1,288 +0,0 @@ -name: Public CI - -on: - push: - branches: [main] - pull_request: - branches: - - main - -concurrency: - group: ${{ github.workflow }}-${{ github.head_ref || github.run_id }} - cancel-in-progress: true - -env: - CARGO_TERM_COLOR: always - RUST_BACKTRACE: 1 - -jobs: - # --------------------------------------------------------------------------- - # Rust: fmt, deps, lint run in parallel -> unit tests after lint passes - # --------------------------------------------------------------------------- - - rust-fmt: - name: Rust Formatting - runs-on: ubuntu-latest - steps: - - uses: actions/checkout@v4 - - - name: Install Rust toolchain - uses: actions-rust-lang/setup-rust-toolchain@v1 - with: - components: rustfmt - - - name: Run cargo fmt - run: cargo fmt --all -- --check - - rust-deps: - name: Rust Dependencies - runs-on: ubuntu-latest - steps: - - uses: actions/checkout@v4 - - - name: Install Rust toolchain - uses: actions-rust-lang/setup-rust-toolchain@v1 - - - name: Install cargo-sort - run: cargo install cargo-sort - - - name: Run cargo sort check - run: cargo sort --workspace --check - - rust-lint: - name: Rust Lint - runs-on: ubuntu-latest - steps: - - uses: actions/checkout@v4 - - - name: Install system dependencies - run: sudo apt-get update && sudo apt-get install -y --no-install-recommends libclang-dev zlib1g-dev - - - name: Install Rust toolchain - uses: actions-rust-lang/setup-rust-toolchain@v1 - with: - components: clippy - cache-shared-key: rust-build - - - name: Run clippy - run: cargo clippy --all-targets --all-features -- -D warnings - - rust-test: - name: Rust Unit Tests - needs: rust-lint - runs-on: ubuntu-latest - steps: - - uses: actions/checkout@v4 - - - name: Install system dependencies - run: sudo apt-get update && sudo apt-get install -y --no-install-recommends libclang-dev zlib1g-dev - - - name: Install Rust toolchain - uses: actions-rust-lang/setup-rust-toolchain@v1 - with: - cache-shared-key: rust-build - - - name: Install nextest - uses: taiki-e/install-action@v2 - with: - tool: nextest - - - name: Run unit tests - run: cargo nextest run --locked --workspace --exclude arc-test-integration - - rust-integration-test: - name: Rust Integration Tests - needs: rust-test - runs-on: ubuntu-latest - steps: - - uses: actions/checkout@v4 - - - name: Init public submodules - run: git submodule update --init contracts/lib/forge-std contracts/lib/openzeppelin-contracts contracts/lib/openzeppelin-contracts-upgradeable - - - name: Install system dependencies - run: sudo apt-get update && sudo apt-get install -y --no-install-recommends libclang-dev zlib1g-dev - - - name: Install Rust toolchain - uses: actions-rust-lang/setup-rust-toolchain@v1 - - - name: Setup Node.js - uses: actions/setup-node@v4 - with: - node-version: 22 - cache: npm - - - name: Install Node dependencies - run: npm ci - - - name: Read Foundry version - id: foundry-version - run: echo "version=$(cat .foundry-version)" >> $GITHUB_OUTPUT - - - name: Install Foundry - uses: foundry-rs/foundry-toolchain@v1 - with: - version: ${{ steps.foundry-version.outputs.version }} - - - name: Install nextest - uses: taiki-e/install-action@v2 - with: - tool: nextest - - - name: Run node integration tests - run: cargo nextest run --locked -p arc-test-integration --test-threads 1 - - - name: Start integration services - run: make up - - - name: Run integration tests - run: cargo nextest run --locked --workspace --exclude arc-test-integration --features integration - - # --------------------------------------------------------------------------- - # Protocol Buffers (standalone, no deps) - # --------------------------------------------------------------------------- - - proto: - name: Protocol Buffers - runs-on: ubuntu-latest - permissions: - contents: read - pull-requests: write - steps: - - uses: actions/checkout@v4 - - - name: Buf lint, format, and breaking change detection - uses: bufbuild/buf-action@v1 - with: - github_token: ${{ secrets.GITHUB_TOKEN }} - push: false - archive: false - - # --------------------------------------------------------------------------- - # Solidity Contracts: lint (fast) -> build -> test - # --------------------------------------------------------------------------- - - contracts-lint: - name: Contracts Lint - runs-on: ubuntu-latest - steps: - - uses: actions/checkout@v4 - - - name: Init public submodules - run: git submodule update --init contracts/lib/forge-std contracts/lib/openzeppelin-contracts contracts/lib/openzeppelin-contracts-upgradeable - - - name: Setup Node.js - uses: actions/setup-node@v4 - with: - node-version: 22 - cache: npm - - - name: Install Node dependencies - run: npm ci - - - name: Read Foundry version - id: foundry-version - run: echo "version=$(cat .foundry-version)" >> $GITHUB_OUTPUT - - - name: Install Foundry - uses: foundry-rs/foundry-toolchain@v1 - with: - version: ${{ steps.foundry-version.outputs.version }} - - - name: Run forge lint - run: forge lint - - # scripts/ and tests/ depend on types from an external submodule. - # Without it, TypeScript cannot resolve types and eslint reports - # no-unsafe-* errors across the entire import chain. - - name: Run eslint - run: | - npx eslint --ignore-pattern 'scripts/' --ignore-pattern 'tests/' - ! grep -R 'it.only' tests/ -q - - contracts-build: - name: Contracts Build - needs: contracts-lint - runs-on: ubuntu-latest - steps: - - uses: actions/checkout@v4 - - - name: Init public submodules - run: git submodule update --init contracts/lib/forge-std contracts/lib/openzeppelin-contracts contracts/lib/openzeppelin-contracts-upgradeable - - - name: Read Foundry version - id: foundry-version - run: echo "version=$(cat .foundry-version)" >> $GITHUB_OUTPUT - - - name: Install Foundry - uses: foundry-rs/foundry-toolchain@v1 - with: - version: ${{ steps.foundry-version.outputs.version }} - - - name: Build contracts - run: forge build --sizes - - contracts-test: - name: Contracts Tests - needs: contracts-build - runs-on: ubuntu-latest - steps: - - uses: actions/checkout@v4 - - - name: Init public submodules - run: git submodule update --init contracts/lib/forge-std contracts/lib/openzeppelin-contracts contracts/lib/openzeppelin-contracts-upgradeable - - - name: Read Foundry version - id: foundry-version - run: echo "version=$(cat .foundry-version)" >> $GITHUB_OUTPUT - - - name: Install Foundry - uses: foundry-rs/foundry-toolchain@v1 - with: - version: ${{ steps.foundry-version.outputs.version }} - - - name: Run Foundry tests - run: forge test -vvv --gas-report - - # --------------------------------------------------------------------------- - # Docker Images - # --------------------------------------------------------------------------- - - docker-build: - name: Docker Build (${{ matrix.target }}) - needs: [rust-lint, contracts-build] - runs-on: ubuntu-latest - strategy: - fail-fast: false - matrix: - include: - - target: execution - dockerfile: deployments/Dockerfile.execution - - target: consensus - dockerfile: deployments/Dockerfile.consensus - steps: - - uses: actions/checkout@v4 - - - name: Get short SHA - id: short-sha - run: echo "sha=$(echo ${{ github.sha }} | cut -c1-8)" >> $GITHUB_OUTPUT - - - name: Set up Docker Buildx - uses: docker/setup-buildx-action@v3 - - - name: Build ${{ matrix.target }} image - uses: docker/build-push-action@v6 - with: - context: . - file: ${{ matrix.dockerfile }} - target: release-runtime - push: false - build-args: | - BUILD_PROFILE=release - GIT_COMMIT_HASH=${{ github.sha }} - GIT_VERSION=${{ github.ref_name }} - GIT_SHORT_HASH=${{ steps.short-sha.outputs.sha }} - build-contexts: | - certs=deployments/certs - cache-from: type=gha - cache-to: type=gha,mode=max diff --git a/.github/workflows/label-external-prs.yml b/.github/workflows/label-external-prs.yml deleted file mode 100644 index 68d2eae..0000000 --- a/.github/workflows/label-external-prs.yml +++ /dev/null @@ -1,40 +0,0 @@ -# SECURITY: This workflow uses pull_request_target. Do NOT add actions/checkout -# with a PR-controlled ref: that would execute attacker code with write access -# to secrets. Only read pull_request metadata. -name: Label External PRs for Import - -on: - pull_request_target: - types: [closed] - branches: [main] - -permissions: - pull-requests: write - -concurrency: - group: label-external-pr-${{ github.event.pull_request.number }} - cancel-in-progress: false - # Partial cancellation could leave a PR unlabeled; label application is - # idempotent so letting both runs finish is safe. - -jobs: - label: - # Skip unmerged closes and the automated upstream-sync squash PRs. - # Bot author AND sync branch are both checked as belt-and-suspenders; - # either alone is sufficient to identify a sync PR. - if: >- - github.repository == 'circlefin/arc-node' && - github.event.pull_request.merged == true && - !(github.event.pull_request.user.login == 'circle-github-action-bot' && - github.event.pull_request.head.ref == 'sync/copybara-export') - runs-on: ubuntu-latest - timeout-minutes: 5 - steps: - - name: Apply pending-import label - env: - GH_TOKEN: ${{ github.token }} - PR_NUMBER: ${{ github.event.pull_request.number }} - REPO: ${{ github.repository }} - run: | - set -euo pipefail - gh pr edit "${PR_NUMBER}" --repo "${REPO}" --add-label pending-import diff --git a/.github/workflows/release-binaries.yaml b/.github/workflows/release-binaries.yaml deleted file mode 100644 index 15ad43e..0000000 --- a/.github/workflows/release-binaries.yaml +++ /dev/null @@ -1,226 +0,0 @@ -name: Release Binaries - -on: - push: - tags: - - 'v*' - workflow_dispatch: - inputs: - tag: - description: 'Tag to build and release (e.g. v0.6.0)' - required: true - type: string - -env: - CARGO_TERM_COLOR: always - RUST_BACKTRACE: 1 - CARGO_NET_GIT_FETCH_WITH_CLI: "true" - -concurrency: - group: release-binaries-${{ inputs.tag || github.ref_name }} - cancel-in-progress: false - -jobs: - build: - name: Build (${{ matrix.target }}) - runs-on: ${{ matrix.runner }} - timeout-minutes: 120 - permissions: - contents: read - strategy: - fail-fast: false - matrix: - include: - - target: x86_64-unknown-linux-gnu - os: linux - runner: ${{ vars.RELEASE_RUNNER_LINUX_AMD64 || 'ubuntu-24.04' }} - - target: aarch64-unknown-linux-gnu - os: linux - runner: ${{ vars.RELEASE_RUNNER_LINUX_ARM64 || 'ubuntu-24.04-arm' }} - - target: aarch64-apple-darwin - os: macos - runner: ${{ vars.RELEASE_RUNNER_MACOS_ARM64 || 'macos-15' }} - steps: - - name: Resolve artifact version - id: version - env: - EVENT_NAME: ${{ github.event_name }} - INPUT_TAG: ${{ inputs.tag }} - run: | - if [[ "$EVENT_NAME" == "workflow_dispatch" ]]; then - VERSION="$INPUT_TAG" - elif [[ "$GITHUB_REF" == refs/tags/* ]]; then - VERSION="${GITHUB_REF#refs/tags/}" - else - echo "::error::Unsupported release trigger: $EVENT_NAME $GITHUB_REF" - exit 1 - fi - if [[ ! "$VERSION" =~ ^v[0-9]+\.[0-9]+\.[0-9]+(-[A-Za-z0-9.+-]+)?$ ]]; then - echo "::error::Invalid tag format: '$VERSION' (expected e.g. v1.2.3 or v1.2.3-rc.1)" - exit 1 - fi - echo "version=$VERSION" >> "$GITHUB_OUTPUT" - - - name: Checkout - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 - with: - ref: ${{ github.event_name == 'workflow_dispatch' && format('refs/tags/{0}', inputs.tag) || github.ref }} - submodules: recursive - - - name: Install Linux system dependencies - if: matrix.os == 'linux' - run: sudo apt-get update && sudo apt-get install -y --no-install-recommends libclang-dev - - - name: Install macOS system dependencies - if: matrix.os == 'macos' - run: | - brew install llvm pkg-config - LLVM_PREFIX="$(brew --prefix llvm)" - echo "${LLVM_PREFIX}/bin" >> "$GITHUB_PATH" - echo "LIBCLANG_PATH=${LLVM_PREFIX}/lib" >> "$GITHUB_ENV" - - - name: Install Rust toolchain - uses: actions-rust-lang/setup-rust-toolchain@150fca883cd4034361b621bd4e6a9d34e5143606 # v1.15.4 - - - name: Validate runner target - env: - TARGET: ${{ matrix.target }} - run: | - HOST="$(rustc -vV | awk '/^host:/ {print $2}')" - if [[ "$HOST" != "$TARGET" ]]; then - echo "::error::Runner host '$HOST' does not match release target '$TARGET'" - exit 1 - fi - - - name: Install sccache - uses: taiki-e/install-action@a661f9d0b5f5222ce08202e6b2e9648d1713ca37 # untagged 2025-07-01 commit - with: - tool: sccache - - - name: Configure sccache - uses: actions/github-script@60a0d83039c74a4aee543508d2ffcb1c3799cdea # v7.0.1 - with: - script: | - core.exportVariable('ACTIONS_RESULTS_URL', process.env.ACTIONS_RESULTS_URL || ''); - core.exportVariable('ACTIONS_RUNTIME_TOKEN', process.env.ACTIONS_RUNTIME_TOKEN || ''); - - - name: Build release binaries - run: | - cargo build --locked --release \ - --bin arc-node-execution \ - --bin arc-node-consensus \ - --bin arc-snapshots - env: - RUSTC_WRAPPER: sccache - SCCACHE_GHA_ENABLED: "true" - - - name: Package release assets - env: - TAG: ${{ steps.version.outputs.version }} - TARGET: ${{ matrix.target }} - run: ./scripts/release-package.sh "$TAG" "$TARGET" - - - name: Upload build artifacts - uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4.6.2 - with: - name: release-${{ matrix.target }} - path: release-assets/ - - sign: - name: Prepare Release Assets - needs: build - runs-on: ubuntu-24.04 - timeout-minutes: 10 - permissions: - contents: read - env: - HAS_RELEASE_GPG_KEY: ${{ secrets.RELEASE_GPG_PRIVATE_KEY != '' }} - steps: - - name: Download build artifacts - uses: actions/download-artifact@d3f86a106a0bac45b974a628896c90dbdf5c8093 # v4.3.0 - with: - pattern: release-* - merge-multiple: true - path: release-assets/ - - - name: Import GPG key - if: env.HAS_RELEASE_GPG_KEY == 'true' - uses: crazy-max/ghaction-import-gpg@e89d40939c28e39f97cf32126055eeae86ba74ec # v6.3.0 - with: - gpg_private_key: ${{ secrets.RELEASE_GPG_PRIVATE_KEY }} - - - name: GPG sign archives - if: env.HAS_RELEASE_GPG_KEY == 'true' - run: | - for archive in release-assets/*.tar.gz; do - gpg --batch --yes --detach-sign --armor --output "${archive}.asc" "${archive}" - done - - - name: Upload release assets - uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4.6.2 - with: - name: release-assets - path: release-assets/ - - release: - name: Create Release - needs: sign - if: startsWith(github.ref, 'refs/tags/') || github.event_name == 'workflow_dispatch' - runs-on: ubuntu-24.04 - timeout-minutes: 10 - permissions: - contents: write - steps: - - name: Resolve tag - id: tag - env: - EVENT_NAME: ${{ github.event_name }} - INPUT_TAG: ${{ inputs.tag }} - run: | - if [[ "$EVENT_NAME" == "workflow_dispatch" ]]; then - TAG="$INPUT_TAG" - elif [[ "$GITHUB_REF" == refs/tags/* ]]; then - TAG="${GITHUB_REF#refs/tags/}" - else - echo "::error::Unsupported release trigger: $EVENT_NAME $GITHUB_REF" - exit 1 - fi - if [[ ! "$TAG" =~ ^v[0-9]+\.[0-9]+\.[0-9]+(-[A-Za-z0-9.+-]+)?$ ]]; then - echo "::error::Invalid tag format: '$TAG' (expected e.g. v1.2.3 or v1.2.3-rc.1)" - exit 1 - fi - echo "tag=$TAG" >> "$GITHUB_OUTPUT" - if [[ "$TAG" =~ ^v[0-9]+\.[0-9]+\.[0-9]+- ]]; then - echo "is_prerelease=true" >> "$GITHUB_OUTPUT" - else - echo "is_prerelease=false" >> "$GITHUB_OUTPUT" - fi - - - name: Download build artifacts - uses: actions/download-artifact@d3f86a106a0bac45b974a628896c90dbdf5c8093 # v4.3.0 - with: - name: release-assets - path: release-assets/ - - - name: Upload release assets - env: - GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} - GH_REPO: ${{ github.repository }} - TAG: ${{ steps.tag.outputs.tag }} - IS_PRERELEASE: ${{ steps.tag.outputs.is_prerelease }} - run: | - create_args=(--draft --generate-notes) - edit_args=(--draft=false) - if [[ "$IS_PRERELEASE" == "true" ]]; then - create_args+=(--prerelease) - edit_args+=(--prerelease=true) - fi - - if gh release view "${TAG}" >/dev/null 2>&1; then - echo "::error::Release ${TAG} already exists; delete the existing draft/release before rerunning" - exit 1 - fi - - gh release create "${TAG}" "${create_args[@]}" - gh release upload "${TAG}" release-assets/* - gh release edit "${TAG}" "${edit_args[@]}" diff --git a/.gitignore b/.gitignore deleted file mode 100644 index 52df018..0000000 --- a/.gitignore +++ /dev/null @@ -1,123 +0,0 @@ -# Rust build artifacts -/target/ -**/target/ - -# IDE and editor files -.vscode/ -.idea/ -*.swp -*.swo -*~ - -# OS generated files -.DS_Store -.DS_Store? -._* -.Spotlight-V100 -.Trashes -ehthumbs.db -Thumbs.db - -# Logs -*.log - -# Runtime data -pids -*.pid -*.seed -*.pid.lock -datadir/ - -# Coverage directory used by tools like istanbul -coverage/ -*.lcov - -# Dependency directories -node_modules/ - -# Optional npm cache directory -.npm - -# Optional eslint cache -.eslintcache - -# Microbundle cache -.rpt2_cache/ -.rts2_cache_cjs/ -.rts2_cache_es/ -.rts2_cache_umd/ - -# Optional REPL history -.node_repl_history - -# Output of 'npm pack' -*.tgz - -# Yarn Integrity file -.yarn-integrity - -# dotenv environment variables file -.env -.env.local -.env.development.local -.env.test.local -.env.production.local -.env.contract-ops - -# Rust-specific -**/*.rs.bk -*.pdb - -# Python-specific -__pycache__/ -*.pyc - -# Test artifacts -/test-results/ -/coverage/ -/volumes - -# Backup files -*.orig -*.rej - -# Temporary files -*.tmp -*.temp -.tmp_ai_logs/ - -# Build output -/dist/ -/build/ -logs/ -typechain-types/ -nodes/ - -# old cache path -/cache/ -/cache_forge/ - -# Foundry -broadcast/ - -# Quake -.quake/ -/quake -.terraform/ -.terraform.* -terraform.tfstate* -terraform.tfvars* - -# Spammer -/spammer - -# Claude — ignore local/personal files, track shared config -.claude/settings.local.json -.claude/CLAUDE.md -.claude/memory/ -.claude/worktrees/ -.claude/todo.md -.planning - -deployments/certs/* -!deployments/certs/.gitkeep diff --git a/.gitmodules b/.gitmodules deleted file mode 100644 index bceee44..0000000 --- a/.gitmodules +++ /dev/null @@ -1,11 +0,0 @@ -[submodule "contracts/lib/forge-std"] - path = contracts/lib/forge-std - url = https://github.com/foundry-rs/forge-std -[submodule "contracts/lib/openzeppelin-contracts"] - path = contracts/lib/openzeppelin-contracts - url = https://github.com/openzeppelin/openzeppelin-contracts - branch = release-v5.4 -[submodule "contracts/lib/openzeppelin-contracts-upgradeable"] - path = contracts/lib/openzeppelin-contracts-upgradeable - url = https://github.com/OpenZeppelin/openzeppelin-contracts-upgradeable - branch = release-v5.4 diff --git a/.licenseignore b/.licenseignore deleted file mode 100644 index 19bb738..0000000 --- a/.licenseignore +++ /dev/null @@ -1 +0,0 @@ -pkg:npm/uri-js@4.4.1 diff --git a/.mcp.json b/.mcp.json deleted file mode 100644 index b97732d..0000000 --- a/.mcp.json +++ /dev/null @@ -1,9 +0,0 @@ -{ - "mcpServers": { - "quake": { - "command": "cargo", - "args": ["run", "--bin", "quake", "--", "mcp"], - "cwd": "${workspaceFolder}" - } - } -} diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml deleted file mode 100644 index 960ff43..0000000 --- a/.pre-commit-config.yaml +++ /dev/null @@ -1,54 +0,0 @@ -# See https://pre-commit.com for more information -# See https://pre-commit.com/hooks.html for more hooks -repos: - - repo: https://github.com/pre-commit/pre-commit-hooks - rev: v6.0.0 - hooks: - - id: fix-byte-order-marker - - id: check-case-conflict - - id: check-merge-conflict - - id: check-symlinks - - id: check-yaml - - id: check-toml - - id: end-of-file-fixer - - id: mixed-line-ending - - id: trailing-whitespace - - - repo: local - hooks: - - id: rust-clippy - name: lint and fix rust code - entry: cargo clippy --all-features --fix --allow-dirty -- -D warnings - language: system - files: \.rs$ - pass_filenames: false - always_run: true - stages: [pre-push] - - id: rust-fmt - name: format rust code - entry: cargo fmt --all - language: system - files: \.rs$ - pass_filenames: false - always_run: true - - id: cargo-sort-check - name: sort rust dependencies - entry: cargo sort --workspace - language: system - files: Cargo\.toml$ - pass_filenames: false - always_run: true - - id: buf-format - name: format protobuf files - entry: buf format -w - language: system - files: \.proto$ - pass_filenames: false - always_run: true - - - repo: https://github.com/pre-commit/mirrors-eslint - rev: v9.32.0 - hooks: - - id: eslint - name: lint and fix javascript code - args: [--fix] diff --git a/.prettierignore b/.prettierignore deleted file mode 100644 index 2df6956..0000000 --- a/.prettierignore +++ /dev/null @@ -1,8 +0,0 @@ -contracts/lib/** -contracts/out/** -contracts/cache/** -.github/** -assets/artifacts/**/*.json -**/*.md -**/*.yaml -**/*.yml diff --git a/.prettierrc b/.prettierrc deleted file mode 100644 index 8a69582..0000000 --- a/.prettierrc +++ /dev/null @@ -1,8 +0,0 @@ -{ - "semi": false, - "arrowParens": "always", - "singleQuote": true, - "proseWrap": "never", - "printWidth": 120, - "trailingComma": "all" -} diff --git a/ACCOUNTS.md b/ACCOUNTS.md deleted file mode 100644 index 2b768eb..0000000 --- a/ACCOUNTS.md +++ /dev/null @@ -1,91 +0,0 @@ -# Standard Development Accounts - -This document lists the standard development accounts used by Ethereum development tools including Hardhat, Anvil (Foundry), and Arc local development mode. - -## 🔑 Account Details - -These accounts are derived from the mnemonic: `test test test test test test test test test test test junk` - -| # | Address | Private Key | Balance (Dev Mode) | -|---|---------|-------------|-------------------| -| 0 | `0xf39Fd6e51aad88F6F4ce6aB8827279cffFb92266` | `0xac0974bec39a17e36ba4a6b4d238ff944bacb478cbed5efcae784d7bf4f2ff80` | 10,000 ETH | -| 1 | `0x70997970C51812dc3A010C7d01b50e0d17dc79C8` | `0x59c6995e998f97a5a0044966f0945389dc9e86dae88c7a8412f4603b6b78690d` | 10,000 ETH | -| 2 | `0x3C44CdDdB6a900fa2b585dd299e03d12FA4293BC` | `0x5de4111afa1a4b94908f83103eb1f1706367c2e68ca870fc3fb9a804cdab365a` | 10,000 ETH | -| 3 | `0x90F79bf6EB2c4f870365E785982E1f101E93b906` | `0x7c852118294e51e653712a81e05800f419141751be58f605c371e15141b007a6` | 10,000 ETH | -| 4 | `0x15d34AAf54267DB7D7c367839AAf71A00a2C6A65` | `0x47e179ec197488593b187f80a00eb0da91f1b9d0b13f8733639f19c30a34926a` | 10,000 ETH | -| 5 | `0x9965507D1a55bcC2695C58ba16FB37d819B0A4dc` | `0x8b3a350cf5c34c9194ca85829a2df0ec3153be0318b5e2d3348e872092edffba` | 10,000 ETH | -| 6 | `0x976EA74026E726554dB657fA54763abd0C3a0aa9` | `0x92db14e403b83dfe3df233f83dfa3a0d7096f21ca9b0d6d6b8d88b2b4ec1564e` | 10,000 ETH | -| 7 | `0x14dC79964da2C08b23698B3D3cc7Ca32193d9955` | `0x4bbbf85ce3377467afe5d46f804f221813b2bb87f24d81f60f1fcdbf7cbf4356` | 10,000 ETH | -| 8 | `0x23618e81E3f5cdF7f54C3d65f7FBc0aBf5B21E8f` | `0xdbda1821b80551c9d65939329250298aa3472ba22feea921c0cf5d620ea67b97` | 10,000 ETH | -| 9 | `0xa0Ee7A142d267C1f36714E4a8F75612F20a79720` | `0x2a871d0798f97d79848a013d4936a73bf4cc922c825d33c1cf7073dff6d409c6` | 10,000 ETH | - -## 🛠️ Usage - -### Hardhat Configuration - -```typescript -// hardhat.config.ts -networks: { - arcnetwork: { - url: "http://localhost:8545", - chainId: 1337, - accounts: [ - "0xac0974bec39a17e36ba4a6b4d238ff944bacb478cbed5efcae784d7bf4f2ff80", - "0x59c6995e998f97a5a0044966f0945389dc9e86dae88c7a8412f4603b6b78690d", - // ... rest of the accounts - ] - } -} -``` - -### Local Development Mode - -```bash -# Start Arc node in local development mode -./scripts/localdev.mjs start -``` - -### Anvil (Foundry) - -```bash -# Start Anvil with default accounts -anvil -``` - -### MetaMask Setup - -To use these accounts in MetaMask: - -1. **Add Network:** - - Network Name: `Arc (Dev)` - - RPC URL: `http://localhost:8545` - - Chain ID: `1337` - - Currency Symbol: `USDC` - -2. **Import Account:** - - Copy any private key from the table above - - In MetaMask: Account Menu → Import Account - - Paste the private key - -## ⚠️ Security Warning - -**NEVER use these accounts in production!** - -- These private keys are publicly known -- They are only safe for local development -- Anyone can access funds sent to these addresses on mainnet -- Use separate, secure accounts for any real value - -## 🤝 Compatibility - -These accounts are compatible with: -- ✅ Hardhat Network -- ✅ Anvil (Foundry) -- ✅ MetaMask -- ✅ Most Ethereum development tools - -## 📚 References - -- [Hardhat Network Configuration](https://hardhat.org/hardhat-network/docs/overview) -- [Anvil Documentation](https://book.getfoundry.sh/anvil/) -- [MetaMask Development Network Setup](https://docs.metamask.io/wallet/how-to/run-devnet/) diff --git a/BREAKING_CHANGES.md b/BREAKING_CHANGES.md deleted file mode 100644 index cb2ff0b..0000000 --- a/BREAKING_CHANGES.md +++ /dev/null @@ -1,108 +0,0 @@ -# Breaking Changes - -Records breaking changes between tagged public releases of [`arc-node`](https://github.com/circlefin/arc-node). - -Each bullet is prefixed with a flag identifying the kind of breaking change: - -- `[CLI]` -- CLI flag added, renamed, removed, or made required. -- `[Config]` -- default value, environment variable, or manifest field change. -- `[Format]` -- log, metric label, or serialized output format change that breaks parsers. - -Entries are split by audience. A change appears under `### For Validators` when validator-mode operation must change; otherwise it appears under `### For Node Operators`. A change requiring both audiences to act appears in both sections (rare). - -Compare and release-notes links resolve once the corresponding tag is published at [`circlefin/arc-node`](https://github.com/circlefin/arc-node). - -## [v0.7.2] - -**Changes:** [v0.7.1...v0.7.2](https://github.com/circlefin/arc-node/compare/v0.7.1...v0.7.2) -- [release notes](https://github.com/circlefin/arc-node/releases/tag/v0.7.2) - -*Note: testnet node operators must use v0.7.2 before timestamp `1781791200` (2026-06-18 14:00:00 UTC), when Zero7 activates on testnet. Earlier versions are not supported.* - -### For Node Operators - -- **[Config] `arc-node-execution`: JSON-RPC gas cap default lowered.** - - Old (`v0.7.1`): `--rpc.gascap` default `50000000` (Reth stock default). - - New (`v0.7.2`): `--rpc.gascap` default `30000000`. - - `eth_call` and `eth_estimateGas` requests that need more than 30M gas now fail with a gas-cap error. Pass `--rpc.gascap 50000000` (or higher) to restore the previous budget. Operators who never set the flag and do not rely on calls above 30M gas are unaffected. - -- **[CLI] `arc-node-execution`: replay-unprotected (pre-EIP-155) transactions are rejected over JSON-RPC by default.** - - Old (`v0.7.1`): pre-EIP-155 (replay-unprotected) transactions were accepted over JSON-RPC. - - New (`v0.7.2`): they are rejected by default with "only replay-protected (EIP-155) transactions allowed over RPC". - - Adds `--arc.rpc.allow-unprotected-txs` (default `false`); set it to accept legacy unprotected transactions over RPC. - -- **[Config] `arc-node-execution`: JSON-RPC batch requests are capped.** - - Old (`v0.7.1`): no limit on the number of entries in a JSON-RPC batch request. - - New (`v0.7.2`): `--arc.rpc.max-batch-entries` defaults to `100`; oversized batches are rejected with JSON-RPC error `-32600` before any per-entry handler runs. A value of `0` is rejected so the cap cannot be silently disabled. - - Operators whose tooling submits larger batches must raise `--arc.rpc.max-batch-entries `. - -- **[Config] `arc-node-execution`: the invalid-transaction list is enabled by default.** - - Old (`v0.7.1`): `--invalid-tx-list-enable` default `false`. - - New (`v0.7.2`): default `true`. On a payload-builder panic, all pending transactions are added to the list and removed from the mempool; resubmit them after investigating the panic. - - Opt out with `--invalid-tx-list-enable=false`. - -## [v0.7.1] - -**Changes:** [v0.7.0...v0.7.1](https://github.com/circlefin/arc-node/compare/v0.7.0...v0.7.1) -- [release notes](https://github.com/circlefin/arc-node/releases/tag/v0.7.1) - -*Note: testnet node operators must use v0.7.1 before timestamp `1779894517` (2026-05-27 15:08:37 UTC), when Zero5/Zero6 activate on testnet. Earlier versions are not supported.* - -### For Node Operators - -- **[Config] `arc-node-execution`: EL RPC connection defaults tightened.** - - `--rpc.max-connections` default: `500` -> `250`. - - `--rpc.max-subscriptions-per-connection` default: `1024` -> `32`. - - Both flags remain accepted on `arc-node-execution`; operators that need the previous behavior must pass them explicitly. The new defaults bound a WebSocket subscription fan-out memory pressure path; real-world clients typically multiplex around five subscriptions per socket and are unaffected. - -## [v0.7.0] - -**Changes:** [v0.6.0...v0.7.0](https://github.com/circlefin/arc-node/compare/v0.6.0...v0.7.0) -- [release notes](https://github.com/circlefin/arc-node/releases/tag/v0.7.0) - -*Note: mainnet node operators must use v0.7.0. Earlier versions are not supported.* - -### For Node Operators - -- **[CLI] `arc-node-execution`: pending-tx flag rename and default flip.** - - Old (`v0.6.0`): `--arc.hide-pending-txs` (opt-in to hide, default exposed). - - New (`v0.7.0`): `--arc.expose-pending-txs` (opt-in to expose, default hidden). - - Adds `--public-api`, a convenience flag for externally-exposed nodes that forces hiding and warns if `--http.api` / `--ws.api` expose namespaces outside `{eth, net, web3, rpc}`. - - Nodes that relied on the default exposure must now pass `--arc.expose-pending-txs` or adopt the new secure-by-default behavior. - -- **[Config] `arc-node-consensus`: `--execution-persistence-backpressure-threshold=0` is rejected at startup.** - - Old (`v0.6.0`): `0` was accepted and caused indefinite stalling. - - New (`v0.7.0`): the value must be `> 0`; the CL refuses to start otherwise. - - Backpressure trigger semantics also changed: the gap now triggers when it *reaches* the threshold (previously *exceeds*). - - The default (`16`) is unchanged. Only operators who set this flag explicitly to `0` (now rejected) or who monitor the exact threshold value need to act. - -- **[Config] CL default `--log-level` changed from `debug` to `info`.** - Not a config syntax change, but a behavior change that affects log volume and content. Pass `--log-level debug` explicitly if your tooling depends on debug-level output. - -- **[Format] libp2p protocol identifiers on mainnet are Arc-branded.** - - The CL on mainnet (chain id `5042`) advertises Arc-branded libp2p protocol IDs from v0.7.0. A pre-v0.7.0 CL **cannot** peer with a v0.7.0 CL on mainnet. - - Operators must upgrade all mainnet nodes before or simultaneously with the v0.7.0 rollout; staged rollouts that leave a subset of nodes on `v0.6.x` will fragment the mainnet mesh. - - Testnet (`5042002`) protocol IDs are unchanged in this release. - -- **[Format] Address and public-key rendering uniformly switched to `0x`-prefixed lowercase hex.** - - Logs, metrics, and JSON-RPC responses now use a single canonical format (signatures continue to use Base64). EIP-55 checksums are not used; Prometheus labels are case-sensitive. - - Log parsers, alerting rules, and dashboards built against the previous mixed formats (EIP-55 checksummed, non-prefixed hex, etc.) must be updated. - -### For Validators - -- **[CLI] `arc-node-consensus`: `--validator` is required for block signing and voting.** - - The CL now runs as a non-voting full node unless `--validator` is explicitly set. - - The flag did not exist in `v0.6.0`. Validator operators upgrading from `v0.6.0` must add `--validator` to their startup command or they will stop participating in consensus. - -- **[CLI] `arc-node-consensus`: `--suggested-fee-recipient` is required when `--validator` is set.** - - Enforced at startup. Omitting the recipient with `--validator` set causes the binary to refuse to start. - - **Important**: this address is where block rewards (tx fees, in USDC) collect after successful proposals are made. - - Example: - - ``` - arc-node-consensus start \ - --validator \ - --suggested-fee-recipient 0xYOUR_ADDRESS \ - ... - ``` - -## [v0.6.0] - -Baseline -- initial public open-source release. Treat the [`v0.6.0`](https://github.com/circlefin/arc-node/releases/tag/v0.6.0) tag as the reference point for subsequent breaking-change notes. No breaking-change entries are recorded for this release. diff --git a/CHANGELOG.md b/CHANGELOG.md deleted file mode 100644 index 515b699..0000000 --- a/CHANGELOG.md +++ /dev/null @@ -1,169 +0,0 @@ -# Changelog - -All notable changes to arc-node are documented in this file. - -## [v0.7.2] - -**Changes:** [v0.7.1...v0.7.2](https://github.com/circlefin/arc-node/compare/v0.7.1...v0.7.2) -- [release notes](https://github.com/circlefin/arc-node/releases/tag/v0.7.2) - -*Note: testnet node operators must use v0.7.2 before timestamp `1781791200` (2026-06-18 14:00:00 UTC), when Zero7 activates on testnet. Earlier versions are not supported.* - -### For Node Operators - -- **[Config] EL JSON-RPC gas cap default lowered to 30M.** `--rpc.gascap` now defaults to `30000000` (previously `50000000`). `eth_call` and `eth_estimateGas` requests that need more than 30M gas now fail unless the cap is raised. Operators who never set `--rpc.gascap` and do not rely on calls above 30M gas are unaffected; pass `--rpc.gascap ` to restore a larger budget. See [BREAKING_CHANGES.md](./BREAKING_CHANGES.md#v072) for migration details. -- **[CLI] Replay-unprotected (pre-EIP-155) transactions are rejected over JSON-RPC by default.** The new `--arc.rpc.allow-unprotected-txs` flag defaults to `false`; raw transaction submission returns "only replay-protected (EIP-155) transactions allowed over RPC", matching Geth. Operators that must accept legacy unprotected transactions over RPC pass `--arc.rpc.allow-unprotected-txs`. See [BREAKING_CHANGES.md](./BREAKING_CHANGES.md#v072) for migration details. -- **[Config] JSON-RPC batch requests are capped at 100 entries by default.** The new `--arc.rpc.max-batch-entries` flag defaults to `100`; batches with more entries are rejected with JSON-RPC error `-32600` before any per-entry handler runs, and `0` is rejected so the cap cannot be silently disabled. Operators whose tooling sends larger batches raise `--arc.rpc.max-batch-entries `. See [BREAKING_CHANGES.md](./BREAKING_CHANGES.md#v072) for migration details. -- **[Config] The invalid-transaction list is enabled by default.** `--invalid-tx-list-enable` now defaults to `true` (previously `false`). On a payload-builder panic, all pending transactions are added to the list and removed from the mempool; resubmit them after investigating. Opt out with `--invalid-tx-list-enable=false`. See [BREAKING_CHANGES.md](./BREAKING_CHANGES.md#v072) for migration details. - -### Fixes - -- [CL] Stop stale proposal streams from blocking live proposals -- [EL] Preserve cold account state when evaluating the SELFDESTRUCT beneficiary check -- [EL] Charge gas before performing storage I/O in precompile helpers -- [EL] Fail closed in the SELFDESTRUCT handler when a blocklist read fails - -### Docs - -Full documentation tree at this release: [`arc-node` v0.7.2 docs](https://github.com/circlefin/arc-node/tree/v0.7.2/docs). New or updated topics in this release: - -- Add an RPC provider node section to the public node-operation guide - -## [v0.7.1] - -**Changes:** [v0.7.0...v0.7.1](https://github.com/circlefin/arc-node/compare/v0.7.0...v0.7.1) -- [release notes](https://github.com/circlefin/arc-node/releases/tag/v0.7.1) - -*Note: testnet node operators must use v0.7.1 before timestamp `1779894517` (2026-05-27 15:08:37 UTC), when Zero5/Zero6 activate on testnet. Earlier versions are not supported.* - -### For Node Operators - -- **[Config] EL RPC connection defaults tightened.** `--rpc.max-connections` default lowered from `500` to `250`; `--rpc.max-subscriptions-per-connection` default lowered from `1024` to `32`. Operators running tooling that opens many concurrent WebSocket connections, or that subscribes more than 32 times on a single connection, must raise these explicitly on the `arc-node-execution` command line. See [BREAKING_CHANGES.md](./BREAKING_CHANGES.md#v071) for migration details. - -### Features - -- [Shared] Enable global keccak cache and asm-backed keccak -- [Spammer] Expose per-run telemetry from the spammer - -### Fixes - -- [EL] Avoid double-hashing initCode on CREATE2 with non-zero value -- [EL] Activate testnet Zero5/Zero6 by timestamp instead of block height to preserve fork-id compatibility across mixed-version peers - -## [v0.7.0] - -**Changes:** [v0.6.0...v0.7.0](https://github.com/circlefin/arc-node/compare/v0.6.0...v0.7.0) -- [release notes](https://github.com/circlefin/arc-node/releases/tag/v0.7.0) - -### For Node Operators - -*Note: mainnet node operators must use v0.7.0. Earlier versions are not supported.* - -- **[Config] Pending transactions are hidden from RPC by default.** Renamed `--arc.hide-pending-txs` (opt-in to hide) to `--arc.expose-pending-txs` (opt-in to expose) and flipped the default. Added `--public-api`, a convenience flag for externally-exposed nodes that forces hiding and warns if `--http.api` / `--ws.api` expose namespaces outside `{eth, net, web3, rpc}`. -- **[Config] CL default log level changed from `debug` to `info`.** Pass `--log-level debug` explicitly if your monitoring depends on debug-level output. -- **[Config] `--follow` no longer requires `--follow.endpoint` for standard chains.** The CL resolves a default RPC endpoint from the chain id at startup; run `arc-node-consensus start --help` for the per-chain defaults. Explicit `--follow.endpoint` still takes precedence. -- **[CLI] New `--txpool.rebroadcast-interval` flag (EL).** Periodic re-announcement of pending transactions to peers (default `60` seconds, `0` to disable). Recovers from missed gossip announcements. -- **[CLI] New `--pprof.heap-prof` flag (EL and CL).** Enables jemalloc heap profiling on demand when built with `--features pprof`. Heap profiling is now inactive by default. -- **[Config] `--execution-persistence-backpressure-threshold` must be greater than zero** and triggers when the gap *reaches* the threshold (previously *exceeds*). Default is `16` and is unchanged; operators who never set this flag explicitly are unaffected. See [BREAKING_CHANGES.md](./BREAKING_CHANGES.md#v070) for migration details. -- **[API] New `/ready` readiness probe** and `sync_state` field on the CL `/status` endpoint. -- **[CLI] New `arc-node-consensus db rollback` command** (alias: `unwind`) for operator-driven rollback. Dry-run by default; pass `--execute` to commit. `--num-heights` and `--to-height` are mutually exclusive. -- **[Config] Arc mainnet is a named chainspec** (`--chain arc-mainnet`, chain id `5042`). - -### For Validators - -- **[CLI] `--validator` flag is required** for a CL to participate in block signing and voting. Without it, the node runs as a non-voting full node. This flag did not exist in `v0.6.0`. See [BREAKING_CHANGES.md](./BREAKING_CHANGES.md#v070). -- **[CLI] `--suggested-fee-recipient` is required when `--validator` is set.** It is required that this address be set / non-zero. - - ``` - arc-node-consensus start \ - --validator \ - --suggested-fee-recipient 0xYOUR_ADDRESS \ - ... - ``` - -- **[Format] Equivocation evidence log levels raised**: persistence failures promoted from `warn` to `error`, successful persistence from `info` to `warn`. Both include validator addresses for forensics. -- **[API] Validator public key exposed in the CL `/status` endpoint.** -- **[Format] Address and public-key rendering uniformly switched to `0x`-prefixed lowercase hex.** Logs, metrics, and JSON-RPC responses use this single canonical format (signatures continue to use Base64). Tooling that parsed EIP-55 checksummed addresses or non-prefixed hex must be updated. - -### Features - -- [CL] Add `--validator` configuration flag -- [CL] Require `--suggested-fee-recipient` when `--validator` is set -- [CL] Resolve default follow endpoint from chain id -- [CL] Add `/ready` readiness probe and `sync_state` to `/status` -- [CL] Add `db rollback` command (alias: `unwind`) for operator-driven rollback -- [CL] Raise equivocation evidence log levels -- [CL] Add versioned wire encoding for consensus network messages -- [CL] Harden validator-set decoding against malformed public keys -- [CL] Count and log invalid payloads across all storage paths -- [CL] Model consensus fork history; narrow `ForkCondition` to height-only -- [CL] Use Arc-branded libp2p protocol names on mainnet; see [BREAKING_CHANGES.md](./BREAKING_CHANGES.md#v070) for cross-version peering implications -- [CL] Detect EL crashes over IPC and log a diagnostic instead of silently stalling -- [EL] Implement **Zero7** hardfork: `CallFrom` subcall precompile, `Multicall3From`, `Memo` -- [EL] Apply EIP-2929 warm/cold pricing to precompile account loads; see [BREAKING_CHANGES.md](./BREAKING_CHANGES.md#v070) for the gas-estimation impact -- [EL] Add periodic transaction rebroadcast to recover from missed gossip -- [EL] Unconditionally use validator-provided beneficiary addresses -- [EL] Apply `0xef` non-deployable prefix (EIP-3541) to Arc precompile addresses in genesis, preventing EOAs or contracts from being deployed at those addresses -- [EL] EEST fixture runner for EVM spec test validation -- [EL] Register `arc-mainnet` as a named chainspec (Zero3-Zero6 active at block `0`) -- [EL] Finalize mainnet genesis with USDC admin roles, denylist, prefunded ops wallet -- [Shared] Add `--pprof.heap-prof` flag for on-demand heap profiling -- [Shared] Uniformize address and key rendering to `0x`-prefixed lowercase hex -- [Contracts] ProtocolConfig upgrade scripts; remove `rewardBeneficiary` field (proposer-provided fee recipient is authoritative) -- [Contracts] Deploy denylist contract on testnet (mainnet ships it pre-deployed in genesis) -- [Quake] Testnet orchestrator improvements: web topology viewer, node-group support in `load` / `spam`, mesh/health/performance/sanity test runner with report generation, manifest fields for EL/CL CPU and memory limits and `block_gas_limit` -- [Bench] `arc-engine-bench` with IPC and RPC engine transports -- [Bench] Nightly engine bench workflow -- [Spammer] Cache gas estimates for ERC-20 and Guzzler transactions -- [Spammer] Reuse with parallel nonce resync and reduced request timeout -- [Spammer] Improved send-stall visibility - -### Fixes - -- [CL] Prevent stream eviction by colluding validators -- [CL] Propagate `pol_round` as `valid_round` in assembled blocks -- [CL] On restream, look up block by hash and preserve `round` / `valid_round` -- [CL] Fetch validator set at `certificate_height - 1` in `get_certificate_info` -- [CL] Align `RemoteSigningProvider` Ed25519 verification with Malachite -- [CL] Bound repeated proto fields to prevent unbounded allocation -- [CL] Use checked arithmetic in `total_voting_power()` -- [CL] Account for EL earliest block in `GetHistoryMinHeight` -- [CL] Skip persistence wait during sync when block is already present or height decided -- [CL] Acknowledge `AppMsg::Decided` so sync advertises a new tip -- [CL] Mark undecided block `Invalid` on engine validation errors; persist verdict -- [CL] Surface duplicate Init/Fin proposal parts as `InsertResult::Invalid` -- [CL] Improve EL/CL height-mismatch error with actionable guidance -- [CL] Update backpressure semantics -- [EL] Suppress pool-based pending-tx leaks in RPC middleware -- [EL] Charge EIP-2929 cold account access cost in `CallFrom` subcalls -- [EL] Blocklist SLOADs are unmetered on native value transfers -- [EL] Consume all gas for subcall in static context -- [EL] Charge gas for subcall completion phase -- [EL] Strictly decode ABI parameters in precompiles -- [EL] Implement EIP-2200 sentry for `SSTORE` -- [EL] Apply new-account surcharge via precompiles -- [EL] Extend early-revert penalty to auth reverts in Zero6 -- [EL] Drop redundant `SLOAD` charge in `storeGasValuesCall` under Zero6 -- [EL] Align `totalSupply` input validation with other precompiles -- [EL] Revert child state when subcall precompile rejects -- [EL] Resolve EIP-7702 delegation when loading subcall target bytecode -- [EL] Check EIP-7702 authorization-list authorities against the denylist -- [EL] `DenylistedAddressError` should not penalize peers -- [EL] Include base fee in payload builder fee totals -- [EL] Use checked arithmetic for cumulative gas accounting in payload builder -- [EL] Panic on missing subcall continuation instead of reverting -- [Shared] Remediate cargo audit advisories -- [Contracts] Capture `Multicall3From` precompile reverts instead of propagating -- [Quake] Validate manifest flags against consensus binary CLI struct; decouple monitoring lifecycle from `clean` and `restart` -- [Spammer] Lift gas-fee caps above testnet base-fee ceiling -- [Spammer] Fix raw tx encoding, TCP backpressure drain, zero-latency warning - -### Docs - -Full documentation tree at this release: [`arc-node` v0.7.0 docs](https://github.com/circlefin/arc-node/tree/v0.7.0/docs). New or updated topics in this release: - -- Add Docker instructions for running an Arc node -- Add single-host monitoring guide for Arc EL + CL - -## [v0.6.0] - -**Released:** 2026-04-08 -- [release notes](https://github.com/circlefin/arc-node/releases/tag/v0.6.0) - -Initial public open-source release of `arc-node`. Baseline for subsequent changelog entries. diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md deleted file mode 100644 index 3297242..0000000 --- a/CONTRIBUTING.md +++ /dev/null @@ -1,31 +0,0 @@ -# Contributing to Arc Node - -## Working with Protocol Buffers - -This project uses Protocol Buffers for consensus and node communication (except consensus-critical serialization). Proto definitions are located in `crates/types/proto` and `crates/remote-signer/proto`. We use [buf](https://buf.build/) to lint, format, and check for breaking changes in our proto files. - -> **Prerequisite:** `buf` must be installed before using these targets. See [Prerequisites](README.md#prerequisites) for installation instructions. - -### Available Make Targets - -- `make buf-lint` - Lint protobuf files to ensure they follow best practices -- `make buf-format` - Format protobuf files (this is included in `make lint`) -- `make buf-breaking` - Check for breaking changes against the main branch - -### Before Committing Changes - -If you modify any `.proto` files, always run `make buf-lint` and `make buf-breaking` to ensure your changes don't introduce linting issues or breaking changes. The `buf-breaking` command compares your changes against the main branch to detect any backwards-incompatible modifications. Breaking changes should be carefully reviewed and documented as they can impact existing deployments. - -### CI - -CI action runs the breaking change detection step on every pull request. To skip this step for a specific pull request, you can add the `buf skip breaking` label to the PR. See [Skip breaking change detection using labels](https://buf.build/docs/bsr/ci-cd/github-actions/#skip-breaking-change-detection-using-labels). - -Note: `make lint` automatically runs `buf-format`. - -### (Optional) Pre-commit hooks - -Developers may install [pre-commit](https://pre-commit.com/) hooks, which will handle all the formatting and linting automatically. - -```bash -pre-commit install -``` diff --git a/COPYRIGHT b/COPYRIGHT deleted file mode 100644 index e2000da..0000000 --- a/COPYRIGHT +++ /dev/null @@ -1,15 +0,0 @@ -Copyright (c) 2026, Circle Internet Group, Inc. All rights reserved. - -SPDX-License-Identifier: Apache-2.0 - -Licensed under the Apache License, Version 2.0 (the "License"); -you may not use this file except in compliance with the License. -You may obtain a copy of the License at - - http://www.apache.org/licenses/LICENSE-2.0 - -Unless required by applicable law or agreed to in writing, software -distributed under the License is distributed on an "AS IS" BASIS, -WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -See the License for the specific language governing permissions and -limitations under the License. diff --git a/Cargo.lock b/Cargo.lock deleted file mode 100644 index 47d7021..0000000 --- a/Cargo.lock +++ /dev/null @@ -1,16339 +0,0 @@ -# This file is automatically @generated by Cargo. -# It is not intended for manual editing. -version = 4 - -[[package]] -name = "addr2line" -version = "0.25.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1b5d307320b3181d6d7954e663bd7c774a838b8220fe0593c86d9fb09f498b4b" -dependencies = [ - "gimli", -] - -[[package]] -name = "adler2" -version = "2.0.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "320119579fcad9c21884f5c4861d16174d0e06250625266f50fe6898340abefa" - -[[package]] -name = "advisory-lock" -version = "0.3.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a6caee7d48f930f9ad3fc9546f8cbf843365da0c5b0ca4eee1d1ac3dd12d8f93" -dependencies = [ - "libc", - "winapi", -] - -[[package]] -name = "aead" -version = "0.5.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d122413f284cf2d62fb1b7db97e02edb8cda96d769b16e443a4f6195e35662b0" -dependencies = [ - "crypto-common 0.1.7", - "generic-array", -] - -[[package]] -name = "aes" -version = "0.8.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b169f7a6d4742236a0a00c541b845991d0ac43e546831af1249753ab4c3aa3a0" -dependencies = [ - "cfg-if", - "cipher", - "cpufeatures 0.2.17", -] - -[[package]] -name = "aes-gcm" -version = "0.10.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "831010a0f742e1209b3bcea8fab6a8e149051ba6099432c8cb2cc117dec3ead1" -dependencies = [ - "aead", - "aes", - "cipher", - "ctr", - "ghash", - "subtle", -] - -[[package]] -name = "ahash" -version = "0.8.12" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5a15f179cd60c4584b8a8c596927aadc462e27f2ca70c04e0071964a73ba7a75" -dependencies = [ - "cfg-if", - "getrandom 0.3.4", - "once_cell", - "version_check", - "zerocopy", -] - -[[package]] -name = "aho-corasick" -version = "1.1.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ddd31a130427c27518df266943a5308ed92d4b226cc639f5a8f1002816174301" -dependencies = [ - "memchr", -] - -[[package]] -name = "aligned-vec" -version = "0.6.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "dc890384c8602f339876ded803c97ad529f3842aba97f6392b3dba0dd171769b" -dependencies = [ - "equator", -] - -[[package]] -name = "alloc-no-stdlib" -version = "2.0.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cc7bb162ec39d46ab1ca8c77bf72e890535becd1751bb45f64c597edb4c8c6b3" - -[[package]] -name = "alloc-stdlib" -version = "0.2.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "94fb8275041c72129eb51b7d0322c29b8387a0386127718b096429201a5d6ece" -dependencies = [ - "alloc-no-stdlib", -] - -[[package]] -name = "allocator-api2" -version = "0.2.21" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "683d7910e743518b0e34f1186f92494becacb047c7b6bf616c96772180fef923" - -[[package]] -name = "alloy-chains" -version = "0.2.30" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "90f374d3c6d729268bbe2d0e0ff992bb97898b2df756691a62ee1d5f0506bc39" -dependencies = [ - "alloy-primitives", - "alloy-rlp", - "arbitrary", - "num_enum", - "proptest", - "serde", - "strum 0.27.2", -] - -[[package]] -name = "alloy-consensus" -version = "1.7.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b0c0dc44157867da82c469c13186015b86abef209bf0e41625e4b68bac61d728" -dependencies = [ - "alloy-eips", - "alloy-primitives", - "alloy-rlp", - "alloy-serde", - "alloy-trie", - "alloy-tx-macros", - "arbitrary", - "auto_impl", - "borsh", - "c-kzg", - "derive_more", - "either", - "k256", - "once_cell", - "rand 0.8.5", - "secp256k1 0.30.0", - "serde", - "serde_json", - "serde_with", - "thiserror 2.0.18", -] - -[[package]] -name = "alloy-consensus-any" -version = "1.7.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ba4cdb42df3871cd6b346d6a938ec2ba69a9a0f49d1f82714bc5c48349268434" -dependencies = [ - "alloy-consensus", - "alloy-eips", - "alloy-primitives", - "alloy-rlp", - "alloy-serde", - "arbitrary", - "serde", -] - -[[package]] -name = "alloy-dyn-abi" -version = "1.5.7" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cc2db5c583aaef0255aa63a4fe827f826090142528bba48d1bf4119b62780cad" -dependencies = [ - "alloy-json-abi", - "alloy-primitives", - "alloy-sol-type-parser", - "alloy-sol-types", - "derive_more", - "itoa", - "serde", - "serde_json", - "winnow", -] - -[[package]] -name = "alloy-eip2124" -version = "0.2.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "741bdd7499908b3aa0b159bba11e71c8cddd009a2c2eb7a06e825f1ec87900a5" -dependencies = [ - "alloy-primitives", - "alloy-rlp", - "arbitrary", - "crc", - "rand 0.8.5", - "serde", - "thiserror 2.0.18", -] - -[[package]] -name = "alloy-eip2930" -version = "0.2.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9441120fa82df73e8959ae0e4ab8ade03de2aaae61be313fbf5746277847ce25" -dependencies = [ - "alloy-primitives", - "alloy-rlp", - "arbitrary", - "borsh", - "rand 0.8.5", - "serde", -] - -[[package]] -name = "alloy-eip7702" -version = "0.6.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2919c5a56a1007492da313e7a3b6d45ef5edc5d33416fdec63c0d7a2702a0d20" -dependencies = [ - "alloy-primitives", - "alloy-rlp", - "arbitrary", - "borsh", - "k256", - "rand 0.8.5", - "serde", - "serde_with", - "thiserror 2.0.18", -] - -[[package]] -name = "alloy-eip7928" -version = "0.3.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d3231de68d5d6e75332b7489cfcc7f4dfabeba94d990a10e4b923af0e6623540" -dependencies = [ - "alloy-primitives", - "alloy-rlp", - "arbitrary", - "borsh", - "serde", -] - -[[package]] -name = "alloy-eips" -version = "1.7.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b9f7ef09f21bd1e9cb8a686f168cb4a206646804567f0889eadb8dcc4c9288c8" -dependencies = [ - "alloy-eip2124", - "alloy-eip2930", - "alloy-eip7702", - "alloy-eip7928", - "alloy-primitives", - "alloy-rlp", - "alloy-serde", - "arbitrary", - "auto_impl", - "borsh", - "c-kzg", - "derive_more", - "either", - "ethereum_ssz 0.9.1", - "ethereum_ssz_derive 0.9.1", - "serde", - "serde_with", - "sha2 0.10.9", - "thiserror 2.0.18", -] - -[[package]] -name = "alloy-evm" -version = "0.27.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d2ccfe6d724ceabd5518350cfb34f17dd3a6c3cc33579eee94d98101d3a511ff" -dependencies = [ - "alloy-consensus", - "alloy-eips", - "alloy-hardforks", - "alloy-primitives", - "alloy-rpc-types-engine", - "alloy-rpc-types-eth", - "alloy-sol-types", - "auto_impl", - "derive_more", - "op-alloy", - "op-revm", - "revm", - "thiserror 2.0.18", -] - -[[package]] -name = "alloy-genesis" -version = "1.7.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7c9cf3b99f46615fbf7dc1add0c96553abb7bf88fc9ec70dfbe7ad0b47ba7fe8" -dependencies = [ - "alloy-eips", - "alloy-primitives", - "alloy-serde", - "alloy-trie", - "borsh", - "serde", - "serde_with", -] - -[[package]] -name = "alloy-hardforks" -version = "0.4.7" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "83ba208044232d14d4adbfa77e57d6329f51bc1acc21f5667bb7db72d88a0831" -dependencies = [ - "alloy-chains", - "alloy-eip2124", - "alloy-primitives", - "auto_impl", - "dyn-clone", - "serde", -] - -[[package]] -name = "alloy-json-abi" -version = "1.5.7" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e9dbe713da0c737d9e5e387b0ba790eb98b14dd207fe53eef50e19a5a8ec3dac" -dependencies = [ - "alloy-primitives", - "alloy-sol-type-parser", - "serde", - "serde_json", -] - -[[package]] -name = "alloy-json-rpc" -version = "1.7.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ff42cd777eea61f370c0b10f2648a1c81e0b783066cd7269228aa993afd487f7" -dependencies = [ - "alloy-primitives", - "alloy-sol-types", - "http", - "serde", - "serde_json", - "thiserror 2.0.18", - "tracing", -] - -[[package]] -name = "alloy-network" -version = "1.7.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8cbca04f9b410fdc51aaaf88433cbac761213905a65fe832058bcf6690585762" -dependencies = [ - "alloy-consensus", - "alloy-consensus-any", - "alloy-eips", - "alloy-json-rpc", - "alloy-network-primitives", - "alloy-primitives", - "alloy-rpc-types-any", - "alloy-rpc-types-eth", - "alloy-serde", - "alloy-signer", - "alloy-sol-types", - "async-trait", - "auto_impl", - "derive_more", - "futures-utils-wasm", - "serde", - "serde_json", - "thiserror 2.0.18", -] - -[[package]] -name = "alloy-network-primitives" -version = "1.7.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "42d6d15e069a8b11f56bef2eccbad2a873c6dd4d4c81d04dda29710f5ea52f04" -dependencies = [ - "alloy-consensus", - "alloy-eips", - "alloy-primitives", - "alloy-serde", - "serde", -] - -[[package]] -name = "alloy-primitives" -version = "1.5.7" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "de3b431b4e72cd8bd0ec7a50b4be18e73dab74de0dba180eef171055e5d5926e" -dependencies = [ - "alloy-rlp", - "arbitrary", - "bytes", - "cfg-if", - "const-hex", - "derive_more", - "fixed-cache", - "foldhash 0.2.0", - "getrandom 0.4.1", - "hashbrown 0.16.1", - "indexmap 2.13.0", - "itoa", - "k256", - "keccak-asm", - "paste", - "proptest", - "proptest-derive", - "rand 0.9.4", - "rapidhash", - "ruint", - "rustc-hash", - "serde", - "sha3 0.10.8", -] - -[[package]] -name = "alloy-provider" -version = "1.7.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d181c8cc7cf4805d7e589bf4074d56d55064fa1a979f005a45a62b047616d870" -dependencies = [ - "alloy-chains", - "alloy-consensus", - "alloy-eips", - "alloy-json-rpc", - "alloy-network", - "alloy-network-primitives", - "alloy-primitives", - "alloy-pubsub", - "alloy-rpc-client", - "alloy-rpc-types-debug", - "alloy-rpc-types-eth", - "alloy-rpc-types-trace", - "alloy-rpc-types-txpool", - "alloy-signer", - "alloy-sol-types", - "alloy-transport", - "alloy-transport-http", - "alloy-transport-ipc", - "alloy-transport-ws", - "async-stream", - "async-trait", - "auto_impl", - "dashmap", - "either", - "futures", - "futures-utils-wasm", - "lru 0.16.3", - "parking_lot", - "pin-project", - "reqwest", - "serde", - "serde_json", - "thiserror 2.0.18", - "tokio", - "tracing", - "url", - "wasmtimer", -] - -[[package]] -name = "alloy-pubsub" -version = "1.7.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e8bd82953194dec221aa4cbbbb0b1e2df46066fe9d0333ac25b43a311e122d13" -dependencies = [ - "alloy-json-rpc", - "alloy-primitives", - "alloy-transport", - "auto_impl", - "bimap", - "futures", - "parking_lot", - "serde", - "serde_json", - "tokio", - "tokio-stream", - "tower 0.5.3", - "tracing", - "wasmtimer", -] - -[[package]] -name = "alloy-rlp" -version = "0.3.13" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e93e50f64a77ad9c5470bf2ad0ca02f228da70c792a8f06634801e202579f35e" -dependencies = [ - "alloy-rlp-derive", - "arrayvec", - "bytes", -] - -[[package]] -name = "alloy-rlp-derive" -version = "0.3.13" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ce8849c74c9ca0f5a03da1c865e3eb6f768df816e67dd3721a398a8a7e398011" -dependencies = [ - "proc-macro2", - "quote", - "syn 2.0.117", -] - -[[package]] -name = "alloy-rpc-client" -version = "1.7.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f2792758a93ae32a32e9047c843d536e1448044f78422d71bf7d7c05149e103f" -dependencies = [ - "alloy-json-rpc", - "alloy-primitives", - "alloy-pubsub", - "alloy-transport", - "alloy-transport-http", - "alloy-transport-ipc", - "alloy-transport-ws", - "futures", - "pin-project", - "reqwest", - "serde", - "serde_json", - "tokio", - "tokio-stream", - "tower 0.5.3", - "tracing", - "url", - "wasmtimer", -] - -[[package]] -name = "alloy-rpc-types" -version = "1.7.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7bdcbf9dfd5eea8bfeb078b1d906da8cd3a39c4d4dbe7a628025648e323611f6" -dependencies = [ - "alloy-primitives", - "alloy-rpc-types-engine", - "alloy-rpc-types-eth", - "alloy-serde", - "serde", -] - -[[package]] -name = "alloy-rpc-types-admin" -version = "1.7.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "42325c117af3a9e49013f881c1474168db57978e02085fc9853a1c89e0562740" -dependencies = [ - "alloy-genesis", - "alloy-primitives", - "serde", - "serde_json", -] - -[[package]] -name = "alloy-rpc-types-anvil" -version = "1.7.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e0a3100b76987c1b1dc81f3abe592b7edc29e92b1242067a69d65e0030b35cf9" -dependencies = [ - "alloy-primitives", - "alloy-rpc-types-eth", - "alloy-serde", - "serde", -] - -[[package]] -name = "alloy-rpc-types-any" -version = "1.7.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "dd720b63f82b457610f2eaaf1f32edf44efffe03ae25d537632e7d23e7929e1a" -dependencies = [ - "alloy-consensus-any", - "alloy-rpc-types-eth", - "alloy-serde", -] - -[[package]] -name = "alloy-rpc-types-beacon" -version = "1.7.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4a22e13215866f5dfd5d3278f4c41f1fad9410dc68ce39022f58593c873c26f8" -dependencies = [ - "alloy-eips", - "alloy-primitives", - "alloy-rpc-types-engine", - "derive_more", - "ethereum_ssz 0.9.1", - "ethereum_ssz_derive 0.9.1", - "serde", - "serde_json", - "serde_with", - "thiserror 2.0.18", - "tree_hash", - "tree_hash_derive", -] - -[[package]] -name = "alloy-rpc-types-debug" -version = "1.7.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e1b21e1ad18ff1b31ff1030e046462ab8168cf8894e6778cd805c8bdfe2bd649" -dependencies = [ - "alloy-primitives", - "derive_more", - "serde", - "serde_with", -] - -[[package]] -name = "alloy-rpc-types-engine" -version = "1.7.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e4ac61f03f1edabccde1c687b5b25fff28f183afee64eaa2e767def3929e4457" -dependencies = [ - "alloy-consensus", - "alloy-eips", - "alloy-primitives", - "alloy-rlp", - "alloy-serde", - "arbitrary", - "derive_more", - "ethereum_ssz 0.9.1", - "ethereum_ssz_derive 0.9.1", - "jsonwebtoken", - "rand 0.8.5", - "serde", - "strum 0.27.2", -] - -[[package]] -name = "alloy-rpc-types-eth" -version = "1.7.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9b2dc411f13092f237d2bf6918caf80977fc2f51485f9b90cb2a2f956912c8c9" -dependencies = [ - "alloy-consensus", - "alloy-consensus-any", - "alloy-eips", - "alloy-network-primitives", - "alloy-primitives", - "alloy-rlp", - "alloy-serde", - "alloy-sol-types", - "arbitrary", - "itertools 0.13.0", - "serde", - "serde_json", - "serde_with", - "thiserror 2.0.18", -] - -[[package]] -name = "alloy-rpc-types-mev" -version = "1.7.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fe85bf3be739126aa593dca9fb3ab13ca93fa7873e6f2247be64d7f2cb15f34a" -dependencies = [ - "alloy-consensus", - "alloy-eips", - "alloy-primitives", - "alloy-rpc-types-eth", - "alloy-serde", - "serde", - "serde_json", -] - -[[package]] -name = "alloy-rpc-types-trace" -version = "1.7.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1ad79f1e27e161943b5a4f99fe5534ef0849876214be411e0032c12f38e94daa" -dependencies = [ - "alloy-primitives", - "alloy-rpc-types-eth", - "alloy-serde", - "serde", - "serde_json", - "thiserror 2.0.18", -] - -[[package]] -name = "alloy-rpc-types-txpool" -version = "1.7.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d459f902a2313737bc66d18ed094c25d2aeb268b74d98c26bbbda2aa44182ab0" -dependencies = [ - "alloy-primitives", - "alloy-rpc-types-eth", - "alloy-serde", - "serde", -] - -[[package]] -name = "alloy-serde" -version = "1.7.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e2ce1e0dbf7720eee747700e300c99aac01b1a95bb93f493a01e78ee28bb1a37" -dependencies = [ - "alloy-primitives", - "arbitrary", - "serde", - "serde_json", -] - -[[package]] -name = "alloy-signer" -version = "1.7.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2425c6f314522c78e8198979c8cbf6769362be4da381d4152ea8eefce383535d" -dependencies = [ - "alloy-primitives", - "async-trait", - "auto_impl", - "either", - "elliptic-curve", - "k256", - "thiserror 2.0.18", -] - -[[package]] -name = "alloy-signer-local" -version = "1.7.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c3ecb71ee53d8d9c3fa7bac17542c8116ebc7a9726c91b1bf333ec3d04f5a789" -dependencies = [ - "alloy-consensus", - "alloy-network", - "alloy-primitives", - "alloy-signer", - "async-trait", - "coins-bip32", - "coins-bip39", - "k256", - "rand 0.8.5", - "thiserror 2.0.18", - "zeroize", -] - -[[package]] -name = "alloy-sol-macro" -version = "1.5.7" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ab81bab693da9bb79f7a95b64b394718259fdd7e41dceeced4cad57cb71c4f6a" -dependencies = [ - "alloy-sol-macro-expander", - "alloy-sol-macro-input", - "proc-macro-error2", - "proc-macro2", - "quote", - "syn 2.0.117", -] - -[[package]] -name = "alloy-sol-macro-expander" -version = "1.5.7" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "489f1620bb7e2483fb5819ed01ab6edc1d2f93939dce35a5695085a1afd1d699" -dependencies = [ - "alloy-sol-macro-input", - "const-hex", - "heck", - "indexmap 2.13.0", - "proc-macro-error2", - "proc-macro2", - "quote", - "sha3 0.10.8", - "syn 2.0.117", - "syn-solidity", -] - -[[package]] -name = "alloy-sol-macro-input" -version = "1.5.7" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "56cef806ad22d4392c5fc83cf8f2089f988eb99c7067b4e0c6f1971fc1cca318" -dependencies = [ - "const-hex", - "dunce", - "heck", - "macro-string", - "proc-macro2", - "quote", - "syn 2.0.117", - "syn-solidity", -] - -[[package]] -name = "alloy-sol-type-parser" -version = "1.5.7" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a6df77fea9d6a2a75c0ef8d2acbdfd92286cc599983d3175ccdc170d3433d249" -dependencies = [ - "serde", - "winnow", -] - -[[package]] -name = "alloy-sol-types" -version = "1.5.7" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "64612d29379782a5dde6f4b6570d9c756d734d760c0c94c254d361e678a6591f" -dependencies = [ - "alloy-json-abi", - "alloy-primitives", - "alloy-sol-macro", - "serde", -] - -[[package]] -name = "alloy-transport" -version = "1.7.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fa186e560d523d196580c48bf00f1bf62e63041f28ecf276acc22f8b27bb9f53" -dependencies = [ - "alloy-json-rpc", - "auto_impl", - "base64 0.22.1", - "derive_more", - "futures", - "futures-utils-wasm", - "parking_lot", - "serde", - "serde_json", - "thiserror 2.0.18", - "tokio", - "tower 0.5.3", - "tracing", - "url", - "wasmtimer", -] - -[[package]] -name = "alloy-transport-http" -version = "1.7.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "aa501ad58dd20acddbfebc65b52e60f05ebf97c52fa40d1b35e91f5e2da0ad0e" -dependencies = [ - "alloy-json-rpc", - "alloy-transport", - "itertools 0.13.0", - "reqwest", - "serde_json", - "tower 0.5.3", - "tracing", - "url", -] - -[[package]] -name = "alloy-transport-ipc" -version = "1.7.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c2ef85688e5ac2da72afc804e0a1f153a1f309f05a864b1998bbbed7804dbaab" -dependencies = [ - "alloy-json-rpc", - "alloy-pubsub", - "alloy-transport", - "bytes", - "futures", - "interprocess", - "pin-project", - "serde", - "serde_json", - "tokio", - "tokio-util", - "tracing", -] - -[[package]] -name = "alloy-transport-ws" -version = "1.7.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b9f00445db69d63298e2b00a0ea1d859f00e6424a3144ffc5eba9c31da995e16" -dependencies = [ - "alloy-pubsub", - "alloy-transport", - "futures", - "http", - "serde_json", - "tokio", - "tokio-tungstenite 0.26.2", - "tracing", - "ws_stream_wasm", -] - -[[package]] -name = "alloy-trie" -version = "0.9.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4d7fd448ab0a017de542de1dcca7a58e7019fe0e7a34ed3f9543ebddf6aceffa" -dependencies = [ - "alloy-primitives", - "alloy-rlp", - "arbitrary", - "arrayvec", - "derive_arbitrary", - "derive_more", - "nybbles", - "proptest", - "proptest-derive", - "serde", - "smallvec", - "thiserror 2.0.18", - "tracing", -] - -[[package]] -name = "alloy-tx-macros" -version = "1.7.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6fa0c53e8c1e1ef4d01066b01c737fb62fc9397ab52c6e7bb5669f97d281b9bc" -dependencies = [ - "darling 0.21.3", - "proc-macro2", - "quote", - "syn 2.0.117", -] - -[[package]] -name = "android_system_properties" -version = "0.1.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "819e7219dbd41043ac279b19830f2efc897156490d7fd6ea916720117ee66311" -dependencies = [ - "libc", -] - -[[package]] -name = "anes" -version = "0.1.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4b46cbb362ab8752921c97e041f5e366ee6297bd428a31275b9fcf1e380f7299" - -[[package]] -name = "anstream" -version = "0.6.21" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "43d5b281e737544384e969a5ccad3f1cdd24b48086a0fc1b2a5262a26b8f4f4a" -dependencies = [ - "anstyle", - "anstyle-parse", - "anstyle-query", - "anstyle-wincon", - "colorchoice", - "is_terminal_polyfill", - "utf8parse", -] - -[[package]] -name = "anstyle" -version = "1.0.13" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5192cca8006f1fd4f7237516f40fa183bb07f8fbdfedaa0036de5ea9b0b45e78" - -[[package]] -name = "anstyle-parse" -version = "0.2.7" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4e7644824f0aa2c7b9384579234ef10eb7efb6a0deb83f9630a49594dd9c15c2" -dependencies = [ - "utf8parse", -] - -[[package]] -name = "anstyle-query" -version = "1.1.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "40c48f72fd53cd289104fc64099abca73db4166ad86ea0b4341abe65af83dadc" -dependencies = [ - "windows-sys 0.61.2", -] - -[[package]] -name = "anstyle-wincon" -version = "3.0.11" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "291e6a250ff86cd4a820112fb8898808a366d8f9f58ce16d1f538353ad55747d" -dependencies = [ - "anstyle", - "once_cell_polyfill", - "windows-sys 0.61.2", -] - -[[package]] -name = "anyhow" -version = "1.0.102" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7f202df86484c868dbad7eaa557ef785d5c66295e41b460ef922eca0723b842c" - -[[package]] -name = "aquamarine" -version = "0.6.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0f50776554130342de4836ba542aa85a4ddb361690d7e8df13774d7284c3d5c2" -dependencies = [ - "include_dir", - "itertools 0.10.5", - "proc-macro-error2", - "proc-macro2", - "quote", - "syn 2.0.117", -] - -[[package]] -name = "arbitrary" -version = "1.4.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c3d036a3c4ab069c7b410a2ce876bd74808d2d0888a82667669f8e783a898bf1" -dependencies = [ - "derive_arbitrary", -] - -[[package]] -name = "arc-checks" -version = "0.0.1" -dependencies = [ - "alloy-provider", - "alloy-rpc-types-txpool", - "arc-consensus-db", - "color-eyre", - "futures", - "futures-util", - "prometheus-parse", - "redb", - "reqwest", - "serde", - "serde_json", - "tokio", - "url", -] - -[[package]] -name = "arc-consensus-db" -version = "0.0.1" -dependencies = [ - "alloy-primitives", - "alloy-rpc-types-engine", - "arbitrary", - "arc-consensus-types", - "arc-malachitebft-app-channel", - "arc-malachitebft-core-types", - "arc-malachitebft-proto", - "bytes", - "bytesize", - "ethereum_ssz 0.9.1", - "mockall", - "prost 0.13.5", - "redb", - "tempfile", - "thiserror 2.0.18", - "tokio", - "tracing", -] - -[[package]] -name = "arc-consensus-types" -version = "0.0.1" -dependencies = [ - "alloy-consensus", - "alloy-primitives", - "alloy-rlp", - "alloy-rpc-types-engine", - "arbitrary", - "arc-malachitebft-app", - "arc-malachitebft-app-channel", - "arc-malachitebft-codec", - "arc-malachitebft-core-consensus", - "arc-malachitebft-core-types", - "arc-malachitebft-proto", - "arc-malachitebft-signing", - "arc-malachitebft-signing-ed25519", - "arc-malachitebft-sync", - "arc-shared", - "arc-signer", - "async-trait", - "bytes", - "bytesize", - "config", - "ethereum_ssz 0.9.1", - "ethereum_ssz_derive 0.9.1", - "eyre", - "humantime-serde", - "prost 0.13.5", - "prost-build 0.13.5", - "protox", - "rand 0.8.5", - "serde", - "serde_json", - "serde_with", - "sha3 0.10.8", - "signature 2.2.0", - "thiserror 2.0.18", - "tokio", - "tracing", - "url", -] - -[[package]] -name = "arc-engine-bench" -version = "0.0.1" -dependencies = [ - "alloy-genesis", - "alloy-primitives", - "alloy-rpc-types-engine", - "arc-eth-engine", - "arc-execution-config", - "arc-version", - "chrono", - "clap", - "color-eyre", - "csv", - "eyre", - "reqwest", - "reth-cli", - "serde", - "serde_json", - "tempfile", - "tokio", - "tracing", - "tracing-subscriber", -] - -[[package]] -name = "arc-eth-engine" -version = "0.0.1" -dependencies = [ - "alloy-consensus", - "alloy-eips", - "alloy-genesis", - "alloy-primitives", - "alloy-rpc-types", - "alloy-rpc-types-engine", - "alloy-rpc-types-eth", - "alloy-rpc-types-txpool", - "alloy-sol-macro", - "alloy-sol-types", - "arc-consensus-types", - "arc-evm-node", - "arc-execution-config", - "arc-execution-txpool", - "arc-malachitebft-core-types", - "arc-shared", - "async-trait", - "backon", - "ethereum_serde_utils", - "eyre", - "hex", - "jsonrpsee", - "jsonrpsee-types", - "jsonwebtoken", - "mockall", - "reqwest", - "reth-chainspec", - "reth-ipc", - "reth-node-builder", - "reth-tasks", - "rstest", - "serde", - "serde_json", - "tempfile", - "thiserror 2.0.18", - "tokio", - "tracing", - "url", - "uuid", - "wiremock", -] - -[[package]] -name = "arc-evm" -version = "0.0.1" -dependencies = [ - "alloy-consensus", - "alloy-eips", - "alloy-evm", - "alloy-genesis", - "alloy-primitives", - "alloy-rpc-types-engine", - "alloy-rpc-types-trace", - "alloy-sol-types", - "arc-execution-config", - "arc-precompiles", - "reth-chainspec", - "reth-ethereum", - "reth-ethereum-primitives", - "reth-evm", - "reth-node-api", - "reth-primitives-traits", - "revm", - "revm-context-interface", - "revm-inspectors", - "revm-interpreter", - "revm-primitives", - "rstest", - "thiserror 2.0.18", - "tracing", -] - -[[package]] -name = "arc-evm-node" -version = "0.0.1" -dependencies = [ - "alloy-consensus", - "alloy-network", - "alloy-primitives", - "alloy-rpc-types-engine", - "alloy-rpc-types-eth", - "arc-consensus-types", - "arc-evm", - "arc-execution-config", - "arc-execution-payload", - "arc-execution-txpool", - "arc-execution-validation", - "arc-version", - "async-trait", - "backon", - "eyre", - "jsonrpsee", - "reqwest", - "reth-chainspec", - "reth-engine-primitives", - "reth-ethereum", - "reth-ethereum-engine-primitives", - "reth-ethereum-payload-builder", - "reth-ethereum-primitives", - "reth-evm", - "reth-network", - "reth-node-api", - "reth-node-builder", - "reth-payload-primitives", - "reth-primitives-traits", - "reth-provider", - "reth-rpc", - "reth-rpc-api", - "reth-rpc-builder", - "reth-rpc-eth-api", - "reth-rpc-eth-types", - "reth-rpc-server-types", - "reth-tracing", - "reth-transaction-pool", - "revm", - "revm-primitives", - "serde", - "serde_json", - "serde_with", - "thiserror 2.0.18", - "tokio", - "tower 0.5.3", - "tracing", -] - -[[package]] -name = "arc-evm-specs-tests" -version = "0.1.0" -dependencies = [ - "alloy-primitives", - "alloy-rlp", - "alloy-trie", - "arc-evm", - "arc-execution-config", - "arc-version", - "clap", - "hash-db", - "plain_hasher", - "reth-evm", - "revm", - "revm-context-interface", - "revm-database", - "revm-inspector", - "revm-primitives", - "revm-statetest-types", - "serde", - "serde_json", - "thiserror 2.0.18", - "triehash", -] - -[[package]] -name = "arc-execution-config" -version = "0.0.1" -dependencies = [ - "alloy-eips", - "alloy-evm", - "alloy-genesis", - "alloy-primitives", - "alloy-serde", - "alloy-sol-types", - "arc-shared", - "clap", - "eyre", - "itertools 0.14.0", - "once_cell", - "reth-chainspec", - "reth-cli", - "reth-cli-commands", - "reth-ethereum", - "reth-ethereum-forks", - "reth-ethereum-primitives", - "reth-evm", - "reth-network-peers", - "reth-node-core", - "reth-primitives-traits", - "revm", - "revm-primitives", - "serde", - "serde_json", - "thiserror 2.0.18", -] - -[[package]] -name = "arc-execution-e2e" -version = "0.0.1" -dependencies = [ - "alloy-consensus", - "alloy-eips", - "alloy-network", - "alloy-primitives", - "alloy-rpc-types-engine", - "alloy-rpc-types-eth", - "alloy-rpc-types-trace", - "alloy-sol-types", - "arc-evm-node", - "arc-execution-config", - "arc-execution-txpool", - "arc-precompiles", - "eyre", - "futures-util", - "jsonrpsee", - "reth-chainspec", - "reth-e2e-test-utils", - "reth-ethereum", - "reth-ethereum-engine-primitives", - "reth-ethereum-primitives", - "reth-node-api", - "reth-node-builder", - "reth-node-core", - "reth-primitives-traits", - "reth-provider", - "reth-rpc-api", - "reth-rpc-server-types", - "reth-tasks", - "reth-tracing", - "reth-transaction-pool", - "revm-bytecode", - "rstest", - "serde_json", - "tokio", - "tracing", -] - -[[package]] -name = "arc-execution-payload" -version = "0.0.1" -dependencies = [ - "alloy-consensus", - "alloy-primitives", - "alloy-rlp", - "arc-execution-txpool", - "eyre", - "metrics", - "reth-basic-payload-builder", - "reth-chainspec", - "reth-consensus-common", - "reth-errors", - "reth-ethereum", - "reth-ethereum-engine-primitives", - "reth-ethereum-payload-builder", - "reth-ethereum-primitives", - "reth-evm", - "reth-node-api", - "reth-node-builder", - "reth-payload-builder", - "reth-payload-primitives", - "reth-primitives-traits", - "reth-revm", - "reth-storage-api", - "reth-transaction-pool", - "revm", - "tokio", - "tracing", - "tracing-test", -] - -[[package]] -name = "arc-execution-txpool" -version = "0.0.1" -dependencies = [ - "alloy-consensus", - "alloy-eips", - "alloy-primitives", - "arc-execution-config", - "arc-execution-validation", - "arc-precompiles", - "arc-shared", - "eyre", - "k256", - "metrics", - "parking_lot", - "reth-chainspec", - "reth-ethereum", - "reth-ethereum-primitives", - "reth-evm", - "reth-evm-ethereum", - "reth-node-api", - "reth-node-builder", - "reth-primitives-traits", - "reth-provider", - "reth-storage-api", - "reth-tracing", - "reth-transaction-pool", - "schnellru", - "serial_test", - "thiserror 2.0.18", - "tokio", - "tracing", -] - -[[package]] -name = "arc-execution-validation" -version = "0.0.1" -dependencies = [ - "alloy-consensus", - "alloy-eips", - "alloy-primitives", - "arc-execution-config", - "mockall", - "reth-chainspec", - "reth-consensus", - "reth-consensus-common", - "reth-ethereum", - "reth-ethereum-primitives", - "reth-primitives-traits", - "reth-storage-api", - "thiserror 2.0.18", - "tracing", -] - -[[package]] -name = "arc-malachitebft-app" -version = "0.7.0-pre" -source = "git+https://github.com/circlefin/malachite.git?rev=8ee5d998#8ee5d998545087e73710c37e457cc03cb56b1463" -dependencies = [ - "arc-malachitebft-codec", - "arc-malachitebft-config", - "arc-malachitebft-core-consensus", - "arc-malachitebft-core-types", - "arc-malachitebft-engine", - "arc-malachitebft-metrics", - "arc-malachitebft-network", - "arc-malachitebft-peer", - "arc-malachitebft-signing", - "arc-malachitebft-sync", - "arc-malachitebft-wal", - "async-trait", - "derive-where", - "eyre", - "libp2p", - "libp2p-identity", - "ractor", - "rand 0.8.5", - "serde", - "tokio", - "tracing", -] - -[[package]] -name = "arc-malachitebft-app-channel" -version = "0.7.0-pre" -source = "git+https://github.com/circlefin/malachite.git?rev=8ee5d998#8ee5d998545087e73710c37e457cc03cb56b1463" -dependencies = [ - "arc-malachitebft-app", - "arc-malachitebft-config", - "arc-malachitebft-engine", - "arc-malachitebft-signing", - "bytes", - "derive-where", - "eyre", - "ractor", - "thiserror 2.0.18", - "tokio", - "tracing", -] - -[[package]] -name = "arc-malachitebft-codec" -version = "0.7.0-pre" -source = "git+https://github.com/circlefin/malachite.git?rev=8ee5d998#8ee5d998545087e73710c37e457cc03cb56b1463" -dependencies = [ - "bytes", -] - -[[package]] -name = "arc-malachitebft-config" -version = "0.7.0-pre" -source = "git+https://github.com/circlefin/malachite.git?rev=8ee5d998#8ee5d998545087e73710c37e457cc03cb56b1463" -dependencies = [ - "arc-malachitebft-core-types", - "bytesize", - "config", - "humantime-serde", - "multiaddr", - "serde", - "tracing", -] - -[[package]] -name = "arc-malachitebft-core-consensus" -version = "0.7.0-pre" -source = "git+https://github.com/circlefin/malachite.git?rev=8ee5d998#8ee5d998545087e73710c37e457cc03cb56b1463" -dependencies = [ - "arc-malachitebft-core-driver", - "arc-malachitebft-core-types", - "arc-malachitebft-core-votekeeper", - "arc-malachitebft-metrics", - "arc-malachitebft-peer", - "async-recursion", - "derive-where", - "futures", - "genawaiter", - "multiaddr", - "thiserror 2.0.18", - "tokio", - "tracing", -] - -[[package]] -name = "arc-malachitebft-core-driver" -version = "0.7.0-pre" -source = "git+https://github.com/circlefin/malachite.git?rev=8ee5d998#8ee5d998545087e73710c37e457cc03cb56b1463" -dependencies = [ - "arc-malachitebft-core-state-machine", - "arc-malachitebft-core-types", - "arc-malachitebft-core-votekeeper", - "derive-where", - "thiserror 2.0.18", - "tracing", -] - -[[package]] -name = "arc-malachitebft-core-state-machine" -version = "0.7.0-pre" -source = "git+https://github.com/circlefin/malachite.git?rev=8ee5d998#8ee5d998545087e73710c37e457cc03cb56b1463" -dependencies = [ - "arc-malachitebft-core-types", - "derive-where", - "displaydoc", -] - -[[package]] -name = "arc-malachitebft-core-types" -version = "0.7.0-pre" -source = "git+https://github.com/circlefin/malachite.git?rev=8ee5d998#8ee5d998545087e73710c37e457cc03cb56b1463" -dependencies = [ - "arc-malachitebft-peer", - "async-trait", - "bytes", - "derive-where", - "serde", - "thiserror 2.0.18", -] - -[[package]] -name = "arc-malachitebft-core-votekeeper" -version = "0.7.0-pre" -source = "git+https://github.com/circlefin/malachite.git?rev=8ee5d998#8ee5d998545087e73710c37e457cc03cb56b1463" -dependencies = [ - "arc-malachitebft-core-types", - "derive-where", - "thiserror 2.0.18", - "tracing", -] - -[[package]] -name = "arc-malachitebft-discovery" -version = "0.7.0-pre" -source = "git+https://github.com/circlefin/malachite.git?rev=8ee5d998#8ee5d998545087e73710c37e457cc03cb56b1463" -dependencies = [ - "arc-malachitebft-metrics", - "either", - "eyre", - "libp2p", - "rand 0.8.5", - "serde", - "tokio", - "tracing", -] - -[[package]] -name = "arc-malachitebft-engine" -version = "0.7.0-pre" -source = "git+https://github.com/circlefin/malachite.git?rev=8ee5d998#8ee5d998545087e73710c37e457cc03cb56b1463" -dependencies = [ - "arc-malachitebft-codec", - "arc-malachitebft-config", - "arc-malachitebft-core-consensus", - "arc-malachitebft-core-driver", - "arc-malachitebft-core-state-machine", - "arc-malachitebft-core-types", - "arc-malachitebft-core-votekeeper", - "arc-malachitebft-metrics", - "arc-malachitebft-network", - "arc-malachitebft-signing", - "arc-malachitebft-sync", - "arc-malachitebft-wal", - "async-recursion", - "async-trait", - "byteorder", - "bytes", - "bytesize", - "derive-where", - "eyre", - "hex", - "itertools 0.14.0", - "libp2p", - "ractor", - "rand 0.8.5", - "tokio", - "tracing", -] - -[[package]] -name = "arc-malachitebft-metrics" -version = "0.7.0-pre" -source = "git+https://github.com/circlefin/malachite.git?rev=8ee5d998#8ee5d998545087e73710c37e457cc03cb56b1463" -dependencies = [ - "arc-malachitebft-core-state-machine", - "prometheus-client", -] - -[[package]] -name = "arc-malachitebft-network" -version = "0.7.0-pre" -source = "git+https://github.com/circlefin/malachite.git?rev=8ee5d998#8ee5d998545087e73710c37e457cc03cb56b1463" -dependencies = [ - "arc-malachitebft-discovery", - "arc-malachitebft-metrics", - "arc-malachitebft-peer", - "arc-malachitebft-sync", - "async-trait", - "asynchronous-codec", - "bytes", - "either", - "eyre", - "futures", - "hex", - "itertools 0.14.0", - "libp2p", - "libp2p-gossipsub", - "libp2p-scatter", - "libp2p-stream", - "seahash", - "serde", - "thiserror 2.0.18", - "tokio", - "tracing", - "unsigned-varint 0.8.0", -] - -[[package]] -name = "arc-malachitebft-peer" -version = "0.7.0-pre" -source = "git+https://github.com/circlefin/malachite.git?rev=8ee5d998#8ee5d998545087e73710c37e457cc03cb56b1463" -dependencies = [ - "bs58", - "multihash", - "rand 0.8.5", - "thiserror 2.0.18", -] - -[[package]] -name = "arc-malachitebft-proto" -version = "0.7.0-pre" -source = "git+https://github.com/circlefin/malachite.git?rev=8ee5d998#8ee5d998545087e73710c37e457cc03cb56b1463" -dependencies = [ - "prost 0.13.5", - "prost-types 0.13.5", - "thiserror 2.0.18", -] - -[[package]] -name = "arc-malachitebft-signing" -version = "0.7.0-pre" -source = "git+https://github.com/circlefin/malachite.git?rev=8ee5d998#8ee5d998545087e73710c37e457cc03cb56b1463" -dependencies = [ - "arc-malachitebft-core-types", - "async-trait", - "signature 2.2.0", -] - -[[package]] -name = "arc-malachitebft-signing-ed25519" -version = "0.7.0-pre" -source = "git+https://github.com/circlefin/malachite.git?rev=8ee5d998#8ee5d998545087e73710c37e457cc03cb56b1463" -dependencies = [ - "arc-malachitebft-core-types", - "base64 0.22.1", - "ed25519-consensus", - "rand 0.8.5", - "serde", - "signature 2.2.0", -] - -[[package]] -name = "arc-malachitebft-sync" -version = "0.7.0-pre" -source = "git+https://github.com/circlefin/malachite.git?rev=8ee5d998#8ee5d998545087e73710c37e457cc03cb56b1463" -dependencies = [ - "arc-malachitebft-core-types", - "arc-malachitebft-metrics", - "arc-malachitebft-peer", - "async-trait", - "bytes", - "dashmap", - "derive-where", - "displaydoc", - "eyre", - "genawaiter", - "libp2p", - "rand 0.8.5", - "serde", - "thiserror 2.0.18", - "tracing", -] - -[[package]] -name = "arc-malachitebft-wal" -version = "0.7.0-pre" -source = "git+https://github.com/circlefin/malachite.git?rev=8ee5d998#8ee5d998545087e73710c37e457cc03cb56b1463" -dependencies = [ - "advisory-lock", - "bytes", - "cfg-if", - "crc32fast", -] - -[[package]] -name = "arc-mesh-analysis" -version = "0.0.1" -dependencies = [ - "clap", - "color-eyre", - "futures-util", - "prometheus-parse", - "reqwest", - "tokio", - "url", -] - -[[package]] -name = "arc-node-consensus" -version = "0.0.1" -dependencies = [ - "alloy-consensus", - "alloy-primitives", - "alloy-provider", - "alloy-rlp", - "alloy-rpc-types-engine", - "alloy-rpc-types-eth", - "alloy-transport-ws", - "arbitrary", - "arc-consensus-db", - "arc-consensus-types", - "arc-eth-engine", - "arc-malachitebft-app-channel", - "arc-malachitebft-core-state-machine", - "arc-malachitebft-core-types", - "arc-malachitebft-network", - "arc-malachitebft-peer", - "arc-malachitebft-sync", - "arc-node-consensus-cli", - "arc-shared", - "arc-signer", - "arc-version", - "assert_cmd", - "atomic-time", - "axum 0.8.8", - "backon", - "base64 0.22.1", - "bon", - "bytes", - "bytesize", - "chrono", - "ethereum_ssz 0.9.1", - "eyre", - "futures", - "hex", - "humantime", - "itertools 0.14.0", - "mockall", - "multihash", - "oneline-eyre", - "pprof_hyper_server", - "predicates", - "procfs 0.17.0", - "proptest", - "ractor", - "rand 0.8.5", - "redb", - "reqwest", - "schnellru", - "serde", - "serde_json", - "serde_with", - "serial_test", - "sha3 0.10.8", - "tempfile", - "thiserror 2.0.18", - "tikv-jemalloc-ctl", - "tikv-jemallocator", - "tokio", - "tokio-tungstenite 0.26.2", - "tokio-util", - "tower 0.5.3", - "tracing", - "url", -] - -[[package]] -name = "arc-node-consensus-cli" -version = "0.0.1" -dependencies = [ - "arc-consensus-types", - "arc-malachitebft-app", - "arc-malachitebft-config", - "arc-snapshots", - "arc-version", - "axum 0.8.8", - "bip32", - "bip39", - "clap", - "color-eyre", - "directories", - "eyre", - "hex", - "itertools 0.14.0", - "lz4", - "rand 0.8.5", - "reqwest", - "serde", - "serde_json", - "tar", - "tempfile", - "thiserror 2.0.18", - "tokio", - "tracing", - "tracing-appender", - "tracing-subscriber", - "url", - "wiremock", -] - -[[package]] -name = "arc-node-execution" -version = "0.0.1" -dependencies = [ - "alloy-evm", - "alloy-genesis", - "alloy-primitives", - "alloy-rlp", - "alloy-rpc-types-trace", - "alloy-sol-types", - "arc-evm", - "arc-evm-node", - "arc-execution-config", - "arc-execution-txpool", - "arc-execution-validation", - "arc-precompiles", - "arc-version", - "clap", - "directories", - "eyre", - "metrics", - "pprof_hyper_server", - "protox", - "reth-chainspec", - "reth-db", - "reth-e2e-test-utils", - "reth-ethereum", - "reth-evm", - "reth-node-builder", - "reth-node-core", - "reth-node-ethereum", - "reth-node-metrics", - "reth-prune-types", - "reth-rpc-builder", - "reth-rpc-eth-types", - "reth-rpc-server-types", - "revm", - "revm-inspectors", - "revm-primitives", - "serde_json", - "tikv-jemalloc-ctl", - "tikv-jemallocator", - "tokio", - "tonic-build", - "tracing", - "tracing-test", -] - -[[package]] -name = "arc-precompiles" -version = "0.0.1" -dependencies = [ - "alloy-consensus", - "alloy-evm", - "alloy-primitives", - "alloy-sol-types", - "arc-evm", - "arc-execution-config", - "criterion", - "reth-chainspec", - "reth-ethereum", - "reth-ethereum-forks", - "reth-evm", - "revm", - "revm-context-interface", - "revm-interpreter", - "revm-primitives", - "serde", - "serde_json", - "serde_with", - "sha2 0.10.9", - "slh-dsa", - "thiserror 2.0.18", -] - -[[package]] -name = "arc-remote-signer" -version = "0.0.1" -dependencies = [ - "arc-consensus-types", - "arc-malachitebft-core-types", - "arc-malachitebft-signing", - "async-trait", - "backon", - "base64 0.22.1", - "eyre", - "hex", - "prometheus-client", - "prost 0.13.5", - "protox", - "rand 0.8.5", - "thiserror 2.0.18", - "tokio", - "tonic 0.12.3", - "tonic-build", - "tracing", - "url", -] - -[[package]] -name = "arc-shared" -version = "0.0.1" -dependencies = [ - "metrics", -] - -[[package]] -name = "arc-signer" -version = "0.0.1" -dependencies = [ - "arc-consensus-types", - "arc-malachitebft-core-types", - "arc-malachitebft-signing-ed25519", - "arc-remote-signer", - "async-trait", - "base64 0.22.1", - "bytes", - "hex", - "rand 0.8.5", - "tokio", - "tracing", -] - -[[package]] -name = "arc-snapshots" -version = "0.0.1" -dependencies = [ - "arc-version", - "clap", - "directories", - "eyre", - "lz4", - "reqwest", - "serde", - "serde_json", - "tar", - "tempfile", - "tokio", - "tracing", - "tracing-subscriber", - "url", - "wiremock", -] - -[[package]] -name = "arc-test-framework" -version = "0.0.1" -dependencies = [ - "alloy-primitives", - "arc-consensus-types", - "arc-malachitebft-core-types", - "async-trait", - "eyre", - "rstest", - "tokio", - "tracing", - "tracing-subscriber", -] - -[[package]] -name = "arc-test-integration" -version = "0.0.1" -dependencies = [ - "alloy-primitives", - "arc-consensus-types", - "arc-evm-node", - "arc-execution-config", - "arc-execution-txpool", - "arc-malachitebft-app-channel", - "arc-node-consensus", - "arc-node-consensus-cli", - "arc-signer", - "arc-test-framework", - "async-trait", - "eyre", - "reth-node-builder", - "reth-tasks", - "rstest", - "tempfile", - "tokio", - "tokio-util", - "tracing", -] - -[[package]] -name = "arc-version" -version = "0.0.1" -dependencies = [ - "chrono", - "dotenvy", - "vergen-git2", -] - -[[package]] -name = "ark-bls12-381" -version = "0.5.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3df4dcc01ff89867cd86b0da835f23c3f02738353aaee7dde7495af71363b8d5" -dependencies = [ - "ark-ec", - "ark-ff 0.5.0", - "ark-serialize 0.5.0", - "ark-std 0.5.0", -] - -[[package]] -name = "ark-bn254" -version = "0.5.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d69eab57e8d2663efa5c63135b2af4f396d66424f88954c21104125ab6b3e6bc" -dependencies = [ - "ark-ec", - "ark-ff 0.5.0", - "ark-r1cs-std", - "ark-std 0.5.0", -] - -[[package]] -name = "ark-ec" -version = "0.5.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "43d68f2d516162846c1238e755a7c4d131b892b70cc70c471a8e3ca3ed818fce" -dependencies = [ - "ahash", - "ark-ff 0.5.0", - "ark-poly", - "ark-serialize 0.5.0", - "ark-std 0.5.0", - "educe", - "fnv", - "hashbrown 0.15.5", - "itertools 0.13.0", - "num-bigint", - "num-integer", - "num-traits", - "zeroize", -] - -[[package]] -name = "ark-ff" -version = "0.3.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6b3235cc41ee7a12aaaf2c575a2ad7b46713a8a50bda2fc3b003a04845c05dd6" -dependencies = [ - "ark-ff-asm 0.3.0", - "ark-ff-macros 0.3.0", - "ark-serialize 0.3.0", - "ark-std 0.3.0", - "derivative", - "num-bigint", - "num-traits", - "paste", - "rustc_version 0.3.3", - "zeroize", -] - -[[package]] -name = "ark-ff" -version = "0.4.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ec847af850f44ad29048935519032c33da8aa03340876d351dfab5660d2966ba" -dependencies = [ - "ark-ff-asm 0.4.2", - "ark-ff-macros 0.4.2", - "ark-serialize 0.4.2", - "ark-std 0.4.0", - "derivative", - "digest 0.10.7", - "itertools 0.10.5", - "num-bigint", - "num-traits", - "paste", - "rustc_version 0.4.1", - "zeroize", -] - -[[package]] -name = "ark-ff" -version = "0.5.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a177aba0ed1e0fbb62aa9f6d0502e9b46dad8c2eab04c14258a1212d2557ea70" -dependencies = [ - "ark-ff-asm 0.5.0", - "ark-ff-macros 0.5.0", - "ark-serialize 0.5.0", - "ark-std 0.5.0", - "arrayvec", - "digest 0.10.7", - "educe", - "itertools 0.13.0", - "num-bigint", - "num-traits", - "paste", - "zeroize", -] - -[[package]] -name = "ark-ff-asm" -version = "0.3.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "db02d390bf6643fb404d3d22d31aee1c4bc4459600aef9113833d17e786c6e44" -dependencies = [ - "quote", - "syn 1.0.109", -] - -[[package]] -name = "ark-ff-asm" -version = "0.4.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3ed4aa4fe255d0bc6d79373f7e31d2ea147bcf486cba1be5ba7ea85abdb92348" -dependencies = [ - "quote", - "syn 1.0.109", -] - -[[package]] -name = "ark-ff-asm" -version = "0.5.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "62945a2f7e6de02a31fe400aa489f0e0f5b2502e69f95f853adb82a96c7a6b60" -dependencies = [ - "quote", - "syn 2.0.117", -] - -[[package]] -name = "ark-ff-macros" -version = "0.3.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "db2fd794a08ccb318058009eefdf15bcaaaaf6f8161eb3345f907222bac38b20" -dependencies = [ - "num-bigint", - "num-traits", - "quote", - "syn 1.0.109", -] - -[[package]] -name = "ark-ff-macros" -version = "0.4.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7abe79b0e4288889c4574159ab790824d0033b9fdcb2a112a3182fac2e514565" -dependencies = [ - "num-bigint", - "num-traits", - "proc-macro2", - "quote", - "syn 1.0.109", -] - -[[package]] -name = "ark-ff-macros" -version = "0.5.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "09be120733ee33f7693ceaa202ca41accd5653b779563608f1234f78ae07c4b3" -dependencies = [ - "num-bigint", - "num-traits", - "proc-macro2", - "quote", - "syn 2.0.117", -] - -[[package]] -name = "ark-poly" -version = "0.5.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "579305839da207f02b89cd1679e50e67b4331e2f9294a57693e5051b7703fe27" -dependencies = [ - "ahash", - "ark-ff 0.5.0", - "ark-serialize 0.5.0", - "ark-std 0.5.0", - "educe", - "fnv", - "hashbrown 0.15.5", -] - -[[package]] -name = "ark-r1cs-std" -version = "0.5.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "941551ef1df4c7a401de7068758db6503598e6f01850bdb2cfdb614a1f9dbea1" -dependencies = [ - "ark-ec", - "ark-ff 0.5.0", - "ark-relations", - "ark-std 0.5.0", - "educe", - "num-bigint", - "num-integer", - "num-traits", - "tracing", -] - -[[package]] -name = "ark-relations" -version = "0.5.1" -source = "git+https://github.com/arkworks-rs/snark.git?rev=9c528529763f1a0a2e0cba83528f93d32247d621#9c528529763f1a0a2e0cba83528f93d32247d621" -dependencies = [ - "ark-ff 0.5.0", - "ark-poly", - "ark-serialize 0.5.0", - "ark-std 0.5.0", - "tracing", - "tracing-subscriber", -] - -[[package]] -name = "ark-serialize" -version = "0.3.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1d6c2b318ee6e10f8c2853e73a83adc0ccb88995aa978d8a3408d492ab2ee671" -dependencies = [ - "ark-std 0.3.0", - "digest 0.9.0", -] - -[[package]] -name = "ark-serialize" -version = "0.4.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "adb7b85a02b83d2f22f89bd5cac66c9c89474240cb6207cb1efc16d098e822a5" -dependencies = [ - "ark-std 0.4.0", - "digest 0.10.7", - "num-bigint", -] - -[[package]] -name = "ark-serialize" -version = "0.5.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3f4d068aaf107ebcd7dfb52bc748f8030e0fc930ac8e360146ca54c1203088f7" -dependencies = [ - "ark-serialize-derive", - "ark-std 0.5.0", - "arrayvec", - "digest 0.10.7", - "num-bigint", -] - -[[package]] -name = "ark-serialize-derive" -version = "0.5.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "213888f660fddcca0d257e88e54ac05bca01885f258ccdf695bafd77031bb69d" -dependencies = [ - "proc-macro2", - "quote", - "syn 2.0.117", -] - -[[package]] -name = "ark-std" -version = "0.3.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1df2c09229cbc5a028b1d70e00fdb2acee28b1055dfb5ca73eea49c5a25c4e7c" -dependencies = [ - "num-traits", - "rand 0.8.5", -] - -[[package]] -name = "ark-std" -version = "0.4.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "94893f1e0c6eeab764ade8dc4c0db24caf4fe7cbbaafc0eba0a9030f447b5185" -dependencies = [ - "num-traits", - "rand 0.8.5", -] - -[[package]] -name = "ark-std" -version = "0.5.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "246a225cc6131e9ee4f24619af0f19d67761fff15d7ccc22e42b80846e69449a" -dependencies = [ - "num-traits", - "rand 0.8.5", -] - -[[package]] -name = "arrayref" -version = "0.3.9" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "76a2e8124351fda1ef8aaaa3bbd7ebbcb486bbcd4225aca0aa0d84bb2db8fecb" - -[[package]] -name = "arrayvec" -version = "0.7.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7c02d123df017efcdfbd739ef81735b36c5ba83ec3c59c80a9d7ecc718f92e50" -dependencies = [ - "serde", -] - -[[package]] -name = "asn1-rs" -version = "0.7.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "56624a96882bb8c26d61312ae18cb45868e5a9992ea73c58e45c3101e56a1e60" -dependencies = [ - "asn1-rs-derive", - "asn1-rs-impl", - "displaydoc", - "nom", - "num-traits", - "rusticata-macros", - "thiserror 2.0.18", - "time", -] - -[[package]] -name = "asn1-rs-derive" -version = "0.6.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3109e49b1e4909e9db6515a30c633684d68cdeaa252f215214cb4fa1a5bfee2c" -dependencies = [ - "proc-macro2", - "quote", - "syn 2.0.117", - "synstructure", -] - -[[package]] -name = "asn1-rs-impl" -version = "0.2.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7b18050c2cd6fe86c3a76584ef5e0baf286d038cda203eb6223df2cc413565f7" -dependencies = [ - "proc-macro2", - "quote", - "syn 2.0.117", -] - -[[package]] -name = "asn1_der" -version = "0.7.7" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4858a9d740c5007a9069007c3b4e91152d0506f13c1b31dd49051fd537656156" - -[[package]] -name = "assert-json-diff" -version = "2.0.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "47e4f2b81832e72834d7518d8487a0396a28cc408186a2e8854c0f98011faf12" -dependencies = [ - "serde", - "serde_json", -] - -[[package]] -name = "assert_cmd" -version = "2.1.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9c5bcfa8749ac45dd12cb11055aeeb6b27a3895560d60d71e3c23bf979e60514" -dependencies = [ - "anstyle", - "bstr", - "libc", - "predicates", - "predicates-core", - "predicates-tree", - "wait-timeout", -] - -[[package]] -name = "async-channel" -version = "2.5.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "924ed96dd52d1b75e9c1a3e6275715fd320f5f9439fb5a4a11fa51f4221158d2" -dependencies = [ - "concurrent-queue", - "event-listener-strategy", - "futures-core", - "pin-project-lite", -] - -[[package]] -name = "async-compression" -version = "0.4.40" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7d67d43201f4d20c78bcda740c142ca52482d81da80681533d33bf3f0596c8e2" -dependencies = [ - "compression-codecs", - "compression-core", - "pin-project-lite", - "tokio", -] - -[[package]] -name = "async-executor" -version = "1.14.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c96bf972d85afc50bf5ab8fe2d54d1586b4e0b46c97c50a0c9e71e2f7bcd812a" -dependencies = [ - "async-task", - "concurrent-queue", - "fastrand", - "futures-lite", - "pin-project-lite", - "slab", -] - -[[package]] -name = "async-io" -version = "2.6.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "456b8a8feb6f42d237746d4b3e9a178494627745c3c56c6ea55d92ba50d026fc" -dependencies = [ - "autocfg", - "cfg-if", - "concurrent-queue", - "futures-io", - "futures-lite", - "parking", - "polling", - "rustix 1.1.3", - "slab", - "windows-sys 0.61.2", -] - -[[package]] -name = "async-recursion" -version = "1.1.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3b43422f69d8ff38f95f1b2bb76517c91589a924d1559a0e935d7c8ce0274c11" -dependencies = [ - "proc-macro2", - "quote", - "syn 2.0.117", -] - -[[package]] -name = "async-stream" -version = "0.3.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0b5a71a6f37880a80d1d7f19efd781e4b5de42c88f0722cc13bcb6cc2cfe8476" -dependencies = [ - "async-stream-impl", - "futures-core", - "pin-project-lite", -] - -[[package]] -name = "async-stream-impl" -version = "0.3.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c7c24de15d275a1ecfd47a380fb4d5ec9bfe0933f309ed5e705b775596a3574d" -dependencies = [ - "proc-macro2", - "quote", - "syn 2.0.117", -] - -[[package]] -name = "async-task" -version = "4.7.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8b75356056920673b02621b35afd0f7dda9306d03c79a30f5c56c44cf256e3de" - -[[package]] -name = "async-trait" -version = "0.1.89" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9035ad2d096bed7955a320ee7e2230574d28fd3c3a0f186cbea1ff3c7eed5dbb" -dependencies = [ - "proc-macro2", - "quote", - "syn 2.0.117", -] - -[[package]] -name = "async_io_stream" -version = "0.3.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b6d7b9decdf35d8908a7e3ef02f64c5e9b1695e230154c0e8de3969142d9b94c" -dependencies = [ - "futures", - "pharos", - "rustc_version 0.4.1", -] - -[[package]] -name = "asynchronous-codec" -version = "0.7.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a860072022177f903e59730004fb5dc13db9275b79bb2aef7ba8ce831956c233" -dependencies = [ - "bytes", - "futures-sink", - "futures-util", - "memchr", - "pin-project-lite", -] - -[[package]] -name = "atomic-time" -version = "0.1.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9622f5c6fb50377516c70f65159e70b25465409760c6bd6d4e581318bf704e83" -dependencies = [ - "once_cell", - "portable-atomic", -] - -[[package]] -name = "atomic-waker" -version = "1.1.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1505bd5d3d116872e7271a6d4e16d81d0c8570876c8de68093a09ac269d8aac0" - -[[package]] -name = "attohttpc" -version = "0.30.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "16e2cdb6d5ed835199484bb92bb8b3edd526effe995c61732580439c1a67e2e9" -dependencies = [ - "base64 0.22.1", - "http", - "log", - "url", -] - -[[package]] -name = "aurora-engine-modexp" -version = "1.2.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "518bc5745a6264b5fd7b09dffb9667e400ee9e2bbe18555fac75e1fe9afa0df9" -dependencies = [ - "hex", - "num", -] - -[[package]] -name = "auto_impl" -version = "1.3.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ffdcb70bdbc4d478427380519163274ac86e52916e10f0a8889adf0f96d3fee7" -dependencies = [ - "proc-macro2", - "quote", - "syn 2.0.117", -] - -[[package]] -name = "autocfg" -version = "1.5.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c08606f8c3cbf4ce6ec8e28fb0014a2c086708fe954eaa885384a6165172e7e8" - -[[package]] -name = "axum" -version = "0.7.9" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "edca88bc138befd0323b20752846e6587272d3b03b0343c8ea28a6f819e6e71f" -dependencies = [ - "async-trait", - "axum-core 0.4.5", - "bytes", - "futures-util", - "http", - "http-body", - "http-body-util", - "itoa", - "matchit 0.7.3", - "memchr", - "mime", - "percent-encoding", - "pin-project-lite", - "rustversion", - "serde", - "sync_wrapper", - "tower 0.5.3", - "tower-layer", - "tower-service", -] - -[[package]] -name = "axum" -version = "0.8.8" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8b52af3cb4058c895d37317bb27508dccc8e5f2d39454016b297bf4a400597b8" -dependencies = [ - "axum-core 0.5.6", - "axum-macros", - "bytes", - "form_urlencoded", - "futures-util", - "http", - "http-body", - "http-body-util", - "hyper", - "hyper-util", - "itoa", - "matchit 0.8.4", - "memchr", - "mime", - "percent-encoding", - "pin-project-lite", - "serde_core", - "serde_json", - "serde_path_to_error", - "serde_urlencoded", - "sync_wrapper", - "tokio", - "tower 0.5.3", - "tower-layer", - "tower-service", - "tracing", -] - -[[package]] -name = "axum-core" -version = "0.4.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "09f2bd6146b97ae3359fa0cc6d6b376d9539582c7b4220f041a33ec24c226199" -dependencies = [ - "async-trait", - "bytes", - "futures-util", - "http", - "http-body", - "http-body-util", - "mime", - "pin-project-lite", - "rustversion", - "sync_wrapper", - "tower-layer", - "tower-service", -] - -[[package]] -name = "axum-core" -version = "0.5.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "08c78f31d7b1291f7ee735c1c6780ccde7785daae9a9206026862dab7d8792d1" -dependencies = [ - "bytes", - "futures-core", - "http", - "http-body", - "http-body-util", - "mime", - "pin-project-lite", - "sync_wrapper", - "tower-layer", - "tower-service", - "tracing", -] - -[[package]] -name = "axum-macros" -version = "0.5.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "604fde5e028fea851ce1d8570bbdc034bec850d157f7569d10f347d06808c05c" -dependencies = [ - "proc-macro2", - "quote", - "syn 2.0.117", -] - -[[package]] -name = "backon" -version = "1.6.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cffb0e931875b666fc4fcb20fee52e9bbd1ef836fd9e9e04ec21555f9f85f7ef" -dependencies = [ - "fastrand", - "tokio", -] - -[[package]] -name = "backtrace" -version = "0.3.76" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bb531853791a215d7c62a30daf0dde835f381ab5de4589cfe7c649d2cbe92bd6" -dependencies = [ - "addr2line", - "cfg-if", - "libc", - "miniz_oxide", - "object", - "rustc-demangle", - "windows-link", -] - -[[package]] -name = "base-x" -version = "0.2.11" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4cbbc9d0964165b47557570cce6c952866c2678457aca742aafc9fb771d30270" - -[[package]] -name = "base16ct" -version = "0.2.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4c7f02d4ea65f2c1853089ffd8d2787bdbc63de2f0d29dedbcf8ccdfa0ccd4cf" - -[[package]] -name = "base256emoji" -version = "1.0.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b5e9430d9a245a77c92176e649af6e275f20839a48389859d1661e9a128d077c" -dependencies = [ - "const-str", - "match-lookup", -] - -[[package]] -name = "base64" -version = "0.21.7" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9d297deb1925b89f2ccc13d7635fa0714f12c87adce1c75356b39ca9b7178567" - -[[package]] -name = "base64" -version = "0.22.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "72b3254f16251a8381aa12e40e3c4d2f0199f8c6508fbecb9d91f575e0fbb8c6" - -[[package]] -name = "base64ct" -version = "1.8.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2af50177e190e07a26ab74f8b1efbfe2ef87da2116221318cb1c2e82baf7de06" - -[[package]] -name = "bech32" -version = "0.9.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d86b93f97252c47b41663388e6d155714a9d0c398b99f1005cbc5f978b29f445" - -[[package]] -name = "beef" -version = "0.5.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3a8241f3ebb85c056b509d4327ad0358fbbba6ffb340bf388f26350aeda225b1" - -[[package]] -name = "bimap" -version = "0.6.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "230c5f1ca6a325a32553f8640d31ac9b49f2411e901e427570154868b46da4f7" - -[[package]] -name = "bincode" -version = "1.3.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b1f45e9417d87227c7a56d22e471c6206462cba514c7590c09aff4cf6d1ddcad" -dependencies = [ - "serde", -] - -[[package]] -name = "bindgen" -version = "0.72.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "993776b509cfb49c750f11b8f07a46fa23e0a1386ffc01fb1e7d343efc387895" -dependencies = [ - "bitflags 2.11.0", - "cexpr", - "clang-sys", - "itertools 0.13.0", - "proc-macro2", - "quote", - "regex", - "rustc-hash", - "shlex", - "syn 2.0.117", -] - -[[package]] -name = "bip32" -version = "0.5.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "db40d3dfbeab4e031d78c844642fa0caa0b0db11ce1607ac9d2986dff1405c69" -dependencies = [ - "bs58", - "hmac 0.12.1", - "k256", - "rand_core 0.6.4", - "ripemd", - "secp256k1 0.27.0", - "sha2 0.10.9", - "subtle", - "zeroize", -] - -[[package]] -name = "bip39" -version = "2.2.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "90dbd31c98227229239363921e60fcf5e558e43ec69094d46fc4996f08d1d5bc" -dependencies = [ - "bitcoin_hashes", - "serde", - "unicode-normalization", -] - -[[package]] -name = "bit-set" -version = "0.8.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "08807e080ed7f9d5433fa9b275196cfc35414f66a0c79d864dc51a0d825231a3" -dependencies = [ - "bit-vec", -] - -[[package]] -name = "bit-vec" -version = "0.8.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5e764a1d40d510daf35e07be9eb06e75770908c27d411ee6c92109c9840eaaf7" - -[[package]] -name = "bitcoin-io" -version = "0.1.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2dee39a0ee5b4095224a0cfc6bf4cc1baf0f9624b96b367e53b66d974e51d953" - -[[package]] -name = "bitcoin_hashes" -version = "0.14.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "26ec84b80c482df901772e931a9a681e26a1b9ee2302edeff23cb30328745c8b" -dependencies = [ - "bitcoin-io", - "hex-conservative", -] - -[[package]] -name = "bitflags" -version = "1.3.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bef38d45163c2f1dde094a7dfd33ccf595c92905c8f8f4fdc18d06fb1037718a" - -[[package]] -name = "bitflags" -version = "2.11.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "843867be96c8daad0d758b57df9392b6d8d271134fce549de6ce169ff98a92af" -dependencies = [ - "arbitrary", - "serde_core", -] - -[[package]] -name = "bitvec" -version = "1.0.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1bc2832c24239b0141d5674bb9174f9d68a8b5b3f2753311927c172ca46f7e9c" -dependencies = [ - "funty", - "radium", - "serde", - "tap", - "wyz", -] - -[[package]] -name = "blake2" -version = "0.10.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "46502ad458c9a52b69d4d4d32775c788b7a1b85e8bc9d482d92250fc0e3f8efe" -dependencies = [ - "digest 0.10.7", -] - -[[package]] -name = "block-buffer" -version = "0.9.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4152116fd6e9dadb291ae18fc1ec3575ed6d84c29642d97890f4b4a3417297e4" -dependencies = [ - "generic-array", -] - -[[package]] -name = "block-buffer" -version = "0.10.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3078c7629b62d3f0439517fa394996acacc5cbc91c5a20d8c658e77abd503a71" -dependencies = [ - "generic-array", -] - -[[package]] -name = "block-buffer" -version = "0.12.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cdd35008169921d80bc60d3d0ab416eecb028c4cd653352907921d95084790be" -dependencies = [ - "hybrid-array", -] - -[[package]] -name = "block-padding" -version = "0.3.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a8894febbff9f758034a5b8e12d87918f56dfc64a8e1fe757d65e29041538d93" -dependencies = [ - "generic-array", -] - -[[package]] -name = "blst" -version = "0.3.16" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "dcdb4c7013139a150f9fc55d123186dbfaba0d912817466282c73ac49e71fb45" -dependencies = [ - "cc", - "glob", - "threadpool", - "zeroize", -] - -[[package]] -name = "boa_ast" -version = "0.21.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bc119a5ad34c3f459062a96907f53358989b173d104258891bb74f95d93747e8" -dependencies = [ - "bitflags 2.11.0", - "boa_interner", - "boa_macros", - "boa_string", - "indexmap 2.13.0", - "num-bigint", - "rustc-hash", -] - -[[package]] -name = "boa_engine" -version = "0.21.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e637ec52ea66d76b0ca86180c259d6c7bb6e6a6e14b2f36b85099306d8b00cc3" -dependencies = [ - "aligned-vec", - "arrayvec", - "bitflags 2.11.0", - "boa_ast", - "boa_gc", - "boa_interner", - "boa_macros", - "boa_parser", - "boa_string", - "bytemuck", - "cfg-if", - "cow-utils", - "dashmap", - "dynify", - "fast-float2", - "float16", - "futures-channel", - "futures-concurrency", - "futures-lite", - "hashbrown 0.16.1", - "icu_normalizer", - "indexmap 2.13.0", - "intrusive-collections", - "itertools 0.14.0", - "num-bigint", - "num-integer", - "num-traits", - "num_enum", - "paste", - "portable-atomic", - "rand 0.9.4", - "regress", - "rustc-hash", - "ryu-js", - "serde", - "serde_json", - "small_btree", - "static_assertions", - "tag_ptr", - "tap", - "thin-vec", - "thiserror 2.0.18", - "time", - "xsum", -] - -[[package]] -name = "boa_gc" -version = "0.21.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f1179f690cbfcbe5364cceee5f1cb577265bb6f07b0be6f210aabe270adcf9da" -dependencies = [ - "boa_macros", - "boa_string", - "hashbrown 0.16.1", - "thin-vec", -] - -[[package]] -name = "boa_interner" -version = "0.21.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9626505d33dc63d349662437297df1d3afd9d5fc4a2b3ad34e5e1ce879a78848" -dependencies = [ - "boa_gc", - "boa_macros", - "hashbrown 0.16.1", - "indexmap 2.13.0", - "once_cell", - "phf", - "rustc-hash", - "static_assertions", -] - -[[package]] -name = "boa_macros" -version = "0.21.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7f36418a46544b152632c141b0a0b7a453cd69ca150caeef83aee9e2f4b48b7d" -dependencies = [ - "cfg-if", - "cow-utils", - "proc-macro2", - "quote", - "syn 2.0.117", - "synstructure", -] - -[[package]] -name = "boa_parser" -version = "0.21.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "02f99bf5b684f0de946378fcfe5f38c3a0fbd51cbf83a0f39ff773a0e218541f" -dependencies = [ - "bitflags 2.11.0", - "boa_ast", - "boa_interner", - "boa_macros", - "fast-float2", - "icu_properties", - "num-bigint", - "num-traits", - "regress", - "rustc-hash", -] - -[[package]] -name = "boa_string" -version = "0.21.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "45ce9d7aa5563a2e14eab111e2ae1a06a69a812f6c0c3d843196c9d03fbef440" -dependencies = [ - "fast-float2", - "itoa", - "paste", - "rustc-hash", - "ryu-js", - "static_assertions", -] - -[[package]] -name = "bon" -version = "2.3.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "97493a391b4b18ee918675fb8663e53646fd09321c58b46afa04e8ce2499c869" -dependencies = [ - "bon-macros", - "rustversion", -] - -[[package]] -name = "bon-macros" -version = "2.3.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2a2af3eac944c12cdf4423eab70d310da0a8e5851a18ffb192c0a5e3f7ae1663" -dependencies = [ - "darling 0.20.11", - "ident_case", - "proc-macro2", - "quote", - "syn 2.0.117", -] - -[[package]] -name = "borsh" -version = "1.6.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d1da5ab77c1437701eeff7c88d968729e7766172279eab0676857b3d63af7a6f" -dependencies = [ - "borsh-derive", - "cfg_aliases", -] - -[[package]] -name = "borsh-derive" -version = "1.6.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0686c856aa6aac0c4498f936d7d6a02df690f614c03e4d906d1018062b5c5e2c" -dependencies = [ - "once_cell", - "proc-macro-crate", - "proc-macro2", - "quote", - "syn 2.0.117", -] - -[[package]] -name = "boyer-moore-magiclen" -version = "0.2.22" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7441b4796eb8a7107d4cd99d829810be75f5573e1081c37faa0e8094169ea0d6" -dependencies = [ - "debug-helper", -] - -[[package]] -name = "brotli" -version = "8.0.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4bd8b9603c7aa97359dbd97ecf258968c95f3adddd6db2f7e7a5bef101c84560" -dependencies = [ - "alloc-no-stdlib", - "alloc-stdlib", - "brotli-decompressor", -] - -[[package]] -name = "brotli-decompressor" -version = "5.0.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "874bb8112abecc98cbd6d81ea4fa7e94fb9449648c93cc89aa40c81c24d7de03" -dependencies = [ - "alloc-no-stdlib", - "alloc-stdlib", -] - -[[package]] -name = "bs58" -version = "0.5.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bf88ba1141d185c399bee5288d850d63b8369520c1eafc32a0430b5b6c287bf4" -dependencies = [ - "sha2 0.10.9", - "tinyvec", -] - -[[package]] -name = "bstr" -version = "1.12.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "63044e1ae8e69f3b5a92c736ca6269b8d12fa7efe39bf34ddb06d102cf0e2cab" -dependencies = [ - "memchr", - "regex-automata", - "serde", -] - -[[package]] -name = "bumpalo" -version = "3.20.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5d20789868f4b01b2f2caec9f5c4e0213b41e3e5702a50157d699ae31ced2fcb" - -[[package]] -name = "byte-slice-cast" -version = "1.2.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7575182f7272186991736b70173b0ea045398f984bf5ebbb3804736ce1330c9d" - -[[package]] -name = "bytemuck" -version = "1.25.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c8efb64bd706a16a1bdde310ae86b351e4d21550d98d056f22f8a7f7a2183fec" -dependencies = [ - "bytemuck_derive", -] - -[[package]] -name = "bytemuck_derive" -version = "1.10.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f9abbd1bc6865053c427f7198e6af43bfdedc55ab791faed4fbd361d789575ff" -dependencies = [ - "proc-macro2", - "quote", - "syn 2.0.117", -] - -[[package]] -name = "byteorder" -version = "1.5.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1fd0f2584146f6f2ef48085050886acf353beff7305ebd1ae69500e27c67f64b" - -[[package]] -name = "bytes" -version = "1.11.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1e748733b7cbc798e1434b6ac524f0c1ff2ab456fe201501e6497c8417a4fc33" -dependencies = [ - "serde", -] - -[[package]] -name = "bytesize" -version = "2.3.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6bd91ee7b2422bcb158d90ef4d14f75ef67f340943fc4149891dcce8f8b972a3" -dependencies = [ - "serde_core", -] - -[[package]] -name = "c-kzg" -version = "2.1.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1a0f582957c24870b7bfd12bf562c40b4734b533cafbaf8ded31d6d85f462c01" -dependencies = [ - "arbitrary", - "blst", - "cc", - "glob", - "hex", - "libc", - "once_cell", - "serde", -] - -[[package]] -name = "camino" -version = "1.2.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e629a66d692cb9ff1a1c664e41771b3dcaf961985a9774c0eb0bd1b51cf60a48" -dependencies = [ - "serde_core", -] - -[[package]] -name = "cargo-platform" -version = "0.3.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "87a0c0e6148f11f01f32650a2ea02d532b2ad4e81d8bd41e6e565b5adc5e6082" -dependencies = [ - "serde", - "serde_core", -] - -[[package]] -name = "cargo_metadata" -version = "0.23.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ef987d17b0a113becdd19d3d0022d04d7ef41f9efe4f3fb63ac44ba61df3ade9" -dependencies = [ - "camino", - "cargo-platform", - "semver 1.0.27", - "serde", - "serde_json", - "thiserror 2.0.18", -] - -[[package]] -name = "cast" -version = "0.3.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "37b2a672a2cb129a2e41c10b1224bb368f9f37a2b16b612598138befd7b37eb5" - -[[package]] -name = "castaway" -version = "0.2.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "dec551ab6e7578819132c713a93c022a05d60159dc86e7a7050223577484c55a" -dependencies = [ - "rustversion", -] - -[[package]] -name = "cbor4ii" -version = "0.3.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "472931dd4dfcc785075b09be910147f9c6258883fc4591d0dac6116392b2daa6" -dependencies = [ - "serde", -] - -[[package]] -name = "cc" -version = "1.2.56" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "aebf35691d1bfb0ac386a69bac2fde4dd276fb618cf8bf4f5318fe285e821bb2" -dependencies = [ - "find-msvc-tools", - "jobserver", - "libc", - "shlex", -] - -[[package]] -name = "cesu8" -version = "1.1.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6d43a04d8753f35258c91f8ec639f792891f748a1edbd759cf1dcea3382ad83c" - -[[package]] -name = "cexpr" -version = "0.6.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6fac387a98bb7c37292057cffc56d62ecb629900026402633ae9160df93a8766" -dependencies = [ - "nom", -] - -[[package]] -name = "cfg-if" -version = "1.0.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9330f8b2ff13f34540b44e946ef35111825727b38d33286ef986142615121801" - -[[package]] -name = "cfg_aliases" -version = "0.2.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "613afe47fcd5fac7ccf1db93babcb082c5994d996f20b8b159f2ad1658eb5724" - -[[package]] -name = "chacha20" -version = "0.9.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c3613f74bd2eac03dad61bd53dbe620703d4371614fe0bc3b9f04dd36fe4e818" -dependencies = [ - "cfg-if", - "cipher", - "cpufeatures 0.2.17", -] - -[[package]] -name = "chacha20" -version = "0.10.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6f8d983286843e49675a4b7a2d174efe136dc93a18d69130dd18198a6c167601" -dependencies = [ - "cfg-if", - "cpufeatures 0.3.0", - "rand_core 0.10.0", -] - -[[package]] -name = "chacha20poly1305" -version = "0.10.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "10cd79432192d1c0f4e1a0fef9527696cc039165d729fb41b3f4f4f354c2dc35" -dependencies = [ - "aead", - "chacha20 0.9.1", - "cipher", - "poly1305", - "zeroize", -] - -[[package]] -name = "chrono" -version = "0.4.44" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c673075a2e0e5f4a1dde27ce9dee1ea4558c7ffe648f576438a20ca1d2acc4b0" -dependencies = [ - "iana-time-zone", - "js-sys", - "num-traits", - "serde", - "wasm-bindgen", - "windows-link", -] - -[[package]] -name = "ciborium" -version = "0.2.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "42e69ffd6f0917f5c029256a24d0161db17cea3997d185db0d35926308770f0e" -dependencies = [ - "ciborium-io", - "ciborium-ll", - "serde", -] - -[[package]] -name = "ciborium-io" -version = "0.2.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "05afea1e0a06c9be33d539b876f1ce3692f4afea2cb41f740e7743225ed1c757" - -[[package]] -name = "ciborium-ll" -version = "0.2.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "57663b653d948a338bfb3eeba9bb2fd5fcfaecb9e199e87e1eda4d9e8b240fd9" -dependencies = [ - "ciborium-io", - "half", -] - -[[package]] -name = "cipher" -version = "0.4.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "773f3b9af64447d2ce9850330c473515014aa235e6a783b02db81ff39e4a3dad" -dependencies = [ - "crypto-common 0.1.7", - "inout", - "zeroize", -] - -[[package]] -name = "clang-sys" -version = "1.8.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0b023947811758c97c59bf9d1c188fd619ad4718dcaa767947df1cadb14f39f4" -dependencies = [ - "glob", - "libc", - "libloading", -] - -[[package]] -name = "clap" -version = "4.5.60" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2797f34da339ce31042b27d23607e051786132987f595b02ba4f6a6dffb7030a" -dependencies = [ - "clap_builder", - "clap_derive", -] - -[[package]] -name = "clap-verbosity-flag" -version = "3.0.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9d92b1fab272fe943881b77cc6e920d6543e5b1bfadbd5ed81c7c5a755742394" -dependencies = [ - "clap", - "log", - "tracing-core", -] - -[[package]] -name = "clap_builder" -version = "4.5.60" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "24a241312cea5059b13574bb9b3861cabf758b879c15190b37b6d6fd63ab6876" -dependencies = [ - "anstream", - "anstyle", - "clap_lex", - "strsim", -] - -[[package]] -name = "clap_derive" -version = "4.5.55" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a92793da1a46a5f2a02a6f4c46c6496b28c43638adea8306fcb0caa1634f24e5" -dependencies = [ - "heck", - "proc-macro2", - "quote", - "syn 2.0.117", -] - -[[package]] -name = "clap_lex" -version = "1.0.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3a822ea5bc7590f9d40f1ba12c0dc3c2760f3482c6984db1573ad11031420831" - -[[package]] -name = "cmov" -version = "0.5.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "de0758edba32d61d1fd9f4d69491b47604b91ee2f7e6b33de7e54ca4ebe55dc3" - -[[package]] -name = "coins-bip32" -version = "0.12.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2073678591747aed4000dd468b97b14d7007f7936851d3f2f01846899f5ebf08" -dependencies = [ - "bs58", - "coins-core", - "digest 0.10.7", - "hmac 0.12.1", - "k256", - "serde", - "sha2 0.10.9", - "thiserror 1.0.69", -] - -[[package]] -name = "coins-bip39" -version = "0.12.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "74b169b26623ff17e9db37a539fe4f15342080df39f129ef7631df7683d6d9d4" -dependencies = [ - "bitvec", - "coins-bip32", - "hmac 0.12.1", - "once_cell", - "pbkdf2", - "rand 0.8.5", - "sha2 0.10.9", - "thiserror 1.0.69", -] - -[[package]] -name = "coins-core" -version = "0.12.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "62b962ad8545e43a28e14e87377812ba9ae748dd4fd963f4c10e9fcc6d13475b" -dependencies = [ - "base64 0.21.7", - "bech32", - "bs58", - "const-hex", - "digest 0.10.7", - "generic-array", - "ripemd", - "serde", - "sha2 0.10.9", - "sha3 0.10.8", - "thiserror 1.0.69", -] - -[[package]] -name = "color-eyre" -version = "0.6.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e5920befb47832a6d61ee3a3a846565cfa39b331331e68a3b1d1116630f2f26d" -dependencies = [ - "backtrace", - "color-spantrace", - "eyre", - "indenter", - "once_cell", - "owo-colors", - "tracing-error", -] - -[[package]] -name = "color-spantrace" -version = "0.3.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b8b88ea9df13354b55bc7234ebcce36e6ef896aca2e42a15de9e10edce01b427" -dependencies = [ - "once_cell", - "owo-colors", - "tracing-core", - "tracing-error", -] - -[[package]] -name = "colorchoice" -version = "1.0.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b05b61dc5112cbb17e4b6cd61790d9845d13888356391624cbe7e41efeac1e75" - -[[package]] -name = "combine" -version = "4.6.7" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ba5a308b75df32fe02788e748662718f03fde005016435c444eea572398219fd" -dependencies = [ - "bytes", - "memchr", -] - -[[package]] -name = "comfy-table" -version = "7.2.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "958c5d6ecf1f214b4c2bbbbf6ab9523a864bd136dcf71a7e8904799acfe1ad47" -dependencies = [ - "crossterm", - "unicode-segmentation", - "unicode-width 0.2.2", -] - -[[package]] -name = "compact_str" -version = "0.9.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3fdb1325a1cece981e8a296ab8f0f9b63ae357bd0784a9faaf548cc7b480707a" -dependencies = [ - "castaway", - "cfg-if", - "itoa", - "rustversion", - "ryu", - "static_assertions", -] - -[[package]] -name = "compression-codecs" -version = "0.4.37" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "eb7b51a7d9c967fc26773061ba86150f19c50c0d65c887cb1fbe295fd16619b7" -dependencies = [ - "brotli", - "compression-core", - "flate2", - "memchr", - "zstd", - "zstd-safe", -] - -[[package]] -name = "compression-core" -version = "0.4.31" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "75984efb6ed102a0d42db99afb6c1948f0380d1d91808d5529916e6c08b49d8d" - -[[package]] -name = "concat-kdf" -version = "0.1.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2d72c1252426a83be2092dd5884a5f6e3b8e7180f6891b6263d2c21b92ec8816" -dependencies = [ - "digest 0.10.7", -] - -[[package]] -name = "concurrent-queue" -version = "2.5.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4ca0197aee26d1ae37445ee532fefce43251d24cc7c166799f4d46817f1d3973" -dependencies = [ - "crossbeam-utils", -] - -[[package]] -name = "config" -version = "0.14.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "68578f196d2a33ff61b27fae256c3164f65e36382648e30666dde05b8cc9dfdf" -dependencies = [ - "nom", - "pathdiff", - "serde", - "toml 0.8.23", -] - -[[package]] -name = "const-hex" -version = "1.17.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3bb320cac8a0750d7f25280aa97b09c26edfe161164238ecbbb31092b079e735" -dependencies = [ - "cfg-if", - "cpufeatures 0.2.17", - "proptest", - "serde_core", -] - -[[package]] -name = "const-oid" -version = "0.9.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c2459377285ad874054d797f3ccebf984978aa39129f6eafde5cdc8315b612f8" - -[[package]] -name = "const-oid" -version = "0.10.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a6ef517f0926dd24a1582492c791b6a4818a4d94e789a334894aa15b0d12f55c" - -[[package]] -name = "const-str" -version = "0.4.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2f421161cb492475f1661ddc9815a745a1c894592070661180fdec3d4872e9c3" - -[[package]] -name = "const_format" -version = "0.2.35" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7faa7469a93a566e9ccc1c73fe783b4a65c274c5ace346038dca9c39fe0030ad" -dependencies = [ - "const_format_proc_macros", -] - -[[package]] -name = "const_format_proc_macros" -version = "0.2.34" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1d57c2eccfb16dbac1f4e61e206105db5820c9d26c3c472bc17c774259ef7744" -dependencies = [ - "proc-macro2", - "quote", - "unicode-xid", -] - -[[package]] -name = "convert_case" -version = "0.10.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "633458d4ef8c78b72454de2d54fd6ab2e60f9e02be22f3c6104cdc8a4e0fceb9" -dependencies = [ - "unicode-segmentation", -] - -[[package]] -name = "core-foundation" -version = "0.9.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "91e195e091a93c46f7102ec7818a2aa394e1e1771c3ab4825963fa03e45afb8f" -dependencies = [ - "core-foundation-sys", - "libc", -] - -[[package]] -name = "core-foundation" -version = "0.10.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b2a6cd9ae233e7f62ba4e9353e81a88df7fc8a5987b8d445b4d90c879bd156f6" -dependencies = [ - "core-foundation-sys", - "libc", -] - -[[package]] -name = "core-foundation-sys" -version = "0.8.7" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "773648b94d0e5d620f64f280777445740e61fe701025087ec8b57f45c791888b" - -[[package]] -name = "core2" -version = "0.4.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b49ba7ef1ad6107f8824dbe97de947cbaac53c44e7f9756a1fba0d37c1eec505" -dependencies = [ - "memchr", -] - -[[package]] -name = "cow-utils" -version = "0.1.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "417bef24afe1460300965a25ff4a24b8b45ad011948302ec221e8a0a81eb2c79" - -[[package]] -name = "cpp_demangle" -version = "0.5.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0667304c32ea56cb4cd6d2d7c0cfe9a2f8041229db8c033af7f8d69492429def" -dependencies = [ - "cfg-if", -] - -[[package]] -name = "cpufeatures" -version = "0.2.17" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "59ed5838eebb26a2bb2e58f6d5b5316989ae9d08bab10e0e6d103e656d1b0280" -dependencies = [ - "libc", -] - -[[package]] -name = "cpufeatures" -version = "0.3.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8b2a41393f66f16b0823bb79094d54ac5fbd34ab292ddafb9a0456ac9f87d201" -dependencies = [ - "libc", -] - -[[package]] -name = "crc" -version = "3.4.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5eb8a2a1cd12ab0d987a5d5e825195d372001a4094a0376319d5a0ad71c1ba0d" -dependencies = [ - "crc-catalog", -] - -[[package]] -name = "crc-catalog" -version = "2.4.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "19d374276b40fb8bbdee95aef7c7fa6b5316ec764510eb64b8dd0e2ed0d7e7f5" - -[[package]] -name = "crc32fast" -version = "1.5.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9481c1c90cbf2ac953f07c8d4a58aa3945c425b7185c9154d67a65e4230da511" -dependencies = [ - "cfg-if", -] - -[[package]] -name = "criterion" -version = "0.7.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e1c047a62b0cc3e145fa84415a3191f628e980b194c2755aa12300a4e6cbd928" -dependencies = [ - "anes", - "cast", - "ciborium", - "clap", - "criterion-plot", - "itertools 0.13.0", - "num-traits", - "oorandom", - "plotters", - "rayon", - "regex", - "serde", - "serde_json", - "tinytemplate", - "walkdir", -] - -[[package]] -name = "criterion-plot" -version = "0.6.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9b1bcc0dc7dfae599d84ad0b1a55f80cde8af3725da8313b528da95ef783e338" -dependencies = [ - "cast", - "itertools 0.13.0", -] - -[[package]] -name = "critical-section" -version = "1.2.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "790eea4361631c5e7d22598ecd5723ff611904e3344ce8720784c93e3d83d40b" - -[[package]] -name = "crossbeam-channel" -version = "0.5.15" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "82b8f8f868b36967f9606790d1903570de9ceaf870a7bf9fbbd3016d636a2cb2" -dependencies = [ - "crossbeam-utils", -] - -[[package]] -name = "crossbeam-deque" -version = "0.8.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9dd111b7b7f7d55b72c0a6ae361660ee5853c9af73f70c3c2ef6858b950e2e51" -dependencies = [ - "crossbeam-epoch", - "crossbeam-utils", -] - -[[package]] -name = "crossbeam-epoch" -version = "0.9.18" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5b82ac4a3c2ca9c3460964f020e1402edd5753411d7737aa39c3714ad1b5420e" -dependencies = [ - "crossbeam-utils", -] - -[[package]] -name = "crossbeam-utils" -version = "0.8.21" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d0a5c400df2834b80a4c3327b3aad3a4c4cd4de0629063962b03235697506a28" - -[[package]] -name = "crossterm" -version = "0.29.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d8b9f2e4c67f833b660cdb0a3523065869fb35570177239812ed4c905aeff87b" -dependencies = [ - "bitflags 2.11.0", - "crossterm_winapi", - "derive_more", - "document-features", - "mio", - "parking_lot", - "rustix 1.1.3", - "signal-hook", - "signal-hook-mio", - "winapi", -] - -[[package]] -name = "crossterm_winapi" -version = "0.9.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "acdd7c62a3665c7f6830a51635d9ac9b23ed385797f70a83bb8bafe9c572ab2b" -dependencies = [ - "winapi", -] - -[[package]] -name = "crunchy" -version = "0.2.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "460fbee9c2c2f33933d720630a6a0bac33ba7053db5344fac858d4b8952d77d5" - -[[package]] -name = "crypto-bigint" -version = "0.5.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0dc92fb57ca44df6db8059111ab3af99a63d5d0f8375d9972e319a379c6bab76" -dependencies = [ - "generic-array", - "rand_core 0.6.4", - "subtle", - "zeroize", -] - -[[package]] -name = "crypto-common" -version = "0.1.7" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "78c8292055d1c1df0cce5d180393dc8cce0abec0a7102adb6c7b1eef6016d60a" -dependencies = [ - "generic-array", - "rand_core 0.6.4", - "typenum", -] - -[[package]] -name = "crypto-common" -version = "0.2.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "77727bb15fa921304124b128af125e7e3b968275d1b108b379190264f4423710" -dependencies = [ - "hybrid-array", -] - -[[package]] -name = "csv" -version = "1.4.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "52cd9d68cf7efc6ddfaaee42e7288d3a99d613d4b50f76ce9827ae0c6e14f938" -dependencies = [ - "csv-core", - "itoa", - "ryu", - "serde_core", -] - -[[package]] -name = "csv-core" -version = "0.1.13" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "704a3c26996a80471189265814dbc2c257598b96b8a7feae2d31ace646bb9782" -dependencies = [ - "memchr", -] - -[[package]] -name = "ctr" -version = "0.9.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0369ee1ad671834580515889b80f2ea915f23b8be8d0daa4bbaf2ac5c7590835" -dependencies = [ - "cipher", -] - -[[package]] -name = "ctutils" -version = "0.4.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1005a6d4446f5120ef475ad3d2af2b30c49c2c9c6904258e3bb30219bebed5e4" -dependencies = [ - "cmov", -] - -[[package]] -name = "curve25519-dalek" -version = "4.1.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "97fb8b7c4503de7d6ae7b42ab72a5a59857b4c937ec27a3d4539dba95b5ab2be" -dependencies = [ - "cfg-if", - "cpufeatures 0.2.17", - "curve25519-dalek-derive", - "digest 0.10.7", - "fiat-crypto", - "rustc_version 0.4.1", - "subtle", - "zeroize", -] - -[[package]] -name = "curve25519-dalek-derive" -version = "0.1.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f46882e17999c6cc590af592290432be3bce0428cb0d5f8b6715e4dc7b383eb3" -dependencies = [ - "proc-macro2", - "quote", - "syn 2.0.117", -] - -[[package]] -name = "curve25519-dalek-ng" -version = "4.1.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1c359b7249347e46fb28804470d071c921156ad62b3eef5d34e2ba867533dec8" -dependencies = [ - "byteorder", - "digest 0.9.0", - "rand_core 0.6.4", - "subtle-ng", - "zeroize", -] - -[[package]] -name = "darling" -version = "0.20.11" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fc7f46116c46ff9ab3eb1597a45688b6715c6e628b5c133e288e709a29bcb4ee" -dependencies = [ - "darling_core 0.20.11", - "darling_macro 0.20.11", -] - -[[package]] -name = "darling" -version = "0.21.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9cdf337090841a411e2a7f3deb9187445851f91b309c0c0a29e05f74a00a48c0" -dependencies = [ - "darling_core 0.21.3", - "darling_macro 0.21.3", -] - -[[package]] -name = "darling" -version = "0.23.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "25ae13da2f202d56bd7f91c25fba009e7717a1e4a1cc98a76d844b65ae912e9d" -dependencies = [ - "darling_core 0.23.0", - "darling_macro 0.23.0", -] - -[[package]] -name = "darling_core" -version = "0.20.11" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0d00b9596d185e565c2207a0b01f8bd1a135483d02d9b7b0a54b11da8d53412e" -dependencies = [ - "fnv", - "ident_case", - "proc-macro2", - "quote", - "strsim", - "syn 2.0.117", -] - -[[package]] -name = "darling_core" -version = "0.21.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1247195ecd7e3c85f83c8d2a366e4210d588e802133e1e355180a9870b517ea4" -dependencies = [ - "fnv", - "ident_case", - "proc-macro2", - "quote", - "serde", - "strsim", - "syn 2.0.117", -] - -[[package]] -name = "darling_core" -version = "0.23.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9865a50f7c335f53564bb694ef660825eb8610e0a53d3e11bf1b0d3df31e03b0" -dependencies = [ - "ident_case", - "proc-macro2", - "quote", - "strsim", - "syn 2.0.117", -] - -[[package]] -name = "darling_macro" -version = "0.20.11" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fc34b93ccb385b40dc71c6fceac4b2ad23662c7eeb248cf10d529b7e055b6ead" -dependencies = [ - "darling_core 0.20.11", - "quote", - "syn 2.0.117", -] - -[[package]] -name = "darling_macro" -version = "0.21.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d38308df82d1080de0afee5d069fa14b0326a88c14f15c5ccda35b4a6c414c81" -dependencies = [ - "darling_core 0.21.3", - "quote", - "syn 2.0.117", -] - -[[package]] -name = "darling_macro" -version = "0.23.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ac3984ec7bd6cfa798e62b4a642426a5be0e68f9401cfc2a01e3fa9ea2fcdb8d" -dependencies = [ - "darling_core 0.23.0", - "quote", - "syn 2.0.117", -] - -[[package]] -name = "dashmap" -version = "6.1.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5041cc499144891f3790297212f32a74fb938e5136a14943f338ef9e0ae276cf" -dependencies = [ - "arbitrary", - "cfg-if", - "crossbeam-utils", - "hashbrown 0.14.5", - "lock_api", - "once_cell", - "parking_lot_core", - "serde", -] - -[[package]] -name = "data-encoding" -version = "2.10.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d7a1e2f27636f116493b8b860f5546edb47c8d8f8ea73e1d2a20be88e28d1fea" - -[[package]] -name = "data-encoding-macro" -version = "0.1.19" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8142a83c17aa9461d637e649271eae18bf2edd00e91f2e105df36c3c16355bdb" -dependencies = [ - "data-encoding", - "data-encoding-macro-internal", -] - -[[package]] -name = "data-encoding-macro-internal" -version = "0.1.17" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7ab67060fc6b8ef687992d439ca0fa36e7ed17e9a0b16b25b601e8757df720de" -dependencies = [ - "data-encoding", - "syn 2.0.117", -] - -[[package]] -name = "deadpool" -version = "0.12.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0be2b1d1d6ec8d846f05e137292d0b89133caf95ef33695424c09568bdd39b1b" -dependencies = [ - "deadpool-runtime", - "lazy_static", - "num_cpus", - "tokio", -] - -[[package]] -name = "deadpool-runtime" -version = "0.1.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "092966b41edc516079bdf31ec78a2e0588d1d0c08f78b91d8307215928642b2b" - -[[package]] -name = "debug-helper" -version = "0.3.13" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f578e8e2c440e7297e008bb5486a3a8a194775224bbc23729b0dbdfaeebf162e" - -[[package]] -name = "debugid" -version = "0.8.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bef552e6f588e446098f6ba40d89ac146c8c7b64aade83c051ee00bb5d2bc18d" -dependencies = [ - "uuid", -] - -[[package]] -name = "delay_map" -version = "0.4.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "88e365f083a5cb5972d50ce8b1b2c9f125dc5ec0f50c0248cfb568ae59efcf0b" -dependencies = [ - "futures", - "tokio", - "tokio-util", -] - -[[package]] -name = "der" -version = "0.7.10" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e7c1832837b905bbfb5101e07cc24c8deddf52f93225eee6ead5f4d63d53ddcb" -dependencies = [ - "const-oid 0.9.6", - "pem-rfc7468", - "zeroize", -] - -[[package]] -name = "der" -version = "0.8.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "71fd89660b2dc699704064e59e9dba0147b903e85319429e131620d022be411b" -dependencies = [ - "const-oid 0.10.2", - "zeroize", -] - -[[package]] -name = "der-parser" -version = "10.0.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "07da5016415d5a3c4dd39b11ed26f915f52fc4e0dc197d87908bc916e51bc1a6" -dependencies = [ - "asn1-rs", - "displaydoc", - "nom", - "num-bigint", - "num-traits", - "rusticata-macros", -] - -[[package]] -name = "deranged" -version = "0.5.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cc3dc5ad92c2e2d1c193bbbbdf2ea477cb81331de4f3103f267ca18368b988c4" -dependencies = [ - "powerfmt", - "serde_core", -] - -[[package]] -name = "derivative" -version = "2.2.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fcc3dd5e9e9c0b295d6e1e4d811fb6f157d5ffd784b8d202fc62eac8035a770b" -dependencies = [ - "proc-macro2", - "quote", - "syn 1.0.109", -] - -[[package]] -name = "derive-where" -version = "1.6.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ef941ded77d15ca19b40374869ac6000af1c9f2a4c0f3d4c70926287e6364a8f" -dependencies = [ - "proc-macro2", - "quote", - "syn 2.0.117", -] - -[[package]] -name = "derive_arbitrary" -version = "1.4.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1e567bd82dcff979e4b03460c307b3cdc9e96fde3d73bed1496d2bc75d9dd62a" -dependencies = [ - "proc-macro2", - "quote", - "syn 2.0.117", -] - -[[package]] -name = "derive_builder" -version = "0.20.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "507dfb09ea8b7fa618fcf76e953f4f5e192547945816d5358edffe39f6f94947" -dependencies = [ - "derive_builder_macro", -] - -[[package]] -name = "derive_builder_core" -version = "0.20.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2d5bcf7b024d6835cfb3d473887cd966994907effbe9227e8c8219824d06c4e8" -dependencies = [ - "darling 0.20.11", - "proc-macro2", - "quote", - "syn 2.0.117", -] - -[[package]] -name = "derive_builder_macro" -version = "0.20.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ab63b0e2bf4d5928aff72e83a7dace85d7bba5fe12dcc3c5a572d78caffd3f3c" -dependencies = [ - "derive_builder_core", - "syn 2.0.117", -] - -[[package]] -name = "derive_more" -version = "2.1.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d751e9e49156b02b44f9c1815bcb94b984cdcc4396ecc32521c739452808b134" -dependencies = [ - "derive_more-impl", -] - -[[package]] -name = "derive_more-impl" -version = "2.1.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "799a97264921d8623a957f6c3b9011f3b5492f557bbb7a5a19b7fa6d06ba8dcb" -dependencies = [ - "convert_case", - "proc-macro2", - "quote", - "rustc_version 0.4.1", - "syn 2.0.117", - "unicode-xid", -] - -[[package]] -name = "diff" -version = "0.1.13" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "56254986775e3233ffa9c4d7d3faaf6d36a2c09d30b20687e9f88bc8bafc16c8" - -[[package]] -name = "difflib" -version = "0.4.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6184e33543162437515c2e2b48714794e37845ec9851711914eec9d308f6ebe8" - -[[package]] -name = "digest" -version = "0.9.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d3dd60d1080a57a05ab032377049e0591415d2b31afd7028356dbf3cc6dcb066" -dependencies = [ - "generic-array", -] - -[[package]] -name = "digest" -version = "0.10.7" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9ed9a281f7bc9b7576e61468ba615a66a5c8cfdff42420a70aa82701a3b1e292" -dependencies = [ - "block-buffer 0.10.4", - "const-oid 0.9.6", - "crypto-common 0.1.7", - "subtle", -] - -[[package]] -name = "digest" -version = "0.11.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4850db49bf08e663084f7fb5c87d202ef91a3907271aff24a94eb97ff039153c" -dependencies = [ - "block-buffer 0.12.0", - "crypto-common 0.2.1", - "ctutils", -] - -[[package]] -name = "directories" -version = "5.0.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9a49173b84e034382284f27f1af4dcbbd231ffa358c0fe316541a7337f376a35" -dependencies = [ - "dirs-sys 0.4.1", -] - -[[package]] -name = "dirs" -version = "6.0.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c3e8aa94d75141228480295a7d0e7feb620b1a5ad9f12bc40be62411e38cce4e" -dependencies = [ - "dirs-sys 0.5.0", -] - -[[package]] -name = "dirs-next" -version = "2.0.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b98cf8ebf19c3d1b223e151f99a4f9f0690dca41414773390fc824184ac833e1" -dependencies = [ - "cfg-if", - "dirs-sys-next", -] - -[[package]] -name = "dirs-sys" -version = "0.4.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "520f05a5cbd335fae5a99ff7a6ab8627577660ee5cfd6a94a6a929b52ff0321c" -dependencies = [ - "libc", - "option-ext", - "redox_users 0.4.6", - "windows-sys 0.48.0", -] - -[[package]] -name = "dirs-sys" -version = "0.5.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e01a3366d27ee9890022452ee61b2b63a67e6f13f58900b651ff5665f0bb1fab" -dependencies = [ - "libc", - "option-ext", - "redox_users 0.5.2", - "windows-sys 0.59.0", -] - -[[package]] -name = "dirs-sys-next" -version = "0.1.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4ebda144c4fe02d1f7ea1a7d9641b6fc6b580adcfa024ae48797ecdeb6825b4d" -dependencies = [ - "libc", - "redox_users 0.4.6", - "winapi", -] - -[[package]] -name = "discv5" -version = "0.10.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f170f4f6ed0e1df52bf43b403899f0081917ecf1500bfe312505cc3b515a8899" -dependencies = [ - "aes", - "aes-gcm", - "alloy-rlp", - "arrayvec", - "ctr", - "delay_map", - "enr", - "fnv", - "futures", - "hashlink 0.9.1", - "hex", - "hkdf", - "lazy_static", - "libp2p-identity", - "lru 0.12.5", - "more-asserts", - "multiaddr", - "parking_lot", - "rand 0.8.5", - "smallvec", - "socket2 0.5.10", - "tokio", - "tracing", - "uint 0.10.0", - "zeroize", -] - -[[package]] -name = "displaydoc" -version = "0.2.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "97369cbbc041bc366949bc74d34658d6cda5621039731c6310521892a3a20ae0" -dependencies = [ - "proc-macro2", - "quote", - "syn 2.0.117", -] - -[[package]] -name = "doctest-file" -version = "1.0.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "aac81fa3e28d21450aa4d2ac065992ba96a1d7303efbce51a95f4fd175b67562" - -[[package]] -name = "document-features" -version = "0.2.12" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d4b8a88685455ed29a21542a33abd9cb6510b6b129abadabdcef0f4c55bc8f61" -dependencies = [ - "litrs", -] - -[[package]] -name = "dotenvy" -version = "0.15.7" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1aaf95b3e5c8f23aa320147307562d361db0ae0d51242340f558153b4eb2439b" - -[[package]] -name = "downcast" -version = "0.11.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1435fa1053d8b2fbbe9be7e97eca7f33d37b28409959813daefc1446a14247f1" - -[[package]] -name = "dtoa" -version = "1.0.11" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4c3cf4824e2d5f025c7b531afcb2325364084a16806f6d47fbc1f5fbd9960590" - -[[package]] -name = "dunce" -version = "1.0.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "92773504d58c093f6de2459af4af33faa518c13451eb8f2b5698ed3d36e7c813" - -[[package]] -name = "dyn-clone" -version = "1.0.20" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d0881ea181b1df73ff77ffaaf9c7544ecc11e82fba9b5f27b262a3c73a332555" - -[[package]] -name = "dynify" -version = "0.1.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "81acb15628a3e22358bf73de5e7e62360b8a777dbcb5fc9ac7dfa9ae73723747" -dependencies = [ - "dynify-macros", -] - -[[package]] -name = "dynify-macros" -version = "0.1.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1ec431cd708430d5029356535259c5d645d60edd3d39c54e5eea9782d46caa7d" -dependencies = [ - "proc-macro2", - "quote", - "syn 2.0.117", -] - -[[package]] -name = "ecdsa" -version = "0.16.9" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ee27f32b5c5292967d2d4a9d7f1e0b0aed2c15daded5a60300e4abb9d8020bca" -dependencies = [ - "der 0.7.10", - "digest 0.10.7", - "elliptic-curve", - "rfc6979", - "serdect", - "signature 2.2.0", - "spki 0.7.3", -] - -[[package]] -name = "ed25519" -version = "2.2.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "115531babc129696a58c64a4fef0a8bf9e9698629fb97e9e40767d235cfbcd53" -dependencies = [ - "pkcs8 0.10.2", - "signature 2.2.0", -] - -[[package]] -name = "ed25519-consensus" -version = "2.1.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3c8465edc8ee7436ffea81d21a019b16676ee3db267aa8d5a8d729581ecf998b" -dependencies = [ - "curve25519-dalek-ng", - "hex", - "rand_core 0.6.4", - "serde", - "sha2 0.9.9", - "thiserror 1.0.69", - "zeroize", -] - -[[package]] -name = "ed25519-dalek" -version = "2.2.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "70e796c081cee67dc755e1a36a0a172b897fab85fc3f6bc48307991f64e4eca9" -dependencies = [ - "curve25519-dalek", - "ed25519", - "rand_core 0.6.4", - "serde", - "sha2 0.10.9", - "subtle", - "zeroize", -] - -[[package]] -name = "educe" -version = "0.6.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1d7bc049e1bd8cdeb31b68bbd586a9464ecf9f3944af3958a7a9d0f8b9799417" -dependencies = [ - "enum-ordinalize", - "proc-macro2", - "quote", - "syn 2.0.117", -] - -[[package]] -name = "either" -version = "1.15.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "48c757948c5ede0e46177b7add2e67155f70e33c07fea8284df6576da70b3719" -dependencies = [ - "serde", -] - -[[package]] -name = "elliptic-curve" -version = "0.13.8" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b5e6043086bf7973472e0c7dff2142ea0b680d30e18d9cc40f267efbf222bd47" -dependencies = [ - "base16ct", - "crypto-bigint", - "digest 0.10.7", - "ff", - "generic-array", - "group", - "pem-rfc7468", - "pkcs8 0.10.2", - "rand_core 0.6.4", - "sec1", - "serdect", - "subtle", - "zeroize", -] - -[[package]] -name = "enr" -version = "0.13.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "851bd664a3d3a3c175cff92b2f0df02df3c541b4895d0ae307611827aae46152" -dependencies = [ - "alloy-rlp", - "base64 0.22.1", - "bytes", - "ed25519-dalek", - "hex", - "k256", - "log", - "rand 0.8.5", - "secp256k1 0.30.0", - "serde", - "sha3 0.10.8", - "zeroize", -] - -[[package]] -name = "enum-as-inner" -version = "0.6.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a1e6a265c649f3f5979b601d26f1d05ada116434c87741c9493cb56218f76cbc" -dependencies = [ - "heck", - "proc-macro2", - "quote", - "syn 2.0.117", -] - -[[package]] -name = "enum-ordinalize" -version = "4.3.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4a1091a7bb1f8f2c4b28f1fe2cef4980ca2d410a3d727d67ecc3178c9b0800f0" -dependencies = [ - "enum-ordinalize-derive", -] - -[[package]] -name = "enum-ordinalize-derive" -version = "4.3.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8ca9601fb2d62598ee17836250842873a413586e5d7ed88b356e38ddbb0ec631" -dependencies = [ - "proc-macro2", - "quote", - "syn 2.0.117", -] - -[[package]] -name = "equator" -version = "0.4.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4711b213838dfee0117e3be6ac926007d7f433d7bbe33595975d4190cb07e6fc" -dependencies = [ - "equator-macro", -] - -[[package]] -name = "equator-macro" -version = "0.4.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "44f23cf4b44bfce11a86ace86f8a73ffdec849c9fd00a386a53d278bd9e81fb3" -dependencies = [ - "proc-macro2", - "quote", - "syn 2.0.117", -] - -[[package]] -name = "equivalent" -version = "1.0.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "877a4ace8713b0bcf2a4e7eec82529c029f1d0619886d18145fea96c3ffe5c0f" - -[[package]] -name = "errno" -version = "0.3.14" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "39cab71617ae0d63f51a36d69f866391735b51691dbda63cf6f96d042b63efeb" -dependencies = [ - "libc", - "windows-sys 0.59.0", -] - -[[package]] -name = "ethereum_hashing" -version = "0.7.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c853bd72c9e5787f8aafc3df2907c2ed03cff3150c3acd94e2e53a98ab70a8ab" -dependencies = [ - "cpufeatures 0.2.17", - "ring", - "sha2 0.10.9", -] - -[[package]] -name = "ethereum_serde_utils" -version = "0.8.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3dc1355dbb41fbbd34ec28d4fb2a57d9a70c67ac3c19f6a5ca4d4a176b9e997a" -dependencies = [ - "alloy-primitives", - "hex", - "serde", - "serde_derive", - "serde_json", -] - -[[package]] -name = "ethereum_ssz" -version = "0.9.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0dcddb2554d19cde19b099fadddde576929d7a4d0c1cd3512d1fd95cf174375c" -dependencies = [ - "alloy-primitives", - "ethereum_serde_utils", - "itertools 0.13.0", - "serde", - "serde_derive", - "smallvec", - "typenum", -] - -[[package]] -name = "ethereum_ssz" -version = "0.10.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2128a84f7a3850d54ee343334e3392cca61f9f6aa9441eec481b9394b43c238b" -dependencies = [ - "alloy-primitives", - "ethereum_serde_utils", - "itertools 0.14.0", - "serde", - "serde_derive", - "smallvec", - "typenum", -] - -[[package]] -name = "ethereum_ssz_derive" -version = "0.9.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a657b6b3b7e153637dc6bdc6566ad9279d9ee11a15b12cfb24a2e04360637e9f" -dependencies = [ - "darling 0.20.11", - "proc-macro2", - "quote", - "syn 2.0.117", -] - -[[package]] -name = "ethereum_ssz_derive" -version = "0.10.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cd596f91cff004fc8d02be44c21c0f9b93140a04b66027ae052f5f8e05b48eba" -dependencies = [ - "darling 0.23.0", - "proc-macro2", - "quote", - "syn 2.0.117", -] - -[[package]] -name = "event-listener" -version = "5.4.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e13b66accf52311f30a0db42147dadea9850cb48cd070028831ae5f5d4b856ab" -dependencies = [ - "concurrent-queue", - "parking", - "pin-project-lite", -] - -[[package]] -name = "event-listener-strategy" -version = "0.5.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8be9f3dfaaffdae2972880079a491a1a8bb7cbed0b8dd7a347f668b4150a3b93" -dependencies = [ - "event-listener", - "pin-project-lite", -] - -[[package]] -name = "eyre" -version = "0.6.12" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7cd915d99f24784cdc19fd37ef22b97e3ff0ae756c7e492e9fbfe897d61e2aec" -dependencies = [ - "indenter", - "once_cell", -] - -[[package]] -name = "fast-float2" -version = "0.2.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f8eb564c5c7423d25c886fb561d1e4ee69f72354d16918afa32c08811f6b6a55" - -[[package]] -name = "fastrand" -version = "2.3.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "37909eebbb50d72f9059c3b6d82c0463f2ff062c9e95845c43a6c9c0355411be" - -[[package]] -name = "fastrlp" -version = "0.3.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "139834ddba373bbdd213dffe02c8d110508dcf1726c2be27e8d1f7d7e1856418" -dependencies = [ - "arrayvec", - "auto_impl", - "bytes", -] - -[[package]] -name = "fastrlp" -version = "0.4.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ce8dba4714ef14b8274c371879b175aa55b16b30f269663f19d576f380018dc4" -dependencies = [ - "arrayvec", - "auto_impl", - "bytes", -] - -[[package]] -name = "fdlimit" -version = "0.3.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e182f7dbc2ef73d9ef67351c5fbbea084729c48362d3ce9dd44c28e32e277fe5" -dependencies = [ - "libc", - "thiserror 1.0.69", -] - -[[package]] -name = "ff" -version = "0.13.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c0b50bfb653653f9ca9095b427bed08ab8d75a137839d9ad64eb11810d5b6393" -dependencies = [ - "rand_core 0.6.4", - "subtle", -] - -[[package]] -name = "fiat-crypto" -version = "0.2.9" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "28dea519a9695b9977216879a3ebfddf92f1c08c05d984f8996aecd6ecdc811d" - -[[package]] -name = "filetime" -version = "0.2.27" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f98844151eee8917efc50bd9e8318cb963ae8b297431495d3f758616ea5c57db" -dependencies = [ - "cfg-if", - "libc", - "libredox", -] - -[[package]] -name = "find-msvc-tools" -version = "0.1.9" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5baebc0774151f905a1a2cc41989300b1e6fbb29aff0ceffa1064fdd3088d582" - -[[package]] -name = "findshlibs" -version = "0.10.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "40b9e59cd0f7e0806cca4be089683ecb6434e602038df21fe6bf6711b2f07f64" -dependencies = [ - "cc", - "lazy_static", - "libc", - "winapi", -] - -[[package]] -name = "fixed-cache" -version = "0.1.8" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c41c7aa69c00ebccf06c3fa7ffe2a6cf26a58b5fe4deabfe646285ff48136a8f" -dependencies = [ - "equivalent", - "rapidhash", - "typeid", -] - -[[package]] -name = "fixed-hash" -version = "0.8.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "835c052cb0c08c1acf6ffd71c022172e18723949c8282f2b9f27efbc51e64534" -dependencies = [ - "byteorder", - "rand 0.8.5", - "rustc-hex", - "static_assertions", -] - -[[package]] -name = "fixed-map" -version = "0.9.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "86ed19add84e8cb9e8cc5f7074de0324247149ffef0b851e215fb0edc50c229b" -dependencies = [ - "fixed-map-derive", - "serde", -] - -[[package]] -name = "fixed-map-derive" -version = "0.9.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6dc7a9cb3326bafb80642c5ce99b39a2c0702d4bfa8ee8a3e773791a6cbe2407" -dependencies = [ - "proc-macro2", - "quote", - "syn 2.0.117", -] - -[[package]] -name = "fixedbitset" -version = "0.4.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0ce7134b9999ecaf8bcd65542e436736ef32ddca1b3e06094cb6ec5755203b80" - -[[package]] -name = "fixedbitset" -version = "0.5.7" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1d674e81391d1e1ab681a28d99df07927c6d4aa5b027d7da16ba32d1d21ecd99" - -[[package]] -name = "flate2" -version = "1.1.9" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "843fba2746e448b37e26a819579957415c8cef339bf08564fe8b7ddbd959573c" -dependencies = [ - "crc32fast", - "libz-sys", - "miniz_oxide", -] - -[[package]] -name = "float-cmp" -version = "0.10.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b09cf3155332e944990140d967ff5eceb70df778b34f77d8075db46e4704e6d8" -dependencies = [ - "num-traits", -] - -[[package]] -name = "float16" -version = "0.1.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7bffafbd079d520191c7c2779ae9cf757601266cf4167d3f659ff09617ff8483" -dependencies = [ - "cfg-if", - "rustc_version 0.2.3", -] - -[[package]] -name = "fnv" -version = "1.0.7" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3f9eec918d3f24069decb9af1554cad7c880e2da24a9afd88aca000531ab82c1" - -[[package]] -name = "foldhash" -version = "0.1.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d9c4f5dac5e15c24eb999c26181a6ca40b39fe946cbe4c263c7209467bc83af2" - -[[package]] -name = "foldhash" -version = "0.2.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "77ce24cb58228fbb8aa041425bb1050850ac19177686ea6e0f41a70416f56fdb" - -[[package]] -name = "foreign-types" -version = "0.3.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f6f339eb8adc052cd2ca78910fda869aefa38d22d5cb648e6485e4d3fc06f3b1" -dependencies = [ - "foreign-types-shared", -] - -[[package]] -name = "foreign-types-shared" -version = "0.1.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "00b0228411908ca8685dba7fc2cdd70ec9990a6e753e89b6ac91a84c40fbaf4b" - -[[package]] -name = "form_urlencoded" -version = "1.2.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cb4cb245038516f5f85277875cdaa4f7d2c9a0fa0468de06ed190163b1581fcf" -dependencies = [ - "percent-encoding", -] - -[[package]] -name = "fragile" -version = "2.0.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "28dd6caf6059519a65843af8fe2a3ae298b14b80179855aeb4adc2c1934ee619" - -[[package]] -name = "fsevent-sys" -version = "4.1.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "76ee7a02da4d231650c7cea31349b889be2f45ddb3ef3032d2ec8185f6313fd2" -dependencies = [ - "libc", -] - -[[package]] -name = "funty" -version = "2.0.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e6d5a32815ae3f33302d95fdcb2ce17862f8c65363dcfd29360480ba1001fc9c" - -[[package]] -name = "futures" -version = "0.3.32" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8b147ee9d1f6d097cef9ce628cd2ee62288d963e16fb287bd9286455b241382d" -dependencies = [ - "futures-channel", - "futures-core", - "futures-executor", - "futures-io", - "futures-sink", - "futures-task", - "futures-util", -] - -[[package]] -name = "futures-bounded" -version = "0.2.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "91f328e7fb845fc832912fb6a34f40cf6d1888c92f974d1893a54e97b5ff542e" -dependencies = [ - "futures-timer", - "futures-util", -] - -[[package]] -name = "futures-channel" -version = "0.3.32" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "07bbe89c50d7a535e539b8c17bc0b49bdb77747034daa8087407d655f3f7cc1d" -dependencies = [ - "futures-core", - "futures-sink", -] - -[[package]] -name = "futures-concurrency" -version = "7.7.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "175cd8cca9e1d45b87f18ffa75088f2099e3c4fe5e2f83e42de112560bea8ea6" -dependencies = [ - "fixedbitset 0.5.7", - "futures-core", - "futures-lite", - "pin-project", - "smallvec", -] - -[[package]] -name = "futures-core" -version = "0.3.32" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7e3450815272ef58cec6d564423f6e755e25379b217b0bc688e295ba24df6b1d" - -[[package]] -name = "futures-executor" -version = "0.3.32" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "baf29c38818342a3b26b5b923639e7b1f4a61fc5e76102d4b1981c6dc7a7579d" -dependencies = [ - "futures-core", - "futures-task", - "futures-util", -] - -[[package]] -name = "futures-io" -version = "0.3.32" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cecba35d7ad927e23624b22ad55235f2239cfa44fd10428eecbeba6d6a717718" - -[[package]] -name = "futures-lite" -version = "2.6.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f78e10609fe0e0b3f4157ffab1876319b5b0db102a2c60dc4626306dc46b44ad" -dependencies = [ - "fastrand", - "futures-core", - "futures-io", - "parking", - "pin-project-lite", -] - -[[package]] -name = "futures-macro" -version = "0.3.32" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e835b70203e41293343137df5c0664546da5745f82ec9b84d40be8336958447b" -dependencies = [ - "proc-macro2", - "quote", - "syn 2.0.117", -] - -[[package]] -name = "futures-rustls" -version = "0.26.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a8f2f12607f92c69b12ed746fabf9ca4f5c482cba46679c1a75b874ed7c26adb" -dependencies = [ - "futures-io", - "rustls", - "rustls-pki-types", -] - -[[package]] -name = "futures-sink" -version = "0.3.32" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c39754e157331b013978ec91992bde1ac089843443c49cbc7f46150b0fad0893" - -[[package]] -name = "futures-task" -version = "0.3.32" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "037711b3d59c33004d3856fbdc83b99d4ff37a24768fa1be9ce3538a1cde4393" - -[[package]] -name = "futures-timer" -version = "3.0.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f288b0a4f20f9a56b5d1da57e2227c661b7b16168e2f72365f57b63326e29b24" -dependencies = [ - "gloo-timers", - "send_wrapper 0.4.0", -] - -[[package]] -name = "futures-util" -version = "0.3.32" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "389ca41296e6190b48053de0321d02a77f32f8a5d2461dd38762c0593805c6d6" -dependencies = [ - "futures-channel", - "futures-core", - "futures-io", - "futures-macro", - "futures-sink", - "futures-task", - "memchr", - "pin-project-lite", - "slab", -] - -[[package]] -name = "futures-utils-wasm" -version = "0.1.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "42012b0f064e01aa58b545fe3727f90f7dd4020f4a3ea735b50344965f5a57e9" - -[[package]] -name = "genawaiter" -version = "0.99.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c86bd0361bcbde39b13475e6e36cb24c329964aa2611be285289d1e4b751c1a0" -dependencies = [ - "genawaiter-macro", -] - -[[package]] -name = "genawaiter-macro" -version = "0.99.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0b32dfe1fdfc0bbde1f22a5da25355514b5e450c33a6af6770884c8750aedfbc" - -[[package]] -name = "generic-array" -version = "0.14.7" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "85649ca51fd72272d7821adaf274ad91c288277713d9c18820d8499a7ff69e9a" -dependencies = [ - "typenum", - "version_check", - "zeroize", -] - -[[package]] -name = "getrandom" -version = "0.2.17" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ff2abc00be7fca6ebc474524697ae276ad847ad0a6b3faa4bcb027e9a4614ad0" -dependencies = [ - "cfg-if", - "js-sys", - "libc", - "wasi", - "wasm-bindgen", -] - -[[package]] -name = "getrandom" -version = "0.3.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "899def5c37c4fd7b2664648c28120ecec138e4d395b459e5ca34f9cce2dd77fd" -dependencies = [ - "cfg-if", - "js-sys", - "libc", - "r-efi", - "wasip2", - "wasm-bindgen", -] - -[[package]] -name = "getrandom" -version = "0.4.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "139ef39800118c7683f2fd3c98c1b23c09ae076556b435f8e9064ae108aaeeec" -dependencies = [ - "cfg-if", - "libc", - "r-efi", - "rand_core 0.10.0", - "wasip2", - "wasip3", -] - -[[package]] -name = "ghash" -version = "0.5.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f0d8a4362ccb29cb0b265253fb0a2728f592895ee6854fd9bc13f2ffda266ff1" -dependencies = [ - "opaque-debug", - "polyval", -] - -[[package]] -name = "gimli" -version = "0.32.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e629b9b98ef3dd8afe6ca2bd0f89306cec16d43d907889945bc5d6687f2f13c7" - -[[package]] -name = "git2" -version = "0.20.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7b88256088d75a56f8ecfa070513a775dd9107f6530ef14919dac831af9cfe2b" -dependencies = [ - "bitflags 2.11.0", - "libc", - "libgit2-sys", - "log", - "url", -] - -[[package]] -name = "glob" -version = "0.3.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0cc23270f6e1808e30a928bdc84dea0b9b4136a8bc82338574f23baf47bbd280" - -[[package]] -name = "gloo-net" -version = "0.6.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c06f627b1a58ca3d42b45d6104bf1e1a03799df472df00988b6ba21accc10580" -dependencies = [ - "futures-channel", - "futures-core", - "futures-sink", - "gloo-utils", - "http", - "js-sys", - "pin-project", - "serde", - "serde_json", - "thiserror 1.0.69", - "wasm-bindgen", - "wasm-bindgen-futures", - "web-sys", -] - -[[package]] -name = "gloo-timers" -version = "0.2.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9b995a66bb87bebce9a0f4a95aed01daca4872c050bfcb21653361c03bc35e5c" -dependencies = [ - "futures-channel", - "futures-core", - "js-sys", - "wasm-bindgen", -] - -[[package]] -name = "gloo-utils" -version = "0.2.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0b5555354113b18c547c1d3a98fbf7fb32a9ff4f6fa112ce823a21641a0ba3aa" -dependencies = [ - "js-sys", - "serde", - "serde_json", - "wasm-bindgen", - "web-sys", -] - -[[package]] -name = "governor" -version = "0.8.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "be93b4ec2e4710b04d9264c0c7350cdd62a8c20e5e4ac732552ebb8f0debe8eb" -dependencies = [ - "cfg-if", - "dashmap", - "futures-sink", - "futures-timer", - "futures-util", - "getrandom 0.3.4", - "no-std-compat", - "nonzero_ext", - "parking_lot", - "portable-atomic", - "quanta", - "rand 0.9.4", - "smallvec", - "spinning_top", - "web-time", -] - -[[package]] -name = "group" -version = "0.13.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f0f9ef7462f7c099f518d754361858f86d8a07af53ba9af0fe635bbccb151a63" -dependencies = [ - "ff", - "rand_core 0.6.4", - "subtle", -] - -[[package]] -name = "h2" -version = "0.4.13" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2f44da3a8150a6703ed5d34e164b875fd14c2cdab9af1252a9a1020bde2bdc54" -dependencies = [ - "atomic-waker", - "bytes", - "fnv", - "futures-core", - "futures-sink", - "http", - "indexmap 2.13.0", - "slab", - "tokio", - "tokio-util", - "tracing", -] - -[[package]] -name = "half" -version = "2.7.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6ea2d84b969582b4b1864a92dc5d27cd2b77b622a8d79306834f1be5ba20d84b" -dependencies = [ - "cfg-if", - "crunchy", - "zerocopy", -] - -[[package]] -name = "handlebars" -version = "4.5.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "faa67bab9ff362228eb3d00bd024a4965d8231bbb7921167f0cfa66c6626b225" -dependencies = [ - "log", - "pest", - "pest_derive", - "serde", - "serde_json", - "thiserror 1.0.69", -] - -[[package]] -name = "hash-db" -version = "0.15.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d23bd4e7b5eda0d0f3a307e8b381fdc8ba9000f26fbe912250c0a4cc3956364a" - -[[package]] -name = "hashbrown" -version = "0.12.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8a9ee70c43aaf417c914396645a0fa852624801b24ebb7ae78fe8272889ac888" - -[[package]] -name = "hashbrown" -version = "0.13.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "43a3c133739dddd0d2990f9a4bdf8eb4b21ef50e4851ca85ab661199821d510e" - -[[package]] -name = "hashbrown" -version = "0.14.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e5274423e17b7c9fc20b6e7e208532f9b19825d82dfd615708b70edd83df41f1" -dependencies = [ - "ahash", -] - -[[package]] -name = "hashbrown" -version = "0.15.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9229cfe53dfd69f0609a49f65461bd93001ea1ef889cd5529dd176593f5338a1" -dependencies = [ - "allocator-api2", - "equivalent", - "foldhash 0.1.5", -] - -[[package]] -name = "hashbrown" -version = "0.16.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "841d1cc9bed7f9236f321df977030373f4a4163ae1a7dbfe1a51a2c1a51d9100" -dependencies = [ - "allocator-api2", - "equivalent", - "foldhash 0.2.0", - "serde", - "serde_core", -] - -[[package]] -name = "hashlink" -version = "0.9.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6ba4ff7128dee98c7dc9794b6a411377e1404dba1c97deb8d1a55297bd25d8af" -dependencies = [ - "hashbrown 0.14.5", -] - -[[package]] -name = "hashlink" -version = "0.10.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7382cf6263419f2d8df38c55d7da83da5c18aef87fc7a7fc1fb1e344edfe14c1" -dependencies = [ - "hashbrown 0.15.5", -] - -[[package]] -name = "hdrhistogram" -version = "7.5.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "765c9198f173dd59ce26ff9f95ef0aafd0a0fe01fb9d72841bc5066a4c06511d" -dependencies = [ - "byteorder", - "num-traits", -] - -[[package]] -name = "heck" -version = "0.5.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2304e00983f87ffb38b55b444b5e3b60a884b5d30c0fca7d82fe33449bbe55ea" - -[[package]] -name = "hermit-abi" -version = "0.5.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fc0fef456e4baa96da950455cd02c081ca953b141298e41db3fc7e36b1da849c" - -[[package]] -name = "hex" -version = "0.4.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7f24254aa9a54b5c858eaee2f5bccdb46aaf0e486a595ed5fd8f86ba55232a70" -dependencies = [ - "serde", -] - -[[package]] -name = "hex-conservative" -version = "0.2.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fda06d18ac606267c40c04e41b9947729bf8b9efe74bd4e82b61a5f26a510b9f" -dependencies = [ - "arrayvec", -] - -[[package]] -name = "hex_fmt" -version = "0.3.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b07f60793ff0a4d9cef0f18e63b5357e06209987153a64648c972c1e5aff336f" - -[[package]] -name = "hickory-proto" -version = "0.25.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f8a6fe56c0038198998a6f217ca4e7ef3a5e51f46163bd6dd60b5c71ca6c6502" -dependencies = [ - "async-trait", - "cfg-if", - "data-encoding", - "enum-as-inner", - "futures-channel", - "futures-io", - "futures-util", - "idna", - "ipnet", - "once_cell", - "rand 0.9.4", - "ring", - "serde", - "socket2 0.5.10", - "thiserror 2.0.18", - "tinyvec", - "tokio", - "tracing", - "url", -] - -[[package]] -name = "hickory-resolver" -version = "0.25.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "dc62a9a99b0bfb44d2ab95a7208ac952d31060efc16241c87eaf36406fecf87a" -dependencies = [ - "cfg-if", - "futures-util", - "hickory-proto", - "ipconfig", - "moka", - "once_cell", - "parking_lot", - "rand 0.9.4", - "resolv-conf", - "serde", - "smallvec", - "thiserror 2.0.18", - "tokio", - "tracing", -] - -[[package]] -name = "hkdf" -version = "0.12.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7b5f8eb2ad728638ea2c7d47a21db23b7b58a72ed6a38256b8a1849f15fbbdf7" -dependencies = [ - "hmac 0.12.1", -] - -[[package]] -name = "hmac" -version = "0.12.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6c49c37c09c17a53d937dfbb742eb3a961d65a994e6bcdcf37e7399d0cc8ab5e" -dependencies = [ - "digest 0.10.7", -] - -[[package]] -name = "hmac" -version = "0.13.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6303bc9732ae41b04cb554b844a762b4115a61bfaa81e3e83050991eeb56863f" -dependencies = [ - "digest 0.11.2", -] - -[[package]] -name = "http" -version = "1.4.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e3ba2a386d7f85a81f119ad7498ebe444d2e22c2af0b86b069416ace48b3311a" -dependencies = [ - "bytes", - "itoa", -] - -[[package]] -name = "http-body" -version = "1.0.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1efedce1fb8e6913f23e0c92de8e62cd5b772a67e7b3946df930a62566c93184" -dependencies = [ - "bytes", - "http", -] - -[[package]] -name = "http-body-util" -version = "0.1.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b021d93e26becf5dc7e1b75b1bed1fd93124b374ceb73f43d4d4eafec896a64a" -dependencies = [ - "bytes", - "futures-core", - "http", - "http-body", - "pin-project-lite", -] - -[[package]] -name = "http-range-header" -version = "0.4.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9171a2ea8a68358193d15dd5d70c1c10a2afc3e7e4c5bc92bc9f025cebd7359c" - -[[package]] -name = "httparse" -version = "1.10.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6dbf3de79e51f3d586ab4cb9d5c3e2c14aa28ed23d180cf89b4df0454a69cc87" - -[[package]] -name = "httpdate" -version = "1.0.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "df3b46402a9d5adb4c86a0cf463f42e19994e3ee891101b1841f30a545cb49a9" - -[[package]] -name = "human_bytes" -version = "0.4.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "91f255a4535024abf7640cb288260811fc14794f62b063652ed349f9a6c2348e" - -[[package]] -name = "humantime" -version = "2.3.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "135b12329e5e3ce057a9f972339ea52bc954fe1e9358ef27f95e89716fbc5424" - -[[package]] -name = "humantime-serde" -version = "1.1.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "57a3db5ea5923d99402c94e9feb261dc5ee9b4efa158b0315f788cf549cc200c" -dependencies = [ - "humantime", - "serde", -] - -[[package]] -name = "hybrid-array" -version = "0.4.7" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e1b229d73f5803b562cc26e4da0396c8610a4ee209f4fac8fa4f8d709166dc45" -dependencies = [ - "typenum", -] - -[[package]] -name = "hyper" -version = "1.8.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2ab2d4f250c3d7b1c9fcdff1cece94ea4e2dfbec68614f7b87cb205f24ca9d11" -dependencies = [ - "atomic-waker", - "bytes", - "futures-channel", - "futures-core", - "h2", - "http", - "http-body", - "httparse", - "httpdate", - "itoa", - "pin-project-lite", - "pin-utils", - "smallvec", - "tokio", - "want", -] - -[[package]] -name = "hyper-rustls" -version = "0.27.7" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e3c93eb611681b207e1fe55d5a71ecf91572ec8a6705cdb6857f7d8d5242cf58" -dependencies = [ - "http", - "hyper", - "hyper-util", - "log", - "rustls", - "rustls-native-certs", - "rustls-pki-types", - "tokio", - "tokio-rustls", - "tower-service", - "webpki-roots 1.0.6", -] - -[[package]] -name = "hyper-timeout" -version = "0.5.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2b90d566bffbce6a75bd8b09a05aa8c2cb1fabb6cb348f8840c9e4c90a0d83b0" -dependencies = [ - "hyper", - "hyper-util", - "pin-project-lite", - "tokio", - "tower-service", -] - -[[package]] -name = "hyper-tls" -version = "0.6.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "70206fc6890eaca9fde8a0bf71caa2ddfc9fe045ac9e5c70df101a7dbde866e0" -dependencies = [ - "bytes", - "http-body-util", - "hyper", - "hyper-util", - "native-tls", - "tokio", - "tokio-native-tls", - "tower-service", -] - -[[package]] -name = "hyper-util" -version = "0.1.20" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "96547c2556ec9d12fb1578c4eaf448b04993e7fb79cbaad930a656880a6bdfa0" -dependencies = [ - "base64 0.22.1", - "bytes", - "futures-channel", - "futures-util", - "http", - "http-body", - "hyper", - "ipnet", - "libc", - "percent-encoding", - "pin-project-lite", - "socket2 0.6.2", - "tokio", - "tower-service", - "tracing", -] - -[[package]] -name = "iana-time-zone" -version = "0.1.65" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e31bc9ad994ba00e440a8aa5c9ef0ec67d5cb5e5cb0cc7f8b744a35b389cc470" -dependencies = [ - "android_system_properties", - "core-foundation-sys", - "iana-time-zone-haiku", - "js-sys", - "log", - "wasm-bindgen", - "windows-core 0.62.2", -] - -[[package]] -name = "iana-time-zone-haiku" -version = "0.1.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f31827a206f56af32e590ba56d5d2d085f558508192593743f16b2306495269f" -dependencies = [ - "cc", -] - -[[package]] -name = "icu_collections" -version = "2.0.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "200072f5d0e3614556f94a9930d5dc3e0662a652823904c3a75dc3b0af7fee47" -dependencies = [ - "displaydoc", - "potential_utf", - "yoke", - "zerofrom", - "zerovec", -] - -[[package]] -name = "icu_locale_core" -version = "2.1.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "edba7861004dd3714265b4db54a3c390e880ab658fec5f7db895fae2046b5bb6" -dependencies = [ - "displaydoc", - "litemap", - "serde", - "tinystr", - "writeable", - "zerovec", -] - -[[package]] -name = "icu_normalizer" -version = "2.0.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8b24a59706036ba941c9476a55cd57b82b77f38a3c667d637ee7cabbc85eaedc" -dependencies = [ - "displaydoc", - "icu_collections", - "icu_normalizer_data", - "icu_properties", - "icu_provider", - "smallvec", - "utf16_iter", - "write16", - "zerovec", -] - -[[package]] -name = "icu_normalizer_data" -version = "2.0.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "00210d6893afc98edb752b664b8890f0ef174c8adbb8d0be9710fa66fbbf72d3" - -[[package]] -name = "icu_properties" -version = "2.0.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f5a97b8ac6235e69506e8dacfb2adf38461d2ce6d3e9bd9c94c4cbc3cd4400a4" -dependencies = [ - "displaydoc", - "icu_collections", - "icu_locale_core", - "icu_properties_data", - "icu_provider", - "potential_utf", - "zerotrie", - "zerovec", -] - -[[package]] -name = "icu_properties_data" -version = "2.0.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "298459143998310acd25ffe6810ed544932242d3f07083eee1084d83a71bd632" - -[[package]] -name = "icu_provider" -version = "2.1.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "85962cf0ce02e1e0a629cc34e7ca3e373ce20dda4c4d7294bbd0bf1fdb59e614" -dependencies = [ - "displaydoc", - "icu_locale_core", - "serde", - "stable_deref_trait", - "writeable", - "yoke", - "zerofrom", - "zerotrie", - "zerovec", -] - -[[package]] -name = "id-arena" -version = "2.3.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3d3067d79b975e8844ca9eb072e16b31c3c1c36928edf9c6789548c524d0d954" - -[[package]] -name = "ident_case" -version = "1.0.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b9e0384b61958566e926dc50660321d12159025e767c18e043daf26b70104c39" - -[[package]] -name = "idna" -version = "1.1.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3b0875f23caa03898994f6ddc501886a45c7d3d62d04d2d90788d47be1b1e4de" -dependencies = [ - "idna_adapter", - "smallvec", - "utf8_iter", -] - -[[package]] -name = "idna_adapter" -version = "1.2.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3acae9609540aa318d1bc588455225fb2085b9ed0c4f6bd0d9d5bcd86f1a0344" -dependencies = [ - "icu_normalizer", - "icu_properties", -] - -[[package]] -name = "if-addrs" -version = "0.10.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cabb0019d51a643781ff15c9c8a3e5dedc365c47211270f4e8f82812fedd8f0a" -dependencies = [ - "libc", - "windows-sys 0.48.0", -] - -[[package]] -name = "if-addrs" -version = "0.14.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bf39cc0423ee66021dc5eccface85580e4a001e0c5288bae8bea7ecb69225e90" -dependencies = [ - "libc", - "windows-sys 0.59.0", -] - -[[package]] -name = "if-watch" -version = "3.2.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cdf9d64cfcf380606e64f9a0bcf493616b65331199f984151a6fa11a7b3cde38" -dependencies = [ - "async-io", - "core-foundation 0.9.4", - "fnv", - "futures", - "if-addrs 0.10.2", - "ipnet", - "log", - "netlink-packet-core", - "netlink-packet-route", - "netlink-proto", - "netlink-sys", - "rtnetlink", - "system-configuration", - "tokio", - "windows 0.53.0", -] - -[[package]] -name = "igd-next" -version = "0.16.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "516893339c97f6011282d5825ac94fc1c7aad5cad26bdc2d0cee068c0bf97f97" -dependencies = [ - "async-trait", - "attohttpc", - "bytes", - "futures", - "http", - "http-body-util", - "hyper", - "hyper-util", - "log", - "rand 0.9.4", - "tokio", - "url", - "xmltree", -] - -[[package]] -name = "impl-codec" -version = "0.6.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ba6a270039626615617f3f36d15fc827041df3b78c439da2cadfa47455a77f2f" -dependencies = [ - "parity-scale-codec", -] - -[[package]] -name = "impl-trait-for-tuples" -version = "0.2.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a0eb5a3343abf848c0984fe4604b2b105da9539376e24fc0a3b0007411ae4fd9" -dependencies = [ - "proc-macro2", - "quote", - "syn 2.0.117", -] - -[[package]] -name = "include_dir" -version = "0.7.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "923d117408f1e49d914f1a379a309cffe4f18c05cf4e3d12e613a15fc81bd0dd" -dependencies = [ - "include_dir_macros", -] - -[[package]] -name = "include_dir_macros" -version = "0.7.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7cab85a7ed0bd5f0e76d93846e0147172bed2e2d3f859bcc33a8d9699cad1a75" -dependencies = [ - "proc-macro2", - "quote", -] - -[[package]] -name = "indenter" -version = "0.3.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "964de6e86d545b246d84badc0fef527924ace5134f30641c203ef52ba83f58d5" - -[[package]] -name = "indexmap" -version = "1.9.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bd070e393353796e801d209ad339e89596eb4c8d430d18ede6a1cced8fafbd99" -dependencies = [ - "autocfg", - "hashbrown 0.12.3", - "serde", -] - -[[package]] -name = "indexmap" -version = "2.13.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7714e70437a7dc3ac8eb7e6f8df75fd8eb422675fc7678aff7364301092b1017" -dependencies = [ - "arbitrary", - "equivalent", - "hashbrown 0.16.1", - "serde", - "serde_core", -] - -[[package]] -name = "indoc" -version = "2.0.7" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "79cf5c93f93228cf8efb3ba362535fb11199ac548a09ce117c9b1adc3030d706" -dependencies = [ - "rustversion", -] - -[[package]] -name = "inotify" -version = "0.11.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f37dccff2791ab604f9babef0ba14fbe0be30bd368dc541e2b08d07c8aa908f3" -dependencies = [ - "bitflags 2.11.0", - "inotify-sys", - "libc", -] - -[[package]] -name = "inotify-sys" -version = "0.1.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e05c02b5e89bff3b946cedeca278abc628fe811e604f027c45a8aa3cf793d0eb" -dependencies = [ - "libc", -] - -[[package]] -name = "inout" -version = "0.1.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "879f10e63c20629ecabbb64a8010319738c66a5cd0c29b02d63d272b03751d01" -dependencies = [ - "block-padding", - "generic-array", -] - -[[package]] -name = "instability" -version = "0.3.11" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "357b7205c6cd18dd2c86ed312d1e70add149aea98e7ef72b9fdf0270e555c11d" -dependencies = [ - "darling 0.23.0", - "indoc", - "proc-macro2", - "quote", - "syn 2.0.117", -] - -[[package]] -name = "interprocess" -version = "2.3.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "53bf2b0e0785c5394a7392f66d7c4fb9c653633c29b27a932280da3cb344c66a" -dependencies = [ - "doctest-file", - "futures-core", - "libc", - "recvmsg", - "tokio", - "widestring", - "windows-sys 0.52.0", -] - -[[package]] -name = "intrusive-collections" -version = "0.9.7" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "189d0897e4cbe8c75efedf3502c18c887b05046e59d28404d4d8e46cbc4d1e86" -dependencies = [ - "memoffset", -] - -[[package]] -name = "inventory" -version = "0.3.22" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "009ae045c87e7082cb72dab0ccd01ae075dd00141ddc108f43a0ea150a9e7227" -dependencies = [ - "rustversion", -] - -[[package]] -name = "ipconfig" -version = "0.3.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b58db92f96b720de98181bbbe63c831e87005ab460c1bf306eb2622b4707997f" -dependencies = [ - "socket2 0.5.10", - "widestring", - "windows-sys 0.48.0", - "winreg", -] - -[[package]] -name = "ipnet" -version = "2.11.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "469fb0b9cefa57e3ef31275ee7cacb78f2fdca44e4765491884a2b119d4eb130" - -[[package]] -name = "iri-string" -version = "0.7.10" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c91338f0783edbd6195decb37bae672fd3b165faffb89bf7b9e6942f8b1a731a" -dependencies = [ - "memchr", - "serde", -] - -[[package]] -name = "is_terminal_polyfill" -version = "1.70.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a6cb138bb79a146c1bd460005623e142ef0181e3d0219cb493e02f7d08a35695" - -[[package]] -name = "itertools" -version = "0.10.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b0fd2260e829bddf4cb6ea802289de2f86d6a7a690192fbe91b3f46e0f2c8473" -dependencies = [ - "either", -] - -[[package]] -name = "itertools" -version = "0.12.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ba291022dbbd398a455acf126c1e341954079855bc60dfdda641363bd6922569" -dependencies = [ - "either", -] - -[[package]] -name = "itertools" -version = "0.13.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "413ee7dfc52ee1a4949ceeb7dbc8a33f2d6c088194d9f922fb8318faf1f01186" -dependencies = [ - "either", -] - -[[package]] -name = "itertools" -version = "0.14.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2b192c782037fadd9cfa75548310488aabdbf3d2da73885b31bd0abd03351285" -dependencies = [ - "either", -] - -[[package]] -name = "itoa" -version = "1.0.17" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "92ecc6618181def0457392ccd0ee51198e065e016d1d527a7ac1b6dc7c1f09d2" - -[[package]] -name = "jemalloc_pprof" -version = "0.8.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8a0d44c349cfe2654897fadcb9de4f0bfbf48288ec344f700b2bd59f152dd209" -dependencies = [ - "anyhow", - "libc", - "mappings", - "once_cell", - "pprof_util", - "tempfile", - "tikv-jemalloc-ctl", - "tokio", - "tracing", -] - -[[package]] -name = "jni" -version = "0.21.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1a87aa2bb7d2af34197c04845522473242e1aa17c12f4935d5856491a7fb8c97" -dependencies = [ - "cesu8", - "cfg-if", - "combine", - "jni-sys", - "log", - "thiserror 1.0.69", - "walkdir", - "windows-sys 0.45.0", -] - -[[package]] -name = "jni-sys" -version = "0.3.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8eaf4bc02d17cbdd7ff4c7438cafcdf7fb9a4613313ad11b4f8fefe7d3fa0130" - -[[package]] -name = "jobserver" -version = "0.1.34" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9afb3de4395d6b3e67a780b6de64b51c978ecf11cb9a462c66be7d4ca9039d33" -dependencies = [ - "getrandom 0.3.4", - "libc", -] - -[[package]] -name = "js-sys" -version = "0.3.86" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d36139f1c97c42c0c86a411910b04e48d4939a0376e6e0f989420cbdee0120e5" -dependencies = [ - "once_cell", - "wasm-bindgen", -] - -[[package]] -name = "jsonrpsee" -version = "0.26.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3f3f48dc3e6b8bd21e15436c1ddd0bc22a6a54e8ec46fedd6adf3425f396ec6a" -dependencies = [ - "jsonrpsee-client-transport", - "jsonrpsee-core", - "jsonrpsee-http-client", - "jsonrpsee-proc-macros", - "jsonrpsee-server", - "jsonrpsee-types", - "jsonrpsee-wasm-client", - "jsonrpsee-ws-client", - "tokio", - "tracing", -] - -[[package]] -name = "jsonrpsee-client-transport" -version = "0.26.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cf36eb27f8e13fa93dcb50ccb44c417e25b818cfa1a481b5470cd07b19c60b98" -dependencies = [ - "base64 0.22.1", - "futures-channel", - "futures-util", - "gloo-net", - "http", - "jsonrpsee-core", - "pin-project", - "rustls", - "rustls-pki-types", - "rustls-platform-verifier", - "soketto", - "thiserror 2.0.18", - "tokio", - "tokio-rustls", - "tokio-util", - "tracing", - "url", -] - -[[package]] -name = "jsonrpsee-core" -version = "0.26.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "316c96719901f05d1137f19ba598b5fe9c9bc39f4335f67f6be8613921946480" -dependencies = [ - "async-trait", - "bytes", - "futures-timer", - "futures-util", - "http", - "http-body", - "http-body-util", - "jsonrpsee-types", - "parking_lot", - "pin-project", - "rand 0.9.4", - "rustc-hash", - "serde", - "serde_json", - "thiserror 2.0.18", - "tokio", - "tokio-stream", - "tower 0.5.3", - "tracing", - "wasm-bindgen-futures", -] - -[[package]] -name = "jsonrpsee-http-client" -version = "0.26.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "790bedefcec85321e007ff3af84b4e417540d5c87b3c9779b9e247d1bcc3dab8" -dependencies = [ - "base64 0.22.1", - "http-body", - "hyper", - "hyper-rustls", - "hyper-util", - "jsonrpsee-core", - "jsonrpsee-types", - "rustls", - "rustls-platform-verifier", - "serde", - "serde_json", - "thiserror 2.0.18", - "tokio", - "tower 0.5.3", - "url", -] - -[[package]] -name = "jsonrpsee-proc-macros" -version = "0.26.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2da3f8ab5ce1bb124b6d082e62dffe997578ceaf0aeb9f3174a214589dc00f07" -dependencies = [ - "heck", - "proc-macro-crate", - "proc-macro2", - "quote", - "syn 2.0.117", -] - -[[package]] -name = "jsonrpsee-server" -version = "0.26.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4c51b7c290bb68ce3af2d029648148403863b982f138484a73f02a9dd52dbd7f" -dependencies = [ - "futures-util", - "http", - "http-body", - "http-body-util", - "hyper", - "hyper-util", - "jsonrpsee-core", - "jsonrpsee-types", - "pin-project", - "route-recognizer", - "serde", - "serde_json", - "soketto", - "thiserror 2.0.18", - "tokio", - "tokio-stream", - "tokio-util", - "tower 0.5.3", - "tracing", -] - -[[package]] -name = "jsonrpsee-types" -version = "0.26.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bc88ff4688e43cc3fa9883a8a95c6fa27aa2e76c96e610b737b6554d650d7fd5" -dependencies = [ - "http", - "serde", - "serde_json", - "thiserror 2.0.18", -] - -[[package]] -name = "jsonrpsee-wasm-client" -version = "0.26.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7902885de4779f711a95d82c8da2d7e5f9f3a7c7cfa44d51c067fd1c29d72a3c" -dependencies = [ - "jsonrpsee-client-transport", - "jsonrpsee-core", - "jsonrpsee-types", - "tower 0.5.3", -] - -[[package]] -name = "jsonrpsee-ws-client" -version = "0.26.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9b6fceceeb05301cc4c065ab3bd2fa990d41ff4eb44e4ca1b30fa99c057c3e79" -dependencies = [ - "http", - "jsonrpsee-client-transport", - "jsonrpsee-core", - "jsonrpsee-types", - "tower 0.5.3", - "url", -] - -[[package]] -name = "jsonwebtoken" -version = "9.3.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5a87cc7a48537badeae96744432de36f4be2b4a34a05a5ef32e9dd8a1c169dde" -dependencies = [ - "base64 0.22.1", - "js-sys", - "pem", - "ring", - "serde", - "serde_json", - "simple_asn1", -] - -[[package]] -name = "k256" -version = "0.13.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f6e3919bbaa2945715f0bb6d3934a173d1e9a59ac23767fbaaef277265a7411b" -dependencies = [ - "cfg-if", - "ecdsa", - "elliptic-curve", - "once_cell", - "serdect", - "sha2 0.10.9", - "signature 2.2.0", -] - -[[package]] -name = "kasuari" -version = "0.4.11" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8fe90c1150662e858c7d5f945089b7517b0a80d8bf7ba4b1b5ffc984e7230a5b" -dependencies = [ - "hashbrown 0.16.1", - "portable-atomic", - "thiserror 2.0.18", -] - -[[package]] -name = "keccak" -version = "0.1.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cb26cec98cce3a3d96cbb7bced3c4b16e3d13f27ec56dbd62cbc8f39cfb9d653" -dependencies = [ - "cpufeatures 0.2.17", -] - -[[package]] -name = "keccak" -version = "0.2.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9e24a010dd405bd7ed803e5253182815b41bf2e6a80cc3bfc066658e03a198aa" -dependencies = [ - "cfg-if", - "cpufeatures 0.3.0", -] - -[[package]] -name = "keccak-asm" -version = "0.1.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b646a74e746cd25045aa0fd42f4f7f78aa6d119380182c7e63a5593c4ab8df6f" -dependencies = [ - "digest 0.10.7", - "sha3-asm", -] - -[[package]] -name = "kqueue" -version = "1.1.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "eac30106d7dce88daf4a3fcb4879ea939476d5074a9b7ddd0fb97fa4bed5596a" -dependencies = [ - "kqueue-sys", - "libc", -] - -[[package]] -name = "kqueue-sys" -version = "1.0.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ed9625ffda8729b85e45cf04090035ac368927b8cebc34898e7c120f52e4838b" -dependencies = [ - "bitflags 1.3.2", - "libc", -] - -[[package]] -name = "lazy_static" -version = "1.5.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bbd2bcb4c963f2ddae06a2efc7e9f3591312473c50c6685e1f298068316e66fe" - -[[package]] -name = "leb128fmt" -version = "0.1.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "09edd9e8b54e49e587e4f6295a7d29c3ea94d469cb40ab8ca70b288248a81db2" - -[[package]] -name = "libc" -version = "0.2.182" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6800badb6cb2082ffd7b6a67e6125bb39f18782f793520caee8cb8846be06112" - -[[package]] -name = "libgit2-sys" -version = "0.18.3+1.9.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c9b3acc4b91781bb0b3386669d325163746af5f6e4f73e6d2d630e09a35f3487" -dependencies = [ - "cc", - "libc", - "libz-sys", - "pkg-config", -] - -[[package]] -name = "libloading" -version = "0.8.9" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d7c4b02199fee7c5d21a5ae7d8cfa79a6ef5bb2fc834d6e9058e89c825efdc55" -dependencies = [ - "cfg-if", - "windows-link", -] - -[[package]] -name = "libm" -version = "0.2.16" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b6d2cec3eae94f9f509c767b45932f1ada8350c4bdb85af2fcab4a3c14807981" - -[[package]] -name = "libp2p" -version = "0.56.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ce71348bf5838e46449ae240631117b487073d5f347c06d434caddcb91dceb5a" -dependencies = [ - "bytes", - "either", - "futures", - "futures-timer", - "getrandom 0.2.17", - "libp2p-allow-block-list", - "libp2p-connection-limits", - "libp2p-core", - "libp2p-dns", - "libp2p-gossipsub", - "libp2p-identify", - "libp2p-identity", - "libp2p-kad", - "libp2p-mdns", - "libp2p-metrics", - "libp2p-noise", - "libp2p-ping", - "libp2p-quic", - "libp2p-request-response", - "libp2p-swarm", - "libp2p-tcp", - "libp2p-upnp", - "libp2p-yamux", - "multiaddr", - "pin-project", - "rw-stream-sink", - "thiserror 2.0.18", -] - -[[package]] -name = "libp2p-allow-block-list" -version = "0.6.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d16ccf824ee859ca83df301e1c0205270206223fd4b1f2e512a693e1912a8f4a" -dependencies = [ - "libp2p-core", - "libp2p-identity", - "libp2p-swarm", -] - -[[package]] -name = "libp2p-connection-limits" -version = "0.6.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a18b8b607cf3bfa2f8c57db9c7d8569a315d5cc0a282e6bfd5ebfc0a9840b2a0" -dependencies = [ - "libp2p-core", - "libp2p-identity", - "libp2p-swarm", -] - -[[package]] -name = "libp2p-core" -version = "0.43.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "249128cd37a2199aff30a7675dffa51caf073b51aa612d2f544b19932b9aebca" -dependencies = [ - "either", - "fnv", - "futures", - "futures-timer", - "libp2p-identity", - "multiaddr", - "multihash", - "multistream-select", - "parking_lot", - "pin-project", - "quick-protobuf", - "rand 0.8.5", - "rw-stream-sink", - "thiserror 2.0.18", - "tracing", - "unsigned-varint 0.8.0", - "web-time", -] - -[[package]] -name = "libp2p-dns" -version = "0.44.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0b770c1c8476736ca98c578cba4b505104ff8e842c2876b528925f9766379f9a" -dependencies = [ - "async-trait", - "futures", - "hickory-resolver", - "libp2p-core", - "libp2p-identity", - "parking_lot", - "smallvec", - "tracing", -] - -[[package]] -name = "libp2p-gossipsub" -version = "0.49.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a538e571cd38f504f761c61b8f79127489ea7a7d6f05c41ca15d31ffb5726326" -dependencies = [ - "async-channel", - "asynchronous-codec", - "base64 0.22.1", - "byteorder", - "bytes", - "either", - "fnv", - "futures", - "futures-timer", - "getrandom 0.2.17", - "hashlink 0.9.1", - "hex_fmt", - "libp2p-core", - "libp2p-identity", - "libp2p-swarm", - "prometheus-client", - "quick-protobuf", - "quick-protobuf-codec", - "rand 0.8.5", - "regex", - "serde", - "sha2 0.10.9", - "tracing", - "web-time", -] - -[[package]] -name = "libp2p-identify" -version = "0.47.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8ab792a8b68fdef443a62155b01970c81c3aadab5e659621b063ef252a8e65e8" -dependencies = [ - "asynchronous-codec", - "either", - "futures", - "futures-bounded", - "futures-timer", - "libp2p-core", - "libp2p-identity", - "libp2p-swarm", - "quick-protobuf", - "quick-protobuf-codec", - "smallvec", - "thiserror 2.0.18", - "tracing", -] - -[[package]] -name = "libp2p-identity" -version = "0.2.13" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f0c7892c221730ba55f7196e98b0b8ba5e04b4155651736036628e9f73ed6fc3" -dependencies = [ - "asn1_der", - "bs58", - "ed25519-dalek", - "hkdf", - "k256", - "multihash", - "p256", - "quick-protobuf", - "rand 0.8.5", - "sec1", - "serde", - "sha2 0.10.9", - "thiserror 2.0.18", - "tracing", - "zeroize", -] - -[[package]] -name = "libp2p-kad" -version = "0.48.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "13d3fd632a5872ec804d37e7413ceea20588f69d027a0fa3c46f82574f4dee60" -dependencies = [ - "asynchronous-codec", - "bytes", - "either", - "fnv", - "futures", - "futures-bounded", - "futures-timer", - "libp2p-core", - "libp2p-identity", - "libp2p-swarm", - "quick-protobuf", - "quick-protobuf-codec", - "rand 0.8.5", - "serde", - "sha2 0.10.9", - "smallvec", - "thiserror 2.0.18", - "tracing", - "uint 0.10.0", - "web-time", -] - -[[package]] -name = "libp2p-mdns" -version = "0.48.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c66872d0f1ffcded2788683f76931be1c52e27f343edb93bc6d0bcd8887be443" -dependencies = [ - "futures", - "hickory-proto", - "if-watch", - "libp2p-core", - "libp2p-identity", - "libp2p-swarm", - "rand 0.8.5", - "smallvec", - "socket2 0.5.10", - "tokio", - "tracing", -] - -[[package]] -name = "libp2p-metrics" -version = "0.17.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "805a555148522cb3414493a5153451910cb1a146c53ffbf4385708349baf62b7" -dependencies = [ - "futures", - "libp2p-core", - "libp2p-gossipsub", - "libp2p-identify", - "libp2p-identity", - "libp2p-kad", - "libp2p-ping", - "libp2p-swarm", - "pin-project", - "prometheus-client", - "web-time", -] - -[[package]] -name = "libp2p-noise" -version = "0.46.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bc73eacbe6462a0eb92a6527cac6e63f02026e5407f8831bde8293f19217bfbf" -dependencies = [ - "asynchronous-codec", - "bytes", - "futures", - "libp2p-core", - "libp2p-identity", - "multiaddr", - "multihash", - "quick-protobuf", - "rand 0.8.5", - "snow", - "static_assertions", - "thiserror 2.0.18", - "tracing", - "x25519-dalek", - "zeroize", -] - -[[package]] -name = "libp2p-ping" -version = "0.47.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "74bb7fcdfd9fead4144a3859da0b49576f171a8c8c7c0bfc7c541921d25e60d3" -dependencies = [ - "futures", - "futures-timer", - "libp2p-core", - "libp2p-identity", - "libp2p-swarm", - "rand 0.8.5", - "tracing", - "web-time", -] - -[[package]] -name = "libp2p-quic" -version = "0.13.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8dc448b2de9f4745784e3751fe8bc6c473d01b8317edd5ababcb0dec803d843f" -dependencies = [ - "futures", - "futures-timer", - "if-watch", - "libp2p-core", - "libp2p-identity", - "libp2p-tls", - "quinn", - "rand 0.8.5", - "ring", - "rustls", - "socket2 0.5.10", - "thiserror 2.0.18", - "tokio", - "tracing", -] - -[[package]] -name = "libp2p-request-response" -version = "0.29.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a9f1cca83488b90102abac7b67d5c36fc65bc02ed47620228af7ed002e6a1478" -dependencies = [ - "async-trait", - "cbor4ii", - "futures", - "futures-bounded", - "libp2p-core", - "libp2p-identity", - "libp2p-swarm", - "rand 0.8.5", - "serde", - "smallvec", - "tracing", -] - -[[package]] -name = "libp2p-scatter" -version = "0.3.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ea402c419f99e6013d5b12f97c5a1a1abe4aca285844b01b0ed96deab11f0b6e" -dependencies = [ - "bytes", - "fnv", - "futures", - "libp2p", - "prometheus-client", - "tracing", - "unsigned-varint 0.8.0", -] - -[[package]] -name = "libp2p-stream" -version = "0.4.0-alpha" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1d6bd8025c80205ec2810cfb28b02f362ab48a01bee32c50ab5f12761e033464" -dependencies = [ - "futures", - "libp2p-core", - "libp2p-identity", - "libp2p-swarm", - "rand 0.8.5", - "tracing", -] - -[[package]] -name = "libp2p-swarm" -version = "0.47.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ce88c6c4bf746c8482480345ea3edfd08301f49e026889d1cbccfa1808a9ed9e" -dependencies = [ - "either", - "fnv", - "futures", - "futures-timer", - "hashlink 0.10.0", - "libp2p-core", - "libp2p-identity", - "libp2p-swarm-derive", - "multistream-select", - "rand 0.8.5", - "smallvec", - "tokio", - "tracing", - "web-time", -] - -[[package]] -name = "libp2p-swarm-derive" -version = "0.35.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "dd297cf53f0cb3dee4d2620bb319ae47ef27c702684309f682bdb7e55a18ae9c" -dependencies = [ - "heck", - "quote", - "syn 2.0.117", -] - -[[package]] -name = "libp2p-tcp" -version = "0.44.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fb6585b9309699f58704ec9ab0bb102eca7a3777170fa91a8678d73ca9cafa93" -dependencies = [ - "futures", - "futures-timer", - "if-watch", - "libc", - "libp2p-core", - "socket2 0.6.2", - "tokio", - "tracing", -] - -[[package]] -name = "libp2p-tls" -version = "0.6.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "96ff65a82e35375cbc31ebb99cacbbf28cb6c4fefe26bf13756ddcf708d40080" -dependencies = [ - "futures", - "futures-rustls", - "libp2p-core", - "libp2p-identity", - "rcgen", - "ring", - "rustls", - "rustls-webpki", - "thiserror 2.0.18", - "x509-parser", - "yasna", -] - -[[package]] -name = "libp2p-upnp" -version = "0.5.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4757e65fe69399c1a243bbb90ec1ae5a2114b907467bf09f3575e899815bb8d3" -dependencies = [ - "futures", - "futures-timer", - "igd-next", - "libp2p-core", - "libp2p-swarm", - "tokio", - "tracing", -] - -[[package]] -name = "libp2p-yamux" -version = "0.47.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f15df094914eb4af272acf9adaa9e287baa269943f32ea348ba29cfb9bfc60d8" -dependencies = [ - "either", - "futures", - "libp2p-core", - "thiserror 2.0.18", - "tracing", - "yamux 0.12.1", - "yamux 0.13.8", -] - -[[package]] -name = "libproc" -version = "0.14.11" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a54ad7278b8bc5301d5ffd2a94251c004feb971feba96c971ea4063645990757" -dependencies = [ - "bindgen", - "errno", - "libc", -] - -[[package]] -name = "libredox" -version = "0.1.12" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3d0b95e02c851351f877147b7deea7b1afb1df71b63aa5f8270716e0c5720616" -dependencies = [ - "bitflags 2.11.0", - "libc", - "redox_syscall 0.7.1", -] - -[[package]] -name = "libz-sys" -version = "1.1.23" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "15d118bbf3771060e7311cc7bb0545b01d08a8b4a7de949198dec1fa0ca1c0f7" -dependencies = [ - "cc", - "libc", - "pkg-config", - "vcpkg", -] - -[[package]] -name = "line-clipping" -version = "0.3.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5f4de44e98ddbf09375cbf4d17714d18f39195f4f4894e8524501726fd9a8a4a" -dependencies = [ - "bitflags 2.11.0", -] - -[[package]] -name = "linked-hash-map" -version = "0.5.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0717cef1bc8b636c6e1c1bbdefc09e6322da8a9321966e8928ef80d20f7f770f" - -[[package]] -name = "linked_hash_set" -version = "0.1.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "984fb35d06508d1e69fc91050cceba9c0b748f983e6739fa2c7a9237154c52c8" -dependencies = [ - "linked-hash-map", - "serde_core", -] - -[[package]] -name = "linux-raw-sys" -version = "0.4.15" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d26c52dbd32dccf2d10cac7725f8eae5296885fb5703b261f7d0a0739ec807ab" - -[[package]] -name = "linux-raw-sys" -version = "0.11.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "df1d3c3b53da64cf5760482273a98e575c651a67eec7f77df96b5b642de8f039" - -[[package]] -name = "litemap" -version = "0.8.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6373607a59f0be73a39b6fe456b8192fcc3585f602af20751600e974dd455e77" - -[[package]] -name = "litrs" -version = "1.0.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "11d3d7f243d5c5a8b9bb5d6dd2b1602c0cb0b9db1621bafc7ed66e35ff9fe092" - -[[package]] -name = "lock_api" -version = "0.4.14" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "224399e74b87b5f3557511d98dff8b14089b3dadafcab6bb93eab67d3aace965" -dependencies = [ - "scopeguard", - "serde", -] - -[[package]] -name = "log" -version = "0.4.29" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5e5032e24019045c762d3c0f28f5b6b8bbf38563a65908389bf7978758920897" - -[[package]] -name = "logos" -version = "0.15.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ff472f899b4ec2d99161c51f60ff7075eeb3097069a36050d8037a6325eb8154" -dependencies = [ - "logos-derive", -] - -[[package]] -name = "logos-codegen" -version = "0.15.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "192a3a2b90b0c05b27a0b2c43eecdb7c415e29243acc3f89cc8247a5b693045c" -dependencies = [ - "beef", - "fnv", - "lazy_static", - "proc-macro2", - "quote", - "regex-syntax", - "rustc_version 0.4.1", - "syn 2.0.117", -] - -[[package]] -name = "logos-derive" -version = "0.15.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "605d9697bcd5ef3a42d38efc51541aa3d6a4a25f7ab6d1ed0da5ac632a26b470" -dependencies = [ - "logos-codegen", -] - -[[package]] -name = "lru" -version = "0.12.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "234cf4f4a04dc1f57e24b96cc0cd600cf2af460d4161ac5ecdd0af8e1f3b2a38" -dependencies = [ - "hashbrown 0.15.5", -] - -[[package]] -name = "lru" -version = "0.16.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a1dc47f592c06f33f8e3aea9591776ec7c9f9e4124778ff8a3c3b87159f7e593" -dependencies = [ - "hashbrown 0.16.1", -] - -[[package]] -name = "lru-slab" -version = "0.1.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "112b39cec0b298b6c1999fee3e31427f74f676e4cb9879ed1a121b43661a4154" - -[[package]] -name = "lz4" -version = "1.28.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a20b523e860d03443e98350ceaac5e71c6ba89aea7d960769ec3ce37f4de5af4" -dependencies = [ - "lz4-sys", -] - -[[package]] -name = "lz4-sys" -version = "1.11.1+lz4-1.10.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6bd8c0d6c6ed0cd30b3652886bb8711dc4bb01d637a68105a3d5158039b418e6" -dependencies = [ - "cc", - "libc", -] - -[[package]] -name = "lz4_flex" -version = "0.12.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "98c23545df7ecf1b16c303910a69b079e8e251d60f7dd2cc9b4177f2afaf1746" - -[[package]] -name = "mach2" -version = "0.5.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6a1b95cd5421ec55b445b5ae102f5ea0e768de1f82bd3001e11f426c269c3aea" -dependencies = [ - "libc", -] - -[[package]] -name = "mach2" -version = "0.6.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "dae608c151f68243f2b000364e1f7b186d9c29845f7d2d85bd31b9ad77ad552b" - -[[package]] -name = "macro-string" -version = "0.1.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1b27834086c65ec3f9387b096d66e99f221cf081c2b738042aa252bcd41204e3" -dependencies = [ - "proc-macro2", - "quote", - "syn 2.0.117", -] - -[[package]] -name = "mappings" -version = "0.7.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8bab1e61a4b76757edb59cd81fcaa7f3ba9018d43b527d9abfad877b4c6c60f2" -dependencies = [ - "anyhow", - "libc", - "once_cell", - "pprof_util", - "tracing", -] - -[[package]] -name = "match-lookup" -version = "0.1.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "757aee279b8bdbb9f9e676796fd459e4207a1f986e87886700abf589f5abf771" -dependencies = [ - "proc-macro2", - "quote", - "syn 2.0.117", -] - -[[package]] -name = "matchers" -version = "0.2.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d1525a2a28c7f4fa0fc98bb91ae755d1e2d1505079e05539e35bc876b5d65ae9" -dependencies = [ - "regex-automata", -] - -[[package]] -name = "matchit" -version = "0.7.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0e7465ac9959cc2b1404e8e2367b43684a6d13790fe23056cc8c6c5a6b7bcb94" - -[[package]] -name = "matchit" -version = "0.8.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "47e1ffaa40ddd1f3ed91f717a33c8c0ee23fff369e3aa8772b9605cc1d22f4c3" - -[[package]] -name = "memchr" -version = "2.8.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f8ca58f447f06ed17d5fc4043ce1b10dd205e060fb3ce5b979b8ed8e59ff3f79" - -[[package]] -name = "memmap2" -version = "0.9.10" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "714098028fe011992e1c3962653c96b2d578c4b4bce9036e15ff220319b1e0e3" -dependencies = [ - "libc", -] - -[[package]] -name = "memoffset" -version = "0.9.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "488016bfae457b036d996092f6cb448677611ce4449e970ceaf42695203f218a" -dependencies = [ - "autocfg", -] - -[[package]] -name = "metrics" -version = "0.24.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5d5312e9ba3771cfa961b585728215e3d972c950a3eed9252aa093d6301277e8" -dependencies = [ - "ahash", - "portable-atomic", -] - -[[package]] -name = "metrics-derive" -version = "0.1.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "161ab904c2c62e7bda0f7562bf22f96440ca35ff79e66c800cbac298f2f4f5ec" -dependencies = [ - "proc-macro2", - "quote", - "syn 2.0.117", -] - -[[package]] -name = "metrics-exporter-prometheus" -version = "0.18.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3589659543c04c7dc5526ec858591015b87cd8746583b51b48ef4353f99dbcda" -dependencies = [ - "base64 0.22.1", - "indexmap 2.13.0", - "metrics", - "metrics-util", - "quanta", - "thiserror 2.0.18", -] - -[[package]] -name = "metrics-process" -version = "2.4.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4268d87f64a752f5a651314fc683f04da10be65701ea3e721ba4d74f79163cac" -dependencies = [ - "libc", - "libproc", - "mach2 0.6.0", - "metrics", - "once_cell", - "procfs 0.18.0", - "rlimit", - "windows 0.62.2", -] - -[[package]] -name = "metrics-util" -version = "0.20.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cdfb1365fea27e6dd9dc1dbc19f570198bc86914533ad639dae939635f096be4" -dependencies = [ - "crossbeam-epoch", - "crossbeam-utils", - "hashbrown 0.16.1", - "metrics", - "quanta", - "rand 0.9.4", - "rand_xoshiro", - "sketches-ddsketch", -] - -[[package]] -name = "miette" -version = "7.6.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5f98efec8807c63c752b5bd61f862c165c115b0a35685bdcfd9238c7aeb592b7" -dependencies = [ - "cfg-if", - "miette-derive", - "unicode-width 0.1.14", -] - -[[package]] -name = "miette-derive" -version = "7.6.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "db5b29714e950dbb20d5e6f74f9dcec4edbcc1067bb7f8ed198c097b8c1a818b" -dependencies = [ - "proc-macro2", - "quote", - "syn 2.0.117", -] - -[[package]] -name = "mime" -version = "0.3.17" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6877bb514081ee2a7ff5ef9de3281f14a4dd4bceac4c09388074a6b5df8a139a" - -[[package]] -name = "mime_guess" -version = "2.0.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f7c44f8e672c00fe5308fa235f821cb4198414e1c77935c1ab6948d3fd78550e" -dependencies = [ - "mime", - "unicase", -] - -[[package]] -name = "minimal-lexical" -version = "0.2.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "68354c5c6bd36d73ff3feceb05efa59b6acb7626617f4962be322a825e61f79a" - -[[package]] -name = "miniz_oxide" -version = "0.8.9" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1fa76a2c86f704bdb222d66965fb3d63269ce38518b83cb0575fca855ebb6316" -dependencies = [ - "adler2", - "simd-adler32", -] - -[[package]] -name = "mio" -version = "1.1.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a69bcab0ad47271a0234d9422b131806bf3968021e5dc9328caf2d4cd58557fc" -dependencies = [ - "libc", - "log", - "wasi", - "windows-sys 0.61.2", -] - -[[package]] -name = "mockall" -version = "0.14.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f58d964098a5f9c6b63d0798e5372fd04708193510a7af313c22e9f29b7b620b" -dependencies = [ - "cfg-if", - "downcast", - "fragile", - "mockall_derive", - "predicates", - "predicates-tree", -] - -[[package]] -name = "mockall_derive" -version = "0.14.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ca41ce716dda6a9be188b385aa78ee5260fc25cd3802cb2a8afdc6afbe6b6dbf" -dependencies = [ - "cfg-if", - "proc-macro2", - "quote", - "syn 2.0.117", -] - -[[package]] -name = "modular-bitfield" -version = "0.13.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2956e537fc68236d2aa048f55704f231cc93f1c4de42fe1ecb5bd7938061fc4a" -dependencies = [ - "modular-bitfield-impl", - "static_assertions", -] - -[[package]] -name = "modular-bitfield-impl" -version = "0.13.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "59b43b4fd69e3437618106f7754f34021b831a514f9e1a98ae863cabcd8d8dad" -dependencies = [ - "proc-macro2", - "quote", - "syn 2.0.117", -] - -[[package]] -name = "moka" -version = "0.12.13" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b4ac832c50ced444ef6be0767a008b02c106a909ba79d1d830501e94b96f6b7e" -dependencies = [ - "crossbeam-channel", - "crossbeam-epoch", - "crossbeam-utils", - "equivalent", - "parking_lot", - "portable-atomic", - "smallvec", - "tagptr", - "uuid", -] - -[[package]] -name = "more-asserts" -version = "0.3.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1fafa6961cabd9c63bcd77a45d7e3b7f3b552b70417831fb0f56db717e72407e" - -[[package]] -name = "multiaddr" -version = "0.18.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fe6351f60b488e04c1d21bc69e56b89cb3f5e8f5d22557d6e8031bdfd79b6961" -dependencies = [ - "arrayref", - "byteorder", - "data-encoding", - "libp2p-identity", - "multibase", - "multihash", - "percent-encoding", - "serde", - "static_assertions", - "unsigned-varint 0.8.0", - "url", -] - -[[package]] -name = "multibase" -version = "0.9.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8694bb4835f452b0e3bb06dbebb1d6fc5385b6ca1caf2e55fd165c042390ec77" -dependencies = [ - "base-x", - "base256emoji", - "data-encoding", - "data-encoding-macro", -] - -[[package]] -name = "multihash" -version = "0.19.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6b430e7953c29dd6a09afc29ff0bb69c6e306329ee6794700aee27b76a1aea8d" -dependencies = [ - "core2", - "serde", - "unsigned-varint 0.8.0", -] - -[[package]] -name = "multimap" -version = "0.10.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1d87ecb2933e8aeadb3e3a02b828fed80a7528047e68b4f424523a0981a3a084" - -[[package]] -name = "multistream-select" -version = "0.13.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ea0df8e5eec2298a62b326ee4f0d7fe1a6b90a09dfcf9df37b38f947a8c42f19" -dependencies = [ - "bytes", - "futures", - "log", - "pin-project", - "smallvec", - "unsigned-varint 0.7.2", -] - -[[package]] -name = "native-tls" -version = "0.2.18" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "465500e14ea162429d264d44189adc38b199b62b1c21eea9f69e4b73cb03bbf2" -dependencies = [ - "libc", - "log", - "openssl", - "openssl-probe", - "openssl-sys", - "schannel", - "security-framework", - "security-framework-sys", - "tempfile", -] - -[[package]] -name = "netlink-packet-core" -version = "0.7.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "72724faf704479d67b388da142b186f916188505e7e0b26719019c525882eda4" -dependencies = [ - "anyhow", - "byteorder", - "netlink-packet-utils", -] - -[[package]] -name = "netlink-packet-route" -version = "0.17.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "053998cea5a306971f88580d0829e90f270f940befd7cf928da179d4187a5a66" -dependencies = [ - "anyhow", - "bitflags 1.3.2", - "byteorder", - "libc", - "netlink-packet-core", - "netlink-packet-utils", -] - -[[package]] -name = "netlink-packet-utils" -version = "0.5.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0ede8a08c71ad5a95cdd0e4e52facd37190977039a4704eb82a283f713747d34" -dependencies = [ - "anyhow", - "byteorder", - "paste", - "thiserror 1.0.69", -] - -[[package]] -name = "netlink-proto" -version = "0.11.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "72452e012c2f8d612410d89eea01e2d9b56205274abb35d53f60200b2ec41d60" -dependencies = [ - "bytes", - "futures", - "log", - "netlink-packet-core", - "netlink-sys", - "thiserror 2.0.18", -] - -[[package]] -name = "netlink-sys" -version = "0.8.8" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cd6c30ed10fa69cc491d491b85cc971f6bdeb8e7367b7cde2ee6cc878d583fae" -dependencies = [ - "bytes", - "futures-util", - "libc", - "log", - "tokio", -] - -[[package]] -name = "nix" -version = "0.26.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "598beaf3cc6fdd9a5dfb1630c2800c7acd31df7aaf0f565796fba2b53ca1af1b" -dependencies = [ - "bitflags 1.3.2", - "cfg-if", - "libc", -] - -[[package]] -name = "no-std-compat" -version = "0.4.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b93853da6d84c2e3c7d730d6473e8817692dd89be387eb01b94d7f108ecb5b8c" - -[[package]] -name = "nohash-hasher" -version = "0.2.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2bf50223579dc7cdcfb3bfcacf7069ff68243f8c363f62ffa99cf000a6b9c451" - -[[package]] -name = "nom" -version = "7.1.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d273983c5a657a70a3e8f2a01329822f3b8c8172b73826411a55751e404a0a4a" -dependencies = [ - "memchr", - "minimal-lexical", -] - -[[package]] -name = "nonzero_ext" -version = "0.3.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "38bf9645c8b145698bb0b18a4637dcacbc421ea49bef2317e4fd8065a387cf21" - -[[package]] -name = "normalize-line-endings" -version = "0.3.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "61807f77802ff30975e01f4f071c8ba10c022052f98b3294119f3e615d13e5be" - -[[package]] -name = "notify" -version = "8.2.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4d3d07927151ff8575b7087f245456e549fea62edf0ec4e565a5ee50c8402bc3" -dependencies = [ - "bitflags 2.11.0", - "fsevent-sys", - "inotify", - "kqueue", - "libc", - "log", - "mio", - "notify-types", - "walkdir", - "windows-sys 0.60.2", -] - -[[package]] -name = "notify-types" -version = "2.1.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "42b8cfee0e339a0337359f3c88165702ac6e600dc01c0cc9579a92d62b08477a" -dependencies = [ - "bitflags 2.11.0", -] - -[[package]] -name = "ntapi" -version = "0.4.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c3b335231dfd352ffb0f8017f3b6027a4917f7df785ea2143d8af2adc66980ae" -dependencies = [ - "winapi", -] - -[[package]] -name = "nu-ansi-term" -version = "0.50.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7957b9740744892f114936ab4a57b3f487491bbeafaf8083688b16841a4240e5" -dependencies = [ - "windows-sys 0.59.0", -] - -[[package]] -name = "num" -version = "0.4.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "35bd024e8b2ff75562e5f34e7f4905839deb4b22955ef5e73d2fea1b9813cb23" -dependencies = [ - "num-bigint", - "num-complex", - "num-integer", - "num-iter", - "num-rational", - "num-traits", -] - -[[package]] -name = "num-bigint" -version = "0.4.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a5e44f723f1133c9deac646763579fdb3ac745e418f2a7af9cd0c431da1f20b9" -dependencies = [ - "num-integer", - "num-traits", - "serde", -] - -[[package]] -name = "num-complex" -version = "0.4.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "73f88a1307638156682bada9d7604135552957b7818057dcef22705b4d509495" -dependencies = [ - "num-traits", -] - -[[package]] -name = "num-conv" -version = "0.2.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cf97ec579c3c42f953ef76dbf8d55ac91fb219dde70e49aa4a6b7d74e9919050" - -[[package]] -name = "num-integer" -version = "0.1.46" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7969661fd2958a5cb096e56c8e1ad0444ac2bbcd0061bd28660485a44879858f" -dependencies = [ - "num-traits", -] - -[[package]] -name = "num-iter" -version = "0.1.45" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1429034a0490724d0075ebb2bc9e875d6503c3cf69e235a8941aa757d83ef5bf" -dependencies = [ - "autocfg", - "num-integer", - "num-traits", -] - -[[package]] -name = "num-rational" -version = "0.4.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f83d14da390562dca69fc84082e73e548e1ad308d24accdedd2720017cb37824" -dependencies = [ - "num-bigint", - "num-integer", - "num-traits", -] - -[[package]] -name = "num-traits" -version = "0.2.19" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "071dfc062690e90b734c0b2273ce72ad0ffa95f0c74596bc250dcfd960262841" -dependencies = [ - "autocfg", - "libm", -] - -[[package]] -name = "num_cpus" -version = "1.17.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "91df4bbde75afed763b708b7eee1e8e7651e02d97f6d5dd763e89367e957b23b" -dependencies = [ - "hermit-abi", - "libc", -] - -[[package]] -name = "num_enum" -version = "0.7.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b1207a7e20ad57b847bbddc6776b968420d38292bbfe2089accff5e19e82454c" -dependencies = [ - "num_enum_derive", - "rustversion", -] - -[[package]] -name = "num_enum_derive" -version = "0.7.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ff32365de1b6743cb203b710788263c44a03de03802daf96092f2da4fe6ba4d7" -dependencies = [ - "proc-macro-crate", - "proc-macro2", - "quote", - "syn 2.0.117", -] - -[[package]] -name = "num_threads" -version = "0.1.7" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5c7398b9c8b70908f6371f47ed36737907c87c52af34c268fed0bf0ceb92ead9" -dependencies = [ - "libc", -] - -[[package]] -name = "nybbles" -version = "0.4.8" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0d49ff0c0d00d4a502b39df9af3a525e1efeb14b9dabb5bb83335284c1309210" -dependencies = [ - "alloy-rlp", - "arbitrary", - "cfg-if", - "proptest", - "ruint", - "serde", - "smallvec", -] - -[[package]] -name = "objc2-core-foundation" -version = "0.3.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1c10c2894a6fed806ade6027bcd50662746363a9589d3ec9d9bef30a4e4bc166" -dependencies = [ - "bitflags 2.11.0", -] - -[[package]] -name = "objc2-io-kit" -version = "0.3.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "71c1c64d6120e51cd86033f67176b1cb66780c2efe34dec55176f77befd93c0a" -dependencies = [ - "libc", - "objc2-core-foundation", -] - -[[package]] -name = "object" -version = "0.37.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ff76201f031d8863c38aa7f905eca4f53abbfa15f609db4277d44cd8938f33fe" -dependencies = [ - "memchr", -] - -[[package]] -name = "oid-registry" -version = "0.8.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "12f40cff3dde1b6087cc5d5f5d4d65712f34016a03ed60e9c08dcc392736b5b7" -dependencies = [ - "asn1-rs", -] - -[[package]] -name = "once_cell" -version = "1.21.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "42f5e15c9953c5e4ccceeb2e7382a716482c34515315f7b03532b8b4e8393d2d" -dependencies = [ - "critical-section", - "portable-atomic", -] - -[[package]] -name = "once_cell_polyfill" -version = "1.70.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "384b8ab6d37215f3c5301a95a4accb5d64aa607f1fcb26a11b5303878451b4fe" - -[[package]] -name = "oneline-eyre" -version = "0.1.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "862f17a1e689c0ce8ca158ea48e776c5101c5d14fdfbed3e01c15f89604c3097" -dependencies = [ - "eyre", -] - -[[package]] -name = "oorandom" -version = "11.1.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d6790f58c7ff633d8771f42965289203411a5e5c68388703c06e14f24770b41e" - -[[package]] -name = "op-alloy" -version = "0.23.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e9b8fee21003dd4f076563de9b9d26f8c97840157ef78593cd7f262c5ca99848" -dependencies = [ - "op-alloy-consensus", - "op-alloy-network", - "op-alloy-provider", - "op-alloy-rpc-types", - "op-alloy-rpc-types-engine", -] - -[[package]] -name = "op-alloy-consensus" -version = "0.23.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "736381a95471d23e267263cfcee9e1d96d30b9754a94a2819148f83379de8a86" -dependencies = [ - "alloy-consensus", - "alloy-eips", - "alloy-network", - "alloy-primitives", - "alloy-rlp", - "alloy-rpc-types-eth", - "alloy-serde", - "arbitrary", - "derive_more", - "serde", - "serde_with", - "thiserror 2.0.18", -] - -[[package]] -name = "op-alloy-network" -version = "0.23.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4034183dca6bff6632e7c24c92e75ff5f0eabb58144edb4d8241814851334d47" -dependencies = [ - "alloy-consensus", - "alloy-network", - "alloy-primitives", - "alloy-provider", - "alloy-rpc-types-eth", - "alloy-signer", - "op-alloy-consensus", - "op-alloy-rpc-types", -] - -[[package]] -name = "op-alloy-provider" -version = "0.23.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6753d90efbaa8ea8bcb89c1737408ca85fa60d7adb875049d3f382c063666f86" -dependencies = [ - "alloy-network", - "alloy-primitives", - "alloy-provider", - "alloy-rpc-types-engine", - "alloy-transport", - "async-trait", - "op-alloy-rpc-types-engine", -] - -[[package]] -name = "op-alloy-rpc-types" -version = "0.23.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ddd87c6b9e5b6eee8d6b76f41b04368dca0e9f38d83338e5b00e730c282098a4" -dependencies = [ - "alloy-consensus", - "alloy-eips", - "alloy-network-primitives", - "alloy-primitives", - "alloy-rpc-types-eth", - "alloy-serde", - "derive_more", - "op-alloy-consensus", - "serde", - "serde_json", - "thiserror 2.0.18", -] - -[[package]] -name = "op-alloy-rpc-types-engine" -version = "0.23.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "77727699310a18cdeed32da3928c709e2704043b6584ed416397d5da65694efc" -dependencies = [ - "alloy-consensus", - "alloy-eips", - "alloy-primitives", - "alloy-rlp", - "alloy-rpc-types-engine", - "alloy-serde", - "derive_more", - "ethereum_ssz 0.9.1", - "ethereum_ssz_derive 0.9.1", - "op-alloy-consensus", - "serde", - "sha2 0.10.9", - "snap", - "thiserror 2.0.18", -] - -[[package]] -name = "op-revm" -version = "15.0.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "79c92b75162c2ed1661849fa51683b11254a5b661798360a2c24be918edafd40" -dependencies = [ - "auto_impl", - "revm", - "serde", -] - -[[package]] -name = "opaque-debug" -version = "0.3.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c08d65885ee38876c4f86fa503fb49d7b507c2b62552df7c70b2fce627e06381" - -[[package]] -name = "openssl" -version = "0.10.75" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "08838db121398ad17ab8531ce9de97b244589089e290a384c900cb9ff7434328" -dependencies = [ - "bitflags 2.11.0", - "cfg-if", - "foreign-types", - "libc", - "once_cell", - "openssl-macros", - "openssl-sys", -] - -[[package]] -name = "openssl-macros" -version = "0.1.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a948666b637a0f465e8564c73e89d4dde00d72d4d473cc972f390fc3dcee7d9c" -dependencies = [ - "proc-macro2", - "quote", - "syn 2.0.117", -] - -[[package]] -name = "openssl-probe" -version = "0.2.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7c87def4c32ab89d880effc9e097653c8da5d6ef28e6b539d313baaacfbafcbe" - -[[package]] -name = "openssl-src" -version = "300.5.5+3.5.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3f1787d533e03597a7934fd0a765f0d28e94ecc5fb7789f8053b1e699a56f709" -dependencies = [ - "cc", -] - -[[package]] -name = "openssl-sys" -version = "0.9.111" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "82cab2d520aa75e3c58898289429321eb788c3106963d0dc886ec7a5f4adc321" -dependencies = [ - "cc", - "libc", - "openssl-src", - "pkg-config", - "vcpkg", -] - -[[package]] -name = "opentelemetry" -version = "0.31.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b84bcd6ae87133e903af7ef497404dda70c60d0ea14895fc8a5e6722754fc2a0" -dependencies = [ - "futures-core", - "futures-sink", - "js-sys", - "pin-project-lite", - "thiserror 2.0.18", - "tracing", -] - -[[package]] -name = "opentelemetry-http" -version = "0.31.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d7a6d09a73194e6b66df7c8f1b680f156d916a1a942abf2de06823dd02b7855d" -dependencies = [ - "async-trait", - "bytes", - "http", - "opentelemetry", - "reqwest", -] - -[[package]] -name = "opentelemetry-otlp" -version = "0.31.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7a2366db2dca4d2ad033cad11e6ee42844fd727007af5ad04a1730f4cb8163bf" -dependencies = [ - "http", - "opentelemetry", - "opentelemetry-http", - "opentelemetry-proto", - "opentelemetry_sdk", - "prost 0.14.3", - "reqwest", - "thiserror 2.0.18", - "tokio", - "tonic 0.14.5", - "tracing", -] - -[[package]] -name = "opentelemetry-proto" -version = "0.31.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a7175df06de5eaee9909d4805a3d07e28bb752c34cab57fa9cff549da596b30f" -dependencies = [ - "opentelemetry", - "opentelemetry_sdk", - "prost 0.14.3", - "tonic 0.14.5", - "tonic-prost", -] - -[[package]] -name = "opentelemetry-semantic-conventions" -version = "0.31.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e62e29dfe041afb8ed2a6c9737ab57db4907285d999ef8ad3a59092a36bdc846" - -[[package]] -name = "opentelemetry_sdk" -version = "0.31.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e14ae4f5991976fd48df6d843de219ca6d31b01daaab2dad5af2badeded372bd" -dependencies = [ - "futures-channel", - "futures-executor", - "futures-util", - "opentelemetry", - "percent-encoding", - "rand 0.9.4", - "thiserror 2.0.18", -] - -[[package]] -name = "option-ext" -version = "0.2.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "04744f49eae99ab78e0d5c0b603ab218f515ea8cfe5a456d7629ad883a3b6e7d" - -[[package]] -name = "owo-colors" -version = "4.2.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9c6901729fa79e91a0913333229e9ca5dc725089d1c363b2f4b4760709dc4a52" - -[[package]] -name = "p256" -version = "0.13.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c9863ad85fa8f4460f9c48cb909d38a0d689dba1f6f6988a5e3e0d31071bcd4b" -dependencies = [ - "ecdsa", - "elliptic-curve", - "primeorder", - "sha2 0.10.9", -] - -[[package]] -name = "page_size" -version = "0.6.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "30d5b2194ed13191c1999ae0704b7839fb18384fa22e49b57eeaa97d79ce40da" -dependencies = [ - "libc", - "winapi", -] - -[[package]] -name = "parity-scale-codec" -version = "3.7.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "799781ae679d79a948e13d4824a40970bfa500058d245760dd857301059810fa" -dependencies = [ - "arbitrary", - "arrayvec", - "bitvec", - "byte-slice-cast", - "bytes", - "const_format", - "impl-trait-for-tuples", - "parity-scale-codec-derive", - "rustversion", - "serde", -] - -[[package]] -name = "parity-scale-codec-derive" -version = "3.7.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "34b4653168b563151153c9e4c08ebed57fb8262bebfa79711552fa983c623e7a" -dependencies = [ - "proc-macro-crate", - "proc-macro2", - "quote", - "syn 2.0.117", -] - -[[package]] -name = "parking" -version = "2.2.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f38d5652c16fde515bb1ecef450ab0f6a219d619a7274976324d5e377f7dceba" - -[[package]] -name = "parking_lot" -version = "0.12.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "93857453250e3077bd71ff98b6a65ea6621a19bb0f559a85248955ac12c45a1a" -dependencies = [ - "lock_api", - "parking_lot_core", -] - -[[package]] -name = "parking_lot_core" -version = "0.9.12" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2621685985a2ebf1c516881c026032ac7deafcda1a2c9b7850dc81e3dfcb64c1" -dependencies = [ - "cfg-if", - "libc", - "redox_syscall 0.5.18", - "smallvec", - "windows-link", -] - -[[package]] -name = "paste" -version = "1.0.15" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "57c0d7b74b563b49d38dae00a0c37d4d6de9b432382b2892f0574ddcae73fd0a" - -[[package]] -name = "pastey" -version = "0.2.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b867cad97c0791bbd3aaa6472142568c6c9e8f71937e98379f584cfb0cf35bec" - -[[package]] -name = "pathdiff" -version = "0.2.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "df94ce210e5bc13cb6651479fa48d14f601d9858cfe0467f43ae157023b938d3" - -[[package]] -name = "pbkdf2" -version = "0.12.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f8ed6a7761f76e3b9f92dfb0a60a6a6477c61024b775147ff0973a02653abaf2" -dependencies = [ - "digest 0.10.7", - "hmac 0.12.1", -] - -[[package]] -name = "pem" -version = "3.0.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1d30c53c26bc5b31a98cd02d20f25a7c8567146caf63ed593a9d87b2775291be" -dependencies = [ - "base64 0.22.1", - "serde_core", -] - -[[package]] -name = "pem-rfc7468" -version = "0.7.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "88b39c9bfcfc231068454382784bb460aae594343fb030d46e9f50a645418412" -dependencies = [ - "base64ct", -] - -[[package]] -name = "percent-encoding" -version = "2.3.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9b4f627cb1b25917193a259e49bdad08f671f8d9708acfd5fe0a8c1455d87220" - -[[package]] -name = "pest" -version = "2.8.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e0848c601009d37dfa3430c4666e147e49cdcf1b92ecd3e63657d8a5f19da662" -dependencies = [ - "memchr", - "ucd-trie", -] - -[[package]] -name = "pest_derive" -version = "2.8.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "11f486f1ea21e6c10ed15d5a7c77165d0ee443402f0780849d1768e7d9d6fe77" -dependencies = [ - "pest", - "pest_generator", -] - -[[package]] -name = "pest_generator" -version = "2.8.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8040c4647b13b210a963c1ed407c1ff4fdfa01c31d6d2a098218702e6664f94f" -dependencies = [ - "pest", - "pest_meta", - "proc-macro2", - "quote", - "syn 2.0.117", -] - -[[package]] -name = "pest_meta" -version = "2.8.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "89815c69d36021a140146f26659a81d6c2afa33d216d736dd4be5381a7362220" -dependencies = [ - "pest", - "sha2 0.10.9", -] - -[[package]] -name = "petgraph" -version = "0.6.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b4c5cc86750666a3ed20bdaf5ca2a0344f9c67674cae0515bec2da16fbaa47db" -dependencies = [ - "fixedbitset 0.4.2", - "indexmap 2.13.0", -] - -[[package]] -name = "petgraph" -version = "0.7.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3672b37090dbd86368a4145bc067582552b29c27377cad4e0a306c97f9bd7772" -dependencies = [ - "fixedbitset 0.5.7", - "indexmap 2.13.0", -] - -[[package]] -name = "pharos" -version = "0.5.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e9567389417feee6ce15dd6527a8a1ecac205ef62c2932bcf3d9f6fc5b78b414" -dependencies = [ - "futures", - "rustc_version 0.4.1", -] - -[[package]] -name = "phf" -version = "0.13.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c1562dc717473dbaa4c1f85a36410e03c047b2e7df7f45ee938fbef64ae7fadf" -dependencies = [ - "phf_macros", - "phf_shared", - "serde", -] - -[[package]] -name = "phf_generator" -version = "0.13.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "135ace3a761e564ec88c03a77317a7c6b80bb7f7135ef2544dbe054243b89737" -dependencies = [ - "fastrand", - "phf_shared", -] - -[[package]] -name = "phf_macros" -version = "0.13.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "812f032b54b1e759ccd5f8b6677695d5268c588701effba24601f6932f8269ef" -dependencies = [ - "phf_generator", - "phf_shared", - "proc-macro2", - "quote", - "syn 2.0.117", -] - -[[package]] -name = "phf_shared" -version = "0.13.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e57fef6bc5981e38c2ce2d63bfa546861309f875b8a75f092d1d54ae2d64f266" -dependencies = [ - "siphasher", -] - -[[package]] -name = "pin-project" -version = "1.1.10" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "677f1add503faace112b9f1373e43e9e054bfdd22ff1a63c1bc485eaec6a6a8a" -dependencies = [ - "pin-project-internal", -] - -[[package]] -name = "pin-project-internal" -version = "1.1.10" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6e918e4ff8c4549eb882f14b3a4bc8c8bc93de829416eacf579f1207a8fbf861" -dependencies = [ - "proc-macro2", - "quote", - "syn 2.0.117", -] - -[[package]] -name = "pin-project-lite" -version = "0.2.16" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3b3cff922bd51709b605d9ead9aa71031d81447142d828eb4a6eba76fe619f9b" - -[[package]] -name = "pin-utils" -version = "0.1.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8b870d8c151b6f2fb93e84a13146138f05d02ed11c7e7c54f8826aaaf7c9f184" - -[[package]] -name = "pkcs8" -version = "0.10.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f950b2377845cebe5cf8b5165cb3cc1a5e0fa5cfa3e1f7f55707d8fd82e0a7b7" -dependencies = [ - "der 0.7.10", - "spki 0.7.3", -] - -[[package]] -name = "pkcs8" -version = "0.11.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "451913da69c775a56034ea8d9003d27ee8948e12443eae7c038ba100a4f21cb7" -dependencies = [ - "der 0.8.0", - "spki 0.8.0", -] - -[[package]] -name = "pkg-config" -version = "0.3.32" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7edddbd0b52d732b21ad9a5fab5c704c14cd949e5e9a1ec5929a24fded1b904c" - -[[package]] -name = "plain_hasher" -version = "0.2.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1e19e6491bdde87c2c43d70f4c194bc8a758f2eb732df00f61e43f7362e3b4cc" -dependencies = [ - "crunchy", -] - -[[package]] -name = "plotters" -version = "0.3.7" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5aeb6f403d7a4911efb1e33402027fc44f29b5bf6def3effcc22d7bb75f2b747" -dependencies = [ - "num-traits", - "plotters-backend", - "plotters-svg", - "wasm-bindgen", - "web-sys", -] - -[[package]] -name = "plotters-backend" -version = "0.3.7" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "df42e13c12958a16b3f7f4386b9ab1f3e7933914ecea48da7139435263a4172a" - -[[package]] -name = "plotters-svg" -version = "0.3.7" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "51bae2ac328883f7acdfea3d66a7c35751187f870bc81f94563733a154d7a670" -dependencies = [ - "plotters-backend", -] - -[[package]] -name = "polling" -version = "3.11.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5d0e4f59085d47d8241c88ead0f274e8a0cb551f3625263c05eb8dd897c34218" -dependencies = [ - "cfg-if", - "concurrent-queue", - "hermit-abi", - "pin-project-lite", - "rustix 1.1.3", - "windows-sys 0.61.2", -] - -[[package]] -name = "poly1305" -version = "0.8.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8159bd90725d2df49889a078b54f4f79e87f1f8a8444194cdca81d38f5393abf" -dependencies = [ - "cpufeatures 0.2.17", - "opaque-debug", - "universal-hash", -] - -[[package]] -name = "polyval" -version = "0.6.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9d1fe60d06143b2430aa532c94cfe9e29783047f06c0d7fd359a9a51b729fa25" -dependencies = [ - "cfg-if", - "cpufeatures 0.2.17", - "opaque-debug", - "universal-hash", -] - -[[package]] -name = "portable-atomic" -version = "1.13.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c33a9471896f1c69cecef8d20cbe2f7accd12527ce60845ff44c153bb2a21b49" - -[[package]] -name = "potential_utf" -version = "0.1.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b73949432f5e2a09657003c25bca5e19a0e9c84f8058ca374f49e0ebe605af77" -dependencies = [ - "zerovec", -] - -[[package]] -name = "powerfmt" -version = "0.2.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "439ee305def115ba05938db6eb1644ff94165c5ab5e9420d1c1bcedbba909391" - -[[package]] -name = "pprof" -version = "0.15.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "38a01da47675efa7673b032bf8efd8214f1917d89685e07e395ab125ea42b187" -dependencies = [ - "aligned-vec", - "backtrace", - "cfg-if", - "findshlibs", - "libc", - "log", - "nix", - "once_cell", - "prost 0.12.6", - "prost-build 0.12.6", - "prost-derive 0.12.6", - "sha2 0.10.9", - "smallvec", - "spin", - "symbolic-demangle", - "tempfile", - "thiserror 2.0.18", -] - -[[package]] -name = "pprof_hyper_server" -version = "0.2.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f5c01362cfe33f17ec7f4b9e04496496a5f1231157333917604261d1285d1675" -dependencies = [ - "anyhow", - "async-channel", - "async-executor", - "async-io", - "flate2", - "form_urlencoded", - "http-body-util", - "hyper", - "jemalloc_pprof", - "pprof", - "smol-hyper", - "tikv-jemallocator", -] - -[[package]] -name = "pprof_util" -version = "0.8.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "eea0cc524de808a6d98d192a3d99fe95617031ad4a52ec0a0f987ef4432e8fe1" -dependencies = [ - "anyhow", - "backtrace", - "flate2", - "num", - "paste", - "prost 0.14.3", -] - -[[package]] -name = "ppv-lite86" -version = "0.2.21" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "85eae3c4ed2f50dcfe72643da4befc30deadb458a9b590d720cde2f2b1e97da9" -dependencies = [ - "zerocopy", -] - -[[package]] -name = "predicates" -version = "3.1.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ada8f2932f28a27ee7b70dd6c1c39ea0675c55a36879ab92f3a715eaa1e63cfe" -dependencies = [ - "anstyle", - "difflib", - "float-cmp", - "normalize-line-endings", - "predicates-core", - "regex", -] - -[[package]] -name = "predicates-core" -version = "1.0.10" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cad38746f3166b4031b1a0d39ad9f954dd291e7854fcc0eed52ee41a0b50d144" - -[[package]] -name = "predicates-tree" -version = "1.0.13" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d0de1b847b39c8131db0467e9df1ff60e6d0562ab8e9a16e568ad0fdb372e2f2" -dependencies = [ - "predicates-core", - "termtree", -] - -[[package]] -name = "pretty_assertions" -version = "1.4.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3ae130e2f271fbc2ac3a40fb1d07180839cdbbe443c7a27e1e3c13c5cac0116d" -dependencies = [ - "diff", - "yansi", -] - -[[package]] -name = "prettyplease" -version = "0.2.37" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "479ca8adacdd7ce8f1fb39ce9ecccbfe93a3f1344b3d0d97f20bc0196208f62b" -dependencies = [ - "proc-macro2", - "syn 2.0.117", -] - -[[package]] -name = "primeorder" -version = "0.13.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "353e1ca18966c16d9deb1c69278edbc5f194139612772bd9537af60ac231e1e6" -dependencies = [ - "elliptic-curve", -] - -[[package]] -name = "primitive-types" -version = "0.12.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0b34d9fd68ae0b74a41b21c03c2f62847aa0ffea044eee893b4c140b37e244e2" -dependencies = [ - "fixed-hash", - "impl-codec", - "uint 0.9.5", -] - -[[package]] -name = "proc-macro-crate" -version = "3.4.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "219cb19e96be00ab2e37d6e299658a0cfa83e52429179969b0f0121b4ac46983" -dependencies = [ - "toml_edit 0.23.10+spec-1.0.0", -] - -[[package]] -name = "proc-macro-error-attr2" -version = "2.0.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "96de42df36bb9bba5542fe9f1a054b8cc87e172759a1868aa05c1f3acc89dfc5" -dependencies = [ - "proc-macro2", - "quote", -] - -[[package]] -name = "proc-macro-error2" -version = "2.0.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "11ec05c52be0a07b08061f7dd003e7d7092e0472bc731b4af7bb1ef876109802" -dependencies = [ - "proc-macro-error-attr2", - "proc-macro2", - "quote", - "syn 2.0.117", -] - -[[package]] -name = "proc-macro2" -version = "1.0.106" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8fd00f0bb2e90d81d1044c2b32617f68fcb9fa3bb7640c23e9c748e53fb30934" -dependencies = [ - "unicode-ident", -] - -[[package]] -name = "procfs" -version = "0.17.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cc5b72d8145275d844d4b5f6d4e1eef00c8cd889edb6035c21675d1bb1f45c9f" -dependencies = [ - "bitflags 2.11.0", - "chrono", - "flate2", - "hex", - "procfs-core 0.17.0", - "rustix 0.38.44", -] - -[[package]] -name = "procfs" -version = "0.18.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "25485360a54d6861439d60facef26de713b1e126bf015ec8f98239467a2b82f7" -dependencies = [ - "bitflags 2.11.0", - "chrono", - "flate2", - "procfs-core 0.18.0", - "rustix 1.1.3", -] - -[[package]] -name = "procfs-core" -version = "0.17.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "239df02d8349b06fc07398a3a1697b06418223b1c7725085e801e7c0fc6a12ec" -dependencies = [ - "bitflags 2.11.0", - "chrono", - "hex", -] - -[[package]] -name = "procfs-core" -version = "0.18.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e6401bf7b6af22f78b563665d15a22e9aef27775b79b149a66ca022468a4e405" -dependencies = [ - "bitflags 2.11.0", - "chrono", - "hex", -] - -[[package]] -name = "prometheus-client" -version = "0.23.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cf41c1a7c32ed72abe5082fb19505b969095c12da9f5732a4bc9878757fd087c" -dependencies = [ - "dtoa", - "itoa", - "parking_lot", - "prometheus-client-derive-encode", -] - -[[package]] -name = "prometheus-client-derive-encode" -version = "0.4.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "440f724eba9f6996b75d63681b0a92b06947f1457076d503a4d2e2c8f56442b8" -dependencies = [ - "proc-macro2", - "quote", - "syn 2.0.117", -] - -[[package]] -name = "prometheus-parse" -version = "0.2.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "811031bea65e5a401fb2e1f37d802cca6601e204ac463809a3189352d13b78a5" -dependencies = [ - "chrono", - "itertools 0.12.1", - "once_cell", - "regex", -] - -[[package]] -name = "proptest" -version = "1.10.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "37566cb3fdacef14c0737f9546df7cfeadbfbc9fef10991038bf5015d0c80532" -dependencies = [ - "bit-set", - "bit-vec", - "bitflags 2.11.0", - "num-traits", - "rand 0.9.4", - "rand_chacha 0.9.0", - "rand_xorshift", - "regex-syntax", - "rusty-fork", - "tempfile", - "unarray", -] - -[[package]] -name = "proptest-arbitrary-interop" -version = "0.1.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a1981e49bd2432249da8b0e11e5557099a8e74690d6b94e721f7dc0bb7f3555f" -dependencies = [ - "arbitrary", - "proptest", -] - -[[package]] -name = "proptest-derive" -version = "0.7.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fb6dc647500e84a25a85b100e76c85b8ace114c209432dc174f20aac11d4ed6c" -dependencies = [ - "proc-macro2", - "quote", - "syn 2.0.117", -] - -[[package]] -name = "prost" -version = "0.12.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "deb1435c188b76130da55f17a466d252ff7b1418b2ad3e037d127b94e3411f29" -dependencies = [ - "bytes", - "prost-derive 0.12.6", -] - -[[package]] -name = "prost" -version = "0.13.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2796faa41db3ec313a31f7624d9286acf277b52de526150b7e69f3debf891ee5" -dependencies = [ - "bytes", - "prost-derive 0.13.5", -] - -[[package]] -name = "prost" -version = "0.14.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d2ea70524a2f82d518bce41317d0fae74151505651af45faf1ffbd6fd33f0568" -dependencies = [ - "bytes", - "prost-derive 0.14.3", -] - -[[package]] -name = "prost-build" -version = "0.12.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "22505a5c94da8e3b7c2996394d1c933236c4d743e81a410bcca4e6989fc066a4" -dependencies = [ - "bytes", - "heck", - "itertools 0.12.1", - "log", - "multimap", - "once_cell", - "petgraph 0.6.5", - "prettyplease", - "prost 0.12.6", - "prost-types 0.12.6", - "regex", - "syn 2.0.117", - "tempfile", -] - -[[package]] -name = "prost-build" -version = "0.13.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "be769465445e8c1474e9c5dac2018218498557af32d9ed057325ec9a41ae81bf" -dependencies = [ - "heck", - "itertools 0.12.1", - "log", - "multimap", - "once_cell", - "petgraph 0.7.1", - "prettyplease", - "prost 0.13.5", - "prost-types 0.13.5", - "regex", - "syn 2.0.117", - "tempfile", -] - -[[package]] -name = "prost-derive" -version = "0.12.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "81bddcdb20abf9501610992b6759a4c888aef7d1a7247ef75e2404275ac24af1" -dependencies = [ - "anyhow", - "itertools 0.12.1", - "proc-macro2", - "quote", - "syn 2.0.117", -] - -[[package]] -name = "prost-derive" -version = "0.13.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8a56d757972c98b346a9b766e3f02746cde6dd1cd1d1d563472929fdd74bec4d" -dependencies = [ - "anyhow", - "itertools 0.12.1", - "proc-macro2", - "quote", - "syn 2.0.117", -] - -[[package]] -name = "prost-derive" -version = "0.14.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "27c6023962132f4b30eb4c172c91ce92d933da334c59c23cddee82358ddafb0b" -dependencies = [ - "anyhow", - "itertools 0.12.1", - "proc-macro2", - "quote", - "syn 2.0.117", -] - -[[package]] -name = "prost-reflect" -version = "0.15.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "37587d5a8a1b3dc9863403d084fc2254b91ab75a702207098837950767e2260b" -dependencies = [ - "logos", - "miette", - "prost 0.13.5", - "prost-types 0.13.5", -] - -[[package]] -name = "prost-types" -version = "0.12.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9091c90b0a32608e984ff2fa4091273cbdd755d54935c51d520887f4a1dbd5b0" -dependencies = [ - "prost 0.12.6", -] - -[[package]] -name = "prost-types" -version = "0.13.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "52c2c1bf36ddb1a1c396b3601a3cec27c2462e45f07c386894ec3ccf5332bd16" -dependencies = [ - "prost 0.13.5", -] - -[[package]] -name = "protox" -version = "0.8.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "424c2bd294b69c49b949f3619362bc3c5d28298cd1163b6d1a62df37c16461aa" -dependencies = [ - "bytes", - "miette", - "prost 0.13.5", - "prost-reflect", - "prost-types 0.13.5", - "protox-parse", - "thiserror 2.0.18", -] - -[[package]] -name = "protox-parse" -version = "0.8.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "57927f9dbeeffcce7192404deee6157a640cbb3fe8ac11eabbe571565949ab75" -dependencies = [ - "logos", - "miette", - "prost-types 0.13.5", - "thiserror 2.0.18", -] - -[[package]] -name = "quake" -version = "0.0.1" -dependencies = [ - "alloy-consensus", - "alloy-primitives", - "alloy-provider", - "alloy-rpc-types-admin", - "alloy-rpc-types-txpool", - "alloy-signer", - "alloy-signer-local", - "alloy-sol-macro", - "alloy-sol-types", - "alloy-transport-ws", - "arc-checks", - "arc-consensus-types", - "arc-malachitebft-config", - "arc-mesh-analysis", - "arc-node-consensus", - "arc-node-consensus-cli", - "arc-version", - "axum 0.8.8", - "backon", - "bytesize", - "chrono", - "clap", - "clap-verbosity-flag", - "color-eyre", - "deranged", - "dotenvy", - "futures", - "futures-util", - "handlebars", - "hex", - "humantime", - "indexmap 2.13.0", - "inventory", - "itertools 0.14.0", - "k256", - "lz4", - "once_cell", - "pathdiff", - "quake-macros", - "rand 0.8.5", - "regex", - "reqwest", - "reth-network-peers", - "rmcp", - "schemars 1.2.1", - "secp256k1 0.30.0", - "serde", - "serde_json", - "spammer", - "strum 0.27.2", - "strum_macros 0.27.2", - "tempfile", - "thiserror 2.0.18", - "tokio", - "toml 0.8.23", - "tracing", - "tracing-subscriber", - "url", -] - -[[package]] -name = "quake-macros" -version = "0.1.0" -dependencies = [ - "proc-macro2", - "quote", - "syn 2.0.117", -] - -[[package]] -name = "quanta" -version = "0.12.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f3ab5a9d756f0d97bdc89019bd2e4ea098cf9cde50ee7564dde6b81ccc8f06c7" -dependencies = [ - "crossbeam-utils", - "libc", - "once_cell", - "raw-cpuid", - "wasi", - "web-sys", - "winapi", -] - -[[package]] -name = "quick-error" -version = "1.2.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a1d01941d82fa2ab50be1e79e6714289dd7cde78eba4c074bc5a4374f650dfe0" - -[[package]] -name = "quick-protobuf" -version = "0.8.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9d6da84cc204722a989e01ba2f6e1e276e190f22263d0cb6ce8526fcdb0d2e1f" -dependencies = [ - "byteorder", -] - -[[package]] -name = "quick-protobuf-codec" -version = "0.3.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "15a0580ab32b169745d7a39db2ba969226ca16738931be152a3209b409de2474" -dependencies = [ - "asynchronous-codec", - "bytes", - "quick-protobuf", - "thiserror 1.0.69", - "unsigned-varint 0.8.0", -] - -[[package]] -name = "quinn" -version = "0.11.9" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b9e20a958963c291dc322d98411f541009df2ced7b5a4f2bd52337638cfccf20" -dependencies = [ - "bytes", - "cfg_aliases", - "futures-io", - "pin-project-lite", - "quinn-proto", - "quinn-udp", - "rustc-hash", - "rustls", - "socket2 0.6.2", - "thiserror 2.0.18", - "tokio", - "tracing", - "web-time", -] - -[[package]] -name = "quinn-proto" -version = "0.11.14" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "434b42fec591c96ef50e21e886936e66d3cc3f737104fdb9b737c40ffb94c098" -dependencies = [ - "bytes", - "getrandom 0.3.4", - "lru-slab", - "rand 0.9.4", - "ring", - "rustc-hash", - "rustls", - "rustls-pki-types", - "slab", - "thiserror 2.0.18", - "tinyvec", - "tracing", - "web-time", -] - -[[package]] -name = "quinn-udp" -version = "0.5.14" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "addec6a0dcad8a8d96a771f815f0eaf55f9d1805756410b39f5fa81332574cbd" -dependencies = [ - "cfg_aliases", - "libc", - "once_cell", - "socket2 0.6.2", - "tracing", - "windows-sys 0.59.0", -] - -[[package]] -name = "quote" -version = "1.0.44" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "21b2ebcf727b7760c461f091f9f0f539b77b8e87f2fd88131e7f1b433b3cece4" -dependencies = [ - "proc-macro2", -] - -[[package]] -name = "r-efi" -version = "5.3.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "69cdb34c158ceb288df11e18b4bd39de994f6657d83847bdffdbd7f346754b0f" - -[[package]] -name = "ractor" -version = "0.15.10" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6102314f700f3e8df466c49110830b18cbfc172f88f27a9d7383e455663b1be7" -dependencies = [ - "async-trait", - "bon", - "dashmap", - "futures", - "js-sys", - "once_cell", - "strum 0.26.3", - "tokio", - "tokio_with_wasm", - "tracing", - "wasm-bindgen", - "wasm-bindgen-futures", - "web-time", -] - -[[package]] -name = "radium" -version = "0.7.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "dc33ff2d4973d518d823d61aa239014831e521c75da58e3df4840d3f47749d09" - -[[package]] -name = "rand" -version = "0.8.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "34af8d1a0e25924bc5b7c43c079c942339d8f0a8b57c39049bef581b46327404" -dependencies = [ - "libc", - "rand_chacha 0.3.1", - "rand_core 0.6.4", - "serde", -] - -[[package]] -name = "rand" -version = "0.9.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "44c5af06bb1b7d3216d91932aed5265164bf384dc89cd6ba05cf59a35f5f76ea" -dependencies = [ - "rand_chacha 0.9.0", - "rand_core 0.9.5", - "serde", -] - -[[package]] -name = "rand" -version = "0.10.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d2e8e8bcc7961af1fdac401278c6a831614941f6164ee3bf4ce61b7edb162207" -dependencies = [ - "chacha20 0.10.0", - "getrandom 0.4.1", - "rand_core 0.10.0", -] - -[[package]] -name = "rand_chacha" -version = "0.3.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e6c10a63a0fa32252be49d21e7709d4d4baf8d231c2dbce1eaa8141b9b127d88" -dependencies = [ - "ppv-lite86", - "rand_core 0.6.4", -] - -[[package]] -name = "rand_chacha" -version = "0.9.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d3022b5f1df60f26e1ffddd6c66e8aa15de382ae63b3a0c1bfc0e4d3e3f325cb" -dependencies = [ - "ppv-lite86", - "rand_core 0.9.5", -] - -[[package]] -name = "rand_core" -version = "0.6.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ec0be4795e2f6a28069bec0b5ff3e2ac9bafc99e6a9a7dc3547996c5c816922c" -dependencies = [ - "getrandom 0.2.17", -] - -[[package]] -name = "rand_core" -version = "0.9.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "76afc826de14238e6e8c374ddcc1fa19e374fd8dd986b0d2af0d02377261d83c" -dependencies = [ - "getrandom 0.3.4", - "serde", -] - -[[package]] -name = "rand_core" -version = "0.10.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0c8d0fd677905edcbeedbf2edb6494d676f0e98d54d5cf9bda0b061cb8fb8aba" - -[[package]] -name = "rand_xorshift" -version = "0.4.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "513962919efc330f829edb2535844d1b912b0fbe2ca165d613e4e8788bb05a5a" -dependencies = [ - "rand_core 0.9.5", -] - -[[package]] -name = "rand_xoshiro" -version = "0.7.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f703f4665700daf5512dcca5f43afa6af89f09db47fb56be587f80636bda2d41" -dependencies = [ - "rand_core 0.9.5", -] - -[[package]] -name = "rapidhash" -version = "4.4.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b5e48930979c155e2f33aa36ab3119b5ee81332beb6482199a8ecd6029b80b59" -dependencies = [ - "rand 0.9.4", - "rustversion", -] - -[[package]] -name = "ratatui" -version = "0.30.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d1ce67fb8ba4446454d1c8dbaeda0557ff5e94d39d5e5ed7f10a65eb4c8266bc" -dependencies = [ - "instability", - "ratatui-core", - "ratatui-crossterm", - "ratatui-widgets", -] - -[[package]] -name = "ratatui-core" -version = "0.1.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5ef8dea09a92caaf73bff7adb70b76162e5937524058a7e5bff37869cbbec293" -dependencies = [ - "bitflags 2.11.0", - "compact_str", - "hashbrown 0.16.1", - "indoc", - "itertools 0.14.0", - "kasuari", - "lru 0.16.3", - "strum 0.27.2", - "thiserror 2.0.18", - "unicode-segmentation", - "unicode-truncate", - "unicode-width 0.2.2", -] - -[[package]] -name = "ratatui-crossterm" -version = "0.1.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "577c9b9f652b4c121fb25c6a391dd06406d3b092ba68827e6d2f09550edc54b3" -dependencies = [ - "cfg-if", - "crossterm", - "instability", - "ratatui-core", -] - -[[package]] -name = "ratatui-widgets" -version = "0.3.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d7dbfa023cd4e604c2553483820c5fe8aa9d71a42eea5aa77c6e7f35756612db" -dependencies = [ - "bitflags 2.11.0", - "hashbrown 0.16.1", - "indoc", - "instability", - "itertools 0.14.0", - "line-clipping", - "ratatui-core", - "strum 0.27.2", - "time", - "unicode-segmentation", - "unicode-width 0.2.2", -] - -[[package]] -name = "raw-cpuid" -version = "11.6.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "498cd0dc59d73224351ee52a95fee0f1a617a2eae0e7d9d720cc622c73a54186" -dependencies = [ - "bitflags 2.11.0", -] - -[[package]] -name = "rayon" -version = "1.11.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "368f01d005bf8fd9b1206fb6fa653e6c4a81ceb1466406b81792d87c5677a58f" -dependencies = [ - "either", - "rayon-core", -] - -[[package]] -name = "rayon-core" -version = "1.13.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "22e18b0f0062d30d4230b2e85ff77fdfe4326feb054b9783a3460d8435c8ab91" -dependencies = [ - "crossbeam-deque", - "crossbeam-utils", -] - -[[package]] -name = "rcgen" -version = "0.13.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "75e669e5202259b5314d1ea5397316ad400819437857b90861765f24c4cf80a2" -dependencies = [ - "pem", - "ring", - "rustls-pki-types", - "time", - "yasna", -] - -[[package]] -name = "recvmsg" -version = "1.0.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d3edd4d5d42c92f0a659926464d4cce56b562761267ecf0f469d85b7de384175" - -[[package]] -name = "redb" -version = "2.6.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8eca1e9d98d5a7e9002d0013e18d5a9b000aee942eb134883a82f06ebffb6c01" -dependencies = [ - "libc", -] - -[[package]] -name = "redox_syscall" -version = "0.5.18" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ed2bf2547551a7053d6fdfafda3f938979645c44812fbfcda098faae3f1a362d" -dependencies = [ - "bitflags 2.11.0", -] - -[[package]] -name = "redox_syscall" -version = "0.7.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "35985aa610addc02e24fc232012c86fd11f14111180f902b67e2d5331f8ebf2b" -dependencies = [ - "bitflags 2.11.0", -] - -[[package]] -name = "redox_users" -version = "0.4.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ba009ff324d1fc1b900bd1fdb31564febe58a8ccc8a6fdbb93b543d33b13ca43" -dependencies = [ - "getrandom 0.2.17", - "libredox", - "thiserror 1.0.69", -] - -[[package]] -name = "redox_users" -version = "0.5.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a4e608c6638b9c18977b00b475ac1f28d14e84b27d8d42f70e0bf1e3dec127ac" -dependencies = [ - "getrandom 0.2.17", - "libredox", - "thiserror 2.0.18", -] - -[[package]] -name = "ref-cast" -version = "1.0.25" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f354300ae66f76f1c85c5f84693f0ce81d747e2c3f21a45fef496d89c960bf7d" -dependencies = [ - "ref-cast-impl", -] - -[[package]] -name = "ref-cast-impl" -version = "1.0.25" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b7186006dcb21920990093f30e3dea63b7d6e977bf1256be20c3563a5db070da" -dependencies = [ - "proc-macro2", - "quote", - "syn 2.0.117", -] - -[[package]] -name = "regex" -version = "1.12.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e10754a14b9137dd7b1e3e5b0493cc9171fdd105e0ab477f51b72e7f3ac0e276" -dependencies = [ - "aho-corasick", - "memchr", - "regex-automata", - "regex-syntax", -] - -[[package]] -name = "regex-automata" -version = "0.4.14" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6e1dd4122fc1595e8162618945476892eefca7b88c52820e74af6262213cae8f" -dependencies = [ - "aho-corasick", - "memchr", - "regex-syntax", -] - -[[package]] -name = "regex-syntax" -version = "0.8.9" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a96887878f22d7bad8a3b6dc5b7440e0ada9a245242924394987b21cf2210a4c" - -[[package]] -name = "regress" -version = "0.10.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2057b2325e68a893284d1538021ab90279adac1139957ca2a74426c6f118fb48" -dependencies = [ - "hashbrown 0.16.1", - "memchr", -] - -[[package]] -name = "relative-path" -version = "1.9.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ba39f3699c378cd8970968dcbff9c43159ea4cfbd88d43c00b22f2ef10a435d2" - -[[package]] -name = "reqwest" -version = "0.12.28" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "eddd3ca559203180a307f12d114c268abf583f59b03cb906fd0b3ff8646c1147" -dependencies = [ - "base64 0.22.1", - "bytes", - "futures-channel", - "futures-core", - "futures-util", - "http", - "http-body", - "http-body-util", - "hyper", - "hyper-rustls", - "hyper-tls", - "hyper-util", - "js-sys", - "log", - "native-tls", - "percent-encoding", - "pin-project-lite", - "quinn", - "rustls", - "rustls-native-certs", - "rustls-pki-types", - "serde", - "serde_json", - "serde_urlencoded", - "sync_wrapper", - "tokio", - "tokio-native-tls", - "tokio-rustls", - "tokio-util", - "tower 0.5.3", - "tower-http", - "tower-service", - "url", - "wasm-bindgen", - "wasm-bindgen-futures", - "wasm-streams", - "web-sys", - "webpki-roots 1.0.6", -] - -[[package]] -name = "resolv-conf" -version = "0.7.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1e061d1b48cb8d38042de4ae0a7a6401009d6143dc80d2e2d6f31f0bdd6470c7" - -[[package]] -name = "reth-basic-payload-builder" -version = "1.11.3" -source = "git+https://github.com/paradigmxyz/reth?tag=v1.11.3#d6324d63e27ef6b7c49cdc9b1977c1b808234c7b" -dependencies = [ - "alloy-consensus", - "alloy-eips", - "alloy-primitives", - "futures-core", - "futures-util", - "metrics", - "reth-chain-state", - "reth-metrics", - "reth-payload-builder", - "reth-payload-builder-primitives", - "reth-payload-primitives", - "reth-primitives-traits", - "reth-revm", - "reth-storage-api", - "reth-tasks", - "tokio", - "tracing", -] - -[[package]] -name = "reth-chain-state" -version = "1.11.3" -source = "git+https://github.com/paradigmxyz/reth?tag=v1.11.3#d6324d63e27ef6b7c49cdc9b1977c1b808234c7b" -dependencies = [ - "alloy-consensus", - "alloy-eips", - "alloy-primitives", - "alloy-signer", - "alloy-signer-local", - "derive_more", - "metrics", - "parking_lot", - "pin-project", - "rand 0.9.4", - "rayon", - "reth-chainspec", - "reth-errors", - "reth-ethereum-primitives", - "reth-execution-types", - "reth-metrics", - "reth-primitives-traits", - "reth-storage-api", - "reth-trie", - "revm-database", - "revm-state", - "serde", - "tokio", - "tokio-stream", - "tracing", -] - -[[package]] -name = "reth-chainspec" -version = "1.11.3" -source = "git+https://github.com/paradigmxyz/reth?tag=v1.11.3#d6324d63e27ef6b7c49cdc9b1977c1b808234c7b" -dependencies = [ - "alloy-chains", - "alloy-consensus", - "alloy-eips", - "alloy-evm", - "alloy-genesis", - "alloy-primitives", - "alloy-trie", - "auto_impl", - "derive_more", - "reth-ethereum-forks", - "reth-network-peers", - "reth-primitives-traits", - "serde_json", -] - -[[package]] -name = "reth-cli" -version = "1.11.3" -source = "git+https://github.com/paradigmxyz/reth?tag=v1.11.3#d6324d63e27ef6b7c49cdc9b1977c1b808234c7b" -dependencies = [ - "alloy-genesis", - "clap", - "eyre", - "reth-cli-runner", - "reth-db", - "serde_json", - "shellexpand", -] - -[[package]] -name = "reth-cli-commands" -version = "1.11.3" -source = "git+https://github.com/paradigmxyz/reth?tag=v1.11.3#d6324d63e27ef6b7c49cdc9b1977c1b808234c7b" -dependencies = [ - "alloy-chains", - "alloy-consensus", - "alloy-eips", - "alloy-primitives", - "alloy-rlp", - "backon", - "clap", - "comfy-table", - "crossterm", - "eyre", - "fdlimit", - "futures", - "human_bytes", - "humantime", - "itertools 0.14.0", - "lz4", - "metrics", - "parking_lot", - "ratatui", - "reqwest", - "reth-chainspec", - "reth-cli", - "reth-cli-runner", - "reth-cli-util", - "reth-codecs", - "reth-config", - "reth-consensus", - "reth-db", - "reth-db-api", - "reth-db-common", - "reth-discv4", - "reth-discv5", - "reth-downloaders", - "reth-ecies", - "reth-era", - "reth-era-downloader", - "reth-era-utils", - "reth-eth-wire", - "reth-etl", - "reth-evm", - "reth-exex", - "reth-fs-util", - "reth-net-nat", - "reth-network", - "reth-network-p2p", - "reth-network-peers", - "reth-node-api", - "reth-node-builder", - "reth-node-core", - "reth-node-events", - "reth-node-metrics", - "reth-primitives-traits", - "reth-provider", - "reth-prune", - "reth-revm", - "reth-stages", - "reth-static-file", - "reth-static-file-types", - "reth-storage-api", - "reth-tasks", - "reth-trie", - "reth-trie-common", - "reth-trie-db", - "secp256k1 0.30.0", - "serde", - "serde_json", - "tar", - "tokio", - "tokio-stream", - "toml 0.9.12+spec-1.1.0", - "tracing", - "url", - "zstd", -] - -[[package]] -name = "reth-cli-runner" -version = "1.11.3" -source = "git+https://github.com/paradigmxyz/reth?tag=v1.11.3#d6324d63e27ef6b7c49cdc9b1977c1b808234c7b" -dependencies = [ - "reth-tasks", - "tokio", - "tracing", -] - -[[package]] -name = "reth-cli-util" -version = "1.11.3" -source = "git+https://github.com/paradigmxyz/reth?tag=v1.11.3#d6324d63e27ef6b7c49cdc9b1977c1b808234c7b" -dependencies = [ - "alloy-eips", - "alloy-primitives", - "cfg-if", - "eyre", - "libc", - "rand 0.8.5", - "reth-fs-util", - "secp256k1 0.30.0", - "serde", - "thiserror 2.0.18", -] - -[[package]] -name = "reth-codecs" -version = "1.11.3" -source = "git+https://github.com/paradigmxyz/reth?tag=v1.11.3#d6324d63e27ef6b7c49cdc9b1977c1b808234c7b" -dependencies = [ - "alloy-consensus", - "alloy-eips", - "alloy-genesis", - "alloy-primitives", - "alloy-trie", - "arbitrary", - "bytes", - "modular-bitfield", - "op-alloy-consensus", - "reth-codecs-derive", - "reth-zstd-compressors", - "serde", - "visibility", -] - -[[package]] -name = "reth-codecs-derive" -version = "1.11.3" -source = "git+https://github.com/paradigmxyz/reth?tag=v1.11.3#d6324d63e27ef6b7c49cdc9b1977c1b808234c7b" -dependencies = [ - "proc-macro2", - "quote", - "syn 2.0.117", -] - -[[package]] -name = "reth-config" -version = "1.11.3" -source = "git+https://github.com/paradigmxyz/reth?tag=v1.11.3#d6324d63e27ef6b7c49cdc9b1977c1b808234c7b" -dependencies = [ - "eyre", - "humantime-serde", - "reth-network-types", - "reth-prune-types", - "reth-stages-types", - "reth-static-file-types", - "serde", - "toml 0.9.12+spec-1.1.0", - "url", -] - -[[package]] -name = "reth-consensus" -version = "1.11.3" -source = "git+https://github.com/paradigmxyz/reth?tag=v1.11.3#d6324d63e27ef6b7c49cdc9b1977c1b808234c7b" -dependencies = [ - "alloy-consensus", - "alloy-primitives", - "auto_impl", - "reth-execution-types", - "reth-primitives-traits", - "thiserror 2.0.18", -] - -[[package]] -name = "reth-consensus-common" -version = "1.11.3" -source = "git+https://github.com/paradigmxyz/reth?tag=v1.11.3#d6324d63e27ef6b7c49cdc9b1977c1b808234c7b" -dependencies = [ - "alloy-consensus", - "alloy-eips", - "reth-chainspec", - "reth-consensus", - "reth-primitives-traits", -] - -[[package]] -name = "reth-consensus-debug-client" -version = "1.11.3" -source = "git+https://github.com/paradigmxyz/reth?tag=v1.11.3#d6324d63e27ef6b7c49cdc9b1977c1b808234c7b" -dependencies = [ - "alloy-consensus", - "alloy-eips", - "alloy-json-rpc", - "alloy-primitives", - "alloy-provider", - "alloy-rpc-types-engine", - "alloy-transport", - "auto_impl", - "derive_more", - "eyre", - "futures", - "reqwest", - "reth-node-api", - "reth-primitives-traits", - "reth-tracing", - "ringbuffer", - "serde", - "serde_json", - "tokio", -] - -[[package]] -name = "reth-db" -version = "1.11.3" -source = "git+https://github.com/paradigmxyz/reth?tag=v1.11.3#d6324d63e27ef6b7c49cdc9b1977c1b808234c7b" -dependencies = [ - "alloy-primitives", - "derive_more", - "eyre", - "metrics", - "page_size", - "parking_lot", - "reth-db-api", - "reth-fs-util", - "reth-libmdbx", - "reth-metrics", - "reth-nippy-jar", - "reth-static-file-types", - "reth-storage-errors", - "reth-tracing", - "rustc-hash", - "strum 0.27.2", - "sysinfo", - "tempfile", - "thiserror 2.0.18", - "tracing", -] - -[[package]] -name = "reth-db-api" -version = "1.11.3" -source = "git+https://github.com/paradigmxyz/reth?tag=v1.11.3#d6324d63e27ef6b7c49cdc9b1977c1b808234c7b" -dependencies = [ - "alloy-consensus", - "alloy-genesis", - "alloy-primitives", - "arbitrary", - "arrayvec", - "bytes", - "derive_more", - "metrics", - "modular-bitfield", - "op-alloy-consensus", - "parity-scale-codec", - "proptest", - "reth-codecs", - "reth-db-models", - "reth-ethereum-primitives", - "reth-primitives-traits", - "reth-prune-types", - "reth-stages-types", - "reth-storage-errors", - "reth-trie-common", - "roaring", - "serde", -] - -[[package]] -name = "reth-db-common" -version = "1.11.3" -source = "git+https://github.com/paradigmxyz/reth?tag=v1.11.3#d6324d63e27ef6b7c49cdc9b1977c1b808234c7b" -dependencies = [ - "alloy-consensus", - "alloy-genesis", - "alloy-primitives", - "boyer-moore-magiclen", - "eyre", - "reth-chainspec", - "reth-codecs", - "reth-config", - "reth-db-api", - "reth-etl", - "reth-execution-errors", - "reth-fs-util", - "reth-node-types", - "reth-primitives-traits", - "reth-provider", - "reth-stages-types", - "reth-static-file-types", - "reth-trie", - "reth-trie-db", - "serde", - "serde_json", - "thiserror 2.0.18", - "tracing", -] - -[[package]] -name = "reth-db-models" -version = "1.11.3" -source = "git+https://github.com/paradigmxyz/reth?tag=v1.11.3#d6324d63e27ef6b7c49cdc9b1977c1b808234c7b" -dependencies = [ - "alloy-eips", - "alloy-primitives", - "arbitrary", - "bytes", - "modular-bitfield", - "reth-codecs", - "reth-primitives-traits", - "serde", -] - -[[package]] -name = "reth-discv4" -version = "1.11.3" -source = "git+https://github.com/paradigmxyz/reth?tag=v1.11.3#d6324d63e27ef6b7c49cdc9b1977c1b808234c7b" -dependencies = [ - "alloy-primitives", - "alloy-rlp", - "discv5", - "enr", - "itertools 0.14.0", - "parking_lot", - "rand 0.8.5", - "reth-ethereum-forks", - "reth-net-banlist", - "reth-net-nat", - "reth-network-peers", - "schnellru", - "secp256k1 0.30.0", - "serde", - "thiserror 2.0.18", - "tokio", - "tokio-stream", - "tracing", -] - -[[package]] -name = "reth-discv5" -version = "1.11.3" -source = "git+https://github.com/paradigmxyz/reth?tag=v1.11.3#d6324d63e27ef6b7c49cdc9b1977c1b808234c7b" -dependencies = [ - "alloy-primitives", - "alloy-rlp", - "derive_more", - "discv5", - "enr", - "futures", - "itertools 0.14.0", - "metrics", - "rand 0.9.4", - "reth-chainspec", - "reth-ethereum-forks", - "reth-metrics", - "reth-network-peers", - "secp256k1 0.30.0", - "thiserror 2.0.18", - "tokio", - "tracing", -] - -[[package]] -name = "reth-dns-discovery" -version = "1.11.3" -source = "git+https://github.com/paradigmxyz/reth?tag=v1.11.3#d6324d63e27ef6b7c49cdc9b1977c1b808234c7b" -dependencies = [ - "alloy-primitives", - "dashmap", - "data-encoding", - "enr", - "hickory-resolver", - "linked_hash_set", - "reth-ethereum-forks", - "reth-network-peers", - "reth-tokio-util", - "schnellru", - "secp256k1 0.30.0", - "serde", - "serde_with", - "thiserror 2.0.18", - "tokio", - "tokio-stream", - "tracing", -] - -[[package]] -name = "reth-downloaders" -version = "1.11.3" -source = "git+https://github.com/paradigmxyz/reth?tag=v1.11.3#d6324d63e27ef6b7c49cdc9b1977c1b808234c7b" -dependencies = [ - "alloy-consensus", - "alloy-eips", - "alloy-primitives", - "alloy-rlp", - "async-compression", - "futures", - "futures-util", - "itertools 0.14.0", - "metrics", - "pin-project", - "rayon", - "reth-config", - "reth-consensus", - "reth-ethereum-primitives", - "reth-metrics", - "reth-network-p2p", - "reth-network-peers", - "reth-primitives-traits", - "reth-provider", - "reth-storage-api", - "reth-tasks", - "reth-testing-utils", - "tempfile", - "thiserror 2.0.18", - "tokio", - "tokio-stream", - "tokio-util", - "tracing", -] - -[[package]] -name = "reth-e2e-test-utils" -version = "1.11.3" -source = "git+https://github.com/paradigmxyz/reth?tag=v1.11.3#d6324d63e27ef6b7c49cdc9b1977c1b808234c7b" -dependencies = [ - "alloy-consensus", - "alloy-eips", - "alloy-network", - "alloy-primitives", - "alloy-provider", - "alloy-rlp", - "alloy-rpc-types-engine", - "alloy-rpc-types-eth", - "alloy-signer", - "alloy-signer-local", - "derive_more", - "eyre", - "futures-util", - "jsonrpsee", - "reth-chainspec", - "reth-cli-commands", - "reth-config", - "reth-consensus", - "reth-db", - "reth-db-common", - "reth-engine-local", - "reth-engine-primitives", - "reth-ethereum-primitives", - "reth-network-api", - "reth-network-p2p", - "reth-network-peers", - "reth-node-api", - "reth-node-builder", - "reth-node-core", - "reth-node-ethereum", - "reth-payload-builder", - "reth-payload-builder-primitives", - "reth-payload-primitives", - "reth-primitives", - "reth-primitives-traits", - "reth-provider", - "reth-rpc-api", - "reth-rpc-builder", - "reth-rpc-eth-api", - "reth-rpc-server-types", - "reth-stages-types", - "reth-tasks", - "reth-tokio-util", - "reth-tracing", - "revm", - "serde_json", - "tempfile", - "tokio", - "tokio-stream", - "tracing", - "url", -] - -[[package]] -name = "reth-ecies" -version = "1.11.3" -source = "git+https://github.com/paradigmxyz/reth?tag=v1.11.3#d6324d63e27ef6b7c49cdc9b1977c1b808234c7b" -dependencies = [ - "aes", - "alloy-primitives", - "alloy-rlp", - "block-padding", - "byteorder", - "cipher", - "concat-kdf", - "ctr", - "digest 0.10.7", - "futures", - "hmac 0.12.1", - "pin-project", - "rand 0.8.5", - "reth-network-peers", - "secp256k1 0.30.0", - "sha2 0.10.9", - "thiserror 2.0.18", - "tokio", - "tokio-stream", - "tokio-util", - "tracing", -] - -[[package]] -name = "reth-engine-local" -version = "1.11.3" -source = "git+https://github.com/paradigmxyz/reth?tag=v1.11.3#d6324d63e27ef6b7c49cdc9b1977c1b808234c7b" -dependencies = [ - "alloy-consensus", - "alloy-primitives", - "alloy-rpc-types-engine", - "eyre", - "futures-util", - "reth-chainspec", - "reth-engine-primitives", - "reth-ethereum-engine-primitives", - "reth-payload-builder", - "reth-payload-primitives", - "reth-primitives-traits", - "reth-storage-api", - "reth-transaction-pool", - "tokio", - "tokio-stream", - "tracing", -] - -[[package]] -name = "reth-engine-primitives" -version = "1.11.3" -source = "git+https://github.com/paradigmxyz/reth?tag=v1.11.3#d6324d63e27ef6b7c49cdc9b1977c1b808234c7b" -dependencies = [ - "alloy-consensus", - "alloy-eips", - "alloy-primitives", - "alloy-rpc-types-engine", - "auto_impl", - "futures", - "reth-chain-state", - "reth-errors", - "reth-ethereum-primitives", - "reth-evm", - "reth-execution-types", - "reth-payload-builder-primitives", - "reth-payload-primitives", - "reth-primitives-traits", - "reth-trie-common", - "serde", - "thiserror 2.0.18", - "tokio", -] - -[[package]] -name = "reth-engine-service" -version = "1.11.3" -source = "git+https://github.com/paradigmxyz/reth?tag=v1.11.3#d6324d63e27ef6b7c49cdc9b1977c1b808234c7b" -dependencies = [ - "futures", - "pin-project", - "reth-chainspec", - "reth-consensus", - "reth-engine-primitives", - "reth-engine-tree", - "reth-evm", - "reth-network-p2p", - "reth-node-types", - "reth-payload-builder", - "reth-provider", - "reth-prune", - "reth-stages-api", - "reth-tasks", - "reth-trie-db", -] - -[[package]] -name = "reth-engine-tree" -version = "1.11.3" -source = "git+https://github.com/paradigmxyz/reth?tag=v1.11.3#d6324d63e27ef6b7c49cdc9b1977c1b808234c7b" -dependencies = [ - "alloy-consensus", - "alloy-eip7928", - "alloy-eips", - "alloy-evm", - "alloy-primitives", - "alloy-rlp", - "alloy-rpc-types-engine", - "crossbeam-channel", - "derive_more", - "fixed-cache", - "futures", - "metrics", - "moka", - "parking_lot", - "rayon", - "reth-chain-state", - "reth-chainspec", - "reth-consensus", - "reth-db", - "reth-engine-primitives", - "reth-errors", - "reth-ethereum-primitives", - "reth-evm", - "reth-execution-types", - "reth-metrics", - "reth-network-p2p", - "reth-payload-builder", - "reth-payload-primitives", - "reth-primitives-traits", - "reth-provider", - "reth-prune", - "reth-prune-types", - "reth-revm", - "reth-stages", - "reth-stages-api", - "reth-static-file", - "reth-tasks", - "reth-tracing", - "reth-trie", - "reth-trie-common", - "reth-trie-db", - "reth-trie-parallel", - "reth-trie-sparse", - "revm", - "revm-primitives", - "schnellru", - "smallvec", - "thiserror 2.0.18", - "tokio", - "tracing", -] - -[[package]] -name = "reth-engine-util" -version = "1.11.3" -source = "git+https://github.com/paradigmxyz/reth?tag=v1.11.3#d6324d63e27ef6b7c49cdc9b1977c1b808234c7b" -dependencies = [ - "alloy-consensus", - "alloy-rpc-types-engine", - "eyre", - "futures", - "itertools 0.14.0", - "pin-project", - "reth-chainspec", - "reth-engine-primitives", - "reth-engine-tree", - "reth-errors", - "reth-evm", - "reth-fs-util", - "reth-payload-primitives", - "reth-primitives-traits", - "reth-revm", - "reth-storage-api", - "serde", - "serde_json", - "tokio", - "tokio-util", - "tracing", -] - -[[package]] -name = "reth-era" -version = "1.11.3" -source = "git+https://github.com/paradigmxyz/reth?tag=v1.11.3#d6324d63e27ef6b7c49cdc9b1977c1b808234c7b" -dependencies = [ - "alloy-consensus", - "alloy-eips", - "alloy-primitives", - "alloy-rlp", - "ethereum_ssz 0.10.1", - "ethereum_ssz_derive 0.10.1", - "snap", - "thiserror 2.0.18", -] - -[[package]] -name = "reth-era-downloader" -version = "1.11.3" -source = "git+https://github.com/paradigmxyz/reth?tag=v1.11.3#d6324d63e27ef6b7c49cdc9b1977c1b808234c7b" -dependencies = [ - "alloy-primitives", - "bytes", - "eyre", - "futures-util", - "reqwest", - "reth-era", - "reth-fs-util", - "sha2 0.10.9", - "tokio", -] - -[[package]] -name = "reth-era-utils" -version = "1.11.3" -source = "git+https://github.com/paradigmxyz/reth?tag=v1.11.3#d6324d63e27ef6b7c49cdc9b1977c1b808234c7b" -dependencies = [ - "alloy-consensus", - "alloy-primitives", - "eyre", - "futures-util", - "reth-db-api", - "reth-era", - "reth-era-downloader", - "reth-etl", - "reth-fs-util", - "reth-primitives-traits", - "reth-provider", - "reth-stages-types", - "reth-storage-api", - "tokio", - "tracing", -] - -[[package]] -name = "reth-errors" -version = "1.11.3" -source = "git+https://github.com/paradigmxyz/reth?tag=v1.11.3#d6324d63e27ef6b7c49cdc9b1977c1b808234c7b" -dependencies = [ - "reth-consensus", - "reth-execution-errors", - "reth-storage-errors", - "thiserror 2.0.18", -] - -[[package]] -name = "reth-eth-wire" -version = "1.11.3" -source = "git+https://github.com/paradigmxyz/reth?tag=v1.11.3#d6324d63e27ef6b7c49cdc9b1977c1b808234c7b" -dependencies = [ - "alloy-chains", - "alloy-primitives", - "alloy-rlp", - "arbitrary", - "bytes", - "derive_more", - "futures", - "pin-project", - "reth-codecs", - "reth-ecies", - "reth-eth-wire-types", - "reth-ethereum-forks", - "reth-metrics", - "reth-network-peers", - "reth-primitives-traits", - "serde", - "snap", - "thiserror 2.0.18", - "tokio", - "tokio-stream", - "tokio-util", - "tracing", -] - -[[package]] -name = "reth-eth-wire-types" -version = "1.11.3" -source = "git+https://github.com/paradigmxyz/reth?tag=v1.11.3#d6324d63e27ef6b7c49cdc9b1977c1b808234c7b" -dependencies = [ - "alloy-chains", - "alloy-consensus", - "alloy-eips", - "alloy-hardforks", - "alloy-primitives", - "alloy-rlp", - "arbitrary", - "bytes", - "derive_more", - "proptest", - "proptest-arbitrary-interop", - "reth-chainspec", - "reth-codecs-derive", - "reth-ethereum-primitives", - "reth-primitives-traits", - "serde", - "thiserror 2.0.18", -] - -[[package]] -name = "reth-ethereum" -version = "1.11.3" -source = "git+https://github.com/paradigmxyz/reth?tag=v1.11.3#d6324d63e27ef6b7c49cdc9b1977c1b808234c7b" -dependencies = [ - "alloy-rpc-types-engine", - "alloy-rpc-types-eth", - "reth-chainspec", - "reth-cli-util", - "reth-codecs", - "reth-consensus", - "reth-consensus-common", - "reth-db", - "reth-engine-local", - "reth-eth-wire", - "reth-ethereum-cli", - "reth-ethereum-consensus", - "reth-ethereum-primitives", - "reth-evm", - "reth-evm-ethereum", - "reth-network", - "reth-network-api", - "reth-node-api", - "reth-node-builder", - "reth-node-core", - "reth-node-ethereum", - "reth-primitives-traits", - "reth-provider", - "reth-revm", - "reth-rpc", - "reth-rpc-api", - "reth-rpc-builder", - "reth-rpc-eth-types", - "reth-storage-api", - "reth-tasks", - "reth-transaction-pool", - "reth-trie", - "reth-trie-db", -] - -[[package]] -name = "reth-ethereum-cli" -version = "1.11.3" -source = "git+https://github.com/paradigmxyz/reth?tag=v1.11.3#d6324d63e27ef6b7c49cdc9b1977c1b808234c7b" -dependencies = [ - "clap", - "eyre", - "reth-chainspec", - "reth-cli", - "reth-cli-commands", - "reth-cli-runner", - "reth-db", - "reth-node-api", - "reth-node-builder", - "reth-node-core", - "reth-node-ethereum", - "reth-node-metrics", - "reth-rpc-server-types", - "reth-tasks", - "reth-tracing", - "tracing", -] - -[[package]] -name = "reth-ethereum-consensus" -version = "1.11.3" -source = "git+https://github.com/paradigmxyz/reth?tag=v1.11.3#d6324d63e27ef6b7c49cdc9b1977c1b808234c7b" -dependencies = [ - "alloy-consensus", - "alloy-eips", - "alloy-primitives", - "reth-chainspec", - "reth-consensus", - "reth-consensus-common", - "reth-execution-types", - "reth-primitives-traits", - "tracing", -] - -[[package]] -name = "reth-ethereum-engine-primitives" -version = "1.11.3" -source = "git+https://github.com/paradigmxyz/reth?tag=v1.11.3#d6324d63e27ef6b7c49cdc9b1977c1b808234c7b" -dependencies = [ - "alloy-eips", - "alloy-primitives", - "alloy-rlp", - "alloy-rpc-types-engine", - "reth-engine-primitives", - "reth-ethereum-primitives", - "reth-payload-primitives", - "reth-primitives-traits", - "serde", - "sha2 0.10.9", - "thiserror 2.0.18", -] - -[[package]] -name = "reth-ethereum-forks" -version = "1.11.3" -source = "git+https://github.com/paradigmxyz/reth?tag=v1.11.3#d6324d63e27ef6b7c49cdc9b1977c1b808234c7b" -dependencies = [ - "alloy-eip2124", - "alloy-hardforks", - "alloy-primitives", - "arbitrary", - "auto_impl", - "once_cell", - "rustc-hash", -] - -[[package]] -name = "reth-ethereum-payload-builder" -version = "1.11.3" -source = "git+https://github.com/paradigmxyz/reth?tag=v1.11.3#d6324d63e27ef6b7c49cdc9b1977c1b808234c7b" -dependencies = [ - "alloy-consensus", - "alloy-eips", - "alloy-primitives", - "alloy-rlp", - "alloy-rpc-types-engine", - "reth-basic-payload-builder", - "reth-chainspec", - "reth-consensus-common", - "reth-errors", - "reth-ethereum-primitives", - "reth-evm", - "reth-evm-ethereum", - "reth-payload-builder", - "reth-payload-builder-primitives", - "reth-payload-primitives", - "reth-payload-validator", - "reth-primitives-traits", - "reth-revm", - "reth-storage-api", - "reth-transaction-pool", - "revm", - "tracing", -] - -[[package]] -name = "reth-ethereum-primitives" -version = "1.11.3" -source = "git+https://github.com/paradigmxyz/reth?tag=v1.11.3#d6324d63e27ef6b7c49cdc9b1977c1b808234c7b" -dependencies = [ - "alloy-consensus", - "alloy-eips", - "alloy-primitives", - "alloy-rlp", - "alloy-rpc-types-eth", - "alloy-serde", - "arbitrary", - "modular-bitfield", - "reth-codecs", - "reth-primitives-traits", - "reth-zstd-compressors", - "serde", - "serde_with", -] - -[[package]] -name = "reth-etl" -version = "1.11.3" -source = "git+https://github.com/paradigmxyz/reth?tag=v1.11.3#d6324d63e27ef6b7c49cdc9b1977c1b808234c7b" -dependencies = [ - "rayon", - "reth-db-api", - "tempfile", -] - -[[package]] -name = "reth-evm" -version = "1.11.3" -source = "git+https://github.com/paradigmxyz/reth?tag=v1.11.3#d6324d63e27ef6b7c49cdc9b1977c1b808234c7b" -dependencies = [ - "alloy-consensus", - "alloy-eips", - "alloy-evm", - "alloy-primitives", - "auto_impl", - "derive_more", - "futures-util", - "metrics", - "rayon", - "reth-execution-errors", - "reth-execution-types", - "reth-metrics", - "reth-primitives-traits", - "reth-storage-api", - "reth-storage-errors", - "reth-trie-common", - "revm", -] - -[[package]] -name = "reth-evm-ethereum" -version = "1.11.3" -source = "git+https://github.com/paradigmxyz/reth?tag=v1.11.3#d6324d63e27ef6b7c49cdc9b1977c1b808234c7b" -dependencies = [ - "alloy-consensus", - "alloy-eips", - "alloy-evm", - "alloy-primitives", - "alloy-rpc-types-engine", - "derive_more", - "parking_lot", - "reth-chainspec", - "reth-ethereum-forks", - "reth-ethereum-primitives", - "reth-evm", - "reth-execution-types", - "reth-primitives-traits", - "reth-storage-errors", - "revm", -] - -[[package]] -name = "reth-execution-errors" -version = "1.11.3" -source = "git+https://github.com/paradigmxyz/reth?tag=v1.11.3#d6324d63e27ef6b7c49cdc9b1977c1b808234c7b" -dependencies = [ - "alloy-evm", - "alloy-primitives", - "alloy-rlp", - "nybbles", - "reth-storage-errors", - "thiserror 2.0.18", -] - -[[package]] -name = "reth-execution-types" -version = "1.11.3" -source = "git+https://github.com/paradigmxyz/reth?tag=v1.11.3#d6324d63e27ef6b7c49cdc9b1977c1b808234c7b" -dependencies = [ - "alloy-consensus", - "alloy-eips", - "alloy-evm", - "alloy-primitives", - "derive_more", - "reth-ethereum-primitives", - "reth-primitives-traits", - "reth-trie-common", - "revm", - "serde", - "serde_with", -] - -[[package]] -name = "reth-exex" -version = "1.11.3" -source = "git+https://github.com/paradigmxyz/reth?tag=v1.11.3#d6324d63e27ef6b7c49cdc9b1977c1b808234c7b" -dependencies = [ - "alloy-consensus", - "alloy-eips", - "alloy-primitives", - "eyre", - "futures", - "itertools 0.14.0", - "metrics", - "parking_lot", - "reth-chain-state", - "reth-chainspec", - "reth-config", - "reth-ethereum-primitives", - "reth-evm", - "reth-exex-types", - "reth-fs-util", - "reth-metrics", - "reth-node-api", - "reth-node-core", - "reth-payload-builder", - "reth-primitives-traits", - "reth-provider", - "reth-prune-types", - "reth-revm", - "reth-stages-api", - "reth-tasks", - "reth-tracing", - "rmp-serde", - "thiserror 2.0.18", - "tokio", - "tokio-util", - "tracing", -] - -[[package]] -name = "reth-exex-types" -version = "1.11.3" -source = "git+https://github.com/paradigmxyz/reth?tag=v1.11.3#d6324d63e27ef6b7c49cdc9b1977c1b808234c7b" -dependencies = [ - "alloy-eips", - "alloy-primitives", - "reth-chain-state", - "reth-execution-types", - "reth-primitives-traits", - "serde", - "serde_with", -] - -[[package]] -name = "reth-fs-util" -version = "1.11.3" -source = "git+https://github.com/paradigmxyz/reth?tag=v1.11.3#d6324d63e27ef6b7c49cdc9b1977c1b808234c7b" -dependencies = [ - "serde", - "serde_json", - "thiserror 2.0.18", -] - -[[package]] -name = "reth-invalid-block-hooks" -version = "1.11.3" -source = "git+https://github.com/paradigmxyz/reth?tag=v1.11.3#d6324d63e27ef6b7c49cdc9b1977c1b808234c7b" -dependencies = [ - "alloy-consensus", - "alloy-primitives", - "alloy-rlp", - "alloy-rpc-types-debug", - "eyre", - "futures", - "jsonrpsee", - "pretty_assertions", - "reth-engine-primitives", - "reth-evm", - "reth-primitives-traits", - "reth-provider", - "reth-revm", - "reth-rpc-api", - "reth-tracing", - "reth-trie", - "revm", - "revm-bytecode", - "revm-database", - "serde", - "serde_json", -] - -[[package]] -name = "reth-ipc" -version = "1.11.3" -source = "git+https://github.com/paradigmxyz/reth?tag=v1.11.3#d6324d63e27ef6b7c49cdc9b1977c1b808234c7b" -dependencies = [ - "bytes", - "futures", - "futures-util", - "interprocess", - "jsonrpsee", - "pin-project", - "serde_json", - "thiserror 2.0.18", - "tokio", - "tokio-stream", - "tokio-util", - "tower 0.5.3", - "tracing", -] - -[[package]] -name = "reth-libmdbx" -version = "1.11.3" -source = "git+https://github.com/paradigmxyz/reth?tag=v1.11.3#d6324d63e27ef6b7c49cdc9b1977c1b808234c7b" -dependencies = [ - "bitflags 2.11.0", - "byteorder", - "dashmap", - "derive_more", - "parking_lot", - "reth-mdbx-sys", - "smallvec", - "thiserror 2.0.18", - "tracing", -] - -[[package]] -name = "reth-mdbx-sys" -version = "1.11.3" -source = "git+https://github.com/paradigmxyz/reth?tag=v1.11.3#d6324d63e27ef6b7c49cdc9b1977c1b808234c7b" -dependencies = [ - "bindgen", - "cc", -] - -[[package]] -name = "reth-metrics" -version = "1.11.3" -source = "git+https://github.com/paradigmxyz/reth?tag=v1.11.3#d6324d63e27ef6b7c49cdc9b1977c1b808234c7b" -dependencies = [ - "futures", - "metrics", - "metrics-derive", - "tokio", - "tokio-util", -] - -[[package]] -name = "reth-net-banlist" -version = "1.11.3" -source = "git+https://github.com/paradigmxyz/reth?tag=v1.11.3#d6324d63e27ef6b7c49cdc9b1977c1b808234c7b" -dependencies = [ - "alloy-primitives", - "ipnet", -] - -[[package]] -name = "reth-net-nat" -version = "1.11.3" -source = "git+https://github.com/paradigmxyz/reth?tag=v1.11.3#d6324d63e27ef6b7c49cdc9b1977c1b808234c7b" -dependencies = [ - "futures-util", - "if-addrs 0.14.0", - "reqwest", - "serde_with", - "thiserror 2.0.18", - "tokio", - "tracing", -] - -[[package]] -name = "reth-network" -version = "1.11.3" -source = "git+https://github.com/paradigmxyz/reth?tag=v1.11.3#d6324d63e27ef6b7c49cdc9b1977c1b808234c7b" -dependencies = [ - "alloy-consensus", - "alloy-eips", - "alloy-primitives", - "alloy-rlp", - "aquamarine", - "auto_impl", - "derive_more", - "discv5", - "enr", - "futures", - "itertools 0.14.0", - "metrics", - "parking_lot", - "pin-project", - "rand 0.8.5", - "rand 0.9.4", - "rayon", - "reth-chainspec", - "reth-consensus", - "reth-discv4", - "reth-discv5", - "reth-dns-discovery", - "reth-ecies", - "reth-eth-wire", - "reth-eth-wire-types", - "reth-ethereum-forks", - "reth-ethereum-primitives", - "reth-evm-ethereum", - "reth-fs-util", - "reth-metrics", - "reth-net-banlist", - "reth-network-api", - "reth-network-p2p", - "reth-network-peers", - "reth-network-types", - "reth-primitives-traits", - "reth-storage-api", - "reth-tasks", - "reth-tokio-util", - "reth-transaction-pool", - "rustc-hash", - "schnellru", - "secp256k1 0.30.0", - "serde", - "smallvec", - "thiserror 2.0.18", - "tokio", - "tokio-stream", - "tokio-util", - "tracing", -] - -[[package]] -name = "reth-network-api" -version = "1.11.3" -source = "git+https://github.com/paradigmxyz/reth?tag=v1.11.3#d6324d63e27ef6b7c49cdc9b1977c1b808234c7b" -dependencies = [ - "alloy-consensus", - "alloy-primitives", - "alloy-rpc-types-admin", - "alloy-rpc-types-eth", - "auto_impl", - "derive_more", - "enr", - "futures", - "reth-eth-wire-types", - "reth-ethereum-forks", - "reth-network-p2p", - "reth-network-peers", - "reth-network-types", - "reth-tokio-util", - "serde", - "thiserror 2.0.18", - "tokio", - "tokio-stream", -] - -[[package]] -name = "reth-network-p2p" -version = "1.11.3" -source = "git+https://github.com/paradigmxyz/reth?tag=v1.11.3#d6324d63e27ef6b7c49cdc9b1977c1b808234c7b" -dependencies = [ - "alloy-consensus", - "alloy-eips", - "alloy-primitives", - "auto_impl", - "derive_more", - "futures", - "parking_lot", - "reth-consensus", - "reth-eth-wire-types", - "reth-ethereum-primitives", - "reth-network-peers", - "reth-network-types", - "reth-primitives-traits", - "reth-storage-errors", - "tokio", - "tracing", -] - -[[package]] -name = "reth-network-peers" -version = "1.11.3" -source = "git+https://github.com/paradigmxyz/reth?tag=v1.11.3#d6324d63e27ef6b7c49cdc9b1977c1b808234c7b" -dependencies = [ - "alloy-primitives", - "alloy-rlp", - "enr", - "secp256k1 0.30.0", - "serde_with", - "thiserror 2.0.18", - "tokio", - "url", -] - -[[package]] -name = "reth-network-types" -version = "1.11.3" -source = "git+https://github.com/paradigmxyz/reth?tag=v1.11.3#d6324d63e27ef6b7c49cdc9b1977c1b808234c7b" -dependencies = [ - "alloy-eip2124", - "humantime-serde", - "reth-net-banlist", - "reth-network-peers", - "serde", - "serde_json", - "tracing", -] - -[[package]] -name = "reth-nippy-jar" -version = "1.11.3" -source = "git+https://github.com/paradigmxyz/reth?tag=v1.11.3#d6324d63e27ef6b7c49cdc9b1977c1b808234c7b" -dependencies = [ - "anyhow", - "bincode", - "derive_more", - "lz4_flex", - "memmap2", - "reth-fs-util", - "serde", - "thiserror 2.0.18", - "tracing", - "zstd", -] - -[[package]] -name = "reth-node-api" -version = "1.11.3" -source = "git+https://github.com/paradigmxyz/reth?tag=v1.11.3#d6324d63e27ef6b7c49cdc9b1977c1b808234c7b" -dependencies = [ - "alloy-rpc-types-engine", - "eyre", - "reth-basic-payload-builder", - "reth-consensus", - "reth-db-api", - "reth-engine-primitives", - "reth-evm", - "reth-network-api", - "reth-node-core", - "reth-node-types", - "reth-payload-builder", - "reth-payload-builder-primitives", - "reth-payload-primitives", - "reth-provider", - "reth-tasks", - "reth-tokio-util", - "reth-transaction-pool", -] - -[[package]] -name = "reth-node-builder" -version = "1.11.3" -source = "git+https://github.com/paradigmxyz/reth?tag=v1.11.3#d6324d63e27ef6b7c49cdc9b1977c1b808234c7b" -dependencies = [ - "alloy-consensus", - "alloy-eips", - "alloy-primitives", - "alloy-provider", - "alloy-rpc-types", - "alloy-rpc-types-engine", - "aquamarine", - "eyre", - "fdlimit", - "futures", - "jsonrpsee", - "parking_lot", - "rayon", - "reth-basic-payload-builder", - "reth-chain-state", - "reth-chainspec", - "reth-config", - "reth-consensus", - "reth-consensus-debug-client", - "reth-db", - "reth-db-api", - "reth-db-common", - "reth-downloaders", - "reth-engine-local", - "reth-engine-primitives", - "reth-engine-service", - "reth-engine-tree", - "reth-engine-util", - "reth-evm", - "reth-exex", - "reth-fs-util", - "reth-invalid-block-hooks", - "reth-network", - "reth-network-api", - "reth-network-p2p", - "reth-node-api", - "reth-node-core", - "reth-node-ethstats", - "reth-node-events", - "reth-node-metrics", - "reth-payload-builder", - "reth-primitives-traits", - "reth-provider", - "reth-prune", - "reth-rpc", - "reth-rpc-api", - "reth-rpc-builder", - "reth-rpc-engine-api", - "reth-rpc-eth-types", - "reth-rpc-layer", - "reth-stages", - "reth-static-file", - "reth-tasks", - "reth-tokio-util", - "reth-tracing", - "reth-transaction-pool", - "reth-trie-db", - "secp256k1 0.30.0", - "serde_json", - "tokio", - "tokio-stream", - "tracing", -] - -[[package]] -name = "reth-node-core" -version = "1.11.3" -source = "git+https://github.com/paradigmxyz/reth?tag=v1.11.3#d6324d63e27ef6b7c49cdc9b1977c1b808234c7b" -dependencies = [ - "alloy-consensus", - "alloy-eips", - "alloy-primitives", - "alloy-rpc-types-engine", - "clap", - "derive_more", - "dirs-next", - "eyre", - "futures", - "humantime", - "ipnet", - "rand 0.9.4", - "reth-chainspec", - "reth-cli-util", - "reth-config", - "reth-consensus", - "reth-db", - "reth-discv4", - "reth-discv5", - "reth-engine-local", - "reth-engine-primitives", - "reth-ethereum-forks", - "reth-net-banlist", - "reth-net-nat", - "reth-network", - "reth-network-p2p", - "reth-network-peers", - "reth-primitives-traits", - "reth-prune-types", - "reth-rpc-convert", - "reth-rpc-eth-types", - "reth-rpc-server-types", - "reth-stages-types", - "reth-storage-api", - "reth-storage-errors", - "reth-tracing", - "reth-tracing-otlp", - "reth-transaction-pool", - "secp256k1 0.30.0", - "serde", - "shellexpand", - "strum 0.27.2", - "thiserror 2.0.18", - "toml 0.9.12+spec-1.1.0", - "tracing", - "url", - "vergen", - "vergen-git2", -] - -[[package]] -name = "reth-node-ethereum" -version = "1.11.3" -source = "git+https://github.com/paradigmxyz/reth?tag=v1.11.3#d6324d63e27ef6b7c49cdc9b1977c1b808234c7b" -dependencies = [ - "alloy-eips", - "alloy-network", - "alloy-rpc-types-engine", - "alloy-rpc-types-eth", - "eyre", - "reth-chainspec", - "reth-engine-local", - "reth-engine-primitives", - "reth-ethereum-consensus", - "reth-ethereum-engine-primitives", - "reth-ethereum-payload-builder", - "reth-ethereum-primitives", - "reth-evm", - "reth-evm-ethereum", - "reth-network", - "reth-node-api", - "reth-node-builder", - "reth-payload-primitives", - "reth-primitives-traits", - "reth-provider", - "reth-revm", - "reth-rpc", - "reth-rpc-api", - "reth-rpc-builder", - "reth-rpc-eth-api", - "reth-rpc-eth-types", - "reth-rpc-server-types", - "reth-tracing", - "reth-transaction-pool", - "revm", - "tokio", -] - -[[package]] -name = "reth-node-ethstats" -version = "1.11.3" -source = "git+https://github.com/paradigmxyz/reth?tag=v1.11.3#d6324d63e27ef6b7c49cdc9b1977c1b808234c7b" -dependencies = [ - "alloy-consensus", - "alloy-primitives", - "chrono", - "futures-util", - "reth-chain-state", - "reth-network-api", - "reth-primitives-traits", - "reth-storage-api", - "reth-transaction-pool", - "serde", - "serde_json", - "thiserror 2.0.18", - "tokio", - "tokio-stream", - "tokio-tungstenite 0.28.0", - "tracing", - "url", -] - -[[package]] -name = "reth-node-events" -version = "1.11.3" -source = "git+https://github.com/paradigmxyz/reth?tag=v1.11.3#d6324d63e27ef6b7c49cdc9b1977c1b808234c7b" -dependencies = [ - "alloy-consensus", - "alloy-eips", - "alloy-primitives", - "alloy-rpc-types-engine", - "derive_more", - "futures", - "humantime", - "pin-project", - "reth-engine-primitives", - "reth-network-api", - "reth-primitives-traits", - "reth-prune-types", - "reth-stages", - "reth-static-file-types", - "reth-storage-api", - "tokio", - "tracing", -] - -[[package]] -name = "reth-node-metrics" -version = "1.11.3" -source = "git+https://github.com/paradigmxyz/reth?tag=v1.11.3#d6324d63e27ef6b7c49cdc9b1977c1b808234c7b" -dependencies = [ - "bytes", - "eyre", - "http", - "http-body-util", - "jsonrpsee-server", - "metrics", - "metrics-exporter-prometheus", - "metrics-process", - "metrics-util", - "procfs 0.18.0", - "reqwest", - "reth-metrics", - "reth-tasks", - "tikv-jemalloc-ctl", - "tokio", - "tower 0.5.3", - "tracing", -] - -[[package]] -name = "reth-node-types" -version = "1.11.3" -source = "git+https://github.com/paradigmxyz/reth?tag=v1.11.3#d6324d63e27ef6b7c49cdc9b1977c1b808234c7b" -dependencies = [ - "reth-chainspec", - "reth-db-api", - "reth-engine-primitives", - "reth-payload-primitives", - "reth-primitives-traits", -] - -[[package]] -name = "reth-payload-builder" -version = "1.11.3" -source = "git+https://github.com/paradigmxyz/reth?tag=v1.11.3#d6324d63e27ef6b7c49cdc9b1977c1b808234c7b" -dependencies = [ - "alloy-consensus", - "alloy-primitives", - "alloy-rpc-types", - "futures-util", - "metrics", - "reth-chain-state", - "reth-ethereum-engine-primitives", - "reth-metrics", - "reth-payload-builder-primitives", - "reth-payload-primitives", - "reth-primitives-traits", - "tokio", - "tokio-stream", - "tracing", -] - -[[package]] -name = "reth-payload-builder-primitives" -version = "1.11.3" -source = "git+https://github.com/paradigmxyz/reth?tag=v1.11.3#d6324d63e27ef6b7c49cdc9b1977c1b808234c7b" -dependencies = [ - "pin-project", - "reth-payload-primitives", - "tokio", - "tokio-stream", - "tracing", -] - -[[package]] -name = "reth-payload-primitives" -version = "1.11.3" -source = "git+https://github.com/paradigmxyz/reth?tag=v1.11.3#d6324d63e27ef6b7c49cdc9b1977c1b808234c7b" -dependencies = [ - "alloy-consensus", - "alloy-eips", - "alloy-primitives", - "alloy-rpc-types-engine", - "auto_impl", - "either", - "op-alloy-rpc-types-engine", - "reth-chain-state", - "reth-chainspec", - "reth-errors", - "reth-execution-types", - "reth-primitives-traits", - "reth-trie-common", - "serde", - "thiserror 2.0.18", - "tokio", -] - -[[package]] -name = "reth-payload-validator" -version = "1.11.3" -source = "git+https://github.com/paradigmxyz/reth?tag=v1.11.3#d6324d63e27ef6b7c49cdc9b1977c1b808234c7b" -dependencies = [ - "alloy-consensus", - "alloy-rpc-types-engine", - "reth-primitives-traits", -] - -[[package]] -name = "reth-primitives" -version = "1.11.3" -source = "git+https://github.com/paradigmxyz/reth?tag=v1.11.3#d6324d63e27ef6b7c49cdc9b1977c1b808234c7b" -dependencies = [ - "alloy-consensus", - "once_cell", - "reth-ethereum-forks", - "reth-ethereum-primitives", - "reth-primitives-traits", - "reth-static-file-types", -] - -[[package]] -name = "reth-primitives-traits" -version = "1.11.3" -source = "git+https://github.com/paradigmxyz/reth?tag=v1.11.3#d6324d63e27ef6b7c49cdc9b1977c1b808234c7b" -dependencies = [ - "alloy-consensus", - "alloy-eips", - "alloy-genesis", - "alloy-primitives", - "alloy-rlp", - "alloy-rpc-types-eth", - "alloy-trie", - "arbitrary", - "auto_impl", - "byteorder", - "bytes", - "dashmap", - "derive_more", - "modular-bitfield", - "once_cell", - "op-alloy-consensus", - "proptest", - "proptest-arbitrary-interop", - "rayon", - "reth-codecs", - "revm-bytecode", - "revm-primitives", - "revm-state", - "secp256k1 0.30.0", - "serde", - "serde_with", - "thiserror 2.0.18", -] - -[[package]] -name = "reth-provider" -version = "1.11.3" -source = "git+https://github.com/paradigmxyz/reth?tag=v1.11.3#d6324d63e27ef6b7c49cdc9b1977c1b808234c7b" -dependencies = [ - "alloy-consensus", - "alloy-eips", - "alloy-primitives", - "alloy-rpc-types-engine", - "eyre", - "itertools 0.14.0", - "metrics", - "notify", - "parking_lot", - "rayon", - "reth-chain-state", - "reth-chainspec", - "reth-codecs", - "reth-db", - "reth-db-api", - "reth-errors", - "reth-ethereum-engine-primitives", - "reth-ethereum-primitives", - "reth-execution-types", - "reth-fs-util", - "reth-metrics", - "reth-nippy-jar", - "reth-node-types", - "reth-primitives-traits", - "reth-prune-types", - "reth-stages-types", - "reth-static-file-types", - "reth-storage-api", - "reth-storage-errors", - "reth-tasks", - "reth-trie", - "reth-trie-db", - "revm-database", - "revm-state", - "strum 0.27.2", - "tokio", - "tracing", -] - -[[package]] -name = "reth-prune" -version = "1.11.3" -source = "git+https://github.com/paradigmxyz/reth?tag=v1.11.3#d6324d63e27ef6b7c49cdc9b1977c1b808234c7b" -dependencies = [ - "alloy-consensus", - "alloy-eips", - "alloy-primitives", - "itertools 0.14.0", - "metrics", - "rayon", - "reth-config", - "reth-db-api", - "reth-errors", - "reth-exex-types", - "reth-metrics", - "reth-primitives-traits", - "reth-provider", - "reth-prune-types", - "reth-stages-types", - "reth-static-file-types", - "reth-storage-api", - "reth-tokio-util", - "rustc-hash", - "thiserror 2.0.18", - "tokio", - "tracing", -] - -[[package]] -name = "reth-prune-types" -version = "1.11.3" -source = "git+https://github.com/paradigmxyz/reth?tag=v1.11.3#d6324d63e27ef6b7c49cdc9b1977c1b808234c7b" -dependencies = [ - "alloy-primitives", - "arbitrary", - "derive_more", - "modular-bitfield", - "reth-codecs", - "serde", - "strum 0.27.2", - "thiserror 2.0.18", - "tracing", -] - -[[package]] -name = "reth-revm" -version = "1.11.3" -source = "git+https://github.com/paradigmxyz/reth?tag=v1.11.3#d6324d63e27ef6b7c49cdc9b1977c1b808234c7b" -dependencies = [ - "alloy-primitives", - "reth-primitives-traits", - "reth-storage-api", - "reth-storage-errors", - "reth-trie", - "revm", -] - -[[package]] -name = "reth-rpc" -version = "1.11.3" -source = "git+https://github.com/paradigmxyz/reth?tag=v1.11.3#d6324d63e27ef6b7c49cdc9b1977c1b808234c7b" -dependencies = [ - "alloy-consensus", - "alloy-dyn-abi", - "alloy-eip7928", - "alloy-eips", - "alloy-evm", - "alloy-genesis", - "alloy-network", - "alloy-primitives", - "alloy-rlp", - "alloy-rpc-client", - "alloy-rpc-types", - "alloy-rpc-types-admin", - "alloy-rpc-types-beacon", - "alloy-rpc-types-debug", - "alloy-rpc-types-engine", - "alloy-rpc-types-eth", - "alloy-rpc-types-mev", - "alloy-rpc-types-trace", - "alloy-rpc-types-txpool", - "alloy-serde", - "alloy-signer", - "alloy-signer-local", - "async-trait", - "derive_more", - "dyn-clone", - "futures", - "itertools 0.14.0", - "jsonrpsee", - "jsonrpsee-types", - "parking_lot", - "pin-project", - "reth-chain-state", - "reth-chainspec", - "reth-consensus", - "reth-consensus-common", - "reth-engine-primitives", - "reth-errors", - "reth-ethereum-engine-primitives", - "reth-ethereum-primitives", - "reth-evm", - "reth-evm-ethereum", - "reth-execution-types", - "reth-metrics", - "reth-network-api", - "reth-network-peers", - "reth-network-types", - "reth-node-api", - "reth-primitives-traits", - "reth-revm", - "reth-rpc-api", - "reth-rpc-convert", - "reth-rpc-engine-api", - "reth-rpc-eth-api", - "reth-rpc-eth-types", - "reth-rpc-server-types", - "reth-storage-api", - "reth-tasks", - "reth-transaction-pool", - "reth-trie-common", - "revm", - "revm-inspectors", - "revm-primitives", - "serde", - "serde_json", - "sha2 0.10.9", - "thiserror 2.0.18", - "tokio", - "tokio-stream", - "tracing", - "tracing-futures", -] - -[[package]] -name = "reth-rpc-api" -version = "1.11.3" -source = "git+https://github.com/paradigmxyz/reth?tag=v1.11.3#d6324d63e27ef6b7c49cdc9b1977c1b808234c7b" -dependencies = [ - "alloy-eip7928", - "alloy-eips", - "alloy-genesis", - "alloy-json-rpc", - "alloy-primitives", - "alloy-rpc-types", - "alloy-rpc-types-admin", - "alloy-rpc-types-anvil", - "alloy-rpc-types-beacon", - "alloy-rpc-types-debug", - "alloy-rpc-types-engine", - "alloy-rpc-types-eth", - "alloy-rpc-types-mev", - "alloy-rpc-types-trace", - "alloy-rpc-types-txpool", - "alloy-serde", - "jsonrpsee", - "reth-chain-state", - "reth-engine-primitives", - "reth-network-peers", - "reth-rpc-eth-api", - "reth-trie-common", - "serde_json", -] - -[[package]] -name = "reth-rpc-builder" -version = "1.11.3" -source = "git+https://github.com/paradigmxyz/reth?tag=v1.11.3#d6324d63e27ef6b7c49cdc9b1977c1b808234c7b" -dependencies = [ - "alloy-network", - "alloy-provider", - "dyn-clone", - "http", - "jsonrpsee", - "metrics", - "pin-project", - "reth-chain-state", - "reth-chainspec", - "reth-consensus", - "reth-engine-primitives", - "reth-evm", - "reth-ipc", - "reth-metrics", - "reth-network-api", - "reth-node-core", - "reth-primitives-traits", - "reth-rpc", - "reth-rpc-api", - "reth-rpc-eth-api", - "reth-rpc-eth-types", - "reth-rpc-layer", - "reth-rpc-server-types", - "reth-storage-api", - "reth-tasks", - "reth-tokio-util", - "reth-transaction-pool", - "serde", - "thiserror 2.0.18", - "tokio", - "tokio-util", - "tower 0.5.3", - "tower-http", - "tracing", -] - -[[package]] -name = "reth-rpc-convert" -version = "1.11.3" -source = "git+https://github.com/paradigmxyz/reth?tag=v1.11.3#d6324d63e27ef6b7c49cdc9b1977c1b808234c7b" -dependencies = [ - "alloy-consensus", - "alloy-evm", - "alloy-json-rpc", - "alloy-network", - "alloy-primitives", - "alloy-rpc-types-eth", - "alloy-signer", - "auto_impl", - "dyn-clone", - "jsonrpsee-types", - "reth-ethereum-primitives", - "reth-evm", - "reth-primitives-traits", - "thiserror 2.0.18", -] - -[[package]] -name = "reth-rpc-engine-api" -version = "1.11.3" -source = "git+https://github.com/paradigmxyz/reth?tag=v1.11.3#d6324d63e27ef6b7c49cdc9b1977c1b808234c7b" -dependencies = [ - "alloy-eips", - "alloy-primitives", - "alloy-rpc-types-engine", - "async-trait", - "jsonrpsee-core", - "jsonrpsee-types", - "metrics", - "reth-chainspec", - "reth-engine-primitives", - "reth-metrics", - "reth-network-api", - "reth-payload-builder", - "reth-payload-builder-primitives", - "reth-payload-primitives", - "reth-primitives-traits", - "reth-rpc-api", - "reth-storage-api", - "reth-tasks", - "reth-transaction-pool", - "serde", - "thiserror 2.0.18", - "tokio", - "tracing", -] - -[[package]] -name = "reth-rpc-eth-api" -version = "1.11.3" -source = "git+https://github.com/paradigmxyz/reth?tag=v1.11.3#d6324d63e27ef6b7c49cdc9b1977c1b808234c7b" -dependencies = [ - "alloy-consensus", - "alloy-dyn-abi", - "alloy-eips", - "alloy-evm", - "alloy-json-rpc", - "alloy-network", - "alloy-primitives", - "alloy-rlp", - "alloy-rpc-types-eth", - "alloy-rpc-types-mev", - "alloy-serde", - "async-trait", - "auto_impl", - "dyn-clone", - "futures", - "jsonrpsee", - "jsonrpsee-types", - "parking_lot", - "reth-chain-state", - "reth-chainspec", - "reth-errors", - "reth-evm", - "reth-network-api", - "reth-node-api", - "reth-primitives-traits", - "reth-revm", - "reth-rpc-convert", - "reth-rpc-eth-types", - "reth-rpc-server-types", - "reth-storage-api", - "reth-tasks", - "reth-transaction-pool", - "reth-trie-common", - "revm", - "revm-inspectors", - "tokio", - "tracing", -] - -[[package]] -name = "reth-rpc-eth-types" -version = "1.11.3" -source = "git+https://github.com/paradigmxyz/reth?tag=v1.11.3#d6324d63e27ef6b7c49cdc9b1977c1b808234c7b" -dependencies = [ - "alloy-consensus", - "alloy-eips", - "alloy-evm", - "alloy-network", - "alloy-primitives", - "alloy-rpc-client", - "alloy-rpc-types-eth", - "alloy-sol-types", - "alloy-transport", - "derive_more", - "futures", - "itertools 0.14.0", - "jsonrpsee-core", - "jsonrpsee-types", - "metrics", - "rand 0.9.4", - "reqwest", - "reth-chain-state", - "reth-chainspec", - "reth-errors", - "reth-ethereum-primitives", - "reth-evm", - "reth-execution-types", - "reth-metrics", - "reth-primitives-traits", - "reth-revm", - "reth-rpc-convert", - "reth-rpc-server-types", - "reth-storage-api", - "reth-tasks", - "reth-transaction-pool", - "reth-trie", - "revm", - "revm-inspectors", - "schnellru", - "serde", - "thiserror 2.0.18", - "tokio", - "tokio-stream", - "tracing", - "url", -] - -[[package]] -name = "reth-rpc-layer" -version = "1.11.3" -source = "git+https://github.com/paradigmxyz/reth?tag=v1.11.3#d6324d63e27ef6b7c49cdc9b1977c1b808234c7b" -dependencies = [ - "alloy-rpc-types-engine", - "http", - "jsonrpsee-http-client", - "pin-project", - "tower 0.5.3", - "tower-http", - "tracing", -] - -[[package]] -name = "reth-rpc-server-types" -version = "1.11.3" -source = "git+https://github.com/paradigmxyz/reth?tag=v1.11.3#d6324d63e27ef6b7c49cdc9b1977c1b808234c7b" -dependencies = [ - "alloy-eips", - "alloy-primitives", - "alloy-rpc-types-engine", - "jsonrpsee-core", - "jsonrpsee-types", - "reth-errors", - "reth-network-api", - "serde", - "strum 0.27.2", -] - -[[package]] -name = "reth-stages" -version = "1.11.3" -source = "git+https://github.com/paradigmxyz/reth?tag=v1.11.3#d6324d63e27ef6b7c49cdc9b1977c1b808234c7b" -dependencies = [ - "alloy-consensus", - "alloy-eips", - "alloy-primitives", - "bincode", - "eyre", - "futures-util", - "itertools 0.14.0", - "num-traits", - "rayon", - "reqwest", - "reth-chainspec", - "reth-codecs", - "reth-config", - "reth-consensus", - "reth-db", - "reth-db-api", - "reth-era", - "reth-era-downloader", - "reth-era-utils", - "reth-ethereum-primitives", - "reth-etl", - "reth-evm", - "reth-execution-types", - "reth-exex", - "reth-fs-util", - "reth-network-p2p", - "reth-primitives-traits", - "reth-provider", - "reth-prune", - "reth-prune-types", - "reth-revm", - "reth-stages-api", - "reth-static-file-types", - "reth-storage-api", - "reth-storage-errors", - "reth-tasks", - "reth-testing-utils", - "reth-trie", - "reth-trie-db", - "tempfile", - "thiserror 2.0.18", - "tokio", - "tracing", -] - -[[package]] -name = "reth-stages-api" -version = "1.11.3" -source = "git+https://github.com/paradigmxyz/reth?tag=v1.11.3#d6324d63e27ef6b7c49cdc9b1977c1b808234c7b" -dependencies = [ - "alloy-eips", - "alloy-primitives", - "aquamarine", - "auto_impl", - "futures-util", - "metrics", - "reth-consensus", - "reth-errors", - "reth-metrics", - "reth-network-p2p", - "reth-primitives-traits", - "reth-provider", - "reth-prune", - "reth-stages-types", - "reth-static-file", - "reth-static-file-types", - "reth-tokio-util", - "thiserror 2.0.18", - "tokio", - "tracing", -] - -[[package]] -name = "reth-stages-types" -version = "1.11.3" -source = "git+https://github.com/paradigmxyz/reth?tag=v1.11.3#d6324d63e27ef6b7c49cdc9b1977c1b808234c7b" -dependencies = [ - "alloy-primitives", - "arbitrary", - "bytes", - "modular-bitfield", - "reth-codecs", - "reth-trie-common", - "serde", -] - -[[package]] -name = "reth-static-file" -version = "1.11.3" -source = "git+https://github.com/paradigmxyz/reth?tag=v1.11.3#d6324d63e27ef6b7c49cdc9b1977c1b808234c7b" -dependencies = [ - "alloy-primitives", - "parking_lot", - "rayon", - "reth-codecs", - "reth-db-api", - "reth-primitives-traits", - "reth-provider", - "reth-prune-types", - "reth-stages-types", - "reth-static-file-types", - "reth-storage-errors", - "reth-tokio-util", - "tracing", -] - -[[package]] -name = "reth-static-file-types" -version = "1.11.3" -source = "git+https://github.com/paradigmxyz/reth?tag=v1.11.3#d6324d63e27ef6b7c49cdc9b1977c1b808234c7b" -dependencies = [ - "alloy-primitives", - "clap", - "derive_more", - "fixed-map", - "reth-stages-types", - "serde", - "strum 0.27.2", - "tracing", -] - -[[package]] -name = "reth-storage-api" -version = "1.11.3" -source = "git+https://github.com/paradigmxyz/reth?tag=v1.11.3#d6324d63e27ef6b7c49cdc9b1977c1b808234c7b" -dependencies = [ - "alloy-consensus", - "alloy-eips", - "alloy-primitives", - "alloy-rpc-types-engine", - "auto_impl", - "reth-chainspec", - "reth-db-api", - "reth-db-models", - "reth-ethereum-primitives", - "reth-execution-types", - "reth-primitives-traits", - "reth-prune-types", - "reth-stages-types", - "reth-storage-errors", - "reth-trie-common", - "revm-database", - "serde_json", -] - -[[package]] -name = "reth-storage-errors" -version = "1.11.3" -source = "git+https://github.com/paradigmxyz/reth?tag=v1.11.3#d6324d63e27ef6b7c49cdc9b1977c1b808234c7b" -dependencies = [ - "alloy-eips", - "alloy-primitives", - "alloy-rlp", - "derive_more", - "reth-primitives-traits", - "reth-prune-types", - "reth-static-file-types", - "revm-database-interface", - "revm-state", - "thiserror 2.0.18", -] - -[[package]] -name = "reth-tasks" -version = "1.11.3" -source = "git+https://github.com/paradigmxyz/reth?tag=v1.11.3#d6324d63e27ef6b7c49cdc9b1977c1b808234c7b" -dependencies = [ - "auto_impl", - "dyn-clone", - "futures-util", - "metrics", - "pin-project", - "rayon", - "reth-metrics", - "thiserror 2.0.18", - "tokio", - "tracing", - "tracing-futures", -] - -[[package]] -name = "reth-testing-utils" -version = "1.11.3" -source = "git+https://github.com/paradigmxyz/reth?tag=v1.11.3#d6324d63e27ef6b7c49cdc9b1977c1b808234c7b" -dependencies = [ - "alloy-consensus", - "alloy-eips", - "alloy-genesis", - "alloy-primitives", - "rand 0.8.5", - "rand 0.9.4", - "reth-ethereum-primitives", - "reth-primitives-traits", - "secp256k1 0.30.0", -] - -[[package]] -name = "reth-tokio-util" -version = "1.11.3" -source = "git+https://github.com/paradigmxyz/reth?tag=v1.11.3#d6324d63e27ef6b7c49cdc9b1977c1b808234c7b" -dependencies = [ - "tokio", - "tokio-stream", - "tracing", -] - -[[package]] -name = "reth-tracing" -version = "1.11.3" -source = "git+https://github.com/paradigmxyz/reth?tag=v1.11.3#d6324d63e27ef6b7c49cdc9b1977c1b808234c7b" -dependencies = [ - "clap", - "eyre", - "reth-tracing-otlp", - "rolling-file", - "tracing", - "tracing-appender", - "tracing-journald", - "tracing-logfmt", - "tracing-samply", - "tracing-subscriber", -] - -[[package]] -name = "reth-tracing-otlp" -version = "1.11.3" -source = "git+https://github.com/paradigmxyz/reth?tag=v1.11.3#d6324d63e27ef6b7c49cdc9b1977c1b808234c7b" -dependencies = [ - "clap", - "eyre", - "opentelemetry", - "opentelemetry-otlp", - "opentelemetry-semantic-conventions", - "opentelemetry_sdk", - "tracing", - "tracing-opentelemetry", - "tracing-subscriber", - "url", -] - -[[package]] -name = "reth-transaction-pool" -version = "1.11.3" -source = "git+https://github.com/paradigmxyz/reth?tag=v1.11.3#d6324d63e27ef6b7c49cdc9b1977c1b808234c7b" -dependencies = [ - "alloy-consensus", - "alloy-eips", - "alloy-primitives", - "alloy-rlp", - "aquamarine", - "auto_impl", - "bitflags 2.11.0", - "futures-util", - "metrics", - "parking_lot", - "paste", - "pin-project", - "proptest", - "proptest-arbitrary-interop", - "rand 0.9.4", - "reth-chain-state", - "reth-chainspec", - "reth-eth-wire-types", - "reth-ethereum-primitives", - "reth-evm", - "reth-evm-ethereum", - "reth-execution-types", - "reth-fs-util", - "reth-metrics", - "reth-primitives-traits", - "reth-storage-api", - "reth-tasks", - "revm", - "revm-interpreter", - "revm-primitives", - "rustc-hash", - "schnellru", - "serde", - "serde_json", - "smallvec", - "thiserror 2.0.18", - "tokio", - "tokio-stream", - "tracing", -] - -[[package]] -name = "reth-trie" -version = "1.11.3" -source = "git+https://github.com/paradigmxyz/reth?tag=v1.11.3#d6324d63e27ef6b7c49cdc9b1977c1b808234c7b" -dependencies = [ - "alloy-consensus", - "alloy-eips", - "alloy-primitives", - "alloy-rlp", - "alloy-trie", - "auto_impl", - "itertools 0.14.0", - "metrics", - "parking_lot", - "reth-execution-errors", - "reth-metrics", - "reth-primitives-traits", - "reth-stages-types", - "reth-storage-errors", - "reth-trie-common", - "reth-trie-sparse", - "revm-database", - "tracing", - "triehash", -] - -[[package]] -name = "reth-trie-common" -version = "1.11.3" -source = "git+https://github.com/paradigmxyz/reth?tag=v1.11.3#d6324d63e27ef6b7c49cdc9b1977c1b808234c7b" -dependencies = [ - "alloy-consensus", - "alloy-primitives", - "alloy-rlp", - "alloy-rpc-types-eth", - "alloy-serde", - "alloy-trie", - "arbitrary", - "arrayvec", - "bytes", - "derive_more", - "hash-db", - "itertools 0.14.0", - "nybbles", - "plain_hasher", - "rayon", - "reth-codecs", - "reth-primitives-traits", - "revm-database", - "serde", - "serde_with", -] - -[[package]] -name = "reth-trie-db" -version = "1.11.3" -source = "git+https://github.com/paradigmxyz/reth?tag=v1.11.3#d6324d63e27ef6b7c49cdc9b1977c1b808234c7b" -dependencies = [ - "alloy-primitives", - "metrics", - "parking_lot", - "reth-db-api", - "reth-execution-errors", - "reth-metrics", - "reth-primitives-traits", - "reth-stages-types", - "reth-storage-api", - "reth-storage-errors", - "reth-trie", - "reth-trie-common", - "tracing", -] - -[[package]] -name = "reth-trie-parallel" -version = "1.11.3" -source = "git+https://github.com/paradigmxyz/reth?tag=v1.11.3#d6324d63e27ef6b7c49cdc9b1977c1b808234c7b" -dependencies = [ - "alloy-primitives", - "alloy-rlp", - "crossbeam-channel", - "derive_more", - "itertools 0.14.0", - "metrics", - "rayon", - "reth-execution-errors", - "reth-metrics", - "reth-primitives-traits", - "reth-provider", - "reth-storage-errors", - "reth-tasks", - "reth-trie", - "reth-trie-common", - "reth-trie-sparse", - "thiserror 2.0.18", - "tracing", -] - -[[package]] -name = "reth-trie-sparse" -version = "1.11.3" -source = "git+https://github.com/paradigmxyz/reth?tag=v1.11.3#d6324d63e27ef6b7c49cdc9b1977c1b808234c7b" -dependencies = [ - "alloy-primitives", - "alloy-rlp", - "alloy-trie", - "auto_impl", - "metrics", - "rayon", - "reth-execution-errors", - "reth-metrics", - "reth-primitives-traits", - "reth-trie-common", - "smallvec", - "tracing", -] - -[[package]] -name = "reth-zstd-compressors" -version = "1.11.3" -source = "git+https://github.com/paradigmxyz/reth?tag=v1.11.3#d6324d63e27ef6b7c49cdc9b1977c1b808234c7b" -dependencies = [ - "zstd", -] - -[[package]] -name = "revm" -version = "34.0.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c2aabdebaa535b3575231a88d72b642897ae8106cf6b0d12eafc6bfdf50abfc7" -dependencies = [ - "revm-bytecode", - "revm-context", - "revm-context-interface", - "revm-database", - "revm-database-interface", - "revm-handler", - "revm-inspector", - "revm-interpreter", - "revm-precompile", - "revm-primitives", - "revm-state", -] - -[[package]] -name = "revm-bytecode" -version = "8.0.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "74d1e5c1eaa44d39d537f668bc5c3409dc01e5c8be954da6c83370bbdf006457" -dependencies = [ - "bitvec", - "phf", - "revm-primitives", - "serde", -] - -[[package]] -name = "revm-context" -version = "13.0.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "892ff3e6a566cf8d72ffb627fdced3becebbd9ba64089c25975b9b028af326a5" -dependencies = [ - "bitvec", - "cfg-if", - "derive-where", - "revm-bytecode", - "revm-context-interface", - "revm-database-interface", - "revm-primitives", - "revm-state", - "serde", -] - -[[package]] -name = "revm-context-interface" -version = "14.0.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "57f61cc6d23678c4840af895b19f8acfbbd546142ec8028b6526c53cc1c16c98" -dependencies = [ - "alloy-eip2930", - "alloy-eip7702", - "auto_impl", - "either", - "revm-database-interface", - "revm-primitives", - "revm-state", - "serde", -] - -[[package]] -name = "revm-database" -version = "10.0.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "529528d0b05fe646be86223032c3e77aa8b05caa2a35447d538c55965956a511" -dependencies = [ - "alloy-eips", - "revm-bytecode", - "revm-database-interface", - "revm-primitives", - "revm-state", - "serde", -] - -[[package]] -name = "revm-database-interface" -version = "9.0.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b7bf93ac5b91347c057610c0d96e923db8c62807e03f036762d03e981feddc1d" -dependencies = [ - "auto_impl", - "either", - "revm-primitives", - "revm-state", - "serde", - "thiserror 2.0.18", -] - -[[package]] -name = "revm-handler" -version = "15.0.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0cd0e43e815a85eded249df886c4badec869195e70cdd808a13cfca2794622d2" -dependencies = [ - "auto_impl", - "derive-where", - "revm-bytecode", - "revm-context", - "revm-context-interface", - "revm-database-interface", - "revm-interpreter", - "revm-precompile", - "revm-primitives", - "revm-state", - "serde", -] - -[[package]] -name = "revm-inspector" -version = "15.0.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4f3ccad59db91ef93696536a0dbaf2f6f17cfe20d4d8843ae118edb7e97947ef" -dependencies = [ - "auto_impl", - "either", - "revm-context", - "revm-database-interface", - "revm-handler", - "revm-interpreter", - "revm-primitives", - "revm-state", - "serde", - "serde_json", -] - -[[package]] -name = "revm-inspectors" -version = "0.34.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6e435414e9de50a1b930da602067c76365fea2fea11e80ceb50783c94ddd127f" -dependencies = [ - "alloy-primitives", - "alloy-rpc-types-eth", - "alloy-rpc-types-trace", - "alloy-sol-types", - "anstyle", - "boa_engine", - "boa_gc", - "colorchoice", - "revm", - "serde", - "serde_json", - "thiserror 2.0.18", -] - -[[package]] -name = "revm-interpreter" -version = "32.0.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "11406408597bc249392d39295831c4b641b3a6f5c471a7c41104a7a1e3564c07" -dependencies = [ - "revm-bytecode", - "revm-context-interface", - "revm-primitives", - "revm-state", - "serde", -] - -[[package]] -name = "revm-precompile" -version = "32.0.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "50c1285c848d240678bf69cb0f6179ff5a4aee6fc8e921d89708087197a0aff3" -dependencies = [ - "ark-bls12-381", - "ark-bn254", - "ark-ec", - "ark-ff 0.5.0", - "ark-serialize 0.5.0", - "arrayref", - "aurora-engine-modexp", - "blst", - "c-kzg", - "cfg-if", - "k256", - "p256", - "revm-primitives", - "ripemd", - "secp256k1 0.31.1", - "sha2 0.10.9", -] - -[[package]] -name = "revm-primitives" -version = "22.0.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ba580c56a8ec824a64f8a1683577876c2e1dbe5247044199e9b881421ad5dcf9" -dependencies = [ - "alloy-primitives", - "num_enum", - "once_cell", - "serde", -] - -[[package]] -name = "revm-state" -version = "9.0.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "311720d4f0f239b041375e7ddafdbd20032a33b7bae718562ea188e188ed9fd3" -dependencies = [ - "alloy-eip7928", - "bitflags 2.11.0", - "revm-bytecode", - "revm-primitives", - "serde", -] - -[[package]] -name = "revm-statetest-types" -version = "14.0.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fab40862cebf520cf8f67dcd6a81dc8207140e57b36a9f15712e0dc7974a036d" -dependencies = [ - "alloy-eip7928", - "k256", - "revm-bytecode", - "revm-context", - "revm-context-interface", - "revm-database", - "revm-primitives", - "revm-state", - "serde", - "serde_json", - "thiserror 2.0.18", -] - -[[package]] -name = "rfc6979" -version = "0.4.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f8dd2a808d456c4a54e300a23e9f5a67e122c3024119acbfd73e3bf664491cb2" -dependencies = [ - "hmac 0.12.1", - "subtle", -] - -[[package]] -name = "ring" -version = "0.17.14" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a4689e6c2294d81e88dc6261c768b63bc4fcdb852be6d1352498b114f61383b7" -dependencies = [ - "cc", - "cfg-if", - "getrandom 0.2.17", - "libc", - "untrusted", - "windows-sys 0.52.0", -] - -[[package]] -name = "ringbuffer" -version = "0.16.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "57b0b88a509053cbfd535726dcaaceee631313cef981266119527a1d110f6d2b" - -[[package]] -name = "ripemd" -version = "0.1.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bd124222d17ad93a644ed9d011a40f4fb64aa54275c08cc216524a9ea82fb09f" -dependencies = [ - "digest 0.10.7", -] - -[[package]] -name = "rlimit" -version = "0.11.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f35ee2729c56bb610f6dba436bf78135f728b7373bdffae2ec815b2d3eb98cc3" -dependencies = [ - "libc", -] - -[[package]] -name = "rlp" -version = "0.5.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bb919243f34364b6bd2fc10ef797edbfa75f33c252e7998527479c6d6b47e1ec" -dependencies = [ - "bytes", - "rustc-hex", -] - -[[package]] -name = "rmcp" -version = "0.16.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cc4c9c94680f75470ee8083a0667988b5d7b5beb70b9f998a8e51de7c682ce60" -dependencies = [ - "async-trait", - "base64 0.22.1", - "bytes", - "chrono", - "futures", - "http", - "http-body", - "http-body-util", - "pastey", - "pin-project-lite", - "rand 0.10.1", - "rmcp-macros", - "schemars 1.2.1", - "serde", - "serde_json", - "sse-stream", - "thiserror 2.0.18", - "tokio", - "tokio-stream", - "tokio-util", - "tower-service", - "tracing", - "uuid", -] - -[[package]] -name = "rmcp-macros" -version = "0.16.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "90c23c8f26cae4da838fbc3eadfaecf2d549d97c04b558e7bd90526a9c28b42a" -dependencies = [ - "darling 0.23.0", - "proc-macro2", - "quote", - "serde_json", - "syn 2.0.117", -] - -[[package]] -name = "rmp" -version = "0.8.15" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4ba8be72d372b2c9b35542551678538b562e7cf86c3315773cae48dfbfe7790c" -dependencies = [ - "num-traits", -] - -[[package]] -name = "rmp-serde" -version = "1.3.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "72f81bee8c8ef9b577d1681a70ebbc962c232461e397b22c208c43c04b67a155" -dependencies = [ - "rmp", - "serde", -] - -[[package]] -name = "roaring" -version = "0.11.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8ba9ce64a8f45d7fc86358410bb1a82e8c987504c0d4900e9141d69a9f26c885" -dependencies = [ - "bytemuck", - "byteorder", -] - -[[package]] -name = "rolling-file" -version = "0.2.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8395b4f860856b740f20a296ea2cd4d823e81a2658cf05ef61be22916026a906" -dependencies = [ - "chrono", -] - -[[package]] -name = "route-recognizer" -version = "0.3.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "afab94fb28594581f62d981211a9a4d53cc8130bbcbbb89a0440d9b8e81a7746" - -[[package]] -name = "rstest" -version = "0.24.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "03e905296805ab93e13c1ec3a03f4b6c4f35e9498a3d5fa96dc626d22c03cd89" -dependencies = [ - "futures-timer", - "futures-util", - "rstest_macros", - "rustc_version 0.4.1", -] - -[[package]] -name = "rstest_macros" -version = "0.24.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ef0053bbffce09062bee4bcc499b0fbe7a57b879f1efe088d6d8d4c7adcdef9b" -dependencies = [ - "cfg-if", - "glob", - "proc-macro-crate", - "proc-macro2", - "quote", - "regex", - "relative-path", - "rustc_version 0.4.1", - "syn 2.0.117", - "unicode-ident", -] - -[[package]] -name = "rtnetlink" -version = "0.13.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7a552eb82d19f38c3beed3f786bd23aa434ceb9ac43ab44419ca6d67a7e186c0" -dependencies = [ - "futures", - "log", - "netlink-packet-core", - "netlink-packet-route", - "netlink-packet-utils", - "netlink-proto", - "netlink-sys", - "nix", - "thiserror 1.0.69", - "tokio", -] - -[[package]] -name = "ruint" -version = "1.17.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c141e807189ad38a07276942c6623032d3753c8859c146104ac2e4d68865945a" -dependencies = [ - "alloy-rlp", - "arbitrary", - "ark-ff 0.3.0", - "ark-ff 0.4.2", - "ark-ff 0.5.0", - "bytes", - "fastrlp 0.3.1", - "fastrlp 0.4.0", - "num-bigint", - "num-integer", - "num-traits", - "parity-scale-codec", - "primitive-types", - "proptest", - "rand 0.8.5", - "rand 0.9.4", - "rlp", - "ruint-macro", - "serde_core", - "valuable", - "zeroize", -] - -[[package]] -name = "ruint-macro" -version = "1.2.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "48fd7bd8a6377e15ad9d42a8ec25371b94ddc67abe7c8b9127bec79bebaaae18" - -[[package]] -name = "rustc-demangle" -version = "0.1.27" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b50b8869d9fc858ce7266cce0194bd74df58b9d0e3f6df3a9fc8eb470d95c09d" - -[[package]] -name = "rustc-hash" -version = "2.1.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "357703d41365b4b27c590e3ed91eabb1b663f07c4c084095e60cbed4362dff0d" -dependencies = [ - "rand 0.8.5", -] - -[[package]] -name = "rustc-hex" -version = "2.1.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3e75f6a532d0fd9f7f13144f392b6ad56a32696bfcd9c78f797f16bbb6f072d6" - -[[package]] -name = "rustc_version" -version = "0.2.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "138e3e0acb6c9fb258b19b67cb8abd63c00679d2851805ea151465464fe9030a" -dependencies = [ - "semver 0.9.0", -] - -[[package]] -name = "rustc_version" -version = "0.3.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f0dfe2087c51c460008730de8b57e6a320782fbfb312e1f4d520e6c6fae155ee" -dependencies = [ - "semver 0.11.0", -] - -[[package]] -name = "rustc_version" -version = "0.4.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cfcb3a22ef46e85b45de6ee7e79d063319ebb6594faafcf1c225ea92ab6e9b92" -dependencies = [ - "semver 1.0.27", -] - -[[package]] -name = "rusticata-macros" -version = "4.1.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "faf0c4a6ece9950b9abdb62b1cfcf2a68b3b67a10ba445b3bb85be2a293d0632" -dependencies = [ - "nom", -] - -[[package]] -name = "rustix" -version = "0.38.44" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fdb5bc1ae2baa591800df16c9ca78619bf65c0488b41b96ccec5d11220d8c154" -dependencies = [ - "bitflags 2.11.0", - "errno", - "libc", - "linux-raw-sys 0.4.15", - "windows-sys 0.59.0", -] - -[[package]] -name = "rustix" -version = "1.1.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "146c9e247ccc180c1f61615433868c99f3de3ae256a30a43b49f67c2d9171f34" -dependencies = [ - "bitflags 2.11.0", - "errno", - "libc", - "linux-raw-sys 0.11.0", - "windows-sys 0.59.0", -] - -[[package]] -name = "rustls" -version = "0.23.36" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c665f33d38cea657d9614f766881e4d510e0eda4239891eea56b4cadcf01801b" -dependencies = [ - "log", - "once_cell", - "ring", - "rustls-pki-types", - "rustls-webpki", - "subtle", - "zeroize", -] - -[[package]] -name = "rustls-native-certs" -version = "0.8.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "612460d5f7bea540c490b2b6395d8e34a953e52b491accd6c86c8164c5932a63" -dependencies = [ - "openssl-probe", - "rustls-pki-types", - "schannel", - "security-framework", -] - -[[package]] -name = "rustls-pemfile" -version = "2.2.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "dce314e5fee3f39953d46bb63bb8a46d40c2f8fb7cc5a3b6cab2bde9721d6e50" -dependencies = [ - "rustls-pki-types", -] - -[[package]] -name = "rustls-pki-types" -version = "1.14.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "be040f8b0a225e40375822a563fa9524378b9d63112f53e19ffff34df5d33fdd" -dependencies = [ - "web-time", - "zeroize", -] - -[[package]] -name = "rustls-platform-verifier" -version = "0.5.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "19787cda76408ec5404443dc8b31795c87cd8fec49762dc75fa727740d34acc1" -dependencies = [ - "core-foundation 0.10.1", - "core-foundation-sys", - "jni", - "log", - "once_cell", - "rustls", - "rustls-native-certs", - "rustls-platform-verifier-android", - "rustls-webpki", - "security-framework", - "security-framework-sys", - "webpki-root-certs 0.26.11", - "windows-sys 0.59.0", -] - -[[package]] -name = "rustls-platform-verifier-android" -version = "0.1.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f87165f0995f63a9fbeea62b64d10b4d9d8e78ec6d7d51fb2125fda7bb36788f" - -[[package]] -name = "rustls-webpki" -version = "0.103.13" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "61c429a8649f110dddef65e2a5ad240f747e85f7758a6bccc7e5777bd33f756e" -dependencies = [ - "ring", - "rustls-pki-types", - "untrusted", -] - -[[package]] -name = "rustversion" -version = "1.0.22" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b39cdef0fa800fc44525c84ccb54a029961a8215f9619753635a9c0d2538d46d" - -[[package]] -name = "rusty-fork" -version = "0.3.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cc6bf79ff24e648f6da1f8d1f011e9cac26491b619e6b9280f2b47f1774e6ee2" -dependencies = [ - "fnv", - "quick-error", - "tempfile", - "wait-timeout", -] - -[[package]] -name = "rw-stream-sink" -version = "0.4.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d8c9026ff5d2f23da5e45bbc283f156383001bfb09c4e44256d02c1a685fe9a1" -dependencies = [ - "futures", - "pin-project", - "static_assertions", -] - -[[package]] -name = "ryu" -version = "1.0.23" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9774ba4a74de5f7b1c1451ed6cd5285a32eddb5cccb8cc655a4e50009e06477f" - -[[package]] -name = "ryu-js" -version = "1.0.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "dd29631678d6fb0903b69223673e122c32e9ae559d0960a38d574695ebc0ea15" - -[[package]] -name = "same-file" -version = "1.0.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "93fc1dc3aaa9bfed95e02e6eadabb4baf7e3078b0bd1b4d7b6b0b68378900502" -dependencies = [ - "winapi-util", -] - -[[package]] -name = "scc" -version = "2.4.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "46e6f046b7fef48e2660c57ed794263155d713de679057f2d0c169bfc6e756cc" -dependencies = [ - "sdd", -] - -[[package]] -name = "schannel" -version = "0.1.28" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "891d81b926048e76efe18581bf793546b4c0eaf8448d72be8de2bbee5fd166e1" -dependencies = [ - "windows-sys 0.61.2", -] - -[[package]] -name = "schemars" -version = "0.9.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4cd191f9397d57d581cddd31014772520aa448f65ef991055d7f61582c65165f" -dependencies = [ - "dyn-clone", - "ref-cast", - "serde", - "serde_json", -] - -[[package]] -name = "schemars" -version = "1.2.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a2b42f36aa1cd011945615b92222f6bf73c599a102a300334cd7f8dbeec726cc" -dependencies = [ - "chrono", - "dyn-clone", - "ref-cast", - "schemars_derive", - "serde", - "serde_json", -] - -[[package]] -name = "schemars_derive" -version = "1.2.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7d115b50f4aaeea07e79c1912f645c7513d81715d0420f8bc77a18c6260b307f" -dependencies = [ - "proc-macro2", - "quote", - "serde_derive_internals", - "syn 2.0.117", -] - -[[package]] -name = "schnellru" -version = "0.2.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "356285bbf17bea63d9e52e96bd18f039672ac92b55b8cb997d6162a2a37d1649" -dependencies = [ - "ahash", - "cfg-if", - "hashbrown 0.13.2", -] - -[[package]] -name = "scopeguard" -version = "1.2.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "94143f37725109f92c262ed2cf5e59bce7498c01bcc1502d7b9afe439a4e9f49" - -[[package]] -name = "sdd" -version = "3.0.10" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "490dcfcbfef26be6800d11870ff2df8774fa6e86d047e3e8c8a76b25655e41ca" - -[[package]] -name = "seahash" -version = "4.1.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1c107b6f4780854c8b126e228ea8869f4d7b71260f962fefb57b996b8959ba6b" - -[[package]] -name = "sec1" -version = "0.7.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d3e97a565f76233a6003f9f5c54be1d9c5bdfa3eccfb189469f11ec4901c47dc" -dependencies = [ - "base16ct", - "der 0.7.10", - "generic-array", - "pkcs8 0.10.2", - "serdect", - "subtle", - "zeroize", -] - -[[package]] -name = "secp256k1" -version = "0.27.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "25996b82292a7a57ed3508f052cfff8640d38d32018784acd714758b43da9c8f" -dependencies = [ - "secp256k1-sys 0.8.2", -] - -[[package]] -name = "secp256k1" -version = "0.30.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b50c5943d326858130af85e049f2661ba3c78b26589b8ab98e65e80ae44a1252" -dependencies = [ - "bitcoin_hashes", - "rand 0.8.5", - "secp256k1-sys 0.10.1", - "serde", -] - -[[package]] -name = "secp256k1" -version = "0.31.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2c3c81b43dc2d8877c216a3fccf76677ee1ebccd429566d3e67447290d0c42b2" -dependencies = [ - "bitcoin_hashes", - "rand 0.9.4", - "secp256k1-sys 0.11.0", -] - -[[package]] -name = "secp256k1-sys" -version = "0.8.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4473013577ec77b4ee3668179ef1186df3146e2cf2d927bd200974c6fe60fd99" -dependencies = [ - "cc", -] - -[[package]] -name = "secp256k1-sys" -version = "0.10.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d4387882333d3aa8cb20530a17c69a3752e97837832f34f6dccc760e715001d9" -dependencies = [ - "cc", -] - -[[package]] -name = "secp256k1-sys" -version = "0.11.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "dcb913707158fadaf0d8702c2db0e857de66eb003ccfdda5924b5f5ac98efb38" -dependencies = [ - "cc", -] - -[[package]] -name = "security-framework" -version = "3.7.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b7f4bc775c73d9a02cde8bf7b2ec4c9d12743edf609006c7facc23998404cd1d" -dependencies = [ - "bitflags 2.11.0", - "core-foundation 0.10.1", - "core-foundation-sys", - "libc", - "security-framework-sys", -] - -[[package]] -name = "security-framework-sys" -version = "2.17.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6ce2691df843ecc5d231c0b14ece2acc3efb62c0a398c7e1d875f3983ce020e3" -dependencies = [ - "core-foundation-sys", - "libc", -] - -[[package]] -name = "semver" -version = "0.9.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1d7eb9ef2c18661902cc47e535f9bc51b78acd254da71d375c2f6720d9a40403" -dependencies = [ - "semver-parser 0.7.0", -] - -[[package]] -name = "semver" -version = "0.11.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f301af10236f6df4160f7c3f04eec6dbc70ace82d23326abad5edee88801c6b6" -dependencies = [ - "semver-parser 0.10.3", -] - -[[package]] -name = "semver" -version = "1.0.27" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d767eb0aabc880b29956c35734170f26ed551a859dbd361d140cdbeca61ab1e2" -dependencies = [ - "serde", - "serde_core", -] - -[[package]] -name = "semver-parser" -version = "0.7.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "388a1df253eca08550bef6c72392cfe7c30914bf41df5269b68cbd6ff8f570a3" - -[[package]] -name = "semver-parser" -version = "0.10.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9900206b54a3527fdc7b8a938bffd94a568bac4f4aa8113b209df75a09c0dec2" -dependencies = [ - "pest", -] - -[[package]] -name = "send_wrapper" -version = "0.4.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f638d531eccd6e23b980caf34876660d38e265409d8e99b397ab71eb3612fad0" - -[[package]] -name = "send_wrapper" -version = "0.6.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cd0b0ec5f1c1ca621c432a25813d8d60c88abe6d3e08a3eb9cf37d97a0fe3d73" - -[[package]] -name = "serde" -version = "1.0.228" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9a8e94ea7f378bd32cbbd37198a4a91436180c5bb472411e48b5ec2e2124ae9e" -dependencies = [ - "serde_core", - "serde_derive", -] - -[[package]] -name = "serde_core" -version = "1.0.228" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "41d385c7d4ca58e59fc732af25c3983b67ac852c1a25000afe1175de458b67ad" -dependencies = [ - "serde_derive", -] - -[[package]] -name = "serde_derive" -version = "1.0.228" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d540f220d3187173da220f885ab66608367b6574e925011a9353e4badda91d79" -dependencies = [ - "proc-macro2", - "quote", - "syn 2.0.117", -] - -[[package]] -name = "serde_derive_internals" -version = "0.29.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "18d26a20a969b9e3fdf2fc2d9f21eda6c40e2de84c9408bb5d3b05d499aae711" -dependencies = [ - "proc-macro2", - "quote", - "syn 2.0.117", -] - -[[package]] -name = "serde_json" -version = "1.0.149" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "83fc039473c5595ace860d8c4fafa220ff474b3fc6bfdb4293327f1a37e94d86" -dependencies = [ - "indexmap 2.13.0", - "itoa", - "memchr", - "serde", - "serde_core", - "zmij", -] - -[[package]] -name = "serde_path_to_error" -version = "0.1.20" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "10a9ff822e371bb5403e391ecd83e182e0e77ba7f6fe0160b795797109d1b457" -dependencies = [ - "itoa", - "serde", - "serde_core", -] - -[[package]] -name = "serde_spanned" -version = "0.6.9" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bf41e0cfaf7226dca15e8197172c295a782857fcb97fad1808a166870dee75a3" -dependencies = [ - "serde", -] - -[[package]] -name = "serde_spanned" -version = "1.0.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f8bbf91e5a4d6315eee45e704372590b30e260ee83af6639d64557f51b067776" -dependencies = [ - "serde_core", -] - -[[package]] -name = "serde_urlencoded" -version = "0.7.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d3491c14715ca2294c4d6a88f15e84739788c1d030eed8c110436aafdaa2f3fd" -dependencies = [ - "form_urlencoded", - "itoa", - "ryu", - "serde", -] - -[[package]] -name = "serde_with" -version = "3.16.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4fa237f2807440d238e0364a218270b98f767a00d3dada77b1c53ae88940e2e7" -dependencies = [ - "base64 0.22.1", - "chrono", - "hex", - "indexmap 1.9.3", - "indexmap 2.13.0", - "schemars 0.9.0", - "schemars 1.2.1", - "serde_core", - "serde_json", - "serde_with_macros", - "time", -] - -[[package]] -name = "serde_with_macros" -version = "3.16.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "52a8e3ca0ca629121f70ab50f95249e5a6f925cc0f6ffe8256c45b728875706c" -dependencies = [ - "darling 0.21.3", - "proc-macro2", - "quote", - "syn 2.0.117", -] - -[[package]] -name = "serdect" -version = "0.2.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a84f14a19e9a014bb9f4512488d9829a68e04ecabffb0f9904cd1ace94598177" -dependencies = [ - "base16ct", - "serde", -] - -[[package]] -name = "serial_test" -version = "3.4.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "911bd979bf1070a3f3aa7b691a3b3e9968f339ceeec89e08c280a8a22207a32f" -dependencies = [ - "futures-executor", - "futures-util", - "log", - "once_cell", - "parking_lot", - "scc", - "serial_test_derive", -] - -[[package]] -name = "serial_test_derive" -version = "3.4.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0a7d91949b85b0d2fb687445e448b40d322b6b3e4af6b44a29b21d9a5f33e6d9" -dependencies = [ - "proc-macro2", - "quote", - "syn 2.0.117", -] - -[[package]] -name = "sha1" -version = "0.10.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e3bf829a2d51ab4a5ddf1352d8470c140cadc8301b2ae1789db023f01cedd6ba" -dependencies = [ - "cfg-if", - "cpufeatures 0.2.17", - "digest 0.10.7", -] - -[[package]] -name = "sha2" -version = "0.9.9" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4d58a1e1bf39749807d89cf2d98ac2dfa0ff1cb3faa38fbb64dd88ac8013d800" -dependencies = [ - "block-buffer 0.9.0", - "cfg-if", - "cpufeatures 0.2.17", - "digest 0.9.0", - "opaque-debug", -] - -[[package]] -name = "sha2" -version = "0.10.9" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a7507d819769d01a365ab707794a4084392c824f54a7a6a7862f8c3d0892b283" -dependencies = [ - "cfg-if", - "cpufeatures 0.2.17", - "digest 0.10.7", -] - -[[package]] -name = "sha2" -version = "0.11.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "446ba717509524cb3f22f17ecc096f10f4822d76ab5c0b9822c5f9c284e825f4" -dependencies = [ - "cfg-if", - "cpufeatures 0.3.0", - "digest 0.11.2", -] - -[[package]] -name = "sha3" -version = "0.10.8" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "75872d278a8f37ef87fa0ddbda7802605cb18344497949862c0d4dcb291eba60" -dependencies = [ - "digest 0.10.7", - "keccak 0.1.6", -] - -[[package]] -name = "sha3" -version = "0.11.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "be176f1a57ce4e3d31c1a166222d9768de5954f811601fb7ca06fc8203905ce1" -dependencies = [ - "digest 0.11.2", - "keccak 0.2.0", -] - -[[package]] -name = "sha3-asm" -version = "0.1.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b31139435f327c93c6038ed350ae4588e2c70a13d50599509fee6349967ba35a" -dependencies = [ - "cc", - "cfg-if", -] - -[[package]] -name = "sharded-slab" -version = "0.1.7" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f40ca3c46823713e0d4209592e8d6e826aa57e928f09752619fc696c499637f6" -dependencies = [ - "lazy_static", -] - -[[package]] -name = "shellexpand" -version = "3.1.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8b1fdf65dd6331831494dd616b30351c38e96e45921a27745cf98490458b90bb" -dependencies = [ - "dirs", -] - -[[package]] -name = "shlex" -version = "1.3.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0fda2ff0d084019ba4d7c6f371c95d8fd75ce3524c3cb8fb653a3023f6323e64" - -[[package]] -name = "signal-hook" -version = "0.3.18" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d881a16cf4426aa584979d30bd82cb33429027e42122b169753d6ef1085ed6e2" -dependencies = [ - "libc", - "signal-hook-registry", -] - -[[package]] -name = "signal-hook-mio" -version = "0.2.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b75a19a7a740b25bc7944bdee6172368f988763b744e3d4dfe753f6b4ece40cc" -dependencies = [ - "libc", - "mio", - "signal-hook", -] - -[[package]] -name = "signal-hook-registry" -version = "1.4.8" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c4db69cba1110affc0e9f7bcd48bbf87b3f4fc7c61fc9155afd4c469eb3d6c1b" -dependencies = [ - "errno", - "libc", -] - -[[package]] -name = "signature" -version = "2.2.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "77549399552de45a898a580c1b41d445bf730df867cc44e6c0233bbc4b8329de" -dependencies = [ - "digest 0.10.7", - "rand_core 0.6.4", -] - -[[package]] -name = "signature" -version = "3.0.0-rc.10" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7f1880df446116126965eeec169136b2e0251dba37c6223bcc819569550edea3" -dependencies = [ - "rand_core 0.10.0", -] - -[[package]] -name = "simd-adler32" -version = "0.3.8" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e320a6c5ad31d271ad523dcf3ad13e2767ad8b1cb8f047f75a8aeaf8da139da2" - -[[package]] -name = "simple_asn1" -version = "0.6.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0d585997b0ac10be3c5ee635f1bab02d512760d14b7c468801ac8a01d9ae5f1d" -dependencies = [ - "num-bigint", - "num-traits", - "thiserror 2.0.18", - "time", -] - -[[package]] -name = "siphasher" -version = "1.0.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b2aa850e253778c88a04c3d7323b043aeda9d3e30d5971937c1855769763678e" - -[[package]] -name = "sketches-ddsketch" -version = "0.3.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c1e9a774a6c28142ac54bb25d25562e6bcf957493a184f15ad4eebccb23e410a" - -[[package]] -name = "slab" -version = "0.4.12" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0c790de23124f9ab44544d7ac05d60440adc586479ce501c1d6d7da3cd8c9cf5" - -[[package]] -name = "slh-dsa" -version = "0.2.0-rc.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "371c02fe34044d8866ddf7cb0e8204a87ef31a39f0408bed41c4253ea9dd61ed" -dependencies = [ - "const-oid 0.10.2", - "digest 0.11.2", - "hmac 0.13.0", - "hybrid-array", - "pkcs8 0.11.0", - "rand_core 0.10.0", - "sha2 0.11.0", - "sha3 0.11.0", - "signature 3.0.0-rc.10", - "typenum", - "zerocopy", -] - -[[package]] -name = "small_btree" -version = "0.1.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0ba60d2df92ba73864714808ca68c059734853e6ab722b40e1cf543ebb3a057a" -dependencies = [ - "arrayvec", -] - -[[package]] -name = "smallvec" -version = "1.15.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "67b1b7a3b5fe4f1376887184045fcf45c69e92af734b7aaddc05fb777b6fbd03" -dependencies = [ - "arbitrary", - "serde", -] - -[[package]] -name = "smol-hyper" -version = "0.1.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7428a49d323867702cd12b97b08a6b0104f39ec13b49117911f101271321bc1a" -dependencies = [ - "futures-io", - "hyper", - "pin-project-lite", -] - -[[package]] -name = "snap" -version = "1.1.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1b6b67fb9a61334225b5b790716f609cd58395f895b3fe8b328786812a40bc3b" - -[[package]] -name = "snow" -version = "0.9.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "850948bee068e713b8ab860fe1adc4d109676ab4c3b621fd8147f06b261f2f85" -dependencies = [ - "aes-gcm", - "blake2", - "chacha20poly1305", - "curve25519-dalek", - "rand_core 0.6.4", - "ring", - "rustc_version 0.4.1", - "sha2 0.10.9", - "subtle", -] - -[[package]] -name = "socket2" -version = "0.5.10" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e22376abed350d73dd1cd119b57ffccad95b4e585a7cda43e286245ce23c0678" -dependencies = [ - "libc", - "windows-sys 0.52.0", -] - -[[package]] -name = "socket2" -version = "0.6.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "86f4aa3ad99f2088c990dfa82d367e19cb29268ed67c574d10d0a4bfe71f07e0" -dependencies = [ - "libc", - "windows-sys 0.60.2", -] - -[[package]] -name = "soketto" -version = "0.8.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2e859df029d160cb88608f5d7df7fb4753fd20fdfb4de5644f3d8b8440841721" -dependencies = [ - "base64 0.22.1", - "bytes", - "futures", - "http", - "httparse", - "log", - "rand 0.8.5", - "sha1", -] - -[[package]] -name = "spammer" -version = "0.0.1" -dependencies = [ - "alloy-consensus", - "alloy-eips", - "alloy-primitives", - "alloy-rpc-types-txpool", - "alloy-signer", - "alloy-signer-local", - "alloy-sol-types", - "arc-version", - "chrono", - "clap", - "clap-verbosity-flag", - "color-eyre", - "futures-util", - "governor", - "hex", - "humantime", - "k256", - "rand 0.8.5", - "serde", - "serde_json", - "strum 0.27.2", - "strum_macros 0.27.2", - "tokio", - "tokio-tungstenite 0.28.0", - "tracing", - "tracing-subscriber", - "tungstenite 0.28.0", - "url", -] - -[[package]] -name = "spin" -version = "0.10.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d5fe4ccb98d9c292d56fec89a5e07da7fc4cf0dc11e156b41793132775d3e591" -dependencies = [ - "lock_api", -] - -[[package]] -name = "spinning_top" -version = "0.3.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d96d2d1d716fb500937168cc09353ffdc7a012be8475ac7308e1bdf0e3923300" -dependencies = [ - "lock_api", -] - -[[package]] -name = "spki" -version = "0.7.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d91ed6c858b01f942cd56b37a94b3e0a1798290327d1236e4d9cf4eaca44d29d" -dependencies = [ - "base64ct", - "der 0.7.10", -] - -[[package]] -name = "spki" -version = "0.8.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1d9efca8738c78ee9484207732f728b1ef517bbb1833d6fc0879ca898a522f6f" -dependencies = [ - "base64ct", - "der 0.8.0", -] - -[[package]] -name = "sse-stream" -version = "0.2.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "eb4dc4d33c68ec1f27d386b5610a351922656e1fdf5c05bbaad930cd1519479a" -dependencies = [ - "bytes", - "futures-util", - "http-body", - "http-body-util", - "pin-project-lite", -] - -[[package]] -name = "stable_deref_trait" -version = "1.2.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6ce2be8dc25455e1f91df71bfa12ad37d7af1092ae736f3a6cd0e37bc7810596" - -[[package]] -name = "static_assertions" -version = "1.1.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a2eb9349b6444b326872e140eb1cf5e7c522154d69e7a0ffb0fb81c06b37543f" - -[[package]] -name = "strsim" -version = "0.11.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7da8b5736845d9f2fcb837ea5d9e2628564b3b043a70948a3f0b778838c5fb4f" - -[[package]] -name = "strum" -version = "0.26.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8fec0f0aef304996cf250b31b5a10dee7980c85da9d759361292b8bca5a18f06" -dependencies = [ - "strum_macros 0.26.4", -] - -[[package]] -name = "strum" -version = "0.27.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "af23d6f6c1a224baef9d3f61e287d2761385a5b88fdab4eb4c6f11aeb54c4bcf" -dependencies = [ - "strum_macros 0.27.2", -] - -[[package]] -name = "strum_macros" -version = "0.26.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4c6bee85a5a24955dc440386795aa378cd9cf82acd5f764469152d2270e581be" -dependencies = [ - "heck", - "proc-macro2", - "quote", - "rustversion", - "syn 2.0.117", -] - -[[package]] -name = "strum_macros" -version = "0.27.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7695ce3845ea4b33927c055a39dc438a45b059f7c1b3d91d38d10355fb8cbca7" -dependencies = [ - "heck", - "proc-macro2", - "quote", - "syn 2.0.117", -] - -[[package]] -name = "subtle" -version = "2.6.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "13c2bddecc57b384dee18652358fb23172facb8a2c51ccc10d74c157bdea3292" - -[[package]] -name = "subtle-ng" -version = "2.5.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "734676eb262c623cec13c3155096e08d1f8f29adce39ba17948b18dad1e54142" - -[[package]] -name = "symbolic-common" -version = "12.17.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "751a2823d606b5d0a7616499e4130a516ebd01a44f39811be2b9600936509c23" -dependencies = [ - "debugid", - "memmap2", - "stable_deref_trait", - "uuid", -] - -[[package]] -name = "symbolic-demangle" -version = "12.17.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "79b237cfbe320601dd24b4ac817a5b68bb28f5508e33f08d42be0682cadc8ac9" -dependencies = [ - "cpp_demangle", - "rustc-demangle", - "symbolic-common", -] - -[[package]] -name = "syn" -version = "1.0.109" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "72b64191b275b66ffe2469e8af2c1cfe3bafa67b529ead792a6d0160888b4237" -dependencies = [ - "proc-macro2", - "quote", - "unicode-ident", -] - -[[package]] -name = "syn" -version = "2.0.117" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e665b8803e7b1d2a727f4023456bbbbe74da67099c585258af0ad9c5013b9b99" -dependencies = [ - "proc-macro2", - "quote", - "unicode-ident", -] - -[[package]] -name = "syn-solidity" -version = "1.5.7" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "53f425ae0b12e2f5ae65542e00898d500d4d318b4baf09f40fd0d410454e9947" -dependencies = [ - "paste", - "proc-macro2", - "quote", - "syn 2.0.117", -] - -[[package]] -name = "sync_wrapper" -version = "1.0.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0bf256ce5efdfa370213c1dabab5935a12e49f2c58d15e9eac2870d3b4f27263" -dependencies = [ - "futures-core", -] - -[[package]] -name = "synstructure" -version = "0.13.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "728a70f3dbaf5bab7f0c4b1ac8d7ae5ea60a4b5549c8a5914361c99147a709d2" -dependencies = [ - "proc-macro2", - "quote", - "syn 2.0.117", -] - -[[package]] -name = "sysinfo" -version = "0.38.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1efc19935b4b66baa6f654ac7924c192f55b175c00a7ab72410fc24284dacda8" -dependencies = [ - "libc", - "memchr", - "ntapi", - "objc2-core-foundation", - "objc2-io-kit", - "windows 0.62.2", -] - -[[package]] -name = "system-configuration" -version = "0.6.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3c879d448e9d986b661742763247d3693ed13609438cf3d006f51f5368a5ba6b" -dependencies = [ - "bitflags 2.11.0", - "core-foundation 0.9.4", - "system-configuration-sys", -] - -[[package]] -name = "system-configuration-sys" -version = "0.6.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8e1d1b10ced5ca923a1fcb8d03e96b8d3268065d724548c0211415ff6ac6bac4" -dependencies = [ - "core-foundation-sys", - "libc", -] - -[[package]] -name = "tag_ptr" -version = "0.1.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c0e973b34477b7823833469eb0f5a3a60370fef7a453e02d751b59180d0a5a05" - -[[package]] -name = "tagptr" -version = "0.2.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7b2093cf4c8eb1e67749a6762251bc9cd836b6fc171623bd0a9d324d37af2417" - -[[package]] -name = "tap" -version = "1.0.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "55937e1799185b12863d447f42597ed69d9928686b8d88a1df17376a097d8369" - -[[package]] -name = "tar" -version = "0.4.45" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "22692a6476a21fa75fdfc11d452fda482af402c008cdbaf3476414e122040973" -dependencies = [ - "filetime", - "libc", - "xattr", -] - -[[package]] -name = "tempfile" -version = "3.25.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0136791f7c95b1f6dd99f9cc786b91bb81c3800b639b3478e561ddb7be95e5f1" -dependencies = [ - "fastrand", - "getrandom 0.4.1", - "once_cell", - "rustix 1.1.3", - "windows-sys 0.59.0", -] - -[[package]] -name = "termtree" -version = "0.5.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8f50febec83f5ee1df3015341d8bd429f2d1cc62bcba7ea2076759d315084683" - -[[package]] -name = "thin-vec" -version = "0.2.16" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "259cdf8ed4e4aca6f1e9d011e10bd53f524a2d0637d7b28450f6c64ac298c4c6" - -[[package]] -name = "thiserror" -version = "1.0.69" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b6aaf5339b578ea85b50e080feb250a3e8ae8cfcdff9a461c9ec2904bc923f52" -dependencies = [ - "thiserror-impl 1.0.69", -] - -[[package]] -name = "thiserror" -version = "2.0.18" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4288b5bcbc7920c07a1149a35cf9590a2aa808e0bc1eafaade0b80947865fbc4" -dependencies = [ - "thiserror-impl 2.0.18", -] - -[[package]] -name = "thiserror-impl" -version = "1.0.69" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4fee6c4efc90059e10f81e6d42c60a18f76588c3d74cb83a0b242a2b6c7504c1" -dependencies = [ - "proc-macro2", - "quote", - "syn 2.0.117", -] - -[[package]] -name = "thiserror-impl" -version = "2.0.18" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ebc4ee7f67670e9b64d05fa4253e753e016c6c95ff35b89b7941d6b856dec1d5" -dependencies = [ - "proc-macro2", - "quote", - "syn 2.0.117", -] - -[[package]] -name = "thread_local" -version = "1.1.9" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f60246a4944f24f6e018aa17cdeffb7818b76356965d03b07d6a9886e8962185" -dependencies = [ - "cfg-if", -] - -[[package]] -name = "threadpool" -version = "1.8.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d050e60b33d41c19108b32cea32164033a9013fe3b46cbd4457559bfbf77afaa" -dependencies = [ - "num_cpus", -] - -[[package]] -name = "tikv-jemalloc-ctl" -version = "0.6.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "661f1f6a57b3a36dc9174a2c10f19513b4866816e13425d3e418b11cc37bc24c" -dependencies = [ - "libc", - "paste", - "tikv-jemalloc-sys", -] - -[[package]] -name = "tikv-jemalloc-sys" -version = "0.6.1+5.3.0-1-ge13ca993e8ccb9ba9847cc330696e02839f328f7" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cd8aa5b2ab86a2cefa406d889139c162cbb230092f7d1d7cbc1716405d852a3b" -dependencies = [ - "cc", - "libc", -] - -[[package]] -name = "tikv-jemallocator" -version = "0.6.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0359b4327f954e0567e69fb191cf1436617748813819c94b8cd4a431422d053a" -dependencies = [ - "libc", - "tikv-jemalloc-sys", -] - -[[package]] -name = "time" -version = "0.3.47" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "743bd48c283afc0388f9b8827b976905fb217ad9e647fae3a379a9283c4def2c" -dependencies = [ - "deranged", - "itoa", - "js-sys", - "libc", - "num-conv", - "num_threads", - "powerfmt", - "serde_core", - "time-core", - "time-macros", -] - -[[package]] -name = "time-core" -version = "0.1.8" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7694e1cfe791f8d31026952abf09c69ca6f6fa4e1a1229e18988f06a04a12dca" - -[[package]] -name = "time-macros" -version = "0.2.27" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2e70e4c5a0e0a8a4823ad65dfe1a6930e4f4d756dcd9dd7939022b5e8c501215" -dependencies = [ - "num-conv", - "time-core", -] - -[[package]] -name = "tinystr" -version = "0.8.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "42d3e9c45c09de15d06dd8acf5f4e0e399e85927b7f00711024eb7ae10fa4869" -dependencies = [ - "displaydoc", - "serde_core", - "zerovec", -] - -[[package]] -name = "tinytemplate" -version = "1.2.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "be4d6b5f19ff7664e8c98d03e2139cb510db9b0a60b55f8e8709b689d939b6bc" -dependencies = [ - "serde", - "serde_json", -] - -[[package]] -name = "tinyvec" -version = "1.10.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bfa5fdc3bce6191a1dbc8c02d5c8bffcf557bafa17c124c5264a458f1b0613fa" -dependencies = [ - "tinyvec_macros", -] - -[[package]] -name = "tinyvec_macros" -version = "0.1.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1f3ccbac311fea05f86f61904b462b55fb3df8837a366dfc601a0161d0532f20" - -[[package]] -name = "tokio" -version = "1.49.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "72a2903cd7736441aac9df9d7688bd0ce48edccaadf181c3b90be801e81d3d86" -dependencies = [ - "bytes", - "libc", - "mio", - "parking_lot", - "pin-project-lite", - "signal-hook-registry", - "socket2 0.6.2", - "tokio-macros", - "tracing", - "windows-sys 0.61.2", -] - -[[package]] -name = "tokio-macros" -version = "2.6.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "af407857209536a95c8e56f8231ef2c2e2aff839b22e07a1ffcbc617e9db9fa5" -dependencies = [ - "proc-macro2", - "quote", - "syn 2.0.117", -] - -[[package]] -name = "tokio-native-tls" -version = "0.3.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bbae76ab933c85776efabc971569dd6119c580d8f5d448769dec1764bf796ef2" -dependencies = [ - "native-tls", - "tokio", -] - -[[package]] -name = "tokio-rustls" -version = "0.26.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1729aa945f29d91ba541258c8df89027d5792d85a8841fb65e8bf0f4ede4ef61" -dependencies = [ - "rustls", - "tokio", -] - -[[package]] -name = "tokio-stream" -version = "0.1.18" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "32da49809aab5c3bc678af03902d4ccddea2a87d028d86392a4b1560c6906c70" -dependencies = [ - "futures-core", - "pin-project-lite", - "tokio", - "tokio-util", -] - -[[package]] -name = "tokio-tungstenite" -version = "0.26.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7a9daff607c6d2bf6c16fd681ccb7eecc83e4e2cdc1ca067ffaadfca5de7f084" -dependencies = [ - "futures-util", - "log", - "rustls", - "rustls-native-certs", - "rustls-pki-types", - "tokio", - "tokio-rustls", - "tungstenite 0.26.2", - "webpki-roots 0.26.11", -] - -[[package]] -name = "tokio-tungstenite" -version = "0.28.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d25a406cddcc431a75d3d9afc6a7c0f7428d4891dd973e4d54c56b46127bf857" -dependencies = [ - "futures-util", - "log", - "tokio", - "tungstenite 0.28.0", -] - -[[package]] -name = "tokio-util" -version = "0.7.18" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9ae9cec805b01e8fc3fd2fe289f89149a9b66dd16786abd8b19cfa7b48cb0098" -dependencies = [ - "bytes", - "futures-core", - "futures-io", - "futures-sink", - "pin-project-lite", - "slab", - "tokio", -] - -[[package]] -name = "tokio_with_wasm" -version = "0.8.8" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "34e40fbbbd95441133fe9483f522db15dbfd26dc636164ebd8f2dd28759a6aa6" -dependencies = [ - "js-sys", - "tokio", - "tokio_with_wasm_proc", - "wasm-bindgen", - "wasm-bindgen-futures", - "web-sys", -] - -[[package]] -name = "tokio_with_wasm_proc" -version = "0.8.8" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d01145a2c788d6aae4cd653afec1e8332534d7d783d01897cefcafe4428de992" -dependencies = [ - "quote", - "syn 2.0.117", -] - -[[package]] -name = "toml" -version = "0.8.23" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "dc1beb996b9d83529a9e75c17a1686767d148d70663143c7854d8b4a09ced362" -dependencies = [ - "indexmap 2.13.0", - "serde", - "serde_spanned 0.6.9", - "toml_datetime 0.6.11", - "toml_edit 0.22.27", -] - -[[package]] -name = "toml" -version = "0.9.12+spec-1.1.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cf92845e79fc2e2def6a5d828f0801e29a2f8acc037becc5ab08595c7d5e9863" -dependencies = [ - "indexmap 2.13.0", - "serde_core", - "serde_spanned 1.0.4", - "toml_datetime 0.7.5+spec-1.1.0", - "toml_parser", - "toml_writer", - "winnow", -] - -[[package]] -name = "toml_datetime" -version = "0.6.11" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "22cddaf88f4fbc13c51aebbf5f8eceb5c7c5a9da2ac40a13519eb5b0a0e8f11c" -dependencies = [ - "serde", -] - -[[package]] -name = "toml_datetime" -version = "0.7.5+spec-1.1.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "92e1cfed4a3038bc5a127e35a2d360f145e1f4b971b551a2ba5fd7aedf7e1347" -dependencies = [ - "serde_core", -] - -[[package]] -name = "toml_edit" -version = "0.22.27" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "41fe8c660ae4257887cf66394862d21dbca4a6ddd26f04a3560410406a2f819a" -dependencies = [ - "indexmap 2.13.0", - "serde", - "serde_spanned 0.6.9", - "toml_datetime 0.6.11", - "toml_write", - "winnow", -] - -[[package]] -name = "toml_edit" -version = "0.23.10+spec-1.0.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "84c8b9f757e028cee9fa244aea147aab2a9ec09d5325a9b01e0a49730c2b5269" -dependencies = [ - "indexmap 2.13.0", - "toml_datetime 0.7.5+spec-1.1.0", - "toml_parser", - "winnow", -] - -[[package]] -name = "toml_parser" -version = "1.0.9+spec-1.1.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "702d4415e08923e7e1ef96cd5727c0dfed80b4d2fa25db9647fe5eb6f7c5a4c4" -dependencies = [ - "winnow", -] - -[[package]] -name = "toml_write" -version = "0.1.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5d99f8c9a7727884afe522e9bd5edbfc91a3312b36a77b5fb8926e4c31a41801" - -[[package]] -name = "toml_writer" -version = "1.0.6+spec-1.1.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ab16f14aed21ee8bfd8ec22513f7287cd4a91aa92e44edfe2c17ddd004e92607" - -[[package]] -name = "tonic" -version = "0.12.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "877c5b330756d856ffcc4553ab34a5684481ade925ecc54bcd1bf02b1d0d4d52" -dependencies = [ - "async-stream", - "async-trait", - "axum 0.7.9", - "base64 0.22.1", - "bytes", - "h2", - "http", - "http-body", - "http-body-util", - "hyper", - "hyper-timeout", - "hyper-util", - "percent-encoding", - "pin-project", - "prost 0.13.5", - "rustls-pemfile", - "socket2 0.5.10", - "tokio", - "tokio-rustls", - "tokio-stream", - "tower 0.4.13", - "tower-layer", - "tower-service", - "tracing", -] - -[[package]] -name = "tonic" -version = "0.14.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fec7c61a0695dc1887c1b53952990f3ad2e3a31453e1f49f10e75424943a93ec" -dependencies = [ - "async-trait", - "base64 0.22.1", - "bytes", - "http", - "http-body", - "http-body-util", - "hyper", - "hyper-timeout", - "hyper-util", - "percent-encoding", - "pin-project", - "sync_wrapper", - "tokio", - "tokio-stream", - "tower 0.5.3", - "tower-layer", - "tower-service", - "tracing", -] - -[[package]] -name = "tonic-build" -version = "0.12.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9557ce109ea773b399c9b9e5dca39294110b74f1f342cb347a80d1fce8c26a11" -dependencies = [ - "prettyplease", - "proc-macro2", - "prost-build 0.13.5", - "prost-types 0.13.5", - "quote", - "syn 2.0.117", -] - -[[package]] -name = "tonic-prost" -version = "0.14.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a55376a0bbaa4975a3f10d009ad763d8f4108f067c7c2e74f3001fb49778d309" -dependencies = [ - "bytes", - "prost 0.14.3", - "tonic 0.14.5", -] - -[[package]] -name = "tower" -version = "0.4.13" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b8fa9be0de6cf49e536ce1851f987bd21a43b771b09473c3549a6c853db37c1c" -dependencies = [ - "futures-core", - "futures-util", - "indexmap 1.9.3", - "pin-project", - "pin-project-lite", - "rand 0.8.5", - "slab", - "tokio", - "tokio-util", - "tower-layer", - "tower-service", - "tracing", -] - -[[package]] -name = "tower" -version = "0.5.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ebe5ef63511595f1344e2d5cfa636d973292adc0eec1f0ad45fae9f0851ab1d4" -dependencies = [ - "futures-core", - "futures-util", - "hdrhistogram", - "indexmap 2.13.0", - "pin-project-lite", - "slab", - "sync_wrapper", - "tokio", - "tokio-util", - "tower-layer", - "tower-service", - "tracing", -] - -[[package]] -name = "tower-http" -version = "0.6.8" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d4e6559d53cc268e5031cd8429d05415bc4cb4aefc4aa5d6cc35fbf5b924a1f8" -dependencies = [ - "async-compression", - "base64 0.22.1", - "bitflags 2.11.0", - "bytes", - "futures-core", - "futures-util", - "http", - "http-body", - "http-body-util", - "http-range-header", - "httpdate", - "iri-string", - "mime", - "mime_guess", - "percent-encoding", - "pin-project-lite", - "tokio", - "tokio-util", - "tower 0.5.3", - "tower-layer", - "tower-service", - "tracing", - "uuid", -] - -[[package]] -name = "tower-layer" -version = "0.3.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "121c2a6cda46980bb0fcd1647ffaf6cd3fc79a013de288782836f6df9c48780e" - -[[package]] -name = "tower-service" -version = "0.3.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8df9b6e13f2d32c91b9bd719c00d1958837bc7dec474d94952798cc8e69eeec3" - -[[package]] -name = "tracing" -version = "0.1.44" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "63e71662fa4b2a2c3a26f570f037eb95bb1f85397f3cd8076caed2f026a6d100" -dependencies = [ - "log", - "pin-project-lite", - "tracing-attributes", - "tracing-core", -] - -[[package]] -name = "tracing-appender" -version = "0.2.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "786d480bce6247ab75f005b14ae1624ad978d3029d9113f0a22fa1ac773faeaf" -dependencies = [ - "crossbeam-channel", - "thiserror 2.0.18", - "time", - "tracing-subscriber", -] - -[[package]] -name = "tracing-attributes" -version = "0.1.31" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7490cfa5ec963746568740651ac6781f701c9c5ea257c58e057f3ba8cf69e8da" -dependencies = [ - "proc-macro2", - "quote", - "syn 2.0.117", -] - -[[package]] -name = "tracing-core" -version = "0.1.36" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "db97caf9d906fbde555dd62fa95ddba9eecfd14cb388e4f491a66d74cd5fb79a" -dependencies = [ - "once_cell", - "valuable", -] - -[[package]] -name = "tracing-error" -version = "0.2.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8b1581020d7a273442f5b45074a6a57d5757ad0a47dac0e9f0bd57b81936f3db" -dependencies = [ - "tracing", - "tracing-subscriber", -] - -[[package]] -name = "tracing-futures" -version = "0.2.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "97d095ae15e245a057c8e8451bab9b3ee1e1f68e9ba2b4fbc18d0ac5237835f2" -dependencies = [ - "pin-project", - "tracing", -] - -[[package]] -name = "tracing-journald" -version = "0.3.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2d3a81ed245bfb62592b1e2bc153e77656d94ee6a0497683a65a12ccaf2438d0" -dependencies = [ - "libc", - "tracing-core", - "tracing-subscriber", -] - -[[package]] -name = "tracing-log" -version = "0.2.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ee855f1f400bd0e5c02d150ae5de3840039a3f54b025156404e34c23c03f47c3" -dependencies = [ - "log", - "once_cell", - "tracing-core", -] - -[[package]] -name = "tracing-logfmt" -version = "0.3.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6b1f47d22deb79c3f59fcf2a1f00f60cbdc05462bf17d1cd356c1fefa3f444bd" -dependencies = [ - "time", - "tracing", - "tracing-core", - "tracing-subscriber", -] - -[[package]] -name = "tracing-opentelemetry" -version = "0.32.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1ac28f2d093c6c477eaa76b23525478f38de514fa9aeb1285738d4b97a9552fc" -dependencies = [ - "js-sys", - "opentelemetry", - "smallvec", - "tracing", - "tracing-core", - "tracing-log", - "tracing-subscriber", - "web-time", -] - -[[package]] -name = "tracing-samply" -version = "0.1.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c175f7ecc002b6ef04776a39f440503e4e788790ddbdbfac8259b7a069526334" -dependencies = [ - "cfg-if", - "itoa", - "libc", - "mach2 0.5.0", - "memmap2", - "smallvec", - "tracing-core", - "tracing-subscriber", -] - -[[package]] -name = "tracing-serde" -version = "0.2.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "704b1aeb7be0d0a84fc9828cae51dab5970fee5088f83d1dd7ee6f6246fc6ff1" -dependencies = [ - "serde", - "tracing-core", -] - -[[package]] -name = "tracing-subscriber" -version = "0.3.22" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2f30143827ddab0d256fd843b7a66d164e9f271cfa0dde49142c5ca0ca291f1e" -dependencies = [ - "matchers", - "nu-ansi-term", - "once_cell", - "regex-automata", - "serde", - "serde_json", - "sharded-slab", - "smallvec", - "thread_local", - "tracing", - "tracing-core", - "tracing-log", - "tracing-serde", -] - -[[package]] -name = "tracing-test" -version = "0.2.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "19a4c448db514d4f24c5ddb9f73f2ee71bfb24c526cf0c570ba142d1119e0051" -dependencies = [ - "tracing-core", - "tracing-subscriber", - "tracing-test-macro", -] - -[[package]] -name = "tracing-test-macro" -version = "0.2.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ad06847b7afb65c7866a36664b75c40b895e318cea4f71299f013fb22965329d" -dependencies = [ - "quote", - "syn 2.0.117", -] - -[[package]] -name = "tree_hash" -version = "0.10.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ee44f4cef85f88b4dea21c0b1f58320bdf35715cf56d840969487cff00613321" -dependencies = [ - "alloy-primitives", - "ethereum_hashing", - "ethereum_ssz 0.9.1", - "smallvec", - "typenum", -] - -[[package]] -name = "tree_hash_derive" -version = "0.10.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0bee2ea1551f90040ab0e34b6fb7f2fa3bad8acc925837ac654f2c78a13e3089" -dependencies = [ - "darling 0.20.11", - "proc-macro2", - "quote", - "syn 2.0.117", -] - -[[package]] -name = "triehash" -version = "0.8.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a1631b201eb031b563d2e85ca18ec8092508e262a3196ce9bd10a67ec87b9f5c" -dependencies = [ - "hash-db", - "rlp", -] - -[[package]] -name = "try-lock" -version = "0.2.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e421abadd41a4225275504ea4d6566923418b7f05506fbc9c0fe86ba7396114b" - -[[package]] -name = "tungstenite" -version = "0.26.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4793cb5e56680ecbb1d843515b23b6de9a75eb04b66643e256a396d43be33c13" -dependencies = [ - "bytes", - "data-encoding", - "http", - "httparse", - "log", - "rand 0.9.4", - "rustls", - "rustls-pki-types", - "sha1", - "thiserror 2.0.18", - "utf-8", -] - -[[package]] -name = "tungstenite" -version = "0.28.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8628dcc84e5a09eb3d8423d6cb682965dea9133204e8fb3efee74c2a0c259442" -dependencies = [ - "bytes", - "data-encoding", - "http", - "httparse", - "log", - "rand 0.9.4", - "sha1", - "thiserror 2.0.18", - "utf-8", -] - -[[package]] -name = "typeid" -version = "1.0.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bc7d623258602320d5c55d1bc22793b57daff0ec7efc270ea7d55ce1d5f5471c" - -[[package]] -name = "typenum" -version = "1.20.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "40ce102ab67701b8526c123c1bab5cbe42d7040ccfd0f64af1a385808d2f43de" - -[[package]] -name = "ucd-trie" -version = "0.1.7" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2896d95c02a80c6d6a5d6e953d479f5ddf2dfdb6a244441010e373ac0fb88971" - -[[package]] -name = "uint" -version = "0.9.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "76f64bba2c53b04fcab63c01a7d7427eadc821e3bc48c34dc9ba29c501164b52" -dependencies = [ - "byteorder", - "crunchy", - "hex", - "static_assertions", -] - -[[package]] -name = "uint" -version = "0.10.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "909988d098b2f738727b161a106cfc7cab00c539c2687a8836f8e565976fb53e" -dependencies = [ - "byteorder", - "crunchy", - "hex", - "static_assertions", -] - -[[package]] -name = "unarray" -version = "0.1.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "eaea85b334db583fe3274d12b4cd1880032beab409c0d774be044d4480ab9a94" - -[[package]] -name = "unicase" -version = "2.9.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "dbc4bc3a9f746d862c45cb89d705aa10f187bb96c76001afab07a0d35ce60142" - -[[package]] -name = "unicode-ident" -version = "1.0.24" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e6e4313cd5fcd3dad5cafa179702e2b244f760991f45397d14d4ebf38247da75" - -[[package]] -name = "unicode-normalization" -version = "0.1.25" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5fd4f6878c9cb28d874b009da9e8d183b5abc80117c40bbd187a1fde336be6e8" -dependencies = [ - "tinyvec", -] - -[[package]] -name = "unicode-segmentation" -version = "1.12.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f6ccf251212114b54433ec949fd6a7841275f9ada20dddd2f29e9ceea4501493" - -[[package]] -name = "unicode-truncate" -version = "2.0.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "16b380a1238663e5f8a691f9039c73e1cdae598a30e9855f541d29b08b53e9a5" -dependencies = [ - "itertools 0.14.0", - "unicode-segmentation", - "unicode-width 0.2.2", -] - -[[package]] -name = "unicode-width" -version = "0.1.14" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7dd6e30e90baa6f72411720665d41d89b9a3d039dc45b8faea1ddd07f617f6af" - -[[package]] -name = "unicode-width" -version = "0.2.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b4ac048d71ede7ee76d585517add45da530660ef4390e49b098733c6e897f254" - -[[package]] -name = "unicode-xid" -version = "0.2.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ebc1c04c71510c7f702b52b7c350734c9ff1295c464a03335b00bb84fc54f853" - -[[package]] -name = "universal-hash" -version = "0.5.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fc1de2c688dc15305988b563c3854064043356019f97a4b46276fe734c4f07ea" -dependencies = [ - "crypto-common 0.1.7", - "subtle", -] - -[[package]] -name = "unsigned-varint" -version = "0.7.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6889a77d49f1f013504cec6bf97a2c730394adedaeb1deb5ea08949a50541105" - -[[package]] -name = "unsigned-varint" -version = "0.8.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "eb066959b24b5196ae73cb057f45598450d2c5f71460e98c49b738086eff9c06" -dependencies = [ - "asynchronous-codec", - "bytes", - "tokio-util", -] - -[[package]] -name = "untrusted" -version = "0.9.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8ecb6da28b8a351d773b68d5825ac39017e680750f980f3a1a85cd8dd28a47c1" - -[[package]] -name = "url" -version = "2.5.8" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ff67a8a4397373c3ef660812acab3268222035010ab8680ec4215f38ba3d0eed" -dependencies = [ - "form_urlencoded", - "idna", - "percent-encoding", - "serde", - "serde_derive", -] - -[[package]] -name = "utf-8" -version = "0.7.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "09cc8ee72d2a9becf2f2febe0205bbed8fc6615b7cb429ad062dc7b7ddd036a9" - -[[package]] -name = "utf16_iter" -version = "1.0.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c8232dd3cdaed5356e0f716d285e4b40b932ac434100fe9b7e0e8e935b9e6246" - -[[package]] -name = "utf8_iter" -version = "1.0.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b6c140620e7ffbb22c2dee59cafe6084a59b5ffc27a8859a5f0d494b5d52b6be" - -[[package]] -name = "utf8parse" -version = "0.2.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "06abde3611657adf66d383f00b093d7faecc7fa57071cce2578660c9f1010821" - -[[package]] -name = "uuid" -version = "1.21.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b672338555252d43fd2240c714dc444b8c6fb0a5c5335e65a07bba7742735ddb" -dependencies = [ - "getrandom 0.4.1", - "js-sys", - "wasm-bindgen", -] - -[[package]] -name = "valuable" -version = "0.1.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ba73ea9cf16a25df0c8caa16c51acb937d5712a8429db78a3ee29d5dcacd3a65" - -[[package]] -name = "vcpkg" -version = "0.2.15" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "accd4ea62f7bb7a82fe23066fb0957d48ef677f6eeb8215f372f52e48bb32426" - -[[package]] -name = "vergen" -version = "9.1.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b849a1f6d8639e8de261e81ee0fc881e3e3620db1af9f2e0da015d4382ceaf75" -dependencies = [ - "anyhow", - "cargo_metadata", - "derive_builder", - "regex", - "rustversion", - "time", - "vergen-lib", -] - -[[package]] -name = "vergen-git2" -version = "9.1.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d51ab55ddf1188c8d679f349775362b0fa9e90bd7a4ac69838b2a087623f0d57" -dependencies = [ - "anyhow", - "derive_builder", - "git2", - "rustversion", - "time", - "vergen", - "vergen-lib", -] - -[[package]] -name = "vergen-lib" -version = "9.1.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b34a29ba7e9c59e62f229ae1932fb1b8fb8a6fdcc99215a641913f5f5a59a569" -dependencies = [ - "anyhow", - "derive_builder", - "rustversion", -] - -[[package]] -name = "version_check" -version = "0.9.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0b928f33d975fc6ad9f86c8f283853ad26bdd5b10b7f1542aa2fa15e2289105a" - -[[package]] -name = "visibility" -version = "0.1.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d674d135b4a8c1d7e813e2f8d1c9a58308aee4a680323066025e53132218bd91" -dependencies = [ - "proc-macro2", - "quote", - "syn 2.0.117", -] - -[[package]] -name = "wait-timeout" -version = "0.2.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "09ac3b126d3914f9849036f826e054cbabdc8519970b8998ddaf3b5bd3c65f11" -dependencies = [ - "libc", -] - -[[package]] -name = "walkdir" -version = "2.5.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "29790946404f91d9c5d06f9874efddea1dc06c5efe94541a7d6863108e3a5e4b" -dependencies = [ - "same-file", - "winapi-util", -] - -[[package]] -name = "want" -version = "0.3.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bfa7760aed19e106de2c7c0b581b509f2f25d3dacaf737cb82ac61bc6d760b0e" -dependencies = [ - "try-lock", -] - -[[package]] -name = "wasi" -version = "0.11.1+wasi-snapshot-preview1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ccf3ec651a847eb01de73ccad15eb7d99f80485de043efb2f370cd654f4ea44b" - -[[package]] -name = "wasip2" -version = "1.0.2+wasi-0.2.9" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9517f9239f02c069db75e65f174b3da828fe5f5b945c4dd26bd25d89c03ebcf5" -dependencies = [ - "wit-bindgen", -] - -[[package]] -name = "wasip3" -version = "0.4.0+wasi-0.3.0-rc-2026-01-06" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5428f8bf88ea5ddc08faddef2ac4a67e390b88186c703ce6dbd955e1c145aca5" -dependencies = [ - "wit-bindgen", -] - -[[package]] -name = "wasm-bindgen" -version = "0.2.109" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9ff9c7baef35ac3c0e17d8bfc9ad75eb62f85a2f02bccc906699dadb0aa9c622" -dependencies = [ - "cfg-if", - "once_cell", - "rustversion", - "wasm-bindgen-macro", - "wasm-bindgen-shared", -] - -[[package]] -name = "wasm-bindgen-futures" -version = "0.4.59" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d24699cd39db9966cf6e2ef10d2f72779c961ad905911f395ea201c3ec9f545d" -dependencies = [ - "cfg-if", - "futures-util", - "js-sys", - "once_cell", - "wasm-bindgen", - "web-sys", -] - -[[package]] -name = "wasm-bindgen-macro" -version = "0.2.109" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "39455e84ad887a0bbc93c116d72403f1bb0a39e37dd6f235a43e2128a0c7f1fd" -dependencies = [ - "quote", - "wasm-bindgen-macro-support", -] - -[[package]] -name = "wasm-bindgen-macro-support" -version = "0.2.109" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "dff4761f60b0b51fd13fec8764167b7bbcc34498ce3e52805fe1db6f2d56b6d6" -dependencies = [ - "bumpalo", - "proc-macro2", - "quote", - "syn 2.0.117", - "wasm-bindgen-shared", -] - -[[package]] -name = "wasm-bindgen-shared" -version = "0.2.109" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bc6a171c53d98021a93a474c4a4579d76ba97f9517d871bc12e27640f218b6dd" -dependencies = [ - "unicode-ident", -] - -[[package]] -name = "wasm-encoder" -version = "0.244.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "990065f2fe63003fe337b932cfb5e3b80e0b4d0f5ff650e6985b1048f62c8319" -dependencies = [ - "leb128fmt", - "wasmparser", -] - -[[package]] -name = "wasm-metadata" -version = "0.244.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bb0e353e6a2fbdc176932bbaab493762eb1255a7900fe0fea1a2f96c296cc909" -dependencies = [ - "anyhow", - "indexmap 2.13.0", - "wasm-encoder", - "wasmparser", -] - -[[package]] -name = "wasm-streams" -version = "0.4.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "15053d8d85c7eccdbefef60f06769760a563c7f0a9d6902a13d35c7800b0ad65" -dependencies = [ - "futures-util", - "js-sys", - "wasm-bindgen", - "wasm-bindgen-futures", - "web-sys", -] - -[[package]] -name = "wasmparser" -version = "0.244.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "47b807c72e1bac69382b3a6fb3dbe8ea4c0ed87ff5629b8685ae6b9a611028fe" -dependencies = [ - "bitflags 2.11.0", - "hashbrown 0.15.5", - "indexmap 2.13.0", - "semver 1.0.27", -] - -[[package]] -name = "wasmtimer" -version = "0.4.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1c598d6b99ea013e35844697fc4670d08339d5cda15588f193c6beedd12f644b" -dependencies = [ - "futures", - "js-sys", - "parking_lot", - "pin-utils", - "slab", - "wasm-bindgen", -] - -[[package]] -name = "web-sys" -version = "0.3.86" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "668fa5d00434e890a452ab060d24e3904d1be93f7bb01b70e5603baa2b8ab23b" -dependencies = [ - "js-sys", - "wasm-bindgen", -] - -[[package]] -name = "web-time" -version = "1.1.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5a6580f308b1fad9207618087a65c04e7a10bc77e02c8e84e9b00dd4b12fa0bb" -dependencies = [ - "js-sys", - "wasm-bindgen", -] - -[[package]] -name = "webpki-root-certs" -version = "0.26.11" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "75c7f0ef91146ebfb530314f5f1d24528d7f0767efbfd31dce919275413e393e" -dependencies = [ - "webpki-root-certs 1.0.6", -] - -[[package]] -name = "webpki-root-certs" -version = "1.0.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "804f18a4ac2676ffb4e8b5b5fa9ae38af06df08162314f96a68d2a363e21a8ca" -dependencies = [ - "rustls-pki-types", -] - -[[package]] -name = "webpki-roots" -version = "0.26.11" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "521bc38abb08001b01866da9f51eb7c5d647a19260e00054a8c7fd5f9e57f7a9" -dependencies = [ - "webpki-roots 1.0.6", -] - -[[package]] -name = "webpki-roots" -version = "1.0.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "22cfaf3c063993ff62e73cb4311efde4db1efb31ab78a3e5c457939ad5cc0bed" -dependencies = [ - "rustls-pki-types", -] - -[[package]] -name = "widestring" -version = "1.2.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "72069c3113ab32ab29e5584db3c6ec55d416895e60715417b5b883a357c3e471" - -[[package]] -name = "winapi" -version = "0.3.9" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5c839a674fcd7a98952e593242ea400abe93992746761e38641405d28b00f419" -dependencies = [ - "winapi-i686-pc-windows-gnu", - "winapi-x86_64-pc-windows-gnu", -] - -[[package]] -name = "winapi-i686-pc-windows-gnu" -version = "0.4.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ac3b87c63620426dd9b991e5ce0329eff545bccbbb34f3be09ff6fb6ab51b7b6" - -[[package]] -name = "winapi-util" -version = "0.1.11" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c2a7b1c03c876122aa43f3020e6c3c3ee5c05081c9a00739faf7503aeba10d22" -dependencies = [ - "windows-sys 0.48.0", -] - -[[package]] -name = "winapi-x86_64-pc-windows-gnu" -version = "0.4.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f" - -[[package]] -name = "windows" -version = "0.53.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "efc5cf48f83140dcaab716eeaea345f9e93d0018fb81162753a3f76c3397b538" -dependencies = [ - "windows-core 0.53.0", - "windows-targets 0.52.6", -] - -[[package]] -name = "windows" -version = "0.62.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "527fadee13e0c05939a6a05d5bd6eec6cd2e3dbd648b9f8e447c6518133d8580" -dependencies = [ - "windows-collections", - "windows-core 0.62.2", - "windows-future", - "windows-numerics", -] - -[[package]] -name = "windows-collections" -version = "0.3.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "23b2d95af1a8a14a3c7367e1ed4fc9c20e0a26e79551b1454d72583c97cc6610" -dependencies = [ - "windows-core 0.62.2", -] - -[[package]] -name = "windows-core" -version = "0.53.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9dcc5b895a6377f1ab9fa55acedab1fd5ac0db66ad1e6c7f47e28a22e446a5dd" -dependencies = [ - "windows-result 0.1.2", - "windows-targets 0.52.6", -] - -[[package]] -name = "windows-core" -version = "0.62.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b8e83a14d34d0623b51dce9581199302a221863196a1dde71a7663a4c2be9deb" -dependencies = [ - "windows-implement", - "windows-interface", - "windows-link", - "windows-result 0.4.1", - "windows-strings", -] - -[[package]] -name = "windows-future" -version = "0.3.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e1d6f90251fe18a279739e78025bd6ddc52a7e22f921070ccdc67dde84c605cb" -dependencies = [ - "windows-core 0.62.2", - "windows-link", - "windows-threading", -] - -[[package]] -name = "windows-implement" -version = "0.60.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "053e2e040ab57b9dc951b72c264860db7eb3b0200ba345b4e4c3b14f67855ddf" -dependencies = [ - "proc-macro2", - "quote", - "syn 2.0.117", -] - -[[package]] -name = "windows-interface" -version = "0.59.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3f316c4a2570ba26bbec722032c4099d8c8bc095efccdc15688708623367e358" -dependencies = [ - "proc-macro2", - "quote", - "syn 2.0.117", -] - -[[package]] -name = "windows-link" -version = "0.2.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f0805222e57f7521d6a62e36fa9163bc891acd422f971defe97d64e70d0a4fe5" - -[[package]] -name = "windows-numerics" -version = "0.3.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6e2e40844ac143cdb44aead537bbf727de9b044e107a0f1220392177d15b0f26" -dependencies = [ - "windows-core 0.62.2", - "windows-link", -] - -[[package]] -name = "windows-result" -version = "0.1.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5e383302e8ec8515204254685643de10811af0ed97ea37210dc26fb0032647f8" -dependencies = [ - "windows-targets 0.52.6", -] - -[[package]] -name = "windows-result" -version = "0.4.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7781fa89eaf60850ac3d2da7af8e5242a5ea78d1a11c49bf2910bb5a73853eb5" -dependencies = [ - "windows-link", -] - -[[package]] -name = "windows-strings" -version = "0.5.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7837d08f69c77cf6b07689544538e017c1bfcf57e34b4c0ff58e6c2cd3b37091" -dependencies = [ - "windows-link", -] - -[[package]] -name = "windows-sys" -version = "0.45.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "75283be5efb2831d37ea142365f009c02ec203cd29a3ebecbc093d52315b66d0" -dependencies = [ - "windows-targets 0.42.2", -] - -[[package]] -name = "windows-sys" -version = "0.48.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "677d2418bec65e3338edb076e806bc1ec15693c5d0104683f2efe857f61056a9" -dependencies = [ - "windows-targets 0.48.5", -] - -[[package]] -name = "windows-sys" -version = "0.52.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "282be5f36a8ce781fad8c8ae18fa3f9beff57ec1b52cb3de0789201425d9a33d" -dependencies = [ - "windows-targets 0.52.6", -] - -[[package]] -name = "windows-sys" -version = "0.59.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1e38bc4d79ed67fd075bcc251a1c39b32a1776bbe92e5bef1f0bf1f8c531853b" -dependencies = [ - "windows-targets 0.52.6", -] - -[[package]] -name = "windows-sys" -version = "0.60.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f2f500e4d28234f72040990ec9d39e3a6b950f9f22d3dba18416c35882612bcb" -dependencies = [ - "windows-targets 0.53.5", -] - -[[package]] -name = "windows-sys" -version = "0.61.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ae137229bcbd6cdf0f7b80a31df61766145077ddf49416a728b02cb3921ff3fc" -dependencies = [ - "windows-link", -] - -[[package]] -name = "windows-targets" -version = "0.42.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8e5180c00cd44c9b1c88adb3693291f1cd93605ded80c250a75d472756b4d071" -dependencies = [ - "windows_aarch64_gnullvm 0.42.2", - "windows_aarch64_msvc 0.42.2", - "windows_i686_gnu 0.42.2", - "windows_i686_msvc 0.42.2", - "windows_x86_64_gnu 0.42.2", - "windows_x86_64_gnullvm 0.42.2", - "windows_x86_64_msvc 0.42.2", -] - -[[package]] -name = "windows-targets" -version = "0.48.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9a2fa6e2155d7247be68c096456083145c183cbbbc2764150dda45a87197940c" -dependencies = [ - "windows_aarch64_gnullvm 0.48.5", - "windows_aarch64_msvc 0.48.5", - "windows_i686_gnu 0.48.5", - "windows_i686_msvc 0.48.5", - "windows_x86_64_gnu 0.48.5", - "windows_x86_64_gnullvm 0.48.5", - "windows_x86_64_msvc 0.48.5", -] - -[[package]] -name = "windows-targets" -version = "0.52.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9b724f72796e036ab90c1021d4780d4d3d648aca59e491e6b98e725b84e99973" -dependencies = [ - "windows_aarch64_gnullvm 0.52.6", - "windows_aarch64_msvc 0.52.6", - "windows_i686_gnu 0.52.6", - "windows_i686_gnullvm 0.52.6", - "windows_i686_msvc 0.52.6", - "windows_x86_64_gnu 0.52.6", - "windows_x86_64_gnullvm 0.52.6", - "windows_x86_64_msvc 0.52.6", -] - -[[package]] -name = "windows-targets" -version = "0.53.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4945f9f551b88e0d65f3db0bc25c33b8acea4d9e41163edf90dcd0b19f9069f3" -dependencies = [ - "windows-link", - "windows_aarch64_gnullvm 0.53.1", - "windows_aarch64_msvc 0.53.1", - "windows_i686_gnu 0.53.1", - "windows_i686_gnullvm 0.53.1", - "windows_i686_msvc 0.53.1", - "windows_x86_64_gnu 0.53.1", - "windows_x86_64_gnullvm 0.53.1", - "windows_x86_64_msvc 0.53.1", -] - -[[package]] -name = "windows-threading" -version = "0.2.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3949bd5b99cafdf1c7ca86b43ca564028dfe27d66958f2470940f73d86d75b37" -dependencies = [ - "windows-link", -] - -[[package]] -name = "windows_aarch64_gnullvm" -version = "0.42.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "597a5118570b68bc08d8d59125332c54f1ba9d9adeedeef5b99b02ba2b0698f8" - -[[package]] -name = "windows_aarch64_gnullvm" -version = "0.48.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2b38e32f0abccf9987a4e3079dfb67dcd799fb61361e53e2882c3cbaf0d905d8" - -[[package]] -name = "windows_aarch64_gnullvm" -version = "0.52.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "32a4622180e7a0ec044bb555404c800bc9fd9ec262ec147edd5989ccd0c02cd3" - -[[package]] -name = "windows_aarch64_gnullvm" -version = "0.53.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a9d8416fa8b42f5c947f8482c43e7d89e73a173cead56d044f6a56104a6d1b53" - -[[package]] -name = "windows_aarch64_msvc" -version = "0.42.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e08e8864a60f06ef0d0ff4ba04124db8b0fb3be5776a5cd47641e942e58c4d43" - -[[package]] -name = "windows_aarch64_msvc" -version = "0.48.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "dc35310971f3b2dbbf3f0690a219f40e2d9afcf64f9ab7cc1be722937c26b4bc" - -[[package]] -name = "windows_aarch64_msvc" -version = "0.52.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "09ec2a7bb152e2252b53fa7803150007879548bc709c039df7627cabbd05d469" - -[[package]] -name = "windows_aarch64_msvc" -version = "0.53.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b9d782e804c2f632e395708e99a94275910eb9100b2114651e04744e9b125006" - -[[package]] -name = "windows_i686_gnu" -version = "0.42.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c61d927d8da41da96a81f029489353e68739737d3beca43145c8afec9a31a84f" - -[[package]] -name = "windows_i686_gnu" -version = "0.48.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a75915e7def60c94dcef72200b9a8e58e5091744960da64ec734a6c6e9b3743e" - -[[package]] -name = "windows_i686_gnu" -version = "0.52.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8e9b5ad5ab802e97eb8e295ac6720e509ee4c243f69d781394014ebfe8bbfa0b" - -[[package]] -name = "windows_i686_gnu" -version = "0.53.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "960e6da069d81e09becb0ca57a65220ddff016ff2d6af6a223cf372a506593a3" - -[[package]] -name = "windows_i686_gnullvm" -version = "0.52.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0eee52d38c090b3caa76c563b86c3a4bd71ef1a819287c19d586d7334ae8ed66" - -[[package]] -name = "windows_i686_gnullvm" -version = "0.53.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fa7359d10048f68ab8b09fa71c3daccfb0e9b559aed648a8f95469c27057180c" - -[[package]] -name = "windows_i686_msvc" -version = "0.42.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "44d840b6ec649f480a41c8d80f9c65108b92d89345dd94027bfe06ac444d1060" - -[[package]] -name = "windows_i686_msvc" -version = "0.48.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8f55c233f70c4b27f66c523580f78f1004e8b5a8b659e05a4eb49d4166cca406" - -[[package]] -name = "windows_i686_msvc" -version = "0.52.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "240948bc05c5e7c6dabba28bf89d89ffce3e303022809e73deaefe4f6ec56c66" - -[[package]] -name = "windows_i686_msvc" -version = "0.53.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1e7ac75179f18232fe9c285163565a57ef8d3c89254a30685b57d83a38d326c2" - -[[package]] -name = "windows_x86_64_gnu" -version = "0.42.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8de912b8b8feb55c064867cf047dda097f92d51efad5b491dfb98f6bbb70cb36" - -[[package]] -name = "windows_x86_64_gnu" -version = "0.48.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "53d40abd2583d23e4718fddf1ebec84dbff8381c07cae67ff7768bbf19c6718e" - -[[package]] -name = "windows_x86_64_gnu" -version = "0.52.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "147a5c80aabfbf0c7d901cb5895d1de30ef2907eb21fbbab29ca94c5b08b1a78" - -[[package]] -name = "windows_x86_64_gnu" -version = "0.53.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9c3842cdd74a865a8066ab39c8a7a473c0778a3f29370b5fd6b4b9aa7df4a499" - -[[package]] -name = "windows_x86_64_gnullvm" -version = "0.42.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "26d41b46a36d453748aedef1486d5c7a85db22e56aff34643984ea85514e94a3" - -[[package]] -name = "windows_x86_64_gnullvm" -version = "0.48.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0b7b52767868a23d5bab768e390dc5f5c55825b6d30b86c844ff2dc7414044cc" - -[[package]] -name = "windows_x86_64_gnullvm" -version = "0.52.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "24d5b23dc417412679681396f2b49f3de8c1473deb516bd34410872eff51ed0d" - -[[package]] -name = "windows_x86_64_gnullvm" -version = "0.53.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0ffa179e2d07eee8ad8f57493436566c7cc30ac536a3379fdf008f47f6bb7ae1" - -[[package]] -name = "windows_x86_64_msvc" -version = "0.42.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9aec5da331524158c6d1a4ac0ab1541149c0b9505fde06423b02f5ef0106b9f0" - -[[package]] -name = "windows_x86_64_msvc" -version = "0.48.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ed94fce61571a4006852b7389a063ab983c02eb1bb37b47f8272ce92d06d9538" - -[[package]] -name = "windows_x86_64_msvc" -version = "0.52.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "589f6da84c646204747d1270a2a5661ea66ed1cced2631d546fdfb155959f9ec" - -[[package]] -name = "windows_x86_64_msvc" -version = "0.53.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d6bbff5f0aada427a1e5a6da5f1f98158182f26556f345ac9e04d36d0ebed650" - -[[package]] -name = "winnow" -version = "0.7.14" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5a5364e9d77fcdeeaa6062ced926ee3381faa2ee02d3eb83a5c27a8825540829" -dependencies = [ - "memchr", -] - -[[package]] -name = "winreg" -version = "0.50.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "524e57b2c537c0f9b1e69f1965311ec12182b4122e45035b1508cd24d2adadb1" -dependencies = [ - "cfg-if", - "windows-sys 0.48.0", -] - -[[package]] -name = "wiremock" -version = "0.6.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "08db1edfb05d9b3c1542e521aea074442088292f00b5f28e435c714a98f85031" -dependencies = [ - "assert-json-diff", - "base64 0.22.1", - "deadpool", - "futures", - "http", - "http-body-util", - "hyper", - "hyper-util", - "log", - "once_cell", - "regex", - "serde", - "serde_json", - "tokio", - "url", -] - -[[package]] -name = "wit-bindgen" -version = "0.51.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d7249219f66ced02969388cf2bb044a09756a083d0fab1e566056b04d9fbcaa5" -dependencies = [ - "wit-bindgen-rust-macro", -] - -[[package]] -name = "wit-bindgen-core" -version = "0.51.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ea61de684c3ea68cb082b7a88508a8b27fcc8b797d738bfc99a82facf1d752dc" -dependencies = [ - "anyhow", - "heck", - "wit-parser", -] - -[[package]] -name = "wit-bindgen-rust" -version = "0.51.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b7c566e0f4b284dd6561c786d9cb0142da491f46a9fbed79ea69cdad5db17f21" -dependencies = [ - "anyhow", - "heck", - "indexmap 2.13.0", - "prettyplease", - "syn 2.0.117", - "wasm-metadata", - "wit-bindgen-core", - "wit-component", -] - -[[package]] -name = "wit-bindgen-rust-macro" -version = "0.51.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0c0f9bfd77e6a48eccf51359e3ae77140a7f50b1e2ebfe62422d8afdaffab17a" -dependencies = [ - "anyhow", - "prettyplease", - "proc-macro2", - "quote", - "syn 2.0.117", - "wit-bindgen-core", - "wit-bindgen-rust", -] - -[[package]] -name = "wit-component" -version = "0.244.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9d66ea20e9553b30172b5e831994e35fbde2d165325bec84fc43dbf6f4eb9cb2" -dependencies = [ - "anyhow", - "bitflags 2.11.0", - "indexmap 2.13.0", - "log", - "serde", - "serde_derive", - "serde_json", - "wasm-encoder", - "wasm-metadata", - "wasmparser", - "wit-parser", -] - -[[package]] -name = "wit-parser" -version = "0.244.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ecc8ac4bc1dc3381b7f59c34f00b67e18f910c2c0f50015669dde7def656a736" -dependencies = [ - "anyhow", - "id-arena", - "indexmap 2.13.0", - "log", - "semver 1.0.27", - "serde", - "serde_derive", - "serde_json", - "unicode-xid", - "wasmparser", -] - -[[package]] -name = "write16" -version = "1.0.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d1890f4022759daae28ed4fe62859b1236caebfc61ede2f63ed4e695f3f6d936" - -[[package]] -name = "writeable" -version = "0.6.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9edde0db4769d2dc68579893f2306b26c6ecfbe0ef499b013d731b7b9247e0b9" - -[[package]] -name = "ws_stream_wasm" -version = "0.7.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6c173014acad22e83f16403ee360115b38846fe754e735c5d9d3803fe70c6abc" -dependencies = [ - "async_io_stream", - "futures", - "js-sys", - "log", - "pharos", - "rustc_version 0.4.1", - "send_wrapper 0.6.0", - "thiserror 2.0.18", - "wasm-bindgen", - "wasm-bindgen-futures", - "web-sys", -] - -[[package]] -name = "wyz" -version = "0.5.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "05f360fc0b24296329c78fda852a1e9ae82de9cf7b27dae4b7f62f118f77b9ed" -dependencies = [ - "tap", -] - -[[package]] -name = "x25519-dalek" -version = "2.0.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c7e468321c81fb07fa7f4c636c3972b9100f0346e5b6a9f2bd0603a52f7ed277" -dependencies = [ - "curve25519-dalek", - "rand_core 0.6.4", - "serde", - "zeroize", -] - -[[package]] -name = "x509-parser" -version = "0.17.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4569f339c0c402346d4a75a9e39cf8dad310e287eef1ff56d4c68e5067f53460" -dependencies = [ - "asn1-rs", - "data-encoding", - "der-parser", - "lazy_static", - "nom", - "oid-registry", - "rusticata-macros", - "thiserror 2.0.18", - "time", -] - -[[package]] -name = "xattr" -version = "1.6.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "32e45ad4206f6d2479085147f02bc2ef834ac85886624a23575ae137c8aa8156" -dependencies = [ - "libc", - "rustix 1.1.3", -] - -[[package]] -name = "xml-rs" -version = "0.8.28" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3ae8337f8a065cfc972643663ea4279e04e7256de865aa66fe25cec5fb912d3f" - -[[package]] -name = "xmltree" -version = "0.10.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d7d8a75eaf6557bb84a65ace8609883db44a29951042ada9b393151532e41fcb" -dependencies = [ - "xml-rs", -] - -[[package]] -name = "xsum" -version = "0.1.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0637d3a5566a82fa5214bae89087bc8c9fb94cd8e8a3c07feb691bb8d9c632db" - -[[package]] -name = "yamux" -version = "0.12.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9ed0164ae619f2dc144909a9f082187ebb5893693d8c0196e8085283ccd4b776" -dependencies = [ - "futures", - "log", - "nohash-hasher", - "parking_lot", - "pin-project", - "rand 0.8.5", - "static_assertions", -] - -[[package]] -name = "yamux" -version = "0.13.8" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "deab71f2e20691b4728b349c6cee8fc7223880fa67b6b4f92225ec32225447e5" -dependencies = [ - "futures", - "log", - "nohash-hasher", - "parking_lot", - "pin-project", - "rand 0.9.4", - "static_assertions", - "web-time", -] - -[[package]] -name = "yansi" -version = "1.0.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cfe53a6657fd280eaa890a3bc59152892ffa3e30101319d168b781ed6529b049" - -[[package]] -name = "yasna" -version = "0.5.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e17bb3549cc1321ae1296b9cdc2698e2b6cb1992adfa19a8c72e5b7a738f44cd" -dependencies = [ - "time", -] - -[[package]] -name = "yoke" -version = "0.8.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "72d6e5c6afb84d73944e5cedb052c4680d5657337201555f9f2a16b7406d4954" -dependencies = [ - "stable_deref_trait", - "yoke-derive", - "zerofrom", -] - -[[package]] -name = "yoke-derive" -version = "0.8.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b659052874eb698efe5b9e8cf382204678a0086ebf46982b79d6ca3182927e5d" -dependencies = [ - "proc-macro2", - "quote", - "syn 2.0.117", - "synstructure", -] - -[[package]] -name = "zerocopy" -version = "0.8.39" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "db6d35d663eadb6c932438e763b262fe1a70987f9ae936e60158176d710cae4a" -dependencies = [ - "zerocopy-derive", -] - -[[package]] -name = "zerocopy-derive" -version = "0.8.39" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4122cd3169e94605190e77839c9a40d40ed048d305bfdc146e7df40ab0f3e517" -dependencies = [ - "proc-macro2", - "quote", - "syn 2.0.117", -] - -[[package]] -name = "zerofrom" -version = "0.1.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "50cc42e0333e05660c3587f3bf9d0478688e15d870fab3346451ce7f8c9fbea5" -dependencies = [ - "zerofrom-derive", -] - -[[package]] -name = "zerofrom-derive" -version = "0.1.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d71e5d6e06ab090c67b5e44993ec16b72dcbaabc526db883a360057678b48502" -dependencies = [ - "proc-macro2", - "quote", - "syn 2.0.117", - "synstructure", -] - -[[package]] -name = "zeroize" -version = "1.8.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b97154e67e32c85465826e8bcc1c59429aaaf107c1e4a9e53c8d8ccd5eff88d0" -dependencies = [ - "zeroize_derive", -] - -[[package]] -name = "zeroize_derive" -version = "1.4.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "85a5b4158499876c763cb03bc4e49185d3cccbabb15b33c627f7884f43db852e" -dependencies = [ - "proc-macro2", - "quote", - "syn 2.0.117", -] - -[[package]] -name = "zerotrie" -version = "0.2.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2a59c17a5562d507e4b54960e8569ebee33bee890c70aa3fe7b97e85a9fd7851" -dependencies = [ - "displaydoc", - "yoke", - "zerofrom", -] - -[[package]] -name = "zerovec" -version = "0.11.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6c28719294829477f525be0186d13efa9a3c602f7ec202ca9e353d310fb9a002" -dependencies = [ - "serde", - "yoke", - "zerofrom", - "zerovec-derive", -] - -[[package]] -name = "zerovec-derive" -version = "0.11.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "eadce39539ca5cb3985590102671f2567e659fca9666581ad3411d59207951f3" -dependencies = [ - "proc-macro2", - "quote", - "syn 2.0.117", -] - -[[package]] -name = "zmij" -version = "1.0.21" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b8848ee67ecc8aedbaf3e4122217aff892639231befc6a1b58d29fff4c2cabaa" - -[[package]] -name = "zstd" -version = "0.13.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e91ee311a569c327171651566e07972200e76fcfe2242a4fa446149a3881c08a" -dependencies = [ - "zstd-safe", -] - -[[package]] -name = "zstd-safe" -version = "7.2.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8f49c4d5f0abb602a93fb8736af2a4f4dd9512e36f7f570d66e65ff867ed3b9d" -dependencies = [ - "zstd-sys", -] - -[[package]] -name = "zstd-sys" -version = "2.0.16+zstd.1.5.7" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "91e19ebc2adc8f83e43039e79776e3fda8ca919132d68a1fed6a5faca2683748" -dependencies = [ - "cc", - "pkg-config", -] diff --git a/Cargo.toml b/Cargo.toml index f4e7ac8..72bbdc6 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,359 +1,10 @@ [workspace] resolver = "2" - -members = [ - "crates/*", - "crates/test/checks", - "crates/test/framework", - "crates/test/integration", -] -exclude = ["crates/test"] +members = ["crates/*"] [workspace.package] -version = "0.0.1" -edition = "2024" -readme = "README.md" -license = "Apache-2.0" -exclude = [".github/"] -rust-version = "1.91" -publish = false -repository = "https://github.com/circlefin/arc-node" - -[workspace.lints.rust] -unexpected_cfgs = { level = "warn", check-cfg = ['cfg(coverage_nightly)'] } - -[workspace.lints.clippy] -arithmetic_side_effects = "deny" -cast_possible_truncation = "deny" -collapsible_if = "allow" -unwrap_used = "deny" - -[workspace.dependencies] - -# eth -alloy-consensus = { version = "1.6.3", default-features = false } -alloy-eips = { version = "1.6.3", default-features = false } -alloy-evm = { version = "0.27.2", default-features = false } -alloy-genesis = { version = "1.6.3", default-features = false } - -alloy-network = { version = "1.6.3", default-features = false } - -# op -alloy-primitives = { version = "1.5.6", default-features = false, features = ["map-foldhash", "keccak-cache-global", "asm-keccak"] } -alloy-provider = { version = "1.6.3", features = ["reqwest"], default-features = false } -alloy-rlp = { version = "0.3.13", default-features = false, features = ["core-net"] } -alloy-rpc-types = { version = "1.6.3", features = ["eth"], default-features = false } -alloy-rpc-types-admin = { version = "1.6.3", default-features = false } -alloy-rpc-types-engine = { version = "1.6.3", default-features = false } -alloy-rpc-types-eth = { version = "1.6.3", default-features = false } -alloy-rpc-types-trace = { version = "1.6.3", default-features = false } -alloy-rpc-types-txpool = { version = "1.6.3", default-features = false } -alloy-serde = { version = "1.6.3", default-features = false } -alloy-signer = { version = "1.6.3", default-features = false } -alloy-signer-local = { version = "1.6.3", default-features = false, features = ["mnemonic"] } -alloy-sol-macro = "1.5.6" -alloy-sol-types = { version = "1.5.6", default-features = false } -alloy-transport-ws = { version = "1.6.3", default-features = false } -alloy-trie = { version = "0.9.4", default-features = false } - -arbitrary = "1.3" - -# internal crates -arc-checks = { version = "0.0.1", path = "crates/test/checks" } -arc-consensus-db = { version = "0.0.1", path = "crates/consensus-db" } -arc-consensus-types = { version = "0.0.1", path = "crates/types" } -arc-eth-engine = { version = "0.0.1", path = "crates/eth-engine" } -arc-evm = { version = "0.0.1", path = "crates/evm" } -arc-evm-node = { version = "0.0.1", path = "crates/evm-node" } -arc-execution-config = { version = "0.0.1", path = "crates/execution-config" } -arc-execution-payload = { version = "0.0.1", path = "crates/execution-payload" } -arc-execution-txpool = { version = "0.0.1", path = "crates/execution-txpool" } -arc-execution-validation = { version = "0.0.1", path = "crates/execution-validation" } -arc-mesh-analysis = { version = "0.0.1", path = "crates/mesh-analysis" } -arc-node-consensus = { version = "0.0.1", path = "crates/malachite-app" } -arc-node-consensus-cli = { version = "0.0.1", path = "crates/malachite-cli" } -arc-node-execution = { version = "0.0.1", path = "crates/node" } -arc-precompiles = { version = "0.0.1", path = "crates/precompiles" } -arc-remote-signer = { version = "0.0.1", path = "crates/remote-signer" } -arc-shared = { version = "0.0.1", path = "crates/shared" } -arc-signer = { version = "0.0.1", path = "crates/signer" } -arc-snapshots = { version = "0.0.1", path = "crates/snapshots" } -arc-test-framework = { version = "0.0.1", path = "crates/test/framework" } -arc-test-integration = { version = "0.0.1", path = "crates/test/integration" } -arc-version = { version = "0.0.1", path = "crates/version" } - -# async -async-trait = "0.1.68" -atomic-time = "0.1" -axum = "0.8" -backon = { version = "1.5", default-features = false, features = ["std-blocking-sleep", "tokio-sleep"] } -base64 = "0.22.0" -bip32 = { version = "0.5.3", default-features = false, features = ["secp256k1", "std"] } -bip39 = { version = "2.2.0", default-features = false, features = ["std"] } - -# arc -bon = "2.3.0" -bytes = { version = "1.5", default-features = false } -bytesize = "2.3" - -# for eip-4844 -cc = "=1.2.15" -chrono = "0.4.44" -clap = "4" -clap-verbosity-flag = { version = "3.0.4", features = ["tracing"] } -color-eyre = "0.6" -config = { version = "0.14", features = ["toml"], default-features = false } -# TODO: When we build for a windows target on an ubuntu runner, crunchy tries to -# get the wrong path, update this when the workflow has been updated -# -# See: https://github.com/eira-fransham/crunchy/issues/13 -criterion = "0.7" -crunchy = "=0.2.2" -csv = "1.4" -deranged = "0.5.5" -directories = "5.0.1" - -# p2p -dotenvy = "0.15" -ed25519-dalek = "2.0" -# misc -# crypto - -# ssz encoding -ethereum_ssz = "0.9.0" -ethereum_ssz_derive = "0.9.0" -eyre = "0.6" -futures = "0.3" -futures-util = { version = "0.3", default-features = false } -hash-db = "=0.15.2" -hex = { version = "0.4.3", features = ["serde"] } - -# http -humantime = "2.1" -humantime-serde = "1.1" -indexmap = "2" -itertools = "0.14" - -# rpc -jsonrpsee = "0.26.0" -jsonrpsee-types = "0.26.0" -jsonwebtoken = "9.3.1" -k256 = { version = "0.13", default-features = false, features = ["ecdsa"] } -lz4 = "1.28.1" - -# metrics -metrics = "0.24.0" -mockall = "0.14.0" -multihash = "0.19.3" -once_cell = { version = "1.19", default-features = false, features = ["critical-section"] } -oneline-eyre = "0.1" - -# reth -parking_lot = "0.12" -plain_hasher = "0.2" - -# proc-macros -proc-macro2 = "1.0" -prometheus-client = "0.23" -prometheus-parse = "0.2.5" -proptest = "1.7" -prost = "0.13" -prost-build = "0.13" -protox = "0.8.0" -quote = "1.0" -ractor = { version = "0.15.10", default-features = false, features = ["async-trait", "tokio_runtime"] } -#FIXME rand = "0.9" -rand = { version = "0.8.5", features = ["std_rng", "small_rng"] } -# rand 8 for secp256k1 -redb = "2.6.0" -reqwest = { version = "0.12", default-features = false } -reth-basic-payload-builder = { git = "https://github.com/paradigmxyz/reth", tag = "v1.11.3" } -reth-chainspec = { git = "https://github.com/paradigmxyz/reth", tag = "v1.11.3", default-features = false } -reth-cli = { git = "https://github.com/paradigmxyz/reth", tag = "v1.11.3" } -reth-cli-commands = { git = "https://github.com/paradigmxyz/reth", tag = "v1.11.3" } -reth-consensus = { git = "https://github.com/paradigmxyz/reth", tag = "v1.11.3", default-features = false } -reth-consensus-common = { git = "https://github.com/paradigmxyz/reth", tag = "v1.11.3", default-features = false } -reth-db = { git = "https://github.com/paradigmxyz/reth", tag = "v1.11.3", default-features = false } -reth-e2e-test-utils = { git = "https://github.com/paradigmxyz/reth", tag = "v1.11.3" } -reth-engine-local = { git = "https://github.com/paradigmxyz/reth", tag = "v1.11.3" } -reth-engine-primitives = { git = "https://github.com/paradigmxyz/reth", tag = "v1.11.3", default-features = false } -reth-errors = { git = "https://github.com/paradigmxyz/reth", tag = "v1.11.3" } -reth-ethereum = { git = "https://github.com/paradigmxyz/reth", tag = "v1.11.3" } -reth-ethereum-engine-primitives = { git = "https://github.com/paradigmxyz/reth", tag = "v1.11.3", default-features = false } -reth-ethereum-forks = { git = "https://github.com/paradigmxyz/reth", tag = "v1.11.3", default-features = false } -reth-ethereum-payload-builder = { git = "https://github.com/paradigmxyz/reth", tag = "v1.11.3" } -reth-ethereum-primitives = { git = "https://github.com/paradigmxyz/reth", tag = "v1.11.3", default-features = false } -reth-evm = { git = "https://github.com/paradigmxyz/reth", tag = "v1.11.3", default-features = false } -reth-evm-ethereum = { git = "https://github.com/paradigmxyz/reth", tag = "v1.11.3", default-features = false } -reth-ipc = { git = "https://github.com/paradigmxyz/reth", tag = "v1.11.3" } -reth-network = { git = "https://github.com/paradigmxyz/reth", tag = "v1.11.3" } -reth-network-peers = { git = "https://github.com/paradigmxyz/reth", tag = "v1.11.3", default-features = false } -reth-node-api = { git = "https://github.com/paradigmxyz/reth", tag = "v1.11.3" } -reth-node-builder = { git = "https://github.com/paradigmxyz/reth", tag = "v1.11.3" } -reth-node-core = { git = "https://github.com/paradigmxyz/reth", tag = "v1.11.3" } -reth-node-ethereum = { git = "https://github.com/paradigmxyz/reth", tag = "v1.11.3" } -reth-node-metrics = { git = "https://github.com/paradigmxyz/reth", tag = "v1.11.3" } -reth-payload-builder = { git = "https://github.com/paradigmxyz/reth", tag = "v1.11.3" } -reth-payload-primitives = { git = "https://github.com/paradigmxyz/reth", tag = "v1.11.3" } -reth-primitives-traits = { git = "https://github.com/paradigmxyz/reth", tag = "v1.11.3", default-features = false } -reth-provider = { git = "https://github.com/paradigmxyz/reth", tag = "v1.11.3" } -reth-prune-types = { git = "https://github.com/paradigmxyz/reth", tag = "v1.11.3" } -reth-revm = { git = "https://github.com/paradigmxyz/reth", tag = "v1.11.3", default-features = false } -reth-rpc = { git = "https://github.com/paradigmxyz/reth", tag = "v1.11.3" } -reth-rpc-api = { git = "https://github.com/paradigmxyz/reth", tag = "v1.11.3" } -reth-rpc-builder = { git = "https://github.com/paradigmxyz/reth", tag = "v1.11.3" } -reth-rpc-eth-api = { git = "https://github.com/paradigmxyz/reth", tag = "v1.11.3" } -reth-rpc-eth-types = { git = "https://github.com/paradigmxyz/reth", tag = "v1.11.3", default-features = false } -reth-rpc-server-types = { git = "https://github.com/paradigmxyz/reth", tag = "v1.11.3" } -reth-storage-api = { git = "https://github.com/paradigmxyz/reth", tag = "v1.11.3", default-features = false } -reth-tasks = { git = "https://github.com/paradigmxyz/reth", tag = "v1.11.3" } -reth-tracing = { git = "https://github.com/paradigmxyz/reth", tag = "v1.11.3" } -reth-transaction-pool = { git = "https://github.com/paradigmxyz/reth", tag = "v1.11.3" } - -# revm -revm = { version = "34.0.0", default-features = false } -revm-bytecode = { version = "8.0.0", default-features = false } -revm-context-interface = { version = "14.0.0", default-features = false } -revm-database = { version = "10.0.0", default-features = false } -revm-inspector = { version = "15.0.0", default-features = false } -revm-inspectors = "0.34.2" -revm-interpreter = { version = "32.0.0", default-features = false } -revm-primitives = { version = "22.0.0", default-features = false } -revm-statetest-types = { version = "14.0.0", default-features = false } - -rmcp = { version = "0.16", features = ["server", "macros", "transport-io", "transport-streamable-http-server"] } -rstest = "0.24.0" -schemars = "1.0" -schnellru = "0.2" -secp256k1 = { version = "0.30", default-features = false, features = ["global-context", "recovery"] } -serde = { version = "1.0", default-features = false } -serde_json = { version = "1.0", default-features = false, features = ["alloc"] } -serde_with = { version = "3", default-features = false, features = ["macros"] } -serial_test = "3" -sha2 = "0.10.9" -sha3 = "0.10.5" -signature = "2.2.0" -# Security fix for RUSTSEC-2025-0047 -slab = "0.4.11" -slh-dsa = "0.2.0-rc.5" - -# spammer -spammer = { version = "0.0.1", path = "crates/spammer" } - -strum = { version = "0.27", default-features = false } -strum_macros = "0.27" -syn = "2.0" -tar = "0.4" -tempfile = "3.20" -thiserror = { version = "2.0.0", default-features = false } - -# tokio -tokio = { version = "1.44.2", default-features = false } -tokio-tungstenite = { version = "0.26.2", features = ["rustls-tls-native-roots"] } -tokio-util = { version = "0.7.4", features = ["codec"] } - -# config -toml = "0.8" -tonic = "0.12" -tonic-build = "0.12" -tower = "0.5" -tracing = { version = "0.1.0", default-features = false } -tracing-appender = "0.2" -tracing-subscriber = { version = "0.3", default-features = false, features = ["ansi"] } -triehash = "0.8" -url = { version = "2.3", default-features = false } -uuid = { version = "1", default-features = false, features = ["v4"] } -vergen-git2 = { version = "9.1.0", default-features = false } -wiremock = "0.6" - -[workspace.dependencies.malachitebft-app] -package = "arc-malachitebft-app" -git = "https://github.com/circlefin/malachite.git" -rev = "8ee5d998" - -[workspace.dependencies.malachitebft-app-channel] -package = "arc-malachitebft-app-channel" -git = "https://github.com/circlefin/malachite.git" -rev = "8ee5d998" - -[workspace.dependencies.malachitebft-codec] -package = "arc-malachitebft-codec" -git = "https://github.com/circlefin/malachite.git" -rev = "8ee5d998" - -[workspace.dependencies.malachitebft-config] -package = "arc-malachitebft-config" -git = "https://github.com/circlefin/malachite.git" -rev = "8ee5d998" - -[workspace.dependencies.malachitebft-core-consensus] -package = "arc-malachitebft-core-consensus" -git = "https://github.com/circlefin/malachite.git" -rev = "8ee5d998" - -[workspace.dependencies.malachitebft-core-state-machine] -package = "arc-malachitebft-core-state-machine" -git = "https://github.com/circlefin/malachite.git" -rev = "8ee5d998" - -[workspace.dependencies.malachitebft-core-types] -package = "arc-malachitebft-core-types" -git = "https://github.com/circlefin/malachite.git" -rev = "8ee5d998" - -[workspace.dependencies.malachitebft-network] -package = "arc-malachitebft-network" -git = "https://github.com/circlefin/malachite.git" -rev = "8ee5d998" - -[workspace.dependencies.malachitebft-peer] -package = "arc-malachitebft-peer" -git = "https://github.com/circlefin/malachite.git" -rev = "8ee5d998" - -[workspace.dependencies.malachitebft-proto] -package = "arc-malachitebft-proto" -git = "https://github.com/circlefin/malachite.git" -rev = "8ee5d998" - -[workspace.dependencies.malachitebft-signing] -package = "arc-malachitebft-signing" -git = "https://github.com/circlefin/malachite.git" -rev = "8ee5d998" - -[workspace.dependencies.malachitebft-signing-ed25519] -package = "arc-malachitebft-signing-ed25519" -git = "https://github.com/circlefin/malachite.git" -rev = "8ee5d998" - -[workspace.dependencies.malachitebft-sync] -package = "arc-malachitebft-sync" -git = "https://github.com/circlefin/malachite.git" -rev = "8ee5d998" - -[profile.dev] -# enable basic optimizations (otherwise reth can't process txs) -opt-level = 1 - -[profile.dev.package."*"] -# Compile all dependencies (but not workspace crates) with higher optimization. -# This significantly speeds up test execution for compute-heavy deps (crypto, -# hashing, EVM) while keeping workspace crate rebuild times fast for development. -opt-level = 2 - -[profile.release] -lto = "thin" -opt-level = 3 # maximize speed; use "z" if minimizing size -codegen-units = 1 -strip = true - -[profile.profiling] -inherits = "release" -debug = true -strip = false - -[patch.crates-io] -# Fix for https://rustsec.org/advisories/RUSTSEC-2025-0055 -# This version of `ark-relations` includes a fix for the issue, -# as it bumped `tracing-subscriber` to v0.3.0. -ark-relations = { git = "https://github.com/arkworks-rs/snark.git", rev = "9c528529763f1a0a2e0cba83528f93d32247d621" } +version = "0.0.0-placeholder" +edition = "2021" +license = "Apache-2.0" +repository = "https://github.com/circlefin/arc-node" +rust-version = "1.76" diff --git a/Makefile b/Makefile deleted file mode 100644 index f7c4892..0000000 --- a/Makefile +++ /dev/null @@ -1,262 +0,0 @@ -# Test configuration -UNIT_TEST_ARGS := --locked --workspace -COV_FILE := target/lcov.info -SCRIPTS="./scripts" -FOUNDRY_VERSION := $(shell cat .foundry-version) -QUAKE_MANIFEST ?= crates/quake/scenarios/localdev.toml -# Recursively expanded so smoke targets that override QUAKE_MANIFEST re-evaluate -# at recipe time. All localdev* scenarios must keep the same validator count -# (5) so `make smoke` produces a single genesis that matches both sub-targets. -NUM_VALIDATORS = $(shell grep -c '^\[nodes\.validator' $(QUAKE_MANIFEST) 2>/dev/null || echo 5) -QUAKE := cargo run --bin quake -- -DEFAULT_BRANCH ?= $(shell git symbolic-ref refs/remotes/origin/HEAD 2>/dev/null | sed 's@^refs/remotes/origin/@@') -ifeq ($(DEFAULT_BRANCH),) -DEFAULT_BRANCH = main -endif - -##@ Help -.PHONY: help -help: ## Display this help message - @awk 'BEGIN {FS = ":.*##"; printf "\nUsage:\n make \033[36m\033[0m\n"} /^[a-zA-Z_-]+:.*?##/ { printf " \033[36m%-15s\033[0m %s\n", $$1, $$2 } /^##@/ { printf "\n\033[1m%s\033[0m\n", substr($$0, 5) } ' $(MAKEFILE_LIST) - -.DEFAULT_GOAL := help - -##@ Development -.PHONY: check-foundry -check-foundry: ## Check Foundry version - @INSTALLED=$$(forge --version 2>/dev/null | grep -oE '[0-9]+\.[0-9]+\.[0-9]+' | head -1); \ - REQUIRED=$(FOUNDRY_VERSION:v%=%); \ - if [ "$$INSTALLED" != "$$REQUIRED" ]; then \ - echo "Error: Foundry $(FOUNDRY_VERSION) required. Run: foundryup -i $(FOUNDRY_VERSION)"; \ - exit 1; \ - fi - -.PHONY: fmt -fmt: ## Format code using rustfmt - cargo fmt - npx prettier --config ./.prettierrc --write '**/*.{ts,js,mts,mjs}' - -.PHONY: clippy -clippy: ## Run clippy lints - cargo clippy \ - --workspace \ - --lib \ - --examples \ - --tests \ - --benches \ - --all-features \ - -- -D warnings - -.PHONY: buf-lint -buf-lint: ## Lint protobuf files using buf - buf lint - -.PHONY: buf-format -buf-format: ## Format protobuf files using buf - buf format -w - -.PHONY: buf-breaking -buf-breaking: ## Check for breaking changes in protobuf files - buf breaking --against '.git#branch=$(DEFAULT_BRANCH)' - -.PHONY: lint -lint: fmt buf-format build-contract ## Run formatting and linting - $(MAKE) clippy - npx eslint - ! grep -R 'it.only' tests/ -q # check there are no it.only in testing scripts - -##@ Build -.PHONY: build -build: ## Build the reth binary into `target` directory - @echo building... - cargo build - -.PHONY: build-quake -build-quake: ## Build quake binary - cargo build --bin quake - -.PHONY: build-docker -build-docker: ## Build Docker images for integration stack - @echo building docker images... - BUILD_PROFILE=dev $(SCRIPTS)/build-docker.sh - -HARDHAT = npx hardhat --config hardhat.config.ts - -.PHONY: build-contract -build-contract: check-foundry ## Build the contracts and bindings - npm install - $(HARDHAT) compile - -genesis: build-contract ## Generate the localdev genesis file idempotently - $(HARDHAT) genesis --network localdev --num-validators $(NUM_VALIDATORS) - -genesis-mainnet: build-contract ## Generate the mainnet genesis file - $(HARDHAT) genesis --network mainnet - -.PHONY: mine-denylist-salt -mine-denylist-salt: check-foundry ## Mine a CREATE2 salt for the Denylist proxy with a 0x360 address prefix (usage: make mine-denylist-salt INIT_CODE_HASH=0x...) - @test -n "$$INIT_CODE_HASH" || { echo "Error: INIT_CODE_HASH is required"; exit 1; } - cast create2 \ - --starts-with 360 \ - --init-code-hash $$INIT_CODE_HASH \ - --seed "$$(printf 'Denylist.v1' | cast keccak)" - -.PHONY: clean -clean: ## Clean contract artifacts files and caches - npx hardhat clean - forge clean - $(RM) contracts/cache/storage-code*.json - -##@ Local Environment -.PHONY: up down dev - -up: genesis ## Start integration services for full local integration - @echo setting up env... - $(SCRIPTS)/up.sh - -down: ## Stop integration services (set CLEAN=true to drop volumes) - @echo tearing down env... - $(SCRIPTS)/down.sh - -dev: genesis ## Run main project in dev mode - ./scripts/localdev.mjs stop clean start $(LAUNCH_ARGS) - -##@ Test -.PHONY: test-unit -test-unit: build genesis ## Run unit tests with linting - @echo running linting and unit tests... - make lint - cargo install cargo-nextest --locked - cargo nextest run $(UNIT_TEST_ARGS) - -.PHONY: test-it -test-it: up ## Run integration tests - @echo running linting, unit tests, and integration tests... - make lint - cargo install cargo-nextest --locked - cargo nextest run $(UNIT_TEST_ARGS) --features integration - -.PHONY: test-all -test-all: test-it test-unit-contract ## Run all tests - @echo running all tests... - make smoke LAUNCH_ARGS="--frozen --healthy-retry=130" - -.PHONY: cov-unit -cov-unit: genesis ## Run unit tests with coverage - rm -f $(COV_FILE) - cargo llvm-cov nextest --lcov --output-path $(COV_FILE) $(UNIT_TEST_ARGS) --exclude-from-report arc-execution-e2e - -.PHONY: cov-it -cov-it: up ## Run integration tests with coverage - rm -f $(COV_FILE) - cargo llvm-cov nextest --lcov --output-path $(COV_FILE) $(UNIT_TEST_ARGS) --features integration --exclude-from-report arc-execution-e2e - -.PHONY: cov-report -cov-report: cov-unit ## Generate the coverage report - cargo llvm-cov report --html - -.PHONY: cov-show -cov-show: cov-report ## Generate coverage report and open in browser - open target/llvm-cov/html/index.html - -.PHONY: test-unit-contract -test-unit-contract: check-foundry ## Run contract unit tests with coverage - @echo "Running contract tests..." - @forge test -vvv - @echo "" - @echo "Coverage Summary:" - @echo "================" - @forge coverage --report summary --offline 2>&1 | \ - grep -E "(protocol-config|validator-manager|AdminUpgradeableProxy|^\| Total)" | \ - grep -v "interfaces" | \ - grep -v "contracts/test" - -.PHONY: test-unit-hardhat -test-unit-hardhat: ## Run hardhat unit tests - npx hardhat test ./tests/helpers/matchers/index.test.ts ./tests/unit/*.test.ts --no-compile - -.PHONY: test-localdev -test-localdev: ## Run hardhat localdev tests - $(HARDHAT) test ./tests/localdev/*.test.ts --network localdev - $(MAKE) test-simulation - -.PHONY: test-simulation -test-simulation: ## Run hardhat simulation tests - $(HARDHAT) test ./tests/simulation/*.test.ts --network localdev - -.PHONY: smoke -smoke: genesis ## Run smoke tests (both reth and malachite) - @echo "Running smoke tests for both reth and malachite..." - @echo "Step 1/2: Running smoke-reth tests..." - $(MAKE) smoke-reth - @echo "smoke-reth completed" - @echo "Step 2/2: Running smoke-malachite tests..." - $(MAKE) smoke-malachite - @echo "smoke-malachite completed" - @echo "All smoke tests completed successfully!" - -.PHONY: smoke-reth -smoke-reth: export ARC_SMOKE_SCENARIO := reth -smoke-reth: genesis ## Run Reth smoke tests (mock CL, single fee recipient) - @echo "Running smoke tests on local reth(mock CL)..." - cargo build --release --bin arc-node-execution - @bash -c '\ - set -ex; \ - trap "./scripts/localdev.mjs stop --network=localdev" EXIT; \ - ./scripts/localdev.mjs stop clean daemon --network=localdev --bin=target/release/arc-node-execution $(LAUNCH_ARGS); \ - $(MAKE) test-localdev; \ - ' - -.PHONY: smoke-malachite -smoke-malachite: export ARC_SMOKE_SCENARIO := malachite -smoke-malachite: testnet ## Run Malachite smoke tests (real CL, per-validator recipients) - @echo "Running smoke tests on local reth + malachite using testnet setup (localdev.toml)..." - @bash -c '\ - set -ex; \ - trap "$(MAKE) testnet-clean" EXIT; \ - $(MAKE) test-localdev; \ - ' - -.PHONY: smoke-quake -smoke-quake: testnet - @echo "Running quake tests against local network..." - $(MAKE) testnet-test - -##@ Testnet -.PHONY: testnet -testnet: genesis build-docker ## Start testnet as defined in QUAKE_MANIFEST file - @echo "Setting up and starting Quake testnet..." - $(QUAKE) -f $(QUAKE_MANIFEST) start $(QUAKE_START_ARGS) - -.PHONY: testnet-test -testnet-test: ## Run tests against running testnet - $(QUAKE) test - -.PHONY: testnet-down -testnet-down: ## Stop testnet - $(QUAKE) stop - -.PHONY: testnet-clean -testnet-clean: ## Remove testnet artifacts - # Remove Docker Compose build containers that don't get cleaned up by 'docker compose down' - # These are transient containers created during image builds (e.g., testnet-arc_execution_build-1) - @docker ps -a --format "{{.ID}} {{.Names}}" | grep -E "arc_execution_build|arc_consensus_build" | awk '{print $$1}' | xargs -r docker rm -f 2>/dev/null || true - $(QUAKE) -f $(QUAKE_MANIFEST) clean --all - # Clean up any stale testnet networks to prevent IP conflicts on next run - @docker network ls --format "{{.Name}}" | grep -E "^localdev_" | xargs -r docker network rm 2>/dev/null || true - -.PHONY: testnet-load -testnet-load: ## Send tx load to testnet (backpressure) (usage: make testnet-load RATE=1000 TIME=60) - @RATE=$${RATE:-1000}; \ - TIME=$${TIME:-60}; \ - echo "Sending $$RATE TPS for $$TIME seconds to testnet..."; \ - $(QUAKE) -f $(QUAKE_MANIFEST) load -r $$RATE -t $$TIME - -.PHONY: testnet-load-stop -testnet-load-stop: ## Stop all running load processes - @pkill -f "quake.*(load|spam)" || true - @echo "Load processes stopped (if any were running)" - -.PHONY: quake-test -quake-test: ## Run e2e tests for Quake - python3 scripts/md-exec.py crates/quake/tests diff --git a/README.md b/README.md index 98318c4..477878b 100644 --- a/README.md +++ b/README.md @@ -1,215 +1,28 @@ -

- - - - Arc - - -

+# Arc crates.io placeholders -

The Economic OS for the internet

+This orphan branch contains placeholder crates for Arc package names whose real +implementations are currently not publishable to crates.io. -

- Website -

+The real Arc node source is developed in the public repository: -> [!IMPORTANT] -> Arc is currently in testnet, and this is alpha software currently undergoing audits. +https://github.com/circlefin/arc-node -Arc is an open EVM-compatible layer 1 built on [Malachite](https://github.com/circlefin/malachite) consensus, delivering the performance and reliability needed to meet the new demands of the global internet economy. +These placeholder releases are not functional libraries. They reserve crate +names for the Arc project while the real implementation crates continue to +depend on Reth SDK crates that are not available from crates.io at compatible +versions. -## Features +Do not depend on `0.0.0-placeholder` releases for runtime functionality. -- **USDC as Gas** - Pay gas in USDC for low, predictable fees on any transaction -- **Deterministic Sub-second Finality** - Near-instant settlement finality powered by Malachite BFT consensus engine -- **Circle Platform Integration** - Integrates with Circle’s full-stack platform (e.g., USDC, Wallets, CCTP, Gateway) to help you go from prototype to production faster -- **(Coming soon) Opt-in Configurable Privacy** - Native privacy tooling enables selective shielding of sensitive financial data while preserving auditability +## Placeholder Packages -## Documentation - -- 🚀 **[Execution](crates/node/README.md)** - Execution binary and configuration -- 🗳️ **[Consensus](crates/malachite-app/README.md)** - Consensus binary and configuration -- More: see Arc [developer docs](https://docs.arc.network/arc/concepts/welcome-to-arc) for guides, APIs, and specs - -## Install and Run a Node - -### Install - -See [Installation](docs/installation.md) for how to obtain the Arc node -binaries or Docker images (pre-built, from source, or via Docker). - -### Run - -See [Running an Arc Node](docs/running-an-arc-node.md) for configuration and -startup (binaries or Docker Compose). - -## Development - -### Repository setup - -Clone the repository (or pull the latest changes). This repository uses Git submodules; initialize and update them with: - -```bash -git submodule update --init --recursive -``` - -**Tip:** To automatically fetch submodules on `git pull`, run in the repo root: - -```bash -git config submodule.recurse true -git config fetch.recurseSubmodules on-demand -``` - -### Prerequisites - -- [Rust](https://rustup.rs/) -- [Docker](https://docs.docker.com/get-started/get-docker/) -- [Node.js](https://nodejs.org/) -- [Foundry](https://getfoundry.sh/) -- [Hardhat](https://hardhat.org/) -- [Protobuf](https://github.com/protocolbuffers/protobuf) -- [TypeScript](https://www.typescriptlang.org/) -- [Yarn](https://yarnpkg.com/) -- [Buf](https://github.com/bufbuild/buf) - -Install required tools on macOS with Homebrew: - -```bash -brew install protobuf node yarn bufbuild/buf/buf - -curl -L https://foundry.paradigm.xyz | bash -foundryup -``` - -**Note:** Hardhat only supports **even** Node.js versions (e.g., 20.x, 22.x). Odd versions like 25.x are not supported. See [Hardhat's Node.js support policy](https://v2.hardhat.org/hardhat-runner/docs/reference/stability-guarantees#node.js-versions-support) for details. - -Install JavaScript dependencies: - -```bash -npm install -``` - -### Build - -Build the project: - -```bash -make build -``` - -### Code Quality - -Format and lint your code: - -```bash -make lint -``` - -### Testing - -The test suite includes unit tests, integration tests, contract tests, and smoke tests. - -Run tests: - -```bash -# Unit tests (Rust + linting) -make test-unit - -# Integration tests -make test-it - -# Contract tests (Solidity) -make test-unit-contract - -# Smoke tests (end-to-end validation) -make smoke - -# Run all tests -make test-all -``` - -### Coverage - -Generate and view test coverage (requires [`cargo-llvm-cov`](https://github.com/taiki-e/cargo-llvm-cov?tab=readme-ov-file#installation)): - -```bash -# Install cargo-llvm-cov on macOS with Homebrew (one-time setup) -brew install cargo-llvm-cov - -# Generate coverage for unit tests -make cov-unit - -# Generate HTML report and open in browser -make cov-show -``` - -### Local Testnet - -Launch a full local testnet with 5 execution nodes, 5 consensus nodes, plus Prometheus, Grafana, and Blockscout: - -```bash -make testnet -``` - -**Note:** If your development environment requires installing custom CA certificates, you can add them to the `deployments/certs` directory. They must be PEM-encoded and have a `.crt` extension. They will be automatically installed into the Docker images at build time. - -To export a certificate from your system's keychain (macOS): - -```bash -security find-certificate -p -c '' > deployments/certs/.crt -``` - -Interact with the testnet: - -```bash -# Send tx load (usage: make testnet-load RATE=1000 TIME=60) -make testnet-load - -# Stop the testnet -make testnet-down - -# Clean up all resources -make testnet-clean -``` - -For an in-depth look at system design and individual components, check out the [Architecture Guide](docs/ARCHITECTURE.md). For architectural decisions and their rationale, refer to our [Architecture Decision Records (ADRs)](docs/adr/README.md). - -## Contributing - -We welcome contributions! Please follow these steps: - -1. **Format and lint**: `make lint` -2. **Build**: `make build` -3. **Test**: `make test-unit` -4. **Check coverage**: `make cov-show` - -For more details, see our [Contributing Guide](CONTRIBUTING.md). - -## Resources - -- [Arc Network](https://www.arc.network/) - Official Arc Network website -- [Arc Documentation](https://docs.arc.network/) - Official Arc developer documentation -- [Reth](https://github.com/paradigmxyz/reth) - The underlying execution layer framework -- [Malachite](https://github.com/circlefin/malachite) - BFT consensus engine -- [Local Documentation](docs/) - Implementation guides and references - -## Acknowledgements -This project is open-source software licensed under Apache 2.0. It is built from a number of open source libraries and inspired by others. We would like to highlight several of them in particular and credit the teams that develop and maintain them. - -[Malachite](https://github.com/circlefin/malachite) - Malachite, a flexible BFT consensus engine written in Rust, was originally developed at [Informal Systems](https://github.com/informalsystems) and [is now maintained by Circle](https://www.circle.com/blog/introducing-arc-an-open-layer-1-blockchain-purpose-built-for-stablecoin-finance) as part of Arc. We thank Informal Systems for originating and stewarding Malachite, and for their continued contributions to the project. - -[Reth / Paradigm](https://github.com/paradigmxyz/reth) - Reth is an EVM execution client that is used in Arc’s execution layer via the Reth SDK. We thank the Paradigm team for continuing to push the envelope with Reth and for their continued emphasis on performance, extensibility, and customization, as well as their commitment to open source. Additionally, we’re big fans of the Foundry toolchain as well! - -[libp2p](https://github.com/libp2p/rust-libp2p) - libp2p is used extensively through the arc-node consensus layer, and we thank the team for their development of it. - -[Tokio](https://github.com/tokio-rs/tokio) - Tokio is used extensively throughout the consensus and execution layers, and we are grateful to the team for their continued development and maintenance of it. - -[Celo](https://celo.org/) - USDC is the native token on Arc and supports interacting with it through an ERC-20 interface; this “linked interface” design was first (as far as we know) pioneered on Celo, and we’d like to credit the team for devising it. - -[Alloy-rs](https://github.com/alloy-rs/alloy) - Alloy is used throughout the consensus and execution layers, and we are very thankful to the team for this excellent library. - -[Revm](https://github.com/bluealloy/revm) - Revm is used via the Reth SDK in the execution layer as the core EVM implementation. We thank the team for their continued development and maintenance of it. - -[Hardhat / Nomic Foundation](https://github.com/NomicFoundation/hardhat) - We thank the team for their continued development of the Hardhat toolchain. - -[Viem](https://github.com/wevm/viem) - We thank the team for their continued development of Viem and other libraries. +- `arc-eth-engine` +- `arc-evm` +- `arc-evm-node` +- `arc-execution-config` +- `arc-execution-payload` +- `arc-execution-txpool` +- `arc-execution-validation` +- `arc-node-consensus` +- `arc-node-execution` +- `arc-precompiles` diff --git a/SECURITY.md b/SECURITY.md deleted file mode 100644 index a637ca2..0000000 --- a/SECURITY.md +++ /dev/null @@ -1,4 +0,0 @@ -# Security Policy - -## Reporting a Vulnerability -Please do not file public issues on Github for security vulnerabilities. All security vulnerabilities should be reported to Circle privately, through Circle's [Bug Bounty Program](https://hackerone.com/circle-bbp). Please read through the program policy before submitting a report. diff --git a/arcup/arcup b/arcup/arcup deleted file mode 100755 index 3590cd3..0000000 --- a/arcup/arcup +++ /dev/null @@ -1,841 +0,0 @@ -#!/usr/bin/env bash -set -euo pipefail - -# Arcup - Arc node installer -# Downloads and installs arc-node binaries from GitHub releases - -# NOTE: if you make modifications to this script, please increment the version number. -# WARNING: the SemVer pattern: major.minor.patch must be followed as we use it to determine if the script is up to date. -ARCUP_INSTALLER_VERSION="0.2.0" - -REPO="${ARC_REPO:-circlefin/arc-node}" -if [[ -n "${ARC_REPO:-}" ]] && [[ ! "$ARC_REPO" =~ ^[A-Za-z0-9_.-]+/[A-Za-z0-9_.-]+$ ]]; then - echo "error: invalid ARC_REPO format: '$ARC_REPO' (expected 'owner/repo')" >&2 - exit 1 -fi -# TODO: set GPG key fingerprint once release signing key is published -GPG_KEY_FINGERPRINT="" -GPG_KEYSERVER="keyserver.ubuntu.com" -ARC_DIR="${ARC_DIR:-${ARC_HOME:-$HOME/.arc}}" -BIN_DIR="${ARC_BIN_DIR:-$ARC_DIR/bin}" -# Self-update always uses the canonical repo to prevent hijack via ARC_REPO -ARCUP_BIN_URL="https://raw.githubusercontent.com/circlefin/arc-node/main/arcup/arcup" -ARCUP_BIN_PATH="$BIN_DIR/arcup" -GITHUB_API_URL="${GITHUB_API_URL:-https://api.github.com}" -case "$GITHUB_API_URL" in - https://?*) GITHUB_API_URL="${GITHUB_API_URL%/}" ;; - *) - echo "error: invalid GITHUB_API_URL: '$GITHUB_API_URL' (expected https:// URL)" >&2 - exit 1 - ;; -esac -CURL_RETRY_ARGS=(--retry 3 --retry-delay 2 --connect-timeout 15) -CURL_HEADERS=() -if [ "${CURL_HEADER:-}" ]; then - CURL_HEADERS=(-H "$CURL_HEADER") -fi -GITHUB_AUTH_TOKEN="${ARC_GITHUB_TOKEN:-${GH_TOKEN:-${GITHUB_TOKEN:-}}}" - -# Binaries included in each release archive -BINARIES=("arc-node-execution" "arc-node-consensus" "arc-snapshots") - -# Color output -RED='\033[0;31m' -GREEN='\033[0;32m' -YELLOW='\033[1;33m' -NC='\033[0m' # No Color - -# Temp paths populated by update_arcup() and main(); cleaned up on EXIT. -_ARCUP_TMP_FILE="" -TMP_DIR=$(mktemp -d) -_arcup_cleanup() { - [[ -n "$_ARCUP_TMP_FILE" ]] && rm -f "$_ARCUP_TMP_FILE" - [[ -n "$TMP_DIR" ]] && rm -rf "$TMP_DIR" -} -trap _arcup_cleanup EXIT - -info() { - echo -e "${GREEN}info${NC}: $1" >&2 -} - -warn() { - echo -e "${YELLOW}warn${NC}: $1" >&2 -} - -error() { - echo -e "${RED}error${NC}: $1" >&2 - exit 1 -} - -require_command() { - local command_name="$1" - local message="$2" - - if ! command -v "$command_name" >/dev/null 2>&1; then - error "$message" - fi -} - -curl_with_headers() { - if ((${#CURL_HEADERS[@]} > 0)); then - curl "${CURL_RETRY_ARGS[@]}" "${CURL_HEADERS[@]}" "$@" - else - curl "${CURL_RETRY_ARGS[@]}" "$@" - fi -} - -curl_github_api_json() { - local args=("${CURL_RETRY_ARGS[@]}" -H "Accept: application/vnd.github+json") - if [[ -n "$GITHUB_AUTH_TOKEN" ]]; then - args+=(-H "Authorization: Bearer $GITHUB_AUTH_TOKEN") - fi - if ((${#CURL_HEADERS[@]} > 0)); then - args+=("${CURL_HEADERS[@]}") - fi - curl "${args[@]}" "$@" -} - -curl_github_api_json_anonymous() { - local args=("${CURL_RETRY_ARGS[@]}" -H "Accept: application/vnd.github+json") - if ((${#CURL_HEADERS[@]} > 0)); then - args+=("${CURL_HEADERS[@]}") - fi - curl "${args[@]}" "$@" -} - -safe_curl_error() { - local message="$1" - - if [[ -n "$GITHUB_AUTH_TOKEN" ]] || ((${#CURL_HEADERS[@]} > 0)); then - echo "request failed" - else - echo "$message" - fi -} - -curl_github_asset_headers() { - if [[ -z "$GITHUB_AUTH_TOKEN" ]] && ((${#CURL_HEADERS[@]} == 0)); then - return 1 - fi - - local args=("${CURL_RETRY_ARGS[@]}" -H "Accept: application/octet-stream") - if [[ -n "$GITHUB_AUTH_TOKEN" ]]; then - args+=(-H "Authorization: Bearer $GITHUB_AUTH_TOKEN") - fi - if ((${#CURL_HEADERS[@]} > 0)); then - args+=("${CURL_HEADERS[@]}") - fi - curl "${args[@]}" "$@" -} - -resolve_github_asset_download_url() { - local asset_api_url="$1" - local headers location - - headers=$(curl_github_asset_headers -fsS -D - -o /dev/null "$asset_api_url") || return 1 - location=$(printf '%s\n' "$headers" | awk ' - /^[Ll][Oo][Cc][Aa][Tt][Ii][Oo][Nn]:[[:space:]]*/ { - sub(/\r$/, "") - sub(/^[^:]+:[[:space:]]*/, "") - print - exit - } - ') - - [[ -n "$location" ]] || return 1 - printf '%s\n' "$location" -} - -require_install_prerequisites() { - require_command curl "curl not found. Please install curl." - require_command tar "tar not found. Please install tar." - - if ! command -v sha256sum >/dev/null 2>&1 && ! command -v shasum >/dev/null 2>&1; then - error "No SHA-256 tool found. Please install sha256sum or shasum." - fi -} - -extract_json_string_field() { - local field="$1" - - awk -v field="$field" ' - { - pattern = "\"" field "\"[[:space:]]*:[[:space:]]*\"[^\"]+\"" - if (match($0, pattern)) { - value = substr($0, RSTART, RLENGTH) - prefix = "\"" field "\"[[:space:]]*:[[:space:]]*\"" - sub("^" prefix, "", value) - sub("\"$", "", value) - print value - exit - } - } - ' -} - -extract_arc_asset_names() { - awk ' - { - while (match($0, /"name"[[:space:]]*:[[:space:]]*"[^"]+"/)) { - value = substr($0, RSTART, RLENGTH) - sub(/^"name"[[:space:]]*:[[:space:]]*"/, "", value) - sub(/"$/, "", value) - if (value ~ /^arc-node-/) { - print value - } - $0 = substr($0, RSTART + RLENGTH) - } - } - ' -} - -extract_asset_api_url() { - local asset_name="$1" - - awk -v asset_name="$asset_name" ' - BEGIN { RS = "\\{" } - { - compact = $0 - gsub(/[[:space:]]+/, "", compact) - if (index(compact, "\"name\":\"" asset_name "\"") > 0 && - match(compact, /"url":"[^"]+\/releases\/assets\/[0-9]+"/)) { - value = substr(compact, RSTART, RLENGTH) - sub(/^"url":"/, "", value) - sub(/"$/, "", value) - print value - exit - } - } - ' -} - -fetch_release_json() { - local tag="$1" - local api_url="$GITHUB_API_URL/repos/$REPO/releases/tags/$tag" - - curl_github_api_json -fsSL "$api_url" -} - -fetch_latest_release_json() { - local api_url="$GITHUB_API_URL/repos/$REPO/releases/latest" - - curl_github_api_json -fsSL "$api_url" -} - -fetch_latest_release_json_anonymous() { - local api_url="$GITHUB_API_URL/repos/$REPO/releases/latest" - - curl_github_api_json_anonymous -fsSL "$api_url" -} - -list_release_assets() { - local tag="$1" - - fetch_release_json "$tag" 2>/dev/null | extract_arc_asset_names || true -} - -download_error() { - local tag="$1" - local filename="$2" - local url="$3" - local assets - - echo -e "${RED}error${NC}: Failed to download $filename from $url" >&2 - echo " repository: $REPO" >&2 - echo " tag: $tag" >&2 - echo " target: ${TARGET:-unknown}" >&2 - assets="$(list_release_assets "$tag")" - if [[ -n "$assets" ]]; then - echo "Available arc-node release assets for $tag:" >&2 - while IFS= read -r asset; do - echo " $asset" >&2 - done <<< "$assets" - else - echo "Could not list release assets for $tag. Confirm the tag exists and contains binary assets." >&2 - fi - exit 1 -} - -# Compare SemVer versions -# Returns 0 if $1 > $2, 1 otherwise -version_gt() { - [ "$1" = "$2" ] && return 1 - - # Remove 'v' prefix if present - local ver1="${1#v}" - local ver2="${2#v}" - ver1="${ver1%%-*}" - ver2="${ver2%%-*}" - - IFS=. read -r major1 minor1 patch1 </dev/null) || return 1 - asset_api_url=$(printf '%s\n' "$release_json" | extract_asset_api_url "$filename") - if [[ -z "$asset_api_url" ]]; then - return 1 - fi - - download_url=$(resolve_github_asset_download_url "$asset_api_url") || return 1 - - if curl "${CURL_RETRY_ARGS[@]}" -fL --max-time 900 -o "$output_path" "$download_url" >/dev/null 2>&1; then - [[ -f "$output_path" ]] && return 0 - fi - - rm -f "$output_path" - return 1 -} - -download_file_with_gh() { - local tag="$1" - local filename="$2" - local output_dir="$3" - local output_path="$output_dir/$filename" - - if ! command -v gh >/dev/null 2>&1; then - return 1 - fi - - if GH_PROMPT_DISABLED=1 GH_NO_UPDATE_NOTIFIER=1 \ - gh release download "$tag" \ - --repo "$REPO" \ - --pattern "$filename" \ - --dir "$output_dir" \ - --clobber \ - >/dev/null 2>&1; then - [[ -f "$output_path" ]] && return 0 - fi - - rm -f "$output_path" - return 1 -} - -# Compute SHA256 hash of a file -compute_sha256() { - if command -v sha256sum >/dev/null 2>&1; then - sha256sum "$1" | cut -d' ' -f1 - else - require_command shasum "No SHA-256 tool found. Please install sha256sum or shasum." - shasum -a 256 "$1" | awk '{print $1}' - fi -} - -# Check available disk space meets minimum requirement -check_disk_space() { - local dir="$1" - local required_mb="${2:-500}" - - local available_kb - available_kb=$(df -Pk "$dir" 2>/dev/null | awk 'NR==2 {print $4}') - - if [[ -n "$available_kb" ]]; then - local available_mb=$((available_kb / 1024)) - if [[ "$available_mb" -lt "$required_mb" ]]; then - error "Insufficient disk space: ${available_mb}MB available, ${required_mb}MB required in $dir" - fi - fi -} - -# Check if arcup installer is up to date -check_installer_up_to_date() { - require_command curl "curl not found. Please install curl." - - local response - local remote_version - response=$(curl_with_headers -fsSL "$ARCUP_BIN_URL" 2>/dev/null || true) - remote_version=$(printf '%s\n' "$response" | awk -F'"' '/^ARCUP_INSTALLER_VERSION=/ {print $2; exit}') - - if [[ -z "$remote_version" ]]; then - return - fi - - if version_gt "$remote_version" "$ARCUP_INSTALLER_VERSION"; then - echo "" - warn "Your arcup version ($ARCUP_INSTALLER_VERSION) is outdated." - warn "Latest version is $remote_version." - warn "Run 'arcup --self-update' to update to the latest version." - echo "" - fi -} - -# Update arcup itself -update_arcup() { - info "Updating arcup..." - - require_command curl "curl not found. Please install curl." - - _ARCUP_TMP_FILE=$(mktemp) - - info "Downloading latest arcup..." - if ! curl_with_headers -fsSL -o "$_ARCUP_TMP_FILE" "$ARCUP_BIN_URL"; then - error "Failed to download arcup update" - fi - - local remote_version - remote_version=$(grep '^ARCUP_INSTALLER_VERSION=' "$_ARCUP_TMP_FILE" | sed -E 's/ARCUP_INSTALLER_VERSION="(.+)"/\1/') - - if [[ -z "$remote_version" ]]; then - error "Failed to determine remote version" - fi - - if ! version_gt "$remote_version" "$ARCUP_INSTALLER_VERSION"; then - info "Arcup is already up to date (version $ARCUP_INSTALLER_VERSION)" - exit 0 - fi - - info "Updating from $ARCUP_INSTALLER_VERSION to $remote_version..." - if cp "$_ARCUP_TMP_FILE" "$ARCUP_BIN_PATH" 2>/dev/null; then - chmod 755 "$ARCUP_BIN_PATH" - else - error "Failed to update arcup at '$ARCUP_BIN_PATH' — ensure it is writable" - fi - - echo "" - info "Arcup updated successfully to version $remote_version" - exit 0 -} - -# Remove arc binaries and env files -uninstall() { - local found=0 - local items=() - - for binary in "${BINARIES[@]}"; do - [[ -f "$BIN_DIR/$binary" ]] && items+=(" $BIN_DIR/$binary") && found=1 - done - [[ -f "$ARCUP_BIN_PATH" ]] && items+=(" $ARCUP_BIN_PATH") && found=1 - [[ -f "$ARC_DIR/env" ]] && items+=(" $ARC_DIR/env") && found=1 - [[ -f "$ARC_DIR/env.fish" ]] && items+=(" $ARC_DIR/env.fish") && found=1 - - if [[ "$found" -eq 0 ]]; then - info "Nothing to uninstall — no arc binaries found in $BIN_DIR" - exit 0 - fi - - echo "The following will be removed:" - for item in "${items[@]}"; do - echo "$item" - done - echo "" - - printf "Proceed? [y/N] " - read -r confirm - if [[ "$confirm" != "y" && "$confirm" != "Y" ]]; then - info "Uninstall cancelled." - exit 0 - fi - - for binary in "${BINARIES[@]}"; do - rm -f "$BIN_DIR/$binary" - done - rm -f "$ARCUP_BIN_PATH" - rm -f "$ARC_DIR/env" "$ARC_DIR/env.fish" - - # Remove BIN_DIR only if empty - rmdir "$BIN_DIR" 2>/dev/null || true - - echo "" - info "Arc binaries removed." - info "You may also want to remove the PATH entries from your shell config (~/.zshenv, ~/.bashrc, etc.)." - exit 0 -} - -# Parse command-line arguments -VERSION="" -while [[ $# -gt 0 ]]; do - case $1 in - -v|--version) - version - ;; - -i|--install) - if [[ $# -lt 2 || -z "$2" || "$2" == -* ]]; then - error "--install requires a version argument (for example: arcup --install v1.0.0)" - fi - VERSION="$2" - shift 2 - ;; - -U|--self-update) - update_arcup - ;; - --uninstall) - uninstall - ;; - -h|--help) - echo "Usage: arcup [OPTIONS]" - echo "" - echo "Options:" - echo " -v, --version Print arcup installer version" - echo " -i, --install Install specific arc-node version (e.g., v1.0.0)" - echo " -U, --self-update Update arcup to the latest version" - echo " --uninstall Remove arc binaries and env files" - echo " -h, --help Show this help message" - echo "" - echo "Examples:" - echo " arcup # Install latest release" - echo " arcup -i v1.0.0 # Install specific version" - echo " arcup -v # Print installer version" - echo " arcup --self-update # Update arcup itself" - echo " arcup --uninstall # Remove arc installation" - exit 0 - ;; - *) - error "Unknown option: $1. Use --help for usage information." - ;; - esac -done - -if [[ -n "$VERSION" ]]; then - VERSION="$(normalize_version "$VERSION")" -fi - -# Detect platform -detect_platform() { - local platform - platform="$(uname -s | tr '[:upper:]' '[:lower:]')" - - case "$platform" in - linux*) - echo "linux" - ;; - darwin* | mac*) - echo "darwin" - ;; - *) - error "Unsupported platform: $platform" - ;; - esac -} - -# Detect architecture -detect_arch() { - local arch - arch="$(uname -m)" - - case "$arch" in - x86_64 | x64 | amd64) - # Check if running under Rosetta on macOS - if [[ "$(uname -s)" == "Darwin" ]] && [[ "$(sysctl -n sysctl.proc_translated 2>/dev/null || echo 0)" == "1" ]]; then - echo "arm64" - else - echo "amd64" - fi - ;; - arm64 | aarch64) - echo "arm64" - ;; - *) - error "Unsupported architecture: $arch" - ;; - esac -} - -# Detect Rust target triple for release assets -detect_target() { - local platform="$1" - local arch="$2" - - case "$platform" in - darwin) - case "$arch" in - arm64) echo "aarch64-apple-darwin" ;; - amd64) error "macOS Intel release builds are not supported" ;; - *) error "Unsupported Darwin architecture: $arch" ;; - esac - ;; - linux) - case "$arch" in - arm64) echo "aarch64-unknown-linux-gnu" ;; - amd64) echo "x86_64-unknown-linux-gnu" ;; - *) error "Unsupported Linux architecture: $arch" ;; - esac - ;; - *) - error "Unsupported platform: $platform" - ;; - esac -} - -# Get latest release tag from GitHub -get_latest_version() { - require_command curl "curl not found. Please install curl." - - local response token_error - if ! response=$(fetch_latest_release_json 2>&1); then - token_error="$(safe_curl_error "$response")" - if [[ -n "$GITHUB_AUTH_TOKEN" ]] && response=$(fetch_latest_release_json_anonymous 2>&1); then - : - else - error "Failed to fetch latest version from '$GITHUB_API_URL/repos/$REPO/releases/latest': $token_error" - fi - fi - - local version - version=$(printf '%s\n' "$response" | extract_json_string_field "tag_name") - - if [[ -z "$version" ]]; then - error "Failed to parse version from GitHub API response" - fi - - normalize_version "$version" -} - -verify_checksum_file() { - local archive_path="$1" - local checksum_path="$2" - local archive_name="$3" - local expected_checksum expected_name actual_checksum - - if ! read -r expected_checksum expected_name < "$checksum_path"; then - error "Checksum file is empty: $checksum_path" - fi - - if [[ ! "$expected_checksum" =~ ^[0-9A-Fa-f]{64}$ ]]; then - error "Checksum file has invalid SHA-256 hash: $checksum_path" - fi - - if [[ -n "$expected_name" ]]; then - expected_name="${expected_name#\*}" - expected_name="${expected_name##*/}" - if [[ "$expected_name" != "$archive_name" ]]; then - error "Checksum file is for '$expected_name', expected '$archive_name'" - fi - fi - - actual_checksum=$(compute_sha256 "$archive_path") - - if [[ "$expected_checksum" != "$actual_checksum" ]]; then - error "Checksum verification failed - Expected: $expected_checksum - Actual: $actual_checksum" - fi -} - -validate_archive_contents() { - local archive_path="$1" - local listing metadata entry type - - if ! listing=$(tar -tzf "$archive_path"); then - error "Failed to read archive: $archive_path" - fi - - while IFS= read -r entry; do - [[ -z "$entry" ]] && continue - case "$entry" in - /* | .. | ../* | */../* | */.. ) - error "Archive contains unsafe path: $entry" ;; - esac - done <<< "$listing" - - if ! metadata=$(tar -tvzf "$archive_path"); then - error "Failed to inspect archive metadata: $archive_path" - fi - - while IFS= read -r entry; do - [[ -z "$entry" ]] && continue - type="${entry:0:1}" - case "$type" in - l | h ) - error "Archive contains unsupported link entry: $entry" ;; - esac - done <<< "$metadata" -} - -# Install a single binary from TMP_DIR to BIN_DIR -install_binary() { - local name="$1" - local src="$TMP_DIR/$name" - - if [[ -L "$src" || ! -f "$src" ]]; then - error "Binary not found as a regular file in archive: $name" - fi - - local dst="$BIN_DIR/$name" - local old="$dst.old" - - # Move old binary aside to avoid ETXTBSY when overwriting a running executable - mv -f "$dst" "$old" 2>/dev/null || true - if cp "$src" "$dst" 2>/dev/null; then - chmod 755 "$dst" - else - mv -f "$old" "$dst" 2>/dev/null || true - error "Failed to install $name to '$BIN_DIR' — ensure it is writable" - fi - rm -f "$old" 2>/dev/null || true - - info "Installed $name" -} - -# Main installation logic -main() { - require_install_prerequisites - check_installer_up_to_date - - info "Installing arc-node binaries..." - info "Using repository: $REPO" - - if [[ -z "$GPG_KEY_FINGERPRINT" ]]; then - warn "GPG signature verification is disabled — no release signing key configured" - fi - - mkdir -p "$BIN_DIR" - - check_disk_space "$BIN_DIR" - - PLATFORM=$(detect_platform) - ARCH=$(detect_arch) - TARGET=$(detect_target "$PLATFORM" "$ARCH") - info "Detected platform: $PLATFORM ($ARCH); release target: $TARGET" - - if [[ -z "$VERSION" ]]; then - info "Fetching latest release version..." - VERSION=$(get_latest_version) - if [[ -z "$VERSION" ]]; then - error "Failed to fetch latest version. Try specifying --version manually." - fi - fi - - VERSION_TAG="$VERSION" - - info "Installing arc-node $VERSION_TAG" - - ARCHIVE_NAME="arc-node-${VERSION_TAG}-${TARGET}.tar.gz" - - # Download archive - download_file "$VERSION_TAG" "$ARCHIVE_NAME" "$TMP_DIR" - - ARCHIVE_PATH="$TMP_DIR/$ARCHIVE_NAME" - - # Download and verify checksum - info "Verifying checksum..." - download_file "$VERSION_TAG" "${ARCHIVE_NAME}.sha256" "$TMP_DIR" - - verify_checksum_file "$ARCHIVE_PATH" "$TMP_DIR/${ARCHIVE_NAME}.sha256" "$ARCHIVE_NAME" - - info "Checksum verified" - - # GPG signature verification - if [[ -n "$GPG_KEY_FINGERPRINT" ]]; then - if command -v gpg >/dev/null 2>&1; then - info "Verifying GPG signature..." - download_file "$VERSION_TAG" "${ARCHIVE_NAME}.asc" "$TMP_DIR" - - if ! gpg --list-keys "$GPG_KEY_FINGERPRINT" >/dev/null 2>&1; then - info "Fetching Arc release signing key from $GPG_KEYSERVER..." - if ! gpg --keyserver "$GPG_KEYSERVER" --recv-keys "$GPG_KEY_FINGERPRINT" 2>/dev/null; then - warn "Failed to fetch GPG key from keyserver. Skipping signature verification." - warn "You can manually import the key: gpg --keyserver $GPG_KEYSERVER --recv-keys $GPG_KEY_FINGERPRINT" - fi - fi - - if gpg --list-keys "$GPG_KEY_FINGERPRINT" >/dev/null 2>&1; then - if gpg --batch --verify "$TMP_DIR/${ARCHIVE_NAME}.asc" "$ARCHIVE_PATH" 2>/dev/null; then - info "GPG signature verified" - else - error "GPG signature verification failed. The binary may have been tampered with." - fi - fi - else - warn "gpg not found. Skipping signature verification." - warn "Install gpg to enable signature verification of releases." - fi - fi - - validate_archive_contents "$ARCHIVE_PATH" - - # Extract archive - info "Extracting archive..." - tar -xzf "$ARCHIVE_PATH" -C "$TMP_DIR" - - # Install each binary - for binary in "${BINARIES[@]}"; do - install_binary "$binary" - done - - echo "" - info "Arc node $VERSION_TAG installed successfully!" - echo "" - - if [[ ":$PATH:" != *":$BIN_DIR:"* ]]; then - warn "$BIN_DIR is not in your PATH" - echo " Add it to your shell config:" - echo " export PATH=\"$BIN_DIR:\$PATH\"" - echo "" - else - info "Run 'arc-node-execution --version' to verify installation" - fi -} - -if [[ "${ARCUP_SKIP_MAIN:-}" != "1" ]]; then - main -fi diff --git a/arcup/install b/arcup/install deleted file mode 100755 index b9a8d2b..0000000 --- a/arcup/install +++ /dev/null @@ -1,128 +0,0 @@ -#!/usr/bin/env bash -set -euo pipefail - -# Arcup bootstrap installer -# Downloads arcup and arc-node binaries to ~/.arc/bin - -ARC_DIR="${ARC_DIR:-${ARC_HOME:-$HOME/.arc}}" -BIN_DIR="${ARC_BIN_DIR:-$ARC_DIR/bin}" -ENV_FILE="$ARC_DIR/env" -ARCUP_URL="https://raw.githubusercontent.com/circlefin/arc-node/main/arcup/arcup" -CURL_RETRY_ARGS=(--retry 3 --retry-delay 2 --connect-timeout 15) -CURL_HEADERS=() -if [ "${CURL_HEADER:-}" ]; then - CURL_HEADERS=(-H "$CURL_HEADER") -fi - -_INSTALL_TMP_FILE="" -_install_cleanup() { rm -f "$_INSTALL_TMP_FILE"; } -trap _install_cleanup EXIT - -# Color output -RED='\033[0;31m' -GREEN='\033[0;32m' -YELLOW='\033[1;33m' -NC='\033[0m' - -info() { - echo -e "${GREEN}info${NC}: $1" >&2 -} - -warn() { - echo -e "${YELLOW}warn${NC}: $1" >&2 -} - -error() { - echo -e "${RED}error${NC}: $1" >&2 - exit 1 -} - -curl_with_headers() { - if ((${#CURL_HEADERS[@]} > 0)); then - curl "${CURL_RETRY_ARGS[@]}" "${CURL_HEADERS[@]}" "$@" - else - curl "${CURL_RETRY_ARGS[@]}" "$@" - fi -} - -install_bin() { - local src="$1" - local dst="$2" - - if cp "$src" "$dst" 2>/dev/null; then - chmod +x "$dst" - else - error "failed to install to '$dst' — ensure it is writable" - fi -} - -main() { - echo "Installing arcup..." - echo "" - - # Ensure BIN_DIR exists - mkdir -p "$ARC_DIR" - mkdir -p "$BIN_DIR" - - # Download arcup script to a temp file - _INSTALL_TMP_FILE="$(mktemp 2>/dev/null || mktemp -t arcup)" - - info "Downloading arcup script..." - if ! command -v curl >/dev/null 2>&1; then - error "curl not found. Please install curl." - fi - - if ! curl_with_headers -#fL --max-time 300 -o "$_INSTALL_TMP_FILE" "$ARCUP_URL" ; then - error "failed to download arcup from '$ARCUP_URL'" - fi - - # Install arcup to BIN_DIR - install_bin "$_INSTALL_TMP_FILE" "$BIN_DIR/arcup" - info "Arcup installed to $BIN_DIR/arcup" - - # Create env file that can be sourced to set PATH (POSIX shells) - cat > "$ENV_FILE" < "$ENV_FILE.fish" <&2 - exit 1 -} - -assert_eq() { - local expected="$1" - local actual="$2" - local name="$3" - - if [[ "$expected" != "$actual" ]]; then - printf 'expected: %s\nactual: %s\n' "$expected" "$actual" >&2 - fail "$name" - fi - pass "$name" -} - -expect_fail() { - local name="$1" - shift - - if ( "$@" ) >"$TEST_TMP/expect_fail.out" 2>&1; then - cat "$TEST_TMP/expect_fail.out" >&2 - fail "$name" - fi - pass "$name" -} - -test_version_normalization() { - assert_eq "v1.2.3" "$(normalize_version "1.2.3")" "normalizes missing v prefix" - assert_eq "v1.2.3" "$(normalize_version "v1.2.3")" "keeps v-prefixed version" - assert_eq "v1.2.3-rc.1" "$(normalize_version "1.2.3-rc.1")" "normalizes prerelease version" - expect_fail "rejects invalid version" normalize_version "latest" -} - -test_version_comparison() { - if ! version_gt "0.3.1-rc.1" "0.3.0"; then - fail "compares prerelease installer versions" - fi - pass "compares prerelease installer versions" - - if version_gt "0.3.0-rc.1" "0.3.0"; then - fail "same prerelease base is not newer" - fi - pass "same prerelease base is not newer" -} - -test_target_mapping() { - assert_eq "x86_64-unknown-linux-gnu" "$(detect_target "linux" "amd64")" "maps linux amd64 target" - assert_eq "aarch64-unknown-linux-gnu" "$(detect_target "linux" "arm64")" "maps linux arm64 target" - assert_eq "aarch64-apple-darwin" "$(detect_target "darwin" "arm64")" "maps macos arm64 target" - expect_fail "rejects macos amd64 target" detect_target "darwin" "amd64" -} - -test_unknown_architecture_fails() { - if ( - uname() { - case "${1:-}" in - -m) echo "sparc64" ;; - -s) echo "Linux" ;; - *) command uname "$@" ;; - esac - } - detect_arch - ) >"$TEST_TMP/unknown_arch.out" 2>&1; then - cat "$TEST_TMP/unknown_arch.out" >&2 - fail "unsupported architecture fails" - fi - pass "unsupported architecture fails" -} - -test_github_api_url_rejects_non_https() { - local out="$TEST_TMP/github-api-url.out" - - if GITHUB_API_URL="http://api.github.com" ARCUP_SKIP_MAIN=1 bash "$ROOT_DIR/arcup/arcup" >"$out" 2>&1; then - cat "$out" >&2 - fail "rejects non-https GitHub API URL" - fi - - if ! grep -q "invalid GITHUB_API_URL" "$out"; then - cat "$out" >&2 - fail "rejects non-https GitHub API URL" - fi - pass "rejects non-https GitHub API URL" -} - -test_checksum_validation() { - local archive="$TEST_TMP/archive.tar.gz" - local checksum_file="$TEST_TMP/archive.tar.gz.sha256" - local archive_name="arc-node-v1.2.3-x86_64-unknown-linux-gnu.tar.gz" - local checksum - - printf 'archive bytes' > "$archive" - checksum="$(compute_sha256 "$archive")" - printf '%s %s\n' "$checksum" "$archive_name" > "$checksum_file" - verify_checksum_file "$archive" "$checksum_file" "$archive_name" - pass "valid checksum file passes" - - printf '%s other-asset.tar.gz\n' "$checksum" > "$checksum_file" - expect_fail "checksum filename mismatch fails" verify_checksum_file "$archive" "$checksum_file" "$archive_name" -} - -test_download_error_lists_assets() { - local out="$TEST_TMP/download_error.out" - - if ( - GITHUB_AUTH_TOKEN="" - CURL_HEADERS=() - TARGET="x86_64-unknown-linux-gnu" - gh() { return 1; } - curl_with_headers() { return 22; } - list_release_assets() { - printf '%s\n' \ - "arc-node-v1.2.3-x86_64-unknown-linux-gnu.tar.gz" \ - "arc-node-v1.2.3-aarch64-apple-darwin.tar.gz" - } - download_file "v1.2.3" "arc-node-v1.2.3-missing.tar.gz" "$TEST_TMP" - ) >"$out" 2>&1; then - cat "$out" >&2 - fail "download error fails" - fi - - if ! grep -q "Available arc-node release assets" "$out"; then - cat "$out" >&2 - fail "download error lists available assets" - fi - pass "download error lists available assets" -} - -test_download_file_uses_github_token_api() { - local fakebin="$TEST_TMP/token-api-fakebin" - local release_dir="$TEST_TMP/token-api-release" - local output_dir="$TEST_TMP/token-api-output" - local asset_name="arc-node-v1.2.3-aarch64-apple-darwin.tar.gz" - - mkdir -p "$fakebin" "$release_dir" "$output_dir" - printf 'token asset\n' > "$release_dir/$asset_name" - - cat > "$fakebin/gh" <<'EOF' -#!/usr/bin/env bash -echo "gh should not be called when token API succeeds" >&2 -exit 99 -EOF - chmod 755 "$fakebin/gh" - - cat > "$fakebin/curl" <<'EOF' -#!/usr/bin/env bash -set -euo pipefail - -out="" -dump="" -url="" -while [[ $# -gt 0 ]]; do - case "$1" in - -o) - out="$2" - shift 2 - ;; - -D) - dump="$2" - shift 2 - ;; - -H | --retry | --retry-delay | --connect-timeout | --max-time) - shift 2 - ;; - -*) - shift - ;; - *) - url="$1" - shift - ;; - esac -done - -case "$url" in - *api.github.com/repos/test/arc-node/releases/tags/v1.2.3) - data='{"assets":[{"url":"https://api.github.com/repos/test/arc-node/releases/assets/123","name":"arc-node-v1.2.3-aarch64-apple-darwin.tar.gz"}]}' - ;; - *api.github.com/repos/test/arc-node/releases/assets/123) - if [[ "$dump" == "-" ]]; then - printf 'HTTP/1.1 302 Found\r\nLocation: https://objects.example/arc-node-v1.2.3-aarch64-apple-darwin.tar.gz\r\n\r\n' - exit 0 - fi - printf 'asset API request should only resolve redirect location\n' >&2 - exit 22 - ;; - *objects.example/arc-node-v1.2.3-aarch64-apple-darwin.tar.gz) - cp "$TOKEN_RELEASE_DIR/arc-node-v1.2.3-aarch64-apple-darwin.tar.gz" "$out" - exit 0 - ;; - *releases/download*) - echo "direct release URL should not be called when token API succeeds" >&2 - exit 99 - ;; - *) - printf 'unexpected curl URL: %s\n' "$url" >&2 - exit 22 - ;; -esac - -if [[ -n "$out" ]]; then - printf '%s\n' "$data" > "$out" -else - printf '%s\n' "$data" -fi -EOF - chmod 755 "$fakebin/curl" - - ( - PATH="$fakebin:$PATH" - export TOKEN_RELEASE_DIR="$release_dir" - GITHUB_AUTH_TOKEN="test-token" - CURL_HEADERS=() - REPO="test/arc-node" - download_file "v1.2.3" "$asset_name" "$output_dir" - ) >"$TEST_TMP/token-api.out" 2>&1 - - if [[ "$(cat "$output_dir/$asset_name")" != "token asset" ]]; then - cat "$TEST_TMP/token-api.out" >&2 - fail "download_file uses token API when available" - fi - pass "download_file uses token API when available" -} - -test_token_api_preserves_custom_header() { - local fakebin="$TEST_TMP/token-header-fakebin" - local release_dir="$TEST_TMP/token-header-release" - local output_dir="$TEST_TMP/token-header-output" - local asset_name="arc-node-v1.2.3-aarch64-apple-darwin.tar.gz" - - mkdir -p "$fakebin" "$release_dir" "$output_dir" - printf 'token header asset\n' > "$release_dir/$asset_name" - - cat > "$fakebin/gh" <<'EOF' -#!/usr/bin/env bash -echo "gh should not be called when token API succeeds" >&2 -exit 99 -EOF - chmod 755 "$fakebin/gh" - - cat > "$fakebin/curl" <<'EOF' -#!/usr/bin/env bash -set -euo pipefail - -out="" -dump="" -url="" -headers="" -while [[ $# -gt 0 ]]; do - case "$1" in - -o) - out="$2" - shift 2 - ;; - -D) - dump="$2" - shift 2 - ;; - -H) - headers="${headers}${2} -" - shift 2 - ;; - --retry | --retry-delay | --connect-timeout | --max-time) - shift 2 - ;; - -*) - shift - ;; - *) - url="$1" - shift - ;; - esac -done - -require_header() { - local expected="$1" - if [[ "$headers" != *"$expected"* ]]; then - printf 'missing header %s for %s\nheaders:\n%s\n' "$expected" "$url" "$headers" >&2 - exit 22 - fi -} - -case "$url" in - *api.github.com/repos/test/arc-node/releases/tags/v1.2.3) - require_header "Authorization: Bearer test-token" - require_header "X-Arc-Test: custom" - require_header "Accept: application/vnd.github+json" - data='{"assets":[{"url":"https://api.github.com/repos/test/arc-node/releases/assets/123","name":"arc-node-v1.2.3-aarch64-apple-darwin.tar.gz"}]}' - ;; - *api.github.com/repos/test/arc-node/releases/assets/123) - require_header "Authorization: Bearer test-token" - require_header "X-Arc-Test: custom" - require_header "Accept: application/octet-stream" - if [[ "$dump" == "-" ]]; then - printf 'HTTP/2 302\r\nlocation: https://objects.example/arc-node-v1.2.3-aarch64-apple-darwin.tar.gz\r\n\r\n' - exit 0 - fi - printf 'asset API request should only resolve redirect location\n' >&2 - exit 22 - ;; - *objects.example/arc-node-v1.2.3-aarch64-apple-darwin.tar.gz) - if [[ "$headers" == *"Authorization:"* || "$headers" == *"X-Arc-Test:"* ]]; then - printf 'signed asset URL received leaked headers:\n%s\n' "$headers" >&2 - exit 22 - fi - cp "$TOKEN_HEADER_RELEASE_DIR/arc-node-v1.2.3-aarch64-apple-darwin.tar.gz" "$out" - exit 0 - ;; - *releases/download*) - echo "direct release URL should not be called when token API succeeds" >&2 - exit 99 - ;; - *) - printf 'unexpected curl URL: %s\n' "$url" >&2 - exit 22 - ;; -esac - -if [[ -n "$out" ]]; then - printf '%s\n' "$data" > "$out" -else - printf '%s\n' "$data" -fi -EOF - chmod 755 "$fakebin/curl" - - ( - PATH="$fakebin:$PATH" - export TOKEN_HEADER_RELEASE_DIR="$release_dir" - GITHUB_AUTH_TOKEN="test-token" - CURL_HEADERS=(-H "X-Arc-Test: custom") - REPO="test/arc-node" - download_file "v1.2.3" "$asset_name" "$output_dir" - ) >"$TEST_TMP/token-header.out" 2>&1 - - if [[ "$(cat "$output_dir/$asset_name")" != "token header asset" ]]; then - cat "$TEST_TMP/token-header.out" >&2 - fail "token API preserves custom header" - fi - pass "token API preserves custom header" -} - -test_latest_version_retries_anonymous_after_token_failure() { - local fakebin="$TEST_TMP/latest-fallback-fakebin" - local out="$TEST_TMP/latest-fallback.out" - - mkdir -p "$fakebin" - cat > "$fakebin/curl" <<'EOF' -#!/usr/bin/env bash -set -euo pipefail - -headers="" -url="" -while [[ $# -gt 0 ]]; do - case "$1" in - -H) - headers="${headers}${2} -" - shift 2 - ;; - --retry | --retry-delay | --connect-timeout | --max-time) - shift 2 - ;; - -*) - shift - ;; - *) - url="$1" - shift - ;; - esac -done - -case "$url" in - *api.github.com/repos/test/arc-node/releases/latest) - if [[ "$headers" == *"Authorization:"* ]]; then - printf 'bad token\n' >&2 - exit 22 - fi - printf '{"tag_name":"v1.2.3"}\n' - ;; - *) - printf 'unexpected curl URL: %s\n' "$url" >&2 - exit 22 - ;; -esac -EOF - chmod 755 "$fakebin/curl" - - ( - PATH="$fakebin:$PATH" - GITHUB_AUTH_TOKEN="stale-token" - CURL_HEADERS=() - REPO="test/arc-node" - GITHUB_API_URL="https://api.github.com" - version="$(get_latest_version)" - [[ "$version" == "v1.2.3" ]] - ) >"$out" 2>&1 || { - cat "$out" >&2 - fail "latest version retries anonymously after token failure" - } - - pass "latest version retries anonymously after token failure" -} - -test_latest_version_redacts_authenticated_failure() { - local fakebin="$TEST_TMP/latest-redaction-fakebin" - local out="$TEST_TMP/latest-redaction.out" - - mkdir -p "$fakebin" - cat > "$fakebin/curl" <<'EOF' -#!/usr/bin/env bash -set -euo pipefail - -headers="" -url="" -while [[ $# -gt 0 ]]; do - case "$1" in - -H) - headers="${headers}${2} -" - shift 2 - ;; - --retry | --retry-delay | --connect-timeout | --max-time) - shift 2 - ;; - -*) - shift - ;; - *) - url="$1" - shift - ;; - esac -done - -case "$url" in - *api.github.com/repos/test/arc-node/releases/latest) - if [[ "$headers" == *"Authorization:"* ]]; then - printf 'curl verbose Authorization: Bearer stale-token\n' >&2 - else - printf 'anonymous failed\n' >&2 - fi - exit 22 - ;; - *) - printf 'unexpected curl URL: %s\n' "$url" >&2 - exit 22 - ;; -esac -EOF - chmod 755 "$fakebin/curl" - - if ( - PATH="$fakebin:$PATH" - GITHUB_AUTH_TOKEN="stale-token" - CURL_HEADERS=() - REPO="test/arc-node" - GITHUB_API_URL="https://api.github.com" - get_latest_version - ) >"$out" 2>&1; then - cat "$out" >&2 - fail "latest version redacts authenticated failure" - fi - - if grep -q "stale-token" "$out" || grep -q "Authorization: Bearer" "$out"; then - cat "$out" >&2 - fail "latest version redacts authenticated failure" - fi - if ! grep -q "request failed" "$out"; then - cat "$out" >&2 - fail "latest version redacts authenticated failure" - fi - pass "latest version redacts authenticated failure" -} - -test_download_file_uses_gh_when_available() { - local fakebin="$TEST_TMP/gh-download-fakebin" - local release_dir="$TEST_TMP/gh-download-release" - local output_dir="$TEST_TMP/gh-download-output" - local asset_name="arc-node-v1.2.3-aarch64-apple-darwin.tar.gz" - - mkdir -p "$fakebin" "$release_dir" "$output_dir" - printf 'release asset\n' > "$release_dir/$asset_name" - - cat > "$fakebin/gh" <<'EOF' -#!/usr/bin/env bash -set -euo pipefail - -if [[ "${1:-}" != "release" || "${2:-}" != "download" ]]; then - exit 1 -fi -shift 2 - -tag="$1" -shift -repo="" -pattern="" -dir="" -while [[ $# -gt 0 ]]; do - case "$1" in - --repo) - repo="$2" - shift 2 - ;; - --pattern) - pattern="$2" - shift 2 - ;; - --dir) - dir="$2" - shift 2 - ;; - --clobber) - shift - ;; - *) - exit 1 - ;; - esac -done - -[[ "$tag" == "v1.2.3" ]] -[[ "$repo" == "test/arc-node" ]] -cp "$GH_RELEASE_DIR/$pattern" "$dir/$pattern" -EOF - chmod 755 "$fakebin/gh" - - cat > "$fakebin/curl" <<'EOF' -#!/usr/bin/env bash -echo "curl should not be called when gh succeeds" >&2 -exit 99 -EOF - chmod 755 "$fakebin/curl" - - ( - PATH="$fakebin:$PATH" - export GH_RELEASE_DIR="$release_dir" - GITHUB_AUTH_TOKEN="" - CURL_HEADERS=() - REPO="test/arc-node" - download_file "v1.2.3" "$asset_name" "$output_dir" - ) >"$TEST_TMP/gh-download.out" 2>&1 - - if [[ "$(cat "$output_dir/$asset_name")" != "release asset" ]]; then - cat "$TEST_TMP/gh-download.out" >&2 - fail "download_file uses gh when available" - fi - pass "download_file uses gh when available" -} - -test_download_file_falls_back_to_curl() { - local fakebin="$TEST_TMP/curl-fallback-fakebin" - local release_dir="$TEST_TMP/curl-fallback-release" - local output_dir="$TEST_TMP/curl-fallback-output" - local asset_name="arc-node-v1.2.3-aarch64-apple-darwin.tar.gz" - - mkdir -p "$fakebin" "$release_dir" "$output_dir" - printf 'fallback asset\n' > "$release_dir/$asset_name" - - cat > "$fakebin/gh" <<'EOF' -#!/usr/bin/env bash -exit 1 -EOF - chmod 755 "$fakebin/gh" - - cat > "$fakebin/curl" <<'EOF' -#!/usr/bin/env bash -set -euo pipefail - -out="" -url="" -while [[ $# -gt 0 ]]; do - case "$1" in - -o) - out="$2" - shift 2 - ;; - --retry | --retry-delay | --connect-timeout | --max-time) - shift 2 - ;; - -*) - shift - ;; - *) - url="$1" - shift - ;; - esac -done - -case "$url" in - *releases/download/v1.2.3/arc-node-v1.2.3-aarch64-apple-darwin.tar.gz) - cp "$FALLBACK_RELEASE_DIR/${url##*/}" "$out" - ;; - *) - printf 'unexpected curl URL: %s\n' "$url" >&2 - exit 22 - ;; -esac -EOF - chmod 755 "$fakebin/curl" - - ( - PATH="$fakebin:$PATH" - export FALLBACK_RELEASE_DIR="$release_dir" - GITHUB_AUTH_TOKEN="" - CURL_HEADERS=() - REPO="test/arc-node" - download_file "v1.2.3" "$asset_name" "$output_dir" - ) >"$TEST_TMP/curl-fallback.out" 2>&1 - - if [[ "$(cat "$output_dir/$asset_name")" != "fallback asset" ]]; then - cat "$TEST_TMP/curl-fallback.out" >&2 - fail "download_file falls back to curl" - fi - pass "download_file falls back to curl" -} - -test_fixture_install_matrix() { - local fixture_dir="$TEST_TMP/fixture" - local fakebin="$TEST_TMP/fakebin" - local release_dir="$fixture_dir/release" - - mkdir -p "$fixture_dir/build" "$release_dir" "$fakebin" - for binary in "${BINARIES[@]}"; do - printf '#!/usr/bin/env bash\nprintf "%s test binary\\n"\n' "$binary" > "$fixture_dir/build/$binary" - chmod 755 "$fixture_dir/build/$binary" - done - - local target archive_name archive checksum_file - for target in \ - "x86_64-unknown-linux-gnu" \ - "aarch64-unknown-linux-gnu" \ - "aarch64-apple-darwin"; do - archive_name="arc-node-v1.2.3-${target}.tar.gz" - archive="$release_dir/$archive_name" - checksum_file="$release_dir/$archive_name.sha256" - tar -czf "$archive" -C "$fixture_dir/build" "${BINARIES[@]}" - printf '%s %s\n' "$(compute_sha256 "$archive")" "$archive_name" > "$checksum_file" - done - - cat > "$fakebin/uname" <<'EOF' -#!/usr/bin/env bash -case "${1:-}" in - -s) echo "$FAKE_UNAME_S" ;; - -m) echo "$FAKE_UNAME_M" ;; - *) /usr/bin/uname "$@" ;; -esac -EOF - chmod 755 "$fakebin/uname" - - cat > "$fakebin/gh" <<'EOF' -#!/usr/bin/env bash -exit 1 -EOF - chmod 755 "$fakebin/gh" - - cat > "$fakebin/curl" <<'EOF' -#!/usr/bin/env bash -set -euo pipefail - -out="" -url="" -while [[ $# -gt 0 ]]; do - case "$1" in - -o) - out="$2" - shift 2 - ;; - --retry | --retry-delay | --connect-timeout | --max-time) - shift 2 - ;; - -*) - shift - ;; - *) - url="$1" - shift - ;; - esac -done - -case "$url" in - *raw.githubusercontent.com/circlefin/arc-node/main/arcup/arcup) - data='ARCUP_INSTALLER_VERSION="0.2.0"' - ;; - *releases/download/v1.2.3/arc-node-v1.2.3-*.tar.gz*) - name="${url##*/}" - cp "$FIXTURE_RELEASE_DIR/$name" "$out" - exit 0 - ;; - *api.github.com/repos/test/arc-node/releases/tags/v1.2.3) - data='{"assets":[{"name":"arc-node-v1.2.3-x86_64-unknown-linux-gnu.tar.gz"},{"name":"arc-node-v1.2.3-aarch64-unknown-linux-gnu.tar.gz"},{"name":"arc-node-v1.2.3-aarch64-apple-darwin.tar.gz"}]}' - ;; - *) - printf 'unexpected curl URL: %s\n' "$url" >&2 - exit 22 - ;; -esac - -if [[ -n "$out" ]]; then - printf '%s\n' "$data" > "$out" -else - printf '%s\n' "$data" -fi -EOF - chmod 755 "$fakebin/curl" - - local case_name os arch install_dir - while read -r case_name os arch target; do - install_dir="$TEST_TMP/install-$case_name" - mkdir -p "$install_dir" - FIXTURE_RELEASE_DIR="$release_dir" \ - FAKE_UNAME_S="$os" \ - FAKE_UNAME_M="$arch" \ - ARC_GITHUB_TOKEN="" \ - GH_TOKEN="" \ - GITHUB_TOKEN="" \ - PATH="$fakebin:$PATH" \ - ARC_REPO="test/arc-node" \ - ARC_BIN_DIR="$install_dir/bin" \ - "$ROOT_DIR/arcup/arcup" -i "1.2.3" >"$TEST_TMP/install-$case_name.out" 2>&1 - - for binary in "${BINARIES[@]}"; do - [[ -x "$install_dir/bin/$binary" ]] || fail "installs $binary for $target" - done - done <<'EOF' -linux-amd64 Linux x86_64 x86_64-unknown-linux-gnu -linux-arm64 Linux aarch64 aarch64-unknown-linux-gnu -macos-arm64 Darwin arm64 aarch64-apple-darwin -EOF - - pass "fixture install covers all release targets" -} - -test_archive_path_traversal_fails() { - local bad_archive="$TEST_TMP/bad.tar.gz" - local bad_src="$TEST_TMP/bad-src" - - mkdir -p "$bad_src" - printf 'bad\n' > "$bad_src/evil" - if tar --version 2>/dev/null | grep -q "GNU tar"; then - tar -czf "$bad_archive" -C "$bad_src" --transform 's|evil|../evil|' evil - else - tar -czf "$bad_archive" -C "$bad_src" -s '|evil|../evil|' evil - fi - - if ( validate_archive_contents "$bad_archive" ) >"$TEST_TMP/path_traversal.out" 2>&1; then - cat "$TEST_TMP/path_traversal.out" >&2 - fail "archive path traversal fails" - fi - pass "archive path traversal fails" -} - -test_archive_link_entries_fail() { - local bad_archive="$TEST_TMP/link-entry.tar.gz" - local bad_src="$TEST_TMP/link-entry-src" - - mkdir -p "$bad_src" - ln -s /etc/passwd "$bad_src/arc-node-execution" - tar -czf "$bad_archive" -C "$bad_src" arc-node-execution - - if ( validate_archive_contents "$bad_archive" ) >"$TEST_TMP/link-entry.out" 2>&1; then - cat "$TEST_TMP/link-entry.out" >&2 - fail "archive link entries fail" - fi - pass "archive link entries fail" -} - -test_install_binary_rejects_symlink() { - local install_src="$TEST_TMP/install-symlink-src" - local install_bin="$TEST_TMP/install-symlink-bin" - - mkdir -p "$install_src" "$install_bin" - ln -s /etc/passwd "$install_src/arc-node-execution" - - if ( - TMP_DIR="$install_src" - BIN_DIR="$install_bin" - install_binary "arc-node-execution" - ) >"$TEST_TMP/install-symlink.out" 2>&1; then - cat "$TEST_TMP/install-symlink.out" >&2 - fail "install_binary rejects symlink" - fi - pass "install_binary rejects symlink" -} - -test_version_normalization -test_version_comparison -test_target_mapping -test_unknown_architecture_fails -test_github_api_url_rejects_non_https -test_checksum_validation -test_download_error_lists_assets -test_download_file_uses_github_token_api -test_token_api_preserves_custom_header -test_latest_version_retries_anonymous_after_token_failure -test_latest_version_redacts_authenticated_failure -test_download_file_uses_gh_when_available -test_download_file_falls_back_to_curl -test_fixture_install_matrix -test_archive_path_traversal_fails -test_archive_link_entries_fail -test_install_binary_rejects_symlink diff --git a/assets/apt/apt-retry.conf b/assets/apt/apt-retry.conf deleted file mode 100644 index ad25985..0000000 --- a/assets/apt/apt-retry.conf +++ /dev/null @@ -1,5 +0,0 @@ -# APT retry configuration for CI reliability -# - Retries: number of retry attempts for failed downloads (with exponential backoff) -# - ForceIPv4: avoid IPv6 connectivity issues in some CI environments -Acquire::Retries "10"; -Acquire::ForceIPv4 "true"; diff --git a/assets/apt/debian.sources b/assets/apt/debian.sources deleted file mode 100644 index bb2132c..0000000 --- a/assets/apt/debian.sources +++ /dev/null @@ -1,23 +0,0 @@ -# Debian package sources with fallback mirrors for CI reliability -# Uses DEB822 format with multiple URIs - APT tries them in order until one works -# -# Mirror selection rationale: -# - Primary: deb.debian.org (Fastly CDN, globally distributed) -# - Fallbacks: University mirrors that also mirror debian-security -# -# NOTE: Uses HTTPS because HTTP port 80 is blocked by StepSecurity egress policy. -# NOTE: The suite "bookworm" must match the Debian version in the Dockerfiles. -# If Dockerfile.execution or Dockerfile.consensus change their base images to a -# different Debian release (e.g., rust:X.Y.Z-trixie), update the Suites below. - -Types: deb -URIs: https://deb.debian.org/debian https://mirror.csclub.uwaterloo.ca/debian https://mirror.xtom.com/debian -Suites: bookworm bookworm-updates -Components: main -Signed-By: /usr/share/keyrings/debian-archive-keyring.gpg - -Types: deb -URIs: https://deb.debian.org/debian-security https://mirror.csclub.uwaterloo.ca/debian-security https://mirror.xtom.com/debian-security -Suites: bookworm-security -Components: main -Signed-By: /usr/share/keyrings/debian-archive-keyring.gpg diff --git a/assets/artifacts/Permit2/Permit2.json b/assets/artifacts/Permit2/Permit2.json deleted file mode 100644 index 9c52549..0000000 --- a/assets/artifacts/Permit2/Permit2.json +++ /dev/null @@ -1 +0,0 @@ -{"abi":[{"type":"function","name":"DOMAIN_SEPARATOR","inputs":[],"outputs":[{"name":"","type":"bytes32","internalType":"bytes32"}],"stateMutability":"view"},{"type":"function","name":"allowance","inputs":[{"name":"","type":"address","internalType":"address"},{"name":"","type":"address","internalType":"address"},{"name":"","type":"address","internalType":"address"}],"outputs":[{"name":"amount","type":"uint160","internalType":"uint160"},{"name":"expiration","type":"uint48","internalType":"uint48"},{"name":"nonce","type":"uint48","internalType":"uint48"}],"stateMutability":"view"},{"type":"function","name":"approve","inputs":[{"name":"token","type":"address","internalType":"address"},{"name":"spender","type":"address","internalType":"address"},{"name":"amount","type":"uint160","internalType":"uint160"},{"name":"expiration","type":"uint48","internalType":"uint48"}],"outputs":[],"stateMutability":"nonpayable"},{"type":"function","name":"invalidateNonces","inputs":[{"name":"token","type":"address","internalType":"address"},{"name":"spender","type":"address","internalType":"address"},{"name":"newNonce","type":"uint48","internalType":"uint48"}],"outputs":[],"stateMutability":"nonpayable"},{"type":"function","name":"invalidateUnorderedNonces","inputs":[{"name":"wordPos","type":"uint256","internalType":"uint256"},{"name":"mask","type":"uint256","internalType":"uint256"}],"outputs":[],"stateMutability":"nonpayable"},{"type":"function","name":"lockdown","inputs":[{"name":"approvals","type":"tuple[]","internalType":"struct IAllowanceTransfer.TokenSpenderPair[]","components":[{"name":"token","type":"address","internalType":"address"},{"name":"spender","type":"address","internalType":"address"}]}],"outputs":[],"stateMutability":"nonpayable"},{"type":"function","name":"nonceBitmap","inputs":[{"name":"","type":"address","internalType":"address"},{"name":"","type":"uint256","internalType":"uint256"}],"outputs":[{"name":"","type":"uint256","internalType":"uint256"}],"stateMutability":"view"},{"type":"function","name":"permit","inputs":[{"name":"owner","type":"address","internalType":"address"},{"name":"permitBatch","type":"tuple","internalType":"struct IAllowanceTransfer.PermitBatch","components":[{"name":"details","type":"tuple[]","internalType":"struct IAllowanceTransfer.PermitDetails[]","components":[{"name":"token","type":"address","internalType":"address"},{"name":"amount","type":"uint160","internalType":"uint160"},{"name":"expiration","type":"uint48","internalType":"uint48"},{"name":"nonce","type":"uint48","internalType":"uint48"}]},{"name":"spender","type":"address","internalType":"address"},{"name":"sigDeadline","type":"uint256","internalType":"uint256"}]},{"name":"signature","type":"bytes","internalType":"bytes"}],"outputs":[],"stateMutability":"nonpayable"},{"type":"function","name":"permit","inputs":[{"name":"owner","type":"address","internalType":"address"},{"name":"permitSingle","type":"tuple","internalType":"struct IAllowanceTransfer.PermitSingle","components":[{"name":"details","type":"tuple","internalType":"struct IAllowanceTransfer.PermitDetails","components":[{"name":"token","type":"address","internalType":"address"},{"name":"amount","type":"uint160","internalType":"uint160"},{"name":"expiration","type":"uint48","internalType":"uint48"},{"name":"nonce","type":"uint48","internalType":"uint48"}]},{"name":"spender","type":"address","internalType":"address"},{"name":"sigDeadline","type":"uint256","internalType":"uint256"}]},{"name":"signature","type":"bytes","internalType":"bytes"}],"outputs":[],"stateMutability":"nonpayable"},{"type":"function","name":"permitTransferFrom","inputs":[{"name":"permit","type":"tuple","internalType":"struct ISignatureTransfer.PermitTransferFrom","components":[{"name":"permitted","type":"tuple","internalType":"struct ISignatureTransfer.TokenPermissions","components":[{"name":"token","type":"address","internalType":"address"},{"name":"amount","type":"uint256","internalType":"uint256"}]},{"name":"nonce","type":"uint256","internalType":"uint256"},{"name":"deadline","type":"uint256","internalType":"uint256"}]},{"name":"transferDetails","type":"tuple","internalType":"struct ISignatureTransfer.SignatureTransferDetails","components":[{"name":"to","type":"address","internalType":"address"},{"name":"requestedAmount","type":"uint256","internalType":"uint256"}]},{"name":"owner","type":"address","internalType":"address"},{"name":"signature","type":"bytes","internalType":"bytes"}],"outputs":[],"stateMutability":"nonpayable"},{"type":"function","name":"permitTransferFrom","inputs":[{"name":"permit","type":"tuple","internalType":"struct ISignatureTransfer.PermitBatchTransferFrom","components":[{"name":"permitted","type":"tuple[]","internalType":"struct ISignatureTransfer.TokenPermissions[]","components":[{"name":"token","type":"address","internalType":"address"},{"name":"amount","type":"uint256","internalType":"uint256"}]},{"name":"nonce","type":"uint256","internalType":"uint256"},{"name":"deadline","type":"uint256","internalType":"uint256"}]},{"name":"transferDetails","type":"tuple[]","internalType":"struct ISignatureTransfer.SignatureTransferDetails[]","components":[{"name":"to","type":"address","internalType":"address"},{"name":"requestedAmount","type":"uint256","internalType":"uint256"}]},{"name":"owner","type":"address","internalType":"address"},{"name":"signature","type":"bytes","internalType":"bytes"}],"outputs":[],"stateMutability":"nonpayable"},{"type":"function","name":"permitWitnessTransferFrom","inputs":[{"name":"permit","type":"tuple","internalType":"struct ISignatureTransfer.PermitTransferFrom","components":[{"name":"permitted","type":"tuple","internalType":"struct ISignatureTransfer.TokenPermissions","components":[{"name":"token","type":"address","internalType":"address"},{"name":"amount","type":"uint256","internalType":"uint256"}]},{"name":"nonce","type":"uint256","internalType":"uint256"},{"name":"deadline","type":"uint256","internalType":"uint256"}]},{"name":"transferDetails","type":"tuple","internalType":"struct ISignatureTransfer.SignatureTransferDetails","components":[{"name":"to","type":"address","internalType":"address"},{"name":"requestedAmount","type":"uint256","internalType":"uint256"}]},{"name":"owner","type":"address","internalType":"address"},{"name":"witness","type":"bytes32","internalType":"bytes32"},{"name":"witnessTypeString","type":"string","internalType":"string"},{"name":"signature","type":"bytes","internalType":"bytes"}],"outputs":[],"stateMutability":"nonpayable"},{"type":"function","name":"permitWitnessTransferFrom","inputs":[{"name":"permit","type":"tuple","internalType":"struct ISignatureTransfer.PermitBatchTransferFrom","components":[{"name":"permitted","type":"tuple[]","internalType":"struct ISignatureTransfer.TokenPermissions[]","components":[{"name":"token","type":"address","internalType":"address"},{"name":"amount","type":"uint256","internalType":"uint256"}]},{"name":"nonce","type":"uint256","internalType":"uint256"},{"name":"deadline","type":"uint256","internalType":"uint256"}]},{"name":"transferDetails","type":"tuple[]","internalType":"struct ISignatureTransfer.SignatureTransferDetails[]","components":[{"name":"to","type":"address","internalType":"address"},{"name":"requestedAmount","type":"uint256","internalType":"uint256"}]},{"name":"owner","type":"address","internalType":"address"},{"name":"witness","type":"bytes32","internalType":"bytes32"},{"name":"witnessTypeString","type":"string","internalType":"string"},{"name":"signature","type":"bytes","internalType":"bytes"}],"outputs":[],"stateMutability":"nonpayable"},{"type":"function","name":"transferFrom","inputs":[{"name":"transferDetails","type":"tuple[]","internalType":"struct IAllowanceTransfer.AllowanceTransferDetails[]","components":[{"name":"from","type":"address","internalType":"address"},{"name":"to","type":"address","internalType":"address"},{"name":"amount","type":"uint160","internalType":"uint160"},{"name":"token","type":"address","internalType":"address"}]}],"outputs":[],"stateMutability":"nonpayable"},{"type":"function","name":"transferFrom","inputs":[{"name":"from","type":"address","internalType":"address"},{"name":"to","type":"address","internalType":"address"},{"name":"amount","type":"uint160","internalType":"uint160"},{"name":"token","type":"address","internalType":"address"}],"outputs":[],"stateMutability":"nonpayable"},{"type":"event","name":"Approval","inputs":[{"name":"owner","type":"address","indexed":true,"internalType":"address"},{"name":"token","type":"address","indexed":true,"internalType":"address"},{"name":"spender","type":"address","indexed":true,"internalType":"address"},{"name":"amount","type":"uint160","indexed":false,"internalType":"uint160"},{"name":"expiration","type":"uint48","indexed":false,"internalType":"uint48"}],"anonymous":false},{"type":"event","name":"Lockdown","inputs":[{"name":"owner","type":"address","indexed":true,"internalType":"address"},{"name":"token","type":"address","indexed":false,"internalType":"address"},{"name":"spender","type":"address","indexed":false,"internalType":"address"}],"anonymous":false},{"type":"event","name":"NonceInvalidation","inputs":[{"name":"owner","type":"address","indexed":true,"internalType":"address"},{"name":"token","type":"address","indexed":true,"internalType":"address"},{"name":"spender","type":"address","indexed":true,"internalType":"address"},{"name":"newNonce","type":"uint48","indexed":false,"internalType":"uint48"},{"name":"oldNonce","type":"uint48","indexed":false,"internalType":"uint48"}],"anonymous":false},{"type":"event","name":"Permit","inputs":[{"name":"owner","type":"address","indexed":true,"internalType":"address"},{"name":"token","type":"address","indexed":true,"internalType":"address"},{"name":"spender","type":"address","indexed":true,"internalType":"address"},{"name":"amount","type":"uint160","indexed":false,"internalType":"uint160"},{"name":"expiration","type":"uint48","indexed":false,"internalType":"uint48"},{"name":"nonce","type":"uint48","indexed":false,"internalType":"uint48"}],"anonymous":false},{"type":"event","name":"UnorderedNonceInvalidation","inputs":[{"name":"owner","type":"address","indexed":true,"internalType":"address"},{"name":"word","type":"uint256","indexed":false,"internalType":"uint256"},{"name":"mask","type":"uint256","indexed":false,"internalType":"uint256"}],"anonymous":false},{"type":"error","name":"AllowanceExpired","inputs":[{"name":"deadline","type":"uint256","internalType":"uint256"}]},{"type":"error","name":"ExcessiveInvalidation","inputs":[]},{"type":"error","name":"InsufficientAllowance","inputs":[{"name":"amount","type":"uint256","internalType":"uint256"}]},{"type":"error","name":"InvalidAmount","inputs":[{"name":"maxAmount","type":"uint256","internalType":"uint256"}]},{"type":"error","name":"InvalidContractSignature","inputs":[]},{"type":"error","name":"InvalidNonce","inputs":[]},{"type":"error","name":"InvalidSignature","inputs":[]},{"type":"error","name":"InvalidSignatureLength","inputs":[]},{"type":"error","name":"InvalidSigner","inputs":[]},{"type":"error","name":"LengthMismatch","inputs":[]},{"type":"error","name":"SignatureExpired","inputs":[{"name":"signatureDeadline","type":"uint256","internalType":"uint256"}]}],"bytecode":{"object":"0x60c0346100bb574660a052602081017f8cad95687ba82c2ce50e74f7b754645e5117c3a5bec8151c0726d5857980a86681527f9ac997416e8ff9d2ff6bebeb7149f65cdae5e32e2b90440b566bb3044041d36a60408301524660608301523060808301526080825260a082019180831060018060401b038411176100a557826040525190206080526123c090816100c1823960805181611b47015260a05181611b210152f35b634e487b7160e01b600052604160045260246000fd5b600080fdfe6040608081526004908136101561001557600080fd5b600090813560e01c80630d58b1db1461126c578063137c29fe146110755780632a2d80d114610db75780632b67b57014610bde57806330f28b7a14610ade5780633644e51514610a9d57806336c7851614610a285780633ff9dcb1146109a85780634fe02b441461093f57806365d9723c146107ac57806387517c451461067a578063927da105146105c3578063cc53287f146104a3578063edd9444b1461033a5763fe8ec1a7146100c657600080fd5b346103365760c07ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffc3601126103365767ffffffffffffffff833581811161033257610114903690860161164b565b60243582811161032e5761012b903690870161161a565b6101336114e6565b9160843585811161032a5761014b9036908a016115c1565b98909560a43590811161032657610164913691016115c1565b969095815190610173826113ff565b606b82527f5065726d697442617463685769746e6573735472616e7366657246726f6d285460208301527f6f6b656e5065726d697373696f6e735b5d207065726d69747465642c61646472838301527f657373207370656e6465722c75696e74323536206e6f6e63652c75696e74323560608301527f3620646561646c696e652c000000000000000000000000000000000000000000608083015282519a8b9181610222602085018096611f93565b918237018a8152039961025b7fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffe09b8c8101835282611437565b5190209085515161026b81611ebb565b908a5b8181106102f95750506102f6999a6102ed9183516102a081610294602082018095611f66565b03848101835282611437565b519020602089810151858b015195519182019687526040820192909252336060820152608081019190915260a081019390935260643560c08401528260e081015b03908101835282611437565b51902093611cf7565b80f35b8061031161030b610321938c5161175e565b51612054565b61031b828661175e565b52611f0a565b61026e565b8880fd5b8780fd5b8480fd5b8380fd5b5080fd5b5091346103365760807ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffc3601126103365767ffffffffffffffff9080358281116103325761038b903690830161164b565b60243583811161032e576103a2903690840161161a565b9390926103ad6114e6565b9160643590811161049f576103c4913691016115c1565b949093835151976103d489611ebb565b98885b81811061047d5750506102f697988151610425816103f9602082018095611f66565b037fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffe08101835282611437565b5190206020860151828701519083519260208401947ffcf35f5ac6a2c28868dc44c302166470266239195f02b0ee408334829333b7668652840152336060840152608083015260a082015260a081526102ed8161141b565b808b61031b8261049461030b61049a968d5161175e565b9261175e565b6103d7565b8680fd5b5082346105bf57602090817ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffc3601126103325780359067ffffffffffffffff821161032e576104f49136910161161a565b929091845b848110610504578580f35b8061051a610515600193888861196c565b61197c565b61052f84610529848a8a61196c565b0161197c565b3389528385528589209173ffffffffffffffffffffffffffffffffffffffff80911692838b528652868a20911690818a5285528589207fffffffffffffffffffffffff000000000000000000000000000000000000000081541690558551918252848201527f89b1add15eff56b3dfe299ad94e01f2b52fbcb80ae1a3baea6ae8c04cb2b98a4853392a2016104f9565b8280fd5b50346103365760607ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffc36011261033657610676816105ff6114a0565b936106086114c3565b6106106114e6565b73ffffffffffffffffffffffffffffffffffffffff968716835260016020908152848420928816845291825283832090871683528152919020549251938316845260a083901c65ffffffffffff169084015260d09190911c604083015281906060820190565b0390f35b50346103365760807ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffc360112610336576106b26114a0565b906106bb6114c3565b916106c46114e6565b65ffffffffffff926064358481169081810361032a5779ffffffffffff0000000000000000000000000000000000000000947fda9fa7c1b00402c17d0161b249b1ab8bbec047c5a52207b9c112deffd817036b94338a5260016020527fffffffffffff0000000000000000000000000000000000000000000000000000858b209873ffffffffffffffffffffffffffffffffffffffff809416998a8d5260205283878d209b169a8b8d52602052868c209486156000146107a457504216925b8454921697889360a01b16911617179055815193845260208401523392a480f35b905092610783565b5082346105bf5760607ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffc3601126105bf576107e56114a0565b906107ee6114c3565b9265ffffffffffff604435818116939084810361032a57338852602091600183528489209673ffffffffffffffffffffffffffffffffffffffff80911697888b528452858a20981697888a5283528489205460d01c93848711156109175761ffff9085840316116108f05750907f55eb90d810e1700b35a8e7e25395ff7f2b2259abd7415ca2284dfb1c246418f393929133895260018252838920878a528252838920888a5282528389209079ffffffffffffffffffffffffffffffffffffffffffffffffffff7fffffffffffff000000000000000000000000000000000000000000000000000083549260d01b16911617905582519485528401523392a480f35b84517f24d35a26000000000000000000000000000000000000000000000000000000008152fd5b5084517f756688fe000000000000000000000000000000000000000000000000000000008152fd5b503461033657807ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffc360112610336578060209273ffffffffffffffffffffffffffffffffffffffff61098f6114a0565b1681528084528181206024358252845220549051908152f35b5082346105bf57817ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffc3601126105bf577f3704902f963766a4e561bbaab6e6cdc1b1dd12f6e9e99648da8843b3f46b918d90359160243533855284602052818520848652602052818520818154179055815193845260208401523392a280f35b8234610a9a5760807ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffc360112610a9a57610a606114a0565b610a686114c3565b610a706114e6565b6064359173ffffffffffffffffffffffffffffffffffffffff8316830361032e576102f6936117a1565b80fd5b503461033657817ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffc36011261033657602090610ad7611b1e565b9051908152f35b508290346105bf576101007ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffc3601126105bf57610b1a3661152a565b90807fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff7c36011261033257610b4c611478565b9160e43567ffffffffffffffff8111610bda576102f694610b6f913691016115c1565b939092610b7c8351612054565b6020840151828501519083519260208401947f939c21a48a8dbe3a9a2404a1d46691e4d39f6583d6ec6b35714604c986d801068652840152336060840152608083015260a082015260a08152610bd18161141b565b51902091611c25565b8580fd5b509134610336576101007ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffc36011261033657610c186114a0565b7fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffdc360160c08112610332576080855191610c51836113e3565b1261033257845190610c6282611398565b73ffffffffffffffffffffffffffffffffffffffff91602435838116810361049f578152604435838116810361049f57602082015265ffffffffffff606435818116810361032a5788830152608435908116810361049f576060820152815260a435938285168503610bda576020820194855260c4359087830182815260e43567ffffffffffffffff811161032657610cfe90369084016115c1565b929093804211610d88575050918591610d786102f6999a610d7e95610d238851611fbe565b90898c511690519083519260208401947ff3841cd1ff0085026a6327b620b67997ce40f282c88a8e905a7a5626e310f3d086528401526060830152608082015260808152610d70816113ff565b519020611bd9565b916120c7565b519251169161199d565b602492508a51917fcd21db4f000000000000000000000000000000000000000000000000000000008352820152fd5b5091346103365760607ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffc93818536011261033257610df36114a0565b9260249081359267ffffffffffffffff9788851161032a578590853603011261049f578051978589018981108282111761104a578252848301358181116103265785019036602383011215610326578382013591610e50836115ef565b90610e5d85519283611437565b838252602093878584019160071b83010191368311611046578801905b828210610fe9575050508a526044610e93868801611509565b96838c01978852013594838b0191868352604435908111610fe557610ebb90369087016115c1565b959096804211610fba575050508998995151610ed681611ebb565b908b5b818110610f9757505092889492610d7892610f6497958351610f02816103f98682018095611f66565b5190209073ffffffffffffffffffffffffffffffffffffffff9a8b8b51169151928551948501957faf1b0d30d2cab0380e68f0689007e3254993c596f2fdd0aaa7f4d04f794408638752850152830152608082015260808152610d70816113ff565b51169082515192845b848110610f78578580f35b80610f918585610f8b600195875161175e565b5161199d565b01610f6d565b80610311610fac8e9f9e93610fb2945161175e565b51611fbe565b9b9a9b610ed9565b8551917fcd21db4f000000000000000000000000000000000000000000000000000000008352820152fd5b8a80fd5b6080823603126110465785608091885161100281611398565b61100b85611509565b8152611018838601611509565b838201526110278a8601611607565b8a8201528d611037818701611607565b90820152815201910190610e7a565b8c80fd5b84896041867f4e487b7100000000000000000000000000000000000000000000000000000000835252fd5b5082346105bf576101407ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffc3601126105bf576110b03661152a565b91807fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff7c360112610332576110e2611478565b67ffffffffffffffff93906101043585811161049f5761110590369086016115c1565b90936101243596871161032a57611125610bd1966102f6983691016115c1565b969095825190611134826113ff565b606482527f5065726d69745769746e6573735472616e7366657246726f6d28546f6b656e5060208301527f65726d697373696f6e73207065726d69747465642c6164647265737320737065848301527f6e6465722c75696e74323536206e6f6e63652c75696e7432353620646561646c60608301527f696e652c0000000000000000000000000000000000000000000000000000000060808301528351948591816111e3602085018096611f93565b918237018b8152039361121c7fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffe095868101835282611437565b5190209261122a8651612054565b6020878101518589015195519182019687526040820192909252336060820152608081019190915260a081019390935260e43560c08401528260e081016102e1565b5082346105bf576020807ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffc36011261033257813567ffffffffffffffff92838211610bda5736602383011215610bda5781013592831161032e576024906007368386831b8401011161049f57865b8581106112e5578780f35b80821b83019060807fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffdc83360301126103265761139288876001946060835161132c81611398565b611368608461133c8d8601611509565b9485845261134c60448201611509565b809785015261135d60648201611509565b809885015201611509565b918291015273ffffffffffffffffffffffffffffffffffffffff80808093169516931691166117a1565b016112da565b6080810190811067ffffffffffffffff8211176113b457604052565b7f4e487b7100000000000000000000000000000000000000000000000000000000600052604160045260246000fd5b6060810190811067ffffffffffffffff8211176113b457604052565b60a0810190811067ffffffffffffffff8211176113b457604052565b60c0810190811067ffffffffffffffff8211176113b457604052565b90601f7fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffe0910116810190811067ffffffffffffffff8211176113b457604052565b60c4359073ffffffffffffffffffffffffffffffffffffffff8216820361149b57565b600080fd5b6004359073ffffffffffffffffffffffffffffffffffffffff8216820361149b57565b6024359073ffffffffffffffffffffffffffffffffffffffff8216820361149b57565b6044359073ffffffffffffffffffffffffffffffffffffffff8216820361149b57565b359073ffffffffffffffffffffffffffffffffffffffff8216820361149b57565b7ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffc01906080821261149b576040805190611563826113e3565b8082941261149b57805181810181811067ffffffffffffffff8211176113b457825260043573ffffffffffffffffffffffffffffffffffffffff8116810361149b578152602435602082015282526044356020830152606435910152565b9181601f8401121561149b5782359167ffffffffffffffff831161149b576020838186019501011161149b57565b67ffffffffffffffff81116113b45760051b60200190565b359065ffffffffffff8216820361149b57565b9181601f8401121561149b5782359167ffffffffffffffff831161149b576020808501948460061b01011161149b57565b91909160608184031261149b576040805191611666836113e3565b8294813567ffffffffffffffff9081811161149b57830182601f8201121561149b578035611693816115ef565b926116a087519485611437565b818452602094858086019360061b8501019381851161149b579086899897969594939201925b8484106116e3575050505050855280820135908501520135910152565b90919293949596978483031261149b578851908982019082821085831117611730578a928992845261171487611509565b81528287013583820152815201930191908897969594936116c6565b602460007f4e487b710000000000000000000000000000000000000000000000000000000081526041600452fd5b80518210156117725760209160051b010190565b7f4e487b7100000000000000000000000000000000000000000000000000000000600052603260045260246000fd5b92919273ffffffffffffffffffffffffffffffffffffffff604060008284168152600160205282828220961695868252602052818120338252602052209485549565ffffffffffff8760a01c16804211611884575082871696838803611812575b5050611810955016926118b5565b565b878484161160001461184f57602488604051907ff96fb0710000000000000000000000000000000000000000000000000000000082526004820152fd5b7fffffffffffffffffffffffff000000000000000000000000000000000000000084846118109a031691161790553880611802565b602490604051907fd81b2f2e0000000000000000000000000000000000000000000000000000000082526004820152fd5b9060006064926020958295604051947f23b872dd0000000000000000000000000000000000000000000000000000000086526004860152602485015260448401525af13d15601f3d116001600051141617161561190e57565b60646040517f08c379a000000000000000000000000000000000000000000000000000000000815260206004820152601460248201527f5452414e534645525f46524f4d5f4641494c45440000000000000000000000006044820152fd5b91908110156117725760061b0190565b3573ffffffffffffffffffffffffffffffffffffffff8116810361149b5790565b9065ffffffffffff908160608401511673ffffffffffffffffffffffffffffffffffffffff908185511694826020820151169280866040809401511695169560009187835260016020528383208984526020528383209916988983526020528282209184835460d01c03611af5579185611ace94927fc6a377bfc4eb120024a8ac08eef205be16b817020812c73223e81d1bdb9708ec98979694508715600014611ad35779ffffffffffff00000000000000000000000000000000000000009042165b60a01b167fffffffffffff00000000000000000000000000000000000000000000000000006001860160d01b1617179055519384938491604091949373ffffffffffffffffffffffffffffffffffffffff606085019616845265ffffffffffff809216602085015216910152565b0390a4565b5079ffffffffffff000000000000000000000000000000000000000087611a60565b600484517f756688fe000000000000000000000000000000000000000000000000000000008152fd5b467f000000000000000000000000000000000000000000000000000000000000000003611b69577f000000000000000000000000000000000000000000000000000000000000000090565b60405160208101907f8cad95687ba82c2ce50e74f7b754645e5117c3a5bec8151c0726d5857980a86682527f9ac997416e8ff9d2ff6bebeb7149f65cdae5e32e2b90440b566bb3044041d36a604082015246606082015230608082015260808152611bd3816113ff565b51902090565b611be1611b1e565b906040519060208201927f190100000000000000000000000000000000000000000000000000000000000084526022830152604282015260428152611bd381611398565b9192909360a435936040840151804211611cc65750602084510151808611611c955750918591610d78611c6594611c60602088015186611e47565b611bd9565b73ffffffffffffffffffffffffffffffffffffffff809151511692608435918216820361149b57611810936118b5565b602490604051907f3728b83d0000000000000000000000000000000000000000000000000000000082526004820152fd5b602490604051907fcd21db4f0000000000000000000000000000000000000000000000000000000082526004820152fd5b959093958051519560409283830151804211611e175750848803611dee57611d2e918691610d7860209b611c608d88015186611e47565b60005b868110611d42575050505050505050565b611d4d81835161175e565b5188611d5a83878a61196c565b01359089810151808311611dbe575091818888886001968596611d84575b50505050505001611d31565b611db395611dad9273ffffffffffffffffffffffffffffffffffffffff6105159351169561196c565b916118b5565b803888888883611d78565b6024908651907f3728b83d0000000000000000000000000000000000000000000000000000000082526004820152fd5b600484517fff633a38000000000000000000000000000000000000000000000000000000008152fd5b6024908551907fcd21db4f0000000000000000000000000000000000000000000000000000000082526004820152fd5b9073ffffffffffffffffffffffffffffffffffffffff600160ff83161b9216600052600060205260406000209060081c6000526020526040600020818154188091551615611e9157565b60046040517f756688fe000000000000000000000000000000000000000000000000000000008152fd5b90611ec5826115ef565b611ed26040519182611437565b8281527fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffe0611f0082946115ef565b0190602036910137565b7fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff8114611f375760010190565b7f4e487b7100000000000000000000000000000000000000000000000000000000600052601160045260246000fd5b805160208092019160005b828110611f7f575050505090565b835185529381019392810192600101611f71565b9081519160005b838110611fab575050016000815290565b8060208092840101518185015201611f9a565b60405160208101917f65626cad6cb96493bf6f5ebea28756c966f023ab9e8a83a7101849d5573b3678835273ffffffffffffffffffffffffffffffffffffffff8082511660408401526020820151166060830152606065ffffffffffff9182604082015116608085015201511660a082015260a0815260c0810181811067ffffffffffffffff8211176113b45760405251902090565b6040516020808201927f618358ac3db8dc274f0cd8829da7e234bd48cd73c4a740aede1adec9846d06a1845273ffffffffffffffffffffffffffffffffffffffff81511660408401520151606082015260608152611bd381611398565b919082604091031261149b576020823592013590565b6000843b61222e5750604182036121ac576120e4828201826120b1565b939092604010156117725760209360009360ff6040608095013560f81c5b60405194855216868401526040830152606082015282805260015afa156121a05773ffffffffffffffffffffffffffffffffffffffff806000511691821561217657160361214c57565b60046040517f815e1d64000000000000000000000000000000000000000000000000000000008152fd5b60046040517f8baa579f000000000000000000000000000000000000000000000000000000008152fd5b6040513d6000823e3d90fd5b60408203612204576121c0918101906120b1565b91601b7f7fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff84169360ff1c019060ff8211611f375760209360009360ff608094612102565b60046040517f4be6321b000000000000000000000000000000000000000000000000000000008152fd5b929391601f928173ffffffffffffffffffffffffffffffffffffffff60646020957fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffe0604051988997889687947f1626ba7e000000000000000000000000000000000000000000000000000000009e8f8752600487015260406024870152816044870152868601378b85828601015201168101030192165afa9081156123a857829161232a575b507fffffffff000000000000000000000000000000000000000000000000000000009150160361230057565b60046040517fb0669cbc000000000000000000000000000000000000000000000000000000008152fd5b90506020813d82116123a0575b8161234460209383611437565b810103126103365751907fffffffff0000000000000000000000000000000000000000000000000000000082168203610a9a57507fffffffff0000000000000000000000000000000000000000000000000000000090386122d4565b3d9150612337565b6040513d84823e3d90fdfea164736f6c6343000811000a","sourceMap":"385:152:37:-:0;;;;918:13:36;899:32;;1631:60;;;788:80;385:152:37;;716:20:36;385:152:37;;;;918:13:36;385:152:37;;;;1685:4:36;385:152:37;;;;;1631:60:36;;899:32;385:152:37;;;;;;;;;;;;;;;;;;;;1621:71:36;;385:152:37;941:74:36;385:152:37;;;;;;;;;;;;899:32:36;385:152:37;;;;;;;;;;-1:-1:-1;385:152:37;;;;;-1:-1:-1;385:152:37;;;;","linkReferences":{}},"deployedBytecode":{"object":"0x6040608081526004908136101561001557600080fd5b600090813560e01c80630d58b1db1461126c578063137c29fe146110755780632a2d80d114610db75780632b67b57014610bde57806330f28b7a14610ade5780633644e51514610a9d57806336c7851614610a285780633ff9dcb1146109a85780634fe02b441461093f57806365d9723c146107ac57806387517c451461067a578063927da105146105c3578063cc53287f146104a3578063edd9444b1461033a5763fe8ec1a7146100c657600080fd5b346103365760c07ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffc3601126103365767ffffffffffffffff833581811161033257610114903690860161164b565b60243582811161032e5761012b903690870161161a565b6101336114e6565b9160843585811161032a5761014b9036908a016115c1565b98909560a43590811161032657610164913691016115c1565b969095815190610173826113ff565b606b82527f5065726d697442617463685769746e6573735472616e7366657246726f6d285460208301527f6f6b656e5065726d697373696f6e735b5d207065726d69747465642c61646472838301527f657373207370656e6465722c75696e74323536206e6f6e63652c75696e74323560608301527f3620646561646c696e652c000000000000000000000000000000000000000000608083015282519a8b9181610222602085018096611f93565b918237018a8152039961025b7fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffe09b8c8101835282611437565b5190209085515161026b81611ebb565b908a5b8181106102f95750506102f6999a6102ed9183516102a081610294602082018095611f66565b03848101835282611437565b519020602089810151858b015195519182019687526040820192909252336060820152608081019190915260a081019390935260643560c08401528260e081015b03908101835282611437565b51902093611cf7565b80f35b8061031161030b610321938c5161175e565b51612054565b61031b828661175e565b52611f0a565b61026e565b8880fd5b8780fd5b8480fd5b8380fd5b5080fd5b5091346103365760807ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffc3601126103365767ffffffffffffffff9080358281116103325761038b903690830161164b565b60243583811161032e576103a2903690840161161a565b9390926103ad6114e6565b9160643590811161049f576103c4913691016115c1565b949093835151976103d489611ebb565b98885b81811061047d5750506102f697988151610425816103f9602082018095611f66565b037fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffe08101835282611437565b5190206020860151828701519083519260208401947ffcf35f5ac6a2c28868dc44c302166470266239195f02b0ee408334829333b7668652840152336060840152608083015260a082015260a081526102ed8161141b565b808b61031b8261049461030b61049a968d5161175e565b9261175e565b6103d7565b8680fd5b5082346105bf57602090817ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffc3601126103325780359067ffffffffffffffff821161032e576104f49136910161161a565b929091845b848110610504578580f35b8061051a610515600193888861196c565b61197c565b61052f84610529848a8a61196c565b0161197c565b3389528385528589209173ffffffffffffffffffffffffffffffffffffffff80911692838b528652868a20911690818a5285528589207fffffffffffffffffffffffff000000000000000000000000000000000000000081541690558551918252848201527f89b1add15eff56b3dfe299ad94e01f2b52fbcb80ae1a3baea6ae8c04cb2b98a4853392a2016104f9565b8280fd5b50346103365760607ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffc36011261033657610676816105ff6114a0565b936106086114c3565b6106106114e6565b73ffffffffffffffffffffffffffffffffffffffff968716835260016020908152848420928816845291825283832090871683528152919020549251938316845260a083901c65ffffffffffff169084015260d09190911c604083015281906060820190565b0390f35b50346103365760807ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffc360112610336576106b26114a0565b906106bb6114c3565b916106c46114e6565b65ffffffffffff926064358481169081810361032a5779ffffffffffff0000000000000000000000000000000000000000947fda9fa7c1b00402c17d0161b249b1ab8bbec047c5a52207b9c112deffd817036b94338a5260016020527fffffffffffff0000000000000000000000000000000000000000000000000000858b209873ffffffffffffffffffffffffffffffffffffffff809416998a8d5260205283878d209b169a8b8d52602052868c209486156000146107a457504216925b8454921697889360a01b16911617179055815193845260208401523392a480f35b905092610783565b5082346105bf5760607ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffc3601126105bf576107e56114a0565b906107ee6114c3565b9265ffffffffffff604435818116939084810361032a57338852602091600183528489209673ffffffffffffffffffffffffffffffffffffffff80911697888b528452858a20981697888a5283528489205460d01c93848711156109175761ffff9085840316116108f05750907f55eb90d810e1700b35a8e7e25395ff7f2b2259abd7415ca2284dfb1c246418f393929133895260018252838920878a528252838920888a5282528389209079ffffffffffffffffffffffffffffffffffffffffffffffffffff7fffffffffffff000000000000000000000000000000000000000000000000000083549260d01b16911617905582519485528401523392a480f35b84517f24d35a26000000000000000000000000000000000000000000000000000000008152fd5b5084517f756688fe000000000000000000000000000000000000000000000000000000008152fd5b503461033657807ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffc360112610336578060209273ffffffffffffffffffffffffffffffffffffffff61098f6114a0565b1681528084528181206024358252845220549051908152f35b5082346105bf57817ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffc3601126105bf577f3704902f963766a4e561bbaab6e6cdc1b1dd12f6e9e99648da8843b3f46b918d90359160243533855284602052818520848652602052818520818154179055815193845260208401523392a280f35b8234610a9a5760807ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffc360112610a9a57610a606114a0565b610a686114c3565b610a706114e6565b6064359173ffffffffffffffffffffffffffffffffffffffff8316830361032e576102f6936117a1565b80fd5b503461033657817ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffc36011261033657602090610ad7611b1e565b9051908152f35b508290346105bf576101007ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffc3601126105bf57610b1a3661152a565b90807fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff7c36011261033257610b4c611478565b9160e43567ffffffffffffffff8111610bda576102f694610b6f913691016115c1565b939092610b7c8351612054565b6020840151828501519083519260208401947f939c21a48a8dbe3a9a2404a1d46691e4d39f6583d6ec6b35714604c986d801068652840152336060840152608083015260a082015260a08152610bd18161141b565b51902091611c25565b8580fd5b509134610336576101007ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffc36011261033657610c186114a0565b7fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffdc360160c08112610332576080855191610c51836113e3565b1261033257845190610c6282611398565b73ffffffffffffffffffffffffffffffffffffffff91602435838116810361049f578152604435838116810361049f57602082015265ffffffffffff606435818116810361032a5788830152608435908116810361049f576060820152815260a435938285168503610bda576020820194855260c4359087830182815260e43567ffffffffffffffff811161032657610cfe90369084016115c1565b929093804211610d88575050918591610d786102f6999a610d7e95610d238851611fbe565b90898c511690519083519260208401947ff3841cd1ff0085026a6327b620b67997ce40f282c88a8e905a7a5626e310f3d086528401526060830152608082015260808152610d70816113ff565b519020611bd9565b916120c7565b519251169161199d565b602492508a51917fcd21db4f000000000000000000000000000000000000000000000000000000008352820152fd5b5091346103365760607ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffc93818536011261033257610df36114a0565b9260249081359267ffffffffffffffff9788851161032a578590853603011261049f578051978589018981108282111761104a578252848301358181116103265785019036602383011215610326578382013591610e50836115ef565b90610e5d85519283611437565b838252602093878584019160071b83010191368311611046578801905b828210610fe9575050508a526044610e93868801611509565b96838c01978852013594838b0191868352604435908111610fe557610ebb90369087016115c1565b959096804211610fba575050508998995151610ed681611ebb565b908b5b818110610f9757505092889492610d7892610f6497958351610f02816103f98682018095611f66565b5190209073ffffffffffffffffffffffffffffffffffffffff9a8b8b51169151928551948501957faf1b0d30d2cab0380e68f0689007e3254993c596f2fdd0aaa7f4d04f794408638752850152830152608082015260808152610d70816113ff565b51169082515192845b848110610f78578580f35b80610f918585610f8b600195875161175e565b5161199d565b01610f6d565b80610311610fac8e9f9e93610fb2945161175e565b51611fbe565b9b9a9b610ed9565b8551917fcd21db4f000000000000000000000000000000000000000000000000000000008352820152fd5b8a80fd5b6080823603126110465785608091885161100281611398565b61100b85611509565b8152611018838601611509565b838201526110278a8601611607565b8a8201528d611037818701611607565b90820152815201910190610e7a565b8c80fd5b84896041867f4e487b7100000000000000000000000000000000000000000000000000000000835252fd5b5082346105bf576101407ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffc3601126105bf576110b03661152a565b91807fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff7c360112610332576110e2611478565b67ffffffffffffffff93906101043585811161049f5761110590369086016115c1565b90936101243596871161032a57611125610bd1966102f6983691016115c1565b969095825190611134826113ff565b606482527f5065726d69745769746e6573735472616e7366657246726f6d28546f6b656e5060208301527f65726d697373696f6e73207065726d69747465642c6164647265737320737065848301527f6e6465722c75696e74323536206e6f6e63652c75696e7432353620646561646c60608301527f696e652c0000000000000000000000000000000000000000000000000000000060808301528351948591816111e3602085018096611f93565b918237018b8152039361121c7fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffe095868101835282611437565b5190209261122a8651612054565b6020878101518589015195519182019687526040820192909252336060820152608081019190915260a081019390935260e43560c08401528260e081016102e1565b5082346105bf576020807ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffc36011261033257813567ffffffffffffffff92838211610bda5736602383011215610bda5781013592831161032e576024906007368386831b8401011161049f57865b8581106112e5578780f35b80821b83019060807fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffdc83360301126103265761139288876001946060835161132c81611398565b611368608461133c8d8601611509565b9485845261134c60448201611509565b809785015261135d60648201611509565b809885015201611509565b918291015273ffffffffffffffffffffffffffffffffffffffff80808093169516931691166117a1565b016112da565b6080810190811067ffffffffffffffff8211176113b457604052565b7f4e487b7100000000000000000000000000000000000000000000000000000000600052604160045260246000fd5b6060810190811067ffffffffffffffff8211176113b457604052565b60a0810190811067ffffffffffffffff8211176113b457604052565b60c0810190811067ffffffffffffffff8211176113b457604052565b90601f7fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffe0910116810190811067ffffffffffffffff8211176113b457604052565b60c4359073ffffffffffffffffffffffffffffffffffffffff8216820361149b57565b600080fd5b6004359073ffffffffffffffffffffffffffffffffffffffff8216820361149b57565b6024359073ffffffffffffffffffffffffffffffffffffffff8216820361149b57565b6044359073ffffffffffffffffffffffffffffffffffffffff8216820361149b57565b359073ffffffffffffffffffffffffffffffffffffffff8216820361149b57565b7ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffc01906080821261149b576040805190611563826113e3565b8082941261149b57805181810181811067ffffffffffffffff8211176113b457825260043573ffffffffffffffffffffffffffffffffffffffff8116810361149b578152602435602082015282526044356020830152606435910152565b9181601f8401121561149b5782359167ffffffffffffffff831161149b576020838186019501011161149b57565b67ffffffffffffffff81116113b45760051b60200190565b359065ffffffffffff8216820361149b57565b9181601f8401121561149b5782359167ffffffffffffffff831161149b576020808501948460061b01011161149b57565b91909160608184031261149b576040805191611666836113e3565b8294813567ffffffffffffffff9081811161149b57830182601f8201121561149b578035611693816115ef565b926116a087519485611437565b818452602094858086019360061b8501019381851161149b579086899897969594939201925b8484106116e3575050505050855280820135908501520135910152565b90919293949596978483031261149b578851908982019082821085831117611730578a928992845261171487611509565b81528287013583820152815201930191908897969594936116c6565b602460007f4e487b710000000000000000000000000000000000000000000000000000000081526041600452fd5b80518210156117725760209160051b010190565b7f4e487b7100000000000000000000000000000000000000000000000000000000600052603260045260246000fd5b92919273ffffffffffffffffffffffffffffffffffffffff604060008284168152600160205282828220961695868252602052818120338252602052209485549565ffffffffffff8760a01c16804211611884575082871696838803611812575b5050611810955016926118b5565b565b878484161160001461184f57602488604051907ff96fb0710000000000000000000000000000000000000000000000000000000082526004820152fd5b7fffffffffffffffffffffffff000000000000000000000000000000000000000084846118109a031691161790553880611802565b602490604051907fd81b2f2e0000000000000000000000000000000000000000000000000000000082526004820152fd5b9060006064926020958295604051947f23b872dd0000000000000000000000000000000000000000000000000000000086526004860152602485015260448401525af13d15601f3d116001600051141617161561190e57565b60646040517f08c379a000000000000000000000000000000000000000000000000000000000815260206004820152601460248201527f5452414e534645525f46524f4d5f4641494c45440000000000000000000000006044820152fd5b91908110156117725760061b0190565b3573ffffffffffffffffffffffffffffffffffffffff8116810361149b5790565b9065ffffffffffff908160608401511673ffffffffffffffffffffffffffffffffffffffff908185511694826020820151169280866040809401511695169560009187835260016020528383208984526020528383209916988983526020528282209184835460d01c03611af5579185611ace94927fc6a377bfc4eb120024a8ac08eef205be16b817020812c73223e81d1bdb9708ec98979694508715600014611ad35779ffffffffffff00000000000000000000000000000000000000009042165b60a01b167fffffffffffff00000000000000000000000000000000000000000000000000006001860160d01b1617179055519384938491604091949373ffffffffffffffffffffffffffffffffffffffff606085019616845265ffffffffffff809216602085015216910152565b0390a4565b5079ffffffffffff000000000000000000000000000000000000000087611a60565b600484517f756688fe000000000000000000000000000000000000000000000000000000008152fd5b467f000000000000000000000000000000000000000000000000000000000000000003611b69577f000000000000000000000000000000000000000000000000000000000000000090565b60405160208101907f8cad95687ba82c2ce50e74f7b754645e5117c3a5bec8151c0726d5857980a86682527f9ac997416e8ff9d2ff6bebeb7149f65cdae5e32e2b90440b566bb3044041d36a604082015246606082015230608082015260808152611bd3816113ff565b51902090565b611be1611b1e565b906040519060208201927f190100000000000000000000000000000000000000000000000000000000000084526022830152604282015260428152611bd381611398565b9192909360a435936040840151804211611cc65750602084510151808611611c955750918591610d78611c6594611c60602088015186611e47565b611bd9565b73ffffffffffffffffffffffffffffffffffffffff809151511692608435918216820361149b57611810936118b5565b602490604051907f3728b83d0000000000000000000000000000000000000000000000000000000082526004820152fd5b602490604051907fcd21db4f0000000000000000000000000000000000000000000000000000000082526004820152fd5b959093958051519560409283830151804211611e175750848803611dee57611d2e918691610d7860209b611c608d88015186611e47565b60005b868110611d42575050505050505050565b611d4d81835161175e565b5188611d5a83878a61196c565b01359089810151808311611dbe575091818888886001968596611d84575b50505050505001611d31565b611db395611dad9273ffffffffffffffffffffffffffffffffffffffff6105159351169561196c565b916118b5565b803888888883611d78565b6024908651907f3728b83d0000000000000000000000000000000000000000000000000000000082526004820152fd5b600484517fff633a38000000000000000000000000000000000000000000000000000000008152fd5b6024908551907fcd21db4f0000000000000000000000000000000000000000000000000000000082526004820152fd5b9073ffffffffffffffffffffffffffffffffffffffff600160ff83161b9216600052600060205260406000209060081c6000526020526040600020818154188091551615611e9157565b60046040517f756688fe000000000000000000000000000000000000000000000000000000008152fd5b90611ec5826115ef565b611ed26040519182611437565b8281527fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffe0611f0082946115ef565b0190602036910137565b7fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff8114611f375760010190565b7f4e487b7100000000000000000000000000000000000000000000000000000000600052601160045260246000fd5b805160208092019160005b828110611f7f575050505090565b835185529381019392810192600101611f71565b9081519160005b838110611fab575050016000815290565b8060208092840101518185015201611f9a565b60405160208101917f65626cad6cb96493bf6f5ebea28756c966f023ab9e8a83a7101849d5573b3678835273ffffffffffffffffffffffffffffffffffffffff8082511660408401526020820151166060830152606065ffffffffffff9182604082015116608085015201511660a082015260a0815260c0810181811067ffffffffffffffff8211176113b45760405251902090565b6040516020808201927f618358ac3db8dc274f0cd8829da7e234bd48cd73c4a740aede1adec9846d06a1845273ffffffffffffffffffffffffffffffffffffffff81511660408401520151606082015260608152611bd381611398565b919082604091031261149b576020823592013590565b6000843b61222e5750604182036121ac576120e4828201826120b1565b939092604010156117725760209360009360ff6040608095013560f81c5b60405194855216868401526040830152606082015282805260015afa156121a05773ffffffffffffffffffffffffffffffffffffffff806000511691821561217657160361214c57565b60046040517f815e1d64000000000000000000000000000000000000000000000000000000008152fd5b60046040517f8baa579f000000000000000000000000000000000000000000000000000000008152fd5b6040513d6000823e3d90fd5b60408203612204576121c0918101906120b1565b91601b7f7fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff84169360ff1c019060ff8211611f375760209360009360ff608094612102565b60046040517f4be6321b000000000000000000000000000000000000000000000000000000008152fd5b929391601f928173ffffffffffffffffffffffffffffffffffffffff60646020957fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffe0604051988997889687947f1626ba7e000000000000000000000000000000000000000000000000000000009e8f8752600487015260406024870152816044870152868601378b85828601015201168101030192165afa9081156123a857829161232a575b507fffffffff000000000000000000000000000000000000000000000000000000009150160361230057565b60046040517fb0669cbc000000000000000000000000000000000000000000000000000000008152fd5b90506020813d82116123a0575b8161234460209383611437565b810103126103365751907fffffffff0000000000000000000000000000000000000000000000000000000082168203610a9a57507fffffffff0000000000000000000000000000000000000000000000000000000090386122d4565b3d9150612337565b6040513d84823e3d90fdfea164736f6c6343000811000a","sourceMap":"385:152:37:-:0;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;:::i;:::-;;;;;;;;;;;;;;;:::i;:::-;;;:::i;:::-;;;;;;;;;;;;;;;;:::i;:::-;;;;;;;;;;;;;;;;;:::i;:::-;;;;;;;;;;:::i;:::-;;;;;;;;;;;;;;;;;;;;;;;;;;4678:86:48;;;;1621:102;385:152:37;4678:86:48;;1621:102;;;:::i;:::-;;;;;;;;4678:86;;;;;;;;;;;;:::i;:::-;385:152:37;4668:97:48;;4799:16;;;385:152:37;4873:27:48;;;:::i;:::-;4916:13;;4931:16;;;;;;385:152:37;;3581:9:39;385:152:37;;5088:234:48;385:152:37;;;5152:39:48;;661:173;385:152:37;5152:39:48;;661:173;;;:::i;:::-;5152:39;;;;;;;;:::i;:::-;385:152:37;5142:50:48;;385:152:37;5238:12:48;;;385:152:37;5268:15:48;;;385:152:37;;;5088:234:48;;;385:152:37;;;1621:102:48;;;385:152:37;;;;5210:10:48;1621:102;;;385:152:37;1621:102:48;;;385:152:37;;;;1621:102:48;;;385:152:37;;;;;;1621:102:48;;;385:152:37;;1621:102:48;;;5088:234;;;;;;;;;:::i;:::-;385:152:37;5065:267:48;;3581:9:39;;:::i;:::-;385:152:37;;4949:3:48;5017:16;4995:42;5017:19;4949:3;5017:16;;;:19;:::i;:::-;;4995:42;:::i;:::-;4968:69;;;;:::i;:::-;385:152:37;4949:3:48;:::i;:::-;4916:13;;385:152:37;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;:::i;:::-;;;;;;;;;;;;;;;:::i;:::-;;;;;;:::i;:::-;;;;;;;;;;;;;;;:::i;:::-;3353:16:48;;;;;385:152:37;3427:27:48;;;;:::i;:::-;3470:13;;3485:16;;;;;;385:152:37;;3109:9:39;385:152:37;;;;3734:39:48;;661:173;385:152:37;3734:39:48;;661:173;;;:::i;:::-;3734:39;;;;;;;;:::i;:::-;385:152:37;3724:50:48;;385:152:37;3820:12:48;;385:152:37;3850:15:48;;;385:152:37;;;;3642:237:48;385:152:37;3642:237:48;;385:152:37;1254:173:48;385:152:37;;1018:166:48;;385:152:37;3792:10:48;1018:166;;;385:152:37;;1018:166:48;;385:152:37;1018:166:48;;;385:152:37;1018:166:48;3642:237;;;;;:::i;3503:3::-;3571:16;;3522:69;3571:16;3549:42;3571:19;3503:3;3571:16;;;:19;:::i;3549:42::-;3522:69;;:::i;3503:3::-;3470:13;;385:152:37;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;:::i;:::-;4514:13:35;;;;4529:10;;;;;;385:152:37;;;4541:3:35;4580:12;:18;:12;385:152:37;4580:12:35;;;;:::i;:::-;:18;:::i;:::-;4634:20;:12;;;;;;:::i;:::-;:20;;:::i;:::-;4351:10;385:152:37;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;4739:31:35;4351:10;;4739:31;;385:152:37;4514:13:35;;385:152:37;;;;;;;;;;;;;;;;;;;;:::i;:::-;;;;:::i;:::-;;;:::i;:::-;;;;;;;1093:92:35;385:152:37;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;:::i;:::-;;;;:::i;:::-;;;;:::i;:::-;;;;;;;;;;;;;;;1378:10:35;1483:56;1378:10;;385:152:37;;1368:9:35;385:152:37;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;1510:54:46;:15;;:54;385:152:37;;;1535:15:46;;385:152:37;1510:54:46;;385:152:37;;;;;;;;;;;;;;;;;;;;;;;;;1378:10:35;1483:56;;385:152:37;;1510:54:46;;;;;;385:152:37;;;;;;;;;;;;;;;:::i;:::-;;;;:::i;:::-;;;;;;;;;;;;;;;4968:10:35;385:152:37;;;;4958:9:35;385:152:37;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;5016:20:35;;;;;5012:47;;385:152:37;;;;;;5228:24:35;5224:60;;4968:10;;5374:65;4968:10;;;;385:152:37;;4958:9:35;385:152:37;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;4968:10:35;5374:65;;385:152:37;;5224:60:35;385:152:37;;5261:23:35;;;;5012:47;385:152:37;;;5045:14:35;;;;385:152:37;;;;;;;;;;;;;;;;;;:::i;:::-;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;5317:53:39;385:152:37;;;;;5273:10:39;385:152:37;;;;;;;;;;;;;;;;;;;5261:40:39;385:152:37;;;;;;;;;;;5273:10:39;5317:53;;385:152:37;;;;;;;;;;;;;;;;:::i;:::-;;;:::i;:::-;;;:::i;:::-;;;;;;;;;;;2836:5:35;;;:::i;385:152:37:-;;;;;;;;;;;;;;;;;;;:::i;:::-;;;;;;;;;;;;;;;;;;;;;;;;:::i;:::-;;;;;;;;;;;:::i;:::-;;;;;;;;;1157:9:39;385:152:37;;;;;;;:::i;:::-;3029:16:48;;;3007:39;3029:16;;3007:39;:::i;:::-;3163:12;;;385:152:37;3177:15:48;;;385:152:37;;;;3086:107:48;3163:12;3086:107;;385:152:37;1018:166:48;385:152:37;;1018:166:48;;385:152:37;3151:10:48;1018:166;;;385:152:37;1018:166:48;;;385:152:37;1018:166:48;;;385:152:37;1018:166:48;3086:107;;;;;:::i;:::-;385:152:37;3063:140:48;;1157:9:39;;:::i;385:152:37:-;;;;;;;;;;;;;;;;;;;:::i;:::-;;;;;;;;;;;;;;;;:::i;:::-;;;;;;;;;;:::i;:::-;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;:::i;:::-;1705:15:35;;;;;:42;1701:97;;2078:20:48;;;;;1883:35:35;1982:20;2078::48;;1920:5:35;2078:20:48;2059:40;2078:20;;2059:40;:::i;:::-;385:152:37;;;;;;;;;;2138:95:48;385:152:37;2138:95:48;;385:152:37;433:172:48;385:152:37;;433:172:48;;385:152:37;;433:172:48;;385:152:37;;433:172:48;;385:152:37;;2138:95:48;;;;;:::i;:::-;385:152:37;2128:106:48;;1883:35:35;:::i;:::-;1920:5;;:::i;:::-;1953:20;385:152:37;;;1982:20:35;;:::i;1701:97::-;385:152:37;;;;;1756:42:35;;;;;;385:152:37;1756:42:35;385:152:37;;;;;;;;;;;;;;;;;;:::i;:::-;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;:::i;:::-;;;;;;;;:::i;:::-;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;:::i;:::-;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;:::i;:::-;2167:15:35;;;;;:41;2163:95;;2375:19:48;;;;;;;385:152:37;2443:25:48;;;:::i;:::-;2483:13;;2498:14;;;;;;385:152:37;;;;;;2343:34:35;385:152:37;2379:5:35;385:152:37;;;;2721:30:48;;661:173;2721:30;;;661:173;;;:::i;2721:30::-;385:152:37;2711:41:48;;385:152:37;;;;;;;;;;;;2643:201:48;;;385:152:37;661:173:48;385:152:37;;433:172:48;;385:152:37;433:172:48;;385:152:37;;433:172:48;;385:152:37;;2643:201:48;;;;;:::i;2379:5:35:-;385:152:37;;2484:19:35;;;385:152:37;2529:13:35;;2544:10;;;;;;385:152:37;;;2556:3:35;2595:19;2626:7;2595:19;;:22;385:152:37;2595:19:35;;;:22;:::i;:::-;;2626:7;:::i;:::-;385:152:37;2529:13:35;;2514:3:48;2570:19;2551:42;2570:22;:19;;;;2514:3;2570:19;;:22;:::i;:::-;;2551:42;:::i;2514:3::-;2483:13;;;;;2163:95:35;385:152:37;;2217:41:35;;;;;;385:152:37;2217:41:35;385:152:37;;;;;;;;;;;;;;;;;;;;:::i;:::-;;;;:::i;:::-;;;;;;;;:::i;:::-;;;;;;;;;;:::i;:::-;;;;;;;;;;;:::i;:::-;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;:::i;:::-;;;;;;;;;;;:::i;:::-;;;;;;;;;;;;;;;;;;:::i;:::-;;;;;;;;;;;4322:94:48;385:152:37;1622:9:39;385:152:37;;;;;:::i;:::-;;;;;;;;;;:::i;:::-;;;;;;;;;;;;;;;;;;;;;;;;;;4133:80:48;;;;1621:102;385:152:37;4133:80:48;;1621:102;;;:::i;:::-;;;;;;;;4133:80;;;;;;;;;;;;:::i;:::-;385:152:37;4123:91:48;;4278:16;4256:39;4278:16;;4256:39;:::i;:::-;385:152:37;4377:12:48;;;385:152:37;4391:15:48;;;385:152:37;;;4322:94:48;;;385:152:37;;;1621:102:48;;;385:152:37;;;;4365:10:48;1621:102;;;385:152:37;1621:102:48;;;385:152:37;;;;1621:102:48;;;385:152:37;;;;;;1621:102:48;;;385:152:37;;1621:102:48;;;4322:94;1621:102;385:152:37;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;3070:13:35;3085:10;;;;;;385:152:37;;;3097:3:35;385:152:37;;;;;;;;;;;;;;;3278:20:35;385:152:37;;;;;;;;;;:::i;:::-;;;;;;;;:::i;:::-;;;;;;;;;;:::i;:::-;;;;;;;;;;;:::i;:::-;;;;;;;;:::i;:::-;;;;;;;;;;;;;;;;;;3278:20:35;:::i;:::-;385:152:37;3070:13:35;;385:152:37;;;;;;;;;;;;;;;:::o;:::-;;;;;;;;;;;;;;;;;;;;;;;;;:::o;:::-;;;;;;;;;;;;;;;:::o;:::-;1018:166:48;385:152:37;;;;;;;;;;;;;:::o;:::-;;;;;;;;;;;;;;;;;;;;:::o;:::-;;;;;;;;;;;:::o;:::-;;;;;;;;;;;;;;;:::o;:::-;;;;;;;;;;;:::o;:::-;;;;;;;;;;;:::o;:::-;;;;;;;;;;:::o;:::-;;;;;;;;;;;;;;;;:::i;:::-;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;:::o;:::-;;;;;;;;;;;;;;;;;;;;;;;;;;;;;:::o;:::-;;;;;;;;;;;:::o;:::-;;;;;;;;;;:::o;:::-;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;:::o;:::-;;;;;;;;;;;;;;;;;;:::i;:::-;;;;;;;;;;;;;;;;;;;;;;;;;;;:::i;:::-;;;;;;;;:::i;:::-;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;:::o;:::-;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;:::i;:::-;;;;;;;;;;;;;;;;;;;;;;;;;;;;-1:-1:-1;385:152:37;;;;;;;;;;;;;;;;;;;;;;:::o;:::-;;;;;;;;;;3477:737:35;;;;385:152:37;;-1:-1:-1;385:152:37;;;;;3605:9:35;385:152:37;;;;;;;;;;;;;;;;;3628:10:35;385:152:37;;;;;;;;;;;;;;3654:15:35;;:36;3650:85;;385:152:37;;;;3794:30:35;;;;3790:289;;3477:737;385:152:37;;4160:47:35;385:152:37;;;4160:47:35;;:::i;:::-;3477:737::o;3790:289::-;385:152:37;;;;3844:18:35;3840:229;3844:18;;;385:152:37;;;;3889:32:35;;;;;;;385:152:37;3889:32:35;3840:229;385:152:37;;;4160:47:35;385:152:37;;;;;;;;3790:289:35;;;;3650:85;385:152:37;;;;3699:36:35;;;;;;;385:152:37;3699:36:35;1328:1616:33;;-1:-1:-1;1532:1355:33;1328:1616;1532:1355;1328:1616;;;1532:1355;;;;;;;;;;;;;;;;;;;;;;;;;;-1:-1:-1;1532:1355:33;;;;;385:152:37;;;1328:1616:33:o;385:152:37:-;1532:1355:33;;385:152:37;;;;1532:1355:33;;385:152:37;;;;1532:1355:33;385:152:37;;;;1532:1355:33;385:152:37;;;;;;;;;;;;;;;;:::o;:::-;;;;;;;;;;:::o;5676:530:35:-;;385:152:37;5796:13:35;;;;;385:152:37;;;;;;;;5875:14:35;;;;;385:152:37;;5919:18:35;;;;;;;385:152:37;;;;-1:-1:-1;;385:152:37;;;;5981:9:35;5875:14;385:152:37;;;;;;;5875:14:35;385:152:37;;;;;;;;;;5875:14:35;385:152:37;;;;;;;;;;6028:22:35;6024:49;;843:79:46;;6143:56:35;843:79:46;;6143:56:35;843:79:46;;;;:40;;;:79;385:152:37;;;;893:15:46;;385:152:37;843:79:46;1883:3;385:152:37;;;5981:9:35;385:152:37;;;;;1834:52:46;:61;1001:59;;385:152:37;6143:56:35;;;;385:152:37;;;;;;;;;;;;;;;;;;;;;;;;;;6143:56:35;;;;5676:530::o;843:79:46:-;;385:152:37;843:79:46;;;6024:49:35;6059:14;385:152:37;;6059:14:35;;;;1185:225:36;1269:13;1286:16;1269:33;1286:16;;1317:24;1185:225;:::o;1269:134::-;385:152:37;;1631:60:36;;;385:152:37;788:80:36;385:152:37;;716:20:36;385:152:37;;;;1269:13:36;385:152:37;;;;1685:4:36;385:152:37;;;;;1631:60:36;;;;;:::i;:::-;385:152:37;1621:71:36;;1185:225;:::o;1756:167::-;1886:18;;:::i;:::-;385:152:37;;;1857:58:36;;;;385:152:37;;;;;;;;;;;;;1857:58:36;;;;;:::i;2075:704:39:-;;;;;2338:31;385:152:37;2402:15:39;;;;385:152:37;2384:15:39;;:33;2380:79;;2491:16;2338:31;2491:16;;:23;385:152:37;2473:41:39;;;2469:92;;2598:12;;;;2639:24;2665:5;2598:12;;2338:31;2598:12;;385:152:37;2598:12:39;;:::i;:::-;2639:24;:::i;2665:5::-;385:152:37;2688:16:39;;;385:152:37;;;;;;;;;;;;2756:15:39;;;:::i;2469:92::-;385:152:37;;2402:15:39;385:152:37;2523:38:39;;;;;;;385:152:37;2523:38:39;2380:79;385:152:37;;2402:15:39;385:152:37;2426:33:39;;;;;;;385:152:37;2426:33:39;3937:1194;;;;;4204:16;;385:152:37;4260:15:39;;;;;;385:152:37;4242:15:39;;:33;4238:79;;4331:38;;;;4327:67;;4497:5;4431:12;;;4471:24;4431:12;;;;;;385:152:37;4431:12:39;;:::i;4497:5::-;-1:-1:-1;4558:16:39;;;;;;3937:1194;;;;;;;;:::o;4576:3::-;4635:19;:16;;;:19;:::i;:::-;;4698:18;;;;;;:::i;:::-;:34;385:152:37;4773:16:39;;;;385:152:37;4755:34:39;;;4751:78;;4852:20;;;;;;385:152:37;4852:20:39;;;4848:253;;4576:3;;;;;;;385:152:37;4543:13:39;;4848:253;5066:15;385:152:37;5043:21:39;385:152:37;;5043:18:39;385:152:37;;;5043:18:39;;:::i;:21::-;5066:15;;:::i;:::-;4848:253;;;;;;;;4751:78;385:152:37;;;;4798:31:39;;;;;;;385:152:37;4798:31:39;4327:67;4378:16;385:152:37;;4378:16:39;;;;4238:79;385:152:37;;;;4284:33:39;;;;;;;385:152:37;4284:33:39;6250:293;;385:152:37;6408:1:39;385:152:37;;;;;;-1:-1:-1;385:152:37;-1:-1:-1;385:152:37;;;-1:-1:-1;385:152:37;;5992:1:39;385:152:37;-1:-1:-1;385:152:37;;;;-1:-1:-1;385:152:37;;;;6447:33:39;385:152:37;;;6495:13:39;:18;6491:45;;6250:293::o;6491:45::-;6522:14;385:152:37;;6522:14:39;;;;385:152:37;;;;;:::i;:::-;;;;;;;:::i;:::-;;;;;;;;;:::i;:::-;;;;;;;;:::o;:::-;;;;;;;;;:::o;:::-;;;;;;;;;;661:173:48;385:152:37;;;;;;661:173:48;;;;;;;;;;;;;:::o;:::-;;;385:152:37;;661:173:48;;;;;;;;;;;;1621:102;;385:152:37;;1621:102:48;;;;;;;;;;;;;;;:::o;:::-;;;;;;;;;;;;;;;;5345:188;385:152:37;;5480:45:48;;;385:152:37;289:87:48;385:152:37;;;289:87:48;;;385:152:37;;289:87:48;;385:152:37;5480:45:48;289:87;;;385:152:37;289:87:48;;;385:152:37;289:87:48;385:152:37;289:87:48;;385:152:37;289:87:48;;;385:152:37;289:87:48;;;385:152:37;289:87:48;;385:152:37;289:87:48;;;385:152:37;289:87:48;5480:45;;289:87;385:152:37;;;;;;;;;;;;;;5470:56:48;;5345:188;:::o;5539:229::-;385:152:37;;5710:50:48;;;;385:152:37;895:59:48;385:152:37;;;895:59:48;;385:152:37;;895:59:48;;385:152:37;895:59:48;;;;;385:152:37;895:59:48;5710:50;;;;;:::i;385:152:37:-;;;;;;;;;;;;;;;;;:::o;700:1109:50:-;-1:-1:-1;863:25:50;;;;-1:-1:-1;933:2:50;913:22;;933:2;;964:41;;;;;;:::i;:::-;955:50;;625:68;1043:2;625:68;;;;;;-1:-1:-1;625:68:50;385:152:37;1043:2:50;625:68;;;1033:13;625:68;;909:490;385:152:37;;;;;;625:68:50;;;;385:152:37;625:68:50;;385:152:37;625:68:50;;;385:152:37;1429:24:50;;;;;;;;;385:152:37;1429:24:50;-1:-1:-1;1429:24:50;385:152:37;1471:20:50;;;1467:51;;385:152:37;1536:23:50;1532:51;;700:1109::o;1532:51::-;1568:15;385:152:37;;1568:15:50;;;;1467:51;1500:18;385:152:37;;1500:18:50;;;;1429:24;385:152:37;;;-1:-1:-1;385:152:37;;;;;909:490:50;1092:2;1072:22;;1092:2;;1180:41;;;;;;:::i;:::-;1170:51;1312:2;626:66;1243:19;;385:152:37;;;625:68:50;;385:152:37;625:68:50;;;;;1280:34;-1:-1:-1;1280:34:50;385:152:37;625:68:50;1280:34;909:490;;1068:331;1360:24;1092:2;385:152:37;1360:24:50;;;;859:944;385:152:37;;;;;;;;1634:57:50;385:152:37;;;;;;;;;;;;1634:57:50;;;;;;;385:152:37;;;;;;;;;;;;;;1621:102:48;;;;;;;;385:152:37;;;;1634:57:50;;385:152:37;;1634:57:50;;;;;;;;;;;859:944;385:152:37;;;;;1709:48:50;1705:87;;700:1109::o;1705:87::-;1634:57;385:152:37;;1766:26:50;;;;1634:57;;;;;;;;;;;;;;;;;:::i;:::-;;;385:152:37;;;;;;;;;;;;;1634:57:50;385:152:37;1634:57:50;;;;;;;-1:-1:-1;1634:57:50;;;385:152:37;;;;;;;;","linkReferences":{},"immutableReferences":{"34334":[{"start":6983,"length":32}],"34336":[{"start":6945,"length":32}]}},"methodIdentifiers":{"DOMAIN_SEPARATOR()":"3644e515","allowance(address,address,address)":"927da105","approve(address,address,uint160,uint48)":"87517c45","invalidateNonces(address,address,uint48)":"65d9723c","invalidateUnorderedNonces(uint256,uint256)":"3ff9dcb1","lockdown((address,address)[])":"cc53287f","nonceBitmap(address,uint256)":"4fe02b44","permit(address,((address,uint160,uint48,uint48),address,uint256),bytes)":"2b67b570","permit(address,((address,uint160,uint48,uint48)[],address,uint256),bytes)":"2a2d80d1","permitTransferFrom(((address,uint256),uint256,uint256),(address,uint256),address,bytes)":"30f28b7a","permitTransferFrom(((address,uint256)[],uint256,uint256),(address,uint256)[],address,bytes)":"edd9444b","permitWitnessTransferFrom(((address,uint256),uint256,uint256),(address,uint256),address,bytes32,string,bytes)":"137c29fe","permitWitnessTransferFrom(((address,uint256)[],uint256,uint256),(address,uint256)[],address,bytes32,string,bytes)":"fe8ec1a7","transferFrom((address,address,uint160,address)[])":"0d58b1db","transferFrom(address,address,uint160,address)":"36c78516"},"rawMetadata":"{\"compiler\":{\"version\":\"0.8.17+commit.8df45f5f\"},\"language\":\"Solidity\",\"output\":{\"abi\":[{\"inputs\":[{\"internalType\":\"uint256\",\"name\":\"deadline\",\"type\":\"uint256\"}],\"name\":\"AllowanceExpired\",\"type\":\"error\"},{\"inputs\":[],\"name\":\"ExcessiveInvalidation\",\"type\":\"error\"},{\"inputs\":[{\"internalType\":\"uint256\",\"name\":\"amount\",\"type\":\"uint256\"}],\"name\":\"InsufficientAllowance\",\"type\":\"error\"},{\"inputs\":[{\"internalType\":\"uint256\",\"name\":\"maxAmount\",\"type\":\"uint256\"}],\"name\":\"InvalidAmount\",\"type\":\"error\"},{\"inputs\":[],\"name\":\"InvalidContractSignature\",\"type\":\"error\"},{\"inputs\":[],\"name\":\"InvalidNonce\",\"type\":\"error\"},{\"inputs\":[],\"name\":\"InvalidSignature\",\"type\":\"error\"},{\"inputs\":[],\"name\":\"InvalidSignatureLength\",\"type\":\"error\"},{\"inputs\":[],\"name\":\"InvalidSigner\",\"type\":\"error\"},{\"inputs\":[],\"name\":\"LengthMismatch\",\"type\":\"error\"},{\"inputs\":[{\"internalType\":\"uint256\",\"name\":\"signatureDeadline\",\"type\":\"uint256\"}],\"name\":\"SignatureExpired\",\"type\":\"error\"},{\"anonymous\":false,\"inputs\":[{\"indexed\":true,\"internalType\":\"address\",\"name\":\"owner\",\"type\":\"address\"},{\"indexed\":true,\"internalType\":\"address\",\"name\":\"token\",\"type\":\"address\"},{\"indexed\":true,\"internalType\":\"address\",\"name\":\"spender\",\"type\":\"address\"},{\"indexed\":false,\"internalType\":\"uint160\",\"name\":\"amount\",\"type\":\"uint160\"},{\"indexed\":false,\"internalType\":\"uint48\",\"name\":\"expiration\",\"type\":\"uint48\"}],\"name\":\"Approval\",\"type\":\"event\"},{\"anonymous\":false,\"inputs\":[{\"indexed\":true,\"internalType\":\"address\",\"name\":\"owner\",\"type\":\"address\"},{\"indexed\":false,\"internalType\":\"address\",\"name\":\"token\",\"type\":\"address\"},{\"indexed\":false,\"internalType\":\"address\",\"name\":\"spender\",\"type\":\"address\"}],\"name\":\"Lockdown\",\"type\":\"event\"},{\"anonymous\":false,\"inputs\":[{\"indexed\":true,\"internalType\":\"address\",\"name\":\"owner\",\"type\":\"address\"},{\"indexed\":true,\"internalType\":\"address\",\"name\":\"token\",\"type\":\"address\"},{\"indexed\":true,\"internalType\":\"address\",\"name\":\"spender\",\"type\":\"address\"},{\"indexed\":false,\"internalType\":\"uint48\",\"name\":\"newNonce\",\"type\":\"uint48\"},{\"indexed\":false,\"internalType\":\"uint48\",\"name\":\"oldNonce\",\"type\":\"uint48\"}],\"name\":\"NonceInvalidation\",\"type\":\"event\"},{\"anonymous\":false,\"inputs\":[{\"indexed\":true,\"internalType\":\"address\",\"name\":\"owner\",\"type\":\"address\"},{\"indexed\":true,\"internalType\":\"address\",\"name\":\"token\",\"type\":\"address\"},{\"indexed\":true,\"internalType\":\"address\",\"name\":\"spender\",\"type\":\"address\"},{\"indexed\":false,\"internalType\":\"uint160\",\"name\":\"amount\",\"type\":\"uint160\"},{\"indexed\":false,\"internalType\":\"uint48\",\"name\":\"expiration\",\"type\":\"uint48\"},{\"indexed\":false,\"internalType\":\"uint48\",\"name\":\"nonce\",\"type\":\"uint48\"}],\"name\":\"Permit\",\"type\":\"event\"},{\"anonymous\":false,\"inputs\":[{\"indexed\":true,\"internalType\":\"address\",\"name\":\"owner\",\"type\":\"address\"},{\"indexed\":false,\"internalType\":\"uint256\",\"name\":\"word\",\"type\":\"uint256\"},{\"indexed\":false,\"internalType\":\"uint256\",\"name\":\"mask\",\"type\":\"uint256\"}],\"name\":\"UnorderedNonceInvalidation\",\"type\":\"event\"},{\"inputs\":[],\"name\":\"DOMAIN_SEPARATOR\",\"outputs\":[{\"internalType\":\"bytes32\",\"name\":\"\",\"type\":\"bytes32\"}],\"stateMutability\":\"view\",\"type\":\"function\"},{\"inputs\":[{\"internalType\":\"address\",\"name\":\"\",\"type\":\"address\"},{\"internalType\":\"address\",\"name\":\"\",\"type\":\"address\"},{\"internalType\":\"address\",\"name\":\"\",\"type\":\"address\"}],\"name\":\"allowance\",\"outputs\":[{\"internalType\":\"uint160\",\"name\":\"amount\",\"type\":\"uint160\"},{\"internalType\":\"uint48\",\"name\":\"expiration\",\"type\":\"uint48\"},{\"internalType\":\"uint48\",\"name\":\"nonce\",\"type\":\"uint48\"}],\"stateMutability\":\"view\",\"type\":\"function\"},{\"inputs\":[{\"internalType\":\"address\",\"name\":\"token\",\"type\":\"address\"},{\"internalType\":\"address\",\"name\":\"spender\",\"type\":\"address\"},{\"internalType\":\"uint160\",\"name\":\"amount\",\"type\":\"uint160\"},{\"internalType\":\"uint48\",\"name\":\"expiration\",\"type\":\"uint48\"}],\"name\":\"approve\",\"outputs\":[],\"stateMutability\":\"nonpayable\",\"type\":\"function\"},{\"inputs\":[{\"internalType\":\"address\",\"name\":\"token\",\"type\":\"address\"},{\"internalType\":\"address\",\"name\":\"spender\",\"type\":\"address\"},{\"internalType\":\"uint48\",\"name\":\"newNonce\",\"type\":\"uint48\"}],\"name\":\"invalidateNonces\",\"outputs\":[],\"stateMutability\":\"nonpayable\",\"type\":\"function\"},{\"inputs\":[{\"internalType\":\"uint256\",\"name\":\"wordPos\",\"type\":\"uint256\"},{\"internalType\":\"uint256\",\"name\":\"mask\",\"type\":\"uint256\"}],\"name\":\"invalidateUnorderedNonces\",\"outputs\":[],\"stateMutability\":\"nonpayable\",\"type\":\"function\"},{\"inputs\":[{\"components\":[{\"internalType\":\"address\",\"name\":\"token\",\"type\":\"address\"},{\"internalType\":\"address\",\"name\":\"spender\",\"type\":\"address\"}],\"internalType\":\"struct IAllowanceTransfer.TokenSpenderPair[]\",\"name\":\"approvals\",\"type\":\"tuple[]\"}],\"name\":\"lockdown\",\"outputs\":[],\"stateMutability\":\"nonpayable\",\"type\":\"function\"},{\"inputs\":[{\"internalType\":\"address\",\"name\":\"\",\"type\":\"address\"},{\"internalType\":\"uint256\",\"name\":\"\",\"type\":\"uint256\"}],\"name\":\"nonceBitmap\",\"outputs\":[{\"internalType\":\"uint256\",\"name\":\"\",\"type\":\"uint256\"}],\"stateMutability\":\"view\",\"type\":\"function\"},{\"inputs\":[{\"internalType\":\"address\",\"name\":\"owner\",\"type\":\"address\"},{\"components\":[{\"components\":[{\"internalType\":\"address\",\"name\":\"token\",\"type\":\"address\"},{\"internalType\":\"uint160\",\"name\":\"amount\",\"type\":\"uint160\"},{\"internalType\":\"uint48\",\"name\":\"expiration\",\"type\":\"uint48\"},{\"internalType\":\"uint48\",\"name\":\"nonce\",\"type\":\"uint48\"}],\"internalType\":\"struct IAllowanceTransfer.PermitDetails[]\",\"name\":\"details\",\"type\":\"tuple[]\"},{\"internalType\":\"address\",\"name\":\"spender\",\"type\":\"address\"},{\"internalType\":\"uint256\",\"name\":\"sigDeadline\",\"type\":\"uint256\"}],\"internalType\":\"struct IAllowanceTransfer.PermitBatch\",\"name\":\"permitBatch\",\"type\":\"tuple\"},{\"internalType\":\"bytes\",\"name\":\"signature\",\"type\":\"bytes\"}],\"name\":\"permit\",\"outputs\":[],\"stateMutability\":\"nonpayable\",\"type\":\"function\"},{\"inputs\":[{\"internalType\":\"address\",\"name\":\"owner\",\"type\":\"address\"},{\"components\":[{\"components\":[{\"internalType\":\"address\",\"name\":\"token\",\"type\":\"address\"},{\"internalType\":\"uint160\",\"name\":\"amount\",\"type\":\"uint160\"},{\"internalType\":\"uint48\",\"name\":\"expiration\",\"type\":\"uint48\"},{\"internalType\":\"uint48\",\"name\":\"nonce\",\"type\":\"uint48\"}],\"internalType\":\"struct IAllowanceTransfer.PermitDetails\",\"name\":\"details\",\"type\":\"tuple\"},{\"internalType\":\"address\",\"name\":\"spender\",\"type\":\"address\"},{\"internalType\":\"uint256\",\"name\":\"sigDeadline\",\"type\":\"uint256\"}],\"internalType\":\"struct IAllowanceTransfer.PermitSingle\",\"name\":\"permitSingle\",\"type\":\"tuple\"},{\"internalType\":\"bytes\",\"name\":\"signature\",\"type\":\"bytes\"}],\"name\":\"permit\",\"outputs\":[],\"stateMutability\":\"nonpayable\",\"type\":\"function\"},{\"inputs\":[{\"components\":[{\"components\":[{\"internalType\":\"address\",\"name\":\"token\",\"type\":\"address\"},{\"internalType\":\"uint256\",\"name\":\"amount\",\"type\":\"uint256\"}],\"internalType\":\"struct ISignatureTransfer.TokenPermissions\",\"name\":\"permitted\",\"type\":\"tuple\"},{\"internalType\":\"uint256\",\"name\":\"nonce\",\"type\":\"uint256\"},{\"internalType\":\"uint256\",\"name\":\"deadline\",\"type\":\"uint256\"}],\"internalType\":\"struct ISignatureTransfer.PermitTransferFrom\",\"name\":\"permit\",\"type\":\"tuple\"},{\"components\":[{\"internalType\":\"address\",\"name\":\"to\",\"type\":\"address\"},{\"internalType\":\"uint256\",\"name\":\"requestedAmount\",\"type\":\"uint256\"}],\"internalType\":\"struct ISignatureTransfer.SignatureTransferDetails\",\"name\":\"transferDetails\",\"type\":\"tuple\"},{\"internalType\":\"address\",\"name\":\"owner\",\"type\":\"address\"},{\"internalType\":\"bytes\",\"name\":\"signature\",\"type\":\"bytes\"}],\"name\":\"permitTransferFrom\",\"outputs\":[],\"stateMutability\":\"nonpayable\",\"type\":\"function\"},{\"inputs\":[{\"components\":[{\"components\":[{\"internalType\":\"address\",\"name\":\"token\",\"type\":\"address\"},{\"internalType\":\"uint256\",\"name\":\"amount\",\"type\":\"uint256\"}],\"internalType\":\"struct ISignatureTransfer.TokenPermissions[]\",\"name\":\"permitted\",\"type\":\"tuple[]\"},{\"internalType\":\"uint256\",\"name\":\"nonce\",\"type\":\"uint256\"},{\"internalType\":\"uint256\",\"name\":\"deadline\",\"type\":\"uint256\"}],\"internalType\":\"struct ISignatureTransfer.PermitBatchTransferFrom\",\"name\":\"permit\",\"type\":\"tuple\"},{\"components\":[{\"internalType\":\"address\",\"name\":\"to\",\"type\":\"address\"},{\"internalType\":\"uint256\",\"name\":\"requestedAmount\",\"type\":\"uint256\"}],\"internalType\":\"struct ISignatureTransfer.SignatureTransferDetails[]\",\"name\":\"transferDetails\",\"type\":\"tuple[]\"},{\"internalType\":\"address\",\"name\":\"owner\",\"type\":\"address\"},{\"internalType\":\"bytes\",\"name\":\"signature\",\"type\":\"bytes\"}],\"name\":\"permitTransferFrom\",\"outputs\":[],\"stateMutability\":\"nonpayable\",\"type\":\"function\"},{\"inputs\":[{\"components\":[{\"components\":[{\"internalType\":\"address\",\"name\":\"token\",\"type\":\"address\"},{\"internalType\":\"uint256\",\"name\":\"amount\",\"type\":\"uint256\"}],\"internalType\":\"struct ISignatureTransfer.TokenPermissions\",\"name\":\"permitted\",\"type\":\"tuple\"},{\"internalType\":\"uint256\",\"name\":\"nonce\",\"type\":\"uint256\"},{\"internalType\":\"uint256\",\"name\":\"deadline\",\"type\":\"uint256\"}],\"internalType\":\"struct ISignatureTransfer.PermitTransferFrom\",\"name\":\"permit\",\"type\":\"tuple\"},{\"components\":[{\"internalType\":\"address\",\"name\":\"to\",\"type\":\"address\"},{\"internalType\":\"uint256\",\"name\":\"requestedAmount\",\"type\":\"uint256\"}],\"internalType\":\"struct ISignatureTransfer.SignatureTransferDetails\",\"name\":\"transferDetails\",\"type\":\"tuple\"},{\"internalType\":\"address\",\"name\":\"owner\",\"type\":\"address\"},{\"internalType\":\"bytes32\",\"name\":\"witness\",\"type\":\"bytes32\"},{\"internalType\":\"string\",\"name\":\"witnessTypeString\",\"type\":\"string\"},{\"internalType\":\"bytes\",\"name\":\"signature\",\"type\":\"bytes\"}],\"name\":\"permitWitnessTransferFrom\",\"outputs\":[],\"stateMutability\":\"nonpayable\",\"type\":\"function\"},{\"inputs\":[{\"components\":[{\"components\":[{\"internalType\":\"address\",\"name\":\"token\",\"type\":\"address\"},{\"internalType\":\"uint256\",\"name\":\"amount\",\"type\":\"uint256\"}],\"internalType\":\"struct ISignatureTransfer.TokenPermissions[]\",\"name\":\"permitted\",\"type\":\"tuple[]\"},{\"internalType\":\"uint256\",\"name\":\"nonce\",\"type\":\"uint256\"},{\"internalType\":\"uint256\",\"name\":\"deadline\",\"type\":\"uint256\"}],\"internalType\":\"struct ISignatureTransfer.PermitBatchTransferFrom\",\"name\":\"permit\",\"type\":\"tuple\"},{\"components\":[{\"internalType\":\"address\",\"name\":\"to\",\"type\":\"address\"},{\"internalType\":\"uint256\",\"name\":\"requestedAmount\",\"type\":\"uint256\"}],\"internalType\":\"struct ISignatureTransfer.SignatureTransferDetails[]\",\"name\":\"transferDetails\",\"type\":\"tuple[]\"},{\"internalType\":\"address\",\"name\":\"owner\",\"type\":\"address\"},{\"internalType\":\"bytes32\",\"name\":\"witness\",\"type\":\"bytes32\"},{\"internalType\":\"string\",\"name\":\"witnessTypeString\",\"type\":\"string\"},{\"internalType\":\"bytes\",\"name\":\"signature\",\"type\":\"bytes\"}],\"name\":\"permitWitnessTransferFrom\",\"outputs\":[],\"stateMutability\":\"nonpayable\",\"type\":\"function\"},{\"inputs\":[{\"components\":[{\"internalType\":\"address\",\"name\":\"from\",\"type\":\"address\"},{\"internalType\":\"address\",\"name\":\"to\",\"type\":\"address\"},{\"internalType\":\"uint160\",\"name\":\"amount\",\"type\":\"uint160\"},{\"internalType\":\"address\",\"name\":\"token\",\"type\":\"address\"}],\"internalType\":\"struct IAllowanceTransfer.AllowanceTransferDetails[]\",\"name\":\"transferDetails\",\"type\":\"tuple[]\"}],\"name\":\"transferFrom\",\"outputs\":[],\"stateMutability\":\"nonpayable\",\"type\":\"function\"},{\"inputs\":[{\"internalType\":\"address\",\"name\":\"from\",\"type\":\"address\"},{\"internalType\":\"address\",\"name\":\"to\",\"type\":\"address\"},{\"internalType\":\"uint160\",\"name\":\"amount\",\"type\":\"uint160\"},{\"internalType\":\"address\",\"name\":\"token\",\"type\":\"address\"}],\"name\":\"transferFrom\",\"outputs\":[],\"stateMutability\":\"nonpayable\",\"type\":\"function\"}],\"devdoc\":{\"details\":\"Users must approve Permit2 before calling any of the transfer functions.\",\"errors\":{\"AllowanceExpired(uint256)\":[{\"params\":{\"deadline\":\"The timestamp at which the allowed amount is no longer valid\"}}],\"InsufficientAllowance(uint256)\":[{\"params\":{\"amount\":\"The maximum amount allowed\"}}],\"InvalidAmount(uint256)\":[{\"params\":{\"maxAmount\":\"The maximum amount a spender can request to transfer\"}}],\"LengthMismatch()\":[{\"details\":\"If the spender does not need to transfer the number of tokens permitted, the spender can request amount 0 to be transferred\"}],\"SignatureExpired(uint256)\":[{\"params\":{\"signatureDeadline\":\"The timestamp at which a signature is no longer valid\"}}]},\"kind\":\"dev\",\"methods\":{\"DOMAIN_SEPARATOR()\":{\"details\":\"Uses cached version if chainid and address are unchanged from construction.\"},\"approve(address,address,uint160,uint48)\":{\"details\":\"The packed allowance also holds a nonce, which will stay unchanged in approveSetting amount to type(uint160).max sets an unlimited approval\",\"params\":{\"amount\":\"The approved amount of the token\",\"expiration\":\"The timestamp at which the approval is no longer valid\",\"spender\":\"The spender address to approve\",\"token\":\"The token to approve\"}},\"invalidateNonces(address,address,uint48)\":{\"details\":\"Can't invalidate more than 2**16 nonces per transaction.\",\"params\":{\"newNonce\":\"The new nonce to set. Invalidates all nonces less than it.\",\"spender\":\"The spender to invalidate nonces for\",\"token\":\"The token to invalidate nonces for\"}},\"invalidateUnorderedNonces(uint256,uint256)\":{\"details\":\"The wordPos is maxed at type(uint248).max\",\"params\":{\"mask\":\"A bitmap masked against msg.sender's current bitmap at the word position\",\"wordPos\":\"A number to index the nonceBitmap at\"}},\"lockdown((address,address)[])\":{\"params\":{\"approvals\":\"Array of approvals to revoke.\"}},\"permit(address,((address,uint160,uint48,uint48),address,uint256),bytes)\":{\"details\":\"May fail if the owner's nonce was invalidated in-flight by invalidateNonce\",\"params\":{\"owner\":\"The owner of the tokens being approved\",\"permitSingle\":\"Data signed over by the owner specifying the terms of approval\",\"signature\":\"The owner's signature over the permit data\"}},\"permit(address,((address,uint160,uint48,uint48)[],address,uint256),bytes)\":{\"details\":\"May fail if the owner's nonce was invalidated in-flight by invalidateNonce\",\"params\":{\"owner\":\"The owner of the tokens being approved\",\"permitBatch\":\"Data signed over by the owner specifying the terms of approval\",\"signature\":\"The owner's signature over the permit data\"}},\"permitTransferFrom(((address,uint256),uint256,uint256),(address,uint256),address,bytes)\":{\"details\":\"Reverts if the requested amount is greater than the permitted signed amount\",\"params\":{\"owner\":\"The owner of the tokens to transfer\",\"permit\":\"The permit data signed over by the owner\",\"signature\":\"The signature to verify\",\"transferDetails\":\"The spender's requested transfer details for the permitted token\"}},\"permitTransferFrom(((address,uint256)[],uint256,uint256),(address,uint256)[],address,bytes)\":{\"params\":{\"owner\":\"The owner of the tokens to transfer\",\"permit\":\"The permit data signed over by the owner\",\"signature\":\"The signature to verify\",\"transferDetails\":\"Specifies the recipient and requested amount for the token transfer\"}},\"permitWitnessTransferFrom(((address,uint256),uint256,uint256),(address,uint256),address,bytes32,string,bytes)\":{\"details\":\"The witness type string must follow EIP712 ordering of nested structs and must include the TokenPermissions type definitionReverts if the requested amount is greater than the permitted signed amount\",\"params\":{\"owner\":\"The owner of the tokens to transfer\",\"permit\":\"The permit data signed over by the owner\",\"signature\":\"The signature to verify\",\"transferDetails\":\"The spender's requested transfer details for the permitted token\",\"witness\":\"Extra data to include when checking the user signature\",\"witnessTypeString\":\"The EIP-712 type definition for remaining string stub of the typehash\"}},\"permitWitnessTransferFrom(((address,uint256)[],uint256,uint256),(address,uint256)[],address,bytes32,string,bytes)\":{\"details\":\"The witness type string must follow EIP712 ordering of nested structs and must include the TokenPermissions type definition\",\"params\":{\"owner\":\"The owner of the tokens to transfer\",\"permit\":\"The permit data signed over by the owner\",\"signature\":\"The signature to verify\",\"transferDetails\":\"Specifies the recipient and requested amount for the token transfer\",\"witness\":\"Extra data to include when checking the user signature\",\"witnessTypeString\":\"The EIP-712 type definition for remaining string stub of the typehash\"}},\"transferFrom((address,address,uint160,address)[])\":{\"details\":\"Requires the from addresses to have approved at least the desired amount of tokens to msg.sender.\",\"params\":{\"transferDetails\":\"Array of owners, recipients, amounts, and tokens for the transfers\"}},\"transferFrom(address,address,uint160,address)\":{\"details\":\"Requires the from address to have approved at least the desired amount of tokens to msg.sender.\",\"params\":{\"amount\":\"The amount of the token to transfer\",\"from\":\"The address to transfer from\",\"to\":\"The address of the recipient\",\"token\":\"The token address to transfer\"}}},\"version\":1},\"userdoc\":{\"errors\":{\"AllowanceExpired(uint256)\":[{\"notice\":\"Thrown when an allowance on a token has expired.\"}],\"ExcessiveInvalidation()\":[{\"notice\":\"Thrown when too many nonces are invalidated.\"}],\"InsufficientAllowance(uint256)\":[{\"notice\":\"Thrown when an allowance on a token has been depleted.\"}],\"InvalidAmount(uint256)\":[{\"notice\":\"Thrown when the requested amount for a transfer is larger than the permissioned amount\"}],\"InvalidContractSignature()\":[{\"notice\":\"Thrown when the recovered contract signature is incorrect\"}],\"InvalidNonce()\":[{\"notice\":\"Thrown when validating that the inputted nonce has not been used\"}],\"InvalidSignature()\":[{\"notice\":\"Thrown when the recovered signer is equal to the zero address\"}],\"InvalidSignatureLength()\":[{\"notice\":\"Thrown when the passed in signature is not a valid length\"}],\"InvalidSigner()\":[{\"notice\":\"Thrown when the recovered signer does not equal the claimedSigner\"}],\"LengthMismatch()\":[{\"notice\":\"Thrown when the number of tokens permissioned to a spender does not match the number of tokens being transferred\"}],\"SignatureExpired(uint256)\":[{\"notice\":\"Thrown when validating an inputted signature that is stale\"}]},\"events\":{\"Approval(address,address,address,uint160,uint48)\":{\"notice\":\"Emits an event when the owner successfully sets permissions on a token for the spender.\"},\"Lockdown(address,address,address)\":{\"notice\":\"Emits an event when the owner sets the allowance back to 0 with the lockdown function.\"},\"NonceInvalidation(address,address,address,uint48,uint48)\":{\"notice\":\"Emits an event when the owner successfully invalidates an ordered nonce.\"},\"Permit(address,address,address,uint160,uint48,uint48)\":{\"notice\":\"Emits an event when the owner successfully sets permissions using a permit signature on a token for the spender.\"},\"UnorderedNonceInvalidation(address,uint256,uint256)\":{\"notice\":\"Emits an event when the owner successfully invalidates an unordered nonce.\"}},\"kind\":\"user\",\"methods\":{\"DOMAIN_SEPARATOR()\":{\"notice\":\"Returns the domain separator for the current chain.\"},\"allowance(address,address,address)\":{\"notice\":\"Maps users to tokens to spender addresses and information about the approval on the token\"},\"approve(address,address,uint160,uint48)\":{\"notice\":\"Approves the spender to use up to amount of the specified token up until the expiration\"},\"invalidateNonces(address,address,uint48)\":{\"notice\":\"Invalidate nonces for a given (token, spender) pair\"},\"invalidateUnorderedNonces(uint256,uint256)\":{\"notice\":\"Invalidates the bits specified in mask for the bitmap at the word position\"},\"lockdown((address,address)[])\":{\"notice\":\"Enables performing a \\\"lockdown\\\" of the sender's Permit2 identity by batch revoking approvals\"},\"nonceBitmap(address,uint256)\":{\"notice\":\"A map from token owner address and a caller specified word index to a bitmap. Used to set bits in the bitmap to prevent against signature replay protection\"},\"permit(address,((address,uint160,uint48,uint48),address,uint256),bytes)\":{\"notice\":\"Permit a spender to a given amount of the owners token via the owner's EIP-712 signature\"},\"permit(address,((address,uint160,uint48,uint48)[],address,uint256),bytes)\":{\"notice\":\"Permit a spender to the signed amounts of the owners tokens via the owner's EIP-712 signature\"},\"permitTransferFrom(((address,uint256),uint256,uint256),(address,uint256),address,bytes)\":{\"notice\":\"Transfers a token using a signed permit message\"},\"permitTransferFrom(((address,uint256)[],uint256,uint256),(address,uint256)[],address,bytes)\":{\"notice\":\"Transfers multiple tokens using a signed permit message\"},\"permitWitnessTransferFrom(((address,uint256),uint256,uint256),(address,uint256),address,bytes32,string,bytes)\":{\"notice\":\"Transfers a token using a signed permit messageIncludes extra data provided by the caller to verify signature over\"},\"permitWitnessTransferFrom(((address,uint256)[],uint256,uint256),(address,uint256)[],address,bytes32,string,bytes)\":{\"notice\":\"Transfers multiple tokens using a signed permit messageIncludes extra data provided by the caller to verify signature over\"},\"transferFrom((address,address,uint160,address)[])\":{\"notice\":\"Transfer approved tokens in a batch\"},\"transferFrom(address,address,uint160,address)\":{\"notice\":\"Transfer approved tokens from one address to another\"}},\"notice\":\"Permit2 handles signature-based transfers in SignatureTransfer and allowance-based transfers in AllowanceTransfer.\",\"version\":1}},\"settings\":{\"compilationTarget\":{\"src/Permit2.sol\":\"Permit2\"},\"evmVersion\":\"london\",\"libraries\":{},\"metadata\":{\"bytecodeHash\":\"none\"},\"optimizer\":{\"enabled\":true,\"runs\":1000000},\"remappings\":[\":ds-test/=lib/forge-std/lib/ds-test/src/\",\":forge-gas-snapshot/=lib/forge-gas-snapshot/src/\",\":forge-std/=lib/forge-std/src/\",\":openzeppelin-contracts/=lib/openzeppelin-contracts/\",\":solmate/=lib/solmate/\"],\"viaIR\":true},\"sources\":{\"lib/solmate/src/tokens/ERC20.sol\":{\"keccak256\":\"0xcdfd8db76b2a3415620e4d18cc5545f3d50de792dbf2c3dd5adb40cbe6f94b10\",\"license\":\"AGPL-3.0-only\",\"urls\":[\"bzz-raw://57b3ab70cde374af1cf2c9888636e8de6cf660f087b1c9abd805e9271e19fa35\",\"dweb:/ipfs/QmNrLDBAHYFjpjSd12jerm1AdBkDqEYUUaXgnT854BUZ97\"]},\"lib/solmate/src/utils/SafeTransferLib.sol\":{\"keccak256\":\"0xbadf3d708cf532b12f75f78a1d423135954b63774a6d4ba15914a551d348db8a\",\"license\":\"AGPL-3.0-only\",\"urls\":[\"bzz-raw://88ac8256bd520d1b8e6f9c4ac9e8777bffdc4a6c8afb1a848f596665779a55b4\",\"dweb:/ipfs/QmXx7X1dxe6f5VM91vgQ5BA4r2eF97GWDcQDrgHytcvfjU\"]},\"src/AllowanceTransfer.sol\":{\"keccak256\":\"0x2e0a14330c1413091f5155d8df0506d753b3f1d6e8c9dcdbfb02cc05ac34b643\",\"license\":\"MIT\",\"urls\":[\"bzz-raw://a6965ea5d52145e4b1362bb9b8a9b9b640ecb13bd4d5d1770abc133c263265b6\",\"dweb:/ipfs/Qme1X5vKR3Nvw36MpsdsyhzvkPzcztUNgJ9RF7umDr6vYY\"]},\"src/EIP712.sol\":{\"keccak256\":\"0x5ac9f1db92c3102fa28911c754cffc54c6bbd3eb793192b67c232c02fb974b99\",\"license\":\"MIT\",\"urls\":[\"bzz-raw://69218a8c22a7683c3ba9417f5629b8038f4793eb5245d49a5631e4ae4dbb90cc\",\"dweb:/ipfs/QmfUtUiLE1aFiKrQPN7Y97M3P1TPiY5Lvwddv646awU3gt\"]},\"src/Permit2.sol\":{\"keccak256\":\"0x934c0eb24a52eb5900f01f5c328374b670366adf995ba9ed49bcd3d7b87b159e\",\"license\":\"MIT\",\"urls\":[\"bzz-raw://bdfd05b3007726dc6dd2822c1dd9dc1b2471fbec507f30efa71a1a214c98bab6\",\"dweb:/ipfs/QmPq4hptCSUACQdynSa86bdEexE7RryzosrhUAZ9Xkqc5a\"]},\"src/PermitErrors.sol\":{\"keccak256\":\"0x9fd1192bbc3ffa9354f2bfc534d7a1cdf2be2c940c96ed4ac7bc37991e1e5dfe\",\"license\":\"MIT\",\"urls\":[\"bzz-raw://77f8b2e2c040c33e2c78f05e7e768a17f433c07adb699235c35c4dac92115070\",\"dweb:/ipfs/QmYX2VTyTm6QLtgp54kCrkAGY8uPxkx28urwLNEJsxTHJs\"]},\"src/SignatureTransfer.sol\":{\"keccak256\":\"0xa821caa24d6231fa8befe24a34bfda2c3b05b56e67fb913c86b26a19b19b6bbe\",\"license\":\"MIT\",\"urls\":[\"bzz-raw://584994a77e33aa2fe804b803ab302cb811ee945632b76f68d78db761b18a24a2\",\"dweb:/ipfs/QmVd67VRKX24tSaREBNwhzVfU6xxqRLNEoPY6CYgG3xU5W\"]},\"src/interfaces/IAllowanceTransfer.sol\":{\"keccak256\":\"0x37f0ac203b6ef605c9533e1a739477e8e9dcea90710b40e645a367f8a21ace29\",\"license\":\"MIT\",\"urls\":[\"bzz-raw://e0104d72aeaec1cd66cc232e7de7b7ead08608efcc179491b8a66387614670b0\",\"dweb:/ipfs/QmfAZDyuNC9FXXbnJUwqHNwmAK6uRrXxtWEytLsxjskPsN\"]},\"src/interfaces/IEIP712.sol\":{\"keccak256\":\"0xfdccf2b9639070803cd0e4198427fb0df3cc452ca59bd3b8a0d957a9a4254138\",\"license\":\"MIT\",\"urls\":[\"bzz-raw://f7c936ac42ce89e827db905a1544397f8bdf46db34cdb6aa1b90dea42fdb4c72\",\"dweb:/ipfs/QmVgurxo1N31qZqkPBirw9Z7S9tLYmv6jSwQp8R8ur2cBk\"]},\"src/interfaces/IERC1271.sol\":{\"keccak256\":\"0x0a546b8535127fb4a49d36d5f306fd5a8bbe6125a1852f935b9bb85a04c1acef\",\"license\":\"MIT\",\"urls\":[\"bzz-raw://4b99651e2df98e283a97c46d8d1ac4eff0d6a3618e25f7f85294472a670b541c\",\"dweb:/ipfs/QmYRy5G8fXE8BfmyvGEbESEYZPPg3zJEFxHzR5GJZEMMTk\"]},\"src/interfaces/ISignatureTransfer.sol\":{\"keccak256\":\"0xe6df9966f8841dc3958ee86169c89de97e7f614c81c28b9dc947b12d732df64e\",\"license\":\"MIT\",\"urls\":[\"bzz-raw://3d4eafdee7f48c3be8350a94eb6edd0bfb2af2c105df65787a77174f356c0317\",\"dweb:/ipfs/QmY1j2adeeAhNpn6cUuthemxGCdLXHTfyMh9yTKsY4mZ2d\"]},\"src/libraries/Allowance.sol\":{\"keccak256\":\"0x65ee20fb1a77d4e25dff2feb84027ff9096b065b6fc064c80f9eee49f1f9d498\",\"license\":\"MIT\",\"urls\":[\"bzz-raw://8f65d62fc64a55b6e3aad9932959ab3f47d701c45f95622215aca0ba076f1a7d\",\"dweb:/ipfs/QmZjDb4Nq9pssFefg8X9bwJNJ4RJEPD8vCaFR2Ur2N4boD\"]},\"src/libraries/PermitHash.sol\":{\"keccak256\":\"0x54af80d9c3193934c6947c31f59b8f3d7918f83676fe92ed6136593ad591d478\",\"license\":\"MIT\",\"urls\":[\"bzz-raw://5264001770be2cdeb7651e4d22af7edbc4e16da6d38747efeb4f54b5472ca5c5\",\"dweb:/ipfs/QmPvwau7DXw6stGQ14hpyTeLdYDYrrrdMnUfkQTPpMXQxz\"]},\"src/libraries/SignatureVerification.sol\":{\"keccak256\":\"0x99f437ffe99aa1ff7885aec8b971f48efac00c6ebc59c02eec78c9ca850a5e30\",\"license\":\"MIT\",\"urls\":[\"bzz-raw://9365414bdb67813d4ef6c89fa152dff05fc2a64992a1a4f212fa414dbdee3eab\",\"dweb:/ipfs/QmfJxSszF1rjmMoNXW5oQMo9gARMHAXYTu68fkZvdEu58i\"]}},\"version\":1}","metadata":{"compiler":{"version":"0.8.17+commit.8df45f5f"},"language":"Solidity","output":{"abi":[{"inputs":[{"internalType":"uint256","name":"deadline","type":"uint256"}],"type":"error","name":"AllowanceExpired"},{"inputs":[],"type":"error","name":"ExcessiveInvalidation"},{"inputs":[{"internalType":"uint256","name":"amount","type":"uint256"}],"type":"error","name":"InsufficientAllowance"},{"inputs":[{"internalType":"uint256","name":"maxAmount","type":"uint256"}],"type":"error","name":"InvalidAmount"},{"inputs":[],"type":"error","name":"InvalidContractSignature"},{"inputs":[],"type":"error","name":"InvalidNonce"},{"inputs":[],"type":"error","name":"InvalidSignature"},{"inputs":[],"type":"error","name":"InvalidSignatureLength"},{"inputs":[],"type":"error","name":"InvalidSigner"},{"inputs":[],"type":"error","name":"LengthMismatch"},{"inputs":[{"internalType":"uint256","name":"signatureDeadline","type":"uint256"}],"type":"error","name":"SignatureExpired"},{"inputs":[{"internalType":"address","name":"owner","type":"address","indexed":true},{"internalType":"address","name":"token","type":"address","indexed":true},{"internalType":"address","name":"spender","type":"address","indexed":true},{"internalType":"uint160","name":"amount","type":"uint160","indexed":false},{"internalType":"uint48","name":"expiration","type":"uint48","indexed":false}],"type":"event","name":"Approval","anonymous":false},{"inputs":[{"internalType":"address","name":"owner","type":"address","indexed":true},{"internalType":"address","name":"token","type":"address","indexed":false},{"internalType":"address","name":"spender","type":"address","indexed":false}],"type":"event","name":"Lockdown","anonymous":false},{"inputs":[{"internalType":"address","name":"owner","type":"address","indexed":true},{"internalType":"address","name":"token","type":"address","indexed":true},{"internalType":"address","name":"spender","type":"address","indexed":true},{"internalType":"uint48","name":"newNonce","type":"uint48","indexed":false},{"internalType":"uint48","name":"oldNonce","type":"uint48","indexed":false}],"type":"event","name":"NonceInvalidation","anonymous":false},{"inputs":[{"internalType":"address","name":"owner","type":"address","indexed":true},{"internalType":"address","name":"token","type":"address","indexed":true},{"internalType":"address","name":"spender","type":"address","indexed":true},{"internalType":"uint160","name":"amount","type":"uint160","indexed":false},{"internalType":"uint48","name":"expiration","type":"uint48","indexed":false},{"internalType":"uint48","name":"nonce","type":"uint48","indexed":false}],"type":"event","name":"Permit","anonymous":false},{"inputs":[{"internalType":"address","name":"owner","type":"address","indexed":true},{"internalType":"uint256","name":"word","type":"uint256","indexed":false},{"internalType":"uint256","name":"mask","type":"uint256","indexed":false}],"type":"event","name":"UnorderedNonceInvalidation","anonymous":false},{"inputs":[],"stateMutability":"view","type":"function","name":"DOMAIN_SEPARATOR","outputs":[{"internalType":"bytes32","name":"","type":"bytes32"}]},{"inputs":[{"internalType":"address","name":"","type":"address"},{"internalType":"address","name":"","type":"address"},{"internalType":"address","name":"","type":"address"}],"stateMutability":"view","type":"function","name":"allowance","outputs":[{"internalType":"uint160","name":"amount","type":"uint160"},{"internalType":"uint48","name":"expiration","type":"uint48"},{"internalType":"uint48","name":"nonce","type":"uint48"}]},{"inputs":[{"internalType":"address","name":"token","type":"address"},{"internalType":"address","name":"spender","type":"address"},{"internalType":"uint160","name":"amount","type":"uint160"},{"internalType":"uint48","name":"expiration","type":"uint48"}],"stateMutability":"nonpayable","type":"function","name":"approve"},{"inputs":[{"internalType":"address","name":"token","type":"address"},{"internalType":"address","name":"spender","type":"address"},{"internalType":"uint48","name":"newNonce","type":"uint48"}],"stateMutability":"nonpayable","type":"function","name":"invalidateNonces"},{"inputs":[{"internalType":"uint256","name":"wordPos","type":"uint256"},{"internalType":"uint256","name":"mask","type":"uint256"}],"stateMutability":"nonpayable","type":"function","name":"invalidateUnorderedNonces"},{"inputs":[{"internalType":"struct IAllowanceTransfer.TokenSpenderPair[]","name":"approvals","type":"tuple[]","components":[{"internalType":"address","name":"token","type":"address"},{"internalType":"address","name":"spender","type":"address"}]}],"stateMutability":"nonpayable","type":"function","name":"lockdown"},{"inputs":[{"internalType":"address","name":"","type":"address"},{"internalType":"uint256","name":"","type":"uint256"}],"stateMutability":"view","type":"function","name":"nonceBitmap","outputs":[{"internalType":"uint256","name":"","type":"uint256"}]},{"inputs":[{"internalType":"address","name":"owner","type":"address"},{"internalType":"struct IAllowanceTransfer.PermitBatch","name":"permitBatch","type":"tuple","components":[{"internalType":"struct IAllowanceTransfer.PermitDetails[]","name":"details","type":"tuple[]","components":[{"internalType":"address","name":"token","type":"address"},{"internalType":"uint160","name":"amount","type":"uint160"},{"internalType":"uint48","name":"expiration","type":"uint48"},{"internalType":"uint48","name":"nonce","type":"uint48"}]},{"internalType":"address","name":"spender","type":"address"},{"internalType":"uint256","name":"sigDeadline","type":"uint256"}]},{"internalType":"bytes","name":"signature","type":"bytes"}],"stateMutability":"nonpayable","type":"function","name":"permit"},{"inputs":[{"internalType":"address","name":"owner","type":"address"},{"internalType":"struct IAllowanceTransfer.PermitSingle","name":"permitSingle","type":"tuple","components":[{"internalType":"struct IAllowanceTransfer.PermitDetails","name":"details","type":"tuple","components":[{"internalType":"address","name":"token","type":"address"},{"internalType":"uint160","name":"amount","type":"uint160"},{"internalType":"uint48","name":"expiration","type":"uint48"},{"internalType":"uint48","name":"nonce","type":"uint48"}]},{"internalType":"address","name":"spender","type":"address"},{"internalType":"uint256","name":"sigDeadline","type":"uint256"}]},{"internalType":"bytes","name":"signature","type":"bytes"}],"stateMutability":"nonpayable","type":"function","name":"permit"},{"inputs":[{"internalType":"struct ISignatureTransfer.PermitTransferFrom","name":"permit","type":"tuple","components":[{"internalType":"struct ISignatureTransfer.TokenPermissions","name":"permitted","type":"tuple","components":[{"internalType":"address","name":"token","type":"address"},{"internalType":"uint256","name":"amount","type":"uint256"}]},{"internalType":"uint256","name":"nonce","type":"uint256"},{"internalType":"uint256","name":"deadline","type":"uint256"}]},{"internalType":"struct ISignatureTransfer.SignatureTransferDetails","name":"transferDetails","type":"tuple","components":[{"internalType":"address","name":"to","type":"address"},{"internalType":"uint256","name":"requestedAmount","type":"uint256"}]},{"internalType":"address","name":"owner","type":"address"},{"internalType":"bytes","name":"signature","type":"bytes"}],"stateMutability":"nonpayable","type":"function","name":"permitTransferFrom"},{"inputs":[{"internalType":"struct ISignatureTransfer.PermitBatchTransferFrom","name":"permit","type":"tuple","components":[{"internalType":"struct ISignatureTransfer.TokenPermissions[]","name":"permitted","type":"tuple[]","components":[{"internalType":"address","name":"token","type":"address"},{"internalType":"uint256","name":"amount","type":"uint256"}]},{"internalType":"uint256","name":"nonce","type":"uint256"},{"internalType":"uint256","name":"deadline","type":"uint256"}]},{"internalType":"struct ISignatureTransfer.SignatureTransferDetails[]","name":"transferDetails","type":"tuple[]","components":[{"internalType":"address","name":"to","type":"address"},{"internalType":"uint256","name":"requestedAmount","type":"uint256"}]},{"internalType":"address","name":"owner","type":"address"},{"internalType":"bytes","name":"signature","type":"bytes"}],"stateMutability":"nonpayable","type":"function","name":"permitTransferFrom"},{"inputs":[{"internalType":"struct ISignatureTransfer.PermitTransferFrom","name":"permit","type":"tuple","components":[{"internalType":"struct ISignatureTransfer.TokenPermissions","name":"permitted","type":"tuple","components":[{"internalType":"address","name":"token","type":"address"},{"internalType":"uint256","name":"amount","type":"uint256"}]},{"internalType":"uint256","name":"nonce","type":"uint256"},{"internalType":"uint256","name":"deadline","type":"uint256"}]},{"internalType":"struct ISignatureTransfer.SignatureTransferDetails","name":"transferDetails","type":"tuple","components":[{"internalType":"address","name":"to","type":"address"},{"internalType":"uint256","name":"requestedAmount","type":"uint256"}]},{"internalType":"address","name":"owner","type":"address"},{"internalType":"bytes32","name":"witness","type":"bytes32"},{"internalType":"string","name":"witnessTypeString","type":"string"},{"internalType":"bytes","name":"signature","type":"bytes"}],"stateMutability":"nonpayable","type":"function","name":"permitWitnessTransferFrom"},{"inputs":[{"internalType":"struct ISignatureTransfer.PermitBatchTransferFrom","name":"permit","type":"tuple","components":[{"internalType":"struct ISignatureTransfer.TokenPermissions[]","name":"permitted","type":"tuple[]","components":[{"internalType":"address","name":"token","type":"address"},{"internalType":"uint256","name":"amount","type":"uint256"}]},{"internalType":"uint256","name":"nonce","type":"uint256"},{"internalType":"uint256","name":"deadline","type":"uint256"}]},{"internalType":"struct ISignatureTransfer.SignatureTransferDetails[]","name":"transferDetails","type":"tuple[]","components":[{"internalType":"address","name":"to","type":"address"},{"internalType":"uint256","name":"requestedAmount","type":"uint256"}]},{"internalType":"address","name":"owner","type":"address"},{"internalType":"bytes32","name":"witness","type":"bytes32"},{"internalType":"string","name":"witnessTypeString","type":"string"},{"internalType":"bytes","name":"signature","type":"bytes"}],"stateMutability":"nonpayable","type":"function","name":"permitWitnessTransferFrom"},{"inputs":[{"internalType":"struct IAllowanceTransfer.AllowanceTransferDetails[]","name":"transferDetails","type":"tuple[]","components":[{"internalType":"address","name":"from","type":"address"},{"internalType":"address","name":"to","type":"address"},{"internalType":"uint160","name":"amount","type":"uint160"},{"internalType":"address","name":"token","type":"address"}]}],"stateMutability":"nonpayable","type":"function","name":"transferFrom"},{"inputs":[{"internalType":"address","name":"from","type":"address"},{"internalType":"address","name":"to","type":"address"},{"internalType":"uint160","name":"amount","type":"uint160"},{"internalType":"address","name":"token","type":"address"}],"stateMutability":"nonpayable","type":"function","name":"transferFrom"}],"devdoc":{"kind":"dev","methods":{"DOMAIN_SEPARATOR()":{"details":"Uses cached version if chainid and address are unchanged from construction."},"approve(address,address,uint160,uint48)":{"details":"The packed allowance also holds a nonce, which will stay unchanged in approveSetting amount to type(uint160).max sets an unlimited approval","params":{"amount":"The approved amount of the token","expiration":"The timestamp at which the approval is no longer valid","spender":"The spender address to approve","token":"The token to approve"}},"invalidateNonces(address,address,uint48)":{"details":"Can't invalidate more than 2**16 nonces per transaction.","params":{"newNonce":"The new nonce to set. Invalidates all nonces less than it.","spender":"The spender to invalidate nonces for","token":"The token to invalidate nonces for"}},"invalidateUnorderedNonces(uint256,uint256)":{"details":"The wordPos is maxed at type(uint248).max","params":{"mask":"A bitmap masked against msg.sender's current bitmap at the word position","wordPos":"A number to index the nonceBitmap at"}},"lockdown((address,address)[])":{"params":{"approvals":"Array of approvals to revoke."}},"permit(address,((address,uint160,uint48,uint48),address,uint256),bytes)":{"details":"May fail if the owner's nonce was invalidated in-flight by invalidateNonce","params":{"owner":"The owner of the tokens being approved","permitSingle":"Data signed over by the owner specifying the terms of approval","signature":"The owner's signature over the permit data"}},"permit(address,((address,uint160,uint48,uint48)[],address,uint256),bytes)":{"details":"May fail if the owner's nonce was invalidated in-flight by invalidateNonce","params":{"owner":"The owner of the tokens being approved","permitBatch":"Data signed over by the owner specifying the terms of approval","signature":"The owner's signature over the permit data"}},"permitTransferFrom(((address,uint256),uint256,uint256),(address,uint256),address,bytes)":{"details":"Reverts if the requested amount is greater than the permitted signed amount","params":{"owner":"The owner of the tokens to transfer","permit":"The permit data signed over by the owner","signature":"The signature to verify","transferDetails":"The spender's requested transfer details for the permitted token"}},"permitTransferFrom(((address,uint256)[],uint256,uint256),(address,uint256)[],address,bytes)":{"params":{"owner":"The owner of the tokens to transfer","permit":"The permit data signed over by the owner","signature":"The signature to verify","transferDetails":"Specifies the recipient and requested amount for the token transfer"}},"permitWitnessTransferFrom(((address,uint256),uint256,uint256),(address,uint256),address,bytes32,string,bytes)":{"details":"The witness type string must follow EIP712 ordering of nested structs and must include the TokenPermissions type definitionReverts if the requested amount is greater than the permitted signed amount","params":{"owner":"The owner of the tokens to transfer","permit":"The permit data signed over by the owner","signature":"The signature to verify","transferDetails":"The spender's requested transfer details for the permitted token","witness":"Extra data to include when checking the user signature","witnessTypeString":"The EIP-712 type definition for remaining string stub of the typehash"}},"permitWitnessTransferFrom(((address,uint256)[],uint256,uint256),(address,uint256)[],address,bytes32,string,bytes)":{"details":"The witness type string must follow EIP712 ordering of nested structs and must include the TokenPermissions type definition","params":{"owner":"The owner of the tokens to transfer","permit":"The permit data signed over by the owner","signature":"The signature to verify","transferDetails":"Specifies the recipient and requested amount for the token transfer","witness":"Extra data to include when checking the user signature","witnessTypeString":"The EIP-712 type definition for remaining string stub of the typehash"}},"transferFrom((address,address,uint160,address)[])":{"details":"Requires the from addresses to have approved at least the desired amount of tokens to msg.sender.","params":{"transferDetails":"Array of owners, recipients, amounts, and tokens for the transfers"}},"transferFrom(address,address,uint160,address)":{"details":"Requires the from address to have approved at least the desired amount of tokens to msg.sender.","params":{"amount":"The amount of the token to transfer","from":"The address to transfer from","to":"The address of the recipient","token":"The token address to transfer"}}},"version":1},"userdoc":{"kind":"user","methods":{"DOMAIN_SEPARATOR()":{"notice":"Returns the domain separator for the current chain."},"allowance(address,address,address)":{"notice":"Maps users to tokens to spender addresses and information about the approval on the token"},"approve(address,address,uint160,uint48)":{"notice":"Approves the spender to use up to amount of the specified token up until the expiration"},"invalidateNonces(address,address,uint48)":{"notice":"Invalidate nonces for a given (token, spender) pair"},"invalidateUnorderedNonces(uint256,uint256)":{"notice":"Invalidates the bits specified in mask for the bitmap at the word position"},"lockdown((address,address)[])":{"notice":"Enables performing a \"lockdown\" of the sender's Permit2 identity by batch revoking approvals"},"nonceBitmap(address,uint256)":{"notice":"A map from token owner address and a caller specified word index to a bitmap. Used to set bits in the bitmap to prevent against signature replay protection"},"permit(address,((address,uint160,uint48,uint48),address,uint256),bytes)":{"notice":"Permit a spender to a given amount of the owners token via the owner's EIP-712 signature"},"permit(address,((address,uint160,uint48,uint48)[],address,uint256),bytes)":{"notice":"Permit a spender to the signed amounts of the owners tokens via the owner's EIP-712 signature"},"permitTransferFrom(((address,uint256),uint256,uint256),(address,uint256),address,bytes)":{"notice":"Transfers a token using a signed permit message"},"permitTransferFrom(((address,uint256)[],uint256,uint256),(address,uint256)[],address,bytes)":{"notice":"Transfers multiple tokens using a signed permit message"},"permitWitnessTransferFrom(((address,uint256),uint256,uint256),(address,uint256),address,bytes32,string,bytes)":{"notice":"Transfers a token using a signed permit messageIncludes extra data provided by the caller to verify signature over"},"permitWitnessTransferFrom(((address,uint256)[],uint256,uint256),(address,uint256)[],address,bytes32,string,bytes)":{"notice":"Transfers multiple tokens using a signed permit messageIncludes extra data provided by the caller to verify signature over"},"transferFrom((address,address,uint160,address)[])":{"notice":"Transfer approved tokens in a batch"},"transferFrom(address,address,uint160,address)":{"notice":"Transfer approved tokens from one address to another"}},"version":1}},"settings":{"remappings":["ds-test/=lib/forge-std/lib/ds-test/src/","forge-gas-snapshot/=lib/forge-gas-snapshot/src/","forge-std/=lib/forge-std/src/","openzeppelin-contracts/=lib/openzeppelin-contracts/","solmate/=lib/solmate/"],"optimizer":{"enabled":true,"runs":1000000},"metadata":{"bytecodeHash":"none"},"compilationTarget":{"src/Permit2.sol":"Permit2"},"evmVersion":"london","libraries":{},"viaIR":true},"sources":{"lib/solmate/src/tokens/ERC20.sol":{"keccak256":"0xcdfd8db76b2a3415620e4d18cc5545f3d50de792dbf2c3dd5adb40cbe6f94b10","urls":["bzz-raw://57b3ab70cde374af1cf2c9888636e8de6cf660f087b1c9abd805e9271e19fa35","dweb:/ipfs/QmNrLDBAHYFjpjSd12jerm1AdBkDqEYUUaXgnT854BUZ97"],"license":"AGPL-3.0-only"},"lib/solmate/src/utils/SafeTransferLib.sol":{"keccak256":"0xbadf3d708cf532b12f75f78a1d423135954b63774a6d4ba15914a551d348db8a","urls":["bzz-raw://88ac8256bd520d1b8e6f9c4ac9e8777bffdc4a6c8afb1a848f596665779a55b4","dweb:/ipfs/QmXx7X1dxe6f5VM91vgQ5BA4r2eF97GWDcQDrgHytcvfjU"],"license":"AGPL-3.0-only"},"src/AllowanceTransfer.sol":{"keccak256":"0x2e0a14330c1413091f5155d8df0506d753b3f1d6e8c9dcdbfb02cc05ac34b643","urls":["bzz-raw://a6965ea5d52145e4b1362bb9b8a9b9b640ecb13bd4d5d1770abc133c263265b6","dweb:/ipfs/Qme1X5vKR3Nvw36MpsdsyhzvkPzcztUNgJ9RF7umDr6vYY"],"license":"MIT"},"src/EIP712.sol":{"keccak256":"0x5ac9f1db92c3102fa28911c754cffc54c6bbd3eb793192b67c232c02fb974b99","urls":["bzz-raw://69218a8c22a7683c3ba9417f5629b8038f4793eb5245d49a5631e4ae4dbb90cc","dweb:/ipfs/QmfUtUiLE1aFiKrQPN7Y97M3P1TPiY5Lvwddv646awU3gt"],"license":"MIT"},"src/Permit2.sol":{"keccak256":"0x934c0eb24a52eb5900f01f5c328374b670366adf995ba9ed49bcd3d7b87b159e","urls":["bzz-raw://bdfd05b3007726dc6dd2822c1dd9dc1b2471fbec507f30efa71a1a214c98bab6","dweb:/ipfs/QmPq4hptCSUACQdynSa86bdEexE7RryzosrhUAZ9Xkqc5a"],"license":"MIT"},"src/PermitErrors.sol":{"keccak256":"0x9fd1192bbc3ffa9354f2bfc534d7a1cdf2be2c940c96ed4ac7bc37991e1e5dfe","urls":["bzz-raw://77f8b2e2c040c33e2c78f05e7e768a17f433c07adb699235c35c4dac92115070","dweb:/ipfs/QmYX2VTyTm6QLtgp54kCrkAGY8uPxkx28urwLNEJsxTHJs"],"license":"MIT"},"src/SignatureTransfer.sol":{"keccak256":"0xa821caa24d6231fa8befe24a34bfda2c3b05b56e67fb913c86b26a19b19b6bbe","urls":["bzz-raw://584994a77e33aa2fe804b803ab302cb811ee945632b76f68d78db761b18a24a2","dweb:/ipfs/QmVd67VRKX24tSaREBNwhzVfU6xxqRLNEoPY6CYgG3xU5W"],"license":"MIT"},"src/interfaces/IAllowanceTransfer.sol":{"keccak256":"0x37f0ac203b6ef605c9533e1a739477e8e9dcea90710b40e645a367f8a21ace29","urls":["bzz-raw://e0104d72aeaec1cd66cc232e7de7b7ead08608efcc179491b8a66387614670b0","dweb:/ipfs/QmfAZDyuNC9FXXbnJUwqHNwmAK6uRrXxtWEytLsxjskPsN"],"license":"MIT"},"src/interfaces/IEIP712.sol":{"keccak256":"0xfdccf2b9639070803cd0e4198427fb0df3cc452ca59bd3b8a0d957a9a4254138","urls":["bzz-raw://f7c936ac42ce89e827db905a1544397f8bdf46db34cdb6aa1b90dea42fdb4c72","dweb:/ipfs/QmVgurxo1N31qZqkPBirw9Z7S9tLYmv6jSwQp8R8ur2cBk"],"license":"MIT"},"src/interfaces/IERC1271.sol":{"keccak256":"0x0a546b8535127fb4a49d36d5f306fd5a8bbe6125a1852f935b9bb85a04c1acef","urls":["bzz-raw://4b99651e2df98e283a97c46d8d1ac4eff0d6a3618e25f7f85294472a670b541c","dweb:/ipfs/QmYRy5G8fXE8BfmyvGEbESEYZPPg3zJEFxHzR5GJZEMMTk"],"license":"MIT"},"src/interfaces/ISignatureTransfer.sol":{"keccak256":"0xe6df9966f8841dc3958ee86169c89de97e7f614c81c28b9dc947b12d732df64e","urls":["bzz-raw://3d4eafdee7f48c3be8350a94eb6edd0bfb2af2c105df65787a77174f356c0317","dweb:/ipfs/QmY1j2adeeAhNpn6cUuthemxGCdLXHTfyMh9yTKsY4mZ2d"],"license":"MIT"},"src/libraries/Allowance.sol":{"keccak256":"0x65ee20fb1a77d4e25dff2feb84027ff9096b065b6fc064c80f9eee49f1f9d498","urls":["bzz-raw://8f65d62fc64a55b6e3aad9932959ab3f47d701c45f95622215aca0ba076f1a7d","dweb:/ipfs/QmZjDb4Nq9pssFefg8X9bwJNJ4RJEPD8vCaFR2Ur2N4boD"],"license":"MIT"},"src/libraries/PermitHash.sol":{"keccak256":"0x54af80d9c3193934c6947c31f59b8f3d7918f83676fe92ed6136593ad591d478","urls":["bzz-raw://5264001770be2cdeb7651e4d22af7edbc4e16da6d38747efeb4f54b5472ca5c5","dweb:/ipfs/QmPvwau7DXw6stGQ14hpyTeLdYDYrrrdMnUfkQTPpMXQxz"],"license":"MIT"},"src/libraries/SignatureVerification.sol":{"keccak256":"0x99f437ffe99aa1ff7885aec8b971f48efac00c6ebc59c02eec78c9ca850a5e30","urls":["bzz-raw://9365414bdb67813d4ef6c89fa152dff05fc2a64992a1a4f212fa414dbdee3eab","dweb:/ipfs/QmfJxSszF1rjmMoNXW5oQMo9gARMHAXYTu68fkZvdEu58i"],"license":"MIT"}},"version":1},"id":37} \ No newline at end of file diff --git a/assets/artifacts/README.md b/assets/artifacts/README.md deleted file mode 100644 index 94dca68..0000000 --- a/assets/artifacts/README.md +++ /dev/null @@ -1,43 +0,0 @@ -# External contract artifacts - -## DeterministicDeploymentProxy - -[Repository](https://github.com/Arachnid/deterministic-deployment-proxy) | [Etherscan](https://etherscan.io/address/0x4e59b44847b379578588920ca78fbf26c0b4956c) - -## Multicall3 - -[Repository](https://github.com/mds1/multicall3) | [Etherscan](https://etherscan.io/address/0xca11bde05977b3631167028862be2a173976ca11) - -## Permit2 - -[Repository](https://github.com/Uniswap/permit2/blob/0x000000000022D473030F116dDEE9F6B43aC78BA3) | [Etherscan](https://etherscan.io/address/0x000000000022d473030f116ddee9f6b43ac78ba3) - -- Solidity version 0.8.17 - -Because in [EIP712](https://github.com/Uniswap/permit2/blob/main/src/EIP712.sol#L20), there are two variables initialized by chain ID in the constructor. The code will not be the same on different EVM blockchains. - -The difference between forge chainID 31337 (0x7a69) and Ethereum chainID 1. - -```diff - 00001b20: CHAINID -+00001b21: PUSH32 0x0000000000000000000000000000000000000000000000000000000000007a69 --00001b21: PUSH32 0x0000000000000000000000000000000000000000000000000000000000000001 - 00001b42: SUB - 00001b43: PUSH2 0x1b69 - 00001b46: JUMPI -+00001b47: PUSH32 0x4d553c58ae79a6c4ba64f0e690a5d1cd2deff8c6b91cf38300e0f2b76f9ee346 --00001b47: PUSH32 0x866a5aba21966af95d6c7ab78eb2b2fc913915c28be3b9aa07cc04ff903e3f28 - 00001b68: SWAP1 -``` - -# Verify the manifest.json and artifacts - -```bash -forge test -vvv --match-contract VerifyArtifactTest -``` - -Compare the code hash with Ethereum mainnet. - -```bash -ETH_RPC_URL=https://eth-mainnet.g.alchemy.com/v2/**** forge test -vvv --match-contract VerifyArtifactTest -``` diff --git a/assets/artifacts/manifest.json b/assets/artifacts/manifest.json deleted file mode 100644 index 7ec5007..0000000 --- a/assets/artifacts/manifest.json +++ /dev/null @@ -1,36 +0,0 @@ -{ - "DeterministicDeploymentProxy": { - "address": "0x4e59b44847b379578588920ca78fbf26c0b4956c", - "type": "one-time-address", - "deployer": "0x3fab184622dc19b6109349b94811493bf2a45362", - "deployerBalance": "10000000000000000", - "rawTransaction": "0xf8a58085174876e800830186a08080b853604580600e600039806000f350fe7fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffe03601600081602082378035828234f58015156039578182fd5b8082525050506014600cf31ba02222222222222222222222222222222222222222222222222222222222222222a02222222222222222222222222222222222222222222222222222222222222222", - "ethCodeHash": "0x2fa86add0aed31f33a762c9d88e807c475bd51d0f52bd0955754b2608f7e4989" - }, - "Multicall3": { - "address": "0xca11bde05977b3631167028862be2a173976ca11", - "type": "one-time-address", - "deployer": "0x05f32b3cc3888453ff71b01135b34ff8e41263f2", - "deployerBalance": "100000000000000000", - "rawTransaction": "0xf90f538085174876e800830f42408080b90f00608060405234801561001057600080fd5b50610ee0806100206000396000f3fe6080604052600436106100f35760003560e01c80634d2301cc1161008a578063a8b0574e11610059578063a8b0574e1461025a578063bce38bd714610275578063c3077fa914610288578063ee82ac5e1461029b57600080fd5b80634d2301cc146101ec57806372425d9d1461022157806382ad56cb1461023457806386d516e81461024757600080fd5b80633408e470116100c65780633408e47014610191578063399542e9146101a45780633e64a696146101c657806342cbb15c146101d957600080fd5b80630f28c97d146100f8578063174dea711461011a578063252dba421461013a57806327e86d6e1461015b575b600080fd5b34801561010457600080fd5b50425b6040519081526020015b60405180910390f35b61012d610128366004610a85565b6102ba565b6040516101119190610bbe565b61014d610148366004610a85565b6104ef565b604051610111929190610bd8565b34801561016757600080fd5b50437fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff0140610107565b34801561019d57600080fd5b5046610107565b6101b76101b2366004610c60565b610690565b60405161011193929190610cba565b3480156101d257600080fd5b5048610107565b3480156101e557600080fd5b5043610107565b3480156101f857600080fd5b50610107610207366004610ce2565b73ffffffffffffffffffffffffffffffffffffffff163190565b34801561022d57600080fd5b5044610107565b61012d610242366004610a85565b6106ab565b34801561025357600080fd5b5045610107565b34801561026657600080fd5b50604051418152602001610111565b61012d610283366004610c60565b61085a565b6101b7610296366004610a85565b610a1a565b3480156102a757600080fd5b506101076102b6366004610d18565b4090565b60606000828067ffffffffffffffff8111156102d8576102d8610d31565b60405190808252806020026020018201604052801561031e57816020015b6040805180820190915260008152606060208201528152602001906001900390816102f65790505b5092503660005b8281101561047757600085828151811061034157610341610d60565b6020026020010151905087878381811061035d5761035d610d60565b905060200281019061036f9190610d8f565b6040810135958601959093506103886020850185610ce2565b73ffffffffffffffffffffffffffffffffffffffff16816103ac6060870187610dcd565b6040516103ba929190610e32565b60006040518083038185875af1925050503d80600081146103f7576040519150601f19603f3d011682016040523d82523d6000602084013e6103fc565b606091505b50602080850191909152901515808452908501351761046d577f08c379a000000000000000000000000000000000000000000000000000000000600052602060045260176024527f4d756c746963616c6c333a2063616c6c206661696c656400000000000000000060445260846000fd5b5050600101610325565b508234146104e6576040517f08c379a000000000000000000000000000000000000000000000000000000000815260206004820152601a60248201527f4d756c746963616c6c333a2076616c7565206d69736d6174636800000000000060448201526064015b60405180910390fd5b50505092915050565b436060828067ffffffffffffffff81111561050c5761050c610d31565b60405190808252806020026020018201604052801561053f57816020015b606081526020019060019003908161052a5790505b5091503660005b8281101561068657600087878381811061056257610562610d60565b90506020028101906105749190610e42565b92506105836020840184610ce2565b73ffffffffffffffffffffffffffffffffffffffff166105a66020850185610dcd565b6040516105b4929190610e32565b6000604051808303816000865af19150503d80600081146105f1576040519150601f19603f3d011682016040523d82523d6000602084013e6105f6565b606091505b5086848151811061060957610609610d60565b602090810291909101015290508061067d576040517f08c379a000000000000000000000000000000000000000000000000000000000815260206004820152601760248201527f4d756c746963616c6c333a2063616c6c206661696c656400000000000000000060448201526064016104dd565b50600101610546565b5050509250929050565b43804060606106a086868661085a565b905093509350939050565b6060818067ffffffffffffffff8111156106c7576106c7610d31565b60405190808252806020026020018201604052801561070d57816020015b6040805180820190915260008152606060208201528152602001906001900390816106e55790505b5091503660005b828110156104e657600084828151811061073057610730610d60565b6020026020010151905086868381811061074c5761074c610d60565b905060200281019061075e9190610e76565b925061076d6020840184610ce2565b73ffffffffffffffffffffffffffffffffffffffff166107906040850185610dcd565b60405161079e929190610e32565b6000604051808303816000865af19150503d80600081146107db576040519150601f19603f3d011682016040523d82523d6000602084013e6107e0565b606091505b506020808401919091529015158083529084013517610851577f08c379a000000000000000000000000000000000000000000000000000000000600052602060045260176024527f4d756c746963616c6c333a2063616c6c206661696c656400000000000000000060445260646000fd5b50600101610714565b6060818067ffffffffffffffff81111561087657610876610d31565b6040519080825280602002602001820160405280156108bc57816020015b6040805180820190915260008152606060208201528152602001906001900390816108945790505b5091503660005b82811015610a105760008482815181106108df576108df610d60565b602002602001015190508686838181106108fb576108fb610d60565b905060200281019061090d9190610e42565b925061091c6020840184610ce2565b73ffffffffffffffffffffffffffffffffffffffff1661093f6020850185610dcd565b60405161094d929190610e32565b6000604051808303816000865af19150503d806000811461098a576040519150601f19603f3d011682016040523d82523d6000602084013e61098f565b606091505b506020830152151581528715610a07578051610a07576040517f08c379a000000000000000000000000000000000000000000000000000000000815260206004820152601760248201527f4d756c746963616c6c333a2063616c6c206661696c656400000000000000000060448201526064016104dd565b506001016108c3565b5050509392505050565b6000806060610a2b60018686610690565b919790965090945092505050565b60008083601f840112610a4b57600080fd5b50813567ffffffffffffffff811115610a6357600080fd5b6020830191508360208260051b8501011115610a7e57600080fd5b9250929050565b60008060208385031215610a9857600080fd5b823567ffffffffffffffff811115610aaf57600080fd5b610abb85828601610a39565b90969095509350505050565b6000815180845260005b81811015610aed57602081850181015186830182015201610ad1565b81811115610aff576000602083870101525b50601f017fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffe0169290920160200192915050565b600082825180855260208086019550808260051b84010181860160005b84811015610bb1578583037fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffe001895281518051151584528401516040858501819052610b9d81860183610ac7565b9a86019a9450505090830190600101610b4f565b5090979650505050505050565b602081526000610bd16020830184610b32565b9392505050565b600060408201848352602060408185015281855180845260608601915060608160051b870101935082870160005b82811015610c52577fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffa0888703018452610c40868351610ac7565b95509284019290840190600101610c06565b509398975050505050505050565b600080600060408486031215610c7557600080fd5b83358015158114610c8557600080fd5b9250602084013567ffffffffffffffff811115610ca157600080fd5b610cad86828701610a39565b9497909650939450505050565b838152826020820152606060408201526000610cd96060830184610b32565b95945050505050565b600060208284031215610cf457600080fd5b813573ffffffffffffffffffffffffffffffffffffffff81168114610bd157600080fd5b600060208284031215610d2a57600080fd5b5035919050565b7f4e487b7100000000000000000000000000000000000000000000000000000000600052604160045260246000fd5b7f4e487b7100000000000000000000000000000000000000000000000000000000600052603260045260246000fd5b600082357fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff81833603018112610dc357600080fd5b9190910192915050565b60008083357fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffe1843603018112610e0257600080fd5b83018035915067ffffffffffffffff821115610e1d57600080fd5b602001915036819003821315610a7e57600080fd5b8183823760009101908152919050565b600082357fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffc1833603018112610dc357600080fd5b600082357fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffa1833603018112610dc357600080fdfea2646970667358221220bb2b5c71a328032f97c676ae39a1ec2148d3e5d6f73d95e9b17910152d61f16264736f6c634300080c00331ca0edce47092c0f398cebf3ffc267f05c8e7076e3b89445e0fe50f6332273d4569ba01b0b9d000e19b24c5869b0fc3b22b0d6fa47cd63316875cbbd577d76e6fde086", - "ethCodeHash": "0xd5c15df687b16f2ff992fc8d767b4216323184a2bbc6ee2f9c398c318e770891" - }, - "BlockHashHistory": { - "address": "0x0000F90827F1C53a10cb7A02335B175320002935", - "type": "one-time-address", - "deployer": "0x3462413Af4609098e1E27A490f554f260213D685", - "deployerBalance": "250000000000000000", - "rawTransaction": "0xf8838085e8d4a510008303d0908080b85c60538060095f395ff33373fffffffffffffffffffffffffffffffffffffffe14604657602036036042575f35600143038111604257611fff81430311604257611fff9006545f5260205ff35b5f5ffd5b5f35611fff600143030655001b820539930aa12693182426612186309f02cfe8a80a0000", - "ethCodeHash": "0x6e49e66782037c0555897870e29fa5e552daf4719552131a0abce779daec0a5d" - }, - "Permit2": { - "type": "deterministic", - "salt": "0x0000000000000000000000000000000000000000d3af2663da51c10215000000", - "address": "0x000000000022D473030F116dDEE9F6B43aC78BA3", - "ethCodeHash": "0xc67d1657868aa5146eaf24fb879fb1fdec3d2d493b3683a61c9c2f4fb2851131", - "bytecode": { - "file": "assets/artifacts/Permit2/Permit2.json", - "selector": ".bytecode.object" - } - } -} diff --git a/assets/artifacts/stablecoin-contracts/FiatTokenProxy.json b/assets/artifacts/stablecoin-contracts/FiatTokenProxy.json deleted file mode 100644 index 81f77cb..0000000 --- a/assets/artifacts/stablecoin-contracts/FiatTokenProxy.json +++ /dev/null @@ -1,128 +0,0 @@ -{ - "_format": "hh-sol-artifact-1", - "contractName": "FiatTokenProxy", - "sourceName": "contracts/v1/FiatTokenProxy.sol", - "abi": [ - { - "inputs": [ - { - "internalType": "address", - "name": "implementationContract", - "type": "address" - } - ], - "stateMutability": "nonpayable", - "type": "constructor" - }, - { - "anonymous": false, - "inputs": [ - { - "indexed": false, - "internalType": "address", - "name": "previousAdmin", - "type": "address" - }, - { - "indexed": false, - "internalType": "address", - "name": "newAdmin", - "type": "address" - } - ], - "name": "AdminChanged", - "type": "event" - }, - { - "anonymous": false, - "inputs": [ - { - "indexed": false, - "internalType": "address", - "name": "implementation", - "type": "address" - } - ], - "name": "Upgraded", - "type": "event" - }, - { - "stateMutability": "payable", - "type": "fallback" - }, - { - "inputs": [], - "name": "admin", - "outputs": [ - { - "internalType": "address", - "name": "", - "type": "address" - } - ], - "stateMutability": "view", - "type": "function" - }, - { - "inputs": [ - { - "internalType": "address", - "name": "newAdmin", - "type": "address" - } - ], - "name": "changeAdmin", - "outputs": [], - "stateMutability": "nonpayable", - "type": "function" - }, - { - "inputs": [], - "name": "implementation", - "outputs": [ - { - "internalType": "address", - "name": "", - "type": "address" - } - ], - "stateMutability": "view", - "type": "function" - }, - { - "inputs": [ - { - "internalType": "address", - "name": "newImplementation", - "type": "address" - } - ], - "name": "upgradeTo", - "outputs": [], - "stateMutability": "nonpayable", - "type": "function" - }, - { - "inputs": [ - { - "internalType": "address", - "name": "newImplementation", - "type": "address" - }, - { - "internalType": "bytes", - "name": "data", - "type": "bytes" - } - ], - "name": "upgradeToAndCall", - "outputs": [], - "stateMutability": "payable", - "type": "function" - } - ], - "bytecode": "0x608060405234801561001057600080fd5b5060405161083d38038061083d8339818101604052602081101561003357600080fd5b5051808061004081610051565b5061004a336100c3565b50506100ed565b610064816100e760201b61042a1760201c565b61009f5760405162461bcd60e51b815260040180806020018281038252603b815260200180610802603b913960400191505060405180910390fd5b7f7050c9e0f4ca769c69bd3a8ef740bc37934f8e2c036e5a723fd8ee048ed3f8c355565b7f10d6a54a4754c8869d6886b5f5d7fbfa5b4522237ea5c60d11bc4e7a1ff9390b55565b3b151590565b610706806100fc6000396000f3fe60806040526004361061005a5760003560e01c80635c60da1b116100435780635c60da1b146101315780638f2839701461016f578063f851a440146101af5761005a565b80633659cfe6146100645780634f1ef286146100a4575b6100626101c4565b005b34801561007057600080fd5b506100626004803603602081101561008757600080fd5b503573ffffffffffffffffffffffffffffffffffffffff166101de565b610062600480360360408110156100ba57600080fd5b73ffffffffffffffffffffffffffffffffffffffff82351691908101906040810160208201356401000000008111156100f257600080fd5b82018360208201111561010457600080fd5b8035906020019184600183028401116401000000008311171561012657600080fd5b509092509050610232565b34801561013d57600080fd5b50610146610309565b6040805173ffffffffffffffffffffffffffffffffffffffff9092168252519081900360200190f35b34801561017b57600080fd5b506100626004803603602081101561019257600080fd5b503573ffffffffffffffffffffffffffffffffffffffff16610318565b3480156101bb57600080fd5b50610146610420565b6101cc610430565b6101dc6101d76104c4565b6104e9565b565b6101e661050d565b73ffffffffffffffffffffffffffffffffffffffff163373ffffffffffffffffffffffffffffffffffffffff1614156102275761022281610532565b61022f565b61022f6101c4565b50565b61023a61050d565b73ffffffffffffffffffffffffffffffffffffffff163373ffffffffffffffffffffffffffffffffffffffff1614156102fc5761027683610532565b60003073ffffffffffffffffffffffffffffffffffffffff16348484604051808383808284376040519201945060009350909150508083038185875af1925050503d80600081146102e3576040519150601f19603f3d011682016040523d82523d6000602084013e6102e8565b606091505b50509050806102f657600080fd5b50610304565b6103046101c4565b505050565b60006103136104c4565b905090565b61032061050d565b73ffffffffffffffffffffffffffffffffffffffff163373ffffffffffffffffffffffffffffffffffffffff1614156102275773ffffffffffffffffffffffffffffffffffffffff81166103bf576040517f08c379a00000000000000000000000000000000000000000000000000000000081526004018080602001828103825260368152602001806106606036913960400191505060405180910390fd5b7f7e644d79422f17c01e4894b5f4f588d331ebfa28653d42ae832dc59e38c9798f6103e861050d565b6040805173ffffffffffffffffffffffffffffffffffffffff928316815291841660208301528051918290030190a161022281610587565b600061031361050d565b3b151590565b61043861050d565b73ffffffffffffffffffffffffffffffffffffffff163373ffffffffffffffffffffffffffffffffffffffff1614156104bc576040517f08c379a000000000000000000000000000000000000000000000000000000000815260040180806020018281038252603281526020018061062e6032913960400191505060405180910390fd5b6101dc6101dc565b7f7050c9e0f4ca769c69bd3a8ef740bc37934f8e2c036e5a723fd8ee048ed3f8c35490565b3660008037600080366000845af43d6000803e808015610508573d6000f35b3d6000fd5b7f10d6a54a4754c8869d6886b5f5d7fbfa5b4522237ea5c60d11bc4e7a1ff9390b5490565b61053b816105ab565b6040805173ffffffffffffffffffffffffffffffffffffffff8316815290517fbc7cd75a20ee27fd9adebab32041f755214dbc6bffa90cc0225b39da2e5c2d3b9181900360200190a150565b7f10d6a54a4754c8869d6886b5f5d7fbfa5b4522237ea5c60d11bc4e7a1ff9390b55565b6105b48161042a565b610609576040517f08c379a000000000000000000000000000000000000000000000000000000000815260040180806020018281038252603b815260200180610696603b913960400191505060405180910390fd5b7f7050c9e0f4ca769c69bd3a8ef740bc37934f8e2c036e5a723fd8ee048ed3f8c35556fe43616e6e6f742063616c6c2066616c6c6261636b2066756e6374696f6e2066726f6d207468652070726f78792061646d696e43616e6e6f74206368616e6765207468652061646d696e206f6620612070726f787920746f20746865207a65726f206164647265737343616e6e6f742073657420612070726f787920696d706c656d656e746174696f6e20746f2061206e6f6e2d636f6e74726163742061646472657373a2646970667358221220015908007e367e7f333ec38084b88f027c06160d2c19e5bdd8027e8d06acf8bf64736f6c634300060c003343616e6e6f742073657420612070726f787920696d706c656d656e746174696f6e20746f2061206e6f6e2d636f6e74726163742061646472657373", - "deployedBytecode": "0x60806040526004361061005a5760003560e01c80635c60da1b116100435780635c60da1b146101315780638f2839701461016f578063f851a440146101af5761005a565b80633659cfe6146100645780634f1ef286146100a4575b6100626101c4565b005b34801561007057600080fd5b506100626004803603602081101561008757600080fd5b503573ffffffffffffffffffffffffffffffffffffffff166101de565b610062600480360360408110156100ba57600080fd5b73ffffffffffffffffffffffffffffffffffffffff82351691908101906040810160208201356401000000008111156100f257600080fd5b82018360208201111561010457600080fd5b8035906020019184600183028401116401000000008311171561012657600080fd5b509092509050610232565b34801561013d57600080fd5b50610146610309565b6040805173ffffffffffffffffffffffffffffffffffffffff9092168252519081900360200190f35b34801561017b57600080fd5b506100626004803603602081101561019257600080fd5b503573ffffffffffffffffffffffffffffffffffffffff16610318565b3480156101bb57600080fd5b50610146610420565b6101cc610430565b6101dc6101d76104c4565b6104e9565b565b6101e661050d565b73ffffffffffffffffffffffffffffffffffffffff163373ffffffffffffffffffffffffffffffffffffffff1614156102275761022281610532565b61022f565b61022f6101c4565b50565b61023a61050d565b73ffffffffffffffffffffffffffffffffffffffff163373ffffffffffffffffffffffffffffffffffffffff1614156102fc5761027683610532565b60003073ffffffffffffffffffffffffffffffffffffffff16348484604051808383808284376040519201945060009350909150508083038185875af1925050503d80600081146102e3576040519150601f19603f3d011682016040523d82523d6000602084013e6102e8565b606091505b50509050806102f657600080fd5b50610304565b6103046101c4565b505050565b60006103136104c4565b905090565b61032061050d565b73ffffffffffffffffffffffffffffffffffffffff163373ffffffffffffffffffffffffffffffffffffffff1614156102275773ffffffffffffffffffffffffffffffffffffffff81166103bf576040517f08c379a00000000000000000000000000000000000000000000000000000000081526004018080602001828103825260368152602001806106606036913960400191505060405180910390fd5b7f7e644d79422f17c01e4894b5f4f588d331ebfa28653d42ae832dc59e38c9798f6103e861050d565b6040805173ffffffffffffffffffffffffffffffffffffffff928316815291841660208301528051918290030190a161022281610587565b600061031361050d565b3b151590565b61043861050d565b73ffffffffffffffffffffffffffffffffffffffff163373ffffffffffffffffffffffffffffffffffffffff1614156104bc576040517f08c379a000000000000000000000000000000000000000000000000000000000815260040180806020018281038252603281526020018061062e6032913960400191505060405180910390fd5b6101dc6101dc565b7f7050c9e0f4ca769c69bd3a8ef740bc37934f8e2c036e5a723fd8ee048ed3f8c35490565b3660008037600080366000845af43d6000803e808015610508573d6000f35b3d6000fd5b7f10d6a54a4754c8869d6886b5f5d7fbfa5b4522237ea5c60d11bc4e7a1ff9390b5490565b61053b816105ab565b6040805173ffffffffffffffffffffffffffffffffffffffff8316815290517fbc7cd75a20ee27fd9adebab32041f755214dbc6bffa90cc0225b39da2e5c2d3b9181900360200190a150565b7f10d6a54a4754c8869d6886b5f5d7fbfa5b4522237ea5c60d11bc4e7a1ff9390b55565b6105b48161042a565b610609576040517f08c379a000000000000000000000000000000000000000000000000000000000815260040180806020018281038252603b815260200180610696603b913960400191505060405180910390fd5b7f7050c9e0f4ca769c69bd3a8ef740bc37934f8e2c036e5a723fd8ee048ed3f8c35556fe43616e6e6f742063616c6c2066616c6c6261636b2066756e6374696f6e2066726f6d207468652070726f78792061646d696e43616e6e6f74206368616e6765207468652061646d696e206f6620612070726f787920746f20746865207a65726f206164647265737343616e6e6f742073657420612070726f787920696d706c656d656e746174696f6e20746f2061206e6f6e2d636f6e74726163742061646472657373a2646970667358221220015908007e367e7f333ec38084b88f027c06160d2c19e5bdd8027e8d06acf8bf64736f6c634300060c0033", - "linkReferences": {}, - "deployedLinkReferences": {} -} diff --git a/assets/artifacts/stablecoin-contracts/NativeFiatTokenV2_2.json b/assets/artifacts/stablecoin-contracts/NativeFiatTokenV2_2.json deleted file mode 100644 index 946c24e..0000000 --- a/assets/artifacts/stablecoin-contracts/NativeFiatTokenV2_2.json +++ /dev/null @@ -1,1462 +0,0 @@ -{ - "_format": "hh-sol-artifact-1", - "contractName": "NativeFiatTokenV2_2", - "sourceName": "contracts/v2/NativeFiatTokenV2_2.sol", - "abi": [ - { - "anonymous": false, - "inputs": [ - { - "indexed": true, - "internalType": "address", - "name": "owner", - "type": "address" - }, - { - "indexed": true, - "internalType": "address", - "name": "spender", - "type": "address" - }, - { - "indexed": false, - "internalType": "uint256", - "name": "value", - "type": "uint256" - } - ], - "name": "Approval", - "type": "event" - }, - { - "anonymous": false, - "inputs": [ - { - "indexed": true, - "internalType": "address", - "name": "authorizer", - "type": "address" - }, - { - "indexed": true, - "internalType": "bytes32", - "name": "nonce", - "type": "bytes32" - } - ], - "name": "AuthorizationCanceled", - "type": "event" - }, - { - "anonymous": false, - "inputs": [ - { - "indexed": true, - "internalType": "address", - "name": "authorizer", - "type": "address" - }, - { - "indexed": true, - "internalType": "bytes32", - "name": "nonce", - "type": "bytes32" - } - ], - "name": "AuthorizationUsed", - "type": "event" - }, - { - "anonymous": false, - "inputs": [ - { - "indexed": true, - "internalType": "address", - "name": "_account", - "type": "address" - } - ], - "name": "Blacklisted", - "type": "event" - }, - { - "anonymous": false, - "inputs": [ - { - "indexed": true, - "internalType": "address", - "name": "newBlacklister", - "type": "address" - } - ], - "name": "BlacklisterChanged", - "type": "event" - }, - { - "anonymous": false, - "inputs": [ - { - "indexed": true, - "internalType": "address", - "name": "burner", - "type": "address" - }, - { - "indexed": false, - "internalType": "uint256", - "name": "amount", - "type": "uint256" - } - ], - "name": "Burn", - "type": "event" - }, - { - "anonymous": false, - "inputs": [ - { - "indexed": true, - "internalType": "address", - "name": "newMasterMinter", - "type": "address" - } - ], - "name": "MasterMinterChanged", - "type": "event" - }, - { - "anonymous": false, - "inputs": [ - { - "indexed": true, - "internalType": "address", - "name": "minter", - "type": "address" - }, - { - "indexed": true, - "internalType": "address", - "name": "to", - "type": "address" - }, - { - "indexed": false, - "internalType": "uint256", - "name": "amount", - "type": "uint256" - } - ], - "name": "Mint", - "type": "event" - }, - { - "anonymous": false, - "inputs": [ - { - "indexed": true, - "internalType": "address", - "name": "minter", - "type": "address" - }, - { - "indexed": false, - "internalType": "uint256", - "name": "minterAllowedAmount", - "type": "uint256" - } - ], - "name": "MinterConfigured", - "type": "event" - }, - { - "anonymous": false, - "inputs": [ - { - "indexed": true, - "internalType": "address", - "name": "oldMinter", - "type": "address" - } - ], - "name": "MinterRemoved", - "type": "event" - }, - { - "anonymous": false, - "inputs": [ - { - "indexed": false, - "internalType": "address", - "name": "previousOwner", - "type": "address" - }, - { - "indexed": false, - "internalType": "address", - "name": "newOwner", - "type": "address" - } - ], - "name": "OwnershipTransferred", - "type": "event" - }, - { - "anonymous": false, - "inputs": [], - "name": "Pause", - "type": "event" - }, - { - "anonymous": false, - "inputs": [ - { - "indexed": true, - "internalType": "address", - "name": "newAddress", - "type": "address" - } - ], - "name": "PauserChanged", - "type": "event" - }, - { - "anonymous": false, - "inputs": [ - { - "indexed": true, - "internalType": "address", - "name": "newRescuer", - "type": "address" - } - ], - "name": "RescuerChanged", - "type": "event" - }, - { - "anonymous": false, - "inputs": [ - { - "indexed": true, - "internalType": "address", - "name": "from", - "type": "address" - }, - { - "indexed": true, - "internalType": "address", - "name": "to", - "type": "address" - }, - { - "indexed": false, - "internalType": "uint256", - "name": "value", - "type": "uint256" - } - ], - "name": "Transfer", - "type": "event" - }, - { - "anonymous": false, - "inputs": [ - { - "indexed": true, - "internalType": "address", - "name": "_account", - "type": "address" - } - ], - "name": "UnBlacklisted", - "type": "event" - }, - { - "anonymous": false, - "inputs": [], - "name": "Unpause", - "type": "event" - }, - { - "inputs": [], - "name": "CANCEL_AUTHORIZATION_TYPEHASH", - "outputs": [ - { - "internalType": "bytes32", - "name": "", - "type": "bytes32" - } - ], - "stateMutability": "view", - "type": "function" - }, - { - "inputs": [], - "name": "DECIMALS_SCALING_FACTOR", - "outputs": [ - { - "internalType": "uint256", - "name": "", - "type": "uint256" - } - ], - "stateMutability": "view", - "type": "function" - }, - { - "inputs": [], - "name": "DOMAIN_SEPARATOR", - "outputs": [ - { - "internalType": "bytes32", - "name": "", - "type": "bytes32" - } - ], - "stateMutability": "view", - "type": "function" - }, - { - "inputs": [], - "name": "NATIVE_COIN_AUTHORITY", - "outputs": [ - { - "internalType": "contract INativeCoinAuthority", - "name": "", - "type": "address" - } - ], - "stateMutability": "view", - "type": "function" - }, - { - "inputs": [], - "name": "NATIVE_COIN_CONTROL", - "outputs": [ - { - "internalType": "contract INativeCoinControl", - "name": "", - "type": "address" - } - ], - "stateMutability": "view", - "type": "function" - }, - { - "inputs": [], - "name": "PERMIT_TYPEHASH", - "outputs": [ - { - "internalType": "bytes32", - "name": "", - "type": "bytes32" - } - ], - "stateMutability": "view", - "type": "function" - }, - { - "inputs": [], - "name": "RECEIVE_WITH_AUTHORIZATION_TYPEHASH", - "outputs": [ - { - "internalType": "bytes32", - "name": "", - "type": "bytes32" - } - ], - "stateMutability": "view", - "type": "function" - }, - { - "inputs": [], - "name": "TRANSFER_WITH_AUTHORIZATION_TYPEHASH", - "outputs": [ - { - "internalType": "bytes32", - "name": "", - "type": "bytes32" - } - ], - "stateMutability": "view", - "type": "function" - }, - { - "inputs": [ - { - "internalType": "address", - "name": "owner", - "type": "address" - }, - { - "internalType": "address", - "name": "spender", - "type": "address" - } - ], - "name": "allowance", - "outputs": [ - { - "internalType": "uint256", - "name": "", - "type": "uint256" - } - ], - "stateMutability": "view", - "type": "function" - }, - { - "inputs": [ - { - "internalType": "address", - "name": "spender", - "type": "address" - }, - { - "internalType": "uint256", - "name": "value", - "type": "uint256" - } - ], - "name": "approve", - "outputs": [ - { - "internalType": "bool", - "name": "", - "type": "bool" - } - ], - "stateMutability": "nonpayable", - "type": "function" - }, - { - "inputs": [ - { - "internalType": "address", - "name": "authorizer", - "type": "address" - }, - { - "internalType": "bytes32", - "name": "nonce", - "type": "bytes32" - } - ], - "name": "authorizationState", - "outputs": [ - { - "internalType": "bool", - "name": "", - "type": "bool" - } - ], - "stateMutability": "view", - "type": "function" - }, - { - "inputs": [ - { - "internalType": "address", - "name": "account", - "type": "address" - } - ], - "name": "balanceOf", - "outputs": [ - { - "internalType": "uint256", - "name": "", - "type": "uint256" - } - ], - "stateMutability": "view", - "type": "function" - }, - { - "inputs": [ - { - "internalType": "address", - "name": "_account", - "type": "address" - } - ], - "name": "blacklist", - "outputs": [], - "stateMutability": "nonpayable", - "type": "function" - }, - { - "inputs": [], - "name": "blacklister", - "outputs": [ - { - "internalType": "address", - "name": "", - "type": "address" - } - ], - "stateMutability": "view", - "type": "function" - }, - { - "inputs": [ - { - "internalType": "uint256", - "name": "_amount", - "type": "uint256" - } - ], - "name": "burn", - "outputs": [], - "stateMutability": "nonpayable", - "type": "function" - }, - { - "inputs": [ - { - "internalType": "address", - "name": "authorizer", - "type": "address" - }, - { - "internalType": "bytes32", - "name": "nonce", - "type": "bytes32" - }, - { - "internalType": "uint8", - "name": "v", - "type": "uint8" - }, - { - "internalType": "bytes32", - "name": "r", - "type": "bytes32" - }, - { - "internalType": "bytes32", - "name": "s", - "type": "bytes32" - } - ], - "name": "cancelAuthorization", - "outputs": [], - "stateMutability": "nonpayable", - "type": "function" - }, - { - "inputs": [ - { - "internalType": "address", - "name": "authorizer", - "type": "address" - }, - { - "internalType": "bytes32", - "name": "nonce", - "type": "bytes32" - }, - { - "internalType": "bytes", - "name": "signature", - "type": "bytes" - } - ], - "name": "cancelAuthorization", - "outputs": [], - "stateMutability": "nonpayable", - "type": "function" - }, - { - "inputs": [ - { - "internalType": "address", - "name": "minter", - "type": "address" - }, - { - "internalType": "uint256", - "name": "minterAllowedAmount", - "type": "uint256" - } - ], - "name": "configureMinter", - "outputs": [ - { - "internalType": "bool", - "name": "", - "type": "bool" - } - ], - "stateMutability": "nonpayable", - "type": "function" - }, - { - "inputs": [], - "name": "currency", - "outputs": [ - { - "internalType": "string", - "name": "", - "type": "string" - } - ], - "stateMutability": "view", - "type": "function" - }, - { - "inputs": [], - "name": "decimals", - "outputs": [ - { - "internalType": "uint8", - "name": "", - "type": "uint8" - } - ], - "stateMutability": "view", - "type": "function" - }, - { - "inputs": [ - { - "internalType": "address", - "name": "spender", - "type": "address" - }, - { - "internalType": "uint256", - "name": "decrement", - "type": "uint256" - } - ], - "name": "decreaseAllowance", - "outputs": [ - { - "internalType": "bool", - "name": "", - "type": "bool" - } - ], - "stateMutability": "nonpayable", - "type": "function" - }, - { - "inputs": [ - { - "internalType": "address", - "name": "spender", - "type": "address" - }, - { - "internalType": "uint256", - "name": "increment", - "type": "uint256" - } - ], - "name": "increaseAllowance", - "outputs": [ - { - "internalType": "bool", - "name": "", - "type": "bool" - } - ], - "stateMutability": "nonpayable", - "type": "function" - }, - { - "inputs": [ - { - "internalType": "string", - "name": "tokenName", - "type": "string" - }, - { - "internalType": "string", - "name": "tokenSymbol", - "type": "string" - }, - { - "internalType": "string", - "name": "tokenCurrency", - "type": "string" - }, - { - "internalType": "uint8", - "name": "tokenDecimals", - "type": "uint8" - }, - { - "internalType": "address", - "name": "newMasterMinter", - "type": "address" - }, - { - "internalType": "address", - "name": "newPauser", - "type": "address" - }, - { - "internalType": "address", - "name": "newBlacklister", - "type": "address" - }, - { - "internalType": "address", - "name": "newOwner", - "type": "address" - } - ], - "name": "initialize", - "outputs": [], - "stateMutability": "nonpayable", - "type": "function" - }, - { - "inputs": [ - { - "internalType": "string", - "name": "newName", - "type": "string" - } - ], - "name": "initializeV2", - "outputs": [], - "stateMutability": "nonpayable", - "type": "function" - }, - { - "inputs": [ - { - "internalType": "address", - "name": "lostAndFound", - "type": "address" - } - ], - "name": "initializeV2_1", - "outputs": [], - "stateMutability": "nonpayable", - "type": "function" - }, - { - "inputs": [ - { - "internalType": "address[]", - "name": "accountsToBlacklist", - "type": "address[]" - }, - { - "internalType": "string", - "name": "newSymbol", - "type": "string" - } - ], - "name": "initializeV2_2", - "outputs": [], - "stateMutability": "nonpayable", - "type": "function" - }, - { - "inputs": [ - { - "internalType": "address", - "name": "_account", - "type": "address" - } - ], - "name": "isBlacklisted", - "outputs": [ - { - "internalType": "bool", - "name": "", - "type": "bool" - } - ], - "stateMutability": "view", - "type": "function" - }, - { - "inputs": [ - { - "internalType": "address", - "name": "account", - "type": "address" - } - ], - "name": "isMinter", - "outputs": [ - { - "internalType": "bool", - "name": "", - "type": "bool" - } - ], - "stateMutability": "view", - "type": "function" - }, - { - "inputs": [], - "name": "masterMinter", - "outputs": [ - { - "internalType": "address", - "name": "", - "type": "address" - } - ], - "stateMutability": "view", - "type": "function" - }, - { - "inputs": [ - { - "internalType": "address", - "name": "_to", - "type": "address" - }, - { - "internalType": "uint256", - "name": "_amount", - "type": "uint256" - } - ], - "name": "mint", - "outputs": [ - { - "internalType": "bool", - "name": "", - "type": "bool" - } - ], - "stateMutability": "nonpayable", - "type": "function" - }, - { - "inputs": [ - { - "internalType": "address", - "name": "minter", - "type": "address" - } - ], - "name": "minterAllowance", - "outputs": [ - { - "internalType": "uint256", - "name": "", - "type": "uint256" - } - ], - "stateMutability": "view", - "type": "function" - }, - { - "inputs": [], - "name": "name", - "outputs": [ - { - "internalType": "string", - "name": "", - "type": "string" - } - ], - "stateMutability": "view", - "type": "function" - }, - { - "inputs": [ - { - "internalType": "address", - "name": "owner", - "type": "address" - } - ], - "name": "nonces", - "outputs": [ - { - "internalType": "uint256", - "name": "", - "type": "uint256" - } - ], - "stateMutability": "view", - "type": "function" - }, - { - "inputs": [], - "name": "owner", - "outputs": [ - { - "internalType": "address", - "name": "", - "type": "address" - } - ], - "stateMutability": "view", - "type": "function" - }, - { - "inputs": [], - "name": "pause", - "outputs": [], - "stateMutability": "nonpayable", - "type": "function" - }, - { - "inputs": [], - "name": "paused", - "outputs": [ - { - "internalType": "bool", - "name": "", - "type": "bool" - } - ], - "stateMutability": "view", - "type": "function" - }, - { - "inputs": [], - "name": "pauser", - "outputs": [ - { - "internalType": "address", - "name": "", - "type": "address" - } - ], - "stateMutability": "view", - "type": "function" - }, - { - "inputs": [ - { - "internalType": "address", - "name": "owner", - "type": "address" - }, - { - "internalType": "address", - "name": "spender", - "type": "address" - }, - { - "internalType": "uint256", - "name": "value", - "type": "uint256" - }, - { - "internalType": "uint256", - "name": "deadline", - "type": "uint256" - }, - { - "internalType": "bytes", - "name": "signature", - "type": "bytes" - } - ], - "name": "permit", - "outputs": [], - "stateMutability": "nonpayable", - "type": "function" - }, - { - "inputs": [ - { - "internalType": "address", - "name": "owner", - "type": "address" - }, - { - "internalType": "address", - "name": "spender", - "type": "address" - }, - { - "internalType": "uint256", - "name": "value", - "type": "uint256" - }, - { - "internalType": "uint256", - "name": "deadline", - "type": "uint256" - }, - { - "internalType": "uint8", - "name": "v", - "type": "uint8" - }, - { - "internalType": "bytes32", - "name": "r", - "type": "bytes32" - }, - { - "internalType": "bytes32", - "name": "s", - "type": "bytes32" - } - ], - "name": "permit", - "outputs": [], - "stateMutability": "nonpayable", - "type": "function" - }, - { - "inputs": [ - { - "internalType": "address", - "name": "from", - "type": "address" - }, - { - "internalType": "address", - "name": "to", - "type": "address" - }, - { - "internalType": "uint256", - "name": "value", - "type": "uint256" - }, - { - "internalType": "uint256", - "name": "validAfter", - "type": "uint256" - }, - { - "internalType": "uint256", - "name": "validBefore", - "type": "uint256" - }, - { - "internalType": "bytes32", - "name": "nonce", - "type": "bytes32" - }, - { - "internalType": "bytes", - "name": "signature", - "type": "bytes" - } - ], - "name": "receiveWithAuthorization", - "outputs": [], - "stateMutability": "nonpayable", - "type": "function" - }, - { - "inputs": [ - { - "internalType": "address", - "name": "from", - "type": "address" - }, - { - "internalType": "address", - "name": "to", - "type": "address" - }, - { - "internalType": "uint256", - "name": "value", - "type": "uint256" - }, - { - "internalType": "uint256", - "name": "validAfter", - "type": "uint256" - }, - { - "internalType": "uint256", - "name": "validBefore", - "type": "uint256" - }, - { - "internalType": "bytes32", - "name": "nonce", - "type": "bytes32" - }, - { - "internalType": "uint8", - "name": "v", - "type": "uint8" - }, - { - "internalType": "bytes32", - "name": "r", - "type": "bytes32" - }, - { - "internalType": "bytes32", - "name": "s", - "type": "bytes32" - } - ], - "name": "receiveWithAuthorization", - "outputs": [], - "stateMutability": "nonpayable", - "type": "function" - }, - { - "inputs": [ - { - "internalType": "address", - "name": "minter", - "type": "address" - } - ], - "name": "removeMinter", - "outputs": [ - { - "internalType": "bool", - "name": "", - "type": "bool" - } - ], - "stateMutability": "nonpayable", - "type": "function" - }, - { - "inputs": [ - { - "internalType": "contract IERC20", - "name": "tokenContract", - "type": "address" - }, - { - "internalType": "address", - "name": "to", - "type": "address" - }, - { - "internalType": "uint256", - "name": "amount", - "type": "uint256" - } - ], - "name": "rescueERC20", - "outputs": [], - "stateMutability": "nonpayable", - "type": "function" - }, - { - "inputs": [], - "name": "rescuer", - "outputs": [ - { - "internalType": "address", - "name": "", - "type": "address" - } - ], - "stateMutability": "view", - "type": "function" - }, - { - "inputs": [], - "name": "symbol", - "outputs": [ - { - "internalType": "string", - "name": "", - "type": "string" - } - ], - "stateMutability": "view", - "type": "function" - }, - { - "inputs": [], - "name": "totalSupply", - "outputs": [ - { - "internalType": "uint256", - "name": "", - "type": "uint256" - } - ], - "stateMutability": "view", - "type": "function" - }, - { - "inputs": [ - { - "internalType": "address", - "name": "to", - "type": "address" - }, - { - "internalType": "uint256", - "name": "value", - "type": "uint256" - } - ], - "name": "transfer", - "outputs": [ - { - "internalType": "bool", - "name": "", - "type": "bool" - } - ], - "stateMutability": "nonpayable", - "type": "function" - }, - { - "inputs": [ - { - "internalType": "address", - "name": "from", - "type": "address" - }, - { - "internalType": "address", - "name": "to", - "type": "address" - }, - { - "internalType": "uint256", - "name": "value", - "type": "uint256" - } - ], - "name": "transferFrom", - "outputs": [ - { - "internalType": "bool", - "name": "", - "type": "bool" - } - ], - "stateMutability": "nonpayable", - "type": "function" - }, - { - "inputs": [ - { - "internalType": "address", - "name": "newOwner", - "type": "address" - } - ], - "name": "transferOwnership", - "outputs": [], - "stateMutability": "nonpayable", - "type": "function" - }, - { - "inputs": [ - { - "internalType": "address", - "name": "from", - "type": "address" - }, - { - "internalType": "address", - "name": "to", - "type": "address" - }, - { - "internalType": "uint256", - "name": "value", - "type": "uint256" - }, - { - "internalType": "uint256", - "name": "validAfter", - "type": "uint256" - }, - { - "internalType": "uint256", - "name": "validBefore", - "type": "uint256" - }, - { - "internalType": "bytes32", - "name": "nonce", - "type": "bytes32" - }, - { - "internalType": "bytes", - "name": "signature", - "type": "bytes" - } - ], - "name": "transferWithAuthorization", - "outputs": [], - "stateMutability": "nonpayable", - "type": "function" - }, - { - "inputs": [ - { - "internalType": "address", - "name": "from", - "type": "address" - }, - { - "internalType": "address", - "name": "to", - "type": "address" - }, - { - "internalType": "uint256", - "name": "value", - "type": "uint256" - }, - { - "internalType": "uint256", - "name": "validAfter", - "type": "uint256" - }, - { - "internalType": "uint256", - "name": "validBefore", - "type": "uint256" - }, - { - "internalType": "bytes32", - "name": "nonce", - "type": "bytes32" - }, - { - "internalType": "uint8", - "name": "v", - "type": "uint8" - }, - { - "internalType": "bytes32", - "name": "r", - "type": "bytes32" - }, - { - "internalType": "bytes32", - "name": "s", - "type": "bytes32" - } - ], - "name": "transferWithAuthorization", - "outputs": [], - "stateMutability": "nonpayable", - "type": "function" - }, - { - "inputs": [ - { - "internalType": "address", - "name": "_account", - "type": "address" - } - ], - "name": "unBlacklist", - "outputs": [], - "stateMutability": "nonpayable", - "type": "function" - }, - { - "inputs": [], - "name": "unpause", - "outputs": [], - "stateMutability": "nonpayable", - "type": "function" - }, - { - "inputs": [ - { - "internalType": "address", - "name": "_newBlacklister", - "type": "address" - } - ], - "name": "updateBlacklister", - "outputs": [], - "stateMutability": "nonpayable", - "type": "function" - }, - { - "inputs": [ - { - "internalType": "address", - "name": "_newMasterMinter", - "type": "address" - } - ], - "name": "updateMasterMinter", - "outputs": [], - "stateMutability": "nonpayable", - "type": "function" - }, - { - "inputs": [ - { - "internalType": "address", - "name": "_newPauser", - "type": "address" - } - ], - "name": "updatePauser", - "outputs": [], - "stateMutability": "nonpayable", - "type": "function" - }, - { - "inputs": [ - { - "internalType": "address", - "name": "newRescuer", - "type": "address" - } - ], - "name": "updateRescuer", - "outputs": [], - "stateMutability": "nonpayable", - "type": "function" - }, - { - "inputs": [], - "name": "version", - "outputs": [ - { - "internalType": "string", - "name": "", - "type": "string" - } - ], - "stateMutability": "pure", - "type": "function" - } - ], - "bytecode": "0x60806040526001805460ff60a01b191690556000600b553480156200002357600080fd5b506200002f3362000035565b62000057565b600080546001600160a01b0319166001600160a01b0392909216919091179055565b615c6b80620000676000396000f3fe608060405234801561001057600080fd5b506004361061038e5760003560e01c806388b7ab63116101de578063bd1024301161010f578063dd62ed3e116100ad578063ef55bec61161007c578063ef55bec61461115b578063f2fde38b146111c7578063f9f92be4146111fa578063fe575a871461122d5761038e565b8063dd62ed3e14611073578063e3ee160e146110ae578063e5a6b10f1461111a578063e94a0102146111225761038e565b8063d505accf116100e9578063d505accf14610f95578063d608ea6414610ff3578063d916948714611063578063dd0743b31461106b5761038e565b8063bd10243014610ea1578063ccd92d3e14610ea9578063cf09299514610eb15761038e565b8063a457c2d71161017c578063aa271e1a11610156578063aa271e1a14610d30578063ad38bf2214610d63578063b2118a8d14610d96578063b7b7289914610dd95761038e565b8063a457c2d714610c8b578063a9059cbb14610cc4578063aa20e1e414610cfd5761038e565b806395d89b41116101b857806395d89b4114610b9b5780639fd0506d14610ba35780639fd5a6cf14610bab578063a0cc6a6814610c835761038e565b806388b7ab6314610a7c5780638a6db9c314610b605780638da5cb5b14610b935761038e565b806339509351116102c3578063554bab3c116102615780637ecebe00116102305780637ecebe0014610a315780637f2eecc314610a645780637fd0991614610a6c5780638456cb5914610a745761038e565b8063554bab3c146109755780635a049a70146109a85780635c975abb146109f657806370a08231146109fe5761038e565b806342966c681161029d57806342966c6814610855578063430239b4146108725780634e44d9561461093457806354fd4d501461096d5761038e565b806339509351146107db5780633f4ba83a1461081457806340c10f191461081c5761038e565b80633092afd5116103305780633357162b1161030a5780633357162b146105ae57806335d99f351461079a5780633644e515146107cb57806338a63183146107d35761038e565b80633092afd51461055557806330adf81f14610588578063313ce567146105905761038e565b80631a8952661161036c5780631a8952661461047757806323b872dd146104ac5780632ab60045146104ef5780632fc81e09146105225761038e565b806306fdde0314610393578063095ea7b31461041057806318160ddd1461045d575b600080fd5b61039b611260565b6040805160208082528351818301528351919283929083019185019080838360005b838110156103d55781810151838201526020016103bd565b50505050905090810190601f1680156104025780820380516001836020036101000a031916815260200191505b509250505060405180910390f35b6104496004803603604081101561042657600080fd5b5073ffffffffffffffffffffffffffffffffffffffff813516906020013561130c565b604080519115158252519081900360200190f35b6104656113ae565b60408051918252519081900360200190f35b6104aa6004803603602081101561048d57600080fd5b503573ffffffffffffffffffffffffffffffffffffffff1661144e565b005b610449600480360360608110156104c257600080fd5b5073ffffffffffffffffffffffffffffffffffffffff81358116916020810135909116906040013561150b565b6104aa6004803603602081101561050557600080fd5b503573ffffffffffffffffffffffffffffffffffffffff16611703565b6104aa6004803603602081101561053857600080fd5b503573ffffffffffffffffffffffffffffffffffffffff16611864565b6104496004803603602081101561056b57600080fd5b503573ffffffffffffffffffffffffffffffffffffffff166118cc565b6104656119c5565b6105986119e9565b6040805160ff9092168252519081900360200190f35b6104aa60048036036101008110156105c557600080fd5b8101906020810181356401000000008111156105e057600080fd5b8201836020820111156105f257600080fd5b8035906020019184600183028401116401000000008311171561061457600080fd5b91908080601f016020809104026020016040519081016040528093929190818152602001838380828437600092019190915250929594936020810193503591505064010000000081111561066757600080fd5b82018360208201111561067957600080fd5b8035906020019184600183028401116401000000008311171561069b57600080fd5b91908080601f01602080910402602001604051908101604052809392919081815260200183838082843760009201919091525092959493602081019350359150506401000000008111156106ee57600080fd5b82018360208201111561070057600080fd5b8035906020019184600183028401116401000000008311171561072257600080fd5b91908080601f0160208091040260200160405190810160405280939291908181526020018383808284376000920191909152509295505050813560ff16925050602081013573ffffffffffffffffffffffffffffffffffffffff908116916040810135821691606082013581169160800135166119f2565b6107a2611d34565b6040805173ffffffffffffffffffffffffffffffffffffffff9092168252519081900360200190f35b610465611d50565b6107a2611d5f565b610449600480360360408110156107f157600080fd5b5073ffffffffffffffffffffffffffffffffffffffff8135169060200135611d7b565b6104aa611e13565b6104496004803603604081101561083257600080fd5b5073ffffffffffffffffffffffffffffffffffffffff8135169060200135611ed6565b6104aa6004803603602081101561086b57600080fd5b5035612324565b6104aa6004803603604081101561088857600080fd5b8101906020810181356401000000008111156108a357600080fd5b8201836020820111156108b557600080fd5b803590602001918460208302840111640100000000831117156108d757600080fd5b9193909290916020810190356401000000008111156108f557600080fd5b82018360208201111561090757600080fd5b8035906020019184600183028401116401000000008311171561092957600080fd5b509092509050612658565b6104496004803603604081101561094a57600080fd5b5073ffffffffffffffffffffffffffffffffffffffff813516906020013561280f565b61039b6129a2565b6104aa6004803603602081101561098b57600080fd5b503573ffffffffffffffffffffffffffffffffffffffff166129d9565b6104aa600480360360a08110156109be57600080fd5b5073ffffffffffffffffffffffffffffffffffffffff8135169060208101359060ff6040820135169060608101359060800135612b40565b610449612bde565b61046560048036036020811015610a1457600080fd5b503573ffffffffffffffffffffffffffffffffffffffff16612bff565b61046560048036036020811015610a4757600080fd5b503573ffffffffffffffffffffffffffffffffffffffff16612c0a565b610465612c32565b6107a2612c56565b6104aa612c6e565b6104aa600480360360e0811015610a9257600080fd5b73ffffffffffffffffffffffffffffffffffffffff823581169260208101359091169160408201359160608101359160808201359160a08101359181019060e0810160c0820135640100000000811115610aeb57600080fd5b820183602082011115610afd57600080fd5b80359060200191846001830284011164010000000083111715610b1f57600080fd5b91908080601f016020809104026020016040519081016040528093929190818152602001838380828437600092019190915250929550612d48945050505050565b61046560048036036020811015610b7657600080fd5b503573ffffffffffffffffffffffffffffffffffffffff16612dea565b6107a2612e12565b61039b612e2e565b6107a2612ea7565b6104aa600480360360a0811015610bc157600080fd5b73ffffffffffffffffffffffffffffffffffffffff823581169260208101359091169160408201359160608101359181019060a081016080820135640100000000811115610c0e57600080fd5b820183602082011115610c2057600080fd5b80359060200191846001830284011164010000000083111715610c4257600080fd5b91908080601f016020809104026020016040519081016040528093929190818152602001838380828437600092019190915250929550612ec3945050505050565b610465612f5a565b61044960048036036040811015610ca157600080fd5b5073ffffffffffffffffffffffffffffffffffffffff8135169060200135612f7e565b61044960048036036040811015610cda57600080fd5b5073ffffffffffffffffffffffffffffffffffffffff8135169060200135613016565b6104aa60048036036020811015610d1357600080fd5b503573ffffffffffffffffffffffffffffffffffffffff166130ae565b61044960048036036020811015610d4657600080fd5b503573ffffffffffffffffffffffffffffffffffffffff16613215565b6104aa60048036036020811015610d7957600080fd5b503573ffffffffffffffffffffffffffffffffffffffff16613240565b6104aa60048036036060811015610dac57600080fd5b5073ffffffffffffffffffffffffffffffffffffffff8135811691602081013590911690604001356133a7565b6104aa60048036036060811015610def57600080fd5b73ffffffffffffffffffffffffffffffffffffffff82351691602081013591810190606081016040820135640100000000811115610e2c57600080fd5b820183602082011115610e3e57600080fd5b80359060200191846001830284011164010000000083111715610e6057600080fd5b91908080601f01602080910402602001604051908101604052809392919081815260200183838082843760009201919091525092955061343d945050505050565b6107a26134d2565b6104656134ee565b6104aa600480360360e0811015610ec757600080fd5b73ffffffffffffffffffffffffffffffffffffffff823581169260208101359091169160408201359160608101359160808201359160a08101359181019060e0810160c0820135640100000000811115610f2057600080fd5b820183602082011115610f3257600080fd5b80359060200191846001830284011164010000000083111715610f5457600080fd5b91908080601f0160208091040260200160405190810160405280939291908181526020018383808284376000920191909152509295506134f7945050505050565b6104aa600480360360e0811015610fab57600080fd5b5073ffffffffffffffffffffffffffffffffffffffff813581169160208101359091169060408101359060608101359060ff6080820135169060a08101359060c00135613590565b6104aa6004803603602081101561100957600080fd5b81019060208101813564010000000081111561102457600080fd5b82018360208201111561103657600080fd5b8035906020019184600183028401116401000000008311171561105857600080fd5b509092509050613629565b610465613712565b6107a2613736565b6104656004803603604081101561108957600080fd5b5073ffffffffffffffffffffffffffffffffffffffff8135811691602001351661374e565b6104aa60048036036101208110156110c557600080fd5b5073ffffffffffffffffffffffffffffffffffffffff813581169160208101359091169060408101359060608101359060808101359060a08101359060ff60c0820135169060e0810135906101000135613786565b61039b61382c565b6104496004803603604081101561113857600080fd5b5073ffffffffffffffffffffffffffffffffffffffff81351690602001356138a5565b6104aa600480360361012081101561117257600080fd5b5073ffffffffffffffffffffffffffffffffffffffff813581169160208101359091169060408101359060608101359060808101359060a08101359060ff60c0820135169060e08101359061010001356138dd565b6104aa600480360360208110156111dd57600080fd5b503573ffffffffffffffffffffffffffffffffffffffff16613978565b6104aa6004803603602081101561121057600080fd5b503573ffffffffffffffffffffffffffffffffffffffff16613acb565b6104496004803603602081101561124357600080fd5b503573ffffffffffffffffffffffffffffffffffffffff16613b88565b6004805460408051602060026001851615610100027fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff0190941693909304601f810184900484028201840190925281815292918301828280156113045780601f106112d957610100808354040283529160200191611304565b820191906000526020600020905b8154815290600101906020018083116112e757829003601f168201915b505050505081565b60015460009074010000000000000000000000000000000000000000900460ff161561139957604080517f08c379a000000000000000000000000000000000000000000000000000000000815260206004820152601060248201527f5061757361626c653a2070617573656400000000000000000000000000000000604482015290519081900360640190fd5b6113a4338484613b93565b5060015b92915050565b60008073180000000000000000000000000000000000000073ffffffffffffffffffffffffffffffffffffffff166318160ddd6040518163ffffffff1660e01b815260040160206040518083038186803b15801561140b57600080fd5b505afa15801561141f573d6000803e3d6000fd5b505050506040513d602081101561143557600080fd5b505190506114488164e8d4a51000613cda565b91505090565b60025473ffffffffffffffffffffffffffffffffffffffff1633146114be576040517f08c379a000000000000000000000000000000000000000000000000000000000815260040180806020018281038252602c8152602001806158c5602c913960400191505060405180910390fd5b6114c781613ced565b60405173ffffffffffffffffffffffffffffffffffffffff8216907f117e3210bb9aa7d9baff172026820255c6f6c30ba8999d1c2fd88e2848137c4e90600090a250565b60015460009074010000000000000000000000000000000000000000900460ff161561159857604080517f08c379a000000000000000000000000000000000000000000000000000000000815260206004820152601060248201527f5061757361626c653a2070617573656400000000000000000000000000000000604482015290519081900360640190fd5b336115a281613cf8565b156115f8576040517f08c379a0000000000000000000000000000000000000000000000000000000008152600401808060200182810382526025815260200180615bc76025913960400191505060405180910390fd5b73ffffffffffffffffffffffffffffffffffffffff85166000908152600a60209081526040808320338452909152902054831115611681576040517f08c379a00000000000000000000000000000000000000000000000000000000081526004018080602001828103825260288152602001806159ac6028913960400191505060405180910390fd5b73ffffffffffffffffffffffffffffffffffffffff85166000908152600a602090815260408083203384529091529020546116bc9084613da7565b73ffffffffffffffffffffffffffffffffffffffff86166000908152600a602090815260408083203384529091529020556116f8858585613e1e565b506001949350505050565b60005473ffffffffffffffffffffffffffffffffffffffff16331461178957604080517f08c379a000000000000000000000000000000000000000000000000000000000815260206004820181905260248201527f4f776e61626c653a2063616c6c6572206973206e6f7420746865206f776e6572604482015290519081900360640190fd5b73ffffffffffffffffffffffffffffffffffffffff81166117f5576040517f08c379a000000000000000000000000000000000000000000000000000000000815260040180806020018281038252602a815260200180615823602a913960400191505060405180910390fd5b600e80547fffffffffffffffffffffffff00000000000000000000000000000000000000001673ffffffffffffffffffffffffffffffffffffffff83169081179091556040517fe475e580d85111348e40d8ca33cfdd74c30fe1655c2d8537a13abc10065ffa5a90600090a250565b60125460ff1660011461187657600080fd5b60006118813061410c565b9050801561189457611894308383613e1e565b61189d30614135565b5050601280547fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff00166002179055565b60085460009073ffffffffffffffffffffffffffffffffffffffff16331461193f576040517f08c379a000000000000000000000000000000000000000000000000000000000815260040180806020018281038252602981526020018061589c6029913960400191505060405180910390fd5b73ffffffffffffffffffffffffffffffffffffffff82166000818152600c6020908152604080832080547fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff00169055600d909152808220829055517fe94479a9f7e1952cc78f2d6baab678adc1b772d936c6583def489e524cb666929190a2506001919050565b7f6e71edae12b1b97f4d1f60370fef10105fa2faae0126114a169c64845d6126c981565b60065460ff1681565b60085474010000000000000000000000000000000000000000900460ff1615611a66576040517f08c379a000000000000000000000000000000000000000000000000000000000815260040180806020018281038252602a815260200180615a27602a913960400191505060405180910390fd5b73ffffffffffffffffffffffffffffffffffffffff8416611ad2576040517f08c379a000000000000000000000000000000000000000000000000000000000815260040180806020018281038252602f815260200180615938602f913960400191505060405180910390fd5b73ffffffffffffffffffffffffffffffffffffffff8316611b3e576040517f08c379a00000000000000000000000000000000000000000000000000000000081526004018080602001828103825260298152602001806157fa6029913960400191505060405180910390fd5b73ffffffffffffffffffffffffffffffffffffffff8216611baa576040517f08c379a000000000000000000000000000000000000000000000000000000000815260040180806020018281038252602e8152602001806159d4602e913960400191505060405180910390fd5b73ffffffffffffffffffffffffffffffffffffffff8116611c16576040517f08c379a0000000000000000000000000000000000000000000000000000000008152600401808060200182810382526028815260200180615b3f6028913960400191505060405180910390fd5b8751611c299060049060208b0190615593565b508651611c3d9060059060208a0190615593565b508551611c51906007906020890190615593565b50600680547fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff001660ff8716179055600880547fffffffffffffffffffffffff000000000000000000000000000000000000000090811673ffffffffffffffffffffffffffffffffffffffff8781169190911790925560018054821686841617905560028054909116918416919091179055611ceb81614140565b5050600880547fffffffffffffffffffffff00ffffffffffffffffffffffffffffffffffffffff1674010000000000000000000000000000000000000000179055505050505050565b60085473ffffffffffffffffffffffffffffffffffffffff1681565b6000611d5a614187565b905090565b600e5473ffffffffffffffffffffffffffffffffffffffff1690565b60015460009074010000000000000000000000000000000000000000900460ff1615611e0857604080517f08c379a000000000000000000000000000000000000000000000000000000000815260206004820152601060248201527f5061757361626c653a2070617573656400000000000000000000000000000000604482015290519081900360640190fd5b6113a433848461427c565b60015473ffffffffffffffffffffffffffffffffffffffff163314611e83576040517f08c379a0000000000000000000000000000000000000000000000000000000008152600401808060200182810382526022815260200180615af36022913960400191505060405180910390fd5b600180547fffffffffffffffffffffff00ffffffffffffffffffffffffffffffffffffffff1690556040517f7805862f689e2f13df9f062ff482ad3ad112aca9e0847911ed832e158c525b3390600090a1565b336000908152600c602052604081205460ff16611f3e576040517f08c379a00000000000000000000000000000000000000000000000000000000081526004018080602001828103825260218152602001806159176021913960400191505060405180910390fd5b33611f4881613cf8565b15611f9e576040517f08c379a0000000000000000000000000000000000000000000000000000000008152600401808060200182810382526025815260200180615bc76025913960400191505060405180910390fd5b60015474010000000000000000000000000000000000000000900460ff161561202857604080517f08c379a000000000000000000000000000000000000000000000000000000000815260206004820152601060248201527f5061757361626c653a2070617573656400000000000000000000000000000000604482015290519081900360640190fd5b73ffffffffffffffffffffffffffffffffffffffff8416612094576040517f08c379a000000000000000000000000000000000000000000000000000000000815260040180806020018281038252602381526020018061578f6023913960400191505060405180910390fd5b600083116120ed576040517f08c379a000000000000000000000000000000000000000000000000000000000815260040180806020018281038252602981526020018061584d6029913960400191505060405180910390fd5b336000908152600d602052604090205480841115612156576040517f08c379a000000000000000000000000000000000000000000000000000000000815260040180806020018281038252602e815260200180615ac5602e913960400191505060405180910390fd5b336000908152600d6020526040902084820390557318000000000000000000000000000000000000006340c10f19866121948764e8d4a510006142c6565b6040518363ffffffff1660e01b8152600401808373ffffffffffffffffffffffffffffffffffffffff16815260200182815260200192505050602060405180830381600087803b1580156121e757600080fd5b505af11580156121fb573d6000803e3d6000fd5b505050506040513d602081101561221157600080fd5b505161227e57604080517f08c379a000000000000000000000000000000000000000000000000000000000815260206004820152601260248201527f4e6174697665206d696e74206661696c65640000000000000000000000000000604482015290519081900360640190fd5b60408051858152905173ffffffffffffffffffffffffffffffffffffffff87169133917fab8530f87dc9b59234c4623bf917212bb2536d647574c8e7e5da92c2ede0c9f89181900360200190a360408051858152905173ffffffffffffffffffffffffffffffffffffffff8716916000917fddf252ad1be2c89b69c2b068fc378daa952ba7f163c4a11628f55a4df523b3ef9181900360200190a3506001949350505050565b336000908152600c602052604090205460ff1661238c576040517f08c379a00000000000000000000000000000000000000000000000000000000081526004018080602001828103825260218152602001806159176021913960400191505060405180910390fd5b60015474010000000000000000000000000000000000000000900460ff161561241657604080517f08c379a000000000000000000000000000000000000000000000000000000000815260206004820152601060248201527f5061757361626c653a2070617573656400000000000000000000000000000000604482015290519081900360640190fd5b6000811161246f576040517f08c379a00000000000000000000000000000000000000000000000000000000081526004018080602001828103825260298152602001806157666029913960400191505060405180910390fd5b60006124808264e8d4a510006142c6565b905033318111156124dc576040517f08c379a00000000000000000000000000000000000000000000000000000000081526004018080602001828103825260268152602001806158f16026913960400191505060405180910390fd5b604080517f9dc29fac00000000000000000000000000000000000000000000000000000000815233600482015260248101839052905173180000000000000000000000000000000000000091639dc29fac9160448083019260209291908290030181600087803b15801561254f57600080fd5b505af1158015612563573d6000803e3d6000fd5b505050506040513d602081101561257957600080fd5b50516125e657604080517f08c379a000000000000000000000000000000000000000000000000000000000815260206004820152601260248201527f4e6174697665206275726e206661696c65640000000000000000000000000000604482015290519081900360640190fd5b60408051838152905133917fcc16f5dbb4873280815c1ee09dbd06736cffcc184412cf7a71a0fdb75d397ca5919081900360200190a260408051838152905160009133917fddf252ad1be2c89b69c2b068fc378daa952ba7f163c4a11628f55a4df523b3ef9181900360200190a35050565b60125460ff1660021461266a57600080fd5b61267660058383615611565b5060005b838110156127b8576003600086868481811061269257fe5b6020908102929092013573ffffffffffffffffffffffffffffffffffffffff168352508101919091526040016000205460ff1661271a576040517f08c379a000000000000000000000000000000000000000000000000000000000815260040180806020018281038252603d8152602001806156b3603d913960400191505060405180910390fd5b61274b85858381811061272957fe5b9050602002013573ffffffffffffffffffffffffffffffffffffffff16614135565b6003600086868481811061275b57fe5b6020908102929092013573ffffffffffffffffffffffffffffffffffffffff1683525081019190915260400160002080547fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff0016905560010161267a565b506127c230614135565b505030600090815260036020819052604090912080547fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff009081169091556012805490911690911790555050565b60015460009074010000000000000000000000000000000000000000900460ff161561289c57604080517f08c379a000000000000000000000000000000000000000000000000000000000815260206004820152601060248201527f5061757361626c653a2070617573656400000000000000000000000000000000604482015290519081900360640190fd5b60085473ffffffffffffffffffffffffffffffffffffffff16331461290c576040517f08c379a000000000000000000000000000000000000000000000000000000000815260040180806020018281038252602981526020018061589c6029913960400191505060405180910390fd5b73ffffffffffffffffffffffffffffffffffffffff83166000818152600c6020908152604080832080547fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff00166001179055600d825291829020859055815185815291517f46980fca912ef9bcdbd36877427b6b90e860769f604e89c0e67720cece530d209281900390910190a250600192915050565b60408051808201909152600181527f3200000000000000000000000000000000000000000000000000000000000000602082015290565b60005473ffffffffffffffffffffffffffffffffffffffff163314612a5f57604080517f08c379a000000000000000000000000000000000000000000000000000000000815260206004820181905260248201527f4f776e61626c653a2063616c6c6572206973206e6f7420746865206f776e6572604482015290519081900360640190fd5b73ffffffffffffffffffffffffffffffffffffffff8116612acb576040517f08c379a00000000000000000000000000000000000000000000000000000000081526004018080602001828103825260288152602001806157136028913960400191505060405180910390fd5b600180547fffffffffffffffffffffffff00000000000000000000000000000000000000001673ffffffffffffffffffffffffffffffffffffffff83811691909117918290556040519116907fb80482a293ca2e013eda8683c9bd7fc8347cfdaeea5ede58cba46df502c2a60490600090a250565b60015474010000000000000000000000000000000000000000900460ff1615612bca57604080517f08c379a000000000000000000000000000000000000000000000000000000000815260206004820152601060248201527f5061757361626c653a2070617573656400000000000000000000000000000000604482015290519081900360640190fd5b612bd785858585856142d2565b5050505050565b60015474010000000000000000000000000000000000000000900460ff1681565b60006113a88261410c565b73ffffffffffffffffffffffffffffffffffffffff1660009081526011602052604090205490565b7fd099cc98ef71107a616c4f0f941f04c322d8e254fe26b3c6668db87aae413de881565b73180000000000000000000000000000000000000081565b60015473ffffffffffffffffffffffffffffffffffffffff163314612cde576040517f08c379a0000000000000000000000000000000000000000000000000000000008152600401808060200182810382526022815260200180615af36022913960400191505060405180910390fd5b600180547fffffffffffffffffffffff00ffffffffffffffffffffffffffffffffffffffff16740100000000000000000000000000000000000000001790556040517f6985a02210a168e66602d3235cb6db0e70f92b3ba4d376a33c0f3d9434bff62590600090a1565b60015474010000000000000000000000000000000000000000900460ff1615612dd257604080517f08c379a000000000000000000000000000000000000000000000000000000000815260206004820152601060248201527f5061757361626c653a2070617573656400000000000000000000000000000000604482015290519081900360640190fd5b612de187878787878787614312565b50505050505050565b73ffffffffffffffffffffffffffffffffffffffff166000908152600d602052604090205490565b60005473ffffffffffffffffffffffffffffffffffffffff1690565b6005805460408051602060026001851615610100027fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff0190941693909304601f810184900484028201840190925281815292918301828280156113045780601f106112d957610100808354040283529160200191611304565b60015473ffffffffffffffffffffffffffffffffffffffff1681565b60015474010000000000000000000000000000000000000000900460ff1615612f4d57604080517f08c379a000000000000000000000000000000000000000000000000000000000815260206004820152601060248201527f5061757361626c653a2070617573656400000000000000000000000000000000604482015290519081900360640190fd5b612bd78585858585614433565b7f7c7c6cdb67a18743f49ec6fa9b35f50d52ed05cbed4cc592e13b44501c1a226781565b60015460009074010000000000000000000000000000000000000000900460ff161561300b57604080517f08c379a000000000000000000000000000000000000000000000000000000000815260206004820152601060248201527f5061757361626c653a2070617573656400000000000000000000000000000000604482015290519081900360640190fd5b6113a43384846146f7565b60015460009074010000000000000000000000000000000000000000900460ff16156130a357604080517f08c379a000000000000000000000000000000000000000000000000000000000815260206004820152601060248201527f5061757361626c653a2070617573656400000000000000000000000000000000604482015290519081900360640190fd5b6113a4338484613e1e565b60005473ffffffffffffffffffffffffffffffffffffffff16331461313457604080517f08c379a000000000000000000000000000000000000000000000000000000000815260206004820181905260248201527f4f776e61626c653a2063616c6c6572206973206e6f7420746865206f776e6572604482015290519081900360640190fd5b73ffffffffffffffffffffffffffffffffffffffff81166131a0576040517f08c379a000000000000000000000000000000000000000000000000000000000815260040180806020018281038252602f815260200180615938602f913960400191505060405180910390fd5b600880547fffffffffffffffffffffffff00000000000000000000000000000000000000001673ffffffffffffffffffffffffffffffffffffffff83811691909117918290556040519116907fdb66dfa9c6b8f5226fe9aac7e51897ae8ee94ac31dc70bb6c9900b2574b707e690600090a250565b73ffffffffffffffffffffffffffffffffffffffff166000908152600c602052604090205460ff1690565b60005473ffffffffffffffffffffffffffffffffffffffff1633146132c657604080517f08c379a000000000000000000000000000000000000000000000000000000000815260206004820181905260248201527f4f776e61626c653a2063616c6c6572206973206e6f7420746865206f776e6572604482015290519081900360640190fd5b73ffffffffffffffffffffffffffffffffffffffff8116613332576040517f08c379a0000000000000000000000000000000000000000000000000000000008152600401808060200182810382526032815260200180615b956032913960400191505060405180910390fd5b600280547fffffffffffffffffffffffff00000000000000000000000000000000000000001673ffffffffffffffffffffffffffffffffffffffff83811691909117918290556040519116907fc67398012c111ce95ecb7429b933096c977380ee6c421175a71a4a4c6c88c06e90600090a250565b600e5473ffffffffffffffffffffffffffffffffffffffff163314613417576040517f08c379a00000000000000000000000000000000000000000000000000000000081526004018080602001828103825260248152602001806159676024913960400191505060405180910390fd5b61343873ffffffffffffffffffffffffffffffffffffffff84168383614753565b505050565b60015474010000000000000000000000000000000000000000900460ff16156134c757604080517f08c379a000000000000000000000000000000000000000000000000000000000815260206004820152601060248201527f5061757361626c653a2070617573656400000000000000000000000000000000604482015290519081900360640190fd5b6134388383836147e0565b60025473ffffffffffffffffffffffffffffffffffffffff1681565b64e8d4a5100081565b60015474010000000000000000000000000000000000000000900460ff161561358157604080517f08c379a000000000000000000000000000000000000000000000000000000000815260206004820152601060248201527f5061757361626c653a2070617573656400000000000000000000000000000000604482015290519081900360640190fd5b612de1878787878787876148ea565b60015474010000000000000000000000000000000000000000900460ff161561361a57604080517f08c379a000000000000000000000000000000000000000000000000000000000815260206004820152601060248201527f5061757361626c653a2070617573656400000000000000000000000000000000604482015290519081900360640190fd5b612de187878787878787614988565b60085474010000000000000000000000000000000000000000900460ff168015613656575060125460ff16155b61365f57600080fd5b61366b60048383615611565b506136e082828080601f0160208091040260200160405190810160405280939291908181526020018383808284376000920191909152505060408051808201909152600181527f3200000000000000000000000000000000000000000000000000000000000000602082015291506149ca9050565b600f555050601280547fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff00166001179055565b7f158b0a9edf7a828aad02f63cd515c68ef2f50ba807396f6d12842833a159742981565b73180000000000000000000000000000000000000181565b73ffffffffffffffffffffffffffffffffffffffff9182166000908152600a6020908152604080832093909416825291909152205490565b60015474010000000000000000000000000000000000000000900460ff161561381057604080517f08c379a000000000000000000000000000000000000000000000000000000000815260206004820152601060248201527f5061757361626c653a2070617573656400000000000000000000000000000000604482015290519081900360640190fd5b6138218989898989898989896149e0565b505050505050505050565b6007805460408051602060026001851615610100027fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff0190941693909304601f810184900484028201840190925281815292918301828280156113045780601f106112d957610100808354040283529160200191611304565b73ffffffffffffffffffffffffffffffffffffffff919091166000908152601060209081526040808320938352929052205460ff1690565b60015474010000000000000000000000000000000000000000900460ff161561396757604080517f08c379a000000000000000000000000000000000000000000000000000000000815260206004820152601060248201527f5061757361626c653a2070617573656400000000000000000000000000000000604482015290519081900360640190fd5b613821898989898989898989614a24565b60005473ffffffffffffffffffffffffffffffffffffffff1633146139fe57604080517f08c379a000000000000000000000000000000000000000000000000000000000815260206004820181905260248201527f4f776e61626c653a2063616c6c6572206973206e6f7420746865206f776e6572604482015290519081900360640190fd5b73ffffffffffffffffffffffffffffffffffffffff8116613a6a576040517f08c379a00000000000000000000000000000000000000000000000000000000081526004018080602001828103825260268152602001806157b26026913960400191505060405180910390fd5b6000546040805173ffffffffffffffffffffffffffffffffffffffff9283168152918316602083015280517f8be0079c531659141344cd1fd0a4f28419497f9722a3daafe3b4186f6b6457e09281900390910190a1613ac881614140565b50565b60025473ffffffffffffffffffffffffffffffffffffffff163314613b3b576040517f08c379a000000000000000000000000000000000000000000000000000000000815260040180806020018281038252602c8152602001806158c5602c913960400191505060405180910390fd5b613b4481614135565b60405173ffffffffffffffffffffffffffffffffffffffff8216907fffa4e6181777692565cf28528fc88fd1516ea86b56da075235fa575af6a4b85590600090a250565b60006113a882613cf8565b73ffffffffffffffffffffffffffffffffffffffff8316613bff576040517f08c379a0000000000000000000000000000000000000000000000000000000008152600401808060200182810382526024815260200180615aa16024913960400191505060405180910390fd5b73ffffffffffffffffffffffffffffffffffffffff8216613c6b576040517f08c379a00000000000000000000000000000000000000000000000000000000081526004018080602001828103825260228152602001806157d86022913960400191505060405180910390fd5b73ffffffffffffffffffffffffffffffffffffffff8084166000818152600a6020908152604080832094871680845294825291829020859055815185815291517f8c5be1e5ebec7d5bd14f71427d1e84f3dd0314c0f7b2291e5b200ac8c7c3b9259281900390910190a3505050565b6000613ce68383614a68565b9392505050565b613ac8816000614ae9565b600073180000000000000000000000000000000000000173ffffffffffffffffffffffffffffffffffffffff16638e204c43836040518263ffffffff1660e01b8152600401808273ffffffffffffffffffffffffffffffffffffffff16815260200191505060206040518083038186803b158015613d7557600080fd5b505afa158015613d89573d6000803e3d6000fd5b505050506040513d6020811015613d9f57600080fd5b505192915050565b600082821115613e1857604080517f08c379a000000000000000000000000000000000000000000000000000000000815260206004820152601e60248201527f536166654d6174683a207375627472616374696f6e206f766572666c6f770000604482015290519081900360640190fd5b50900390565b73ffffffffffffffffffffffffffffffffffffffff8316613e8a576040517f08c379a0000000000000000000000000000000000000000000000000000000008152600401808060200182810382526025815260200180615a7c6025913960400191505060405180910390fd5b73ffffffffffffffffffffffffffffffffffffffff8216613ef6576040517f08c379a00000000000000000000000000000000000000000000000000000000081526004018080602001828103825260238152602001806156f06023913960400191505060405180910390fd5b6000613f078264e8d4a510006142c6565b90508373ffffffffffffffffffffffffffffffffffffffff1631811115613f79576040517f08c379a00000000000000000000000000000000000000000000000000000000081526004018080602001828103825260268152602001806158766026913960400191505060405180910390fd5b604080517fbeabacc800000000000000000000000000000000000000000000000000000000815273ffffffffffffffffffffffffffffffffffffffff8681166004830152851660248201526044810183905290517318000000000000000000000000000000000000009163beabacc89160648083019260209291908290030181600087803b15801561400a57600080fd5b505af115801561401e573d6000803e3d6000fd5b505050506040513d602081101561403457600080fd5b50516140a157604080517f08c379a000000000000000000000000000000000000000000000000000000000815260206004820152601660248201527f4e6174697665207472616e73666572206661696c656400000000000000000000604482015290519081900360640190fd5b8273ffffffffffffffffffffffffffffffffffffffff168473ffffffffffffffffffffffffffffffffffffffff167fddf252ad1be2c89b69c2b068fc378daa952ba7f163c4a11628f55a4df523b3ef846040518082815260200191505060405180910390a350505050565b600073ffffffffffffffffffffffffffffffffffffffff821631613ce68164e8d4a51000613cda565b613ac8816001614ae9565b600080547fffffffffffffffffffffffff00000000000000000000000000000000000000001673ffffffffffffffffffffffffffffffffffffffff92909216919091179055565b6004805460408051602060026001851615610100027fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff0190941693909304601f8101849004840282018401909252818152600093611d5a93919290918301828280156142345780601f1061420957610100808354040283529160200191614234565b820191906000526020600020905b81548152906001019060200180831161421757829003601f168201915b50505050506040518060400160405280600181526020017f3200000000000000000000000000000000000000000000000000000000000000815250614277614d39565b614d3d565b73ffffffffffffffffffffffffffffffffffffffff8084166000908152600a602090815260408083209386168352929052205461343890849084906142c19085614db1565b613b93565b6000613ce68383614e25565b612bd78585848487604051602001808481526020018381526020018260ff1660f81b815260010193505050506040516020818303038152906040526147e0565b73ffffffffffffffffffffffffffffffffffffffff86163314614380576040517f08c379a0000000000000000000000000000000000000000000000000000000008152600401808060200182810382526025815260200180615a026025913960400191505060405180910390fd5b61438c87838686614e98565b604080517fd099cc98ef71107a616c4f0f941f04c322d8e254fe26b3c6668db87aae413de860208083019190915273ffffffffffffffffffffffffffffffffffffffff808b1683850152891660608301526080820188905260a0820187905260c0820186905260e080830186905283518084039091018152610100909201909252805191012061441e90889083614f52565b61442887836150d0565b612de1878787613e1e565b7fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff8214806144615750428210155b6144cc57604080517f08c379a000000000000000000000000000000000000000000000000000000000815260206004820152601e60248201527f46696174546f6b656e56323a207065726d697420697320657870697265640000604482015290519081900360640190fd5b60006145746144d9614187565b73ffffffffffffffffffffffffffffffffffffffff80891660008181526011602090815260409182902080546001810190915582517f6e71edae12b1b97f4d1f60370fef10105fa2faae0126114a169c64845d6126c98184015280840194909452938b166060840152608083018a905260a083019390935260c08083018990528151808403909101815260e090920190528051910120615155565b905073__$715109b5d747ea58b675c6ea3f0dba8c60$__636ccea6528783856040518463ffffffff1660e01b8152600401808473ffffffffffffffffffffffffffffffffffffffff16815260200183815260200180602001828103825283818151815260200191508051906020019080838360005b838110156146015781810151838201526020016145e9565b50505050905090810190601f16801561462e5780820380516001836020036101000a031916815260200191505b5094505050505060206040518083038186803b15801561464d57600080fd5b505af4158015614661573d6000803e3d6000fd5b505050506040513d602081101561467757600080fd5b50516146e457604080517f08c379a000000000000000000000000000000000000000000000000000000000815260206004820152601a60248201527f454950323631323a20696e76616c6964207369676e6174757265000000000000604482015290519081900360640190fd5b6146ef868686613b93565b505050505050565b61343883836142c184604051806060016040528060258152602001615c116025913973ffffffffffffffffffffffffffffffffffffffff808a166000908152600a60209081526040808320938c1683529290522054919061518f565b6040805173ffffffffffffffffffffffffffffffffffffffff8416602482015260448082018490528251808303909101815260649091019091526020810180517bffffffffffffffffffffffffffffffffffffffffffffffffffffffff167fa9059cbb00000000000000000000000000000000000000000000000000000000179052613438908490615240565b6147ea8383615318565b614864837f158b0a9edf7a828aad02f63cd515c68ef2f50ba807396f6d12842833a159742960001b8585604051602001808481526020018373ffffffffffffffffffffffffffffffffffffffff16815260200182815260200193505050506040516020818303038152906040528051906020012083614f52565b73ffffffffffffffffffffffffffffffffffffffff8316600081815260106020908152604080832086845290915280822080547fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff00166001179055518492917f1cdd46ff242716cdaa72d159d339a485b3438398348d68f09d7c8c0a59353d8191a3505050565b6148f687838686614e98565b604080517f7c7c6cdb67a18743f49ec6fa9b35f50d52ed05cbed4cc592e13b44501c1a226760208083019190915273ffffffffffffffffffffffffffffffffffffffff808b1683850152891660608301526080820188905260a0820187905260c0820186905260e080830186905283518084039091018152610100909201909252805191012061441e90889083614f52565b612de187878787868689604051602001808481526020018381526020018260ff1660f81b81526001019350505050604051602081830303815290604052614433565b6000466149d8848483614d3d565b949350505050565b61382189898989898988888b604051602001808481526020018381526020018260ff1660f81b815260010193505050506040516020818303038152906040526148ea565b61382189898989898988888b604051602001808481526020018381526020018260ff1660f81b81526001019350505050604051602081830303815290604052614312565b6000808211614ad857604080517f08c379a000000000000000000000000000000000000000000000000000000000815260206004820152601a60248201527f536166654d6174683a206469766973696f6e206279207a65726f000000000000604482015290519081900360640190fd5b818381614ae157fe5b049392505050565b8015614c86573073ffffffffffffffffffffffffffffffffffffffff16638da5cb5b6040518163ffffffff1660e01b815260040160206040518083038186803b158015614b3557600080fd5b505afa158015614b49573d6000803e3d6000fd5b505050506040513d6020811015614b5f57600080fd5b505173ffffffffffffffffffffffffffffffffffffffff83811691161415614bd2576040517f08c379a000000000000000000000000000000000000000000000000000000000815260040180806020018281038252602b815260200180615a51602b913960400191505060405180910390fd5b604080517fe5c7160b00000000000000000000000000000000000000000000000000000000815273ffffffffffffffffffffffffffffffffffffffff8416600482015290517318000000000000000000000000000000000000019163e5c7160b9160248083019260209291908290030181600087803b158015614c5457600080fd5b505af1158015614c68573d6000803e3d6000fd5b505050506040513d6020811015614c7e57600080fd5b50614d359050565b604080517f31b2302000000000000000000000000000000000000000000000000000000000815273ffffffffffffffffffffffffffffffffffffffff841660048201529051731800000000000000000000000000000000000001916331b230209160248083019260209291908290030181600087803b158015614d0857600080fd5b505af1158015614d1c573d6000803e3d6000fd5b505050506040513d6020811015614d3257600080fd5b50505b5050565b4690565b8251602093840120825192840192909220604080517f8b73c3c69bb8fe3d512ecc4cf759cc79239f7b179b0ffacaa9a75d522b39400f8187015280820194909452606084019190915260808301919091523060a0808401919091528151808403909101815260c09092019052805191012090565b600082820183811015613ce657604080517f08c379a000000000000000000000000000000000000000000000000000000000815260206004820152601b60248201527f536166654d6174683a206164646974696f6e206f766572666c6f770000000000604482015290519081900360640190fd5b600082614e34575060006113a8565b82820282848281614e4157fe5b0414613ce6576040517f08c379a000000000000000000000000000000000000000000000000000000000815260040180806020018281038252602181526020018061598b6021913960400191505060405180910390fd5b814211614ef0576040517f08c379a000000000000000000000000000000000000000000000000000000000815260040180806020018281038252602b81526020018061573b602b913960400191505060405180910390fd5b804210614f48576040517f08c379a0000000000000000000000000000000000000000000000000000000008152600401808060200182810382526025815260200180615bec6025913960400191505060405180910390fd5b614d328484615318565b73__$715109b5d747ea58b675c6ea3f0dba8c60$__636ccea65284614f7e614f78614187565b86615155565b846040518463ffffffff1660e01b8152600401808473ffffffffffffffffffffffffffffffffffffffff16815260200183815260200180602001828103825283818151815260200191508051906020019080838360005b83811015614fed578181015183820152602001614fd5565b50505050905090810190601f16801561501a5780820380516001836020036101000a031916815260200191505b5094505050505060206040518083038186803b15801561503957600080fd5b505af415801561504d573d6000803e3d6000fd5b505050506040513d602081101561506357600080fd5b505161343857604080517f08c379a000000000000000000000000000000000000000000000000000000000815260206004820152601e60248201527f46696174546f6b656e56323a20696e76616c6964207369676e61747572650000604482015290519081900360640190fd5b73ffffffffffffffffffffffffffffffffffffffff8216600081815260106020908152604080832085845290915280822080547fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff00166001179055518392917f98de503528ee59b575ef0c0a2576a82497bfc029a5685b209e9ec333479b10a591a35050565b6040517f19010000000000000000000000000000000000000000000000000000000000008152600281019290925260228201526042902090565b60008184841115615238576040517f08c379a00000000000000000000000000000000000000000000000000000000081526004018080602001828103825283818151815260200191508051906020019080838360005b838110156151fd5781810151838201526020016151e5565b50505050905090810190601f16801561522a5780820380516001836020036101000a031916815260200191505b509250505060405180910390fd5b505050900390565b60606152a2826040518060400160405280602081526020017f5361666545524332303a206c6f772d6c6576656c2063616c6c206661696c65648152508573ffffffffffffffffffffffffffffffffffffffff166153a29092919063ffffffff16565b805190915015613438578080602001905160208110156152c157600080fd5b5051613438576040517f08c379a000000000000000000000000000000000000000000000000000000000815260040180806020018281038252602a815260200180615b15602a913960400191505060405180910390fd5b73ffffffffffffffffffffffffffffffffffffffff8216600090815260106020908152604080832084845290915290205460ff1615614d35576040517f08c379a000000000000000000000000000000000000000000000000000000000815260040180806020018281038252602e815260200180615b67602e913960400191505060405180910390fd5b60606149d88484600085856153b68561550d565b61542157604080517f08c379a000000000000000000000000000000000000000000000000000000000815260206004820152601d60248201527f416464726573733a2063616c6c20746f206e6f6e2d636f6e7472616374000000604482015290519081900360640190fd5b600060608673ffffffffffffffffffffffffffffffffffffffff1685876040518082805190602001908083835b6020831061548b57805182527fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffe0909201916020918201910161544e565b6001836020036101000a03801982511681845116808217855250505050505090500191505060006040518083038185875af1925050503d80600081146154ed576040519150601f19603f3d011682016040523d82523d6000602084013e6154f2565b606091505b5091509150615502828286615513565b979650505050505050565b3b151590565b60608315615522575081613ce6565b8251156155325782518084602001fd5b6040517f08c379a00000000000000000000000000000000000000000000000000000000081526020600482018181528451602484015284518593919283926044019190850190808383600083156151fd5781810151838201526020016151e5565b828054600181600116156101000203166002900490600052602060002090601f016020900481019282601f106155d457805160ff1916838001178555615601565b82800160010185558215615601579182015b828111156156015782518255916020019190600101906155e6565b5061560d92915061569d565b5090565b828054600181600116156101000203166002900490600052602060002090601f016020900481019282601f10615670578280017fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff00823516178555615601565b82800160010185558215615601579182015b82811115615601578235825591602001919060010190615682565b5b8082111561560d576000815560010161569e56fe46696174546f6b656e56325f323a20426c61636b6c697374696e672070726576696f75736c7920756e626c61636b6c6973746564206163636f756e742145524332303a207472616e7366657220746f20746865207a65726f20616464726573735061757361626c653a206e65772070617573657220697320746865207a65726f206164647265737346696174546f6b656e56323a20617574686f72697a6174696f6e206973206e6f74207965742076616c696446696174546f6b656e3a206275726e20616d6f756e74206e6f742067726561746572207468616e203046696174546f6b656e3a206d696e7420746f20746865207a65726f20616464726573734f776e61626c653a206e6577206f776e657220697320746865207a65726f206164647265737345524332303a20617070726f766520746f20746865207a65726f206164647265737346696174546f6b656e3a206e65772070617573657220697320746865207a65726f2061646472657373526573637561626c653a206e6577207265736375657220697320746865207a65726f206164647265737346696174546f6b656e3a206d696e7420616d6f756e74206e6f742067726561746572207468616e203045524332303a207472616e7366657220616d6f756e7420657863656564732062616c616e636546696174546f6b656e3a2063616c6c6572206973206e6f7420746865206d61737465724d696e746572426c61636b6c69737461626c653a2063616c6c6572206973206e6f742074686520626c61636b6c697374657246696174546f6b656e3a206275726e20616d6f756e7420657863656564732062616c616e636546696174546f6b656e3a2063616c6c6572206973206e6f742061206d696e74657246696174546f6b656e3a206e6577206d61737465724d696e74657220697320746865207a65726f2061646472657373526573637561626c653a2063616c6c6572206973206e6f74207468652072657363756572536166654d6174683a206d756c7469706c69636174696f6e206f766572666c6f7745524332303a207472616e7366657220616d6f756e74206578636565647320616c6c6f77616e636546696174546f6b656e3a206e657720626c61636b6c697374657220697320746865207a65726f206164647265737346696174546f6b656e56323a2063616c6c6572206d7573742062652074686520706179656546696174546f6b656e3a20636f6e747261637420697320616c726561647920696e697469616c697a65644e617469766546696174546f6b656e56325f323a2063616e6e6f7420626c61636b6c697374206f776e657245524332303a207472616e736665722066726f6d20746865207a65726f206164647265737345524332303a20617070726f76652066726f6d20746865207a65726f206164647265737346696174546f6b656e3a206d696e7420616d6f756e742065786365656473206d696e746572416c6c6f77616e63655061757361626c653a2063616c6c6572206973206e6f7420746865207061757365725361666545524332303a204552433230206f7065726174696f6e20646964206e6f74207375636365656446696174546f6b656e3a206e6577206f776e657220697320746865207a65726f206164647265737346696174546f6b656e56323a20617574686f72697a6174696f6e2069732075736564206f722063616e63656c6564426c61636b6c69737461626c653a206e657720626c61636b6c697374657220697320746865207a65726f2061646472657373426c61636b6c69737461626c653a206163636f756e7420697320626c61636b6c697374656446696174546f6b656e56323a20617574686f72697a6174696f6e206973206578706972656445524332303a2064656372656173656420616c6c6f77616e63652062656c6f77207a65726fa2646970667358221220e06530c9518c3f8a05f1004a77ca5bf40453a85869b880a2f64f7273f330cb2a64736f6c634300060c0033", - "deployedBytecode": "0x608060405234801561001057600080fd5b506004361061038e5760003560e01c806388b7ab63116101de578063bd1024301161010f578063dd62ed3e116100ad578063ef55bec61161007c578063ef55bec61461115b578063f2fde38b146111c7578063f9f92be4146111fa578063fe575a871461122d5761038e565b8063dd62ed3e14611073578063e3ee160e146110ae578063e5a6b10f1461111a578063e94a0102146111225761038e565b8063d505accf116100e9578063d505accf14610f95578063d608ea6414610ff3578063d916948714611063578063dd0743b31461106b5761038e565b8063bd10243014610ea1578063ccd92d3e14610ea9578063cf09299514610eb15761038e565b8063a457c2d71161017c578063aa271e1a11610156578063aa271e1a14610d30578063ad38bf2214610d63578063b2118a8d14610d96578063b7b7289914610dd95761038e565b8063a457c2d714610c8b578063a9059cbb14610cc4578063aa20e1e414610cfd5761038e565b806395d89b41116101b857806395d89b4114610b9b5780639fd0506d14610ba35780639fd5a6cf14610bab578063a0cc6a6814610c835761038e565b806388b7ab6314610a7c5780638a6db9c314610b605780638da5cb5b14610b935761038e565b806339509351116102c3578063554bab3c116102615780637ecebe00116102305780637ecebe0014610a315780637f2eecc314610a645780637fd0991614610a6c5780638456cb5914610a745761038e565b8063554bab3c146109755780635a049a70146109a85780635c975abb146109f657806370a08231146109fe5761038e565b806342966c681161029d57806342966c6814610855578063430239b4146108725780634e44d9561461093457806354fd4d501461096d5761038e565b806339509351146107db5780633f4ba83a1461081457806340c10f191461081c5761038e565b80633092afd5116103305780633357162b1161030a5780633357162b146105ae57806335d99f351461079a5780633644e515146107cb57806338a63183146107d35761038e565b80633092afd51461055557806330adf81f14610588578063313ce567146105905761038e565b80631a8952661161036c5780631a8952661461047757806323b872dd146104ac5780632ab60045146104ef5780632fc81e09146105225761038e565b806306fdde0314610393578063095ea7b31461041057806318160ddd1461045d575b600080fd5b61039b611260565b6040805160208082528351818301528351919283929083019185019080838360005b838110156103d55781810151838201526020016103bd565b50505050905090810190601f1680156104025780820380516001836020036101000a031916815260200191505b509250505060405180910390f35b6104496004803603604081101561042657600080fd5b5073ffffffffffffffffffffffffffffffffffffffff813516906020013561130c565b604080519115158252519081900360200190f35b6104656113ae565b60408051918252519081900360200190f35b6104aa6004803603602081101561048d57600080fd5b503573ffffffffffffffffffffffffffffffffffffffff1661144e565b005b610449600480360360608110156104c257600080fd5b5073ffffffffffffffffffffffffffffffffffffffff81358116916020810135909116906040013561150b565b6104aa6004803603602081101561050557600080fd5b503573ffffffffffffffffffffffffffffffffffffffff16611703565b6104aa6004803603602081101561053857600080fd5b503573ffffffffffffffffffffffffffffffffffffffff16611864565b6104496004803603602081101561056b57600080fd5b503573ffffffffffffffffffffffffffffffffffffffff166118cc565b6104656119c5565b6105986119e9565b6040805160ff9092168252519081900360200190f35b6104aa60048036036101008110156105c557600080fd5b8101906020810181356401000000008111156105e057600080fd5b8201836020820111156105f257600080fd5b8035906020019184600183028401116401000000008311171561061457600080fd5b91908080601f016020809104026020016040519081016040528093929190818152602001838380828437600092019190915250929594936020810193503591505064010000000081111561066757600080fd5b82018360208201111561067957600080fd5b8035906020019184600183028401116401000000008311171561069b57600080fd5b91908080601f01602080910402602001604051908101604052809392919081815260200183838082843760009201919091525092959493602081019350359150506401000000008111156106ee57600080fd5b82018360208201111561070057600080fd5b8035906020019184600183028401116401000000008311171561072257600080fd5b91908080601f0160208091040260200160405190810160405280939291908181526020018383808284376000920191909152509295505050813560ff16925050602081013573ffffffffffffffffffffffffffffffffffffffff908116916040810135821691606082013581169160800135166119f2565b6107a2611d34565b6040805173ffffffffffffffffffffffffffffffffffffffff9092168252519081900360200190f35b610465611d50565b6107a2611d5f565b610449600480360360408110156107f157600080fd5b5073ffffffffffffffffffffffffffffffffffffffff8135169060200135611d7b565b6104aa611e13565b6104496004803603604081101561083257600080fd5b5073ffffffffffffffffffffffffffffffffffffffff8135169060200135611ed6565b6104aa6004803603602081101561086b57600080fd5b5035612324565b6104aa6004803603604081101561088857600080fd5b8101906020810181356401000000008111156108a357600080fd5b8201836020820111156108b557600080fd5b803590602001918460208302840111640100000000831117156108d757600080fd5b9193909290916020810190356401000000008111156108f557600080fd5b82018360208201111561090757600080fd5b8035906020019184600183028401116401000000008311171561092957600080fd5b509092509050612658565b6104496004803603604081101561094a57600080fd5b5073ffffffffffffffffffffffffffffffffffffffff813516906020013561280f565b61039b6129a2565b6104aa6004803603602081101561098b57600080fd5b503573ffffffffffffffffffffffffffffffffffffffff166129d9565b6104aa600480360360a08110156109be57600080fd5b5073ffffffffffffffffffffffffffffffffffffffff8135169060208101359060ff6040820135169060608101359060800135612b40565b610449612bde565b61046560048036036020811015610a1457600080fd5b503573ffffffffffffffffffffffffffffffffffffffff16612bff565b61046560048036036020811015610a4757600080fd5b503573ffffffffffffffffffffffffffffffffffffffff16612c0a565b610465612c32565b6107a2612c56565b6104aa612c6e565b6104aa600480360360e0811015610a9257600080fd5b73ffffffffffffffffffffffffffffffffffffffff823581169260208101359091169160408201359160608101359160808201359160a08101359181019060e0810160c0820135640100000000811115610aeb57600080fd5b820183602082011115610afd57600080fd5b80359060200191846001830284011164010000000083111715610b1f57600080fd5b91908080601f016020809104026020016040519081016040528093929190818152602001838380828437600092019190915250929550612d48945050505050565b61046560048036036020811015610b7657600080fd5b503573ffffffffffffffffffffffffffffffffffffffff16612dea565b6107a2612e12565b61039b612e2e565b6107a2612ea7565b6104aa600480360360a0811015610bc157600080fd5b73ffffffffffffffffffffffffffffffffffffffff823581169260208101359091169160408201359160608101359181019060a081016080820135640100000000811115610c0e57600080fd5b820183602082011115610c2057600080fd5b80359060200191846001830284011164010000000083111715610c4257600080fd5b91908080601f016020809104026020016040519081016040528093929190818152602001838380828437600092019190915250929550612ec3945050505050565b610465612f5a565b61044960048036036040811015610ca157600080fd5b5073ffffffffffffffffffffffffffffffffffffffff8135169060200135612f7e565b61044960048036036040811015610cda57600080fd5b5073ffffffffffffffffffffffffffffffffffffffff8135169060200135613016565b6104aa60048036036020811015610d1357600080fd5b503573ffffffffffffffffffffffffffffffffffffffff166130ae565b61044960048036036020811015610d4657600080fd5b503573ffffffffffffffffffffffffffffffffffffffff16613215565b6104aa60048036036020811015610d7957600080fd5b503573ffffffffffffffffffffffffffffffffffffffff16613240565b6104aa60048036036060811015610dac57600080fd5b5073ffffffffffffffffffffffffffffffffffffffff8135811691602081013590911690604001356133a7565b6104aa60048036036060811015610def57600080fd5b73ffffffffffffffffffffffffffffffffffffffff82351691602081013591810190606081016040820135640100000000811115610e2c57600080fd5b820183602082011115610e3e57600080fd5b80359060200191846001830284011164010000000083111715610e6057600080fd5b91908080601f01602080910402602001604051908101604052809392919081815260200183838082843760009201919091525092955061343d945050505050565b6107a26134d2565b6104656134ee565b6104aa600480360360e0811015610ec757600080fd5b73ffffffffffffffffffffffffffffffffffffffff823581169260208101359091169160408201359160608101359160808201359160a08101359181019060e0810160c0820135640100000000811115610f2057600080fd5b820183602082011115610f3257600080fd5b80359060200191846001830284011164010000000083111715610f5457600080fd5b91908080601f0160208091040260200160405190810160405280939291908181526020018383808284376000920191909152509295506134f7945050505050565b6104aa600480360360e0811015610fab57600080fd5b5073ffffffffffffffffffffffffffffffffffffffff813581169160208101359091169060408101359060608101359060ff6080820135169060a08101359060c00135613590565b6104aa6004803603602081101561100957600080fd5b81019060208101813564010000000081111561102457600080fd5b82018360208201111561103657600080fd5b8035906020019184600183028401116401000000008311171561105857600080fd5b509092509050613629565b610465613712565b6107a2613736565b6104656004803603604081101561108957600080fd5b5073ffffffffffffffffffffffffffffffffffffffff8135811691602001351661374e565b6104aa60048036036101208110156110c557600080fd5b5073ffffffffffffffffffffffffffffffffffffffff813581169160208101359091169060408101359060608101359060808101359060a08101359060ff60c0820135169060e0810135906101000135613786565b61039b61382c565b6104496004803603604081101561113857600080fd5b5073ffffffffffffffffffffffffffffffffffffffff81351690602001356138a5565b6104aa600480360361012081101561117257600080fd5b5073ffffffffffffffffffffffffffffffffffffffff813581169160208101359091169060408101359060608101359060808101359060a08101359060ff60c0820135169060e08101359061010001356138dd565b6104aa600480360360208110156111dd57600080fd5b503573ffffffffffffffffffffffffffffffffffffffff16613978565b6104aa6004803603602081101561121057600080fd5b503573ffffffffffffffffffffffffffffffffffffffff16613acb565b6104496004803603602081101561124357600080fd5b503573ffffffffffffffffffffffffffffffffffffffff16613b88565b6004805460408051602060026001851615610100027fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff0190941693909304601f810184900484028201840190925281815292918301828280156113045780601f106112d957610100808354040283529160200191611304565b820191906000526020600020905b8154815290600101906020018083116112e757829003601f168201915b505050505081565b60015460009074010000000000000000000000000000000000000000900460ff161561139957604080517f08c379a000000000000000000000000000000000000000000000000000000000815260206004820152601060248201527f5061757361626c653a2070617573656400000000000000000000000000000000604482015290519081900360640190fd5b6113a4338484613b93565b5060015b92915050565b60008073180000000000000000000000000000000000000073ffffffffffffffffffffffffffffffffffffffff166318160ddd6040518163ffffffff1660e01b815260040160206040518083038186803b15801561140b57600080fd5b505afa15801561141f573d6000803e3d6000fd5b505050506040513d602081101561143557600080fd5b505190506114488164e8d4a51000613cda565b91505090565b60025473ffffffffffffffffffffffffffffffffffffffff1633146114be576040517f08c379a000000000000000000000000000000000000000000000000000000000815260040180806020018281038252602c8152602001806158c5602c913960400191505060405180910390fd5b6114c781613ced565b60405173ffffffffffffffffffffffffffffffffffffffff8216907f117e3210bb9aa7d9baff172026820255c6f6c30ba8999d1c2fd88e2848137c4e90600090a250565b60015460009074010000000000000000000000000000000000000000900460ff161561159857604080517f08c379a000000000000000000000000000000000000000000000000000000000815260206004820152601060248201527f5061757361626c653a2070617573656400000000000000000000000000000000604482015290519081900360640190fd5b336115a281613cf8565b156115f8576040517f08c379a0000000000000000000000000000000000000000000000000000000008152600401808060200182810382526025815260200180615bc76025913960400191505060405180910390fd5b73ffffffffffffffffffffffffffffffffffffffff85166000908152600a60209081526040808320338452909152902054831115611681576040517f08c379a00000000000000000000000000000000000000000000000000000000081526004018080602001828103825260288152602001806159ac6028913960400191505060405180910390fd5b73ffffffffffffffffffffffffffffffffffffffff85166000908152600a602090815260408083203384529091529020546116bc9084613da7565b73ffffffffffffffffffffffffffffffffffffffff86166000908152600a602090815260408083203384529091529020556116f8858585613e1e565b506001949350505050565b60005473ffffffffffffffffffffffffffffffffffffffff16331461178957604080517f08c379a000000000000000000000000000000000000000000000000000000000815260206004820181905260248201527f4f776e61626c653a2063616c6c6572206973206e6f7420746865206f776e6572604482015290519081900360640190fd5b73ffffffffffffffffffffffffffffffffffffffff81166117f5576040517f08c379a000000000000000000000000000000000000000000000000000000000815260040180806020018281038252602a815260200180615823602a913960400191505060405180910390fd5b600e80547fffffffffffffffffffffffff00000000000000000000000000000000000000001673ffffffffffffffffffffffffffffffffffffffff83169081179091556040517fe475e580d85111348e40d8ca33cfdd74c30fe1655c2d8537a13abc10065ffa5a90600090a250565b60125460ff1660011461187657600080fd5b60006118813061410c565b9050801561189457611894308383613e1e565b61189d30614135565b5050601280547fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff00166002179055565b60085460009073ffffffffffffffffffffffffffffffffffffffff16331461193f576040517f08c379a000000000000000000000000000000000000000000000000000000000815260040180806020018281038252602981526020018061589c6029913960400191505060405180910390fd5b73ffffffffffffffffffffffffffffffffffffffff82166000818152600c6020908152604080832080547fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff00169055600d909152808220829055517fe94479a9f7e1952cc78f2d6baab678adc1b772d936c6583def489e524cb666929190a2506001919050565b7f6e71edae12b1b97f4d1f60370fef10105fa2faae0126114a169c64845d6126c981565b60065460ff1681565b60085474010000000000000000000000000000000000000000900460ff1615611a66576040517f08c379a000000000000000000000000000000000000000000000000000000000815260040180806020018281038252602a815260200180615a27602a913960400191505060405180910390fd5b73ffffffffffffffffffffffffffffffffffffffff8416611ad2576040517f08c379a000000000000000000000000000000000000000000000000000000000815260040180806020018281038252602f815260200180615938602f913960400191505060405180910390fd5b73ffffffffffffffffffffffffffffffffffffffff8316611b3e576040517f08c379a00000000000000000000000000000000000000000000000000000000081526004018080602001828103825260298152602001806157fa6029913960400191505060405180910390fd5b73ffffffffffffffffffffffffffffffffffffffff8216611baa576040517f08c379a000000000000000000000000000000000000000000000000000000000815260040180806020018281038252602e8152602001806159d4602e913960400191505060405180910390fd5b73ffffffffffffffffffffffffffffffffffffffff8116611c16576040517f08c379a0000000000000000000000000000000000000000000000000000000008152600401808060200182810382526028815260200180615b3f6028913960400191505060405180910390fd5b8751611c299060049060208b0190615593565b508651611c3d9060059060208a0190615593565b508551611c51906007906020890190615593565b50600680547fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff001660ff8716179055600880547fffffffffffffffffffffffff000000000000000000000000000000000000000090811673ffffffffffffffffffffffffffffffffffffffff8781169190911790925560018054821686841617905560028054909116918416919091179055611ceb81614140565b5050600880547fffffffffffffffffffffff00ffffffffffffffffffffffffffffffffffffffff1674010000000000000000000000000000000000000000179055505050505050565b60085473ffffffffffffffffffffffffffffffffffffffff1681565b6000611d5a614187565b905090565b600e5473ffffffffffffffffffffffffffffffffffffffff1690565b60015460009074010000000000000000000000000000000000000000900460ff1615611e0857604080517f08c379a000000000000000000000000000000000000000000000000000000000815260206004820152601060248201527f5061757361626c653a2070617573656400000000000000000000000000000000604482015290519081900360640190fd5b6113a433848461427c565b60015473ffffffffffffffffffffffffffffffffffffffff163314611e83576040517f08c379a0000000000000000000000000000000000000000000000000000000008152600401808060200182810382526022815260200180615af36022913960400191505060405180910390fd5b600180547fffffffffffffffffffffff00ffffffffffffffffffffffffffffffffffffffff1690556040517f7805862f689e2f13df9f062ff482ad3ad112aca9e0847911ed832e158c525b3390600090a1565b336000908152600c602052604081205460ff16611f3e576040517f08c379a00000000000000000000000000000000000000000000000000000000081526004018080602001828103825260218152602001806159176021913960400191505060405180910390fd5b33611f4881613cf8565b15611f9e576040517f08c379a0000000000000000000000000000000000000000000000000000000008152600401808060200182810382526025815260200180615bc76025913960400191505060405180910390fd5b60015474010000000000000000000000000000000000000000900460ff161561202857604080517f08c379a000000000000000000000000000000000000000000000000000000000815260206004820152601060248201527f5061757361626c653a2070617573656400000000000000000000000000000000604482015290519081900360640190fd5b73ffffffffffffffffffffffffffffffffffffffff8416612094576040517f08c379a000000000000000000000000000000000000000000000000000000000815260040180806020018281038252602381526020018061578f6023913960400191505060405180910390fd5b600083116120ed576040517f08c379a000000000000000000000000000000000000000000000000000000000815260040180806020018281038252602981526020018061584d6029913960400191505060405180910390fd5b336000908152600d602052604090205480841115612156576040517f08c379a000000000000000000000000000000000000000000000000000000000815260040180806020018281038252602e815260200180615ac5602e913960400191505060405180910390fd5b336000908152600d6020526040902084820390557318000000000000000000000000000000000000006340c10f19866121948764e8d4a510006142c6565b6040518363ffffffff1660e01b8152600401808373ffffffffffffffffffffffffffffffffffffffff16815260200182815260200192505050602060405180830381600087803b1580156121e757600080fd5b505af11580156121fb573d6000803e3d6000fd5b505050506040513d602081101561221157600080fd5b505161227e57604080517f08c379a000000000000000000000000000000000000000000000000000000000815260206004820152601260248201527f4e6174697665206d696e74206661696c65640000000000000000000000000000604482015290519081900360640190fd5b60408051858152905173ffffffffffffffffffffffffffffffffffffffff87169133917fab8530f87dc9b59234c4623bf917212bb2536d647574c8e7e5da92c2ede0c9f89181900360200190a360408051858152905173ffffffffffffffffffffffffffffffffffffffff8716916000917fddf252ad1be2c89b69c2b068fc378daa952ba7f163c4a11628f55a4df523b3ef9181900360200190a3506001949350505050565b336000908152600c602052604090205460ff1661238c576040517f08c379a00000000000000000000000000000000000000000000000000000000081526004018080602001828103825260218152602001806159176021913960400191505060405180910390fd5b60015474010000000000000000000000000000000000000000900460ff161561241657604080517f08c379a000000000000000000000000000000000000000000000000000000000815260206004820152601060248201527f5061757361626c653a2070617573656400000000000000000000000000000000604482015290519081900360640190fd5b6000811161246f576040517f08c379a00000000000000000000000000000000000000000000000000000000081526004018080602001828103825260298152602001806157666029913960400191505060405180910390fd5b60006124808264e8d4a510006142c6565b905033318111156124dc576040517f08c379a00000000000000000000000000000000000000000000000000000000081526004018080602001828103825260268152602001806158f16026913960400191505060405180910390fd5b604080517f9dc29fac00000000000000000000000000000000000000000000000000000000815233600482015260248101839052905173180000000000000000000000000000000000000091639dc29fac9160448083019260209291908290030181600087803b15801561254f57600080fd5b505af1158015612563573d6000803e3d6000fd5b505050506040513d602081101561257957600080fd5b50516125e657604080517f08c379a000000000000000000000000000000000000000000000000000000000815260206004820152601260248201527f4e6174697665206275726e206661696c65640000000000000000000000000000604482015290519081900360640190fd5b60408051838152905133917fcc16f5dbb4873280815c1ee09dbd06736cffcc184412cf7a71a0fdb75d397ca5919081900360200190a260408051838152905160009133917fddf252ad1be2c89b69c2b068fc378daa952ba7f163c4a11628f55a4df523b3ef9181900360200190a35050565b60125460ff1660021461266a57600080fd5b61267660058383615611565b5060005b838110156127b8576003600086868481811061269257fe5b6020908102929092013573ffffffffffffffffffffffffffffffffffffffff168352508101919091526040016000205460ff1661271a576040517f08c379a000000000000000000000000000000000000000000000000000000000815260040180806020018281038252603d8152602001806156b3603d913960400191505060405180910390fd5b61274b85858381811061272957fe5b9050602002013573ffffffffffffffffffffffffffffffffffffffff16614135565b6003600086868481811061275b57fe5b6020908102929092013573ffffffffffffffffffffffffffffffffffffffff1683525081019190915260400160002080547fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff0016905560010161267a565b506127c230614135565b505030600090815260036020819052604090912080547fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff009081169091556012805490911690911790555050565b60015460009074010000000000000000000000000000000000000000900460ff161561289c57604080517f08c379a000000000000000000000000000000000000000000000000000000000815260206004820152601060248201527f5061757361626c653a2070617573656400000000000000000000000000000000604482015290519081900360640190fd5b60085473ffffffffffffffffffffffffffffffffffffffff16331461290c576040517f08c379a000000000000000000000000000000000000000000000000000000000815260040180806020018281038252602981526020018061589c6029913960400191505060405180910390fd5b73ffffffffffffffffffffffffffffffffffffffff83166000818152600c6020908152604080832080547fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff00166001179055600d825291829020859055815185815291517f46980fca912ef9bcdbd36877427b6b90e860769f604e89c0e67720cece530d209281900390910190a250600192915050565b60408051808201909152600181527f3200000000000000000000000000000000000000000000000000000000000000602082015290565b60005473ffffffffffffffffffffffffffffffffffffffff163314612a5f57604080517f08c379a000000000000000000000000000000000000000000000000000000000815260206004820181905260248201527f4f776e61626c653a2063616c6c6572206973206e6f7420746865206f776e6572604482015290519081900360640190fd5b73ffffffffffffffffffffffffffffffffffffffff8116612acb576040517f08c379a00000000000000000000000000000000000000000000000000000000081526004018080602001828103825260288152602001806157136028913960400191505060405180910390fd5b600180547fffffffffffffffffffffffff00000000000000000000000000000000000000001673ffffffffffffffffffffffffffffffffffffffff83811691909117918290556040519116907fb80482a293ca2e013eda8683c9bd7fc8347cfdaeea5ede58cba46df502c2a60490600090a250565b60015474010000000000000000000000000000000000000000900460ff1615612bca57604080517f08c379a000000000000000000000000000000000000000000000000000000000815260206004820152601060248201527f5061757361626c653a2070617573656400000000000000000000000000000000604482015290519081900360640190fd5b612bd785858585856142d2565b5050505050565b60015474010000000000000000000000000000000000000000900460ff1681565b60006113a88261410c565b73ffffffffffffffffffffffffffffffffffffffff1660009081526011602052604090205490565b7fd099cc98ef71107a616c4f0f941f04c322d8e254fe26b3c6668db87aae413de881565b73180000000000000000000000000000000000000081565b60015473ffffffffffffffffffffffffffffffffffffffff163314612cde576040517f08c379a0000000000000000000000000000000000000000000000000000000008152600401808060200182810382526022815260200180615af36022913960400191505060405180910390fd5b600180547fffffffffffffffffffffff00ffffffffffffffffffffffffffffffffffffffff16740100000000000000000000000000000000000000001790556040517f6985a02210a168e66602d3235cb6db0e70f92b3ba4d376a33c0f3d9434bff62590600090a1565b60015474010000000000000000000000000000000000000000900460ff1615612dd257604080517f08c379a000000000000000000000000000000000000000000000000000000000815260206004820152601060248201527f5061757361626c653a2070617573656400000000000000000000000000000000604482015290519081900360640190fd5b612de187878787878787614312565b50505050505050565b73ffffffffffffffffffffffffffffffffffffffff166000908152600d602052604090205490565b60005473ffffffffffffffffffffffffffffffffffffffff1690565b6005805460408051602060026001851615610100027fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff0190941693909304601f810184900484028201840190925281815292918301828280156113045780601f106112d957610100808354040283529160200191611304565b60015473ffffffffffffffffffffffffffffffffffffffff1681565b60015474010000000000000000000000000000000000000000900460ff1615612f4d57604080517f08c379a000000000000000000000000000000000000000000000000000000000815260206004820152601060248201527f5061757361626c653a2070617573656400000000000000000000000000000000604482015290519081900360640190fd5b612bd78585858585614433565b7f7c7c6cdb67a18743f49ec6fa9b35f50d52ed05cbed4cc592e13b44501c1a226781565b60015460009074010000000000000000000000000000000000000000900460ff161561300b57604080517f08c379a000000000000000000000000000000000000000000000000000000000815260206004820152601060248201527f5061757361626c653a2070617573656400000000000000000000000000000000604482015290519081900360640190fd5b6113a43384846146f7565b60015460009074010000000000000000000000000000000000000000900460ff16156130a357604080517f08c379a000000000000000000000000000000000000000000000000000000000815260206004820152601060248201527f5061757361626c653a2070617573656400000000000000000000000000000000604482015290519081900360640190fd5b6113a4338484613e1e565b60005473ffffffffffffffffffffffffffffffffffffffff16331461313457604080517f08c379a000000000000000000000000000000000000000000000000000000000815260206004820181905260248201527f4f776e61626c653a2063616c6c6572206973206e6f7420746865206f776e6572604482015290519081900360640190fd5b73ffffffffffffffffffffffffffffffffffffffff81166131a0576040517f08c379a000000000000000000000000000000000000000000000000000000000815260040180806020018281038252602f815260200180615938602f913960400191505060405180910390fd5b600880547fffffffffffffffffffffffff00000000000000000000000000000000000000001673ffffffffffffffffffffffffffffffffffffffff83811691909117918290556040519116907fdb66dfa9c6b8f5226fe9aac7e51897ae8ee94ac31dc70bb6c9900b2574b707e690600090a250565b73ffffffffffffffffffffffffffffffffffffffff166000908152600c602052604090205460ff1690565b60005473ffffffffffffffffffffffffffffffffffffffff1633146132c657604080517f08c379a000000000000000000000000000000000000000000000000000000000815260206004820181905260248201527f4f776e61626c653a2063616c6c6572206973206e6f7420746865206f776e6572604482015290519081900360640190fd5b73ffffffffffffffffffffffffffffffffffffffff8116613332576040517f08c379a0000000000000000000000000000000000000000000000000000000008152600401808060200182810382526032815260200180615b956032913960400191505060405180910390fd5b600280547fffffffffffffffffffffffff00000000000000000000000000000000000000001673ffffffffffffffffffffffffffffffffffffffff83811691909117918290556040519116907fc67398012c111ce95ecb7429b933096c977380ee6c421175a71a4a4c6c88c06e90600090a250565b600e5473ffffffffffffffffffffffffffffffffffffffff163314613417576040517f08c379a00000000000000000000000000000000000000000000000000000000081526004018080602001828103825260248152602001806159676024913960400191505060405180910390fd5b61343873ffffffffffffffffffffffffffffffffffffffff84168383614753565b505050565b60015474010000000000000000000000000000000000000000900460ff16156134c757604080517f08c379a000000000000000000000000000000000000000000000000000000000815260206004820152601060248201527f5061757361626c653a2070617573656400000000000000000000000000000000604482015290519081900360640190fd5b6134388383836147e0565b60025473ffffffffffffffffffffffffffffffffffffffff1681565b64e8d4a5100081565b60015474010000000000000000000000000000000000000000900460ff161561358157604080517f08c379a000000000000000000000000000000000000000000000000000000000815260206004820152601060248201527f5061757361626c653a2070617573656400000000000000000000000000000000604482015290519081900360640190fd5b612de1878787878787876148ea565b60015474010000000000000000000000000000000000000000900460ff161561361a57604080517f08c379a000000000000000000000000000000000000000000000000000000000815260206004820152601060248201527f5061757361626c653a2070617573656400000000000000000000000000000000604482015290519081900360640190fd5b612de187878787878787614988565b60085474010000000000000000000000000000000000000000900460ff168015613656575060125460ff16155b61365f57600080fd5b61366b60048383615611565b506136e082828080601f0160208091040260200160405190810160405280939291908181526020018383808284376000920191909152505060408051808201909152600181527f3200000000000000000000000000000000000000000000000000000000000000602082015291506149ca9050565b600f555050601280547fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff00166001179055565b7f158b0a9edf7a828aad02f63cd515c68ef2f50ba807396f6d12842833a159742981565b73180000000000000000000000000000000000000181565b73ffffffffffffffffffffffffffffffffffffffff9182166000908152600a6020908152604080832093909416825291909152205490565b60015474010000000000000000000000000000000000000000900460ff161561381057604080517f08c379a000000000000000000000000000000000000000000000000000000000815260206004820152601060248201527f5061757361626c653a2070617573656400000000000000000000000000000000604482015290519081900360640190fd5b6138218989898989898989896149e0565b505050505050505050565b6007805460408051602060026001851615610100027fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff0190941693909304601f810184900484028201840190925281815292918301828280156113045780601f106112d957610100808354040283529160200191611304565b73ffffffffffffffffffffffffffffffffffffffff919091166000908152601060209081526040808320938352929052205460ff1690565b60015474010000000000000000000000000000000000000000900460ff161561396757604080517f08c379a000000000000000000000000000000000000000000000000000000000815260206004820152601060248201527f5061757361626c653a2070617573656400000000000000000000000000000000604482015290519081900360640190fd5b613821898989898989898989614a24565b60005473ffffffffffffffffffffffffffffffffffffffff1633146139fe57604080517f08c379a000000000000000000000000000000000000000000000000000000000815260206004820181905260248201527f4f776e61626c653a2063616c6c6572206973206e6f7420746865206f776e6572604482015290519081900360640190fd5b73ffffffffffffffffffffffffffffffffffffffff8116613a6a576040517f08c379a00000000000000000000000000000000000000000000000000000000081526004018080602001828103825260268152602001806157b26026913960400191505060405180910390fd5b6000546040805173ffffffffffffffffffffffffffffffffffffffff9283168152918316602083015280517f8be0079c531659141344cd1fd0a4f28419497f9722a3daafe3b4186f6b6457e09281900390910190a1613ac881614140565b50565b60025473ffffffffffffffffffffffffffffffffffffffff163314613b3b576040517f08c379a000000000000000000000000000000000000000000000000000000000815260040180806020018281038252602c8152602001806158c5602c913960400191505060405180910390fd5b613b4481614135565b60405173ffffffffffffffffffffffffffffffffffffffff8216907fffa4e6181777692565cf28528fc88fd1516ea86b56da075235fa575af6a4b85590600090a250565b60006113a882613cf8565b73ffffffffffffffffffffffffffffffffffffffff8316613bff576040517f08c379a0000000000000000000000000000000000000000000000000000000008152600401808060200182810382526024815260200180615aa16024913960400191505060405180910390fd5b73ffffffffffffffffffffffffffffffffffffffff8216613c6b576040517f08c379a00000000000000000000000000000000000000000000000000000000081526004018080602001828103825260228152602001806157d86022913960400191505060405180910390fd5b73ffffffffffffffffffffffffffffffffffffffff8084166000818152600a6020908152604080832094871680845294825291829020859055815185815291517f8c5be1e5ebec7d5bd14f71427d1e84f3dd0314c0f7b2291e5b200ac8c7c3b9259281900390910190a3505050565b6000613ce68383614a68565b9392505050565b613ac8816000614ae9565b600073180000000000000000000000000000000000000173ffffffffffffffffffffffffffffffffffffffff16638e204c43836040518263ffffffff1660e01b8152600401808273ffffffffffffffffffffffffffffffffffffffff16815260200191505060206040518083038186803b158015613d7557600080fd5b505afa158015613d89573d6000803e3d6000fd5b505050506040513d6020811015613d9f57600080fd5b505192915050565b600082821115613e1857604080517f08c379a000000000000000000000000000000000000000000000000000000000815260206004820152601e60248201527f536166654d6174683a207375627472616374696f6e206f766572666c6f770000604482015290519081900360640190fd5b50900390565b73ffffffffffffffffffffffffffffffffffffffff8316613e8a576040517f08c379a0000000000000000000000000000000000000000000000000000000008152600401808060200182810382526025815260200180615a7c6025913960400191505060405180910390fd5b73ffffffffffffffffffffffffffffffffffffffff8216613ef6576040517f08c379a00000000000000000000000000000000000000000000000000000000081526004018080602001828103825260238152602001806156f06023913960400191505060405180910390fd5b6000613f078264e8d4a510006142c6565b90508373ffffffffffffffffffffffffffffffffffffffff1631811115613f79576040517f08c379a00000000000000000000000000000000000000000000000000000000081526004018080602001828103825260268152602001806158766026913960400191505060405180910390fd5b604080517fbeabacc800000000000000000000000000000000000000000000000000000000815273ffffffffffffffffffffffffffffffffffffffff8681166004830152851660248201526044810183905290517318000000000000000000000000000000000000009163beabacc89160648083019260209291908290030181600087803b15801561400a57600080fd5b505af115801561401e573d6000803e3d6000fd5b505050506040513d602081101561403457600080fd5b50516140a157604080517f08c379a000000000000000000000000000000000000000000000000000000000815260206004820152601660248201527f4e6174697665207472616e73666572206661696c656400000000000000000000604482015290519081900360640190fd5b8273ffffffffffffffffffffffffffffffffffffffff168473ffffffffffffffffffffffffffffffffffffffff167fddf252ad1be2c89b69c2b068fc378daa952ba7f163c4a11628f55a4df523b3ef846040518082815260200191505060405180910390a350505050565b600073ffffffffffffffffffffffffffffffffffffffff821631613ce68164e8d4a51000613cda565b613ac8816001614ae9565b600080547fffffffffffffffffffffffff00000000000000000000000000000000000000001673ffffffffffffffffffffffffffffffffffffffff92909216919091179055565b6004805460408051602060026001851615610100027fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff0190941693909304601f8101849004840282018401909252818152600093611d5a93919290918301828280156142345780601f1061420957610100808354040283529160200191614234565b820191906000526020600020905b81548152906001019060200180831161421757829003601f168201915b50505050506040518060400160405280600181526020017f3200000000000000000000000000000000000000000000000000000000000000815250614277614d39565b614d3d565b73ffffffffffffffffffffffffffffffffffffffff8084166000908152600a602090815260408083209386168352929052205461343890849084906142c19085614db1565b613b93565b6000613ce68383614e25565b612bd78585848487604051602001808481526020018381526020018260ff1660f81b815260010193505050506040516020818303038152906040526147e0565b73ffffffffffffffffffffffffffffffffffffffff86163314614380576040517f08c379a0000000000000000000000000000000000000000000000000000000008152600401808060200182810382526025815260200180615a026025913960400191505060405180910390fd5b61438c87838686614e98565b604080517fd099cc98ef71107a616c4f0f941f04c322d8e254fe26b3c6668db87aae413de860208083019190915273ffffffffffffffffffffffffffffffffffffffff808b1683850152891660608301526080820188905260a0820187905260c0820186905260e080830186905283518084039091018152610100909201909252805191012061441e90889083614f52565b61442887836150d0565b612de1878787613e1e565b7fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff8214806144615750428210155b6144cc57604080517f08c379a000000000000000000000000000000000000000000000000000000000815260206004820152601e60248201527f46696174546f6b656e56323a207065726d697420697320657870697265640000604482015290519081900360640190fd5b60006145746144d9614187565b73ffffffffffffffffffffffffffffffffffffffff80891660008181526011602090815260409182902080546001810190915582517f6e71edae12b1b97f4d1f60370fef10105fa2faae0126114a169c64845d6126c98184015280840194909452938b166060840152608083018a905260a083019390935260c08083018990528151808403909101815260e090920190528051910120615155565b905073__$715109b5d747ea58b675c6ea3f0dba8c60$__636ccea6528783856040518463ffffffff1660e01b8152600401808473ffffffffffffffffffffffffffffffffffffffff16815260200183815260200180602001828103825283818151815260200191508051906020019080838360005b838110156146015781810151838201526020016145e9565b50505050905090810190601f16801561462e5780820380516001836020036101000a031916815260200191505b5094505050505060206040518083038186803b15801561464d57600080fd5b505af4158015614661573d6000803e3d6000fd5b505050506040513d602081101561467757600080fd5b50516146e457604080517f08c379a000000000000000000000000000000000000000000000000000000000815260206004820152601a60248201527f454950323631323a20696e76616c6964207369676e6174757265000000000000604482015290519081900360640190fd5b6146ef868686613b93565b505050505050565b61343883836142c184604051806060016040528060258152602001615c116025913973ffffffffffffffffffffffffffffffffffffffff808a166000908152600a60209081526040808320938c1683529290522054919061518f565b6040805173ffffffffffffffffffffffffffffffffffffffff8416602482015260448082018490528251808303909101815260649091019091526020810180517bffffffffffffffffffffffffffffffffffffffffffffffffffffffff167fa9059cbb00000000000000000000000000000000000000000000000000000000179052613438908490615240565b6147ea8383615318565b614864837f158b0a9edf7a828aad02f63cd515c68ef2f50ba807396f6d12842833a159742960001b8585604051602001808481526020018373ffffffffffffffffffffffffffffffffffffffff16815260200182815260200193505050506040516020818303038152906040528051906020012083614f52565b73ffffffffffffffffffffffffffffffffffffffff8316600081815260106020908152604080832086845290915280822080547fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff00166001179055518492917f1cdd46ff242716cdaa72d159d339a485b3438398348d68f09d7c8c0a59353d8191a3505050565b6148f687838686614e98565b604080517f7c7c6cdb67a18743f49ec6fa9b35f50d52ed05cbed4cc592e13b44501c1a226760208083019190915273ffffffffffffffffffffffffffffffffffffffff808b1683850152891660608301526080820188905260a0820187905260c0820186905260e080830186905283518084039091018152610100909201909252805191012061441e90889083614f52565b612de187878787868689604051602001808481526020018381526020018260ff1660f81b81526001019350505050604051602081830303815290604052614433565b6000466149d8848483614d3d565b949350505050565b61382189898989898988888b604051602001808481526020018381526020018260ff1660f81b815260010193505050506040516020818303038152906040526148ea565b61382189898989898988888b604051602001808481526020018381526020018260ff1660f81b81526001019350505050604051602081830303815290604052614312565b6000808211614ad857604080517f08c379a000000000000000000000000000000000000000000000000000000000815260206004820152601a60248201527f536166654d6174683a206469766973696f6e206279207a65726f000000000000604482015290519081900360640190fd5b818381614ae157fe5b049392505050565b8015614c86573073ffffffffffffffffffffffffffffffffffffffff16638da5cb5b6040518163ffffffff1660e01b815260040160206040518083038186803b158015614b3557600080fd5b505afa158015614b49573d6000803e3d6000fd5b505050506040513d6020811015614b5f57600080fd5b505173ffffffffffffffffffffffffffffffffffffffff83811691161415614bd2576040517f08c379a000000000000000000000000000000000000000000000000000000000815260040180806020018281038252602b815260200180615a51602b913960400191505060405180910390fd5b604080517fe5c7160b00000000000000000000000000000000000000000000000000000000815273ffffffffffffffffffffffffffffffffffffffff8416600482015290517318000000000000000000000000000000000000019163e5c7160b9160248083019260209291908290030181600087803b158015614c5457600080fd5b505af1158015614c68573d6000803e3d6000fd5b505050506040513d6020811015614c7e57600080fd5b50614d359050565b604080517f31b2302000000000000000000000000000000000000000000000000000000000815273ffffffffffffffffffffffffffffffffffffffff841660048201529051731800000000000000000000000000000000000001916331b230209160248083019260209291908290030181600087803b158015614d0857600080fd5b505af1158015614d1c573d6000803e3d6000fd5b505050506040513d6020811015614d3257600080fd5b50505b5050565b4690565b8251602093840120825192840192909220604080517f8b73c3c69bb8fe3d512ecc4cf759cc79239f7b179b0ffacaa9a75d522b39400f8187015280820194909452606084019190915260808301919091523060a0808401919091528151808403909101815260c09092019052805191012090565b600082820183811015613ce657604080517f08c379a000000000000000000000000000000000000000000000000000000000815260206004820152601b60248201527f536166654d6174683a206164646974696f6e206f766572666c6f770000000000604482015290519081900360640190fd5b600082614e34575060006113a8565b82820282848281614e4157fe5b0414613ce6576040517f08c379a000000000000000000000000000000000000000000000000000000000815260040180806020018281038252602181526020018061598b6021913960400191505060405180910390fd5b814211614ef0576040517f08c379a000000000000000000000000000000000000000000000000000000000815260040180806020018281038252602b81526020018061573b602b913960400191505060405180910390fd5b804210614f48576040517f08c379a0000000000000000000000000000000000000000000000000000000008152600401808060200182810382526025815260200180615bec6025913960400191505060405180910390fd5b614d328484615318565b73__$715109b5d747ea58b675c6ea3f0dba8c60$__636ccea65284614f7e614f78614187565b86615155565b846040518463ffffffff1660e01b8152600401808473ffffffffffffffffffffffffffffffffffffffff16815260200183815260200180602001828103825283818151815260200191508051906020019080838360005b83811015614fed578181015183820152602001614fd5565b50505050905090810190601f16801561501a5780820380516001836020036101000a031916815260200191505b5094505050505060206040518083038186803b15801561503957600080fd5b505af415801561504d573d6000803e3d6000fd5b505050506040513d602081101561506357600080fd5b505161343857604080517f08c379a000000000000000000000000000000000000000000000000000000000815260206004820152601e60248201527f46696174546f6b656e56323a20696e76616c6964207369676e61747572650000604482015290519081900360640190fd5b73ffffffffffffffffffffffffffffffffffffffff8216600081815260106020908152604080832085845290915280822080547fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff00166001179055518392917f98de503528ee59b575ef0c0a2576a82497bfc029a5685b209e9ec333479b10a591a35050565b6040517f19010000000000000000000000000000000000000000000000000000000000008152600281019290925260228201526042902090565b60008184841115615238576040517f08c379a00000000000000000000000000000000000000000000000000000000081526004018080602001828103825283818151815260200191508051906020019080838360005b838110156151fd5781810151838201526020016151e5565b50505050905090810190601f16801561522a5780820380516001836020036101000a031916815260200191505b509250505060405180910390fd5b505050900390565b60606152a2826040518060400160405280602081526020017f5361666545524332303a206c6f772d6c6576656c2063616c6c206661696c65648152508573ffffffffffffffffffffffffffffffffffffffff166153a29092919063ffffffff16565b805190915015613438578080602001905160208110156152c157600080fd5b5051613438576040517f08c379a000000000000000000000000000000000000000000000000000000000815260040180806020018281038252602a815260200180615b15602a913960400191505060405180910390fd5b73ffffffffffffffffffffffffffffffffffffffff8216600090815260106020908152604080832084845290915290205460ff1615614d35576040517f08c379a000000000000000000000000000000000000000000000000000000000815260040180806020018281038252602e815260200180615b67602e913960400191505060405180910390fd5b60606149d88484600085856153b68561550d565b61542157604080517f08c379a000000000000000000000000000000000000000000000000000000000815260206004820152601d60248201527f416464726573733a2063616c6c20746f206e6f6e2d636f6e7472616374000000604482015290519081900360640190fd5b600060608673ffffffffffffffffffffffffffffffffffffffff1685876040518082805190602001908083835b6020831061548b57805182527fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffe0909201916020918201910161544e565b6001836020036101000a03801982511681845116808217855250505050505090500191505060006040518083038185875af1925050503d80600081146154ed576040519150601f19603f3d011682016040523d82523d6000602084013e6154f2565b606091505b5091509150615502828286615513565b979650505050505050565b3b151590565b60608315615522575081613ce6565b8251156155325782518084602001fd5b6040517f08c379a00000000000000000000000000000000000000000000000000000000081526020600482018181528451602484015284518593919283926044019190850190808383600083156151fd5781810151838201526020016151e5565b828054600181600116156101000203166002900490600052602060002090601f016020900481019282601f106155d457805160ff1916838001178555615601565b82800160010185558215615601579182015b828111156156015782518255916020019190600101906155e6565b5061560d92915061569d565b5090565b828054600181600116156101000203166002900490600052602060002090601f016020900481019282601f10615670578280017fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff00823516178555615601565b82800160010185558215615601579182015b82811115615601578235825591602001919060010190615682565b5b8082111561560d576000815560010161569e56fe46696174546f6b656e56325f323a20426c61636b6c697374696e672070726576696f75736c7920756e626c61636b6c6973746564206163636f756e742145524332303a207472616e7366657220746f20746865207a65726f20616464726573735061757361626c653a206e65772070617573657220697320746865207a65726f206164647265737346696174546f6b656e56323a20617574686f72697a6174696f6e206973206e6f74207965742076616c696446696174546f6b656e3a206275726e20616d6f756e74206e6f742067726561746572207468616e203046696174546f6b656e3a206d696e7420746f20746865207a65726f20616464726573734f776e61626c653a206e6577206f776e657220697320746865207a65726f206164647265737345524332303a20617070726f766520746f20746865207a65726f206164647265737346696174546f6b656e3a206e65772070617573657220697320746865207a65726f2061646472657373526573637561626c653a206e6577207265736375657220697320746865207a65726f206164647265737346696174546f6b656e3a206d696e7420616d6f756e74206e6f742067726561746572207468616e203045524332303a207472616e7366657220616d6f756e7420657863656564732062616c616e636546696174546f6b656e3a2063616c6c6572206973206e6f7420746865206d61737465724d696e746572426c61636b6c69737461626c653a2063616c6c6572206973206e6f742074686520626c61636b6c697374657246696174546f6b656e3a206275726e20616d6f756e7420657863656564732062616c616e636546696174546f6b656e3a2063616c6c6572206973206e6f742061206d696e74657246696174546f6b656e3a206e6577206d61737465724d696e74657220697320746865207a65726f2061646472657373526573637561626c653a2063616c6c6572206973206e6f74207468652072657363756572536166654d6174683a206d756c7469706c69636174696f6e206f766572666c6f7745524332303a207472616e7366657220616d6f756e74206578636565647320616c6c6f77616e636546696174546f6b656e3a206e657720626c61636b6c697374657220697320746865207a65726f206164647265737346696174546f6b656e56323a2063616c6c6572206d7573742062652074686520706179656546696174546f6b656e3a20636f6e747261637420697320616c726561647920696e697469616c697a65644e617469766546696174546f6b656e56325f323a2063616e6e6f7420626c61636b6c697374206f776e657245524332303a207472616e736665722066726f6d20746865207a65726f206164647265737345524332303a20617070726f76652066726f6d20746865207a65726f206164647265737346696174546f6b656e3a206d696e7420616d6f756e742065786365656473206d696e746572416c6c6f77616e63655061757361626c653a2063616c6c6572206973206e6f7420746865207061757365725361666545524332303a204552433230206f7065726174696f6e20646964206e6f74207375636365656446696174546f6b656e3a206e6577206f776e657220697320746865207a65726f206164647265737346696174546f6b656e56323a20617574686f72697a6174696f6e2069732075736564206f722063616e63656c6564426c61636b6c69737461626c653a206e657720626c61636b6c697374657220697320746865207a65726f2061646472657373426c61636b6c69737461626c653a206163636f756e7420697320626c61636b6c697374656446696174546f6b656e56323a20617574686f72697a6174696f6e206973206578706972656445524332303a2064656372656173656420616c6c6f77616e63652062656c6f77207a65726fa2646970667358221220e06530c9518c3f8a05f1004a77ca5bf40453a85869b880a2f64f7273f330cb2a64736f6c634300060c0033", - "linkReferences": { - "contracts/util/SignatureChecker.sol": { - "SignatureChecker": [ - { - "length": 20, - "start": 17887 - }, - { - "length": 20, - "start": 20411 - } - ] - } - }, - "deployedLinkReferences": { - "contracts/util/SignatureChecker.sol": { - "SignatureChecker": [ - { - "length": 20, - "start": 17784 - }, - { - "length": 20, - "start": 20308 - } - ] - } - } -} diff --git a/assets/artifacts/stablecoin-contracts/README.md b/assets/artifacts/stablecoin-contracts/README.md deleted file mode 100644 index 9ce972d..0000000 --- a/assets/artifacts/stablecoin-contracts/README.md +++ /dev/null @@ -1,13 +0,0 @@ -# Stablecoin Contract Artifacts - -This directory contains pre-compiled Hardhat JSON artifacts for the USDC ERC-20 implementation on Arc. - -## Contracts - -- **SignatureChecker.json** - Library for signature verification (linked to NativeFiatTokenV2_2) -- **NativeFiatTokenV2_2.json** - USDC ERC-20 implementation contract with Arc-specific modifications -- **FiatTokenProxy.json** - USDC transparent proxy contract - -## Deployment - -The genesis builder (`contracts/scripts/ArtifactHelper.s.sol`) uses these artifacts to set the USDC contract bytecode in the Arc genesis block for local development. diff --git a/assets/artifacts/stablecoin-contracts/SignatureChecker.json b/assets/artifacts/stablecoin-contracts/SignatureChecker.json deleted file mode 100644 index 510d73f..0000000 --- a/assets/artifacts/stablecoin-contracts/SignatureChecker.json +++ /dev/null @@ -1,40 +0,0 @@ -{ - "_format": "hh-sol-artifact-1", - "contractName": "SignatureChecker", - "sourceName": "contracts/util/SignatureChecker.sol", - "abi": [ - { - "inputs": [ - { - "internalType": "address", - "name": "signer", - "type": "address" - }, - { - "internalType": "bytes32", - "name": "digest", - "type": "bytes32" - }, - { - "internalType": "bytes", - "name": "signature", - "type": "bytes" - } - ], - "name": "isValidSignatureNow", - "outputs": [ - { - "internalType": "bool", - "name": "", - "type": "bool" - } - ], - "stateMutability": "view", - "type": "function" - } - ], - "bytecode": "0x6106cd610026600b82828239805160001a60731461001957fe5b30600052607381538281f3fe73000000000000000000000000000000000000000030146080604052600436106100355760003560e01c80636ccea6521461003a575b600080fd5b6101026004803603606081101561005057600080fd5b73ffffffffffffffffffffffffffffffffffffffff8235169160208101359181019060608101604082013564010000000081111561008d57600080fd5b82018360208201111561009f57600080fd5b803590602001918460018302840111640100000000831117156100c157600080fd5b91908080601f016020809104026020016040519081016040528093929190818152602001838380828437600092019190915250929550610116945050505050565b604080519115158252519081900360200190f35b600061012184610179565b610164578373ffffffffffffffffffffffffffffffffffffffff16610146848461017f565b73ffffffffffffffffffffffffffffffffffffffff16149050610172565b61016f848484610203565b90505b9392505050565b3b151590565b600081516041146101db576040517f08c379a00000000000000000000000000000000000000000000000000000000081526004018080602001828103825260238152602001806106296023913960400191505060405180910390fd5b60208201516040830151606084015160001a6101f98682858561042d565b9695505050505050565b60008060608573ffffffffffffffffffffffffffffffffffffffff16631626ba7e60e01b86866040516024018083815260200180602001828103825283818151815260200191508051906020019080838360005b8381101561026f578181015183820152602001610257565b50505050905090810190601f16801561029c5780820380516001836020036101000a031916815260200191505b50604080517fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffe08184030181529181526020820180517bffffffffffffffffffffffffffffffffffffffffffffffffffffffff167fffffffff000000000000000000000000000000000000000000000000000000009098169790971787525181519196909550859450925090508083835b6020831061036957805182527fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffe0909201916020918201910161032c565b6001836020036101000a038019825116818451168082178552505050505050905001915050600060405180830381855afa9150503d80600081146103c9576040519150601f19603f3d011682016040523d82523d6000602084013e6103ce565b606091505b50915091508180156103e257506020815110155b80156101f9575080517f1626ba7e00000000000000000000000000000000000000000000000000000000906020808401919081101561042057600080fd5b5051149695505050505050565b60007f7fffffffffffffffffffffffffffffff5d576e7357a4501ddfe92f46681b20a08211156104a8576040517f08c379a00000000000000000000000000000000000000000000000000000000081526004018080602001828103825260268152602001806106726026913960400191505060405180910390fd5b8360ff16601b141580156104c057508360ff16601c14155b15610516576040517f08c379a000000000000000000000000000000000000000000000000000000000815260040180806020018281038252602681526020018061064c6026913960400191505060405180910390fd5b600060018686868660405160008152602001604052604051808581526020018460ff1681526020018381526020018281526020019450505050506020604051602081039080840390855afa158015610572573d6000803e3d6000fd5b50506040517fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffe0015191505073ffffffffffffffffffffffffffffffffffffffff811661061f57604080517f08c379a000000000000000000000000000000000000000000000000000000000815260206004820152601c60248201527f45435265636f7665723a20696e76616c6964207369676e617475726500000000604482015290519081900360640190fd5b9594505050505056fe45435265636f7665723a20696e76616c6964207369676e6174757265206c656e67746845435265636f7665723a20696e76616c6964207369676e6174757265202776272076616c756545435265636f7665723a20696e76616c6964207369676e6174757265202773272076616c7565a2646970667358221220289705d6ae8701c9ed586541fa0ba342f754518aa4f0e59dbbda380bc9f2323964736f6c634300060c0033", - "deployedBytecode": "0x73000000000000000000000000000000000000000030146080604052600436106100355760003560e01c80636ccea6521461003a575b600080fd5b6101026004803603606081101561005057600080fd5b73ffffffffffffffffffffffffffffffffffffffff8235169160208101359181019060608101604082013564010000000081111561008d57600080fd5b82018360208201111561009f57600080fd5b803590602001918460018302840111640100000000831117156100c157600080fd5b91908080601f016020809104026020016040519081016040528093929190818152602001838380828437600092019190915250929550610116945050505050565b604080519115158252519081900360200190f35b600061012184610179565b610164578373ffffffffffffffffffffffffffffffffffffffff16610146848461017f565b73ffffffffffffffffffffffffffffffffffffffff16149050610172565b61016f848484610203565b90505b9392505050565b3b151590565b600081516041146101db576040517f08c379a00000000000000000000000000000000000000000000000000000000081526004018080602001828103825260238152602001806106296023913960400191505060405180910390fd5b60208201516040830151606084015160001a6101f98682858561042d565b9695505050505050565b60008060608573ffffffffffffffffffffffffffffffffffffffff16631626ba7e60e01b86866040516024018083815260200180602001828103825283818151815260200191508051906020019080838360005b8381101561026f578181015183820152602001610257565b50505050905090810190601f16801561029c5780820380516001836020036101000a031916815260200191505b50604080517fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffe08184030181529181526020820180517bffffffffffffffffffffffffffffffffffffffffffffffffffffffff167fffffffff000000000000000000000000000000000000000000000000000000009098169790971787525181519196909550859450925090508083835b6020831061036957805182527fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffe0909201916020918201910161032c565b6001836020036101000a038019825116818451168082178552505050505050905001915050600060405180830381855afa9150503d80600081146103c9576040519150601f19603f3d011682016040523d82523d6000602084013e6103ce565b606091505b50915091508180156103e257506020815110155b80156101f9575080517f1626ba7e00000000000000000000000000000000000000000000000000000000906020808401919081101561042057600080fd5b5051149695505050505050565b60007f7fffffffffffffffffffffffffffffff5d576e7357a4501ddfe92f46681b20a08211156104a8576040517f08c379a00000000000000000000000000000000000000000000000000000000081526004018080602001828103825260268152602001806106726026913960400191505060405180910390fd5b8360ff16601b141580156104c057508360ff16601c14155b15610516576040517f08c379a000000000000000000000000000000000000000000000000000000000815260040180806020018281038252602681526020018061064c6026913960400191505060405180910390fd5b600060018686868660405160008152602001604052604051808581526020018460ff1681526020018381526020018281526020019450505050506020604051602081039080840390855afa158015610572573d6000803e3d6000fd5b50506040517fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffe0015191505073ffffffffffffffffffffffffffffffffffffffff811661061f57604080517f08c379a000000000000000000000000000000000000000000000000000000000815260206004820152601c60248201527f45435265636f7665723a20696e76616c6964207369676e617475726500000000604482015290519081900360640190fd5b9594505050505056fe45435265636f7665723a20696e76616c6964207369676e6174757265206c656e67746845435265636f7665723a20696e76616c6964207369676e6174757265202776272076616c756545435265636f7665723a20696e76616c6964207369676e6174757265202773272076616c7565a2646970667358221220289705d6ae8701c9ed586541fa0ba342f754518aa4f0e59dbbda380bc9f2323964736f6c634300060c0033", - "linkReferences": {}, - "deployedLinkReferences": {} -} diff --git a/assets/devnet/.gitignore b/assets/devnet/.gitignore deleted file mode 100644 index cab975a..0000000 --- a/assets/devnet/.gitignore +++ /dev/null @@ -1,4 +0,0 @@ -jwtsecret - -validator*.json -wallet-secrets.json diff --git a/assets/devnet/config.json b/assets/devnet/config.json deleted file mode 100644 index 1400a18..0000000 --- a/assets/devnet/config.json +++ /dev/null @@ -1,189 +0,0 @@ -{ - "timestamp": "1758617247", - "coinbase": "0x65E0a200006D4FF91bD59F9694220dafc49dbBC1", - "NativeFiatToken": { - "proxy": { - "admin": "0x2454fd9B923cD74d690bf500b4120E2BAEe3781a" - }, - "owner": "0x46F6a3Fb2F3E897c7A4c3aD1B18D864884cf0FAc", - "pauser": "0xc9dE92071ceadbfFf0ee4d222B7E0F9bF9c8A584", - "masterMinter": "0x7f44B75d3EE7414c61ac3a255BE1fD0Ec140130D", - "rescuer": "0xf496BB9e77c05Ce18452ae6d444D87Cd04EbD4F3", - "blacklister": "0x334FF04B5d0E14Abbc1019F62981f7A4eEc5298C", - "minters": [ - { - "address": "0x281CBe8Dd1D7B64995133EbA264dfb5b21e9e053", - "allowance": "1000000000000000000000000" - } - ] - }, - "ProtocolConfig": { - "proxy": { - "admin": "0xdC151920e1F7CD214a108a3Ad0E6a586d46ca015" - }, - "owner": "0xE73C02301f1e6877Ad8B5FB7cfbbD51AF97bC0Ca", - "controller": "0xFdfC974f9A66bd01353f39dfC32E6BaBC6f22ff6", - "pauser": "0xe054635B17c4Bd648b820ecf5Ab832688733AAD1", - "beneficiary": "0x65E0a200006D4FF91bD59F9694220dafc49dbBC1", - "feeParams": { - "alpha": "20", - "kRate": "25", - "inverseElasticityMultiplier": "5000", - "minBaseFee": "1", - "maxBaseFee": "1000", - "blockGasLimit": "30000000" - }, - "consensusParams": { - "timeoutProposeMs": "3000", - "timeoutProposeDeltaMs": "500", - "timeoutPrevoteMs": "1000", - "timeoutPrevoteDeltaMs": "500", - "timeoutPrecommitMs": "1000", - "timeoutPrecommitDeltaMs": "500", - "timeoutRebroadcastMs": "1000", - "targetBlockTimeMs": "500" - } - }, - "ValidatorManager": { - "proxy": { - "admin": "0xfD9A0965Db3D6376f498347f4C129bA6c72f9628" - }, - "PermissionedValidatorManager": { - "proxy": { - "admin": "0xEe33d2085E034E5B9D9346C775f10D6ea284D620" - }, - "owner": "0xE2Ffed765eEc0cc29507Be6328CD7Ac8E110f02A", - "validatorRegisterers": [ - "0x9ED6CB4C5651d81C50Ea3109D55E87c913cc2Ba7", - "0x90A963443ADaBdDB73bCa009795E94f0E7b9b0e9" - ], - "controllers": [ - "0xae7713c841C0E4d96c28479eB1BD3E73A6E3B48E", - "0xb4e1373fEbB3bB1750cC2Ee7C4eeaFA113bbCE4f", - "0x9d6E79502e3B2b94178830BA6Bb76A7f54f9ff61", - "0x30D30792030b8B499760dB18C447aa0d83003932", - "0xfc8D6aa4F924C410E0F2449bed4b0CaCfC8f1F1A", - "0x2247f8fEF4b001eD8785eb5e292Ff8829D755012", - "0xBD62385032AE4c8BF1BA2743A187F4e6c7462061", - "0xa3ae2E82b6195b425b8C2Da8D5DB84f7dd1380B6", - "0x9965a08ba97ed0332FCbBf8188f9700Aca620860" - ] - }, - "validators": [ - { - "publicKey": "0xfdbcdaad0c66b97534141879ba6128a6a108609d7d68b4c66c69804fd8c989ad", - "votingPower": "2000" - }, - { - "publicKey": "0xa5bf8cec34f820f8f6b8385ade09ab771be1c04500ca912de1b3cb09e43b86e8", - "votingPower": "2000" - }, - { - "publicKey": "0x385e0c5aeca4f54c442b5bf0b2320a79b2a67386df4d5461f3b358f957667480", - "votingPower": "2000" - } - ] - }, - "prefund": [ - { - "address": "0x2454fd9B923cD74d690bf500b4120E2BAEe3781a", - "balance": "1000000000000000000000" - }, - { - "address": "0x46F6a3Fb2F3E897c7A4c3aD1B18D864884cf0FAc", - "balance": "1000000000000000000000" - }, - { - "address": "0xc9dE92071ceadbfFf0ee4d222B7E0F9bF9c8A584", - "balance": "1000000000000000000000" - }, - { - "address": "0x7f44B75d3EE7414c61ac3a255BE1fD0Ec140130D", - "balance": "1000000000000000000000" - }, - { - "address": "0xf496BB9e77c05Ce18452ae6d444D87Cd04EbD4F3", - "balance": "1000000000000000000000" - }, - { - "address": "0x334FF04B5d0E14Abbc1019F62981f7A4eEc5298C", - "balance": "1000000000000000000000" - }, - { - "address": "0x281CBe8Dd1D7B64995133EbA264dfb5b21e9e053", - "balance": "1000000000000000000000" - }, - { - "address": "0xdC151920e1F7CD214a108a3Ad0E6a586d46ca015", - "balance": "1000000000000000000000" - }, - { - "address": "0xE73C02301f1e6877Ad8B5FB7cfbbD51AF97bC0Ca", - "balance": "1000000000000000000000" - }, - { - "address": "0xFdfC974f9A66bd01353f39dfC32E6BaBC6f22ff6", - "balance": "1000000000000000000000" - }, - { - "address": "0xe054635B17c4Bd648b820ecf5Ab832688733AAD1", - "balance": "1000000000000000000000" - }, - { - "address": "0xfD9A0965Db3D6376f498347f4C129bA6c72f9628", - "balance": "1000000000000000000000" - }, - { - "address": "0xEe33d2085E034E5B9D9346C775f10D6ea284D620", - "balance": "1000000000000000000000" - }, - { - "address": "0xE2Ffed765eEc0cc29507Be6328CD7Ac8E110f02A", - "balance": "1000000000000000000000" - }, - { - "address": "0x9ED6CB4C5651d81C50Ea3109D55E87c913cc2Ba7", - "balance": "1000000000000000000000" - }, - { - "address": "0x90A963443ADaBdDB73bCa009795E94f0E7b9b0e9", - "balance": "1000000000000000000000" - }, - { - "address": "0xae7713c841C0E4d96c28479eB1BD3E73A6E3B48E", - "balance": "1000000000000000000000" - }, - { - "address": "0xb4e1373fEbB3bB1750cC2Ee7C4eeaFA113bbCE4f", - "balance": "1000000000000000000000" - }, - { - "address": "0x9d6E79502e3B2b94178830BA6Bb76A7f54f9ff61", - "balance": "1000000000000000000000" - }, - { - "address": "0x30D30792030b8B499760dB18C447aa0d83003932", - "balance": "1000000000000000000000" - }, - { - "address": "0xfc8D6aa4F924C410E0F2449bed4b0CaCfC8f1F1A", - "balance": "1000000000000000000000" - }, - { - "address": "0x2247f8fEF4b001eD8785eb5e292Ff8829D755012", - "balance": "1000000000000000000000" - }, - { - "address": "0xBD62385032AE4c8BF1BA2743A187F4e6c7462061", - "balance": "1000000000000000000000" - }, - { - "address": "0xa3ae2E82b6195b425b8C2Da8D5DB84f7dd1380B6", - "balance": "1000000000000000000000" - }, - { - "address": "0x9965a08ba97ed0332FCbBf8188f9700Aca620860", - "balance": "1000000000000000000000" - } - ] -} \ No newline at end of file diff --git a/assets/devnet/genesis.config.ts b/assets/devnet/genesis.config.ts deleted file mode 100644 index e5da0a8..0000000 --- a/assets/devnet/genesis.config.ts +++ /dev/null @@ -1,138 +0,0 @@ -// Copyright 2026 Circle Internet Group, Inc. All rights reserved. -// -// SPDX-License-Identifier: Apache-2.0 -// -// Licensed under the Apache License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. -// You may obtain a copy of the License at -// -// http://www.apache.org/licenses/LICENSE-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// See the License for the specific language governing permissions and -// limitations under the License. - -import { parseEther } from 'viem' -import { createBuilderContext, buildGenesis, schemaGenesisConfig, GenesisConfig } from '../../scripts/genesis' -import fs from 'fs' -import path from 'path' -import { bigintReplacer, currentTimestamp } from '../../scripts/genesis/types' -import { MnemonicAccountCreator } from '../../scripts/genesis/AccountCreator' - -const build = async () => { - const ctx = await createBuilderContext({ network: 'devnet', chainId: 5042001 }) - const configPath = path.join(ctx.projectRoot, `assets/${ctx.network}/config.json`) - const walletSecretsPath = path.join(ctx.projectRoot, `assets/${ctx.network}/wallet-secrets.json`) - - // Load existing config file for CI - if (fs.existsSync(configPath)) { - const config = schemaGenesisConfig.parse(JSON.parse(fs.readFileSync(configPath, 'utf8'))) - return await buildGenesis(ctx, config) - } - - if (!process.env.ARC_DEVNET_ADMIN_MNEMONIC) { - throw new Error('ARC_DEVNET_ADMIN_MNEMONIC is not set') - } - if (!process.env.ARC_DEVNET_VALIDATOR_MNEMONIC) { - throw new Error('ARC_DEVNET_VALIDATOR_MNEMONIC is not set') - } - const creator = new MnemonicAccountCreator({ - adminMnemonic: process.env.ARC_DEVNET_ADMIN_MNEMONIC, - validatorMnemonic: process.env.ARC_DEVNET_VALIDATOR_MNEMONIC, - }) - const adminPrefund = parseEther('1000') - - const config: GenesisConfig = { - timestamp: currentTimestamp(), - coinbase: '0x65E0a200006D4FF91bD59F9694220dafc49dbBC1', - - NativeFiatToken: { - proxy: { admin: creator.nextAccount('NativeFiatToken.proxyAdmin', adminPrefund) }, - owner: creator.nextAccount('NativeFiatToken.owner', adminPrefund), - pauser: creator.nextAccount('NativeFiatToken.pauser', adminPrefund), - masterMinter: creator.nextAccount('NativeFiatToken.masterMinter', adminPrefund), - rescuer: creator.nextAccount('NativeFiatToken.rescuer', adminPrefund), - blacklister: creator.nextAccount('NativeFiatToken.blacklister', adminPrefund), - minters: [ - { - address: creator.nextAccount('NativeFiatToken.minter', adminPrefund), - allowance: parseEther('1000000'), - }, - ], - }, - - ProtocolConfig: { - proxy: { admin: creator.nextAccount('ProtocolConfig.proxyAdmin', adminPrefund) }, - owner: creator.nextAccount('ProtocolConfig.owner', adminPrefund), - controller: creator.nextAccount('ProtocolConfig.controller', adminPrefund), - pauser: creator.nextAccount('ProtocolConfig.pauser', adminPrefund), - feeParams: { - alpha: 20n, - kRate: 25n, - inverseElasticityMultiplier: 5000n, - minBaseFee: 1n, - maxBaseFee: 1000n, - blockGasLimit: 30_000_000n, - }, - consensusParams: { - timeoutProposeMs: 3000n, - timeoutProposeDeltaMs: 500n, - timeoutPrevoteMs: 1000n, - timeoutPrevoteDeltaMs: 500n, - timeoutPrecommitMs: 1000n, - timeoutPrecommitDeltaMs: 500n, - timeoutRebroadcastMs: 1000n, - targetBlockTimeMs: 500n, - }, - }, - - ValidatorManager: { - proxy: { admin: creator.nextAccount('ValidatorManager.proxyAdmin', adminPrefund) }, - PermissionedValidatorManager: { - proxy: { admin: creator.nextAccount('PermissionedValidatorManager.proxyAdmin', adminPrefund) }, - owner: creator.nextAccount('PermissionedValidatorManager.owner', adminPrefund), - validatorRegisterers: [ - creator.nextAccount('PermissionedValidatorManager.validatorRegisterer1', adminPrefund), - creator.nextAccount('PermissionedValidatorManager.validatorRegisterer2', adminPrefund), - ], - controllers: [ - creator.nextAccount('PermissionedValidatorManager.controller1', adminPrefund), - creator.nextAccount('PermissionedValidatorManager.controller2', adminPrefund), - creator.nextAccount('PermissionedValidatorManager.controller3', adminPrefund), - creator.nextAccount('PermissionedValidatorManager.controller4', adminPrefund), - creator.nextAccount('PermissionedValidatorManager.controller5', adminPrefund), - creator.nextAccount('PermissionedValidatorManager.controller6', adminPrefund), - creator.nextAccount('PermissionedValidatorManager.controller7', adminPrefund), - creator.nextAccount('PermissionedValidatorManager.controller8', adminPrefund), - creator.nextAccount('PermissionedValidatorManager.controller9', adminPrefund), - ], - }, - validators: [ - await creator.nextValidatorKey('validator1', 2000n), - await creator.nextValidatorKey('validator2', 2000n), - await creator.nextValidatorKey('validator3', 2000n), - ], - }, - } - - // Add the prefund accounts, collect from the creator. - config.prefund = creator.getPrefunds() - - // Output secrets for the first time generation - fs.writeFileSync(walletSecretsPath, JSON.stringify(creator.getAdminConfig(), null, 2)) - for (const validator of creator.getValidatorConfig()) { - fs.writeFileSync( - path.join(`assets/${ctx.network}`, `${validator.name}.json`), - JSON.stringify(validator, bigintReplacer, 2), - ) - } - - // Save config to file. Then CI do not required the mnemonic. - fs.writeFileSync(configPath, JSON.stringify(config, bigintReplacer, 2)) - - return await buildGenesis(ctx, config) -} - -export default build diff --git a/assets/devnet/genesis.json b/assets/devnet/genesis.json deleted file mode 100644 index c7716c6..0000000 --- a/assets/devnet/genesis.json +++ /dev/null @@ -1,1531 +0,0 @@ -{ - "config": { - "chainId": 5042001, - "daoForkSupport": false, - "terminalTotalDifficulty": "0x0", - "terminalTotalDifficultyPassed": true, - "homesteadBlock": 0, - "eip150Block": 0, - "eip150Hash": "0x0000000000000000000000000000000000000000000000000000000000000000", - "eip155Block": 0, - "eip158Block": 0, - "byzantiumBlock": 0, - "constantinopleBlock": 0, - "petersburgBlock": 0, - "istanbulBlock": 0, - "muirGlacierBlock": 0, - "berlinBlock": 0, - "londonBlock": 0, - "arrowGlacierBlock": 0, - "grayGlacierBlock": 0, - "shanghaiTime": 0, - "cancunTime": 0, - "pragueTime": 0 - }, - "nonce": "0x0", - "timestamp": "0x68d25e9f", - "extraData": "0x", - "gasLimit": "0x1c9c380", - "difficulty": "0x0", - "mixHash": "0x0000000000000000000000000000000000000000000000000000000000000000", - "coinbase": "0x65E0a200006D4FF91bD59F9694220dafc49dbBC1", - "number": "0x0", - "alloc": { - "0x3600000000000000000000000000000000000000": { - "balance": "0x0", - "nonce": "0x1", - "code": "0x60806040526004361061005a5760003560e01c80635c60da1b116100435780635c60da1b146101315780638f2839701461016f578063f851a440146101af5761005a565b80633659cfe6146100645780634f1ef286146100a4575b6100626101c4565b005b34801561007057600080fd5b506100626004803603602081101561008757600080fd5b503573ffffffffffffffffffffffffffffffffffffffff166101de565b610062600480360360408110156100ba57600080fd5b73ffffffffffffffffffffffffffffffffffffffff82351691908101906040810160208201356401000000008111156100f257600080fd5b82018360208201111561010457600080fd5b8035906020019184600183028401116401000000008311171561012657600080fd5b509092509050610232565b34801561013d57600080fd5b50610146610309565b6040805173ffffffffffffffffffffffffffffffffffffffff9092168252519081900360200190f35b34801561017b57600080fd5b506100626004803603602081101561019257600080fd5b503573ffffffffffffffffffffffffffffffffffffffff16610318565b3480156101bb57600080fd5b50610146610420565b6101cc610430565b6101dc6101d76104c4565b6104e9565b565b6101e661050d565b73ffffffffffffffffffffffffffffffffffffffff163373ffffffffffffffffffffffffffffffffffffffff1614156102275761022281610532565b61022f565b61022f6101c4565b50565b61023a61050d565b73ffffffffffffffffffffffffffffffffffffffff163373ffffffffffffffffffffffffffffffffffffffff1614156102fc5761027683610532565b60003073ffffffffffffffffffffffffffffffffffffffff16348484604051808383808284376040519201945060009350909150508083038185875af1925050503d80600081146102e3576040519150601f19603f3d011682016040523d82523d6000602084013e6102e8565b606091505b50509050806102f657600080fd5b50610304565b6103046101c4565b505050565b60006103136104c4565b905090565b61032061050d565b73ffffffffffffffffffffffffffffffffffffffff163373ffffffffffffffffffffffffffffffffffffffff1614156102275773ffffffffffffffffffffffffffffffffffffffff81166103bf576040517f08c379a00000000000000000000000000000000000000000000000000000000081526004018080602001828103825260368152602001806106606036913960400191505060405180910390fd5b7f7e644d79422f17c01e4894b5f4f588d331ebfa28653d42ae832dc59e38c9798f6103e861050d565b6040805173ffffffffffffffffffffffffffffffffffffffff928316815291841660208301528051918290030190a161022281610587565b600061031361050d565b3b151590565b61043861050d565b73ffffffffffffffffffffffffffffffffffffffff163373ffffffffffffffffffffffffffffffffffffffff1614156104bc576040517f08c379a000000000000000000000000000000000000000000000000000000000815260040180806020018281038252603281526020018061062e6032913960400191505060405180910390fd5b6101dc6101dc565b7f7050c9e0f4ca769c69bd3a8ef740bc37934f8e2c036e5a723fd8ee048ed3f8c35490565b3660008037600080366000845af43d6000803e808015610508573d6000f35b3d6000fd5b7f10d6a54a4754c8869d6886b5f5d7fbfa5b4522237ea5c60d11bc4e7a1ff9390b5490565b61053b816105ab565b6040805173ffffffffffffffffffffffffffffffffffffffff8316815290517fbc7cd75a20ee27fd9adebab32041f755214dbc6bffa90cc0225b39da2e5c2d3b9181900360200190a150565b7f10d6a54a4754c8869d6886b5f5d7fbfa5b4522237ea5c60d11bc4e7a1ff9390b55565b6105b48161042a565b610609576040517f08c379a000000000000000000000000000000000000000000000000000000000815260040180806020018281038252603b815260200180610696603b913960400191505060405180910390fd5b7f7050c9e0f4ca769c69bd3a8ef740bc37934f8e2c036e5a723fd8ee048ed3f8c35556fe43616e6e6f742063616c6c2066616c6c6261636b2066756e6374696f6e2066726f6d207468652070726f78792061646d696e43616e6e6f74206368616e6765207468652061646d696e206f6620612070726f787920746f20746865207a65726f206164647265737343616e6e6f742073657420612070726f787920696d706c656d656e746174696f6e20746f2061206e6f6e2d636f6e74726163742061646472657373a2646970667358221220015908007e367e7f333ec38084b88f027c06160d2c19e5bdd8027e8d06acf8bf64736f6c634300060c0033", - "storage": { - "0x10d6a54a4754c8869d6886b5f5d7fbfa5b4522237ea5c60d11bc4e7a1ff9390b": "0x0000000000000000000000002454fd9b923cd74d690bf500b4120e2baee3781a", - "0x7050c9e0f4ca769c69bd3a8ef740bc37934f8e2c036e5a723fd8ee048ed3f8c3": "0x000000000000000000000000e89332e517aa1239abae680912e1b296232a0bab", - "0x0000000000000000000000000000000000000000000000000000000000000000": "0x00000000000000000000000046f6a3fb2f3e897c7a4c3ad1b18d864884cf0fac", - "0x0000000000000000000000000000000000000000000000000000000000000001": "0x000000000000000000000000c9de92071ceadbfff0ee4d222b7e0f9bf9c8a584", - "0x0000000000000000000000000000000000000000000000000000000000000002": "0x000000000000000000000000334ff04b5d0e14abbc1019f62981f7a4eec5298c", - "0x0000000000000000000000000000000000000000000000000000000000000004": "0x5553444300000000000000000000000000000000000000000000000000000008", - "0x0000000000000000000000000000000000000000000000000000000000000005": "0x5553444300000000000000000000000000000000000000000000000000000008", - "0x0000000000000000000000000000000000000000000000000000000000000006": "0x0000000000000000000000000000000000000000000000000000000000000006", - "0x0000000000000000000000000000000000000000000000000000000000000007": "0x5553440000000000000000000000000000000000000000000000000000000006", - "0x0000000000000000000000000000000000000000000000000000000000000008": "0x0000000000000000000000017f44B75d3EE7414c61ac3a255BE1fD0Ec140130D", - "0x3428ed7cf0f8b087fdf676dc8111e3e8bc1f2395b4f026d9e4b585366b1b5632": "0x0000000000000000000000000000000000000000000000000000000000000001", - "0x9c8817704852488127335f6ef3f832f73593165d1609b637e0a6947ba0476312": "0x00000000000000000000000000000000000000000000d3c21bcecceda1000000", - "0x000000000000000000000000000000000000000000000000000000000000000e": "0x000000000000000000000000f496bb9e77c05ce18452ae6d444d87cd04ebd4f3", - "0x0000000000000000000000000000000000000000000000000000000000000012": "0x0000000000000000000000000000000000000000000000000000000000000003" - } - }, - "0xE89332E517aA1239AbaE680912e1b296232A0bAB": { - "balance": "0x0", - "nonce": "0x1", - "code": "0x608060405234801561001057600080fd5b506004361061038e5760003560e01c806388b7ab63116101de578063bd1024301161010f578063dd62ed3e116100ad578063ef55bec61161007c578063ef55bec61461115b578063f2fde38b146111c7578063f9f92be4146111fa578063fe575a871461122d5761038e565b8063dd62ed3e14611073578063e3ee160e146110ae578063e5a6b10f1461111a578063e94a0102146111225761038e565b8063d505accf116100e9578063d505accf14610f95578063d608ea6414610ff3578063d916948714611063578063dd0743b31461106b5761038e565b8063bd10243014610ea1578063ccd92d3e14610ea9578063cf09299514610eb15761038e565b8063a457c2d71161017c578063aa271e1a11610156578063aa271e1a14610d30578063ad38bf2214610d63578063b2118a8d14610d96578063b7b7289914610dd95761038e565b8063a457c2d714610c8b578063a9059cbb14610cc4578063aa20e1e414610cfd5761038e565b806395d89b41116101b857806395d89b4114610b9b5780639fd0506d14610ba35780639fd5a6cf14610bab578063a0cc6a6814610c835761038e565b806388b7ab6314610a7c5780638a6db9c314610b605780638da5cb5b14610b935761038e565b806339509351116102c3578063554bab3c116102615780637ecebe00116102305780637ecebe0014610a315780637f2eecc314610a645780637fd0991614610a6c5780638456cb5914610a745761038e565b8063554bab3c146109755780635a049a70146109a85780635c975abb146109f657806370a08231146109fe5761038e565b806342966c681161029d57806342966c6814610855578063430239b4146108725780634e44d9561461093457806354fd4d501461096d5761038e565b806339509351146107db5780633f4ba83a1461081457806340c10f191461081c5761038e565b80633092afd5116103305780633357162b1161030a5780633357162b146105ae57806335d99f351461079a5780633644e515146107cb57806338a63183146107d35761038e565b80633092afd51461055557806330adf81f14610588578063313ce567146105905761038e565b80631a8952661161036c5780631a8952661461047757806323b872dd146104ac5780632ab60045146104ef5780632fc81e09146105225761038e565b806306fdde0314610393578063095ea7b31461041057806318160ddd1461045d575b600080fd5b61039b611260565b6040805160208082528351818301528351919283929083019185019080838360005b838110156103d55781810151838201526020016103bd565b50505050905090810190601f1680156104025780820380516001836020036101000a031916815260200191505b509250505060405180910390f35b6104496004803603604081101561042657600080fd5b5073ffffffffffffffffffffffffffffffffffffffff813516906020013561130c565b604080519115158252519081900360200190f35b6104656113ae565b60408051918252519081900360200190f35b6104aa6004803603602081101561048d57600080fd5b503573ffffffffffffffffffffffffffffffffffffffff1661144e565b005b610449600480360360608110156104c257600080fd5b5073ffffffffffffffffffffffffffffffffffffffff81358116916020810135909116906040013561150b565b6104aa6004803603602081101561050557600080fd5b503573ffffffffffffffffffffffffffffffffffffffff16611703565b6104aa6004803603602081101561053857600080fd5b503573ffffffffffffffffffffffffffffffffffffffff16611864565b6104496004803603602081101561056b57600080fd5b503573ffffffffffffffffffffffffffffffffffffffff166118cc565b6104656119c5565b6105986119e9565b6040805160ff9092168252519081900360200190f35b6104aa60048036036101008110156105c557600080fd5b8101906020810181356401000000008111156105e057600080fd5b8201836020820111156105f257600080fd5b8035906020019184600183028401116401000000008311171561061457600080fd5b91908080601f016020809104026020016040519081016040528093929190818152602001838380828437600092019190915250929594936020810193503591505064010000000081111561066757600080fd5b82018360208201111561067957600080fd5b8035906020019184600183028401116401000000008311171561069b57600080fd5b91908080601f01602080910402602001604051908101604052809392919081815260200183838082843760009201919091525092959493602081019350359150506401000000008111156106ee57600080fd5b82018360208201111561070057600080fd5b8035906020019184600183028401116401000000008311171561072257600080fd5b91908080601f0160208091040260200160405190810160405280939291908181526020018383808284376000920191909152509295505050813560ff16925050602081013573ffffffffffffffffffffffffffffffffffffffff908116916040810135821691606082013581169160800135166119f2565b6107a2611d34565b6040805173ffffffffffffffffffffffffffffffffffffffff9092168252519081900360200190f35b610465611d50565b6107a2611d5f565b610449600480360360408110156107f157600080fd5b5073ffffffffffffffffffffffffffffffffffffffff8135169060200135611d7b565b6104aa611e13565b6104496004803603604081101561083257600080fd5b5073ffffffffffffffffffffffffffffffffffffffff8135169060200135611ed6565b6104aa6004803603602081101561086b57600080fd5b5035612324565b6104aa6004803603604081101561088857600080fd5b8101906020810181356401000000008111156108a357600080fd5b8201836020820111156108b557600080fd5b803590602001918460208302840111640100000000831117156108d757600080fd5b9193909290916020810190356401000000008111156108f557600080fd5b82018360208201111561090757600080fd5b8035906020019184600183028401116401000000008311171561092957600080fd5b509092509050612658565b6104496004803603604081101561094a57600080fd5b5073ffffffffffffffffffffffffffffffffffffffff813516906020013561280f565b61039b6129a2565b6104aa6004803603602081101561098b57600080fd5b503573ffffffffffffffffffffffffffffffffffffffff166129d9565b6104aa600480360360a08110156109be57600080fd5b5073ffffffffffffffffffffffffffffffffffffffff8135169060208101359060ff6040820135169060608101359060800135612b40565b610449612bde565b61046560048036036020811015610a1457600080fd5b503573ffffffffffffffffffffffffffffffffffffffff16612bff565b61046560048036036020811015610a4757600080fd5b503573ffffffffffffffffffffffffffffffffffffffff16612c0a565b610465612c32565b6107a2612c56565b6104aa612c6e565b6104aa600480360360e0811015610a9257600080fd5b73ffffffffffffffffffffffffffffffffffffffff823581169260208101359091169160408201359160608101359160808201359160a08101359181019060e0810160c0820135640100000000811115610aeb57600080fd5b820183602082011115610afd57600080fd5b80359060200191846001830284011164010000000083111715610b1f57600080fd5b91908080601f016020809104026020016040519081016040528093929190818152602001838380828437600092019190915250929550612d48945050505050565b61046560048036036020811015610b7657600080fd5b503573ffffffffffffffffffffffffffffffffffffffff16612dea565b6107a2612e12565b61039b612e2e565b6107a2612ea7565b6104aa600480360360a0811015610bc157600080fd5b73ffffffffffffffffffffffffffffffffffffffff823581169260208101359091169160408201359160608101359181019060a081016080820135640100000000811115610c0e57600080fd5b820183602082011115610c2057600080fd5b80359060200191846001830284011164010000000083111715610c4257600080fd5b91908080601f016020809104026020016040519081016040528093929190818152602001838380828437600092019190915250929550612ec3945050505050565b610465612f5a565b61044960048036036040811015610ca157600080fd5b5073ffffffffffffffffffffffffffffffffffffffff8135169060200135612f7e565b61044960048036036040811015610cda57600080fd5b5073ffffffffffffffffffffffffffffffffffffffff8135169060200135613016565b6104aa60048036036020811015610d1357600080fd5b503573ffffffffffffffffffffffffffffffffffffffff166130ae565b61044960048036036020811015610d4657600080fd5b503573ffffffffffffffffffffffffffffffffffffffff16613215565b6104aa60048036036020811015610d7957600080fd5b503573ffffffffffffffffffffffffffffffffffffffff16613240565b6104aa60048036036060811015610dac57600080fd5b5073ffffffffffffffffffffffffffffffffffffffff8135811691602081013590911690604001356133a7565b6104aa60048036036060811015610def57600080fd5b73ffffffffffffffffffffffffffffffffffffffff82351691602081013591810190606081016040820135640100000000811115610e2c57600080fd5b820183602082011115610e3e57600080fd5b80359060200191846001830284011164010000000083111715610e6057600080fd5b91908080601f01602080910402602001604051908101604052809392919081815260200183838082843760009201919091525092955061343d945050505050565b6107a26134d2565b6104656134ee565b6104aa600480360360e0811015610ec757600080fd5b73ffffffffffffffffffffffffffffffffffffffff823581169260208101359091169160408201359160608101359160808201359160a08101359181019060e0810160c0820135640100000000811115610f2057600080fd5b820183602082011115610f3257600080fd5b80359060200191846001830284011164010000000083111715610f5457600080fd5b91908080601f0160208091040260200160405190810160405280939291908181526020018383808284376000920191909152509295506134f7945050505050565b6104aa600480360360e0811015610fab57600080fd5b5073ffffffffffffffffffffffffffffffffffffffff813581169160208101359091169060408101359060608101359060ff6080820135169060a08101359060c00135613590565b6104aa6004803603602081101561100957600080fd5b81019060208101813564010000000081111561102457600080fd5b82018360208201111561103657600080fd5b8035906020019184600183028401116401000000008311171561105857600080fd5b509092509050613629565b610465613712565b6107a2613736565b6104656004803603604081101561108957600080fd5b5073ffffffffffffffffffffffffffffffffffffffff8135811691602001351661374e565b6104aa60048036036101208110156110c557600080fd5b5073ffffffffffffffffffffffffffffffffffffffff813581169160208101359091169060408101359060608101359060808101359060a08101359060ff60c0820135169060e0810135906101000135613786565b61039b61382c565b6104496004803603604081101561113857600080fd5b5073ffffffffffffffffffffffffffffffffffffffff81351690602001356138a5565b6104aa600480360361012081101561117257600080fd5b5073ffffffffffffffffffffffffffffffffffffffff813581169160208101359091169060408101359060608101359060808101359060a08101359060ff60c0820135169060e08101359061010001356138dd565b6104aa600480360360208110156111dd57600080fd5b503573ffffffffffffffffffffffffffffffffffffffff16613978565b6104aa6004803603602081101561121057600080fd5b503573ffffffffffffffffffffffffffffffffffffffff16613acb565b6104496004803603602081101561124357600080fd5b503573ffffffffffffffffffffffffffffffffffffffff16613b88565b6004805460408051602060026001851615610100027fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff0190941693909304601f810184900484028201840190925281815292918301828280156113045780601f106112d957610100808354040283529160200191611304565b820191906000526020600020905b8154815290600101906020018083116112e757829003601f168201915b505050505081565b60015460009074010000000000000000000000000000000000000000900460ff161561139957604080517f08c379a000000000000000000000000000000000000000000000000000000000815260206004820152601060248201527f5061757361626c653a2070617573656400000000000000000000000000000000604482015290519081900360640190fd5b6113a4338484613b93565b5060015b92915050565b60008073180000000000000000000000000000000000000073ffffffffffffffffffffffffffffffffffffffff166318160ddd6040518163ffffffff1660e01b815260040160206040518083038186803b15801561140b57600080fd5b505afa15801561141f573d6000803e3d6000fd5b505050506040513d602081101561143557600080fd5b505190506114488164e8d4a51000613cda565b91505090565b60025473ffffffffffffffffffffffffffffffffffffffff1633146114be576040517f08c379a000000000000000000000000000000000000000000000000000000000815260040180806020018281038252602c8152602001806157e2602c913960400191505060405180910390fd5b6114c781613ced565b60405173ffffffffffffffffffffffffffffffffffffffff8216907f117e3210bb9aa7d9baff172026820255c6f6c30ba8999d1c2fd88e2848137c4e90600090a250565b60015460009074010000000000000000000000000000000000000000900460ff161561159857604080517f08c379a000000000000000000000000000000000000000000000000000000000815260206004820152601060248201527f5061757361626c653a2070617573656400000000000000000000000000000000604482015290519081900360640190fd5b336115a281613cf8565b156115f8576040517f08c379a0000000000000000000000000000000000000000000000000000000008152600401808060200182810382526025815260200180615ab96025913960400191505060405180910390fd5b73ffffffffffffffffffffffffffffffffffffffff85166000908152600a60209081526040808320338452909152902054831115611681576040517f08c379a00000000000000000000000000000000000000000000000000000000081526004018080602001828103825260288152602001806158c96028913960400191505060405180910390fd5b73ffffffffffffffffffffffffffffffffffffffff85166000908152600a602090815260408083203384529091529020546116bc9084613da7565b73ffffffffffffffffffffffffffffffffffffffff86166000908152600a602090815260408083203384529091529020556116f8858585613e1e565b506001949350505050565b60005473ffffffffffffffffffffffffffffffffffffffff16331461178957604080517f08c379a000000000000000000000000000000000000000000000000000000000815260206004820181905260248201527f4f776e61626c653a2063616c6c6572206973206e6f7420746865206f776e6572604482015290519081900360640190fd5b73ffffffffffffffffffffffffffffffffffffffff81166117f5576040517f08c379a000000000000000000000000000000000000000000000000000000000815260040180806020018281038252602a815260200180615740602a913960400191505060405180910390fd5b600e80547fffffffffffffffffffffffff00000000000000000000000000000000000000001673ffffffffffffffffffffffffffffffffffffffff83169081179091556040517fe475e580d85111348e40d8ca33cfdd74c30fe1655c2d8537a13abc10065ffa5a90600090a250565b60125460ff1660011461187657600080fd5b60006118813061410c565b9050801561189457611894308383613e1e565b61189d30614135565b5050601280547fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff00166002179055565b60085460009073ffffffffffffffffffffffffffffffffffffffff16331461193f576040517f08c379a00000000000000000000000000000000000000000000000000000000081526004018080602001828103825260298152602001806157b96029913960400191505060405180910390fd5b73ffffffffffffffffffffffffffffffffffffffff82166000818152600c6020908152604080832080547fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff00169055600d909152808220829055517fe94479a9f7e1952cc78f2d6baab678adc1b772d936c6583def489e524cb666929190a2506001919050565b7f6e71edae12b1b97f4d1f60370fef10105fa2faae0126114a169c64845d6126c981565b60065460ff1681565b60085474010000000000000000000000000000000000000000900460ff1615611a66576040517f08c379a000000000000000000000000000000000000000000000000000000000815260040180806020018281038252602a815260200180615944602a913960400191505060405180910390fd5b73ffffffffffffffffffffffffffffffffffffffff8416611ad2576040517f08c379a000000000000000000000000000000000000000000000000000000000815260040180806020018281038252602f815260200180615855602f913960400191505060405180910390fd5b73ffffffffffffffffffffffffffffffffffffffff8316611b3e576040517f08c379a00000000000000000000000000000000000000000000000000000000081526004018080602001828103825260298152602001806157176029913960400191505060405180910390fd5b73ffffffffffffffffffffffffffffffffffffffff8216611baa576040517f08c379a000000000000000000000000000000000000000000000000000000000815260040180806020018281038252602e8152602001806158f1602e913960400191505060405180910390fd5b73ffffffffffffffffffffffffffffffffffffffff8116611c16576040517f08c379a0000000000000000000000000000000000000000000000000000000008152600401808060200182810382526028815260200180615a316028913960400191505060405180910390fd5b8751611c299060049060208b01906154b0565b508651611c3d9060059060208a01906154b0565b508551611c519060079060208901906154b0565b50600680547fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff001660ff8716179055600880547fffffffffffffffffffffffff000000000000000000000000000000000000000090811673ffffffffffffffffffffffffffffffffffffffff8781169190911790925560018054821686841617905560028054909116918416919091179055611ceb81614140565b5050600880547fffffffffffffffffffffff00ffffffffffffffffffffffffffffffffffffffff1674010000000000000000000000000000000000000000179055505050505050565b60085473ffffffffffffffffffffffffffffffffffffffff1681565b6000611d5a614187565b905090565b600e5473ffffffffffffffffffffffffffffffffffffffff1690565b60015460009074010000000000000000000000000000000000000000900460ff1615611e0857604080517f08c379a000000000000000000000000000000000000000000000000000000000815260206004820152601060248201527f5061757361626c653a2070617573656400000000000000000000000000000000604482015290519081900360640190fd5b6113a433848461427c565b60015473ffffffffffffffffffffffffffffffffffffffff163314611e83576040517f08c379a00000000000000000000000000000000000000000000000000000000081526004018080602001828103825260228152602001806159e56022913960400191505060405180910390fd5b600180547fffffffffffffffffffffff00ffffffffffffffffffffffffffffffffffffffff1690556040517f7805862f689e2f13df9f062ff482ad3ad112aca9e0847911ed832e158c525b3390600090a1565b336000908152600c602052604081205460ff16611f3e576040517f08c379a00000000000000000000000000000000000000000000000000000000081526004018080602001828103825260218152602001806158346021913960400191505060405180910390fd5b33611f4881613cf8565b15611f9e576040517f08c379a0000000000000000000000000000000000000000000000000000000008152600401808060200182810382526025815260200180615ab96025913960400191505060405180910390fd5b60015474010000000000000000000000000000000000000000900460ff161561202857604080517f08c379a000000000000000000000000000000000000000000000000000000000815260206004820152601060248201527f5061757361626c653a2070617573656400000000000000000000000000000000604482015290519081900360640190fd5b73ffffffffffffffffffffffffffffffffffffffff8416612094576040517f08c379a00000000000000000000000000000000000000000000000000000000081526004018080602001828103825260238152602001806156ac6023913960400191505060405180910390fd5b600083116120ed576040517f08c379a000000000000000000000000000000000000000000000000000000000815260040180806020018281038252602981526020018061576a6029913960400191505060405180910390fd5b336000908152600d602052604090205480841115612156576040517f08c379a000000000000000000000000000000000000000000000000000000000815260040180806020018281038252602e8152602001806159b7602e913960400191505060405180910390fd5b336000908152600d6020526040902084820390557318000000000000000000000000000000000000006340c10f19866121948764e8d4a510006142c6565b6040518363ffffffff1660e01b8152600401808373ffffffffffffffffffffffffffffffffffffffff16815260200182815260200192505050602060405180830381600087803b1580156121e757600080fd5b505af11580156121fb573d6000803e3d6000fd5b505050506040513d602081101561221157600080fd5b505161227e57604080517f08c379a000000000000000000000000000000000000000000000000000000000815260206004820152601260248201527f4e6174697665206d696e74206661696c65640000000000000000000000000000604482015290519081900360640190fd5b60408051858152905173ffffffffffffffffffffffffffffffffffffffff87169133917fab8530f87dc9b59234c4623bf917212bb2536d647574c8e7e5da92c2ede0c9f89181900360200190a360408051858152905173ffffffffffffffffffffffffffffffffffffffff8716916000917fddf252ad1be2c89b69c2b068fc378daa952ba7f163c4a11628f55a4df523b3ef9181900360200190a3506001949350505050565b336000908152600c602052604090205460ff1661238c576040517f08c379a00000000000000000000000000000000000000000000000000000000081526004018080602001828103825260218152602001806158346021913960400191505060405180910390fd5b60015474010000000000000000000000000000000000000000900460ff161561241657604080517f08c379a000000000000000000000000000000000000000000000000000000000815260206004820152601060248201527f5061757361626c653a2070617573656400000000000000000000000000000000604482015290519081900360640190fd5b6000811161246f576040517f08c379a00000000000000000000000000000000000000000000000000000000081526004018080602001828103825260298152602001806156836029913960400191505060405180910390fd5b60006124808264e8d4a510006142c6565b905033318111156124dc576040517f08c379a000000000000000000000000000000000000000000000000000000000815260040180806020018281038252602681526020018061580e6026913960400191505060405180910390fd5b604080517f9dc29fac00000000000000000000000000000000000000000000000000000000815233600482015260248101839052905173180000000000000000000000000000000000000091639dc29fac9160448083019260209291908290030181600087803b15801561254f57600080fd5b505af1158015612563573d6000803e3d6000fd5b505050506040513d602081101561257957600080fd5b50516125e657604080517f08c379a000000000000000000000000000000000000000000000000000000000815260206004820152601260248201527f4e6174697665206275726e206661696c65640000000000000000000000000000604482015290519081900360640190fd5b60408051838152905133917fcc16f5dbb4873280815c1ee09dbd06736cffcc184412cf7a71a0fdb75d397ca5919081900360200190a260408051838152905160009133917fddf252ad1be2c89b69c2b068fc378daa952ba7f163c4a11628f55a4df523b3ef9181900360200190a35050565b60125460ff1660021461266a57600080fd5b6126766005838361552e565b5060005b838110156127b8576003600086868481811061269257fe5b6020908102929092013573ffffffffffffffffffffffffffffffffffffffff168352508101919091526040016000205460ff1661271a576040517f08c379a000000000000000000000000000000000000000000000000000000000815260040180806020018281038252603d8152602001806155d0603d913960400191505060405180910390fd5b61274b85858381811061272957fe5b9050602002013573ffffffffffffffffffffffffffffffffffffffff16614135565b6003600086868481811061275b57fe5b6020908102929092013573ffffffffffffffffffffffffffffffffffffffff1683525081019190915260400160002080547fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff0016905560010161267a565b506127c230614135565b505030600090815260036020819052604090912080547fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff009081169091556012805490911690911790555050565b60015460009074010000000000000000000000000000000000000000900460ff161561289c57604080517f08c379a000000000000000000000000000000000000000000000000000000000815260206004820152601060248201527f5061757361626c653a2070617573656400000000000000000000000000000000604482015290519081900360640190fd5b60085473ffffffffffffffffffffffffffffffffffffffff16331461290c576040517f08c379a00000000000000000000000000000000000000000000000000000000081526004018080602001828103825260298152602001806157b96029913960400191505060405180910390fd5b73ffffffffffffffffffffffffffffffffffffffff83166000818152600c6020908152604080832080547fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff00166001179055600d825291829020859055815185815291517f46980fca912ef9bcdbd36877427b6b90e860769f604e89c0e67720cece530d209281900390910190a250600192915050565b60408051808201909152600181527f3200000000000000000000000000000000000000000000000000000000000000602082015290565b60005473ffffffffffffffffffffffffffffffffffffffff163314612a5f57604080517f08c379a000000000000000000000000000000000000000000000000000000000815260206004820181905260248201527f4f776e61626c653a2063616c6c6572206973206e6f7420746865206f776e6572604482015290519081900360640190fd5b73ffffffffffffffffffffffffffffffffffffffff8116612acb576040517f08c379a00000000000000000000000000000000000000000000000000000000081526004018080602001828103825260288152602001806156306028913960400191505060405180910390fd5b600180547fffffffffffffffffffffffff00000000000000000000000000000000000000001673ffffffffffffffffffffffffffffffffffffffff83811691909117918290556040519116907fb80482a293ca2e013eda8683c9bd7fc8347cfdaeea5ede58cba46df502c2a60490600090a250565b60015474010000000000000000000000000000000000000000900460ff1615612bca57604080517f08c379a000000000000000000000000000000000000000000000000000000000815260206004820152601060248201527f5061757361626c653a2070617573656400000000000000000000000000000000604482015290519081900360640190fd5b612bd785858585856142d2565b5050505050565b60015474010000000000000000000000000000000000000000900460ff1681565b60006113a88261410c565b73ffffffffffffffffffffffffffffffffffffffff1660009081526011602052604090205490565b7fd099cc98ef71107a616c4f0f941f04c322d8e254fe26b3c6668db87aae413de881565b73180000000000000000000000000000000000000081565b60015473ffffffffffffffffffffffffffffffffffffffff163314612cde576040517f08c379a00000000000000000000000000000000000000000000000000000000081526004018080602001828103825260228152602001806159e56022913960400191505060405180910390fd5b600180547fffffffffffffffffffffff00ffffffffffffffffffffffffffffffffffffffff16740100000000000000000000000000000000000000001790556040517f6985a02210a168e66602d3235cb6db0e70f92b3ba4d376a33c0f3d9434bff62590600090a1565b60015474010000000000000000000000000000000000000000900460ff1615612dd257604080517f08c379a000000000000000000000000000000000000000000000000000000000815260206004820152601060248201527f5061757361626c653a2070617573656400000000000000000000000000000000604482015290519081900360640190fd5b612de187878787878787614312565b50505050505050565b73ffffffffffffffffffffffffffffffffffffffff166000908152600d602052604090205490565b60005473ffffffffffffffffffffffffffffffffffffffff1690565b6005805460408051602060026001851615610100027fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff0190941693909304601f810184900484028201840190925281815292918301828280156113045780601f106112d957610100808354040283529160200191611304565b60015473ffffffffffffffffffffffffffffffffffffffff1681565b60015474010000000000000000000000000000000000000000900460ff1615612f4d57604080517f08c379a000000000000000000000000000000000000000000000000000000000815260206004820152601060248201527f5061757361626c653a2070617573656400000000000000000000000000000000604482015290519081900360640190fd5b612bd78585858585614433565b7f7c7c6cdb67a18743f49ec6fa9b35f50d52ed05cbed4cc592e13b44501c1a226781565b60015460009074010000000000000000000000000000000000000000900460ff161561300b57604080517f08c379a000000000000000000000000000000000000000000000000000000000815260206004820152601060248201527f5061757361626c653a2070617573656400000000000000000000000000000000604482015290519081900360640190fd5b6113a43384846146f7565b60015460009074010000000000000000000000000000000000000000900460ff16156130a357604080517f08c379a000000000000000000000000000000000000000000000000000000000815260206004820152601060248201527f5061757361626c653a2070617573656400000000000000000000000000000000604482015290519081900360640190fd5b6113a4338484613e1e565b60005473ffffffffffffffffffffffffffffffffffffffff16331461313457604080517f08c379a000000000000000000000000000000000000000000000000000000000815260206004820181905260248201527f4f776e61626c653a2063616c6c6572206973206e6f7420746865206f776e6572604482015290519081900360640190fd5b73ffffffffffffffffffffffffffffffffffffffff81166131a0576040517f08c379a000000000000000000000000000000000000000000000000000000000815260040180806020018281038252602f815260200180615855602f913960400191505060405180910390fd5b600880547fffffffffffffffffffffffff00000000000000000000000000000000000000001673ffffffffffffffffffffffffffffffffffffffff83811691909117918290556040519116907fdb66dfa9c6b8f5226fe9aac7e51897ae8ee94ac31dc70bb6c9900b2574b707e690600090a250565b73ffffffffffffffffffffffffffffffffffffffff166000908152600c602052604090205460ff1690565b60005473ffffffffffffffffffffffffffffffffffffffff1633146132c657604080517f08c379a000000000000000000000000000000000000000000000000000000000815260206004820181905260248201527f4f776e61626c653a2063616c6c6572206973206e6f7420746865206f776e6572604482015290519081900360640190fd5b73ffffffffffffffffffffffffffffffffffffffff8116613332576040517f08c379a0000000000000000000000000000000000000000000000000000000008152600401808060200182810382526032815260200180615a876032913960400191505060405180910390fd5b600280547fffffffffffffffffffffffff00000000000000000000000000000000000000001673ffffffffffffffffffffffffffffffffffffffff83811691909117918290556040519116907fc67398012c111ce95ecb7429b933096c977380ee6c421175a71a4a4c6c88c06e90600090a250565b600e5473ffffffffffffffffffffffffffffffffffffffff163314613417576040517f08c379a00000000000000000000000000000000000000000000000000000000081526004018080602001828103825260248152602001806158846024913960400191505060405180910390fd5b61343873ffffffffffffffffffffffffffffffffffffffff84168383614753565b505050565b60015474010000000000000000000000000000000000000000900460ff16156134c757604080517f08c379a000000000000000000000000000000000000000000000000000000000815260206004820152601060248201527f5061757361626c653a2070617573656400000000000000000000000000000000604482015290519081900360640190fd5b6134388383836147e0565b60025473ffffffffffffffffffffffffffffffffffffffff1681565b64e8d4a5100081565b60015474010000000000000000000000000000000000000000900460ff161561358157604080517f08c379a000000000000000000000000000000000000000000000000000000000815260206004820152601060248201527f5061757361626c653a2070617573656400000000000000000000000000000000604482015290519081900360640190fd5b612de1878787878787876148ea565b60015474010000000000000000000000000000000000000000900460ff161561361a57604080517f08c379a000000000000000000000000000000000000000000000000000000000815260206004820152601060248201527f5061757361626c653a2070617573656400000000000000000000000000000000604482015290519081900360640190fd5b612de187878787878787614988565b60085474010000000000000000000000000000000000000000900460ff168015613656575060125460ff16155b61365f57600080fd5b61366b6004838361552e565b506136e082828080601f0160208091040260200160405190810160405280939291908181526020018383808284376000920191909152505060408051808201909152600181527f3200000000000000000000000000000000000000000000000000000000000000602082015291506149ca9050565b600f555050601280547fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff00166001179055565b7f158b0a9edf7a828aad02f63cd515c68ef2f50ba807396f6d12842833a159742981565b73180000000000000000000000000000000000000181565b73ffffffffffffffffffffffffffffffffffffffff9182166000908152600a6020908152604080832093909416825291909152205490565b60015474010000000000000000000000000000000000000000900460ff161561381057604080517f08c379a000000000000000000000000000000000000000000000000000000000815260206004820152601060248201527f5061757361626c653a2070617573656400000000000000000000000000000000604482015290519081900360640190fd5b6138218989898989898989896149e0565b505050505050505050565b6007805460408051602060026001851615610100027fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff0190941693909304601f810184900484028201840190925281815292918301828280156113045780601f106112d957610100808354040283529160200191611304565b73ffffffffffffffffffffffffffffffffffffffff919091166000908152601060209081526040808320938352929052205460ff1690565b60015474010000000000000000000000000000000000000000900460ff161561396757604080517f08c379a000000000000000000000000000000000000000000000000000000000815260206004820152601060248201527f5061757361626c653a2070617573656400000000000000000000000000000000604482015290519081900360640190fd5b613821898989898989898989614a24565b60005473ffffffffffffffffffffffffffffffffffffffff1633146139fe57604080517f08c379a000000000000000000000000000000000000000000000000000000000815260206004820181905260248201527f4f776e61626c653a2063616c6c6572206973206e6f7420746865206f776e6572604482015290519081900360640190fd5b73ffffffffffffffffffffffffffffffffffffffff8116613a6a576040517f08c379a00000000000000000000000000000000000000000000000000000000081526004018080602001828103825260268152602001806156cf6026913960400191505060405180910390fd5b6000546040805173ffffffffffffffffffffffffffffffffffffffff9283168152918316602083015280517f8be0079c531659141344cd1fd0a4f28419497f9722a3daafe3b4186f6b6457e09281900390910190a1613ac881614140565b50565b60025473ffffffffffffffffffffffffffffffffffffffff163314613b3b576040517f08c379a000000000000000000000000000000000000000000000000000000000815260040180806020018281038252602c8152602001806157e2602c913960400191505060405180910390fd5b613b4481614135565b60405173ffffffffffffffffffffffffffffffffffffffff8216907fffa4e6181777692565cf28528fc88fd1516ea86b56da075235fa575af6a4b85590600090a250565b60006113a882613cf8565b73ffffffffffffffffffffffffffffffffffffffff8316613bff576040517f08c379a00000000000000000000000000000000000000000000000000000000081526004018080602001828103825260248152602001806159936024913960400191505060405180910390fd5b73ffffffffffffffffffffffffffffffffffffffff8216613c6b576040517f08c379a00000000000000000000000000000000000000000000000000000000081526004018080602001828103825260228152602001806156f56022913960400191505060405180910390fd5b73ffffffffffffffffffffffffffffffffffffffff8084166000818152600a6020908152604080832094871680845294825291829020859055815185815291517f8c5be1e5ebec7d5bd14f71427d1e84f3dd0314c0f7b2291e5b200ac8c7c3b9259281900390910190a3505050565b6000613ce68383614a68565b9392505050565b613ac8816000614ae9565b600073180000000000000000000000000000000000000173ffffffffffffffffffffffffffffffffffffffff16638e204c43836040518263ffffffff1660e01b8152600401808273ffffffffffffffffffffffffffffffffffffffff16815260200191505060206040518083038186803b158015613d7557600080fd5b505afa158015613d89573d6000803e3d6000fd5b505050506040513d6020811015613d9f57600080fd5b505192915050565b600082821115613e1857604080517f08c379a000000000000000000000000000000000000000000000000000000000815260206004820152601e60248201527f536166654d6174683a207375627472616374696f6e206f766572666c6f770000604482015290519081900360640190fd5b50900390565b73ffffffffffffffffffffffffffffffffffffffff8316613e8a576040517f08c379a000000000000000000000000000000000000000000000000000000000815260040180806020018281038252602581526020018061596e6025913960400191505060405180910390fd5b73ffffffffffffffffffffffffffffffffffffffff8216613ef6576040517f08c379a000000000000000000000000000000000000000000000000000000000815260040180806020018281038252602381526020018061560d6023913960400191505060405180910390fd5b6000613f078264e8d4a510006142c6565b90508373ffffffffffffffffffffffffffffffffffffffff1631811115613f79576040517f08c379a00000000000000000000000000000000000000000000000000000000081526004018080602001828103825260268152602001806157936026913960400191505060405180910390fd5b604080517fbeabacc800000000000000000000000000000000000000000000000000000000815273ffffffffffffffffffffffffffffffffffffffff8681166004830152851660248201526044810183905290517318000000000000000000000000000000000000009163beabacc89160648083019260209291908290030181600087803b15801561400a57600080fd5b505af115801561401e573d6000803e3d6000fd5b505050506040513d602081101561403457600080fd5b50516140a157604080517f08c379a000000000000000000000000000000000000000000000000000000000815260206004820152601660248201527f4e6174697665207472616e73666572206661696c656400000000000000000000604482015290519081900360640190fd5b8273ffffffffffffffffffffffffffffffffffffffff168473ffffffffffffffffffffffffffffffffffffffff167fddf252ad1be2c89b69c2b068fc378daa952ba7f163c4a11628f55a4df523b3ef846040518082815260200191505060405180910390a350505050565b600073ffffffffffffffffffffffffffffffffffffffff821631613ce68164e8d4a51000613cda565b613ac8816001614ae9565b600080547fffffffffffffffffffffffff00000000000000000000000000000000000000001673ffffffffffffffffffffffffffffffffffffffff92909216919091179055565b6004805460408051602060026001851615610100027fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff0190941693909304601f8101849004840282018401909252818152600093611d5a93919290918301828280156142345780601f1061420957610100808354040283529160200191614234565b820191906000526020600020905b81548152906001019060200180831161421757829003601f168201915b50505050506040518060400160405280600181526020017f3200000000000000000000000000000000000000000000000000000000000000815250614277614c56565b614c5a565b73ffffffffffffffffffffffffffffffffffffffff8084166000908152600a602090815260408083209386168352929052205461343890849084906142c19085614cce565b613b93565b6000613ce68383614d42565b612bd78585848487604051602001808481526020018381526020018260ff1660f81b815260010193505050506040516020818303038152906040526147e0565b73ffffffffffffffffffffffffffffffffffffffff86163314614380576040517f08c379a000000000000000000000000000000000000000000000000000000000815260040180806020018281038252602581526020018061591f6025913960400191505060405180910390fd5b61438c87838686614db5565b604080517fd099cc98ef71107a616c4f0f941f04c322d8e254fe26b3c6668db87aae413de860208083019190915273ffffffffffffffffffffffffffffffffffffffff808b1683850152891660608301526080820188905260a0820187905260c0820186905260e080830186905283518084039091018152610100909201909252805191012061441e90889083614e6f565b6144288783614fed565b612de1878787613e1e565b7fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff8214806144615750428210155b6144cc57604080517f08c379a000000000000000000000000000000000000000000000000000000000815260206004820152601e60248201527f46696174546f6b656e56323a207065726d697420697320657870697265640000604482015290519081900360640190fd5b60006145746144d9614187565b73ffffffffffffffffffffffffffffffffffffffff80891660008181526011602090815260409182902080546001810190915582517f6e71edae12b1b97f4d1f60370fef10105fa2faae0126114a169c64845d6126c98184015280840194909452938b166060840152608083018a905260a083019390935260c08083018990528151808403909101815260e090920190528051910120615072565b905073fcff98b65f9ea559ec0df36f4072c7e3be0520df636ccea6528783856040518463ffffffff1660e01b8152600401808473ffffffffffffffffffffffffffffffffffffffff16815260200183815260200180602001828103825283818151815260200191508051906020019080838360005b838110156146015781810151838201526020016145e9565b50505050905090810190601f16801561462e5780820380516001836020036101000a031916815260200191505b5094505050505060206040518083038186803b15801561464d57600080fd5b505af4158015614661573d6000803e3d6000fd5b505050506040513d602081101561467757600080fd5b50516146e457604080517f08c379a000000000000000000000000000000000000000000000000000000000815260206004820152601a60248201527f454950323631323a20696e76616c6964207369676e6174757265000000000000604482015290519081900360640190fd5b6146ef868686613b93565b505050505050565b61343883836142c184604051806060016040528060258152602001615b036025913973ffffffffffffffffffffffffffffffffffffffff808a166000908152600a60209081526040808320938c168352929052205491906150ac565b6040805173ffffffffffffffffffffffffffffffffffffffff8416602482015260448082018490528251808303909101815260649091019091526020810180517bffffffffffffffffffffffffffffffffffffffffffffffffffffffff167fa9059cbb0000000000000000000000000000000000000000000000000000000017905261343890849061515d565b6147ea8383615235565b614864837f158b0a9edf7a828aad02f63cd515c68ef2f50ba807396f6d12842833a159742960001b8585604051602001808481526020018373ffffffffffffffffffffffffffffffffffffffff16815260200182815260200193505050506040516020818303038152906040528051906020012083614e6f565b73ffffffffffffffffffffffffffffffffffffffff8316600081815260106020908152604080832086845290915280822080547fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff00166001179055518492917f1cdd46ff242716cdaa72d159d339a485b3438398348d68f09d7c8c0a59353d8191a3505050565b6148f687838686614db5565b604080517f7c7c6cdb67a18743f49ec6fa9b35f50d52ed05cbed4cc592e13b44501c1a226760208083019190915273ffffffffffffffffffffffffffffffffffffffff808b1683850152891660608301526080820188905260a0820187905260c0820186905260e080830186905283518084039091018152610100909201909252805191012061441e90889083614e6f565b612de187878787868689604051602001808481526020018381526020018260ff1660f81b81526001019350505050604051602081830303815290604052614433565b6000466149d8848483614c5a565b949350505050565b61382189898989898988888b604051602001808481526020018381526020018260ff1660f81b815260010193505050506040516020818303038152906040526148ea565b61382189898989898988888b604051602001808481526020018381526020018260ff1660f81b81526001019350505050604051602081830303815290604052614312565b6000808211614ad857604080517f08c379a000000000000000000000000000000000000000000000000000000000815260206004820152601a60248201527f536166654d6174683a206469766973696f6e206279207a65726f000000000000604482015290519081900360640190fd5b818381614ae157fe5b049392505050565b8015614ba357604080517fe5c7160b00000000000000000000000000000000000000000000000000000000815273ffffffffffffffffffffffffffffffffffffffff8416600482015290517318000000000000000000000000000000000000019163e5c7160b9160248083019260209291908290030181600087803b158015614b7157600080fd5b505af1158015614b85573d6000803e3d6000fd5b505050506040513d6020811015614b9b57600080fd5b50614c529050565b604080517f31b2302000000000000000000000000000000000000000000000000000000000815273ffffffffffffffffffffffffffffffffffffffff841660048201529051731800000000000000000000000000000000000001916331b230209160248083019260209291908290030181600087803b158015614c2557600080fd5b505af1158015614c39573d6000803e3d6000fd5b505050506040513d6020811015614c4f57600080fd5b50505b5050565b4690565b8251602093840120825192840192909220604080517f8b73c3c69bb8fe3d512ecc4cf759cc79239f7b179b0ffacaa9a75d522b39400f8187015280820194909452606084019190915260808301919091523060a0808401919091528151808403909101815260c09092019052805191012090565b600082820183811015613ce657604080517f08c379a000000000000000000000000000000000000000000000000000000000815260206004820152601b60248201527f536166654d6174683a206164646974696f6e206f766572666c6f770000000000604482015290519081900360640190fd5b600082614d51575060006113a8565b82820282848281614d5e57fe5b0414613ce6576040517f08c379a00000000000000000000000000000000000000000000000000000000081526004018080602001828103825260218152602001806158a86021913960400191505060405180910390fd5b814211614e0d576040517f08c379a000000000000000000000000000000000000000000000000000000000815260040180806020018281038252602b815260200180615658602b913960400191505060405180910390fd5b804210614e65576040517f08c379a0000000000000000000000000000000000000000000000000000000008152600401808060200182810382526025815260200180615ade6025913960400191505060405180910390fd5b614c4f8484615235565b73fcff98b65f9ea559ec0df36f4072c7e3be0520df636ccea65284614e9b614e95614187565b86615072565b846040518463ffffffff1660e01b8152600401808473ffffffffffffffffffffffffffffffffffffffff16815260200183815260200180602001828103825283818151815260200191508051906020019080838360005b83811015614f0a578181015183820152602001614ef2565b50505050905090810190601f168015614f375780820380516001836020036101000a031916815260200191505b5094505050505060206040518083038186803b158015614f5657600080fd5b505af4158015614f6a573d6000803e3d6000fd5b505050506040513d6020811015614f8057600080fd5b505161343857604080517f08c379a000000000000000000000000000000000000000000000000000000000815260206004820152601e60248201527f46696174546f6b656e56323a20696e76616c6964207369676e61747572650000604482015290519081900360640190fd5b73ffffffffffffffffffffffffffffffffffffffff8216600081815260106020908152604080832085845290915280822080547fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff00166001179055518392917f98de503528ee59b575ef0c0a2576a82497bfc029a5685b209e9ec333479b10a591a35050565b6040517f19010000000000000000000000000000000000000000000000000000000000008152600281019290925260228201526042902090565b60008184841115615155576040517f08c379a00000000000000000000000000000000000000000000000000000000081526004018080602001828103825283818151815260200191508051906020019080838360005b8381101561511a578181015183820152602001615102565b50505050905090810190601f1680156151475780820380516001836020036101000a031916815260200191505b509250505060405180910390fd5b505050900390565b60606151bf826040518060400160405280602081526020017f5361666545524332303a206c6f772d6c6576656c2063616c6c206661696c65648152508573ffffffffffffffffffffffffffffffffffffffff166152bf9092919063ffffffff16565b805190915015613438578080602001905160208110156151de57600080fd5b5051613438576040517f08c379a000000000000000000000000000000000000000000000000000000000815260040180806020018281038252602a815260200180615a07602a913960400191505060405180910390fd5b73ffffffffffffffffffffffffffffffffffffffff8216600090815260106020908152604080832084845290915290205460ff1615614c52576040517f08c379a000000000000000000000000000000000000000000000000000000000815260040180806020018281038252602e815260200180615a59602e913960400191505060405180910390fd5b60606149d88484600085856152d38561542a565b61533e57604080517f08c379a000000000000000000000000000000000000000000000000000000000815260206004820152601d60248201527f416464726573733a2063616c6c20746f206e6f6e2d636f6e7472616374000000604482015290519081900360640190fd5b600060608673ffffffffffffffffffffffffffffffffffffffff1685876040518082805190602001908083835b602083106153a857805182527fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffe0909201916020918201910161536b565b6001836020036101000a03801982511681845116808217855250505050505090500191505060006040518083038185875af1925050503d806000811461540a576040519150601f19603f3d011682016040523d82523d6000602084013e61540f565b606091505b509150915061541f828286615430565b979650505050505050565b3b151590565b6060831561543f575081613ce6565b82511561544f5782518084602001fd5b6040517f08c379a000000000000000000000000000000000000000000000000000000000815260206004820181815284516024840152845185939192839260440191908501908083836000831561511a578181015183820152602001615102565b828054600181600116156101000203166002900490600052602060002090601f016020900481019282601f106154f157805160ff191683800117855561551e565b8280016001018555821561551e579182015b8281111561551e578251825591602001919060010190615503565b5061552a9291506155ba565b5090565b828054600181600116156101000203166002900490600052602060002090601f016020900481019282601f1061558d578280017fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff0082351617855561551e565b8280016001018555821561551e579182015b8281111561551e57823582559160200191906001019061559f565b5b8082111561552a57600081556001016155bb56fe46696174546f6b656e56325f323a20426c61636b6c697374696e672070726576696f75736c7920756e626c61636b6c6973746564206163636f756e742145524332303a207472616e7366657220746f20746865207a65726f20616464726573735061757361626c653a206e65772070617573657220697320746865207a65726f206164647265737346696174546f6b656e56323a20617574686f72697a6174696f6e206973206e6f74207965742076616c696446696174546f6b656e3a206275726e20616d6f756e74206e6f742067726561746572207468616e203046696174546f6b656e3a206d696e7420746f20746865207a65726f20616464726573734f776e61626c653a206e6577206f776e657220697320746865207a65726f206164647265737345524332303a20617070726f766520746f20746865207a65726f206164647265737346696174546f6b656e3a206e65772070617573657220697320746865207a65726f2061646472657373526573637561626c653a206e6577207265736375657220697320746865207a65726f206164647265737346696174546f6b656e3a206d696e7420616d6f756e74206e6f742067726561746572207468616e203045524332303a207472616e7366657220616d6f756e7420657863656564732062616c616e636546696174546f6b656e3a2063616c6c6572206973206e6f7420746865206d61737465724d696e746572426c61636b6c69737461626c653a2063616c6c6572206973206e6f742074686520626c61636b6c697374657246696174546f6b656e3a206275726e20616d6f756e7420657863656564732062616c616e636546696174546f6b656e3a2063616c6c6572206973206e6f742061206d696e74657246696174546f6b656e3a206e6577206d61737465724d696e74657220697320746865207a65726f2061646472657373526573637561626c653a2063616c6c6572206973206e6f74207468652072657363756572536166654d6174683a206d756c7469706c69636174696f6e206f766572666c6f7745524332303a207472616e7366657220616d6f756e74206578636565647320616c6c6f77616e636546696174546f6b656e3a206e657720626c61636b6c697374657220697320746865207a65726f206164647265737346696174546f6b656e56323a2063616c6c6572206d7573742062652074686520706179656546696174546f6b656e3a20636f6e747261637420697320616c726561647920696e697469616c697a656445524332303a207472616e736665722066726f6d20746865207a65726f206164647265737345524332303a20617070726f76652066726f6d20746865207a65726f206164647265737346696174546f6b656e3a206d696e7420616d6f756e742065786365656473206d696e746572416c6c6f77616e63655061757361626c653a2063616c6c6572206973206e6f7420746865207061757365725361666545524332303a204552433230206f7065726174696f6e20646964206e6f74207375636365656446696174546f6b656e3a206e6577206f776e657220697320746865207a65726f206164647265737346696174546f6b656e56323a20617574686f72697a6174696f6e2069732075736564206f722063616e63656c6564426c61636b6c69737461626c653a206e657720626c61636b6c697374657220697320746865207a65726f2061646472657373426c61636b6c69737461626c653a206163636f756e7420697320626c61636b6c697374656446696174546f6b656e56323a20617574686f72697a6174696f6e206973206578706972656445524332303a2064656372656173656420616c6c6f77616e63652062656c6f77207a65726fa26469706673582212203680055bb3d452c16b52648ad61ea1fe220d3a7d3cd9660344db1c04b6f7404a64736f6c634300060c0033" - }, - "0xfcFf98B65F9ea559EC0df36F4072C7E3BE0520Df": { - "balance": "0x0", - "nonce": "0x1", - "code": "0x73fcff98b65f9ea559ec0df36f4072c7e3be0520df30146080604052600436106100355760003560e01c80636ccea6521461003a575b600080fd5b6101026004803603606081101561005057600080fd5b73ffffffffffffffffffffffffffffffffffffffff8235169160208101359181019060608101604082013564010000000081111561008d57600080fd5b82018360208201111561009f57600080fd5b803590602001918460018302840111640100000000831117156100c157600080fd5b91908080601f016020809104026020016040519081016040528093929190818152602001838380828437600092019190915250929550610116945050505050565b604080519115158252519081900360200190f35b600061012184610179565b610164578373ffffffffffffffffffffffffffffffffffffffff16610146848461017f565b73ffffffffffffffffffffffffffffffffffffffff16149050610172565b61016f848484610203565b90505b9392505050565b3b151590565b600081516041146101db576040517f08c379a00000000000000000000000000000000000000000000000000000000081526004018080602001828103825260238152602001806106296023913960400191505060405180910390fd5b60208201516040830151606084015160001a6101f98682858561042d565b9695505050505050565b60008060608573ffffffffffffffffffffffffffffffffffffffff16631626ba7e60e01b86866040516024018083815260200180602001828103825283818151815260200191508051906020019080838360005b8381101561026f578181015183820152602001610257565b50505050905090810190601f16801561029c5780820380516001836020036101000a031916815260200191505b50604080517fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffe08184030181529181526020820180517bffffffffffffffffffffffffffffffffffffffffffffffffffffffff167fffffffff000000000000000000000000000000000000000000000000000000009098169790971787525181519196909550859450925090508083835b6020831061036957805182527fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffe0909201916020918201910161032c565b6001836020036101000a038019825116818451168082178552505050505050905001915050600060405180830381855afa9150503d80600081146103c9576040519150601f19603f3d011682016040523d82523d6000602084013e6103ce565b606091505b50915091508180156103e257506020815110155b80156101f9575080517f1626ba7e00000000000000000000000000000000000000000000000000000000906020808401919081101561042057600080fd5b5051149695505050505050565b60007f7fffffffffffffffffffffffffffffff5d576e7357a4501ddfe92f46681b20a08211156104a8576040517f08c379a00000000000000000000000000000000000000000000000000000000081526004018080602001828103825260268152602001806106726026913960400191505060405180910390fd5b8360ff16601b141580156104c057508360ff16601c14155b15610516576040517f08c379a000000000000000000000000000000000000000000000000000000000815260040180806020018281038252602681526020018061064c6026913960400191505060405180910390fd5b600060018686868660405160008152602001604052604051808581526020018460ff1681526020018381526020018281526020019450505050506020604051602081039080840390855afa158015610572573d6000803e3d6000fd5b50506040517fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffe0015191505073ffffffffffffffffffffffffffffffffffffffff811661061f57604080517f08c379a000000000000000000000000000000000000000000000000000000000815260206004820152601c60248201527f45435265636f7665723a20696e76616c6964207369676e617475726500000000604482015290519081900360640190fd5b9594505050505056fe45435265636f7665723a20696e76616c6964207369676e6174757265206c656e67746845435265636f7665723a20696e76616c6964207369676e6174757265202776272076616c756545435265636f7665723a20696e76616c6964207369676e6174757265202773272076616c7565a2646970667358221220289705d6ae8701c9ed586541fa0ba342f754518aa4f0e59dbbda380bc9f2323964736f6c634300060c0033" - }, - "0xF511eC138a4868BE5924b5B1d2bC148154DD1Bbf": { - "balance": "0x0", - "nonce": "0x1", - "code": "0x608060405234801561000f575f5ffd5b50600436106100fb575f3560e01c80638456cb59116100935780639fd0506d116100635780639fd0506d146102fe578063e30c397814610311578063f2fde38b14610319578063f77c47911461032c575f5ffd5b80638456cb59146101bf5780638da5cb5b146101c75780638e207848146101cf5780639242164f146101e2575f5ffd5b8063554bab3c116100ce578063554bab3c146101785780635c975abb1461018b578063715018a6146101af57806379ba5097146101b7575f5ffd5b806306cb5b66146100ff5780633466e3d4146101145780633f4ba83a1461012757806341a56c591461012f575b5f5ffd5b61011261010d366004610c1a565b61033e565b005b610112610122366004610c1a565b6103fb565b61011261056b565b7f668f09ce856848ead6cb1ddee963f15ef833cea8958030868f867aec84385204546001600160a01b03165b6040516001600160a01b0390911681526020015b60405180910390f35b610112610186366004610c1a565b6105cc565b60015461019f90600160a01b900460ff1681565b604051901515815260200161016f565b610112610684565b610112610697565b6101126106df565b61015b610746565b6101126101dd366004610c47565b61077a565b6102986040805160c0810182525f80825260208201819052918101829052606081018290526080810182905260a08101829052907f668f09ce856848ead6cb1ddee963f15ef833cea8958030868f867aec843852006040805160c08101825282546001600160401b03808216835268010000000000000000820481166020840152600160801b9091041691810191909152600182015460608201526002820154608082015260039091015460a082015292915050565b60405161016f91905f60c0820190506001600160401b0383511682526001600160401b0360208401511660208301526001600160401b036040840151166040830152606083015160608301526080830151608083015260a083015160a083015292915050565b60015461015b906001600160a01b031681565b61015b610a8f565b610112610327366004610c1a565b610ab7565b5f5461015b906001600160a01b031681565b610346610b3c565b6001600160a01b0381166103b45760405162461bcd60e51b815260206004820152602a60248201527f436f6e74726f6c6c65723a206e657720636f6e74726f6c6c6572206973207a65604482015269726f206164647265737360b01b60648201526084015b60405180910390fd5b5f80546001600160a01b0319166001600160a01b038316908117825560405190917f1304018cfe79741dcf02ba6b61d39cc4757d59395d03224d9925c7aa8300214691a250565b5f546001600160a01b031633146104245760405162461bcd60e51b81526004016103ab90610c60565b600154600160a01b900460ff16156104715760405162461bcd60e51b815260206004820152601060248201526f14185d5cd8589b194e881c185d5cd95960821b60448201526064016103ab565b6001600160a01b0381166104df5760405162461bcd60e51b815260206004820152602f60248201527f50726f746f636f6c436f6e6669673a206e65772062656e65666963696172792060448201526e6973207a65726f206164647265737360881b60648201526084016103ab565b7f668f09ce856848ead6cb1ddee963f15ef833cea8958030868f867aec8438520480546001600160a01b0319166001600160a01b0383169081179091556040517f668f09ce856848ead6cb1ddee963f15ef833cea8958030868f867aec8438520091907fdec90d8bfa3fe33f0b0d876fc2cfc0e936e625e503ed170de964f15e6e17d15c905f90a25050565b6001546001600160a01b031633146105955760405162461bcd60e51b81526004016103ab90610ca8565b6001805460ff60a01b191690556040517f7805862f689e2f13df9f062ff482ad3ad112aca9e0847911ed832e158c525b33905f90a1565b6105d4610b3c565b6001600160a01b03811661063b5760405162461bcd60e51b815260206004820152602860248201527f5061757361626c653a206e65772070617573657220697320746865207a65726f604482015267206164647265737360c01b60648201526084016103ab565b600180546001600160a01b0319166001600160a01b0383169081179091556040517fb80482a293ca2e013eda8683c9bd7fc8347cfdaeea5ede58cba46df502c2a604905f90a250565b61068c610b3c565b6106955f610b6e565b565b33806106a1610a8f565b6001600160a01b0316146106d35760405163118cdaa760e01b81526001600160a01b03821660048201526024016103ab565b6106dc81610b6e565b50565b6001546001600160a01b031633146107095760405162461bcd60e51b81526004016103ab90610ca8565b6001805460ff60a01b1916600160a01b1790556040517f6985a02210a168e66602d3235cb6db0e70f92b3ba4d376a33c0f3d9434bff625905f90a1565b5f807f9016d09d72d40fdae2fd8ceac6b6234c7706214fd39c1cd1e609a0528c1993005b546001600160a01b031692915050565b5f546001600160a01b031633146107a35760405162461bcd60e51b81526004016103ab90610c60565b600154600160a01b900460ff16156107f05760405162461bcd60e51b815260206004820152601060248201526f14185d5cd8589b194e881c185d5cd95960821b60448201526064016103ab565b60646107ff6020830183610cfe565b6001600160401b031611156108625760405162461bcd60e51b8152602060048201526024808201527f50726f746f636f6c436f6e6669673a20616c706861206d757374206265203c3d6044820152630203130360e41b60648201526084016103ab565b60646108746040830160208401610cfe565b6001600160401b031611156108d75760405162461bcd60e51b8152602060048201526024808201527f50726f746f636f6c436f6e6669673a206b52617465206d757374206265203c3d6044820152630203130360e41b60648201526084016103ab565b80608001358160600135111561093f5760405162461bcd60e51b815260206004820152602760248201527f50726f746f636f6c436f6e6669673a206d696e42617365466565203e206d61786044820152664261736546656560c81b60648201526084016103ab565b5f8160a00135116109a45760405162461bcd60e51b815260206004820152602960248201527f50726f746f636f6c436f6e6669673a20626c6f636b4761734c696d6974206d7560448201526807374206265203e20360bc1b60648201526084016103ab565b5f6109b56060830160408401610cfe565b6001600160401b031611610a245760405162461bcd60e51b815260206004820152603060248201527f50726f746f636f6c436f6e6669673a20656c61737469636974794d756c74697060448201526f06c696572206d757374206265203e20360841b60648201526084016103ab565b7f668f09ce856848ead6cb1ddee963f15ef833cea8958030868f867aec843852008181610a518282610d19565b9050507f413a821e8d0eadcc30efbe627a655111b9f28f5999003a85b7718a14b0e329ef82604051610a839190610dd6565b60405180910390a15050565b5f807f237e158222e3e6968b72b9db0d8043aacf074ad9f650f0d1606b4d82ee432c0061076a565b610abf610b3c565b7f237e158222e3e6968b72b9db0d8043aacf074ad9f650f0d1606b4d82ee432c0080546001600160a01b0319166001600160a01b0383169081178255610b03610746565b6001600160a01b03167f38d16b8cac22d99fc7c124b9cd0de2d3fa1faef420bfe791d8c362d765e2270060405160405180910390a35050565b33610b45610746565b6001600160a01b0316146106955760405163118cdaa760e01b81523360048201526024016103ab565b7f237e158222e3e6968b72b9db0d8043aacf074ad9f650f0d1606b4d82ee432c0080546001600160a01b0319168155610ba682610baa565b5050565b7f9016d09d72d40fdae2fd8ceac6b6234c7706214fd39c1cd1e609a0528c19930080546001600160a01b031981166001600160a01b03848116918217845560405192169182907f8be0079c531659141344cd1fd0a4f28419497f9722a3daafe3b4186f6b6457e0905f90a3505050565b5f60208284031215610c2a575f5ffd5b81356001600160a01b0381168114610c40575f5ffd5b9392505050565b5f60c0828403128015610c58575f5ffd5b509092915050565b60208082526028908201527f436f6e74726f6c6c65723a2063616c6c6572206973206e6f742074686520636f604082015267373a3937b63632b960c11b606082015260800190565b60208082526022908201527f5061757361626c653a2063616c6c6572206973206e6f7420746865207061757360408201526132b960f11b606082015260800190565b6001600160401b03811681146106dc575f5ffd5b5f60208284031215610d0e575f5ffd5b8135610c4081610cea565b8135610d2481610cea565b6001600160401b03811690508154816001600160401b031982161783556020840135610d4f81610cea565b6fffffffffffffffff00000000000000008160401b16836fffffffffffffffffffffffffffffffff198416171784555050505f6040830135610d9081610cea565b825467ffffffffffffffff60801b1916608091821b67ffffffffffffffff60801b161783556060840135600184015583013560028301555060a090910135600390910155565b60c081018235610de581610cea565b6001600160401b031682526020830135610dfe81610cea565b6001600160401b031660208301526040830135610e1a81610cea565b6001600160401b03166040830152606083810135908301526080808401359083015260a09283013592909101919091529056fea2646970667358221220895e00e7d70f2261393e930bce98d421664d393e55f725d70675cb4ed5353b2f64736f6c634300081d0033" - }, - "0x3600000000000000000000000000000000000001": { - "balance": "0x0", - "nonce": "0x1", - "code": "0x608060405260043610610049575f3560e01c80633659cfe6146100535780634f1ef286146100725780635c60da1b146100855780638f283970146100b5578063f851a440146100d4575b6100516100e8565b005b34801561005e575f5ffd5b5061005161006d3660046104f3565b6100fa565b61005161008036600461050c565b610134565b348015610090575f5ffd5b50610099610197565b6040516001600160a01b03909116815260200160405180910390f35b3480156100c0575f5ffd5b506100516100cf3660046104f3565b6101a5565b3480156100df575f5ffd5b506100996101c5565b6100f86100f3610197565b6101ce565b565b6101026101ec565b6001600160a01b0316330361012c576101298160405180602001604052805f81525061021e565b50565b6101296100e8565b61013c6101ec565b6001600160a01b0316330361018f5761018a8383838080601f0160208091040260200160405190810160405280939291908181526020018383808284375f9201919091525061021e92505050565b505050565b61018a6100e8565b5f6101a0610277565b905090565b6101ad6101ec565b6001600160a01b0316330361012c576101298161029e565b5f6101a06101ec565b365f5f375f5f365f845af43d5f5f3e8080156101e8573d5ff35b3d5ffd5b5f7fb53127684a568b3173ae13b9f8a6016e243e63b6e8ee1178d6a717850b5d61035b546001600160a01b0316919050565b610227826102f2565b6040516001600160a01b038316907fbc7cd75a20ee27fd9adebab32041f755214dbc6bffa90cc0225b39da2e5c2d3b905f90a280511561026b5761018a8282610370565b6102736103e2565b5050565b5f7f360894a13ba1a3210667c828492db98dca3e2076cc3735a920a3ca505d382bbc61020f565b7f7e644d79422f17c01e4894b5f4f588d331ebfa28653d42ae832dc59e38c9798f6102c76101ec565b604080516001600160a01b03928316815291841660208301520160405180910390a161012981610401565b806001600160a01b03163b5f0361032c57604051634c9c8ce360e01b81526001600160a01b03821660048201526024015b60405180910390fd5b807f360894a13ba1a3210667c828492db98dca3e2076cc3735a920a3ca505d382bbc5b80546001600160a01b0319166001600160a01b039290921691909117905550565b60605f5f846001600160a01b03168460405161038c919061058a565b5f60405180830381855af49150503d805f81146103c4576040519150601f19603f3d011682016040523d82523d5f602084013e6103c9565b606091505b50915091506103d9858383610451565b95945050505050565b34156100f85760405163b398979f60e01b815260040160405180910390fd5b6001600160a01b03811661042a57604051633173bdd160e11b81525f6004820152602401610323565b807fb53127684a568b3173ae13b9f8a6016e243e63b6e8ee1178d6a717850b5d610361034f565b60608261046657610461826104b0565b6104a9565b815115801561047d57506001600160a01b0384163b155b156104a657604051639996b31560e01b81526001600160a01b0385166004820152602401610323565b50805b9392505050565b8051156104bf57805160208201fd5b60405163d6bda27560e01b815260040160405180910390fd5b80356001600160a01b03811681146104ee575f5ffd5b919050565b5f60208284031215610503575f5ffd5b6104a9826104d8565b5f5f5f6040848603121561051e575f5ffd5b610527846104d8565b9250602084013567ffffffffffffffff811115610542575f5ffd5b8401601f81018613610552575f5ffd5b803567ffffffffffffffff811115610568575f5ffd5b866020828401011115610579575f5ffd5b939660209190910195509293505050565b5f82518060208501845e5f92019182525091905056fea2646970667358221220407750ff6315cb87de3aba2eb532b034b7d9acff9bfba8abac9b674d66a9a95064736f6c634300081d0033", - "storage": { - "0xb53127684a568b3173ae13b9f8a6016e243e63b6e8ee1178d6a717850b5d6103": "0x000000000000000000000000dc151920e1f7cd214a108a3ad0e6a586d46ca015", - "0x360894a13ba1a3210667c828492db98dca3e2076cc3735a920a3ca505d382bbc": "0x000000000000000000000000f511ec138a4868be5924b5b1d2bc148154dd1bbf", - "0x9016d09d72d40fdae2fd8ceac6b6234c7706214fd39c1cd1e609a0528c199300": "0x000000000000000000000000e73c02301f1e6877ad8b5fb7cfbbd51af97bc0ca", - "0xf0c57e16840df040f15088dc2f81fe391c3923bec73e23a9662efc9c229c6a00": "0x000000000000000000000000000000000000000000000000ffffffffffffffff", - "0x0000000000000000000000000000000000000000000000000000000000000000": "0x000000000000000000000000fdfc974f9a66bd01353f39dfc32e6babc6f22ff6", - "0x0000000000000000000000000000000000000000000000000000000000000001": "0x000000000000000000000000e054635b17c4bd648b820ecf5ab832688733aad1", - "0x668f09ce856848ead6cb1ddee963f15ef833cea8958030868f867aec84385200": "0x0000000000000000000000000000000200000000000000190000000000000014", - "0x668f09ce856848ead6cb1ddee963f15ef833cea8958030868f867aec84385201": "0x0000000000000000000000000000000000000000000000000000000000000001", - "0x668f09ce856848ead6cb1ddee963f15ef833cea8958030868f867aec84385202": "0x00000000000000000000000000000000000000000000000000000000000003e8", - "0x668f09ce856848ead6cb1ddee963f15ef833cea8958030868f867aec84385203": "0x0000000000000000000000000000000000000000000000000000000001c9c380", - "0x668f09ce856848ead6cb1ddee963f15ef833cea8958030868f867aec84385204": "0x00000000000000000000000065e0a200006d4ff91bd59f9694220dafc49dbbc1" - } - }, - "0x9e80cF48c80Af8814030836992EF92A5d4C0EBfd": { - "balance": "0x0", - "nonce": "0x1", - "code": "0x608060405234801561000f575f5ffd5b50600436106100cb575f3560e01c806383ca642f11610088578063c4899f0c11610063578063c4899f0c146101aa578063e30c3978146101bd578063f2fde38b146101c5578063f94e1867146101d8575f5ffd5b806383ca642f146101435780638da5cb5b1461016a578063b5d896271461018a575f5ffd5b806303a9b132146100cf57806324408a68146100f65780632469d6721461010b5780634ebae6171461011e578063715018a61461013357806379ba50971461013b575b5f5ffd5b6100e35f5160206112005f395f51905f5281565b6040519081526020015b60405180910390f35b6100fe6101eb565b6040516100ed9190610dfd565b6100e3610119366004610e8f565b6103cb565b61013161012c366004610f50565b61056f565b005b610131610632565b610131610645565b7fb58da0dce03316992faea3e12c60705b8ac05a309e27e3bc8421e5b271c9d204546100e3565b61017261068d565b6040516001600160a01b0390911681526020016100ed565b61019d610198366004610f50565b6106c1565b6040516100ed9190610f67565b6101316101b8366004610f79565b6107e6565b6101726108e2565b6101316101d3366004610f9a565b61090a565b6101316101e6366004610f50565b61098f565b60605f5160206112005f395f51905f525f6102257fb58da0dce03316992faea3e12c60705b8ac05a309e27e3bc8421e5b271c9d201610ab4565b9050806001600160401b0381111561023f5761023f610e60565b60405190808252806020026020018201604052801561029357816020015b6102806040805160608101909152805f8152606060208201525f60409091015290565b81526020019060019003908161025d5790505b5092505f5b818110156103c5575f6102ae6001850183610abd565b5f818152602086905260409081902081516060810190925280549293509091829060ff1660028111156102e3576102e3610d62565b60028111156102f4576102f4610d62565b815260200160018201805461030890610fc0565b80601f016020809104026020016040519081016040528092919081815260200182805461033490610fc0565b801561037f5780601f106103565761010080835404028352916020019161037f565b820191905f5260205f20905b81548152906001019060200180831161036257829003601f168201915b5050509183525050600291909101546001600160401b031660209091015285518690849081106103b1576103b1610ff8565b602090810291909101015250600101610298565b50505090565b5f6103d4610acf565b60208351146103f657604051630717c3c160e01b815260040160405180910390fd5b82516020808501919091205f8181527fb58da0dce03316992faea3e12c60705b8ac05a309e27e3bc8421e5b271c9d2039092526040909120545f5160206112005f395f51905f52919060ff1615610468576040516338a14a4160e01b8152600481018290526024015b60405180910390fd5b600482018054905f61047983611020565b909155506040805160608101909152909350806001815260208082018890526001600160401b0387166040928301525f868152908590522081518154829060ff191660018360028111156104cf576104cf610d62565b0217905550602082015160018201906104e89082611084565b50604091820151600291909101805467ffffffffffffffff19166001600160401b039092169190911790555f82815260038401602052819020805460ff191660011790555183907fea20c1f9de86768933c1d4eff47ce667e26f886c4877d1bedc253e42b2f04a7c9061055e908790899061113e565b60405180910390a250505b92915050565b610577610acf565b5f8181525f5160206112005f395f51905f5260208190526040909120805460019060ff1660028111156105ac576105ac610d62565b1483906105cf5760405163b1d5901360e01b815260040161045f91815260200190565b50805460ff191660021781556105e86001830184610b01565b5060028101546040516001600160401b03909116815283907fbc5136437746415b39af863272d0ca406634cf14bb7c3e5fa03855781add87e59060200160405180910390a2505050565b61063a610acf565b6106435f610b0c565b565b338061064f6108e2565b6001600160a01b0316146106815760405163118cdaa760e01b81526001600160a01b038216600482015260240161045f565b61068a81610b0c565b50565b5f807f9016d09d72d40fdae2fd8ceac6b6234c7706214fd39c1cd1e609a0528c1993005b546001600160a01b031692915050565b6106e46040805160608101909152805f8152606060208201525f60409091015290565b5f8281525f5160206112005f395f51905f5260208190526040918290208251606081019093528054919291829060ff16600281111561072557610725610d62565b600281111561073657610736610d62565b815260200160018201805461074a90610fc0565b80601f016020809104026020016040519081016040528092919081815260200182805461077690610fc0565b80156107c15780601f10610798576101008083540402835291602001916107c1565b820191905f5260205f20905b8154815290600101906020018083116107a457829003601f168201915b5050509183525050600291909101546001600160401b03166020909101529392505050565b6107ee610acf565b5f5f5160206112005f395f51905f525f848152602082905260408120919250815460ff16600281111561082357610823610d62565b141584906108475760405163b1d5901360e01b815260040161045f91815260200190565b5060028101546001600160401b03908116908416810361087a57604051632bf0186d60e21b815260040160405180910390fd5b60028201805467ffffffffffffffff19166001600160401b03868116918217909255604080519284168352602083019190915286917f4a7e1cc2e075be9a3c913bf00ec8ff10f6630f23433f65eee31e20ef69110ae491015b60405180910390a25050505050565b5f807f237e158222e3e6968b72b9db0d8043aacf074ad9f650f0d1606b4d82ee432c006106b1565b610912610acf565b7f237e158222e3e6968b72b9db0d8043aacf074ad9f650f0d1606b4d82ee432c0080546001600160a01b0319166001600160a01b038316908117825561095661068d565b6001600160a01b03167f38d16b8cac22d99fc7c124b9cd0de2d3fa1faef420bfe791d8c362d765e2270060405160405180910390a35050565b610997610acf565b5f5f5160206112005f395f51905f525f838152602082905260408120919250815460ff1660028111156109cc576109cc610d62565b141583906109f05760405163b1d5901360e01b815260040161045f91815260200190565b505f81600101604051610a039190611167565b60405190819003902060028301549091506001600160401b0316610a2a6001850186610b48565b505f858152602085905260408120805460ff1916815590610a4e6001830182610d18565b50600201805467ffffffffffffffff191690555f828152600385016020908152604091829020805460ff1916905590516001600160401b038316815286917f70d022b699831c183688ed9d9d1a1cbf586698b506eaacfd5195a4e3738d285b91016108d3565b5f610569825490565b5f610ac88383610b53565b9392505050565b33610ad861068d565b6001600160a01b0316146106435760405163118cdaa760e01b815233600482015260240161045f565b5f610ac88383610b79565b7f237e158222e3e6968b72b9db0d8043aacf074ad9f650f0d1606b4d82ee432c0080546001600160a01b0319168155610b4482610bc5565b5050565b5f610ac88383610c35565b5f825f018281548110610b6857610b68610ff8565b905f5260205f200154905092915050565b5f818152600183016020526040812054610bbe57508154600181810184555f848152602080822090930184905584548482528286019093526040902091909155610569565b505f610569565b7f9016d09d72d40fdae2fd8ceac6b6234c7706214fd39c1cd1e609a0528c19930080546001600160a01b031981166001600160a01b03848116918217845560405192169182907f8be0079c531659141344cd1fd0a4f28419497f9722a3daafe3b4186f6b6457e0905f90a3505050565b5f8181526001830160205260408120548015610d0f575f610c576001836111d8565b85549091505f90610c6a906001906111d8565b9050808214610cc9575f865f018281548110610c8857610c88610ff8565b905f5260205f200154905080875f018481548110610ca857610ca8610ff8565b5f918252602080832090910192909255918252600188019052604090208390555b8554869080610cda57610cda6111eb565b600190038181905f5260205f20015f90559055856001015f8681526020019081526020015f205f905560019350505050610569565b5f915050610569565b508054610d2490610fc0565b5f825580601f10610d33575050565b601f0160209004905f5260205f209081019061068a91905b80821115610d5e575f8155600101610d4b565b5090565b634e487b7160e01b5f52602160045260245ffd5b5f81518084528060208401602086015e5f602082860101526020601f19601f83011685010191505092915050565b5f815160038110610dc357634e487b7160e01b5f52602160045260245ffd5b80845250602082015160606020850152610de06060850182610d76565b6040938401516001600160401b0316949093019390935250919050565b5f602082016020835280845180835260408501915060408160051b8601019250602086015f5b82811015610e5457603f19878603018452610e3f858351610da4565b94506020938401939190910190600101610e23565b50929695505050505050565b634e487b7160e01b5f52604160045260245ffd5b80356001600160401b0381168114610e8a575f5ffd5b919050565b5f5f60408385031215610ea0575f5ffd5b82356001600160401b03811115610eb5575f5ffd5b8301601f81018513610ec5575f5ffd5b80356001600160401b03811115610ede57610ede610e60565b604051601f8201601f19908116603f011681016001600160401b0381118282101715610f0c57610f0c610e60565b604052818152828201602001871015610f23575f5ffd5b816020840160208301375f60208383010152809450505050610f4760208401610e74565b90509250929050565b5f60208284031215610f60575f5ffd5b5035919050565b602081525f610ac86020830184610da4565b5f5f60408385031215610f8a575f5ffd5b82359150610f4760208401610e74565b5f60208284031215610faa575f5ffd5b81356001600160a01b0381168114610ac8575f5ffd5b600181811c90821680610fd457607f821691505b602082108103610ff257634e487b7160e01b5f52602260045260245ffd5b50919050565b634e487b7160e01b5f52603260045260245ffd5b634e487b7160e01b5f52601160045260245ffd5b5f600182016110315761103161100c565b5060010190565b601f82111561107f57805f5260205f20601f840160051c8101602085101561105d5750805b601f840160051c820191505b8181101561107c575f8155600101611069565b50505b505050565b81516001600160401b0381111561109d5761109d610e60565b6110b1816110ab8454610fc0565b84611038565b6020601f8211600181146110e3575f83156110cc5750848201515b5f19600385901b1c1916600184901b17845561107c565b5f84815260208120601f198516915b8281101561111257878501518255602094850194600190920191016110f2565b508482101561112f57868401515f19600387901b60f8161c191681555b50505050600190811b01905550565b6001600160401b0383168152604060208201525f61115f6040830184610d76565b949350505050565b5f5f835461117481610fc0565b60018216801561118b57600181146111a0576111cd565b60ff19831686528115158202860193506111cd565b865f5260205f205f5b838110156111c5578154888201526001909101906020016111a9565b505081860193505b509195945050505050565b818103818111156105695761056961100c565b634e487b7160e01b5f52603160045260245ffdfeb58da0dce03316992faea3e12c60705b8ac05a309e27e3bc8421e5b271c9d200a264697066735822122013a1ce3be2b24b3e0366fc4e35557e488a73c76cb161d34007b54463b62e0bb764736f6c634300081d0033" - }, - "0x3600000000000000000000000000000000000002": { - "balance": "0x0", - "nonce": "0x1", - "code": "0x608060405260043610610049575f3560e01c80633659cfe6146100535780634f1ef286146100725780635c60da1b146100855780638f283970146100b5578063f851a440146100d4575b6100516100e8565b005b34801561005e575f5ffd5b5061005161006d3660046104f3565b6100fa565b61005161008036600461050c565b610134565b348015610090575f5ffd5b50610099610197565b6040516001600160a01b03909116815260200160405180910390f35b3480156100c0575f5ffd5b506100516100cf3660046104f3565b6101a5565b3480156100df575f5ffd5b506100996101c5565b6100f86100f3610197565b6101ce565b565b6101026101ec565b6001600160a01b0316330361012c576101298160405180602001604052805f81525061021e565b50565b6101296100e8565b61013c6101ec565b6001600160a01b0316330361018f5761018a8383838080601f0160208091040260200160405190810160405280939291908181526020018383808284375f9201919091525061021e92505050565b505050565b61018a6100e8565b5f6101a0610277565b905090565b6101ad6101ec565b6001600160a01b0316330361012c576101298161029e565b5f6101a06101ec565b365f5f375f5f365f845af43d5f5f3e8080156101e8573d5ff35b3d5ffd5b5f7fb53127684a568b3173ae13b9f8a6016e243e63b6e8ee1178d6a717850b5d61035b546001600160a01b0316919050565b610227826102f2565b6040516001600160a01b038316907fbc7cd75a20ee27fd9adebab32041f755214dbc6bffa90cc0225b39da2e5c2d3b905f90a280511561026b5761018a8282610370565b6102736103e2565b5050565b5f7f360894a13ba1a3210667c828492db98dca3e2076cc3735a920a3ca505d382bbc61020f565b7f7e644d79422f17c01e4894b5f4f588d331ebfa28653d42ae832dc59e38c9798f6102c76101ec565b604080516001600160a01b03928316815291841660208301520160405180910390a161012981610401565b806001600160a01b03163b5f0361032c57604051634c9c8ce360e01b81526001600160a01b03821660048201526024015b60405180910390fd5b807f360894a13ba1a3210667c828492db98dca3e2076cc3735a920a3ca505d382bbc5b80546001600160a01b0319166001600160a01b039290921691909117905550565b60605f5f846001600160a01b03168460405161038c919061058a565b5f60405180830381855af49150503d805f81146103c4576040519150601f19603f3d011682016040523d82523d5f602084013e6103c9565b606091505b50915091506103d9858383610451565b95945050505050565b34156100f85760405163b398979f60e01b815260040160405180910390fd5b6001600160a01b03811661042a57604051633173bdd160e11b81525f6004820152602401610323565b807fb53127684a568b3173ae13b9f8a6016e243e63b6e8ee1178d6a717850b5d610361034f565b60608261046657610461826104b0565b6104a9565b815115801561047d57506001600160a01b0384163b155b156104a657604051639996b31560e01b81526001600160a01b0385166004820152602401610323565b50805b9392505050565b8051156104bf57805160208201fd5b60405163d6bda27560e01b815260040160405180910390fd5b80356001600160a01b03811681146104ee575f5ffd5b919050565b5f60208284031215610503575f5ffd5b6104a9826104d8565b5f5f5f6040848603121561051e575f5ffd5b610527846104d8565b9250602084013567ffffffffffffffff811115610542575f5ffd5b8401601f81018613610552575f5ffd5b803567ffffffffffffffff811115610568575f5ffd5b866020828401011115610579575f5ffd5b939660209190910195509293505050565b5f82518060208501845e5f92019182525091905056fea2646970667358221220407750ff6315cb87de3aba2eb532b034b7d9acff9bfba8abac9b674d66a9a95064736f6c634300081d0033", - "storage": { - "0xb53127684a568b3173ae13b9f8a6016e243e63b6e8ee1178d6a717850b5d6103": "0x000000000000000000000000fd9a0965db3d6376f498347f4c129ba6c72f9628", - "0x360894a13ba1a3210667c828492db98dca3e2076cc3735a920a3ca505d382bbc": "0x0000000000000000000000009e80cf48c80af8814030836992ef92a5d4c0ebfd", - "0xf0c57e16840df040f15088dc2f81fe391c3923bec73e23a9662efc9c229c6a00": "0x000000000000000000000000000000000000000000000000ffffffffffffffff", - "0x9016d09d72d40fdae2fd8ceac6b6234c7706214fd39c1cd1e609a0528c199300": "0x0000000000000000000000003600000000000000000000000000000000000003", - "0x0499f4c2de309a54da703ff88d2d815fa145b3fb32cf77b2528c8033db8fd7ee": "0x0000000000000000000000000000000000000000000000000000000000000002", - "0x0499f4c2de309a54da703ff88d2d815fa145b3fb32cf77b2528c8033db8fd7ef": "0x0000000000000000000000000000000000000000000000000000000000000041", - "0xe76fb05174802623d7f5b79138467ede24c07a48fb794feb0f195f82c29e25f6": "0xfdbcdaad0c66b97534141879ba6128a6a108609d7d68b4c66c69804fd8c989ad", - "0x0499f4c2de309a54da703ff88d2d815fa145b3fb32cf77b2528c8033db8fd7f0": "0x00000000000000000000000000000000000000000000000000000000000007d0", - "0x2559e4fd6bae46fd4a933fbdfc2024f30e3f30caf9b658f266112e99c269472e": "0x0000000000000000000000000000000000000000000000000000000000000001", - "0x3c22fd2722f4c68bee66b0771765c1374bf0b4f04712a4b14dee7a423e372484": "0x0000000000000000000000000000000000000000000000000000000000000001", - "0xcc1220f12586af55eec49d83d6500e10002316aa4790142e1b451ccc79351374": "0x0000000000000000000000000000000000000000000000000000000000000001", - "0x799a8c5a05520e2bd7f998b6bef5c37455a07d2f4637750d18e8585f32667020": "0x0000000000000000000000000000000000000000000000000000000000000002", - "0x799a8c5a05520e2bd7f998b6bef5c37455a07d2f4637750d18e8585f32667021": "0x0000000000000000000000000000000000000000000000000000000000000041", - "0x88471ae418c3e7b280a001c3ca67ebba02159f936da01ae6a2e84cabdddd1ff9": "0xa5bf8cec34f820f8f6b8385ade09ab771be1c04500ca912de1b3cb09e43b86e8", - "0x799a8c5a05520e2bd7f998b6bef5c37455a07d2f4637750d18e8585f32667022": "0x00000000000000000000000000000000000000000000000000000000000007d0", - "0x2559e4fd6bae46fd4a933fbdfc2024f30e3f30caf9b658f266112e99c269472f": "0x0000000000000000000000000000000000000000000000000000000000000002", - "0x095131cda7b4097ad9172b46969f424386afbffb0fe58d634a10ceead00fe90f": "0x0000000000000000000000000000000000000000000000000000000000000002", - "0xd642c55e66cdab5ca555d7126206097fc1f8e92fbe1331811776ffa32ecaa44b": "0x0000000000000000000000000000000000000000000000000000000000000001", - "0x5823124c2578a0659c1d18b6eac7cd5d39d30d9e0a439c0dc79350c911078892": "0x0000000000000000000000000000000000000000000000000000000000000002", - "0x5823124c2578a0659c1d18b6eac7cd5d39d30d9e0a439c0dc79350c911078893": "0x0000000000000000000000000000000000000000000000000000000000000041", - "0xe4bbd97d770f505bda53f50c8dce4c86cb0dc400a1b5c97a9aacf7305b0907af": "0x385e0c5aeca4f54c442b5bf0b2320a79b2a67386df4d5461f3b358f957667480", - "0x5823124c2578a0659c1d18b6eac7cd5d39d30d9e0a439c0dc79350c911078894": "0x00000000000000000000000000000000000000000000000000000000000007d0", - "0x2559e4fd6bae46fd4a933fbdfc2024f30e3f30caf9b658f266112e99c2694730": "0x0000000000000000000000000000000000000000000000000000000000000003", - "0xe0ed4630bad2f511c8f295d4c67204cad855df4001547c1a63b7ba76c4642997": "0x0000000000000000000000000000000000000000000000000000000000000003", - "0x4b01e81ec3c978454fa9b9e65aa8de5aa4f682b9671014d6c2e1eb774f61ec24": "0x0000000000000000000000000000000000000000000000000000000000000001", - "0xb58da0dce03316992faea3e12c60705b8ac05a309e27e3bc8421e5b271c9d201": "0x0000000000000000000000000000000000000000000000000000000000000003", - "0xb58da0dce03316992faea3e12c60705b8ac05a309e27e3bc8421e5b271c9d204": "0x0000000000000000000000000000000000000000000000000000000000000004" - } - }, - "0xDE2CbcCBF304b03647d579997D66CC24172fB6EC": { - "balance": "0x0", - "nonce": "0x1", - "code": "0x608060405234801561000f575f5ffd5b5060043610610111575f3560e01c80638da5cb5b1161009e578063b429afeb1161006e578063b429afeb1461024c578063da3003f914610276578063e30c397814610289578063f2fde38b14610291578063f6a74ed7146102a4575f5ffd5b80638da5cb5b146101de57806398adeb97146101fe5780639b6ec8b714610211578063aa5ecb5214610239575f5ffd5b80634cb4850a116100e45780634cb4850a146101a05780635e5feee6146101b3578063715018a6146101c657806379ba5097146101ce5780637f77403d146101d6575f5ffd5b8063048544a4146101155780631904bb2e146101555780632469d67214610175578063263a340214610196575b5f5ffd5b610140610123366004610d63565b6001600160a01b03165f9081526001602052604090205460ff1690565b60405190151581526020015b60405180910390f35b610168610163366004610d63565b6102b7565b60405161014c9190610daa565b610188610183366004610eb7565b61037e565b60405190815260200161014c565b61019e610479565b005b61019e6101ae366004610f46565b61052f565b61019e6101c1366004610d63565b6105f5565b61019e6106da565b61019e6106ed565b61019e610735565b6101e66107bf565b6040516001600160a01b03909116815260200161014c565b61019e61020c366004610f61565b6107f3565b61018861021f366004610d63565b6001600160a01b03165f9081526020819052604090205490565b61019e610247366004610d63565b61091b565b61014061025a366004610d63565b6001600160a01b03165f90815260208190526040902054151590565b61019e610284366004610d63565b6109f8565b6101e6610ae6565b61019e61029f366004610d63565b610b0e565b61019e6102b2366004610d63565b610b93565b6102da6040805160608101909152805f8152606060208201525f60409091015290565b6001600160a01b038281165f908152602081905260409081902054905163b5d8962760e01b81526004810182905290917f0000000000000000000000003600000000000000000000000000000000000002169063b5d89627906024015f60405180830381865afa158015610350573d5f5f3e3d5ffd5b505050506040513d5f823e601f3d908101601f191682016040526103779190810190610f94565b9392505050565b335f9081526001602052604081205460ff166103eb5760405162461bcd60e51b815260206004820152602160248201527f43616c6c6572206973206e6f742076616c696461746f725265676973746572656044820152603960f91b60648201526084015b60405180910390fd5b604051631234eb3960e11b81526001600160a01b037f00000000000000000000000036000000000000000000000000000000000000021690632469d672906104399086908690600401611062565b6020604051808303815f875af1158015610455573d5f5f3e3d5ffd5b505050506040513d601f19601f82011682018060405250810190610377919061108d565b335f9081526020819052604081205490036104a65760405162461bcd60e51b81526004016103e2906110a4565b335f9081526020819052604090819020549051634ebae61760e01b8152600481018290526001600160a01b037f00000000000000000000000036000000000000000000000000000000000000021690634ebae617906024015b5f604051808303815f87803b158015610516575f5ffd5b505af1158015610528573d5f5f3e3d5ffd5b5050505050565b335f90815260208190526040812054900361055c5760405162461bcd60e51b81526004016103e2906110a4565b335f908152602081905260409081902054905163312267c360e21b81526004810182905267ffffffffffffffff831660248201526001600160a01b037f0000000000000000000000003600000000000000000000000000000000000002169063c4899f0c906044015f604051808303815f87803b1580156105db575f5ffd5b505af11580156105ed573d5f5f3e3d5ffd5b505050505050565b6105fd610c6a565b6001600160a01b038116610624576040516342bcdf7f60e11b815260040160405180910390fd5b60405163f2fde38b60e01b81526001600160a01b0382811660048301527f0000000000000000000000003600000000000000000000000000000000000002169063f2fde38b906024015f604051808303815f87803b158015610684575f5ffd5b505af1158015610696573d5f5f3e3d5ffd5b50506040516001600160a01b03841681527ff8e2a8d4b93bd709cc57c9f21b79403c532f960ef18f77d0c23403cae0de78d99250602001905060405180910390a150565b6106e2610c6a565b6106eb5f610c9c565b565b33806106f7610ae6565b6001600160a01b0316146107295760405163118cdaa760e01b81526001600160a01b03821660048201526024016103e2565b61073281610c9c565b50565b335f9081526020819052604081205490036107625760405162461bcd60e51b81526004016103e2906110a4565b335f908152602081905260409081902054905163f94e186760e01b8152600481018290526001600160a01b037f0000000000000000000000003600000000000000000000000000000000000002169063f94e1867906024016104ff565b5f807f9016d09d72d40fdae2fd8ceac6b6234c7706214fd39c1cd1e609a0528c1993005b546001600160a01b031692915050565b6107fb610c6a565b805f0361084a5760405162461bcd60e51b815260206004820152601e60248201527f526567697374726174696f6e2049442063616e6e6f74206265207a65726f000060448201526064016103e2565b6001600160a01b0382166108705760405162461bcd60e51b81526004016103e2906110db565b6001600160a01b0382165f90815260208190526040902054156108d55760405162461bcd60e51b815260206004820152601d60248201527f436f6e74726f6c6c657220616c726561647920636f6e6669677572656400000060448201526064016103e2565b6001600160a01b0382165f81815260208190526040808220849055518392917fd765df254a1ab86d330d7f337f70eca404f7fc78a93b587f4ffebcb135f4466991a35050565b610923610c6a565b6001600160a01b0381166109495760405162461bcd60e51b81526004016103e290611120565b6001600160a01b0381165f9081526001602052604090205460ff166109b05760405162461bcd60e51b815260206004820152601d60248201527f56616c696461746f7252656769737465726572206e6f7420616464656400000060448201526064016103e2565b6001600160a01b0381165f81815260016020526040808220805460ff19169055517f195d741377fcd6e23556d115769d0b5f54decc24349a916f6de7371077fe7fd99190a250565b610a00610c6a565b6001600160a01b038116610a265760405162461bcd60e51b81526004016103e290611120565b6001600160a01b0381165f9081526001602052604090205460ff1615610a985760405162461bcd60e51b815260206004820152602160248201527f56616c696461746f725265676973746572657220616c726561647920616464656044820152601960fa1b60648201526084016103e2565b6001600160a01b0381165f818152600160208190526040808320805460ff1916909217909155517fd25faf73acccddf9a7ac429be872d2109cd7bd0c2370658b7c019518f3159d8e9190a250565b5f807f237e158222e3e6968b72b9db0d8043aacf074ad9f650f0d1606b4d82ee432c006107e3565b610b16610c6a565b7f237e158222e3e6968b72b9db0d8043aacf074ad9f650f0d1606b4d82ee432c0080546001600160a01b0319166001600160a01b0383169081178255610b5a6107bf565b6001600160a01b03167f38d16b8cac22d99fc7c124b9cd0de2d3fa1faef420bfe791d8c362d765e2270060405160405180910390a35050565b610b9b610c6a565b6001600160a01b038116610bc15760405162461bcd60e51b81526004016103e2906110db565b6001600160a01b0381165f908152602081905260408120549003610c275760405162461bcd60e51b815260206004820152601960248201527f436f6e74726f6c6c6572206e6f7420636f6e666967757265640000000000000060448201526064016103e2565b6001600160a01b0381165f81815260208190526040808220829055517f33d83959be2573f5453b12eb9d43b3499bc57d96bd2f067ba44803c859e811139190a250565b33610c736107bf565b6001600160a01b0316146106eb5760405163118cdaa760e01b81523360048201526024016103e2565b7f237e158222e3e6968b72b9db0d8043aacf074ad9f650f0d1606b4d82ee432c0080546001600160a01b0319168155610cd482610cd8565b5050565b7f9016d09d72d40fdae2fd8ceac6b6234c7706214fd39c1cd1e609a0528c19930080546001600160a01b031981166001600160a01b03848116918217845560405192169182907f8be0079c531659141344cd1fd0a4f28419497f9722a3daafe3b4186f6b6457e0905f90a3505050565b80356001600160a01b0381168114610d5e575f5ffd5b919050565b5f60208284031215610d73575f5ffd5b61037782610d48565b5f81518084528060208401602086015e5f602082860101526020601f19601f83011685010191505092915050565b602081525f825160038110610dcd57634e487b7160e01b5f52602160045260245ffd5b80602084015250602083015160606040840152610ded6080840182610d7c565b905067ffffffffffffffff60408501511660608401528091505092915050565b634e487b7160e01b5f52604160045260245ffd5b6040516060810167ffffffffffffffff81118282101715610e4457610e44610e0d565b60405290565b604051601f8201601f1916810167ffffffffffffffff81118282101715610e7357610e73610e0d565b604052919050565b5f67ffffffffffffffff821115610e9457610e94610e0d565b50601f01601f191660200190565b67ffffffffffffffff81168114610732575f5ffd5b5f5f60408385031215610ec8575f5ffd5b823567ffffffffffffffff811115610ede575f5ffd5b8301601f81018513610eee575f5ffd5b8035610f01610efc82610e7b565b610e4a565b818152866020838501011115610f15575f5ffd5b816020840160208301375f602083830101528094505050506020830135610f3b81610ea2565b809150509250929050565b5f60208284031215610f56575f5ffd5b813561037781610ea2565b5f5f60408385031215610f72575f5ffd5b610f7b83610d48565b946020939093013593505050565b8051610d5e81610ea2565b5f60208284031215610fa4575f5ffd5b815167ffffffffffffffff811115610fba575f5ffd5b820160608185031215610fcb575f5ffd5b610fd3610e21565b815160038110610fe1575f5ffd5b8152602082015167ffffffffffffffff811115610ffc575f5ffd5b8201601f8101861361100c575f5ffd5b805161101a610efc82610e7b565b81815287602083850101111561102e575f5ffd5b8160208401602083015e5f6020838301015280602085015250505061105560408301610f89565b6040820152949350505050565b604081525f6110746040830185610d7c565b905067ffffffffffffffff831660208301529392505050565b5f6020828403121561109d575f5ffd5b5051919050565b60208082526018908201527f43616c6c6572206973206e6f7420636f6e74726f6c6c65720000000000000000604082015260600190565b60208082526025908201527f436f6e74726f6c6c6572206d7573742062652061206e6f6e2d7a65726f206164604082015264647265737360d81b606082015260800190565b6020808252602e908201527f56616c696461746f7252656769737465726572206d7573742062652061206e6f60408201526d6e2d7a65726f206164647265737360901b60608201526080019056fea2646970667358221220376b8409b51e30139477fbd772e85f741572aec3be0009832785ca8e9b4554e164736f6c634300081d0033" - }, - "0x3600000000000000000000000000000000000003": { - "balance": "0x0", - "nonce": "0x1", - "code": "0x608060405260043610610049575f3560e01c80633659cfe6146100535780634f1ef286146100725780635c60da1b146100855780638f283970146100b5578063f851a440146100d4575b6100516100e8565b005b34801561005e575f5ffd5b5061005161006d3660046104f3565b6100fa565b61005161008036600461050c565b610134565b348015610090575f5ffd5b50610099610197565b6040516001600160a01b03909116815260200160405180910390f35b3480156100c0575f5ffd5b506100516100cf3660046104f3565b6101a5565b3480156100df575f5ffd5b506100996101c5565b6100f86100f3610197565b6101ce565b565b6101026101ec565b6001600160a01b0316330361012c576101298160405180602001604052805f81525061021e565b50565b6101296100e8565b61013c6101ec565b6001600160a01b0316330361018f5761018a8383838080601f0160208091040260200160405190810160405280939291908181526020018383808284375f9201919091525061021e92505050565b505050565b61018a6100e8565b5f6101a0610277565b905090565b6101ad6101ec565b6001600160a01b0316330361012c576101298161029e565b5f6101a06101ec565b365f5f375f5f365f845af43d5f5f3e8080156101e8573d5ff35b3d5ffd5b5f7fb53127684a568b3173ae13b9f8a6016e243e63b6e8ee1178d6a717850b5d61035b546001600160a01b0316919050565b610227826102f2565b6040516001600160a01b038316907fbc7cd75a20ee27fd9adebab32041f755214dbc6bffa90cc0225b39da2e5c2d3b905f90a280511561026b5761018a8282610370565b6102736103e2565b5050565b5f7f360894a13ba1a3210667c828492db98dca3e2076cc3735a920a3ca505d382bbc61020f565b7f7e644d79422f17c01e4894b5f4f588d331ebfa28653d42ae832dc59e38c9798f6102c76101ec565b604080516001600160a01b03928316815291841660208301520160405180910390a161012981610401565b806001600160a01b03163b5f0361032c57604051634c9c8ce360e01b81526001600160a01b03821660048201526024015b60405180910390fd5b807f360894a13ba1a3210667c828492db98dca3e2076cc3735a920a3ca505d382bbc5b80546001600160a01b0319166001600160a01b039290921691909117905550565b60605f5f846001600160a01b03168460405161038c919061058a565b5f60405180830381855af49150503d805f81146103c4576040519150601f19603f3d011682016040523d82523d5f602084013e6103c9565b606091505b50915091506103d9858383610451565b95945050505050565b34156100f85760405163b398979f60e01b815260040160405180910390fd5b6001600160a01b03811661042a57604051633173bdd160e11b81525f6004820152602401610323565b807fb53127684a568b3173ae13b9f8a6016e243e63b6e8ee1178d6a717850b5d610361034f565b60608261046657610461826104b0565b6104a9565b815115801561047d57506001600160a01b0384163b155b156104a657604051639996b31560e01b81526001600160a01b0385166004820152602401610323565b50805b9392505050565b8051156104bf57805160208201fd5b60405163d6bda27560e01b815260040160405180910390fd5b80356001600160a01b03811681146104ee575f5ffd5b919050565b5f60208284031215610503575f5ffd5b6104a9826104d8565b5f5f5f6040848603121561051e575f5ffd5b610527846104d8565b9250602084013567ffffffffffffffff811115610542575f5ffd5b8401601f81018613610552575f5ffd5b803567ffffffffffffffff811115610568575f5ffd5b866020828401011115610579575f5ffd5b939660209190910195509293505050565b5f82518060208501845e5f92019182525091905056fea2646970667358221220407750ff6315cb87de3aba2eb532b034b7d9acff9bfba8abac9b674d66a9a95064736f6c634300081d0033", - "storage": { - "0xb53127684a568b3173ae13b9f8a6016e243e63b6e8ee1178d6a717850b5d6103": "0x000000000000000000000000ee33d2085e034e5b9d9346c775f10d6ea284d620", - "0x360894a13ba1a3210667c828492db98dca3e2076cc3735a920a3ca505d382bbc": "0x000000000000000000000000de2cbccbf304b03647d579997d66cc24172fb6ec", - "0x9016d09d72d40fdae2fd8ceac6b6234c7706214fd39c1cd1e609a0528c199300": "0x000000000000000000000000e2ffed765eec0cc29507be6328cd7ac8e110f02a", - "0x64362cf79d67c76d16ac3aa79af2added4a5e53bc5a9965b320147be9da3e78f": "0x0000000000000000000000000000000000000000000000000000000000000001", - "0xe26a63e00a5f07831346dba43e94200693d0f6cf769a97c60a36e9ad68463e2e": "0x0000000000000000000000000000000000000000000000000000000000000002", - "0x88caa574abc15a2fa686c88c2b1f2947cecbd78ff002a8a62ec5cb26d754b954": "0x0000000000000000000000000000000000000000000000000000000000000003", - "0xda7e7292d66eb8cb5a6833a2b0b8b2af5a9540153b2a5bab39a7b55e0670b2da": "0x0000000000000000000000000000000000000000000000000000000000000004", - "0xdd24c933277ba0fe6e2c8df504594884e1c2cb4bd57ad94644bc90d16e25d982": "0x0000000000000000000000000000000000000000000000000000000000000005", - "0x681169651226423c500c2103c7ea31d177ce43034bd8ee8dbea00cf09c1e0d00": "0x0000000000000000000000000000000000000000000000000000000000000006", - "0x9d81e6762089e3655857958ad8e87f1060fbe9abe9c373323f3ded75ff1e3b96": "0x0000000000000000000000000000000000000000000000000000000000000007", - "0x9bb27a1b10a9bb6126bcb2c06a9e4652078397c740be1f5e04157dc52ffac4d4": "0x0000000000000000000000000000000000000000000000000000000000000008", - "0x9cd86cc67a7c7f78142ece24ba2acb60880094c09e8656fc01e523e103fef509": "0x0000000000000000000000000000000000000000000000000000000000000009", - "0x85f0b71cb682dcdb741ef2781c23d6f24b66e88c46277ede053a32775e94599d": "0x0000000000000000000000000000000000000000000000000000000000000001", - "0x99fce3416b2a42703e1f5bb2d612b8489dbbc954a45b4b21229ae44a22272d48": "0x0000000000000000000000000000000000000000000000000000000000000001" - } - }, - "0x4e59b44847b379578588920cA78FbF26c0B4956C": { - "balance": "0x0", - "nonce": "0x1", - "code": "0x7fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffe03601600081602082378035828234f58015156039578182fd5b8082525050506014600cf3" - }, - "0xcA11bde05977b3631167028862bE2a173976CA11": { - "balance": "0x0", - "nonce": "0x1", - "code": "0x6080604052600436106100f35760003560e01c80634d2301cc1161008a578063a8b0574e11610059578063a8b0574e1461025a578063bce38bd714610275578063c3077fa914610288578063ee82ac5e1461029b57600080fd5b80634d2301cc146101ec57806372425d9d1461022157806382ad56cb1461023457806386d516e81461024757600080fd5b80633408e470116100c65780633408e47014610191578063399542e9146101a45780633e64a696146101c657806342cbb15c146101d957600080fd5b80630f28c97d146100f8578063174dea711461011a578063252dba421461013a57806327e86d6e1461015b575b600080fd5b34801561010457600080fd5b50425b6040519081526020015b60405180910390f35b61012d610128366004610a85565b6102ba565b6040516101119190610bbe565b61014d610148366004610a85565b6104ef565b604051610111929190610bd8565b34801561016757600080fd5b50437fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff0140610107565b34801561019d57600080fd5b5046610107565b6101b76101b2366004610c60565b610690565b60405161011193929190610cba565b3480156101d257600080fd5b5048610107565b3480156101e557600080fd5b5043610107565b3480156101f857600080fd5b50610107610207366004610ce2565b73ffffffffffffffffffffffffffffffffffffffff163190565b34801561022d57600080fd5b5044610107565b61012d610242366004610a85565b6106ab565b34801561025357600080fd5b5045610107565b34801561026657600080fd5b50604051418152602001610111565b61012d610283366004610c60565b61085a565b6101b7610296366004610a85565b610a1a565b3480156102a757600080fd5b506101076102b6366004610d18565b4090565b60606000828067ffffffffffffffff8111156102d8576102d8610d31565b60405190808252806020026020018201604052801561031e57816020015b6040805180820190915260008152606060208201528152602001906001900390816102f65790505b5092503660005b8281101561047757600085828151811061034157610341610d60565b6020026020010151905087878381811061035d5761035d610d60565b905060200281019061036f9190610d8f565b6040810135958601959093506103886020850185610ce2565b73ffffffffffffffffffffffffffffffffffffffff16816103ac6060870187610dcd565b6040516103ba929190610e32565b60006040518083038185875af1925050503d80600081146103f7576040519150601f19603f3d011682016040523d82523d6000602084013e6103fc565b606091505b50602080850191909152901515808452908501351761046d577f08c379a000000000000000000000000000000000000000000000000000000000600052602060045260176024527f4d756c746963616c6c333a2063616c6c206661696c656400000000000000000060445260846000fd5b5050600101610325565b508234146104e6576040517f08c379a000000000000000000000000000000000000000000000000000000000815260206004820152601a60248201527f4d756c746963616c6c333a2076616c7565206d69736d6174636800000000000060448201526064015b60405180910390fd5b50505092915050565b436060828067ffffffffffffffff81111561050c5761050c610d31565b60405190808252806020026020018201604052801561053f57816020015b606081526020019060019003908161052a5790505b5091503660005b8281101561068657600087878381811061056257610562610d60565b90506020028101906105749190610e42565b92506105836020840184610ce2565b73ffffffffffffffffffffffffffffffffffffffff166105a66020850185610dcd565b6040516105b4929190610e32565b6000604051808303816000865af19150503d80600081146105f1576040519150601f19603f3d011682016040523d82523d6000602084013e6105f6565b606091505b5086848151811061060957610609610d60565b602090810291909101015290508061067d576040517f08c379a000000000000000000000000000000000000000000000000000000000815260206004820152601760248201527f4d756c746963616c6c333a2063616c6c206661696c656400000000000000000060448201526064016104dd565b50600101610546565b5050509250929050565b43804060606106a086868661085a565b905093509350939050565b6060818067ffffffffffffffff8111156106c7576106c7610d31565b60405190808252806020026020018201604052801561070d57816020015b6040805180820190915260008152606060208201528152602001906001900390816106e55790505b5091503660005b828110156104e657600084828151811061073057610730610d60565b6020026020010151905086868381811061074c5761074c610d60565b905060200281019061075e9190610e76565b925061076d6020840184610ce2565b73ffffffffffffffffffffffffffffffffffffffff166107906040850185610dcd565b60405161079e929190610e32565b6000604051808303816000865af19150503d80600081146107db576040519150601f19603f3d011682016040523d82523d6000602084013e6107e0565b606091505b506020808401919091529015158083529084013517610851577f08c379a000000000000000000000000000000000000000000000000000000000600052602060045260176024527f4d756c746963616c6c333a2063616c6c206661696c656400000000000000000060445260646000fd5b50600101610714565b6060818067ffffffffffffffff81111561087657610876610d31565b6040519080825280602002602001820160405280156108bc57816020015b6040805180820190915260008152606060208201528152602001906001900390816108945790505b5091503660005b82811015610a105760008482815181106108df576108df610d60565b602002602001015190508686838181106108fb576108fb610d60565b905060200281019061090d9190610e42565b925061091c6020840184610ce2565b73ffffffffffffffffffffffffffffffffffffffff1661093f6020850185610dcd565b60405161094d929190610e32565b6000604051808303816000865af19150503d806000811461098a576040519150601f19603f3d011682016040523d82523d6000602084013e61098f565b606091505b506020830152151581528715610a07578051610a07576040517f08c379a000000000000000000000000000000000000000000000000000000000815260206004820152601760248201527f4d756c746963616c6c333a2063616c6c206661696c656400000000000000000060448201526064016104dd565b506001016108c3565b5050509392505050565b6000806060610a2b60018686610690565b919790965090945092505050565b60008083601f840112610a4b57600080fd5b50813567ffffffffffffffff811115610a6357600080fd5b6020830191508360208260051b8501011115610a7e57600080fd5b9250929050565b60008060208385031215610a9857600080fd5b823567ffffffffffffffff811115610aaf57600080fd5b610abb85828601610a39565b90969095509350505050565b6000815180845260005b81811015610aed57602081850181015186830182015201610ad1565b81811115610aff576000602083870101525b50601f017fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffe0169290920160200192915050565b600082825180855260208086019550808260051b84010181860160005b84811015610bb1578583037fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffe001895281518051151584528401516040858501819052610b9d81860183610ac7565b9a86019a9450505090830190600101610b4f565b5090979650505050505050565b602081526000610bd16020830184610b32565b9392505050565b600060408201848352602060408185015281855180845260608601915060608160051b870101935082870160005b82811015610c52577fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffa0888703018452610c40868351610ac7565b95509284019290840190600101610c06565b509398975050505050505050565b600080600060408486031215610c7557600080fd5b83358015158114610c8557600080fd5b9250602084013567ffffffffffffffff811115610ca157600080fd5b610cad86828701610a39565b9497909650939450505050565b838152826020820152606060408201526000610cd96060830184610b32565b95945050505050565b600060208284031215610cf457600080fd5b813573ffffffffffffffffffffffffffffffffffffffff81168114610bd157600080fd5b600060208284031215610d2a57600080fd5b5035919050565b7f4e487b7100000000000000000000000000000000000000000000000000000000600052604160045260246000fd5b7f4e487b7100000000000000000000000000000000000000000000000000000000600052603260045260246000fd5b600082357fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff81833603018112610dc357600080fd5b9190910192915050565b60008083357fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffe1843603018112610e0257600080fd5b83018035915067ffffffffffffffff821115610e1d57600080fd5b602001915036819003821315610a7e57600080fd5b8183823760009101908152919050565b600082357fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffc1833603018112610dc357600080fd5b600082357fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffa1833603018112610dc357600080fdfea2646970667358221220bb2b5c71a328032f97c676ae39a1ec2148d3e5d6f73d95e9b17910152d61f16264736f6c634300080c0033" - }, - "0x000000000022D473030F116dDEE9F6B43aC78BA3": { - "balance": "0x0", - "nonce": "0x1", - "code": "0x6040608081526004908136101561001557600080fd5b600090813560e01c80630d58b1db1461126c578063137c29fe146110755780632a2d80d114610db75780632b67b57014610bde57806330f28b7a14610ade5780633644e51514610a9d57806336c7851614610a285780633ff9dcb1146109a85780634fe02b441461093f57806365d9723c146107ac57806387517c451461067a578063927da105146105c3578063cc53287f146104a3578063edd9444b1461033a5763fe8ec1a7146100c657600080fd5b346103365760c07ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffc3601126103365767ffffffffffffffff833581811161033257610114903690860161164b565b60243582811161032e5761012b903690870161161a565b6101336114e6565b9160843585811161032a5761014b9036908a016115c1565b98909560a43590811161032657610164913691016115c1565b969095815190610173826113ff565b606b82527f5065726d697442617463685769746e6573735472616e7366657246726f6d285460208301527f6f6b656e5065726d697373696f6e735b5d207065726d69747465642c61646472838301527f657373207370656e6465722c75696e74323536206e6f6e63652c75696e74323560608301527f3620646561646c696e652c000000000000000000000000000000000000000000608083015282519a8b9181610222602085018096611f93565b918237018a8152039961025b7fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffe09b8c8101835282611437565b5190209085515161026b81611ebb565b908a5b8181106102f95750506102f6999a6102ed9183516102a081610294602082018095611f66565b03848101835282611437565b519020602089810151858b015195519182019687526040820192909252336060820152608081019190915260a081019390935260643560c08401528260e081015b03908101835282611437565b51902093611cf7565b80f35b8061031161030b610321938c5161175e565b51612054565b61031b828661175e565b52611f0a565b61026e565b8880fd5b8780fd5b8480fd5b8380fd5b5080fd5b5091346103365760807ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffc3601126103365767ffffffffffffffff9080358281116103325761038b903690830161164b565b60243583811161032e576103a2903690840161161a565b9390926103ad6114e6565b9160643590811161049f576103c4913691016115c1565b949093835151976103d489611ebb565b98885b81811061047d5750506102f697988151610425816103f9602082018095611f66565b037fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffe08101835282611437565b5190206020860151828701519083519260208401947ffcf35f5ac6a2c28868dc44c302166470266239195f02b0ee408334829333b7668652840152336060840152608083015260a082015260a081526102ed8161141b565b808b61031b8261049461030b61049a968d5161175e565b9261175e565b6103d7565b8680fd5b5082346105bf57602090817ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffc3601126103325780359067ffffffffffffffff821161032e576104f49136910161161a565b929091845b848110610504578580f35b8061051a610515600193888861196c565b61197c565b61052f84610529848a8a61196c565b0161197c565b3389528385528589209173ffffffffffffffffffffffffffffffffffffffff80911692838b528652868a20911690818a5285528589207fffffffffffffffffffffffff000000000000000000000000000000000000000081541690558551918252848201527f89b1add15eff56b3dfe299ad94e01f2b52fbcb80ae1a3baea6ae8c04cb2b98a4853392a2016104f9565b8280fd5b50346103365760607ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffc36011261033657610676816105ff6114a0565b936106086114c3565b6106106114e6565b73ffffffffffffffffffffffffffffffffffffffff968716835260016020908152848420928816845291825283832090871683528152919020549251938316845260a083901c65ffffffffffff169084015260d09190911c604083015281906060820190565b0390f35b50346103365760807ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffc360112610336576106b26114a0565b906106bb6114c3565b916106c46114e6565b65ffffffffffff926064358481169081810361032a5779ffffffffffff0000000000000000000000000000000000000000947fda9fa7c1b00402c17d0161b249b1ab8bbec047c5a52207b9c112deffd817036b94338a5260016020527fffffffffffff0000000000000000000000000000000000000000000000000000858b209873ffffffffffffffffffffffffffffffffffffffff809416998a8d5260205283878d209b169a8b8d52602052868c209486156000146107a457504216925b8454921697889360a01b16911617179055815193845260208401523392a480f35b905092610783565b5082346105bf5760607ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffc3601126105bf576107e56114a0565b906107ee6114c3565b9265ffffffffffff604435818116939084810361032a57338852602091600183528489209673ffffffffffffffffffffffffffffffffffffffff80911697888b528452858a20981697888a5283528489205460d01c93848711156109175761ffff9085840316116108f05750907f55eb90d810e1700b35a8e7e25395ff7f2b2259abd7415ca2284dfb1c246418f393929133895260018252838920878a528252838920888a5282528389209079ffffffffffffffffffffffffffffffffffffffffffffffffffff7fffffffffffff000000000000000000000000000000000000000000000000000083549260d01b16911617905582519485528401523392a480f35b84517f24d35a26000000000000000000000000000000000000000000000000000000008152fd5b5084517f756688fe000000000000000000000000000000000000000000000000000000008152fd5b503461033657807ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffc360112610336578060209273ffffffffffffffffffffffffffffffffffffffff61098f6114a0565b1681528084528181206024358252845220549051908152f35b5082346105bf57817ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffc3601126105bf577f3704902f963766a4e561bbaab6e6cdc1b1dd12f6e9e99648da8843b3f46b918d90359160243533855284602052818520848652602052818520818154179055815193845260208401523392a280f35b8234610a9a5760807ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffc360112610a9a57610a606114a0565b610a686114c3565b610a706114e6565b6064359173ffffffffffffffffffffffffffffffffffffffff8316830361032e576102f6936117a1565b80fd5b503461033657817ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffc36011261033657602090610ad7611b1e565b9051908152f35b508290346105bf576101007ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffc3601126105bf57610b1a3661152a565b90807fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff7c36011261033257610b4c611478565b9160e43567ffffffffffffffff8111610bda576102f694610b6f913691016115c1565b939092610b7c8351612054565b6020840151828501519083519260208401947f939c21a48a8dbe3a9a2404a1d46691e4d39f6583d6ec6b35714604c986d801068652840152336060840152608083015260a082015260a08152610bd18161141b565b51902091611c25565b8580fd5b509134610336576101007ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffc36011261033657610c186114a0565b7fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffdc360160c08112610332576080855191610c51836113e3565b1261033257845190610c6282611398565b73ffffffffffffffffffffffffffffffffffffffff91602435838116810361049f578152604435838116810361049f57602082015265ffffffffffff606435818116810361032a5788830152608435908116810361049f576060820152815260a435938285168503610bda576020820194855260c4359087830182815260e43567ffffffffffffffff811161032657610cfe90369084016115c1565b929093804211610d88575050918591610d786102f6999a610d7e95610d238851611fbe565b90898c511690519083519260208401947ff3841cd1ff0085026a6327b620b67997ce40f282c88a8e905a7a5626e310f3d086528401526060830152608082015260808152610d70816113ff565b519020611bd9565b916120c7565b519251169161199d565b602492508a51917fcd21db4f000000000000000000000000000000000000000000000000000000008352820152fd5b5091346103365760607ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffc93818536011261033257610df36114a0565b9260249081359267ffffffffffffffff9788851161032a578590853603011261049f578051978589018981108282111761104a578252848301358181116103265785019036602383011215610326578382013591610e50836115ef565b90610e5d85519283611437565b838252602093878584019160071b83010191368311611046578801905b828210610fe9575050508a526044610e93868801611509565b96838c01978852013594838b0191868352604435908111610fe557610ebb90369087016115c1565b959096804211610fba575050508998995151610ed681611ebb565b908b5b818110610f9757505092889492610d7892610f6497958351610f02816103f98682018095611f66565b5190209073ffffffffffffffffffffffffffffffffffffffff9a8b8b51169151928551948501957faf1b0d30d2cab0380e68f0689007e3254993c596f2fdd0aaa7f4d04f794408638752850152830152608082015260808152610d70816113ff565b51169082515192845b848110610f78578580f35b80610f918585610f8b600195875161175e565b5161199d565b01610f6d565b80610311610fac8e9f9e93610fb2945161175e565b51611fbe565b9b9a9b610ed9565b8551917fcd21db4f000000000000000000000000000000000000000000000000000000008352820152fd5b8a80fd5b6080823603126110465785608091885161100281611398565b61100b85611509565b8152611018838601611509565b838201526110278a8601611607565b8a8201528d611037818701611607565b90820152815201910190610e7a565b8c80fd5b84896041867f4e487b7100000000000000000000000000000000000000000000000000000000835252fd5b5082346105bf576101407ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffc3601126105bf576110b03661152a565b91807fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff7c360112610332576110e2611478565b67ffffffffffffffff93906101043585811161049f5761110590369086016115c1565b90936101243596871161032a57611125610bd1966102f6983691016115c1565b969095825190611134826113ff565b606482527f5065726d69745769746e6573735472616e7366657246726f6d28546f6b656e5060208301527f65726d697373696f6e73207065726d69747465642c6164647265737320737065848301527f6e6465722c75696e74323536206e6f6e63652c75696e7432353620646561646c60608301527f696e652c0000000000000000000000000000000000000000000000000000000060808301528351948591816111e3602085018096611f93565b918237018b8152039361121c7fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffe095868101835282611437565b5190209261122a8651612054565b6020878101518589015195519182019687526040820192909252336060820152608081019190915260a081019390935260e43560c08401528260e081016102e1565b5082346105bf576020807ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffc36011261033257813567ffffffffffffffff92838211610bda5736602383011215610bda5781013592831161032e576024906007368386831b8401011161049f57865b8581106112e5578780f35b80821b83019060807fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffdc83360301126103265761139288876001946060835161132c81611398565b611368608461133c8d8601611509565b9485845261134c60448201611509565b809785015261135d60648201611509565b809885015201611509565b918291015273ffffffffffffffffffffffffffffffffffffffff80808093169516931691166117a1565b016112da565b6080810190811067ffffffffffffffff8211176113b457604052565b7f4e487b7100000000000000000000000000000000000000000000000000000000600052604160045260246000fd5b6060810190811067ffffffffffffffff8211176113b457604052565b60a0810190811067ffffffffffffffff8211176113b457604052565b60c0810190811067ffffffffffffffff8211176113b457604052565b90601f7fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffe0910116810190811067ffffffffffffffff8211176113b457604052565b60c4359073ffffffffffffffffffffffffffffffffffffffff8216820361149b57565b600080fd5b6004359073ffffffffffffffffffffffffffffffffffffffff8216820361149b57565b6024359073ffffffffffffffffffffffffffffffffffffffff8216820361149b57565b6044359073ffffffffffffffffffffffffffffffffffffffff8216820361149b57565b359073ffffffffffffffffffffffffffffffffffffffff8216820361149b57565b7ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffc01906080821261149b576040805190611563826113e3565b8082941261149b57805181810181811067ffffffffffffffff8211176113b457825260043573ffffffffffffffffffffffffffffffffffffffff8116810361149b578152602435602082015282526044356020830152606435910152565b9181601f8401121561149b5782359167ffffffffffffffff831161149b576020838186019501011161149b57565b67ffffffffffffffff81116113b45760051b60200190565b359065ffffffffffff8216820361149b57565b9181601f8401121561149b5782359167ffffffffffffffff831161149b576020808501948460061b01011161149b57565b91909160608184031261149b576040805191611666836113e3565b8294813567ffffffffffffffff9081811161149b57830182601f8201121561149b578035611693816115ef565b926116a087519485611437565b818452602094858086019360061b8501019381851161149b579086899897969594939201925b8484106116e3575050505050855280820135908501520135910152565b90919293949596978483031261149b578851908982019082821085831117611730578a928992845261171487611509565b81528287013583820152815201930191908897969594936116c6565b602460007f4e487b710000000000000000000000000000000000000000000000000000000081526041600452fd5b80518210156117725760209160051b010190565b7f4e487b7100000000000000000000000000000000000000000000000000000000600052603260045260246000fd5b92919273ffffffffffffffffffffffffffffffffffffffff604060008284168152600160205282828220961695868252602052818120338252602052209485549565ffffffffffff8760a01c16804211611884575082871696838803611812575b5050611810955016926118b5565b565b878484161160001461184f57602488604051907ff96fb0710000000000000000000000000000000000000000000000000000000082526004820152fd5b7fffffffffffffffffffffffff000000000000000000000000000000000000000084846118109a031691161790553880611802565b602490604051907fd81b2f2e0000000000000000000000000000000000000000000000000000000082526004820152fd5b9060006064926020958295604051947f23b872dd0000000000000000000000000000000000000000000000000000000086526004860152602485015260448401525af13d15601f3d116001600051141617161561190e57565b60646040517f08c379a000000000000000000000000000000000000000000000000000000000815260206004820152601460248201527f5452414e534645525f46524f4d5f4641494c45440000000000000000000000006044820152fd5b91908110156117725760061b0190565b3573ffffffffffffffffffffffffffffffffffffffff8116810361149b5790565b9065ffffffffffff908160608401511673ffffffffffffffffffffffffffffffffffffffff908185511694826020820151169280866040809401511695169560009187835260016020528383208984526020528383209916988983526020528282209184835460d01c03611af5579185611ace94927fc6a377bfc4eb120024a8ac08eef205be16b817020812c73223e81d1bdb9708ec98979694508715600014611ad35779ffffffffffff00000000000000000000000000000000000000009042165b60a01b167fffffffffffff00000000000000000000000000000000000000000000000000006001860160d01b1617179055519384938491604091949373ffffffffffffffffffffffffffffffffffffffff606085019616845265ffffffffffff809216602085015216910152565b0390a4565b5079ffffffffffff000000000000000000000000000000000000000087611a60565b600484517f756688fe000000000000000000000000000000000000000000000000000000008152fd5b467f00000000000000000000000000000000000000000000000000000000004cef5103611b69577fb8191940c73685712f6c444828a36b9cf4324bf9202be67442f9eea0b24fdaed90565b60405160208101907f8cad95687ba82c2ce50e74f7b754645e5117c3a5bec8151c0726d5857980a86682527f9ac997416e8ff9d2ff6bebeb7149f65cdae5e32e2b90440b566bb3044041d36a604082015246606082015230608082015260808152611bd3816113ff565b51902090565b611be1611b1e565b906040519060208201927f190100000000000000000000000000000000000000000000000000000000000084526022830152604282015260428152611bd381611398565b9192909360a435936040840151804211611cc65750602084510151808611611c955750918591610d78611c6594611c60602088015186611e47565b611bd9565b73ffffffffffffffffffffffffffffffffffffffff809151511692608435918216820361149b57611810936118b5565b602490604051907f3728b83d0000000000000000000000000000000000000000000000000000000082526004820152fd5b602490604051907fcd21db4f0000000000000000000000000000000000000000000000000000000082526004820152fd5b959093958051519560409283830151804211611e175750848803611dee57611d2e918691610d7860209b611c608d88015186611e47565b60005b868110611d42575050505050505050565b611d4d81835161175e565b5188611d5a83878a61196c565b01359089810151808311611dbe575091818888886001968596611d84575b50505050505001611d31565b611db395611dad9273ffffffffffffffffffffffffffffffffffffffff6105159351169561196c565b916118b5565b803888888883611d78565b6024908651907f3728b83d0000000000000000000000000000000000000000000000000000000082526004820152fd5b600484517fff633a38000000000000000000000000000000000000000000000000000000008152fd5b6024908551907fcd21db4f0000000000000000000000000000000000000000000000000000000082526004820152fd5b9073ffffffffffffffffffffffffffffffffffffffff600160ff83161b9216600052600060205260406000209060081c6000526020526040600020818154188091551615611e9157565b60046040517f756688fe000000000000000000000000000000000000000000000000000000008152fd5b90611ec5826115ef565b611ed26040519182611437565b8281527fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffe0611f0082946115ef565b0190602036910137565b7fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff8114611f375760010190565b7f4e487b7100000000000000000000000000000000000000000000000000000000600052601160045260246000fd5b805160208092019160005b828110611f7f575050505090565b835185529381019392810192600101611f71565b9081519160005b838110611fab575050016000815290565b8060208092840101518185015201611f9a565b60405160208101917f65626cad6cb96493bf6f5ebea28756c966f023ab9e8a83a7101849d5573b3678835273ffffffffffffffffffffffffffffffffffffffff8082511660408401526020820151166060830152606065ffffffffffff9182604082015116608085015201511660a082015260a0815260c0810181811067ffffffffffffffff8211176113b45760405251902090565b6040516020808201927f618358ac3db8dc274f0cd8829da7e234bd48cd73c4a740aede1adec9846d06a1845273ffffffffffffffffffffffffffffffffffffffff81511660408401520151606082015260608152611bd381611398565b919082604091031261149b576020823592013590565b6000843b61222e5750604182036121ac576120e4828201826120b1565b939092604010156117725760209360009360ff6040608095013560f81c5b60405194855216868401526040830152606082015282805260015afa156121a05773ffffffffffffffffffffffffffffffffffffffff806000511691821561217657160361214c57565b60046040517f815e1d64000000000000000000000000000000000000000000000000000000008152fd5b60046040517f8baa579f000000000000000000000000000000000000000000000000000000008152fd5b6040513d6000823e3d90fd5b60408203612204576121c0918101906120b1565b91601b7f7fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff84169360ff1c019060ff8211611f375760209360009360ff608094612102565b60046040517f4be6321b000000000000000000000000000000000000000000000000000000008152fd5b929391601f928173ffffffffffffffffffffffffffffffffffffffff60646020957fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffe0604051988997889687947f1626ba7e000000000000000000000000000000000000000000000000000000009e8f8752600487015260406024870152816044870152868601378b85828601015201168101030192165afa9081156123a857829161232a575b507fffffffff000000000000000000000000000000000000000000000000000000009150160361230057565b60046040517fb0669cbc000000000000000000000000000000000000000000000000000000008152fd5b90506020813d82116123a0575b8161234460209383611437565b810103126103365751907fffffffff0000000000000000000000000000000000000000000000000000000082168203610a9a57507fffffffff0000000000000000000000000000000000000000000000000000000090386122d4565b3d9150612337565b6040513d84823e3d90fdfea164736f6c6343000811000a" - }, - "0x1800000000000000000000000000000000000002": { - "balance": "0x0", - "nonce": "0x1", - "code": "0x01" - }, - "0x1800000000000000000000000000000000000003": { - "balance": "0x0", - "nonce": "0x1", - "code": "0x01" - }, - "0x1800000000000000000000000000000000000004": { - "balance": "0x0", - "nonce": "0x1", - "code": "0x01" - }, - "0x1800000000000000000000000000000000000005": { - "balance": "0x0", - "nonce": "0x1", - "code": "0x01" - }, - "0x1800000000000000000000000000000000000006": { - "balance": "0x0", - "nonce": "0x1", - "code": "0x01" - }, - "0x1800000000000000000000000000000000000007": { - "balance": "0x0", - "nonce": "0x1", - "code": "0x01" - }, - "0x1800000000000000000000000000000000000008": { - "balance": "0x0", - "nonce": "0x1", - "code": "0x01" - }, - "0x1800000000000000000000000000000000000009": { - "balance": "0x0", - "nonce": "0x1", - "code": "0x01" - }, - "0x180000000000000000000000000000000000000a": { - "balance": "0x0", - "nonce": "0x1", - "code": "0x01" - }, - "0x180000000000000000000000000000000000000b": { - "balance": "0x0", - "nonce": "0x1", - "code": "0x01" - }, - "0x180000000000000000000000000000000000000c": { - "balance": "0x0", - "nonce": "0x1", - "code": "0x01" - }, - "0x180000000000000000000000000000000000000d": { - "balance": "0x0", - "nonce": "0x1", - "code": "0x01" - }, - "0x180000000000000000000000000000000000000e": { - "balance": "0x0", - "nonce": "0x1", - "code": "0x01" - }, - "0x180000000000000000000000000000000000000f": { - "balance": "0x0", - "nonce": "0x1", - "code": "0x01" - }, - "0x1800000000000000000000000000000000000010": { - "balance": "0x0", - "nonce": "0x1", - "code": "0x01" - }, - "0x1800000000000000000000000000000000000011": { - "balance": "0x0", - "nonce": "0x1", - "code": "0x01" - }, - "0x1800000000000000000000000000000000000012": { - "balance": "0x0", - "nonce": "0x1", - "code": "0x01" - }, - "0x1800000000000000000000000000000000000013": { - "balance": "0x0", - "nonce": "0x1", - "code": "0x01" - }, - "0x1800000000000000000000000000000000000014": { - "balance": "0x0", - "nonce": "0x1", - "code": "0x01" - }, - "0x1800000000000000000000000000000000000015": { - "balance": "0x0", - "nonce": "0x1", - "code": "0x01" - }, - "0x1800000000000000000000000000000000000016": { - "balance": "0x0", - "nonce": "0x1", - "code": "0x01" - }, - "0x1800000000000000000000000000000000000017": { - "balance": "0x0", - "nonce": "0x1", - "code": "0x01" - }, - "0x1800000000000000000000000000000000000018": { - "balance": "0x0", - "nonce": "0x1", - "code": "0x01" - }, - "0x1800000000000000000000000000000000000019": { - "balance": "0x0", - "nonce": "0x1", - "code": "0x01" - }, - "0x180000000000000000000000000000000000001a": { - "balance": "0x0", - "nonce": "0x1", - "code": "0x01" - }, - "0x180000000000000000000000000000000000001b": { - "balance": "0x0", - "nonce": "0x1", - "code": "0x01" - }, - "0x180000000000000000000000000000000000001c": { - "balance": "0x0", - "nonce": "0x1", - "code": "0x01" - }, - "0x180000000000000000000000000000000000001d": { - "balance": "0x0", - "nonce": "0x1", - "code": "0x01" - }, - "0x180000000000000000000000000000000000001e": { - "balance": "0x0", - "nonce": "0x1", - "code": "0x01" - }, - "0x180000000000000000000000000000000000001f": { - "balance": "0x0", - "nonce": "0x1", - "code": "0x01" - }, - "0x1800000000000000000000000000000000000020": { - "balance": "0x0", - "nonce": "0x1", - "code": "0x01" - }, - "0x1800000000000000000000000000000000000021": { - "balance": "0x0", - "nonce": "0x1", - "code": "0x01" - }, - "0x1800000000000000000000000000000000000022": { - "balance": "0x0", - "nonce": "0x1", - "code": "0x01" - }, - "0x1800000000000000000000000000000000000023": { - "balance": "0x0", - "nonce": "0x1", - "code": "0x01" - }, - "0x1800000000000000000000000000000000000024": { - "balance": "0x0", - "nonce": "0x1", - "code": "0x01" - }, - "0x1800000000000000000000000000000000000025": { - "balance": "0x0", - "nonce": "0x1", - "code": "0x01" - }, - "0x1800000000000000000000000000000000000026": { - "balance": "0x0", - "nonce": "0x1", - "code": "0x01" - }, - "0x1800000000000000000000000000000000000027": { - "balance": "0x0", - "nonce": "0x1", - "code": "0x01" - }, - "0x1800000000000000000000000000000000000028": { - "balance": "0x0", - "nonce": "0x1", - "code": "0x01" - }, - "0x1800000000000000000000000000000000000029": { - "balance": "0x0", - "nonce": "0x1", - "code": "0x01" - }, - "0x180000000000000000000000000000000000002a": { - "balance": "0x0", - "nonce": "0x1", - "code": "0x01" - }, - "0x180000000000000000000000000000000000002b": { - "balance": "0x0", - "nonce": "0x1", - "code": "0x01" - }, - "0x180000000000000000000000000000000000002c": { - "balance": "0x0", - "nonce": "0x1", - "code": "0x01" - }, - "0x180000000000000000000000000000000000002d": { - "balance": "0x0", - "nonce": "0x1", - "code": "0x01" - }, - "0x180000000000000000000000000000000000002e": { - "balance": "0x0", - "nonce": "0x1", - "code": "0x01" - }, - "0x180000000000000000000000000000000000002f": { - "balance": "0x0", - "nonce": "0x1", - "code": "0x01" - }, - "0x1800000000000000000000000000000000000030": { - "balance": "0x0", - "nonce": "0x1", - "code": "0x01" - }, - "0x1800000000000000000000000000000000000031": { - "balance": "0x0", - "nonce": "0x1", - "code": "0x01" - }, - "0x1800000000000000000000000000000000000032": { - "balance": "0x0", - "nonce": "0x1", - "code": "0x01" - }, - "0x1800000000000000000000000000000000000033": { - "balance": "0x0", - "nonce": "0x1", - "code": "0x01" - }, - "0x1800000000000000000000000000000000000034": { - "balance": "0x0", - "nonce": "0x1", - "code": "0x01" - }, - "0x1800000000000000000000000000000000000035": { - "balance": "0x0", - "nonce": "0x1", - "code": "0x01" - }, - "0x1800000000000000000000000000000000000036": { - "balance": "0x0", - "nonce": "0x1", - "code": "0x01" - }, - "0x1800000000000000000000000000000000000037": { - "balance": "0x0", - "nonce": "0x1", - "code": "0x01" - }, - "0x1800000000000000000000000000000000000038": { - "balance": "0x0", - "nonce": "0x1", - "code": "0x01" - }, - "0x1800000000000000000000000000000000000039": { - "balance": "0x0", - "nonce": "0x1", - "code": "0x01" - }, - "0x180000000000000000000000000000000000003a": { - "balance": "0x0", - "nonce": "0x1", - "code": "0x01" - }, - "0x180000000000000000000000000000000000003b": { - "balance": "0x0", - "nonce": "0x1", - "code": "0x01" - }, - "0x180000000000000000000000000000000000003c": { - "balance": "0x0", - "nonce": "0x1", - "code": "0x01" - }, - "0x180000000000000000000000000000000000003d": { - "balance": "0x0", - "nonce": "0x1", - "code": "0x01" - }, - "0x180000000000000000000000000000000000003e": { - "balance": "0x0", - "nonce": "0x1", - "code": "0x01" - }, - "0x180000000000000000000000000000000000003f": { - "balance": "0x0", - "nonce": "0x1", - "code": "0x01" - }, - "0x1800000000000000000000000000000000000040": { - "balance": "0x0", - "nonce": "0x1", - "code": "0x01" - }, - "0x1800000000000000000000000000000000000041": { - "balance": "0x0", - "nonce": "0x1", - "code": "0x01" - }, - "0x1800000000000000000000000000000000000042": { - "balance": "0x0", - "nonce": "0x1", - "code": "0x01" - }, - "0x1800000000000000000000000000000000000043": { - "balance": "0x0", - "nonce": "0x1", - "code": "0x01" - }, - "0x1800000000000000000000000000000000000044": { - "balance": "0x0", - "nonce": "0x1", - "code": "0x01" - }, - "0x1800000000000000000000000000000000000045": { - "balance": "0x0", - "nonce": "0x1", - "code": "0x01" - }, - "0x1800000000000000000000000000000000000046": { - "balance": "0x0", - "nonce": "0x1", - "code": "0x01" - }, - "0x1800000000000000000000000000000000000047": { - "balance": "0x0", - "nonce": "0x1", - "code": "0x01" - }, - "0x1800000000000000000000000000000000000048": { - "balance": "0x0", - "nonce": "0x1", - "code": "0x01" - }, - "0x1800000000000000000000000000000000000049": { - "balance": "0x0", - "nonce": "0x1", - "code": "0x01" - }, - "0x180000000000000000000000000000000000004a": { - "balance": "0x0", - "nonce": "0x1", - "code": "0x01" - }, - "0x180000000000000000000000000000000000004b": { - "balance": "0x0", - "nonce": "0x1", - "code": "0x01" - }, - "0x180000000000000000000000000000000000004c": { - "balance": "0x0", - "nonce": "0x1", - "code": "0x01" - }, - "0x180000000000000000000000000000000000004d": { - "balance": "0x0", - "nonce": "0x1", - "code": "0x01" - }, - "0x180000000000000000000000000000000000004e": { - "balance": "0x0", - "nonce": "0x1", - "code": "0x01" - }, - "0x180000000000000000000000000000000000004f": { - "balance": "0x0", - "nonce": "0x1", - "code": "0x01" - }, - "0x1800000000000000000000000000000000000050": { - "balance": "0x0", - "nonce": "0x1", - "code": "0x01" - }, - "0x1800000000000000000000000000000000000051": { - "balance": "0x0", - "nonce": "0x1", - "code": "0x01" - }, - "0x1800000000000000000000000000000000000052": { - "balance": "0x0", - "nonce": "0x1", - "code": "0x01" - }, - "0x1800000000000000000000000000000000000053": { - "balance": "0x0", - "nonce": "0x1", - "code": "0x01" - }, - "0x1800000000000000000000000000000000000054": { - "balance": "0x0", - "nonce": "0x1", - "code": "0x01" - }, - "0x1800000000000000000000000000000000000055": { - "balance": "0x0", - "nonce": "0x1", - "code": "0x01" - }, - "0x1800000000000000000000000000000000000056": { - "balance": "0x0", - "nonce": "0x1", - "code": "0x01" - }, - "0x1800000000000000000000000000000000000057": { - "balance": "0x0", - "nonce": "0x1", - "code": "0x01" - }, - "0x1800000000000000000000000000000000000058": { - "balance": "0x0", - "nonce": "0x1", - "code": "0x01" - }, - "0x1800000000000000000000000000000000000059": { - "balance": "0x0", - "nonce": "0x1", - "code": "0x01" - }, - "0x180000000000000000000000000000000000005a": { - "balance": "0x0", - "nonce": "0x1", - "code": "0x01" - }, - "0x180000000000000000000000000000000000005b": { - "balance": "0x0", - "nonce": "0x1", - "code": "0x01" - }, - "0x180000000000000000000000000000000000005c": { - "balance": "0x0", - "nonce": "0x1", - "code": "0x01" - }, - "0x180000000000000000000000000000000000005d": { - "balance": "0x0", - "nonce": "0x1", - "code": "0x01" - }, - "0x180000000000000000000000000000000000005e": { - "balance": "0x0", - "nonce": "0x1", - "code": "0x01" - }, - "0x180000000000000000000000000000000000005f": { - "balance": "0x0", - "nonce": "0x1", - "code": "0x01" - }, - "0x1800000000000000000000000000000000000060": { - "balance": "0x0", - "nonce": "0x1", - "code": "0x01" - }, - "0x1800000000000000000000000000000000000061": { - "balance": "0x0", - "nonce": "0x1", - "code": "0x01" - }, - "0x1800000000000000000000000000000000000062": { - "balance": "0x0", - "nonce": "0x1", - "code": "0x01" - }, - "0x1800000000000000000000000000000000000063": { - "balance": "0x0", - "nonce": "0x1", - "code": "0x01" - }, - "0x1800000000000000000000000000000000000064": { - "balance": "0x0", - "nonce": "0x1", - "code": "0x01" - }, - "0x1800000000000000000000000000000000000065": { - "balance": "0x0", - "nonce": "0x1", - "code": "0x01" - }, - "0x1800000000000000000000000000000000000066": { - "balance": "0x0", - "nonce": "0x1", - "code": "0x01" - }, - "0x1800000000000000000000000000000000000067": { - "balance": "0x0", - "nonce": "0x1", - "code": "0x01" - }, - "0x1800000000000000000000000000000000000068": { - "balance": "0x0", - "nonce": "0x1", - "code": "0x01" - }, - "0x1800000000000000000000000000000000000069": { - "balance": "0x0", - "nonce": "0x1", - "code": "0x01" - }, - "0x180000000000000000000000000000000000006a": { - "balance": "0x0", - "nonce": "0x1", - "code": "0x01" - }, - "0x180000000000000000000000000000000000006b": { - "balance": "0x0", - "nonce": "0x1", - "code": "0x01" - }, - "0x180000000000000000000000000000000000006c": { - "balance": "0x0", - "nonce": "0x1", - "code": "0x01" - }, - "0x180000000000000000000000000000000000006d": { - "balance": "0x0", - "nonce": "0x1", - "code": "0x01" - }, - "0x180000000000000000000000000000000000006e": { - "balance": "0x0", - "nonce": "0x1", - "code": "0x01" - }, - "0x180000000000000000000000000000000000006f": { - "balance": "0x0", - "nonce": "0x1", - "code": "0x01" - }, - "0x1800000000000000000000000000000000000070": { - "balance": "0x0", - "nonce": "0x1", - "code": "0x01" - }, - "0x1800000000000000000000000000000000000071": { - "balance": "0x0", - "nonce": "0x1", - "code": "0x01" - }, - "0x1800000000000000000000000000000000000072": { - "balance": "0x0", - "nonce": "0x1", - "code": "0x01" - }, - "0x1800000000000000000000000000000000000073": { - "balance": "0x0", - "nonce": "0x1", - "code": "0x01" - }, - "0x1800000000000000000000000000000000000074": { - "balance": "0x0", - "nonce": "0x1", - "code": "0x01" - }, - "0x1800000000000000000000000000000000000075": { - "balance": "0x0", - "nonce": "0x1", - "code": "0x01" - }, - "0x1800000000000000000000000000000000000076": { - "balance": "0x0", - "nonce": "0x1", - "code": "0x01" - }, - "0x1800000000000000000000000000000000000077": { - "balance": "0x0", - "nonce": "0x1", - "code": "0x01" - }, - "0x1800000000000000000000000000000000000078": { - "balance": "0x0", - "nonce": "0x1", - "code": "0x01" - }, - "0x1800000000000000000000000000000000000079": { - "balance": "0x0", - "nonce": "0x1", - "code": "0x01" - }, - "0x180000000000000000000000000000000000007a": { - "balance": "0x0", - "nonce": "0x1", - "code": "0x01" - }, - "0x180000000000000000000000000000000000007b": { - "balance": "0x0", - "nonce": "0x1", - "code": "0x01" - }, - "0x180000000000000000000000000000000000007c": { - "balance": "0x0", - "nonce": "0x1", - "code": "0x01" - }, - "0x180000000000000000000000000000000000007d": { - "balance": "0x0", - "nonce": "0x1", - "code": "0x01" - }, - "0x180000000000000000000000000000000000007e": { - "balance": "0x0", - "nonce": "0x1", - "code": "0x01" - }, - "0x180000000000000000000000000000000000007f": { - "balance": "0x0", - "nonce": "0x1", - "code": "0x01" - }, - "0x1800000000000000000000000000000000000080": { - "balance": "0x0", - "nonce": "0x1", - "code": "0x01" - }, - "0x1800000000000000000000000000000000000081": { - "balance": "0x0", - "nonce": "0x1", - "code": "0x01" - }, - "0x1800000000000000000000000000000000000082": { - "balance": "0x0", - "nonce": "0x1", - "code": "0x01" - }, - "0x1800000000000000000000000000000000000083": { - "balance": "0x0", - "nonce": "0x1", - "code": "0x01" - }, - "0x1800000000000000000000000000000000000084": { - "balance": "0x0", - "nonce": "0x1", - "code": "0x01" - }, - "0x1800000000000000000000000000000000000085": { - "balance": "0x0", - "nonce": "0x1", - "code": "0x01" - }, - "0x1800000000000000000000000000000000000086": { - "balance": "0x0", - "nonce": "0x1", - "code": "0x01" - }, - "0x1800000000000000000000000000000000000087": { - "balance": "0x0", - "nonce": "0x1", - "code": "0x01" - }, - "0x1800000000000000000000000000000000000088": { - "balance": "0x0", - "nonce": "0x1", - "code": "0x01" - }, - "0x1800000000000000000000000000000000000089": { - "balance": "0x0", - "nonce": "0x1", - "code": "0x01" - }, - "0x180000000000000000000000000000000000008a": { - "balance": "0x0", - "nonce": "0x1", - "code": "0x01" - }, - "0x180000000000000000000000000000000000008b": { - "balance": "0x0", - "nonce": "0x1", - "code": "0x01" - }, - "0x180000000000000000000000000000000000008c": { - "balance": "0x0", - "nonce": "0x1", - "code": "0x01" - }, - "0x180000000000000000000000000000000000008d": { - "balance": "0x0", - "nonce": "0x1", - "code": "0x01" - }, - "0x180000000000000000000000000000000000008e": { - "balance": "0x0", - "nonce": "0x1", - "code": "0x01" - }, - "0x180000000000000000000000000000000000008f": { - "balance": "0x0", - "nonce": "0x1", - "code": "0x01" - }, - "0x1800000000000000000000000000000000000090": { - "balance": "0x0", - "nonce": "0x1", - "code": "0x01" - }, - "0x1800000000000000000000000000000000000091": { - "balance": "0x0", - "nonce": "0x1", - "code": "0x01" - }, - "0x1800000000000000000000000000000000000092": { - "balance": "0x0", - "nonce": "0x1", - "code": "0x01" - }, - "0x1800000000000000000000000000000000000093": { - "balance": "0x0", - "nonce": "0x1", - "code": "0x01" - }, - "0x1800000000000000000000000000000000000094": { - "balance": "0x0", - "nonce": "0x1", - "code": "0x01" - }, - "0x1800000000000000000000000000000000000095": { - "balance": "0x0", - "nonce": "0x1", - "code": "0x01" - }, - "0x1800000000000000000000000000000000000096": { - "balance": "0x0", - "nonce": "0x1", - "code": "0x01" - }, - "0x1800000000000000000000000000000000000097": { - "balance": "0x0", - "nonce": "0x1", - "code": "0x01" - }, - "0x1800000000000000000000000000000000000098": { - "balance": "0x0", - "nonce": "0x1", - "code": "0x01" - }, - "0x1800000000000000000000000000000000000099": { - "balance": "0x0", - "nonce": "0x1", - "code": "0x01" - }, - "0x180000000000000000000000000000000000009a": { - "balance": "0x0", - "nonce": "0x1", - "code": "0x01" - }, - "0x180000000000000000000000000000000000009b": { - "balance": "0x0", - "nonce": "0x1", - "code": "0x01" - }, - "0x180000000000000000000000000000000000009c": { - "balance": "0x0", - "nonce": "0x1", - "code": "0x01" - }, - "0x180000000000000000000000000000000000009d": { - "balance": "0x0", - "nonce": "0x1", - "code": "0x01" - }, - "0x180000000000000000000000000000000000009e": { - "balance": "0x0", - "nonce": "0x1", - "code": "0x01" - }, - "0x180000000000000000000000000000000000009f": { - "balance": "0x0", - "nonce": "0x1", - "code": "0x01" - }, - "0x18000000000000000000000000000000000000a0": { - "balance": "0x0", - "nonce": "0x1", - "code": "0x01" - }, - "0x18000000000000000000000000000000000000a1": { - "balance": "0x0", - "nonce": "0x1", - "code": "0x01" - }, - "0x18000000000000000000000000000000000000a2": { - "balance": "0x0", - "nonce": "0x1", - "code": "0x01" - }, - "0x18000000000000000000000000000000000000a3": { - "balance": "0x0", - "nonce": "0x1", - "code": "0x01" - }, - "0x18000000000000000000000000000000000000a4": { - "balance": "0x0", - "nonce": "0x1", - "code": "0x01" - }, - "0x18000000000000000000000000000000000000a5": { - "balance": "0x0", - "nonce": "0x1", - "code": "0x01" - }, - "0x18000000000000000000000000000000000000a6": { - "balance": "0x0", - "nonce": "0x1", - "code": "0x01" - }, - "0x18000000000000000000000000000000000000a7": { - "balance": "0x0", - "nonce": "0x1", - "code": "0x01" - }, - "0x18000000000000000000000000000000000000a8": { - "balance": "0x0", - "nonce": "0x1", - "code": "0x01" - }, - "0x18000000000000000000000000000000000000a9": { - "balance": "0x0", - "nonce": "0x1", - "code": "0x01" - }, - "0x18000000000000000000000000000000000000aa": { - "balance": "0x0", - "nonce": "0x1", - "code": "0x01" - }, - "0x18000000000000000000000000000000000000ab": { - "balance": "0x0", - "nonce": "0x1", - "code": "0x01" - }, - "0x18000000000000000000000000000000000000ac": { - "balance": "0x0", - "nonce": "0x1", - "code": "0x01" - }, - "0x18000000000000000000000000000000000000ad": { - "balance": "0x0", - "nonce": "0x1", - "code": "0x01" - }, - "0x18000000000000000000000000000000000000ae": { - "balance": "0x0", - "nonce": "0x1", - "code": "0x01" - }, - "0x18000000000000000000000000000000000000af": { - "balance": "0x0", - "nonce": "0x1", - "code": "0x01" - }, - "0x18000000000000000000000000000000000000b0": { - "balance": "0x0", - "nonce": "0x1", - "code": "0x01" - }, - "0x18000000000000000000000000000000000000b1": { - "balance": "0x0", - "nonce": "0x1", - "code": "0x01" - }, - "0x18000000000000000000000000000000000000b2": { - "balance": "0x0", - "nonce": "0x1", - "code": "0x01" - }, - "0x18000000000000000000000000000000000000b3": { - "balance": "0x0", - "nonce": "0x1", - "code": "0x01" - }, - "0x18000000000000000000000000000000000000b4": { - "balance": "0x0", - "nonce": "0x1", - "code": "0x01" - }, - "0x18000000000000000000000000000000000000b5": { - "balance": "0x0", - "nonce": "0x1", - "code": "0x01" - }, - "0x18000000000000000000000000000000000000b6": { - "balance": "0x0", - "nonce": "0x1", - "code": "0x01" - }, - "0x18000000000000000000000000000000000000b7": { - "balance": "0x0", - "nonce": "0x1", - "code": "0x01" - }, - "0x18000000000000000000000000000000000000b8": { - "balance": "0x0", - "nonce": "0x1", - "code": "0x01" - }, - "0x18000000000000000000000000000000000000b9": { - "balance": "0x0", - "nonce": "0x1", - "code": "0x01" - }, - "0x18000000000000000000000000000000000000ba": { - "balance": "0x0", - "nonce": "0x1", - "code": "0x01" - }, - "0x18000000000000000000000000000000000000bb": { - "balance": "0x0", - "nonce": "0x1", - "code": "0x01" - }, - "0x18000000000000000000000000000000000000bc": { - "balance": "0x0", - "nonce": "0x1", - "code": "0x01" - }, - "0x18000000000000000000000000000000000000bd": { - "balance": "0x0", - "nonce": "0x1", - "code": "0x01" - }, - "0x18000000000000000000000000000000000000be": { - "balance": "0x0", - "nonce": "0x1", - "code": "0x01" - }, - "0x18000000000000000000000000000000000000bf": { - "balance": "0x0", - "nonce": "0x1", - "code": "0x01" - }, - "0x18000000000000000000000000000000000000c0": { - "balance": "0x0", - "nonce": "0x1", - "code": "0x01" - }, - "0x18000000000000000000000000000000000000c1": { - "balance": "0x0", - "nonce": "0x1", - "code": "0x01" - }, - "0x18000000000000000000000000000000000000c2": { - "balance": "0x0", - "nonce": "0x1", - "code": "0x01" - }, - "0x18000000000000000000000000000000000000c3": { - "balance": "0x0", - "nonce": "0x1", - "code": "0x01" - }, - "0x18000000000000000000000000000000000000c4": { - "balance": "0x0", - "nonce": "0x1", - "code": "0x01" - }, - "0x18000000000000000000000000000000000000c5": { - "balance": "0x0", - "nonce": "0x1", - "code": "0x01" - }, - "0x18000000000000000000000000000000000000c6": { - "balance": "0x0", - "nonce": "0x1", - "code": "0x01" - }, - "0x18000000000000000000000000000000000000c7": { - "balance": "0x0", - "nonce": "0x1", - "code": "0x01" - }, - "0x18000000000000000000000000000000000000c8": { - "balance": "0x0", - "nonce": "0x1", - "code": "0x01" - }, - "0x18000000000000000000000000000000000000c9": { - "balance": "0x0", - "nonce": "0x1", - "code": "0x01" - }, - "0x18000000000000000000000000000000000000ca": { - "balance": "0x0", - "nonce": "0x1", - "code": "0x01" - }, - "0x18000000000000000000000000000000000000cb": { - "balance": "0x0", - "nonce": "0x1", - "code": "0x01" - }, - "0x18000000000000000000000000000000000000cc": { - "balance": "0x0", - "nonce": "0x1", - "code": "0x01" - }, - "0x18000000000000000000000000000000000000cd": { - "balance": "0x0", - "nonce": "0x1", - "code": "0x01" - }, - "0x18000000000000000000000000000000000000ce": { - "balance": "0x0", - "nonce": "0x1", - "code": "0x01" - }, - "0x18000000000000000000000000000000000000cf": { - "balance": "0x0", - "nonce": "0x1", - "code": "0x01" - }, - "0x18000000000000000000000000000000000000d0": { - "balance": "0x0", - "nonce": "0x1", - "code": "0x01" - }, - "0x18000000000000000000000000000000000000d1": { - "balance": "0x0", - "nonce": "0x1", - "code": "0x01" - }, - "0x18000000000000000000000000000000000000d2": { - "balance": "0x0", - "nonce": "0x1", - "code": "0x01" - }, - "0x18000000000000000000000000000000000000d3": { - "balance": "0x0", - "nonce": "0x1", - "code": "0x01" - }, - "0x18000000000000000000000000000000000000d4": { - "balance": "0x0", - "nonce": "0x1", - "code": "0x01" - }, - "0x18000000000000000000000000000000000000d5": { - "balance": "0x0", - "nonce": "0x1", - "code": "0x01" - }, - "0x18000000000000000000000000000000000000d6": { - "balance": "0x0", - "nonce": "0x1", - "code": "0x01" - }, - "0x18000000000000000000000000000000000000d7": { - "balance": "0x0", - "nonce": "0x1", - "code": "0x01" - }, - "0x18000000000000000000000000000000000000d8": { - "balance": "0x0", - "nonce": "0x1", - "code": "0x01" - }, - "0x18000000000000000000000000000000000000d9": { - "balance": "0x0", - "nonce": "0x1", - "code": "0x01" - }, - "0x18000000000000000000000000000000000000da": { - "balance": "0x0", - "nonce": "0x1", - "code": "0x01" - }, - "0x18000000000000000000000000000000000000db": { - "balance": "0x0", - "nonce": "0x1", - "code": "0x01" - }, - "0x18000000000000000000000000000000000000dc": { - "balance": "0x0", - "nonce": "0x1", - "code": "0x01" - }, - "0x18000000000000000000000000000000000000dd": { - "balance": "0x0", - "nonce": "0x1", - "code": "0x01" - }, - "0x18000000000000000000000000000000000000de": { - "balance": "0x0", - "nonce": "0x1", - "code": "0x01" - }, - "0x18000000000000000000000000000000000000df": { - "balance": "0x0", - "nonce": "0x1", - "code": "0x01" - }, - "0x18000000000000000000000000000000000000e0": { - "balance": "0x0", - "nonce": "0x1", - "code": "0x01" - }, - "0x18000000000000000000000000000000000000e1": { - "balance": "0x0", - "nonce": "0x1", - "code": "0x01" - }, - "0x18000000000000000000000000000000000000e2": { - "balance": "0x0", - "nonce": "0x1", - "code": "0x01" - }, - "0x18000000000000000000000000000000000000e3": { - "balance": "0x0", - "nonce": "0x1", - "code": "0x01" - }, - "0x18000000000000000000000000000000000000e4": { - "balance": "0x0", - "nonce": "0x1", - "code": "0x01" - }, - "0x18000000000000000000000000000000000000e5": { - "balance": "0x0", - "nonce": "0x1", - "code": "0x01" - }, - "0x18000000000000000000000000000000000000e6": { - "balance": "0x0", - "nonce": "0x1", - "code": "0x01" - }, - "0x18000000000000000000000000000000000000e7": { - "balance": "0x0", - "nonce": "0x1", - "code": "0x01" - }, - "0x18000000000000000000000000000000000000e8": { - "balance": "0x0", - "nonce": "0x1", - "code": "0x01" - }, - "0x18000000000000000000000000000000000000e9": { - "balance": "0x0", - "nonce": "0x1", - "code": "0x01" - }, - "0x18000000000000000000000000000000000000ea": { - "balance": "0x0", - "nonce": "0x1", - "code": "0x01" - }, - "0x18000000000000000000000000000000000000eb": { - "balance": "0x0", - "nonce": "0x1", - "code": "0x01" - }, - "0x18000000000000000000000000000000000000ec": { - "balance": "0x0", - "nonce": "0x1", - "code": "0x01" - }, - "0x18000000000000000000000000000000000000ed": { - "balance": "0x0", - "nonce": "0x1", - "code": "0x01" - }, - "0x18000000000000000000000000000000000000ee": { - "balance": "0x0", - "nonce": "0x1", - "code": "0x01" - }, - "0x18000000000000000000000000000000000000ef": { - "balance": "0x0", - "nonce": "0x1", - "code": "0x01" - }, - "0x18000000000000000000000000000000000000f0": { - "balance": "0x0", - "nonce": "0x1", - "code": "0x01" - }, - "0x18000000000000000000000000000000000000f1": { - "balance": "0x0", - "nonce": "0x1", - "code": "0x01" - }, - "0x18000000000000000000000000000000000000f2": { - "balance": "0x0", - "nonce": "0x1", - "code": "0x01" - }, - "0x18000000000000000000000000000000000000f3": { - "balance": "0x0", - "nonce": "0x1", - "code": "0x01" - }, - "0x18000000000000000000000000000000000000f4": { - "balance": "0x0", - "nonce": "0x1", - "code": "0x01" - }, - "0x18000000000000000000000000000000000000f5": { - "balance": "0x0", - "nonce": "0x1", - "code": "0x01" - }, - "0x18000000000000000000000000000000000000f6": { - "balance": "0x0", - "nonce": "0x1", - "code": "0x01" - }, - "0x18000000000000000000000000000000000000f7": { - "balance": "0x0", - "nonce": "0x1", - "code": "0x01" - }, - "0x18000000000000000000000000000000000000f8": { - "balance": "0x0", - "nonce": "0x1", - "code": "0x01" - }, - "0x18000000000000000000000000000000000000f9": { - "balance": "0x0", - "nonce": "0x1", - "code": "0x01" - }, - "0x18000000000000000000000000000000000000fa": { - "balance": "0x0", - "nonce": "0x1", - "code": "0x01" - }, - "0x18000000000000000000000000000000000000fb": { - "balance": "0x0", - "nonce": "0x1", - "code": "0x01" - }, - "0x18000000000000000000000000000000000000fc": { - "balance": "0x0", - "nonce": "0x1", - "code": "0x01" - }, - "0x18000000000000000000000000000000000000fd": { - "balance": "0x0", - "nonce": "0x1", - "code": "0x01" - }, - "0x18000000000000000000000000000000000000fe": { - "balance": "0x0", - "nonce": "0x1", - "code": "0x01" - }, - "0x18000000000000000000000000000000000000ff": { - "balance": "0x0", - "nonce": "0x1", - "code": "0x01" - }, - "0x2454fd9B923cD74d690bf500b4120E2BAEe3781a": { - "balance": "0x3635c9adc5dea00000" - }, - "0x46F6a3Fb2F3E897c7A4c3aD1B18D864884cf0FAc": { - "balance": "0x3635c9adc5dea00000" - }, - "0xc9dE92071ceadbfFf0ee4d222B7E0F9bF9c8A584": { - "balance": "0x3635c9adc5dea00000" - }, - "0x7f44B75d3EE7414c61ac3a255BE1fD0Ec140130D": { - "balance": "0x3635c9adc5dea00000" - }, - "0xf496BB9e77c05Ce18452ae6d444D87Cd04EbD4F3": { - "balance": "0x3635c9adc5dea00000" - }, - "0x334FF04B5d0E14Abbc1019F62981f7A4eEc5298C": { - "balance": "0x3635c9adc5dea00000" - }, - "0x281CBe8Dd1D7B64995133EbA264dfb5b21e9e053": { - "balance": "0x3635c9adc5dea00000" - }, - "0xdC151920e1F7CD214a108a3Ad0E6a586d46ca015": { - "balance": "0x3635c9adc5dea00000" - }, - "0xE73C02301f1e6877Ad8B5FB7cfbbD51AF97bC0Ca": { - "balance": "0x3635c9adc5dea00000" - }, - "0xFdfC974f9A66bd01353f39dfC32E6BaBC6f22ff6": { - "balance": "0x3635c9adc5dea00000" - }, - "0xe054635B17c4Bd648b820ecf5Ab832688733AAD1": { - "balance": "0x3635c9adc5dea00000" - }, - "0xfD9A0965Db3D6376f498347f4C129bA6c72f9628": { - "balance": "0x3635c9adc5dea00000" - }, - "0xEe33d2085E034E5B9D9346C775f10D6ea284D620": { - "balance": "0x3635c9adc5dea00000" - }, - "0xE2Ffed765eEc0cc29507Be6328CD7Ac8E110f02A": { - "balance": "0x3635c9adc5dea00000" - }, - "0x9ED6CB4C5651d81C50Ea3109D55E87c913cc2Ba7": { - "balance": "0x3635c9adc5dea00000" - }, - "0x90A963443ADaBdDB73bCa009795E94f0E7b9b0e9": { - "balance": "0x3635c9adc5dea00000" - }, - "0xae7713c841C0E4d96c28479eB1BD3E73A6E3B48E": { - "balance": "0x3635c9adc5dea00000" - }, - "0xb4e1373fEbB3bB1750cC2Ee7C4eeaFA113bbCE4f": { - "balance": "0x3635c9adc5dea00000" - }, - "0x9d6E79502e3B2b94178830BA6Bb76A7f54f9ff61": { - "balance": "0x3635c9adc5dea00000" - }, - "0x30D30792030b8B499760dB18C447aa0d83003932": { - "balance": "0x3635c9adc5dea00000" - }, - "0xfc8D6aa4F924C410E0F2449bed4b0CaCfC8f1F1A": { - "balance": "0x3635c9adc5dea00000" - }, - "0x2247f8fEF4b001eD8785eb5e292Ff8829D755012": { - "balance": "0x3635c9adc5dea00000" - }, - "0xBD62385032AE4c8BF1BA2743A187F4e6c7462061": { - "balance": "0x3635c9adc5dea00000" - }, - "0xa3ae2E82b6195b425b8C2Da8D5DB84f7dd1380B6": { - "balance": "0x3635c9adc5dea00000" - }, - "0x9965a08ba97ed0332FCbBf8188f9700Aca620860": { - "balance": "0x3635c9adc5dea00000" - }, - "0x1800000000000000000000000000000000000000": { - "balance": "0x0", - "nonce": "0x1", - "code": "0x01", - "storage": { - "0x0000000000000000000000000000000000000000000000000000000000000001": "0x0000000000000000000000003600000000000000000000000000000000000000", - "0x0000000000000000000000000000000000000000000000000000000000000002": "0x00000000000000000000000000000000000000000000054b40b1f852bda00000" - } - }, - "0x1800000000000000000000000000000000000001": { - "balance": "0x0", - "nonce": "0x1", - "code": "0x01", - "storage": { - "0x0000000000000000000000000000000000000000000000000000000000000001": "0x0000000000000000000000003600000000000000000000000000000000000000" - } - } - } -} \ No newline at end of file diff --git a/assets/localdev/.gitignore b/assets/localdev/.gitignore deleted file mode 100644 index 3becb13..0000000 --- a/assets/localdev/.gitignore +++ /dev/null @@ -1,4 +0,0 @@ -#genesis.json -jwtsecret -config.json -controllers-config.json diff --git a/assets/localdev/genesis.config.ts b/assets/localdev/genesis.config.ts deleted file mode 100644 index fbf4315..0000000 --- a/assets/localdev/genesis.config.ts +++ /dev/null @@ -1,171 +0,0 @@ -// Copyright 2026 Circle Internet Group, Inc. All rights reserved. -// -// SPDX-License-Identifier: Apache-2.0 -// -// Licensed under the Apache License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. -// You may obtain a copy of the License at -// -// http://www.apache.org/licenses/LICENSE-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// See the License for the specific language governing permissions and -// limitations under the License. - -import fs from 'fs' -import { z } from 'zod' -import { parseEther, parseGwei, toHex } from 'viem' -import { privateKeyToAccount } from 'viem/accounts' -import { - createBuilderContext, - buildGenesis, - GenesisConfig, - schemaGenesisConfig, - localdevFeeRecipient, -} from '../../scripts/genesis' -import { bigintReplacer } from '../../scripts/genesis/types' -import { LocalDevAccountCreator } from '../../scripts/genesis/AccountCreator' - -const UINT64_MAX = (1n << 64n) - 1n - -// localBuilderOptionsSchema defines the options to customize the localdev genesis. -export const localBuilderOptionsSchema = LocalDevAccountCreator.optionsSchema.and( - z.object({ - outputControllersConfig: z.string().optional(), - outputGenesisConfig: z.string().optional(), - validatorNames: z.array(z.string()).min(1).optional(), - hardforks: schemaGenesisConfig.shape.hardforks, - extraAccountBalance: z.bigint().optional(), - blockGasLimit: z.bigint().optional(), - }), -) - -const build = async (options: z.infer) => { - const ctx = await createBuilderContext({ - network: 'localdev', - chainId: 1337, - }) - const { outputControllersConfig, outputGenesisConfig, validatorNames, hardforks, extraAccountBalance, blockGasLimit, ...accountOptions } = options - const accountCreator = new LocalDevAccountCreator(accountOptions) - - // Default account for hardhat environment. - const accounts = accountCreator.defaultAccounts() - const { operator, admin, proxyAdmin } = accountCreator.namedAccounts(accounts) - - const one = privateKeyToAccount(toHex(1n, { size: 32 })) - const controllers = accountCreator.controllers() - const validators = await accountCreator.validators() - - if (outputControllersConfig) { - if (!validatorNames) { - throw new Error('validatorNames is required when outputControllersConfig is set') - } - if (validatorNames.length !== controllers.length) { - throw new Error( - `validatorNames length (${validatorNames.length}) must match controllers length (${controllers.length})`, - ) - } - const uniqueNames = new Set(validatorNames) - if (uniqueNames.size !== validatorNames.length) { - throw new Error('validatorNames must be unique') - } - // Dump controllers config file for quake reference - const controllersConfig = Object.fromEntries( - controllers.map((x, i) => { - const key = validatorNames[i] - return [key, { index: i + 1, address: x.address, signingKey: toHex(x.getHdKey().privateKey!), nonce: 0 }] - }), - ) - fs.writeFileSync(outputControllersConfig, JSON.stringify(controllersConfig, null, 2)) - } - - const config: GenesisConfig = { - timestamp: 1763620028n, - coinbase: localdevFeeRecipient, - hardforks: { - zero3Block: 0, - ...hardforks, - }, - - prefund: accounts - .map((account) => account.address) - .concat([one.address]) - .concat(controllers.map((x) => x.address)) - .concat(accountCreator.extraPrefundAccounts().map((x) => x.address)) - .map((address) => ({ address: address, balance: extraAccountBalance !== undefined ? parseEther(extraAccountBalance.toString()) : parseEther('1000000') })), - - NativeFiatToken: { - proxy: { admin: proxyAdmin.address }, - owner: admin.address, - pauser: admin.address, - masterMinter: admin.address, - rescuer: admin.address, - blacklister: operator.address, - minters: [ - { address: operator.address, allowance: parseEther('1000000') }, - ...accountCreator.extraMinters().map((x) => ({ address: x.address, allowance: parseEther('1000000') })), - ], - }, - - ProtocolConfig: { - proxy: { admin: proxyAdmin.address }, - owner: admin.address, - controller: admin.address, - pauser: admin.address, - feeParams: { - alpha: 20n, // 20% - kRate: 200n, // 2% - inverseElasticityMultiplier: 5000n, // 50% - minBaseFee: 1n, - maxBaseFee: parseGwei('1000'), - blockGasLimit: blockGasLimit ?? 30_000_000n, - }, - consensusParams: { - timeoutProposeMs: 3000n, - timeoutProposeDeltaMs: 500n, - timeoutPrevoteMs: 1000n, - timeoutPrevoteDeltaMs: 500n, - timeoutPrecommitMs: 1000n, - timeoutPrecommitDeltaMs: 500n, - timeoutRebroadcastMs: 1000n, - targetBlockTimeMs: 500n, - }, - }, - - Denylist: { - proxy: { admin: proxyAdmin.address }, - owner: admin.address, - denylisters: [operator.address], - }, - - ValidatorManager: { - proxy: { admin: proxyAdmin.address }, - PermissionedValidatorManager: { - proxy: { admin: proxyAdmin.address }, - owner: admin.address, - pauser: admin.address, - validatorRegisterers: [admin.address, operator.address], - }, - validators: validators.map((x, i) => ({ - publicKey: x.publicKey, - votingPower: x.votingPower, - controllers: [{ address: controllers[i].address, votingPowerLimit: UINT64_MAX }], - })), - }, - GasGuzzler: true, - Memo: true, - Multicall3From: true, - TestToken: true, - } - - if (outputGenesisConfig) { - // Save config to file. Then CI do not required the mnemonic. - fs.writeFileSync(outputGenesisConfig, JSON.stringify(config, bigintReplacer, 2)) - } - - return await buildGenesis(ctx, config) -} - -export default build diff --git a/assets/localdev/genesis.json b/assets/localdev/genesis.json deleted file mode 100644 index 433c210..0000000 --- a/assets/localdev/genesis.json +++ /dev/null @@ -1,1599 +0,0 @@ -{ - "config": { - "chainId": 1337, - "daoForkSupport": false, - "terminalTotalDifficulty": "0x0", - "terminalTotalDifficultyPassed": true, - "homesteadBlock": 0, - "eip150Block": 0, - "eip150Hash": "0x0000000000000000000000000000000000000000000000000000000000000000", - "eip155Block": 0, - "eip158Block": 0, - "byzantiumBlock": 0, - "constantinopleBlock": 0, - "petersburgBlock": 0, - "istanbulBlock": 0, - "muirGlacierBlock": 0, - "berlinBlock": 0, - "londonBlock": 0, - "arrowGlacierBlock": 0, - "grayGlacierBlock": 0, - "shanghaiTime": 0, - "cancunTime": 0, - "pragueTime": 0, - "zero3Block": 0, - "zero4Block": 0, - "zero5Block": 0, - "zero6Block": 0, - "zero7Time": 0, - "osakaTime": 0 - }, - "nonce": "0x0", - "timestamp": "0x691eb4bc", - "extraData": "0x", - "gasLimit": "0x1c9c380", - "difficulty": "0x0", - "mixHash": "0x0000000000000000000000000000000000000000000000000000000000000000", - "coinbase": "0x65E0a200006D4FF91bD59F9694220dafc49dbBC1", - "number": "0x0", - "alloc": { - "0x3600000000000000000000000000000000000000": { - "balance": "0x0", - "nonce": "0x1", - "code": "0x60806040526004361061005a5760003560e01c80635c60da1b116100435780635c60da1b146101315780638f2839701461016f578063f851a440146101af5761005a565b80633659cfe6146100645780634f1ef286146100a4575b6100626101c4565b005b34801561007057600080fd5b506100626004803603602081101561008757600080fd5b503573ffffffffffffffffffffffffffffffffffffffff166101de565b610062600480360360408110156100ba57600080fd5b73ffffffffffffffffffffffffffffffffffffffff82351691908101906040810160208201356401000000008111156100f257600080fd5b82018360208201111561010457600080fd5b8035906020019184600183028401116401000000008311171561012657600080fd5b509092509050610232565b34801561013d57600080fd5b50610146610309565b6040805173ffffffffffffffffffffffffffffffffffffffff9092168252519081900360200190f35b34801561017b57600080fd5b506100626004803603602081101561019257600080fd5b503573ffffffffffffffffffffffffffffffffffffffff16610318565b3480156101bb57600080fd5b50610146610420565b6101cc610430565b6101dc6101d76104c4565b6104e9565b565b6101e661050d565b73ffffffffffffffffffffffffffffffffffffffff163373ffffffffffffffffffffffffffffffffffffffff1614156102275761022281610532565b61022f565b61022f6101c4565b50565b61023a61050d565b73ffffffffffffffffffffffffffffffffffffffff163373ffffffffffffffffffffffffffffffffffffffff1614156102fc5761027683610532565b60003073ffffffffffffffffffffffffffffffffffffffff16348484604051808383808284376040519201945060009350909150508083038185875af1925050503d80600081146102e3576040519150601f19603f3d011682016040523d82523d6000602084013e6102e8565b606091505b50509050806102f657600080fd5b50610304565b6103046101c4565b505050565b60006103136104c4565b905090565b61032061050d565b73ffffffffffffffffffffffffffffffffffffffff163373ffffffffffffffffffffffffffffffffffffffff1614156102275773ffffffffffffffffffffffffffffffffffffffff81166103bf576040517f08c379a00000000000000000000000000000000000000000000000000000000081526004018080602001828103825260368152602001806106606036913960400191505060405180910390fd5b7f7e644d79422f17c01e4894b5f4f588d331ebfa28653d42ae832dc59e38c9798f6103e861050d565b6040805173ffffffffffffffffffffffffffffffffffffffff928316815291841660208301528051918290030190a161022281610587565b600061031361050d565b3b151590565b61043861050d565b73ffffffffffffffffffffffffffffffffffffffff163373ffffffffffffffffffffffffffffffffffffffff1614156104bc576040517f08c379a000000000000000000000000000000000000000000000000000000000815260040180806020018281038252603281526020018061062e6032913960400191505060405180910390fd5b6101dc6101dc565b7f7050c9e0f4ca769c69bd3a8ef740bc37934f8e2c036e5a723fd8ee048ed3f8c35490565b3660008037600080366000845af43d6000803e808015610508573d6000f35b3d6000fd5b7f10d6a54a4754c8869d6886b5f5d7fbfa5b4522237ea5c60d11bc4e7a1ff9390b5490565b61053b816105ab565b6040805173ffffffffffffffffffffffffffffffffffffffff8316815290517fbc7cd75a20ee27fd9adebab32041f755214dbc6bffa90cc0225b39da2e5c2d3b9181900360200190a150565b7f10d6a54a4754c8869d6886b5f5d7fbfa5b4522237ea5c60d11bc4e7a1ff9390b55565b6105b48161042a565b610609576040517f08c379a000000000000000000000000000000000000000000000000000000000815260040180806020018281038252603b815260200180610696603b913960400191505060405180910390fd5b7f7050c9e0f4ca769c69bd3a8ef740bc37934f8e2c036e5a723fd8ee048ed3f8c35556fe43616e6e6f742063616c6c2066616c6c6261636b2066756e6374696f6e2066726f6d207468652070726f78792061646d696e43616e6e6f74206368616e6765207468652061646d696e206f6620612070726f787920746f20746865207a65726f206164647265737343616e6e6f742073657420612070726f787920696d706c656d656e746174696f6e20746f2061206e6f6e2d636f6e74726163742061646472657373a2646970667358221220015908007e367e7f333ec38084b88f027c06160d2c19e5bdd8027e8d06acf8bf64736f6c634300060c0033", - "storage": { - "0x10d6a54a4754c8869d6886b5f5d7fbfa5b4522237ea5c60d11bc4e7a1ff9390b": "0x000000000000000000000000a0ee7a142d267c1f36714e4a8f75612f20a79720", - "0x7050c9e0f4ca769c69bd3a8ef740bc37934f8e2c036e5a723fd8ee048ed3f8c3": "0x000000000000000000000000c6ad664ac6679f4ce74e10e91449c93ec1ae3ca6", - "0x0000000000000000000000000000000000000000000000000000000000000000": "0x00000000000000000000000023618e81e3f5cdf7f54c3d65f7fbc0abf5b21e8f", - "0x0000000000000000000000000000000000000000000000000000000000000001": "0x00000000000000000000000023618e81e3f5cdf7f54c3d65f7fbc0abf5b21e8f", - "0x0000000000000000000000000000000000000000000000000000000000000002": "0x00000000000000000000000014dc79964da2c08b23698b3d3cc7ca32193d9955", - "0x0000000000000000000000000000000000000000000000000000000000000004": "0x5553444300000000000000000000000000000000000000000000000000000008", - "0x0000000000000000000000000000000000000000000000000000000000000005": "0x5553444300000000000000000000000000000000000000000000000000000008", - "0x0000000000000000000000000000000000000000000000000000000000000006": "0x0000000000000000000000000000000000000000000000000000000000000006", - "0x0000000000000000000000000000000000000000000000000000000000000007": "0x5553440000000000000000000000000000000000000000000000000000000006", - "0x0000000000000000000000000000000000000000000000000000000000000008": "0x00000000000000000000000123618e81E3f5cdF7f54C3d65f7FBc0aBf5B21E8f", - "0xa36443b8604571fcb8dd4845d6a017bab48c2ee9a789ffbf5700057aab21e7a0": "0x0000000000000000000000000000000000000000000000000000000000000001", - "0xc25631c556adfd3079e401e32563533cc4d7cc4de034ff460f088383571be6eb": "0x00000000000000000000000000000000000000000000d3c21bcecceda1000000", - "0x000000000000000000000000000000000000000000000000000000000000000e": "0x00000000000000000000000023618e81e3f5cdf7f54c3d65f7fbc0abf5b21e8f", - "0x0000000000000000000000000000000000000000000000000000000000000012": "0x0000000000000000000000000000000000000000000000000000000000000003" - } - }, - "0xC6AD664ac6679F4Ce74e10E91449C93Ec1ae3cA6": { - "balance": "0x0", - "nonce": "0x1", - "code": "0x608060405234801561001057600080fd5b506004361061038e5760003560e01c806388b7ab63116101de578063bd1024301161010f578063dd62ed3e116100ad578063ef55bec61161007c578063ef55bec61461115b578063f2fde38b146111c7578063f9f92be4146111fa578063fe575a871461122d5761038e565b8063dd62ed3e14611073578063e3ee160e146110ae578063e5a6b10f1461111a578063e94a0102146111225761038e565b8063d505accf116100e9578063d505accf14610f95578063d608ea6414610ff3578063d916948714611063578063dd0743b31461106b5761038e565b8063bd10243014610ea1578063ccd92d3e14610ea9578063cf09299514610eb15761038e565b8063a457c2d71161017c578063aa271e1a11610156578063aa271e1a14610d30578063ad38bf2214610d63578063b2118a8d14610d96578063b7b7289914610dd95761038e565b8063a457c2d714610c8b578063a9059cbb14610cc4578063aa20e1e414610cfd5761038e565b806395d89b41116101b857806395d89b4114610b9b5780639fd0506d14610ba35780639fd5a6cf14610bab578063a0cc6a6814610c835761038e565b806388b7ab6314610a7c5780638a6db9c314610b605780638da5cb5b14610b935761038e565b806339509351116102c3578063554bab3c116102615780637ecebe00116102305780637ecebe0014610a315780637f2eecc314610a645780637fd0991614610a6c5780638456cb5914610a745761038e565b8063554bab3c146109755780635a049a70146109a85780635c975abb146109f657806370a08231146109fe5761038e565b806342966c681161029d57806342966c6814610855578063430239b4146108725780634e44d9561461093457806354fd4d501461096d5761038e565b806339509351146107db5780633f4ba83a1461081457806340c10f191461081c5761038e565b80633092afd5116103305780633357162b1161030a5780633357162b146105ae57806335d99f351461079a5780633644e515146107cb57806338a63183146107d35761038e565b80633092afd51461055557806330adf81f14610588578063313ce567146105905761038e565b80631a8952661161036c5780631a8952661461047757806323b872dd146104ac5780632ab60045146104ef5780632fc81e09146105225761038e565b806306fdde0314610393578063095ea7b31461041057806318160ddd1461045d575b600080fd5b61039b611260565b6040805160208082528351818301528351919283929083019185019080838360005b838110156103d55781810151838201526020016103bd565b50505050905090810190601f1680156104025780820380516001836020036101000a031916815260200191505b509250505060405180910390f35b6104496004803603604081101561042657600080fd5b5073ffffffffffffffffffffffffffffffffffffffff813516906020013561130c565b604080519115158252519081900360200190f35b6104656113ae565b60408051918252519081900360200190f35b6104aa6004803603602081101561048d57600080fd5b503573ffffffffffffffffffffffffffffffffffffffff1661144e565b005b610449600480360360608110156104c257600080fd5b5073ffffffffffffffffffffffffffffffffffffffff81358116916020810135909116906040013561150b565b6104aa6004803603602081101561050557600080fd5b503573ffffffffffffffffffffffffffffffffffffffff16611703565b6104aa6004803603602081101561053857600080fd5b503573ffffffffffffffffffffffffffffffffffffffff16611864565b6104496004803603602081101561056b57600080fd5b503573ffffffffffffffffffffffffffffffffffffffff166118cc565b6104656119c5565b6105986119e9565b6040805160ff9092168252519081900360200190f35b6104aa60048036036101008110156105c557600080fd5b8101906020810181356401000000008111156105e057600080fd5b8201836020820111156105f257600080fd5b8035906020019184600183028401116401000000008311171561061457600080fd5b91908080601f016020809104026020016040519081016040528093929190818152602001838380828437600092019190915250929594936020810193503591505064010000000081111561066757600080fd5b82018360208201111561067957600080fd5b8035906020019184600183028401116401000000008311171561069b57600080fd5b91908080601f01602080910402602001604051908101604052809392919081815260200183838082843760009201919091525092959493602081019350359150506401000000008111156106ee57600080fd5b82018360208201111561070057600080fd5b8035906020019184600183028401116401000000008311171561072257600080fd5b91908080601f0160208091040260200160405190810160405280939291908181526020018383808284376000920191909152509295505050813560ff16925050602081013573ffffffffffffffffffffffffffffffffffffffff908116916040810135821691606082013581169160800135166119f2565b6107a2611d34565b6040805173ffffffffffffffffffffffffffffffffffffffff9092168252519081900360200190f35b610465611d50565b6107a2611d5f565b610449600480360360408110156107f157600080fd5b5073ffffffffffffffffffffffffffffffffffffffff8135169060200135611d7b565b6104aa611e13565b6104496004803603604081101561083257600080fd5b5073ffffffffffffffffffffffffffffffffffffffff8135169060200135611ed6565b6104aa6004803603602081101561086b57600080fd5b5035612324565b6104aa6004803603604081101561088857600080fd5b8101906020810181356401000000008111156108a357600080fd5b8201836020820111156108b557600080fd5b803590602001918460208302840111640100000000831117156108d757600080fd5b9193909290916020810190356401000000008111156108f557600080fd5b82018360208201111561090757600080fd5b8035906020019184600183028401116401000000008311171561092957600080fd5b509092509050612658565b6104496004803603604081101561094a57600080fd5b5073ffffffffffffffffffffffffffffffffffffffff813516906020013561280f565b61039b6129a2565b6104aa6004803603602081101561098b57600080fd5b503573ffffffffffffffffffffffffffffffffffffffff166129d9565b6104aa600480360360a08110156109be57600080fd5b5073ffffffffffffffffffffffffffffffffffffffff8135169060208101359060ff6040820135169060608101359060800135612b40565b610449612bde565b61046560048036036020811015610a1457600080fd5b503573ffffffffffffffffffffffffffffffffffffffff16612bff565b61046560048036036020811015610a4757600080fd5b503573ffffffffffffffffffffffffffffffffffffffff16612c0a565b610465612c32565b6107a2612c56565b6104aa612c6e565b6104aa600480360360e0811015610a9257600080fd5b73ffffffffffffffffffffffffffffffffffffffff823581169260208101359091169160408201359160608101359160808201359160a08101359181019060e0810160c0820135640100000000811115610aeb57600080fd5b820183602082011115610afd57600080fd5b80359060200191846001830284011164010000000083111715610b1f57600080fd5b91908080601f016020809104026020016040519081016040528093929190818152602001838380828437600092019190915250929550612d48945050505050565b61046560048036036020811015610b7657600080fd5b503573ffffffffffffffffffffffffffffffffffffffff16612dea565b6107a2612e12565b61039b612e2e565b6107a2612ea7565b6104aa600480360360a0811015610bc157600080fd5b73ffffffffffffffffffffffffffffffffffffffff823581169260208101359091169160408201359160608101359181019060a081016080820135640100000000811115610c0e57600080fd5b820183602082011115610c2057600080fd5b80359060200191846001830284011164010000000083111715610c4257600080fd5b91908080601f016020809104026020016040519081016040528093929190818152602001838380828437600092019190915250929550612ec3945050505050565b610465612f5a565b61044960048036036040811015610ca157600080fd5b5073ffffffffffffffffffffffffffffffffffffffff8135169060200135612f7e565b61044960048036036040811015610cda57600080fd5b5073ffffffffffffffffffffffffffffffffffffffff8135169060200135613016565b6104aa60048036036020811015610d1357600080fd5b503573ffffffffffffffffffffffffffffffffffffffff166130ae565b61044960048036036020811015610d4657600080fd5b503573ffffffffffffffffffffffffffffffffffffffff16613215565b6104aa60048036036020811015610d7957600080fd5b503573ffffffffffffffffffffffffffffffffffffffff16613240565b6104aa60048036036060811015610dac57600080fd5b5073ffffffffffffffffffffffffffffffffffffffff8135811691602081013590911690604001356133a7565b6104aa60048036036060811015610def57600080fd5b73ffffffffffffffffffffffffffffffffffffffff82351691602081013591810190606081016040820135640100000000811115610e2c57600080fd5b820183602082011115610e3e57600080fd5b80359060200191846001830284011164010000000083111715610e6057600080fd5b91908080601f01602080910402602001604051908101604052809392919081815260200183838082843760009201919091525092955061343d945050505050565b6107a26134d2565b6104656134ee565b6104aa600480360360e0811015610ec757600080fd5b73ffffffffffffffffffffffffffffffffffffffff823581169260208101359091169160408201359160608101359160808201359160a08101359181019060e0810160c0820135640100000000811115610f2057600080fd5b820183602082011115610f3257600080fd5b80359060200191846001830284011164010000000083111715610f5457600080fd5b91908080601f0160208091040260200160405190810160405280939291908181526020018383808284376000920191909152509295506134f7945050505050565b6104aa600480360360e0811015610fab57600080fd5b5073ffffffffffffffffffffffffffffffffffffffff813581169160208101359091169060408101359060608101359060ff6080820135169060a08101359060c00135613590565b6104aa6004803603602081101561100957600080fd5b81019060208101813564010000000081111561102457600080fd5b82018360208201111561103657600080fd5b8035906020019184600183028401116401000000008311171561105857600080fd5b509092509050613629565b610465613712565b6107a2613736565b6104656004803603604081101561108957600080fd5b5073ffffffffffffffffffffffffffffffffffffffff8135811691602001351661374e565b6104aa60048036036101208110156110c557600080fd5b5073ffffffffffffffffffffffffffffffffffffffff813581169160208101359091169060408101359060608101359060808101359060a08101359060ff60c0820135169060e0810135906101000135613786565b61039b61382c565b6104496004803603604081101561113857600080fd5b5073ffffffffffffffffffffffffffffffffffffffff81351690602001356138a5565b6104aa600480360361012081101561117257600080fd5b5073ffffffffffffffffffffffffffffffffffffffff813581169160208101359091169060408101359060608101359060808101359060a08101359060ff60c0820135169060e08101359061010001356138dd565b6104aa600480360360208110156111dd57600080fd5b503573ffffffffffffffffffffffffffffffffffffffff16613978565b6104aa6004803603602081101561121057600080fd5b503573ffffffffffffffffffffffffffffffffffffffff16613acb565b6104496004803603602081101561124357600080fd5b503573ffffffffffffffffffffffffffffffffffffffff16613b88565b6004805460408051602060026001851615610100027fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff0190941693909304601f810184900484028201840190925281815292918301828280156113045780601f106112d957610100808354040283529160200191611304565b820191906000526020600020905b8154815290600101906020018083116112e757829003601f168201915b505050505081565b60015460009074010000000000000000000000000000000000000000900460ff161561139957604080517f08c379a000000000000000000000000000000000000000000000000000000000815260206004820152601060248201527f5061757361626c653a2070617573656400000000000000000000000000000000604482015290519081900360640190fd5b6113a4338484613b93565b5060015b92915050565b60008073180000000000000000000000000000000000000073ffffffffffffffffffffffffffffffffffffffff166318160ddd6040518163ffffffff1660e01b815260040160206040518083038186803b15801561140b57600080fd5b505afa15801561141f573d6000803e3d6000fd5b505050506040513d602081101561143557600080fd5b505190506114488164e8d4a51000613cda565b91505090565b60025473ffffffffffffffffffffffffffffffffffffffff1633146114be576040517f08c379a000000000000000000000000000000000000000000000000000000000815260040180806020018281038252602c8152602001806158c5602c913960400191505060405180910390fd5b6114c781613ced565b60405173ffffffffffffffffffffffffffffffffffffffff8216907f117e3210bb9aa7d9baff172026820255c6f6c30ba8999d1c2fd88e2848137c4e90600090a250565b60015460009074010000000000000000000000000000000000000000900460ff161561159857604080517f08c379a000000000000000000000000000000000000000000000000000000000815260206004820152601060248201527f5061757361626c653a2070617573656400000000000000000000000000000000604482015290519081900360640190fd5b336115a281613cf8565b156115f8576040517f08c379a0000000000000000000000000000000000000000000000000000000008152600401808060200182810382526025815260200180615bc76025913960400191505060405180910390fd5b73ffffffffffffffffffffffffffffffffffffffff85166000908152600a60209081526040808320338452909152902054831115611681576040517f08c379a00000000000000000000000000000000000000000000000000000000081526004018080602001828103825260288152602001806159ac6028913960400191505060405180910390fd5b73ffffffffffffffffffffffffffffffffffffffff85166000908152600a602090815260408083203384529091529020546116bc9084613da7565b73ffffffffffffffffffffffffffffffffffffffff86166000908152600a602090815260408083203384529091529020556116f8858585613e1e565b506001949350505050565b60005473ffffffffffffffffffffffffffffffffffffffff16331461178957604080517f08c379a000000000000000000000000000000000000000000000000000000000815260206004820181905260248201527f4f776e61626c653a2063616c6c6572206973206e6f7420746865206f776e6572604482015290519081900360640190fd5b73ffffffffffffffffffffffffffffffffffffffff81166117f5576040517f08c379a000000000000000000000000000000000000000000000000000000000815260040180806020018281038252602a815260200180615823602a913960400191505060405180910390fd5b600e80547fffffffffffffffffffffffff00000000000000000000000000000000000000001673ffffffffffffffffffffffffffffffffffffffff83169081179091556040517fe475e580d85111348e40d8ca33cfdd74c30fe1655c2d8537a13abc10065ffa5a90600090a250565b60125460ff1660011461187657600080fd5b60006118813061410c565b9050801561189457611894308383613e1e565b61189d30614135565b5050601280547fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff00166002179055565b60085460009073ffffffffffffffffffffffffffffffffffffffff16331461193f576040517f08c379a000000000000000000000000000000000000000000000000000000000815260040180806020018281038252602981526020018061589c6029913960400191505060405180910390fd5b73ffffffffffffffffffffffffffffffffffffffff82166000818152600c6020908152604080832080547fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff00169055600d909152808220829055517fe94479a9f7e1952cc78f2d6baab678adc1b772d936c6583def489e524cb666929190a2506001919050565b7f6e71edae12b1b97f4d1f60370fef10105fa2faae0126114a169c64845d6126c981565b60065460ff1681565b60085474010000000000000000000000000000000000000000900460ff1615611a66576040517f08c379a000000000000000000000000000000000000000000000000000000000815260040180806020018281038252602a815260200180615a27602a913960400191505060405180910390fd5b73ffffffffffffffffffffffffffffffffffffffff8416611ad2576040517f08c379a000000000000000000000000000000000000000000000000000000000815260040180806020018281038252602f815260200180615938602f913960400191505060405180910390fd5b73ffffffffffffffffffffffffffffffffffffffff8316611b3e576040517f08c379a00000000000000000000000000000000000000000000000000000000081526004018080602001828103825260298152602001806157fa6029913960400191505060405180910390fd5b73ffffffffffffffffffffffffffffffffffffffff8216611baa576040517f08c379a000000000000000000000000000000000000000000000000000000000815260040180806020018281038252602e8152602001806159d4602e913960400191505060405180910390fd5b73ffffffffffffffffffffffffffffffffffffffff8116611c16576040517f08c379a0000000000000000000000000000000000000000000000000000000008152600401808060200182810382526028815260200180615b3f6028913960400191505060405180910390fd5b8751611c299060049060208b0190615593565b508651611c3d9060059060208a0190615593565b508551611c51906007906020890190615593565b50600680547fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff001660ff8716179055600880547fffffffffffffffffffffffff000000000000000000000000000000000000000090811673ffffffffffffffffffffffffffffffffffffffff8781169190911790925560018054821686841617905560028054909116918416919091179055611ceb81614140565b5050600880547fffffffffffffffffffffff00ffffffffffffffffffffffffffffffffffffffff1674010000000000000000000000000000000000000000179055505050505050565b60085473ffffffffffffffffffffffffffffffffffffffff1681565b6000611d5a614187565b905090565b600e5473ffffffffffffffffffffffffffffffffffffffff1690565b60015460009074010000000000000000000000000000000000000000900460ff1615611e0857604080517f08c379a000000000000000000000000000000000000000000000000000000000815260206004820152601060248201527f5061757361626c653a2070617573656400000000000000000000000000000000604482015290519081900360640190fd5b6113a433848461427c565b60015473ffffffffffffffffffffffffffffffffffffffff163314611e83576040517f08c379a0000000000000000000000000000000000000000000000000000000008152600401808060200182810382526022815260200180615af36022913960400191505060405180910390fd5b600180547fffffffffffffffffffffff00ffffffffffffffffffffffffffffffffffffffff1690556040517f7805862f689e2f13df9f062ff482ad3ad112aca9e0847911ed832e158c525b3390600090a1565b336000908152600c602052604081205460ff16611f3e576040517f08c379a00000000000000000000000000000000000000000000000000000000081526004018080602001828103825260218152602001806159176021913960400191505060405180910390fd5b33611f4881613cf8565b15611f9e576040517f08c379a0000000000000000000000000000000000000000000000000000000008152600401808060200182810382526025815260200180615bc76025913960400191505060405180910390fd5b60015474010000000000000000000000000000000000000000900460ff161561202857604080517f08c379a000000000000000000000000000000000000000000000000000000000815260206004820152601060248201527f5061757361626c653a2070617573656400000000000000000000000000000000604482015290519081900360640190fd5b73ffffffffffffffffffffffffffffffffffffffff8416612094576040517f08c379a000000000000000000000000000000000000000000000000000000000815260040180806020018281038252602381526020018061578f6023913960400191505060405180910390fd5b600083116120ed576040517f08c379a000000000000000000000000000000000000000000000000000000000815260040180806020018281038252602981526020018061584d6029913960400191505060405180910390fd5b336000908152600d602052604090205480841115612156576040517f08c379a000000000000000000000000000000000000000000000000000000000815260040180806020018281038252602e815260200180615ac5602e913960400191505060405180910390fd5b336000908152600d6020526040902084820390557318000000000000000000000000000000000000006340c10f19866121948764e8d4a510006142c6565b6040518363ffffffff1660e01b8152600401808373ffffffffffffffffffffffffffffffffffffffff16815260200182815260200192505050602060405180830381600087803b1580156121e757600080fd5b505af11580156121fb573d6000803e3d6000fd5b505050506040513d602081101561221157600080fd5b505161227e57604080517f08c379a000000000000000000000000000000000000000000000000000000000815260206004820152601260248201527f4e6174697665206d696e74206661696c65640000000000000000000000000000604482015290519081900360640190fd5b60408051858152905173ffffffffffffffffffffffffffffffffffffffff87169133917fab8530f87dc9b59234c4623bf917212bb2536d647574c8e7e5da92c2ede0c9f89181900360200190a360408051858152905173ffffffffffffffffffffffffffffffffffffffff8716916000917fddf252ad1be2c89b69c2b068fc378daa952ba7f163c4a11628f55a4df523b3ef9181900360200190a3506001949350505050565b336000908152600c602052604090205460ff1661238c576040517f08c379a00000000000000000000000000000000000000000000000000000000081526004018080602001828103825260218152602001806159176021913960400191505060405180910390fd5b60015474010000000000000000000000000000000000000000900460ff161561241657604080517f08c379a000000000000000000000000000000000000000000000000000000000815260206004820152601060248201527f5061757361626c653a2070617573656400000000000000000000000000000000604482015290519081900360640190fd5b6000811161246f576040517f08c379a00000000000000000000000000000000000000000000000000000000081526004018080602001828103825260298152602001806157666029913960400191505060405180910390fd5b60006124808264e8d4a510006142c6565b905033318111156124dc576040517f08c379a00000000000000000000000000000000000000000000000000000000081526004018080602001828103825260268152602001806158f16026913960400191505060405180910390fd5b604080517f9dc29fac00000000000000000000000000000000000000000000000000000000815233600482015260248101839052905173180000000000000000000000000000000000000091639dc29fac9160448083019260209291908290030181600087803b15801561254f57600080fd5b505af1158015612563573d6000803e3d6000fd5b505050506040513d602081101561257957600080fd5b50516125e657604080517f08c379a000000000000000000000000000000000000000000000000000000000815260206004820152601260248201527f4e6174697665206275726e206661696c65640000000000000000000000000000604482015290519081900360640190fd5b60408051838152905133917fcc16f5dbb4873280815c1ee09dbd06736cffcc184412cf7a71a0fdb75d397ca5919081900360200190a260408051838152905160009133917fddf252ad1be2c89b69c2b068fc378daa952ba7f163c4a11628f55a4df523b3ef9181900360200190a35050565b60125460ff1660021461266a57600080fd5b61267660058383615611565b5060005b838110156127b8576003600086868481811061269257fe5b6020908102929092013573ffffffffffffffffffffffffffffffffffffffff168352508101919091526040016000205460ff1661271a576040517f08c379a000000000000000000000000000000000000000000000000000000000815260040180806020018281038252603d8152602001806156b3603d913960400191505060405180910390fd5b61274b85858381811061272957fe5b9050602002013573ffffffffffffffffffffffffffffffffffffffff16614135565b6003600086868481811061275b57fe5b6020908102929092013573ffffffffffffffffffffffffffffffffffffffff1683525081019190915260400160002080547fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff0016905560010161267a565b506127c230614135565b505030600090815260036020819052604090912080547fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff009081169091556012805490911690911790555050565b60015460009074010000000000000000000000000000000000000000900460ff161561289c57604080517f08c379a000000000000000000000000000000000000000000000000000000000815260206004820152601060248201527f5061757361626c653a2070617573656400000000000000000000000000000000604482015290519081900360640190fd5b60085473ffffffffffffffffffffffffffffffffffffffff16331461290c576040517f08c379a000000000000000000000000000000000000000000000000000000000815260040180806020018281038252602981526020018061589c6029913960400191505060405180910390fd5b73ffffffffffffffffffffffffffffffffffffffff83166000818152600c6020908152604080832080547fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff00166001179055600d825291829020859055815185815291517f46980fca912ef9bcdbd36877427b6b90e860769f604e89c0e67720cece530d209281900390910190a250600192915050565b60408051808201909152600181527f3200000000000000000000000000000000000000000000000000000000000000602082015290565b60005473ffffffffffffffffffffffffffffffffffffffff163314612a5f57604080517f08c379a000000000000000000000000000000000000000000000000000000000815260206004820181905260248201527f4f776e61626c653a2063616c6c6572206973206e6f7420746865206f776e6572604482015290519081900360640190fd5b73ffffffffffffffffffffffffffffffffffffffff8116612acb576040517f08c379a00000000000000000000000000000000000000000000000000000000081526004018080602001828103825260288152602001806157136028913960400191505060405180910390fd5b600180547fffffffffffffffffffffffff00000000000000000000000000000000000000001673ffffffffffffffffffffffffffffffffffffffff83811691909117918290556040519116907fb80482a293ca2e013eda8683c9bd7fc8347cfdaeea5ede58cba46df502c2a60490600090a250565b60015474010000000000000000000000000000000000000000900460ff1615612bca57604080517f08c379a000000000000000000000000000000000000000000000000000000000815260206004820152601060248201527f5061757361626c653a2070617573656400000000000000000000000000000000604482015290519081900360640190fd5b612bd785858585856142d2565b5050505050565b60015474010000000000000000000000000000000000000000900460ff1681565b60006113a88261410c565b73ffffffffffffffffffffffffffffffffffffffff1660009081526011602052604090205490565b7fd099cc98ef71107a616c4f0f941f04c322d8e254fe26b3c6668db87aae413de881565b73180000000000000000000000000000000000000081565b60015473ffffffffffffffffffffffffffffffffffffffff163314612cde576040517f08c379a0000000000000000000000000000000000000000000000000000000008152600401808060200182810382526022815260200180615af36022913960400191505060405180910390fd5b600180547fffffffffffffffffffffff00ffffffffffffffffffffffffffffffffffffffff16740100000000000000000000000000000000000000001790556040517f6985a02210a168e66602d3235cb6db0e70f92b3ba4d376a33c0f3d9434bff62590600090a1565b60015474010000000000000000000000000000000000000000900460ff1615612dd257604080517f08c379a000000000000000000000000000000000000000000000000000000000815260206004820152601060248201527f5061757361626c653a2070617573656400000000000000000000000000000000604482015290519081900360640190fd5b612de187878787878787614312565b50505050505050565b73ffffffffffffffffffffffffffffffffffffffff166000908152600d602052604090205490565b60005473ffffffffffffffffffffffffffffffffffffffff1690565b6005805460408051602060026001851615610100027fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff0190941693909304601f810184900484028201840190925281815292918301828280156113045780601f106112d957610100808354040283529160200191611304565b60015473ffffffffffffffffffffffffffffffffffffffff1681565b60015474010000000000000000000000000000000000000000900460ff1615612f4d57604080517f08c379a000000000000000000000000000000000000000000000000000000000815260206004820152601060248201527f5061757361626c653a2070617573656400000000000000000000000000000000604482015290519081900360640190fd5b612bd78585858585614433565b7f7c7c6cdb67a18743f49ec6fa9b35f50d52ed05cbed4cc592e13b44501c1a226781565b60015460009074010000000000000000000000000000000000000000900460ff161561300b57604080517f08c379a000000000000000000000000000000000000000000000000000000000815260206004820152601060248201527f5061757361626c653a2070617573656400000000000000000000000000000000604482015290519081900360640190fd5b6113a43384846146f7565b60015460009074010000000000000000000000000000000000000000900460ff16156130a357604080517f08c379a000000000000000000000000000000000000000000000000000000000815260206004820152601060248201527f5061757361626c653a2070617573656400000000000000000000000000000000604482015290519081900360640190fd5b6113a4338484613e1e565b60005473ffffffffffffffffffffffffffffffffffffffff16331461313457604080517f08c379a000000000000000000000000000000000000000000000000000000000815260206004820181905260248201527f4f776e61626c653a2063616c6c6572206973206e6f7420746865206f776e6572604482015290519081900360640190fd5b73ffffffffffffffffffffffffffffffffffffffff81166131a0576040517f08c379a000000000000000000000000000000000000000000000000000000000815260040180806020018281038252602f815260200180615938602f913960400191505060405180910390fd5b600880547fffffffffffffffffffffffff00000000000000000000000000000000000000001673ffffffffffffffffffffffffffffffffffffffff83811691909117918290556040519116907fdb66dfa9c6b8f5226fe9aac7e51897ae8ee94ac31dc70bb6c9900b2574b707e690600090a250565b73ffffffffffffffffffffffffffffffffffffffff166000908152600c602052604090205460ff1690565b60005473ffffffffffffffffffffffffffffffffffffffff1633146132c657604080517f08c379a000000000000000000000000000000000000000000000000000000000815260206004820181905260248201527f4f776e61626c653a2063616c6c6572206973206e6f7420746865206f776e6572604482015290519081900360640190fd5b73ffffffffffffffffffffffffffffffffffffffff8116613332576040517f08c379a0000000000000000000000000000000000000000000000000000000008152600401808060200182810382526032815260200180615b956032913960400191505060405180910390fd5b600280547fffffffffffffffffffffffff00000000000000000000000000000000000000001673ffffffffffffffffffffffffffffffffffffffff83811691909117918290556040519116907fc67398012c111ce95ecb7429b933096c977380ee6c421175a71a4a4c6c88c06e90600090a250565b600e5473ffffffffffffffffffffffffffffffffffffffff163314613417576040517f08c379a00000000000000000000000000000000000000000000000000000000081526004018080602001828103825260248152602001806159676024913960400191505060405180910390fd5b61343873ffffffffffffffffffffffffffffffffffffffff84168383614753565b505050565b60015474010000000000000000000000000000000000000000900460ff16156134c757604080517f08c379a000000000000000000000000000000000000000000000000000000000815260206004820152601060248201527f5061757361626c653a2070617573656400000000000000000000000000000000604482015290519081900360640190fd5b6134388383836147e0565b60025473ffffffffffffffffffffffffffffffffffffffff1681565b64e8d4a5100081565b60015474010000000000000000000000000000000000000000900460ff161561358157604080517f08c379a000000000000000000000000000000000000000000000000000000000815260206004820152601060248201527f5061757361626c653a2070617573656400000000000000000000000000000000604482015290519081900360640190fd5b612de1878787878787876148ea565b60015474010000000000000000000000000000000000000000900460ff161561361a57604080517f08c379a000000000000000000000000000000000000000000000000000000000815260206004820152601060248201527f5061757361626c653a2070617573656400000000000000000000000000000000604482015290519081900360640190fd5b612de187878787878787614988565b60085474010000000000000000000000000000000000000000900460ff168015613656575060125460ff16155b61365f57600080fd5b61366b60048383615611565b506136e082828080601f0160208091040260200160405190810160405280939291908181526020018383808284376000920191909152505060408051808201909152600181527f3200000000000000000000000000000000000000000000000000000000000000602082015291506149ca9050565b600f555050601280547fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff00166001179055565b7f158b0a9edf7a828aad02f63cd515c68ef2f50ba807396f6d12842833a159742981565b73180000000000000000000000000000000000000181565b73ffffffffffffffffffffffffffffffffffffffff9182166000908152600a6020908152604080832093909416825291909152205490565b60015474010000000000000000000000000000000000000000900460ff161561381057604080517f08c379a000000000000000000000000000000000000000000000000000000000815260206004820152601060248201527f5061757361626c653a2070617573656400000000000000000000000000000000604482015290519081900360640190fd5b6138218989898989898989896149e0565b505050505050505050565b6007805460408051602060026001851615610100027fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff0190941693909304601f810184900484028201840190925281815292918301828280156113045780601f106112d957610100808354040283529160200191611304565b73ffffffffffffffffffffffffffffffffffffffff919091166000908152601060209081526040808320938352929052205460ff1690565b60015474010000000000000000000000000000000000000000900460ff161561396757604080517f08c379a000000000000000000000000000000000000000000000000000000000815260206004820152601060248201527f5061757361626c653a2070617573656400000000000000000000000000000000604482015290519081900360640190fd5b613821898989898989898989614a24565b60005473ffffffffffffffffffffffffffffffffffffffff1633146139fe57604080517f08c379a000000000000000000000000000000000000000000000000000000000815260206004820181905260248201527f4f776e61626c653a2063616c6c6572206973206e6f7420746865206f776e6572604482015290519081900360640190fd5b73ffffffffffffffffffffffffffffffffffffffff8116613a6a576040517f08c379a00000000000000000000000000000000000000000000000000000000081526004018080602001828103825260268152602001806157b26026913960400191505060405180910390fd5b6000546040805173ffffffffffffffffffffffffffffffffffffffff9283168152918316602083015280517f8be0079c531659141344cd1fd0a4f28419497f9722a3daafe3b4186f6b6457e09281900390910190a1613ac881614140565b50565b60025473ffffffffffffffffffffffffffffffffffffffff163314613b3b576040517f08c379a000000000000000000000000000000000000000000000000000000000815260040180806020018281038252602c8152602001806158c5602c913960400191505060405180910390fd5b613b4481614135565b60405173ffffffffffffffffffffffffffffffffffffffff8216907fffa4e6181777692565cf28528fc88fd1516ea86b56da075235fa575af6a4b85590600090a250565b60006113a882613cf8565b73ffffffffffffffffffffffffffffffffffffffff8316613bff576040517f08c379a0000000000000000000000000000000000000000000000000000000008152600401808060200182810382526024815260200180615aa16024913960400191505060405180910390fd5b73ffffffffffffffffffffffffffffffffffffffff8216613c6b576040517f08c379a00000000000000000000000000000000000000000000000000000000081526004018080602001828103825260228152602001806157d86022913960400191505060405180910390fd5b73ffffffffffffffffffffffffffffffffffffffff8084166000818152600a6020908152604080832094871680845294825291829020859055815185815291517f8c5be1e5ebec7d5bd14f71427d1e84f3dd0314c0f7b2291e5b200ac8c7c3b9259281900390910190a3505050565b6000613ce68383614a68565b9392505050565b613ac8816000614ae9565b600073180000000000000000000000000000000000000173ffffffffffffffffffffffffffffffffffffffff16638e204c43836040518263ffffffff1660e01b8152600401808273ffffffffffffffffffffffffffffffffffffffff16815260200191505060206040518083038186803b158015613d7557600080fd5b505afa158015613d89573d6000803e3d6000fd5b505050506040513d6020811015613d9f57600080fd5b505192915050565b600082821115613e1857604080517f08c379a000000000000000000000000000000000000000000000000000000000815260206004820152601e60248201527f536166654d6174683a207375627472616374696f6e206f766572666c6f770000604482015290519081900360640190fd5b50900390565b73ffffffffffffffffffffffffffffffffffffffff8316613e8a576040517f08c379a0000000000000000000000000000000000000000000000000000000008152600401808060200182810382526025815260200180615a7c6025913960400191505060405180910390fd5b73ffffffffffffffffffffffffffffffffffffffff8216613ef6576040517f08c379a00000000000000000000000000000000000000000000000000000000081526004018080602001828103825260238152602001806156f06023913960400191505060405180910390fd5b6000613f078264e8d4a510006142c6565b90508373ffffffffffffffffffffffffffffffffffffffff1631811115613f79576040517f08c379a00000000000000000000000000000000000000000000000000000000081526004018080602001828103825260268152602001806158766026913960400191505060405180910390fd5b604080517fbeabacc800000000000000000000000000000000000000000000000000000000815273ffffffffffffffffffffffffffffffffffffffff8681166004830152851660248201526044810183905290517318000000000000000000000000000000000000009163beabacc89160648083019260209291908290030181600087803b15801561400a57600080fd5b505af115801561401e573d6000803e3d6000fd5b505050506040513d602081101561403457600080fd5b50516140a157604080517f08c379a000000000000000000000000000000000000000000000000000000000815260206004820152601660248201527f4e6174697665207472616e73666572206661696c656400000000000000000000604482015290519081900360640190fd5b8273ffffffffffffffffffffffffffffffffffffffff168473ffffffffffffffffffffffffffffffffffffffff167fddf252ad1be2c89b69c2b068fc378daa952ba7f163c4a11628f55a4df523b3ef846040518082815260200191505060405180910390a350505050565b600073ffffffffffffffffffffffffffffffffffffffff821631613ce68164e8d4a51000613cda565b613ac8816001614ae9565b600080547fffffffffffffffffffffffff00000000000000000000000000000000000000001673ffffffffffffffffffffffffffffffffffffffff92909216919091179055565b6004805460408051602060026001851615610100027fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff0190941693909304601f8101849004840282018401909252818152600093611d5a93919290918301828280156142345780601f1061420957610100808354040283529160200191614234565b820191906000526020600020905b81548152906001019060200180831161421757829003601f168201915b50505050506040518060400160405280600181526020017f3200000000000000000000000000000000000000000000000000000000000000815250614277614d39565b614d3d565b73ffffffffffffffffffffffffffffffffffffffff8084166000908152600a602090815260408083209386168352929052205461343890849084906142c19085614db1565b613b93565b6000613ce68383614e25565b612bd78585848487604051602001808481526020018381526020018260ff1660f81b815260010193505050506040516020818303038152906040526147e0565b73ffffffffffffffffffffffffffffffffffffffff86163314614380576040517f08c379a0000000000000000000000000000000000000000000000000000000008152600401808060200182810382526025815260200180615a026025913960400191505060405180910390fd5b61438c87838686614e98565b604080517fd099cc98ef71107a616c4f0f941f04c322d8e254fe26b3c6668db87aae413de860208083019190915273ffffffffffffffffffffffffffffffffffffffff808b1683850152891660608301526080820188905260a0820187905260c0820186905260e080830186905283518084039091018152610100909201909252805191012061441e90889083614f52565b61442887836150d0565b612de1878787613e1e565b7fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff8214806144615750428210155b6144cc57604080517f08c379a000000000000000000000000000000000000000000000000000000000815260206004820152601e60248201527f46696174546f6b656e56323a207065726d697420697320657870697265640000604482015290519081900360640190fd5b60006145746144d9614187565b73ffffffffffffffffffffffffffffffffffffffff80891660008181526011602090815260409182902080546001810190915582517f6e71edae12b1b97f4d1f60370fef10105fa2faae0126114a169c64845d6126c98184015280840194909452938b166060840152608083018a905260a083019390935260c08083018990528151808403909101815260e090920190528051910120615155565b905073fcff98b65f9ea559ec0df36f4072c7e3be0520df636ccea6528783856040518463ffffffff1660e01b8152600401808473ffffffffffffffffffffffffffffffffffffffff16815260200183815260200180602001828103825283818151815260200191508051906020019080838360005b838110156146015781810151838201526020016145e9565b50505050905090810190601f16801561462e5780820380516001836020036101000a031916815260200191505b5094505050505060206040518083038186803b15801561464d57600080fd5b505af4158015614661573d6000803e3d6000fd5b505050506040513d602081101561467757600080fd5b50516146e457604080517f08c379a000000000000000000000000000000000000000000000000000000000815260206004820152601a60248201527f454950323631323a20696e76616c6964207369676e6174757265000000000000604482015290519081900360640190fd5b6146ef868686613b93565b505050505050565b61343883836142c184604051806060016040528060258152602001615c116025913973ffffffffffffffffffffffffffffffffffffffff808a166000908152600a60209081526040808320938c1683529290522054919061518f565b6040805173ffffffffffffffffffffffffffffffffffffffff8416602482015260448082018490528251808303909101815260649091019091526020810180517bffffffffffffffffffffffffffffffffffffffffffffffffffffffff167fa9059cbb00000000000000000000000000000000000000000000000000000000179052613438908490615240565b6147ea8383615318565b614864837f158b0a9edf7a828aad02f63cd515c68ef2f50ba807396f6d12842833a159742960001b8585604051602001808481526020018373ffffffffffffffffffffffffffffffffffffffff16815260200182815260200193505050506040516020818303038152906040528051906020012083614f52565b73ffffffffffffffffffffffffffffffffffffffff8316600081815260106020908152604080832086845290915280822080547fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff00166001179055518492917f1cdd46ff242716cdaa72d159d339a485b3438398348d68f09d7c8c0a59353d8191a3505050565b6148f687838686614e98565b604080517f7c7c6cdb67a18743f49ec6fa9b35f50d52ed05cbed4cc592e13b44501c1a226760208083019190915273ffffffffffffffffffffffffffffffffffffffff808b1683850152891660608301526080820188905260a0820187905260c0820186905260e080830186905283518084039091018152610100909201909252805191012061441e90889083614f52565b612de187878787868689604051602001808481526020018381526020018260ff1660f81b81526001019350505050604051602081830303815290604052614433565b6000466149d8848483614d3d565b949350505050565b61382189898989898988888b604051602001808481526020018381526020018260ff1660f81b815260010193505050506040516020818303038152906040526148ea565b61382189898989898988888b604051602001808481526020018381526020018260ff1660f81b81526001019350505050604051602081830303815290604052614312565b6000808211614ad857604080517f08c379a000000000000000000000000000000000000000000000000000000000815260206004820152601a60248201527f536166654d6174683a206469766973696f6e206279207a65726f000000000000604482015290519081900360640190fd5b818381614ae157fe5b049392505050565b8015614c86573073ffffffffffffffffffffffffffffffffffffffff16638da5cb5b6040518163ffffffff1660e01b815260040160206040518083038186803b158015614b3557600080fd5b505afa158015614b49573d6000803e3d6000fd5b505050506040513d6020811015614b5f57600080fd5b505173ffffffffffffffffffffffffffffffffffffffff83811691161415614bd2576040517f08c379a000000000000000000000000000000000000000000000000000000000815260040180806020018281038252602b815260200180615a51602b913960400191505060405180910390fd5b604080517fe5c7160b00000000000000000000000000000000000000000000000000000000815273ffffffffffffffffffffffffffffffffffffffff8416600482015290517318000000000000000000000000000000000000019163e5c7160b9160248083019260209291908290030181600087803b158015614c5457600080fd5b505af1158015614c68573d6000803e3d6000fd5b505050506040513d6020811015614c7e57600080fd5b50614d359050565b604080517f31b2302000000000000000000000000000000000000000000000000000000000815273ffffffffffffffffffffffffffffffffffffffff841660048201529051731800000000000000000000000000000000000001916331b230209160248083019260209291908290030181600087803b158015614d0857600080fd5b505af1158015614d1c573d6000803e3d6000fd5b505050506040513d6020811015614d3257600080fd5b50505b5050565b4690565b8251602093840120825192840192909220604080517f8b73c3c69bb8fe3d512ecc4cf759cc79239f7b179b0ffacaa9a75d522b39400f8187015280820194909452606084019190915260808301919091523060a0808401919091528151808403909101815260c09092019052805191012090565b600082820183811015613ce657604080517f08c379a000000000000000000000000000000000000000000000000000000000815260206004820152601b60248201527f536166654d6174683a206164646974696f6e206f766572666c6f770000000000604482015290519081900360640190fd5b600082614e34575060006113a8565b82820282848281614e4157fe5b0414613ce6576040517f08c379a000000000000000000000000000000000000000000000000000000000815260040180806020018281038252602181526020018061598b6021913960400191505060405180910390fd5b814211614ef0576040517f08c379a000000000000000000000000000000000000000000000000000000000815260040180806020018281038252602b81526020018061573b602b913960400191505060405180910390fd5b804210614f48576040517f08c379a0000000000000000000000000000000000000000000000000000000008152600401808060200182810382526025815260200180615bec6025913960400191505060405180910390fd5b614d328484615318565b73fcff98b65f9ea559ec0df36f4072c7e3be0520df636ccea65284614f7e614f78614187565b86615155565b846040518463ffffffff1660e01b8152600401808473ffffffffffffffffffffffffffffffffffffffff16815260200183815260200180602001828103825283818151815260200191508051906020019080838360005b83811015614fed578181015183820152602001614fd5565b50505050905090810190601f16801561501a5780820380516001836020036101000a031916815260200191505b5094505050505060206040518083038186803b15801561503957600080fd5b505af415801561504d573d6000803e3d6000fd5b505050506040513d602081101561506357600080fd5b505161343857604080517f08c379a000000000000000000000000000000000000000000000000000000000815260206004820152601e60248201527f46696174546f6b656e56323a20696e76616c6964207369676e61747572650000604482015290519081900360640190fd5b73ffffffffffffffffffffffffffffffffffffffff8216600081815260106020908152604080832085845290915280822080547fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff00166001179055518392917f98de503528ee59b575ef0c0a2576a82497bfc029a5685b209e9ec333479b10a591a35050565b6040517f19010000000000000000000000000000000000000000000000000000000000008152600281019290925260228201526042902090565b60008184841115615238576040517f08c379a00000000000000000000000000000000000000000000000000000000081526004018080602001828103825283818151815260200191508051906020019080838360005b838110156151fd5781810151838201526020016151e5565b50505050905090810190601f16801561522a5780820380516001836020036101000a031916815260200191505b509250505060405180910390fd5b505050900390565b60606152a2826040518060400160405280602081526020017f5361666545524332303a206c6f772d6c6576656c2063616c6c206661696c65648152508573ffffffffffffffffffffffffffffffffffffffff166153a29092919063ffffffff16565b805190915015613438578080602001905160208110156152c157600080fd5b5051613438576040517f08c379a000000000000000000000000000000000000000000000000000000000815260040180806020018281038252602a815260200180615b15602a913960400191505060405180910390fd5b73ffffffffffffffffffffffffffffffffffffffff8216600090815260106020908152604080832084845290915290205460ff1615614d35576040517f08c379a000000000000000000000000000000000000000000000000000000000815260040180806020018281038252602e815260200180615b67602e913960400191505060405180910390fd5b60606149d88484600085856153b68561550d565b61542157604080517f08c379a000000000000000000000000000000000000000000000000000000000815260206004820152601d60248201527f416464726573733a2063616c6c20746f206e6f6e2d636f6e7472616374000000604482015290519081900360640190fd5b600060608673ffffffffffffffffffffffffffffffffffffffff1685876040518082805190602001908083835b6020831061548b57805182527fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffe0909201916020918201910161544e565b6001836020036101000a03801982511681845116808217855250505050505090500191505060006040518083038185875af1925050503d80600081146154ed576040519150601f19603f3d011682016040523d82523d6000602084013e6154f2565b606091505b5091509150615502828286615513565b979650505050505050565b3b151590565b60608315615522575081613ce6565b8251156155325782518084602001fd5b6040517f08c379a00000000000000000000000000000000000000000000000000000000081526020600482018181528451602484015284518593919283926044019190850190808383600083156151fd5781810151838201526020016151e5565b828054600181600116156101000203166002900490600052602060002090601f016020900481019282601f106155d457805160ff1916838001178555615601565b82800160010185558215615601579182015b828111156156015782518255916020019190600101906155e6565b5061560d92915061569d565b5090565b828054600181600116156101000203166002900490600052602060002090601f016020900481019282601f10615670578280017fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff00823516178555615601565b82800160010185558215615601579182015b82811115615601578235825591602001919060010190615682565b5b8082111561560d576000815560010161569e56fe46696174546f6b656e56325f323a20426c61636b6c697374696e672070726576696f75736c7920756e626c61636b6c6973746564206163636f756e742145524332303a207472616e7366657220746f20746865207a65726f20616464726573735061757361626c653a206e65772070617573657220697320746865207a65726f206164647265737346696174546f6b656e56323a20617574686f72697a6174696f6e206973206e6f74207965742076616c696446696174546f6b656e3a206275726e20616d6f756e74206e6f742067726561746572207468616e203046696174546f6b656e3a206d696e7420746f20746865207a65726f20616464726573734f776e61626c653a206e6577206f776e657220697320746865207a65726f206164647265737345524332303a20617070726f766520746f20746865207a65726f206164647265737346696174546f6b656e3a206e65772070617573657220697320746865207a65726f2061646472657373526573637561626c653a206e6577207265736375657220697320746865207a65726f206164647265737346696174546f6b656e3a206d696e7420616d6f756e74206e6f742067726561746572207468616e203045524332303a207472616e7366657220616d6f756e7420657863656564732062616c616e636546696174546f6b656e3a2063616c6c6572206973206e6f7420746865206d61737465724d696e746572426c61636b6c69737461626c653a2063616c6c6572206973206e6f742074686520626c61636b6c697374657246696174546f6b656e3a206275726e20616d6f756e7420657863656564732062616c616e636546696174546f6b656e3a2063616c6c6572206973206e6f742061206d696e74657246696174546f6b656e3a206e6577206d61737465724d696e74657220697320746865207a65726f2061646472657373526573637561626c653a2063616c6c6572206973206e6f74207468652072657363756572536166654d6174683a206d756c7469706c69636174696f6e206f766572666c6f7745524332303a207472616e7366657220616d6f756e74206578636565647320616c6c6f77616e636546696174546f6b656e3a206e657720626c61636b6c697374657220697320746865207a65726f206164647265737346696174546f6b656e56323a2063616c6c6572206d7573742062652074686520706179656546696174546f6b656e3a20636f6e747261637420697320616c726561647920696e697469616c697a65644e617469766546696174546f6b656e56325f323a2063616e6e6f7420626c61636b6c697374206f776e657245524332303a207472616e736665722066726f6d20746865207a65726f206164647265737345524332303a20617070726f76652066726f6d20746865207a65726f206164647265737346696174546f6b656e3a206d696e7420616d6f756e742065786365656473206d696e746572416c6c6f77616e63655061757361626c653a2063616c6c6572206973206e6f7420746865207061757365725361666545524332303a204552433230206f7065726174696f6e20646964206e6f74207375636365656446696174546f6b656e3a206e6577206f776e657220697320746865207a65726f206164647265737346696174546f6b656e56323a20617574686f72697a6174696f6e2069732075736564206f722063616e63656c6564426c61636b6c69737461626c653a206e657720626c61636b6c697374657220697320746865207a65726f2061646472657373426c61636b6c69737461626c653a206163636f756e7420697320626c61636b6c697374656446696174546f6b656e56323a20617574686f72697a6174696f6e206973206578706972656445524332303a2064656372656173656420616c6c6f77616e63652062656c6f77207a65726fa2646970667358221220e06530c9518c3f8a05f1004a77ca5bf40453a85869b880a2f64f7273f330cb2a64736f6c634300060c0033" - }, - "0xfcFf98B65F9ea559EC0df36F4072C7E3BE0520Df": { - "balance": "0x0", - "nonce": "0x1", - "code": "0x73fcff98b65f9ea559ec0df36f4072c7e3be0520df30146080604052600436106100355760003560e01c80636ccea6521461003a575b600080fd5b6101026004803603606081101561005057600080fd5b73ffffffffffffffffffffffffffffffffffffffff8235169160208101359181019060608101604082013564010000000081111561008d57600080fd5b82018360208201111561009f57600080fd5b803590602001918460018302840111640100000000831117156100c157600080fd5b91908080601f016020809104026020016040519081016040528093929190818152602001838380828437600092019190915250929550610116945050505050565b604080519115158252519081900360200190f35b600061012184610179565b610164578373ffffffffffffffffffffffffffffffffffffffff16610146848461017f565b73ffffffffffffffffffffffffffffffffffffffff16149050610172565b61016f848484610203565b90505b9392505050565b3b151590565b600081516041146101db576040517f08c379a00000000000000000000000000000000000000000000000000000000081526004018080602001828103825260238152602001806106296023913960400191505060405180910390fd5b60208201516040830151606084015160001a6101f98682858561042d565b9695505050505050565b60008060608573ffffffffffffffffffffffffffffffffffffffff16631626ba7e60e01b86866040516024018083815260200180602001828103825283818151815260200191508051906020019080838360005b8381101561026f578181015183820152602001610257565b50505050905090810190601f16801561029c5780820380516001836020036101000a031916815260200191505b50604080517fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffe08184030181529181526020820180517bffffffffffffffffffffffffffffffffffffffffffffffffffffffff167fffffffff000000000000000000000000000000000000000000000000000000009098169790971787525181519196909550859450925090508083835b6020831061036957805182527fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffe0909201916020918201910161032c565b6001836020036101000a038019825116818451168082178552505050505050905001915050600060405180830381855afa9150503d80600081146103c9576040519150601f19603f3d011682016040523d82523d6000602084013e6103ce565b606091505b50915091508180156103e257506020815110155b80156101f9575080517f1626ba7e00000000000000000000000000000000000000000000000000000000906020808401919081101561042057600080fd5b5051149695505050505050565b60007f7fffffffffffffffffffffffffffffff5d576e7357a4501ddfe92f46681b20a08211156104a8576040517f08c379a00000000000000000000000000000000000000000000000000000000081526004018080602001828103825260268152602001806106726026913960400191505060405180910390fd5b8360ff16601b141580156104c057508360ff16601c14155b15610516576040517f08c379a000000000000000000000000000000000000000000000000000000000815260040180806020018281038252602681526020018061064c6026913960400191505060405180910390fd5b600060018686868660405160008152602001604052604051808581526020018460ff1681526020018381526020018281526020019450505050506020604051602081039080840390855afa158015610572573d6000803e3d6000fd5b50506040517fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffe0015191505073ffffffffffffffffffffffffffffffffffffffff811661061f57604080517f08c379a000000000000000000000000000000000000000000000000000000000815260206004820152601c60248201527f45435265636f7665723a20696e76616c6964207369676e617475726500000000604482015290519081900360640190fd5b9594505050505056fe45435265636f7665723a20696e76616c6964207369676e6174757265206c656e67746845435265636f7665723a20696e76616c6964207369676e6174757265202776272076616c756545435265636f7665723a20696e76616c6964207369676e6174757265202773272076616c7565a2646970667358221220289705d6ae8701c9ed586541fa0ba342f754518aa4f0e59dbbda380bc9f2323964736f6c634300060c0033" - }, - "0x695D85E3437DaA1F8f6DE97C5aEdF75B82deA80a": { - "balance": "0x0", - "nonce": "0x1", - "code": "0x608060405234801561000f575f5ffd5b5060043610610111575f3560e01c80638da5cb5b1161009e5780639fd0506d1161006e5780639fd0506d146103cb578063da980fed146103d3578063e30c3978146103e6578063f2fde38b146103ee578063f77c479114610401575f5ffd5b80638da5cb5b146101b15780638e207848146101d15780639242164f146101e45780639fd02a36146102e8575f5ffd5b8063554bab3c116100e4578063554bab3c146101585780635c975abb1461016b578063715018a61461019957806379ba5097146101a15780638456cb59146101a9575f5ffd5b8063032b901b1461011557806306cb5b661461012a5780632bbdb79f1461013d5780633f4ba83a14610150575b5f5ffd5b610128610123366004610e06565b610409565b005b610128610138366004610e28565b6104b7565b61012861014b366004610e4e565b61054e565b61012861065b565b610128610166366004610e28565b6106b9565b5f5160206113515f395f51905f5254600160a01b900460ff1660405190151581526020015b60405180910390f35b61012861073d565b610128610750565b61012861079a565b6101b96107fc565b6040516001600160a01b039091168152602001610190565b6101286101df366004610e65565b610830565b6102826040805160c0810182525f80825260208201819052918101829052606081018290526080810182905260a08101829052905f5160206113315f395f51905f526040805160c08101825282546001600160401b038082168352600160401b820481166020840152600160801b9091041691810191909152600182015460608201526002820154608082015260039091015460a082015292915050565b60405161019091905f60c0820190506001600160401b0383511682526001600160401b0360208401511660208301526001600160401b036040840151166040830152606083015160608301526080830151608083015260a083015160a083015292915050565b6103be60408051610100810182525f80825260208201819052918101829052606081018290526080810182905260a0810182905260c0810182905260e08101829052905f5160206113315f395f51905f52604080516101008101825260059092015461ffff80821684526201000082048116602085015264010000000082048116928401929092526601000000000000810482166060840152600160401b810482166080840152600160501b8104821660a0840152600160601b8104821660c0840152600160701b90041660e082015292915050565b6040516101909190610e7e565b6101b9610986565b6101286103e1366004610f1e565b61099b565b6101b9610b79565b6101286103fc366004610e28565b610ba1565b6101b9610c26565b610411610c4e565b610419610c99565b5f8161ffff161161043d5760405163811da5f160e01b815260040160405180910390fd5b7f668f09ce856848ead6cb1ddee963f15ef833cea8958030868f867aec84385205805461ffff191661ffff83161781556040515f5160206113315f395f51905f52917faba8150f8eae6a1acc6824830944cdc19c670da2fae4fe28f4dfa04c0d6932d4916104ab9190610f30565b60405180910390a15050565b6104bf610cd1565b6001600160a01b0381166104e6576040516371ab47bb60e11b815260040160405180910390fd5b7f958f8fec699b51a1249f513eceda5429078000657f74abd1721bba363087af0080546001600160a01b0319166001600160a01b03831690811782556040517f1304018cfe79741dcf02ba6b61d39cc4757d59395d03224d9925c7aa83002146905f90a25050565b610556610c4e565b61055e610c99565b5f811161057e57604051635251cbd760e01b815260040160405180910390fd5b7f668f09ce856848ead6cb1ddee963f15ef833cea8958030868f867aec84385203819055604080515f5160206113315f395f51905f5280546001600160401b03808216845281851c81166020850152608091821c16938301939093527f668f09ce856848ead6cb1ddee963f15ef833cea8958030868f867aec843852015460608301527f668f09ce856848ead6cb1ddee963f15ef833cea8958030868f867aec84385202549282019290925260a081018390527f413a821e8d0eadcc30efbe627a655111b9f28f5999003a85b7718a14b0e329ef9060c0016104ab565b610663610d03565b5f5160206113515f395f51905f528054600160a01b900460ff16156106b657805460ff60a01b191681556040517f7805862f689e2f13df9f062ff482ad3ad112aca9e0847911ed832e158c525b33905f90a15b50565b6106c1610cd1565b6001600160a01b0381166106e85760405163a74995ab60e01b815260040160405180910390fd5b5f5160206113515f395f51905f5280546001600160a01b0319166001600160a01b03831690811782556040517fb80482a293ca2e013eda8683c9bd7fc8347cfdaeea5ede58cba46df502c2a604905f90a25050565b610745610cd1565b61074e5f610d3b565b565b338061075a610b79565b6001600160a01b0316146107915760405163118cdaa760e01b81526001600160a01b03821660048201526024015b60405180910390fd5b6106b681610d3b565b6107a2610d03565b5f5160206113515f395f51905f528054600160a01b900460ff166106b657805460ff60a01b1916600160a01b1781556040517f6985a02210a168e66602d3235cb6db0e70f92b3ba4d376a33c0f3d9434bff625905f90a150565b5f807f9016d09d72d40fdae2fd8ceac6b6234c7706214fd39c1cd1e609a0528c1993005b546001600160a01b031692915050565b610838610c4e565b610840610c99565b606461084f6020830183610fb8565b6001600160401b0316111561087757604051633e82ffd960e21b815260040160405180910390fd5b61271061088a6040830160208401610fb8565b6001600160401b031611156108b25760405163f87b4d6d60e01b815260040160405180910390fd5b8060800135816060013511156108db576040516336b1b98d60e11b815260040160405180910390fd5b5f8160a00135116108ff57604051635251cbd760e01b815260040160405180910390fd5b6127106109126060830160408401610fb8565b6001600160401b0316111561093a5760405163279eca6760e21b815260040160405180910390fd5b5f5160206113315f395f51905f5281816109548282610fd3565b9050507f413a821e8d0eadcc30efbe627a655111b9f28f5999003a85b7718a14b0e329ef826040516104ab9190611090565b5f805f5160206113515f395f51905f52610820565b6109a3610c4e565b6109ab610c99565b5f6109b96020830183610e06565b61ffff16116109db5760405163811da5f160e01b815260040160405180910390fd5b5f6109ec6040830160208401610e06565b61ffff1611610a0e5760405163055dd8c360e01b815260040160405180910390fd5b5f610a1f6060830160408401610e06565b61ffff1611610a4157604051632de6d8ed60e11b815260040160405180910390fd5b5f610a526080830160608401610e06565b61ffff1611610a7457604051634c14d46960e11b815260040160405180910390fd5b5f610a8560a0830160808401610e06565b61ffff1611610aa7576040516396e398e160e01b815260040160405180910390fd5b5f610ab860c0830160a08401610e06565b61ffff1611610ada57604051632218f17b60e21b815260040160405180910390fd5b5f610aeb60e0830160c08401610e06565b61ffff1611610b0d57604051631805fa5160e11b815260040160405180910390fd5b5f5160206113315f395f51905f52817f668f09ce856848ead6cb1ddee963f15ef833cea8958030868f867aec84385205610b478282611119565b9050507faba8150f8eae6a1acc6824830944cdc19c670da2fae4fe28f4dfa04c0d6932d4826040516104ab919061127f565b5f807f237e158222e3e6968b72b9db0d8043aacf074ad9f650f0d1606b4d82ee432c00610820565b610ba9610cd1565b7f237e158222e3e6968b72b9db0d8043aacf074ad9f650f0d1606b4d82ee432c0080546001600160a01b0319166001600160a01b0383169081178255610bed6107fc565b6001600160a01b03167f38d16b8cac22d99fc7c124b9cd0de2d3fa1faef420bfe791d8c362d765e2270060405160405180910390a35050565b5f807f958f8fec699b51a1249f513eceda5429078000657f74abd1721bba363087af00610820565b7f958f8fec699b51a1249f513eceda5429078000657f74abd1721bba363087af0080546001600160a01b031633146106b657604051630e971e6360e11b815260040160405180910390fd5b5f5160206113515f395f51905f528054600160a01b900460ff16156106b65760405163ab35696f60e01b815260040160405180910390fd5b33610cda6107fc565b6001600160a01b03161461074e5760405163118cdaa760e01b8152336004820152602401610788565b5f5160206113515f395f51905f5280546001600160a01b031633146106b65760405163daeefd6560e01b815260040160405180910390fd5b7f237e158222e3e6968b72b9db0d8043aacf074ad9f650f0d1606b4d82ee432c0080546001600160a01b0319168155610d7382610d77565b5050565b7f9016d09d72d40fdae2fd8ceac6b6234c7706214fd39c1cd1e609a0528c19930080546001600160a01b031981166001600160a01b03848116918217845560405192169182907f8be0079c531659141344cd1fd0a4f28419497f9722a3daafe3b4186f6b6457e0905f90a3505050565b61ffff811681146106b6575f5ffd5b8035610e0181610de7565b919050565b5f60208284031215610e16575f5ffd5b8135610e2181610de7565b9392505050565b5f60208284031215610e38575f5ffd5b81356001600160a01b0381168114610e21575f5ffd5b5f60208284031215610e5e575f5ffd5b5035919050565b5f60c0828403128015610e76575f5ffd5b509092915050565b5f6101008201905061ffff835116825261ffff602084015116602083015261ffff60408401511660408301526060830151610ebf606084018261ffff169052565b506080830151610ed5608084018261ffff169052565b5060a0830151610eeb60a084018261ffff169052565b5060c0830151610f0160c084018261ffff169052565b5060e0830151610f1760e084018261ffff169052565b5092915050565b5f610100828403128015610e76575f5ffd5b815461ffff8082168352601082901c166020830152610100820190602081901c61ffff166040840152603081901c61ffff166060840152604081901c61ffff166080840152605081901c61ffff1660a0840152606081901c61ffff1660c0840152607081901c61ffff1660e0840152610f17565b6001600160401b03811681146106b6575f5ffd5b5f60208284031215610fc8575f5ffd5b8135610e2181610fa4565b8135610fde81610fa4565b6001600160401b03811690508154816001600160401b03198216178355602084013561100981610fa4565b6fffffffffffffffff00000000000000008160401b16836fffffffffffffffffffffffffffffffff198416171784555050505f604083013561104a81610fa4565b825467ffffffffffffffff60801b1916608091821b67ffffffffffffffff60801b161783556060840135600184015583013560028301555060a090910135600390910155565b60c08101823561109f81610fa4565b6001600160401b0316825260208301356110b881610fa4565b6001600160401b0316602083015260408301356110d481610fa4565b6001600160401b03166040830152606083810135908301526080808401359083015260a092830135929091019190915290565b5f813561111381610de7565b92915050565b813561112481610de7565b61ffff8116905081548161ffff198216178355602084013561114581610de7565b63ffff00008160101b168363ffffffff1984161717845550505061118c61116e60408401611107565b825465ffff00000000191660209190911b65ffff0000000016178255565b6111bd61119b60608401611107565b825467ffff000000000000191660309190911b67ffff00000000000016178255565b6111f26111cc60808401611107565b825469ffff0000000000000000191660409190911b69ffff000000000000000016178255565b61122161120160a08401611107565b82805461ffff60501b191660509290921b61ffff60501b16919091179055565b61125061123060c08401611107565b82805461ffff60601b191660609290921b61ffff60601b16919091179055565b610d7361125f60e08401611107565b82805461ffff60701b191660709290921b61ffff60701b16919091179055565b6101008101823561128f81610de7565b61ffff16825260208301356112a381610de7565b61ffff1660208301526112b860408401610df6565b61ffff1660408301526112cd60608401610df6565b61ffff1660608301526112e260808401610df6565b61ffff1660808301526112f760a08401610df6565b61ffff1660a083015261130c60c08401610df6565b61ffff1660c083015261132160e08401610df6565b61ffff811660e0840152610f1756fe668f09ce856848ead6cb1ddee963f15ef833cea8958030868f867aec843852000642d7922329a434cf4fd17a3c95eb692c24fd95f9f94d0b55420a5d895f4a00a2646970667358221220046372fe4609a57ed6251208e9ef58c11217d4f47c96763ebfa709c47905168464736f6c634300081d0033" - }, - "0x3600000000000000000000000000000000000001": { - "balance": "0x0", - "nonce": "0x1", - "code": "0x608060405260043610610049575f3560e01c80633659cfe6146100535780634f1ef286146100725780635c60da1b146100855780638f283970146100b5578063f851a440146100d4575b6100516100e8565b005b34801561005e575f5ffd5b5061005161006d3660046104f3565b6100fa565b61005161008036600461050c565b610134565b348015610090575f5ffd5b50610099610197565b6040516001600160a01b03909116815260200160405180910390f35b3480156100c0575f5ffd5b506100516100cf3660046104f3565b6101a5565b3480156100df575f5ffd5b506100996101c5565b6100f86100f3610197565b6101ce565b565b6101026101ec565b6001600160a01b0316330361012c576101298160405180602001604052805f81525061021e565b50565b6101296100e8565b61013c6101ec565b6001600160a01b0316330361018f5761018a8383838080601f0160208091040260200160405190810160405280939291908181526020018383808284375f9201919091525061021e92505050565b505050565b61018a6100e8565b5f6101a0610277565b905090565b6101ad6101ec565b6001600160a01b0316330361012c576101298161029e565b5f6101a06101ec565b365f5f375f5f365f845af43d5f5f3e8080156101e8573d5ff35b3d5ffd5b5f7fb53127684a568b3173ae13b9f8a6016e243e63b6e8ee1178d6a717850b5d61035b546001600160a01b0316919050565b610227826102f2565b6040516001600160a01b038316907fbc7cd75a20ee27fd9adebab32041f755214dbc6bffa90cc0225b39da2e5c2d3b905f90a280511561026b5761018a8282610370565b6102736103e2565b5050565b5f7f360894a13ba1a3210667c828492db98dca3e2076cc3735a920a3ca505d382bbc61020f565b7f7e644d79422f17c01e4894b5f4f588d331ebfa28653d42ae832dc59e38c9798f6102c76101ec565b604080516001600160a01b03928316815291841660208301520160405180910390a161012981610401565b806001600160a01b03163b5f0361032c57604051634c9c8ce360e01b81526001600160a01b03821660048201526024015b60405180910390fd5b807f360894a13ba1a3210667c828492db98dca3e2076cc3735a920a3ca505d382bbc5b80546001600160a01b0319166001600160a01b039290921691909117905550565b60605f5f846001600160a01b03168460405161038c919061058a565b5f60405180830381855af49150503d805f81146103c4576040519150601f19603f3d011682016040523d82523d5f602084013e6103c9565b606091505b50915091506103d9858383610451565b95945050505050565b34156100f85760405163b398979f60e01b815260040160405180910390fd5b6001600160a01b03811661042a57604051633173bdd160e11b81525f6004820152602401610323565b807fb53127684a568b3173ae13b9f8a6016e243e63b6e8ee1178d6a717850b5d610361034f565b60608261046657610461826104b0565b6104a9565b815115801561047d57506001600160a01b0384163b155b156104a657604051639996b31560e01b81526001600160a01b0385166004820152602401610323565b50805b9392505050565b8051156104bf57805160208201fd5b60405163d6bda27560e01b815260040160405180910390fd5b80356001600160a01b03811681146104ee575f5ffd5b919050565b5f60208284031215610503575f5ffd5b6104a9826104d8565b5f5f5f6040848603121561051e575f5ffd5b610527846104d8565b9250602084013567ffffffffffffffff811115610542575f5ffd5b8401601f81018613610552575f5ffd5b803567ffffffffffffffff811115610568575f5ffd5b866020828401011115610579575f5ffd5b939660209190910195509293505050565b5f82518060208501845e5f92019182525091905056fea26469706673582212204c0881af9b906ca964ce2bc1cc4525350de8dd57658dacb806266290556eb9ae64736f6c634300081d0033", - "storage": { - "0xb53127684a568b3173ae13b9f8a6016e243e63b6e8ee1178d6a717850b5d6103": "0x000000000000000000000000a0ee7a142d267c1f36714e4a8f75612f20a79720", - "0x360894a13ba1a3210667c828492db98dca3e2076cc3735a920a3ca505d382bbc": "0x000000000000000000000000695d85e3437daa1f8f6de97c5aedf75b82dea80a", - "0x9016d09d72d40fdae2fd8ceac6b6234c7706214fd39c1cd1e609a0528c199300": "0x00000000000000000000000023618e81e3f5cdf7f54c3d65f7fbc0abf5b21e8f", - "0xf0c57e16840df040f15088dc2f81fe391c3923bec73e23a9662efc9c229c6a00": "0x0000000000000000000000000000000000000000000000000000000000000001", - "0x958f8fec699b51a1249f513eceda5429078000657f74abd1721bba363087af00": "0x00000000000000000000000023618e81e3f5cdf7f54c3d65f7fbc0abf5b21e8f", - "0x0642d7922329a434cf4fd17a3c95eb692c24fd95f9f94d0b55420a5d895f4a00": "0x00000000000000000000000023618e81e3f5cdf7f54c3d65f7fbc0abf5b21e8f", - "0x668f09ce856848ead6cb1ddee963f15ef833cea8958030868f867aec84385200": "0x0000000000000000000000000000138800000000000000c80000000000000014", - "0x668f09ce856848ead6cb1ddee963f15ef833cea8958030868f867aec84385201": "0x0000000000000000000000000000000000000000000000000000000000000001", - "0x668f09ce856848ead6cb1ddee963f15ef833cea8958030868f867aec84385202": "0x000000000000000000000000000000000000000000000000000000e8d4a51000", - "0x668f09ce856848ead6cb1ddee963f15ef833cea8958030868f867aec84385203": "0x0000000000000000000000000000000000000000000000000000000001c9c380", - "0x668f09ce856848ead6cb1ddee963f15ef833cea8958030868f867aec84385205": "0x0000000000000000000000000000000001f403e801f403e801f403e801f40bb8" - } - }, - "0xfCc314DD5Ad756C6bBA725617438C0d25450a0dE": { - "balance": "0x0", - "nonce": "0x1", - "code": "0x608060405234801561000f575f5ffd5b50600436106100e5575f3560e01c80638da5cb5b11610088578063c4899f0c11610063578063c4899f0c146101cc578063e30c3978146101df578063f2fde38b146101e7578063f94e1867146101fa575f5ffd5b80638da5cb5b14610184578063b5d89627146101a4578063b86444b1146101c4575f5ffd5b80634ebae617116100c35780634ebae61714610138578063715018a61461014d57806377db16691461015557806379ba50971461017c575f5ffd5b806303a9b132146100e957806324408a68146101105780632469d67214610125575b5f5ffd5b6100fd5f5160206113f05f395f51905f5281565b6040519081526020015b60405180910390f35b61011861020d565b604051610107919061104a565b6100fd6101333660046110dc565b6103ed565b61014b61014636600461119d565b610598565b005b61014b6106a0565b7fb58da0dce03316992faea3e12c60705b8ac05a309e27e3bc8421e5b271c9d204546100fd565b61014b6106b3565b61018c6106fb565b6040516001600160a01b039091168152602001610107565b6101b76101b236600461119d565b61072f565b60405161010791906111b4565b6100fd610854565b61014b6101da3660046111c6565b610872565b61018c6109db565b61014b6101f53660046111e7565b610a03565b61014b61020836600461119d565b610a88565b60605f5160206113f05f395f51905f525f6102477fb58da0dce03316992faea3e12c60705b8ac05a309e27e3bc8421e5b271c9d201610c92565b9050806001600160401b03811115610261576102616110ad565b6040519080825280602002602001820160405280156102b557816020015b6102a26040805160608101909152805f8152606060208201525f60409091015290565b81526020019060019003908161027f5790505b5092505f5b818110156103e7575f6102d06001850183610c9b565b5f818152602086905260409081902081516060810190925280549293509091829060ff16600281111561030557610305610fa6565b600281111561031657610316610fa6565b815260200160018201805461032a9061120d565b80601f01602080910402602001604051908101604052809291908181526020018280546103569061120d565b80156103a15780601f10610378576101008083540402835291602001916103a1565b820191905f5260205f20905b81548152906001019060200180831161038457829003601f168201915b5050509183525050600291909101546001600160401b031660209091015285518690849081106103d3576103d3611245565b6020908102919091010152506001016102ba565b50505090565b5f6103f6610cad565b602083511461041857604051630717c3c160e01b815260040160405180910390fd5b82516020808501919091205f8181527fb58da0dce03316992faea3e12c60705b8ac05a309e27e3bc8421e5b271c9d2039092526040909120545f5160206113f05f395f51905f529190819060ff1615610490576040516338a14a4160e01b815260040161048791815260200190565b60405180910390fd5b50600482018054905f6104a28361126d565b909155506040805160608101909152909350806001815260208082018890526001600160401b0387166040928301525f868152908590522081518154829060ff191660018360028111156104f8576104f8610fa6565b02179055506020820151600182019061051190826112d1565b50604091820151600291909101805467ffffffffffffffff19166001600160401b039092169190911790555f82815260038401602052819020805460ff191660011790555183907fea20c1f9de86768933c1d4eff47ce667e26f886c4877d1bedc253e42b2f04a7c90610587908790899061138b565b60405180910390a250505b92915050565b6105a0610cad565b5f5f5160206113f05f395f51905f525f838152602082905260408120919250815460ff1660028111156105d5576105d5610fa6565b141583906105f9576040516355b54a8d60e01b815260040161048791815260200190565b506001815460ff16600281111561061257610612610fa6565b8254859260ff909116911461063c57604051635c12665560e01b81526004016104879291906113b4565b5050805460ff191660021781556106566001830184610cdf565b5060028101546040516001600160401b03909116815283907fbc5136437746415b39af863272d0ca406634cf14bb7c3e5fa03855781add87e59060200160405180910390a2505050565b6106a8610cad565b6106b15f610cea565b565b33806106bd6109db565b6001600160a01b0316146106ef5760405163118cdaa760e01b81526001600160a01b0382166004820152602401610487565b6106f881610cea565b50565b5f807f9016d09d72d40fdae2fd8ceac6b6234c7706214fd39c1cd1e609a0528c1993005b546001600160a01b031692915050565b6107526040805160608101909152805f8152606060208201525f60409091015290565b5f8281525f5160206113f05f395f51905f5260208190526040918290208251606081019093528054919291829060ff16600281111561079357610793610fa6565b60028111156107a4576107a4610fa6565b81526020016001820180546107b89061120d565b80601f01602080910402602001604051908101604052809291908181526020018280546107e49061120d565b801561082f5780601f106108065761010080835404028352916020019161082f565b820191905f5260205f20905b81548152906001019060200180831161081257829003601f168201915b5050509183525050600291909101546001600160401b03166020909101529392505050565b5f5f5160206113f05f395f51905f5261086c81610d26565b91505090565b61087a610cad565b5f5f5160206113f05f395f51905f525f848152602082905260408120919250815460ff1660028111156108af576108af610fa6565b141584906108d3576040516355b54a8d60e01b815260040161048791815260200190565b5060028101546001600160401b03908116908416810361090657604051632bf0186d60e21b815260040160405180910390fd5b6002825460ff16600281111561091e5761091e610fa6565b14801561093357505f816001600160401b0316115b801561094657506001600160401b038416155b1561097457600161095684610d26565b116109745760405163aff8365b60e01b815260040160405180910390fd5b60028201805467ffffffffffffffff19166001600160401b03868116918217909255604080519284168352602083019190915286917f4a7e1cc2e075be9a3c913bf00ec8ff10f6630f23433f65eee31e20ef69110ae4910160405180910390a25050505050565b5f807f237e158222e3e6968b72b9db0d8043aacf074ad9f650f0d1606b4d82ee432c0061071f565b610a0b610cad565b7f237e158222e3e6968b72b9db0d8043aacf074ad9f650f0d1606b4d82ee432c0080546001600160a01b0319166001600160a01b0383169081178255610a4f6106fb565b6001600160a01b03167f38d16b8cac22d99fc7c124b9cd0de2d3fa1faef420bfe791d8c362d765e2270060405160405180910390a35050565b610a90610cad565b5f5f5160206113f05f395f51905f525f838152602082905260408120919250815460ff166002811115610ac557610ac5610fa6565b14158390610ae9576040516355b54a8d60e01b815260040161048791815260200190565b506002815460ff166002811115610b0257610b02610fa6565b148015610b1b575060028101546001600160401b031615155b15610b49576001610b2b83610d26565b11610b495760405163aff8365b60e01b815260040160405180910390fd5b5f816001018054610b599061120d565b80601f0160208091040260200160405190810160405280929190818152602001828054610b859061120d565b8015610bd05780601f10610ba757610100808354040283529160200191610bd0565b820191905f5260205f20905b815481529060010190602001808311610bb357829003601f168201915b5050835160208501206002870154949550936001600160401b03169250610bfd9150506001860187610d8c565b505f868152602086905260408120805460ff1916815590610c216001830182610f5c565b50600201805467ffffffffffffffff191690555f828152600386016020908152604091829020805460ff1916905590516001600160401b038316815287917f70d022b699831c183688ed9d9d1a1cbf586698b506eaacfd5195a4e3738d285b910160405180910390a2505050505050565b5f610592825490565b5f610ca68383610d97565b9392505050565b33610cb66106fb565b6001600160a01b0316146106b15760405163118cdaa760e01b8152336004820152602401610487565b5f610ca68383610dbd565b7f237e158222e3e6968b72b9db0d8043aacf074ad9f650f0d1606b4d82ee432c0080546001600160a01b0319168155610d2282610e09565b5050565b5f5f610d3483600101610c92565b90505f5b81811015610d85575f610d4e6001860183610c9b565b5f818152602087905260409020600201549091506001600160401b031615610d7c57610d798461126d565b93505b50600101610d38565b5050919050565b5f610ca68383610e79565b5f825f018281548110610dac57610dac611245565b905f5260205f200154905092915050565b5f818152600183016020526040812054610e0257508154600181810184555f848152602080822090930184905584548482528286019093526040902091909155610592565b505f610592565b7f9016d09d72d40fdae2fd8ceac6b6234c7706214fd39c1cd1e609a0528c19930080546001600160a01b031981166001600160a01b03848116918217845560405192169182907f8be0079c531659141344cd1fd0a4f28419497f9722a3daafe3b4186f6b6457e0905f90a3505050565b5f8181526001830160205260408120548015610f53575f610e9b6001836113c8565b85549091505f90610eae906001906113c8565b9050808214610f0d575f865f018281548110610ecc57610ecc611245565b905f5260205f200154905080875f018481548110610eec57610eec611245565b5f918252602080832090910192909255918252600188019052604090208390555b8554869080610f1e57610f1e6113db565b600190038181905f5260205f20015f90559055856001015f8681526020019081526020015f205f905560019350505050610592565b5f915050610592565b508054610f689061120d565b5f825580601f10610f77575050565b601f0160209004905f5260205f20908101906106f891905b80821115610fa2575f8155600101610f8f565b5090565b634e487b7160e01b5f52602160045260245ffd5b60038110610fd657634e487b7160e01b5f52602160045260245ffd5b9052565b5f81518084528060208401602086015e5f602082860101526020601f19601f83011685010191505092915050565b611013828251610fba565b5f60208201516060602085015261102d6060850182610fda565b6040938401516001600160401b0316949093019390935250919050565b5f602082016020835280845180835260408501915060408160051b8601019250602086015f5b828110156110a157603f1987860301845261108c858351611008565b94506020938401939190910190600101611070565b50929695505050505050565b634e487b7160e01b5f52604160045260245ffd5b80356001600160401b03811681146110d7575f5ffd5b919050565b5f5f604083850312156110ed575f5ffd5b82356001600160401b03811115611102575f5ffd5b8301601f81018513611112575f5ffd5b80356001600160401b0381111561112b5761112b6110ad565b604051601f8201601f19908116603f011681016001600160401b0381118282101715611159576111596110ad565b604052818152828201602001871015611170575f5ffd5b816020840160208301375f60208383010152809450505050611194602084016110c1565b90509250929050565b5f602082840312156111ad575f5ffd5b5035919050565b602081525f610ca66020830184611008565b5f5f604083850312156111d7575f5ffd5b82359150611194602084016110c1565b5f602082840312156111f7575f5ffd5b81356001600160a01b0381168114610ca6575f5ffd5b600181811c9082168061122157607f821691505b60208210810361123f57634e487b7160e01b5f52602260045260245ffd5b50919050565b634e487b7160e01b5f52603260045260245ffd5b634e487b7160e01b5f52601160045260245ffd5b5f6001820161127e5761127e611259565b5060010190565b601f8211156112cc57805f5260205f20601f840160051c810160208510156112aa5750805b601f840160051c820191505b818110156112c9575f81556001016112b6565b50505b505050565b81516001600160401b038111156112ea576112ea6110ad565b6112fe816112f8845461120d565b84611285565b6020601f821160018114611330575f83156113195750848201515b5f19600385901b1c1916600184901b1784556112c9565b5f84815260208120601f198516915b8281101561135f578785015182556020948501946001909201910161133f565b508482101561137c57868401515f19600387901b60f8161c191681555b50505050600190811b01905550565b6001600160401b0383168152604060208201525f6113ac6040830184610fda565b949350505050565b82815260408101610ca66020830184610fba565b8181038181111561059257610592611259565b634e487b7160e01b5f52603160045260245ffdfeb58da0dce03316992faea3e12c60705b8ac05a309e27e3bc8421e5b271c9d200a2646970667358221220e5d087314472ac48a65ba1a9c38dbf61b71eb3c2cc0f9b03ade2ac62ec97aecc64736f6c634300081d0033" - }, - "0x3600000000000000000000000000000000000002": { - "balance": "0x0", - "nonce": "0x1", - "code": "0x608060405260043610610049575f3560e01c80633659cfe6146100535780634f1ef286146100725780635c60da1b146100855780638f283970146100b5578063f851a440146100d4575b6100516100e8565b005b34801561005e575f5ffd5b5061005161006d3660046104f3565b6100fa565b61005161008036600461050c565b610134565b348015610090575f5ffd5b50610099610197565b6040516001600160a01b03909116815260200160405180910390f35b3480156100c0575f5ffd5b506100516100cf3660046104f3565b6101a5565b3480156100df575f5ffd5b506100996101c5565b6100f86100f3610197565b6101ce565b565b6101026101ec565b6001600160a01b0316330361012c576101298160405180602001604052805f81525061021e565b50565b6101296100e8565b61013c6101ec565b6001600160a01b0316330361018f5761018a8383838080601f0160208091040260200160405190810160405280939291908181526020018383808284375f9201919091525061021e92505050565b505050565b61018a6100e8565b5f6101a0610277565b905090565b6101ad6101ec565b6001600160a01b0316330361012c576101298161029e565b5f6101a06101ec565b365f5f375f5f365f845af43d5f5f3e8080156101e8573d5ff35b3d5ffd5b5f7fb53127684a568b3173ae13b9f8a6016e243e63b6e8ee1178d6a717850b5d61035b546001600160a01b0316919050565b610227826102f2565b6040516001600160a01b038316907fbc7cd75a20ee27fd9adebab32041f755214dbc6bffa90cc0225b39da2e5c2d3b905f90a280511561026b5761018a8282610370565b6102736103e2565b5050565b5f7f360894a13ba1a3210667c828492db98dca3e2076cc3735a920a3ca505d382bbc61020f565b7f7e644d79422f17c01e4894b5f4f588d331ebfa28653d42ae832dc59e38c9798f6102c76101ec565b604080516001600160a01b03928316815291841660208301520160405180910390a161012981610401565b806001600160a01b03163b5f0361032c57604051634c9c8ce360e01b81526001600160a01b03821660048201526024015b60405180910390fd5b807f360894a13ba1a3210667c828492db98dca3e2076cc3735a920a3ca505d382bbc5b80546001600160a01b0319166001600160a01b039290921691909117905550565b60605f5f846001600160a01b03168460405161038c919061058a565b5f60405180830381855af49150503d805f81146103c4576040519150601f19603f3d011682016040523d82523d5f602084013e6103c9565b606091505b50915091506103d9858383610451565b95945050505050565b34156100f85760405163b398979f60e01b815260040160405180910390fd5b6001600160a01b03811661042a57604051633173bdd160e11b81525f6004820152602401610323565b807fb53127684a568b3173ae13b9f8a6016e243e63b6e8ee1178d6a717850b5d610361034f565b60608261046657610461826104b0565b6104a9565b815115801561047d57506001600160a01b0384163b155b156104a657604051639996b31560e01b81526001600160a01b0385166004820152602401610323565b50805b9392505050565b8051156104bf57805160208201fd5b60405163d6bda27560e01b815260040160405180910390fd5b80356001600160a01b03811681146104ee575f5ffd5b919050565b5f60208284031215610503575f5ffd5b6104a9826104d8565b5f5f5f6040848603121561051e575f5ffd5b610527846104d8565b9250602084013567ffffffffffffffff811115610542575f5ffd5b8401601f81018613610552575f5ffd5b803567ffffffffffffffff811115610568575f5ffd5b866020828401011115610579575f5ffd5b939660209190910195509293505050565b5f82518060208501845e5f92019182525091905056fea26469706673582212204c0881af9b906ca964ce2bc1cc4525350de8dd57658dacb806266290556eb9ae64736f6c634300081d0033", - "storage": { - "0xb53127684a568b3173ae13b9f8a6016e243e63b6e8ee1178d6a717850b5d6103": "0x000000000000000000000000a0ee7a142d267c1f36714e4a8f75612f20a79720", - "0x360894a13ba1a3210667c828492db98dca3e2076cc3735a920a3ca505d382bbc": "0x000000000000000000000000fcc314dd5ad756c6bba725617438c0d25450a0de", - "0xf0c57e16840df040f15088dc2f81fe391c3923bec73e23a9662efc9c229c6a00": "0x0000000000000000000000000000000000000000000000000000000000000001", - "0x9016d09d72d40fdae2fd8ceac6b6234c7706214fd39c1cd1e609a0528c199300": "0x0000000000000000000000003600000000000000000000000000000000000003", - "0x0499f4c2de309a54da703ff88d2d815fa145b3fb32cf77b2528c8033db8fd7ee": "0x0000000000000000000000000000000000000000000000000000000000000002", - "0x0499f4c2de309a54da703ff88d2d815fa145b3fb32cf77b2528c8033db8fd7ef": "0x0000000000000000000000000000000000000000000000000000000000000041", - "0xe76fb05174802623d7f5b79138467ede24c07a48fb794feb0f195f82c29e25f6": "0xc992c8696818bda11d628f38584022a6332c144b7f929b4d972bd39a23244aec", - "0x0499f4c2de309a54da703ff88d2d815fa145b3fb32cf77b2528c8033db8fd7f0": "0x0000000000000000000000000000000000000000000000000000000000000014", - "0x2559e4fd6bae46fd4a933fbdfc2024f30e3f30caf9b658f266112e99c269472e": "0x0000000000000000000000000000000000000000000000000000000000000001", - "0x3c22fd2722f4c68bee66b0771765c1374bf0b4f04712a4b14dee7a423e372484": "0x0000000000000000000000000000000000000000000000000000000000000001", - "0x2054cc904f2a6ebd28b7112562733ad9096dff6391c1f32ca7c9de3a68d45cdb": "0x0000000000000000000000000000000000000000000000000000000000000001", - "0x799a8c5a05520e2bd7f998b6bef5c37455a07d2f4637750d18e8585f32667020": "0x0000000000000000000000000000000000000000000000000000000000000002", - "0x799a8c5a05520e2bd7f998b6bef5c37455a07d2f4637750d18e8585f32667021": "0x0000000000000000000000000000000000000000000000000000000000000041", - "0x88471ae418c3e7b280a001c3ca67ebba02159f936da01ae6a2e84cabdddd1ff9": "0x35121369a803f64463e1688af1ba5d963a40b7d71eeffadd8496f1d5b8d61d53", - "0x799a8c5a05520e2bd7f998b6bef5c37455a07d2f4637750d18e8585f32667022": "0x0000000000000000000000000000000000000000000000000000000000000014", - "0x2559e4fd6bae46fd4a933fbdfc2024f30e3f30caf9b658f266112e99c269472f": "0x0000000000000000000000000000000000000000000000000000000000000002", - "0x095131cda7b4097ad9172b46969f424386afbffb0fe58d634a10ceead00fe90f": "0x0000000000000000000000000000000000000000000000000000000000000002", - "0x0cde826277e13063cb2aca0b317414dd401796eac98c911f623b91595e6d63c2": "0x0000000000000000000000000000000000000000000000000000000000000001", - "0x5823124c2578a0659c1d18b6eac7cd5d39d30d9e0a439c0dc79350c911078892": "0x0000000000000000000000000000000000000000000000000000000000000002", - "0x5823124c2578a0659c1d18b6eac7cd5d39d30d9e0a439c0dc79350c911078893": "0x0000000000000000000000000000000000000000000000000000000000000041", - "0xe4bbd97d770f505bda53f50c8dce4c86cb0dc400a1b5c97a9aacf7305b0907af": "0xeda755457e2e7b8cb56956372611f5b5d37698eef26aa7fdb01616a6e7824f22", - "0x5823124c2578a0659c1d18b6eac7cd5d39d30d9e0a439c0dc79350c911078894": "0x0000000000000000000000000000000000000000000000000000000000000014", - "0x2559e4fd6bae46fd4a933fbdfc2024f30e3f30caf9b658f266112e99c2694730": "0x0000000000000000000000000000000000000000000000000000000000000003", - "0xe0ed4630bad2f511c8f295d4c67204cad855df4001547c1a63b7ba76c4642997": "0x0000000000000000000000000000000000000000000000000000000000000003", - "0xe58a244b526592696f8534b954151702c1af6e74ebe9591b0ea8dfcb7f7e79cf": "0x0000000000000000000000000000000000000000000000000000000000000001", - "0x57b2761a28897dbfc1a24bc792964a22adcbfdba40250a97c45c81e485bce164": "0x0000000000000000000000000000000000000000000000000000000000000002", - "0x57b2761a28897dbfc1a24bc792964a22adcbfdba40250a97c45c81e485bce165": "0x0000000000000000000000000000000000000000000000000000000000000041", - "0x7561b913640321094a9a7cd9b03d1d14d3f4896580bd453b53749e3a0a7f8f2f": "0x1d9bb50136b82ed67804df002b5aff9adb98ee83160cbbd1bdf133c3a5320316", - "0x57b2761a28897dbfc1a24bc792964a22adcbfdba40250a97c45c81e485bce166": "0x0000000000000000000000000000000000000000000000000000000000000014", - "0x2559e4fd6bae46fd4a933fbdfc2024f30e3f30caf9b658f266112e99c2694731": "0x0000000000000000000000000000000000000000000000000000000000000004", - "0x2b469f95fec2239186437567ca1ebd5a0dcb6f582da84f4a3e535a89c60299ce": "0x0000000000000000000000000000000000000000000000000000000000000004", - "0x7d8d471c9ae79270b53b982036568a58893f19f98dc66043513c06a78f2c1471": "0x0000000000000000000000000000000000000000000000000000000000000001", - "0x5217b172c9c68060ac3f2cea97f04d4b6a19ffc38de72bde7614d108da793171": "0x0000000000000000000000000000000000000000000000000000000000000002", - "0x5217b172c9c68060ac3f2cea97f04d4b6a19ffc38de72bde7614d108da793172": "0x0000000000000000000000000000000000000000000000000000000000000041", - "0xb81fd3b52b30fb1cee951216c9225dedbf87cab17122027c6b61d64229d70ce1": "0xce27e3155b7cfc8c34af4c80bc6170847cebcada4db441e353f5f6a590240dde", - "0x5217b172c9c68060ac3f2cea97f04d4b6a19ffc38de72bde7614d108da793173": "0x0000000000000000000000000000000000000000000000000000000000000014", - "0x2559e4fd6bae46fd4a933fbdfc2024f30e3f30caf9b658f266112e99c2694732": "0x0000000000000000000000000000000000000000000000000000000000000005", - "0x1f0160c61ae74490159fc3ec6e25ec82a084ca7c5a75f5a3e2fb22830c3d10db": "0x0000000000000000000000000000000000000000000000000000000000000005", - "0x083cda9c763adfb7ca56577a2544979b5a10a6d2926f61e06132506a54242995": "0x0000000000000000000000000000000000000000000000000000000000000001", - "0xb58da0dce03316992faea3e12c60705b8ac05a309e27e3bc8421e5b271c9d201": "0x0000000000000000000000000000000000000000000000000000000000000005", - "0xb58da0dce03316992faea3e12c60705b8ac05a309e27e3bc8421e5b271c9d204": "0x0000000000000000000000000000000000000000000000000000000000000006" - } - }, - "0x98C74F7eF54BA22be94415d7C967B352c6a42D6d": { - "balance": "0x0", - "nonce": "0x1", - "code": "0x608060405234801561000f575f5ffd5b50600436106101dc575f3560e01c806379ba509711610109578063d2c20cb31161009e578063ead130111161006e578063ead130111461042f578063ec4a44d714610443578063f2fde38b14610457578063f6a74ed71461046a575f5ffd5b8063d2c20cb3146103ee578063d773a74114610401578063da3003f914610414578063e30c397814610427575f5ffd5b80638da5cb5b116100d95780638da5cb5b146103955780639fd0506d1461039d578063aa5ecb52146103a5578063b429afeb146103b8575f5ffd5b806379ba5097146103765780637f77403d1461037e5780637ffc51fd146103865780638456cb591461038d575f5ffd5b8063485cc9551161017f5780635c975abb1161014f5780635c975abb1461031c5780635e5feee61461033a578063602a9eee1461034d578063715018a61461036e575f5ffd5b8063485cc955146102b85780634cb4850a146102cb578063554bab3c146102de5780635acac2b6146102f1575f5ffd5b8063263a3402116101ba578063263a34021461028b5780632eb5c6581461029557806337ec36d1146102a85780633f4ba83a146102b0575f5ffd5b8063048544a4146101e057806306433b1b1461022c5780631904bb2e1461026b575b5f5ffd5b6102176101ee366004611514565b6001600160a01b03165f9081525f5160206118f35f395f51905f52602052604090205460ff1690565b60405190151581526020015b60405180910390f35b6102537f000000000000000000000000360000000000000000000000000000000000000281565b6040516001600160a01b039091168152602001610223565b61027e610279366004611514565b61047d565b604051610223919061155b565b610293610558565b005b6102936102a33660046115d1565b610603565b6102936106e7565b610293610787565b6102936102c6366004611606565b6107e5565b6102936102d9366004611637565b610971565b6102936102ec366004611514565b610a92565b6103046102ff366004611514565b610b16565b6040516001600160401b039091168152602001610223565b5f5160206118d35f395f51905f5254600160a01b900460ff16610217565b610293610348366004611514565b610b84565b61036061035b3660046116e4565b610c5f565b604051908152602001610223565b610293610d06565b610293610d19565b610293610d5e565b6103045f81565b610293610ddc565b610253610e3e565b610253610e72565b6102936103b3366004611514565b610e87565b6102176103c6366004611514565b6001600160a01b03165f9081525f5160206118b35f395f51905f526020526040902054151590565b6102936103fc36600461175d565b610f46565b61036061040f366004611514565b611055565b610293610422366004611514565b6110a2565b610253611165565b6103605f5160206118b35f395f51905f5281565b6103605f5160206118f35f395f51905f5281565b610293610465366004611514565b61118d565b610293610478366004611514565b611212565b6104a06040805160608101909152805f8152606060208201525f60409091015290565b5f5f5160206118b35f395f51905f526001600160a01b038481165f908152602083905260409081902054905163b5d8962760e01b815260048101829052929350917f00000000000000000000000036000000000000000000000000000000000000029091169063b5d89627906024015f60405180830381865afa158015610529573d5f5f3e3d5ffd5b505050506040513d5f823e601f3d908101601f1916820160405261055091908101906117a5565b949350505050565b6105606112e4565b610568611321565b335f9081525f5160206118b35f395f51905f526020819052604091829020549151634ebae61760e01b8152600481018390529091907f00000000000000000000000036000000000000000000000000000000000000026001600160a01b031690634ebae617906024015b5f604051808303815f87803b1580156105e9575f5ffd5b505af11580156105fb573d5f5f3e3d5ffd5b505050505050565b61060b611359565b6001600160a01b038216610632576040516371ab47bb60e11b815260040160405180910390fd5b6001600160a01b0382165f9081525f5160206118b35f395f51905f5260208190526040822054909103610678576040516308c2ec5b60e41b815260040160405180910390fd5b6001600160a01b0383165f818152600183016020908152604091829020805467ffffffffffffffff19166001600160401b03871690811790915591519182527fcf03087b939040458ea3c3a6791532c7c4432baf98e770abc930afc9cda81d87910160405180910390a2505050565b6106ef611359565b7f00000000000000000000000036000000000000000000000000000000000000026001600160a01b03166379ba50976040518163ffffffff1660e01b81526004015f604051808303815f87803b158015610747575f5ffd5b505af1158015610759573d5f5f3e3d5ffd5b50506040517f54b70ab40993761a2b0e96f42fdf47939a56830ede1325df83e05ed33e3e8fd592505f9150a1565b61078f61138b565b5f5160206118d35f395f51905f528054600160a01b900460ff16156107e257805460ff60a01b191681556040517f7805862f689e2f13df9f062ff482ad3ad112aca9e0847911ed832e158c525b33905f90a15b50565b7ff0c57e16840df040f15088dc2f81fe391c3923bec73e23a9662efc9c229c6a008054600160401b810460ff1615906001600160401b03165f811580156108295750825b90505f826001600160401b031660011480156108445750303b155b905081158015610852575080155b156108705760405163f92ee8a960e01b815260040160405180910390fd5b845467ffffffffffffffff19166001178555831561089a57845460ff60401b1916600160401b1785555b6001600160a01b0387166108c1576040516342cad95760e01b815260040160405180910390fd5b6001600160a01b0386166108e85760405163a74995ab60e01b815260040160405180910390fd5b6108f06113c3565b6108f9876113cb565b5f5f5160206118d35f395f51905f5280546001600160a01b0319166001600160a01b03891617905550831561096857845460ff60401b19168555604051600181527fc7f505b2f371ae2175ee4913f4499e1f2633a7b5936321eed1cdaeb6115181d29060200160405180910390a15b50505050505050565b6109796112e4565b610981611321565b335f9081525f5160206118b35f395f51905f5260208181526040808420547fe90ec3add3e251bfbe914c9e482b511e91a3b187718c1dc10223f64a8a644a01909252909220549091906001600160401b03908116908416811015610a0857604051635af79e0d60e11b81526001600160401b03821660048201526024015b60405180910390fd5b60405163312267c360e21b8152600481018390526001600160401b03851660248201527f00000000000000000000000036000000000000000000000000000000000000026001600160a01b03169063c4899f0c906044015f604051808303815f87803b158015610a76575f5ffd5b505af1158015610a88573d5f5f3e3d5ffd5b5050505050505050565b610a9a611359565b6001600160a01b038116610ac15760405163a74995ab60e01b815260040160405180910390fd5b5f5160206118d35f395f51905f5280546001600160a01b0319166001600160a01b03831690811782556040517fb80482a293ca2e013eda8683c9bd7fc8347cfdaeea5ede58cba46df502c2a604905f90a25050565b6001600160a01b0381165f9081525f5160206118b35f395f51905f52602081905260408220548203610b5b576040516308c2ec5b60e41b815260040160405180910390fd5b6001600160a01b039092165f90815260019092016020525060409020546001600160401b031690565b610b8c611359565b6001600160a01b038116610bb3576040516342cad95760e01b815260040160405180910390fd5b60405163f2fde38b60e01b81526001600160a01b0382811660048301527f0000000000000000000000003600000000000000000000000000000000000002169063f2fde38b906024015f604051808303815f87803b158015610c13575f5ffd5b505af1158015610c25573d5f5f3e3d5ffd5b50506040516001600160a01b03841692507ff8e2a8d4b93bd709cc57c9f21b79403c532f960ef18f77d0c23403cae0de78d991505f90a250565b5f610c68611407565b610c70611321565b604051631234eb3960e11b81526001600160a01b037f00000000000000000000000036000000000000000000000000000000000000021690632469d67290610cbe9085905f90600401611871565b6020604051808303815f875af1158015610cda573d5f5f3e3d5ffd5b505050506040513d601f19601f82011682018060405250810190610cfe919061189b565b90505b919050565b610d0e611359565b610d175f6113cb565b565b3380610d23611165565b6001600160a01b031614610d555760405163118cdaa760e01b81526001600160a01b03821660048201526024016109ff565b6107e2816113cb565b610d666112e4565b610d6e611321565b335f9081525f5160206118b35f395f51905f52602081905260409182902054915163f94e186760e01b8152600481018390529091907f00000000000000000000000036000000000000000000000000000000000000026001600160a01b03169063f94e1867906024016105d2565b610de461138b565b5f5160206118d35f395f51905f528054600160a01b900460ff166107e257805460ff60a01b1916600160a01b1781556040517f6985a02210a168e66602d3235cb6db0e70f92b3ba4d376a33c0f3d9434bff625905f90a150565b5f807f9016d09d72d40fdae2fd8ceac6b6234c7706214fd39c1cd1e609a0528c1993005b546001600160a01b031692915050565b5f805f5160206118d35f395f51905f52610e62565b610e8f611359565b6001600160a01b038116610eb657604051634a1653dd60e11b815260040160405180910390fd5b6001600160a01b0381165f9081525f5160206118f35f395f51905f52602081905260409091205460ff16610efd576040516335c0bed760e21b815260040160405180910390fd5b6001600160a01b0382165f81815260208390526040808220805460ff19169055517f195d741377fcd6e23556d115769d0b5f54decc24349a916f6de7371077fe7fd99190a25050565b610f4e611359565b815f03610f6e576040516309e56b6960e21b815260040160405180910390fd5b6001600160a01b038316610f95576040516371ab47bb60e11b815260040160405180910390fd5b6001600160a01b0383165f9081525f5160206118b35f395f51905f52602081905260409091205415610fda57604051633dd8918760e01b815260040160405180910390fd5b6001600160a01b0384165f8181526020838152604080832087905560018501825291829020805467ffffffffffffffff19166001600160401b03871690811790915591519182528592917fa1f041d5612451bd93cf2d0b47fbfde4782731f7612386774a8ab2b4caf57955910160405180910390a350505050565b6001600160a01b0381165f9081525f5160206118b35f395f51905f526020819052604082205480830361109b576040516308c2ec5b60e41b815260040160405180910390fd5b9392505050565b6110aa611359565b6001600160a01b0381166110d157604051634a1653dd60e11b815260040160405180910390fd5b6001600160a01b0381165f9081525f5160206118f35f395f51905f52602081905260409091205460ff16156111195760405163348917ed60e01b815260040160405180910390fd5b6001600160a01b0382165f81815260208390526040808220805460ff19166001179055517fd25faf73acccddf9a7ac429be872d2109cd7bd0c2370658b7c019518f3159d8e9190a25050565b5f807f237e158222e3e6968b72b9db0d8043aacf074ad9f650f0d1606b4d82ee432c00610e62565b611195611359565b7f237e158222e3e6968b72b9db0d8043aacf074ad9f650f0d1606b4d82ee432c0080546001600160a01b0319166001600160a01b03831690811782556111d9610e3e565b6001600160a01b03167f38d16b8cac22d99fc7c124b9cd0de2d3fa1faef420bfe791d8c362d765e2270060405160405180910390a35050565b61121a611359565b6001600160a01b038116611241576040516371ab47bb60e11b815260040160405180910390fd5b6001600160a01b0381165f9081525f5160206118b35f395f51905f5260208190526040822054909103611287576040516308c2ec5b60e41b815260040160405180910390fd5b6001600160a01b0382165f8181526020838152604080832083905560018501909152808220805467ffffffffffffffff19169055517f33d83959be2573f5453b12eb9d43b3499bc57d96bd2f067ba44803c859e811139190a25050565b335f9081525f5160206118b35f395f51905f52602081905260408220549091036107e257604051630e971e6360e11b815260040160405180910390fd5b5f5160206118d35f395f51905f528054600160a01b900460ff16156107e25760405163ab35696f60e01b815260040160405180910390fd5b33611362610e3e565b6001600160a01b031614610d175760405163118cdaa760e01b81523360048201526024016109ff565b5f5160206118d35f395f51905f5280546001600160a01b031633146107e25760405163daeefd6560e01b815260040160405180910390fd5b610d17611445565b7f237e158222e3e6968b72b9db0d8043aacf074ad9f650f0d1606b4d82ee432c0080546001600160a01b03191681556114038261148e565b5050565b335f9081525f5160206118f35f395f51905f52602081905260409091205460ff166107e25760405163eb4086bb60e01b815260040160405180910390fd5b7ff0c57e16840df040f15088dc2f81fe391c3923bec73e23a9662efc9c229c6a0054600160401b900460ff16610d1757604051631afcd79f60e31b815260040160405180910390fd5b7f9016d09d72d40fdae2fd8ceac6b6234c7706214fd39c1cd1e609a0528c19930080546001600160a01b031981166001600160a01b03848116918217845560405192169182907f8be0079c531659141344cd1fd0a4f28419497f9722a3daafe3b4186f6b6457e0905f90a3505050565b80356001600160a01b0381168114610d01575f5ffd5b5f60208284031215611524575f5ffd5b61109b826114fe565b5f81518084528060208401602086015e5f602082860101526020601f19601f83011685010191505092915050565b602081525f82516003811061157e57634e487b7160e01b5f52602160045260245ffd5b8060208401525060208301516060604084015261159e608084018261152d565b90506001600160401b0360408501511660608401528091505092915050565b6001600160401b03811681146107e2575f5ffd5b5f5f604083850312156115e2575f5ffd5b6115eb836114fe565b915060208301356115fb816115bd565b809150509250929050565b5f5f60408385031215611617575f5ffd5b611620836114fe565b915061162e602084016114fe565b90509250929050565b5f60208284031215611647575f5ffd5b813561109b816115bd565b634e487b7160e01b5f52604160045260245ffd5b604051606081016001600160401b038111828210171561168857611688611652565b60405290565b604051601f8201601f191681016001600160401b03811182821017156116b6576116b6611652565b604052919050565b5f6001600160401b038211156116d6576116d6611652565b50601f01601f191660200190565b5f602082840312156116f4575f5ffd5b81356001600160401b03811115611709575f5ffd5b8201601f81018413611719575f5ffd5b803561172c611727826116be565b61168e565b818152856020838501011115611740575f5ffd5b816020840160208301375f91810160200191909152949350505050565b5f5f5f6060848603121561176f575f5ffd5b611778846114fe565b925060208401359150604084013561178f816115bd565b809150509250925092565b8051610d01816115bd565b5f602082840312156117b5575f5ffd5b81516001600160401b038111156117ca575f5ffd5b8201606081850312156117db575f5ffd5b6117e3611666565b8151600381106117f1575f5ffd5b815260208201516001600160401b0381111561180b575f5ffd5b8201601f8101861361181b575f5ffd5b8051611829611727826116be565b81815287602083850101111561183d575f5ffd5b8160208401602083015e5f602083830101528060208501525050506118646040830161179a565b6040820152949350505050565b604081525f611883604083018561152d565b90506001600160401b03831660208301529392505050565b5f602082840312156118ab575f5ffd5b505191905056fee90ec3add3e251bfbe914c9e482b511e91a3b187718c1dc10223f64a8a644a000642d7922329a434cf4fd17a3c95eb692c24fd95f9f94d0b55420a5d895f4a0036c39aeb5f498ae36546fc14573b003abf87227a5a2df6caec16ee566f1ad800a2646970667358221220642f9a6a9dcca31815d2c62a7e16a0101b8be0fd9834c20c55b2b5bfc4847b8f64736f6c634300081d0033" - }, - "0x3600000000000000000000000000000000000003": { - "balance": "0x0", - "nonce": "0x1", - "code": "0x608060405260043610610049575f3560e01c80633659cfe6146100535780634f1ef286146100725780635c60da1b146100855780638f283970146100b5578063f851a440146100d4575b6100516100e8565b005b34801561005e575f5ffd5b5061005161006d3660046104f3565b6100fa565b61005161008036600461050c565b610134565b348015610090575f5ffd5b50610099610197565b6040516001600160a01b03909116815260200160405180910390f35b3480156100c0575f5ffd5b506100516100cf3660046104f3565b6101a5565b3480156100df575f5ffd5b506100996101c5565b6100f86100f3610197565b6101ce565b565b6101026101ec565b6001600160a01b0316330361012c576101298160405180602001604052805f81525061021e565b50565b6101296100e8565b61013c6101ec565b6001600160a01b0316330361018f5761018a8383838080601f0160208091040260200160405190810160405280939291908181526020018383808284375f9201919091525061021e92505050565b505050565b61018a6100e8565b5f6101a0610277565b905090565b6101ad6101ec565b6001600160a01b0316330361012c576101298161029e565b5f6101a06101ec565b365f5f375f5f365f845af43d5f5f3e8080156101e8573d5ff35b3d5ffd5b5f7fb53127684a568b3173ae13b9f8a6016e243e63b6e8ee1178d6a717850b5d61035b546001600160a01b0316919050565b610227826102f2565b6040516001600160a01b038316907fbc7cd75a20ee27fd9adebab32041f755214dbc6bffa90cc0225b39da2e5c2d3b905f90a280511561026b5761018a8282610370565b6102736103e2565b5050565b5f7f360894a13ba1a3210667c828492db98dca3e2076cc3735a920a3ca505d382bbc61020f565b7f7e644d79422f17c01e4894b5f4f588d331ebfa28653d42ae832dc59e38c9798f6102c76101ec565b604080516001600160a01b03928316815291841660208301520160405180910390a161012981610401565b806001600160a01b03163b5f0361032c57604051634c9c8ce360e01b81526001600160a01b03821660048201526024015b60405180910390fd5b807f360894a13ba1a3210667c828492db98dca3e2076cc3735a920a3ca505d382bbc5b80546001600160a01b0319166001600160a01b039290921691909117905550565b60605f5f846001600160a01b03168460405161038c919061058a565b5f60405180830381855af49150503d805f81146103c4576040519150601f19603f3d011682016040523d82523d5f602084013e6103c9565b606091505b50915091506103d9858383610451565b95945050505050565b34156100f85760405163b398979f60e01b815260040160405180910390fd5b6001600160a01b03811661042a57604051633173bdd160e11b81525f6004820152602401610323565b807fb53127684a568b3173ae13b9f8a6016e243e63b6e8ee1178d6a717850b5d610361034f565b60608261046657610461826104b0565b6104a9565b815115801561047d57506001600160a01b0384163b155b156104a657604051639996b31560e01b81526001600160a01b0385166004820152602401610323565b50805b9392505050565b8051156104bf57805160208201fd5b60405163d6bda27560e01b815260040160405180910390fd5b80356001600160a01b03811681146104ee575f5ffd5b919050565b5f60208284031215610503575f5ffd5b6104a9826104d8565b5f5f5f6040848603121561051e575f5ffd5b610527846104d8565b9250602084013567ffffffffffffffff811115610542575f5ffd5b8401601f81018613610552575f5ffd5b803567ffffffffffffffff811115610568575f5ffd5b866020828401011115610579575f5ffd5b939660209190910195509293505050565b5f82518060208501845e5f92019182525091905056fea26469706673582212204c0881af9b906ca964ce2bc1cc4525350de8dd57658dacb806266290556eb9ae64736f6c634300081d0033", - "storage": { - "0xb53127684a568b3173ae13b9f8a6016e243e63b6e8ee1178d6a717850b5d6103": "0x000000000000000000000000a0ee7a142d267c1f36714e4a8f75612f20a79720", - "0x360894a13ba1a3210667c828492db98dca3e2076cc3735a920a3ca505d382bbc": "0x00000000000000000000000098c74f7ef54ba22be94415d7c967b352c6a42d6d", - "0xf0c57e16840df040f15088dc2f81fe391c3923bec73e23a9662efc9c229c6a00": "0x0000000000000000000000000000000000000000000000000000000000000001", - "0x9016d09d72d40fdae2fd8ceac6b6234c7706214fd39c1cd1e609a0528c199300": "0x00000000000000000000000023618e81e3f5cdf7f54c3d65f7fbc0abf5b21e8f", - "0x0642d7922329a434cf4fd17a3c95eb692c24fd95f9f94d0b55420a5d895f4a00": "0x00000000000000000000000023618e81e3f5cdf7f54c3d65f7fbc0abf5b21e8f", - "0xa4ec95f560ce81c285cd4e5462195a1e0b5658e60e5765bb1a90935498ab31ef": "0x0000000000000000000000000000000000000000000000000000000000000001", - "0x19b40b2473dd966ccd4d715d67bfd92096d9806e14811df67ea648479a5da0bc": "0x0000000000000000000000000000000000000000000000000000000000000002", - "0xbe5b579ee369ec22a252ada0ff6e111a69f0d836b1eb118e23ac21f1c0c00785": "0x0000000000000000000000000000000000000000000000000000000000000003", - "0xfe898c4aed7ea37490cbfb561028c8a912ff93639e571d038511e249fe4ba834": "0x0000000000000000000000000000000000000000000000000000000000000004", - "0x71a931c06caa7d88209767fd52679332d831e08de18757be1dd379baae585fab": "0x0000000000000000000000000000000000000000000000000000000000000005", - "0x16988468ad88213f7efbed37dfc8bd1498e5f3956184cb7dababe5f4a4d381ac": "0x000000000000000000000000000000000000000000000000ffffffffffffffff", - "0x4705f61d0053e6b3820aee40e99d7aa01ac6b53dec42dd907435f59b89c3bf79": "0x000000000000000000000000000000000000000000000000ffffffffffffffff", - "0x14431871092853131738f7005dd2b66ccae659205f8a9fad72a77da99183ddf5": "0x000000000000000000000000000000000000000000000000ffffffffffffffff", - "0x2b839897663ed45bf35027f944beaa200449ee3194f13af52fabbe8af352fe69": "0x000000000000000000000000000000000000000000000000ffffffffffffffff", - "0xa3eb5937efaeb5379cca5ba0f30b21d4f6e4589aa68ab2c4925de8b2458e785f": "0x000000000000000000000000000000000000000000000000ffffffffffffffff", - "0x5cd122a2d8a08ca6b68953722ea18259ef308d77be6dbc8fdcc8adaabce55249": "0x0000000000000000000000000000000000000000000000000000000000000001", - "0xc15fa7a907ba1d41aff5e5ded523fc00e97bf156e8a98a179bd9b65a05161d7c": "0x0000000000000000000000000000000000000000000000000000000000000001" - } - }, - "0x4b5A1f0cf56B193cCA1a0B16fCE0713d44abdDbE": { - "balance": "0x0", - "nonce": "0x1", - "code": "0x608060405234801561000f575f5ffd5b50600436106100cb575f3560e01c8063c4d66de811610088578063e8746fed11610063578063e8746fed146101b0578063e877a526146101c3578063f2fde38b146101fa578063fab48ccf1461020d575f5ffd5b8063c4d66de814610182578063ca2f731314610195578063e30c3978146101a8575f5ffd5b806331d798b5146100cf578063715018a61461011b57806379ba5097146101255780637a7ceb161461012d5780638a8ee6ac146101405780638da5cb5b14610162575b5f5ffd5b6101066100dd366004610980565b6001600160a01b03165f9081525f516020610a335f395f51905f52602052604090205460ff1690565b60405190151581526020015b60405180910390f35b610123610220565b005b610123610233565b61012361013b3660046109ad565b610280565b6101545f516020610a535f395f51905f5281565b604051908152602001610112565b61016a610385565b6040516001600160a01b039091168152602001610112565b610123610190366004610980565b6103b9565b6101236101a3366004610980565b6104f5565b61016a6105ab565b6101236101be3660046109ad565b6105d3565b6101066101d1366004610980565b6001600160a01b03165f9081525f516020610a535f395f51905f52602052604090205460ff1690565b610123610208366004610980565b610717565b61012361021b366004610980565b61079c565b610228610855565b6102315f610887565b565b338061023d6105ab565b6001600160a01b0316146102745760405163118cdaa760e01b81526001600160a01b03821660048201526024015b60405180910390fd5b61027d81610887565b50565b335f9081525f516020610a335f395f51905f5260205260409020545f516020610a535f395f51905f529060ff166102ca57604051637628c31d60e01b815260040160405180910390fd5b5f516020610a535f395f51905f52825f5b8181101561037d575f8686838181106102f6576102f6610a1e565b905060200201602081019061030b9190610980565b6001600160a01b0381165f9081526020869052604090205490915060ff1615610374576001600160a01b0381165f81815260208690526040808220805460ff19169055517fc904e1b03de0c20d7fcf9dbd056daf1bd3815e93f251199de815fd0f0b96e1669190a25b506001016102db565b505050505050565b5f807f9016d09d72d40fdae2fd8ceac6b6234c7706214fd39c1cd1e609a0528c1993005b546001600160a01b031692915050565b7ff0c57e16840df040f15088dc2f81fe391c3923bec73e23a9662efc9c229c6a008054600160401b810460ff16159067ffffffffffffffff165f811580156103fe5750825b90505f8267ffffffffffffffff16600114801561041a5750303b155b905081158015610428575080155b156104465760405163f92ee8a960e01b815260040160405180910390fd5b845467ffffffffffffffff19166001178555831561047057845460ff60401b1916600160401b1785555b6001600160a01b0386166104975760405163d92e233d60e01b815260040160405180910390fd5b61049f6108bf565b6104a886610887565b831561037d57845460ff60401b19168555604051600181527fc7f505b2f371ae2175ee4913f4499e1f2633a7b5936321eed1cdaeb6115181d29060200160405180910390a1505050505050565b6104fd610855565b6001600160a01b0381166105245760405163d92e233d60e01b815260040160405180910390fd5b6001600160a01b0381165f9081525f516020610a335f395f51905f5260205260409020545f516020610a535f395f51905f529060ff16156105a7576001600160a01b0382165f818152600183016020526040808220805460ff19169055517f2ee9bf58aeff79a8ee48ae2ebaea69b7fc533af3060aaa8d8956b225785db10a9190a25b5050565b5f807f237e158222e3e6968b72b9db0d8043aacf074ad9f650f0d1606b4d82ee432c006103a9565b335f9081525f516020610a335f395f51905f5260205260409020545f516020610a535f395f51905f529060ff1661061d57604051637628c31d60e01b815260040160405180910390fd5b5f610626610385565b90505f516020610a535f395f51905f52835f5b8181101561070e575f87878381811061065457610654610a1e565b90506020020160208101906106699190610980565b9050846001600160a01b0316816001600160a01b03160361069d5760405163266f648160e01b815260040160405180910390fd5b6001600160a01b0381165f9081526020859052604090205460ff16610705576001600160a01b0381165f81815260208690526040808220805460ff19166001179055517ffa4507bc1f9c730e6e95897024f1fe7d576cf2deb53579d55c14f1ac3439e1149190a25b50600101610639565b50505050505050565b61071f610855565b7f237e158222e3e6968b72b9db0d8043aacf074ad9f650f0d1606b4d82ee432c0080546001600160a01b0319166001600160a01b0383169081178255610763610385565b6001600160a01b03167f38d16b8cac22d99fc7c124b9cd0de2d3fa1faef420bfe791d8c362d765e2270060405160405180910390a35050565b6107a4610855565b6001600160a01b0381166107cb5760405163d92e233d60e01b815260040160405180910390fd5b6001600160a01b0381165f9081525f516020610a335f395f51905f5260205260409020545f516020610a535f395f51905f529060ff166105a7576001600160a01b0382165f81815260018381016020526040808320805460ff1916909217909155517f1ac0162c9c4096b260eb12be2731ca291739aa6ed7c94780f52c55df88589e7c9190a25050565b3361085e610385565b6001600160a01b0316146102315760405163118cdaa760e01b815233600482015260240161026b565b7f237e158222e3e6968b72b9db0d8043aacf074ad9f650f0d1606b4d82ee432c0080546001600160a01b03191681556105a7826108c7565b610231610937565b7f9016d09d72d40fdae2fd8ceac6b6234c7706214fd39c1cd1e609a0528c19930080546001600160a01b031981166001600160a01b03848116918217845560405192169182907f8be0079c531659141344cd1fd0a4f28419497f9722a3daafe3b4186f6b6457e0905f90a3505050565b7ff0c57e16840df040f15088dc2f81fe391c3923bec73e23a9662efc9c229c6a0054600160401b900460ff1661023157604051631afcd79f60e31b815260040160405180910390fd5b5f60208284031215610990575f5ffd5b81356001600160a01b03811681146109a6575f5ffd5b9392505050565b5f5f602083850312156109be575f5ffd5b823567ffffffffffffffff8111156109d4575f5ffd5b8301601f810185136109e4575f5ffd5b803567ffffffffffffffff8111156109fa575f5ffd5b8560208260051b8401011115610a0e575f5ffd5b6020919091019590945092505050565b634e487b7160e01b5f52603260045260245ffdfe1d7e1388d3ae56f3d9c18b1ce8d2b3b1a238a0edf682d2053af5d8a1d2f12f011d7e1388d3ae56f3d9c18b1ce8d2b3b1a238a0edf682d2053af5d8a1d2f12f00a264697066735822122057fdea9cb356478df3dad5dbaec8a47476bd3266f5a49729c6d02d8e049ee9b864736f6c634300081d0033" - }, - "0x36059b615370eB999e8eC0c9401835B407834221": { - "balance": "0x0", - "nonce": "0x1", - "code": "0x608060405260043610610049575f3560e01c80633659cfe6146100535780634f1ef286146100725780635c60da1b146100855780638f283970146100b5578063f851a440146100d4575b6100516100e8565b005b34801561005e575f5ffd5b5061005161006d3660046104f3565b6100fa565b61005161008036600461050c565b610134565b348015610090575f5ffd5b50610099610197565b6040516001600160a01b03909116815260200160405180910390f35b3480156100c0575f5ffd5b506100516100cf3660046104f3565b6101a5565b3480156100df575f5ffd5b506100996101c5565b6100f86100f3610197565b6101ce565b565b6101026101ec565b6001600160a01b0316330361012c576101298160405180602001604052805f81525061021e565b50565b6101296100e8565b61013c6101ec565b6001600160a01b0316330361018f5761018a8383838080601f0160208091040260200160405190810160405280939291908181526020018383808284375f9201919091525061021e92505050565b505050565b61018a6100e8565b5f6101a0610277565b905090565b6101ad6101ec565b6001600160a01b0316330361012c576101298161029e565b5f6101a06101ec565b365f5f375f5f365f845af43d5f5f3e8080156101e8573d5ff35b3d5ffd5b5f7fb53127684a568b3173ae13b9f8a6016e243e63b6e8ee1178d6a717850b5d61035b546001600160a01b0316919050565b610227826102f2565b6040516001600160a01b038316907fbc7cd75a20ee27fd9adebab32041f755214dbc6bffa90cc0225b39da2e5c2d3b905f90a280511561026b5761018a8282610370565b6102736103e2565b5050565b5f7f360894a13ba1a3210667c828492db98dca3e2076cc3735a920a3ca505d382bbc61020f565b7f7e644d79422f17c01e4894b5f4f588d331ebfa28653d42ae832dc59e38c9798f6102c76101ec565b604080516001600160a01b03928316815291841660208301520160405180910390a161012981610401565b806001600160a01b03163b5f0361032c57604051634c9c8ce360e01b81526001600160a01b03821660048201526024015b60405180910390fd5b807f360894a13ba1a3210667c828492db98dca3e2076cc3735a920a3ca505d382bbc5b80546001600160a01b0319166001600160a01b039290921691909117905550565b60605f5f846001600160a01b03168460405161038c919061058a565b5f60405180830381855af49150503d805f81146103c4576040519150601f19603f3d011682016040523d82523d5f602084013e6103c9565b606091505b50915091506103d9858383610451565b95945050505050565b34156100f85760405163b398979f60e01b815260040160405180910390fd5b6001600160a01b03811661042a57604051633173bdd160e11b81525f6004820152602401610323565b807fb53127684a568b3173ae13b9f8a6016e243e63b6e8ee1178d6a717850b5d610361034f565b60608261046657610461826104b0565b6104a9565b815115801561047d57506001600160a01b0384163b155b156104a657604051639996b31560e01b81526001600160a01b0385166004820152602401610323565b50805b9392505050565b8051156104bf57805160208201fd5b60405163d6bda27560e01b815260040160405180910390fd5b80356001600160a01b03811681146104ee575f5ffd5b919050565b5f60208284031215610503575f5ffd5b6104a9826104d8565b5f5f5f6040848603121561051e575f5ffd5b610527846104d8565b9250602084013567ffffffffffffffff811115610542575f5ffd5b8401601f81018613610552575f5ffd5b803567ffffffffffffffff811115610568575f5ffd5b866020828401011115610579575f5ffd5b939660209190910195509293505050565b5f82518060208501845e5f92019182525091905056fea26469706673582212204c0881af9b906ca964ce2bc1cc4525350de8dd57658dacb806266290556eb9ae64736f6c634300081d0033", - "storage": { - "0xb53127684a568b3173ae13b9f8a6016e243e63b6e8ee1178d6a717850b5d6103": "0x000000000000000000000000a0ee7a142d267c1f36714e4a8f75612f20a79720", - "0x360894a13ba1a3210667c828492db98dca3e2076cc3735a920a3ca505d382bbc": "0x0000000000000000000000004b5a1f0cf56b193cca1a0b16fce0713d44abddbe", - "0x9016d09d72d40fdae2fd8ceac6b6234c7706214fd39c1cd1e609a0528c199300": "0x00000000000000000000000023618e81e3f5cdf7f54c3d65f7fbc0abf5b21e8f", - "0xf0c57e16840df040f15088dc2f81fe391c3923bec73e23a9662efc9c229c6a00": "0x0000000000000000000000000000000000000000000000000000000000000001", - "0x7c227092eb1f4e3f31917fac32f58d3c183f1245a2f05a65068f491ee1a24948": "0x0000000000000000000000000000000000000000000000000000000000000001" - } - }, - "0x4e59b44847b379578588920cA78FbF26c0B4956C": { - "balance": "0x0", - "nonce": "0x1", - "code": "0x7fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffe03601600081602082378035828234f58015156039578182fd5b8082525050506014600cf3" - }, - "0x3fab184622dc19b6109349b94811493bf2a45362": { - "balance": "0x0", - "nonce": "0x1" - }, - "0xcA11bde05977b3631167028862bE2a173976CA11": { - "balance": "0x0", - "nonce": "0x1", - "code": "0x6080604052600436106100f35760003560e01c80634d2301cc1161008a578063a8b0574e11610059578063a8b0574e1461025a578063bce38bd714610275578063c3077fa914610288578063ee82ac5e1461029b57600080fd5b80634d2301cc146101ec57806372425d9d1461022157806382ad56cb1461023457806386d516e81461024757600080fd5b80633408e470116100c65780633408e47014610191578063399542e9146101a45780633e64a696146101c657806342cbb15c146101d957600080fd5b80630f28c97d146100f8578063174dea711461011a578063252dba421461013a57806327e86d6e1461015b575b600080fd5b34801561010457600080fd5b50425b6040519081526020015b60405180910390f35b61012d610128366004610a85565b6102ba565b6040516101119190610bbe565b61014d610148366004610a85565b6104ef565b604051610111929190610bd8565b34801561016757600080fd5b50437fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff0140610107565b34801561019d57600080fd5b5046610107565b6101b76101b2366004610c60565b610690565b60405161011193929190610cba565b3480156101d257600080fd5b5048610107565b3480156101e557600080fd5b5043610107565b3480156101f857600080fd5b50610107610207366004610ce2565b73ffffffffffffffffffffffffffffffffffffffff163190565b34801561022d57600080fd5b5044610107565b61012d610242366004610a85565b6106ab565b34801561025357600080fd5b5045610107565b34801561026657600080fd5b50604051418152602001610111565b61012d610283366004610c60565b61085a565b6101b7610296366004610a85565b610a1a565b3480156102a757600080fd5b506101076102b6366004610d18565b4090565b60606000828067ffffffffffffffff8111156102d8576102d8610d31565b60405190808252806020026020018201604052801561031e57816020015b6040805180820190915260008152606060208201528152602001906001900390816102f65790505b5092503660005b8281101561047757600085828151811061034157610341610d60565b6020026020010151905087878381811061035d5761035d610d60565b905060200281019061036f9190610d8f565b6040810135958601959093506103886020850185610ce2565b73ffffffffffffffffffffffffffffffffffffffff16816103ac6060870187610dcd565b6040516103ba929190610e32565b60006040518083038185875af1925050503d80600081146103f7576040519150601f19603f3d011682016040523d82523d6000602084013e6103fc565b606091505b50602080850191909152901515808452908501351761046d577f08c379a000000000000000000000000000000000000000000000000000000000600052602060045260176024527f4d756c746963616c6c333a2063616c6c206661696c656400000000000000000060445260846000fd5b5050600101610325565b508234146104e6576040517f08c379a000000000000000000000000000000000000000000000000000000000815260206004820152601a60248201527f4d756c746963616c6c333a2076616c7565206d69736d6174636800000000000060448201526064015b60405180910390fd5b50505092915050565b436060828067ffffffffffffffff81111561050c5761050c610d31565b60405190808252806020026020018201604052801561053f57816020015b606081526020019060019003908161052a5790505b5091503660005b8281101561068657600087878381811061056257610562610d60565b90506020028101906105749190610e42565b92506105836020840184610ce2565b73ffffffffffffffffffffffffffffffffffffffff166105a66020850185610dcd565b6040516105b4929190610e32565b6000604051808303816000865af19150503d80600081146105f1576040519150601f19603f3d011682016040523d82523d6000602084013e6105f6565b606091505b5086848151811061060957610609610d60565b602090810291909101015290508061067d576040517f08c379a000000000000000000000000000000000000000000000000000000000815260206004820152601760248201527f4d756c746963616c6c333a2063616c6c206661696c656400000000000000000060448201526064016104dd565b50600101610546565b5050509250929050565b43804060606106a086868661085a565b905093509350939050565b6060818067ffffffffffffffff8111156106c7576106c7610d31565b60405190808252806020026020018201604052801561070d57816020015b6040805180820190915260008152606060208201528152602001906001900390816106e55790505b5091503660005b828110156104e657600084828151811061073057610730610d60565b6020026020010151905086868381811061074c5761074c610d60565b905060200281019061075e9190610e76565b925061076d6020840184610ce2565b73ffffffffffffffffffffffffffffffffffffffff166107906040850185610dcd565b60405161079e929190610e32565b6000604051808303816000865af19150503d80600081146107db576040519150601f19603f3d011682016040523d82523d6000602084013e6107e0565b606091505b506020808401919091529015158083529084013517610851577f08c379a000000000000000000000000000000000000000000000000000000000600052602060045260176024527f4d756c746963616c6c333a2063616c6c206661696c656400000000000000000060445260646000fd5b50600101610714565b6060818067ffffffffffffffff81111561087657610876610d31565b6040519080825280602002602001820160405280156108bc57816020015b6040805180820190915260008152606060208201528152602001906001900390816108945790505b5091503660005b82811015610a105760008482815181106108df576108df610d60565b602002602001015190508686838181106108fb576108fb610d60565b905060200281019061090d9190610e42565b925061091c6020840184610ce2565b73ffffffffffffffffffffffffffffffffffffffff1661093f6020850185610dcd565b60405161094d929190610e32565b6000604051808303816000865af19150503d806000811461098a576040519150601f19603f3d011682016040523d82523d6000602084013e61098f565b606091505b506020830152151581528715610a07578051610a07576040517f08c379a000000000000000000000000000000000000000000000000000000000815260206004820152601760248201527f4d756c746963616c6c333a2063616c6c206661696c656400000000000000000060448201526064016104dd565b506001016108c3565b5050509392505050565b6000806060610a2b60018686610690565b919790965090945092505050565b60008083601f840112610a4b57600080fd5b50813567ffffffffffffffff811115610a6357600080fd5b6020830191508360208260051b8501011115610a7e57600080fd5b9250929050565b60008060208385031215610a9857600080fd5b823567ffffffffffffffff811115610aaf57600080fd5b610abb85828601610a39565b90969095509350505050565b6000815180845260005b81811015610aed57602081850181015186830182015201610ad1565b81811115610aff576000602083870101525b50601f017fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffe0169290920160200192915050565b600082825180855260208086019550808260051b84010181860160005b84811015610bb1578583037fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffe001895281518051151584528401516040858501819052610b9d81860183610ac7565b9a86019a9450505090830190600101610b4f565b5090979650505050505050565b602081526000610bd16020830184610b32565b9392505050565b600060408201848352602060408185015281855180845260608601915060608160051b870101935082870160005b82811015610c52577fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffa0888703018452610c40868351610ac7565b95509284019290840190600101610c06565b509398975050505050505050565b600080600060408486031215610c7557600080fd5b83358015158114610c8557600080fd5b9250602084013567ffffffffffffffff811115610ca157600080fd5b610cad86828701610a39565b9497909650939450505050565b838152826020820152606060408201526000610cd96060830184610b32565b95945050505050565b600060208284031215610cf457600080fd5b813573ffffffffffffffffffffffffffffffffffffffff81168114610bd157600080fd5b600060208284031215610d2a57600080fd5b5035919050565b7f4e487b7100000000000000000000000000000000000000000000000000000000600052604160045260246000fd5b7f4e487b7100000000000000000000000000000000000000000000000000000000600052603260045260246000fd5b600082357fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff81833603018112610dc357600080fd5b9190910192915050565b60008083357fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffe1843603018112610e0257600080fd5b83018035915067ffffffffffffffff821115610e1d57600080fd5b602001915036819003821315610a7e57600080fd5b8183823760009101908152919050565b600082357fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffc1833603018112610dc357600080fd5b600082357fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffa1833603018112610dc357600080fdfea2646970667358221220bb2b5c71a328032f97c676ae39a1ec2148d3e5d6f73d95e9b17910152d61f16264736f6c634300080c0033" - }, - "0x05f32b3cc3888453ff71b01135b34ff8e41263f2": { - "balance": "0x0", - "nonce": "0x1" - }, - "0x0000F90827F1C53a10cb7A02335B175320002935": { - "balance": "0x0", - "nonce": "0x1", - "code": "0x3373fffffffffffffffffffffffffffffffffffffffe14604657602036036042575f35600143038111604257611fff81430311604257611fff9006545f5260205ff35b5f5ffd5b5f35611fff60014303065500" - }, - "0x3462413Af4609098e1E27A490f554f260213D685": { - "balance": "0x0", - "nonce": "0x1" - }, - "0x000000000022D473030F116dDEE9F6B43aC78BA3": { - "balance": "0x0", - "nonce": "0x1", - "code": "0x6040608081526004908136101561001557600080fd5b600090813560e01c80630d58b1db1461126c578063137c29fe146110755780632a2d80d114610db75780632b67b57014610bde57806330f28b7a14610ade5780633644e51514610a9d57806336c7851614610a285780633ff9dcb1146109a85780634fe02b441461093f57806365d9723c146107ac57806387517c451461067a578063927da105146105c3578063cc53287f146104a3578063edd9444b1461033a5763fe8ec1a7146100c657600080fd5b346103365760c07ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffc3601126103365767ffffffffffffffff833581811161033257610114903690860161164b565b60243582811161032e5761012b903690870161161a565b6101336114e6565b9160843585811161032a5761014b9036908a016115c1565b98909560a43590811161032657610164913691016115c1565b969095815190610173826113ff565b606b82527f5065726d697442617463685769746e6573735472616e7366657246726f6d285460208301527f6f6b656e5065726d697373696f6e735b5d207065726d69747465642c61646472838301527f657373207370656e6465722c75696e74323536206e6f6e63652c75696e74323560608301527f3620646561646c696e652c000000000000000000000000000000000000000000608083015282519a8b9181610222602085018096611f93565b918237018a8152039961025b7fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffe09b8c8101835282611437565b5190209085515161026b81611ebb565b908a5b8181106102f95750506102f6999a6102ed9183516102a081610294602082018095611f66565b03848101835282611437565b519020602089810151858b015195519182019687526040820192909252336060820152608081019190915260a081019390935260643560c08401528260e081015b03908101835282611437565b51902093611cf7565b80f35b8061031161030b610321938c5161175e565b51612054565b61031b828661175e565b52611f0a565b61026e565b8880fd5b8780fd5b8480fd5b8380fd5b5080fd5b5091346103365760807ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffc3601126103365767ffffffffffffffff9080358281116103325761038b903690830161164b565b60243583811161032e576103a2903690840161161a565b9390926103ad6114e6565b9160643590811161049f576103c4913691016115c1565b949093835151976103d489611ebb565b98885b81811061047d5750506102f697988151610425816103f9602082018095611f66565b037fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffe08101835282611437565b5190206020860151828701519083519260208401947ffcf35f5ac6a2c28868dc44c302166470266239195f02b0ee408334829333b7668652840152336060840152608083015260a082015260a081526102ed8161141b565b808b61031b8261049461030b61049a968d5161175e565b9261175e565b6103d7565b8680fd5b5082346105bf57602090817ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffc3601126103325780359067ffffffffffffffff821161032e576104f49136910161161a565b929091845b848110610504578580f35b8061051a610515600193888861196c565b61197c565b61052f84610529848a8a61196c565b0161197c565b3389528385528589209173ffffffffffffffffffffffffffffffffffffffff80911692838b528652868a20911690818a5285528589207fffffffffffffffffffffffff000000000000000000000000000000000000000081541690558551918252848201527f89b1add15eff56b3dfe299ad94e01f2b52fbcb80ae1a3baea6ae8c04cb2b98a4853392a2016104f9565b8280fd5b50346103365760607ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffc36011261033657610676816105ff6114a0565b936106086114c3565b6106106114e6565b73ffffffffffffffffffffffffffffffffffffffff968716835260016020908152848420928816845291825283832090871683528152919020549251938316845260a083901c65ffffffffffff169084015260d09190911c604083015281906060820190565b0390f35b50346103365760807ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffc360112610336576106b26114a0565b906106bb6114c3565b916106c46114e6565b65ffffffffffff926064358481169081810361032a5779ffffffffffff0000000000000000000000000000000000000000947fda9fa7c1b00402c17d0161b249b1ab8bbec047c5a52207b9c112deffd817036b94338a5260016020527fffffffffffff0000000000000000000000000000000000000000000000000000858b209873ffffffffffffffffffffffffffffffffffffffff809416998a8d5260205283878d209b169a8b8d52602052868c209486156000146107a457504216925b8454921697889360a01b16911617179055815193845260208401523392a480f35b905092610783565b5082346105bf5760607ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffc3601126105bf576107e56114a0565b906107ee6114c3565b9265ffffffffffff604435818116939084810361032a57338852602091600183528489209673ffffffffffffffffffffffffffffffffffffffff80911697888b528452858a20981697888a5283528489205460d01c93848711156109175761ffff9085840316116108f05750907f55eb90d810e1700b35a8e7e25395ff7f2b2259abd7415ca2284dfb1c246418f393929133895260018252838920878a528252838920888a5282528389209079ffffffffffffffffffffffffffffffffffffffffffffffffffff7fffffffffffff000000000000000000000000000000000000000000000000000083549260d01b16911617905582519485528401523392a480f35b84517f24d35a26000000000000000000000000000000000000000000000000000000008152fd5b5084517f756688fe000000000000000000000000000000000000000000000000000000008152fd5b503461033657807ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffc360112610336578060209273ffffffffffffffffffffffffffffffffffffffff61098f6114a0565b1681528084528181206024358252845220549051908152f35b5082346105bf57817ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffc3601126105bf577f3704902f963766a4e561bbaab6e6cdc1b1dd12f6e9e99648da8843b3f46b918d90359160243533855284602052818520848652602052818520818154179055815193845260208401523392a280f35b8234610a9a5760807ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffc360112610a9a57610a606114a0565b610a686114c3565b610a706114e6565b6064359173ffffffffffffffffffffffffffffffffffffffff8316830361032e576102f6936117a1565b80fd5b503461033657817ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffc36011261033657602090610ad7611b1e565b9051908152f35b508290346105bf576101007ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffc3601126105bf57610b1a3661152a565b90807fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff7c36011261033257610b4c611478565b9160e43567ffffffffffffffff8111610bda576102f694610b6f913691016115c1565b939092610b7c8351612054565b6020840151828501519083519260208401947f939c21a48a8dbe3a9a2404a1d46691e4d39f6583d6ec6b35714604c986d801068652840152336060840152608083015260a082015260a08152610bd18161141b565b51902091611c25565b8580fd5b509134610336576101007ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffc36011261033657610c186114a0565b7fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffdc360160c08112610332576080855191610c51836113e3565b1261033257845190610c6282611398565b73ffffffffffffffffffffffffffffffffffffffff91602435838116810361049f578152604435838116810361049f57602082015265ffffffffffff606435818116810361032a5788830152608435908116810361049f576060820152815260a435938285168503610bda576020820194855260c4359087830182815260e43567ffffffffffffffff811161032657610cfe90369084016115c1565b929093804211610d88575050918591610d786102f6999a610d7e95610d238851611fbe565b90898c511690519083519260208401947ff3841cd1ff0085026a6327b620b67997ce40f282c88a8e905a7a5626e310f3d086528401526060830152608082015260808152610d70816113ff565b519020611bd9565b916120c7565b519251169161199d565b602492508a51917fcd21db4f000000000000000000000000000000000000000000000000000000008352820152fd5b5091346103365760607ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffc93818536011261033257610df36114a0565b9260249081359267ffffffffffffffff9788851161032a578590853603011261049f578051978589018981108282111761104a578252848301358181116103265785019036602383011215610326578382013591610e50836115ef565b90610e5d85519283611437565b838252602093878584019160071b83010191368311611046578801905b828210610fe9575050508a526044610e93868801611509565b96838c01978852013594838b0191868352604435908111610fe557610ebb90369087016115c1565b959096804211610fba575050508998995151610ed681611ebb565b908b5b818110610f9757505092889492610d7892610f6497958351610f02816103f98682018095611f66565b5190209073ffffffffffffffffffffffffffffffffffffffff9a8b8b51169151928551948501957faf1b0d30d2cab0380e68f0689007e3254993c596f2fdd0aaa7f4d04f794408638752850152830152608082015260808152610d70816113ff565b51169082515192845b848110610f78578580f35b80610f918585610f8b600195875161175e565b5161199d565b01610f6d565b80610311610fac8e9f9e93610fb2945161175e565b51611fbe565b9b9a9b610ed9565b8551917fcd21db4f000000000000000000000000000000000000000000000000000000008352820152fd5b8a80fd5b6080823603126110465785608091885161100281611398565b61100b85611509565b8152611018838601611509565b838201526110278a8601611607565b8a8201528d611037818701611607565b90820152815201910190610e7a565b8c80fd5b84896041867f4e487b7100000000000000000000000000000000000000000000000000000000835252fd5b5082346105bf576101407ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffc3601126105bf576110b03661152a565b91807fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff7c360112610332576110e2611478565b67ffffffffffffffff93906101043585811161049f5761110590369086016115c1565b90936101243596871161032a57611125610bd1966102f6983691016115c1565b969095825190611134826113ff565b606482527f5065726d69745769746e6573735472616e7366657246726f6d28546f6b656e5060208301527f65726d697373696f6e73207065726d69747465642c6164647265737320737065848301527f6e6465722c75696e74323536206e6f6e63652c75696e7432353620646561646c60608301527f696e652c0000000000000000000000000000000000000000000000000000000060808301528351948591816111e3602085018096611f93565b918237018b8152039361121c7fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffe095868101835282611437565b5190209261122a8651612054565b6020878101518589015195519182019687526040820192909252336060820152608081019190915260a081019390935260e43560c08401528260e081016102e1565b5082346105bf576020807ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffc36011261033257813567ffffffffffffffff92838211610bda5736602383011215610bda5781013592831161032e576024906007368386831b8401011161049f57865b8581106112e5578780f35b80821b83019060807fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffdc83360301126103265761139288876001946060835161132c81611398565b611368608461133c8d8601611509565b9485845261134c60448201611509565b809785015261135d60648201611509565b809885015201611509565b918291015273ffffffffffffffffffffffffffffffffffffffff80808093169516931691166117a1565b016112da565b6080810190811067ffffffffffffffff8211176113b457604052565b7f4e487b7100000000000000000000000000000000000000000000000000000000600052604160045260246000fd5b6060810190811067ffffffffffffffff8211176113b457604052565b60a0810190811067ffffffffffffffff8211176113b457604052565b60c0810190811067ffffffffffffffff8211176113b457604052565b90601f7fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffe0910116810190811067ffffffffffffffff8211176113b457604052565b60c4359073ffffffffffffffffffffffffffffffffffffffff8216820361149b57565b600080fd5b6004359073ffffffffffffffffffffffffffffffffffffffff8216820361149b57565b6024359073ffffffffffffffffffffffffffffffffffffffff8216820361149b57565b6044359073ffffffffffffffffffffffffffffffffffffffff8216820361149b57565b359073ffffffffffffffffffffffffffffffffffffffff8216820361149b57565b7ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffc01906080821261149b576040805190611563826113e3565b8082941261149b57805181810181811067ffffffffffffffff8211176113b457825260043573ffffffffffffffffffffffffffffffffffffffff8116810361149b578152602435602082015282526044356020830152606435910152565b9181601f8401121561149b5782359167ffffffffffffffff831161149b576020838186019501011161149b57565b67ffffffffffffffff81116113b45760051b60200190565b359065ffffffffffff8216820361149b57565b9181601f8401121561149b5782359167ffffffffffffffff831161149b576020808501948460061b01011161149b57565b91909160608184031261149b576040805191611666836113e3565b8294813567ffffffffffffffff9081811161149b57830182601f8201121561149b578035611693816115ef565b926116a087519485611437565b818452602094858086019360061b8501019381851161149b579086899897969594939201925b8484106116e3575050505050855280820135908501520135910152565b90919293949596978483031261149b578851908982019082821085831117611730578a928992845261171487611509565b81528287013583820152815201930191908897969594936116c6565b602460007f4e487b710000000000000000000000000000000000000000000000000000000081526041600452fd5b80518210156117725760209160051b010190565b7f4e487b7100000000000000000000000000000000000000000000000000000000600052603260045260246000fd5b92919273ffffffffffffffffffffffffffffffffffffffff604060008284168152600160205282828220961695868252602052818120338252602052209485549565ffffffffffff8760a01c16804211611884575082871696838803611812575b5050611810955016926118b5565b565b878484161160001461184f57602488604051907ff96fb0710000000000000000000000000000000000000000000000000000000082526004820152fd5b7fffffffffffffffffffffffff000000000000000000000000000000000000000084846118109a031691161790553880611802565b602490604051907fd81b2f2e0000000000000000000000000000000000000000000000000000000082526004820152fd5b9060006064926020958295604051947f23b872dd0000000000000000000000000000000000000000000000000000000086526004860152602485015260448401525af13d15601f3d116001600051141617161561190e57565b60646040517f08c379a000000000000000000000000000000000000000000000000000000000815260206004820152601460248201527f5452414e534645525f46524f4d5f4641494c45440000000000000000000000006044820152fd5b91908110156117725760061b0190565b3573ffffffffffffffffffffffffffffffffffffffff8116810361149b5790565b9065ffffffffffff908160608401511673ffffffffffffffffffffffffffffffffffffffff908185511694826020820151169280866040809401511695169560009187835260016020528383208984526020528383209916988983526020528282209184835460d01c03611af5579185611ace94927fc6a377bfc4eb120024a8ac08eef205be16b817020812c73223e81d1bdb9708ec98979694508715600014611ad35779ffffffffffff00000000000000000000000000000000000000009042165b60a01b167fffffffffffff00000000000000000000000000000000000000000000000000006001860160d01b1617179055519384938491604091949373ffffffffffffffffffffffffffffffffffffffff606085019616845265ffffffffffff809216602085015216910152565b0390a4565b5079ffffffffffff000000000000000000000000000000000000000087611a60565b600484517f756688fe000000000000000000000000000000000000000000000000000000008152fd5b467f000000000000000000000000000000000000000000000000000000000000053903611b69577f1f520b5ee38ad937955892c3dfc7055e8eeb515d905781b6951e4c687917c53090565b60405160208101907f8cad95687ba82c2ce50e74f7b754645e5117c3a5bec8151c0726d5857980a86682527f9ac997416e8ff9d2ff6bebeb7149f65cdae5e32e2b90440b566bb3044041d36a604082015246606082015230608082015260808152611bd3816113ff565b51902090565b611be1611b1e565b906040519060208201927f190100000000000000000000000000000000000000000000000000000000000084526022830152604282015260428152611bd381611398565b9192909360a435936040840151804211611cc65750602084510151808611611c955750918591610d78611c6594611c60602088015186611e47565b611bd9565b73ffffffffffffffffffffffffffffffffffffffff809151511692608435918216820361149b57611810936118b5565b602490604051907f3728b83d0000000000000000000000000000000000000000000000000000000082526004820152fd5b602490604051907fcd21db4f0000000000000000000000000000000000000000000000000000000082526004820152fd5b959093958051519560409283830151804211611e175750848803611dee57611d2e918691610d7860209b611c608d88015186611e47565b60005b868110611d42575050505050505050565b611d4d81835161175e565b5188611d5a83878a61196c565b01359089810151808311611dbe575091818888886001968596611d84575b50505050505001611d31565b611db395611dad9273ffffffffffffffffffffffffffffffffffffffff6105159351169561196c565b916118b5565b803888888883611d78565b6024908651907f3728b83d0000000000000000000000000000000000000000000000000000000082526004820152fd5b600484517fff633a38000000000000000000000000000000000000000000000000000000008152fd5b6024908551907fcd21db4f0000000000000000000000000000000000000000000000000000000082526004820152fd5b9073ffffffffffffffffffffffffffffffffffffffff600160ff83161b9216600052600060205260406000209060081c6000526020526040600020818154188091551615611e9157565b60046040517f756688fe000000000000000000000000000000000000000000000000000000008152fd5b90611ec5826115ef565b611ed26040519182611437565b8281527fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffe0611f0082946115ef565b0190602036910137565b7fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff8114611f375760010190565b7f4e487b7100000000000000000000000000000000000000000000000000000000600052601160045260246000fd5b805160208092019160005b828110611f7f575050505090565b835185529381019392810192600101611f71565b9081519160005b838110611fab575050016000815290565b8060208092840101518185015201611f9a565b60405160208101917f65626cad6cb96493bf6f5ebea28756c966f023ab9e8a83a7101849d5573b3678835273ffffffffffffffffffffffffffffffffffffffff8082511660408401526020820151166060830152606065ffffffffffff9182604082015116608085015201511660a082015260a0815260c0810181811067ffffffffffffffff8211176113b45760405251902090565b6040516020808201927f618358ac3db8dc274f0cd8829da7e234bd48cd73c4a740aede1adec9846d06a1845273ffffffffffffffffffffffffffffffffffffffff81511660408401520151606082015260608152611bd381611398565b919082604091031261149b576020823592013590565b6000843b61222e5750604182036121ac576120e4828201826120b1565b939092604010156117725760209360009360ff6040608095013560f81c5b60405194855216868401526040830152606082015282805260015afa156121a05773ffffffffffffffffffffffffffffffffffffffff806000511691821561217657160361214c57565b60046040517f815e1d64000000000000000000000000000000000000000000000000000000008152fd5b60046040517f8baa579f000000000000000000000000000000000000000000000000000000008152fd5b6040513d6000823e3d90fd5b60408203612204576121c0918101906120b1565b91601b7f7fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff84169360ff1c019060ff8211611f375760209360009360ff608094612102565b60046040517f4be6321b000000000000000000000000000000000000000000000000000000008152fd5b929391601f928173ffffffffffffffffffffffffffffffffffffffff60646020957fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffe0604051988997889687947f1626ba7e000000000000000000000000000000000000000000000000000000009e8f8752600487015260406024870152816044870152868601378b85828601015201168101030192165afa9081156123a857829161232a575b507fffffffff000000000000000000000000000000000000000000000000000000009150160361230057565b60046040517fb0669cbc000000000000000000000000000000000000000000000000000000008152fd5b90506020813d82116123a0575b8161234460209383611437565b810103126103365751907fffffffff0000000000000000000000000000000000000000000000000000000082168203610a9a57507fffffffff0000000000000000000000000000000000000000000000000000000090386122d4565b3d9150612337565b6040513d84823e3d90fdfea164736f6c6343000811000a" - }, - "0x45a834A6bB86F516D4157a8cBcc60f2F35F8398C": { - "balance": "0x0", - "nonce": "0x1", - "code": "0x60806040526004361061008f575f3560e01c806386fdb4d31161005757806386fdb4d314610137578063c1ee9d5e1461014c578063caeda9a614610161578063cb1ed51814610180578063fa0ff39f14610193575f5ffd5b806332e43a111461009357806357043888146100b95780635e666e4a146100ce578063665c5b98146100f95780636715131914610118575b5f5ffd5b34801561009e575f5ffd5b506100a75f5481565b60405190815260200160405180910390f35b3480156100c4575f5ffd5b506100a760025481565b3480156100d9575f5ffd5b506100a76100e83660046103fb565b60016020525f908152604090205481565b348015610104575f5ffd5b506100a76101133660046103fb565b6101b1565b348015610123575f5ffd5b506100a76101323660046103fb565b61023d565b61014a6101453660046103fb565b610284565b005b348015610157575f5ffd5b506100a760035481565b34801561016c575f5ffd5b506100a761017b3660046103fb565b61034d565b61014a61018e3660046103fb565b6103db565b34801561019e575f5ffd5b5061014a6101ad3660046103fb565b5f55565b5f815f036101c057505f919050565b6002546040516bffffffffffffffffffffffff193360601b16602082015260348101919091525f9060540160408051601f19818403018152919052805160209091012090505f5b8381101561022e578181015f90815260016020819052604090912054939093189201610207565b50506002805490920190915590565b5f805b8281101561027e57604080516020810184905290810182905260600160408051601f1981840301815291905280516020909101209150600101610240565b50919050565b5f30825a6102929190610426565b604080514260248083019190915282518083039091018152604490910182526020810180516001600160e01b031663fa0ff39f60e01b17905290516102d7919061043f565b5f604051808303818686fa925050503d805f8114610310576040519150601f19603f3d011682016040523d82523d5f602084013e610315565b606091505b505090505f81610325575f610328565b60015b60ff1690505b825a1115610348578061034081610455565b91505061032e565b505050565b5f815f0361035c57505f919050565b6003546040516bffffffffffffffffffffffff193360601b16602082015260348101919091525f9060540160408051601f19818403018152919052805160209091012090505f5b838110156103cc578181015f8181526001602081905260409091209490911893849055016103a3565b50506003805490920190915590565b5f5b815a11156103f757806103ef81610455565b9150506103dd565b5050565b5f6020828403121561040b575f5ffd5b5035919050565b634e487b7160e01b5f52601160045260245ffd5b8181038181111561043957610439610412565b92915050565b5f82518060208501845e5f920191825250919050565b5f6001820161046657610466610412565b506001019056fea26469706673582212202b8f1c900a394275a0d8f46b1573db0b518b0424e872e3ddb6309e56e5730c7764736f6c634300081d0033" - }, - "0x5294E9927c3306DcBaDb03fe70b92e01cCede505": { - "balance": "0x0", - "nonce": "0x1", - "code": "0x608060405234801561000f575f5ffd5b506004361061003f575f3560e01c8063c3b2c4f814610043578063ea94cddd14610058578063f884e35514610083575b5f5ffd5b610056610051366004610231565b610099565b005b61006660036003609b1b0181565b6040516001600160a01b0390911681526020015b60405180910390f35b61008b5f5481565b60405190815260200161007a565b5f805481806100a7836102c7565b9091555060405190915081907fb252e055da754c72fbf7542cf424b190808a9b541e912894c5e15b4238c41501905f90a2604051631595ec0b60e01b81525f90819060036003609b1b0190631595ec0b9061010c9033908d908d908d90600401610313565b5f604051808303815f875af1158015610127573d5f5f3e3d5ffd5b505050506040513d5f823e601f3d908101601f1916820160405261014e919081019061035d565b915091508161017b578060405163768cb35160e11b81526004016101729190610428565b60405180910390fd5b85896001600160a01b0316336001600160a01b03167feb15ee720798341c37739df41be53acfbbf70ae6802dade35457beec6e47a5e48b8b6040516101c192919061045d565b6040519081900381206101d9918b908b908b9061046c565b60405180910390a4505050505050505050565b5f5f83601f8401126101fc575f5ffd5b50813567ffffffffffffffff811115610213575f5ffd5b60208301915083602082850101111561022a575f5ffd5b9250929050565b5f5f5f5f5f5f60808789031215610246575f5ffd5b86356001600160a01b038116811461025c575f5ffd5b9550602087013567ffffffffffffffff811115610277575f5ffd5b61028389828a016101ec565b90965094505060408701359250606087013567ffffffffffffffff8111156102a9575f5ffd5b6102b589828a016101ec565b979a9699509497509295939492505050565b5f600182016102e457634e487b7160e01b5f52601160045260245ffd5b5060010190565b81835281816020850137505f828201602090810191909152601f909101601f19169091010190565b6001600160a01b038581168252841660208201526060604082018190525f9061033f90830184866102eb565b9695505050505050565b634e487b7160e01b5f52604160045260245ffd5b5f5f6040838503121561036e575f5ffd5b8251801515811461037d575f5ffd5b602084015190925067ffffffffffffffff811115610399575f5ffd5b8301601f810185136103a9575f5ffd5b805167ffffffffffffffff8111156103c3576103c3610349565b604051601f8201601f19908116603f0116810167ffffffffffffffff811182821017156103f2576103f2610349565b604052818152828201602001871015610409575f5ffd5b8160208401602083015e5f602083830101528093505050509250929050565b602081525f82518060208401528060208501604085015e5f604082850101526040601f19601f83011684010191505092915050565b818382375f9101908152919050565b848152606060208201525f6104856060830185876102eb565b90508260408301529594505050505056fea2646970667358221220dc659493b17f8025af73b0b01138f0edcef92362d7b72a66dced7d061709f59964736f6c634300081d0033" - }, - "0x522fAf9A91c41c443c66765030741e4AaCe147D0": { - "balance": "0x0", - "nonce": "0x1", - "code": "0x608060405234801561000f575f5ffd5b50600436106100fb575f3560e01c806372425d9d11610093578063bce38bd711610063578063bce38bd7146101d4578063c3077fa9146101e7578063ea94cddd146101fa578063ee82ac5e14610208575f5ffd5b806372425d9d1461018e57806382ad56cb1461019457806386d516e8146101b4578063a8b0574e146101ba575f5ffd5b8063399542e9116100ce578063399542e9146101455780633e64a6961461016757806342cbb15c1461016d5780634d2301cc14610173575f5ffd5b80630f28c97d146100ff578063252dba421461011457806327e86d6e146101355780633408e4701461013f575b5f5ffd5b425b6040519081526020015b60405180910390f35b610127610122366004610831565b61021a565b60405161010b92919061089e565b435f190140610101565b46610101565b610158610153366004610918565b610328565b60405161010b939291906109dc565b48610101565b43610101565b610101610181366004610a03565b6001600160a01b03163190565b44610101565b6101a76101a2366004610831565b610346565b60405161010b9190610a30565b45610101565b415b6040516001600160a01b03909116815260200161010b565b6101a76101e2366004610918565b6104b5565b6101586101f5366004610831565b6105d0565b6101bc60036003609b1b0181565b610101610216366004610a42565b4090565b436060828067ffffffffffffffff81111561023757610237610a59565b60405190808252806020026020018201604052801561026a57816020015b60608152602001906001900390816102555790505b5091505f5b8181101561031f575f5f6102e63389898681811061028f5761028f610a6d565b90506020028101906102a19190610a81565b6102af906020810190610a03565b8a8a878181106102c1576102c1610a6d565b90506020028101906102d39190610a81565b6102e1906020810190610a9f565b6105ee565b91509150816102f757805160208201fd5b8085848151811061030a5761030a610a6d565b6020908102919091010152505060010161026f565b50509250929050565b5f5f60606103378686866106c9565b91989097509095509350505050565b6060818067ffffffffffffffff81111561036257610362610a59565b6040519080825280602002602001820160405280156103a757816020015b604080518082019091525f8152606060208201528152602001906001900390816103805790505b5091505f5b818110156104ad575f5f61041e338888868181106103cc576103cc610a6d565b90506020028101906103de9190610ae2565b6103ec906020810190610a03565b8989878181106103fe576103fe610a6d565b90506020028101906104109190610ae2565b6102e1906040810190610a9f565b91509150604051806040016040528083151581526020018281525085848151811061044b5761044b610a6d565b602002602001018190525086868481811061046857610468610a6d565b905060200281019061047a9190610ae2565b61048b906040810190602001610af6565b158015610496575081155b156104a357805160208201fd5b50506001016103ac565b505092915050565b6060818067ffffffffffffffff8111156104d1576104d1610a59565b60405190808252806020026020018201604052801561051657816020015b604080518082019091525f8152606060208201528152602001906001900390816104ef5790505b5091505f5b818110156105c7575f5f61056d3388888681811061053b5761053b610a6d565b905060200281019061054d9190610a81565b61055b906020810190610a03565b8989878181106102c1576102c1610a6d565b9150915087801561057c575081155b1561058957805160208201fd5b60405180604001604052808315158152602001828152508584815181106105b2576105b2610a6d565b6020908102919091010152505060010161051b565b50509392505050565b5f5f60606105e0600186866106c9565b919790965090945092505050565b5f60605f5f60036003609b1b016001600160a01b03168888888860405160240161061b9493929190610b11565b60408051601f198184030181529181526020820180516001600160e01b0316631595ec0b60e01b179052516106509190610b59565b5f604051808303815f865af19150503d805f8114610689576040519150601f19603f3d011682016040523d82523d5f602084013e61068e565b606091505b509150915081156106b757808060200190518101906106ad9190610b6f565b90945092506106be565b5f93508092505b505094509492505050565b4380406060838067ffffffffffffffff8111156106e8576106e8610a59565b60405190808252806020026020018201604052801561072d57816020015b604080518082019091525f8152606060208201528152602001906001900390816107065790505b5091505f5b818110156107de575f5f610784338a8a8681811061075257610752610a6d565b90506020028101906107649190610a81565b610772906020810190610a03565b8b8b878181106102c1576102c1610a6d565b91509150898015610793575081155b156107a057805160208201fd5b60405180604001604052808315158152602001828152508584815181106107c9576107c9610a6d565b60209081029190910101525050600101610732565b505093509350939050565b5f5f83601f8401126107f9575f5ffd5b50813567ffffffffffffffff811115610810575f5ffd5b6020830191508360208260051b850101111561082a575f5ffd5b9250929050565b5f5f60208385031215610842575f5ffd5b823567ffffffffffffffff811115610858575f5ffd5b610864858286016107e9565b90969095509350505050565b5f81518084528060208401602086015e5f602082860101526020601f19601f83011685010191505092915050565b5f604082018483526040602084015280845180835260608501915060608160051b8601019250602086015f5b828110156108fb57605f198786030184526108e6858351610870565b945060209384019391909101906001016108ca565b5092979650505050505050565b8015158114610915575f5ffd5b50565b5f5f5f6040848603121561092a575f5ffd5b833561093581610908565b9250602084013567ffffffffffffffff811115610950575f5ffd5b61095c868287016107e9565b9497909650939450505050565b5f82825180855260208501945060208160051b830101602085015f5b838110156109d057601f19858403018852815180511515845260208101519050604060208501526109b96040850182610870565b6020998a0199909450929092019150600101610985565b50909695505050505050565b838152826020820152606060408201525f6109fa6060830184610969565b95945050505050565b5f60208284031215610a13575f5ffd5b81356001600160a01b0381168114610a29575f5ffd5b9392505050565b602081525f610a296020830184610969565b5f60208284031215610a52575f5ffd5b5035919050565b634e487b7160e01b5f52604160045260245ffd5b634e487b7160e01b5f52603260045260245ffd5b5f8235603e19833603018112610a95575f5ffd5b9190910192915050565b5f5f8335601e19843603018112610ab4575f5ffd5b83018035915067ffffffffffffffff821115610ace575f5ffd5b60200191503681900382131561082a575f5ffd5b5f8235605e19833603018112610a95575f5ffd5b5f60208284031215610b06575f5ffd5b8135610a2981610908565b6001600160a01b038581168252841660208201526060604082018190528101829052818360808301375f818301608090810191909152601f909201601f191601019392505050565b5f82518060208501845e5f920191825250919050565b5f5f60408385031215610b80575f5ffd5b8251610b8b81610908565b602084015190925067ffffffffffffffff811115610ba7575f5ffd5b8301601f81018513610bb7575f5ffd5b805167ffffffffffffffff811115610bd157610bd1610a59565b604051601f8201601f19908116603f0116810167ffffffffffffffff81118282101715610c0057610c00610a59565b604052818152828201602001871015610c17575f5ffd5b8160208401602083015e5f60208383010152809350505050925092905056fea26469706673582212203a46c6d8768363ec59bf5aa5a61ddd8f421c4777c568057c3340d263f7960bad64736f6c634300081d0033" - }, - "0x298122B4bF05CC897662e535C18417f44C7f274b": { - "balance": "0x0", - "nonce": "0x1", - "code": "0x608060405234801561000f575f5ffd5b5060043610610090575f3560e01c8063313ce56711610063578063313ce567146100fa57806370a082311461010957806395d89b4114610131578063a9059cbb14610139578063dd62ed3e1461014c575f5ffd5b806306fdde0314610094578063095ea7b3146100b257806318160ddd146100d557806323b872dd146100e7575b5f5ffd5b61009c610184565b6040516100a99190610554565b60405180910390f35b6100c56100c03660046105a4565b610214565b60405190151581526020016100a9565b6002545b6040519081526020016100a9565b6100c56100f53660046105cc565b61022d565b604051601281526020016100a9565b6100d9610117366004610606565b6001600160a01b03165f9081526020819052604090205490565b61009c610250565b6100c56101473660046105a4565b61025f565b6100d961015a366004610626565b6001600160a01b039182165f90815260016020908152604080832093909416825291909152205490565b60606003805461019390610657565b80601f01602080910402602001604051908101604052809291908181526020018280546101bf90610657565b801561020a5780601f106101e15761010080835404028352916020019161020a565b820191905f5260205f20905b8154815290600101906020018083116101ed57829003601f168201915b5050505050905090565b5f3361022181858561026c565b60019150505b92915050565b5f3361023a85828561027e565b6102458585856102ff565b506001949350505050565b60606004805461019390610657565b5f336102218185856102ff565b610279838383600161035c565b505050565b6001600160a01b038381165f908152600160209081526040808320938616835292905220545f198110156102f957818110156102eb57604051637dc7a0d960e11b81526001600160a01b038416600482015260248101829052604481018390526064015b60405180910390fd5b6102f984848484035f61035c565b50505050565b6001600160a01b03831661032857604051634b637e8f60e11b81525f60048201526024016102e2565b6001600160a01b0382166103515760405163ec442f0560e01b81525f60048201526024016102e2565b61027983838361042e565b6001600160a01b0384166103855760405163e602df0560e01b81525f60048201526024016102e2565b6001600160a01b0383166103ae57604051634a1406b160e11b81525f60048201526024016102e2565b6001600160a01b038085165f90815260016020908152604080832093871683529290522082905580156102f957826001600160a01b0316846001600160a01b03167f8c5be1e5ebec7d5bd14f71427d1e84f3dd0314c0f7b2291e5b200ac8c7c3b9258460405161042091815260200190565b60405180910390a350505050565b6001600160a01b038316610458578060025f82825461044d919061068f565b909155506104c89050565b6001600160a01b0383165f90815260208190526040902054818110156104aa5760405163391434e360e21b81526001600160a01b038516600482015260248101829052604481018390526064016102e2565b6001600160a01b0384165f9081526020819052604090209082900390555b6001600160a01b0382166104e457600280548290039055610502565b6001600160a01b0382165f9081526020819052604090208054820190555b816001600160a01b0316836001600160a01b03167fddf252ad1be2c89b69c2b068fc378daa952ba7f163c4a11628f55a4df523b3ef8360405161054791815260200190565b60405180910390a3505050565b602081525f82518060208401528060208501604085015e5f604082850101526040601f19601f83011684010191505092915050565b80356001600160a01b038116811461059f575f5ffd5b919050565b5f5f604083850312156105b5575f5ffd5b6105be83610589565b946020939093013593505050565b5f5f5f606084860312156105de575f5ffd5b6105e784610589565b92506105f560208501610589565b929592945050506040919091013590565b5f60208284031215610616575f5ffd5b61061f82610589565b9392505050565b5f5f60408385031215610637575f5ffd5b61064083610589565b915061064e60208401610589565b90509250929050565b600181811c9082168061066b57607f821691505b60208210810361068957634e487b7160e01b5f52602260045260245ffd5b50919050565b8082018082111561022757634e487b7160e01b5f52601160045260245ffdfea2646970667358221220ccdc2d87d4833be51b291c8c0fb8b7a70b9625f6a58ddecdd2aced25e83cf63764736f6c634300081d0033", - "storage": { - "0x0000000000000000000000000000000000000000000000000000000000000002": "0x0000000000000000000000000000000000000000000d3c21bcecceda10000000", - "0x723077b8a1b173adc35e5f0e7e3662fd1208212cb629f9c128551ea7168da722": "0x00000000000000000000000000000000000000000000d3c21bcecceda1000000", - "0x14e04a66bf74771820a7400ff6cf065175b3d7eb25805a5bd1633b161af5d101": "0x00000000000000000000000000000000000000000000d3c21bcecceda1000000", - "0x215be5d23550ceb1beff54fb579a765903ba2ccc85b6f79bcf9bda4e8cb86034": "0x00000000000000000000000000000000000000000000d3c21bcecceda1000000", - "0x6d1035fce6503985ab075a4ff3f7ce2e57cd5a9c5e6a0589dccacfea7bcb0af4": "0x00000000000000000000000000000000000000000000d3c21bcecceda1000000", - "0x2a95ee547cef07a2fff0a68144824a0d9ded35ed87da118a53e1cda4aca8b944": "0x00000000000000000000000000000000000000000000d3c21bcecceda1000000", - "0x7fcecd2a720442e9bc0cf1a8a6976f9fbddf6b996dc0d78af7e94dadf360d579": "0x00000000000000000000000000000000000000000000d3c21bcecceda1000000", - "0x18bbf5fcf8fe870ecff419c4677497c08b2e6a5431bb94541d06c9da3f308e55": "0x00000000000000000000000000000000000000000000d3c21bcecceda1000000", - "0x6e3431b4e42570cb9e3d926eb26f9e54de2df536ae0741ae16350d17a6c16ddc": "0x00000000000000000000000000000000000000000000d3c21bcecceda1000000", - "0xdb302bf24b1ad5f23949da8e6b05747dc699499a995361a7bf40ec7204696d6f": "0x00000000000000000000000000000000000000000000d3c21bcecceda1000000", - "0xa1d47ef1a6916dfbe65888f77739da164feb3a9a6afc95ee57e8b3e85ea5e955": "0x00000000000000000000000000000000000000000000d3c21bcecceda1000000", - "0x4f3023ab66ce27b62950d9c11c45cdaffd2f3d837fccc21313b89f9d93d20dd1": "0x00000000000000000000000000000000000000000000d3c21bcecceda1000000", - "0xdbf9dd8da26aaf6e07d4d925dce20b04b26ca187a463d495e173cf3c15cfccc1": "0x00000000000000000000000000000000000000000000d3c21bcecceda1000000", - "0x400950d3c7dab835a895de1a6b743e20bd2ce96e73f098aa213f4553b3eb16cc": "0x00000000000000000000000000000000000000000000d3c21bcecceda1000000", - "0xe234f35d6d6894ba3a49e76a5ba36ff004256d3fe3253cc5f5c1f92cdde2a3ed": "0x00000000000000000000000000000000000000000000d3c21bcecceda1000000", - "0x0c636e5d9bb04bb0c3ba5e32852bd9bdde7bfff0e0a579a83f1a1f7eb788470b": "0x00000000000000000000000000000000000000000000d3c21bcecceda1000000", - "0x12323e4c8a72000bfdc2625276f637e25b3cefa268e59508e425c5684acf27f5": "0x00000000000000000000000000000000000000000000d3c21bcecceda1000000" - } - }, - "0x1800000000000000000000000000000000000002": { - "balance": "0x0", - "nonce": "0x1", - "code": "0xef" - }, - "0x1800000000000000000000000000000000000003": { - "balance": "0x0", - "nonce": "0x1", - "code": "0xef" - }, - "0x1800000000000000000000000000000000000004": { - "balance": "0x0", - "nonce": "0x1", - "code": "0xef" - }, - "0x1800000000000000000000000000000000000005": { - "balance": "0x0", - "nonce": "0x1", - "code": "0xef" - }, - "0x1800000000000000000000000000000000000006": { - "balance": "0x0", - "nonce": "0x1", - "code": "0xef" - }, - "0x1800000000000000000000000000000000000007": { - "balance": "0x0", - "nonce": "0x1", - "code": "0xef" - }, - "0x1800000000000000000000000000000000000008": { - "balance": "0x0", - "nonce": "0x1", - "code": "0xef" - }, - "0x1800000000000000000000000000000000000009": { - "balance": "0x0", - "nonce": "0x1", - "code": "0xef" - }, - "0x180000000000000000000000000000000000000a": { - "balance": "0x0", - "nonce": "0x1", - "code": "0xef" - }, - "0x180000000000000000000000000000000000000b": { - "balance": "0x0", - "nonce": "0x1", - "code": "0xef" - }, - "0x180000000000000000000000000000000000000c": { - "balance": "0x0", - "nonce": "0x1", - "code": "0xef" - }, - "0x180000000000000000000000000000000000000d": { - "balance": "0x0", - "nonce": "0x1", - "code": "0xef" - }, - "0x180000000000000000000000000000000000000e": { - "balance": "0x0", - "nonce": "0x1", - "code": "0xef" - }, - "0x180000000000000000000000000000000000000f": { - "balance": "0x0", - "nonce": "0x1", - "code": "0xef" - }, - "0x1800000000000000000000000000000000000010": { - "balance": "0x0", - "nonce": "0x1", - "code": "0xef" - }, - "0x1800000000000000000000000000000000000011": { - "balance": "0x0", - "nonce": "0x1", - "code": "0xef" - }, - "0x1800000000000000000000000000000000000012": { - "balance": "0x0", - "nonce": "0x1", - "code": "0xef" - }, - "0x1800000000000000000000000000000000000013": { - "balance": "0x0", - "nonce": "0x1", - "code": "0xef" - }, - "0x1800000000000000000000000000000000000014": { - "balance": "0x0", - "nonce": "0x1", - "code": "0xef" - }, - "0x1800000000000000000000000000000000000015": { - "balance": "0x0", - "nonce": "0x1", - "code": "0xef" - }, - "0x1800000000000000000000000000000000000016": { - "balance": "0x0", - "nonce": "0x1", - "code": "0xef" - }, - "0x1800000000000000000000000000000000000017": { - "balance": "0x0", - "nonce": "0x1", - "code": "0xef" - }, - "0x1800000000000000000000000000000000000018": { - "balance": "0x0", - "nonce": "0x1", - "code": "0xef" - }, - "0x1800000000000000000000000000000000000019": { - "balance": "0x0", - "nonce": "0x1", - "code": "0xef" - }, - "0x180000000000000000000000000000000000001a": { - "balance": "0x0", - "nonce": "0x1", - "code": "0xef" - }, - "0x180000000000000000000000000000000000001b": { - "balance": "0x0", - "nonce": "0x1", - "code": "0xef" - }, - "0x180000000000000000000000000000000000001c": { - "balance": "0x0", - "nonce": "0x1", - "code": "0xef" - }, - "0x180000000000000000000000000000000000001d": { - "balance": "0x0", - "nonce": "0x1", - "code": "0xef" - }, - "0x180000000000000000000000000000000000001e": { - "balance": "0x0", - "nonce": "0x1", - "code": "0xef" - }, - "0x180000000000000000000000000000000000001f": { - "balance": "0x0", - "nonce": "0x1", - "code": "0xef" - }, - "0x1800000000000000000000000000000000000020": { - "balance": "0x0", - "nonce": "0x1", - "code": "0xef" - }, - "0x1800000000000000000000000000000000000021": { - "balance": "0x0", - "nonce": "0x1", - "code": "0xef" - }, - "0x1800000000000000000000000000000000000022": { - "balance": "0x0", - "nonce": "0x1", - "code": "0xef" - }, - "0x1800000000000000000000000000000000000023": { - "balance": "0x0", - "nonce": "0x1", - "code": "0xef" - }, - "0x1800000000000000000000000000000000000024": { - "balance": "0x0", - "nonce": "0x1", - "code": "0xef" - }, - "0x1800000000000000000000000000000000000025": { - "balance": "0x0", - "nonce": "0x1", - "code": "0xef" - }, - "0x1800000000000000000000000000000000000026": { - "balance": "0x0", - "nonce": "0x1", - "code": "0xef" - }, - "0x1800000000000000000000000000000000000027": { - "balance": "0x0", - "nonce": "0x1", - "code": "0xef" - }, - "0x1800000000000000000000000000000000000028": { - "balance": "0x0", - "nonce": "0x1", - "code": "0xef" - }, - "0x1800000000000000000000000000000000000029": { - "balance": "0x0", - "nonce": "0x1", - "code": "0xef" - }, - "0x180000000000000000000000000000000000002a": { - "balance": "0x0", - "nonce": "0x1", - "code": "0xef" - }, - "0x180000000000000000000000000000000000002b": { - "balance": "0x0", - "nonce": "0x1", - "code": "0xef" - }, - "0x180000000000000000000000000000000000002c": { - "balance": "0x0", - "nonce": "0x1", - "code": "0xef" - }, - "0x180000000000000000000000000000000000002d": { - "balance": "0x0", - "nonce": "0x1", - "code": "0xef" - }, - "0x180000000000000000000000000000000000002e": { - "balance": "0x0", - "nonce": "0x1", - "code": "0xef" - }, - "0x180000000000000000000000000000000000002f": { - "balance": "0x0", - "nonce": "0x1", - "code": "0xef" - }, - "0x1800000000000000000000000000000000000030": { - "balance": "0x0", - "nonce": "0x1", - "code": "0xef" - }, - "0x1800000000000000000000000000000000000031": { - "balance": "0x0", - "nonce": "0x1", - "code": "0xef" - }, - "0x1800000000000000000000000000000000000032": { - "balance": "0x0", - "nonce": "0x1", - "code": "0xef" - }, - "0x1800000000000000000000000000000000000033": { - "balance": "0x0", - "nonce": "0x1", - "code": "0xef" - }, - "0x1800000000000000000000000000000000000034": { - "balance": "0x0", - "nonce": "0x1", - "code": "0xef" - }, - "0x1800000000000000000000000000000000000035": { - "balance": "0x0", - "nonce": "0x1", - "code": "0xef" - }, - "0x1800000000000000000000000000000000000036": { - "balance": "0x0", - "nonce": "0x1", - "code": "0xef" - }, - "0x1800000000000000000000000000000000000037": { - "balance": "0x0", - "nonce": "0x1", - "code": "0xef" - }, - "0x1800000000000000000000000000000000000038": { - "balance": "0x0", - "nonce": "0x1", - "code": "0xef" - }, - "0x1800000000000000000000000000000000000039": { - "balance": "0x0", - "nonce": "0x1", - "code": "0xef" - }, - "0x180000000000000000000000000000000000003a": { - "balance": "0x0", - "nonce": "0x1", - "code": "0xef" - }, - "0x180000000000000000000000000000000000003b": { - "balance": "0x0", - "nonce": "0x1", - "code": "0xef" - }, - "0x180000000000000000000000000000000000003c": { - "balance": "0x0", - "nonce": "0x1", - "code": "0xef" - }, - "0x180000000000000000000000000000000000003d": { - "balance": "0x0", - "nonce": "0x1", - "code": "0xef" - }, - "0x180000000000000000000000000000000000003e": { - "balance": "0x0", - "nonce": "0x1", - "code": "0xef" - }, - "0x180000000000000000000000000000000000003f": { - "balance": "0x0", - "nonce": "0x1", - "code": "0xef" - }, - "0x1800000000000000000000000000000000000040": { - "balance": "0x0", - "nonce": "0x1", - "code": "0xef" - }, - "0x1800000000000000000000000000000000000041": { - "balance": "0x0", - "nonce": "0x1", - "code": "0xef" - }, - "0x1800000000000000000000000000000000000042": { - "balance": "0x0", - "nonce": "0x1", - "code": "0xef" - }, - "0x1800000000000000000000000000000000000043": { - "balance": "0x0", - "nonce": "0x1", - "code": "0xef" - }, - "0x1800000000000000000000000000000000000044": { - "balance": "0x0", - "nonce": "0x1", - "code": "0xef" - }, - "0x1800000000000000000000000000000000000045": { - "balance": "0x0", - "nonce": "0x1", - "code": "0xef" - }, - "0x1800000000000000000000000000000000000046": { - "balance": "0x0", - "nonce": "0x1", - "code": "0xef" - }, - "0x1800000000000000000000000000000000000047": { - "balance": "0x0", - "nonce": "0x1", - "code": "0xef" - }, - "0x1800000000000000000000000000000000000048": { - "balance": "0x0", - "nonce": "0x1", - "code": "0xef" - }, - "0x1800000000000000000000000000000000000049": { - "balance": "0x0", - "nonce": "0x1", - "code": "0xef" - }, - "0x180000000000000000000000000000000000004a": { - "balance": "0x0", - "nonce": "0x1", - "code": "0xef" - }, - "0x180000000000000000000000000000000000004b": { - "balance": "0x0", - "nonce": "0x1", - "code": "0xef" - }, - "0x180000000000000000000000000000000000004c": { - "balance": "0x0", - "nonce": "0x1", - "code": "0xef" - }, - "0x180000000000000000000000000000000000004d": { - "balance": "0x0", - "nonce": "0x1", - "code": "0xef" - }, - "0x180000000000000000000000000000000000004e": { - "balance": "0x0", - "nonce": "0x1", - "code": "0xef" - }, - "0x180000000000000000000000000000000000004f": { - "balance": "0x0", - "nonce": "0x1", - "code": "0xef" - }, - "0x1800000000000000000000000000000000000050": { - "balance": "0x0", - "nonce": "0x1", - "code": "0xef" - }, - "0x1800000000000000000000000000000000000051": { - "balance": "0x0", - "nonce": "0x1", - "code": "0xef" - }, - "0x1800000000000000000000000000000000000052": { - "balance": "0x0", - "nonce": "0x1", - "code": "0xef" - }, - "0x1800000000000000000000000000000000000053": { - "balance": "0x0", - "nonce": "0x1", - "code": "0xef" - }, - "0x1800000000000000000000000000000000000054": { - "balance": "0x0", - "nonce": "0x1", - "code": "0xef" - }, - "0x1800000000000000000000000000000000000055": { - "balance": "0x0", - "nonce": "0x1", - "code": "0xef" - }, - "0x1800000000000000000000000000000000000056": { - "balance": "0x0", - "nonce": "0x1", - "code": "0xef" - }, - "0x1800000000000000000000000000000000000057": { - "balance": "0x0", - "nonce": "0x1", - "code": "0xef" - }, - "0x1800000000000000000000000000000000000058": { - "balance": "0x0", - "nonce": "0x1", - "code": "0xef" - }, - "0x1800000000000000000000000000000000000059": { - "balance": "0x0", - "nonce": "0x1", - "code": "0xef" - }, - "0x180000000000000000000000000000000000005a": { - "balance": "0x0", - "nonce": "0x1", - "code": "0xef" - }, - "0x180000000000000000000000000000000000005b": { - "balance": "0x0", - "nonce": "0x1", - "code": "0xef" - }, - "0x180000000000000000000000000000000000005c": { - "balance": "0x0", - "nonce": "0x1", - "code": "0xef" - }, - "0x180000000000000000000000000000000000005d": { - "balance": "0x0", - "nonce": "0x1", - "code": "0xef" - }, - "0x180000000000000000000000000000000000005e": { - "balance": "0x0", - "nonce": "0x1", - "code": "0xef" - }, - "0x180000000000000000000000000000000000005f": { - "balance": "0x0", - "nonce": "0x1", - "code": "0xef" - }, - "0x1800000000000000000000000000000000000060": { - "balance": "0x0", - "nonce": "0x1", - "code": "0xef" - }, - "0x1800000000000000000000000000000000000061": { - "balance": "0x0", - "nonce": "0x1", - "code": "0xef" - }, - "0x1800000000000000000000000000000000000062": { - "balance": "0x0", - "nonce": "0x1", - "code": "0xef" - }, - "0x1800000000000000000000000000000000000063": { - "balance": "0x0", - "nonce": "0x1", - "code": "0xef" - }, - "0x1800000000000000000000000000000000000064": { - "balance": "0x0", - "nonce": "0x1", - "code": "0xef" - }, - "0x1800000000000000000000000000000000000065": { - "balance": "0x0", - "nonce": "0x1", - "code": "0xef" - }, - "0x1800000000000000000000000000000000000066": { - "balance": "0x0", - "nonce": "0x1", - "code": "0xef" - }, - "0x1800000000000000000000000000000000000067": { - "balance": "0x0", - "nonce": "0x1", - "code": "0xef" - }, - "0x1800000000000000000000000000000000000068": { - "balance": "0x0", - "nonce": "0x1", - "code": "0xef" - }, - "0x1800000000000000000000000000000000000069": { - "balance": "0x0", - "nonce": "0x1", - "code": "0xef" - }, - "0x180000000000000000000000000000000000006a": { - "balance": "0x0", - "nonce": "0x1", - "code": "0xef" - }, - "0x180000000000000000000000000000000000006b": { - "balance": "0x0", - "nonce": "0x1", - "code": "0xef" - }, - "0x180000000000000000000000000000000000006c": { - "balance": "0x0", - "nonce": "0x1", - "code": "0xef" - }, - "0x180000000000000000000000000000000000006d": { - "balance": "0x0", - "nonce": "0x1", - "code": "0xef" - }, - "0x180000000000000000000000000000000000006e": { - "balance": "0x0", - "nonce": "0x1", - "code": "0xef" - }, - "0x180000000000000000000000000000000000006f": { - "balance": "0x0", - "nonce": "0x1", - "code": "0xef" - }, - "0x1800000000000000000000000000000000000070": { - "balance": "0x0", - "nonce": "0x1", - "code": "0xef" - }, - "0x1800000000000000000000000000000000000071": { - "balance": "0x0", - "nonce": "0x1", - "code": "0xef" - }, - "0x1800000000000000000000000000000000000072": { - "balance": "0x0", - "nonce": "0x1", - "code": "0xef" - }, - "0x1800000000000000000000000000000000000073": { - "balance": "0x0", - "nonce": "0x1", - "code": "0xef" - }, - "0x1800000000000000000000000000000000000074": { - "balance": "0x0", - "nonce": "0x1", - "code": "0xef" - }, - "0x1800000000000000000000000000000000000075": { - "balance": "0x0", - "nonce": "0x1", - "code": "0xef" - }, - "0x1800000000000000000000000000000000000076": { - "balance": "0x0", - "nonce": "0x1", - "code": "0xef" - }, - "0x1800000000000000000000000000000000000077": { - "balance": "0x0", - "nonce": "0x1", - "code": "0xef" - }, - "0x1800000000000000000000000000000000000078": { - "balance": "0x0", - "nonce": "0x1", - "code": "0xef" - }, - "0x1800000000000000000000000000000000000079": { - "balance": "0x0", - "nonce": "0x1", - "code": "0xef" - }, - "0x180000000000000000000000000000000000007a": { - "balance": "0x0", - "nonce": "0x1", - "code": "0xef" - }, - "0x180000000000000000000000000000000000007b": { - "balance": "0x0", - "nonce": "0x1", - "code": "0xef" - }, - "0x180000000000000000000000000000000000007c": { - "balance": "0x0", - "nonce": "0x1", - "code": "0xef" - }, - "0x180000000000000000000000000000000000007d": { - "balance": "0x0", - "nonce": "0x1", - "code": "0xef" - }, - "0x180000000000000000000000000000000000007e": { - "balance": "0x0", - "nonce": "0x1", - "code": "0xef" - }, - "0x180000000000000000000000000000000000007f": { - "balance": "0x0", - "nonce": "0x1", - "code": "0xef" - }, - "0x1800000000000000000000000000000000000080": { - "balance": "0x0", - "nonce": "0x1", - "code": "0xef" - }, - "0x1800000000000000000000000000000000000081": { - "balance": "0x0", - "nonce": "0x1", - "code": "0xef" - }, - "0x1800000000000000000000000000000000000082": { - "balance": "0x0", - "nonce": "0x1", - "code": "0xef" - }, - "0x1800000000000000000000000000000000000083": { - "balance": "0x0", - "nonce": "0x1", - "code": "0xef" - }, - "0x1800000000000000000000000000000000000084": { - "balance": "0x0", - "nonce": "0x1", - "code": "0xef" - }, - "0x1800000000000000000000000000000000000085": { - "balance": "0x0", - "nonce": "0x1", - "code": "0xef" - }, - "0x1800000000000000000000000000000000000086": { - "balance": "0x0", - "nonce": "0x1", - "code": "0xef" - }, - "0x1800000000000000000000000000000000000087": { - "balance": "0x0", - "nonce": "0x1", - "code": "0xef" - }, - "0x1800000000000000000000000000000000000088": { - "balance": "0x0", - "nonce": "0x1", - "code": "0xef" - }, - "0x1800000000000000000000000000000000000089": { - "balance": "0x0", - "nonce": "0x1", - "code": "0xef" - }, - "0x180000000000000000000000000000000000008a": { - "balance": "0x0", - "nonce": "0x1", - "code": "0xef" - }, - "0x180000000000000000000000000000000000008b": { - "balance": "0x0", - "nonce": "0x1", - "code": "0xef" - }, - "0x180000000000000000000000000000000000008c": { - "balance": "0x0", - "nonce": "0x1", - "code": "0xef" - }, - "0x180000000000000000000000000000000000008d": { - "balance": "0x0", - "nonce": "0x1", - "code": "0xef" - }, - "0x180000000000000000000000000000000000008e": { - "balance": "0x0", - "nonce": "0x1", - "code": "0xef" - }, - "0x180000000000000000000000000000000000008f": { - "balance": "0x0", - "nonce": "0x1", - "code": "0xef" - }, - "0x1800000000000000000000000000000000000090": { - "balance": "0x0", - "nonce": "0x1", - "code": "0xef" - }, - "0x1800000000000000000000000000000000000091": { - "balance": "0x0", - "nonce": "0x1", - "code": "0xef" - }, - "0x1800000000000000000000000000000000000092": { - "balance": "0x0", - "nonce": "0x1", - "code": "0xef" - }, - "0x1800000000000000000000000000000000000093": { - "balance": "0x0", - "nonce": "0x1", - "code": "0xef" - }, - "0x1800000000000000000000000000000000000094": { - "balance": "0x0", - "nonce": "0x1", - "code": "0xef" - }, - "0x1800000000000000000000000000000000000095": { - "balance": "0x0", - "nonce": "0x1", - "code": "0xef" - }, - "0x1800000000000000000000000000000000000096": { - "balance": "0x0", - "nonce": "0x1", - "code": "0xef" - }, - "0x1800000000000000000000000000000000000097": { - "balance": "0x0", - "nonce": "0x1", - "code": "0xef" - }, - "0x1800000000000000000000000000000000000098": { - "balance": "0x0", - "nonce": "0x1", - "code": "0xef" - }, - "0x1800000000000000000000000000000000000099": { - "balance": "0x0", - "nonce": "0x1", - "code": "0xef" - }, - "0x180000000000000000000000000000000000009a": { - "balance": "0x0", - "nonce": "0x1", - "code": "0xef" - }, - "0x180000000000000000000000000000000000009b": { - "balance": "0x0", - "nonce": "0x1", - "code": "0xef" - }, - "0x180000000000000000000000000000000000009c": { - "balance": "0x0", - "nonce": "0x1", - "code": "0xef" - }, - "0x180000000000000000000000000000000000009d": { - "balance": "0x0", - "nonce": "0x1", - "code": "0xef" - }, - "0x180000000000000000000000000000000000009e": { - "balance": "0x0", - "nonce": "0x1", - "code": "0xef" - }, - "0x180000000000000000000000000000000000009f": { - "balance": "0x0", - "nonce": "0x1", - "code": "0xef" - }, - "0x18000000000000000000000000000000000000a0": { - "balance": "0x0", - "nonce": "0x1", - "code": "0xef" - }, - "0x18000000000000000000000000000000000000a1": { - "balance": "0x0", - "nonce": "0x1", - "code": "0xef" - }, - "0x18000000000000000000000000000000000000a2": { - "balance": "0x0", - "nonce": "0x1", - "code": "0xef" - }, - "0x18000000000000000000000000000000000000a3": { - "balance": "0x0", - "nonce": "0x1", - "code": "0xef" - }, - "0x18000000000000000000000000000000000000a4": { - "balance": "0x0", - "nonce": "0x1", - "code": "0xef" - }, - "0x18000000000000000000000000000000000000a5": { - "balance": "0x0", - "nonce": "0x1", - "code": "0xef" - }, - "0x18000000000000000000000000000000000000a6": { - "balance": "0x0", - "nonce": "0x1", - "code": "0xef" - }, - "0x18000000000000000000000000000000000000a7": { - "balance": "0x0", - "nonce": "0x1", - "code": "0xef" - }, - "0x18000000000000000000000000000000000000a8": { - "balance": "0x0", - "nonce": "0x1", - "code": "0xef" - }, - "0x18000000000000000000000000000000000000a9": { - "balance": "0x0", - "nonce": "0x1", - "code": "0xef" - }, - "0x18000000000000000000000000000000000000aa": { - "balance": "0x0", - "nonce": "0x1", - "code": "0xef" - }, - "0x18000000000000000000000000000000000000ab": { - "balance": "0x0", - "nonce": "0x1", - "code": "0xef" - }, - "0x18000000000000000000000000000000000000ac": { - "balance": "0x0", - "nonce": "0x1", - "code": "0xef" - }, - "0x18000000000000000000000000000000000000ad": { - "balance": "0x0", - "nonce": "0x1", - "code": "0xef" - }, - "0x18000000000000000000000000000000000000ae": { - "balance": "0x0", - "nonce": "0x1", - "code": "0xef" - }, - "0x18000000000000000000000000000000000000af": { - "balance": "0x0", - "nonce": "0x1", - "code": "0xef" - }, - "0x18000000000000000000000000000000000000b0": { - "balance": "0x0", - "nonce": "0x1", - "code": "0xef" - }, - "0x18000000000000000000000000000000000000b1": { - "balance": "0x0", - "nonce": "0x1", - "code": "0xef" - }, - "0x18000000000000000000000000000000000000b2": { - "balance": "0x0", - "nonce": "0x1", - "code": "0xef" - }, - "0x18000000000000000000000000000000000000b3": { - "balance": "0x0", - "nonce": "0x1", - "code": "0xef" - }, - "0x18000000000000000000000000000000000000b4": { - "balance": "0x0", - "nonce": "0x1", - "code": "0xef" - }, - "0x18000000000000000000000000000000000000b5": { - "balance": "0x0", - "nonce": "0x1", - "code": "0xef" - }, - "0x18000000000000000000000000000000000000b6": { - "balance": "0x0", - "nonce": "0x1", - "code": "0xef" - }, - "0x18000000000000000000000000000000000000b7": { - "balance": "0x0", - "nonce": "0x1", - "code": "0xef" - }, - "0x18000000000000000000000000000000000000b8": { - "balance": "0x0", - "nonce": "0x1", - "code": "0xef" - }, - "0x18000000000000000000000000000000000000b9": { - "balance": "0x0", - "nonce": "0x1", - "code": "0xef" - }, - "0x18000000000000000000000000000000000000ba": { - "balance": "0x0", - "nonce": "0x1", - "code": "0xef" - }, - "0x18000000000000000000000000000000000000bb": { - "balance": "0x0", - "nonce": "0x1", - "code": "0xef" - }, - "0x18000000000000000000000000000000000000bc": { - "balance": "0x0", - "nonce": "0x1", - "code": "0xef" - }, - "0x18000000000000000000000000000000000000bd": { - "balance": "0x0", - "nonce": "0x1", - "code": "0xef" - }, - "0x18000000000000000000000000000000000000be": { - "balance": "0x0", - "nonce": "0x1", - "code": "0xef" - }, - "0x18000000000000000000000000000000000000bf": { - "balance": "0x0", - "nonce": "0x1", - "code": "0xef" - }, - "0x18000000000000000000000000000000000000c0": { - "balance": "0x0", - "nonce": "0x1", - "code": "0xef" - }, - "0x18000000000000000000000000000000000000c1": { - "balance": "0x0", - "nonce": "0x1", - "code": "0xef" - }, - "0x18000000000000000000000000000000000000c2": { - "balance": "0x0", - "nonce": "0x1", - "code": "0xef" - }, - "0x18000000000000000000000000000000000000c3": { - "balance": "0x0", - "nonce": "0x1", - "code": "0xef" - }, - "0x18000000000000000000000000000000000000c4": { - "balance": "0x0", - "nonce": "0x1", - "code": "0xef" - }, - "0x18000000000000000000000000000000000000c5": { - "balance": "0x0", - "nonce": "0x1", - "code": "0xef" - }, - "0x18000000000000000000000000000000000000c6": { - "balance": "0x0", - "nonce": "0x1", - "code": "0xef" - }, - "0x18000000000000000000000000000000000000c7": { - "balance": "0x0", - "nonce": "0x1", - "code": "0xef" - }, - "0x18000000000000000000000000000000000000c8": { - "balance": "0x0", - "nonce": "0x1", - "code": "0xef" - }, - "0x18000000000000000000000000000000000000c9": { - "balance": "0x0", - "nonce": "0x1", - "code": "0xef" - }, - "0x18000000000000000000000000000000000000ca": { - "balance": "0x0", - "nonce": "0x1", - "code": "0xef" - }, - "0x18000000000000000000000000000000000000cb": { - "balance": "0x0", - "nonce": "0x1", - "code": "0xef" - }, - "0x18000000000000000000000000000000000000cc": { - "balance": "0x0", - "nonce": "0x1", - "code": "0xef" - }, - "0x18000000000000000000000000000000000000cd": { - "balance": "0x0", - "nonce": "0x1", - "code": "0xef" - }, - "0x18000000000000000000000000000000000000ce": { - "balance": "0x0", - "nonce": "0x1", - "code": "0xef" - }, - "0x18000000000000000000000000000000000000cf": { - "balance": "0x0", - "nonce": "0x1", - "code": "0xef" - }, - "0x18000000000000000000000000000000000000d0": { - "balance": "0x0", - "nonce": "0x1", - "code": "0xef" - }, - "0x18000000000000000000000000000000000000d1": { - "balance": "0x0", - "nonce": "0x1", - "code": "0xef" - }, - "0x18000000000000000000000000000000000000d2": { - "balance": "0x0", - "nonce": "0x1", - "code": "0xef" - }, - "0x18000000000000000000000000000000000000d3": { - "balance": "0x0", - "nonce": "0x1", - "code": "0xef" - }, - "0x18000000000000000000000000000000000000d4": { - "balance": "0x0", - "nonce": "0x1", - "code": "0xef" - }, - "0x18000000000000000000000000000000000000d5": { - "balance": "0x0", - "nonce": "0x1", - "code": "0xef" - }, - "0x18000000000000000000000000000000000000d6": { - "balance": "0x0", - "nonce": "0x1", - "code": "0xef" - }, - "0x18000000000000000000000000000000000000d7": { - "balance": "0x0", - "nonce": "0x1", - "code": "0xef" - }, - "0x18000000000000000000000000000000000000d8": { - "balance": "0x0", - "nonce": "0x1", - "code": "0xef" - }, - "0x18000000000000000000000000000000000000d9": { - "balance": "0x0", - "nonce": "0x1", - "code": "0xef" - }, - "0x18000000000000000000000000000000000000da": { - "balance": "0x0", - "nonce": "0x1", - "code": "0xef" - }, - "0x18000000000000000000000000000000000000db": { - "balance": "0x0", - "nonce": "0x1", - "code": "0xef" - }, - "0x18000000000000000000000000000000000000dc": { - "balance": "0x0", - "nonce": "0x1", - "code": "0xef" - }, - "0x18000000000000000000000000000000000000dd": { - "balance": "0x0", - "nonce": "0x1", - "code": "0xef" - }, - "0x18000000000000000000000000000000000000de": { - "balance": "0x0", - "nonce": "0x1", - "code": "0xef" - }, - "0x18000000000000000000000000000000000000df": { - "balance": "0x0", - "nonce": "0x1", - "code": "0xef" - }, - "0x18000000000000000000000000000000000000e0": { - "balance": "0x0", - "nonce": "0x1", - "code": "0xef" - }, - "0x18000000000000000000000000000000000000e1": { - "balance": "0x0", - "nonce": "0x1", - "code": "0xef" - }, - "0x18000000000000000000000000000000000000e2": { - "balance": "0x0", - "nonce": "0x1", - "code": "0xef" - }, - "0x18000000000000000000000000000000000000e3": { - "balance": "0x0", - "nonce": "0x1", - "code": "0xef" - }, - "0x18000000000000000000000000000000000000e4": { - "balance": "0x0", - "nonce": "0x1", - "code": "0xef" - }, - "0x18000000000000000000000000000000000000e5": { - "balance": "0x0", - "nonce": "0x1", - "code": "0xef" - }, - "0x18000000000000000000000000000000000000e6": { - "balance": "0x0", - "nonce": "0x1", - "code": "0xef" - }, - "0x18000000000000000000000000000000000000e7": { - "balance": "0x0", - "nonce": "0x1", - "code": "0xef" - }, - "0x18000000000000000000000000000000000000e8": { - "balance": "0x0", - "nonce": "0x1", - "code": "0xef" - }, - "0x18000000000000000000000000000000000000e9": { - "balance": "0x0", - "nonce": "0x1", - "code": "0xef" - }, - "0x18000000000000000000000000000000000000ea": { - "balance": "0x0", - "nonce": "0x1", - "code": "0xef" - }, - "0x18000000000000000000000000000000000000eb": { - "balance": "0x0", - "nonce": "0x1", - "code": "0xef" - }, - "0x18000000000000000000000000000000000000ec": { - "balance": "0x0", - "nonce": "0x1", - "code": "0xef" - }, - "0x18000000000000000000000000000000000000ed": { - "balance": "0x0", - "nonce": "0x1", - "code": "0xef" - }, - "0x18000000000000000000000000000000000000ee": { - "balance": "0x0", - "nonce": "0x1", - "code": "0xef" - }, - "0x18000000000000000000000000000000000000ef": { - "balance": "0x0", - "nonce": "0x1", - "code": "0xef" - }, - "0x18000000000000000000000000000000000000f0": { - "balance": "0x0", - "nonce": "0x1", - "code": "0xef" - }, - "0x18000000000000000000000000000000000000f1": { - "balance": "0x0", - "nonce": "0x1", - "code": "0xef" - }, - "0x18000000000000000000000000000000000000f2": { - "balance": "0x0", - "nonce": "0x1", - "code": "0xef" - }, - "0x18000000000000000000000000000000000000f3": { - "balance": "0x0", - "nonce": "0x1", - "code": "0xef" - }, - "0x18000000000000000000000000000000000000f4": { - "balance": "0x0", - "nonce": "0x1", - "code": "0xef" - }, - "0x18000000000000000000000000000000000000f5": { - "balance": "0x0", - "nonce": "0x1", - "code": "0xef" - }, - "0x18000000000000000000000000000000000000f6": { - "balance": "0x0", - "nonce": "0x1", - "code": "0xef" - }, - "0x18000000000000000000000000000000000000f7": { - "balance": "0x0", - "nonce": "0x1", - "code": "0xef" - }, - "0x18000000000000000000000000000000000000f8": { - "balance": "0x0", - "nonce": "0x1", - "code": "0xef" - }, - "0x18000000000000000000000000000000000000f9": { - "balance": "0x0", - "nonce": "0x1", - "code": "0xef" - }, - "0x18000000000000000000000000000000000000fa": { - "balance": "0x0", - "nonce": "0x1", - "code": "0xef" - }, - "0x18000000000000000000000000000000000000fb": { - "balance": "0x0", - "nonce": "0x1", - "code": "0xef" - }, - "0x18000000000000000000000000000000000000fc": { - "balance": "0x0", - "nonce": "0x1", - "code": "0xef" - }, - "0x18000000000000000000000000000000000000fd": { - "balance": "0x0", - "nonce": "0x1", - "code": "0xef" - }, - "0x18000000000000000000000000000000000000fe": { - "balance": "0x0", - "nonce": "0x1", - "code": "0xef" - }, - "0x18000000000000000000000000000000000000ff": { - "balance": "0x0", - "nonce": "0x1", - "code": "0xef" - }, - "0xf39Fd6e51aad88F6F4ce6aB8827279cffFb92266": { - "balance": "0xd3c21bcecceda1000000" - }, - "0x70997970C51812dc3A010C7d01b50e0d17dc79C8": { - "balance": "0xd3c21bcecceda1000000" - }, - "0x3C44CdDdB6a900fa2b585dd299e03d12FA4293BC": { - "balance": "0xd3c21bcecceda1000000" - }, - "0x90F79bf6EB2c4f870365E785982E1f101E93b906": { - "balance": "0xd3c21bcecceda1000000" - }, - "0x15d34AAf54267DB7D7c367839AAf71A00a2C6A65": { - "balance": "0xd3c21bcecceda1000000" - }, - "0x9965507D1a55bcC2695C58ba16FB37d819B0A4dc": { - "balance": "0xd3c21bcecceda1000000" - }, - "0x976EA74026E726554dB657fA54763abd0C3a0aa9": { - "balance": "0xd3c21bcecceda1000000" - }, - "0x14dC79964da2C08b23698B3D3cc7Ca32193d9955": { - "balance": "0xd3c21bcecceda1000000" - }, - "0x23618e81E3f5cdF7f54C3d65f7FBc0aBf5B21E8f": { - "balance": "0xd3c21bcecceda1000000" - }, - "0xa0Ee7A142d267C1f36714E4a8F75612F20a79720": { - "balance": "0xd3c21bcecceda1000000" - }, - "0x7E5F4552091A69125d5DfCb7b8C2659029395Bdf": { - "balance": "0xd3c21bcecceda1000000" - }, - "0x98e503f35D0a019cB0a251aD243a4cCFCF371F46": { - "balance": "0xd3c21bcecceda1000000" - }, - "0x8C161d85fDC086AC6726bCEDe39f2CCB1Afa3bc8": { - "balance": "0xd3c21bcecceda1000000" - }, - "0xa5d32fADC0D92feBc80CfE80bB6992A23549A516": { - "balance": "0xd3c21bcecceda1000000" - }, - "0xeed329483E4539a45Db863Df7b50436d50cC5276": { - "balance": "0xd3c21bcecceda1000000" - }, - "0xD308a07F97db36C338e8FE2AfB09267781d00811": { - "balance": "0xd3c21bcecceda1000000" - }, - "0x1800000000000000000000000000000000000000": { - "balance": "0x0", - "nonce": "0x1", - "code": "0xef", - "storage": { - "0x0000000000000000000000000000000000000000000000000000000000000002": "0x0000000000000000000000000000000000000000000d3c21bcecceda10000000" - } - }, - "0x1800000000000000000000000000000000000001": { - "balance": "0x0", - "nonce": "0x1", - "code": "0xef", - "storage": { - "0xc141df5e4f31a4ad000065d1c11b38075b88c74e6e761baa68cd227ec7b803c6": "0x0000000000000000000000000000000000000000000000000000000000000001" - } - } - } -} diff --git a/assets/localdev/reth.toml b/assets/localdev/reth.toml deleted file mode 100644 index 43dcca6..0000000 --- a/assets/localdev/reth.toml +++ /dev/null @@ -1,105 +0,0 @@ -[stages.era] - -[stages.headers] -downloader_max_concurrent_requests = 100 -downloader_min_concurrent_requests = 5 -downloader_max_buffered_responses = 100 -downloader_request_limit = 1000 -commit_threshold = 10000 - -[stages.bodies] -downloader_request_limit = 200 -downloader_stream_batch_size = 1000 -downloader_max_buffered_blocks_size_bytes = 2147483648 -downloader_min_concurrent_requests = 5 -downloader_max_concurrent_requests = 100 - -[stages.sender_recovery] -commit_threshold = 5000000 - -[stages.execution] -max_blocks = 500000 -max_changes = 5000000 -max_cumulative_gas = 1500000000000 -max_duration = "10m" - -[stages.prune] -commit_threshold = 1000000 - -[stages.account_hashing] -clean_threshold = 500000 -commit_threshold = 100000 - -[stages.storage_hashing] -clean_threshold = 500000 -commit_threshold = 100000 - -[stages.merkle] -incremental_threshold = 7000 -rebuild_threshold = 100000 - -[stages.transaction_lookup] -chunk_size = 5000000 - -[stages.index_account_history] -commit_threshold = 100000 - -[stages.index_storage_history] -commit_threshold = 100000 - -[stages.etl] -file_size = 524288000 - -[prune] -block_interval = 5 - -[prune.segments.receipts_log_filter] - -[peers] -refill_slots_interval = "5s" -trusted_nodes = [] -trusted_nodes_only = false -trusted_nodes_resolution_interval = "1h" -max_backoff_count = 5 -ban_duration = "12h" -incoming_ip_throttle_duration = "30s" - -[peers.connection_info] -max_outbound = 100 -max_inbound = 30 -max_concurrent_outbound_dials = 15 - -[peers.reputation_weights] -bad_message = -16384 -bad_block = -16384 -bad_transactions = -16384 -already_seen_transactions = 0 -timeout = -4096 -bad_protocol = -2147483648 -failed_to_connect = -25600 -dropped = -4096 -bad_announcement = -1024 - -[peers.backoff_durations] -low = "30s" -medium = "3m" -high = "15m" -max = "1h" - -[sessions] -session_command_buffer = 32 -session_event_buffer = 260 - -[sessions.limits] - -[sessions.initial_internal_request_timeout] -secs = 20 -nanos = 0 - -[sessions.protocol_breach_request_timeout] -secs = 120 -nanos = 0 - -[sessions.pending_session_timeout] -secs = 20 -nanos = 0 diff --git a/assets/mainnet/.gitignore b/assets/mainnet/.gitignore deleted file mode 100644 index 9abf911..0000000 --- a/assets/mainnet/.gitignore +++ /dev/null @@ -1,4 +0,0 @@ -validator*.json - -genesis-*.json -config-*.json diff --git a/assets/mainnet/config.json b/assets/mainnet/config.json deleted file mode 100644 index 28880af..0000000 --- a/assets/mainnet/config.json +++ /dev/null @@ -1,230 +0,0 @@ -{ - "timestamp": "1778544000", - "coinbase": "0x3141592653589793238462643383279502884197", - "NativeFiatToken": { - "proxy": { - "admin": "0x9005E53E3ee2f27999F15e7a52C58f804Fc716e0" - }, - "owner": "0xf5a5658b55983E2Aa037cAC7A8431B510E8A97F4", - "pauser": "0x0000000000000000000000000000000000000000", - "masterMinter": "0x0000000000000000000000000000000000000000", - "blacklister": "0x0000000000000000000000000000000000000000", - "rescuer": "0x0000000000000000000000000000000000000000", - "minters": [] - }, - "ProtocolConfig": { - "proxy": { - "admin": "0xBD3738ab866eff9B0908Ef8985d00eECA22DA4eF" - }, - "owner": "0xA5FeD552f38E11291Dd2Fc9cb91e2a5F6Ae86eD4", - "controller": "0x41fE044f1f71ff69F46F35f41EC93369a0E94733", - "pauser": "0x85c8825829fC649694D569bcCc374E8382Df2D12", - "feeParams": { - "alpha": "20", - "kRate": "200", - "inverseElasticityMultiplier": "5000", - "minBaseFee": "20000000000", - "maxBaseFee": "20000000000000", - "blockGasLimit": "30000000" - }, - "consensusParams": { - "timeoutProposeMs": "3000", - "timeoutProposeDeltaMs": "500", - "timeoutPrevoteMs": "1000", - "timeoutPrevoteDeltaMs": "500", - "timeoutPrecommitMs": "1000", - "timeoutPrecommitDeltaMs": "500", - "timeoutRebroadcastMs": "5000", - "targetBlockTimeMs": "500" - } - }, - "ValidatorManager": { - "proxy": { - "admin": "0x20Db45729BC366833107524804d16cEb44e946c6" - }, - "PermissionedValidatorManager": { - "proxy": { - "admin": "0x131E6B8E466aC8046c38c3ae7de77595CfEAf0D1" - }, - "owner": "0x20E61a9CC8d010928Aa9997e4e773e84dE1B8306", - "pauser": "0x5bC1d44F8e844863cbC45D2f425F4c3758faf7b8", - "validatorRegisterers": [ - "0xF65A7d2E6C16d263B7B263369deD8C78f3aa4813", - "0x38c57d852eddE831CE368D127CDabf5AdB86Ce96" - ] - }, - "validators": [ - { - "publicKey": "0x12a68aa84643efd6fb79b4097ef9b5ae1bef849c9c75e8eba2b977e09d227c35", - "votingPower": "2000", - "controllers": [ - { - "address": "0x5f86a4dCBD49c86Becc4FB89680f66FeC8eF358a", - "votingPowerLimit": "2000" - }, - { - "address": "0x6F80738F31019B70B616Ac9Fac1C853dFC35Cc37", - "votingPowerLimit": "0" - } - ] - }, - { - "publicKey": "0xae89fa207bb808e8a34e05b5892a16625e8b66b92e597f8866aa586b487dfbfe", - "votingPower": "2000", - "controllers": [ - { - "address": "0xb0100454798fD2a11d7e7Bde0804a29171d2F492", - "votingPowerLimit": "2000" - }, - { - "address": "0xA53dd220650550BC68dFC63faAF954b85eAfED7c", - "votingPowerLimit": "0" - } - ] - }, - { - "publicKey": "0xd5c96c5d0daf70f25fb5bfc20c0747b2ce0408cc6e54b8494f3a62b61ffca7cd", - "votingPower": "2000", - "controllers": [ - { - "address": "0x64637C1f9899684722da4657d94396ec4Fd85E09", - "votingPowerLimit": "2000" - }, - { - "address": "0x882b0d0E0952170a7d3D62946327fF3F6d7AD329", - "votingPowerLimit": "0" - } - ] - }, - { - "publicKey": "0xed259c1abb397823afb31cb36089547367221242de3057df803a0b091e9a1c8d", - "votingPower": "2000", - "controllers": [ - { - "address": "0x5c2220b253c4E02Eec51A3959b9f67E599E8ECF9", - "votingPowerLimit": "2000" - }, - { - "address": "0x386976F806739bDA4126a18eEF4157F92Fb53Cce", - "votingPowerLimit": "0" - } - ] - }, - { - "publicKey": "0xf43562191fe65ba250ac32d4602efb664586bf4e8150e4c80cf2dbf64650d2a9", - "votingPower": "2000", - "controllers": [ - { - "address": "0xB0556E9AE15cA9e02fFD66C1651134bf343ad2B2", - "votingPowerLimit": "2000" - }, - { - "address": "0x5e887aFa6E659995A546e72B9959594D8DAEDD71", - "votingPowerLimit": "0" - } - ] - }, - { - "publicKey": "0xf927d6659307a219c3a59b51a31c29412b6d2b1c1717697a0ae0d5c9999ba740", - "votingPower": "2000", - "controllers": [ - { - "address": "0x5802620312d16b94E70147AAf730c662Ceee821C", - "votingPowerLimit": "2000" - }, - { - "address": "0xBFB35262B8dF18829D33Da3A220DdE13505Cf485", - "votingPowerLimit": "0" - } - ] - }, - { - "publicKey": "0xda4f31b2d26acac4143d67f6b0ebc5a7b9fa829e4b63970f28130bff361316c9", - "votingPower": "2000", - "controllers": [ - { - "address": "0x9c4a7E4964d0b14c3B436b522F0BFA33DD517350", - "votingPowerLimit": "2000" - }, - { - "address": "0xF75431B62AECD7da6AF24b6c0dFEd419579760c8", - "votingPowerLimit": "0" - } - ] - }, - { - "publicKey": "0x7bd0ca04c9423d39c25ee7747d2a5977acf19ff9d7f835f8dc1a746c83e71070", - "votingPower": "2000", - "controllers": [ - { - "address": "0x22C1Ba671ad7c01d00Afb8E159424A8568B6b3a7", - "votingPowerLimit": "2000" - }, - { - "address": "0x4e5bF0aBC4EFA3c94Bc1ed23b1dB4b40De43AafA", - "votingPowerLimit": "0" - } - ] - }, - { - "publicKey": "0xc3fbb251a140db946283233a3fe5762a05a8305e2a8eaefc41d5df2820e071fc", - "votingPower": "2000", - "controllers": [ - { - "address": "0xD28Ae083CFEefd89cb2b0f6466351d4C5630Ac5F", - "votingPowerLimit": "2000" - }, - { - "address": "0x034BB1f6239a70f956d2b18054CF7Bf7Bbe77Cc4", - "votingPowerLimit": "0" - } - ] - }, - { - "publicKey": "0xdeb9620fb017834b90e4b0cf66fdc7fd06b3edb68f0147938e59a9fa7db83380", - "votingPower": "2000", - "controllers": [ - { - "address": "0x5dD30B6C339ec59eB7fBeD6026C43978e5663FbE", - "votingPowerLimit": "2000" - }, - { - "address": "0x7dCCcD3f36dDc820265F6e5758044EA3d4739C80", - "votingPowerLimit": "0" - } - ] - }, - { - "publicKey": "0x254192a339b277911fae7dac3a52ed4b0196bb90670fb3b893f521849086fc8d", - "votingPower": "2000", - "controllers": [ - { - "address": "0x4e8FC724848B3F119E031C1e17c5e68Af75D29C2", - "votingPowerLimit": "2000" - }, - { - "address": "0x5519855988085f55Ac7aCcD7aED1C53988D9aF18", - "votingPowerLimit": "0" - } - ] - } - ] - }, - "Denylist": { - "proxy": { - "address": "0x3600000000000000000000000000000000000004", - "admin": "0xAfb5b6a4725459959ef931a3e5df758a72A8cA7f" - }, - "owner": "0x49ec36db19623e4DaDc1Aa821CbA2D1476F8E859", - "denylisters": [] - }, - "prefund": [ - { - "address": "0x50A2b0B577eC24d7ce1aeD372A8a6fd14CE1bE57", - "balance": "10000000000000000000000" - } - ], - "hardforks": { - "osakaTime": 0 - } -} diff --git a/assets/mainnet/genesis.config.ts b/assets/mainnet/genesis.config.ts deleted file mode 100644 index 393fdc9..0000000 --- a/assets/mainnet/genesis.config.ts +++ /dev/null @@ -1,177 +0,0 @@ -// Copyright 2026 Circle Internet Group, Inc. All rights reserved. -// -// SPDX-License-Identifier: Apache-2.0 -// -// Licensed under the Apache License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. -// You may obtain a copy of the License at -// -// http://www.apache.org/licenses/LICENSE-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// See the License for the specific language governing permissions and -// limitations under the License. - -import fs from 'fs' -import path from 'path' -import { Address, Hex, parseEther, parseGwei, zeroAddress } from 'viem' -import { createBuilderContext, buildGenesis, GenesisConfig } from '../../scripts/genesis' -import { bigintReplacer } from '../../scripts/genesis/types' - -type Validator = { - publicKey: Hex - activateController: Address - removeController: Address -} - -const initialVotingPower = 2000n - -const build = async () => { - const ctx = await createBuilderContext({ network: 'mainnet', chainId: 5042 }) - - // The sequences of validator registration must follow the order of the following array due to registrationId assignment. - const validators: Validator[] = [ - { - publicKey: '0x12a68aa84643efd6fb79b4097ef9b5ae1bef849c9c75e8eba2b977e09d227c35', - activateController: '0x5f86a4dCBD49c86Becc4FB89680f66FeC8eF358a', - removeController: '0x6F80738F31019B70B616Ac9Fac1C853dFC35Cc37', - }, - { - publicKey: '0xae89fa207bb808e8a34e05b5892a16625e8b66b92e597f8866aa586b487dfbfe', - activateController: '0xb0100454798fD2a11d7e7Bde0804a29171d2F492', - removeController: '0xA53dd220650550BC68dFC63faAF954b85eAfED7c', - }, - { - publicKey: '0xd5c96c5d0daf70f25fb5bfc20c0747b2ce0408cc6e54b8494f3a62b61ffca7cd', - activateController: '0x64637C1f9899684722da4657d94396ec4Fd85E09', - removeController: '0x882b0d0E0952170a7d3D62946327fF3F6d7AD329', - }, - { - publicKey: '0xed259c1abb397823afb31cb36089547367221242de3057df803a0b091e9a1c8d', - activateController: '0x5c2220b253c4E02Eec51A3959b9f67E599E8ECF9', - removeController: '0x386976F806739bDA4126a18eEF4157F92Fb53Cce', - }, - { - publicKey: '0xf43562191fe65ba250ac32d4602efb664586bf4e8150e4c80cf2dbf64650d2a9', - activateController: '0xB0556E9AE15cA9e02fFD66C1651134bf343ad2B2', - removeController: '0x5e887aFa6E659995A546e72B9959594D8DAEDD71', - }, - { - publicKey: '0xf927d6659307a219c3a59b51a31c29412b6d2b1c1717697a0ae0d5c9999ba740', - activateController: '0x5802620312d16b94E70147AAf730c662Ceee821C', - removeController: '0xBFB35262B8dF18829D33Da3A220DdE13505Cf485', - }, - { - publicKey: '0xda4f31b2d26acac4143d67f6b0ebc5a7b9fa829e4b63970f28130bff361316c9', - activateController: '0x9c4a7E4964d0b14c3B436b522F0BFA33DD517350', - removeController: '0xF75431B62AECD7da6AF24b6c0dFEd419579760c8', - }, - { - publicKey: '0x7bd0ca04c9423d39c25ee7747d2a5977acf19ff9d7f835f8dc1a746c83e71070', - activateController: '0x22C1Ba671ad7c01d00Afb8E159424A8568B6b3a7', - removeController: '0x4e5bF0aBC4EFA3c94Bc1ed23b1dB4b40De43AafA', - }, - { - publicKey: '0xc3fbb251a140db946283233a3fe5762a05a8305e2a8eaefc41d5df2820e071fc', - activateController: '0xD28Ae083CFEefd89cb2b0f6466351d4C5630Ac5F', - removeController: '0x034BB1f6239a70f956d2b18054CF7Bf7Bbe77Cc4', - }, - { - publicKey: '0xdeb9620fb017834b90e4b0cf66fdc7fd06b3edb68f0147938e59a9fa7db83380', - activateController: '0x5dD30B6C339ec59eB7fBeD6026C43978e5663FbE', - removeController: '0x7dCCcD3f36dDc820265F6e5758044EA3d4739C80', - }, - { - publicKey: '0x254192a339b277911fae7dac3a52ed4b0196bb90670fb3b893f521849086fc8d', - activateController: '0x4e8FC724848B3F119E031C1e17c5e68Af75D29C2', - removeController: '0x5519855988085f55Ac7aCcD7aED1C53988D9aF18', - }, - ] - - const config: GenesisConfig = { - timestamp: BigInt(Math.floor(new Date('2026-05-12T00:00:00-00:00').getTime() / 1000)), - - coinbase: '0x3141592653589793238462643383279502884197', - - NativeFiatToken: { - proxy: { admin: '0x9005E53E3ee2f27999F15e7a52C58f804Fc716e0' }, - owner: '0xf5a5658b55983E2Aa037cAC7A8431B510E8A97F4', - pauser: zeroAddress, - masterMinter: zeroAddress, - blacklister: zeroAddress, - rescuer: zeroAddress, - minters: [], - }, - - ProtocolConfig: { - proxy: { admin: '0xBD3738ab866eff9B0908Ef8985d00eECA22DA4eF' }, - owner: '0xA5FeD552f38E11291Dd2Fc9cb91e2a5F6Ae86eD4', - controller: '0x41fE044f1f71ff69F46F35f41EC93369a0E94733', - pauser: '0x85c8825829fC649694D569bcCc374E8382Df2D12', - feeParams: { - alpha: 20n, - kRate: 200n, - inverseElasticityMultiplier: 5000n, - minBaseFee: parseGwei('20'), - maxBaseFee: parseGwei('20000'), - blockGasLimit: 30_000_000n, - }, - consensusParams: { - timeoutProposeMs: 3000n, - timeoutProposeDeltaMs: 500n, - timeoutPrevoteMs: 1000n, - timeoutPrevoteDeltaMs: 500n, - timeoutPrecommitMs: 1000n, - timeoutPrecommitDeltaMs: 500n, - timeoutRebroadcastMs: 5000n, - targetBlockTimeMs: 500n, - }, - }, - - ValidatorManager: { - proxy: { admin: '0x20Db45729BC366833107524804d16cEb44e946c6' }, - PermissionedValidatorManager: { - proxy: { admin: '0x131E6B8E466aC8046c38c3ae7de77595CfEAf0D1' }, - owner: '0x20E61a9CC8d010928Aa9997e4e773e84dE1B8306', - pauser: '0x5bC1d44F8e844863cbC45D2f425F4c3758faf7b8', - validatorRegisterers: [ - '0xF65A7d2E6C16d263B7B263369deD8C78f3aa4813', - '0x38c57d852eddE831CE368D127CDabf5AdB86Ce96', - ], - }, - // Each validator gets a pair of controllers: activate - // (limit = initial voting power, can raise voting power) and remove - // (limit = 0, can only zero it out). - validators: validators.map((v) => ({ - publicKey: v.publicKey, - votingPower: initialVotingPower, - controllers: [ - { address: v.activateController, votingPowerLimit: initialVotingPower }, - { address: v.removeController, votingPowerLimit: 0n }, - ], - })), - }, - - Denylist: { - proxy: { - address: '0x3600000000000000000000000000000000000004', - admin: '0xAfb5b6a4725459959ef931a3e5df758a72A8cA7f', - }, - owner: '0x49ec36db19623e4DaDc1Aa821CbA2D1476F8E859', - denylisters: [], - }, - - prefund: [{ address: '0x50A2b0B577eC24d7ce1aeD372A8a6fd14CE1bE57', balance: parseEther('10000') }], - hardforks: { osakaTime: 0 }, - } - - fs.writeFileSync( - path.join(ctx.projectRoot, `assets/${ctx.network}/config.json`), - JSON.stringify(config, bigintReplacer, 2) + '\n', - ) - return await buildGenesis(ctx, config) -} - -export default build diff --git a/assets/mainnet/genesis.json b/assets/mainnet/genesis.json deleted file mode 100644 index 1799184..0000000 --- a/assets/mainnet/genesis.json +++ /dev/null @@ -1,1583 +0,0 @@ -{ - "config": { - "chainId": 5042, - "daoForkSupport": false, - "terminalTotalDifficulty": "0x0", - "terminalTotalDifficultyPassed": true, - "homesteadBlock": 0, - "eip150Block": 0, - "eip150Hash": "0x0000000000000000000000000000000000000000000000000000000000000000", - "eip155Block": 0, - "eip158Block": 0, - "byzantiumBlock": 0, - "constantinopleBlock": 0, - "petersburgBlock": 0, - "istanbulBlock": 0, - "muirGlacierBlock": 0, - "berlinBlock": 0, - "londonBlock": 0, - "arrowGlacierBlock": 0, - "grayGlacierBlock": 0, - "shanghaiTime": 0, - "cancunTime": 0, - "pragueTime": 0, - "osakaTime": 0 - }, - "nonce": "0x0", - "timestamp": "0x6a026d80", - "extraData": "0x", - "gasLimit": "0x1c9c380", - "difficulty": "0x0", - "mixHash": "0x0000000000000000000000000000000000000000000000000000000000000000", - "coinbase": "0x3141592653589793238462643383279502884197", - "number": "0x0", - "alloc": { - "0x3600000000000000000000000000000000000000": { - "balance": "0x0", - "nonce": "0x1", - "code": "0x60806040526004361061005a5760003560e01c80635c60da1b116100435780635c60da1b146101315780638f2839701461016f578063f851a440146101af5761005a565b80633659cfe6146100645780634f1ef286146100a4575b6100626101c4565b005b34801561007057600080fd5b506100626004803603602081101561008757600080fd5b503573ffffffffffffffffffffffffffffffffffffffff166101de565b610062600480360360408110156100ba57600080fd5b73ffffffffffffffffffffffffffffffffffffffff82351691908101906040810160208201356401000000008111156100f257600080fd5b82018360208201111561010457600080fd5b8035906020019184600183028401116401000000008311171561012657600080fd5b509092509050610232565b34801561013d57600080fd5b50610146610309565b6040805173ffffffffffffffffffffffffffffffffffffffff9092168252519081900360200190f35b34801561017b57600080fd5b506100626004803603602081101561019257600080fd5b503573ffffffffffffffffffffffffffffffffffffffff16610318565b3480156101bb57600080fd5b50610146610420565b6101cc610430565b6101dc6101d76104c4565b6104e9565b565b6101e661050d565b73ffffffffffffffffffffffffffffffffffffffff163373ffffffffffffffffffffffffffffffffffffffff1614156102275761022281610532565b61022f565b61022f6101c4565b50565b61023a61050d565b73ffffffffffffffffffffffffffffffffffffffff163373ffffffffffffffffffffffffffffffffffffffff1614156102fc5761027683610532565b60003073ffffffffffffffffffffffffffffffffffffffff16348484604051808383808284376040519201945060009350909150508083038185875af1925050503d80600081146102e3576040519150601f19603f3d011682016040523d82523d6000602084013e6102e8565b606091505b50509050806102f657600080fd5b50610304565b6103046101c4565b505050565b60006103136104c4565b905090565b61032061050d565b73ffffffffffffffffffffffffffffffffffffffff163373ffffffffffffffffffffffffffffffffffffffff1614156102275773ffffffffffffffffffffffffffffffffffffffff81166103bf576040517f08c379a00000000000000000000000000000000000000000000000000000000081526004018080602001828103825260368152602001806106606036913960400191505060405180910390fd5b7f7e644d79422f17c01e4894b5f4f588d331ebfa28653d42ae832dc59e38c9798f6103e861050d565b6040805173ffffffffffffffffffffffffffffffffffffffff928316815291841660208301528051918290030190a161022281610587565b600061031361050d565b3b151590565b61043861050d565b73ffffffffffffffffffffffffffffffffffffffff163373ffffffffffffffffffffffffffffffffffffffff1614156104bc576040517f08c379a000000000000000000000000000000000000000000000000000000000815260040180806020018281038252603281526020018061062e6032913960400191505060405180910390fd5b6101dc6101dc565b7f7050c9e0f4ca769c69bd3a8ef740bc37934f8e2c036e5a723fd8ee048ed3f8c35490565b3660008037600080366000845af43d6000803e808015610508573d6000f35b3d6000fd5b7f10d6a54a4754c8869d6886b5f5d7fbfa5b4522237ea5c60d11bc4e7a1ff9390b5490565b61053b816105ab565b6040805173ffffffffffffffffffffffffffffffffffffffff8316815290517fbc7cd75a20ee27fd9adebab32041f755214dbc6bffa90cc0225b39da2e5c2d3b9181900360200190a150565b7f10d6a54a4754c8869d6886b5f5d7fbfa5b4522237ea5c60d11bc4e7a1ff9390b55565b6105b48161042a565b610609576040517f08c379a000000000000000000000000000000000000000000000000000000000815260040180806020018281038252603b815260200180610696603b913960400191505060405180910390fd5b7f7050c9e0f4ca769c69bd3a8ef740bc37934f8e2c036e5a723fd8ee048ed3f8c35556fe43616e6e6f742063616c6c2066616c6c6261636b2066756e6374696f6e2066726f6d207468652070726f78792061646d696e43616e6e6f74206368616e6765207468652061646d696e206f6620612070726f787920746f20746865207a65726f206164647265737343616e6e6f742073657420612070726f787920696d706c656d656e746174696f6e20746f2061206e6f6e2d636f6e74726163742061646472657373a2646970667358221220015908007e367e7f333ec38084b88f027c06160d2c19e5bdd8027e8d06acf8bf64736f6c634300060c0033", - "storage": { - "0x10d6a54a4754c8869d6886b5f5d7fbfa5b4522237ea5c60d11bc4e7a1ff9390b": "0x0000000000000000000000009005e53e3ee2f27999f15e7a52c58f804fc716e0", - "0x7050c9e0f4ca769c69bd3a8ef740bc37934f8e2c036e5a723fd8ee048ed3f8c3": "0x000000000000000000000000c6ad664ac6679f4ce74e10e91449c93ec1ae3ca6", - "0x0000000000000000000000000000000000000000000000000000000000000000": "0x000000000000000000000000f5a5658b55983e2aa037cac7a8431b510e8a97f4", - "0x0000000000000000000000000000000000000000000000000000000000000001": "0x0000000000000000000000000000000000000000000000000000000000000000", - "0x0000000000000000000000000000000000000000000000000000000000000002": "0x0000000000000000000000000000000000000000000000000000000000000000", - "0x0000000000000000000000000000000000000000000000000000000000000004": "0x5553444300000000000000000000000000000000000000000000000000000008", - "0x0000000000000000000000000000000000000000000000000000000000000005": "0x5553444300000000000000000000000000000000000000000000000000000008", - "0x0000000000000000000000000000000000000000000000000000000000000006": "0x0000000000000000000000000000000000000000000000000000000000000006", - "0x0000000000000000000000000000000000000000000000000000000000000007": "0x5553440000000000000000000000000000000000000000000000000000000006", - "0x0000000000000000000000000000000000000000000000000000000000000008": "0x0000000000000000000000010000000000000000000000000000000000000000", - "0x000000000000000000000000000000000000000000000000000000000000000e": "0x0000000000000000000000000000000000000000000000000000000000000000", - "0x0000000000000000000000000000000000000000000000000000000000000012": "0x0000000000000000000000000000000000000000000000000000000000000003" - } - }, - "0xC6AD664ac6679F4Ce74e10E91449C93Ec1ae3cA6": { - "balance": "0x0", - "nonce": "0x1", - "code": "0x608060405234801561001057600080fd5b506004361061038e5760003560e01c806388b7ab63116101de578063bd1024301161010f578063dd62ed3e116100ad578063ef55bec61161007c578063ef55bec61461115b578063f2fde38b146111c7578063f9f92be4146111fa578063fe575a871461122d5761038e565b8063dd62ed3e14611073578063e3ee160e146110ae578063e5a6b10f1461111a578063e94a0102146111225761038e565b8063d505accf116100e9578063d505accf14610f95578063d608ea6414610ff3578063d916948714611063578063dd0743b31461106b5761038e565b8063bd10243014610ea1578063ccd92d3e14610ea9578063cf09299514610eb15761038e565b8063a457c2d71161017c578063aa271e1a11610156578063aa271e1a14610d30578063ad38bf2214610d63578063b2118a8d14610d96578063b7b7289914610dd95761038e565b8063a457c2d714610c8b578063a9059cbb14610cc4578063aa20e1e414610cfd5761038e565b806395d89b41116101b857806395d89b4114610b9b5780639fd0506d14610ba35780639fd5a6cf14610bab578063a0cc6a6814610c835761038e565b806388b7ab6314610a7c5780638a6db9c314610b605780638da5cb5b14610b935761038e565b806339509351116102c3578063554bab3c116102615780637ecebe00116102305780637ecebe0014610a315780637f2eecc314610a645780637fd0991614610a6c5780638456cb5914610a745761038e565b8063554bab3c146109755780635a049a70146109a85780635c975abb146109f657806370a08231146109fe5761038e565b806342966c681161029d57806342966c6814610855578063430239b4146108725780634e44d9561461093457806354fd4d501461096d5761038e565b806339509351146107db5780633f4ba83a1461081457806340c10f191461081c5761038e565b80633092afd5116103305780633357162b1161030a5780633357162b146105ae57806335d99f351461079a5780633644e515146107cb57806338a63183146107d35761038e565b80633092afd51461055557806330adf81f14610588578063313ce567146105905761038e565b80631a8952661161036c5780631a8952661461047757806323b872dd146104ac5780632ab60045146104ef5780632fc81e09146105225761038e565b806306fdde0314610393578063095ea7b31461041057806318160ddd1461045d575b600080fd5b61039b611260565b6040805160208082528351818301528351919283929083019185019080838360005b838110156103d55781810151838201526020016103bd565b50505050905090810190601f1680156104025780820380516001836020036101000a031916815260200191505b509250505060405180910390f35b6104496004803603604081101561042657600080fd5b5073ffffffffffffffffffffffffffffffffffffffff813516906020013561130c565b604080519115158252519081900360200190f35b6104656113ae565b60408051918252519081900360200190f35b6104aa6004803603602081101561048d57600080fd5b503573ffffffffffffffffffffffffffffffffffffffff1661144e565b005b610449600480360360608110156104c257600080fd5b5073ffffffffffffffffffffffffffffffffffffffff81358116916020810135909116906040013561150b565b6104aa6004803603602081101561050557600080fd5b503573ffffffffffffffffffffffffffffffffffffffff16611703565b6104aa6004803603602081101561053857600080fd5b503573ffffffffffffffffffffffffffffffffffffffff16611864565b6104496004803603602081101561056b57600080fd5b503573ffffffffffffffffffffffffffffffffffffffff166118cc565b6104656119c5565b6105986119e9565b6040805160ff9092168252519081900360200190f35b6104aa60048036036101008110156105c557600080fd5b8101906020810181356401000000008111156105e057600080fd5b8201836020820111156105f257600080fd5b8035906020019184600183028401116401000000008311171561061457600080fd5b91908080601f016020809104026020016040519081016040528093929190818152602001838380828437600092019190915250929594936020810193503591505064010000000081111561066757600080fd5b82018360208201111561067957600080fd5b8035906020019184600183028401116401000000008311171561069b57600080fd5b91908080601f01602080910402602001604051908101604052809392919081815260200183838082843760009201919091525092959493602081019350359150506401000000008111156106ee57600080fd5b82018360208201111561070057600080fd5b8035906020019184600183028401116401000000008311171561072257600080fd5b91908080601f0160208091040260200160405190810160405280939291908181526020018383808284376000920191909152509295505050813560ff16925050602081013573ffffffffffffffffffffffffffffffffffffffff908116916040810135821691606082013581169160800135166119f2565b6107a2611d34565b6040805173ffffffffffffffffffffffffffffffffffffffff9092168252519081900360200190f35b610465611d50565b6107a2611d5f565b610449600480360360408110156107f157600080fd5b5073ffffffffffffffffffffffffffffffffffffffff8135169060200135611d7b565b6104aa611e13565b6104496004803603604081101561083257600080fd5b5073ffffffffffffffffffffffffffffffffffffffff8135169060200135611ed6565b6104aa6004803603602081101561086b57600080fd5b5035612324565b6104aa6004803603604081101561088857600080fd5b8101906020810181356401000000008111156108a357600080fd5b8201836020820111156108b557600080fd5b803590602001918460208302840111640100000000831117156108d757600080fd5b9193909290916020810190356401000000008111156108f557600080fd5b82018360208201111561090757600080fd5b8035906020019184600183028401116401000000008311171561092957600080fd5b509092509050612658565b6104496004803603604081101561094a57600080fd5b5073ffffffffffffffffffffffffffffffffffffffff813516906020013561280f565b61039b6129a2565b6104aa6004803603602081101561098b57600080fd5b503573ffffffffffffffffffffffffffffffffffffffff166129d9565b6104aa600480360360a08110156109be57600080fd5b5073ffffffffffffffffffffffffffffffffffffffff8135169060208101359060ff6040820135169060608101359060800135612b40565b610449612bde565b61046560048036036020811015610a1457600080fd5b503573ffffffffffffffffffffffffffffffffffffffff16612bff565b61046560048036036020811015610a4757600080fd5b503573ffffffffffffffffffffffffffffffffffffffff16612c0a565b610465612c32565b6107a2612c56565b6104aa612c6e565b6104aa600480360360e0811015610a9257600080fd5b73ffffffffffffffffffffffffffffffffffffffff823581169260208101359091169160408201359160608101359160808201359160a08101359181019060e0810160c0820135640100000000811115610aeb57600080fd5b820183602082011115610afd57600080fd5b80359060200191846001830284011164010000000083111715610b1f57600080fd5b91908080601f016020809104026020016040519081016040528093929190818152602001838380828437600092019190915250929550612d48945050505050565b61046560048036036020811015610b7657600080fd5b503573ffffffffffffffffffffffffffffffffffffffff16612dea565b6107a2612e12565b61039b612e2e565b6107a2612ea7565b6104aa600480360360a0811015610bc157600080fd5b73ffffffffffffffffffffffffffffffffffffffff823581169260208101359091169160408201359160608101359181019060a081016080820135640100000000811115610c0e57600080fd5b820183602082011115610c2057600080fd5b80359060200191846001830284011164010000000083111715610c4257600080fd5b91908080601f016020809104026020016040519081016040528093929190818152602001838380828437600092019190915250929550612ec3945050505050565b610465612f5a565b61044960048036036040811015610ca157600080fd5b5073ffffffffffffffffffffffffffffffffffffffff8135169060200135612f7e565b61044960048036036040811015610cda57600080fd5b5073ffffffffffffffffffffffffffffffffffffffff8135169060200135613016565b6104aa60048036036020811015610d1357600080fd5b503573ffffffffffffffffffffffffffffffffffffffff166130ae565b61044960048036036020811015610d4657600080fd5b503573ffffffffffffffffffffffffffffffffffffffff16613215565b6104aa60048036036020811015610d7957600080fd5b503573ffffffffffffffffffffffffffffffffffffffff16613240565b6104aa60048036036060811015610dac57600080fd5b5073ffffffffffffffffffffffffffffffffffffffff8135811691602081013590911690604001356133a7565b6104aa60048036036060811015610def57600080fd5b73ffffffffffffffffffffffffffffffffffffffff82351691602081013591810190606081016040820135640100000000811115610e2c57600080fd5b820183602082011115610e3e57600080fd5b80359060200191846001830284011164010000000083111715610e6057600080fd5b91908080601f01602080910402602001604051908101604052809392919081815260200183838082843760009201919091525092955061343d945050505050565b6107a26134d2565b6104656134ee565b6104aa600480360360e0811015610ec757600080fd5b73ffffffffffffffffffffffffffffffffffffffff823581169260208101359091169160408201359160608101359160808201359160a08101359181019060e0810160c0820135640100000000811115610f2057600080fd5b820183602082011115610f3257600080fd5b80359060200191846001830284011164010000000083111715610f5457600080fd5b91908080601f0160208091040260200160405190810160405280939291908181526020018383808284376000920191909152509295506134f7945050505050565b6104aa600480360360e0811015610fab57600080fd5b5073ffffffffffffffffffffffffffffffffffffffff813581169160208101359091169060408101359060608101359060ff6080820135169060a08101359060c00135613590565b6104aa6004803603602081101561100957600080fd5b81019060208101813564010000000081111561102457600080fd5b82018360208201111561103657600080fd5b8035906020019184600183028401116401000000008311171561105857600080fd5b509092509050613629565b610465613712565b6107a2613736565b6104656004803603604081101561108957600080fd5b5073ffffffffffffffffffffffffffffffffffffffff8135811691602001351661374e565b6104aa60048036036101208110156110c557600080fd5b5073ffffffffffffffffffffffffffffffffffffffff813581169160208101359091169060408101359060608101359060808101359060a08101359060ff60c0820135169060e0810135906101000135613786565b61039b61382c565b6104496004803603604081101561113857600080fd5b5073ffffffffffffffffffffffffffffffffffffffff81351690602001356138a5565b6104aa600480360361012081101561117257600080fd5b5073ffffffffffffffffffffffffffffffffffffffff813581169160208101359091169060408101359060608101359060808101359060a08101359060ff60c0820135169060e08101359061010001356138dd565b6104aa600480360360208110156111dd57600080fd5b503573ffffffffffffffffffffffffffffffffffffffff16613978565b6104aa6004803603602081101561121057600080fd5b503573ffffffffffffffffffffffffffffffffffffffff16613acb565b6104496004803603602081101561124357600080fd5b503573ffffffffffffffffffffffffffffffffffffffff16613b88565b6004805460408051602060026001851615610100027fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff0190941693909304601f810184900484028201840190925281815292918301828280156113045780601f106112d957610100808354040283529160200191611304565b820191906000526020600020905b8154815290600101906020018083116112e757829003601f168201915b505050505081565b60015460009074010000000000000000000000000000000000000000900460ff161561139957604080517f08c379a000000000000000000000000000000000000000000000000000000000815260206004820152601060248201527f5061757361626c653a2070617573656400000000000000000000000000000000604482015290519081900360640190fd5b6113a4338484613b93565b5060015b92915050565b60008073180000000000000000000000000000000000000073ffffffffffffffffffffffffffffffffffffffff166318160ddd6040518163ffffffff1660e01b815260040160206040518083038186803b15801561140b57600080fd5b505afa15801561141f573d6000803e3d6000fd5b505050506040513d602081101561143557600080fd5b505190506114488164e8d4a51000613cda565b91505090565b60025473ffffffffffffffffffffffffffffffffffffffff1633146114be576040517f08c379a000000000000000000000000000000000000000000000000000000000815260040180806020018281038252602c8152602001806158c5602c913960400191505060405180910390fd5b6114c781613ced565b60405173ffffffffffffffffffffffffffffffffffffffff8216907f117e3210bb9aa7d9baff172026820255c6f6c30ba8999d1c2fd88e2848137c4e90600090a250565b60015460009074010000000000000000000000000000000000000000900460ff161561159857604080517f08c379a000000000000000000000000000000000000000000000000000000000815260206004820152601060248201527f5061757361626c653a2070617573656400000000000000000000000000000000604482015290519081900360640190fd5b336115a281613cf8565b156115f8576040517f08c379a0000000000000000000000000000000000000000000000000000000008152600401808060200182810382526025815260200180615bc76025913960400191505060405180910390fd5b73ffffffffffffffffffffffffffffffffffffffff85166000908152600a60209081526040808320338452909152902054831115611681576040517f08c379a00000000000000000000000000000000000000000000000000000000081526004018080602001828103825260288152602001806159ac6028913960400191505060405180910390fd5b73ffffffffffffffffffffffffffffffffffffffff85166000908152600a602090815260408083203384529091529020546116bc9084613da7565b73ffffffffffffffffffffffffffffffffffffffff86166000908152600a602090815260408083203384529091529020556116f8858585613e1e565b506001949350505050565b60005473ffffffffffffffffffffffffffffffffffffffff16331461178957604080517f08c379a000000000000000000000000000000000000000000000000000000000815260206004820181905260248201527f4f776e61626c653a2063616c6c6572206973206e6f7420746865206f776e6572604482015290519081900360640190fd5b73ffffffffffffffffffffffffffffffffffffffff81166117f5576040517f08c379a000000000000000000000000000000000000000000000000000000000815260040180806020018281038252602a815260200180615823602a913960400191505060405180910390fd5b600e80547fffffffffffffffffffffffff00000000000000000000000000000000000000001673ffffffffffffffffffffffffffffffffffffffff83169081179091556040517fe475e580d85111348e40d8ca33cfdd74c30fe1655c2d8537a13abc10065ffa5a90600090a250565b60125460ff1660011461187657600080fd5b60006118813061410c565b9050801561189457611894308383613e1e565b61189d30614135565b5050601280547fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff00166002179055565b60085460009073ffffffffffffffffffffffffffffffffffffffff16331461193f576040517f08c379a000000000000000000000000000000000000000000000000000000000815260040180806020018281038252602981526020018061589c6029913960400191505060405180910390fd5b73ffffffffffffffffffffffffffffffffffffffff82166000818152600c6020908152604080832080547fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff00169055600d909152808220829055517fe94479a9f7e1952cc78f2d6baab678adc1b772d936c6583def489e524cb666929190a2506001919050565b7f6e71edae12b1b97f4d1f60370fef10105fa2faae0126114a169c64845d6126c981565b60065460ff1681565b60085474010000000000000000000000000000000000000000900460ff1615611a66576040517f08c379a000000000000000000000000000000000000000000000000000000000815260040180806020018281038252602a815260200180615a27602a913960400191505060405180910390fd5b73ffffffffffffffffffffffffffffffffffffffff8416611ad2576040517f08c379a000000000000000000000000000000000000000000000000000000000815260040180806020018281038252602f815260200180615938602f913960400191505060405180910390fd5b73ffffffffffffffffffffffffffffffffffffffff8316611b3e576040517f08c379a00000000000000000000000000000000000000000000000000000000081526004018080602001828103825260298152602001806157fa6029913960400191505060405180910390fd5b73ffffffffffffffffffffffffffffffffffffffff8216611baa576040517f08c379a000000000000000000000000000000000000000000000000000000000815260040180806020018281038252602e8152602001806159d4602e913960400191505060405180910390fd5b73ffffffffffffffffffffffffffffffffffffffff8116611c16576040517f08c379a0000000000000000000000000000000000000000000000000000000008152600401808060200182810382526028815260200180615b3f6028913960400191505060405180910390fd5b8751611c299060049060208b0190615593565b508651611c3d9060059060208a0190615593565b508551611c51906007906020890190615593565b50600680547fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff001660ff8716179055600880547fffffffffffffffffffffffff000000000000000000000000000000000000000090811673ffffffffffffffffffffffffffffffffffffffff8781169190911790925560018054821686841617905560028054909116918416919091179055611ceb81614140565b5050600880547fffffffffffffffffffffff00ffffffffffffffffffffffffffffffffffffffff1674010000000000000000000000000000000000000000179055505050505050565b60085473ffffffffffffffffffffffffffffffffffffffff1681565b6000611d5a614187565b905090565b600e5473ffffffffffffffffffffffffffffffffffffffff1690565b60015460009074010000000000000000000000000000000000000000900460ff1615611e0857604080517f08c379a000000000000000000000000000000000000000000000000000000000815260206004820152601060248201527f5061757361626c653a2070617573656400000000000000000000000000000000604482015290519081900360640190fd5b6113a433848461427c565b60015473ffffffffffffffffffffffffffffffffffffffff163314611e83576040517f08c379a0000000000000000000000000000000000000000000000000000000008152600401808060200182810382526022815260200180615af36022913960400191505060405180910390fd5b600180547fffffffffffffffffffffff00ffffffffffffffffffffffffffffffffffffffff1690556040517f7805862f689e2f13df9f062ff482ad3ad112aca9e0847911ed832e158c525b3390600090a1565b336000908152600c602052604081205460ff16611f3e576040517f08c379a00000000000000000000000000000000000000000000000000000000081526004018080602001828103825260218152602001806159176021913960400191505060405180910390fd5b33611f4881613cf8565b15611f9e576040517f08c379a0000000000000000000000000000000000000000000000000000000008152600401808060200182810382526025815260200180615bc76025913960400191505060405180910390fd5b60015474010000000000000000000000000000000000000000900460ff161561202857604080517f08c379a000000000000000000000000000000000000000000000000000000000815260206004820152601060248201527f5061757361626c653a2070617573656400000000000000000000000000000000604482015290519081900360640190fd5b73ffffffffffffffffffffffffffffffffffffffff8416612094576040517f08c379a000000000000000000000000000000000000000000000000000000000815260040180806020018281038252602381526020018061578f6023913960400191505060405180910390fd5b600083116120ed576040517f08c379a000000000000000000000000000000000000000000000000000000000815260040180806020018281038252602981526020018061584d6029913960400191505060405180910390fd5b336000908152600d602052604090205480841115612156576040517f08c379a000000000000000000000000000000000000000000000000000000000815260040180806020018281038252602e815260200180615ac5602e913960400191505060405180910390fd5b336000908152600d6020526040902084820390557318000000000000000000000000000000000000006340c10f19866121948764e8d4a510006142c6565b6040518363ffffffff1660e01b8152600401808373ffffffffffffffffffffffffffffffffffffffff16815260200182815260200192505050602060405180830381600087803b1580156121e757600080fd5b505af11580156121fb573d6000803e3d6000fd5b505050506040513d602081101561221157600080fd5b505161227e57604080517f08c379a000000000000000000000000000000000000000000000000000000000815260206004820152601260248201527f4e6174697665206d696e74206661696c65640000000000000000000000000000604482015290519081900360640190fd5b60408051858152905173ffffffffffffffffffffffffffffffffffffffff87169133917fab8530f87dc9b59234c4623bf917212bb2536d647574c8e7e5da92c2ede0c9f89181900360200190a360408051858152905173ffffffffffffffffffffffffffffffffffffffff8716916000917fddf252ad1be2c89b69c2b068fc378daa952ba7f163c4a11628f55a4df523b3ef9181900360200190a3506001949350505050565b336000908152600c602052604090205460ff1661238c576040517f08c379a00000000000000000000000000000000000000000000000000000000081526004018080602001828103825260218152602001806159176021913960400191505060405180910390fd5b60015474010000000000000000000000000000000000000000900460ff161561241657604080517f08c379a000000000000000000000000000000000000000000000000000000000815260206004820152601060248201527f5061757361626c653a2070617573656400000000000000000000000000000000604482015290519081900360640190fd5b6000811161246f576040517f08c379a00000000000000000000000000000000000000000000000000000000081526004018080602001828103825260298152602001806157666029913960400191505060405180910390fd5b60006124808264e8d4a510006142c6565b905033318111156124dc576040517f08c379a00000000000000000000000000000000000000000000000000000000081526004018080602001828103825260268152602001806158f16026913960400191505060405180910390fd5b604080517f9dc29fac00000000000000000000000000000000000000000000000000000000815233600482015260248101839052905173180000000000000000000000000000000000000091639dc29fac9160448083019260209291908290030181600087803b15801561254f57600080fd5b505af1158015612563573d6000803e3d6000fd5b505050506040513d602081101561257957600080fd5b50516125e657604080517f08c379a000000000000000000000000000000000000000000000000000000000815260206004820152601260248201527f4e6174697665206275726e206661696c65640000000000000000000000000000604482015290519081900360640190fd5b60408051838152905133917fcc16f5dbb4873280815c1ee09dbd06736cffcc184412cf7a71a0fdb75d397ca5919081900360200190a260408051838152905160009133917fddf252ad1be2c89b69c2b068fc378daa952ba7f163c4a11628f55a4df523b3ef9181900360200190a35050565b60125460ff1660021461266a57600080fd5b61267660058383615611565b5060005b838110156127b8576003600086868481811061269257fe5b6020908102929092013573ffffffffffffffffffffffffffffffffffffffff168352508101919091526040016000205460ff1661271a576040517f08c379a000000000000000000000000000000000000000000000000000000000815260040180806020018281038252603d8152602001806156b3603d913960400191505060405180910390fd5b61274b85858381811061272957fe5b9050602002013573ffffffffffffffffffffffffffffffffffffffff16614135565b6003600086868481811061275b57fe5b6020908102929092013573ffffffffffffffffffffffffffffffffffffffff1683525081019190915260400160002080547fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff0016905560010161267a565b506127c230614135565b505030600090815260036020819052604090912080547fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff009081169091556012805490911690911790555050565b60015460009074010000000000000000000000000000000000000000900460ff161561289c57604080517f08c379a000000000000000000000000000000000000000000000000000000000815260206004820152601060248201527f5061757361626c653a2070617573656400000000000000000000000000000000604482015290519081900360640190fd5b60085473ffffffffffffffffffffffffffffffffffffffff16331461290c576040517f08c379a000000000000000000000000000000000000000000000000000000000815260040180806020018281038252602981526020018061589c6029913960400191505060405180910390fd5b73ffffffffffffffffffffffffffffffffffffffff83166000818152600c6020908152604080832080547fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff00166001179055600d825291829020859055815185815291517f46980fca912ef9bcdbd36877427b6b90e860769f604e89c0e67720cece530d209281900390910190a250600192915050565b60408051808201909152600181527f3200000000000000000000000000000000000000000000000000000000000000602082015290565b60005473ffffffffffffffffffffffffffffffffffffffff163314612a5f57604080517f08c379a000000000000000000000000000000000000000000000000000000000815260206004820181905260248201527f4f776e61626c653a2063616c6c6572206973206e6f7420746865206f776e6572604482015290519081900360640190fd5b73ffffffffffffffffffffffffffffffffffffffff8116612acb576040517f08c379a00000000000000000000000000000000000000000000000000000000081526004018080602001828103825260288152602001806157136028913960400191505060405180910390fd5b600180547fffffffffffffffffffffffff00000000000000000000000000000000000000001673ffffffffffffffffffffffffffffffffffffffff83811691909117918290556040519116907fb80482a293ca2e013eda8683c9bd7fc8347cfdaeea5ede58cba46df502c2a60490600090a250565b60015474010000000000000000000000000000000000000000900460ff1615612bca57604080517f08c379a000000000000000000000000000000000000000000000000000000000815260206004820152601060248201527f5061757361626c653a2070617573656400000000000000000000000000000000604482015290519081900360640190fd5b612bd785858585856142d2565b5050505050565b60015474010000000000000000000000000000000000000000900460ff1681565b60006113a88261410c565b73ffffffffffffffffffffffffffffffffffffffff1660009081526011602052604090205490565b7fd099cc98ef71107a616c4f0f941f04c322d8e254fe26b3c6668db87aae413de881565b73180000000000000000000000000000000000000081565b60015473ffffffffffffffffffffffffffffffffffffffff163314612cde576040517f08c379a0000000000000000000000000000000000000000000000000000000008152600401808060200182810382526022815260200180615af36022913960400191505060405180910390fd5b600180547fffffffffffffffffffffff00ffffffffffffffffffffffffffffffffffffffff16740100000000000000000000000000000000000000001790556040517f6985a02210a168e66602d3235cb6db0e70f92b3ba4d376a33c0f3d9434bff62590600090a1565b60015474010000000000000000000000000000000000000000900460ff1615612dd257604080517f08c379a000000000000000000000000000000000000000000000000000000000815260206004820152601060248201527f5061757361626c653a2070617573656400000000000000000000000000000000604482015290519081900360640190fd5b612de187878787878787614312565b50505050505050565b73ffffffffffffffffffffffffffffffffffffffff166000908152600d602052604090205490565b60005473ffffffffffffffffffffffffffffffffffffffff1690565b6005805460408051602060026001851615610100027fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff0190941693909304601f810184900484028201840190925281815292918301828280156113045780601f106112d957610100808354040283529160200191611304565b60015473ffffffffffffffffffffffffffffffffffffffff1681565b60015474010000000000000000000000000000000000000000900460ff1615612f4d57604080517f08c379a000000000000000000000000000000000000000000000000000000000815260206004820152601060248201527f5061757361626c653a2070617573656400000000000000000000000000000000604482015290519081900360640190fd5b612bd78585858585614433565b7f7c7c6cdb67a18743f49ec6fa9b35f50d52ed05cbed4cc592e13b44501c1a226781565b60015460009074010000000000000000000000000000000000000000900460ff161561300b57604080517f08c379a000000000000000000000000000000000000000000000000000000000815260206004820152601060248201527f5061757361626c653a2070617573656400000000000000000000000000000000604482015290519081900360640190fd5b6113a43384846146f7565b60015460009074010000000000000000000000000000000000000000900460ff16156130a357604080517f08c379a000000000000000000000000000000000000000000000000000000000815260206004820152601060248201527f5061757361626c653a2070617573656400000000000000000000000000000000604482015290519081900360640190fd5b6113a4338484613e1e565b60005473ffffffffffffffffffffffffffffffffffffffff16331461313457604080517f08c379a000000000000000000000000000000000000000000000000000000000815260206004820181905260248201527f4f776e61626c653a2063616c6c6572206973206e6f7420746865206f776e6572604482015290519081900360640190fd5b73ffffffffffffffffffffffffffffffffffffffff81166131a0576040517f08c379a000000000000000000000000000000000000000000000000000000000815260040180806020018281038252602f815260200180615938602f913960400191505060405180910390fd5b600880547fffffffffffffffffffffffff00000000000000000000000000000000000000001673ffffffffffffffffffffffffffffffffffffffff83811691909117918290556040519116907fdb66dfa9c6b8f5226fe9aac7e51897ae8ee94ac31dc70bb6c9900b2574b707e690600090a250565b73ffffffffffffffffffffffffffffffffffffffff166000908152600c602052604090205460ff1690565b60005473ffffffffffffffffffffffffffffffffffffffff1633146132c657604080517f08c379a000000000000000000000000000000000000000000000000000000000815260206004820181905260248201527f4f776e61626c653a2063616c6c6572206973206e6f7420746865206f776e6572604482015290519081900360640190fd5b73ffffffffffffffffffffffffffffffffffffffff8116613332576040517f08c379a0000000000000000000000000000000000000000000000000000000008152600401808060200182810382526032815260200180615b956032913960400191505060405180910390fd5b600280547fffffffffffffffffffffffff00000000000000000000000000000000000000001673ffffffffffffffffffffffffffffffffffffffff83811691909117918290556040519116907fc67398012c111ce95ecb7429b933096c977380ee6c421175a71a4a4c6c88c06e90600090a250565b600e5473ffffffffffffffffffffffffffffffffffffffff163314613417576040517f08c379a00000000000000000000000000000000000000000000000000000000081526004018080602001828103825260248152602001806159676024913960400191505060405180910390fd5b61343873ffffffffffffffffffffffffffffffffffffffff84168383614753565b505050565b60015474010000000000000000000000000000000000000000900460ff16156134c757604080517f08c379a000000000000000000000000000000000000000000000000000000000815260206004820152601060248201527f5061757361626c653a2070617573656400000000000000000000000000000000604482015290519081900360640190fd5b6134388383836147e0565b60025473ffffffffffffffffffffffffffffffffffffffff1681565b64e8d4a5100081565b60015474010000000000000000000000000000000000000000900460ff161561358157604080517f08c379a000000000000000000000000000000000000000000000000000000000815260206004820152601060248201527f5061757361626c653a2070617573656400000000000000000000000000000000604482015290519081900360640190fd5b612de1878787878787876148ea565b60015474010000000000000000000000000000000000000000900460ff161561361a57604080517f08c379a000000000000000000000000000000000000000000000000000000000815260206004820152601060248201527f5061757361626c653a2070617573656400000000000000000000000000000000604482015290519081900360640190fd5b612de187878787878787614988565b60085474010000000000000000000000000000000000000000900460ff168015613656575060125460ff16155b61365f57600080fd5b61366b60048383615611565b506136e082828080601f0160208091040260200160405190810160405280939291908181526020018383808284376000920191909152505060408051808201909152600181527f3200000000000000000000000000000000000000000000000000000000000000602082015291506149ca9050565b600f555050601280547fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff00166001179055565b7f158b0a9edf7a828aad02f63cd515c68ef2f50ba807396f6d12842833a159742981565b73180000000000000000000000000000000000000181565b73ffffffffffffffffffffffffffffffffffffffff9182166000908152600a6020908152604080832093909416825291909152205490565b60015474010000000000000000000000000000000000000000900460ff161561381057604080517f08c379a000000000000000000000000000000000000000000000000000000000815260206004820152601060248201527f5061757361626c653a2070617573656400000000000000000000000000000000604482015290519081900360640190fd5b6138218989898989898989896149e0565b505050505050505050565b6007805460408051602060026001851615610100027fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff0190941693909304601f810184900484028201840190925281815292918301828280156113045780601f106112d957610100808354040283529160200191611304565b73ffffffffffffffffffffffffffffffffffffffff919091166000908152601060209081526040808320938352929052205460ff1690565b60015474010000000000000000000000000000000000000000900460ff161561396757604080517f08c379a000000000000000000000000000000000000000000000000000000000815260206004820152601060248201527f5061757361626c653a2070617573656400000000000000000000000000000000604482015290519081900360640190fd5b613821898989898989898989614a24565b60005473ffffffffffffffffffffffffffffffffffffffff1633146139fe57604080517f08c379a000000000000000000000000000000000000000000000000000000000815260206004820181905260248201527f4f776e61626c653a2063616c6c6572206973206e6f7420746865206f776e6572604482015290519081900360640190fd5b73ffffffffffffffffffffffffffffffffffffffff8116613a6a576040517f08c379a00000000000000000000000000000000000000000000000000000000081526004018080602001828103825260268152602001806157b26026913960400191505060405180910390fd5b6000546040805173ffffffffffffffffffffffffffffffffffffffff9283168152918316602083015280517f8be0079c531659141344cd1fd0a4f28419497f9722a3daafe3b4186f6b6457e09281900390910190a1613ac881614140565b50565b60025473ffffffffffffffffffffffffffffffffffffffff163314613b3b576040517f08c379a000000000000000000000000000000000000000000000000000000000815260040180806020018281038252602c8152602001806158c5602c913960400191505060405180910390fd5b613b4481614135565b60405173ffffffffffffffffffffffffffffffffffffffff8216907fffa4e6181777692565cf28528fc88fd1516ea86b56da075235fa575af6a4b85590600090a250565b60006113a882613cf8565b73ffffffffffffffffffffffffffffffffffffffff8316613bff576040517f08c379a0000000000000000000000000000000000000000000000000000000008152600401808060200182810382526024815260200180615aa16024913960400191505060405180910390fd5b73ffffffffffffffffffffffffffffffffffffffff8216613c6b576040517f08c379a00000000000000000000000000000000000000000000000000000000081526004018080602001828103825260228152602001806157d86022913960400191505060405180910390fd5b73ffffffffffffffffffffffffffffffffffffffff8084166000818152600a6020908152604080832094871680845294825291829020859055815185815291517f8c5be1e5ebec7d5bd14f71427d1e84f3dd0314c0f7b2291e5b200ac8c7c3b9259281900390910190a3505050565b6000613ce68383614a68565b9392505050565b613ac8816000614ae9565b600073180000000000000000000000000000000000000173ffffffffffffffffffffffffffffffffffffffff16638e204c43836040518263ffffffff1660e01b8152600401808273ffffffffffffffffffffffffffffffffffffffff16815260200191505060206040518083038186803b158015613d7557600080fd5b505afa158015613d89573d6000803e3d6000fd5b505050506040513d6020811015613d9f57600080fd5b505192915050565b600082821115613e1857604080517f08c379a000000000000000000000000000000000000000000000000000000000815260206004820152601e60248201527f536166654d6174683a207375627472616374696f6e206f766572666c6f770000604482015290519081900360640190fd5b50900390565b73ffffffffffffffffffffffffffffffffffffffff8316613e8a576040517f08c379a0000000000000000000000000000000000000000000000000000000008152600401808060200182810382526025815260200180615a7c6025913960400191505060405180910390fd5b73ffffffffffffffffffffffffffffffffffffffff8216613ef6576040517f08c379a00000000000000000000000000000000000000000000000000000000081526004018080602001828103825260238152602001806156f06023913960400191505060405180910390fd5b6000613f078264e8d4a510006142c6565b90508373ffffffffffffffffffffffffffffffffffffffff1631811115613f79576040517f08c379a00000000000000000000000000000000000000000000000000000000081526004018080602001828103825260268152602001806158766026913960400191505060405180910390fd5b604080517fbeabacc800000000000000000000000000000000000000000000000000000000815273ffffffffffffffffffffffffffffffffffffffff8681166004830152851660248201526044810183905290517318000000000000000000000000000000000000009163beabacc89160648083019260209291908290030181600087803b15801561400a57600080fd5b505af115801561401e573d6000803e3d6000fd5b505050506040513d602081101561403457600080fd5b50516140a157604080517f08c379a000000000000000000000000000000000000000000000000000000000815260206004820152601660248201527f4e6174697665207472616e73666572206661696c656400000000000000000000604482015290519081900360640190fd5b8273ffffffffffffffffffffffffffffffffffffffff168473ffffffffffffffffffffffffffffffffffffffff167fddf252ad1be2c89b69c2b068fc378daa952ba7f163c4a11628f55a4df523b3ef846040518082815260200191505060405180910390a350505050565b600073ffffffffffffffffffffffffffffffffffffffff821631613ce68164e8d4a51000613cda565b613ac8816001614ae9565b600080547fffffffffffffffffffffffff00000000000000000000000000000000000000001673ffffffffffffffffffffffffffffffffffffffff92909216919091179055565b6004805460408051602060026001851615610100027fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff0190941693909304601f8101849004840282018401909252818152600093611d5a93919290918301828280156142345780601f1061420957610100808354040283529160200191614234565b820191906000526020600020905b81548152906001019060200180831161421757829003601f168201915b50505050506040518060400160405280600181526020017f3200000000000000000000000000000000000000000000000000000000000000815250614277614d39565b614d3d565b73ffffffffffffffffffffffffffffffffffffffff8084166000908152600a602090815260408083209386168352929052205461343890849084906142c19085614db1565b613b93565b6000613ce68383614e25565b612bd78585848487604051602001808481526020018381526020018260ff1660f81b815260010193505050506040516020818303038152906040526147e0565b73ffffffffffffffffffffffffffffffffffffffff86163314614380576040517f08c379a0000000000000000000000000000000000000000000000000000000008152600401808060200182810382526025815260200180615a026025913960400191505060405180910390fd5b61438c87838686614e98565b604080517fd099cc98ef71107a616c4f0f941f04c322d8e254fe26b3c6668db87aae413de860208083019190915273ffffffffffffffffffffffffffffffffffffffff808b1683850152891660608301526080820188905260a0820187905260c0820186905260e080830186905283518084039091018152610100909201909252805191012061441e90889083614f52565b61442887836150d0565b612de1878787613e1e565b7fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff8214806144615750428210155b6144cc57604080517f08c379a000000000000000000000000000000000000000000000000000000000815260206004820152601e60248201527f46696174546f6b656e56323a207065726d697420697320657870697265640000604482015290519081900360640190fd5b60006145746144d9614187565b73ffffffffffffffffffffffffffffffffffffffff80891660008181526011602090815260409182902080546001810190915582517f6e71edae12b1b97f4d1f60370fef10105fa2faae0126114a169c64845d6126c98184015280840194909452938b166060840152608083018a905260a083019390935260c08083018990528151808403909101815260e090920190528051910120615155565b905073fcff98b65f9ea559ec0df36f4072c7e3be0520df636ccea6528783856040518463ffffffff1660e01b8152600401808473ffffffffffffffffffffffffffffffffffffffff16815260200183815260200180602001828103825283818151815260200191508051906020019080838360005b838110156146015781810151838201526020016145e9565b50505050905090810190601f16801561462e5780820380516001836020036101000a031916815260200191505b5094505050505060206040518083038186803b15801561464d57600080fd5b505af4158015614661573d6000803e3d6000fd5b505050506040513d602081101561467757600080fd5b50516146e457604080517f08c379a000000000000000000000000000000000000000000000000000000000815260206004820152601a60248201527f454950323631323a20696e76616c6964207369676e6174757265000000000000604482015290519081900360640190fd5b6146ef868686613b93565b505050505050565b61343883836142c184604051806060016040528060258152602001615c116025913973ffffffffffffffffffffffffffffffffffffffff808a166000908152600a60209081526040808320938c1683529290522054919061518f565b6040805173ffffffffffffffffffffffffffffffffffffffff8416602482015260448082018490528251808303909101815260649091019091526020810180517bffffffffffffffffffffffffffffffffffffffffffffffffffffffff167fa9059cbb00000000000000000000000000000000000000000000000000000000179052613438908490615240565b6147ea8383615318565b614864837f158b0a9edf7a828aad02f63cd515c68ef2f50ba807396f6d12842833a159742960001b8585604051602001808481526020018373ffffffffffffffffffffffffffffffffffffffff16815260200182815260200193505050506040516020818303038152906040528051906020012083614f52565b73ffffffffffffffffffffffffffffffffffffffff8316600081815260106020908152604080832086845290915280822080547fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff00166001179055518492917f1cdd46ff242716cdaa72d159d339a485b3438398348d68f09d7c8c0a59353d8191a3505050565b6148f687838686614e98565b604080517f7c7c6cdb67a18743f49ec6fa9b35f50d52ed05cbed4cc592e13b44501c1a226760208083019190915273ffffffffffffffffffffffffffffffffffffffff808b1683850152891660608301526080820188905260a0820187905260c0820186905260e080830186905283518084039091018152610100909201909252805191012061441e90889083614f52565b612de187878787868689604051602001808481526020018381526020018260ff1660f81b81526001019350505050604051602081830303815290604052614433565b6000466149d8848483614d3d565b949350505050565b61382189898989898988888b604051602001808481526020018381526020018260ff1660f81b815260010193505050506040516020818303038152906040526148ea565b61382189898989898988888b604051602001808481526020018381526020018260ff1660f81b81526001019350505050604051602081830303815290604052614312565b6000808211614ad857604080517f08c379a000000000000000000000000000000000000000000000000000000000815260206004820152601a60248201527f536166654d6174683a206469766973696f6e206279207a65726f000000000000604482015290519081900360640190fd5b818381614ae157fe5b049392505050565b8015614c86573073ffffffffffffffffffffffffffffffffffffffff16638da5cb5b6040518163ffffffff1660e01b815260040160206040518083038186803b158015614b3557600080fd5b505afa158015614b49573d6000803e3d6000fd5b505050506040513d6020811015614b5f57600080fd5b505173ffffffffffffffffffffffffffffffffffffffff83811691161415614bd2576040517f08c379a000000000000000000000000000000000000000000000000000000000815260040180806020018281038252602b815260200180615a51602b913960400191505060405180910390fd5b604080517fe5c7160b00000000000000000000000000000000000000000000000000000000815273ffffffffffffffffffffffffffffffffffffffff8416600482015290517318000000000000000000000000000000000000019163e5c7160b9160248083019260209291908290030181600087803b158015614c5457600080fd5b505af1158015614c68573d6000803e3d6000fd5b505050506040513d6020811015614c7e57600080fd5b50614d359050565b604080517f31b2302000000000000000000000000000000000000000000000000000000000815273ffffffffffffffffffffffffffffffffffffffff841660048201529051731800000000000000000000000000000000000001916331b230209160248083019260209291908290030181600087803b158015614d0857600080fd5b505af1158015614d1c573d6000803e3d6000fd5b505050506040513d6020811015614d3257600080fd5b50505b5050565b4690565b8251602093840120825192840192909220604080517f8b73c3c69bb8fe3d512ecc4cf759cc79239f7b179b0ffacaa9a75d522b39400f8187015280820194909452606084019190915260808301919091523060a0808401919091528151808403909101815260c09092019052805191012090565b600082820183811015613ce657604080517f08c379a000000000000000000000000000000000000000000000000000000000815260206004820152601b60248201527f536166654d6174683a206164646974696f6e206f766572666c6f770000000000604482015290519081900360640190fd5b600082614e34575060006113a8565b82820282848281614e4157fe5b0414613ce6576040517f08c379a000000000000000000000000000000000000000000000000000000000815260040180806020018281038252602181526020018061598b6021913960400191505060405180910390fd5b814211614ef0576040517f08c379a000000000000000000000000000000000000000000000000000000000815260040180806020018281038252602b81526020018061573b602b913960400191505060405180910390fd5b804210614f48576040517f08c379a0000000000000000000000000000000000000000000000000000000008152600401808060200182810382526025815260200180615bec6025913960400191505060405180910390fd5b614d328484615318565b73fcff98b65f9ea559ec0df36f4072c7e3be0520df636ccea65284614f7e614f78614187565b86615155565b846040518463ffffffff1660e01b8152600401808473ffffffffffffffffffffffffffffffffffffffff16815260200183815260200180602001828103825283818151815260200191508051906020019080838360005b83811015614fed578181015183820152602001614fd5565b50505050905090810190601f16801561501a5780820380516001836020036101000a031916815260200191505b5094505050505060206040518083038186803b15801561503957600080fd5b505af415801561504d573d6000803e3d6000fd5b505050506040513d602081101561506357600080fd5b505161343857604080517f08c379a000000000000000000000000000000000000000000000000000000000815260206004820152601e60248201527f46696174546f6b656e56323a20696e76616c6964207369676e61747572650000604482015290519081900360640190fd5b73ffffffffffffffffffffffffffffffffffffffff8216600081815260106020908152604080832085845290915280822080547fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff00166001179055518392917f98de503528ee59b575ef0c0a2576a82497bfc029a5685b209e9ec333479b10a591a35050565b6040517f19010000000000000000000000000000000000000000000000000000000000008152600281019290925260228201526042902090565b60008184841115615238576040517f08c379a00000000000000000000000000000000000000000000000000000000081526004018080602001828103825283818151815260200191508051906020019080838360005b838110156151fd5781810151838201526020016151e5565b50505050905090810190601f16801561522a5780820380516001836020036101000a031916815260200191505b509250505060405180910390fd5b505050900390565b60606152a2826040518060400160405280602081526020017f5361666545524332303a206c6f772d6c6576656c2063616c6c206661696c65648152508573ffffffffffffffffffffffffffffffffffffffff166153a29092919063ffffffff16565b805190915015613438578080602001905160208110156152c157600080fd5b5051613438576040517f08c379a000000000000000000000000000000000000000000000000000000000815260040180806020018281038252602a815260200180615b15602a913960400191505060405180910390fd5b73ffffffffffffffffffffffffffffffffffffffff8216600090815260106020908152604080832084845290915290205460ff1615614d35576040517f08c379a000000000000000000000000000000000000000000000000000000000815260040180806020018281038252602e815260200180615b67602e913960400191505060405180910390fd5b60606149d88484600085856153b68561550d565b61542157604080517f08c379a000000000000000000000000000000000000000000000000000000000815260206004820152601d60248201527f416464726573733a2063616c6c20746f206e6f6e2d636f6e7472616374000000604482015290519081900360640190fd5b600060608673ffffffffffffffffffffffffffffffffffffffff1685876040518082805190602001908083835b6020831061548b57805182527fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffe0909201916020918201910161544e565b6001836020036101000a03801982511681845116808217855250505050505090500191505060006040518083038185875af1925050503d80600081146154ed576040519150601f19603f3d011682016040523d82523d6000602084013e6154f2565b606091505b5091509150615502828286615513565b979650505050505050565b3b151590565b60608315615522575081613ce6565b8251156155325782518084602001fd5b6040517f08c379a00000000000000000000000000000000000000000000000000000000081526020600482018181528451602484015284518593919283926044019190850190808383600083156151fd5781810151838201526020016151e5565b828054600181600116156101000203166002900490600052602060002090601f016020900481019282601f106155d457805160ff1916838001178555615601565b82800160010185558215615601579182015b828111156156015782518255916020019190600101906155e6565b5061560d92915061569d565b5090565b828054600181600116156101000203166002900490600052602060002090601f016020900481019282601f10615670578280017fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff00823516178555615601565b82800160010185558215615601579182015b82811115615601578235825591602001919060010190615682565b5b8082111561560d576000815560010161569e56fe46696174546f6b656e56325f323a20426c61636b6c697374696e672070726576696f75736c7920756e626c61636b6c6973746564206163636f756e742145524332303a207472616e7366657220746f20746865207a65726f20616464726573735061757361626c653a206e65772070617573657220697320746865207a65726f206164647265737346696174546f6b656e56323a20617574686f72697a6174696f6e206973206e6f74207965742076616c696446696174546f6b656e3a206275726e20616d6f756e74206e6f742067726561746572207468616e203046696174546f6b656e3a206d696e7420746f20746865207a65726f20616464726573734f776e61626c653a206e6577206f776e657220697320746865207a65726f206164647265737345524332303a20617070726f766520746f20746865207a65726f206164647265737346696174546f6b656e3a206e65772070617573657220697320746865207a65726f2061646472657373526573637561626c653a206e6577207265736375657220697320746865207a65726f206164647265737346696174546f6b656e3a206d696e7420616d6f756e74206e6f742067726561746572207468616e203045524332303a207472616e7366657220616d6f756e7420657863656564732062616c616e636546696174546f6b656e3a2063616c6c6572206973206e6f7420746865206d61737465724d696e746572426c61636b6c69737461626c653a2063616c6c6572206973206e6f742074686520626c61636b6c697374657246696174546f6b656e3a206275726e20616d6f756e7420657863656564732062616c616e636546696174546f6b656e3a2063616c6c6572206973206e6f742061206d696e74657246696174546f6b656e3a206e6577206d61737465724d696e74657220697320746865207a65726f2061646472657373526573637561626c653a2063616c6c6572206973206e6f74207468652072657363756572536166654d6174683a206d756c7469706c69636174696f6e206f766572666c6f7745524332303a207472616e7366657220616d6f756e74206578636565647320616c6c6f77616e636546696174546f6b656e3a206e657720626c61636b6c697374657220697320746865207a65726f206164647265737346696174546f6b656e56323a2063616c6c6572206d7573742062652074686520706179656546696174546f6b656e3a20636f6e747261637420697320616c726561647920696e697469616c697a65644e617469766546696174546f6b656e56325f323a2063616e6e6f7420626c61636b6c697374206f776e657245524332303a207472616e736665722066726f6d20746865207a65726f206164647265737345524332303a20617070726f76652066726f6d20746865207a65726f206164647265737346696174546f6b656e3a206d696e7420616d6f756e742065786365656473206d696e746572416c6c6f77616e63655061757361626c653a2063616c6c6572206973206e6f7420746865207061757365725361666545524332303a204552433230206f7065726174696f6e20646964206e6f74207375636365656446696174546f6b656e3a206e6577206f776e657220697320746865207a65726f206164647265737346696174546f6b656e56323a20617574686f72697a6174696f6e2069732075736564206f722063616e63656c6564426c61636b6c69737461626c653a206e657720626c61636b6c697374657220697320746865207a65726f2061646472657373426c61636b6c69737461626c653a206163636f756e7420697320626c61636b6c697374656446696174546f6b656e56323a20617574686f72697a6174696f6e206973206578706972656445524332303a2064656372656173656420616c6c6f77616e63652062656c6f77207a65726fa2646970667358221220e06530c9518c3f8a05f1004a77ca5bf40453a85869b880a2f64f7273f330cb2a64736f6c634300060c0033" - }, - "0xfcFf98B65F9ea559EC0df36F4072C7E3BE0520Df": { - "balance": "0x0", - "nonce": "0x1", - "code": "0x73fcff98b65f9ea559ec0df36f4072c7e3be0520df30146080604052600436106100355760003560e01c80636ccea6521461003a575b600080fd5b6101026004803603606081101561005057600080fd5b73ffffffffffffffffffffffffffffffffffffffff8235169160208101359181019060608101604082013564010000000081111561008d57600080fd5b82018360208201111561009f57600080fd5b803590602001918460018302840111640100000000831117156100c157600080fd5b91908080601f016020809104026020016040519081016040528093929190818152602001838380828437600092019190915250929550610116945050505050565b604080519115158252519081900360200190f35b600061012184610179565b610164578373ffffffffffffffffffffffffffffffffffffffff16610146848461017f565b73ffffffffffffffffffffffffffffffffffffffff16149050610172565b61016f848484610203565b90505b9392505050565b3b151590565b600081516041146101db576040517f08c379a00000000000000000000000000000000000000000000000000000000081526004018080602001828103825260238152602001806106296023913960400191505060405180910390fd5b60208201516040830151606084015160001a6101f98682858561042d565b9695505050505050565b60008060608573ffffffffffffffffffffffffffffffffffffffff16631626ba7e60e01b86866040516024018083815260200180602001828103825283818151815260200191508051906020019080838360005b8381101561026f578181015183820152602001610257565b50505050905090810190601f16801561029c5780820380516001836020036101000a031916815260200191505b50604080517fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffe08184030181529181526020820180517bffffffffffffffffffffffffffffffffffffffffffffffffffffffff167fffffffff000000000000000000000000000000000000000000000000000000009098169790971787525181519196909550859450925090508083835b6020831061036957805182527fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffe0909201916020918201910161032c565b6001836020036101000a038019825116818451168082178552505050505050905001915050600060405180830381855afa9150503d80600081146103c9576040519150601f19603f3d011682016040523d82523d6000602084013e6103ce565b606091505b50915091508180156103e257506020815110155b80156101f9575080517f1626ba7e00000000000000000000000000000000000000000000000000000000906020808401919081101561042057600080fd5b5051149695505050505050565b60007f7fffffffffffffffffffffffffffffff5d576e7357a4501ddfe92f46681b20a08211156104a8576040517f08c379a00000000000000000000000000000000000000000000000000000000081526004018080602001828103825260268152602001806106726026913960400191505060405180910390fd5b8360ff16601b141580156104c057508360ff16601c14155b15610516576040517f08c379a000000000000000000000000000000000000000000000000000000000815260040180806020018281038252602681526020018061064c6026913960400191505060405180910390fd5b600060018686868660405160008152602001604052604051808581526020018460ff1681526020018381526020018281526020019450505050506020604051602081039080840390855afa158015610572573d6000803e3d6000fd5b50506040517fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffe0015191505073ffffffffffffffffffffffffffffffffffffffff811661061f57604080517f08c379a000000000000000000000000000000000000000000000000000000000815260206004820152601c60248201527f45435265636f7665723a20696e76616c6964207369676e617475726500000000604482015290519081900360640190fd5b9594505050505056fe45435265636f7665723a20696e76616c6964207369676e6174757265206c656e67746845435265636f7665723a20696e76616c6964207369676e6174757265202776272076616c756545435265636f7665723a20696e76616c6964207369676e6174757265202773272076616c7565a2646970667358221220289705d6ae8701c9ed586541fa0ba342f754518aa4f0e59dbbda380bc9f2323964736f6c634300060c0033" - }, - "0x695D85E3437DaA1F8f6DE97C5aEdF75B82deA80a": { - "balance": "0x0", - "nonce": "0x1", - "code": "0x608060405234801561000f575f5ffd5b5060043610610111575f3560e01c80638da5cb5b1161009e5780639fd0506d1161006e5780639fd0506d146103cb578063da980fed146103d3578063e30c3978146103e6578063f2fde38b146103ee578063f77c479114610401575f5ffd5b80638da5cb5b146101b15780638e207848146101d15780639242164f146101e45780639fd02a36146102e8575f5ffd5b8063554bab3c116100e4578063554bab3c146101585780635c975abb1461016b578063715018a61461019957806379ba5097146101a15780638456cb59146101a9575f5ffd5b8063032b901b1461011557806306cb5b661461012a5780632bbdb79f1461013d5780633f4ba83a14610150575b5f5ffd5b610128610123366004610e06565b610409565b005b610128610138366004610e28565b6104b7565b61012861014b366004610e4e565b61054e565b61012861065b565b610128610166366004610e28565b6106b9565b5f5160206113515f395f51905f5254600160a01b900460ff1660405190151581526020015b60405180910390f35b61012861073d565b610128610750565b61012861079a565b6101b96107fc565b6040516001600160a01b039091168152602001610190565b6101286101df366004610e65565b610830565b6102826040805160c0810182525f80825260208201819052918101829052606081018290526080810182905260a08101829052905f5160206113315f395f51905f526040805160c08101825282546001600160401b038082168352600160401b820481166020840152600160801b9091041691810191909152600182015460608201526002820154608082015260039091015460a082015292915050565b60405161019091905f60c0820190506001600160401b0383511682526001600160401b0360208401511660208301526001600160401b036040840151166040830152606083015160608301526080830151608083015260a083015160a083015292915050565b6103be60408051610100810182525f80825260208201819052918101829052606081018290526080810182905260a0810182905260c0810182905260e08101829052905f5160206113315f395f51905f52604080516101008101825260059092015461ffff80821684526201000082048116602085015264010000000082048116928401929092526601000000000000810482166060840152600160401b810482166080840152600160501b8104821660a0840152600160601b8104821660c0840152600160701b90041660e082015292915050565b6040516101909190610e7e565b6101b9610986565b6101286103e1366004610f1e565b61099b565b6101b9610b79565b6101286103fc366004610e28565b610ba1565b6101b9610c26565b610411610c4e565b610419610c99565b5f8161ffff161161043d5760405163811da5f160e01b815260040160405180910390fd5b7f668f09ce856848ead6cb1ddee963f15ef833cea8958030868f867aec84385205805461ffff191661ffff83161781556040515f5160206113315f395f51905f52917faba8150f8eae6a1acc6824830944cdc19c670da2fae4fe28f4dfa04c0d6932d4916104ab9190610f30565b60405180910390a15050565b6104bf610cd1565b6001600160a01b0381166104e6576040516371ab47bb60e11b815260040160405180910390fd5b7f958f8fec699b51a1249f513eceda5429078000657f74abd1721bba363087af0080546001600160a01b0319166001600160a01b03831690811782556040517f1304018cfe79741dcf02ba6b61d39cc4757d59395d03224d9925c7aa83002146905f90a25050565b610556610c4e565b61055e610c99565b5f811161057e57604051635251cbd760e01b815260040160405180910390fd5b7f668f09ce856848ead6cb1ddee963f15ef833cea8958030868f867aec84385203819055604080515f5160206113315f395f51905f5280546001600160401b03808216845281851c81166020850152608091821c16938301939093527f668f09ce856848ead6cb1ddee963f15ef833cea8958030868f867aec843852015460608301527f668f09ce856848ead6cb1ddee963f15ef833cea8958030868f867aec84385202549282019290925260a081018390527f413a821e8d0eadcc30efbe627a655111b9f28f5999003a85b7718a14b0e329ef9060c0016104ab565b610663610d03565b5f5160206113515f395f51905f528054600160a01b900460ff16156106b657805460ff60a01b191681556040517f7805862f689e2f13df9f062ff482ad3ad112aca9e0847911ed832e158c525b33905f90a15b50565b6106c1610cd1565b6001600160a01b0381166106e85760405163a74995ab60e01b815260040160405180910390fd5b5f5160206113515f395f51905f5280546001600160a01b0319166001600160a01b03831690811782556040517fb80482a293ca2e013eda8683c9bd7fc8347cfdaeea5ede58cba46df502c2a604905f90a25050565b610745610cd1565b61074e5f610d3b565b565b338061075a610b79565b6001600160a01b0316146107915760405163118cdaa760e01b81526001600160a01b03821660048201526024015b60405180910390fd5b6106b681610d3b565b6107a2610d03565b5f5160206113515f395f51905f528054600160a01b900460ff166106b657805460ff60a01b1916600160a01b1781556040517f6985a02210a168e66602d3235cb6db0e70f92b3ba4d376a33c0f3d9434bff625905f90a150565b5f807f9016d09d72d40fdae2fd8ceac6b6234c7706214fd39c1cd1e609a0528c1993005b546001600160a01b031692915050565b610838610c4e565b610840610c99565b606461084f6020830183610fb8565b6001600160401b0316111561087757604051633e82ffd960e21b815260040160405180910390fd5b61271061088a6040830160208401610fb8565b6001600160401b031611156108b25760405163f87b4d6d60e01b815260040160405180910390fd5b8060800135816060013511156108db576040516336b1b98d60e11b815260040160405180910390fd5b5f8160a00135116108ff57604051635251cbd760e01b815260040160405180910390fd5b6127106109126060830160408401610fb8565b6001600160401b0316111561093a5760405163279eca6760e21b815260040160405180910390fd5b5f5160206113315f395f51905f5281816109548282610fd3565b9050507f413a821e8d0eadcc30efbe627a655111b9f28f5999003a85b7718a14b0e329ef826040516104ab9190611090565b5f805f5160206113515f395f51905f52610820565b6109a3610c4e565b6109ab610c99565b5f6109b96020830183610e06565b61ffff16116109db5760405163811da5f160e01b815260040160405180910390fd5b5f6109ec6040830160208401610e06565b61ffff1611610a0e5760405163055dd8c360e01b815260040160405180910390fd5b5f610a1f6060830160408401610e06565b61ffff1611610a4157604051632de6d8ed60e11b815260040160405180910390fd5b5f610a526080830160608401610e06565b61ffff1611610a7457604051634c14d46960e11b815260040160405180910390fd5b5f610a8560a0830160808401610e06565b61ffff1611610aa7576040516396e398e160e01b815260040160405180910390fd5b5f610ab860c0830160a08401610e06565b61ffff1611610ada57604051632218f17b60e21b815260040160405180910390fd5b5f610aeb60e0830160c08401610e06565b61ffff1611610b0d57604051631805fa5160e11b815260040160405180910390fd5b5f5160206113315f395f51905f52817f668f09ce856848ead6cb1ddee963f15ef833cea8958030868f867aec84385205610b478282611119565b9050507faba8150f8eae6a1acc6824830944cdc19c670da2fae4fe28f4dfa04c0d6932d4826040516104ab919061127f565b5f807f237e158222e3e6968b72b9db0d8043aacf074ad9f650f0d1606b4d82ee432c00610820565b610ba9610cd1565b7f237e158222e3e6968b72b9db0d8043aacf074ad9f650f0d1606b4d82ee432c0080546001600160a01b0319166001600160a01b0383169081178255610bed6107fc565b6001600160a01b03167f38d16b8cac22d99fc7c124b9cd0de2d3fa1faef420bfe791d8c362d765e2270060405160405180910390a35050565b5f807f958f8fec699b51a1249f513eceda5429078000657f74abd1721bba363087af00610820565b7f958f8fec699b51a1249f513eceda5429078000657f74abd1721bba363087af0080546001600160a01b031633146106b657604051630e971e6360e11b815260040160405180910390fd5b5f5160206113515f395f51905f528054600160a01b900460ff16156106b65760405163ab35696f60e01b815260040160405180910390fd5b33610cda6107fc565b6001600160a01b03161461074e5760405163118cdaa760e01b8152336004820152602401610788565b5f5160206113515f395f51905f5280546001600160a01b031633146106b65760405163daeefd6560e01b815260040160405180910390fd5b7f237e158222e3e6968b72b9db0d8043aacf074ad9f650f0d1606b4d82ee432c0080546001600160a01b0319168155610d7382610d77565b5050565b7f9016d09d72d40fdae2fd8ceac6b6234c7706214fd39c1cd1e609a0528c19930080546001600160a01b031981166001600160a01b03848116918217845560405192169182907f8be0079c531659141344cd1fd0a4f28419497f9722a3daafe3b4186f6b6457e0905f90a3505050565b61ffff811681146106b6575f5ffd5b8035610e0181610de7565b919050565b5f60208284031215610e16575f5ffd5b8135610e2181610de7565b9392505050565b5f60208284031215610e38575f5ffd5b81356001600160a01b0381168114610e21575f5ffd5b5f60208284031215610e5e575f5ffd5b5035919050565b5f60c0828403128015610e76575f5ffd5b509092915050565b5f6101008201905061ffff835116825261ffff602084015116602083015261ffff60408401511660408301526060830151610ebf606084018261ffff169052565b506080830151610ed5608084018261ffff169052565b5060a0830151610eeb60a084018261ffff169052565b5060c0830151610f0160c084018261ffff169052565b5060e0830151610f1760e084018261ffff169052565b5092915050565b5f610100828403128015610e76575f5ffd5b815461ffff8082168352601082901c166020830152610100820190602081901c61ffff166040840152603081901c61ffff166060840152604081901c61ffff166080840152605081901c61ffff1660a0840152606081901c61ffff1660c0840152607081901c61ffff1660e0840152610f17565b6001600160401b03811681146106b6575f5ffd5b5f60208284031215610fc8575f5ffd5b8135610e2181610fa4565b8135610fde81610fa4565b6001600160401b03811690508154816001600160401b03198216178355602084013561100981610fa4565b6fffffffffffffffff00000000000000008160401b16836fffffffffffffffffffffffffffffffff198416171784555050505f604083013561104a81610fa4565b825467ffffffffffffffff60801b1916608091821b67ffffffffffffffff60801b161783556060840135600184015583013560028301555060a090910135600390910155565b60c08101823561109f81610fa4565b6001600160401b0316825260208301356110b881610fa4565b6001600160401b0316602083015260408301356110d481610fa4565b6001600160401b03166040830152606083810135908301526080808401359083015260a092830135929091019190915290565b5f813561111381610de7565b92915050565b813561112481610de7565b61ffff8116905081548161ffff198216178355602084013561114581610de7565b63ffff00008160101b168363ffffffff1984161717845550505061118c61116e60408401611107565b825465ffff00000000191660209190911b65ffff0000000016178255565b6111bd61119b60608401611107565b825467ffff000000000000191660309190911b67ffff00000000000016178255565b6111f26111cc60808401611107565b825469ffff0000000000000000191660409190911b69ffff000000000000000016178255565b61122161120160a08401611107565b82805461ffff60501b191660509290921b61ffff60501b16919091179055565b61125061123060c08401611107565b82805461ffff60601b191660609290921b61ffff60601b16919091179055565b610d7361125f60e08401611107565b82805461ffff60701b191660709290921b61ffff60701b16919091179055565b6101008101823561128f81610de7565b61ffff16825260208301356112a381610de7565b61ffff1660208301526112b860408401610df6565b61ffff1660408301526112cd60608401610df6565b61ffff1660608301526112e260808401610df6565b61ffff1660808301526112f760a08401610df6565b61ffff1660a083015261130c60c08401610df6565b61ffff1660c083015261132160e08401610df6565b61ffff811660e0840152610f1756fe668f09ce856848ead6cb1ddee963f15ef833cea8958030868f867aec843852000642d7922329a434cf4fd17a3c95eb692c24fd95f9f94d0b55420a5d895f4a00a2646970667358221220046372fe4609a57ed6251208e9ef58c11217d4f47c96763ebfa709c47905168464736f6c634300081d0033" - }, - "0x3600000000000000000000000000000000000001": { - "balance": "0x0", - "nonce": "0x1", - "code": "0x608060405260043610610049575f3560e01c80633659cfe6146100535780634f1ef286146100725780635c60da1b146100855780638f283970146100b5578063f851a440146100d4575b6100516100e8565b005b34801561005e575f5ffd5b5061005161006d3660046104f3565b6100fa565b61005161008036600461050c565b610134565b348015610090575f5ffd5b50610099610197565b6040516001600160a01b03909116815260200160405180910390f35b3480156100c0575f5ffd5b506100516100cf3660046104f3565b6101a5565b3480156100df575f5ffd5b506100996101c5565b6100f86100f3610197565b6101ce565b565b6101026101ec565b6001600160a01b0316330361012c576101298160405180602001604052805f81525061021e565b50565b6101296100e8565b61013c6101ec565b6001600160a01b0316330361018f5761018a8383838080601f0160208091040260200160405190810160405280939291908181526020018383808284375f9201919091525061021e92505050565b505050565b61018a6100e8565b5f6101a0610277565b905090565b6101ad6101ec565b6001600160a01b0316330361012c576101298161029e565b5f6101a06101ec565b365f5f375f5f365f845af43d5f5f3e8080156101e8573d5ff35b3d5ffd5b5f7fb53127684a568b3173ae13b9f8a6016e243e63b6e8ee1178d6a717850b5d61035b546001600160a01b0316919050565b610227826102f2565b6040516001600160a01b038316907fbc7cd75a20ee27fd9adebab32041f755214dbc6bffa90cc0225b39da2e5c2d3b905f90a280511561026b5761018a8282610370565b6102736103e2565b5050565b5f7f360894a13ba1a3210667c828492db98dca3e2076cc3735a920a3ca505d382bbc61020f565b7f7e644d79422f17c01e4894b5f4f588d331ebfa28653d42ae832dc59e38c9798f6102c76101ec565b604080516001600160a01b03928316815291841660208301520160405180910390a161012981610401565b806001600160a01b03163b5f0361032c57604051634c9c8ce360e01b81526001600160a01b03821660048201526024015b60405180910390fd5b807f360894a13ba1a3210667c828492db98dca3e2076cc3735a920a3ca505d382bbc5b80546001600160a01b0319166001600160a01b039290921691909117905550565b60605f5f846001600160a01b03168460405161038c919061058a565b5f60405180830381855af49150503d805f81146103c4576040519150601f19603f3d011682016040523d82523d5f602084013e6103c9565b606091505b50915091506103d9858383610451565b95945050505050565b34156100f85760405163b398979f60e01b815260040160405180910390fd5b6001600160a01b03811661042a57604051633173bdd160e11b81525f6004820152602401610323565b807fb53127684a568b3173ae13b9f8a6016e243e63b6e8ee1178d6a717850b5d610361034f565b60608261046657610461826104b0565b6104a9565b815115801561047d57506001600160a01b0384163b155b156104a657604051639996b31560e01b81526001600160a01b0385166004820152602401610323565b50805b9392505050565b8051156104bf57805160208201fd5b60405163d6bda27560e01b815260040160405180910390fd5b80356001600160a01b03811681146104ee575f5ffd5b919050565b5f60208284031215610503575f5ffd5b6104a9826104d8565b5f5f5f6040848603121561051e575f5ffd5b610527846104d8565b9250602084013567ffffffffffffffff811115610542575f5ffd5b8401601f81018613610552575f5ffd5b803567ffffffffffffffff811115610568575f5ffd5b866020828401011115610579575f5ffd5b939660209190910195509293505050565b5f82518060208501845e5f92019182525091905056fea26469706673582212204c0881af9b906ca964ce2bc1cc4525350de8dd57658dacb806266290556eb9ae64736f6c634300081d0033", - "storage": { - "0xb53127684a568b3173ae13b9f8a6016e243e63b6e8ee1178d6a717850b5d6103": "0x000000000000000000000000bd3738ab866eff9b0908ef8985d00eeca22da4ef", - "0x360894a13ba1a3210667c828492db98dca3e2076cc3735a920a3ca505d382bbc": "0x000000000000000000000000695d85e3437daa1f8f6de97c5aedf75b82dea80a", - "0x9016d09d72d40fdae2fd8ceac6b6234c7706214fd39c1cd1e609a0528c199300": "0x000000000000000000000000a5fed552f38e11291dd2fc9cb91e2a5f6ae86ed4", - "0xf0c57e16840df040f15088dc2f81fe391c3923bec73e23a9662efc9c229c6a00": "0x0000000000000000000000000000000000000000000000000000000000000001", - "0x958f8fec699b51a1249f513eceda5429078000657f74abd1721bba363087af00": "0x00000000000000000000000041fe044f1f71ff69f46f35f41ec93369a0e94733", - "0x0642d7922329a434cf4fd17a3c95eb692c24fd95f9f94d0b55420a5d895f4a00": "0x00000000000000000000000085c8825829fc649694d569bccc374e8382df2d12", - "0x668f09ce856848ead6cb1ddee963f15ef833cea8958030868f867aec84385200": "0x0000000000000000000000000000138800000000000000c80000000000000014", - "0x668f09ce856848ead6cb1ddee963f15ef833cea8958030868f867aec84385201": "0x00000000000000000000000000000000000000000000000000000004a817c800", - "0x668f09ce856848ead6cb1ddee963f15ef833cea8958030868f867aec84385202": "0x000000000000000000000000000000000000000000000000000012309ce54000", - "0x668f09ce856848ead6cb1ddee963f15ef833cea8958030868f867aec84385203": "0x0000000000000000000000000000000000000000000000000000000001c9c380", - "0x668f09ce856848ead6cb1ddee963f15ef833cea8958030868f867aec84385205": "0x0000000000000000000000000000000001f4138801f403e801f403e801f40bb8" - } - }, - "0xfCc314DD5Ad756C6bBA725617438C0d25450a0dE": { - "balance": "0x0", - "nonce": "0x1", - "code": "0x608060405234801561000f575f5ffd5b50600436106100e5575f3560e01c80638da5cb5b11610088578063c4899f0c11610063578063c4899f0c146101cc578063e30c3978146101df578063f2fde38b146101e7578063f94e1867146101fa575f5ffd5b80638da5cb5b14610184578063b5d89627146101a4578063b86444b1146101c4575f5ffd5b80634ebae617116100c35780634ebae61714610138578063715018a61461014d57806377db16691461015557806379ba50971461017c575f5ffd5b806303a9b132146100e957806324408a68146101105780632469d67214610125575b5f5ffd5b6100fd5f5160206113f05f395f51905f5281565b6040519081526020015b60405180910390f35b61011861020d565b604051610107919061104a565b6100fd6101333660046110dc565b6103ed565b61014b61014636600461119d565b610598565b005b61014b6106a0565b7fb58da0dce03316992faea3e12c60705b8ac05a309e27e3bc8421e5b271c9d204546100fd565b61014b6106b3565b61018c6106fb565b6040516001600160a01b039091168152602001610107565b6101b76101b236600461119d565b61072f565b60405161010791906111b4565b6100fd610854565b61014b6101da3660046111c6565b610872565b61018c6109db565b61014b6101f53660046111e7565b610a03565b61014b61020836600461119d565b610a88565b60605f5160206113f05f395f51905f525f6102477fb58da0dce03316992faea3e12c60705b8ac05a309e27e3bc8421e5b271c9d201610c92565b9050806001600160401b03811115610261576102616110ad565b6040519080825280602002602001820160405280156102b557816020015b6102a26040805160608101909152805f8152606060208201525f60409091015290565b81526020019060019003908161027f5790505b5092505f5b818110156103e7575f6102d06001850183610c9b565b5f818152602086905260409081902081516060810190925280549293509091829060ff16600281111561030557610305610fa6565b600281111561031657610316610fa6565b815260200160018201805461032a9061120d565b80601f01602080910402602001604051908101604052809291908181526020018280546103569061120d565b80156103a15780601f10610378576101008083540402835291602001916103a1565b820191905f5260205f20905b81548152906001019060200180831161038457829003601f168201915b5050509183525050600291909101546001600160401b031660209091015285518690849081106103d3576103d3611245565b6020908102919091010152506001016102ba565b50505090565b5f6103f6610cad565b602083511461041857604051630717c3c160e01b815260040160405180910390fd5b82516020808501919091205f8181527fb58da0dce03316992faea3e12c60705b8ac05a309e27e3bc8421e5b271c9d2039092526040909120545f5160206113f05f395f51905f529190819060ff1615610490576040516338a14a4160e01b815260040161048791815260200190565b60405180910390fd5b50600482018054905f6104a28361126d565b909155506040805160608101909152909350806001815260208082018890526001600160401b0387166040928301525f868152908590522081518154829060ff191660018360028111156104f8576104f8610fa6565b02179055506020820151600182019061051190826112d1565b50604091820151600291909101805467ffffffffffffffff19166001600160401b039092169190911790555f82815260038401602052819020805460ff191660011790555183907fea20c1f9de86768933c1d4eff47ce667e26f886c4877d1bedc253e42b2f04a7c90610587908790899061138b565b60405180910390a250505b92915050565b6105a0610cad565b5f5f5160206113f05f395f51905f525f838152602082905260408120919250815460ff1660028111156105d5576105d5610fa6565b141583906105f9576040516355b54a8d60e01b815260040161048791815260200190565b506001815460ff16600281111561061257610612610fa6565b8254859260ff909116911461063c57604051635c12665560e01b81526004016104879291906113b4565b5050805460ff191660021781556106566001830184610cdf565b5060028101546040516001600160401b03909116815283907fbc5136437746415b39af863272d0ca406634cf14bb7c3e5fa03855781add87e59060200160405180910390a2505050565b6106a8610cad565b6106b15f610cea565b565b33806106bd6109db565b6001600160a01b0316146106ef5760405163118cdaa760e01b81526001600160a01b0382166004820152602401610487565b6106f881610cea565b50565b5f807f9016d09d72d40fdae2fd8ceac6b6234c7706214fd39c1cd1e609a0528c1993005b546001600160a01b031692915050565b6107526040805160608101909152805f8152606060208201525f60409091015290565b5f8281525f5160206113f05f395f51905f5260208190526040918290208251606081019093528054919291829060ff16600281111561079357610793610fa6565b60028111156107a4576107a4610fa6565b81526020016001820180546107b89061120d565b80601f01602080910402602001604051908101604052809291908181526020018280546107e49061120d565b801561082f5780601f106108065761010080835404028352916020019161082f565b820191905f5260205f20905b81548152906001019060200180831161081257829003601f168201915b5050509183525050600291909101546001600160401b03166020909101529392505050565b5f5f5160206113f05f395f51905f5261086c81610d26565b91505090565b61087a610cad565b5f5f5160206113f05f395f51905f525f848152602082905260408120919250815460ff1660028111156108af576108af610fa6565b141584906108d3576040516355b54a8d60e01b815260040161048791815260200190565b5060028101546001600160401b03908116908416810361090657604051632bf0186d60e21b815260040160405180910390fd5b6002825460ff16600281111561091e5761091e610fa6565b14801561093357505f816001600160401b0316115b801561094657506001600160401b038416155b1561097457600161095684610d26565b116109745760405163aff8365b60e01b815260040160405180910390fd5b60028201805467ffffffffffffffff19166001600160401b03868116918217909255604080519284168352602083019190915286917f4a7e1cc2e075be9a3c913bf00ec8ff10f6630f23433f65eee31e20ef69110ae4910160405180910390a25050505050565b5f807f237e158222e3e6968b72b9db0d8043aacf074ad9f650f0d1606b4d82ee432c0061071f565b610a0b610cad565b7f237e158222e3e6968b72b9db0d8043aacf074ad9f650f0d1606b4d82ee432c0080546001600160a01b0319166001600160a01b0383169081178255610a4f6106fb565b6001600160a01b03167f38d16b8cac22d99fc7c124b9cd0de2d3fa1faef420bfe791d8c362d765e2270060405160405180910390a35050565b610a90610cad565b5f5f5160206113f05f395f51905f525f838152602082905260408120919250815460ff166002811115610ac557610ac5610fa6565b14158390610ae9576040516355b54a8d60e01b815260040161048791815260200190565b506002815460ff166002811115610b0257610b02610fa6565b148015610b1b575060028101546001600160401b031615155b15610b49576001610b2b83610d26565b11610b495760405163aff8365b60e01b815260040160405180910390fd5b5f816001018054610b599061120d565b80601f0160208091040260200160405190810160405280929190818152602001828054610b859061120d565b8015610bd05780601f10610ba757610100808354040283529160200191610bd0565b820191905f5260205f20905b815481529060010190602001808311610bb357829003601f168201915b5050835160208501206002870154949550936001600160401b03169250610bfd9150506001860187610d8c565b505f868152602086905260408120805460ff1916815590610c216001830182610f5c565b50600201805467ffffffffffffffff191690555f828152600386016020908152604091829020805460ff1916905590516001600160401b038316815287917f70d022b699831c183688ed9d9d1a1cbf586698b506eaacfd5195a4e3738d285b910160405180910390a2505050505050565b5f610592825490565b5f610ca68383610d97565b9392505050565b33610cb66106fb565b6001600160a01b0316146106b15760405163118cdaa760e01b8152336004820152602401610487565b5f610ca68383610dbd565b7f237e158222e3e6968b72b9db0d8043aacf074ad9f650f0d1606b4d82ee432c0080546001600160a01b0319168155610d2282610e09565b5050565b5f5f610d3483600101610c92565b90505f5b81811015610d85575f610d4e6001860183610c9b565b5f818152602087905260409020600201549091506001600160401b031615610d7c57610d798461126d565b93505b50600101610d38565b5050919050565b5f610ca68383610e79565b5f825f018281548110610dac57610dac611245565b905f5260205f200154905092915050565b5f818152600183016020526040812054610e0257508154600181810184555f848152602080822090930184905584548482528286019093526040902091909155610592565b505f610592565b7f9016d09d72d40fdae2fd8ceac6b6234c7706214fd39c1cd1e609a0528c19930080546001600160a01b031981166001600160a01b03848116918217845560405192169182907f8be0079c531659141344cd1fd0a4f28419497f9722a3daafe3b4186f6b6457e0905f90a3505050565b5f8181526001830160205260408120548015610f53575f610e9b6001836113c8565b85549091505f90610eae906001906113c8565b9050808214610f0d575f865f018281548110610ecc57610ecc611245565b905f5260205f200154905080875f018481548110610eec57610eec611245565b5f918252602080832090910192909255918252600188019052604090208390555b8554869080610f1e57610f1e6113db565b600190038181905f5260205f20015f90559055856001015f8681526020019081526020015f205f905560019350505050610592565b5f915050610592565b508054610f689061120d565b5f825580601f10610f77575050565b601f0160209004905f5260205f20908101906106f891905b80821115610fa2575f8155600101610f8f565b5090565b634e487b7160e01b5f52602160045260245ffd5b60038110610fd657634e487b7160e01b5f52602160045260245ffd5b9052565b5f81518084528060208401602086015e5f602082860101526020601f19601f83011685010191505092915050565b611013828251610fba565b5f60208201516060602085015261102d6060850182610fda565b6040938401516001600160401b0316949093019390935250919050565b5f602082016020835280845180835260408501915060408160051b8601019250602086015f5b828110156110a157603f1987860301845261108c858351611008565b94506020938401939190910190600101611070565b50929695505050505050565b634e487b7160e01b5f52604160045260245ffd5b80356001600160401b03811681146110d7575f5ffd5b919050565b5f5f604083850312156110ed575f5ffd5b82356001600160401b03811115611102575f5ffd5b8301601f81018513611112575f5ffd5b80356001600160401b0381111561112b5761112b6110ad565b604051601f8201601f19908116603f011681016001600160401b0381118282101715611159576111596110ad565b604052818152828201602001871015611170575f5ffd5b816020840160208301375f60208383010152809450505050611194602084016110c1565b90509250929050565b5f602082840312156111ad575f5ffd5b5035919050565b602081525f610ca66020830184611008565b5f5f604083850312156111d7575f5ffd5b82359150611194602084016110c1565b5f602082840312156111f7575f5ffd5b81356001600160a01b0381168114610ca6575f5ffd5b600181811c9082168061122157607f821691505b60208210810361123f57634e487b7160e01b5f52602260045260245ffd5b50919050565b634e487b7160e01b5f52603260045260245ffd5b634e487b7160e01b5f52601160045260245ffd5b5f6001820161127e5761127e611259565b5060010190565b601f8211156112cc57805f5260205f20601f840160051c810160208510156112aa5750805b601f840160051c820191505b818110156112c9575f81556001016112b6565b50505b505050565b81516001600160401b038111156112ea576112ea6110ad565b6112fe816112f8845461120d565b84611285565b6020601f821160018114611330575f83156113195750848201515b5f19600385901b1c1916600184901b1784556112c9565b5f84815260208120601f198516915b8281101561135f578785015182556020948501946001909201910161133f565b508482101561137c57868401515f19600387901b60f8161c191681555b50505050600190811b01905550565b6001600160401b0383168152604060208201525f6113ac6040830184610fda565b949350505050565b82815260408101610ca66020830184610fba565b8181038181111561059257610592611259565b634e487b7160e01b5f52603160045260245ffdfeb58da0dce03316992faea3e12c60705b8ac05a309e27e3bc8421e5b271c9d200a2646970667358221220e5d087314472ac48a65ba1a9c38dbf61b71eb3c2cc0f9b03ade2ac62ec97aecc64736f6c634300081d0033" - }, - "0x3600000000000000000000000000000000000002": { - "balance": "0x0", - "nonce": "0x1", - "code": "0x608060405260043610610049575f3560e01c80633659cfe6146100535780634f1ef286146100725780635c60da1b146100855780638f283970146100b5578063f851a440146100d4575b6100516100e8565b005b34801561005e575f5ffd5b5061005161006d3660046104f3565b6100fa565b61005161008036600461050c565b610134565b348015610090575f5ffd5b50610099610197565b6040516001600160a01b03909116815260200160405180910390f35b3480156100c0575f5ffd5b506100516100cf3660046104f3565b6101a5565b3480156100df575f5ffd5b506100996101c5565b6100f86100f3610197565b6101ce565b565b6101026101ec565b6001600160a01b0316330361012c576101298160405180602001604052805f81525061021e565b50565b6101296100e8565b61013c6101ec565b6001600160a01b0316330361018f5761018a8383838080601f0160208091040260200160405190810160405280939291908181526020018383808284375f9201919091525061021e92505050565b505050565b61018a6100e8565b5f6101a0610277565b905090565b6101ad6101ec565b6001600160a01b0316330361012c576101298161029e565b5f6101a06101ec565b365f5f375f5f365f845af43d5f5f3e8080156101e8573d5ff35b3d5ffd5b5f7fb53127684a568b3173ae13b9f8a6016e243e63b6e8ee1178d6a717850b5d61035b546001600160a01b0316919050565b610227826102f2565b6040516001600160a01b038316907fbc7cd75a20ee27fd9adebab32041f755214dbc6bffa90cc0225b39da2e5c2d3b905f90a280511561026b5761018a8282610370565b6102736103e2565b5050565b5f7f360894a13ba1a3210667c828492db98dca3e2076cc3735a920a3ca505d382bbc61020f565b7f7e644d79422f17c01e4894b5f4f588d331ebfa28653d42ae832dc59e38c9798f6102c76101ec565b604080516001600160a01b03928316815291841660208301520160405180910390a161012981610401565b806001600160a01b03163b5f0361032c57604051634c9c8ce360e01b81526001600160a01b03821660048201526024015b60405180910390fd5b807f360894a13ba1a3210667c828492db98dca3e2076cc3735a920a3ca505d382bbc5b80546001600160a01b0319166001600160a01b039290921691909117905550565b60605f5f846001600160a01b03168460405161038c919061058a565b5f60405180830381855af49150503d805f81146103c4576040519150601f19603f3d011682016040523d82523d5f602084013e6103c9565b606091505b50915091506103d9858383610451565b95945050505050565b34156100f85760405163b398979f60e01b815260040160405180910390fd5b6001600160a01b03811661042a57604051633173bdd160e11b81525f6004820152602401610323565b807fb53127684a568b3173ae13b9f8a6016e243e63b6e8ee1178d6a717850b5d610361034f565b60608261046657610461826104b0565b6104a9565b815115801561047d57506001600160a01b0384163b155b156104a657604051639996b31560e01b81526001600160a01b0385166004820152602401610323565b50805b9392505050565b8051156104bf57805160208201fd5b60405163d6bda27560e01b815260040160405180910390fd5b80356001600160a01b03811681146104ee575f5ffd5b919050565b5f60208284031215610503575f5ffd5b6104a9826104d8565b5f5f5f6040848603121561051e575f5ffd5b610527846104d8565b9250602084013567ffffffffffffffff811115610542575f5ffd5b8401601f81018613610552575f5ffd5b803567ffffffffffffffff811115610568575f5ffd5b866020828401011115610579575f5ffd5b939660209190910195509293505050565b5f82518060208501845e5f92019182525091905056fea26469706673582212204c0881af9b906ca964ce2bc1cc4525350de8dd57658dacb806266290556eb9ae64736f6c634300081d0033", - "storage": { - "0xb53127684a568b3173ae13b9f8a6016e243e63b6e8ee1178d6a717850b5d6103": "0x00000000000000000000000020db45729bc366833107524804d16ceb44e946c6", - "0x360894a13ba1a3210667c828492db98dca3e2076cc3735a920a3ca505d382bbc": "0x000000000000000000000000fcc314dd5ad756c6bba725617438c0d25450a0de", - "0xf0c57e16840df040f15088dc2f81fe391c3923bec73e23a9662efc9c229c6a00": "0x0000000000000000000000000000000000000000000000000000000000000001", - "0x9016d09d72d40fdae2fd8ceac6b6234c7706214fd39c1cd1e609a0528c199300": "0x0000000000000000000000003600000000000000000000000000000000000003", - "0x0499f4c2de309a54da703ff88d2d815fa145b3fb32cf77b2528c8033db8fd7ee": "0x0000000000000000000000000000000000000000000000000000000000000002", - "0x0499f4c2de309a54da703ff88d2d815fa145b3fb32cf77b2528c8033db8fd7ef": "0x0000000000000000000000000000000000000000000000000000000000000041", - "0xe76fb05174802623d7f5b79138467ede24c07a48fb794feb0f195f82c29e25f6": "0x12a68aa84643efd6fb79b4097ef9b5ae1bef849c9c75e8eba2b977e09d227c35", - "0x0499f4c2de309a54da703ff88d2d815fa145b3fb32cf77b2528c8033db8fd7f0": "0x00000000000000000000000000000000000000000000000000000000000007d0", - "0x2559e4fd6bae46fd4a933fbdfc2024f30e3f30caf9b658f266112e99c269472e": "0x0000000000000000000000000000000000000000000000000000000000000001", - "0x3c22fd2722f4c68bee66b0771765c1374bf0b4f04712a4b14dee7a423e372484": "0x0000000000000000000000000000000000000000000000000000000000000001", - "0x69b629ad7a5ed64ea24217eca51dd1c81c56db4820bffd61bdc454fa86c45a37": "0x0000000000000000000000000000000000000000000000000000000000000001", - "0x799a8c5a05520e2bd7f998b6bef5c37455a07d2f4637750d18e8585f32667020": "0x0000000000000000000000000000000000000000000000000000000000000002", - "0x799a8c5a05520e2bd7f998b6bef5c37455a07d2f4637750d18e8585f32667021": "0x0000000000000000000000000000000000000000000000000000000000000041", - "0x88471ae418c3e7b280a001c3ca67ebba02159f936da01ae6a2e84cabdddd1ff9": "0xae89fa207bb808e8a34e05b5892a16625e8b66b92e597f8866aa586b487dfbfe", - "0x799a8c5a05520e2bd7f998b6bef5c37455a07d2f4637750d18e8585f32667022": "0x00000000000000000000000000000000000000000000000000000000000007d0", - "0x2559e4fd6bae46fd4a933fbdfc2024f30e3f30caf9b658f266112e99c269472f": "0x0000000000000000000000000000000000000000000000000000000000000002", - "0x095131cda7b4097ad9172b46969f424386afbffb0fe58d634a10ceead00fe90f": "0x0000000000000000000000000000000000000000000000000000000000000002", - "0x3aef2944b926031ba11588b90de1c1af67c2876a1083cfaadcc46dc3a23f394b": "0x0000000000000000000000000000000000000000000000000000000000000001", - "0x5823124c2578a0659c1d18b6eac7cd5d39d30d9e0a439c0dc79350c911078892": "0x0000000000000000000000000000000000000000000000000000000000000002", - "0x5823124c2578a0659c1d18b6eac7cd5d39d30d9e0a439c0dc79350c911078893": "0x0000000000000000000000000000000000000000000000000000000000000041", - "0xe4bbd97d770f505bda53f50c8dce4c86cb0dc400a1b5c97a9aacf7305b0907af": "0xd5c96c5d0daf70f25fb5bfc20c0747b2ce0408cc6e54b8494f3a62b61ffca7cd", - "0x5823124c2578a0659c1d18b6eac7cd5d39d30d9e0a439c0dc79350c911078894": "0x00000000000000000000000000000000000000000000000000000000000007d0", - "0x2559e4fd6bae46fd4a933fbdfc2024f30e3f30caf9b658f266112e99c2694730": "0x0000000000000000000000000000000000000000000000000000000000000003", - "0xe0ed4630bad2f511c8f295d4c67204cad855df4001547c1a63b7ba76c4642997": "0x0000000000000000000000000000000000000000000000000000000000000003", - "0x49c0e421f0d792e126c0e387181e587935dc85ee33a0e4ed906fd76b2f9693ea": "0x0000000000000000000000000000000000000000000000000000000000000001", - "0x57b2761a28897dbfc1a24bc792964a22adcbfdba40250a97c45c81e485bce164": "0x0000000000000000000000000000000000000000000000000000000000000002", - "0x57b2761a28897dbfc1a24bc792964a22adcbfdba40250a97c45c81e485bce165": "0x0000000000000000000000000000000000000000000000000000000000000041", - "0x7561b913640321094a9a7cd9b03d1d14d3f4896580bd453b53749e3a0a7f8f2f": "0xed259c1abb397823afb31cb36089547367221242de3057df803a0b091e9a1c8d", - "0x57b2761a28897dbfc1a24bc792964a22adcbfdba40250a97c45c81e485bce166": "0x00000000000000000000000000000000000000000000000000000000000007d0", - "0x2559e4fd6bae46fd4a933fbdfc2024f30e3f30caf9b658f266112e99c2694731": "0x0000000000000000000000000000000000000000000000000000000000000004", - "0x2b469f95fec2239186437567ca1ebd5a0dcb6f582da84f4a3e535a89c60299ce": "0x0000000000000000000000000000000000000000000000000000000000000004", - "0xd90b9770d39268440db589a25d9a3728ca5f5f41cd9a6f5d3bb3c9c0233e6014": "0x0000000000000000000000000000000000000000000000000000000000000001", - "0x5217b172c9c68060ac3f2cea97f04d4b6a19ffc38de72bde7614d108da793171": "0x0000000000000000000000000000000000000000000000000000000000000002", - "0x5217b172c9c68060ac3f2cea97f04d4b6a19ffc38de72bde7614d108da793172": "0x0000000000000000000000000000000000000000000000000000000000000041", - "0xb81fd3b52b30fb1cee951216c9225dedbf87cab17122027c6b61d64229d70ce1": "0xf43562191fe65ba250ac32d4602efb664586bf4e8150e4c80cf2dbf64650d2a9", - "0x5217b172c9c68060ac3f2cea97f04d4b6a19ffc38de72bde7614d108da793173": "0x00000000000000000000000000000000000000000000000000000000000007d0", - "0x2559e4fd6bae46fd4a933fbdfc2024f30e3f30caf9b658f266112e99c2694732": "0x0000000000000000000000000000000000000000000000000000000000000005", - "0x1f0160c61ae74490159fc3ec6e25ec82a084ca7c5a75f5a3e2fb22830c3d10db": "0x0000000000000000000000000000000000000000000000000000000000000005", - "0x762650592f09e61c6bfeb64d440a3ee1b1cca58fc3e4b2ede70cf75b4603c6ed": "0x0000000000000000000000000000000000000000000000000000000000000001", - "0x036c38b635f62c71be584c9bd1b40365acf30671f9f753f98080692c1a7880c9": "0x0000000000000000000000000000000000000000000000000000000000000002", - "0x036c38b635f62c71be584c9bd1b40365acf30671f9f753f98080692c1a7880ca": "0x0000000000000000000000000000000000000000000000000000000000000041", - "0xcf73efe09ec4d087234a83ec0d87436622fc85763f310d044eb6fa2053eb09e9": "0xf927d6659307a219c3a59b51a31c29412b6d2b1c1717697a0ae0d5c9999ba740", - "0x036c38b635f62c71be584c9bd1b40365acf30671f9f753f98080692c1a7880cb": "0x00000000000000000000000000000000000000000000000000000000000007d0", - "0x2559e4fd6bae46fd4a933fbdfc2024f30e3f30caf9b658f266112e99c2694733": "0x0000000000000000000000000000000000000000000000000000000000000006", - "0x83618a90b7985f85f10fb07aeb429c9df200b04d98de4740e2108171b60b9ba4": "0x0000000000000000000000000000000000000000000000000000000000000006", - "0x0a50deff56ce24acaf1ca5cb14126c2427e9f4cab71cc938011fb80d615013ed": "0x0000000000000000000000000000000000000000000000000000000000000001", - "0x7d4d1af396c360acccda295ce959178f0a4741206f750526f43f024051b37c9a": "0x0000000000000000000000000000000000000000000000000000000000000002", - "0x7d4d1af396c360acccda295ce959178f0a4741206f750526f43f024051b37c9b": "0x0000000000000000000000000000000000000000000000000000000000000041", - "0xc5ecd545e569aa1fc231f287db5e889a0a7f8bf1c6040f6219e3417aa6cd3a69": "0xda4f31b2d26acac4143d67f6b0ebc5a7b9fa829e4b63970f28130bff361316c9", - "0x7d4d1af396c360acccda295ce959178f0a4741206f750526f43f024051b37c9c": "0x00000000000000000000000000000000000000000000000000000000000007d0", - "0x2559e4fd6bae46fd4a933fbdfc2024f30e3f30caf9b658f266112e99c2694734": "0x0000000000000000000000000000000000000000000000000000000000000007", - "0xaa91b6cbf3a62bac4363f28c9e7a17ed03fa172b9e36d4d699be2e0ba2104a29": "0x0000000000000000000000000000000000000000000000000000000000000007", - "0x9f29139c8b3ecf5fbec2c769003875b0502f8bd2e08b6cb11dfeb19d50b8a69e": "0x0000000000000000000000000000000000000000000000000000000000000001", - "0x6ebdd2e8f28a6177d4fd7d1440de91296636b8ba71407c25ccc02e95a11d5959": "0x0000000000000000000000000000000000000000000000000000000000000002", - "0x6ebdd2e8f28a6177d4fd7d1440de91296636b8ba71407c25ccc02e95a11d595a": "0x0000000000000000000000000000000000000000000000000000000000000041", - "0xdc3456907dbce854efbe5fab4c6dd881a300a762d86e448ce767c600db77d6dc": "0x7bd0ca04c9423d39c25ee7747d2a5977acf19ff9d7f835f8dc1a746c83e71070", - "0x6ebdd2e8f28a6177d4fd7d1440de91296636b8ba71407c25ccc02e95a11d595b": "0x00000000000000000000000000000000000000000000000000000000000007d0", - "0x2559e4fd6bae46fd4a933fbdfc2024f30e3f30caf9b658f266112e99c2694735": "0x0000000000000000000000000000000000000000000000000000000000000008", - "0xa40cb6afff5604565ae4febfee9e9b6d28aac41cc6b469b086a5c8c372146e04": "0x0000000000000000000000000000000000000000000000000000000000000008", - "0xe48ec2b679bea5ea62eeb32cdf010d3534f751b0981989eb6f5135ace326e0d5": "0x0000000000000000000000000000000000000000000000000000000000000001", - "0x1036a022fb9dcf8f9f9ca34ad6b12868abc3041e29322d43806d11dfd2808b54": "0x0000000000000000000000000000000000000000000000000000000000000002", - "0x1036a022fb9dcf8f9f9ca34ad6b12868abc3041e29322d43806d11dfd2808b55": "0x0000000000000000000000000000000000000000000000000000000000000041", - "0x9aa5d26f5fcc5e1e798ab3b392cbd29d19e0e696108ce71f798ed565faf3c83b": "0xc3fbb251a140db946283233a3fe5762a05a8305e2a8eaefc41d5df2820e071fc", - "0x1036a022fb9dcf8f9f9ca34ad6b12868abc3041e29322d43806d11dfd2808b56": "0x00000000000000000000000000000000000000000000000000000000000007d0", - "0x2559e4fd6bae46fd4a933fbdfc2024f30e3f30caf9b658f266112e99c2694736": "0x0000000000000000000000000000000000000000000000000000000000000009", - "0x2ce5e9b7f4d6cc2943d8914ec8fc903f2be8b733f74fca6e808d595ab7c333a4": "0x0000000000000000000000000000000000000000000000000000000000000009", - "0xcce14a1bd7d43386fe00a9f61cc79c3cb7b3416a9a5d35d2046bb8a4f9beac9e": "0x0000000000000000000000000000000000000000000000000000000000000001", - "0x9bdfb94158fad0c6f7b28212d5c61394304fac97217ff5b47fb1264fb4d3d263": "0x0000000000000000000000000000000000000000000000000000000000000002", - "0x9bdfb94158fad0c6f7b28212d5c61394304fac97217ff5b47fb1264fb4d3d264": "0x0000000000000000000000000000000000000000000000000000000000000041", - "0xfdb1936dbf9415dbe0461d4456dcf559f839ed0cf1e9580a9f8264ef6427550b": "0xdeb9620fb017834b90e4b0cf66fdc7fd06b3edb68f0147938e59a9fa7db83380", - "0x9bdfb94158fad0c6f7b28212d5c61394304fac97217ff5b47fb1264fb4d3d265": "0x00000000000000000000000000000000000000000000000000000000000007d0", - "0x2559e4fd6bae46fd4a933fbdfc2024f30e3f30caf9b658f266112e99c2694737": "0x000000000000000000000000000000000000000000000000000000000000000a", - "0xdc0be93e54ba6a7b6fe6e83b098eef5a9f0c78c7a0d0527364d7e5fc618e3e55": "0x000000000000000000000000000000000000000000000000000000000000000a", - "0x2032597be0a44488174621966df442c8c77e5c686ec742a4600cb0cd295f8561": "0x0000000000000000000000000000000000000000000000000000000000000001", - "0x29c5c54da8fa03ee409f98f51fe246e9e0fab30d34a5258186bfe0c2feabaaa3": "0x0000000000000000000000000000000000000000000000000000000000000002", - "0x29c5c54da8fa03ee409f98f51fe246e9e0fab30d34a5258186bfe0c2feabaaa4": "0x0000000000000000000000000000000000000000000000000000000000000041", - "0x069f8539631136694cc8b55b72f751fc9b09894db1e54fc694ed567b3f9f6eab": "0x254192a339b277911fae7dac3a52ed4b0196bb90670fb3b893f521849086fc8d", - "0x29c5c54da8fa03ee409f98f51fe246e9e0fab30d34a5258186bfe0c2feabaaa5": "0x00000000000000000000000000000000000000000000000000000000000007d0", - "0x2559e4fd6bae46fd4a933fbdfc2024f30e3f30caf9b658f266112e99c2694738": "0x000000000000000000000000000000000000000000000000000000000000000b", - "0x0180ad68974e030dadf48dba740cc80485e9121ea87ebc1bf162543accaf3904": "0x000000000000000000000000000000000000000000000000000000000000000b", - "0x07e80b26f488a70b4989560a271056bd9762fb9fabea5a36d0af4e627b669688": "0x0000000000000000000000000000000000000000000000000000000000000001", - "0xb58da0dce03316992faea3e12c60705b8ac05a309e27e3bc8421e5b271c9d201": "0x000000000000000000000000000000000000000000000000000000000000000b", - "0xb58da0dce03316992faea3e12c60705b8ac05a309e27e3bc8421e5b271c9d204": "0x000000000000000000000000000000000000000000000000000000000000000c" - } - }, - "0x98C74F7eF54BA22be94415d7C967B352c6a42D6d": { - "balance": "0x0", - "nonce": "0x1", - "code": "0x608060405234801561000f575f5ffd5b50600436106101dc575f3560e01c806379ba509711610109578063d2c20cb31161009e578063ead130111161006e578063ead130111461042f578063ec4a44d714610443578063f2fde38b14610457578063f6a74ed71461046a575f5ffd5b8063d2c20cb3146103ee578063d773a74114610401578063da3003f914610414578063e30c397814610427575f5ffd5b80638da5cb5b116100d95780638da5cb5b146103955780639fd0506d1461039d578063aa5ecb52146103a5578063b429afeb146103b8575f5ffd5b806379ba5097146103765780637f77403d1461037e5780637ffc51fd146103865780638456cb591461038d575f5ffd5b8063485cc9551161017f5780635c975abb1161014f5780635c975abb1461031c5780635e5feee61461033a578063602a9eee1461034d578063715018a61461036e575f5ffd5b8063485cc955146102b85780634cb4850a146102cb578063554bab3c146102de5780635acac2b6146102f1575f5ffd5b8063263a3402116101ba578063263a34021461028b5780632eb5c6581461029557806337ec36d1146102a85780633f4ba83a146102b0575f5ffd5b8063048544a4146101e057806306433b1b1461022c5780631904bb2e1461026b575b5f5ffd5b6102176101ee366004611514565b6001600160a01b03165f9081525f5160206118f35f395f51905f52602052604090205460ff1690565b60405190151581526020015b60405180910390f35b6102537f000000000000000000000000360000000000000000000000000000000000000281565b6040516001600160a01b039091168152602001610223565b61027e610279366004611514565b61047d565b604051610223919061155b565b610293610558565b005b6102936102a33660046115d1565b610603565b6102936106e7565b610293610787565b6102936102c6366004611606565b6107e5565b6102936102d9366004611637565b610971565b6102936102ec366004611514565b610a92565b6103046102ff366004611514565b610b16565b6040516001600160401b039091168152602001610223565b5f5160206118d35f395f51905f5254600160a01b900460ff16610217565b610293610348366004611514565b610b84565b61036061035b3660046116e4565b610c5f565b604051908152602001610223565b610293610d06565b610293610d19565b610293610d5e565b6103045f81565b610293610ddc565b610253610e3e565b610253610e72565b6102936103b3366004611514565b610e87565b6102176103c6366004611514565b6001600160a01b03165f9081525f5160206118b35f395f51905f526020526040902054151590565b6102936103fc36600461175d565b610f46565b61036061040f366004611514565b611055565b610293610422366004611514565b6110a2565b610253611165565b6103605f5160206118b35f395f51905f5281565b6103605f5160206118f35f395f51905f5281565b610293610465366004611514565b61118d565b610293610478366004611514565b611212565b6104a06040805160608101909152805f8152606060208201525f60409091015290565b5f5f5160206118b35f395f51905f526001600160a01b038481165f908152602083905260409081902054905163b5d8962760e01b815260048101829052929350917f00000000000000000000000036000000000000000000000000000000000000029091169063b5d89627906024015f60405180830381865afa158015610529573d5f5f3e3d5ffd5b505050506040513d5f823e601f3d908101601f1916820160405261055091908101906117a5565b949350505050565b6105606112e4565b610568611321565b335f9081525f5160206118b35f395f51905f526020819052604091829020549151634ebae61760e01b8152600481018390529091907f00000000000000000000000036000000000000000000000000000000000000026001600160a01b031690634ebae617906024015b5f604051808303815f87803b1580156105e9575f5ffd5b505af11580156105fb573d5f5f3e3d5ffd5b505050505050565b61060b611359565b6001600160a01b038216610632576040516371ab47bb60e11b815260040160405180910390fd5b6001600160a01b0382165f9081525f5160206118b35f395f51905f5260208190526040822054909103610678576040516308c2ec5b60e41b815260040160405180910390fd5b6001600160a01b0383165f818152600183016020908152604091829020805467ffffffffffffffff19166001600160401b03871690811790915591519182527fcf03087b939040458ea3c3a6791532c7c4432baf98e770abc930afc9cda81d87910160405180910390a2505050565b6106ef611359565b7f00000000000000000000000036000000000000000000000000000000000000026001600160a01b03166379ba50976040518163ffffffff1660e01b81526004015f604051808303815f87803b158015610747575f5ffd5b505af1158015610759573d5f5f3e3d5ffd5b50506040517f54b70ab40993761a2b0e96f42fdf47939a56830ede1325df83e05ed33e3e8fd592505f9150a1565b61078f61138b565b5f5160206118d35f395f51905f528054600160a01b900460ff16156107e257805460ff60a01b191681556040517f7805862f689e2f13df9f062ff482ad3ad112aca9e0847911ed832e158c525b33905f90a15b50565b7ff0c57e16840df040f15088dc2f81fe391c3923bec73e23a9662efc9c229c6a008054600160401b810460ff1615906001600160401b03165f811580156108295750825b90505f826001600160401b031660011480156108445750303b155b905081158015610852575080155b156108705760405163f92ee8a960e01b815260040160405180910390fd5b845467ffffffffffffffff19166001178555831561089a57845460ff60401b1916600160401b1785555b6001600160a01b0387166108c1576040516342cad95760e01b815260040160405180910390fd5b6001600160a01b0386166108e85760405163a74995ab60e01b815260040160405180910390fd5b6108f06113c3565b6108f9876113cb565b5f5f5160206118d35f395f51905f5280546001600160a01b0319166001600160a01b03891617905550831561096857845460ff60401b19168555604051600181527fc7f505b2f371ae2175ee4913f4499e1f2633a7b5936321eed1cdaeb6115181d29060200160405180910390a15b50505050505050565b6109796112e4565b610981611321565b335f9081525f5160206118b35f395f51905f5260208181526040808420547fe90ec3add3e251bfbe914c9e482b511e91a3b187718c1dc10223f64a8a644a01909252909220549091906001600160401b03908116908416811015610a0857604051635af79e0d60e11b81526001600160401b03821660048201526024015b60405180910390fd5b60405163312267c360e21b8152600481018390526001600160401b03851660248201527f00000000000000000000000036000000000000000000000000000000000000026001600160a01b03169063c4899f0c906044015f604051808303815f87803b158015610a76575f5ffd5b505af1158015610a88573d5f5f3e3d5ffd5b5050505050505050565b610a9a611359565b6001600160a01b038116610ac15760405163a74995ab60e01b815260040160405180910390fd5b5f5160206118d35f395f51905f5280546001600160a01b0319166001600160a01b03831690811782556040517fb80482a293ca2e013eda8683c9bd7fc8347cfdaeea5ede58cba46df502c2a604905f90a25050565b6001600160a01b0381165f9081525f5160206118b35f395f51905f52602081905260408220548203610b5b576040516308c2ec5b60e41b815260040160405180910390fd5b6001600160a01b039092165f90815260019092016020525060409020546001600160401b031690565b610b8c611359565b6001600160a01b038116610bb3576040516342cad95760e01b815260040160405180910390fd5b60405163f2fde38b60e01b81526001600160a01b0382811660048301527f0000000000000000000000003600000000000000000000000000000000000002169063f2fde38b906024015f604051808303815f87803b158015610c13575f5ffd5b505af1158015610c25573d5f5f3e3d5ffd5b50506040516001600160a01b03841692507ff8e2a8d4b93bd709cc57c9f21b79403c532f960ef18f77d0c23403cae0de78d991505f90a250565b5f610c68611407565b610c70611321565b604051631234eb3960e11b81526001600160a01b037f00000000000000000000000036000000000000000000000000000000000000021690632469d67290610cbe9085905f90600401611871565b6020604051808303815f875af1158015610cda573d5f5f3e3d5ffd5b505050506040513d601f19601f82011682018060405250810190610cfe919061189b565b90505b919050565b610d0e611359565b610d175f6113cb565b565b3380610d23611165565b6001600160a01b031614610d555760405163118cdaa760e01b81526001600160a01b03821660048201526024016109ff565b6107e2816113cb565b610d666112e4565b610d6e611321565b335f9081525f5160206118b35f395f51905f52602081905260409182902054915163f94e186760e01b8152600481018390529091907f00000000000000000000000036000000000000000000000000000000000000026001600160a01b03169063f94e1867906024016105d2565b610de461138b565b5f5160206118d35f395f51905f528054600160a01b900460ff166107e257805460ff60a01b1916600160a01b1781556040517f6985a02210a168e66602d3235cb6db0e70f92b3ba4d376a33c0f3d9434bff625905f90a150565b5f807f9016d09d72d40fdae2fd8ceac6b6234c7706214fd39c1cd1e609a0528c1993005b546001600160a01b031692915050565b5f805f5160206118d35f395f51905f52610e62565b610e8f611359565b6001600160a01b038116610eb657604051634a1653dd60e11b815260040160405180910390fd5b6001600160a01b0381165f9081525f5160206118f35f395f51905f52602081905260409091205460ff16610efd576040516335c0bed760e21b815260040160405180910390fd5b6001600160a01b0382165f81815260208390526040808220805460ff19169055517f195d741377fcd6e23556d115769d0b5f54decc24349a916f6de7371077fe7fd99190a25050565b610f4e611359565b815f03610f6e576040516309e56b6960e21b815260040160405180910390fd5b6001600160a01b038316610f95576040516371ab47bb60e11b815260040160405180910390fd5b6001600160a01b0383165f9081525f5160206118b35f395f51905f52602081905260409091205415610fda57604051633dd8918760e01b815260040160405180910390fd5b6001600160a01b0384165f8181526020838152604080832087905560018501825291829020805467ffffffffffffffff19166001600160401b03871690811790915591519182528592917fa1f041d5612451bd93cf2d0b47fbfde4782731f7612386774a8ab2b4caf57955910160405180910390a350505050565b6001600160a01b0381165f9081525f5160206118b35f395f51905f526020819052604082205480830361109b576040516308c2ec5b60e41b815260040160405180910390fd5b9392505050565b6110aa611359565b6001600160a01b0381166110d157604051634a1653dd60e11b815260040160405180910390fd5b6001600160a01b0381165f9081525f5160206118f35f395f51905f52602081905260409091205460ff16156111195760405163348917ed60e01b815260040160405180910390fd5b6001600160a01b0382165f81815260208390526040808220805460ff19166001179055517fd25faf73acccddf9a7ac429be872d2109cd7bd0c2370658b7c019518f3159d8e9190a25050565b5f807f237e158222e3e6968b72b9db0d8043aacf074ad9f650f0d1606b4d82ee432c00610e62565b611195611359565b7f237e158222e3e6968b72b9db0d8043aacf074ad9f650f0d1606b4d82ee432c0080546001600160a01b0319166001600160a01b03831690811782556111d9610e3e565b6001600160a01b03167f38d16b8cac22d99fc7c124b9cd0de2d3fa1faef420bfe791d8c362d765e2270060405160405180910390a35050565b61121a611359565b6001600160a01b038116611241576040516371ab47bb60e11b815260040160405180910390fd5b6001600160a01b0381165f9081525f5160206118b35f395f51905f5260208190526040822054909103611287576040516308c2ec5b60e41b815260040160405180910390fd5b6001600160a01b0382165f8181526020838152604080832083905560018501909152808220805467ffffffffffffffff19169055517f33d83959be2573f5453b12eb9d43b3499bc57d96bd2f067ba44803c859e811139190a25050565b335f9081525f5160206118b35f395f51905f52602081905260408220549091036107e257604051630e971e6360e11b815260040160405180910390fd5b5f5160206118d35f395f51905f528054600160a01b900460ff16156107e25760405163ab35696f60e01b815260040160405180910390fd5b33611362610e3e565b6001600160a01b031614610d175760405163118cdaa760e01b81523360048201526024016109ff565b5f5160206118d35f395f51905f5280546001600160a01b031633146107e25760405163daeefd6560e01b815260040160405180910390fd5b610d17611445565b7f237e158222e3e6968b72b9db0d8043aacf074ad9f650f0d1606b4d82ee432c0080546001600160a01b03191681556114038261148e565b5050565b335f9081525f5160206118f35f395f51905f52602081905260409091205460ff166107e25760405163eb4086bb60e01b815260040160405180910390fd5b7ff0c57e16840df040f15088dc2f81fe391c3923bec73e23a9662efc9c229c6a0054600160401b900460ff16610d1757604051631afcd79f60e31b815260040160405180910390fd5b7f9016d09d72d40fdae2fd8ceac6b6234c7706214fd39c1cd1e609a0528c19930080546001600160a01b031981166001600160a01b03848116918217845560405192169182907f8be0079c531659141344cd1fd0a4f28419497f9722a3daafe3b4186f6b6457e0905f90a3505050565b80356001600160a01b0381168114610d01575f5ffd5b5f60208284031215611524575f5ffd5b61109b826114fe565b5f81518084528060208401602086015e5f602082860101526020601f19601f83011685010191505092915050565b602081525f82516003811061157e57634e487b7160e01b5f52602160045260245ffd5b8060208401525060208301516060604084015261159e608084018261152d565b90506001600160401b0360408501511660608401528091505092915050565b6001600160401b03811681146107e2575f5ffd5b5f5f604083850312156115e2575f5ffd5b6115eb836114fe565b915060208301356115fb816115bd565b809150509250929050565b5f5f60408385031215611617575f5ffd5b611620836114fe565b915061162e602084016114fe565b90509250929050565b5f60208284031215611647575f5ffd5b813561109b816115bd565b634e487b7160e01b5f52604160045260245ffd5b604051606081016001600160401b038111828210171561168857611688611652565b60405290565b604051601f8201601f191681016001600160401b03811182821017156116b6576116b6611652565b604052919050565b5f6001600160401b038211156116d6576116d6611652565b50601f01601f191660200190565b5f602082840312156116f4575f5ffd5b81356001600160401b03811115611709575f5ffd5b8201601f81018413611719575f5ffd5b803561172c611727826116be565b61168e565b818152856020838501011115611740575f5ffd5b816020840160208301375f91810160200191909152949350505050565b5f5f5f6060848603121561176f575f5ffd5b611778846114fe565b925060208401359150604084013561178f816115bd565b809150509250925092565b8051610d01816115bd565b5f602082840312156117b5575f5ffd5b81516001600160401b038111156117ca575f5ffd5b8201606081850312156117db575f5ffd5b6117e3611666565b8151600381106117f1575f5ffd5b815260208201516001600160401b0381111561180b575f5ffd5b8201601f8101861361181b575f5ffd5b8051611829611727826116be565b81815287602083850101111561183d575f5ffd5b8160208401602083015e5f602083830101528060208501525050506118646040830161179a565b6040820152949350505050565b604081525f611883604083018561152d565b90506001600160401b03831660208301529392505050565b5f602082840312156118ab575f5ffd5b505191905056fee90ec3add3e251bfbe914c9e482b511e91a3b187718c1dc10223f64a8a644a000642d7922329a434cf4fd17a3c95eb692c24fd95f9f94d0b55420a5d895f4a0036c39aeb5f498ae36546fc14573b003abf87227a5a2df6caec16ee566f1ad800a2646970667358221220642f9a6a9dcca31815d2c62a7e16a0101b8be0fd9834c20c55b2b5bfc4847b8f64736f6c634300081d0033" - }, - "0x3600000000000000000000000000000000000003": { - "balance": "0x0", - "nonce": "0x1", - "code": "0x608060405260043610610049575f3560e01c80633659cfe6146100535780634f1ef286146100725780635c60da1b146100855780638f283970146100b5578063f851a440146100d4575b6100516100e8565b005b34801561005e575f5ffd5b5061005161006d3660046104f3565b6100fa565b61005161008036600461050c565b610134565b348015610090575f5ffd5b50610099610197565b6040516001600160a01b03909116815260200160405180910390f35b3480156100c0575f5ffd5b506100516100cf3660046104f3565b6101a5565b3480156100df575f5ffd5b506100996101c5565b6100f86100f3610197565b6101ce565b565b6101026101ec565b6001600160a01b0316330361012c576101298160405180602001604052805f81525061021e565b50565b6101296100e8565b61013c6101ec565b6001600160a01b0316330361018f5761018a8383838080601f0160208091040260200160405190810160405280939291908181526020018383808284375f9201919091525061021e92505050565b505050565b61018a6100e8565b5f6101a0610277565b905090565b6101ad6101ec565b6001600160a01b0316330361012c576101298161029e565b5f6101a06101ec565b365f5f375f5f365f845af43d5f5f3e8080156101e8573d5ff35b3d5ffd5b5f7fb53127684a568b3173ae13b9f8a6016e243e63b6e8ee1178d6a717850b5d61035b546001600160a01b0316919050565b610227826102f2565b6040516001600160a01b038316907fbc7cd75a20ee27fd9adebab32041f755214dbc6bffa90cc0225b39da2e5c2d3b905f90a280511561026b5761018a8282610370565b6102736103e2565b5050565b5f7f360894a13ba1a3210667c828492db98dca3e2076cc3735a920a3ca505d382bbc61020f565b7f7e644d79422f17c01e4894b5f4f588d331ebfa28653d42ae832dc59e38c9798f6102c76101ec565b604080516001600160a01b03928316815291841660208301520160405180910390a161012981610401565b806001600160a01b03163b5f0361032c57604051634c9c8ce360e01b81526001600160a01b03821660048201526024015b60405180910390fd5b807f360894a13ba1a3210667c828492db98dca3e2076cc3735a920a3ca505d382bbc5b80546001600160a01b0319166001600160a01b039290921691909117905550565b60605f5f846001600160a01b03168460405161038c919061058a565b5f60405180830381855af49150503d805f81146103c4576040519150601f19603f3d011682016040523d82523d5f602084013e6103c9565b606091505b50915091506103d9858383610451565b95945050505050565b34156100f85760405163b398979f60e01b815260040160405180910390fd5b6001600160a01b03811661042a57604051633173bdd160e11b81525f6004820152602401610323565b807fb53127684a568b3173ae13b9f8a6016e243e63b6e8ee1178d6a717850b5d610361034f565b60608261046657610461826104b0565b6104a9565b815115801561047d57506001600160a01b0384163b155b156104a657604051639996b31560e01b81526001600160a01b0385166004820152602401610323565b50805b9392505050565b8051156104bf57805160208201fd5b60405163d6bda27560e01b815260040160405180910390fd5b80356001600160a01b03811681146104ee575f5ffd5b919050565b5f60208284031215610503575f5ffd5b6104a9826104d8565b5f5f5f6040848603121561051e575f5ffd5b610527846104d8565b9250602084013567ffffffffffffffff811115610542575f5ffd5b8401601f81018613610552575f5ffd5b803567ffffffffffffffff811115610568575f5ffd5b866020828401011115610579575f5ffd5b939660209190910195509293505050565b5f82518060208501845e5f92019182525091905056fea26469706673582212204c0881af9b906ca964ce2bc1cc4525350de8dd57658dacb806266290556eb9ae64736f6c634300081d0033", - "storage": { - "0xb53127684a568b3173ae13b9f8a6016e243e63b6e8ee1178d6a717850b5d6103": "0x000000000000000000000000131e6b8e466ac8046c38c3ae7de77595cfeaf0d1", - "0x360894a13ba1a3210667c828492db98dca3e2076cc3735a920a3ca505d382bbc": "0x00000000000000000000000098c74f7ef54ba22be94415d7c967b352c6a42d6d", - "0xf0c57e16840df040f15088dc2f81fe391c3923bec73e23a9662efc9c229c6a00": "0x0000000000000000000000000000000000000000000000000000000000000001", - "0x9016d09d72d40fdae2fd8ceac6b6234c7706214fd39c1cd1e609a0528c199300": "0x00000000000000000000000020e61a9cc8d010928aa9997e4e773e84de1b8306", - "0x0642d7922329a434cf4fd17a3c95eb692c24fd95f9f94d0b55420a5d895f4a00": "0x0000000000000000000000005bc1d44f8e844863cbc45d2f425f4c3758faf7b8", - "0x1c6ec1cacbb518b0389f77529ac2e30d6563e25886268b41b48d50c6395a8d2d": "0x0000000000000000000000000000000000000000000000000000000000000001", - "0x548a5b3f2c3df95f9d3b8a4dd2bc69ecc72a131136c94413ef16f9b4e537f335": "0x0000000000000000000000000000000000000000000000000000000000000001", - "0xec1e055d6238aede62b966e68ea8e0a0b21c4af7ef8389e2a5ee0e2c385a8c73": "0x0000000000000000000000000000000000000000000000000000000000000002", - "0x483764f4055fa8283ecf55dce48062d552970414119757e1e8a63991968b541a": "0x0000000000000000000000000000000000000000000000000000000000000002", - "0x0967dd3edc72a1cb057b905c3c9359f9251b0a2c12052760c16d77e23de717c6": "0x0000000000000000000000000000000000000000000000000000000000000003", - "0xe7f0e043144823913b30543d456b0f772cb9646ed9471337963c7f3684a16213": "0x0000000000000000000000000000000000000000000000000000000000000003", - "0xaf82ff8349a17d14627c0aff6abb73110b9c7e14624691f2063ba28af9fbc7d4": "0x0000000000000000000000000000000000000000000000000000000000000004", - "0x7bbffe6a7d08f11d15300d7505c818e267c0b80e9d85c55cadaa5ae125757ebe": "0x0000000000000000000000000000000000000000000000000000000000000004", - "0xacf4a072e0ad7c16d4dd5f029b0a39e9a33cf1c9055df127840e04958db38252": "0x0000000000000000000000000000000000000000000000000000000000000005", - "0x27afa9776521e070c3bf1b7db944adb6688e2279f8c75a7eccc8b4abe8e02c6f": "0x0000000000000000000000000000000000000000000000000000000000000005", - "0x4d0a40b24b120de62c44fea1d981476a936f2db6856a9ad380655a40893ce222": "0x0000000000000000000000000000000000000000000000000000000000000006", - "0xd640da1027cd77fbc230500682845251adcb8527179d8a412900e6daf6df5280": "0x0000000000000000000000000000000000000000000000000000000000000006", - "0x163ffef6869e4331a9bc5187b959463cc2d80968d25e55c6b73afbfae8800693": "0x0000000000000000000000000000000000000000000000000000000000000007", - "0xa9b56aa859b852d420f7fabe9cc332dc0c4c4decb3c7d8d470a46736f4e676e9": "0x0000000000000000000000000000000000000000000000000000000000000007", - "0xb3c0c2336a4da51d23a2109cc38ca1cbcd8e41e007cb90005293dc1dbc024554": "0x0000000000000000000000000000000000000000000000000000000000000008", - "0x1b8b25ac282cb00bf2a429aee329a75321b9046048a916088991b8f31379209d": "0x0000000000000000000000000000000000000000000000000000000000000008", - "0x590d7fad0db97d79e0cc340a95f6230133d58f063d82e3ae4829320d45eefa67": "0x0000000000000000000000000000000000000000000000000000000000000009", - "0x36f7325e9d8622c16e5e692fb1a76494187284ed832dcf704bd88142d60be122": "0x0000000000000000000000000000000000000000000000000000000000000009", - "0x7b285fa910def941cb8d797ff7ff9e503e858cf9305e192bab943eea83dbbde7": "0x000000000000000000000000000000000000000000000000000000000000000a", - "0x0cd41ab158b6d95adf760c7842ceb9d75cf60165c1a5976e833d6fc55abdd248": "0x000000000000000000000000000000000000000000000000000000000000000a", - "0xc0e489cbfb5efacf149851728e17e49373162a6636f86152b51575ce2e61e39d": "0x000000000000000000000000000000000000000000000000000000000000000b", - "0x047d7247dc4ef04489e2bf1c5432acd01930a1cf4974c2479d2951a44ba851cc": "0x000000000000000000000000000000000000000000000000000000000000000b", - "0xe3b95e004499627f9ba49eceea49b136dd732e7539bb3659450e89704c607f27": "0x00000000000000000000000000000000000000000000000000000000000007d0", - "0xde25968ff4be3538ffa34e06752bfb0cc70f8e911a0efa6c7803fa9ff47c8233": "0x0000000000000000000000000000000000000000000000000000000000000000", - "0x085d1157429fc641e3d17c9054502e4347248f6d17a7092d1817ac49e9f9d172": "0x00000000000000000000000000000000000000000000000000000000000007d0", - "0x44c6dcda9372fa2eb1684d5b99459494830f6ca2385bd25d8fefa582911e8121": "0x0000000000000000000000000000000000000000000000000000000000000000", - "0xae72526874b4d44523f9bebcca264c14a1b41aef716271d2e4b80715ddb6c2ec": "0x00000000000000000000000000000000000000000000000000000000000007d0", - "0xb9a88c9aa828493b972e4b55c73da46d4ea14d798639462f1c25355ca173df63": "0x0000000000000000000000000000000000000000000000000000000000000000", - "0xee9d097b164ce84829f407edbdf171d5667324ce2788c9b8b6ae54c361cd15a9": "0x00000000000000000000000000000000000000000000000000000000000007d0", - "0x8ad6ac7dee1dbfa50bf134be6f65b1791a8d7e768fc9ebf8f21aa887adc3cf4e": "0x0000000000000000000000000000000000000000000000000000000000000000", - "0xc1665827e69e6438bb9a669229c4443a6ad6b9848fd933c44131ba119d116ee3": "0x00000000000000000000000000000000000000000000000000000000000007d0", - "0xcc15aecc4a120be3518700169a5fb3d0b56df7f538f1839aa09d96695274247e": "0x0000000000000000000000000000000000000000000000000000000000000000", - "0xec76d38fc12b1c3448395ec2fdebdac75a9dcde690180298c4332c412c8e8c1c": "0x00000000000000000000000000000000000000000000000000000000000007d0", - "0x0ab453a5839bbbca8d73b8429848be79a5adbc5cf3990aabcca065fb951968e6": "0x0000000000000000000000000000000000000000000000000000000000000000", - "0xffaf135ec0bcd44b22aa46b43e9f3ba3d2728e2a60d4c55e3e1a2a0b2f73bb3a": "0x00000000000000000000000000000000000000000000000000000000000007d0", - "0xfa4b5c618c52f975495435c4c260c2949b48ed5f9a0fbefea6fcbf4d067e876a": "0x0000000000000000000000000000000000000000000000000000000000000000", - "0x8758b3661862833ec1e68b4057d4c3d968f639dcfbcdeeb31c7ecb563c06c48d": "0x00000000000000000000000000000000000000000000000000000000000007d0", - "0xe1832ca4fab71938f45d7756b23da44232ad31297df774671cea85d4acbe6ac8": "0x0000000000000000000000000000000000000000000000000000000000000000", - "0x5151797eff20ebe871e505a1ff10f4c43f000a85d507d6c6f6f157107fb0fb9c": "0x00000000000000000000000000000000000000000000000000000000000007d0", - "0x49e46a4d7af1cb84550f6f567de333acbc0799e5327e24dc2d45a99599996aff": "0x0000000000000000000000000000000000000000000000000000000000000000", - "0xd0fe00adfa714409d8434a5e7665a33cc3411b0d0753fd4670011326b7dee2ce": "0x00000000000000000000000000000000000000000000000000000000000007d0", - "0x5b1c14c8ac144ab3635042d92d3331eb063ac143e7d163ffc78ab034c1d6a94b": "0x0000000000000000000000000000000000000000000000000000000000000000", - "0x27a904bc0a897d94420bf8993e33a929271e68862825ae75976f923ca91bcf92": "0x00000000000000000000000000000000000000000000000000000000000007d0", - "0x2b81136c55f68fd91cfce4df1cd783c286658c2105ecd4c86baa64325b7d1322": "0x0000000000000000000000000000000000000000000000000000000000000000", - "0x1d61743442fd750af808d6571d9e8809b7d99c4e40497a2fc775acd436624baf": "0x0000000000000000000000000000000000000000000000000000000000000001", - "0xdf008954709fc88e758ca7a2ba528f00fc25eeb49ccfa12177f3fc8bfa6cb1f1": "0x0000000000000000000000000000000000000000000000000000000000000001" - } - }, - "0x4b5A1f0cf56B193cCA1a0B16fCE0713d44abdDbE": { - "balance": "0x0", - "nonce": "0x1", - "code": "0x608060405234801561000f575f5ffd5b50600436106100cb575f3560e01c8063c4d66de811610088578063e8746fed11610063578063e8746fed146101b0578063e877a526146101c3578063f2fde38b146101fa578063fab48ccf1461020d575f5ffd5b8063c4d66de814610182578063ca2f731314610195578063e30c3978146101a8575f5ffd5b806331d798b5146100cf578063715018a61461011b57806379ba5097146101255780637a7ceb161461012d5780638a8ee6ac146101405780638da5cb5b14610162575b5f5ffd5b6101066100dd366004610980565b6001600160a01b03165f9081525f516020610a335f395f51905f52602052604090205460ff1690565b60405190151581526020015b60405180910390f35b610123610220565b005b610123610233565b61012361013b3660046109ad565b610280565b6101545f516020610a535f395f51905f5281565b604051908152602001610112565b61016a610385565b6040516001600160a01b039091168152602001610112565b610123610190366004610980565b6103b9565b6101236101a3366004610980565b6104f5565b61016a6105ab565b6101236101be3660046109ad565b6105d3565b6101066101d1366004610980565b6001600160a01b03165f9081525f516020610a535f395f51905f52602052604090205460ff1690565b610123610208366004610980565b610717565b61012361021b366004610980565b61079c565b610228610855565b6102315f610887565b565b338061023d6105ab565b6001600160a01b0316146102745760405163118cdaa760e01b81526001600160a01b03821660048201526024015b60405180910390fd5b61027d81610887565b50565b335f9081525f516020610a335f395f51905f5260205260409020545f516020610a535f395f51905f529060ff166102ca57604051637628c31d60e01b815260040160405180910390fd5b5f516020610a535f395f51905f52825f5b8181101561037d575f8686838181106102f6576102f6610a1e565b905060200201602081019061030b9190610980565b6001600160a01b0381165f9081526020869052604090205490915060ff1615610374576001600160a01b0381165f81815260208690526040808220805460ff19169055517fc904e1b03de0c20d7fcf9dbd056daf1bd3815e93f251199de815fd0f0b96e1669190a25b506001016102db565b505050505050565b5f807f9016d09d72d40fdae2fd8ceac6b6234c7706214fd39c1cd1e609a0528c1993005b546001600160a01b031692915050565b7ff0c57e16840df040f15088dc2f81fe391c3923bec73e23a9662efc9c229c6a008054600160401b810460ff16159067ffffffffffffffff165f811580156103fe5750825b90505f8267ffffffffffffffff16600114801561041a5750303b155b905081158015610428575080155b156104465760405163f92ee8a960e01b815260040160405180910390fd5b845467ffffffffffffffff19166001178555831561047057845460ff60401b1916600160401b1785555b6001600160a01b0386166104975760405163d92e233d60e01b815260040160405180910390fd5b61049f6108bf565b6104a886610887565b831561037d57845460ff60401b19168555604051600181527fc7f505b2f371ae2175ee4913f4499e1f2633a7b5936321eed1cdaeb6115181d29060200160405180910390a1505050505050565b6104fd610855565b6001600160a01b0381166105245760405163d92e233d60e01b815260040160405180910390fd5b6001600160a01b0381165f9081525f516020610a335f395f51905f5260205260409020545f516020610a535f395f51905f529060ff16156105a7576001600160a01b0382165f818152600183016020526040808220805460ff19169055517f2ee9bf58aeff79a8ee48ae2ebaea69b7fc533af3060aaa8d8956b225785db10a9190a25b5050565b5f807f237e158222e3e6968b72b9db0d8043aacf074ad9f650f0d1606b4d82ee432c006103a9565b335f9081525f516020610a335f395f51905f5260205260409020545f516020610a535f395f51905f529060ff1661061d57604051637628c31d60e01b815260040160405180910390fd5b5f610626610385565b90505f516020610a535f395f51905f52835f5b8181101561070e575f87878381811061065457610654610a1e565b90506020020160208101906106699190610980565b9050846001600160a01b0316816001600160a01b03160361069d5760405163266f648160e01b815260040160405180910390fd5b6001600160a01b0381165f9081526020859052604090205460ff16610705576001600160a01b0381165f81815260208690526040808220805460ff19166001179055517ffa4507bc1f9c730e6e95897024f1fe7d576cf2deb53579d55c14f1ac3439e1149190a25b50600101610639565b50505050505050565b61071f610855565b7f237e158222e3e6968b72b9db0d8043aacf074ad9f650f0d1606b4d82ee432c0080546001600160a01b0319166001600160a01b0383169081178255610763610385565b6001600160a01b03167f38d16b8cac22d99fc7c124b9cd0de2d3fa1faef420bfe791d8c362d765e2270060405160405180910390a35050565b6107a4610855565b6001600160a01b0381166107cb5760405163d92e233d60e01b815260040160405180910390fd5b6001600160a01b0381165f9081525f516020610a335f395f51905f5260205260409020545f516020610a535f395f51905f529060ff166105a7576001600160a01b0382165f81815260018381016020526040808320805460ff1916909217909155517f1ac0162c9c4096b260eb12be2731ca291739aa6ed7c94780f52c55df88589e7c9190a25050565b3361085e610385565b6001600160a01b0316146102315760405163118cdaa760e01b815233600482015260240161026b565b7f237e158222e3e6968b72b9db0d8043aacf074ad9f650f0d1606b4d82ee432c0080546001600160a01b03191681556105a7826108c7565b610231610937565b7f9016d09d72d40fdae2fd8ceac6b6234c7706214fd39c1cd1e609a0528c19930080546001600160a01b031981166001600160a01b03848116918217845560405192169182907f8be0079c531659141344cd1fd0a4f28419497f9722a3daafe3b4186f6b6457e0905f90a3505050565b7ff0c57e16840df040f15088dc2f81fe391c3923bec73e23a9662efc9c229c6a0054600160401b900460ff1661023157604051631afcd79f60e31b815260040160405180910390fd5b5f60208284031215610990575f5ffd5b81356001600160a01b03811681146109a6575f5ffd5b9392505050565b5f5f602083850312156109be575f5ffd5b823567ffffffffffffffff8111156109d4575f5ffd5b8301601f810185136109e4575f5ffd5b803567ffffffffffffffff8111156109fa575f5ffd5b8560208260051b8401011115610a0e575f5ffd5b6020919091019590945092505050565b634e487b7160e01b5f52603260045260245ffdfe1d7e1388d3ae56f3d9c18b1ce8d2b3b1a238a0edf682d2053af5d8a1d2f12f011d7e1388d3ae56f3d9c18b1ce8d2b3b1a238a0edf682d2053af5d8a1d2f12f00a264697066735822122057fdea9cb356478df3dad5dbaec8a47476bd3266f5a49729c6d02d8e049ee9b864736f6c634300081d0033" - }, - "0x3600000000000000000000000000000000000004": { - "balance": "0x0", - "nonce": "0x1", - "code": "0x608060405260043610610049575f3560e01c80633659cfe6146100535780634f1ef286146100725780635c60da1b146100855780638f283970146100b5578063f851a440146100d4575b6100516100e8565b005b34801561005e575f5ffd5b5061005161006d3660046104f3565b6100fa565b61005161008036600461050c565b610134565b348015610090575f5ffd5b50610099610197565b6040516001600160a01b03909116815260200160405180910390f35b3480156100c0575f5ffd5b506100516100cf3660046104f3565b6101a5565b3480156100df575f5ffd5b506100996101c5565b6100f86100f3610197565b6101ce565b565b6101026101ec565b6001600160a01b0316330361012c576101298160405180602001604052805f81525061021e565b50565b6101296100e8565b61013c6101ec565b6001600160a01b0316330361018f5761018a8383838080601f0160208091040260200160405190810160405280939291908181526020018383808284375f9201919091525061021e92505050565b505050565b61018a6100e8565b5f6101a0610277565b905090565b6101ad6101ec565b6001600160a01b0316330361012c576101298161029e565b5f6101a06101ec565b365f5f375f5f365f845af43d5f5f3e8080156101e8573d5ff35b3d5ffd5b5f7fb53127684a568b3173ae13b9f8a6016e243e63b6e8ee1178d6a717850b5d61035b546001600160a01b0316919050565b610227826102f2565b6040516001600160a01b038316907fbc7cd75a20ee27fd9adebab32041f755214dbc6bffa90cc0225b39da2e5c2d3b905f90a280511561026b5761018a8282610370565b6102736103e2565b5050565b5f7f360894a13ba1a3210667c828492db98dca3e2076cc3735a920a3ca505d382bbc61020f565b7f7e644d79422f17c01e4894b5f4f588d331ebfa28653d42ae832dc59e38c9798f6102c76101ec565b604080516001600160a01b03928316815291841660208301520160405180910390a161012981610401565b806001600160a01b03163b5f0361032c57604051634c9c8ce360e01b81526001600160a01b03821660048201526024015b60405180910390fd5b807f360894a13ba1a3210667c828492db98dca3e2076cc3735a920a3ca505d382bbc5b80546001600160a01b0319166001600160a01b039290921691909117905550565b60605f5f846001600160a01b03168460405161038c919061058a565b5f60405180830381855af49150503d805f81146103c4576040519150601f19603f3d011682016040523d82523d5f602084013e6103c9565b606091505b50915091506103d9858383610451565b95945050505050565b34156100f85760405163b398979f60e01b815260040160405180910390fd5b6001600160a01b03811661042a57604051633173bdd160e11b81525f6004820152602401610323565b807fb53127684a568b3173ae13b9f8a6016e243e63b6e8ee1178d6a717850b5d610361034f565b60608261046657610461826104b0565b6104a9565b815115801561047d57506001600160a01b0384163b155b156104a657604051639996b31560e01b81526001600160a01b0385166004820152602401610323565b50805b9392505050565b8051156104bf57805160208201fd5b60405163d6bda27560e01b815260040160405180910390fd5b80356001600160a01b03811681146104ee575f5ffd5b919050565b5f60208284031215610503575f5ffd5b6104a9826104d8565b5f5f5f6040848603121561051e575f5ffd5b610527846104d8565b9250602084013567ffffffffffffffff811115610542575f5ffd5b8401601f81018613610552575f5ffd5b803567ffffffffffffffff811115610568575f5ffd5b866020828401011115610579575f5ffd5b939660209190910195509293505050565b5f82518060208501845e5f92019182525091905056fea26469706673582212204c0881af9b906ca964ce2bc1cc4525350de8dd57658dacb806266290556eb9ae64736f6c634300081d0033", - "storage": { - "0xb53127684a568b3173ae13b9f8a6016e243e63b6e8ee1178d6a717850b5d6103": "0x000000000000000000000000afb5b6a4725459959ef931a3e5df758a72a8ca7f", - "0x360894a13ba1a3210667c828492db98dca3e2076cc3735a920a3ca505d382bbc": "0x0000000000000000000000004b5a1f0cf56b193cca1a0b16fce0713d44abddbe", - "0x9016d09d72d40fdae2fd8ceac6b6234c7706214fd39c1cd1e609a0528c199300": "0x00000000000000000000000049ec36db19623e4dadc1aa821cba2d1476f8e859", - "0xf0c57e16840df040f15088dc2f81fe391c3923bec73e23a9662efc9c229c6a00": "0x0000000000000000000000000000000000000000000000000000000000000001" - } - }, - "0x4e59b44847b379578588920cA78FbF26c0B4956C": { - "balance": "0x0", - "nonce": "0x1", - "code": "0x7fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffe03601600081602082378035828234f58015156039578182fd5b8082525050506014600cf3" - }, - "0x3fab184622dc19b6109349b94811493bf2a45362": { - "balance": "0x0", - "nonce": "0x1" - }, - "0xcA11bde05977b3631167028862bE2a173976CA11": { - "balance": "0x0", - "nonce": "0x1", - "code": "0x6080604052600436106100f35760003560e01c80634d2301cc1161008a578063a8b0574e11610059578063a8b0574e1461025a578063bce38bd714610275578063c3077fa914610288578063ee82ac5e1461029b57600080fd5b80634d2301cc146101ec57806372425d9d1461022157806382ad56cb1461023457806386d516e81461024757600080fd5b80633408e470116100c65780633408e47014610191578063399542e9146101a45780633e64a696146101c657806342cbb15c146101d957600080fd5b80630f28c97d146100f8578063174dea711461011a578063252dba421461013a57806327e86d6e1461015b575b600080fd5b34801561010457600080fd5b50425b6040519081526020015b60405180910390f35b61012d610128366004610a85565b6102ba565b6040516101119190610bbe565b61014d610148366004610a85565b6104ef565b604051610111929190610bd8565b34801561016757600080fd5b50437fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff0140610107565b34801561019d57600080fd5b5046610107565b6101b76101b2366004610c60565b610690565b60405161011193929190610cba565b3480156101d257600080fd5b5048610107565b3480156101e557600080fd5b5043610107565b3480156101f857600080fd5b50610107610207366004610ce2565b73ffffffffffffffffffffffffffffffffffffffff163190565b34801561022d57600080fd5b5044610107565b61012d610242366004610a85565b6106ab565b34801561025357600080fd5b5045610107565b34801561026657600080fd5b50604051418152602001610111565b61012d610283366004610c60565b61085a565b6101b7610296366004610a85565b610a1a565b3480156102a757600080fd5b506101076102b6366004610d18565b4090565b60606000828067ffffffffffffffff8111156102d8576102d8610d31565b60405190808252806020026020018201604052801561031e57816020015b6040805180820190915260008152606060208201528152602001906001900390816102f65790505b5092503660005b8281101561047757600085828151811061034157610341610d60565b6020026020010151905087878381811061035d5761035d610d60565b905060200281019061036f9190610d8f565b6040810135958601959093506103886020850185610ce2565b73ffffffffffffffffffffffffffffffffffffffff16816103ac6060870187610dcd565b6040516103ba929190610e32565b60006040518083038185875af1925050503d80600081146103f7576040519150601f19603f3d011682016040523d82523d6000602084013e6103fc565b606091505b50602080850191909152901515808452908501351761046d577f08c379a000000000000000000000000000000000000000000000000000000000600052602060045260176024527f4d756c746963616c6c333a2063616c6c206661696c656400000000000000000060445260846000fd5b5050600101610325565b508234146104e6576040517f08c379a000000000000000000000000000000000000000000000000000000000815260206004820152601a60248201527f4d756c746963616c6c333a2076616c7565206d69736d6174636800000000000060448201526064015b60405180910390fd5b50505092915050565b436060828067ffffffffffffffff81111561050c5761050c610d31565b60405190808252806020026020018201604052801561053f57816020015b606081526020019060019003908161052a5790505b5091503660005b8281101561068657600087878381811061056257610562610d60565b90506020028101906105749190610e42565b92506105836020840184610ce2565b73ffffffffffffffffffffffffffffffffffffffff166105a66020850185610dcd565b6040516105b4929190610e32565b6000604051808303816000865af19150503d80600081146105f1576040519150601f19603f3d011682016040523d82523d6000602084013e6105f6565b606091505b5086848151811061060957610609610d60565b602090810291909101015290508061067d576040517f08c379a000000000000000000000000000000000000000000000000000000000815260206004820152601760248201527f4d756c746963616c6c333a2063616c6c206661696c656400000000000000000060448201526064016104dd565b50600101610546565b5050509250929050565b43804060606106a086868661085a565b905093509350939050565b6060818067ffffffffffffffff8111156106c7576106c7610d31565b60405190808252806020026020018201604052801561070d57816020015b6040805180820190915260008152606060208201528152602001906001900390816106e55790505b5091503660005b828110156104e657600084828151811061073057610730610d60565b6020026020010151905086868381811061074c5761074c610d60565b905060200281019061075e9190610e76565b925061076d6020840184610ce2565b73ffffffffffffffffffffffffffffffffffffffff166107906040850185610dcd565b60405161079e929190610e32565b6000604051808303816000865af19150503d80600081146107db576040519150601f19603f3d011682016040523d82523d6000602084013e6107e0565b606091505b506020808401919091529015158083529084013517610851577f08c379a000000000000000000000000000000000000000000000000000000000600052602060045260176024527f4d756c746963616c6c333a2063616c6c206661696c656400000000000000000060445260646000fd5b50600101610714565b6060818067ffffffffffffffff81111561087657610876610d31565b6040519080825280602002602001820160405280156108bc57816020015b6040805180820190915260008152606060208201528152602001906001900390816108945790505b5091503660005b82811015610a105760008482815181106108df576108df610d60565b602002602001015190508686838181106108fb576108fb610d60565b905060200281019061090d9190610e42565b925061091c6020840184610ce2565b73ffffffffffffffffffffffffffffffffffffffff1661093f6020850185610dcd565b60405161094d929190610e32565b6000604051808303816000865af19150503d806000811461098a576040519150601f19603f3d011682016040523d82523d6000602084013e61098f565b606091505b506020830152151581528715610a07578051610a07576040517f08c379a000000000000000000000000000000000000000000000000000000000815260206004820152601760248201527f4d756c746963616c6c333a2063616c6c206661696c656400000000000000000060448201526064016104dd565b506001016108c3565b5050509392505050565b6000806060610a2b60018686610690565b919790965090945092505050565b60008083601f840112610a4b57600080fd5b50813567ffffffffffffffff811115610a6357600080fd5b6020830191508360208260051b8501011115610a7e57600080fd5b9250929050565b60008060208385031215610a9857600080fd5b823567ffffffffffffffff811115610aaf57600080fd5b610abb85828601610a39565b90969095509350505050565b6000815180845260005b81811015610aed57602081850181015186830182015201610ad1565b81811115610aff576000602083870101525b50601f017fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffe0169290920160200192915050565b600082825180855260208086019550808260051b84010181860160005b84811015610bb1578583037fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffe001895281518051151584528401516040858501819052610b9d81860183610ac7565b9a86019a9450505090830190600101610b4f565b5090979650505050505050565b602081526000610bd16020830184610b32565b9392505050565b600060408201848352602060408185015281855180845260608601915060608160051b870101935082870160005b82811015610c52577fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffa0888703018452610c40868351610ac7565b95509284019290840190600101610c06565b509398975050505050505050565b600080600060408486031215610c7557600080fd5b83358015158114610c8557600080fd5b9250602084013567ffffffffffffffff811115610ca157600080fd5b610cad86828701610a39565b9497909650939450505050565b838152826020820152606060408201526000610cd96060830184610b32565b95945050505050565b600060208284031215610cf457600080fd5b813573ffffffffffffffffffffffffffffffffffffffff81168114610bd157600080fd5b600060208284031215610d2a57600080fd5b5035919050565b7f4e487b7100000000000000000000000000000000000000000000000000000000600052604160045260246000fd5b7f4e487b7100000000000000000000000000000000000000000000000000000000600052603260045260246000fd5b600082357fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff81833603018112610dc357600080fd5b9190910192915050565b60008083357fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffe1843603018112610e0257600080fd5b83018035915067ffffffffffffffff821115610e1d57600080fd5b602001915036819003821315610a7e57600080fd5b8183823760009101908152919050565b600082357fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffc1833603018112610dc357600080fd5b600082357fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffa1833603018112610dc357600080fdfea2646970667358221220bb2b5c71a328032f97c676ae39a1ec2148d3e5d6f73d95e9b17910152d61f16264736f6c634300080c0033" - }, - "0x05f32b3cc3888453ff71b01135b34ff8e41263f2": { - "balance": "0x0", - "nonce": "0x1" - }, - "0x0000F90827F1C53a10cb7A02335B175320002935": { - "balance": "0x0", - "nonce": "0x1", - "code": "0x3373fffffffffffffffffffffffffffffffffffffffe14604657602036036042575f35600143038111604257611fff81430311604257611fff9006545f5260205ff35b5f5ffd5b5f35611fff60014303065500" - }, - "0x3462413Af4609098e1E27A490f554f260213D685": { - "balance": "0x0", - "nonce": "0x1" - }, - "0x000000000022D473030F116dDEE9F6B43aC78BA3": { - "balance": "0x0", - "nonce": "0x1", - "code": "0x6040608081526004908136101561001557600080fd5b600090813560e01c80630d58b1db1461126c578063137c29fe146110755780632a2d80d114610db75780632b67b57014610bde57806330f28b7a14610ade5780633644e51514610a9d57806336c7851614610a285780633ff9dcb1146109a85780634fe02b441461093f57806365d9723c146107ac57806387517c451461067a578063927da105146105c3578063cc53287f146104a3578063edd9444b1461033a5763fe8ec1a7146100c657600080fd5b346103365760c07ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffc3601126103365767ffffffffffffffff833581811161033257610114903690860161164b565b60243582811161032e5761012b903690870161161a565b6101336114e6565b9160843585811161032a5761014b9036908a016115c1565b98909560a43590811161032657610164913691016115c1565b969095815190610173826113ff565b606b82527f5065726d697442617463685769746e6573735472616e7366657246726f6d285460208301527f6f6b656e5065726d697373696f6e735b5d207065726d69747465642c61646472838301527f657373207370656e6465722c75696e74323536206e6f6e63652c75696e74323560608301527f3620646561646c696e652c000000000000000000000000000000000000000000608083015282519a8b9181610222602085018096611f93565b918237018a8152039961025b7fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffe09b8c8101835282611437565b5190209085515161026b81611ebb565b908a5b8181106102f95750506102f6999a6102ed9183516102a081610294602082018095611f66565b03848101835282611437565b519020602089810151858b015195519182019687526040820192909252336060820152608081019190915260a081019390935260643560c08401528260e081015b03908101835282611437565b51902093611cf7565b80f35b8061031161030b610321938c5161175e565b51612054565b61031b828661175e565b52611f0a565b61026e565b8880fd5b8780fd5b8480fd5b8380fd5b5080fd5b5091346103365760807ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffc3601126103365767ffffffffffffffff9080358281116103325761038b903690830161164b565b60243583811161032e576103a2903690840161161a565b9390926103ad6114e6565b9160643590811161049f576103c4913691016115c1565b949093835151976103d489611ebb565b98885b81811061047d5750506102f697988151610425816103f9602082018095611f66565b037fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffe08101835282611437565b5190206020860151828701519083519260208401947ffcf35f5ac6a2c28868dc44c302166470266239195f02b0ee408334829333b7668652840152336060840152608083015260a082015260a081526102ed8161141b565b808b61031b8261049461030b61049a968d5161175e565b9261175e565b6103d7565b8680fd5b5082346105bf57602090817ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffc3601126103325780359067ffffffffffffffff821161032e576104f49136910161161a565b929091845b848110610504578580f35b8061051a610515600193888861196c565b61197c565b61052f84610529848a8a61196c565b0161197c565b3389528385528589209173ffffffffffffffffffffffffffffffffffffffff80911692838b528652868a20911690818a5285528589207fffffffffffffffffffffffff000000000000000000000000000000000000000081541690558551918252848201527f89b1add15eff56b3dfe299ad94e01f2b52fbcb80ae1a3baea6ae8c04cb2b98a4853392a2016104f9565b8280fd5b50346103365760607ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffc36011261033657610676816105ff6114a0565b936106086114c3565b6106106114e6565b73ffffffffffffffffffffffffffffffffffffffff968716835260016020908152848420928816845291825283832090871683528152919020549251938316845260a083901c65ffffffffffff169084015260d09190911c604083015281906060820190565b0390f35b50346103365760807ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffc360112610336576106b26114a0565b906106bb6114c3565b916106c46114e6565b65ffffffffffff926064358481169081810361032a5779ffffffffffff0000000000000000000000000000000000000000947fda9fa7c1b00402c17d0161b249b1ab8bbec047c5a52207b9c112deffd817036b94338a5260016020527fffffffffffff0000000000000000000000000000000000000000000000000000858b209873ffffffffffffffffffffffffffffffffffffffff809416998a8d5260205283878d209b169a8b8d52602052868c209486156000146107a457504216925b8454921697889360a01b16911617179055815193845260208401523392a480f35b905092610783565b5082346105bf5760607ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffc3601126105bf576107e56114a0565b906107ee6114c3565b9265ffffffffffff604435818116939084810361032a57338852602091600183528489209673ffffffffffffffffffffffffffffffffffffffff80911697888b528452858a20981697888a5283528489205460d01c93848711156109175761ffff9085840316116108f05750907f55eb90d810e1700b35a8e7e25395ff7f2b2259abd7415ca2284dfb1c246418f393929133895260018252838920878a528252838920888a5282528389209079ffffffffffffffffffffffffffffffffffffffffffffffffffff7fffffffffffff000000000000000000000000000000000000000000000000000083549260d01b16911617905582519485528401523392a480f35b84517f24d35a26000000000000000000000000000000000000000000000000000000008152fd5b5084517f756688fe000000000000000000000000000000000000000000000000000000008152fd5b503461033657807ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffc360112610336578060209273ffffffffffffffffffffffffffffffffffffffff61098f6114a0565b1681528084528181206024358252845220549051908152f35b5082346105bf57817ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffc3601126105bf577f3704902f963766a4e561bbaab6e6cdc1b1dd12f6e9e99648da8843b3f46b918d90359160243533855284602052818520848652602052818520818154179055815193845260208401523392a280f35b8234610a9a5760807ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffc360112610a9a57610a606114a0565b610a686114c3565b610a706114e6565b6064359173ffffffffffffffffffffffffffffffffffffffff8316830361032e576102f6936117a1565b80fd5b503461033657817ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffc36011261033657602090610ad7611b1e565b9051908152f35b508290346105bf576101007ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffc3601126105bf57610b1a3661152a565b90807fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff7c36011261033257610b4c611478565b9160e43567ffffffffffffffff8111610bda576102f694610b6f913691016115c1565b939092610b7c8351612054565b6020840151828501519083519260208401947f939c21a48a8dbe3a9a2404a1d46691e4d39f6583d6ec6b35714604c986d801068652840152336060840152608083015260a082015260a08152610bd18161141b565b51902091611c25565b8580fd5b509134610336576101007ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffc36011261033657610c186114a0565b7fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffdc360160c08112610332576080855191610c51836113e3565b1261033257845190610c6282611398565b73ffffffffffffffffffffffffffffffffffffffff91602435838116810361049f578152604435838116810361049f57602082015265ffffffffffff606435818116810361032a5788830152608435908116810361049f576060820152815260a435938285168503610bda576020820194855260c4359087830182815260e43567ffffffffffffffff811161032657610cfe90369084016115c1565b929093804211610d88575050918591610d786102f6999a610d7e95610d238851611fbe565b90898c511690519083519260208401947ff3841cd1ff0085026a6327b620b67997ce40f282c88a8e905a7a5626e310f3d086528401526060830152608082015260808152610d70816113ff565b519020611bd9565b916120c7565b519251169161199d565b602492508a51917fcd21db4f000000000000000000000000000000000000000000000000000000008352820152fd5b5091346103365760607ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffc93818536011261033257610df36114a0565b9260249081359267ffffffffffffffff9788851161032a578590853603011261049f578051978589018981108282111761104a578252848301358181116103265785019036602383011215610326578382013591610e50836115ef565b90610e5d85519283611437565b838252602093878584019160071b83010191368311611046578801905b828210610fe9575050508a526044610e93868801611509565b96838c01978852013594838b0191868352604435908111610fe557610ebb90369087016115c1565b959096804211610fba575050508998995151610ed681611ebb565b908b5b818110610f9757505092889492610d7892610f6497958351610f02816103f98682018095611f66565b5190209073ffffffffffffffffffffffffffffffffffffffff9a8b8b51169151928551948501957faf1b0d30d2cab0380e68f0689007e3254993c596f2fdd0aaa7f4d04f794408638752850152830152608082015260808152610d70816113ff565b51169082515192845b848110610f78578580f35b80610f918585610f8b600195875161175e565b5161199d565b01610f6d565b80610311610fac8e9f9e93610fb2945161175e565b51611fbe565b9b9a9b610ed9565b8551917fcd21db4f000000000000000000000000000000000000000000000000000000008352820152fd5b8a80fd5b6080823603126110465785608091885161100281611398565b61100b85611509565b8152611018838601611509565b838201526110278a8601611607565b8a8201528d611037818701611607565b90820152815201910190610e7a565b8c80fd5b84896041867f4e487b7100000000000000000000000000000000000000000000000000000000835252fd5b5082346105bf576101407ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffc3601126105bf576110b03661152a565b91807fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff7c360112610332576110e2611478565b67ffffffffffffffff93906101043585811161049f5761110590369086016115c1565b90936101243596871161032a57611125610bd1966102f6983691016115c1565b969095825190611134826113ff565b606482527f5065726d69745769746e6573735472616e7366657246726f6d28546f6b656e5060208301527f65726d697373696f6e73207065726d69747465642c6164647265737320737065848301527f6e6465722c75696e74323536206e6f6e63652c75696e7432353620646561646c60608301527f696e652c0000000000000000000000000000000000000000000000000000000060808301528351948591816111e3602085018096611f93565b918237018b8152039361121c7fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffe095868101835282611437565b5190209261122a8651612054565b6020878101518589015195519182019687526040820192909252336060820152608081019190915260a081019390935260e43560c08401528260e081016102e1565b5082346105bf576020807ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffc36011261033257813567ffffffffffffffff92838211610bda5736602383011215610bda5781013592831161032e576024906007368386831b8401011161049f57865b8581106112e5578780f35b80821b83019060807fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffdc83360301126103265761139288876001946060835161132c81611398565b611368608461133c8d8601611509565b9485845261134c60448201611509565b809785015261135d60648201611509565b809885015201611509565b918291015273ffffffffffffffffffffffffffffffffffffffff80808093169516931691166117a1565b016112da565b6080810190811067ffffffffffffffff8211176113b457604052565b7f4e487b7100000000000000000000000000000000000000000000000000000000600052604160045260246000fd5b6060810190811067ffffffffffffffff8211176113b457604052565b60a0810190811067ffffffffffffffff8211176113b457604052565b60c0810190811067ffffffffffffffff8211176113b457604052565b90601f7fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffe0910116810190811067ffffffffffffffff8211176113b457604052565b60c4359073ffffffffffffffffffffffffffffffffffffffff8216820361149b57565b600080fd5b6004359073ffffffffffffffffffffffffffffffffffffffff8216820361149b57565b6024359073ffffffffffffffffffffffffffffffffffffffff8216820361149b57565b6044359073ffffffffffffffffffffffffffffffffffffffff8216820361149b57565b359073ffffffffffffffffffffffffffffffffffffffff8216820361149b57565b7ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffc01906080821261149b576040805190611563826113e3565b8082941261149b57805181810181811067ffffffffffffffff8211176113b457825260043573ffffffffffffffffffffffffffffffffffffffff8116810361149b578152602435602082015282526044356020830152606435910152565b9181601f8401121561149b5782359167ffffffffffffffff831161149b576020838186019501011161149b57565b67ffffffffffffffff81116113b45760051b60200190565b359065ffffffffffff8216820361149b57565b9181601f8401121561149b5782359167ffffffffffffffff831161149b576020808501948460061b01011161149b57565b91909160608184031261149b576040805191611666836113e3565b8294813567ffffffffffffffff9081811161149b57830182601f8201121561149b578035611693816115ef565b926116a087519485611437565b818452602094858086019360061b8501019381851161149b579086899897969594939201925b8484106116e3575050505050855280820135908501520135910152565b90919293949596978483031261149b578851908982019082821085831117611730578a928992845261171487611509565b81528287013583820152815201930191908897969594936116c6565b602460007f4e487b710000000000000000000000000000000000000000000000000000000081526041600452fd5b80518210156117725760209160051b010190565b7f4e487b7100000000000000000000000000000000000000000000000000000000600052603260045260246000fd5b92919273ffffffffffffffffffffffffffffffffffffffff604060008284168152600160205282828220961695868252602052818120338252602052209485549565ffffffffffff8760a01c16804211611884575082871696838803611812575b5050611810955016926118b5565b565b878484161160001461184f57602488604051907ff96fb0710000000000000000000000000000000000000000000000000000000082526004820152fd5b7fffffffffffffffffffffffff000000000000000000000000000000000000000084846118109a031691161790553880611802565b602490604051907fd81b2f2e0000000000000000000000000000000000000000000000000000000082526004820152fd5b9060006064926020958295604051947f23b872dd0000000000000000000000000000000000000000000000000000000086526004860152602485015260448401525af13d15601f3d116001600051141617161561190e57565b60646040517f08c379a000000000000000000000000000000000000000000000000000000000815260206004820152601460248201527f5452414e534645525f46524f4d5f4641494c45440000000000000000000000006044820152fd5b91908110156117725760061b0190565b3573ffffffffffffffffffffffffffffffffffffffff8116810361149b5790565b9065ffffffffffff908160608401511673ffffffffffffffffffffffffffffffffffffffff908185511694826020820151169280866040809401511695169560009187835260016020528383208984526020528383209916988983526020528282209184835460d01c03611af5579185611ace94927fc6a377bfc4eb120024a8ac08eef205be16b817020812c73223e81d1bdb9708ec98979694508715600014611ad35779ffffffffffff00000000000000000000000000000000000000009042165b60a01b167fffffffffffff00000000000000000000000000000000000000000000000000006001860160d01b1617179055519384938491604091949373ffffffffffffffffffffffffffffffffffffffff606085019616845265ffffffffffff809216602085015216910152565b0390a4565b5079ffffffffffff000000000000000000000000000000000000000087611a60565b600484517f756688fe000000000000000000000000000000000000000000000000000000008152fd5b467f00000000000000000000000000000000000000000000000000000000000013b203611b69577fa88a1b742ab6890402e6ee74f2359f3723ab547c6dc3b850249bdbffebe2b18f90565b60405160208101907f8cad95687ba82c2ce50e74f7b754645e5117c3a5bec8151c0726d5857980a86682527f9ac997416e8ff9d2ff6bebeb7149f65cdae5e32e2b90440b566bb3044041d36a604082015246606082015230608082015260808152611bd3816113ff565b51902090565b611be1611b1e565b906040519060208201927f190100000000000000000000000000000000000000000000000000000000000084526022830152604282015260428152611bd381611398565b9192909360a435936040840151804211611cc65750602084510151808611611c955750918591610d78611c6594611c60602088015186611e47565b611bd9565b73ffffffffffffffffffffffffffffffffffffffff809151511692608435918216820361149b57611810936118b5565b602490604051907f3728b83d0000000000000000000000000000000000000000000000000000000082526004820152fd5b602490604051907fcd21db4f0000000000000000000000000000000000000000000000000000000082526004820152fd5b959093958051519560409283830151804211611e175750848803611dee57611d2e918691610d7860209b611c608d88015186611e47565b60005b868110611d42575050505050505050565b611d4d81835161175e565b5188611d5a83878a61196c565b01359089810151808311611dbe575091818888886001968596611d84575b50505050505001611d31565b611db395611dad9273ffffffffffffffffffffffffffffffffffffffff6105159351169561196c565b916118b5565b803888888883611d78565b6024908651907f3728b83d0000000000000000000000000000000000000000000000000000000082526004820152fd5b600484517fff633a38000000000000000000000000000000000000000000000000000000008152fd5b6024908551907fcd21db4f0000000000000000000000000000000000000000000000000000000082526004820152fd5b9073ffffffffffffffffffffffffffffffffffffffff600160ff83161b9216600052600060205260406000209060081c6000526020526040600020818154188091551615611e9157565b60046040517f756688fe000000000000000000000000000000000000000000000000000000008152fd5b90611ec5826115ef565b611ed26040519182611437565b8281527fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffe0611f0082946115ef565b0190602036910137565b7fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff8114611f375760010190565b7f4e487b7100000000000000000000000000000000000000000000000000000000600052601160045260246000fd5b805160208092019160005b828110611f7f575050505090565b835185529381019392810192600101611f71565b9081519160005b838110611fab575050016000815290565b8060208092840101518185015201611f9a565b60405160208101917f65626cad6cb96493bf6f5ebea28756c966f023ab9e8a83a7101849d5573b3678835273ffffffffffffffffffffffffffffffffffffffff8082511660408401526020820151166060830152606065ffffffffffff9182604082015116608085015201511660a082015260a0815260c0810181811067ffffffffffffffff8211176113b45760405251902090565b6040516020808201927f618358ac3db8dc274f0cd8829da7e234bd48cd73c4a740aede1adec9846d06a1845273ffffffffffffffffffffffffffffffffffffffff81511660408401520151606082015260608152611bd381611398565b919082604091031261149b576020823592013590565b6000843b61222e5750604182036121ac576120e4828201826120b1565b939092604010156117725760209360009360ff6040608095013560f81c5b60405194855216868401526040830152606082015282805260015afa156121a05773ffffffffffffffffffffffffffffffffffffffff806000511691821561217657160361214c57565b60046040517f815e1d64000000000000000000000000000000000000000000000000000000008152fd5b60046040517f8baa579f000000000000000000000000000000000000000000000000000000008152fd5b6040513d6000823e3d90fd5b60408203612204576121c0918101906120b1565b91601b7f7fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff84169360ff1c019060ff8211611f375760209360009360ff608094612102565b60046040517f4be6321b000000000000000000000000000000000000000000000000000000008152fd5b929391601f928173ffffffffffffffffffffffffffffffffffffffff60646020957fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffe0604051988997889687947f1626ba7e000000000000000000000000000000000000000000000000000000009e8f8752600487015260406024870152816044870152868601378b85828601015201168101030192165afa9081156123a857829161232a575b507fffffffff000000000000000000000000000000000000000000000000000000009150160361230057565b60046040517fb0669cbc000000000000000000000000000000000000000000000000000000008152fd5b90506020813d82116123a0575b8161234460209383611437565b810103126103365751907fffffffff0000000000000000000000000000000000000000000000000000000082168203610a9a57507fffffffff0000000000000000000000000000000000000000000000000000000090386122d4565b3d9150612337565b6040513d84823e3d90fdfea164736f6c6343000811000a" - }, - "0x1800000000000000000000000000000000000002": { - "balance": "0x0", - "nonce": "0x1", - "code": "0xef" - }, - "0x1800000000000000000000000000000000000003": { - "balance": "0x0", - "nonce": "0x1", - "code": "0xef" - }, - "0x1800000000000000000000000000000000000004": { - "balance": "0x0", - "nonce": "0x1", - "code": "0xef" - }, - "0x1800000000000000000000000000000000000005": { - "balance": "0x0", - "nonce": "0x1", - "code": "0xef" - }, - "0x1800000000000000000000000000000000000006": { - "balance": "0x0", - "nonce": "0x1", - "code": "0xef" - }, - "0x1800000000000000000000000000000000000007": { - "balance": "0x0", - "nonce": "0x1", - "code": "0xef" - }, - "0x1800000000000000000000000000000000000008": { - "balance": "0x0", - "nonce": "0x1", - "code": "0xef" - }, - "0x1800000000000000000000000000000000000009": { - "balance": "0x0", - "nonce": "0x1", - "code": "0xef" - }, - "0x180000000000000000000000000000000000000a": { - "balance": "0x0", - "nonce": "0x1", - "code": "0xef" - }, - "0x180000000000000000000000000000000000000b": { - "balance": "0x0", - "nonce": "0x1", - "code": "0xef" - }, - "0x180000000000000000000000000000000000000c": { - "balance": "0x0", - "nonce": "0x1", - "code": "0xef" - }, - "0x180000000000000000000000000000000000000d": { - "balance": "0x0", - "nonce": "0x1", - "code": "0xef" - }, - "0x180000000000000000000000000000000000000e": { - "balance": "0x0", - "nonce": "0x1", - "code": "0xef" - }, - "0x180000000000000000000000000000000000000f": { - "balance": "0x0", - "nonce": "0x1", - "code": "0xef" - }, - "0x1800000000000000000000000000000000000010": { - "balance": "0x0", - "nonce": "0x1", - "code": "0xef" - }, - "0x1800000000000000000000000000000000000011": { - "balance": "0x0", - "nonce": "0x1", - "code": "0xef" - }, - "0x1800000000000000000000000000000000000012": { - "balance": "0x0", - "nonce": "0x1", - "code": "0xef" - }, - "0x1800000000000000000000000000000000000013": { - "balance": "0x0", - "nonce": "0x1", - "code": "0xef" - }, - "0x1800000000000000000000000000000000000014": { - "balance": "0x0", - "nonce": "0x1", - "code": "0xef" - }, - "0x1800000000000000000000000000000000000015": { - "balance": "0x0", - "nonce": "0x1", - "code": "0xef" - }, - "0x1800000000000000000000000000000000000016": { - "balance": "0x0", - "nonce": "0x1", - "code": "0xef" - }, - "0x1800000000000000000000000000000000000017": { - "balance": "0x0", - "nonce": "0x1", - "code": "0xef" - }, - "0x1800000000000000000000000000000000000018": { - "balance": "0x0", - "nonce": "0x1", - "code": "0xef" - }, - "0x1800000000000000000000000000000000000019": { - "balance": "0x0", - "nonce": "0x1", - "code": "0xef" - }, - "0x180000000000000000000000000000000000001a": { - "balance": "0x0", - "nonce": "0x1", - "code": "0xef" - }, - "0x180000000000000000000000000000000000001b": { - "balance": "0x0", - "nonce": "0x1", - "code": "0xef" - }, - "0x180000000000000000000000000000000000001c": { - "balance": "0x0", - "nonce": "0x1", - "code": "0xef" - }, - "0x180000000000000000000000000000000000001d": { - "balance": "0x0", - "nonce": "0x1", - "code": "0xef" - }, - "0x180000000000000000000000000000000000001e": { - "balance": "0x0", - "nonce": "0x1", - "code": "0xef" - }, - "0x180000000000000000000000000000000000001f": { - "balance": "0x0", - "nonce": "0x1", - "code": "0xef" - }, - "0x1800000000000000000000000000000000000020": { - "balance": "0x0", - "nonce": "0x1", - "code": "0xef" - }, - "0x1800000000000000000000000000000000000021": { - "balance": "0x0", - "nonce": "0x1", - "code": "0xef" - }, - "0x1800000000000000000000000000000000000022": { - "balance": "0x0", - "nonce": "0x1", - "code": "0xef" - }, - "0x1800000000000000000000000000000000000023": { - "balance": "0x0", - "nonce": "0x1", - "code": "0xef" - }, - "0x1800000000000000000000000000000000000024": { - "balance": "0x0", - "nonce": "0x1", - "code": "0xef" - }, - "0x1800000000000000000000000000000000000025": { - "balance": "0x0", - "nonce": "0x1", - "code": "0xef" - }, - "0x1800000000000000000000000000000000000026": { - "balance": "0x0", - "nonce": "0x1", - "code": "0xef" - }, - "0x1800000000000000000000000000000000000027": { - "balance": "0x0", - "nonce": "0x1", - "code": "0xef" - }, - "0x1800000000000000000000000000000000000028": { - "balance": "0x0", - "nonce": "0x1", - "code": "0xef" - }, - "0x1800000000000000000000000000000000000029": { - "balance": "0x0", - "nonce": "0x1", - "code": "0xef" - }, - "0x180000000000000000000000000000000000002a": { - "balance": "0x0", - "nonce": "0x1", - "code": "0xef" - }, - "0x180000000000000000000000000000000000002b": { - "balance": "0x0", - "nonce": "0x1", - "code": "0xef" - }, - "0x180000000000000000000000000000000000002c": { - "balance": "0x0", - "nonce": "0x1", - "code": "0xef" - }, - "0x180000000000000000000000000000000000002d": { - "balance": "0x0", - "nonce": "0x1", - "code": "0xef" - }, - "0x180000000000000000000000000000000000002e": { - "balance": "0x0", - "nonce": "0x1", - "code": "0xef" - }, - "0x180000000000000000000000000000000000002f": { - "balance": "0x0", - "nonce": "0x1", - "code": "0xef" - }, - "0x1800000000000000000000000000000000000030": { - "balance": "0x0", - "nonce": "0x1", - "code": "0xef" - }, - "0x1800000000000000000000000000000000000031": { - "balance": "0x0", - "nonce": "0x1", - "code": "0xef" - }, - "0x1800000000000000000000000000000000000032": { - "balance": "0x0", - "nonce": "0x1", - "code": "0xef" - }, - "0x1800000000000000000000000000000000000033": { - "balance": "0x0", - "nonce": "0x1", - "code": "0xef" - }, - "0x1800000000000000000000000000000000000034": { - "balance": "0x0", - "nonce": "0x1", - "code": "0xef" - }, - "0x1800000000000000000000000000000000000035": { - "balance": "0x0", - "nonce": "0x1", - "code": "0xef" - }, - "0x1800000000000000000000000000000000000036": { - "balance": "0x0", - "nonce": "0x1", - "code": "0xef" - }, - "0x1800000000000000000000000000000000000037": { - "balance": "0x0", - "nonce": "0x1", - "code": "0xef" - }, - "0x1800000000000000000000000000000000000038": { - "balance": "0x0", - "nonce": "0x1", - "code": "0xef" - }, - "0x1800000000000000000000000000000000000039": { - "balance": "0x0", - "nonce": "0x1", - "code": "0xef" - }, - "0x180000000000000000000000000000000000003a": { - "balance": "0x0", - "nonce": "0x1", - "code": "0xef" - }, - "0x180000000000000000000000000000000000003b": { - "balance": "0x0", - "nonce": "0x1", - "code": "0xef" - }, - "0x180000000000000000000000000000000000003c": { - "balance": "0x0", - "nonce": "0x1", - "code": "0xef" - }, - "0x180000000000000000000000000000000000003d": { - "balance": "0x0", - "nonce": "0x1", - "code": "0xef" - }, - "0x180000000000000000000000000000000000003e": { - "balance": "0x0", - "nonce": "0x1", - "code": "0xef" - }, - "0x180000000000000000000000000000000000003f": { - "balance": "0x0", - "nonce": "0x1", - "code": "0xef" - }, - "0x1800000000000000000000000000000000000040": { - "balance": "0x0", - "nonce": "0x1", - "code": "0xef" - }, - "0x1800000000000000000000000000000000000041": { - "balance": "0x0", - "nonce": "0x1", - "code": "0xef" - }, - "0x1800000000000000000000000000000000000042": { - "balance": "0x0", - "nonce": "0x1", - "code": "0xef" - }, - "0x1800000000000000000000000000000000000043": { - "balance": "0x0", - "nonce": "0x1", - "code": "0xef" - }, - "0x1800000000000000000000000000000000000044": { - "balance": "0x0", - "nonce": "0x1", - "code": "0xef" - }, - "0x1800000000000000000000000000000000000045": { - "balance": "0x0", - "nonce": "0x1", - "code": "0xef" - }, - "0x1800000000000000000000000000000000000046": { - "balance": "0x0", - "nonce": "0x1", - "code": "0xef" - }, - "0x1800000000000000000000000000000000000047": { - "balance": "0x0", - "nonce": "0x1", - "code": "0xef" - }, - "0x1800000000000000000000000000000000000048": { - "balance": "0x0", - "nonce": "0x1", - "code": "0xef" - }, - "0x1800000000000000000000000000000000000049": { - "balance": "0x0", - "nonce": "0x1", - "code": "0xef" - }, - "0x180000000000000000000000000000000000004a": { - "balance": "0x0", - "nonce": "0x1", - "code": "0xef" - }, - "0x180000000000000000000000000000000000004b": { - "balance": "0x0", - "nonce": "0x1", - "code": "0xef" - }, - "0x180000000000000000000000000000000000004c": { - "balance": "0x0", - "nonce": "0x1", - "code": "0xef" - }, - "0x180000000000000000000000000000000000004d": { - "balance": "0x0", - "nonce": "0x1", - "code": "0xef" - }, - "0x180000000000000000000000000000000000004e": { - "balance": "0x0", - "nonce": "0x1", - "code": "0xef" - }, - "0x180000000000000000000000000000000000004f": { - "balance": "0x0", - "nonce": "0x1", - "code": "0xef" - }, - "0x1800000000000000000000000000000000000050": { - "balance": "0x0", - "nonce": "0x1", - "code": "0xef" - }, - "0x1800000000000000000000000000000000000051": { - "balance": "0x0", - "nonce": "0x1", - "code": "0xef" - }, - "0x1800000000000000000000000000000000000052": { - "balance": "0x0", - "nonce": "0x1", - "code": "0xef" - }, - "0x1800000000000000000000000000000000000053": { - "balance": "0x0", - "nonce": "0x1", - "code": "0xef" - }, - "0x1800000000000000000000000000000000000054": { - "balance": "0x0", - "nonce": "0x1", - "code": "0xef" - }, - "0x1800000000000000000000000000000000000055": { - "balance": "0x0", - "nonce": "0x1", - "code": "0xef" - }, - "0x1800000000000000000000000000000000000056": { - "balance": "0x0", - "nonce": "0x1", - "code": "0xef" - }, - "0x1800000000000000000000000000000000000057": { - "balance": "0x0", - "nonce": "0x1", - "code": "0xef" - }, - "0x1800000000000000000000000000000000000058": { - "balance": "0x0", - "nonce": "0x1", - "code": "0xef" - }, - "0x1800000000000000000000000000000000000059": { - "balance": "0x0", - "nonce": "0x1", - "code": "0xef" - }, - "0x180000000000000000000000000000000000005a": { - "balance": "0x0", - "nonce": "0x1", - "code": "0xef" - }, - "0x180000000000000000000000000000000000005b": { - "balance": "0x0", - "nonce": "0x1", - "code": "0xef" - }, - "0x180000000000000000000000000000000000005c": { - "balance": "0x0", - "nonce": "0x1", - "code": "0xef" - }, - "0x180000000000000000000000000000000000005d": { - "balance": "0x0", - "nonce": "0x1", - "code": "0xef" - }, - "0x180000000000000000000000000000000000005e": { - "balance": "0x0", - "nonce": "0x1", - "code": "0xef" - }, - "0x180000000000000000000000000000000000005f": { - "balance": "0x0", - "nonce": "0x1", - "code": "0xef" - }, - "0x1800000000000000000000000000000000000060": { - "balance": "0x0", - "nonce": "0x1", - "code": "0xef" - }, - "0x1800000000000000000000000000000000000061": { - "balance": "0x0", - "nonce": "0x1", - "code": "0xef" - }, - "0x1800000000000000000000000000000000000062": { - "balance": "0x0", - "nonce": "0x1", - "code": "0xef" - }, - "0x1800000000000000000000000000000000000063": { - "balance": "0x0", - "nonce": "0x1", - "code": "0xef" - }, - "0x1800000000000000000000000000000000000064": { - "balance": "0x0", - "nonce": "0x1", - "code": "0xef" - }, - "0x1800000000000000000000000000000000000065": { - "balance": "0x0", - "nonce": "0x1", - "code": "0xef" - }, - "0x1800000000000000000000000000000000000066": { - "balance": "0x0", - "nonce": "0x1", - "code": "0xef" - }, - "0x1800000000000000000000000000000000000067": { - "balance": "0x0", - "nonce": "0x1", - "code": "0xef" - }, - "0x1800000000000000000000000000000000000068": { - "balance": "0x0", - "nonce": "0x1", - "code": "0xef" - }, - "0x1800000000000000000000000000000000000069": { - "balance": "0x0", - "nonce": "0x1", - "code": "0xef" - }, - "0x180000000000000000000000000000000000006a": { - "balance": "0x0", - "nonce": "0x1", - "code": "0xef" - }, - "0x180000000000000000000000000000000000006b": { - "balance": "0x0", - "nonce": "0x1", - "code": "0xef" - }, - "0x180000000000000000000000000000000000006c": { - "balance": "0x0", - "nonce": "0x1", - "code": "0xef" - }, - "0x180000000000000000000000000000000000006d": { - "balance": "0x0", - "nonce": "0x1", - "code": "0xef" - }, - "0x180000000000000000000000000000000000006e": { - "balance": "0x0", - "nonce": "0x1", - "code": "0xef" - }, - "0x180000000000000000000000000000000000006f": { - "balance": "0x0", - "nonce": "0x1", - "code": "0xef" - }, - "0x1800000000000000000000000000000000000070": { - "balance": "0x0", - "nonce": "0x1", - "code": "0xef" - }, - "0x1800000000000000000000000000000000000071": { - "balance": "0x0", - "nonce": "0x1", - "code": "0xef" - }, - "0x1800000000000000000000000000000000000072": { - "balance": "0x0", - "nonce": "0x1", - "code": "0xef" - }, - "0x1800000000000000000000000000000000000073": { - "balance": "0x0", - "nonce": "0x1", - "code": "0xef" - }, - "0x1800000000000000000000000000000000000074": { - "balance": "0x0", - "nonce": "0x1", - "code": "0xef" - }, - "0x1800000000000000000000000000000000000075": { - "balance": "0x0", - "nonce": "0x1", - "code": "0xef" - }, - "0x1800000000000000000000000000000000000076": { - "balance": "0x0", - "nonce": "0x1", - "code": "0xef" - }, - "0x1800000000000000000000000000000000000077": { - "balance": "0x0", - "nonce": "0x1", - "code": "0xef" - }, - "0x1800000000000000000000000000000000000078": { - "balance": "0x0", - "nonce": "0x1", - "code": "0xef" - }, - "0x1800000000000000000000000000000000000079": { - "balance": "0x0", - "nonce": "0x1", - "code": "0xef" - }, - "0x180000000000000000000000000000000000007a": { - "balance": "0x0", - "nonce": "0x1", - "code": "0xef" - }, - "0x180000000000000000000000000000000000007b": { - "balance": "0x0", - "nonce": "0x1", - "code": "0xef" - }, - "0x180000000000000000000000000000000000007c": { - "balance": "0x0", - "nonce": "0x1", - "code": "0xef" - }, - "0x180000000000000000000000000000000000007d": { - "balance": "0x0", - "nonce": "0x1", - "code": "0xef" - }, - "0x180000000000000000000000000000000000007e": { - "balance": "0x0", - "nonce": "0x1", - "code": "0xef" - }, - "0x180000000000000000000000000000000000007f": { - "balance": "0x0", - "nonce": "0x1", - "code": "0xef" - }, - "0x1800000000000000000000000000000000000080": { - "balance": "0x0", - "nonce": "0x1", - "code": "0xef" - }, - "0x1800000000000000000000000000000000000081": { - "balance": "0x0", - "nonce": "0x1", - "code": "0xef" - }, - "0x1800000000000000000000000000000000000082": { - "balance": "0x0", - "nonce": "0x1", - "code": "0xef" - }, - "0x1800000000000000000000000000000000000083": { - "balance": "0x0", - "nonce": "0x1", - "code": "0xef" - }, - "0x1800000000000000000000000000000000000084": { - "balance": "0x0", - "nonce": "0x1", - "code": "0xef" - }, - "0x1800000000000000000000000000000000000085": { - "balance": "0x0", - "nonce": "0x1", - "code": "0xef" - }, - "0x1800000000000000000000000000000000000086": { - "balance": "0x0", - "nonce": "0x1", - "code": "0xef" - }, - "0x1800000000000000000000000000000000000087": { - "balance": "0x0", - "nonce": "0x1", - "code": "0xef" - }, - "0x1800000000000000000000000000000000000088": { - "balance": "0x0", - "nonce": "0x1", - "code": "0xef" - }, - "0x1800000000000000000000000000000000000089": { - "balance": "0x0", - "nonce": "0x1", - "code": "0xef" - }, - "0x180000000000000000000000000000000000008a": { - "balance": "0x0", - "nonce": "0x1", - "code": "0xef" - }, - "0x180000000000000000000000000000000000008b": { - "balance": "0x0", - "nonce": "0x1", - "code": "0xef" - }, - "0x180000000000000000000000000000000000008c": { - "balance": "0x0", - "nonce": "0x1", - "code": "0xef" - }, - "0x180000000000000000000000000000000000008d": { - "balance": "0x0", - "nonce": "0x1", - "code": "0xef" - }, - "0x180000000000000000000000000000000000008e": { - "balance": "0x0", - "nonce": "0x1", - "code": "0xef" - }, - "0x180000000000000000000000000000000000008f": { - "balance": "0x0", - "nonce": "0x1", - "code": "0xef" - }, - "0x1800000000000000000000000000000000000090": { - "balance": "0x0", - "nonce": "0x1", - "code": "0xef" - }, - "0x1800000000000000000000000000000000000091": { - "balance": "0x0", - "nonce": "0x1", - "code": "0xef" - }, - "0x1800000000000000000000000000000000000092": { - "balance": "0x0", - "nonce": "0x1", - "code": "0xef" - }, - "0x1800000000000000000000000000000000000093": { - "balance": "0x0", - "nonce": "0x1", - "code": "0xef" - }, - "0x1800000000000000000000000000000000000094": { - "balance": "0x0", - "nonce": "0x1", - "code": "0xef" - }, - "0x1800000000000000000000000000000000000095": { - "balance": "0x0", - "nonce": "0x1", - "code": "0xef" - }, - "0x1800000000000000000000000000000000000096": { - "balance": "0x0", - "nonce": "0x1", - "code": "0xef" - }, - "0x1800000000000000000000000000000000000097": { - "balance": "0x0", - "nonce": "0x1", - "code": "0xef" - }, - "0x1800000000000000000000000000000000000098": { - "balance": "0x0", - "nonce": "0x1", - "code": "0xef" - }, - "0x1800000000000000000000000000000000000099": { - "balance": "0x0", - "nonce": "0x1", - "code": "0xef" - }, - "0x180000000000000000000000000000000000009a": { - "balance": "0x0", - "nonce": "0x1", - "code": "0xef" - }, - "0x180000000000000000000000000000000000009b": { - "balance": "0x0", - "nonce": "0x1", - "code": "0xef" - }, - "0x180000000000000000000000000000000000009c": { - "balance": "0x0", - "nonce": "0x1", - "code": "0xef" - }, - "0x180000000000000000000000000000000000009d": { - "balance": "0x0", - "nonce": "0x1", - "code": "0xef" - }, - "0x180000000000000000000000000000000000009e": { - "balance": "0x0", - "nonce": "0x1", - "code": "0xef" - }, - "0x180000000000000000000000000000000000009f": { - "balance": "0x0", - "nonce": "0x1", - "code": "0xef" - }, - "0x18000000000000000000000000000000000000a0": { - "balance": "0x0", - "nonce": "0x1", - "code": "0xef" - }, - "0x18000000000000000000000000000000000000a1": { - "balance": "0x0", - "nonce": "0x1", - "code": "0xef" - }, - "0x18000000000000000000000000000000000000a2": { - "balance": "0x0", - "nonce": "0x1", - "code": "0xef" - }, - "0x18000000000000000000000000000000000000a3": { - "balance": "0x0", - "nonce": "0x1", - "code": "0xef" - }, - "0x18000000000000000000000000000000000000a4": { - "balance": "0x0", - "nonce": "0x1", - "code": "0xef" - }, - "0x18000000000000000000000000000000000000a5": { - "balance": "0x0", - "nonce": "0x1", - "code": "0xef" - }, - "0x18000000000000000000000000000000000000a6": { - "balance": "0x0", - "nonce": "0x1", - "code": "0xef" - }, - "0x18000000000000000000000000000000000000a7": { - "balance": "0x0", - "nonce": "0x1", - "code": "0xef" - }, - "0x18000000000000000000000000000000000000a8": { - "balance": "0x0", - "nonce": "0x1", - "code": "0xef" - }, - "0x18000000000000000000000000000000000000a9": { - "balance": "0x0", - "nonce": "0x1", - "code": "0xef" - }, - "0x18000000000000000000000000000000000000aa": { - "balance": "0x0", - "nonce": "0x1", - "code": "0xef" - }, - "0x18000000000000000000000000000000000000ab": { - "balance": "0x0", - "nonce": "0x1", - "code": "0xef" - }, - "0x18000000000000000000000000000000000000ac": { - "balance": "0x0", - "nonce": "0x1", - "code": "0xef" - }, - "0x18000000000000000000000000000000000000ad": { - "balance": "0x0", - "nonce": "0x1", - "code": "0xef" - }, - "0x18000000000000000000000000000000000000ae": { - "balance": "0x0", - "nonce": "0x1", - "code": "0xef" - }, - "0x18000000000000000000000000000000000000af": { - "balance": "0x0", - "nonce": "0x1", - "code": "0xef" - }, - "0x18000000000000000000000000000000000000b0": { - "balance": "0x0", - "nonce": "0x1", - "code": "0xef" - }, - "0x18000000000000000000000000000000000000b1": { - "balance": "0x0", - "nonce": "0x1", - "code": "0xef" - }, - "0x18000000000000000000000000000000000000b2": { - "balance": "0x0", - "nonce": "0x1", - "code": "0xef" - }, - "0x18000000000000000000000000000000000000b3": { - "balance": "0x0", - "nonce": "0x1", - "code": "0xef" - }, - "0x18000000000000000000000000000000000000b4": { - "balance": "0x0", - "nonce": "0x1", - "code": "0xef" - }, - "0x18000000000000000000000000000000000000b5": { - "balance": "0x0", - "nonce": "0x1", - "code": "0xef" - }, - "0x18000000000000000000000000000000000000b6": { - "balance": "0x0", - "nonce": "0x1", - "code": "0xef" - }, - "0x18000000000000000000000000000000000000b7": { - "balance": "0x0", - "nonce": "0x1", - "code": "0xef" - }, - "0x18000000000000000000000000000000000000b8": { - "balance": "0x0", - "nonce": "0x1", - "code": "0xef" - }, - "0x18000000000000000000000000000000000000b9": { - "balance": "0x0", - "nonce": "0x1", - "code": "0xef" - }, - "0x18000000000000000000000000000000000000ba": { - "balance": "0x0", - "nonce": "0x1", - "code": "0xef" - }, - "0x18000000000000000000000000000000000000bb": { - "balance": "0x0", - "nonce": "0x1", - "code": "0xef" - }, - "0x18000000000000000000000000000000000000bc": { - "balance": "0x0", - "nonce": "0x1", - "code": "0xef" - }, - "0x18000000000000000000000000000000000000bd": { - "balance": "0x0", - "nonce": "0x1", - "code": "0xef" - }, - "0x18000000000000000000000000000000000000be": { - "balance": "0x0", - "nonce": "0x1", - "code": "0xef" - }, - "0x18000000000000000000000000000000000000bf": { - "balance": "0x0", - "nonce": "0x1", - "code": "0xef" - }, - "0x18000000000000000000000000000000000000c0": { - "balance": "0x0", - "nonce": "0x1", - "code": "0xef" - }, - "0x18000000000000000000000000000000000000c1": { - "balance": "0x0", - "nonce": "0x1", - "code": "0xef" - }, - "0x18000000000000000000000000000000000000c2": { - "balance": "0x0", - "nonce": "0x1", - "code": "0xef" - }, - "0x18000000000000000000000000000000000000c3": { - "balance": "0x0", - "nonce": "0x1", - "code": "0xef" - }, - "0x18000000000000000000000000000000000000c4": { - "balance": "0x0", - "nonce": "0x1", - "code": "0xef" - }, - "0x18000000000000000000000000000000000000c5": { - "balance": "0x0", - "nonce": "0x1", - "code": "0xef" - }, - "0x18000000000000000000000000000000000000c6": { - "balance": "0x0", - "nonce": "0x1", - "code": "0xef" - }, - "0x18000000000000000000000000000000000000c7": { - "balance": "0x0", - "nonce": "0x1", - "code": "0xef" - }, - "0x18000000000000000000000000000000000000c8": { - "balance": "0x0", - "nonce": "0x1", - "code": "0xef" - }, - "0x18000000000000000000000000000000000000c9": { - "balance": "0x0", - "nonce": "0x1", - "code": "0xef" - }, - "0x18000000000000000000000000000000000000ca": { - "balance": "0x0", - "nonce": "0x1", - "code": "0xef" - }, - "0x18000000000000000000000000000000000000cb": { - "balance": "0x0", - "nonce": "0x1", - "code": "0xef" - }, - "0x18000000000000000000000000000000000000cc": { - "balance": "0x0", - "nonce": "0x1", - "code": "0xef" - }, - "0x18000000000000000000000000000000000000cd": { - "balance": "0x0", - "nonce": "0x1", - "code": "0xef" - }, - "0x18000000000000000000000000000000000000ce": { - "balance": "0x0", - "nonce": "0x1", - "code": "0xef" - }, - "0x18000000000000000000000000000000000000cf": { - "balance": "0x0", - "nonce": "0x1", - "code": "0xef" - }, - "0x18000000000000000000000000000000000000d0": { - "balance": "0x0", - "nonce": "0x1", - "code": "0xef" - }, - "0x18000000000000000000000000000000000000d1": { - "balance": "0x0", - "nonce": "0x1", - "code": "0xef" - }, - "0x18000000000000000000000000000000000000d2": { - "balance": "0x0", - "nonce": "0x1", - "code": "0xef" - }, - "0x18000000000000000000000000000000000000d3": { - "balance": "0x0", - "nonce": "0x1", - "code": "0xef" - }, - "0x18000000000000000000000000000000000000d4": { - "balance": "0x0", - "nonce": "0x1", - "code": "0xef" - }, - "0x18000000000000000000000000000000000000d5": { - "balance": "0x0", - "nonce": "0x1", - "code": "0xef" - }, - "0x18000000000000000000000000000000000000d6": { - "balance": "0x0", - "nonce": "0x1", - "code": "0xef" - }, - "0x18000000000000000000000000000000000000d7": { - "balance": "0x0", - "nonce": "0x1", - "code": "0xef" - }, - "0x18000000000000000000000000000000000000d8": { - "balance": "0x0", - "nonce": "0x1", - "code": "0xef" - }, - "0x18000000000000000000000000000000000000d9": { - "balance": "0x0", - "nonce": "0x1", - "code": "0xef" - }, - "0x18000000000000000000000000000000000000da": { - "balance": "0x0", - "nonce": "0x1", - "code": "0xef" - }, - "0x18000000000000000000000000000000000000db": { - "balance": "0x0", - "nonce": "0x1", - "code": "0xef" - }, - "0x18000000000000000000000000000000000000dc": { - "balance": "0x0", - "nonce": "0x1", - "code": "0xef" - }, - "0x18000000000000000000000000000000000000dd": { - "balance": "0x0", - "nonce": "0x1", - "code": "0xef" - }, - "0x18000000000000000000000000000000000000de": { - "balance": "0x0", - "nonce": "0x1", - "code": "0xef" - }, - "0x18000000000000000000000000000000000000df": { - "balance": "0x0", - "nonce": "0x1", - "code": "0xef" - }, - "0x18000000000000000000000000000000000000e0": { - "balance": "0x0", - "nonce": "0x1", - "code": "0xef" - }, - "0x18000000000000000000000000000000000000e1": { - "balance": "0x0", - "nonce": "0x1", - "code": "0xef" - }, - "0x18000000000000000000000000000000000000e2": { - "balance": "0x0", - "nonce": "0x1", - "code": "0xef" - }, - "0x18000000000000000000000000000000000000e3": { - "balance": "0x0", - "nonce": "0x1", - "code": "0xef" - }, - "0x18000000000000000000000000000000000000e4": { - "balance": "0x0", - "nonce": "0x1", - "code": "0xef" - }, - "0x18000000000000000000000000000000000000e5": { - "balance": "0x0", - "nonce": "0x1", - "code": "0xef" - }, - "0x18000000000000000000000000000000000000e6": { - "balance": "0x0", - "nonce": "0x1", - "code": "0xef" - }, - "0x18000000000000000000000000000000000000e7": { - "balance": "0x0", - "nonce": "0x1", - "code": "0xef" - }, - "0x18000000000000000000000000000000000000e8": { - "balance": "0x0", - "nonce": "0x1", - "code": "0xef" - }, - "0x18000000000000000000000000000000000000e9": { - "balance": "0x0", - "nonce": "0x1", - "code": "0xef" - }, - "0x18000000000000000000000000000000000000ea": { - "balance": "0x0", - "nonce": "0x1", - "code": "0xef" - }, - "0x18000000000000000000000000000000000000eb": { - "balance": "0x0", - "nonce": "0x1", - "code": "0xef" - }, - "0x18000000000000000000000000000000000000ec": { - "balance": "0x0", - "nonce": "0x1", - "code": "0xef" - }, - "0x18000000000000000000000000000000000000ed": { - "balance": "0x0", - "nonce": "0x1", - "code": "0xef" - }, - "0x18000000000000000000000000000000000000ee": { - "balance": "0x0", - "nonce": "0x1", - "code": "0xef" - }, - "0x18000000000000000000000000000000000000ef": { - "balance": "0x0", - "nonce": "0x1", - "code": "0xef" - }, - "0x18000000000000000000000000000000000000f0": { - "balance": "0x0", - "nonce": "0x1", - "code": "0xef" - }, - "0x18000000000000000000000000000000000000f1": { - "balance": "0x0", - "nonce": "0x1", - "code": "0xef" - }, - "0x18000000000000000000000000000000000000f2": { - "balance": "0x0", - "nonce": "0x1", - "code": "0xef" - }, - "0x18000000000000000000000000000000000000f3": { - "balance": "0x0", - "nonce": "0x1", - "code": "0xef" - }, - "0x18000000000000000000000000000000000000f4": { - "balance": "0x0", - "nonce": "0x1", - "code": "0xef" - }, - "0x18000000000000000000000000000000000000f5": { - "balance": "0x0", - "nonce": "0x1", - "code": "0xef" - }, - "0x18000000000000000000000000000000000000f6": { - "balance": "0x0", - "nonce": "0x1", - "code": "0xef" - }, - "0x18000000000000000000000000000000000000f7": { - "balance": "0x0", - "nonce": "0x1", - "code": "0xef" - }, - "0x18000000000000000000000000000000000000f8": { - "balance": "0x0", - "nonce": "0x1", - "code": "0xef" - }, - "0x18000000000000000000000000000000000000f9": { - "balance": "0x0", - "nonce": "0x1", - "code": "0xef" - }, - "0x18000000000000000000000000000000000000fa": { - "balance": "0x0", - "nonce": "0x1", - "code": "0xef" - }, - "0x18000000000000000000000000000000000000fb": { - "balance": "0x0", - "nonce": "0x1", - "code": "0xef" - }, - "0x18000000000000000000000000000000000000fc": { - "balance": "0x0", - "nonce": "0x1", - "code": "0xef" - }, - "0x18000000000000000000000000000000000000fd": { - "balance": "0x0", - "nonce": "0x1", - "code": "0xef" - }, - "0x18000000000000000000000000000000000000fe": { - "balance": "0x0", - "nonce": "0x1", - "code": "0xef" - }, - "0x18000000000000000000000000000000000000ff": { - "balance": "0x0", - "nonce": "0x1", - "code": "0xef" - }, - "0x50A2b0B577eC24d7ce1aeD372A8a6fd14CE1bE57": { - "balance": "0x21e19e0c9bab2400000" - }, - "0x1800000000000000000000000000000000000000": { - "balance": "0x0", - "nonce": "0x1", - "code": "0xef", - "storage": { - "0x0000000000000000000000000000000000000000000000000000000000000002": "0x00000000000000000000000000000000000000000000021e19e0c9bab2400000" - } - }, - "0x1800000000000000000000000000000000000001": { - "balance": "0x0", - "nonce": "0x1", - "code": "0xef", - "storage": { - "0xc141df5e4f31a4ad000065d1c11b38075b88c74e6e761baa68cd227ec7b803c6": "0x0000000000000000000000000000000000000000000000000000000000000001" - } - } - } -} diff --git a/assets/testnet/.gitignore b/assets/testnet/.gitignore deleted file mode 100644 index cab975a..0000000 --- a/assets/testnet/.gitignore +++ /dev/null @@ -1,4 +0,0 @@ -jwtsecret - -validator*.json -wallet-secrets.json diff --git a/assets/testnet/config.json b/assets/testnet/config.json deleted file mode 100644 index c019427..0000000 --- a/assets/testnet/config.json +++ /dev/null @@ -1,179 +0,0 @@ -{ - "timestamp": "1757515087", - "coinbase": "0xa693CC18Aa09d33dD388013B7A02E5Ff863b8760", - "NativeFiatToken": { - "proxy": { - "admin": "0x49f78af090F1f98e7184B7f61f1F1a8a8064b40d" - }, - "owner": "0xDC29Bab4A7d5425cA44eeF20a5B67E3D897F9a03", - "pauser": "0xbc639a0A060E5831a7c437b491B8d3C1f58F554e", - "masterMinter": "0xc7EdB7223a08a11D05cbE83F436816F1dc7Ea541", - "rescuer": "0x28E2716951f9fC8dF918e1BeDB54E4e2BDB2a362", - "blacklister": "0xE29672dcF004AE74da3700262D384ce23de16729", - "minters": [ - { - "address": "0x735A7918f97Fbd3Cb5A5f72B0D1dDfdc9a7D8A86", - "allowance": "1000000000000000000000000" - } - ] - }, - "ProtocolConfig": { - "proxy": { - "admin": "0xD2C1210Ce932A416e1f9f3147E404d37696D0c41" - }, - "owner": "0x2904f3A8226630b40cD5BF414fF5c112E2A0aAC6", - "controller": "0x5196FF0e6f24ca69d07539A0547fD84988D29feE", - "pauser": "0xe09f434194E5430301F8D80fD1B7676A1052DF47", - "beneficiary": "0xa693CC18Aa09d33dD388013B7A02E5Ff863b8760", - "feeParams": { - "alpha": "20", - "kRate": "25", - "inverseElasticityMultiplier": "5000", - "minBaseFee": "1", - "maxBaseFee": "1000", - "blockGasLimit": "30000000" - } - }, - "ValidatorManager": { - "proxy": { - "admin": "0x605fa3FeF59b7bA218c910E6671Ce88CB9D2F1bf" - }, - "PermissionedValidatorManager": { - "proxy": { - "admin": "0x456f90Eb4023Bcc821C1a9164046C1Bd92898696" - }, - "owner": "0x5f5dC64f8926Dd61C57812565D308c04Be68546A", - "validatorRegisterers": [ - "0xB8217B0eFa254e56f902DB6cCA2f1Cd88E5251d5", - "0x5F3b933Bb7637944683eE6CECE4a24C8E1205c42" - ], - "controllers": [ - "0xca1e629E0652bbf5784c2b8D0B45723Bd61D5FEE", - "0x6A34bC9F35a753A946D1829082E804980f8F35D3", - "0x18965Fff34CaFD729159E761f552915D85D6cE79", - "0xa1C4230035EA81ECE502a1D316910C6d60b04cE8", - "0x749Deb48FE74bbbC3aADeEaF19ED03F3f39e5ce4", - "0x0E5BeEEC3eBCF2b6FF35b6eB7D55fB775D8C7264", - "0x030eefff3aE6e1d3b04B16d1AC7df4498dC29317", - "0xE3A331A13d27B1E63Be687b9b773b4e362823ff0", - "0x0799c3ac9FB9c7BD89979D63A664Bf10Eaf008FB" - ] - }, - "validators": [ - { - "publicKey": "0x8396f9da70e4daf1af3bcba234d7247f34bceee859a83b09d9b22e938ce0eccb", - "votingPower": "2000" - }, - { - "publicKey": "0x634059e702a3d9991215218098266f65cd363f82e4a8f82862bdcc36761116da", - "votingPower": "2000" - }, - { - "publicKey": "0xc3dcde30cbe413e56c023514d2eef54db3ef7330542ae0adc3122696175d194e", - "votingPower": "2000" - } - ] - }, - "prefund": [ - { - "address": "0x49f78af090F1f98e7184B7f61f1F1a8a8064b40d", - "balance": "1000000000000000000000" - }, - { - "address": "0xDC29Bab4A7d5425cA44eeF20a5B67E3D897F9a03", - "balance": "1000000000000000000000" - }, - { - "address": "0xbc639a0A060E5831a7c437b491B8d3C1f58F554e", - "balance": "1000000000000000000000" - }, - { - "address": "0xc7EdB7223a08a11D05cbE83F436816F1dc7Ea541", - "balance": "1000000000000000000000" - }, - { - "address": "0x28E2716951f9fC8dF918e1BeDB54E4e2BDB2a362", - "balance": "1000000000000000000000" - }, - { - "address": "0xE29672dcF004AE74da3700262D384ce23de16729", - "balance": "1000000000000000000000" - }, - { - "address": "0x735A7918f97Fbd3Cb5A5f72B0D1dDfdc9a7D8A86", - "balance": "1000000000000000000000" - }, - { - "address": "0xD2C1210Ce932A416e1f9f3147E404d37696D0c41", - "balance": "1000000000000000000000" - }, - { - "address": "0x2904f3A8226630b40cD5BF414fF5c112E2A0aAC6", - "balance": "1000000000000000000000" - }, - { - "address": "0x5196FF0e6f24ca69d07539A0547fD84988D29feE", - "balance": "1000000000000000000000" - }, - { - "address": "0xe09f434194E5430301F8D80fD1B7676A1052DF47", - "balance": "1000000000000000000000" - }, - { - "address": "0x605fa3FeF59b7bA218c910E6671Ce88CB9D2F1bf", - "balance": "1000000000000000000000" - }, - { - "address": "0x456f90Eb4023Bcc821C1a9164046C1Bd92898696", - "balance": "1000000000000000000000" - }, - { - "address": "0x5f5dC64f8926Dd61C57812565D308c04Be68546A", - "balance": "1000000000000000000000" - }, - { - "address": "0xB8217B0eFa254e56f902DB6cCA2f1Cd88E5251d5", - "balance": "1000000000000000000000" - }, - { - "address": "0x5F3b933Bb7637944683eE6CECE4a24C8E1205c42", - "balance": "1000000000000000000000" - }, - { - "address": "0xca1e629E0652bbf5784c2b8D0B45723Bd61D5FEE", - "balance": "1000000000000000000000" - }, - { - "address": "0x6A34bC9F35a753A946D1829082E804980f8F35D3", - "balance": "1000000000000000000000" - }, - { - "address": "0x18965Fff34CaFD729159E761f552915D85D6cE79", - "balance": "1000000000000000000000" - }, - { - "address": "0xa1C4230035EA81ECE502a1D316910C6d60b04cE8", - "balance": "1000000000000000000000" - }, - { - "address": "0x749Deb48FE74bbbC3aADeEaF19ED03F3f39e5ce4", - "balance": "1000000000000000000000" - }, - { - "address": "0x0E5BeEEC3eBCF2b6FF35b6eB7D55fB775D8C7264", - "balance": "1000000000000000000000" - }, - { - "address": "0x030eefff3aE6e1d3b04B16d1AC7df4498dC29317", - "balance": "1000000000000000000000" - }, - { - "address": "0xE3A331A13d27B1E63Be687b9b773b4e362823ff0", - "balance": "1000000000000000000000" - }, - { - "address": "0x0799c3ac9FB9c7BD89979D63A664Bf10Eaf008FB", - "balance": "1000000000000000000000" - } - ] -} \ No newline at end of file diff --git a/assets/testnet/genesis.config.ts b/assets/testnet/genesis.config.ts deleted file mode 100644 index 5e70228..0000000 --- a/assets/testnet/genesis.config.ts +++ /dev/null @@ -1,128 +0,0 @@ -// Copyright 2026 Circle Internet Group, Inc. All rights reserved. -// -// SPDX-License-Identifier: Apache-2.0 -// -// Licensed under the Apache License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. -// You may obtain a copy of the License at -// -// http://www.apache.org/licenses/LICENSE-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// See the License for the specific language governing permissions and -// limitations under the License. - -import { parseEther } from 'viem' -import { createBuilderContext, buildGenesis, schemaGenesisConfig, GenesisConfig } from '../../scripts/genesis' -import fs from 'fs' -import path from 'path' -import { bigintReplacer, currentTimestamp } from '../../scripts/genesis/types' -import { MnemonicAccountCreator } from '../../scripts/genesis/AccountCreator' - -const build = async () => { - const ctx = await createBuilderContext({ network: 'testnet', chainId: 5042002 }) - const configPath = path.join(ctx.projectRoot, `assets/${ctx.network}/config.json`) - const walletSecretsPath = path.join(ctx.projectRoot, `assets/${ctx.network}/wallet-secrets.json`) - - // Load existing config file for CI - if (fs.existsSync(configPath)) { - const config = schemaGenesisConfig.parse(JSON.parse(fs.readFileSync(configPath, 'utf8'))) - return await buildGenesis(ctx, config) - } - - if (!process.env.ARC_TESTNET_ADMIN_MNEMONIC) { - throw new Error('ARC_TESTNET_ADMIN_MNEMONIC is not set') - } - if (!process.env.ARC_TESTNET_VALIDATOR_MNEMONIC) { - throw new Error('ARC_TESTNET_VALIDATOR_MNEMONIC is not set') - } - const creator = new MnemonicAccountCreator({ - adminMnemonic: process.env.ARC_TESTNET_ADMIN_MNEMONIC, - validatorMnemonic: process.env.ARC_TESTNET_VALIDATOR_MNEMONIC, - }) - const adminPrefund = parseEther('1000') - - const config: GenesisConfig = { - timestamp: currentTimestamp(), - coinbase: '0xa693CC18Aa09d33dD388013B7A02E5Ff863b8760', - - NativeFiatToken: { - proxy: { admin: creator.nextAccount('FiatTokenCircleChain.proxyAdmin', adminPrefund) }, - owner: creator.nextAccount('FiatTokenCircleChain.owner', adminPrefund), - pauser: creator.nextAccount('FiatTokenCircleChain.pauser', adminPrefund), - masterMinter: creator.nextAccount('FiatTokenCircleChain.masterMinter', adminPrefund), - rescuer: creator.nextAccount('FiatTokenCircleChain.rescuer', adminPrefund), - blacklister: creator.nextAccount('FiatTokenCircleChain.blacklister', adminPrefund), - minters: [ - { - address: creator.nextAccount('FiatTokenCircleChain.minter', adminPrefund), - allowance: parseEther('1000000'), - }, - ], - }, - - ProtocolConfig: { - proxy: { admin: creator.nextAccount('ProtocolConfig.proxyAdmin', adminPrefund) }, - owner: creator.nextAccount('ProtocolConfig.owner', adminPrefund), - controller: creator.nextAccount('ProtocolConfig.controller', adminPrefund), - pauser: creator.nextAccount('ProtocolConfig.pauser', adminPrefund), - feeParams: { - alpha: 20n, - kRate: 25n, - inverseElasticityMultiplier: 5000n, - minBaseFee: 1n, - maxBaseFee: 1000n, - blockGasLimit: 30_000_000n, - }, - }, - - ValidatorManager: { - proxy: { admin: creator.nextAccount('ValidatorManager.proxyAdmin', adminPrefund) }, - PermissionedValidatorManager: { - proxy: { admin: creator.nextAccount('PermissionedValidatorManager.proxyAdmin', adminPrefund) }, - owner: creator.nextAccount('PermissionedValidatorManager.owner', adminPrefund), - validatorRegisterers: [ - creator.nextAccount('PermissionedValidatorManager.validatorRegisterer1', adminPrefund), - creator.nextAccount('PermissionedValidatorManager.validatorRegisterer2', adminPrefund), - ], - controllers: [ - creator.nextAccount('PermissionedValidatorManager.controller1', adminPrefund), - creator.nextAccount('PermissionedValidatorManager.controller2', adminPrefund), - creator.nextAccount('PermissionedValidatorManager.controller3', adminPrefund), - creator.nextAccount('PermissionedValidatorManager.controller4', adminPrefund), - creator.nextAccount('PermissionedValidatorManager.controller5', adminPrefund), - creator.nextAccount('PermissionedValidatorManager.controller6', adminPrefund), - creator.nextAccount('PermissionedValidatorManager.controller7', adminPrefund), - creator.nextAccount('PermissionedValidatorManager.controller8', adminPrefund), - creator.nextAccount('PermissionedValidatorManager.controller9', adminPrefund), - ], - }, - validators: [ - await creator.nextValidatorKey('validator1', 2000n), - await creator.nextValidatorKey('validator2', 2000n), - await creator.nextValidatorKey('validator3', 2000n), - ], - }, - } - - // Add the prefund accounts, collect from the creator. - config.prefund = creator.getPrefunds() - - // Output secrets for the first time generation - fs.writeFileSync(walletSecretsPath, JSON.stringify(creator.getAdminConfig(), null, 2)) - for (const validator of creator.getValidatorConfig()) { - fs.writeFileSync( - path.join(`assets/${ctx.network}`, `${validator.name}.json`), - JSON.stringify(validator, bigintReplacer, 2), - ) - } - - // Save config to file. Then CI do not required the mnemonic. - fs.writeFileSync(configPath, JSON.stringify(config, bigintReplacer, 2)) - - return await buildGenesis(ctx, config) -} - -export default build diff --git a/assets/testnet/genesis.json b/assets/testnet/genesis.json deleted file mode 100644 index 0c8349e..0000000 --- a/assets/testnet/genesis.json +++ /dev/null @@ -1,1531 +0,0 @@ -{ - "config": { - "chainId": 5042002, - "daoForkSupport": false, - "terminalTotalDifficulty": "0x0", - "terminalTotalDifficultyPassed": true, - "homesteadBlock": 0, - "eip150Block": 0, - "eip150Hash": "0x0000000000000000000000000000000000000000000000000000000000000000", - "eip155Block": 0, - "eip158Block": 0, - "byzantiumBlock": 0, - "constantinopleBlock": 0, - "petersburgBlock": 0, - "istanbulBlock": 0, - "muirGlacierBlock": 0, - "berlinBlock": 0, - "londonBlock": 0, - "arrowGlacierBlock": 0, - "grayGlacierBlock": 0, - "shanghaiTime": 0, - "cancunTime": 0, - "pragueTime": 0 - }, - "nonce": "0x0", - "timestamp": "0x68c18d4f", - "extraData": "0x", - "gasLimit": "0x1c9c380", - "difficulty": "0x0", - "mixHash": "0x0000000000000000000000000000000000000000000000000000000000000000", - "coinbase": "0xa693CC18Aa09d33dD388013B7A02E5Ff863b8760", - "number": "0x0", - "alloc": { - "0x3600000000000000000000000000000000000000": { - "balance": "0x0", - "nonce": "0x1", - "code": "0x60806040526004361061005a5760003560e01c80635c60da1b116100435780635c60da1b146101315780638f2839701461016f578063f851a440146101af5761005a565b80633659cfe6146100645780634f1ef286146100a4575b6100626101c4565b005b34801561007057600080fd5b506100626004803603602081101561008757600080fd5b503573ffffffffffffffffffffffffffffffffffffffff166101de565b610062600480360360408110156100ba57600080fd5b73ffffffffffffffffffffffffffffffffffffffff82351691908101906040810160208201356401000000008111156100f257600080fd5b82018360208201111561010457600080fd5b8035906020019184600183028401116401000000008311171561012657600080fd5b509092509050610232565b34801561013d57600080fd5b50610146610309565b6040805173ffffffffffffffffffffffffffffffffffffffff9092168252519081900360200190f35b34801561017b57600080fd5b506100626004803603602081101561019257600080fd5b503573ffffffffffffffffffffffffffffffffffffffff16610318565b3480156101bb57600080fd5b50610146610420565b6101cc610430565b6101dc6101d76104c4565b6104e9565b565b6101e661050d565b73ffffffffffffffffffffffffffffffffffffffff163373ffffffffffffffffffffffffffffffffffffffff1614156102275761022281610532565b61022f565b61022f6101c4565b50565b61023a61050d565b73ffffffffffffffffffffffffffffffffffffffff163373ffffffffffffffffffffffffffffffffffffffff1614156102fc5761027683610532565b60003073ffffffffffffffffffffffffffffffffffffffff16348484604051808383808284376040519201945060009350909150508083038185875af1925050503d80600081146102e3576040519150601f19603f3d011682016040523d82523d6000602084013e6102e8565b606091505b50509050806102f657600080fd5b50610304565b6103046101c4565b505050565b60006103136104c4565b905090565b61032061050d565b73ffffffffffffffffffffffffffffffffffffffff163373ffffffffffffffffffffffffffffffffffffffff1614156102275773ffffffffffffffffffffffffffffffffffffffff81166103bf576040517f08c379a00000000000000000000000000000000000000000000000000000000081526004018080602001828103825260368152602001806106606036913960400191505060405180910390fd5b7f7e644d79422f17c01e4894b5f4f588d331ebfa28653d42ae832dc59e38c9798f6103e861050d565b6040805173ffffffffffffffffffffffffffffffffffffffff928316815291841660208301528051918290030190a161022281610587565b600061031361050d565b3b151590565b61043861050d565b73ffffffffffffffffffffffffffffffffffffffff163373ffffffffffffffffffffffffffffffffffffffff1614156104bc576040517f08c379a000000000000000000000000000000000000000000000000000000000815260040180806020018281038252603281526020018061062e6032913960400191505060405180910390fd5b6101dc6101dc565b7f7050c9e0f4ca769c69bd3a8ef740bc37934f8e2c036e5a723fd8ee048ed3f8c35490565b3660008037600080366000845af43d6000803e808015610508573d6000f35b3d6000fd5b7f10d6a54a4754c8869d6886b5f5d7fbfa5b4522237ea5c60d11bc4e7a1ff9390b5490565b61053b816105ab565b6040805173ffffffffffffffffffffffffffffffffffffffff8316815290517fbc7cd75a20ee27fd9adebab32041f755214dbc6bffa90cc0225b39da2e5c2d3b9181900360200190a150565b7f10d6a54a4754c8869d6886b5f5d7fbfa5b4522237ea5c60d11bc4e7a1ff9390b55565b6105b48161042a565b610609576040517f08c379a000000000000000000000000000000000000000000000000000000000815260040180806020018281038252603b815260200180610696603b913960400191505060405180910390fd5b7f7050c9e0f4ca769c69bd3a8ef740bc37934f8e2c036e5a723fd8ee048ed3f8c35556fe43616e6e6f742063616c6c2066616c6c6261636b2066756e6374696f6e2066726f6d207468652070726f78792061646d696e43616e6e6f74206368616e6765207468652061646d696e206f6620612070726f787920746f20746865207a65726f206164647265737343616e6e6f742073657420612070726f787920696d706c656d656e746174696f6e20746f2061206e6f6e2d636f6e74726163742061646472657373a2646970667358221220015908007e367e7f333ec38084b88f027c06160d2c19e5bdd8027e8d06acf8bf64736f6c634300060c0033", - "storage": { - "0x10d6a54a4754c8869d6886b5f5d7fbfa5b4522237ea5c60d11bc4e7a1ff9390b": "0x00000000000000000000000049f78af090f1f98e7184b7f61f1f1a8a8064b40d", - "0x7050c9e0f4ca769c69bd3a8ef740bc37934f8e2c036e5a723fd8ee048ed3f8c3": "0x000000000000000000000000f44daeafa093c414441a1fd668ac4acc73855e7d", - "0x0000000000000000000000000000000000000000000000000000000000000000": "0x000000000000000000000000dc29bab4a7d5425ca44eef20a5b67e3d897f9a03", - "0x0000000000000000000000000000000000000000000000000000000000000001": "0x000000000000000000000000bc639a0a060e5831a7c437b491b8d3c1f58f554e", - "0x0000000000000000000000000000000000000000000000000000000000000002": "0x000000000000000000000000e29672dcf004ae74da3700262d384ce23de16729", - "0x0000000000000000000000000000000000000000000000000000000000000004": "0x5553444300000000000000000000000000000000000000000000000000000008", - "0x0000000000000000000000000000000000000000000000000000000000000005": "0x5553444300000000000000000000000000000000000000000000000000000008", - "0x0000000000000000000000000000000000000000000000000000000000000006": "0x0000000000000000000000000000000000000000000000000000000000000006", - "0x0000000000000000000000000000000000000000000000000000000000000007": "0x5553440000000000000000000000000000000000000000000000000000000006", - "0x0000000000000000000000000000000000000000000000000000000000000008": "0x000000000000000000000001c7EdB7223a08a11D05cbE83F436816F1dc7Ea541", - "0x50b67ea6a636af6227ac26ec7743f95891f2e0ecd316d7a3a5b894cf7a0fb5f8": "0x0000000000000000000000000000000000000000000000000000000000000001", - "0x563038b7b6c29998dee42f276d03142f54b99bbf972b313c386f200463316330": "0x00000000000000000000000000000000000000000000d3c21bcecceda1000000", - "0x000000000000000000000000000000000000000000000000000000000000000e": "0x00000000000000000000000028e2716951f9fc8df918e1bedb54e4e2bdb2a362", - "0x0000000000000000000000000000000000000000000000000000000000000012": "0x0000000000000000000000000000000000000000000000000000000000000003" - } - }, - "0xF44DaEafA093C414441a1fD668Ac4AcC73855e7d": { - "balance": "0x0", - "nonce": "0x1", - "code": "0x608060405234801561001057600080fd5b506004361061038e5760003560e01c806388b7ab63116101de578063bd1024301161010f578063dd62ed3e116100ad578063ef55bec61161007c578063ef55bec61461115b578063f2fde38b146111c7578063f9f92be4146111fa578063fe575a871461122d5761038e565b8063dd62ed3e14611073578063e3ee160e146110ae578063e5a6b10f1461111a578063e94a0102146111225761038e565b8063d505accf116100e9578063d505accf14610f95578063d608ea6414610ff3578063d916948714611063578063dd0743b31461106b5761038e565b8063bd10243014610ea1578063ccd92d3e14610ea9578063cf09299514610eb15761038e565b8063a457c2d71161017c578063aa271e1a11610156578063aa271e1a14610d30578063ad38bf2214610d63578063b2118a8d14610d96578063b7b7289914610dd95761038e565b8063a457c2d714610c8b578063a9059cbb14610cc4578063aa20e1e414610cfd5761038e565b806395d89b41116101b857806395d89b4114610b9b5780639fd0506d14610ba35780639fd5a6cf14610bab578063a0cc6a6814610c835761038e565b806388b7ab6314610a7c5780638a6db9c314610b605780638da5cb5b14610b935761038e565b806339509351116102c3578063554bab3c116102615780637ecebe00116102305780637ecebe0014610a315780637f2eecc314610a645780637fd0991614610a6c5780638456cb5914610a745761038e565b8063554bab3c146109755780635a049a70146109a85780635c975abb146109f657806370a08231146109fe5761038e565b806342966c681161029d57806342966c6814610855578063430239b4146108725780634e44d9561461093457806354fd4d501461096d5761038e565b806339509351146107db5780633f4ba83a1461081457806340c10f191461081c5761038e565b80633092afd5116103305780633357162b1161030a5780633357162b146105ae57806335d99f351461079a5780633644e515146107cb57806338a63183146107d35761038e565b80633092afd51461055557806330adf81f14610588578063313ce567146105905761038e565b80631a8952661161036c5780631a8952661461047757806323b872dd146104ac5780632ab60045146104ef5780632fc81e09146105225761038e565b806306fdde0314610393578063095ea7b31461041057806318160ddd1461045d575b600080fd5b61039b611260565b6040805160208082528351818301528351919283929083019185019080838360005b838110156103d55781810151838201526020016103bd565b50505050905090810190601f1680156104025780820380516001836020036101000a031916815260200191505b509250505060405180910390f35b6104496004803603604081101561042657600080fd5b5073ffffffffffffffffffffffffffffffffffffffff813516906020013561130c565b604080519115158252519081900360200190f35b6104656113ae565b60408051918252519081900360200190f35b6104aa6004803603602081101561048d57600080fd5b503573ffffffffffffffffffffffffffffffffffffffff1661144e565b005b610449600480360360608110156104c257600080fd5b5073ffffffffffffffffffffffffffffffffffffffff81358116916020810135909116906040013561150b565b6104aa6004803603602081101561050557600080fd5b503573ffffffffffffffffffffffffffffffffffffffff16611703565b6104aa6004803603602081101561053857600080fd5b503573ffffffffffffffffffffffffffffffffffffffff16611864565b6104496004803603602081101561056b57600080fd5b503573ffffffffffffffffffffffffffffffffffffffff166118cc565b6104656119c5565b6105986119e9565b6040805160ff9092168252519081900360200190f35b6104aa60048036036101008110156105c557600080fd5b8101906020810181356401000000008111156105e057600080fd5b8201836020820111156105f257600080fd5b8035906020019184600183028401116401000000008311171561061457600080fd5b91908080601f016020809104026020016040519081016040528093929190818152602001838380828437600092019190915250929594936020810193503591505064010000000081111561066757600080fd5b82018360208201111561067957600080fd5b8035906020019184600183028401116401000000008311171561069b57600080fd5b91908080601f01602080910402602001604051908101604052809392919081815260200183838082843760009201919091525092959493602081019350359150506401000000008111156106ee57600080fd5b82018360208201111561070057600080fd5b8035906020019184600183028401116401000000008311171561072257600080fd5b91908080601f0160208091040260200160405190810160405280939291908181526020018383808284376000920191909152509295505050813560ff16925050602081013573ffffffffffffffffffffffffffffffffffffffff908116916040810135821691606082013581169160800135166119f2565b6107a2611d34565b6040805173ffffffffffffffffffffffffffffffffffffffff9092168252519081900360200190f35b610465611d50565b6107a2611d5f565b610449600480360360408110156107f157600080fd5b5073ffffffffffffffffffffffffffffffffffffffff8135169060200135611d7b565b6104aa611e13565b6104496004803603604081101561083257600080fd5b5073ffffffffffffffffffffffffffffffffffffffff8135169060200135611ed6565b6104aa6004803603602081101561086b57600080fd5b5035612324565b6104aa6004803603604081101561088857600080fd5b8101906020810181356401000000008111156108a357600080fd5b8201836020820111156108b557600080fd5b803590602001918460208302840111640100000000831117156108d757600080fd5b9193909290916020810190356401000000008111156108f557600080fd5b82018360208201111561090757600080fd5b8035906020019184600183028401116401000000008311171561092957600080fd5b509092509050612658565b6104496004803603604081101561094a57600080fd5b5073ffffffffffffffffffffffffffffffffffffffff813516906020013561280f565b61039b6129a2565b6104aa6004803603602081101561098b57600080fd5b503573ffffffffffffffffffffffffffffffffffffffff166129d9565b6104aa600480360360a08110156109be57600080fd5b5073ffffffffffffffffffffffffffffffffffffffff8135169060208101359060ff6040820135169060608101359060800135612b40565b610449612bde565b61046560048036036020811015610a1457600080fd5b503573ffffffffffffffffffffffffffffffffffffffff16612bff565b61046560048036036020811015610a4757600080fd5b503573ffffffffffffffffffffffffffffffffffffffff16612c0a565b610465612c32565b6107a2612c56565b6104aa612c6e565b6104aa600480360360e0811015610a9257600080fd5b73ffffffffffffffffffffffffffffffffffffffff823581169260208101359091169160408201359160608101359160808201359160a08101359181019060e0810160c0820135640100000000811115610aeb57600080fd5b820183602082011115610afd57600080fd5b80359060200191846001830284011164010000000083111715610b1f57600080fd5b91908080601f016020809104026020016040519081016040528093929190818152602001838380828437600092019190915250929550612d48945050505050565b61046560048036036020811015610b7657600080fd5b503573ffffffffffffffffffffffffffffffffffffffff16612dea565b6107a2612e12565b61039b612e2e565b6107a2612ea7565b6104aa600480360360a0811015610bc157600080fd5b73ffffffffffffffffffffffffffffffffffffffff823581169260208101359091169160408201359160608101359181019060a081016080820135640100000000811115610c0e57600080fd5b820183602082011115610c2057600080fd5b80359060200191846001830284011164010000000083111715610c4257600080fd5b91908080601f016020809104026020016040519081016040528093929190818152602001838380828437600092019190915250929550612ec3945050505050565b610465612f5a565b61044960048036036040811015610ca157600080fd5b5073ffffffffffffffffffffffffffffffffffffffff8135169060200135612f7e565b61044960048036036040811015610cda57600080fd5b5073ffffffffffffffffffffffffffffffffffffffff8135169060200135613016565b6104aa60048036036020811015610d1357600080fd5b503573ffffffffffffffffffffffffffffffffffffffff166130ae565b61044960048036036020811015610d4657600080fd5b503573ffffffffffffffffffffffffffffffffffffffff16613215565b6104aa60048036036020811015610d7957600080fd5b503573ffffffffffffffffffffffffffffffffffffffff16613240565b6104aa60048036036060811015610dac57600080fd5b5073ffffffffffffffffffffffffffffffffffffffff8135811691602081013590911690604001356133a7565b6104aa60048036036060811015610def57600080fd5b73ffffffffffffffffffffffffffffffffffffffff82351691602081013591810190606081016040820135640100000000811115610e2c57600080fd5b820183602082011115610e3e57600080fd5b80359060200191846001830284011164010000000083111715610e6057600080fd5b91908080601f01602080910402602001604051908101604052809392919081815260200183838082843760009201919091525092955061343d945050505050565b6107a26134d2565b6104656134ee565b6104aa600480360360e0811015610ec757600080fd5b73ffffffffffffffffffffffffffffffffffffffff823581169260208101359091169160408201359160608101359160808201359160a08101359181019060e0810160c0820135640100000000811115610f2057600080fd5b820183602082011115610f3257600080fd5b80359060200191846001830284011164010000000083111715610f5457600080fd5b91908080601f0160208091040260200160405190810160405280939291908181526020018383808284376000920191909152509295506134f7945050505050565b6104aa600480360360e0811015610fab57600080fd5b5073ffffffffffffffffffffffffffffffffffffffff813581169160208101359091169060408101359060608101359060ff6080820135169060a08101359060c00135613590565b6104aa6004803603602081101561100957600080fd5b81019060208101813564010000000081111561102457600080fd5b82018360208201111561103657600080fd5b8035906020019184600183028401116401000000008311171561105857600080fd5b509092509050613629565b610465613712565b6107a2613736565b6104656004803603604081101561108957600080fd5b5073ffffffffffffffffffffffffffffffffffffffff8135811691602001351661374e565b6104aa60048036036101208110156110c557600080fd5b5073ffffffffffffffffffffffffffffffffffffffff813581169160208101359091169060408101359060608101359060808101359060a08101359060ff60c0820135169060e0810135906101000135613786565b61039b61382c565b6104496004803603604081101561113857600080fd5b5073ffffffffffffffffffffffffffffffffffffffff81351690602001356138a5565b6104aa600480360361012081101561117257600080fd5b5073ffffffffffffffffffffffffffffffffffffffff813581169160208101359091169060408101359060608101359060808101359060a08101359060ff60c0820135169060e08101359061010001356138dd565b6104aa600480360360208110156111dd57600080fd5b503573ffffffffffffffffffffffffffffffffffffffff16613978565b6104aa6004803603602081101561121057600080fd5b503573ffffffffffffffffffffffffffffffffffffffff16613acb565b6104496004803603602081101561124357600080fd5b503573ffffffffffffffffffffffffffffffffffffffff16613b88565b6004805460408051602060026001851615610100027fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff0190941693909304601f810184900484028201840190925281815292918301828280156113045780601f106112d957610100808354040283529160200191611304565b820191906000526020600020905b8154815290600101906020018083116112e757829003601f168201915b505050505081565b60015460009074010000000000000000000000000000000000000000900460ff161561139957604080517f08c379a000000000000000000000000000000000000000000000000000000000815260206004820152601060248201527f5061757361626c653a2070617573656400000000000000000000000000000000604482015290519081900360640190fd5b6113a4338484613b93565b5060015b92915050565b60008073180000000000000000000000000000000000000073ffffffffffffffffffffffffffffffffffffffff166318160ddd6040518163ffffffff1660e01b815260040160206040518083038186803b15801561140b57600080fd5b505afa15801561141f573d6000803e3d6000fd5b505050506040513d602081101561143557600080fd5b505190506114488164e8d4a51000613cda565b91505090565b60025473ffffffffffffffffffffffffffffffffffffffff1633146114be576040517f08c379a000000000000000000000000000000000000000000000000000000000815260040180806020018281038252602c8152602001806157e2602c913960400191505060405180910390fd5b6114c781613ced565b60405173ffffffffffffffffffffffffffffffffffffffff8216907f117e3210bb9aa7d9baff172026820255c6f6c30ba8999d1c2fd88e2848137c4e90600090a250565b60015460009074010000000000000000000000000000000000000000900460ff161561159857604080517f08c379a000000000000000000000000000000000000000000000000000000000815260206004820152601060248201527f5061757361626c653a2070617573656400000000000000000000000000000000604482015290519081900360640190fd5b336115a281613cf8565b156115f8576040517f08c379a0000000000000000000000000000000000000000000000000000000008152600401808060200182810382526025815260200180615ab96025913960400191505060405180910390fd5b73ffffffffffffffffffffffffffffffffffffffff85166000908152600a60209081526040808320338452909152902054831115611681576040517f08c379a00000000000000000000000000000000000000000000000000000000081526004018080602001828103825260288152602001806158c96028913960400191505060405180910390fd5b73ffffffffffffffffffffffffffffffffffffffff85166000908152600a602090815260408083203384529091529020546116bc9084613da7565b73ffffffffffffffffffffffffffffffffffffffff86166000908152600a602090815260408083203384529091529020556116f8858585613e1e565b506001949350505050565b60005473ffffffffffffffffffffffffffffffffffffffff16331461178957604080517f08c379a000000000000000000000000000000000000000000000000000000000815260206004820181905260248201527f4f776e61626c653a2063616c6c6572206973206e6f7420746865206f776e6572604482015290519081900360640190fd5b73ffffffffffffffffffffffffffffffffffffffff81166117f5576040517f08c379a000000000000000000000000000000000000000000000000000000000815260040180806020018281038252602a815260200180615740602a913960400191505060405180910390fd5b600e80547fffffffffffffffffffffffff00000000000000000000000000000000000000001673ffffffffffffffffffffffffffffffffffffffff83169081179091556040517fe475e580d85111348e40d8ca33cfdd74c30fe1655c2d8537a13abc10065ffa5a90600090a250565b60125460ff1660011461187657600080fd5b60006118813061410c565b9050801561189457611894308383613e1e565b61189d30614135565b5050601280547fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff00166002179055565b60085460009073ffffffffffffffffffffffffffffffffffffffff16331461193f576040517f08c379a00000000000000000000000000000000000000000000000000000000081526004018080602001828103825260298152602001806157b96029913960400191505060405180910390fd5b73ffffffffffffffffffffffffffffffffffffffff82166000818152600c6020908152604080832080547fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff00169055600d909152808220829055517fe94479a9f7e1952cc78f2d6baab678adc1b772d936c6583def489e524cb666929190a2506001919050565b7f6e71edae12b1b97f4d1f60370fef10105fa2faae0126114a169c64845d6126c981565b60065460ff1681565b60085474010000000000000000000000000000000000000000900460ff1615611a66576040517f08c379a000000000000000000000000000000000000000000000000000000000815260040180806020018281038252602a815260200180615944602a913960400191505060405180910390fd5b73ffffffffffffffffffffffffffffffffffffffff8416611ad2576040517f08c379a000000000000000000000000000000000000000000000000000000000815260040180806020018281038252602f815260200180615855602f913960400191505060405180910390fd5b73ffffffffffffffffffffffffffffffffffffffff8316611b3e576040517f08c379a00000000000000000000000000000000000000000000000000000000081526004018080602001828103825260298152602001806157176029913960400191505060405180910390fd5b73ffffffffffffffffffffffffffffffffffffffff8216611baa576040517f08c379a000000000000000000000000000000000000000000000000000000000815260040180806020018281038252602e8152602001806158f1602e913960400191505060405180910390fd5b73ffffffffffffffffffffffffffffffffffffffff8116611c16576040517f08c379a0000000000000000000000000000000000000000000000000000000008152600401808060200182810382526028815260200180615a316028913960400191505060405180910390fd5b8751611c299060049060208b01906154b0565b508651611c3d9060059060208a01906154b0565b508551611c519060079060208901906154b0565b50600680547fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff001660ff8716179055600880547fffffffffffffffffffffffff000000000000000000000000000000000000000090811673ffffffffffffffffffffffffffffffffffffffff8781169190911790925560018054821686841617905560028054909116918416919091179055611ceb81614140565b5050600880547fffffffffffffffffffffff00ffffffffffffffffffffffffffffffffffffffff1674010000000000000000000000000000000000000000179055505050505050565b60085473ffffffffffffffffffffffffffffffffffffffff1681565b6000611d5a614187565b905090565b600e5473ffffffffffffffffffffffffffffffffffffffff1690565b60015460009074010000000000000000000000000000000000000000900460ff1615611e0857604080517f08c379a000000000000000000000000000000000000000000000000000000000815260206004820152601060248201527f5061757361626c653a2070617573656400000000000000000000000000000000604482015290519081900360640190fd5b6113a433848461427c565b60015473ffffffffffffffffffffffffffffffffffffffff163314611e83576040517f08c379a00000000000000000000000000000000000000000000000000000000081526004018080602001828103825260228152602001806159e56022913960400191505060405180910390fd5b600180547fffffffffffffffffffffff00ffffffffffffffffffffffffffffffffffffffff1690556040517f7805862f689e2f13df9f062ff482ad3ad112aca9e0847911ed832e158c525b3390600090a1565b336000908152600c602052604081205460ff16611f3e576040517f08c379a00000000000000000000000000000000000000000000000000000000081526004018080602001828103825260218152602001806158346021913960400191505060405180910390fd5b33611f4881613cf8565b15611f9e576040517f08c379a0000000000000000000000000000000000000000000000000000000008152600401808060200182810382526025815260200180615ab96025913960400191505060405180910390fd5b60015474010000000000000000000000000000000000000000900460ff161561202857604080517f08c379a000000000000000000000000000000000000000000000000000000000815260206004820152601060248201527f5061757361626c653a2070617573656400000000000000000000000000000000604482015290519081900360640190fd5b73ffffffffffffffffffffffffffffffffffffffff8416612094576040517f08c379a00000000000000000000000000000000000000000000000000000000081526004018080602001828103825260238152602001806156ac6023913960400191505060405180910390fd5b600083116120ed576040517f08c379a000000000000000000000000000000000000000000000000000000000815260040180806020018281038252602981526020018061576a6029913960400191505060405180910390fd5b336000908152600d602052604090205480841115612156576040517f08c379a000000000000000000000000000000000000000000000000000000000815260040180806020018281038252602e8152602001806159b7602e913960400191505060405180910390fd5b336000908152600d6020526040902084820390557318000000000000000000000000000000000000006340c10f19866121948764e8d4a510006142c6565b6040518363ffffffff1660e01b8152600401808373ffffffffffffffffffffffffffffffffffffffff16815260200182815260200192505050602060405180830381600087803b1580156121e757600080fd5b505af11580156121fb573d6000803e3d6000fd5b505050506040513d602081101561221157600080fd5b505161227e57604080517f08c379a000000000000000000000000000000000000000000000000000000000815260206004820152601260248201527f4e6174697665206d696e74206661696c65640000000000000000000000000000604482015290519081900360640190fd5b60408051858152905173ffffffffffffffffffffffffffffffffffffffff87169133917fab8530f87dc9b59234c4623bf917212bb2536d647574c8e7e5da92c2ede0c9f89181900360200190a360408051858152905173ffffffffffffffffffffffffffffffffffffffff8716916000917fddf252ad1be2c89b69c2b068fc378daa952ba7f163c4a11628f55a4df523b3ef9181900360200190a3506001949350505050565b336000908152600c602052604090205460ff1661238c576040517f08c379a00000000000000000000000000000000000000000000000000000000081526004018080602001828103825260218152602001806158346021913960400191505060405180910390fd5b60015474010000000000000000000000000000000000000000900460ff161561241657604080517f08c379a000000000000000000000000000000000000000000000000000000000815260206004820152601060248201527f5061757361626c653a2070617573656400000000000000000000000000000000604482015290519081900360640190fd5b6000811161246f576040517f08c379a00000000000000000000000000000000000000000000000000000000081526004018080602001828103825260298152602001806156836029913960400191505060405180910390fd5b60006124808264e8d4a510006142c6565b905033318111156124dc576040517f08c379a000000000000000000000000000000000000000000000000000000000815260040180806020018281038252602681526020018061580e6026913960400191505060405180910390fd5b604080517f9dc29fac00000000000000000000000000000000000000000000000000000000815233600482015260248101839052905173180000000000000000000000000000000000000091639dc29fac9160448083019260209291908290030181600087803b15801561254f57600080fd5b505af1158015612563573d6000803e3d6000fd5b505050506040513d602081101561257957600080fd5b50516125e657604080517f08c379a000000000000000000000000000000000000000000000000000000000815260206004820152601260248201527f4e6174697665206275726e206661696c65640000000000000000000000000000604482015290519081900360640190fd5b60408051838152905133917fcc16f5dbb4873280815c1ee09dbd06736cffcc184412cf7a71a0fdb75d397ca5919081900360200190a260408051838152905160009133917fddf252ad1be2c89b69c2b068fc378daa952ba7f163c4a11628f55a4df523b3ef9181900360200190a35050565b60125460ff1660021461266a57600080fd5b6126766005838361552e565b5060005b838110156127b8576003600086868481811061269257fe5b6020908102929092013573ffffffffffffffffffffffffffffffffffffffff168352508101919091526040016000205460ff1661271a576040517f08c379a000000000000000000000000000000000000000000000000000000000815260040180806020018281038252603d8152602001806155d0603d913960400191505060405180910390fd5b61274b85858381811061272957fe5b9050602002013573ffffffffffffffffffffffffffffffffffffffff16614135565b6003600086868481811061275b57fe5b6020908102929092013573ffffffffffffffffffffffffffffffffffffffff1683525081019190915260400160002080547fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff0016905560010161267a565b506127c230614135565b505030600090815260036020819052604090912080547fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff009081169091556012805490911690911790555050565b60015460009074010000000000000000000000000000000000000000900460ff161561289c57604080517f08c379a000000000000000000000000000000000000000000000000000000000815260206004820152601060248201527f5061757361626c653a2070617573656400000000000000000000000000000000604482015290519081900360640190fd5b60085473ffffffffffffffffffffffffffffffffffffffff16331461290c576040517f08c379a00000000000000000000000000000000000000000000000000000000081526004018080602001828103825260298152602001806157b96029913960400191505060405180910390fd5b73ffffffffffffffffffffffffffffffffffffffff83166000818152600c6020908152604080832080547fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff00166001179055600d825291829020859055815185815291517f46980fca912ef9bcdbd36877427b6b90e860769f604e89c0e67720cece530d209281900390910190a250600192915050565b60408051808201909152600181527f3200000000000000000000000000000000000000000000000000000000000000602082015290565b60005473ffffffffffffffffffffffffffffffffffffffff163314612a5f57604080517f08c379a000000000000000000000000000000000000000000000000000000000815260206004820181905260248201527f4f776e61626c653a2063616c6c6572206973206e6f7420746865206f776e6572604482015290519081900360640190fd5b73ffffffffffffffffffffffffffffffffffffffff8116612acb576040517f08c379a00000000000000000000000000000000000000000000000000000000081526004018080602001828103825260288152602001806156306028913960400191505060405180910390fd5b600180547fffffffffffffffffffffffff00000000000000000000000000000000000000001673ffffffffffffffffffffffffffffffffffffffff83811691909117918290556040519116907fb80482a293ca2e013eda8683c9bd7fc8347cfdaeea5ede58cba46df502c2a60490600090a250565b60015474010000000000000000000000000000000000000000900460ff1615612bca57604080517f08c379a000000000000000000000000000000000000000000000000000000000815260206004820152601060248201527f5061757361626c653a2070617573656400000000000000000000000000000000604482015290519081900360640190fd5b612bd785858585856142d2565b5050505050565b60015474010000000000000000000000000000000000000000900460ff1681565b60006113a88261410c565b73ffffffffffffffffffffffffffffffffffffffff1660009081526011602052604090205490565b7fd099cc98ef71107a616c4f0f941f04c322d8e254fe26b3c6668db87aae413de881565b73180000000000000000000000000000000000000081565b60015473ffffffffffffffffffffffffffffffffffffffff163314612cde576040517f08c379a00000000000000000000000000000000000000000000000000000000081526004018080602001828103825260228152602001806159e56022913960400191505060405180910390fd5b600180547fffffffffffffffffffffff00ffffffffffffffffffffffffffffffffffffffff16740100000000000000000000000000000000000000001790556040517f6985a02210a168e66602d3235cb6db0e70f92b3ba4d376a33c0f3d9434bff62590600090a1565b60015474010000000000000000000000000000000000000000900460ff1615612dd257604080517f08c379a000000000000000000000000000000000000000000000000000000000815260206004820152601060248201527f5061757361626c653a2070617573656400000000000000000000000000000000604482015290519081900360640190fd5b612de187878787878787614312565b50505050505050565b73ffffffffffffffffffffffffffffffffffffffff166000908152600d602052604090205490565b60005473ffffffffffffffffffffffffffffffffffffffff1690565b6005805460408051602060026001851615610100027fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff0190941693909304601f810184900484028201840190925281815292918301828280156113045780601f106112d957610100808354040283529160200191611304565b60015473ffffffffffffffffffffffffffffffffffffffff1681565b60015474010000000000000000000000000000000000000000900460ff1615612f4d57604080517f08c379a000000000000000000000000000000000000000000000000000000000815260206004820152601060248201527f5061757361626c653a2070617573656400000000000000000000000000000000604482015290519081900360640190fd5b612bd78585858585614433565b7f7c7c6cdb67a18743f49ec6fa9b35f50d52ed05cbed4cc592e13b44501c1a226781565b60015460009074010000000000000000000000000000000000000000900460ff161561300b57604080517f08c379a000000000000000000000000000000000000000000000000000000000815260206004820152601060248201527f5061757361626c653a2070617573656400000000000000000000000000000000604482015290519081900360640190fd5b6113a43384846146f7565b60015460009074010000000000000000000000000000000000000000900460ff16156130a357604080517f08c379a000000000000000000000000000000000000000000000000000000000815260206004820152601060248201527f5061757361626c653a2070617573656400000000000000000000000000000000604482015290519081900360640190fd5b6113a4338484613e1e565b60005473ffffffffffffffffffffffffffffffffffffffff16331461313457604080517f08c379a000000000000000000000000000000000000000000000000000000000815260206004820181905260248201527f4f776e61626c653a2063616c6c6572206973206e6f7420746865206f776e6572604482015290519081900360640190fd5b73ffffffffffffffffffffffffffffffffffffffff81166131a0576040517f08c379a000000000000000000000000000000000000000000000000000000000815260040180806020018281038252602f815260200180615855602f913960400191505060405180910390fd5b600880547fffffffffffffffffffffffff00000000000000000000000000000000000000001673ffffffffffffffffffffffffffffffffffffffff83811691909117918290556040519116907fdb66dfa9c6b8f5226fe9aac7e51897ae8ee94ac31dc70bb6c9900b2574b707e690600090a250565b73ffffffffffffffffffffffffffffffffffffffff166000908152600c602052604090205460ff1690565b60005473ffffffffffffffffffffffffffffffffffffffff1633146132c657604080517f08c379a000000000000000000000000000000000000000000000000000000000815260206004820181905260248201527f4f776e61626c653a2063616c6c6572206973206e6f7420746865206f776e6572604482015290519081900360640190fd5b73ffffffffffffffffffffffffffffffffffffffff8116613332576040517f08c379a0000000000000000000000000000000000000000000000000000000008152600401808060200182810382526032815260200180615a876032913960400191505060405180910390fd5b600280547fffffffffffffffffffffffff00000000000000000000000000000000000000001673ffffffffffffffffffffffffffffffffffffffff83811691909117918290556040519116907fc67398012c111ce95ecb7429b933096c977380ee6c421175a71a4a4c6c88c06e90600090a250565b600e5473ffffffffffffffffffffffffffffffffffffffff163314613417576040517f08c379a00000000000000000000000000000000000000000000000000000000081526004018080602001828103825260248152602001806158846024913960400191505060405180910390fd5b61343873ffffffffffffffffffffffffffffffffffffffff84168383614753565b505050565b60015474010000000000000000000000000000000000000000900460ff16156134c757604080517f08c379a000000000000000000000000000000000000000000000000000000000815260206004820152601060248201527f5061757361626c653a2070617573656400000000000000000000000000000000604482015290519081900360640190fd5b6134388383836147e0565b60025473ffffffffffffffffffffffffffffffffffffffff1681565b64e8d4a5100081565b60015474010000000000000000000000000000000000000000900460ff161561358157604080517f08c379a000000000000000000000000000000000000000000000000000000000815260206004820152601060248201527f5061757361626c653a2070617573656400000000000000000000000000000000604482015290519081900360640190fd5b612de1878787878787876148ea565b60015474010000000000000000000000000000000000000000900460ff161561361a57604080517f08c379a000000000000000000000000000000000000000000000000000000000815260206004820152601060248201527f5061757361626c653a2070617573656400000000000000000000000000000000604482015290519081900360640190fd5b612de187878787878787614988565b60085474010000000000000000000000000000000000000000900460ff168015613656575060125460ff16155b61365f57600080fd5b61366b6004838361552e565b506136e082828080601f0160208091040260200160405190810160405280939291908181526020018383808284376000920191909152505060408051808201909152600181527f3200000000000000000000000000000000000000000000000000000000000000602082015291506149ca9050565b600f555050601280547fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff00166001179055565b7f158b0a9edf7a828aad02f63cd515c68ef2f50ba807396f6d12842833a159742981565b73180000000000000000000000000000000000000181565b73ffffffffffffffffffffffffffffffffffffffff9182166000908152600a6020908152604080832093909416825291909152205490565b60015474010000000000000000000000000000000000000000900460ff161561381057604080517f08c379a000000000000000000000000000000000000000000000000000000000815260206004820152601060248201527f5061757361626c653a2070617573656400000000000000000000000000000000604482015290519081900360640190fd5b6138218989898989898989896149e0565b505050505050505050565b6007805460408051602060026001851615610100027fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff0190941693909304601f810184900484028201840190925281815292918301828280156113045780601f106112d957610100808354040283529160200191611304565b73ffffffffffffffffffffffffffffffffffffffff919091166000908152601060209081526040808320938352929052205460ff1690565b60015474010000000000000000000000000000000000000000900460ff161561396757604080517f08c379a000000000000000000000000000000000000000000000000000000000815260206004820152601060248201527f5061757361626c653a2070617573656400000000000000000000000000000000604482015290519081900360640190fd5b613821898989898989898989614a24565b60005473ffffffffffffffffffffffffffffffffffffffff1633146139fe57604080517f08c379a000000000000000000000000000000000000000000000000000000000815260206004820181905260248201527f4f776e61626c653a2063616c6c6572206973206e6f7420746865206f776e6572604482015290519081900360640190fd5b73ffffffffffffffffffffffffffffffffffffffff8116613a6a576040517f08c379a00000000000000000000000000000000000000000000000000000000081526004018080602001828103825260268152602001806156cf6026913960400191505060405180910390fd5b6000546040805173ffffffffffffffffffffffffffffffffffffffff9283168152918316602083015280517f8be0079c531659141344cd1fd0a4f28419497f9722a3daafe3b4186f6b6457e09281900390910190a1613ac881614140565b50565b60025473ffffffffffffffffffffffffffffffffffffffff163314613b3b576040517f08c379a000000000000000000000000000000000000000000000000000000000815260040180806020018281038252602c8152602001806157e2602c913960400191505060405180910390fd5b613b4481614135565b60405173ffffffffffffffffffffffffffffffffffffffff8216907fffa4e6181777692565cf28528fc88fd1516ea86b56da075235fa575af6a4b85590600090a250565b60006113a882613cf8565b73ffffffffffffffffffffffffffffffffffffffff8316613bff576040517f08c379a00000000000000000000000000000000000000000000000000000000081526004018080602001828103825260248152602001806159936024913960400191505060405180910390fd5b73ffffffffffffffffffffffffffffffffffffffff8216613c6b576040517f08c379a00000000000000000000000000000000000000000000000000000000081526004018080602001828103825260228152602001806156f56022913960400191505060405180910390fd5b73ffffffffffffffffffffffffffffffffffffffff8084166000818152600a6020908152604080832094871680845294825291829020859055815185815291517f8c5be1e5ebec7d5bd14f71427d1e84f3dd0314c0f7b2291e5b200ac8c7c3b9259281900390910190a3505050565b6000613ce68383614a68565b9392505050565b613ac8816000614ae9565b600073180000000000000000000000000000000000000173ffffffffffffffffffffffffffffffffffffffff16638e204c43836040518263ffffffff1660e01b8152600401808273ffffffffffffffffffffffffffffffffffffffff16815260200191505060206040518083038186803b158015613d7557600080fd5b505afa158015613d89573d6000803e3d6000fd5b505050506040513d6020811015613d9f57600080fd5b505192915050565b600082821115613e1857604080517f08c379a000000000000000000000000000000000000000000000000000000000815260206004820152601e60248201527f536166654d6174683a207375627472616374696f6e206f766572666c6f770000604482015290519081900360640190fd5b50900390565b73ffffffffffffffffffffffffffffffffffffffff8316613e8a576040517f08c379a000000000000000000000000000000000000000000000000000000000815260040180806020018281038252602581526020018061596e6025913960400191505060405180910390fd5b73ffffffffffffffffffffffffffffffffffffffff8216613ef6576040517f08c379a000000000000000000000000000000000000000000000000000000000815260040180806020018281038252602381526020018061560d6023913960400191505060405180910390fd5b6000613f078264e8d4a510006142c6565b90508373ffffffffffffffffffffffffffffffffffffffff1631811115613f79576040517f08c379a00000000000000000000000000000000000000000000000000000000081526004018080602001828103825260268152602001806157936026913960400191505060405180910390fd5b604080517fbeabacc800000000000000000000000000000000000000000000000000000000815273ffffffffffffffffffffffffffffffffffffffff8681166004830152851660248201526044810183905290517318000000000000000000000000000000000000009163beabacc89160648083019260209291908290030181600087803b15801561400a57600080fd5b505af115801561401e573d6000803e3d6000fd5b505050506040513d602081101561403457600080fd5b50516140a157604080517f08c379a000000000000000000000000000000000000000000000000000000000815260206004820152601660248201527f4e6174697665207472616e73666572206661696c656400000000000000000000604482015290519081900360640190fd5b8273ffffffffffffffffffffffffffffffffffffffff168473ffffffffffffffffffffffffffffffffffffffff167fddf252ad1be2c89b69c2b068fc378daa952ba7f163c4a11628f55a4df523b3ef846040518082815260200191505060405180910390a350505050565b600073ffffffffffffffffffffffffffffffffffffffff821631613ce68164e8d4a51000613cda565b613ac8816001614ae9565b600080547fffffffffffffffffffffffff00000000000000000000000000000000000000001673ffffffffffffffffffffffffffffffffffffffff92909216919091179055565b6004805460408051602060026001851615610100027fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff0190941693909304601f8101849004840282018401909252818152600093611d5a93919290918301828280156142345780601f1061420957610100808354040283529160200191614234565b820191906000526020600020905b81548152906001019060200180831161421757829003601f168201915b50505050506040518060400160405280600181526020017f3200000000000000000000000000000000000000000000000000000000000000815250614277614c56565b614c5a565b73ffffffffffffffffffffffffffffffffffffffff8084166000908152600a602090815260408083209386168352929052205461343890849084906142c19085614cce565b613b93565b6000613ce68383614d42565b612bd78585848487604051602001808481526020018381526020018260ff1660f81b815260010193505050506040516020818303038152906040526147e0565b73ffffffffffffffffffffffffffffffffffffffff86163314614380576040517f08c379a000000000000000000000000000000000000000000000000000000000815260040180806020018281038252602581526020018061591f6025913960400191505060405180910390fd5b61438c87838686614db5565b604080517fd099cc98ef71107a616c4f0f941f04c322d8e254fe26b3c6668db87aae413de860208083019190915273ffffffffffffffffffffffffffffffffffffffff808b1683850152891660608301526080820188905260a0820187905260c0820186905260e080830186905283518084039091018152610100909201909252805191012061441e90889083614e6f565b6144288783614fed565b612de1878787613e1e565b7fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff8214806144615750428210155b6144cc57604080517f08c379a000000000000000000000000000000000000000000000000000000000815260206004820152601e60248201527f46696174546f6b656e56323a207065726d697420697320657870697265640000604482015290519081900360640190fd5b60006145746144d9614187565b73ffffffffffffffffffffffffffffffffffffffff80891660008181526011602090815260409182902080546001810190915582517f6e71edae12b1b97f4d1f60370fef10105fa2faae0126114a169c64845d6126c98184015280840194909452938b166060840152608083018a905260a083019390935260c08083018990528151808403909101815260e090920190528051910120615072565b905073fcff98b65f9ea559ec0df36f4072c7e3be0520df636ccea6528783856040518463ffffffff1660e01b8152600401808473ffffffffffffffffffffffffffffffffffffffff16815260200183815260200180602001828103825283818151815260200191508051906020019080838360005b838110156146015781810151838201526020016145e9565b50505050905090810190601f16801561462e5780820380516001836020036101000a031916815260200191505b5094505050505060206040518083038186803b15801561464d57600080fd5b505af4158015614661573d6000803e3d6000fd5b505050506040513d602081101561467757600080fd5b50516146e457604080517f08c379a000000000000000000000000000000000000000000000000000000000815260206004820152601a60248201527f454950323631323a20696e76616c6964207369676e6174757265000000000000604482015290519081900360640190fd5b6146ef868686613b93565b505050505050565b61343883836142c184604051806060016040528060258152602001615b036025913973ffffffffffffffffffffffffffffffffffffffff808a166000908152600a60209081526040808320938c168352929052205491906150ac565b6040805173ffffffffffffffffffffffffffffffffffffffff8416602482015260448082018490528251808303909101815260649091019091526020810180517bffffffffffffffffffffffffffffffffffffffffffffffffffffffff167fa9059cbb0000000000000000000000000000000000000000000000000000000017905261343890849061515d565b6147ea8383615235565b614864837f158b0a9edf7a828aad02f63cd515c68ef2f50ba807396f6d12842833a159742960001b8585604051602001808481526020018373ffffffffffffffffffffffffffffffffffffffff16815260200182815260200193505050506040516020818303038152906040528051906020012083614e6f565b73ffffffffffffffffffffffffffffffffffffffff8316600081815260106020908152604080832086845290915280822080547fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff00166001179055518492917f1cdd46ff242716cdaa72d159d339a485b3438398348d68f09d7c8c0a59353d8191a3505050565b6148f687838686614db5565b604080517f7c7c6cdb67a18743f49ec6fa9b35f50d52ed05cbed4cc592e13b44501c1a226760208083019190915273ffffffffffffffffffffffffffffffffffffffff808b1683850152891660608301526080820188905260a0820187905260c0820186905260e080830186905283518084039091018152610100909201909252805191012061441e90889083614e6f565b612de187878787868689604051602001808481526020018381526020018260ff1660f81b81526001019350505050604051602081830303815290604052614433565b6000466149d8848483614c5a565b949350505050565b61382189898989898988888b604051602001808481526020018381526020018260ff1660f81b815260010193505050506040516020818303038152906040526148ea565b61382189898989898988888b604051602001808481526020018381526020018260ff1660f81b81526001019350505050604051602081830303815290604052614312565b6000808211614ad857604080517f08c379a000000000000000000000000000000000000000000000000000000000815260206004820152601a60248201527f536166654d6174683a206469766973696f6e206279207a65726f000000000000604482015290519081900360640190fd5b818381614ae157fe5b049392505050565b8015614ba357604080517fe5c7160b00000000000000000000000000000000000000000000000000000000815273ffffffffffffffffffffffffffffffffffffffff8416600482015290517318000000000000000000000000000000000000019163e5c7160b9160248083019260209291908290030181600087803b158015614b7157600080fd5b505af1158015614b85573d6000803e3d6000fd5b505050506040513d6020811015614b9b57600080fd5b50614c529050565b604080517f31b2302000000000000000000000000000000000000000000000000000000000815273ffffffffffffffffffffffffffffffffffffffff841660048201529051731800000000000000000000000000000000000001916331b230209160248083019260209291908290030181600087803b158015614c2557600080fd5b505af1158015614c39573d6000803e3d6000fd5b505050506040513d6020811015614c4f57600080fd5b50505b5050565b4690565b8251602093840120825192840192909220604080517f8b73c3c69bb8fe3d512ecc4cf759cc79239f7b179b0ffacaa9a75d522b39400f8187015280820194909452606084019190915260808301919091523060a0808401919091528151808403909101815260c09092019052805191012090565b600082820183811015613ce657604080517f08c379a000000000000000000000000000000000000000000000000000000000815260206004820152601b60248201527f536166654d6174683a206164646974696f6e206f766572666c6f770000000000604482015290519081900360640190fd5b600082614d51575060006113a8565b82820282848281614d5e57fe5b0414613ce6576040517f08c379a00000000000000000000000000000000000000000000000000000000081526004018080602001828103825260218152602001806158a86021913960400191505060405180910390fd5b814211614e0d576040517f08c379a000000000000000000000000000000000000000000000000000000000815260040180806020018281038252602b815260200180615658602b913960400191505060405180910390fd5b804210614e65576040517f08c379a0000000000000000000000000000000000000000000000000000000008152600401808060200182810382526025815260200180615ade6025913960400191505060405180910390fd5b614c4f8484615235565b73fcff98b65f9ea559ec0df36f4072c7e3be0520df636ccea65284614e9b614e95614187565b86615072565b846040518463ffffffff1660e01b8152600401808473ffffffffffffffffffffffffffffffffffffffff16815260200183815260200180602001828103825283818151815260200191508051906020019080838360005b83811015614f0a578181015183820152602001614ef2565b50505050905090810190601f168015614f375780820380516001836020036101000a031916815260200191505b5094505050505060206040518083038186803b158015614f5657600080fd5b505af4158015614f6a573d6000803e3d6000fd5b505050506040513d6020811015614f8057600080fd5b505161343857604080517f08c379a000000000000000000000000000000000000000000000000000000000815260206004820152601e60248201527f46696174546f6b656e56323a20696e76616c6964207369676e61747572650000604482015290519081900360640190fd5b73ffffffffffffffffffffffffffffffffffffffff8216600081815260106020908152604080832085845290915280822080547fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff00166001179055518392917f98de503528ee59b575ef0c0a2576a82497bfc029a5685b209e9ec333479b10a591a35050565b6040517f19010000000000000000000000000000000000000000000000000000000000008152600281019290925260228201526042902090565b60008184841115615155576040517f08c379a00000000000000000000000000000000000000000000000000000000081526004018080602001828103825283818151815260200191508051906020019080838360005b8381101561511a578181015183820152602001615102565b50505050905090810190601f1680156151475780820380516001836020036101000a031916815260200191505b509250505060405180910390fd5b505050900390565b60606151bf826040518060400160405280602081526020017f5361666545524332303a206c6f772d6c6576656c2063616c6c206661696c65648152508573ffffffffffffffffffffffffffffffffffffffff166152bf9092919063ffffffff16565b805190915015613438578080602001905160208110156151de57600080fd5b5051613438576040517f08c379a000000000000000000000000000000000000000000000000000000000815260040180806020018281038252602a815260200180615a07602a913960400191505060405180910390fd5b73ffffffffffffffffffffffffffffffffffffffff8216600090815260106020908152604080832084845290915290205460ff1615614c52576040517f08c379a000000000000000000000000000000000000000000000000000000000815260040180806020018281038252602e815260200180615a59602e913960400191505060405180910390fd5b60606149d88484600085856152d38561542a565b61533e57604080517f08c379a000000000000000000000000000000000000000000000000000000000815260206004820152601d60248201527f416464726573733a2063616c6c20746f206e6f6e2d636f6e7472616374000000604482015290519081900360640190fd5b600060608673ffffffffffffffffffffffffffffffffffffffff1685876040518082805190602001908083835b602083106153a857805182527fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffe0909201916020918201910161536b565b6001836020036101000a03801982511681845116808217855250505050505090500191505060006040518083038185875af1925050503d806000811461540a576040519150601f19603f3d011682016040523d82523d6000602084013e61540f565b606091505b509150915061541f828286615430565b979650505050505050565b3b151590565b6060831561543f575081613ce6565b82511561544f5782518084602001fd5b6040517f08c379a000000000000000000000000000000000000000000000000000000000815260206004820181815284516024840152845185939192839260440191908501908083836000831561511a578181015183820152602001615102565b828054600181600116156101000203166002900490600052602060002090601f016020900481019282601f106154f157805160ff191683800117855561551e565b8280016001018555821561551e579182015b8281111561551e578251825591602001919060010190615503565b5061552a9291506155ba565b5090565b828054600181600116156101000203166002900490600052602060002090601f016020900481019282601f1061558d578280017fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff0082351617855561551e565b8280016001018555821561551e579182015b8281111561551e57823582559160200191906001019061559f565b5b8082111561552a57600081556001016155bb56fe46696174546f6b656e56325f323a20426c61636b6c697374696e672070726576696f75736c7920756e626c61636b6c6973746564206163636f756e742145524332303a207472616e7366657220746f20746865207a65726f20616464726573735061757361626c653a206e65772070617573657220697320746865207a65726f206164647265737346696174546f6b656e56323a20617574686f72697a6174696f6e206973206e6f74207965742076616c696446696174546f6b656e3a206275726e20616d6f756e74206e6f742067726561746572207468616e203046696174546f6b656e3a206d696e7420746f20746865207a65726f20616464726573734f776e61626c653a206e6577206f776e657220697320746865207a65726f206164647265737345524332303a20617070726f766520746f20746865207a65726f206164647265737346696174546f6b656e3a206e65772070617573657220697320746865207a65726f2061646472657373526573637561626c653a206e6577207265736375657220697320746865207a65726f206164647265737346696174546f6b656e3a206d696e7420616d6f756e74206e6f742067726561746572207468616e203045524332303a207472616e7366657220616d6f756e7420657863656564732062616c616e636546696174546f6b656e3a2063616c6c6572206973206e6f7420746865206d61737465724d696e746572426c61636b6c69737461626c653a2063616c6c6572206973206e6f742074686520626c61636b6c697374657246696174546f6b656e3a206275726e20616d6f756e7420657863656564732062616c616e636546696174546f6b656e3a2063616c6c6572206973206e6f742061206d696e74657246696174546f6b656e3a206e6577206d61737465724d696e74657220697320746865207a65726f2061646472657373526573637561626c653a2063616c6c6572206973206e6f74207468652072657363756572536166654d6174683a206d756c7469706c69636174696f6e206f766572666c6f7745524332303a207472616e7366657220616d6f756e74206578636565647320616c6c6f77616e636546696174546f6b656e3a206e657720626c61636b6c697374657220697320746865207a65726f206164647265737346696174546f6b656e56323a2063616c6c6572206d7573742062652074686520706179656546696174546f6b656e3a20636f6e747261637420697320616c726561647920696e697469616c697a656445524332303a207472616e736665722066726f6d20746865207a65726f206164647265737345524332303a20617070726f76652066726f6d20746865207a65726f206164647265737346696174546f6b656e3a206d696e7420616d6f756e742065786365656473206d696e746572416c6c6f77616e63655061757361626c653a2063616c6c6572206973206e6f7420746865207061757365725361666545524332303a204552433230206f7065726174696f6e20646964206e6f74207375636365656446696174546f6b656e3a206e6577206f776e657220697320746865207a65726f206164647265737346696174546f6b656e56323a20617574686f72697a6174696f6e2069732075736564206f722063616e63656c6564426c61636b6c69737461626c653a206e657720626c61636b6c697374657220697320746865207a65726f2061646472657373426c61636b6c69737461626c653a206163636f756e7420697320626c61636b6c697374656446696174546f6b656e56323a20617574686f72697a6174696f6e206973206578706972656445524332303a2064656372656173656420616c6c6f77616e63652062656c6f77207a65726fa2646970667358221220751fec4c5563f047e302371c387a730293c7309d22807a459cd82e8c047744e764736f6c634300060c0033" - }, - "0xfcFf98B65F9ea559EC0df36F4072C7E3BE0520Df": { - "balance": "0x0", - "nonce": "0x1", - "code": "0x73fcff98b65f9ea559ec0df36f4072c7e3be0520df30146080604052600436106100355760003560e01c80636ccea6521461003a575b600080fd5b6101026004803603606081101561005057600080fd5b73ffffffffffffffffffffffffffffffffffffffff8235169160208101359181019060608101604082013564010000000081111561008d57600080fd5b82018360208201111561009f57600080fd5b803590602001918460018302840111640100000000831117156100c157600080fd5b91908080601f016020809104026020016040519081016040528093929190818152602001838380828437600092019190915250929550610116945050505050565b604080519115158252519081900360200190f35b600061012184610179565b610164578373ffffffffffffffffffffffffffffffffffffffff16610146848461017f565b73ffffffffffffffffffffffffffffffffffffffff16149050610172565b61016f848484610203565b90505b9392505050565b3b151590565b600081516041146101db576040517f08c379a00000000000000000000000000000000000000000000000000000000081526004018080602001828103825260238152602001806106296023913960400191505060405180910390fd5b60208201516040830151606084015160001a6101f98682858561042d565b9695505050505050565b60008060608573ffffffffffffffffffffffffffffffffffffffff16631626ba7e60e01b86866040516024018083815260200180602001828103825283818151815260200191508051906020019080838360005b8381101561026f578181015183820152602001610257565b50505050905090810190601f16801561029c5780820380516001836020036101000a031916815260200191505b50604080517fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffe08184030181529181526020820180517bffffffffffffffffffffffffffffffffffffffffffffffffffffffff167fffffffff000000000000000000000000000000000000000000000000000000009098169790971787525181519196909550859450925090508083835b6020831061036957805182527fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffe0909201916020918201910161032c565b6001836020036101000a038019825116818451168082178552505050505050905001915050600060405180830381855afa9150503d80600081146103c9576040519150601f19603f3d011682016040523d82523d6000602084013e6103ce565b606091505b50915091508180156103e257506020815110155b80156101f9575080517f1626ba7e00000000000000000000000000000000000000000000000000000000906020808401919081101561042057600080fd5b5051149695505050505050565b60007f7fffffffffffffffffffffffffffffff5d576e7357a4501ddfe92f46681b20a08211156104a8576040517f08c379a00000000000000000000000000000000000000000000000000000000081526004018080602001828103825260268152602001806106726026913960400191505060405180910390fd5b8360ff16601b141580156104c057508360ff16601c14155b15610516576040517f08c379a000000000000000000000000000000000000000000000000000000000815260040180806020018281038252602681526020018061064c6026913960400191505060405180910390fd5b600060018686868660405160008152602001604052604051808581526020018460ff1681526020018381526020018281526020019450505050506020604051602081039080840390855afa158015610572573d6000803e3d6000fd5b50506040517fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffe0015191505073ffffffffffffffffffffffffffffffffffffffff811661061f57604080517f08c379a000000000000000000000000000000000000000000000000000000000815260206004820152601c60248201527f45435265636f7665723a20696e76616c6964207369676e617475726500000000604482015290519081900360640190fd5b9594505050505056fe45435265636f7665723a20696e76616c6964207369676e6174757265206c656e67746845435265636f7665723a20696e76616c6964207369676e6174757265202776272076616c756545435265636f7665723a20696e76616c6964207369676e6174757265202773272076616c7565a2646970667358221220289705d6ae8701c9ed586541fa0ba342f754518aa4f0e59dbbda380bc9f2323964736f6c634300060c0033" - }, - "0xF511eC138a4868BE5924b5B1d2bC148154DD1Bbf": { - "balance": "0x0", - "nonce": "0x1", - "code": "0x608060405234801561000f575f5ffd5b50600436106100fb575f3560e01c80638456cb59116100935780639fd0506d116100635780639fd0506d146102fe578063e30c397814610311578063f2fde38b14610319578063f77c47911461032c575f5ffd5b80638456cb59146101bf5780638da5cb5b146101c75780638e207848146101cf5780639242164f146101e2575f5ffd5b8063554bab3c116100ce578063554bab3c146101785780635c975abb1461018b578063715018a6146101af57806379ba5097146101b7575f5ffd5b806306cb5b66146100ff5780633466e3d4146101145780633f4ba83a1461012757806341a56c591461012f575b5f5ffd5b61011261010d366004610c1a565b61033e565b005b610112610122366004610c1a565b6103fb565b61011261056b565b7f668f09ce856848ead6cb1ddee963f15ef833cea8958030868f867aec84385204546001600160a01b03165b6040516001600160a01b0390911681526020015b60405180910390f35b610112610186366004610c1a565b6105cc565b60015461019f90600160a01b900460ff1681565b604051901515815260200161016f565b610112610684565b610112610697565b6101126106df565b61015b610746565b6101126101dd366004610c47565b61077a565b6102986040805160c0810182525f80825260208201819052918101829052606081018290526080810182905260a08101829052907f668f09ce856848ead6cb1ddee963f15ef833cea8958030868f867aec843852006040805160c08101825282546001600160401b03808216835268010000000000000000820481166020840152600160801b9091041691810191909152600182015460608201526002820154608082015260039091015460a082015292915050565b60405161016f91905f60c0820190506001600160401b0383511682526001600160401b0360208401511660208301526001600160401b036040840151166040830152606083015160608301526080830151608083015260a083015160a083015292915050565b60015461015b906001600160a01b031681565b61015b610a8f565b610112610327366004610c1a565b610ab7565b5f5461015b906001600160a01b031681565b610346610b3c565b6001600160a01b0381166103b45760405162461bcd60e51b815260206004820152602a60248201527f436f6e74726f6c6c65723a206e657720636f6e74726f6c6c6572206973207a65604482015269726f206164647265737360b01b60648201526084015b60405180910390fd5b5f80546001600160a01b0319166001600160a01b038316908117825560405190917f1304018cfe79741dcf02ba6b61d39cc4757d59395d03224d9925c7aa8300214691a250565b5f546001600160a01b031633146104245760405162461bcd60e51b81526004016103ab90610c60565b600154600160a01b900460ff16156104715760405162461bcd60e51b815260206004820152601060248201526f14185d5cd8589b194e881c185d5cd95960821b60448201526064016103ab565b6001600160a01b0381166104df5760405162461bcd60e51b815260206004820152602f60248201527f50726f746f636f6c436f6e6669673a206e65772062656e65666963696172792060448201526e6973207a65726f206164647265737360881b60648201526084016103ab565b7f668f09ce856848ead6cb1ddee963f15ef833cea8958030868f867aec8438520480546001600160a01b0319166001600160a01b0383169081179091556040517f668f09ce856848ead6cb1ddee963f15ef833cea8958030868f867aec8438520091907fdec90d8bfa3fe33f0b0d876fc2cfc0e936e625e503ed170de964f15e6e17d15c905f90a25050565b6001546001600160a01b031633146105955760405162461bcd60e51b81526004016103ab90610ca8565b6001805460ff60a01b191690556040517f7805862f689e2f13df9f062ff482ad3ad112aca9e0847911ed832e158c525b33905f90a1565b6105d4610b3c565b6001600160a01b03811661063b5760405162461bcd60e51b815260206004820152602860248201527f5061757361626c653a206e65772070617573657220697320746865207a65726f604482015267206164647265737360c01b60648201526084016103ab565b600180546001600160a01b0319166001600160a01b0383169081179091556040517fb80482a293ca2e013eda8683c9bd7fc8347cfdaeea5ede58cba46df502c2a604905f90a250565b61068c610b3c565b6106955f610b6e565b565b33806106a1610a8f565b6001600160a01b0316146106d35760405163118cdaa760e01b81526001600160a01b03821660048201526024016103ab565b6106dc81610b6e565b50565b6001546001600160a01b031633146107095760405162461bcd60e51b81526004016103ab90610ca8565b6001805460ff60a01b1916600160a01b1790556040517f6985a02210a168e66602d3235cb6db0e70f92b3ba4d376a33c0f3d9434bff625905f90a1565b5f807f9016d09d72d40fdae2fd8ceac6b6234c7706214fd39c1cd1e609a0528c1993005b546001600160a01b031692915050565b5f546001600160a01b031633146107a35760405162461bcd60e51b81526004016103ab90610c60565b600154600160a01b900460ff16156107f05760405162461bcd60e51b815260206004820152601060248201526f14185d5cd8589b194e881c185d5cd95960821b60448201526064016103ab565b60646107ff6020830183610cfe565b6001600160401b031611156108625760405162461bcd60e51b8152602060048201526024808201527f50726f746f636f6c436f6e6669673a20616c706861206d757374206265203c3d6044820152630203130360e41b60648201526084016103ab565b60646108746040830160208401610cfe565b6001600160401b031611156108d75760405162461bcd60e51b8152602060048201526024808201527f50726f746f636f6c436f6e6669673a206b52617465206d757374206265203c3d6044820152630203130360e41b60648201526084016103ab565b80608001358160600135111561093f5760405162461bcd60e51b815260206004820152602760248201527f50726f746f636f6c436f6e6669673a206d696e42617365466565203e206d61786044820152664261736546656560c81b60648201526084016103ab565b5f8160a00135116109a45760405162461bcd60e51b815260206004820152602960248201527f50726f746f636f6c436f6e6669673a20626c6f636b4761734c696d6974206d7560448201526807374206265203e20360bc1b60648201526084016103ab565b5f6109b56060830160408401610cfe565b6001600160401b031611610a245760405162461bcd60e51b815260206004820152603060248201527f50726f746f636f6c436f6e6669673a20656c61737469636974794d756c74697060448201526f06c696572206d757374206265203e20360841b60648201526084016103ab565b7f668f09ce856848ead6cb1ddee963f15ef833cea8958030868f867aec843852008181610a518282610d19565b9050507f413a821e8d0eadcc30efbe627a655111b9f28f5999003a85b7718a14b0e329ef82604051610a839190610dd6565b60405180910390a15050565b5f807f237e158222e3e6968b72b9db0d8043aacf074ad9f650f0d1606b4d82ee432c0061076a565b610abf610b3c565b7f237e158222e3e6968b72b9db0d8043aacf074ad9f650f0d1606b4d82ee432c0080546001600160a01b0319166001600160a01b0383169081178255610b03610746565b6001600160a01b03167f38d16b8cac22d99fc7c124b9cd0de2d3fa1faef420bfe791d8c362d765e2270060405160405180910390a35050565b33610b45610746565b6001600160a01b0316146106955760405163118cdaa760e01b81523360048201526024016103ab565b7f237e158222e3e6968b72b9db0d8043aacf074ad9f650f0d1606b4d82ee432c0080546001600160a01b0319168155610ba682610baa565b5050565b7f9016d09d72d40fdae2fd8ceac6b6234c7706214fd39c1cd1e609a0528c19930080546001600160a01b031981166001600160a01b03848116918217845560405192169182907f8be0079c531659141344cd1fd0a4f28419497f9722a3daafe3b4186f6b6457e0905f90a3505050565b5f60208284031215610c2a575f5ffd5b81356001600160a01b0381168114610c40575f5ffd5b9392505050565b5f60c0828403128015610c58575f5ffd5b509092915050565b60208082526028908201527f436f6e74726f6c6c65723a2063616c6c6572206973206e6f742074686520636f604082015267373a3937b63632b960c11b606082015260800190565b60208082526022908201527f5061757361626c653a2063616c6c6572206973206e6f7420746865207061757360408201526132b960f11b606082015260800190565b6001600160401b03811681146106dc575f5ffd5b5f60208284031215610d0e575f5ffd5b8135610c4081610cea565b8135610d2481610cea565b6001600160401b03811690508154816001600160401b031982161783556020840135610d4f81610cea565b6fffffffffffffffff00000000000000008160401b16836fffffffffffffffffffffffffffffffff198416171784555050505f6040830135610d9081610cea565b825467ffffffffffffffff60801b1916608091821b67ffffffffffffffff60801b161783556060840135600184015583013560028301555060a090910135600390910155565b60c081018235610de581610cea565b6001600160401b031682526020830135610dfe81610cea565b6001600160401b031660208301526040830135610e1a81610cea565b6001600160401b03166040830152606083810135908301526080808401359083015260a09283013592909101919091529056fea2646970667358221220895e00e7d70f2261393e930bce98d421664d393e55f725d70675cb4ed5353b2f64736f6c634300081d0033" - }, - "0x3600000000000000000000000000000000000001": { - "balance": "0x0", - "nonce": "0x1", - "code": "0x608060405260043610610049575f3560e01c80633659cfe6146100535780634f1ef286146100725780635c60da1b146100855780638f283970146100b5578063f851a440146100d4575b6100516100e8565b005b34801561005e575f5ffd5b5061005161006d3660046104f3565b6100fa565b61005161008036600461050c565b610134565b348015610090575f5ffd5b50610099610197565b6040516001600160a01b03909116815260200160405180910390f35b3480156100c0575f5ffd5b506100516100cf3660046104f3565b6101a5565b3480156100df575f5ffd5b506100996101c5565b6100f86100f3610197565b6101ce565b565b6101026101ec565b6001600160a01b0316330361012c576101298160405180602001604052805f81525061021e565b50565b6101296100e8565b61013c6101ec565b6001600160a01b0316330361018f5761018a8383838080601f0160208091040260200160405190810160405280939291908181526020018383808284375f9201919091525061021e92505050565b505050565b61018a6100e8565b5f6101a0610277565b905090565b6101ad6101ec565b6001600160a01b0316330361012c576101298161029e565b5f6101a06101ec565b365f5f375f5f365f845af43d5f5f3e8080156101e8573d5ff35b3d5ffd5b5f7fb53127684a568b3173ae13b9f8a6016e243e63b6e8ee1178d6a717850b5d61035b546001600160a01b0316919050565b610227826102f2565b6040516001600160a01b038316907fbc7cd75a20ee27fd9adebab32041f755214dbc6bffa90cc0225b39da2e5c2d3b905f90a280511561026b5761018a8282610370565b6102736103e2565b5050565b5f7f360894a13ba1a3210667c828492db98dca3e2076cc3735a920a3ca505d382bbc61020f565b7f7e644d79422f17c01e4894b5f4f588d331ebfa28653d42ae832dc59e38c9798f6102c76101ec565b604080516001600160a01b03928316815291841660208301520160405180910390a161012981610401565b806001600160a01b03163b5f0361032c57604051634c9c8ce360e01b81526001600160a01b03821660048201526024015b60405180910390fd5b807f360894a13ba1a3210667c828492db98dca3e2076cc3735a920a3ca505d382bbc5b80546001600160a01b0319166001600160a01b039290921691909117905550565b60605f5f846001600160a01b03168460405161038c919061058a565b5f60405180830381855af49150503d805f81146103c4576040519150601f19603f3d011682016040523d82523d5f602084013e6103c9565b606091505b50915091506103d9858383610451565b95945050505050565b34156100f85760405163b398979f60e01b815260040160405180910390fd5b6001600160a01b03811661042a57604051633173bdd160e11b81525f6004820152602401610323565b807fb53127684a568b3173ae13b9f8a6016e243e63b6e8ee1178d6a717850b5d610361034f565b60608261046657610461826104b0565b6104a9565b815115801561047d57506001600160a01b0384163b155b156104a657604051639996b31560e01b81526001600160a01b0385166004820152602401610323565b50805b9392505050565b8051156104bf57805160208201fd5b60405163d6bda27560e01b815260040160405180910390fd5b80356001600160a01b03811681146104ee575f5ffd5b919050565b5f60208284031215610503575f5ffd5b6104a9826104d8565b5f5f5f6040848603121561051e575f5ffd5b610527846104d8565b9250602084013567ffffffffffffffff811115610542575f5ffd5b8401601f81018613610552575f5ffd5b803567ffffffffffffffff811115610568575f5ffd5b866020828401011115610579575f5ffd5b939660209190910195509293505050565b5f82518060208501845e5f92019182525091905056fea2646970667358221220407750ff6315cb87de3aba2eb532b034b7d9acff9bfba8abac9b674d66a9a95064736f6c634300081d0033", - "storage": { - "0xb53127684a568b3173ae13b9f8a6016e243e63b6e8ee1178d6a717850b5d6103": "0x000000000000000000000000d2c1210ce932a416e1f9f3147e404d37696d0c41", - "0x360894a13ba1a3210667c828492db98dca3e2076cc3735a920a3ca505d382bbc": "0x000000000000000000000000f511ec138a4868be5924b5b1d2bc148154dd1bbf", - "0x9016d09d72d40fdae2fd8ceac6b6234c7706214fd39c1cd1e609a0528c199300": "0x0000000000000000000000002904f3a8226630b40cd5bf414ff5c112e2a0aac6", - "0xf0c57e16840df040f15088dc2f81fe391c3923bec73e23a9662efc9c229c6a00": "0x000000000000000000000000000000000000000000000000ffffffffffffffff", - "0x0000000000000000000000000000000000000000000000000000000000000000": "0x0000000000000000000000005196ff0e6f24ca69d07539a0547fd84988d29fee", - "0x0000000000000000000000000000000000000000000000000000000000000001": "0x000000000000000000000000e09f434194e5430301f8d80fd1b7676a1052df47", - "0x668f09ce856848ead6cb1ddee963f15ef833cea8958030868f867aec84385200": "0x0000000000000000000000000000000200000000000000190000000000000014", - "0x668f09ce856848ead6cb1ddee963f15ef833cea8958030868f867aec84385201": "0x0000000000000000000000000000000000000000000000000000000000000001", - "0x668f09ce856848ead6cb1ddee963f15ef833cea8958030868f867aec84385202": "0x00000000000000000000000000000000000000000000000000000000000003e8", - "0x668f09ce856848ead6cb1ddee963f15ef833cea8958030868f867aec84385203": "0x0000000000000000000000000000000000000000000000000000000001c9c380", - "0x668f09ce856848ead6cb1ddee963f15ef833cea8958030868f867aec84385204": "0x000000000000000000000000a693cc18aa09d33dd388013b7a02e5ff863b8760" - } - }, - "0x084D0f0F9C15eB3dc3008b13dc7327dFc9799A9A": { - "balance": "0x0", - "nonce": "0x1", - "code": "0x608060405234801561000f575f5ffd5b50600436106100cb575f3560e01c806383ca642f11610088578063c4899f0c11610063578063c4899f0c146101aa578063e30c3978146101bd578063f2fde38b146101c5578063f94e1867146101d8575f5ffd5b806383ca642f146101435780638da5cb5b1461016a578063b5d896271461018a575f5ffd5b806303a9b132146100cf57806324408a68146100f65780632469d6721461010b5780634ebae6171461011e578063715018a61461013357806379ba50971461013b575b5f5ffd5b6100e35f5160206111bd5f395f51905f5281565b6040519081526020015b60405180910390f35b6100fe6101eb565b6040516100ed9190610de3565b6100e3610119366004610e75565b6103cb565b61013161012c366004610f36565b610569565b005b61013161062c565b61013161063f565b7fb58da0dce03316992faea3e12c60705b8ac05a309e27e3bc8421e5b271c9d204546100e3565b610172610687565b6040516001600160a01b0390911681526020016100ed565b61019d610198366004610f36565b6106bb565b6040516100ed9190610f4d565b6101316101b8366004610f5f565b6107e0565b6101726108dc565b6101316101d3366004610f80565b610904565b6101316101e6366004610f36565b610989565b60605f5160206111bd5f395f51905f525f6102257fb58da0dce03316992faea3e12c60705b8ac05a309e27e3bc8421e5b271c9d201610aae565b9050806001600160401b0381111561023f5761023f610e46565b60405190808252806020026020018201604052801561029357816020015b6102806040805160608101909152805f8152606060208201525f60409091015290565b81526020019060019003908161025d5790505b5092505f5b818110156103c5575f6102ae6001850183610ab7565b5f818152602086905260409081902081516060810190925280549293509091829060ff1660028111156102e3576102e3610d5c565b60028111156102f4576102f4610d5c565b815260200160018201805461030890610fa6565b80601f016020809104026020016040519081016040528092919081815260200182805461033490610fa6565b801561037f5780601f106103565761010080835404028352916020019161037f565b820191905f5260205f20905b81548152906001019060200180831161036257829003601f168201915b5050509183525050600291909101546001600160401b031660209091015285518690849081106103b1576103b1610fde565b602090810291909101015250600101610298565b50505090565b5f6103d4610ac9565b60208351146103f657604051630717c3c160e01b815260040160405180910390fd5b82516020808501919091205f8181527fb58da0dce03316992faea3e12c60705b8ac05a309e27e3bc8421e5b271c9d2039092526040909120545f5160206111bd5f395f51905f52919060ff1615610468576040516338a14a4160e01b8152600481018290526024015b60405180910390fd5b600482018054905f61047983611006565b909155506040805160608101909152909350806001815260208082018890526001600160401b0387166040928301525f868152908590522081518154829060ff191660018360028111156104cf576104cf610d5c565b0217905550602082015160018201906104e8908261106a565b50604091820151600291909101805467ffffffffffffffff19166001600160401b039283161790555f83815260038501602090815290839020805460ff191660011790559151908616815284917f5ca62f992d8b39734b493138299e30c9160b3801fec3ae051a777101e95d7da7910160405180910390a250505b92915050565b610571610ac9565b5f8181525f5160206111bd5f395f51905f5260208190526040909120805460019060ff1660028111156105a6576105a6610d5c565b1483906105c95760405163b1d5901360e01b815260040161045f91815260200190565b50805460ff191660021781556105e26001830184610afb565b5060028101546040516001600160401b03909116815283907fbc5136437746415b39af863272d0ca406634cf14bb7c3e5fa03855781add87e59060200160405180910390a2505050565b610634610ac9565b61063d5f610b06565b565b33806106496108dc565b6001600160a01b03161461067b5760405163118cdaa760e01b81526001600160a01b038216600482015260240161045f565b61068481610b06565b50565b5f807f9016d09d72d40fdae2fd8ceac6b6234c7706214fd39c1cd1e609a0528c1993005b546001600160a01b031692915050565b6106de6040805160608101909152805f8152606060208201525f60409091015290565b5f8281525f5160206111bd5f395f51905f5260208190526040918290208251606081019093528054919291829060ff16600281111561071f5761071f610d5c565b600281111561073057610730610d5c565b815260200160018201805461074490610fa6565b80601f016020809104026020016040519081016040528092919081815260200182805461077090610fa6565b80156107bb5780601f10610792576101008083540402835291602001916107bb565b820191905f5260205f20905b81548152906001019060200180831161079e57829003601f168201915b5050509183525050600291909101546001600160401b03166020909101529392505050565b6107e8610ac9565b5f5f5160206111bd5f395f51905f525f848152602082905260408120919250815460ff16600281111561081d5761081d610d5c565b141584906108415760405163b1d5901360e01b815260040161045f91815260200190565b5060028101546001600160401b03908116908416810361087457604051632bf0186d60e21b815260040160405180910390fd5b60028201805467ffffffffffffffff19166001600160401b03868116918217909255604080519284168352602083019190915286917f4a7e1cc2e075be9a3c913bf00ec8ff10f6630f23433f65eee31e20ef69110ae491015b60405180910390a25050505050565b5f807f237e158222e3e6968b72b9db0d8043aacf074ad9f650f0d1606b4d82ee432c006106ab565b61090c610ac9565b7f237e158222e3e6968b72b9db0d8043aacf074ad9f650f0d1606b4d82ee432c0080546001600160a01b0319166001600160a01b0383169081178255610950610687565b6001600160a01b03167f38d16b8cac22d99fc7c124b9cd0de2d3fa1faef420bfe791d8c362d765e2270060405160405180910390a35050565b610991610ac9565b5f5f5160206111bd5f395f51905f525f838152602082905260408120919250815460ff1660028111156109c6576109c6610d5c565b141583906109ea5760405163b1d5901360e01b815260040161045f91815260200190565b505f816001016040516109fd9190611124565b60405190819003902060028301549091506001600160401b0316610a246001850186610b42565b505f858152602085905260408120805460ff1916815590610a486001830182610d12565b50600201805467ffffffffffffffff191690555f828152600385016020908152604091829020805460ff1916905590516001600160401b038316815286917f70d022b699831c183688ed9d9d1a1cbf586698b506eaacfd5195a4e3738d285b91016108cd565b5f610563825490565b5f610ac28383610b4d565b9392505050565b33610ad2610687565b6001600160a01b03161461063d5760405163118cdaa760e01b815233600482015260240161045f565b5f610ac28383610b73565b7f237e158222e3e6968b72b9db0d8043aacf074ad9f650f0d1606b4d82ee432c0080546001600160a01b0319168155610b3e82610bbf565b5050565b5f610ac28383610c2f565b5f825f018281548110610b6257610b62610fde565b905f5260205f200154905092915050565b5f818152600183016020526040812054610bb857508154600181810184555f848152602080822090930184905584548482528286019093526040902091909155610563565b505f610563565b7f9016d09d72d40fdae2fd8ceac6b6234c7706214fd39c1cd1e609a0528c19930080546001600160a01b031981166001600160a01b03848116918217845560405192169182907f8be0079c531659141344cd1fd0a4f28419497f9722a3daafe3b4186f6b6457e0905f90a3505050565b5f8181526001830160205260408120548015610d09575f610c51600183611195565b85549091505f90610c6490600190611195565b9050808214610cc3575f865f018281548110610c8257610c82610fde565b905f5260205f200154905080875f018481548110610ca257610ca2610fde565b5f918252602080832090910192909255918252600188019052604090208390555b8554869080610cd457610cd46111a8565b600190038181905f5260205f20015f90559055856001015f8681526020019081526020015f205f905560019350505050610563565b5f915050610563565b508054610d1e90610fa6565b5f825580601f10610d2d575050565b601f0160209004905f5260205f209081019061068491905b80821115610d58575f8155600101610d45565b5090565b634e487b7160e01b5f52602160045260245ffd5b5f815160038110610d8f57634e487b7160e01b5f52602160045260245ffd5b8084525060208201516060602085015280518060608601528060208301608087015e5f608082870101526001600160401b0360408501511660408601526080601f19601f8301168601019250505092915050565b5f602082016020835280845180835260408501915060408160051b8601019250602086015f5b82811015610e3a57603f19878603018452610e25858351610d70565b94506020938401939190910190600101610e09565b50929695505050505050565b634e487b7160e01b5f52604160045260245ffd5b80356001600160401b0381168114610e70575f5ffd5b919050565b5f5f60408385031215610e86575f5ffd5b82356001600160401b03811115610e9b575f5ffd5b8301601f81018513610eab575f5ffd5b80356001600160401b03811115610ec457610ec4610e46565b604051601f8201601f19908116603f011681016001600160401b0381118282101715610ef257610ef2610e46565b604052818152828201602001871015610f09575f5ffd5b816020840160208301375f60208383010152809450505050610f2d60208401610e5a565b90509250929050565b5f60208284031215610f46575f5ffd5b5035919050565b602081525f610ac26020830184610d70565b5f5f60408385031215610f70575f5ffd5b82359150610f2d60208401610e5a565b5f60208284031215610f90575f5ffd5b81356001600160a01b0381168114610ac2575f5ffd5b600181811c90821680610fba57607f821691505b602082108103610fd857634e487b7160e01b5f52602260045260245ffd5b50919050565b634e487b7160e01b5f52603260045260245ffd5b634e487b7160e01b5f52601160045260245ffd5b5f6001820161101757611017610ff2565b5060010190565b601f82111561106557805f5260205f20601f840160051c810160208510156110435750805b601f840160051c820191505b81811015611062575f815560010161104f565b50505b505050565b81516001600160401b0381111561108357611083610e46565b611097816110918454610fa6565b8461101e565b6020601f8211600181146110c9575f83156110b25750848201515b5f19600385901b1c1916600184901b178455611062565b5f84815260208120601f198516915b828110156110f857878501518255602094850194600190920191016110d8565b508482101561111557868401515f19600387901b60f8161c191681555b50505050600190811b01905550565b5f5f835461113181610fa6565b600182168015611148576001811461115d5761118a565b60ff198316865281151582028601935061118a565b865f5260205f205f5b8381101561118257815488820152600190910190602001611166565b505081860193505b509195945050505050565b8181038181111561056357610563610ff2565b634e487b7160e01b5f52603160045260245ffdfeb58da0dce03316992faea3e12c60705b8ac05a309e27e3bc8421e5b271c9d200a264697066735822122013e30fa1a3b24816cf30de655e1992c56e86c9c674d444eee1dc89861aa5b36764736f6c634300081d0033" - }, - "0x3600000000000000000000000000000000000002": { - "balance": "0x0", - "nonce": "0x1", - "code": "0x608060405260043610610049575f3560e01c80633659cfe6146100535780634f1ef286146100725780635c60da1b146100855780638f283970146100b5578063f851a440146100d4575b6100516100e8565b005b34801561005e575f5ffd5b5061005161006d3660046104f3565b6100fa565b61005161008036600461050c565b610134565b348015610090575f5ffd5b50610099610197565b6040516001600160a01b03909116815260200160405180910390f35b3480156100c0575f5ffd5b506100516100cf3660046104f3565b6101a5565b3480156100df575f5ffd5b506100996101c5565b6100f86100f3610197565b6101ce565b565b6101026101ec565b6001600160a01b0316330361012c576101298160405180602001604052805f81525061021e565b50565b6101296100e8565b61013c6101ec565b6001600160a01b0316330361018f5761018a8383838080601f0160208091040260200160405190810160405280939291908181526020018383808284375f9201919091525061021e92505050565b505050565b61018a6100e8565b5f6101a0610277565b905090565b6101ad6101ec565b6001600160a01b0316330361012c576101298161029e565b5f6101a06101ec565b365f5f375f5f365f845af43d5f5f3e8080156101e8573d5ff35b3d5ffd5b5f7fb53127684a568b3173ae13b9f8a6016e243e63b6e8ee1178d6a717850b5d61035b546001600160a01b0316919050565b610227826102f2565b6040516001600160a01b038316907fbc7cd75a20ee27fd9adebab32041f755214dbc6bffa90cc0225b39da2e5c2d3b905f90a280511561026b5761018a8282610370565b6102736103e2565b5050565b5f7f360894a13ba1a3210667c828492db98dca3e2076cc3735a920a3ca505d382bbc61020f565b7f7e644d79422f17c01e4894b5f4f588d331ebfa28653d42ae832dc59e38c9798f6102c76101ec565b604080516001600160a01b03928316815291841660208301520160405180910390a161012981610401565b806001600160a01b03163b5f0361032c57604051634c9c8ce360e01b81526001600160a01b03821660048201526024015b60405180910390fd5b807f360894a13ba1a3210667c828492db98dca3e2076cc3735a920a3ca505d382bbc5b80546001600160a01b0319166001600160a01b039290921691909117905550565b60605f5f846001600160a01b03168460405161038c919061058a565b5f60405180830381855af49150503d805f81146103c4576040519150601f19603f3d011682016040523d82523d5f602084013e6103c9565b606091505b50915091506103d9858383610451565b95945050505050565b34156100f85760405163b398979f60e01b815260040160405180910390fd5b6001600160a01b03811661042a57604051633173bdd160e11b81525f6004820152602401610323565b807fb53127684a568b3173ae13b9f8a6016e243e63b6e8ee1178d6a717850b5d610361034f565b60608261046657610461826104b0565b6104a9565b815115801561047d57506001600160a01b0384163b155b156104a657604051639996b31560e01b81526001600160a01b0385166004820152602401610323565b50805b9392505050565b8051156104bf57805160208201fd5b60405163d6bda27560e01b815260040160405180910390fd5b80356001600160a01b03811681146104ee575f5ffd5b919050565b5f60208284031215610503575f5ffd5b6104a9826104d8565b5f5f5f6040848603121561051e575f5ffd5b610527846104d8565b9250602084013567ffffffffffffffff811115610542575f5ffd5b8401601f81018613610552575f5ffd5b803567ffffffffffffffff811115610568575f5ffd5b866020828401011115610579575f5ffd5b939660209190910195509293505050565b5f82518060208501845e5f92019182525091905056fea2646970667358221220407750ff6315cb87de3aba2eb532b034b7d9acff9bfba8abac9b674d66a9a95064736f6c634300081d0033", - "storage": { - "0xb53127684a568b3173ae13b9f8a6016e243e63b6e8ee1178d6a717850b5d6103": "0x000000000000000000000000605fa3fef59b7ba218c910e6671ce88cb9d2f1bf", - "0x360894a13ba1a3210667c828492db98dca3e2076cc3735a920a3ca505d382bbc": "0x000000000000000000000000084d0f0f9c15eb3dc3008b13dc7327dfc9799a9a", - "0xf0c57e16840df040f15088dc2f81fe391c3923bec73e23a9662efc9c229c6a00": "0x000000000000000000000000000000000000000000000000ffffffffffffffff", - "0x9016d09d72d40fdae2fd8ceac6b6234c7706214fd39c1cd1e609a0528c199300": "0x0000000000000000000000003600000000000000000000000000000000000003", - "0x0499f4c2de309a54da703ff88d2d815fa145b3fb32cf77b2528c8033db8fd7ee": "0x0000000000000000000000000000000000000000000000000000000000000002", - "0x0499f4c2de309a54da703ff88d2d815fa145b3fb32cf77b2528c8033db8fd7ef": "0x0000000000000000000000000000000000000000000000000000000000000041", - "0xe76fb05174802623d7f5b79138467ede24c07a48fb794feb0f195f82c29e25f6": "0x8396f9da70e4daf1af3bcba234d7247f34bceee859a83b09d9b22e938ce0eccb", - "0x0499f4c2de309a54da703ff88d2d815fa145b3fb32cf77b2528c8033db8fd7f0": "0x00000000000000000000000000000000000000000000000000000000000007d0", - "0x2559e4fd6bae46fd4a933fbdfc2024f30e3f30caf9b658f266112e99c269472e": "0x0000000000000000000000000000000000000000000000000000000000000001", - "0x3c22fd2722f4c68bee66b0771765c1374bf0b4f04712a4b14dee7a423e372484": "0x0000000000000000000000000000000000000000000000000000000000000001", - "0x3102412f7d6964aed7888d0afc2b4c2f1dbbfaf8be41e712f7d1621eda71211c": "0x0000000000000000000000000000000000000000000000000000000000000001", - "0x799a8c5a05520e2bd7f998b6bef5c37455a07d2f4637750d18e8585f32667020": "0x0000000000000000000000000000000000000000000000000000000000000002", - "0x799a8c5a05520e2bd7f998b6bef5c37455a07d2f4637750d18e8585f32667021": "0x0000000000000000000000000000000000000000000000000000000000000041", - "0x88471ae418c3e7b280a001c3ca67ebba02159f936da01ae6a2e84cabdddd1ff9": "0x634059e702a3d9991215218098266f65cd363f82e4a8f82862bdcc36761116da", - "0x799a8c5a05520e2bd7f998b6bef5c37455a07d2f4637750d18e8585f32667022": "0x00000000000000000000000000000000000000000000000000000000000007d0", - "0x2559e4fd6bae46fd4a933fbdfc2024f30e3f30caf9b658f266112e99c269472f": "0x0000000000000000000000000000000000000000000000000000000000000002", - "0x095131cda7b4097ad9172b46969f424386afbffb0fe58d634a10ceead00fe90f": "0x0000000000000000000000000000000000000000000000000000000000000002", - "0x89d3beeb403a682e167d5953de0ddcf012b784a68436e962f4eb9a2254058514": "0x0000000000000000000000000000000000000000000000000000000000000001", - "0x5823124c2578a0659c1d18b6eac7cd5d39d30d9e0a439c0dc79350c911078892": "0x0000000000000000000000000000000000000000000000000000000000000002", - "0x5823124c2578a0659c1d18b6eac7cd5d39d30d9e0a439c0dc79350c911078893": "0x0000000000000000000000000000000000000000000000000000000000000041", - "0xe4bbd97d770f505bda53f50c8dce4c86cb0dc400a1b5c97a9aacf7305b0907af": "0xc3dcde30cbe413e56c023514d2eef54db3ef7330542ae0adc3122696175d194e", - "0x5823124c2578a0659c1d18b6eac7cd5d39d30d9e0a439c0dc79350c911078894": "0x00000000000000000000000000000000000000000000000000000000000007d0", - "0x2559e4fd6bae46fd4a933fbdfc2024f30e3f30caf9b658f266112e99c2694730": "0x0000000000000000000000000000000000000000000000000000000000000003", - "0xe0ed4630bad2f511c8f295d4c67204cad855df4001547c1a63b7ba76c4642997": "0x0000000000000000000000000000000000000000000000000000000000000003", - "0x76f9effdd3943cf39996739cf42926ef87c8c7c4ac08f5bc3616582f0b6505dd": "0x0000000000000000000000000000000000000000000000000000000000000001", - "0xb58da0dce03316992faea3e12c60705b8ac05a309e27e3bc8421e5b271c9d201": "0x0000000000000000000000000000000000000000000000000000000000000003", - "0xb58da0dce03316992faea3e12c60705b8ac05a309e27e3bc8421e5b271c9d204": "0x0000000000000000000000000000000000000000000000000000000000000004" - } - }, - "0x7E9BF8C5eDcEeE2ffF5A969BcE00B89956397A0F": { - "balance": "0x0", - "nonce": "0x1", - "code": "0x608060405234801561000f575f5ffd5b50600436106100fb575f3560e01c80638da5cb5b11610093578063da3003f911610063578063da3003f914610218578063e30c39781461022b578063f2fde38b14610233578063f6a74ed714610246575f5ffd5b80638da5cb5b146101a857806398adeb97146101c8578063aa5ecb52146101db578063b429afeb146101ee575f5ffd5b80635e5feee6116100ce5780635e5feee61461017d578063715018a61461019057806379ba5097146101985780637f77403d146101a0575f5ffd5b8063048544a4146100ff5780632469d6721461013f578063263a3402146101605780634cb4850a1461016a575b5f5ffd5b61012a61010d366004610c45565b6001600160a01b03165f9081526001602052604090205460ff1690565b60405190151581526020015b60405180910390f35b61015261014d366004610c89565b610259565b604051908152602001610136565b61016861035b565b005b610168610178366004610d4d565b610411565b61016861018b366004610c45565b6104d7565b6101686105bc565b6101686105cf565b610168610617565b6101b06106a1565b6040516001600160a01b039091168152602001610136565b6101686101d6366004610d66565b6106d5565b6101686101e9366004610c45565b6107fd565b61012a6101fc366004610c45565b6001600160a01b03165f90815260208190526040902054151590565b610168610226366004610c45565b6108da565b6101b06109c8565b610168610241366004610c45565b6109f0565b610168610254366004610c45565b610a75565b335f9081526001602052604081205460ff166102c65760405162461bcd60e51b815260206004820152602160248201527f43616c6c6572206973206e6f742076616c696461746f725265676973746572656044820152603960f91b60648201526084015b60405180910390fd5b604051631234eb3960e11b81526001600160a01b037f00000000000000000000000036000000000000000000000000000000000000021690632469d672906103149086908690600401610d8e565b6020604051808303815f875af1158015610330573d5f5f3e3d5ffd5b505050506040513d601f19601f820116820180604052508101906103549190610dd4565b9392505050565b335f9081526020819052604081205490036103885760405162461bcd60e51b81526004016102bd90610deb565b335f9081526020819052604090819020549051634ebae61760e01b8152600481018290526001600160a01b037f00000000000000000000000036000000000000000000000000000000000000021690634ebae617906024015b5f604051808303815f87803b1580156103f8575f5ffd5b505af115801561040a573d5f5f3e3d5ffd5b5050505050565b335f90815260208190526040812054900361043e5760405162461bcd60e51b81526004016102bd90610deb565b335f908152602081905260409081902054905163312267c360e21b81526004810182905267ffffffffffffffff831660248201526001600160a01b037f0000000000000000000000003600000000000000000000000000000000000002169063c4899f0c906044015f604051808303815f87803b1580156104bd575f5ffd5b505af11580156104cf573d5f5f3e3d5ffd5b505050505050565b6104df610b4c565b6001600160a01b038116610506576040516342bcdf7f60e11b815260040160405180910390fd5b60405163f2fde38b60e01b81526001600160a01b0382811660048301527f0000000000000000000000003600000000000000000000000000000000000002169063f2fde38b906024015f604051808303815f87803b158015610566575f5ffd5b505af1158015610578573d5f5f3e3d5ffd5b50506040516001600160a01b03841681527ff8e2a8d4b93bd709cc57c9f21b79403c532f960ef18f77d0c23403cae0de78d99250602001905060405180910390a150565b6105c4610b4c565b6105cd5f610b7e565b565b33806105d96109c8565b6001600160a01b03161461060b5760405163118cdaa760e01b81526001600160a01b03821660048201526024016102bd565b61061481610b7e565b50565b335f9081526020819052604081205490036106445760405162461bcd60e51b81526004016102bd90610deb565b335f908152602081905260409081902054905163f94e186760e01b8152600481018290526001600160a01b037f0000000000000000000000003600000000000000000000000000000000000002169063f94e1867906024016103e1565b5f807f9016d09d72d40fdae2fd8ceac6b6234c7706214fd39c1cd1e609a0528c1993005b546001600160a01b031692915050565b6106dd610b4c565b805f0361072c5760405162461bcd60e51b815260206004820152601e60248201527f526567697374726174696f6e2049442063616e6e6f74206265207a65726f000060448201526064016102bd565b6001600160a01b0382166107525760405162461bcd60e51b81526004016102bd90610e22565b6001600160a01b0382165f90815260208190526040902054156107b75760405162461bcd60e51b815260206004820152601d60248201527f436f6e74726f6c6c657220616c726561647920636f6e6669677572656400000060448201526064016102bd565b6001600160a01b0382165f81815260208190526040808220849055518392917fd765df254a1ab86d330d7f337f70eca404f7fc78a93b587f4ffebcb135f4466991a35050565b610805610b4c565b6001600160a01b03811661082b5760405162461bcd60e51b81526004016102bd90610e67565b6001600160a01b0381165f9081526001602052604090205460ff166108925760405162461bcd60e51b815260206004820152601d60248201527f56616c696461746f7252656769737465726572206e6f7420616464656400000060448201526064016102bd565b6001600160a01b0381165f81815260016020526040808220805460ff19169055517f195d741377fcd6e23556d115769d0b5f54decc24349a916f6de7371077fe7fd99190a250565b6108e2610b4c565b6001600160a01b0381166109085760405162461bcd60e51b81526004016102bd90610e67565b6001600160a01b0381165f9081526001602052604090205460ff161561097a5760405162461bcd60e51b815260206004820152602160248201527f56616c696461746f725265676973746572657220616c726561647920616464656044820152601960fa1b60648201526084016102bd565b6001600160a01b0381165f818152600160208190526040808320805460ff1916909217909155517fd25faf73acccddf9a7ac429be872d2109cd7bd0c2370658b7c019518f3159d8e9190a250565b5f807f237e158222e3e6968b72b9db0d8043aacf074ad9f650f0d1606b4d82ee432c006106c5565b6109f8610b4c565b7f237e158222e3e6968b72b9db0d8043aacf074ad9f650f0d1606b4d82ee432c0080546001600160a01b0319166001600160a01b0383169081178255610a3c6106a1565b6001600160a01b03167f38d16b8cac22d99fc7c124b9cd0de2d3fa1faef420bfe791d8c362d765e2270060405160405180910390a35050565b610a7d610b4c565b6001600160a01b038116610aa35760405162461bcd60e51b81526004016102bd90610e22565b6001600160a01b0381165f908152602081905260408120549003610b095760405162461bcd60e51b815260206004820152601960248201527f436f6e74726f6c6c6572206e6f7420636f6e666967757265640000000000000060448201526064016102bd565b6001600160a01b0381165f81815260208190526040808220829055517f33d83959be2573f5453b12eb9d43b3499bc57d96bd2f067ba44803c859e811139190a250565b33610b556106a1565b6001600160a01b0316146105cd5760405163118cdaa760e01b81523360048201526024016102bd565b7f237e158222e3e6968b72b9db0d8043aacf074ad9f650f0d1606b4d82ee432c0080546001600160a01b0319168155610bb682610bba565b5050565b7f9016d09d72d40fdae2fd8ceac6b6234c7706214fd39c1cd1e609a0528c19930080546001600160a01b031981166001600160a01b03848116918217845560405192169182907f8be0079c531659141344cd1fd0a4f28419497f9722a3daafe3b4186f6b6457e0905f90a3505050565b80356001600160a01b0381168114610c40575f5ffd5b919050565b5f60208284031215610c55575f5ffd5b61035482610c2a565b634e487b7160e01b5f52604160045260245ffd5b803567ffffffffffffffff81168114610c40575f5ffd5b5f5f60408385031215610c9a575f5ffd5b823567ffffffffffffffff811115610cb0575f5ffd5b8301601f81018513610cc0575f5ffd5b803567ffffffffffffffff811115610cda57610cda610c5e565b604051601f8201601f19908116603f0116810167ffffffffffffffff81118282101715610d0957610d09610c5e565b604052818152828201602001871015610d20575f5ffd5b816020840160208301375f60208383010152809450505050610d4460208401610c72565b90509250929050565b5f60208284031215610d5d575f5ffd5b61035482610c72565b5f5f60408385031215610d77575f5ffd5b610d8083610c2a565b946020939093013593505050565b604081525f83518060408401528060208601606085015e5f606082850101526060601f19601f83011684010191505067ffffffffffffffff831660208301529392505050565b5f60208284031215610de4575f5ffd5b5051919050565b60208082526018908201527f43616c6c6572206973206e6f7420636f6e74726f6c6c65720000000000000000604082015260600190565b60208082526025908201527f436f6e74726f6c6c6572206d7573742062652061206e6f6e2d7a65726f206164604082015264647265737360d81b606082015260800190565b6020808252602e908201527f56616c696461746f7252656769737465726572206d7573742062652061206e6f60408201526d6e2d7a65726f206164647265737360901b60608201526080019056fea2646970667358221220fedd35edb498f5084c7e905194559e6e88f9ef051e5c94ede68104b338c5320b64736f6c634300081d0033" - }, - "0x3600000000000000000000000000000000000003": { - "balance": "0x0", - "nonce": "0x1", - "code": "0x608060405260043610610049575f3560e01c80633659cfe6146100535780634f1ef286146100725780635c60da1b146100855780638f283970146100b5578063f851a440146100d4575b6100516100e8565b005b34801561005e575f5ffd5b5061005161006d3660046104f3565b6100fa565b61005161008036600461050c565b610134565b348015610090575f5ffd5b50610099610197565b6040516001600160a01b03909116815260200160405180910390f35b3480156100c0575f5ffd5b506100516100cf3660046104f3565b6101a5565b3480156100df575f5ffd5b506100996101c5565b6100f86100f3610197565b6101ce565b565b6101026101ec565b6001600160a01b0316330361012c576101298160405180602001604052805f81525061021e565b50565b6101296100e8565b61013c6101ec565b6001600160a01b0316330361018f5761018a8383838080601f0160208091040260200160405190810160405280939291908181526020018383808284375f9201919091525061021e92505050565b505050565b61018a6100e8565b5f6101a0610277565b905090565b6101ad6101ec565b6001600160a01b0316330361012c576101298161029e565b5f6101a06101ec565b365f5f375f5f365f845af43d5f5f3e8080156101e8573d5ff35b3d5ffd5b5f7fb53127684a568b3173ae13b9f8a6016e243e63b6e8ee1178d6a717850b5d61035b546001600160a01b0316919050565b610227826102f2565b6040516001600160a01b038316907fbc7cd75a20ee27fd9adebab32041f755214dbc6bffa90cc0225b39da2e5c2d3b905f90a280511561026b5761018a8282610370565b6102736103e2565b5050565b5f7f360894a13ba1a3210667c828492db98dca3e2076cc3735a920a3ca505d382bbc61020f565b7f7e644d79422f17c01e4894b5f4f588d331ebfa28653d42ae832dc59e38c9798f6102c76101ec565b604080516001600160a01b03928316815291841660208301520160405180910390a161012981610401565b806001600160a01b03163b5f0361032c57604051634c9c8ce360e01b81526001600160a01b03821660048201526024015b60405180910390fd5b807f360894a13ba1a3210667c828492db98dca3e2076cc3735a920a3ca505d382bbc5b80546001600160a01b0319166001600160a01b039290921691909117905550565b60605f5f846001600160a01b03168460405161038c919061058a565b5f60405180830381855af49150503d805f81146103c4576040519150601f19603f3d011682016040523d82523d5f602084013e6103c9565b606091505b50915091506103d9858383610451565b95945050505050565b34156100f85760405163b398979f60e01b815260040160405180910390fd5b6001600160a01b03811661042a57604051633173bdd160e11b81525f6004820152602401610323565b807fb53127684a568b3173ae13b9f8a6016e243e63b6e8ee1178d6a717850b5d610361034f565b60608261046657610461826104b0565b6104a9565b815115801561047d57506001600160a01b0384163b155b156104a657604051639996b31560e01b81526001600160a01b0385166004820152602401610323565b50805b9392505050565b8051156104bf57805160208201fd5b60405163d6bda27560e01b815260040160405180910390fd5b80356001600160a01b03811681146104ee575f5ffd5b919050565b5f60208284031215610503575f5ffd5b6104a9826104d8565b5f5f5f6040848603121561051e575f5ffd5b610527846104d8565b9250602084013567ffffffffffffffff811115610542575f5ffd5b8401601f81018613610552575f5ffd5b803567ffffffffffffffff811115610568575f5ffd5b866020828401011115610579575f5ffd5b939660209190910195509293505050565b5f82518060208501845e5f92019182525091905056fea2646970667358221220407750ff6315cb87de3aba2eb532b034b7d9acff9bfba8abac9b674d66a9a95064736f6c634300081d0033", - "storage": { - "0xb53127684a568b3173ae13b9f8a6016e243e63b6e8ee1178d6a717850b5d6103": "0x000000000000000000000000456f90eb4023bcc821c1a9164046c1bd92898696", - "0x360894a13ba1a3210667c828492db98dca3e2076cc3735a920a3ca505d382bbc": "0x0000000000000000000000007e9bf8c5edceee2fff5a969bce00b89956397a0f", - "0x9016d09d72d40fdae2fd8ceac6b6234c7706214fd39c1cd1e609a0528c199300": "0x0000000000000000000000005f5dc64f8926dd61c57812565d308c04be68546a", - "0xcfd3069c5a806e81871b52995cceff023dd72bd06262a5614d4acef5490141a8": "0x0000000000000000000000000000000000000000000000000000000000000001", - "0xf7e079a6823699d09db9fd53087a1e76888c4744fe1f0640485c0845bd369e4a": "0x0000000000000000000000000000000000000000000000000000000000000002", - "0x4f60acd9c0f9c5064d20e9a12f8258173684b0460bb66a5c4def2df45d006165": "0x0000000000000000000000000000000000000000000000000000000000000003", - "0x38b64211caa0ee7085908f8d53aae2f60caa990be015d343d700dd0fb439f6f8": "0x0000000000000000000000000000000000000000000000000000000000000004", - "0x7afd34c314602f70b99b74bd9ec141eca368954a82ebf9486e698d9b911dd988": "0x0000000000000000000000000000000000000000000000000000000000000005", - "0xe3bf69db800fe666843cb0f4f7f8349b1f40d1fbb1637e0f14dcae973c3df310": "0x0000000000000000000000000000000000000000000000000000000000000006", - "0x54f67a3f29a3106355ed6809f9a9d181c23f892e5441e69f1f854fd277b8bd44": "0x0000000000000000000000000000000000000000000000000000000000000007", - "0x631cc8613eabc907a2fea6d093193dfd64f169db23e51a8c050c01a2976fe1be": "0x0000000000000000000000000000000000000000000000000000000000000008", - "0x3849c8f648bbba7ce606a96689dab6559ce3a4d672d864e2029d984dd21aa218": "0x0000000000000000000000000000000000000000000000000000000000000009", - "0xa240fdf646331965dad6f2e86f4cbedab33d4616cacb143639d5582d7ddd83f8": "0x0000000000000000000000000000000000000000000000000000000000000001", - "0x04ada9283f18bb861b2e3884625dd8be264a495117deba3304756ee681830b05": "0x0000000000000000000000000000000000000000000000000000000000000001" - } - }, - "0x4e59b44847b379578588920cA78FbF26c0B4956C": { - "balance": "0x0", - "nonce": "0x1", - "code": "0x7fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffe03601600081602082378035828234f58015156039578182fd5b8082525050506014600cf3" - }, - "0xcA11bde05977b3631167028862bE2a173976CA11": { - "balance": "0x0", - "nonce": "0x1", - "code": "0x6080604052600436106100f35760003560e01c80634d2301cc1161008a578063a8b0574e11610059578063a8b0574e1461025a578063bce38bd714610275578063c3077fa914610288578063ee82ac5e1461029b57600080fd5b80634d2301cc146101ec57806372425d9d1461022157806382ad56cb1461023457806386d516e81461024757600080fd5b80633408e470116100c65780633408e47014610191578063399542e9146101a45780633e64a696146101c657806342cbb15c146101d957600080fd5b80630f28c97d146100f8578063174dea711461011a578063252dba421461013a57806327e86d6e1461015b575b600080fd5b34801561010457600080fd5b50425b6040519081526020015b60405180910390f35b61012d610128366004610a85565b6102ba565b6040516101119190610bbe565b61014d610148366004610a85565b6104ef565b604051610111929190610bd8565b34801561016757600080fd5b50437fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff0140610107565b34801561019d57600080fd5b5046610107565b6101b76101b2366004610c60565b610690565b60405161011193929190610cba565b3480156101d257600080fd5b5048610107565b3480156101e557600080fd5b5043610107565b3480156101f857600080fd5b50610107610207366004610ce2565b73ffffffffffffffffffffffffffffffffffffffff163190565b34801561022d57600080fd5b5044610107565b61012d610242366004610a85565b6106ab565b34801561025357600080fd5b5045610107565b34801561026657600080fd5b50604051418152602001610111565b61012d610283366004610c60565b61085a565b6101b7610296366004610a85565b610a1a565b3480156102a757600080fd5b506101076102b6366004610d18565b4090565b60606000828067ffffffffffffffff8111156102d8576102d8610d31565b60405190808252806020026020018201604052801561031e57816020015b6040805180820190915260008152606060208201528152602001906001900390816102f65790505b5092503660005b8281101561047757600085828151811061034157610341610d60565b6020026020010151905087878381811061035d5761035d610d60565b905060200281019061036f9190610d8f565b6040810135958601959093506103886020850185610ce2565b73ffffffffffffffffffffffffffffffffffffffff16816103ac6060870187610dcd565b6040516103ba929190610e32565b60006040518083038185875af1925050503d80600081146103f7576040519150601f19603f3d011682016040523d82523d6000602084013e6103fc565b606091505b50602080850191909152901515808452908501351761046d577f08c379a000000000000000000000000000000000000000000000000000000000600052602060045260176024527f4d756c746963616c6c333a2063616c6c206661696c656400000000000000000060445260846000fd5b5050600101610325565b508234146104e6576040517f08c379a000000000000000000000000000000000000000000000000000000000815260206004820152601a60248201527f4d756c746963616c6c333a2076616c7565206d69736d6174636800000000000060448201526064015b60405180910390fd5b50505092915050565b436060828067ffffffffffffffff81111561050c5761050c610d31565b60405190808252806020026020018201604052801561053f57816020015b606081526020019060019003908161052a5790505b5091503660005b8281101561068657600087878381811061056257610562610d60565b90506020028101906105749190610e42565b92506105836020840184610ce2565b73ffffffffffffffffffffffffffffffffffffffff166105a66020850185610dcd565b6040516105b4929190610e32565b6000604051808303816000865af19150503d80600081146105f1576040519150601f19603f3d011682016040523d82523d6000602084013e6105f6565b606091505b5086848151811061060957610609610d60565b602090810291909101015290508061067d576040517f08c379a000000000000000000000000000000000000000000000000000000000815260206004820152601760248201527f4d756c746963616c6c333a2063616c6c206661696c656400000000000000000060448201526064016104dd565b50600101610546565b5050509250929050565b43804060606106a086868661085a565b905093509350939050565b6060818067ffffffffffffffff8111156106c7576106c7610d31565b60405190808252806020026020018201604052801561070d57816020015b6040805180820190915260008152606060208201528152602001906001900390816106e55790505b5091503660005b828110156104e657600084828151811061073057610730610d60565b6020026020010151905086868381811061074c5761074c610d60565b905060200281019061075e9190610e76565b925061076d6020840184610ce2565b73ffffffffffffffffffffffffffffffffffffffff166107906040850185610dcd565b60405161079e929190610e32565b6000604051808303816000865af19150503d80600081146107db576040519150601f19603f3d011682016040523d82523d6000602084013e6107e0565b606091505b506020808401919091529015158083529084013517610851577f08c379a000000000000000000000000000000000000000000000000000000000600052602060045260176024527f4d756c746963616c6c333a2063616c6c206661696c656400000000000000000060445260646000fd5b50600101610714565b6060818067ffffffffffffffff81111561087657610876610d31565b6040519080825280602002602001820160405280156108bc57816020015b6040805180820190915260008152606060208201528152602001906001900390816108945790505b5091503660005b82811015610a105760008482815181106108df576108df610d60565b602002602001015190508686838181106108fb576108fb610d60565b905060200281019061090d9190610e42565b925061091c6020840184610ce2565b73ffffffffffffffffffffffffffffffffffffffff1661093f6020850185610dcd565b60405161094d929190610e32565b6000604051808303816000865af19150503d806000811461098a576040519150601f19603f3d011682016040523d82523d6000602084013e61098f565b606091505b506020830152151581528715610a07578051610a07576040517f08c379a000000000000000000000000000000000000000000000000000000000815260206004820152601760248201527f4d756c746963616c6c333a2063616c6c206661696c656400000000000000000060448201526064016104dd565b506001016108c3565b5050509392505050565b6000806060610a2b60018686610690565b919790965090945092505050565b60008083601f840112610a4b57600080fd5b50813567ffffffffffffffff811115610a6357600080fd5b6020830191508360208260051b8501011115610a7e57600080fd5b9250929050565b60008060208385031215610a9857600080fd5b823567ffffffffffffffff811115610aaf57600080fd5b610abb85828601610a39565b90969095509350505050565b6000815180845260005b81811015610aed57602081850181015186830182015201610ad1565b81811115610aff576000602083870101525b50601f017fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffe0169290920160200192915050565b600082825180855260208086019550808260051b84010181860160005b84811015610bb1578583037fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffe001895281518051151584528401516040858501819052610b9d81860183610ac7565b9a86019a9450505090830190600101610b4f565b5090979650505050505050565b602081526000610bd16020830184610b32565b9392505050565b600060408201848352602060408185015281855180845260608601915060608160051b870101935082870160005b82811015610c52577fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffa0888703018452610c40868351610ac7565b95509284019290840190600101610c06565b509398975050505050505050565b600080600060408486031215610c7557600080fd5b83358015158114610c8557600080fd5b9250602084013567ffffffffffffffff811115610ca157600080fd5b610cad86828701610a39565b9497909650939450505050565b838152826020820152606060408201526000610cd96060830184610b32565b95945050505050565b600060208284031215610cf457600080fd5b813573ffffffffffffffffffffffffffffffffffffffff81168114610bd157600080fd5b600060208284031215610d2a57600080fd5b5035919050565b7f4e487b7100000000000000000000000000000000000000000000000000000000600052604160045260246000fd5b7f4e487b7100000000000000000000000000000000000000000000000000000000600052603260045260246000fd5b600082357fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff81833603018112610dc357600080fd5b9190910192915050565b60008083357fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffe1843603018112610e0257600080fd5b83018035915067ffffffffffffffff821115610e1d57600080fd5b602001915036819003821315610a7e57600080fd5b8183823760009101908152919050565b600082357fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffc1833603018112610dc357600080fd5b600082357fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffa1833603018112610dc357600080fdfea2646970667358221220bb2b5c71a328032f97c676ae39a1ec2148d3e5d6f73d95e9b17910152d61f16264736f6c634300080c0033" - }, - "0x000000000022D473030F116dDEE9F6B43aC78BA3": { - "balance": "0x0", - "nonce": "0x1", - "code": "0x6040608081526004908136101561001557600080fd5b600090813560e01c80630d58b1db1461126c578063137c29fe146110755780632a2d80d114610db75780632b67b57014610bde57806330f28b7a14610ade5780633644e51514610a9d57806336c7851614610a285780633ff9dcb1146109a85780634fe02b441461093f57806365d9723c146107ac57806387517c451461067a578063927da105146105c3578063cc53287f146104a3578063edd9444b1461033a5763fe8ec1a7146100c657600080fd5b346103365760c07ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffc3601126103365767ffffffffffffffff833581811161033257610114903690860161164b565b60243582811161032e5761012b903690870161161a565b6101336114e6565b9160843585811161032a5761014b9036908a016115c1565b98909560a43590811161032657610164913691016115c1565b969095815190610173826113ff565b606b82527f5065726d697442617463685769746e6573735472616e7366657246726f6d285460208301527f6f6b656e5065726d697373696f6e735b5d207065726d69747465642c61646472838301527f657373207370656e6465722c75696e74323536206e6f6e63652c75696e74323560608301527f3620646561646c696e652c000000000000000000000000000000000000000000608083015282519a8b9181610222602085018096611f93565b918237018a8152039961025b7fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffe09b8c8101835282611437565b5190209085515161026b81611ebb565b908a5b8181106102f95750506102f6999a6102ed9183516102a081610294602082018095611f66565b03848101835282611437565b519020602089810151858b015195519182019687526040820192909252336060820152608081019190915260a081019390935260643560c08401528260e081015b03908101835282611437565b51902093611cf7565b80f35b8061031161030b610321938c5161175e565b51612054565b61031b828661175e565b52611f0a565b61026e565b8880fd5b8780fd5b8480fd5b8380fd5b5080fd5b5091346103365760807ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffc3601126103365767ffffffffffffffff9080358281116103325761038b903690830161164b565b60243583811161032e576103a2903690840161161a565b9390926103ad6114e6565b9160643590811161049f576103c4913691016115c1565b949093835151976103d489611ebb565b98885b81811061047d5750506102f697988151610425816103f9602082018095611f66565b037fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffe08101835282611437565b5190206020860151828701519083519260208401947ffcf35f5ac6a2c28868dc44c302166470266239195f02b0ee408334829333b7668652840152336060840152608083015260a082015260a081526102ed8161141b565b808b61031b8261049461030b61049a968d5161175e565b9261175e565b6103d7565b8680fd5b5082346105bf57602090817ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffc3601126103325780359067ffffffffffffffff821161032e576104f49136910161161a565b929091845b848110610504578580f35b8061051a610515600193888861196c565b61197c565b61052f84610529848a8a61196c565b0161197c565b3389528385528589209173ffffffffffffffffffffffffffffffffffffffff80911692838b528652868a20911690818a5285528589207fffffffffffffffffffffffff000000000000000000000000000000000000000081541690558551918252848201527f89b1add15eff56b3dfe299ad94e01f2b52fbcb80ae1a3baea6ae8c04cb2b98a4853392a2016104f9565b8280fd5b50346103365760607ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffc36011261033657610676816105ff6114a0565b936106086114c3565b6106106114e6565b73ffffffffffffffffffffffffffffffffffffffff968716835260016020908152848420928816845291825283832090871683528152919020549251938316845260a083901c65ffffffffffff169084015260d09190911c604083015281906060820190565b0390f35b50346103365760807ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffc360112610336576106b26114a0565b906106bb6114c3565b916106c46114e6565b65ffffffffffff926064358481169081810361032a5779ffffffffffff0000000000000000000000000000000000000000947fda9fa7c1b00402c17d0161b249b1ab8bbec047c5a52207b9c112deffd817036b94338a5260016020527fffffffffffff0000000000000000000000000000000000000000000000000000858b209873ffffffffffffffffffffffffffffffffffffffff809416998a8d5260205283878d209b169a8b8d52602052868c209486156000146107a457504216925b8454921697889360a01b16911617179055815193845260208401523392a480f35b905092610783565b5082346105bf5760607ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffc3601126105bf576107e56114a0565b906107ee6114c3565b9265ffffffffffff604435818116939084810361032a57338852602091600183528489209673ffffffffffffffffffffffffffffffffffffffff80911697888b528452858a20981697888a5283528489205460d01c93848711156109175761ffff9085840316116108f05750907f55eb90d810e1700b35a8e7e25395ff7f2b2259abd7415ca2284dfb1c246418f393929133895260018252838920878a528252838920888a5282528389209079ffffffffffffffffffffffffffffffffffffffffffffffffffff7fffffffffffff000000000000000000000000000000000000000000000000000083549260d01b16911617905582519485528401523392a480f35b84517f24d35a26000000000000000000000000000000000000000000000000000000008152fd5b5084517f756688fe000000000000000000000000000000000000000000000000000000008152fd5b503461033657807ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffc360112610336578060209273ffffffffffffffffffffffffffffffffffffffff61098f6114a0565b1681528084528181206024358252845220549051908152f35b5082346105bf57817ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffc3601126105bf577f3704902f963766a4e561bbaab6e6cdc1b1dd12f6e9e99648da8843b3f46b918d90359160243533855284602052818520848652602052818520818154179055815193845260208401523392a280f35b8234610a9a5760807ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffc360112610a9a57610a606114a0565b610a686114c3565b610a706114e6565b6064359173ffffffffffffffffffffffffffffffffffffffff8316830361032e576102f6936117a1565b80fd5b503461033657817ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffc36011261033657602090610ad7611b1e565b9051908152f35b508290346105bf576101007ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffc3601126105bf57610b1a3661152a565b90807fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff7c36011261033257610b4c611478565b9160e43567ffffffffffffffff8111610bda576102f694610b6f913691016115c1565b939092610b7c8351612054565b6020840151828501519083519260208401947f939c21a48a8dbe3a9a2404a1d46691e4d39f6583d6ec6b35714604c986d801068652840152336060840152608083015260a082015260a08152610bd18161141b565b51902091611c25565b8580fd5b509134610336576101007ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffc36011261033657610c186114a0565b7fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffdc360160c08112610332576080855191610c51836113e3565b1261033257845190610c6282611398565b73ffffffffffffffffffffffffffffffffffffffff91602435838116810361049f578152604435838116810361049f57602082015265ffffffffffff606435818116810361032a5788830152608435908116810361049f576060820152815260a435938285168503610bda576020820194855260c4359087830182815260e43567ffffffffffffffff811161032657610cfe90369084016115c1565b929093804211610d88575050918591610d786102f6999a610d7e95610d238851611fbe565b90898c511690519083519260208401947ff3841cd1ff0085026a6327b620b67997ce40f282c88a8e905a7a5626e310f3d086528401526060830152608082015260808152610d70816113ff565b519020611bd9565b916120c7565b519251169161199d565b602492508a51917fcd21db4f000000000000000000000000000000000000000000000000000000008352820152fd5b5091346103365760607ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffc93818536011261033257610df36114a0565b9260249081359267ffffffffffffffff9788851161032a578590853603011261049f578051978589018981108282111761104a578252848301358181116103265785019036602383011215610326578382013591610e50836115ef565b90610e5d85519283611437565b838252602093878584019160071b83010191368311611046578801905b828210610fe9575050508a526044610e93868801611509565b96838c01978852013594838b0191868352604435908111610fe557610ebb90369087016115c1565b959096804211610fba575050508998995151610ed681611ebb565b908b5b818110610f9757505092889492610d7892610f6497958351610f02816103f98682018095611f66565b5190209073ffffffffffffffffffffffffffffffffffffffff9a8b8b51169151928551948501957faf1b0d30d2cab0380e68f0689007e3254993c596f2fdd0aaa7f4d04f794408638752850152830152608082015260808152610d70816113ff565b51169082515192845b848110610f78578580f35b80610f918585610f8b600195875161175e565b5161199d565b01610f6d565b80610311610fac8e9f9e93610fb2945161175e565b51611fbe565b9b9a9b610ed9565b8551917fcd21db4f000000000000000000000000000000000000000000000000000000008352820152fd5b8a80fd5b6080823603126110465785608091885161100281611398565b61100b85611509565b8152611018838601611509565b838201526110278a8601611607565b8a8201528d611037818701611607565b90820152815201910190610e7a565b8c80fd5b84896041867f4e487b7100000000000000000000000000000000000000000000000000000000835252fd5b5082346105bf576101407ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffc3601126105bf576110b03661152a565b91807fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff7c360112610332576110e2611478565b67ffffffffffffffff93906101043585811161049f5761110590369086016115c1565b90936101243596871161032a57611125610bd1966102f6983691016115c1565b969095825190611134826113ff565b606482527f5065726d69745769746e6573735472616e7366657246726f6d28546f6b656e5060208301527f65726d697373696f6e73207065726d69747465642c6164647265737320737065848301527f6e6465722c75696e74323536206e6f6e63652c75696e7432353620646561646c60608301527f696e652c0000000000000000000000000000000000000000000000000000000060808301528351948591816111e3602085018096611f93565b918237018b8152039361121c7fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffe095868101835282611437565b5190209261122a8651612054565b6020878101518589015195519182019687526040820192909252336060820152608081019190915260a081019390935260e43560c08401528260e081016102e1565b5082346105bf576020807ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffc36011261033257813567ffffffffffffffff92838211610bda5736602383011215610bda5781013592831161032e576024906007368386831b8401011161049f57865b8581106112e5578780f35b80821b83019060807fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffdc83360301126103265761139288876001946060835161132c81611398565b611368608461133c8d8601611509565b9485845261134c60448201611509565b809785015261135d60648201611509565b809885015201611509565b918291015273ffffffffffffffffffffffffffffffffffffffff80808093169516931691166117a1565b016112da565b6080810190811067ffffffffffffffff8211176113b457604052565b7f4e487b7100000000000000000000000000000000000000000000000000000000600052604160045260246000fd5b6060810190811067ffffffffffffffff8211176113b457604052565b60a0810190811067ffffffffffffffff8211176113b457604052565b60c0810190811067ffffffffffffffff8211176113b457604052565b90601f7fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffe0910116810190811067ffffffffffffffff8211176113b457604052565b60c4359073ffffffffffffffffffffffffffffffffffffffff8216820361149b57565b600080fd5b6004359073ffffffffffffffffffffffffffffffffffffffff8216820361149b57565b6024359073ffffffffffffffffffffffffffffffffffffffff8216820361149b57565b6044359073ffffffffffffffffffffffffffffffffffffffff8216820361149b57565b359073ffffffffffffffffffffffffffffffffffffffff8216820361149b57565b7ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffc01906080821261149b576040805190611563826113e3565b8082941261149b57805181810181811067ffffffffffffffff8211176113b457825260043573ffffffffffffffffffffffffffffffffffffffff8116810361149b578152602435602082015282526044356020830152606435910152565b9181601f8401121561149b5782359167ffffffffffffffff831161149b576020838186019501011161149b57565b67ffffffffffffffff81116113b45760051b60200190565b359065ffffffffffff8216820361149b57565b9181601f8401121561149b5782359167ffffffffffffffff831161149b576020808501948460061b01011161149b57565b91909160608184031261149b576040805191611666836113e3565b8294813567ffffffffffffffff9081811161149b57830182601f8201121561149b578035611693816115ef565b926116a087519485611437565b818452602094858086019360061b8501019381851161149b579086899897969594939201925b8484106116e3575050505050855280820135908501520135910152565b90919293949596978483031261149b578851908982019082821085831117611730578a928992845261171487611509565b81528287013583820152815201930191908897969594936116c6565b602460007f4e487b710000000000000000000000000000000000000000000000000000000081526041600452fd5b80518210156117725760209160051b010190565b7f4e487b7100000000000000000000000000000000000000000000000000000000600052603260045260246000fd5b92919273ffffffffffffffffffffffffffffffffffffffff604060008284168152600160205282828220961695868252602052818120338252602052209485549565ffffffffffff8760a01c16804211611884575082871696838803611812575b5050611810955016926118b5565b565b878484161160001461184f57602488604051907ff96fb0710000000000000000000000000000000000000000000000000000000082526004820152fd5b7fffffffffffffffffffffffff000000000000000000000000000000000000000084846118109a031691161790553880611802565b602490604051907fd81b2f2e0000000000000000000000000000000000000000000000000000000082526004820152fd5b9060006064926020958295604051947f23b872dd0000000000000000000000000000000000000000000000000000000086526004860152602485015260448401525af13d15601f3d116001600051141617161561190e57565b60646040517f08c379a000000000000000000000000000000000000000000000000000000000815260206004820152601460248201527f5452414e534645525f46524f4d5f4641494c45440000000000000000000000006044820152fd5b91908110156117725760061b0190565b3573ffffffffffffffffffffffffffffffffffffffff8116810361149b5790565b9065ffffffffffff908160608401511673ffffffffffffffffffffffffffffffffffffffff908185511694826020820151169280866040809401511695169560009187835260016020528383208984526020528383209916988983526020528282209184835460d01c03611af5579185611ace94927fc6a377bfc4eb120024a8ac08eef205be16b817020812c73223e81d1bdb9708ec98979694508715600014611ad35779ffffffffffff00000000000000000000000000000000000000009042165b60a01b167fffffffffffff00000000000000000000000000000000000000000000000000006001860160d01b1617179055519384938491604091949373ffffffffffffffffffffffffffffffffffffffff606085019616845265ffffffffffff809216602085015216910152565b0390a4565b5079ffffffffffff000000000000000000000000000000000000000087611a60565b600484517f756688fe000000000000000000000000000000000000000000000000000000008152fd5b467f00000000000000000000000000000000000000000000000000000000004cef5203611b69577fe59c8d3fa907f1186bfa334839eb895f53f88b07e4cf5aafaef4af163d83ce9390565b60405160208101907f8cad95687ba82c2ce50e74f7b754645e5117c3a5bec8151c0726d5857980a86682527f9ac997416e8ff9d2ff6bebeb7149f65cdae5e32e2b90440b566bb3044041d36a604082015246606082015230608082015260808152611bd3816113ff565b51902090565b611be1611b1e565b906040519060208201927f190100000000000000000000000000000000000000000000000000000000000084526022830152604282015260428152611bd381611398565b9192909360a435936040840151804211611cc65750602084510151808611611c955750918591610d78611c6594611c60602088015186611e47565b611bd9565b73ffffffffffffffffffffffffffffffffffffffff809151511692608435918216820361149b57611810936118b5565b602490604051907f3728b83d0000000000000000000000000000000000000000000000000000000082526004820152fd5b602490604051907fcd21db4f0000000000000000000000000000000000000000000000000000000082526004820152fd5b959093958051519560409283830151804211611e175750848803611dee57611d2e918691610d7860209b611c608d88015186611e47565b60005b868110611d42575050505050505050565b611d4d81835161175e565b5188611d5a83878a61196c565b01359089810151808311611dbe575091818888886001968596611d84575b50505050505001611d31565b611db395611dad9273ffffffffffffffffffffffffffffffffffffffff6105159351169561196c565b916118b5565b803888888883611d78565b6024908651907f3728b83d0000000000000000000000000000000000000000000000000000000082526004820152fd5b600484517fff633a38000000000000000000000000000000000000000000000000000000008152fd5b6024908551907fcd21db4f0000000000000000000000000000000000000000000000000000000082526004820152fd5b9073ffffffffffffffffffffffffffffffffffffffff600160ff83161b9216600052600060205260406000209060081c6000526020526040600020818154188091551615611e9157565b60046040517f756688fe000000000000000000000000000000000000000000000000000000008152fd5b90611ec5826115ef565b611ed26040519182611437565b8281527fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffe0611f0082946115ef565b0190602036910137565b7fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff8114611f375760010190565b7f4e487b7100000000000000000000000000000000000000000000000000000000600052601160045260246000fd5b805160208092019160005b828110611f7f575050505090565b835185529381019392810192600101611f71565b9081519160005b838110611fab575050016000815290565b8060208092840101518185015201611f9a565b60405160208101917f65626cad6cb96493bf6f5ebea28756c966f023ab9e8a83a7101849d5573b3678835273ffffffffffffffffffffffffffffffffffffffff8082511660408401526020820151166060830152606065ffffffffffff9182604082015116608085015201511660a082015260a0815260c0810181811067ffffffffffffffff8211176113b45760405251902090565b6040516020808201927f618358ac3db8dc274f0cd8829da7e234bd48cd73c4a740aede1adec9846d06a1845273ffffffffffffffffffffffffffffffffffffffff81511660408401520151606082015260608152611bd381611398565b919082604091031261149b576020823592013590565b6000843b61222e5750604182036121ac576120e4828201826120b1565b939092604010156117725760209360009360ff6040608095013560f81c5b60405194855216868401526040830152606082015282805260015afa156121a05773ffffffffffffffffffffffffffffffffffffffff806000511691821561217657160361214c57565b60046040517f815e1d64000000000000000000000000000000000000000000000000000000008152fd5b60046040517f8baa579f000000000000000000000000000000000000000000000000000000008152fd5b6040513d6000823e3d90fd5b60408203612204576121c0918101906120b1565b91601b7f7fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff84169360ff1c019060ff8211611f375760209360009360ff608094612102565b60046040517f4be6321b000000000000000000000000000000000000000000000000000000008152fd5b929391601f928173ffffffffffffffffffffffffffffffffffffffff60646020957fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffe0604051988997889687947f1626ba7e000000000000000000000000000000000000000000000000000000009e8f8752600487015260406024870152816044870152868601378b85828601015201168101030192165afa9081156123a857829161232a575b507fffffffff000000000000000000000000000000000000000000000000000000009150160361230057565b60046040517fb0669cbc000000000000000000000000000000000000000000000000000000008152fd5b90506020813d82116123a0575b8161234460209383611437565b810103126103365751907fffffffff0000000000000000000000000000000000000000000000000000000082168203610a9a57507fffffffff0000000000000000000000000000000000000000000000000000000090386122d4565b3d9150612337565b6040513d84823e3d90fdfea164736f6c6343000811000a" - }, - "0x1800000000000000000000000000000000000002": { - "balance": "0x0", - "nonce": "0x1", - "code": "0x01" - }, - "0x1800000000000000000000000000000000000003": { - "balance": "0x0", - "nonce": "0x1", - "code": "0x01" - }, - "0x1800000000000000000000000000000000000004": { - "balance": "0x0", - "nonce": "0x1", - "code": "0x01" - }, - "0x1800000000000000000000000000000000000005": { - "balance": "0x0", - "nonce": "0x1", - "code": "0x01" - }, - "0x1800000000000000000000000000000000000006": { - "balance": "0x0", - "nonce": "0x1", - "code": "0x01" - }, - "0x1800000000000000000000000000000000000007": { - "balance": "0x0", - "nonce": "0x1", - "code": "0x01" - }, - "0x1800000000000000000000000000000000000008": { - "balance": "0x0", - "nonce": "0x1", - "code": "0x01" - }, - "0x1800000000000000000000000000000000000009": { - "balance": "0x0", - "nonce": "0x1", - "code": "0x01" - }, - "0x180000000000000000000000000000000000000a": { - "balance": "0x0", - "nonce": "0x1", - "code": "0x01" - }, - "0x180000000000000000000000000000000000000b": { - "balance": "0x0", - "nonce": "0x1", - "code": "0x01" - }, - "0x180000000000000000000000000000000000000c": { - "balance": "0x0", - "nonce": "0x1", - "code": "0x01" - }, - "0x180000000000000000000000000000000000000d": { - "balance": "0x0", - "nonce": "0x1", - "code": "0x01" - }, - "0x180000000000000000000000000000000000000e": { - "balance": "0x0", - "nonce": "0x1", - "code": "0x01" - }, - "0x180000000000000000000000000000000000000f": { - "balance": "0x0", - "nonce": "0x1", - "code": "0x01" - }, - "0x1800000000000000000000000000000000000010": { - "balance": "0x0", - "nonce": "0x1", - "code": "0x01" - }, - "0x1800000000000000000000000000000000000011": { - "balance": "0x0", - "nonce": "0x1", - "code": "0x01" - }, - "0x1800000000000000000000000000000000000012": { - "balance": "0x0", - "nonce": "0x1", - "code": "0x01" - }, - "0x1800000000000000000000000000000000000013": { - "balance": "0x0", - "nonce": "0x1", - "code": "0x01" - }, - "0x1800000000000000000000000000000000000014": { - "balance": "0x0", - "nonce": "0x1", - "code": "0x01" - }, - "0x1800000000000000000000000000000000000015": { - "balance": "0x0", - "nonce": "0x1", - "code": "0x01" - }, - "0x1800000000000000000000000000000000000016": { - "balance": "0x0", - "nonce": "0x1", - "code": "0x01" - }, - "0x1800000000000000000000000000000000000017": { - "balance": "0x0", - "nonce": "0x1", - "code": "0x01" - }, - "0x1800000000000000000000000000000000000018": { - "balance": "0x0", - "nonce": "0x1", - "code": "0x01" - }, - "0x1800000000000000000000000000000000000019": { - "balance": "0x0", - "nonce": "0x1", - "code": "0x01" - }, - "0x180000000000000000000000000000000000001a": { - "balance": "0x0", - "nonce": "0x1", - "code": "0x01" - }, - "0x180000000000000000000000000000000000001b": { - "balance": "0x0", - "nonce": "0x1", - "code": "0x01" - }, - "0x180000000000000000000000000000000000001c": { - "balance": "0x0", - "nonce": "0x1", - "code": "0x01" - }, - "0x180000000000000000000000000000000000001d": { - "balance": "0x0", - "nonce": "0x1", - "code": "0x01" - }, - "0x180000000000000000000000000000000000001e": { - "balance": "0x0", - "nonce": "0x1", - "code": "0x01" - }, - "0x180000000000000000000000000000000000001f": { - "balance": "0x0", - "nonce": "0x1", - "code": "0x01" - }, - "0x1800000000000000000000000000000000000020": { - "balance": "0x0", - "nonce": "0x1", - "code": "0x01" - }, - "0x1800000000000000000000000000000000000021": { - "balance": "0x0", - "nonce": "0x1", - "code": "0x01" - }, - "0x1800000000000000000000000000000000000022": { - "balance": "0x0", - "nonce": "0x1", - "code": "0x01" - }, - "0x1800000000000000000000000000000000000023": { - "balance": "0x0", - "nonce": "0x1", - "code": "0x01" - }, - "0x1800000000000000000000000000000000000024": { - "balance": "0x0", - "nonce": "0x1", - "code": "0x01" - }, - "0x1800000000000000000000000000000000000025": { - "balance": "0x0", - "nonce": "0x1", - "code": "0x01" - }, - "0x1800000000000000000000000000000000000026": { - "balance": "0x0", - "nonce": "0x1", - "code": "0x01" - }, - "0x1800000000000000000000000000000000000027": { - "balance": "0x0", - "nonce": "0x1", - "code": "0x01" - }, - "0x1800000000000000000000000000000000000028": { - "balance": "0x0", - "nonce": "0x1", - "code": "0x01" - }, - "0x1800000000000000000000000000000000000029": { - "balance": "0x0", - "nonce": "0x1", - "code": "0x01" - }, - "0x180000000000000000000000000000000000002a": { - "balance": "0x0", - "nonce": "0x1", - "code": "0x01" - }, - "0x180000000000000000000000000000000000002b": { - "balance": "0x0", - "nonce": "0x1", - "code": "0x01" - }, - "0x180000000000000000000000000000000000002c": { - "balance": "0x0", - "nonce": "0x1", - "code": "0x01" - }, - "0x180000000000000000000000000000000000002d": { - "balance": "0x0", - "nonce": "0x1", - "code": "0x01" - }, - "0x180000000000000000000000000000000000002e": { - "balance": "0x0", - "nonce": "0x1", - "code": "0x01" - }, - "0x180000000000000000000000000000000000002f": { - "balance": "0x0", - "nonce": "0x1", - "code": "0x01" - }, - "0x1800000000000000000000000000000000000030": { - "balance": "0x0", - "nonce": "0x1", - "code": "0x01" - }, - "0x1800000000000000000000000000000000000031": { - "balance": "0x0", - "nonce": "0x1", - "code": "0x01" - }, - "0x1800000000000000000000000000000000000032": { - "balance": "0x0", - "nonce": "0x1", - "code": "0x01" - }, - "0x1800000000000000000000000000000000000033": { - "balance": "0x0", - "nonce": "0x1", - "code": "0x01" - }, - "0x1800000000000000000000000000000000000034": { - "balance": "0x0", - "nonce": "0x1", - "code": "0x01" - }, - "0x1800000000000000000000000000000000000035": { - "balance": "0x0", - "nonce": "0x1", - "code": "0x01" - }, - "0x1800000000000000000000000000000000000036": { - "balance": "0x0", - "nonce": "0x1", - "code": "0x01" - }, - "0x1800000000000000000000000000000000000037": { - "balance": "0x0", - "nonce": "0x1", - "code": "0x01" - }, - "0x1800000000000000000000000000000000000038": { - "balance": "0x0", - "nonce": "0x1", - "code": "0x01" - }, - "0x1800000000000000000000000000000000000039": { - "balance": "0x0", - "nonce": "0x1", - "code": "0x01" - }, - "0x180000000000000000000000000000000000003a": { - "balance": "0x0", - "nonce": "0x1", - "code": "0x01" - }, - "0x180000000000000000000000000000000000003b": { - "balance": "0x0", - "nonce": "0x1", - "code": "0x01" - }, - "0x180000000000000000000000000000000000003c": { - "balance": "0x0", - "nonce": "0x1", - "code": "0x01" - }, - "0x180000000000000000000000000000000000003d": { - "balance": "0x0", - "nonce": "0x1", - "code": "0x01" - }, - "0x180000000000000000000000000000000000003e": { - "balance": "0x0", - "nonce": "0x1", - "code": "0x01" - }, - "0x180000000000000000000000000000000000003f": { - "balance": "0x0", - "nonce": "0x1", - "code": "0x01" - }, - "0x1800000000000000000000000000000000000040": { - "balance": "0x0", - "nonce": "0x1", - "code": "0x01" - }, - "0x1800000000000000000000000000000000000041": { - "balance": "0x0", - "nonce": "0x1", - "code": "0x01" - }, - "0x1800000000000000000000000000000000000042": { - "balance": "0x0", - "nonce": "0x1", - "code": "0x01" - }, - "0x1800000000000000000000000000000000000043": { - "balance": "0x0", - "nonce": "0x1", - "code": "0x01" - }, - "0x1800000000000000000000000000000000000044": { - "balance": "0x0", - "nonce": "0x1", - "code": "0x01" - }, - "0x1800000000000000000000000000000000000045": { - "balance": "0x0", - "nonce": "0x1", - "code": "0x01" - }, - "0x1800000000000000000000000000000000000046": { - "balance": "0x0", - "nonce": "0x1", - "code": "0x01" - }, - "0x1800000000000000000000000000000000000047": { - "balance": "0x0", - "nonce": "0x1", - "code": "0x01" - }, - "0x1800000000000000000000000000000000000048": { - "balance": "0x0", - "nonce": "0x1", - "code": "0x01" - }, - "0x1800000000000000000000000000000000000049": { - "balance": "0x0", - "nonce": "0x1", - "code": "0x01" - }, - "0x180000000000000000000000000000000000004a": { - "balance": "0x0", - "nonce": "0x1", - "code": "0x01" - }, - "0x180000000000000000000000000000000000004b": { - "balance": "0x0", - "nonce": "0x1", - "code": "0x01" - }, - "0x180000000000000000000000000000000000004c": { - "balance": "0x0", - "nonce": "0x1", - "code": "0x01" - }, - "0x180000000000000000000000000000000000004d": { - "balance": "0x0", - "nonce": "0x1", - "code": "0x01" - }, - "0x180000000000000000000000000000000000004e": { - "balance": "0x0", - "nonce": "0x1", - "code": "0x01" - }, - "0x180000000000000000000000000000000000004f": { - "balance": "0x0", - "nonce": "0x1", - "code": "0x01" - }, - "0x1800000000000000000000000000000000000050": { - "balance": "0x0", - "nonce": "0x1", - "code": "0x01" - }, - "0x1800000000000000000000000000000000000051": { - "balance": "0x0", - "nonce": "0x1", - "code": "0x01" - }, - "0x1800000000000000000000000000000000000052": { - "balance": "0x0", - "nonce": "0x1", - "code": "0x01" - }, - "0x1800000000000000000000000000000000000053": { - "balance": "0x0", - "nonce": "0x1", - "code": "0x01" - }, - "0x1800000000000000000000000000000000000054": { - "balance": "0x0", - "nonce": "0x1", - "code": "0x01" - }, - "0x1800000000000000000000000000000000000055": { - "balance": "0x0", - "nonce": "0x1", - "code": "0x01" - }, - "0x1800000000000000000000000000000000000056": { - "balance": "0x0", - "nonce": "0x1", - "code": "0x01" - }, - "0x1800000000000000000000000000000000000057": { - "balance": "0x0", - "nonce": "0x1", - "code": "0x01" - }, - "0x1800000000000000000000000000000000000058": { - "balance": "0x0", - "nonce": "0x1", - "code": "0x01" - }, - "0x1800000000000000000000000000000000000059": { - "balance": "0x0", - "nonce": "0x1", - "code": "0x01" - }, - "0x180000000000000000000000000000000000005a": { - "balance": "0x0", - "nonce": "0x1", - "code": "0x01" - }, - "0x180000000000000000000000000000000000005b": { - "balance": "0x0", - "nonce": "0x1", - "code": "0x01" - }, - "0x180000000000000000000000000000000000005c": { - "balance": "0x0", - "nonce": "0x1", - "code": "0x01" - }, - "0x180000000000000000000000000000000000005d": { - "balance": "0x0", - "nonce": "0x1", - "code": "0x01" - }, - "0x180000000000000000000000000000000000005e": { - "balance": "0x0", - "nonce": "0x1", - "code": "0x01" - }, - "0x180000000000000000000000000000000000005f": { - "balance": "0x0", - "nonce": "0x1", - "code": "0x01" - }, - "0x1800000000000000000000000000000000000060": { - "balance": "0x0", - "nonce": "0x1", - "code": "0x01" - }, - "0x1800000000000000000000000000000000000061": { - "balance": "0x0", - "nonce": "0x1", - "code": "0x01" - }, - "0x1800000000000000000000000000000000000062": { - "balance": "0x0", - "nonce": "0x1", - "code": "0x01" - }, - "0x1800000000000000000000000000000000000063": { - "balance": "0x0", - "nonce": "0x1", - "code": "0x01" - }, - "0x1800000000000000000000000000000000000064": { - "balance": "0x0", - "nonce": "0x1", - "code": "0x01" - }, - "0x1800000000000000000000000000000000000065": { - "balance": "0x0", - "nonce": "0x1", - "code": "0x01" - }, - "0x1800000000000000000000000000000000000066": { - "balance": "0x0", - "nonce": "0x1", - "code": "0x01" - }, - "0x1800000000000000000000000000000000000067": { - "balance": "0x0", - "nonce": "0x1", - "code": "0x01" - }, - "0x1800000000000000000000000000000000000068": { - "balance": "0x0", - "nonce": "0x1", - "code": "0x01" - }, - "0x1800000000000000000000000000000000000069": { - "balance": "0x0", - "nonce": "0x1", - "code": "0x01" - }, - "0x180000000000000000000000000000000000006a": { - "balance": "0x0", - "nonce": "0x1", - "code": "0x01" - }, - "0x180000000000000000000000000000000000006b": { - "balance": "0x0", - "nonce": "0x1", - "code": "0x01" - }, - "0x180000000000000000000000000000000000006c": { - "balance": "0x0", - "nonce": "0x1", - "code": "0x01" - }, - "0x180000000000000000000000000000000000006d": { - "balance": "0x0", - "nonce": "0x1", - "code": "0x01" - }, - "0x180000000000000000000000000000000000006e": { - "balance": "0x0", - "nonce": "0x1", - "code": "0x01" - }, - "0x180000000000000000000000000000000000006f": { - "balance": "0x0", - "nonce": "0x1", - "code": "0x01" - }, - "0x1800000000000000000000000000000000000070": { - "balance": "0x0", - "nonce": "0x1", - "code": "0x01" - }, - "0x1800000000000000000000000000000000000071": { - "balance": "0x0", - "nonce": "0x1", - "code": "0x01" - }, - "0x1800000000000000000000000000000000000072": { - "balance": "0x0", - "nonce": "0x1", - "code": "0x01" - }, - "0x1800000000000000000000000000000000000073": { - "balance": "0x0", - "nonce": "0x1", - "code": "0x01" - }, - "0x1800000000000000000000000000000000000074": { - "balance": "0x0", - "nonce": "0x1", - "code": "0x01" - }, - "0x1800000000000000000000000000000000000075": { - "balance": "0x0", - "nonce": "0x1", - "code": "0x01" - }, - "0x1800000000000000000000000000000000000076": { - "balance": "0x0", - "nonce": "0x1", - "code": "0x01" - }, - "0x1800000000000000000000000000000000000077": { - "balance": "0x0", - "nonce": "0x1", - "code": "0x01" - }, - "0x1800000000000000000000000000000000000078": { - "balance": "0x0", - "nonce": "0x1", - "code": "0x01" - }, - "0x1800000000000000000000000000000000000079": { - "balance": "0x0", - "nonce": "0x1", - "code": "0x01" - }, - "0x180000000000000000000000000000000000007a": { - "balance": "0x0", - "nonce": "0x1", - "code": "0x01" - }, - "0x180000000000000000000000000000000000007b": { - "balance": "0x0", - "nonce": "0x1", - "code": "0x01" - }, - "0x180000000000000000000000000000000000007c": { - "balance": "0x0", - "nonce": "0x1", - "code": "0x01" - }, - "0x180000000000000000000000000000000000007d": { - "balance": "0x0", - "nonce": "0x1", - "code": "0x01" - }, - "0x180000000000000000000000000000000000007e": { - "balance": "0x0", - "nonce": "0x1", - "code": "0x01" - }, - "0x180000000000000000000000000000000000007f": { - "balance": "0x0", - "nonce": "0x1", - "code": "0x01" - }, - "0x1800000000000000000000000000000000000080": { - "balance": "0x0", - "nonce": "0x1", - "code": "0x01" - }, - "0x1800000000000000000000000000000000000081": { - "balance": "0x0", - "nonce": "0x1", - "code": "0x01" - }, - "0x1800000000000000000000000000000000000082": { - "balance": "0x0", - "nonce": "0x1", - "code": "0x01" - }, - "0x1800000000000000000000000000000000000083": { - "balance": "0x0", - "nonce": "0x1", - "code": "0x01" - }, - "0x1800000000000000000000000000000000000084": { - "balance": "0x0", - "nonce": "0x1", - "code": "0x01" - }, - "0x1800000000000000000000000000000000000085": { - "balance": "0x0", - "nonce": "0x1", - "code": "0x01" - }, - "0x1800000000000000000000000000000000000086": { - "balance": "0x0", - "nonce": "0x1", - "code": "0x01" - }, - "0x1800000000000000000000000000000000000087": { - "balance": "0x0", - "nonce": "0x1", - "code": "0x01" - }, - "0x1800000000000000000000000000000000000088": { - "balance": "0x0", - "nonce": "0x1", - "code": "0x01" - }, - "0x1800000000000000000000000000000000000089": { - "balance": "0x0", - "nonce": "0x1", - "code": "0x01" - }, - "0x180000000000000000000000000000000000008a": { - "balance": "0x0", - "nonce": "0x1", - "code": "0x01" - }, - "0x180000000000000000000000000000000000008b": { - "balance": "0x0", - "nonce": "0x1", - "code": "0x01" - }, - "0x180000000000000000000000000000000000008c": { - "balance": "0x0", - "nonce": "0x1", - "code": "0x01" - }, - "0x180000000000000000000000000000000000008d": { - "balance": "0x0", - "nonce": "0x1", - "code": "0x01" - }, - "0x180000000000000000000000000000000000008e": { - "balance": "0x0", - "nonce": "0x1", - "code": "0x01" - }, - "0x180000000000000000000000000000000000008f": { - "balance": "0x0", - "nonce": "0x1", - "code": "0x01" - }, - "0x1800000000000000000000000000000000000090": { - "balance": "0x0", - "nonce": "0x1", - "code": "0x01" - }, - "0x1800000000000000000000000000000000000091": { - "balance": "0x0", - "nonce": "0x1", - "code": "0x01" - }, - "0x1800000000000000000000000000000000000092": { - "balance": "0x0", - "nonce": "0x1", - "code": "0x01" - }, - "0x1800000000000000000000000000000000000093": { - "balance": "0x0", - "nonce": "0x1", - "code": "0x01" - }, - "0x1800000000000000000000000000000000000094": { - "balance": "0x0", - "nonce": "0x1", - "code": "0x01" - }, - "0x1800000000000000000000000000000000000095": { - "balance": "0x0", - "nonce": "0x1", - "code": "0x01" - }, - "0x1800000000000000000000000000000000000096": { - "balance": "0x0", - "nonce": "0x1", - "code": "0x01" - }, - "0x1800000000000000000000000000000000000097": { - "balance": "0x0", - "nonce": "0x1", - "code": "0x01" - }, - "0x1800000000000000000000000000000000000098": { - "balance": "0x0", - "nonce": "0x1", - "code": "0x01" - }, - "0x1800000000000000000000000000000000000099": { - "balance": "0x0", - "nonce": "0x1", - "code": "0x01" - }, - "0x180000000000000000000000000000000000009a": { - "balance": "0x0", - "nonce": "0x1", - "code": "0x01" - }, - "0x180000000000000000000000000000000000009b": { - "balance": "0x0", - "nonce": "0x1", - "code": "0x01" - }, - "0x180000000000000000000000000000000000009c": { - "balance": "0x0", - "nonce": "0x1", - "code": "0x01" - }, - "0x180000000000000000000000000000000000009d": { - "balance": "0x0", - "nonce": "0x1", - "code": "0x01" - }, - "0x180000000000000000000000000000000000009e": { - "balance": "0x0", - "nonce": "0x1", - "code": "0x01" - }, - "0x180000000000000000000000000000000000009f": { - "balance": "0x0", - "nonce": "0x1", - "code": "0x01" - }, - "0x18000000000000000000000000000000000000a0": { - "balance": "0x0", - "nonce": "0x1", - "code": "0x01" - }, - "0x18000000000000000000000000000000000000a1": { - "balance": "0x0", - "nonce": "0x1", - "code": "0x01" - }, - "0x18000000000000000000000000000000000000a2": { - "balance": "0x0", - "nonce": "0x1", - "code": "0x01" - }, - "0x18000000000000000000000000000000000000a3": { - "balance": "0x0", - "nonce": "0x1", - "code": "0x01" - }, - "0x18000000000000000000000000000000000000a4": { - "balance": "0x0", - "nonce": "0x1", - "code": "0x01" - }, - "0x18000000000000000000000000000000000000a5": { - "balance": "0x0", - "nonce": "0x1", - "code": "0x01" - }, - "0x18000000000000000000000000000000000000a6": { - "balance": "0x0", - "nonce": "0x1", - "code": "0x01" - }, - "0x18000000000000000000000000000000000000a7": { - "balance": "0x0", - "nonce": "0x1", - "code": "0x01" - }, - "0x18000000000000000000000000000000000000a8": { - "balance": "0x0", - "nonce": "0x1", - "code": "0x01" - }, - "0x18000000000000000000000000000000000000a9": { - "balance": "0x0", - "nonce": "0x1", - "code": "0x01" - }, - "0x18000000000000000000000000000000000000aa": { - "balance": "0x0", - "nonce": "0x1", - "code": "0x01" - }, - "0x18000000000000000000000000000000000000ab": { - "balance": "0x0", - "nonce": "0x1", - "code": "0x01" - }, - "0x18000000000000000000000000000000000000ac": { - "balance": "0x0", - "nonce": "0x1", - "code": "0x01" - }, - "0x18000000000000000000000000000000000000ad": { - "balance": "0x0", - "nonce": "0x1", - "code": "0x01" - }, - "0x18000000000000000000000000000000000000ae": { - "balance": "0x0", - "nonce": "0x1", - "code": "0x01" - }, - "0x18000000000000000000000000000000000000af": { - "balance": "0x0", - "nonce": "0x1", - "code": "0x01" - }, - "0x18000000000000000000000000000000000000b0": { - "balance": "0x0", - "nonce": "0x1", - "code": "0x01" - }, - "0x18000000000000000000000000000000000000b1": { - "balance": "0x0", - "nonce": "0x1", - "code": "0x01" - }, - "0x18000000000000000000000000000000000000b2": { - "balance": "0x0", - "nonce": "0x1", - "code": "0x01" - }, - "0x18000000000000000000000000000000000000b3": { - "balance": "0x0", - "nonce": "0x1", - "code": "0x01" - }, - "0x18000000000000000000000000000000000000b4": { - "balance": "0x0", - "nonce": "0x1", - "code": "0x01" - }, - "0x18000000000000000000000000000000000000b5": { - "balance": "0x0", - "nonce": "0x1", - "code": "0x01" - }, - "0x18000000000000000000000000000000000000b6": { - "balance": "0x0", - "nonce": "0x1", - "code": "0x01" - }, - "0x18000000000000000000000000000000000000b7": { - "balance": "0x0", - "nonce": "0x1", - "code": "0x01" - }, - "0x18000000000000000000000000000000000000b8": { - "balance": "0x0", - "nonce": "0x1", - "code": "0x01" - }, - "0x18000000000000000000000000000000000000b9": { - "balance": "0x0", - "nonce": "0x1", - "code": "0x01" - }, - "0x18000000000000000000000000000000000000ba": { - "balance": "0x0", - "nonce": "0x1", - "code": "0x01" - }, - "0x18000000000000000000000000000000000000bb": { - "balance": "0x0", - "nonce": "0x1", - "code": "0x01" - }, - "0x18000000000000000000000000000000000000bc": { - "balance": "0x0", - "nonce": "0x1", - "code": "0x01" - }, - "0x18000000000000000000000000000000000000bd": { - "balance": "0x0", - "nonce": "0x1", - "code": "0x01" - }, - "0x18000000000000000000000000000000000000be": { - "balance": "0x0", - "nonce": "0x1", - "code": "0x01" - }, - "0x18000000000000000000000000000000000000bf": { - "balance": "0x0", - "nonce": "0x1", - "code": "0x01" - }, - "0x18000000000000000000000000000000000000c0": { - "balance": "0x0", - "nonce": "0x1", - "code": "0x01" - }, - "0x18000000000000000000000000000000000000c1": { - "balance": "0x0", - "nonce": "0x1", - "code": "0x01" - }, - "0x18000000000000000000000000000000000000c2": { - "balance": "0x0", - "nonce": "0x1", - "code": "0x01" - }, - "0x18000000000000000000000000000000000000c3": { - "balance": "0x0", - "nonce": "0x1", - "code": "0x01" - }, - "0x18000000000000000000000000000000000000c4": { - "balance": "0x0", - "nonce": "0x1", - "code": "0x01" - }, - "0x18000000000000000000000000000000000000c5": { - "balance": "0x0", - "nonce": "0x1", - "code": "0x01" - }, - "0x18000000000000000000000000000000000000c6": { - "balance": "0x0", - "nonce": "0x1", - "code": "0x01" - }, - "0x18000000000000000000000000000000000000c7": { - "balance": "0x0", - "nonce": "0x1", - "code": "0x01" - }, - "0x18000000000000000000000000000000000000c8": { - "balance": "0x0", - "nonce": "0x1", - "code": "0x01" - }, - "0x18000000000000000000000000000000000000c9": { - "balance": "0x0", - "nonce": "0x1", - "code": "0x01" - }, - "0x18000000000000000000000000000000000000ca": { - "balance": "0x0", - "nonce": "0x1", - "code": "0x01" - }, - "0x18000000000000000000000000000000000000cb": { - "balance": "0x0", - "nonce": "0x1", - "code": "0x01" - }, - "0x18000000000000000000000000000000000000cc": { - "balance": "0x0", - "nonce": "0x1", - "code": "0x01" - }, - "0x18000000000000000000000000000000000000cd": { - "balance": "0x0", - "nonce": "0x1", - "code": "0x01" - }, - "0x18000000000000000000000000000000000000ce": { - "balance": "0x0", - "nonce": "0x1", - "code": "0x01" - }, - "0x18000000000000000000000000000000000000cf": { - "balance": "0x0", - "nonce": "0x1", - "code": "0x01" - }, - "0x18000000000000000000000000000000000000d0": { - "balance": "0x0", - "nonce": "0x1", - "code": "0x01" - }, - "0x18000000000000000000000000000000000000d1": { - "balance": "0x0", - "nonce": "0x1", - "code": "0x01" - }, - "0x18000000000000000000000000000000000000d2": { - "balance": "0x0", - "nonce": "0x1", - "code": "0x01" - }, - "0x18000000000000000000000000000000000000d3": { - "balance": "0x0", - "nonce": "0x1", - "code": "0x01" - }, - "0x18000000000000000000000000000000000000d4": { - "balance": "0x0", - "nonce": "0x1", - "code": "0x01" - }, - "0x18000000000000000000000000000000000000d5": { - "balance": "0x0", - "nonce": "0x1", - "code": "0x01" - }, - "0x18000000000000000000000000000000000000d6": { - "balance": "0x0", - "nonce": "0x1", - "code": "0x01" - }, - "0x18000000000000000000000000000000000000d7": { - "balance": "0x0", - "nonce": "0x1", - "code": "0x01" - }, - "0x18000000000000000000000000000000000000d8": { - "balance": "0x0", - "nonce": "0x1", - "code": "0x01" - }, - "0x18000000000000000000000000000000000000d9": { - "balance": "0x0", - "nonce": "0x1", - "code": "0x01" - }, - "0x18000000000000000000000000000000000000da": { - "balance": "0x0", - "nonce": "0x1", - "code": "0x01" - }, - "0x18000000000000000000000000000000000000db": { - "balance": "0x0", - "nonce": "0x1", - "code": "0x01" - }, - "0x18000000000000000000000000000000000000dc": { - "balance": "0x0", - "nonce": "0x1", - "code": "0x01" - }, - "0x18000000000000000000000000000000000000dd": { - "balance": "0x0", - "nonce": "0x1", - "code": "0x01" - }, - "0x18000000000000000000000000000000000000de": { - "balance": "0x0", - "nonce": "0x1", - "code": "0x01" - }, - "0x18000000000000000000000000000000000000df": { - "balance": "0x0", - "nonce": "0x1", - "code": "0x01" - }, - "0x18000000000000000000000000000000000000e0": { - "balance": "0x0", - "nonce": "0x1", - "code": "0x01" - }, - "0x18000000000000000000000000000000000000e1": { - "balance": "0x0", - "nonce": "0x1", - "code": "0x01" - }, - "0x18000000000000000000000000000000000000e2": { - "balance": "0x0", - "nonce": "0x1", - "code": "0x01" - }, - "0x18000000000000000000000000000000000000e3": { - "balance": "0x0", - "nonce": "0x1", - "code": "0x01" - }, - "0x18000000000000000000000000000000000000e4": { - "balance": "0x0", - "nonce": "0x1", - "code": "0x01" - }, - "0x18000000000000000000000000000000000000e5": { - "balance": "0x0", - "nonce": "0x1", - "code": "0x01" - }, - "0x18000000000000000000000000000000000000e6": { - "balance": "0x0", - "nonce": "0x1", - "code": "0x01" - }, - "0x18000000000000000000000000000000000000e7": { - "balance": "0x0", - "nonce": "0x1", - "code": "0x01" - }, - "0x18000000000000000000000000000000000000e8": { - "balance": "0x0", - "nonce": "0x1", - "code": "0x01" - }, - "0x18000000000000000000000000000000000000e9": { - "balance": "0x0", - "nonce": "0x1", - "code": "0x01" - }, - "0x18000000000000000000000000000000000000ea": { - "balance": "0x0", - "nonce": "0x1", - "code": "0x01" - }, - "0x18000000000000000000000000000000000000eb": { - "balance": "0x0", - "nonce": "0x1", - "code": "0x01" - }, - "0x18000000000000000000000000000000000000ec": { - "balance": "0x0", - "nonce": "0x1", - "code": "0x01" - }, - "0x18000000000000000000000000000000000000ed": { - "balance": "0x0", - "nonce": "0x1", - "code": "0x01" - }, - "0x18000000000000000000000000000000000000ee": { - "balance": "0x0", - "nonce": "0x1", - "code": "0x01" - }, - "0x18000000000000000000000000000000000000ef": { - "balance": "0x0", - "nonce": "0x1", - "code": "0x01" - }, - "0x18000000000000000000000000000000000000f0": { - "balance": "0x0", - "nonce": "0x1", - "code": "0x01" - }, - "0x18000000000000000000000000000000000000f1": { - "balance": "0x0", - "nonce": "0x1", - "code": "0x01" - }, - "0x18000000000000000000000000000000000000f2": { - "balance": "0x0", - "nonce": "0x1", - "code": "0x01" - }, - "0x18000000000000000000000000000000000000f3": { - "balance": "0x0", - "nonce": "0x1", - "code": "0x01" - }, - "0x18000000000000000000000000000000000000f4": { - "balance": "0x0", - "nonce": "0x1", - "code": "0x01" - }, - "0x18000000000000000000000000000000000000f5": { - "balance": "0x0", - "nonce": "0x1", - "code": "0x01" - }, - "0x18000000000000000000000000000000000000f6": { - "balance": "0x0", - "nonce": "0x1", - "code": "0x01" - }, - "0x18000000000000000000000000000000000000f7": { - "balance": "0x0", - "nonce": "0x1", - "code": "0x01" - }, - "0x18000000000000000000000000000000000000f8": { - "balance": "0x0", - "nonce": "0x1", - "code": "0x01" - }, - "0x18000000000000000000000000000000000000f9": { - "balance": "0x0", - "nonce": "0x1", - "code": "0x01" - }, - "0x18000000000000000000000000000000000000fa": { - "balance": "0x0", - "nonce": "0x1", - "code": "0x01" - }, - "0x18000000000000000000000000000000000000fb": { - "balance": "0x0", - "nonce": "0x1", - "code": "0x01" - }, - "0x18000000000000000000000000000000000000fc": { - "balance": "0x0", - "nonce": "0x1", - "code": "0x01" - }, - "0x18000000000000000000000000000000000000fd": { - "balance": "0x0", - "nonce": "0x1", - "code": "0x01" - }, - "0x18000000000000000000000000000000000000fe": { - "balance": "0x0", - "nonce": "0x1", - "code": "0x01" - }, - "0x18000000000000000000000000000000000000ff": { - "balance": "0x0", - "nonce": "0x1", - "code": "0x01" - }, - "0x49f78af090F1f98e7184B7f61f1F1a8a8064b40d": { - "balance": "0x3635c9adc5dea00000" - }, - "0xDC29Bab4A7d5425cA44eeF20a5B67E3D897F9a03": { - "balance": "0x3635c9adc5dea00000" - }, - "0xbc639a0A060E5831a7c437b491B8d3C1f58F554e": { - "balance": "0x3635c9adc5dea00000" - }, - "0xc7EdB7223a08a11D05cbE83F436816F1dc7Ea541": { - "balance": "0x3635c9adc5dea00000" - }, - "0x28E2716951f9fC8dF918e1BeDB54E4e2BDB2a362": { - "balance": "0x3635c9adc5dea00000" - }, - "0xE29672dcF004AE74da3700262D384ce23de16729": { - "balance": "0x3635c9adc5dea00000" - }, - "0x735A7918f97Fbd3Cb5A5f72B0D1dDfdc9a7D8A86": { - "balance": "0x3635c9adc5dea00000" - }, - "0xD2C1210Ce932A416e1f9f3147E404d37696D0c41": { - "balance": "0x3635c9adc5dea00000" - }, - "0x2904f3A8226630b40cD5BF414fF5c112E2A0aAC6": { - "balance": "0x3635c9adc5dea00000" - }, - "0x5196FF0e6f24ca69d07539A0547fD84988D29feE": { - "balance": "0x3635c9adc5dea00000" - }, - "0xe09f434194E5430301F8D80fD1B7676A1052DF47": { - "balance": "0x3635c9adc5dea00000" - }, - "0x605fa3FeF59b7bA218c910E6671Ce88CB9D2F1bf": { - "balance": "0x3635c9adc5dea00000" - }, - "0x456f90Eb4023Bcc821C1a9164046C1Bd92898696": { - "balance": "0x3635c9adc5dea00000" - }, - "0x5f5dC64f8926Dd61C57812565D308c04Be68546A": { - "balance": "0x3635c9adc5dea00000" - }, - "0xB8217B0eFa254e56f902DB6cCA2f1Cd88E5251d5": { - "balance": "0x3635c9adc5dea00000" - }, - "0x5F3b933Bb7637944683eE6CECE4a24C8E1205c42": { - "balance": "0x3635c9adc5dea00000" - }, - "0xca1e629E0652bbf5784c2b8D0B45723Bd61D5FEE": { - "balance": "0x3635c9adc5dea00000" - }, - "0x6A34bC9F35a753A946D1829082E804980f8F35D3": { - "balance": "0x3635c9adc5dea00000" - }, - "0x18965Fff34CaFD729159E761f552915D85D6cE79": { - "balance": "0x3635c9adc5dea00000" - }, - "0xa1C4230035EA81ECE502a1D316910C6d60b04cE8": { - "balance": "0x3635c9adc5dea00000" - }, - "0x749Deb48FE74bbbC3aADeEaF19ED03F3f39e5ce4": { - "balance": "0x3635c9adc5dea00000" - }, - "0x0E5BeEEC3eBCF2b6FF35b6eB7D55fB775D8C7264": { - "balance": "0x3635c9adc5dea00000" - }, - "0x030eefff3aE6e1d3b04B16d1AC7df4498dC29317": { - "balance": "0x3635c9adc5dea00000" - }, - "0xE3A331A13d27B1E63Be687b9b773b4e362823ff0": { - "balance": "0x3635c9adc5dea00000" - }, - "0x0799c3ac9FB9c7BD89979D63A664Bf10Eaf008FB": { - "balance": "0x3635c9adc5dea00000" - }, - "0x1800000000000000000000000000000000000000": { - "balance": "0x0", - "nonce": "0x1", - "code": "0x01", - "storage": { - "0x0000000000000000000000000000000000000000000000000000000000000001": "0x0000000000000000000000003600000000000000000000000000000000000000", - "0x0000000000000000000000000000000000000000000000000000000000000002": "0x00000000000000000000000000000000000000000000054b40b1f852bda00000" - } - }, - "0x1800000000000000000000000000000000000001": { - "balance": "0x0", - "nonce": "0x1", - "code": "0x01", - "storage": { - "0x0000000000000000000000000000000000000000000000000000000000000001": "0x0000000000000000000000003600000000000000000000000000000000000000" - } - } - } -} diff --git a/buf.yaml b/buf.yaml deleted file mode 100644 index 8c8c422..0000000 --- a/buf.yaml +++ /dev/null @@ -1,40 +0,0 @@ -# Copyright 2025 Circle Internet Group, Inc. All rights reserved. -# -# SPDX-License-Identifier: Apache-2.0 -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. - -version: v2 -modules: - - path: crates/types/proto - name: buf.build/arc/types - lint: - use: - - STANDARD - except: - - ENUM_ZERO_VALUE_SUFFIX - breaking: - use: - - FILE - - path: crates/remote-signer/proto - name: buf.build/arc/signer - lint: - use: - - STANDARD - breaking: - use: - - FILE - ignore_only: - # Allow changing/removing go_package; this repo generates Rust only (tonic), not Go. - FILE_SAME_GO_PACKAGE: - - crates/remote-signer/proto/arc/signer/v1/signer.proto diff --git a/clippy.toml b/clippy.toml deleted file mode 100644 index 154626e..0000000 --- a/clippy.toml +++ /dev/null @@ -1 +0,0 @@ -allow-unwrap-in-tests = true diff --git a/contracts/.gitignore b/contracts/.gitignore deleted file mode 100644 index a4f97d8..0000000 --- a/contracts/.gitignore +++ /dev/null @@ -1,17 +0,0 @@ -node_modules -.env - -# Hardhat or forge files -/cache -/artifacts - -# TypeChain files -/typechain -/typechain-types - -# solidity-coverage files -/coverage -/coverage.json - -# Foundry local deployed folder -/out diff --git a/contracts/README.md b/contracts/README.md deleted file mode 100644 index d158c99..0000000 --- a/contracts/README.md +++ /dev/null @@ -1,92 +0,0 @@ -# Arc Contracts - -This directory contains Solidity contracts and tests for the Arc project, built with Foundry. - -## Compiler choice for genesis-deployed contracts - -**Forge is the canonical compiler** for every CREATE2-deployed contract in Arc genesis -(`Memo`, `Multicall3From`, `Denylist` impl, `ProtocolConfig` impl, `ValidatorRegistry` -impl, `PermissionedValidatorManager` impl, `GasGuzzler`, `TestToken`). - -The genesis builder (`contracts/scripts/ArtifactHelper.s.sol`), all CREATE2-sensitive -tests (`tests/localdev/genesis.test.ts`) all read from `contracts/out/forge/`. -Hardhat's compile output is **not** consumed for any CREATE2-sensitive path. - -## Foundry - -**Foundry is a blazing fast, portable and modular toolkit for Ethereum application development written in Rust.** - -Foundry consists of: - -- **Forge**: Ethereum testing framework (like Truffle, Hardhat and DappTools). -- **Cast**: Swiss army knife for interacting with EVM smart contracts, sending transactions and getting chain data. -- **Anvil**: Local Ethereum node, akin to Ganache, Hardhat Network. -- **Chisel**: Fast, utilitarian, and verbose solidity REPL. - -## Documentation - -https://book.getfoundry.sh/ - -## Usage - -### Unit Testing (Current Directory) - -#### Build Contracts -```shell -$ forge build -``` - -#### Run All Unit Tests -```shell -$ forge test -``` - -#### Run Specific Test Contract -```shell -$ forge test --match-contract -``` - -#### Run Tests with Verbose Output -```shell -$ forge test -v -``` - -#### Run Tests with Gas Reports -```shell -$ forge test --gas-report -``` - -#### Format Code -```shell -$ forge fmt -``` - -#### Gas Snapshots -```shell -$ forge snapshot -``` - -### Development Tools - -#### Local Development Node -```shell -$ anvil -``` - -#### Interact with Contracts -```shell -$ cast -``` - -#### Deploy Contracts (if needed) -```shell -$ forge script script/Deploy.s.sol:DeployScript --rpc-url --private-key -``` - -### Help - -```shell -$ forge --help -$ anvil --help -$ cast --help -``` \ No newline at end of file diff --git a/contracts/lib/forge-std b/contracts/lib/forge-std deleted file mode 160000 index c7be2a3..0000000 --- a/contracts/lib/forge-std +++ /dev/null @@ -1 +0,0 @@ -Subproject commit c7be2a3481f9e51230880bb0949072c7e3a4da82 diff --git a/contracts/lib/openzeppelin-contracts b/contracts/lib/openzeppelin-contracts deleted file mode 160000 index c64a1ed..0000000 --- a/contracts/lib/openzeppelin-contracts +++ /dev/null @@ -1 +0,0 @@ -Subproject commit c64a1edb67b6e3f4a15cca8909c9482ad33a02b0 diff --git a/contracts/lib/openzeppelin-contracts-upgradeable b/contracts/lib/openzeppelin-contracts-upgradeable deleted file mode 160000 index e725abd..0000000 --- a/contracts/lib/openzeppelin-contracts-upgradeable +++ /dev/null @@ -1 +0,0 @@ -Subproject commit e725abddf1e01cf05ace496e950fc8e243cc7cab diff --git a/contracts/scripts/Addresses.sol b/contracts/scripts/Addresses.sol deleted file mode 100644 index a051475..0000000 --- a/contracts/scripts/Addresses.sol +++ /dev/null @@ -1,50 +0,0 @@ -// Copyright 2025 Circle Internet Group, Inc. All rights reserved. -// -// SPDX-License-Identifier: Apache-2.0 -// -// Licensed under the Apache License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. -// You may obtain a copy of the License at -// -// http://www.apache.org/licenses/LICENSE-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// See the License for the specific language governing permissions and -// limitations under the License. - -pragma solidity ^0.8.29; - -/** - * @title Addresses - * @notice Shared constants for system contract addresses - */ -library Addresses { - // ============ System Contracts ============ - address internal constant FIAT_TOKEN_PROXY = 0x3600000000000000000000000000000000000000; - address internal constant PROTOCOL_CONFIG = 0x3600000000000000000000000000000000000001; - address internal constant VALIDATOR_REGISTRY = 0x3600000000000000000000000000000000000002; - address internal constant PERMISSIONED_MANAGER = 0x3600000000000000000000000000000000000003; - - // ============ Precompiles ============ - address internal constant NATIVE_COIN_AUTHORITY = 0x1800000000000000000000000000000000000000; - address internal constant NATIVE_COIN_CONTROL = 0x1800000000000000000000000000000000000001; - address internal constant SYSTEM_ACCOUNTING = 0x1800000000000000000000000000000000000002; - address internal constant CALL_FROM = 0x1800000000000000000000000000000000000003; - - // ============ Predeployed Contracts ============ - address internal constant DETERMINISTIC_DEPLOYER_PROXY = 0x4e59b44847b379578588920cA78FbF26c0B4956C; - address internal constant MULTICALL3 = 0xcA11bde05977b3631167028862bE2a173976CA11; - address internal constant MULTICALL3_FROM = 0x522fAf9A91c41c443c66765030741e4AaCe147D0; - address internal constant MEMO = 0x5294E9927c3306DcBaDb03fe70b92e01cCede505; - - // ============ Helpers ============ - - function _hasSystemAddressPrefix(address account) internal pure returns (bool) { - // The Arc system address range starts at 0x3600...0000. - // Addresses are 160 bits wide; we isolate the top 12 bits by right-shifting - // by 148 (= 160 - 12) and comparing against 0x360. - return (uint160(account) >> 148) == 0x360; - } -} diff --git a/contracts/scripts/ArtifactHelper.s.sol b/contracts/scripts/ArtifactHelper.s.sol deleted file mode 100644 index a0644ab..0000000 --- a/contracts/scripts/ArtifactHelper.s.sol +++ /dev/null @@ -1,314 +0,0 @@ -// Copyright 2026 Circle Internet Group, Inc. All rights reserved. -// -// SPDX-License-Identifier: Apache-2.0 -// -// Licensed under the Apache License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. -// You may obtain a copy of the License at -// -// http://www.apache.org/licenses/LICENSE-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// See the License for the specific language governing permissions and -// limitations under the License. - -pragma solidity ^0.8.29; - -// ArtifactHelper — genesis builder entrypoint. Reads compiled contract bytecode and -// simulates CREATE2 deployments to produce the `alloc` entries baked into genesis.json. -// -// Forge is canonical for all CREATE2-deployed genesis contracts — do NOT switch this -// to read Hardhat's output. -import {Script, console} from "forge-std/Script.sol"; -import {stdJson} from "forge-std/StdJson.sol"; - -import {AdminUpgradeableProxy} from "../src/proxy/AdminUpgradeableProxy.sol"; -import {ProtocolConfig} from "../src/protocol-config/ProtocolConfig.sol"; -import {ValidatorRegistry} from "../src/validator-manager/ValidatorRegistry.sol"; -import {PermissionedValidatorManager} from "../src/validator-manager/PermissionedValidatorManager.sol"; - -contract ArtifactHelper is Script { - using stdJson for string; - - address constant DETERMINISTIC_DEPLOYMENT_PROXY = address(0x4e59b44847b379578588920cA78FbF26c0B4956C); - string manifest; - string[] externalContracts = [ - "DeterministicDeploymentProxy", - "Multicall3", - "BlockHashHistory", - "Permit2" - ]; - - enum DeploymentType { - deterministic, - oneTimeAddress - } - - function loadManifest(string memory path) public returns (string memory) { - manifest = vm.readFile(string.concat(vm.projectRoot(), "/", path)); - return manifest; - } - - function getDeploymentType(string memory contractName) public view returns (DeploymentType) { - string memory prefix = string.concat(".", contractName); - string memory typeStr = vm.parseJsonString(manifest, string.concat(prefix, ".type")); - if (keccak256(bytes(typeStr)) == keccak256(bytes("deterministic"))) { - return DeploymentType.deterministic; - } else { - return DeploymentType.oneTimeAddress; - } - } - - struct OneTimeAddressDeployment { - address deployer; - uint256 deployerBalance; - bytes rawTransaction; - address addr; - bytes32 ethCodeHash; - } - - function loadOneTimeAddressDeployment(string memory contractName) public view returns (OneTimeAddressDeployment memory) { - OneTimeAddressDeployment memory deployment; - string memory prefix = string.concat(".", contractName); - deployment.addr = manifest.readAddress(string.concat(prefix, ".address")); - deployment.deployer = manifest.readAddress(string.concat(prefix, ".deployer")); - deployment.deployerBalance = manifest.readUint(string.concat(prefix, ".deployerBalance")); - deployment.rawTransaction = manifest.readBytes(string.concat(prefix, ".rawTransaction")); - deployment.ethCodeHash = manifest.readBytes32(string.concat(prefix, ".ethCodeHash")); - return deployment; - } - - struct DeterministicDeployment { - address addr; - bytes32 salt; - bytes32 ethCodeHash; - bytes bytecode; - } - - function loadBytecode(string memory filePath, string memory selector) public view returns (bytes memory) { - string memory artifact = vm.readFile(string.concat(vm.projectRoot(), "/", filePath)); - return artifact.readBytes(selector); - } - - struct LinkReplacement { - string placeholder; - address addr; - } - - function loadBytecode(string memory filePath, string memory selector, LinkReplacement[] memory linkReplacements) public view returns (bytes memory) { - string memory artifact = vm.readFile(string.concat(vm.projectRoot(), "/", filePath)); - string memory bytecodeStr = artifact.readString(selector); - - for (uint256 i = 0; i < linkReplacements.length; i++) { - string memory addrHex = vm.toString(linkReplacements[i].addr); - addrHex = vm.replace(addrHex, "0x", ""); - bytecodeStr = vm.replace(bytecodeStr, linkReplacements[i].placeholder, addrHex); - } - bytecodeStr = string.concat("{\"x\":\"", bytecodeStr, "\"}"); - return bytecodeStr.readBytes(".x"); - } - - function loadDeterministicDeployment(string memory contractName) public view returns (DeterministicDeployment memory) { - DeterministicDeployment memory deployment; - string memory prefix = string.concat(".", contractName); - deployment.addr = manifest.readAddress(string.concat(prefix, ".address")); - deployment.salt = manifest.readBytes32Or(string.concat(prefix, ".salt"), bytes32(0)); - deployment.ethCodeHash = manifest.readBytes32Or(string.concat(prefix, ".ethCodeHash"), bytes32(0)); - string memory filePath = manifest.readString(string.concat(prefix, ".bytecode.file")); - string memory selector = manifest.readString(string.concat(prefix, ".bytecode.selector")); - deployment.bytecode = loadBytecode(filePath, selector); - return deployment; - } - - function deployDeterministicContract(bytes memory bytecode) public returns (address) { - bytes32 salt = bytes32(0); - return deployDeterministicContract(bytecode, salt); - } - - function deployDeterministicContract(bytes memory bytecode, bytes32 salt) public returns (address) { - (bool success, bytes memory result) = DETERMINISTIC_DEPLOYMENT_PROXY.call( - abi.encodePacked(salt, bytecode)); - require(success, "Deployment failed"); - return address(bytes20(result)); - } - - function deployDeterministicContract(DeterministicDeployment memory deployment) public returns (address) { - address deployedAddr = deployDeterministicContract(deployment.bytecode, deployment.salt); - require(deployedAddr == deployment.addr, "deployed address mismatch"); - return deployment.addr; - } - - function deployOneTimeAddressContract(OneTimeAddressDeployment memory deployment) public returns (address) { - // Forge env already provide same determinisic deployment proxy, remove it to deploy again. - vm.etch(deployment.addr, hex""); - vm.resetNonce(deployment.addr); - vm.resetNonce(deployment.deployer); - - vm.deal(deployment.deployer, deployment.deployerBalance); - vm.broadcastRawTransaction(deployment.rawTransaction); - - require(deployment.addr.code.length > 0, "deployed code length mismatch"); - return deployment.addr; - } - - function getJsonContractCode(address contractAddr) internal returns (string memory) { - vm.serializeString("forgeContractCode", "address", vm.toString(contractAddr)); - return vm.serializeString("forgeContractCode", "code", vm.toString(contractAddr.code)); - } - - function deployArcNetworkContracts(string memory arcNetworkContractDir, address validatorRegistryProxyAddr) internal returns (string memory) { - // Reads Forge's flat `.sol/.json` layout via `.bytecode.object`. - // ProtocolConfig - address protocolConfig = deployDeterministicContract( - loadBytecode( - string.concat(arcNetworkContractDir, "ProtocolConfig.sol/ProtocolConfig.json"), - ".bytecode.object" - ) - ); - vm.serializeString("output", "ProtocolConfig", getJsonContractCode(address(protocolConfig))); - - // Denylist - address denylist = deployDeterministicContract( - loadBytecode( - string.concat(arcNetworkContractDir, "Denylist.sol/Denylist.json"), - ".bytecode.object" - ) - ); - vm.serializeString("output", "Denylist", getJsonContractCode(address(denylist))); - - // ValidatorRegistry - address validatorRegistry = deployDeterministicContract( - loadBytecode( - string.concat(arcNetworkContractDir, "ValidatorRegistry.sol/ValidatorRegistry.json"), - ".bytecode.object" - ) - ); - vm.serializeString("output", "ValidatorRegistry", getJsonContractCode(address(validatorRegistry))); - - // AdminUpgradeableProxy - address proxy = deployDeterministicContract( - bytes.concat( - loadBytecode( - string.concat(arcNetworkContractDir, "AdminUpgradeableProxy.sol/AdminUpgradeableProxy.json"), - ".bytecode.object" - ), - abi.encode(address(validatorRegistry), address(0x0000000000000000000000000000000000000001), hex"") - ) - ); - vm.serializeString("output", "AdminUpgradeableProxy", getJsonContractCode(address(proxy))); - - // PermissionedValidatorManager - address poaManager = deployDeterministicContract( - bytes.concat( - loadBytecode( - string.concat(arcNetworkContractDir, "PermissionedValidatorManager.sol/PermissionedValidatorManager.json"), - ".bytecode.object" - ), - abi.encode(address(validatorRegistryProxyAddr)) - ) - ); - vm.serializeString("output", "PermissionedValidatorManager", getJsonContractCode(address(poaManager))); - - // GasGuzzler - address gasGuzzler = deployDeterministicContract( - loadBytecode( - string.concat(arcNetworkContractDir, "GasGuzzler.sol/GasGuzzler.json"), - ".bytecode.object" - ) - ); - vm.serializeString("output", "GasGuzzler", getJsonContractCode(address(gasGuzzler))); - - // TestToken (ERC-20 for spammer load testing) - address testToken = deployDeterministicContract( - loadBytecode( - string.concat(arcNetworkContractDir, "TestToken.sol/TestToken.json"), - ".bytecode.object" - ) - ); - vm.serializeString("output", "TestToken", getJsonContractCode(address(testToken))); - - // Memo - address memo = deployDeterministicContract( - loadBytecode( - string.concat(arcNetworkContractDir, "Memo.sol/Memo.json"), - ".bytecode.object" - ) - ); - vm.serializeString("output", "Memo", getJsonContractCode(address(memo))); - - // Multicall3From - address multicall3From = deployDeterministicContract( - loadBytecode( - string.concat(arcNetworkContractDir, "Multicall3From.sol/Multicall3From.json"), - ".bytecode.object" - ) - ); - return vm.serializeString("output", "Multicall3From", getJsonContractCode(address(multicall3From))); - } - - function run() public pure { - console.log("usage: ArtifactHelper --sig 'run(uint256)' {chainId}"); - } - - function run(uint256 chainId, string memory outputPath, address validatorRegistryProxyAddr) public { - vm.chainId(chainId); - loadManifest("assets/artifacts/manifest.json"); - - // External contracts - for (uint256 i = 0; i < externalContracts.length; i++) { - string memory contractName = externalContracts[i]; - DeploymentType deploymentType = getDeploymentType(contractName); - if (deploymentType == DeploymentType.deterministic) { - DeterministicDeployment memory deployment = loadDeterministicDeployment(contractName); - address addr = deployDeterministicContract(deployment); - vm.serializeString("output", contractName, getJsonContractCode(addr)); - } else { - OneTimeAddressDeployment memory deployment = loadOneTimeAddressDeployment(contractName); - address addr = deployOneTimeAddressContract(deployment); - vm.serializeString("output", contractName, getJsonContractCode(addr)); - } - } - - string memory stablecoinArtifactsDir = "assets/artifacts/stablecoin-contracts"; - - // SignatureChecker - address signatureCheckerAddr = deployDeterministicContract( - loadBytecode( - string.concat(stablecoinArtifactsDir, "/SignatureChecker.json"), - ".bytecode") - ); - vm.serializeString("output", "SignatureChecker", getJsonContractCode(signatureCheckerAddr)); - - // NativeFiatTokenV2_2 - LinkReplacement[] memory linkReplacements = new LinkReplacement[](1); - linkReplacements[0] = LinkReplacement({ - // This is a hardhat link reference placeholder for the SignatureChecker library. - placeholder: "__$715109b5d747ea58b675c6ea3f0dba8c60$__", - addr: signatureCheckerAddr - }); - address fiatTokenAddr = deployDeterministicContract( - loadBytecode( - string.concat(stablecoinArtifactsDir, "/NativeFiatTokenV2_2.json"), - ".bytecode", - linkReplacements) - ); - vm.serializeString("output", "NativeFiatTokenV2_2", getJsonContractCode(fiatTokenAddr)); - - // FiatTokenProxy - address fiatTokenProxyAddr = deployDeterministicContract( - bytes.concat( - loadBytecode( - string.concat(stablecoinArtifactsDir, "/FiatTokenProxy.json"), - ".bytecode"), - abi.encode(fiatTokenAddr) - ) - ); - vm.serializeString("output", "FiatTokenProxy", getJsonContractCode(fiatTokenProxyAddr)); - - // Deploy ArcNetwork contracts (extracted to avoid stack too deep) - string memory output = deployArcNetworkContracts("contracts/out/forge/", validatorRegistryProxyAddr); - vm.writeJson(output, outputPath); - } -} diff --git a/contracts/scripts/DenylistManagement.s.sol b/contracts/scripts/DenylistManagement.s.sol deleted file mode 100644 index 6f6a01d..0000000 --- a/contracts/scripts/DenylistManagement.s.sol +++ /dev/null @@ -1,214 +0,0 @@ -// Copyright 2026 Circle Internet Group, Inc. All rights reserved. -// -// SPDX-License-Identifier: Apache-2.0 -// -// Licensed under the Apache License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. -// You may obtain a copy of the License at -// -// http://www.apache.org/licenses/LICENSE-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// See the License for the specific language governing permissions and -// limitations under the License. - -pragma solidity ^0.8.29; - -import {Script, console} from "forge-std/Script.sol"; -import {Denylist} from "../src/Denylist.sol"; -import {AdminUpgradeableProxy} from "../src/proxy/AdminUpgradeableProxy.sol"; -import {Ownable2StepUpgradeable} from "@openzeppelin/contracts-upgradeable/access/Ownable2StepUpgradeable.sol"; - -/** - * @notice Helper script for managing the Denylist contract (denylisters, denylisted addresses, ownership) - * @dev All functions read the proxy address from DENYLIST_PROXY env var. - * - * Setup: - * export DENYLIST_PROXY= - * - * ============ Print Functions (read-only, no key required) ============ - * Print contract info (proxy, implementation, admin, owner, pending owner): - * forge script scripts/DenylistManagement.s.sol --rpc-url --sig "printContractInfo()" - * - * Check if an address is denylisted: - * forge script scripts/DenylistManagement.s.sol --rpc-url --sig "printIsDenylisted(address)" - * - * Check if an address is a denylister: - * forge script scripts/DenylistManagement.s.sol --rpc-url --sig "printIsDenylister(address)" - * - * Batch-check denylisted status: - * forge script scripts/DenylistManagement.s.sol --rpc-url --sig "printBatchDenylistStatus(address[])" "[addr1,addr2,...]" - * - * ============ Denylister Management (owner only, requires OWNER_KEY env var) ============ - * Add a denylister: - * forge script scripts/DenylistManagement.s.sol --rpc-url --sig "addDenylister(address)" --broadcast - * - * Remove a denylister: - * forge script scripts/DenylistManagement.s.sol --rpc-url --sig "removeDenylister(address)" --broadcast - * - * ============ Denylist Operations (denylister only, requires DENYLISTER_KEY env var) ============ - * Denylist addresses: - * forge script scripts/DenylistManagement.s.sol --rpc-url --sig "denylist(address[])" "[addr1,addr2,...]" --broadcast - * - * Un-denylist addresses: - * forge script scripts/DenylistManagement.s.sol --rpc-url --sig "unDenylist(address[])" "[addr1,addr2,...]" --broadcast - * - * ============ Ownership Transfer (owner only, requires OWNER_KEY env var) ============ - * Transfer ownership (two-step): - * forge script scripts/DenylistManagement.s.sol --rpc-url --sig "transferOwnership(address)" --broadcast - * - * Accept ownership (new owner, requires NEW_OWNER_KEY env var): - * forge script scripts/DenylistManagement.s.sol --rpc-url --sig "acceptOwnership()" --broadcast - */ -contract DenylistManagement is Script { - - // ============ Helpers ============ - - function printContractInfo() public view { - Denylist _denylist = _getDenylist(); - address proxy = address(_denylist); - AdminUpgradeableProxy proxyContract = AdminUpgradeableProxy(payable(proxy)); - - console.log("Denylist Contract Info:"); - console.log("proxy:", proxy); - console.log("implementation:", proxyContract.implementation()); - console.log("proxyAdmin:", proxyContract.admin()); - console.log("owner:", _denylist.owner()); - console.log("pendingOwner:", Ownable2StepUpgradeable(proxy).pendingOwner()); - } - - function printIsDenylisted(address account) public view { - bool denylisted = _getDenylist().isDenylisted(account); - console.log("isDenylisted(%s): %s", account, denylisted); - } - - function printIsDenylister(address account) public view { - bool denylister = _getDenylist().isDenylister(account); - console.log("isDenylister(%s): %s", account, denylister); - } - - function printBatchDenylistStatus(address[] calldata accounts) public view { - Denylist _denylist = _getDenylist(); - console.log("Denylist Status (%s accounts):", accounts.length); - console.log("-------------------------"); - for (uint256 i = 0; i < accounts.length; i++) { - console.log("%s: %s", accounts[i], _denylist.isDenylisted(accounts[i])); - } - } - - // ============ Denylister Management (owner only) ============ - - function addDenylister(address account) public { - Denylist _denylist = _getDenylist(); - uint256 ownerKey = _getOwnerKey(); - - vm.startBroadcast(ownerKey); - _denylist.addDenylister(account); - vm.stopBroadcast(); - - console.log("Added denylister:", account); - } - - function removeDenylister(address account) public { - Denylist _denylist = _getDenylist(); - uint256 ownerKey = _getOwnerKey(); - - vm.startBroadcast(ownerKey); - _denylist.removeDenylister(account); - vm.stopBroadcast(); - - console.log("Removed denylister:", account); - } - - // ============ Denylist Operations (denylister only) ============ - - function denylist(address[] calldata accounts) public { - require(accounts.length > 0, "accounts cannot be empty"); - - Denylist _denylist = _getDenylist(); - uint256 denylisterKey = _getDenylisterKey(); - - _rejectProtectedAddresses(_denylist, accounts); - - vm.startBroadcast(denylisterKey); - _denylist.denylist(accounts); - vm.stopBroadcast(); - - console.log("Denylisted %s address(es)", accounts.length); - } - - function unDenylist(address[] calldata accounts) public { - require(accounts.length > 0, "accounts cannot be empty"); - - Denylist _denylist = _getDenylist(); - uint256 denylisterKey = _getDenylisterKey(); - - vm.startBroadcast(denylisterKey); - _denylist.unDenylist(accounts); - vm.stopBroadcast(); - - console.log("Un-denylisted %s address(es)", accounts.length); - } - - // ============ Ownership Transfer ============ - - function transferOwnership(address newOwner) public { - require(newOwner != address(0), "new owner cannot be zero address"); - - address proxy = address(_getDenylist()); - uint256 ownerKey = _getOwnerKey(); - - vm.startBroadcast(ownerKey); - Ownable2StepUpgradeable(proxy).transferOwnership(newOwner); - vm.stopBroadcast(); - - console.log("Ownership transfer initiated to:", newOwner); - } - - function acceptOwnership() public { - address proxy = address(_getDenylist()); - uint256 newOwnerKey = vm.envUint("NEW_OWNER_KEY"); - require(newOwnerKey != 0, "NEW_OWNER_KEY env var not set or is zero"); - - vm.startBroadcast(newOwnerKey); - Ownable2StepUpgradeable(proxy).acceptOwnership(); - vm.stopBroadcast(); - - console.log("Ownership accepted by:", vm.addr(newOwnerKey)); - } - - // ============ Internal Helpers ============ - - function _getDenylist() internal view returns (Denylist) { - address proxy = vm.envAddress("DENYLIST_PROXY"); - require(proxy != address(0), "DENYLIST_PROXY env var not set or is zero"); - require(proxy.code.length > 0, "DENYLIST_PROXY has no code"); - return Denylist(proxy); - } - - function _getOwnerKey() internal view returns (uint256) { - uint256 ownerKey = vm.envUint("OWNER_KEY"); - require(ownerKey != 0, "OWNER_KEY env var not set or is zero"); - return ownerKey; - } - - function _getDenylisterKey() internal view returns (uint256) { - uint256 denylisterKey = vm.envUint("DENYLISTER_KEY"); - require(denylisterKey != 0, "DENYLISTER_KEY env var not set or is zero"); - return denylisterKey; - } - - function _rejectProtectedAddresses(Denylist _denylist, address[] calldata accounts) internal view { - address proxy = address(_denylist); - address proxyAdmin = AdminUpgradeableProxy(payable(proxy)).admin(); - address owner = _denylist.owner(); - - for (uint256 i = 0; i < accounts.length; i++) { - address account = accounts[i]; - require(account != proxyAdmin, "cannot denylist proxy admin"); - require(account != owner, "cannot denylist owner"); - } - } -} diff --git a/contracts/scripts/DeployGasGuzzler.s.sol b/contracts/scripts/DeployGasGuzzler.s.sol deleted file mode 100644 index 7154dac..0000000 --- a/contracts/scripts/DeployGasGuzzler.s.sol +++ /dev/null @@ -1,32 +0,0 @@ -// Copyright 2025 Circle Internet Group, Inc. All rights reserved. -// -// SPDX-License-Identifier: Apache-2.0 -// -// Licensed under the Apache License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. -// You may obtain a copy of the License at -// -// http://www.apache.org/licenses/LICENSE-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// See the License for the specific language governing permissions and -// limitations under the License. - -pragma solidity ^0.8.29; - -import {Script, console} from "forge-std/Script.sol"; -import {GasGuzzler} from "../src/mocks/GasGuzzler.sol"; - -contract DeployGasGuzzler is Script { - function run() public { - uint256 deployerKey = vm.envUint("DEPLOYER_KEY"); - vm.startBroadcast(deployerKey); - GasGuzzler gasGuzzler = new GasGuzzler{salt: bytes32(0)}(); - vm.stopBroadcast(); - - console.log("Deployed GasGuzzler at:", address(gasGuzzler)); - } -} - diff --git a/contracts/scripts/DeployMemo.s.sol b/contracts/scripts/DeployMemo.s.sol deleted file mode 100644 index 31e759d..0000000 --- a/contracts/scripts/DeployMemo.s.sol +++ /dev/null @@ -1,55 +0,0 @@ -// Copyright 2026 Circle Internet Group, Inc. All rights reserved. -// -// SPDX-License-Identifier: Apache-2.0 -// -// Licensed under the Apache License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. -// You may obtain a copy of the License at -// -// http://www.apache.org/licenses/LICENSE-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// See the License for the specific language governing permissions and -// limitations under the License. - -pragma solidity ^0.8.29; - -import {Script, console} from "forge-std/Script.sol"; -import {Memo} from "../src/memo/Memo.sol"; - -/** - * @notice Script to deploy Memo - * @dev Not upgradeable, no proxy. Deployed via CREATE2 for deterministic addressing. - * - * Usage: - * forge script contracts/scripts/DeployMemo.s.sol \ - * --rpc-url \ - * --broadcast \ - * --verify - * - * Environment Variables: - * DEPLOYER_KEY - Private key of the deployer account (required) - */ -contract DeployMemo is Script { - function run() external returns (address deployment) { - uint256 deployerKey = vm.envUint("DEPLOYER_KEY"); - address deployer = vm.addr(deployerKey); - - console.log("Deployer:", deployer); - - vm.startBroadcast(deployerKey); - - Memo memoContract = new Memo{salt: bytes32(0)}(); - deployment = address(memoContract); - - vm.stopBroadcast(); - - require(deployment.code.length > 0, "Deployment failed: no bytecode"); - - console.log("Memo:", deployment); - - return deployment; - } -} diff --git a/contracts/scripts/DeployMulticall3From.s.sol b/contracts/scripts/DeployMulticall3From.s.sol deleted file mode 100644 index 150b5d9..0000000 --- a/contracts/scripts/DeployMulticall3From.s.sol +++ /dev/null @@ -1,31 +0,0 @@ -// Copyright 2026 Circle Internet Group, Inc. All rights reserved. -// -// SPDX-License-Identifier: Apache-2.0 -// -// Licensed under the Apache License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. -// You may obtain a copy of the License at -// -// http://www.apache.org/licenses/LICENSE-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// See the License for the specific language governing permissions and -// limitations under the License. - -pragma solidity ^0.8.29; - -import {Script, console} from "forge-std/Script.sol"; -import {Multicall3From} from "../src/batch/Multicall3From.sol"; - -contract DeployMulticall3From is Script { - function run() public { - uint256 deployerKey = vm.envUint("DEPLOYER_KEY"); - vm.startBroadcast(deployerKey); - Multicall3From multicall3From = new Multicall3From{salt: bytes32(0)}(); - vm.stopBroadcast(); - - console.log("Deployed Multicall3From at:", address(multicall3From)); - } -} diff --git a/contracts/scripts/DeployPermissionedValidatorManager.s.sol b/contracts/scripts/DeployPermissionedValidatorManager.s.sol deleted file mode 100644 index a180d0e..0000000 --- a/contracts/scripts/DeployPermissionedValidatorManager.s.sol +++ /dev/null @@ -1,107 +0,0 @@ -// Copyright 2025 Circle Internet Group, Inc. All rights reserved. -// -// SPDX-License-Identifier: Apache-2.0 -// -// Licensed under the Apache License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. -// You may obtain a copy of the License at -// -// http://www.apache.org/licenses/LICENSE-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// See the License for the specific language governing permissions and -// limitations under the License. - -pragma solidity ^0.8.29; - -import {Script, console} from "forge-std/Script.sol"; -import {PermissionedValidatorManager, IValidatorRegistry} from "../src/validator-manager/PermissionedValidatorManager.sol"; -import {AdminUpgradeableProxy} from "../src/proxy/AdminUpgradeableProxy.sol"; -import {Addresses} from "./Addresses.sol"; - -/** - * @notice Deploys the PermissionedValidatorManager contract - * command: DEPLOYER_KEY=$DEPLOYER_KEY \ - forge script contracts/scripts/DeployPermissionedValidatorManager.s.sol:DeployPermissionedValidatorManager \ - --sig "run()" \ - --rpc-url http://localhost:8545 \ - --broadcast - Note: Owner and proxy admin automatically read from old PVM at 0x36...03 - */ -contract DeployPermissionedValidatorManager is Script { - - // ============ Constants ============ - - IValidatorRegistry VALIDATOR_REGISTRY = IValidatorRegistry(Addresses.VALIDATOR_REGISTRY); - - /** - * @notice Read configuration from old PVM - * @return owner The owner from old PVM - * @return pauser The original pauser from old PVM - * @return proxyAdmin The proxy admin from old PVM - */ - function readOldPVMConfig() public view returns (address owner, address pauser, address proxyAdmin) { - PermissionedValidatorManager oldPvm = PermissionedValidatorManager(Addresses.PERMISSIONED_MANAGER); - owner = oldPvm.owner(); - pauser = oldPvm.pauser(); - proxyAdmin = AdminUpgradeableProxy(payable(Addresses.PERMISSIONED_MANAGER)).admin(); - } - - /** - * @notice Deploy PVM with given configuration - * @param owner Owner address for new PVM - * @param pauser Pauser address for new PVM - * @param proxyAdmin Proxy admin address for new PVM - * @return implAddress Deployed implementation address - * @return proxyAddress Deployed proxy address - */ - function deploy(address owner, address pauser, address proxyAdmin) public returns (address implAddress, address proxyAddress) { - console.log("=== Deploy PermissionedValidatorManager ==="); - console.log("Owner:", owner); - console.log("Pauser:", pauser); - console.log("Proxy Admin:", proxyAdmin); - console.log("ValidatorRegistry:", Addresses.VALIDATOR_REGISTRY); - console.log(""); - - // Deploy implementation - console.log("Deploying implementation..."); - PermissionedValidatorManager impl = new PermissionedValidatorManager( - IValidatorRegistry(Addresses.VALIDATOR_REGISTRY) - ); - implAddress = address(impl); - console.log("Implementation:", implAddress); - console.log(""); - - // Deploy proxy with initialization - console.log("Deploying proxy..."); - bytes memory initData = abi.encodeWithSignature("initialize(address,address)", owner, pauser); - AdminUpgradeableProxy proxy = new AdminUpgradeableProxy( - implAddress, - proxyAdmin, - initData - ); - proxyAddress = address(proxy); - console.log("Proxy:", proxyAddress); - console.log(""); - - console.log("DEPLOYMENT COMPLETE"); - } - - /** - * @notice Full deployment flow (read config + deploy) - * @dev Expects DEPLOYER_KEY environment variable - */ - function run() public returns (address implAddress, address proxyAddress) { - uint256 deployerKey = vm.envUint("DEPLOYER_KEY"); - - // Step 1: Read configuration from old PVM - (address owner, address pauser, address proxyAdmin) = readOldPVMConfig(); - - // Step 2: Deploy with configuration - vm.startBroadcast(deployerKey); - (implAddress, proxyAddress) = deploy(owner, pauser, proxyAdmin); - vm.stopBroadcast(); - } -} diff --git a/contracts/scripts/DeployProtocolConfig.s.sol b/contracts/scripts/DeployProtocolConfig.s.sol deleted file mode 100644 index 6888414..0000000 --- a/contracts/scripts/DeployProtocolConfig.s.sol +++ /dev/null @@ -1,58 +0,0 @@ -// Copyright 2025 Circle Internet Group, Inc. All rights reserved. -// -// SPDX-License-Identifier: Apache-2.0 -// -// Licensed under the Apache License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. -// You may obtain a copy of the License at -// -// http://www.apache.org/licenses/LICENSE-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// See the License for the specific language governing permissions and -// limitations under the License. - -pragma solidity ^0.8.29; - -import {Script, console} from "forge-std/Script.sol"; -import {ProtocolConfig} from "../src/protocol-config/ProtocolConfig.sol"; - -/** - * @notice Script to deploy a new ProtocolConfig implementation - * @dev This script deploys the implementation contract only (not the proxy) - * - * Usage: - * forge script contracts/scripts/DeployProtocolConfig.s.sol \ - * --rpc-url \ - * --broadcast \ - * --verify - * - * Environment Variables: - * DEPLOYER_PRIVATE_KEY - Private key of the deployer account (required) - */ -contract DeployProtocolConfig is Script { - function run() external returns (address implementation) { - uint256 deployerKey = vm.envUint("DEPLOYER_PRIVATE_KEY"); - address deployer = vm.addr(deployerKey); - - console.log("Deployer:", deployer); - - vm.startBroadcast(deployerKey); - - // Deploy new ProtocolConfig implementation - ProtocolConfig protocolConfig = new ProtocolConfig(); - implementation = address(protocolConfig); - - vm.stopBroadcast(); - - // Verify deployment - require(implementation.code.length > 0, "Deployment failed: no bytecode"); - - console.log("Implementation:", implementation); - - return implementation; - } -} - diff --git a/contracts/scripts/ProtocolConfigManagement.s.sol b/contracts/scripts/ProtocolConfigManagement.s.sol deleted file mode 100644 index c72cad3..0000000 --- a/contracts/scripts/ProtocolConfigManagement.s.sol +++ /dev/null @@ -1,403 +0,0 @@ -// Copyright 2025 Circle Internet Group, Inc. All rights reserved. -// -// SPDX-License-Identifier: Apache-2.0 -// -// Licensed under the Apache License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. -// You may obtain a copy of the License at -// -// http://www.apache.org/licenses/LICENSE-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// See the License for the specific language governing permissions and -// limitations under the License. - -pragma solidity ^0.8.29; - -import {Script, console} from "forge-std/Script.sol"; -import {ProtocolConfig} from "../src/protocol-config/ProtocolConfig.sol"; -import {IProtocolConfig} from "../src/protocol-config/interfaces/IProtocolConfig.sol"; -import {Addresses} from "./Addresses.sol"; - -/** - * @notice Helper script for managing ProtocolConfig parameters - * @dev Usage: - * - * ============ Print Functions ============ - * Print fee params: - * forge script scripts/ProtocolConfigManagement.s.sol --rpc-url --sig "printFeeParams()" - * - * Print consensus params: - * forge script scripts/ProtocolConfigManagement.s.sol --rpc-url --sig "printConsensusParams()" - * - * Print all params: - * forge script scripts/ProtocolConfigManagement.s.sol --rpc-url --sig "printAllParams()" - * - * ============ Update Fee Params (controller only, requires CONTROLLER_KEY env var) ============ - * Update block gas limit: - * forge script scripts/ProtocolConfigManagement.s.sol --rpc-url --sig "updateBlockGasLimit(uint64)" --broadcast - * - * Update base fee bounds: - * forge script scripts/ProtocolConfigManagement.s.sol --rpc-url --sig "updateBaseFeeBounds(uint256,uint256)" --broadcast - * - * Update alpha: - * forge script scripts/ProtocolConfigManagement.s.sol --rpc-url --sig "updateAlpha(uint64)" --broadcast - * - * Update kRate: - * forge script scripts/ProtocolConfigManagement.s.sol --rpc-url --sig "updateKRate(uint64)" --broadcast - * - * Update elasticity multiplier: - * forge script scripts/ProtocolConfigManagement.s.sol --rpc-url --sig "updateElasticityMultiplier(uint64)" --broadcast - * - * Update all fee params: - * forge script scripts/ProtocolConfigManagement.s.sol --rpc-url --sig "updateAllFeeParams(uint64,uint64,uint64,uint256,uint256,uint256)" --broadcast - * - * ============ Update Consensus Params (controller only, requires CONTROLLER_KEY env var) ============ - * Update target block time: - * forge script scripts/ProtocolConfigManagement.s.sol --rpc-url --sig "updateTargetBlockTime(uint16)" --broadcast - * - * Update individual timeout params: - * forge script scripts/ProtocolConfigManagement.s.sol --rpc-url --sig "updateTimeoutPropose(uint16)" --broadcast - * forge script scripts/ProtocolConfigManagement.s.sol --rpc-url --sig "updateTimeoutProposeDelta(uint16)" --broadcast - * forge script scripts/ProtocolConfigManagement.s.sol --rpc-url --sig "updateTimeoutPrevote(uint16)" --broadcast - * forge script scripts/ProtocolConfigManagement.s.sol --rpc-url --sig "updateTimeoutPrevoteDelta(uint16)" --broadcast - * forge script scripts/ProtocolConfigManagement.s.sol --rpc-url --sig "updateTimeoutPrecommit(uint16)" --broadcast - * forge script scripts/ProtocolConfigManagement.s.sol --rpc-url --sig "updateTimeoutPrecommitDelta(uint16)" --broadcast - * forge script scripts/ProtocolConfigManagement.s.sol --rpc-url --sig "updateTimeoutRebroadcast(uint16)" --broadcast - * - * Update all consensus params: - * forge script scripts/ProtocolConfigManagement.s.sol --rpc-url --sig "updateAllConsensusParams(uint16,uint16,uint16,uint16,uint16,uint16,uint16,uint16)" --broadcast - */ -contract ProtocolConfigManagement is Script { - // ============ Constants ============ - - ProtocolConfig PROTOCOL_CONFIG = ProtocolConfig(Addresses.PROTOCOL_CONFIG); - - // ============ Helpers ============ - - function printFeeParams() public view returns (IProtocolConfig.FeeParams memory params) { - params = PROTOCOL_CONFIG.feeParams(); - - console.log("ProtocolConfig FeeParams:"); - console.log("alpha:", params.alpha); - console.log("kRate:", params.kRate); - console.log("minBaseFee:", params.minBaseFee); - console.log("maxBaseFee:", params.maxBaseFee); - console.log("blockGasLimit:", params.blockGasLimit); - console.log("inverseElasticityMultiplier:", params.inverseElasticityMultiplier); - } - - function printConsensusParams() public view returns (IProtocolConfig.ConsensusParams memory params) { - params = PROTOCOL_CONFIG.consensusParams(); - - console.log("ProtocolConfig ConsensusParams:"); - console.log("timeoutProposeMs:", params.timeoutProposeMs); - console.log("timeoutProposeDeltaMs:", params.timeoutProposeDeltaMs); - console.log("timeoutPrevoteMs:", params.timeoutPrevoteMs); - console.log("timeoutPrevoteDeltaMs:", params.timeoutPrevoteDeltaMs); - console.log("timeoutPrecommitMs:", params.timeoutPrecommitMs); - console.log("timeoutPrecommitDeltaMs:", params.timeoutPrecommitDeltaMs); - console.log("timeoutRebroadcastMs:", params.timeoutRebroadcastMs); - console.log("targetBlockTimeMs:", params.targetBlockTimeMs); - } - - function printPauseState() public view { - console.log("ProtocolConfig PauseState:"); - console.log("pauser:", PROTOCOL_CONFIG.pauser()); - console.log("paused:", PROTOCOL_CONFIG.paused()); - } - - function printAllParams() public view { - printFeeParams(); - console.log(""); - printConsensusParams(); - console.log(""); - printPauseState(); - } - - // ============ Internal Helpers ============ - - /** - * @notice Gets and validates the controller key from environment - * @return The controller private key - */ - function _getControllerKey() internal view returns (uint256) { - uint256 controllerKey = vm.envUint("CONTROLLER_KEY"); - require(controllerKey != 0, "CONTROLLER_KEY env var not set or is zero"); - return controllerKey; - } - - /** - * @notice Broadcasts a fee params update transaction - * @param params The fee parameters to update - */ - function _broadcastFeeParamsUpdate(IProtocolConfig.FeeParams memory params) internal { - uint256 controllerKey = _getControllerKey(); - vm.startBroadcast(controllerKey); - PROTOCOL_CONFIG.updateFeeParams(params); - vm.stopBroadcast(); - } - - /** - * @notice Broadcasts a consensus params update transaction - * @param params The consensus parameters to update - */ - function _broadcastConsensusParamsUpdate(IProtocolConfig.ConsensusParams memory params) internal { - uint256 controllerKey = _getControllerKey(); - vm.startBroadcast(controllerKey); - PROTOCOL_CONFIG.updateConsensusParams(params); - vm.stopBroadcast(); - } - - // ============ Mutations ============ - - /** - * @notice Updates only the block gas limit while preserving other fee params - * @dev Requires CONTROLLER_KEY env var and controller role on ProtocolConfig - */ - function updateBlockGasLimit(uint64 newBlockGasLimit) public { - require(newBlockGasLimit > 0, "newBlockGasLimit must be > 0"); - - IProtocolConfig.FeeParams memory params = PROTOCOL_CONFIG.feeParams(); - - // Keep other fields unchanged; ensure invariants if uninitialized - if (params.inverseElasticityMultiplier == 0) params.inverseElasticityMultiplier = 5000; - if (params.maxBaseFee < params.minBaseFee) params.maxBaseFee = params.minBaseFee; - - params.blockGasLimit = newBlockGasLimit; - - _broadcastFeeParamsUpdate(params); - } - - /** - * @notice Updates only the target block time while preserving other consensus params. Setting to 0 disables the block time control mechanism. - * @dev Requires CONTROLLER_KEY env var and controller role on ProtocolConfig - */ - function updateTargetBlockTime(uint16 t) public { - IProtocolConfig.ConsensusParams memory params = PROTOCOL_CONFIG.consensusParams(); - params.targetBlockTimeMs = t; - _broadcastConsensusParamsUpdate(params); - } - - /** - * @notice Updates the base fee min/max bounds, preserving other fields - * @dev Requires CONTROLLER_KEY env var; performs no input validation - */ - function updateBaseFeeBounds(uint256 newMinBaseFee, uint256 newMaxBaseFee) public { - IProtocolConfig.FeeParams memory params = PROTOCOL_CONFIG.feeParams(); - params.minBaseFee = newMinBaseFee; - params.maxBaseFee = newMaxBaseFee; - _broadcastFeeParamsUpdate(params); - } - - /** - * @notice Updates only the alpha parameter while preserving other fee params - * @dev Requires CONTROLLER_KEY env var and controller role on ProtocolConfig - */ - function updateAlpha(uint64 newAlpha) public { - IProtocolConfig.FeeParams memory params = PROTOCOL_CONFIG.feeParams(); - params.alpha = newAlpha; - _broadcastFeeParamsUpdate(params); - } - - /** - * @notice Updates only the kRate parameter while preserving other fee params - * @dev Requires CONTROLLER_KEY env var and controller role on ProtocolConfig - */ - function updateKRate(uint64 newKRate) public { - IProtocolConfig.FeeParams memory params = PROTOCOL_CONFIG.feeParams(); - params.kRate = newKRate; - _broadcastFeeParamsUpdate(params); - } - - /** - * @notice Updates only the inverse elasticity multiplier while preserving other fee params - * @dev Requires CONTROLLER_KEY env var and controller role on ProtocolConfig - */ - function updateInverseElasticityMultiplier(uint64 newInverseElasticityMultiplier) public { - require(newInverseElasticityMultiplier <= 10000, "inverseElasticityMultiplier must be <= 10000"); - - IProtocolConfig.FeeParams memory params = PROTOCOL_CONFIG.feeParams(); - params.inverseElasticityMultiplier = newInverseElasticityMultiplier; - _broadcastFeeParamsUpdate(params); - } - - /** - * @notice Updates all fee parameters at once - * @dev Requires CONTROLLER_KEY env var and controller role on ProtocolConfig - */ - function updateAllFeeParams( - uint64 newAlpha, - uint64 newKRate, - uint64 newInverseElasticityMultiplier, - uint256 newMinBaseFee, - uint256 newMaxBaseFee, - uint256 newBlockGasLimit - ) public { - require(newBlockGasLimit > 0, "blockGasLimit must be > 0"); - require(newInverseElasticityMultiplier <= 10000, "inverseElasticityMultiplier must be <= 10000"); - - IProtocolConfig.FeeParams memory params = IProtocolConfig.FeeParams({ - alpha: newAlpha, - kRate: newKRate, - inverseElasticityMultiplier: newInverseElasticityMultiplier, - minBaseFee: newMinBaseFee, - maxBaseFee: newMaxBaseFee, - blockGasLimit: newBlockGasLimit - }); - - _broadcastFeeParamsUpdate(params); - } - - /** - * @notice Updates only the timeout propose while preserving other consensus params - * @dev Requires CONTROLLER_KEY env var and controller role on ProtocolConfig - */ - function updateTimeoutPropose(uint16 newTimeoutProposeMs) public { - require(newTimeoutProposeMs > 0, "timeoutProposeMs must be > 0"); - - IProtocolConfig.ConsensusParams memory params = PROTOCOL_CONFIG.consensusParams(); - params.timeoutProposeMs = newTimeoutProposeMs; - _broadcastConsensusParamsUpdate(params); - } - - /** - * @notice Updates only the timeout propose delta while preserving other consensus params - * @dev Requires CONTROLLER_KEY env var and controller role on ProtocolConfig - */ - function updateTimeoutProposeDelta(uint16 newTimeoutProposeDeltaMs) public { - require(newTimeoutProposeDeltaMs > 0, "timeoutProposeDeltaMs must be > 0"); - - IProtocolConfig.ConsensusParams memory params = PROTOCOL_CONFIG.consensusParams(); - params.timeoutProposeDeltaMs = newTimeoutProposeDeltaMs; - _broadcastConsensusParamsUpdate(params); - } - - /** - * @notice Updates only the timeout prevote while preserving other consensus params - * @dev Requires CONTROLLER_KEY env var and controller role on ProtocolConfig - */ - function updateTimeoutPrevote(uint16 newTimeoutPrevoteMs) public { - require(newTimeoutPrevoteMs > 0, "timeoutPrevoteMs must be > 0"); - - IProtocolConfig.ConsensusParams memory params = PROTOCOL_CONFIG.consensusParams(); - params.timeoutPrevoteMs = newTimeoutPrevoteMs; - _broadcastConsensusParamsUpdate(params); - } - - /** - * @notice Updates only the timeout prevote delta while preserving other consensus params - * @dev Requires CONTROLLER_KEY env var and controller role on ProtocolConfig - */ - function updateTimeoutPrevoteDelta(uint16 newTimeoutPrevoteDeltaMs) public { - require(newTimeoutPrevoteDeltaMs > 0, "timeoutPrevoteDeltaMs must be > 0"); - - IProtocolConfig.ConsensusParams memory params = PROTOCOL_CONFIG.consensusParams(); - params.timeoutPrevoteDeltaMs = newTimeoutPrevoteDeltaMs; - _broadcastConsensusParamsUpdate(params); - } - - /** - * @notice Updates only the timeout precommit while preserving other consensus params - * @dev Requires CONTROLLER_KEY env var and controller role on ProtocolConfig - */ - function updateTimeoutPrecommit(uint16 newTimeoutPrecommitMs) public { - require(newTimeoutPrecommitMs > 0, "timeoutPrecommitMs must be > 0"); - - IProtocolConfig.ConsensusParams memory params = PROTOCOL_CONFIG.consensusParams(); - params.timeoutPrecommitMs = newTimeoutPrecommitMs; - _broadcastConsensusParamsUpdate(params); - } - - /** - * @notice Updates only the timeout precommit delta while preserving other consensus params - * @dev Requires CONTROLLER_KEY env var and controller role on ProtocolConfig - */ - function updateTimeoutPrecommitDelta(uint16 newTimeoutPrecommitDeltaMs) public { - require(newTimeoutPrecommitDeltaMs > 0, "timeoutPrecommitDeltaMs must be > 0"); - - IProtocolConfig.ConsensusParams memory params = PROTOCOL_CONFIG.consensusParams(); - params.timeoutPrecommitDeltaMs = newTimeoutPrecommitDeltaMs; - _broadcastConsensusParamsUpdate(params); - } - - /** - * @notice Updates only the timeout rebroadcast while preserving other consensus params - * @dev Requires CONTROLLER_KEY env var and controller role on ProtocolConfig - */ - function updateTimeoutRebroadcast(uint16 newTimeoutRebroadcastMs) public { - require(newTimeoutRebroadcastMs > 0, "timeoutRebroadcastMs must be > 0"); - - IProtocolConfig.ConsensusParams memory params = PROTOCOL_CONFIG.consensusParams(); - params.timeoutRebroadcastMs = newTimeoutRebroadcastMs; - _broadcastConsensusParamsUpdate(params); - } - - /** - * @notice Updates all consensus parameters at once - * @dev Requires CONTROLLER_KEY env var and controller role on ProtocolConfig - */ - function updateAllConsensusParams( - uint16 newTimeoutProposeMs, - uint16 newTimeoutProposeDeltaMs, - uint16 newTimeoutPrevoteMs, - uint16 newTimeoutPrevoteDeltaMs, - uint16 newTimeoutPrecommitMs, - uint16 newTimeoutPrecommitDeltaMs, - uint16 newTimeoutRebroadcastMs, - uint16 newTargetBlockTimeMs - ) public { - require(newTimeoutProposeMs > 0, "timeoutProposeMs must be > 0"); - require(newTimeoutProposeDeltaMs > 0, "timeoutProposeDeltaMs must be > 0"); - require(newTimeoutPrevoteMs > 0, "timeoutPrevoteMs must be > 0"); - require(newTimeoutPrevoteDeltaMs > 0, "timeoutPrevoteDeltaMs must be > 0"); - require(newTimeoutPrecommitMs > 0, "timeoutPrecommitMs must be > 0"); - require(newTimeoutPrecommitDeltaMs > 0, "timeoutPrecommitDeltaMs must be > 0"); - require(newTimeoutRebroadcastMs > 0, "timeoutRebroadcastMs must be > 0"); - - IProtocolConfig.ConsensusParams memory params = IProtocolConfig.ConsensusParams({ - timeoutProposeMs: newTimeoutProposeMs, - timeoutProposeDeltaMs: newTimeoutProposeDeltaMs, - timeoutPrevoteMs: newTimeoutPrevoteMs, - timeoutPrevoteDeltaMs: newTimeoutPrevoteDeltaMs, - timeoutPrecommitMs: newTimeoutPrecommitMs, - timeoutPrecommitDeltaMs: newTimeoutPrecommitDeltaMs, - timeoutRebroadcastMs: newTimeoutRebroadcastMs, - targetBlockTimeMs: newTargetBlockTimeMs - }); - - _broadcastConsensusParamsUpdate(params); - } - -} - -/// @title ProtocolConfigState -/// @notice Preserved-state hash helper used by upgrade/rollback scripts under -/// `contracts/deployments/-protocol-config-*/scripts/`. -/// -/// Aggregates every field an upgrade/rollback must preserve byte-for-byte and returns a -/// single hash. Pre-boundary and post-boundary calls should produce equal hashes; any -/// divergence indicates a storage-slot collision, accidental overwrite, or layout drift -/// between old and new implementations. -/// -/// Validity condition: valid only when the struct definitions returned by the getters -/// (`FeeParams`, `ConsensusParams`) are unchanged between old and new impl. A struct -/// layout change would make `abi.encode` produce different bytes for the same logical -/// state — when that happens, replace this helper with field-by-field comparison on -/// the surviving fields. -/// -/// `pauser` / `paused` are intentionally excluded. Their ERC-7201 slot may move across -/// a given upgrade, and during the upgrade window the value can be read from different -/// slots pre-vs-post boundary. Those fields belong in explicit specific-value -/// assertions at the call sites, not in this hash. -library ProtocolConfigState { - function hash(address proxy) internal view returns (bytes32) { - IProtocolConfig.FeeParams memory fee = ProtocolConfig(proxy).feeParams(); - IProtocolConfig.ConsensusParams memory cons = ProtocolConfig(proxy).consensusParams(); - return keccak256( - abi.encode(fee, cons, ProtocolConfig(proxy).owner(), ProtocolConfig(proxy).controller()) - ); - } -} diff --git a/contracts/scripts/ProxyManagement.s.sol b/contracts/scripts/ProxyManagement.s.sol deleted file mode 100644 index 4e12942..0000000 --- a/contracts/scripts/ProxyManagement.s.sol +++ /dev/null @@ -1,51 +0,0 @@ -// Copyright 2026 Circle Internet Group, Inc. All rights reserved. -// -// SPDX-License-Identifier: Apache-2.0 -// -// Licensed under the Apache License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. -// You may obtain a copy of the License at -// -// http://www.apache.org/licenses/LICENSE-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// See the License for the specific language governing permissions and -// limitations under the License. - -pragma solidity ^0.8.29; - -import {Script, console} from "forge-std/Script.sol"; -import {AdminUpgradeableProxy} from "../src/proxy/AdminUpgradeableProxy.sol"; - -/// @notice Shared helpers to upgrade proxies with `upgradeTo(address)`. -contract ProxyManagement is Script { - - // ============ Helpers ============ - - function printProxyInfo(address proxyAddress) public view returns (address implementation, address admin) { - implementation = AdminUpgradeableProxy(payable(proxyAddress)).implementation(); - admin = AdminUpgradeableProxy(payable(proxyAddress)).admin(); - - console.log("Proxy Info:"); - console.log("proxyAddress:", proxyAddress); - console.log("implementation:", implementation); - console.log("admin:", admin); - } - - /// @dev Must be called within an active broadcast context. - /// @dev Should be called by proxy admin. - function upgradeProxyTo(address proxyAddress, address newImplementation) public { - require(proxyAddress != address(0), "Proxy address is zero"); - require(proxyAddress.code.length > 0, "Proxy has no code"); - require(newImplementation != address(0), "New implementation is zero"); - require(newImplementation.code.length > 0, "New implementation has no code"); - - // Call upgradeTo on proxy. - AdminUpgradeableProxy(payable(proxyAddress)).upgradeTo(newImplementation); - - address actual = AdminUpgradeableProxy(payable(proxyAddress)).implementation(); - require(actual == newImplementation, "Implementation not updated"); - } -} diff --git a/contracts/scripts/ValidatorManagement.s.sol b/contracts/scripts/ValidatorManagement.s.sol deleted file mode 100644 index 6ddd8d5..0000000 --- a/contracts/scripts/ValidatorManagement.s.sol +++ /dev/null @@ -1,322 +0,0 @@ -// Copyright 2025 Circle Internet Group, Inc. All rights reserved. -// -// SPDX-License-Identifier: Apache-2.0 -// -// Licensed under the Apache License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. -// You may obtain a copy of the License at -// -// http://www.apache.org/licenses/LICENSE-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// See the License for the specific language governing permissions and -// limitations under the License. - -pragma solidity ^0.8.29; - -import {Script, console, console2} from "forge-std/Script.sol"; -import {ValidatorRegistry, Validator, ValidatorStatus} from "../src/validator-manager/ValidatorRegistry.sol"; -import {Ownable2StepUpgradeable} from "@openzeppelin/contracts-upgradeable/access/Ownable2StepUpgradeable.sol"; -import {PermissionedValidatorManager} from "../src/validator-manager/PermissionedValidatorManager.sol"; -import {Addresses} from "./Addresses.sol"; - -/** - * @notice Helper script for managing validator registrations, activations, and voting power updates - * @dev Usage: - * - * Print the active validator set - * forge script script/ValidatorManagement.s.sol --rpc-url --sig "printActiveValidatorSet()" - * - * Register a new validator public key - * forge script scrips/ValidatorManagement.s.sol --rpc-url --sig "registerValidator()" - * - * Configure controller - * forge script scrips/ValidatorManagement.s.sol --rpc-url --sig "configureController()" - * - * Activate validator - * forge script scrips/ValidatorManagement.s.sol --rpc-url --sig "activateValidator()" - * - * Update voting power (no safety checks) - * forge script scrips/ValidatorManagement.s.sol --rpc-url --sig "updateVotingPowerUnsafe(10000)" - * - * Update voting power (with safety checks) - * forge script scrips/ValidatorManagement.s.sol --rpc-url --sig "updateVotingPower(10000)" - */ -contract ValidatorManagement is Script { - - // ============ Constants ============ - - ValidatorRegistry VALIDATOR_REGISTRY = ValidatorRegistry(Addresses.VALIDATOR_REGISTRY); - PermissionedValidatorManager PERMISSIONED_VALIDATOR_MANAGER = PermissionedValidatorManager(Addresses.PERMISSIONED_MANAGER); - - // ============ Helpers ============ - - /** - * @notice Pretty-prints the currently registered validators - */ - function printActiveValidatorSet() public view returns (Validator[] memory _validators) { - _validators = VALIDATOR_REGISTRY.getActiveValidatorSet(); - - console.log("Active Validators:"); - console.log("Count: ", _validators.length); - console.log("-------------------------"); - for (uint256 i = 0; i < _validators.length; i++) { - console.log("PubKey: ", vm.toString(_validators[i].publicKey)); - console.log("Power: ", _validators[i].votingPower); - console.log("Status: ", _statusToString(_validators[i].status)); - console.log("-------------------------"); - } - } - - /** - * @notice Pretty-prints a validator managed by a controller - */ - function printValidatorByController(address controller) public view { - Validator memory _validator = PERMISSIONED_VALIDATOR_MANAGER.getValidator(controller); - - console.log("-------------------------"); - console.log("PubKey: ", vm.toString(_validator.publicKey)); - console.log("Power: ", _validator.votingPower); - console.log("Status: ", _statusToString(_validator.status)); - console.log("-------------------------"); - } - - /** - * @notice Pretty-prints a validator managed by a controller - */ - function printValidatorByID(uint256 _registrationId) public view returns (Validator memory _validator) { - _validator = VALIDATOR_REGISTRY.getValidator(_registrationId); - - console.log("-------------------------"); - console.log("PubKey: ", vm.toString(_validator.publicKey)); - console.log("Power: ", _validator.votingPower); - console.log("Status: ", _statusToString(_validator.status)); - console.log("-------------------------"); - } - - // ============ Registration Flows ============ - - /** - * @notice Register's a new validators public key - * @dev Requires VALIDATOR_REGISTERER_KEY to be set in the environment - * @dev Requires VALIDATOR_PUBLIC_KEY_BYTES to be set in the environment - * - * Runs a cheap sanity check (length, not all-zero) before broadcasting. Full ed25519 - * curve-point validation cannot be done on-chain, and a bad key on-chain forces the - * consensus layer to skip the affected validator — reducing BFT security, and halting - * consensus entirely if the surviving validators no longer hold enough voting power - * for quorum. Operators should additionally cross-check VALIDATOR_PUBLIC_KEY_BYTES - * with off-chain tooling before running this script. - */ - function registerValidator() public returns (uint256 _registrationId) { - uint256 _validatorRegistererKey = vm.envUint( - "VALIDATOR_REGISTERER_KEY" - ); - bytes memory _validatorPublicKey = vm.envBytes( - "VALIDATOR_PUBLIC_KEY_BYTES" - ); - - requirePublicKeyBasicSanity(_validatorPublicKey); - - vm.startBroadcast(_validatorRegistererKey); - _registrationId = PERMISSIONED_VALIDATOR_MANAGER.registerValidator(_validatorPublicKey); - vm.stopBroadcast(); - - console.log( - "Registered validator with registrationId:", _registrationId, - "and public key:", vm.toString(_validatorPublicKey) - ); - } - - - /** - * @notice Configure a controller to manage a validator registrationId - * @dev Requires CONTROLLER_ADDRESS to be set in the environment - * @dev Requires REGISTRATION_ID to be set in the environment - * @dev Requires CONTROLLER_VOTING_POWER_LIMIT to be set in the environment - * @dev Requires PERMISSIONED_VALIDATOR_MANAGER_OWNER to be set in the environment - */ - function configureController() public { - address _controller = vm.envAddress( - "CONTROLLER_ADDRESS" - ); - uint256 _registrationId = vm.envUint( - "REGISTRATION_ID" - ); - uint64 _maxVotingPower = uint64(vm.envUint("CONTROLLER_VOTING_POWER_LIMIT")); - uint256 _permissionedOwnerKey = vm.envUint( - "PERMISSIONED_VALIDATOR_MANAGER_OWNER" - ); - - // Broadcast update - vm.startBroadcast(_permissionedOwnerKey); - PERMISSIONED_VALIDATOR_MANAGER.configureController(_controller, _registrationId, _maxVotingPower); - vm.stopBroadcast(); - - // Log configuration parameters for visibility - console2.log("Configure controller"); - console2.log("controller", _controller); - console2.log("registrationId", _registrationId); - console2.log("maxVotingPower", uint256(_maxVotingPower)); - } - - /** - * @notice Activates a new validator, using its controller - * @dev Requires CONTROLLER_KEY to be set in the environment - */ - function activateValidator() public { - uint256 _controllerKey = vm.envUint( - "CONTROLLER_KEY" - ); - - // Sanity check: validator voting power should be 0 - Validator memory _validator = PERMISSIONED_VALIDATOR_MANAGER.getValidator(vm.addr(_controllerKey)); - require(_validator.votingPower == 0, "Validator voting power should be 0"); - - // Activate the validator - vm.startBroadcast(_controllerKey); - PERMISSIONED_VALIDATOR_MANAGER.activateValidator(); - vm.stopBroadcast(); - } - - /** - * @notice Updates the voting power of a validator, using its controller - * @dev Requires CONTROLLER_KEY to be set in the environment - * @dev Enforces invariants that no validator can have critical voting power after update - */ - function updateVotingPower(uint64 _newVotingPower) public { - uint256 _controllerKey = vm.envUint( - "CONTROLLER_KEY" - ); - address _controllerAddress = vm.addr(_controllerKey); - Validator memory _validator = _checkControllerForVotingPowerUpdate(_controllerAddress); - - // Update the voting power - // Sanity check: make sure the validator mutation does not give it - // or another validator more than 1/3 of the total voting power - Validator[] memory _validators = VALIDATOR_REGISTRY.getActiveValidatorSet(); - - uint256 _totalVotingPower = 0; - uint256 _highestVotingPower = 0; - for (uint256 i = 0; i < _validators.length; i++) { - // Simulate as if the validator was updated - if (keccak256(_validators[i].publicKey) == keccak256(_validator.publicKey)) { - _validators[i].votingPower = _newVotingPower; - } - - // Record running highest voting power - if (_validators[i].votingPower > _highestVotingPower) { - _highestVotingPower = _validators[i].votingPower; - } - _totalVotingPower += _validators[i].votingPower; - } - - // Enforce invariant check - require(_highestVotingPower * 3 < _totalVotingPower, "Highest voting power exceeds 1/3 of total voting power"); - - // Broadcast update - vm.startBroadcast(_controllerKey); - PERMISSIONED_VALIDATOR_MANAGER.updateValidatorVotingPower(_newVotingPower); - vm.stopBroadcast(); - - console.log("Voting power updated:", _newVotingPower); - } - - /** - * @notice Updates the voting power of a validator, using its controller - * @dev Requires CONTROLLER_KEY to be set in the environment - * @dev WARNING: does not enforce any invariant checks - */ - function updateVotingPowerUnsafe(uint64 _newVotingPower) public { - uint256 _controllerKey = vm.envUint( - "CONTROLLER_KEY" - ); - address _controllerAddress = vm.addr(_controllerKey); - _checkControllerForVotingPowerUpdate(_controllerAddress); - - // Broadcast update - vm.startBroadcast(_controllerKey); - PERMISSIONED_VALIDATOR_MANAGER.updateValidatorVotingPower(_newVotingPower); - vm.stopBroadcast(); - - console.log("Voting power updated:", _newVotingPower); - } - - // ============ Internal Utils ============ - - /** - * @notice Cheap off-chain sanity check on an ed25519 public key. - * @dev Catches obvious operator mistakes (wrong length, zero/placeholder bytes) before - * they reach the registry. Full curve-point validation requires ed25519 math that - * is not feasible on-chain; operators must validate with off-chain tooling as well. - * Public (rather than internal) to allow direct unit testing without going through - * environment-variable plumbing. - */ - function requirePublicKeyBasicSanity(bytes memory publicKey) public pure { - require(publicKey.length == 32, "Public key must be 32 bytes"); - bool allZero = true; - for (uint256 i = 0; i < publicKey.length; i++) { - if (publicKey[i] != 0x00) { allZero = false; break; } - } - require(!allZero, "Public key must not be all zero"); - } - - /** - * @notice Sanity-checks that a controller is valid for a voting power update - */ - function _checkControllerForVotingPowerUpdate(address _controllerAddress) internal view returns (Validator memory _validator) { - _validator = PERMISSIONED_VALIDATOR_MANAGER.getValidator(_controllerAddress); - require(_validator.status == ValidatorStatus.Active || _validator.status == ValidatorStatus.Registered, "Validator not active or registered"); - uint256 _registrationId = PERMISSIONED_VALIDATOR_MANAGER.getRegistrationId(_controllerAddress); - require(_registrationId != 0, "RegistrationId not found"); - } - - /** - * @notice String representation of a ValidatorStatus case - */ - function _statusToString(ValidatorStatus status) internal pure returns (string memory) { - if (status == ValidatorStatus.Unknown) return "Unknown"; - if (status == ValidatorStatus.Registered) return "Registered"; - if (status == ValidatorStatus.Active) return "Active"; - return "Invalid"; - } -} - -/// @title ValidatorRegistryState -/// @notice Preserved-state hash helper used by upgrade/rollback scripts under -/// `contracts/deployments/-validator-registry-*/scripts/`. -/// -/// Aggregates every field an implementation-only upgrade must preserve and returns a -/// single hash. Pre-boundary and post-boundary calls should produce equal hashes; any -/// divergence indicates a storage-slot collision, accidental overwrite, or layout drift -/// between old and new implementations. -/// -/// Validity condition: valid only when the `Validator` struct layout returned by -/// `getValidator` and `getActiveValidatorSet` is unchanged between old and new impl. -/// A struct layout change would make `abi.encode` produce different bytes for the same -/// logical state — when that happens, replace this helper with field-by-field comparison -/// on the surviving fields. -library ValidatorRegistryState { - function hash(address proxy) internal view returns (bytes32) { - ValidatorRegistry vr = ValidatorRegistry(proxy); - uint256 nextId = vr.getNextRegistrationId(); - - bytes32[] memory perValidator = new bytes32[](nextId); - for (uint256 i = 0; i < nextId; ++i) { - Validator memory v = vr.getValidator(i); - perValidator[i] = keccak256(abi.encode(v.status, v.publicKey, v.votingPower)); - } - - return keccak256( - abi.encode( - Ownable2StepUpgradeable(proxy).owner(), - Ownable2StepUpgradeable(proxy).pendingOwner(), - nextId, - vr.getActiveValidatorSet(), - perValidator - ) - ); - } -} diff --git a/contracts/src/Denylist.sol b/contracts/src/Denylist.sol deleted file mode 100644 index bf0292d..0000000 --- a/contracts/src/Denylist.sol +++ /dev/null @@ -1,177 +0,0 @@ -// Copyright 2025 Circle Internet Group, Inc. All rights reserved. -// -// SPDX-License-Identifier: Apache-2.0 -// -// Licensed under the Apache License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. -// You may obtain a copy of the License at -// -// http://www.apache.org/licenses/LICENSE-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// See the License for the specific language governing permissions and -// limitations under the License. - -pragma solidity ^0.8.29; - -import {Ownable2StepUpgradeable} from "@openzeppelin/contracts-upgradeable/access/Ownable2StepUpgradeable.sol"; -import {Initializable} from "@openzeppelin/contracts-upgradeable/proxy/utils/Initializable.sol"; - -/** - * @title Denylist - */ -contract Denylist is Initializable, Ownable2StepUpgradeable { - // ============ Errors ============ - - /// @notice Thrown when a denylister attempts to denylist the owner - error CannotDenylistOwner(); - /// @notice Thrown when caller is not a denylister - error CallerIsNotDenylister(); - /// @notice Thrown when address is zero - error ZeroAddress(); - - // ============ Storage (ERC-7201) ============ - - /// @custom:storage-location erc7201:arc.storage.Denylist.v1 - struct DenylistStorage { - mapping(address => bool) denylisted; // baseSlot + 0 - mapping(address => bool) denylisters; // baseSlot + 1 - } - - /// @dev keccak256(abi.encode(uint256(keccak256("arc.storage.Denylist.v1")) - 1)) & ~bytes32(uint256(0xff)) - bytes32 public constant DENYLIST_STORAGE_LOCATION = - 0x1d7e1388d3ae56f3d9c18b1ce8d2b3b1a238a0edf682d2053af5d8a1d2f12f00; - - // ============ Events ============ - - event Denylisted(address indexed account); - event UnDenylisted(address indexed account); - event DenylisterAdded(address indexed account); - event DenylisterRemoved(address indexed account); - - // ============ Modifiers ============ - - modifier onlyDenylister() { - DenylistStorage storage $ = _getDenylistStorage(); - if (!$.denylisters[msg.sender]) revert CallerIsNotDenylister(); - _; - } - - // ============ Constructor / Initializer ============ - - /// @custom:oz-upgrades-unsafe-allow constructor - constructor() { - _disableInitializers(); - assert( - DENYLIST_STORAGE_LOCATION - == keccak256(abi.encode(uint256(keccak256("arc.storage.Denylist.v1")) - 1)) - & ~bytes32(uint256(0xff)) - ); - } - - /** - * @notice Initialize the Denylist (for proxy deployment). - * @param initialOwner The initial owner of the contract. - */ - function initialize(address initialOwner) external initializer { - if (initialOwner == address(0)) revert ZeroAddress(); - __Ownable2Step_init(); - _transferOwnership(initialOwner); - } - - // ============ Denylist / UnDenylist ============ - - /** - * @notice Add addresses to the denylist. Caller must be a denylister. Owner cannot be denylisted. - * @param accounts Addresses to denylist. - */ - function denylist(address[] calldata accounts) external onlyDenylister { - address _owner = owner(); - DenylistStorage storage $ = _getDenylistStorage(); - uint256 _accounts_length = accounts.length; - for (uint256 i = 0; i < _accounts_length; ++i) { - address account = accounts[i]; - if (account == _owner) revert CannotDenylistOwner(); - if (!$.denylisted[account]) { - $.denylisted[account] = true; - emit Denylisted(account); - } - } - } - - /** - * @notice Remove addresses from the denylist. Caller must be a denylister. - * @param accounts Addresses to remove from the denylist. - */ - function unDenylist(address[] calldata accounts) external onlyDenylister { - DenylistStorage storage $ = _getDenylistStorage(); - uint256 _accounts_length = accounts.length; - for (uint256 i = 0; i < _accounts_length; ++i) { - address account = accounts[i]; - if ($.denylisted[account]) { - delete $.denylisted[account]; - emit UnDenylisted(account); - } - } - } - - // ============ View functions ============ - - /** - * @notice Check if an address is denylisted. - * @param account Address to check. - * @return True if denylisted, false otherwise. - */ - function isDenylisted(address account) external view returns (bool) { - DenylistStorage storage $ = _getDenylistStorage(); - return $.denylisted[account]; - } - - /** - * @notice Check if an address is a denylister. - * @param account Address to check. - * @return True if denylister, false otherwise. - */ - function isDenylister(address account) external view returns (bool) { - DenylistStorage storage $ = _getDenylistStorage(); - return $.denylisters[account]; - } - - // ============ Owner: manage denylisters ============ - - /** - * @notice Add a denylister. Only owner. - * @param account Address to grant denylister role. - */ - function addDenylister(address account) external onlyOwner { - if (account == address(0)) revert ZeroAddress(); - DenylistStorage storage $ = _getDenylistStorage(); - if (!$.denylisters[account]) { - $.denylisters[account] = true; - emit DenylisterAdded(account); - } - } - - /** - * @notice Remove a denylister. Only owner. - * @param account Address to revoke denylister role. - */ - function removeDenylister(address account) external onlyOwner { - if (account == address(0)) revert ZeroAddress(); - DenylistStorage storage $ = _getDenylistStorage(); - if ($.denylisters[account]) { - delete $.denylisters[account]; - emit DenylisterRemoved(account); - } - } - - // ============ Internal ============ - - function _getDenylistStorage() private pure returns (DenylistStorage storage $) { - assembly { - $.slot := DENYLIST_STORAGE_LOCATION - } - } -} diff --git a/contracts/src/Precompiles.sol b/contracts/src/Precompiles.sol deleted file mode 100644 index 496bb04..0000000 --- a/contracts/src/Precompiles.sol +++ /dev/null @@ -1,27 +0,0 @@ -// Copyright 2026 Circle Internet Group, Inc. All rights reserved. -// -// SPDX-License-Identifier: Apache-2.0 -// -// Licensed under the Apache License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. -// You may obtain a copy of the License at -// -// http://www.apache.org/licenses/LICENSE-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// See the License for the specific language governing permissions and -// limitations under the License. - -pragma solidity ^0.8.29; - -/// @title Precompiles -/// @notice Genesis-fixed addresses of Arc precompiles. -library Precompiles { - address internal constant NATIVE_COIN_AUTHORITY = 0x1800000000000000000000000000000000000000; - address internal constant NATIVE_COIN_CONTROL = 0x1800000000000000000000000000000000000001; - address internal constant SYSTEM_ACCOUNTING = 0x1800000000000000000000000000000000000002; - address internal constant CALL_FROM = 0x1800000000000000000000000000000000000003; - address internal constant PQ = 0x1800000000000000000000000000000000000004; -} diff --git a/contracts/src/batch/IMulticall3From.sol b/contracts/src/batch/IMulticall3From.sol deleted file mode 100644 index 9626f0b..0000000 --- a/contracts/src/batch/IMulticall3From.sol +++ /dev/null @@ -1,112 +0,0 @@ -// Copyright 2026 Circle Internet Group, Inc. All rights reserved. -// -// SPDX-License-Identifier: Apache-2.0 -// -// Licensed under the Apache License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. -// You may obtain a copy of the License at -// -// http://www.apache.org/licenses/LICENSE-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// See the License for the specific language governing permissions and -// limitations under the License. - -pragma solidity ^0.8.29; - -/// @title IMulticall3From -/// @notice Interface for Multicall3From — a sender-preserving batch-call -/// contract that routes subcalls through the callFrom precompile. -/// Mirrors the original Multicall3 API but without value-forwarding -/// methods and without payable modifiers. -/// @dev EOA-only: aggregate-family entrypoints invoke the callFrom precompile, -/// which requires the sender argument (`msg.sender` of Multicall3From) -/// to equal the precompile caller or `tx.origin`. A contract caller is -/// neither, so the precompile rejects every subcall. -/// -/// Precompile rejections are captured per-call in the `(success, -/// returnData)` tuple, just like target-contract reverts. Failure- -/// tolerant entrypoints (`aggregate3` with `allowFailure = true`, -/// `tryAggregate(false, …)`, `tryBlockAndAggregate(false, …)`) return -/// `success = false` with the precompile's revert reason in -/// `returnData`. Hard-fail entrypoints (`aggregate`, -/// `blockAndAggregate`, `tryAggregate(true, …)`, and `allowFailure = -/// false` paths) still revert the entire batch. -interface IMulticall3From { - struct Call { - address target; - bytes callData; - } - - struct Call3 { - address target; - bool allowFailure; - bytes callData; - } - - struct Result { - bool success; - bytes returnData; - } - - /// @notice Aggregates calls, requiring all to succeed. Returns block - /// number and an array of return data. - /// @dev EOA-only. See {IMulticall3From}. - function aggregate(Call[] calldata calls) external returns (uint256 blockNumber, bytes[] memory returnData); - - /// @notice Aggregates calls with per-call failure flags. - /// @dev EOA-only. See {IMulticall3From}. - function aggregate3(Call3[] calldata calls) external returns (Result[] memory returnData); - - /// @notice Aggregates calls, requiring all to succeed. Returns block - /// number, block hash, and results. - /// @dev EOA-only. See {IMulticall3From}. - function blockAndAggregate(Call[] calldata calls) - external - returns (uint256 blockNumber, bytes32 blockHash, Result[] memory returnData); - - /// @notice Aggregates calls, with opt-in success requirement. - /// @dev EOA-only. See {IMulticall3From}. - function tryAggregate(bool requireSuccess, Call[] calldata calls) - external - returns (Result[] memory returnData); - - /// @notice Aggregates calls with opt-in success requirement, returning - /// block number, block hash, and results. - /// @dev EOA-only. See {IMulticall3From}. - function tryBlockAndAggregate(bool requireSuccess, Call[] calldata calls) - external - returns (uint256 blockNumber, bytes32 blockHash, Result[] memory returnData); - - /// @notice Returns the block hash for the given block number. - function getBlockHash(uint256 blockNumber) external view returns (bytes32 blockHash); - - /// @notice Returns the block number of the current block. - function getBlockNumber() external view returns (uint256 blockNumber); - - /// @notice Returns the coinbase of the current block. - function getCurrentBlockCoinbase() external view returns (address coinbase); - - /// @notice Returns the prevrandao (difficulty) of the current block. - function getCurrentBlockDifficulty() external view returns (uint256 difficulty); - - /// @notice Returns the gas limit of the current block. - function getCurrentBlockGasLimit() external view returns (uint256 gaslimit); - - /// @notice Returns the timestamp of the current block. - function getCurrentBlockTimestamp() external view returns (uint256 timestamp); - - /// @notice Returns the native token balance of the given address. - function getEthBalance(address addr) external view returns (uint256 balance); - - /// @notice Returns the hash of the previous block. - function getLastBlockHash() external view returns (bytes32 blockHash); - - /// @notice Returns the base fee of the current block. - function getBasefee() external view returns (uint256 basefee); - - /// @notice Returns the chain ID. - function getChainId() external view returns (uint256 chainid); -} diff --git a/contracts/src/batch/Multicall3From.sol b/contracts/src/batch/Multicall3From.sol deleted file mode 100644 index e173b08..0000000 --- a/contracts/src/batch/Multicall3From.sol +++ /dev/null @@ -1,189 +0,0 @@ -// Copyright 2026 Circle Internet Group, Inc. All rights reserved. -// -// SPDX-License-Identifier: Apache-2.0 -// -// Licensed under the Apache License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. -// You may obtain a copy of the License at -// -// http://www.apache.org/licenses/LICENSE-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// See the License for the specific language governing permissions and -// limitations under the License. - -pragma solidity ^0.8.29; - -import {ICallFrom} from "../call-from/ICallFrom.sol"; -import {Precompiles} from "../Precompiles.sol"; -import {IMulticall3From} from "./IMulticall3From.sol"; - -/// @title Multicall3From -/// @notice Sender-preserving batch-call contract that mirrors the original -/// Multicall3 API. Subcalls are routed through the callFrom precompile -/// so each target sees the original caller as `msg.sender`. -/// @dev Stateless and non-payable. Omits `aggregate3Value` because the -/// callFrom precompile does not support value forwarding on Arc. -/// Reentrancy is safe: the contract holds no state that could be -/// corrupted by a reentrant call. -/// @dev EOA-only: contract callers hit the callFrom sender-spoofing -/// constraint. Failure-tolerant paths return `success = false`; -/// hard-fail paths revert the batch. See {IMulticall3From}. -contract Multicall3From is IMulticall3From { - ICallFrom public constant CALL_FROM = ICallFrom(Precompiles.CALL_FROM); - - /// @inheritdoc IMulticall3From - function aggregate(Call[] calldata calls) external returns (uint256 blockNumber, bytes[] memory returnData) { - blockNumber = block.number; - uint256 length = calls.length; - returnData = new bytes[](length); - for (uint256 i = 0; i < length; ++i) { - (bool success, bytes memory result) = _callFrom(msg.sender, calls[i].target, calls[i].callData); - if (!success) { - assembly { - revert(add(result, 0x20), mload(result)) - } - } - returnData[i] = result; - } - } - - /// @inheritdoc IMulticall3From - function aggregate3(Call3[] calldata calls) external returns (Result[] memory returnData) { - uint256 length = calls.length; - returnData = new Result[](length); - for (uint256 i = 0; i < length; ++i) { - (bool success, bytes memory result) = _callFrom(msg.sender, calls[i].target, calls[i].callData); - returnData[i] = Result(success, result); - if (!calls[i].allowFailure && !success) { - assembly { - revert(add(result, 0x20), mload(result)) - } - } - } - } - - /// @inheritdoc IMulticall3From - function blockAndAggregate(Call[] calldata calls) - external - returns (uint256 blockNumber, bytes32 blockHash, Result[] memory returnData) - { - (blockNumber, blockHash, returnData) = _tryBlockAndAggregate(true, calls); - } - - /// @inheritdoc IMulticall3From - function tryAggregate(bool requireSuccess, Call[] calldata calls) external returns (Result[] memory returnData) { - uint256 length = calls.length; - returnData = new Result[](length); - for (uint256 i = 0; i < length; ++i) { - (bool success, bytes memory result) = _callFrom(msg.sender, calls[i].target, calls[i].callData); - if (requireSuccess && !success) { - assembly { - revert(add(result, 0x20), mload(result)) - } - } - returnData[i] = Result(success, result); - } - } - - /// @inheritdoc IMulticall3From - function tryBlockAndAggregate(bool requireSuccess, Call[] calldata calls) - external - returns (uint256 blockNumber, bytes32 blockHash, Result[] memory returnData) - { - (blockNumber, blockHash, returnData) = _tryBlockAndAggregate(requireSuccess, calls); - } - - /// @inheritdoc IMulticall3From - function getBlockHash(uint256 blockNumber) external view returns (bytes32 blockHash) { - blockHash = blockhash(blockNumber); - } - - /// @inheritdoc IMulticall3From - function getBlockNumber() external view returns (uint256 blockNumber) { - blockNumber = block.number; - } - - /// @inheritdoc IMulticall3From - function getCurrentBlockCoinbase() external view returns (address coinbase) { - coinbase = block.coinbase; - } - - /// @inheritdoc IMulticall3From - function getCurrentBlockDifficulty() external view returns (uint256 difficulty) { - difficulty = block.prevrandao; - } - - /// @inheritdoc IMulticall3From - function getCurrentBlockGasLimit() external view returns (uint256 gaslimit) { - gaslimit = block.gaslimit; - } - - /// @inheritdoc IMulticall3From - function getCurrentBlockTimestamp() external view returns (uint256 timestamp) { - timestamp = block.timestamp; - } - - /// @inheritdoc IMulticall3From - function getEthBalance(address addr) external view returns (uint256 balance) { - balance = addr.balance; - } - - /// @inheritdoc IMulticall3From - function getLastBlockHash() external view returns (bytes32 blockHash) { - unchecked { - blockHash = blockhash(block.number - 1); - } - } - - /// @inheritdoc IMulticall3From - function getBasefee() external view returns (uint256 basefee) { - basefee = block.basefee; - } - - /// @inheritdoc IMulticall3From - function getChainId() external view returns (uint256 chainid) { - chainid = block.chainid; - } - - /// @dev Shared implementation for blockAndAggregate and tryBlockAndAggregate. - function _tryBlockAndAggregate(bool requireSuccess, Call[] calldata calls) - internal - returns (uint256 blockNumber, bytes32 blockHash, Result[] memory returnData) - { - blockNumber = block.number; - blockHash = blockhash(block.number); - uint256 length = calls.length; - returnData = new Result[](length); - for (uint256 i = 0; i < length; ++i) { - (bool success, bytes memory result) = _callFrom(msg.sender, calls[i].target, calls[i].callData); - if (requireSuccess && !success) { - assembly { - revert(add(result, 0x20), mload(result)) - } - } - returnData[i] = Result(success, result); - } - } - - /// @dev Low-level call to the callFrom precompile. Captures framework-level - /// reverts (e.g. static context, sender-spoofing rejection) into the - /// return tuple instead of propagating them, so callers can honor - /// allowFailure / requireSuccess uniformly. - function _callFrom(address sender, address target, bytes calldata data) - internal - returns (bool success, bytes memory returnData) - { - (bool ok, bytes memory raw) = address(CALL_FROM).call( - abi.encodeCall(ICallFrom.callFrom, (sender, target, data)) - ); - if (ok) { - (success, returnData) = abi.decode(raw, (bool, bytes)); - } else { - success = false; - returnData = raw; - } - } -} diff --git a/contracts/src/call-from/ICallFrom.sol b/contracts/src/call-from/ICallFrom.sol deleted file mode 100644 index 21eefb5..0000000 --- a/contracts/src/call-from/ICallFrom.sol +++ /dev/null @@ -1,31 +0,0 @@ -// Copyright 2026 Circle Internet Group, Inc. All rights reserved. -// -// SPDX-License-Identifier: Apache-2.0 -// -// Licensed under the Apache License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. -// You may obtain a copy of the License at -// -// http://www.apache.org/licenses/LICENSE-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// See the License for the specific language governing permissions and -// limitations under the License. - -pragma solidity ^0.8.29; - -/// @title ICallFrom -/// @notice Interface for the callFrom precompile that executes a call to a `target` as the specified `sender`, i.e `target` will see `msg.sender` set as `sender`. -interface ICallFrom { - /// @notice Executes a call to `target` with `data` as if `sender` were the caller. - /// @param sender The address to impersonate as msg.sender for the subcall. - /// @param target The address to call. - /// @param data The calldata to forward to the target. - /// @return success Whether the subcall succeeded. - /// @return returnData The raw return data from the subcall. - function callFrom(address sender, address target, bytes calldata data) - external - returns (bool success, bytes memory returnData); -} diff --git a/contracts/src/common/roles/Pausable.sol b/contracts/src/common/roles/Pausable.sol deleted file mode 100644 index eaf3785..0000000 --- a/contracts/src/common/roles/Pausable.sol +++ /dev/null @@ -1,181 +0,0 @@ -// Copyright 2026 Circle Internet Group, Inc. All rights reserved. -// -// SPDX-License-Identifier: Apache-2.0 -// -// Licensed under the Apache License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. -// You may obtain a copy of the License at -// -// http://www.apache.org/licenses/LICENSE-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// See the License for the specific language governing permissions and -// limitations under the License. - -pragma solidity ^0.8.29; - -import {Ownable2StepUpgradeable} from "@openzeppelin/contracts-upgradeable/access/Ownable2StepUpgradeable.sol"; - -/** - * @title Pausable - * @notice Base contract which allows children to implement an emergency stop mechanism - * @dev Forked from https://github.com/circlefin/stablecoin-evm/blob/c8c31b249341bf3ffb2e8dbff41977c392a260c5/contracts/v1/Pausable.sol - * @dev Features: - * - Separate pauser role (different from owner) - * - Owner can update pauser address - * - Only pauser can pause/unpause - * @dev Uses ERC-7201 namespaced storage pattern for upgrade safety. - */ -abstract contract Pausable is Ownable2StepUpgradeable { - // ============ Errors ============ - - /// @notice Thrown when pauser address is zero - error ZeroPauserAddress(); - - /// @notice Thrown when caller is not the pauser - error CallerIsNotPauser(); - - /// @notice Thrown when contract is paused - error ContractPaused(); - - // ============ Storage (ERC-7201) ============ - - /// @custom:storage-location erc7201:arc.storage.Pausable - struct PausableStorage { - /// @notice Current pauser address - address pauser; - /// @notice Whether the contract is paused - bool paused; - } - - // keccak256(abi.encode(uint256(keccak256("arc.storage.Pausable")) - 1)) & ~bytes32(uint256(0xff)) - bytes32 private constant PAUSABLE_STORAGE_LOCATION = - 0x0642d7922329a434cf4fd17a3c95eb692c24fd95f9f94d0b55420a5d895f4a00; - - function _getPausableStorage() internal pure returns (PausableStorage storage $) { - assembly { - $.slot := PAUSABLE_STORAGE_LOCATION - } - } - - /// @custom:oz-upgrades-unsafe-allow constructor - constructor() { - // Verify storage slot calculation is correct - assert( - PAUSABLE_STORAGE_LOCATION - == keccak256(abi.encode(uint256(keccak256("arc.storage.Pausable")) - 1)) & ~bytes32(uint256(0xff)) - ); - } - - // ============ Events ============ - - /** - * @notice Emitted when contract is paused - */ - event Pause(); - - /** - * @notice Emitted when contract is unpaused - */ - event Unpause(); - - /** - * @notice Emitted when pauser address is updated - * @param newAddress new pauser address - */ - event PauserChanged(address indexed newAddress); - - // ============ Modifiers ============ - - /** - * @dev Modifier to make a function callable only when the contract is not paused - */ - modifier whenNotPaused() { - _whenNotPaused(); - _; - } - - /** - * @dev Modifier to restrict access to pauser only - */ - modifier onlyPauser() { - _onlyPauser(); - _; - } - - // ============ Functions ============ - - /** - * @dev Internal function to check if contract is not paused - */ - function _whenNotPaused() internal view { - PausableStorage storage $ = _getPausableStorage(); - require(!$.paused, ContractPaused()); - } - - /** - * @dev Internal function to check if caller is pauser - */ - function _onlyPauser() internal view { - PausableStorage storage $ = _getPausableStorage(); - require(msg.sender == $.pauser, CallerIsNotPauser()); - } - - /** - * @notice Returns the current pauser address - * @return The address of the current pauser - */ - function pauser() public view virtual returns (address) { - PausableStorage storage $ = _getPausableStorage(); - return $.pauser; - } - - /** - * @notice Returns whether the contract is paused - * @return True if the contract is paused, false otherwise - */ - function paused() public view virtual returns (bool) { - PausableStorage storage $ = _getPausableStorage(); - return $.paused; - } - - /** - * @notice Updates the pauser address - * @param newPauser The address of the new pauser - * @dev Only callable by owner - */ - function updatePauser(address newPauser) external virtual onlyOwner { - require(newPauser != address(0), ZeroPauserAddress()); - PausableStorage storage $ = _getPausableStorage(); - $.pauser = newPauser; - emit PauserChanged(newPauser); - } - - /** - * @notice Pauses the contract - * @dev Only callable by pauser - * @dev Idempotent: repeated calls while paused leave state unchanged and emit no event. - */ - function pause() external virtual onlyPauser { - PausableStorage storage $ = _getPausableStorage(); - if (!$.paused) { - $.paused = true; - emit Pause(); - } - } - - /** - * @notice Unpauses the contract - * @dev Only callable by pauser - * @dev Idempotent: repeated calls while unpaused leave state unchanged and emit no event. - */ - function unpause() external virtual onlyPauser { - PausableStorage storage $ = _getPausableStorage(); - if ($.paused) { - $.paused = false; - emit Unpause(); - } - } -} diff --git a/contracts/src/memo/IMemo.sol b/contracts/src/memo/IMemo.sol deleted file mode 100644 index 55333ba..0000000 --- a/contracts/src/memo/IMemo.sol +++ /dev/null @@ -1,55 +0,0 @@ -// Copyright 2026 Circle Internet Group, Inc. All rights reserved. -// -// SPDX-License-Identifier: Apache-2.0 -// -// Licensed under the Apache License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. -// You may obtain a copy of the License at -// -// http://www.apache.org/licenses/LICENSE-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// See the License for the specific language governing permissions and -// limitations under the License. - -pragma solidity ^0.8.29; - -/// @title IMemo -/// @notice Interface for the Memo contract. -/// @dev EOA-only: {memo} invokes the callFrom precompile, which requires the -/// sender argument (`msg.sender` of Memo) to equal the precompile caller -/// or `tx.origin`. A contract caller is neither, so callFrom reverts and -/// the entire call reverts without raising {MemoFailed}. -/// -/// Target-contract reverts (callFrom returning `success=false`) are -/// re-raised as {MemoFailed} with the target's revert bytes. -interface IMemo { - /// @notice Thrown when the call via callFrom fails. - error MemoFailed(bytes returnData); - - /// @notice Emitted before the subcall is executed, carrying the memoIndex that will be used. - event BeforeMemo(uint256 indexed memoIndex); - - /// @notice Emitted after a successful subcall with the associated memo metadata. - event Memo( - address indexed sender, - address indexed target, - bytes32 callDataHash, - bytes32 indexed memoId, - bytes memo, - uint256 memoIndex - ); - - /// @notice Returns the current memo index. - function memoIndex() external view returns (uint256); - - /// @notice Executes a subcall via the callFrom precompile and emits memo metadata. - /// @dev EOA-only. See {IMemo}. - /// @param target The address to call via the precompile. - /// @param data The calldata to forward to the target. - /// @param memoId A caller-supplied identifier for the memo. - /// @param memoData Arbitrary memo bytes attached to the subcall. - function memo(address target, bytes calldata data, bytes32 memoId, bytes calldata memoData) external; -} diff --git a/contracts/src/memo/Memo.sol b/contracts/src/memo/Memo.sol deleted file mode 100644 index da6b11b..0000000 --- a/contracts/src/memo/Memo.sol +++ /dev/null @@ -1,47 +0,0 @@ -// Copyright 2026 Circle Internet Group, Inc. All rights reserved. -// -// SPDX-License-Identifier: Apache-2.0 -// -// Licensed under the Apache License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. -// You may obtain a copy of the License at -// -// http://www.apache.org/licenses/LICENSE-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// See the License for the specific language governing permissions and -// limitations under the License. - -pragma solidity ^0.8.29; - -import {ICallFrom} from "../call-from/ICallFrom.sol"; -import {Precompiles} from "../Precompiles.sol"; -import {IMemo} from "./IMemo.sol"; - -/** - * @title Memo - * @notice Wraps the callFrom precompile to attach memo metadata to subcalls. - * @dev Not upgradeable, no proxy. Deployed at runtime via CREATE2. - * The callFrom precompile enforces its own allowlist — this contract does not add access control. - * @dev EOA-only: contract callers hit the callFrom sender-spoofing constraint - * and the call reverts without raising {MemoFailed}. See {IMemo}. - */ -contract Memo is IMemo { - /// @inheritdoc IMemo - uint256 public memoIndex; - - /// @notice The callFrom precompile used to forward subcalls with caller preservation. - ICallFrom public constant CALL_FROM = ICallFrom(Precompiles.CALL_FROM); - - /// @inheritdoc IMemo - function memo(address target, bytes calldata data, bytes32 memoId, bytes calldata memoData) external { - // Pre-increment ensures unique indices under reentrancy; each call captures its own local index. - uint256 currentMemoIndex = memoIndex++; - emit BeforeMemo(currentMemoIndex); - (bool success, bytes memory returnData) = CALL_FROM.callFrom(msg.sender, target, data); - if (!success) revert MemoFailed(returnData); - emit Memo(msg.sender, target, keccak256(data), memoId, memoData, currentMemoIndex); - } -} diff --git a/contracts/src/mocks/CallHelper.sol b/contracts/src/mocks/CallHelper.sol deleted file mode 100644 index 920cd29..0000000 --- a/contracts/src/mocks/CallHelper.sol +++ /dev/null @@ -1,192 +0,0 @@ -// Copyright 2025 Circle Internet Group, Inc. All rights reserved. -// -// SPDX-License-Identifier: Apache-2.0 -// -// Licensed under the Apache License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. -// You may obtain a copy of the License at -// -// http://www.apache.org/licenses/LICENSE-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// See the License for the specific language governing permissions and -// limitations under the License. - -pragma solidity ^0.8.29; - -import {IMulticall3} from "./IMulticall3.sol"; - -contract CallHelper { - constructor() payable {} - - event StorageSet(address indexed sender, uint256 indexed slot, uint256 indexed value); - - function setStorage(uint256 slot, uint256 value) external payable { - emit StorageSet(msg.sender, slot, value); - assembly { - sstore(slot, value) - } - } - - function getStorage(uint256 slot) external view returns (uint256) { - uint256 value; - assembly { - value := sload(slot) - } - return value; - } - - receive() external payable {} - - event ExecutionContext(address indexed sender, uint256 value); - event ExecutionResult(bool indexed success, bytes result); - - error ExecutionFailed(bytes result); - - function transfer(address to, uint256 value) external payable { - (bool success, bytes memory result) = to.call{value: value}(hex""); - emit ExecutionContext(msg.sender, msg.value); - emit ExecutionResult(success, result); - } - - function execute(address target, bytes calldata data, uint256 value) - external - payable - returns (bool success, bytes memory result) - { - (success, result) = target.call{value: value}(data); - emit ExecutionResult(success, result); - return (success, result); - } - - function executeBatch(IMulticall3.Call3Value[] calldata calls) - external - payable - returns (IMulticall3.Result[] memory results) - { - results = new IMulticall3.Result[](calls.length); - for (uint256 i = 0; i < calls.length; i++) { - IMulticall3.Call3Value memory x = calls[i]; - (bool success, bytes memory result) = x.target.call{value: x.value}(x.callData); - emit ExecutionResult(success, result); - results[i] = IMulticall3.Result(success, result); - if (!x.allowFailure && !success) { - revert ExecutionFailed(result); - } - } - return results; - } - - function staticCall(address target, bytes calldata data) - external - payable - returns (bool success, bytes memory result) - { - (success, result) = target.staticcall(data); - emit ExecutionResult(success, result); - return (success, result); - } - - function delegateCall(address target, bytes calldata data) - external - payable - returns (bool success, bytes memory result) - { - (success, result) = target.delegatecall(data); - emit ExecutionResult(success, result); - return (success, result); - } - - function callCode(address target, bytes memory data, uint256 value) - external - payable - returns (bool success, bytes memory result) - { - assembly { - let inLen := mload(data) - let callSuccess := callcode(gas(), target, value, add(data, 0x20), inLen, 0, 0) - let size := returndatasize() - let ptr := mload(0x40) - mstore(ptr, size) - returndatacopy(add(ptr, 0x20), 0, size) - success := callSuccess - result := ptr - mstore(0x40, add(ptr, add(size, 0x20))) - } - emit ExecutionResult(success, result); - return (success, result); - } - - error ErrorMessage(string message); - - function revertWithString(string memory message) external payable { - revert(message); - } - - function revertWithError(string memory message) external payable { - revert ErrorMessage(message); - } - - struct BlockInfo { - address coinbase; - uint256 timestamp; - uint256 number; - uint256 baseFee; - uint256 blobBaseFee; - uint256 prevRandao; - uint256 gasLimit; - } - - function getBlockInfo() external view returns (BlockInfo memory) { - return BlockInfo({ - coinbase: block.coinbase, - timestamp: block.timestamp, - number: block.number, - baseFee: block.basefee, - blobBaseFee: block.blobbasefee, - prevRandao: block.prevrandao, - gasLimit: block.gaslimit - }); - } - - struct TransactionInfo { - uint256 gasPrice; - address origin; - } - - function getTxInfo() external view returns (TransactionInfo memory) { - TransactionInfo memory info = TransactionInfo({gasPrice: tx.gasprice, origin: tx.origin}); - return info; - } - - function blobHash(uint256 index) external view returns (bytes32) { - return blobhash(index); - } - - function callAndRevert(address target, bytes calldata targetCalldata) external returns (bool success, bytes memory result) { - (success, result) = target.call(targetCalldata); - revert("Intentional revert after call"); - } - - /// @notice Self-destructs the contract, sending any remaining balance to the target address - function triggerSelfDestruct(address payable target) external payable { - // solhint-disable-next-line avoid-selfdestruct - selfdestruct(target); - } - - /// @notice Deploys a contract using CREATE2 with the provided salt and bytecode - function create2(bytes memory bytecode, bytes32 salt) external payable returns (address deployed) { - uint256 amount = msg.value; - assembly { - // Forked from: https://github.com/OpenZeppelin/openzeppelin-contracts/blob/255e27e6d22934ddaf00c7f279039142d725382d/contracts/utils/Create2.sol#L46 - deployed := create2(amount, add(bytecode, 0x20), mload(bytecode), salt) - if iszero(deployed) { revert(0, 0) } - } - } -} - -interface Token { - function transfer(address to, uint256 amount) external returns (bool); -} diff --git a/contracts/src/mocks/GasGuzzler.sol b/contracts/src/mocks/GasGuzzler.sol deleted file mode 100644 index 5e33f73..0000000 --- a/contracts/src/mocks/GasGuzzler.sol +++ /dev/null @@ -1,111 +0,0 @@ -// Copyright 2025 Circle Internet Group, Inc. All rights reserved. -// -// SPDX-License-Identifier: Apache-2.0 -// -// Licensed under the Apache License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. -// You may obtain a copy of the License at -// -// http://www.apache.org/licenses/LICENSE-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// See the License for the specific language governing permissions and -// limitations under the License. - -pragma solidity ^0.8.29; - -/// @title GasGuzzler -/// @notice Contract for simulating heavy computation and gas-intensive workloads -contract GasGuzzler { - // ============ Storage ============ - - /// @notice Dummy variable for gas guzzling operations - uint256 public dummy; - - /// @notice Storage mapping for read/write tests - mapping(uint256 => uint256) public storageMap; - - /// @notice Total number of storage reads performed - uint256 public totalRead; - - /// @notice Total number of storage writes performed - uint256 public totalWrite; - - // ============ Gas Guzzler Functions ============ - - /// @notice Consumes gas until gasRemaining is left - /// @param gasRemaining Target gas to leave remaining - function guzzle(uint256 gasRemaining) external payable { - uint256 counter; - while (gasleft() > gasRemaining) { - counter++; - } - } - - /// @notice Consumes gas by forcing a revert in static context, then loops - /// @param gasRemaining Target gas to leave remaining - function guzzle2(uint256 gasRemaining) external payable { - // call sstore in static context will revert and consume all gas - (bool success,) = address(this).staticcall{gas: gasleft() - gasRemaining}( - abi.encodeWithSelector(this.setDummy.selector, block.timestamp) - ); - // just for eliminating unused variable warning - uint256 counter = success ? 1 : 0; - while (gasleft() > gasRemaining) { - counter++; - } - } - - /// @notice Set dummy variable (used by guzzle2 to trigger revert in static context) - /// @param newDummy New value to set - function setDummy(uint256 newDummy) external { - assembly { - sstore(dummy.slot, newDummy) - } - } - - // ================ Recursive Hashing ================ - - /// @notice Perform CPU-intensive repeated hashing - /// @param iterations Number of hash iterations to perform - function hashLoop(uint256 iterations) external pure returns (bytes32 result) { - for (uint256 i = 0; i < iterations; i++) { - result = keccak256(abi.encodePacked(result, i)); - } - } - - // =================== Storage Read =================== - - /// @notice Read storage slots in pseudo-random order - maximizes cold SLOAD costs - /// @param iterations Number of storage slots to read - function storageRead(uint256 iterations) external returns (uint256 result) { - if (iterations == 0) return 0; - - uint256 baseIndex = uint256(keccak256(abi.encodePacked(msg.sender, totalRead))); - unchecked { - for (uint256 i = 0; i < iterations; i++) { - result ^= storageMap[baseIndex + i]; - } - totalRead += iterations; - } - } - - // ================== Storage Write =================== - - /// @notice Write to storage slots in pseudo-random order - maximizes cold SSTORE costs - /// @param iterations Number of storage slots to write - function storageWrite(uint256 iterations) external returns (uint256 result) { - if (iterations == 0) return 0; - - uint256 baseIndex = uint256(keccak256(abi.encodePacked(msg.sender, totalWrite))); - unchecked { - for (uint256 i = 0; i < iterations; i++) { - result ^= baseIndex + i; - storageMap[baseIndex + i] = result; - } - totalWrite += iterations; - } - } -} diff --git a/contracts/src/mocks/IMulticall3.sol b/contracts/src/mocks/IMulticall3.sol deleted file mode 100644 index 3c7a39d..0000000 --- a/contracts/src/mocks/IMulticall3.sol +++ /dev/null @@ -1,85 +0,0 @@ -// Copyright 2025 Circle Internet Group, Inc. All rights reserved. -// -// SPDX-License-Identifier: Apache-2.0 -// -// Licensed under the Apache License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. -// You may obtain a copy of the License at -// -// http://www.apache.org/licenses/LICENSE-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// See the License for the specific language governing permissions and -// limitations under the License. -pragma solidity ^0.8.29; - -interface IMulticall3 { - struct Call { - address target; - bytes callData; - } - - struct Call3 { - address target; - bool allowFailure; - bytes callData; - } - - struct Call3Value { - address target; - bool allowFailure; - uint256 value; - bytes callData; - } - - struct Result { - bool success; - bytes returnData; - } - - function aggregate(Call[] calldata calls) - external - payable - returns (uint256 blockNumber, bytes[] memory returnData); - - function aggregate3(Call3[] calldata calls) external payable returns (Result[] memory returnData); - - function aggregate3Value(Call3Value[] calldata calls) external payable returns (Result[] memory returnData); - - function blockAndAggregate(Call[] calldata calls) - external - payable - returns (uint256 blockNumber, bytes32 blockHash, Result[] memory returnData); - - function getBasefee() external view returns (uint256 basefee); - - function getBlockHash(uint256 blockNumber) external view returns (bytes32 blockHash); - - function getBlockNumber() external view returns (uint256 blockNumber); - - function getChainId() external view returns (uint256 chainid); - - function getCurrentBlockCoinbase() external view returns (address coinbase); - - function getCurrentBlockDifficulty() external view returns (uint256 difficulty); - - function getCurrentBlockGasLimit() external view returns (uint256 gaslimit); - - function getCurrentBlockTimestamp() external view returns (uint256 timestamp); - - function getEthBalance(address addr) external view returns (uint256 balance); - - function getLastBlockHash() external view returns (bytes32 blockHash); - - function tryAggregate(bool requireSuccess, Call[] calldata calls) - external - payable - returns (Result[] memory returnData); - - function tryBlockAndAggregate(bool requireSuccess, Call[] calldata calls) - external - payable - returns (uint256 blockNumber, bytes32 blockHash, Result[] memory returnData); -} diff --git a/contracts/src/mocks/NativeTransferHelper.sol b/contracts/src/mocks/NativeTransferHelper.sol deleted file mode 100644 index f726b18..0000000 --- a/contracts/src/mocks/NativeTransferHelper.sol +++ /dev/null @@ -1,72 +0,0 @@ -// Copyright 2025 Circle Internet Group, Inc. All rights reserved. -// -// SPDX-License-Identifier: Apache-2.0 -// -// Licensed under the Apache License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. -// You may obtain a copy of the License at -// -// http://www.apache.org/licenses/LICENSE-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// See the License for the specific language governing permissions and -// limitations under the License. - -pragma solidity ^0.8.29; - -interface IBurnToken { - function burn(uint256 amount) external; -} - -/// @dev Uses selfdestruct for testing purposes. -/// The deprecation warnings are expected and can be safely ignored. -contract NativeTransferHelper { - constructor(address payable _target, bool _selfdestruct) payable { - if (_selfdestruct) { - // solhint-disable-next-line avoid-selfdestruct - selfdestruct(_target); - } - } - - /// @notice A function that cannot receive native token - function cannotReceive() external {} - - /// @notice A function that can receive native token - function canReceive() external payable {} - - /// @notice Deploys a contract using CREATE with the provided bytecode - function create(bytes memory bytecode, uint256 createValue) external payable returns (address deployed) { - assembly { - deployed := create(createValue, add(bytecode, 0x20), mload(bytecode)) - } - } - - /// @notice Deploys a contract using CREATE2 with the provided salt and bytecode - function create2(bytes memory bytecode, bytes32 salt) external payable returns (address deployed) { - uint256 amount = msg.value; - assembly { - // Forked from: https://github.com/OpenZeppelin/openzeppelin-contracts/blob/255e27e6d22934ddaf00c7f279039142d725382d/contracts/utils/Create2.sol#L46 - deployed := create2(amount, add(bytecode, 0x20), mload(bytecode), salt) - if iszero(deployed) { revert(0, 0) } - } - } - - /// @notice Relays a call with value to the target, optionally requiring success - function relay(address target, uint256 amount, bool requireSuccess, bytes calldata data) external payable { - (bool success,) = target.call{value: amount}(data); - require(success || !requireSuccess, "Relay reverted"); - } - - /// @notice Self-destructs the contract, sending any remaining balance to the target address - function triggerSelfDestruct(address payable target) external payable { - // solhint-disable-next-line avoid-selfdestruct - selfdestruct(target); - } - - /// @notice Executes a burn against a burnToken - function burn(address burnToken, uint256 amount) external payable { - IBurnToken(burnToken).burn(amount); - } -} diff --git a/contracts/src/mocks/PrecompileCallCode.sol b/contracts/src/mocks/PrecompileCallCode.sol deleted file mode 100644 index ebf3975..0000000 --- a/contracts/src/mocks/PrecompileCallCode.sol +++ /dev/null @@ -1,77 +0,0 @@ -// Copyright 2025 Circle Internet Group, Inc. All rights reserved. -// -// SPDX-License-Identifier: Apache-2.0 -// -// Licensed under the Apache License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. -// You may obtain a copy of the License at -// -// http://www.apache.org/licenses/LICENSE-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// See the License for the specific language governing permissions and -// limitations under the License. - -pragma solidity ^0.8.29; - -import {Precompiles} from "../Precompiles.sol"; - -/** - * @title PrecompileCallCode - * @notice Uses CALLCODE opcode to call NativeCoinAuthority precompile - */ -contract PrecompileCallCode { - address public owner; - - constructor() { - owner = msg.sender; - } - - function callCodeToPrecompile() internal returns (bool) { - // Try to mint via callCode to precompile - bytes memory mintCall = abi.encodeWithSignature( - "mint(address,uint256)", - owner, - 6660000000000000000 // 6.66 token (18 decimals) - ); - - bool success; - - // Get the precompile address - address target = Precompiles.NATIVE_COIN_AUTHORITY; - - // Use assembly to perform CALLCODE and bubble up errors - assembly { - let inLen := mload(mintCall) - success := callcode( - gas(), // forward all gas - target, // target address (precompile) - 0, // value - add(mintCall, 0x20), // input data pointer - inLen, // input data size - 0, // output pointer - 0 // output size - ) - - // If call failed, copy return data and revert with it - if iszero(success) { - let size := returndatasize() - let ptr := mload(0x40) - returndatacopy(ptr, 0, size) - revert(ptr, size) - } - } - - return true; - } - - // Call flow: from USDC.rescueERC20() -CALL-> PrecompileCallCode.transfer() -CALLCODE-> NATIVE_COIN_AUTHORITY.mint() - // Transfer parameters are ignored, just to match ERC20 interface - function transfer(address to, uint256 amount) external returns (bool) { - bool success = callCodeToPrecompile(); - return success; - } -} - diff --git a/contracts/src/mocks/PrecompileDelegater.sol b/contracts/src/mocks/PrecompileDelegater.sol deleted file mode 100644 index 3eac3a9..0000000 --- a/contracts/src/mocks/PrecompileDelegater.sol +++ /dev/null @@ -1,69 +0,0 @@ -// Copyright 2025 Circle Internet Group, Inc. All rights reserved. -// -// SPDX-License-Identifier: Apache-2.0 -// -// Licensed under the Apache License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. -// You may obtain a copy of the License at -// -// http://www.apache.org/licenses/LICENSE-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// See the License for the specific language governing permissions and -// limitations under the License. - -pragma solidity ^0.8.29; - -import {Precompiles} from "../Precompiles.sol"; - -/** - * @title PrecompileDelegater - */ -contract PrecompileDelegater { - address public owner; - - constructor() { - owner = msg.sender; - } - - function delegateToPrecompile() internal returns (bool) { - // Try to mint via delegatecall to precompile - bytes memory mintCall = abi.encodeWithSignature( - "mint(address,uint256)", - owner, - 6660000000000000000 // 6.66 token (18 decimals) - ); - - (bool success, bytes memory returnData) = Precompiles.NATIVE_COIN_AUTHORITY.delegatecall(mintCall); - - // Bubble up the error if call failed - if (!success) { - assembly { - revert(add(returnData, 32), mload(returnData)) - } - } - - return success; - } - - // Call flow: from USDC.permit() -CALL-> PrecompileDelegater.isValidSignature() -DELEGATECALL-> NATIVE_COIN_AUTHORITY.mint() - // Signature parameters are ignored, just to match ERC1271 interface - // Intentionally not a view function to allow delegatecall - function isValidSignature( - bytes32 /* digest */, - bytes memory /* signature */ - ) external returns (bytes4) { - delegateToPrecompile(); - return 0x1626ba7e; // ERC1271 MAGICVALUE - } - - // Call flow: from USDC.rescueERC20() -CALL-> PrecompileDelegater.transfer() -DELEGATECALL-> NATIVE_COIN_AUTHORITY.mint() - // Transfer parameters are ignored, just to match ERC20 interface - function transfer(address /* to */, uint256 /* amount */) external returns (bool) { - bool success = delegateToPrecompile(); - return success; - } -} - diff --git a/contracts/src/mocks/RevertingProtocolConfig.sol b/contracts/src/mocks/RevertingProtocolConfig.sol deleted file mode 100644 index 809c1f4..0000000 --- a/contracts/src/mocks/RevertingProtocolConfig.sol +++ /dev/null @@ -1,27 +0,0 @@ -// Copyright 2025 Circle Internet Group, Inc. All rights reserved. -// -// SPDX-License-Identifier: Apache-2.0 -// -// Licensed under the Apache License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. -// You may obtain a copy of the License at -// -// http://www.apache.org/licenses/LICENSE-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// See the License for the specific language governing permissions and -// limitations under the License. - -pragma solidity ^0.8.29; - -import {IProtocolConfig} from "../protocol-config/interfaces/IProtocolConfig.sol"; - -contract RevertingProtocolConfig { - constructor() payable {} - - function feeParams() external pure returns (IProtocolConfig.FeeParams memory) { - revert("RevertingProtocolConfig: feeParams always reverts"); - } -} diff --git a/contracts/src/mocks/TestImplementation.sol b/contracts/src/mocks/TestImplementation.sol deleted file mode 100644 index d879eae..0000000 --- a/contracts/src/mocks/TestImplementation.sol +++ /dev/null @@ -1,180 +0,0 @@ -// Copyright 2025 Circle Internet Group, Inc. All rights reserved. -// -// SPDX-License-Identifier: Apache-2.0 -// -// Licensed under the Apache License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. -// You may obtain a copy of the License at -// -// http://www.apache.org/licenses/LICENSE-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// See the License for the specific language governing permissions and -// limitations under the License. - -pragma solidity ^0.8.29; - -/** - * @title TestImplementation - * @notice A simple implementation contract for testing proxy functionality - * @dev This contract provides basic functionality to test proxy patterns without - * being tied to specific business logic - */ -contract TestImplementation { - // ============ Storage Variables ============ - - address public owner; - uint256 public value; - string public name; - bool public initialized; - - // ============ Events ============ - - event ValueUpdated(uint256 oldValue, uint256 newValue); - event NameUpdated(string oldName, string newName); - event OwnerChanged(address oldOwner, address newOwner); - event InitializeCalled(address owner, uint256 value, string name); - - // ============ Errors ============ - - error NotOwner(); - error AlreadyInitialized(); - - // ============ Modifiers ============ - - modifier onlyOwner() { - _onlyOwner(); - _; - } - - // ============ Internal Functions ============ - - function _onlyOwner() internal view { - if (msg.sender != owner) { - revert NotOwner(); - } - } - - // ============ Constructor ============ - - constructor() { - // Leave uninitialized for proxy pattern - } - - // ============ Initialization ============ - - /** - * @notice Initialize the contract (for proxy pattern) - * @param _owner Initial owner address - * @param _value Initial value - * @param _name Initial name - */ - function initialize(address _owner, uint256 _value, string memory _name) external { - if (initialized) { - revert AlreadyInitialized(); - } - - owner = _owner; - value = _value; - name = _name; - initialized = true; - - emit InitializeCalled(_owner, _value, _name); - } - - // ============ Public Functions ============ - - /** - * @notice Update the stored value (owner only) - * @param _newValue New value to set - */ - function updateValue(uint256 _newValue) external onlyOwner { - uint256 oldValue = value; - value = _newValue; - emit ValueUpdated(oldValue, _newValue); - } - - /** - * @notice Update the stored name (owner only) - * @param _newName New name to set - */ - function updateName(string memory _newName) external onlyOwner { - string memory oldName = name; - name = _newName; - emit NameUpdated(oldName, _newName); - } - - /** - * @notice Change the owner (owner only) - * @param _newOwner New owner address - */ - function changeOwner(address _newOwner) external onlyOwner { - address oldOwner = owner; - owner = _newOwner; - emit OwnerChanged(oldOwner, _newOwner); - } - - /** - * @notice Get the current value (public read) - * @return Current stored value - */ - function getValue() external view returns (uint256) { - return value; - } - - /** - * @notice Get the current name (public read) - * @return Current stored name - */ - function getName() external view returns (string memory) { - return name; - } - - /** - * @notice Get the current owner (public read) - * @return Current owner address - */ - function getOwner() external view returns (address) { - return owner; - } - - /** - * @notice Check if initialized (public read) - * @return Whether contract is initialized - */ - function isInitialized() external view returns (bool) { - return initialized; - } - - /** - * @notice Payable function for testing ETH handling - * @param _data Arbitrary data to process - * @return success Whether operation succeeded - */ - function processWithValue(bytes memory _data) external payable returns (bool success) { - // Simple processing - just return true if we received ETH and data - success = (msg.value > 0 || _data.length > 0); - return success; - } - - /** - * @notice Function that always reverts (for testing revert scenarios) - */ - function alwaysReverts() external pure { - revert("This function always reverts"); - } - - // ============ Fallback & Receive ============ - - /** - * @notice Fallback function that accepts ETH - */ - fallback() external payable {} - - /** - * @notice Receive function that accepts ETH - */ - receive() external payable {} -} diff --git a/contracts/src/mocks/TestToken.sol b/contracts/src/mocks/TestToken.sol deleted file mode 100644 index 12c0535..0000000 --- a/contracts/src/mocks/TestToken.sol +++ /dev/null @@ -1,26 +0,0 @@ -// Copyright 2026 Circle Internet Group, Inc. All rights reserved. -// -// SPDX-License-Identifier: Apache-2.0 -// -// Licensed under the Apache License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. -// You may obtain a copy of the License at -// -// http://www.apache.org/licenses/LICENSE-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// See the License for the specific language governing permissions and -// limitations under the License. - -pragma solidity ^0.8.29; - -import {ERC20} from "@openzeppelin/contracts/token/ERC20/ERC20.sol"; - -/// @title TestToken -/// @notice Minimal ERC-20 token for spammer load testing. -/// Balances are initialized via genesis storage slots; no constructor mint. -contract TestToken is ERC20 { - constructor() ERC20("TestToken", "TEST") {} -} diff --git a/contracts/src/pq/IPQ.sol b/contracts/src/pq/IPQ.sol deleted file mode 100644 index 5bb9af1..0000000 --- a/contracts/src/pq/IPQ.sol +++ /dev/null @@ -1,31 +0,0 @@ -// Copyright 2026 Circle Internet Group, Inc. All rights reserved. -// -// SPDX-License-Identifier: Apache-2.0 -// -// Licensed under the Apache License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. -// You may obtain a copy of the License at -// -// http://www.apache.org/licenses/LICENSE-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// See the License for the specific language governing permissions and -// limitations under the License. -pragma solidity ^0.8.29; - -/// @title IPQ — Experimental post-quantum cryptography precompile interface -/// @notice Exposes post-quantum cryptographic primitives for early integrations. -/// Additional algorithms may be added in future hardforks. -interface IPQ { - /// @notice Verify an SLH-DSA-SHA2-128s signature (FIPS 205). - /// @dev Since PQ signatures are still very new, we recommend not to solely rely on them for - /// authentication, but pair them with classical signatures. Gas cost: 230,000 base + 6 - /// per 32-byte word of message (same rate as KECCAK256). - /// @param vk Verifying key (32 bytes) - /// @param message Message that was signed - /// @param sig Signature (7856 bytes) - /// @return True if the signature is valid - function verifySlhDsaSha2128s(bytes memory vk, bytes memory message, bytes memory sig) external view returns (bool); -} diff --git a/contracts/src/protocol-config/ProtocolConfig.sol b/contracts/src/protocol-config/ProtocolConfig.sol deleted file mode 100644 index 2f4d2ab..0000000 --- a/contracts/src/protocol-config/ProtocolConfig.sol +++ /dev/null @@ -1,194 +0,0 @@ -// Copyright 2025 Circle Internet Group, Inc. All rights reserved. -// -// SPDX-License-Identifier: Apache-2.0 -// -// Licensed under the Apache License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. -// You may obtain a copy of the License at -// -// http://www.apache.org/licenses/LICENSE-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// See the License for the specific language governing permissions and -// limitations under the License. - -pragma solidity ^0.8.29; - -import {Controller} from "./roles/Controller.sol"; -import {Pausable} from "../common/roles/Pausable.sol"; -import {IProtocolConfig} from "./interfaces/IProtocolConfig.sol"; - -/** - * @title ProtocolConfig - * @dev Contract for managing protocol configuration parameters including fee settings - * @dev This contract is designed to be used with upgradeable transparent proxies - */ -contract ProtocolConfig is Controller, Pausable, IProtocolConfig { - // ============ Errors ============ - - /// @notice Thrown when contract is already initialized - error AlreadyInitialized(); - - /// @notice Thrown when alpha parameter is invalid (must be <= 100) - error InvalidAlpha(); - - /// @notice Thrown when kRate parameter is invalid (must be <= 10000) - error InvalidKRate(); - - /// @notice Thrown when minBaseFee is greater than maxBaseFee - error InvalidBaseFeeRange(); - - /// @notice Thrown when blockGasLimit is zero - error InvalidBlockGasLimit(); - - /// @notice Thrown when inverseElasticityMultiplier exceeds 10000 - error InvalidInverseElasticityMultiplier(); - - /// @notice Thrown when timeoutProposeMs is zero - error InvalidTimeoutProposeMs(); - - /// @notice Thrown when timeoutProposeDeltaMs is zero - error InvalidTimeoutProposeDeltaMs(); - - /// @notice Thrown when timeoutPrevoteMs is zero - error InvalidTimeoutPrevoteMs(); - - /// @notice Thrown when timeoutPrevoteDeltaMs is zero - error InvalidTimeoutPrevoteDeltaMs(); - - /// @notice Thrown when timeoutPrecommitMs is zero - error InvalidTimeoutPrecommitMs(); - - /// @notice Thrown when timeoutPrecommitDeltaMs is zero - error InvalidTimeoutPrecommitDeltaMs(); - - /// @notice Thrown when timeoutRebroadcastMs is zero - error InvalidTimeoutRebroadcastMs(); - - // ============ Storage ============ - - /// @custom:storage-location erc7201:arc.storage.ProtocolConfig - struct ProtocolConfigStorage { - FeeParams feeParams; - /// @custom:oz-renamed-from rewardBeneficiary - address _deprecatedSlot; - ConsensusParams consensusParams; - } - - // keccak256(abi.encode(uint256(keccak256("arc.storage.ProtocolConfig")) - 1)) & ~bytes32(uint256(0xff)) - bytes32 private constant PROTOCOL_CONFIG_STORAGE_LOCATION = - 0x668f09ce856848ead6cb1ddee963f15ef833cea8958030868f867aec84385200; - - /** - * @dev Returns the storage pointer for ProtocolConfig state - */ - function _getProtocolConfigStorage() private pure returns (ProtocolConfigStorage storage $) { - assembly { - $.slot := PROTOCOL_CONFIG_STORAGE_LOCATION - } - } - - // ============ Constructor ============ - - /** - * @dev Constructor for implementation contract - disables initializers to prevent misuse - */ - /// @custom:oz-upgrades-unsafe-allow constructor - constructor() { - // Verify storage slot calculation is correct - assert( - PROTOCOL_CONFIG_STORAGE_LOCATION - == keccak256(abi.encode(uint256(keccak256("arc.storage.ProtocolConfig")) - 1)) & ~bytes32(uint256(0xff)) - ); - // Disable initializers on implementation contract - _disableInitializers(); - } - - // ============ Read-Only Functions ============ - - /** - * @notice Returns the current fee parameters - * @return The current fee parameters struct - */ - function feeParams() external view override returns (FeeParams memory) { - ProtocolConfigStorage storage $ = _getProtocolConfigStorage(); - return $.feeParams; - } - - /** - * @notice Returns the current consensus parameters - * @return The current consensus parameters struct - */ - function consensusParams() external view override returns (ConsensusParams memory) { - ProtocolConfigStorage storage $ = _getProtocolConfigStorage(); - return $.consensusParams; - } - - // ============ Mutative Functions ============ - - /** - * @notice Updates the fee parameters - * @dev Only callable by controller when not paused - * @param newParams The new fee parameters - */ - function updateFeeParams(FeeParams calldata newParams) external override onlyController whenNotPaused { - // Validate parameters - require(newParams.alpha <= 100, InvalidAlpha()); - require(newParams.kRate <= 10000, InvalidKRate()); - require(newParams.minBaseFee <= newParams.maxBaseFee, InvalidBaseFeeRange()); - require(newParams.blockGasLimit > 0, InvalidBlockGasLimit()); - require(newParams.inverseElasticityMultiplier <= 10000, InvalidInverseElasticityMultiplier()); - - ProtocolConfigStorage storage $ = _getProtocolConfigStorage(); - $.feeParams = newParams; - emit FeeParamsUpdated(newParams); - } - - /** - * @notice Updates the consensus parameters - * @dev Only callable by controller when not paused - * @param newParams The new consensus parameters - */ - function updateConsensusParams(ConsensusParams calldata newParams) external override onlyController whenNotPaused { - // Validate parameters - require(newParams.timeoutProposeMs > 0, InvalidTimeoutProposeMs()); - require(newParams.timeoutProposeDeltaMs > 0, InvalidTimeoutProposeDeltaMs()); - require(newParams.timeoutPrevoteMs > 0, InvalidTimeoutPrevoteMs()); - require(newParams.timeoutPrevoteDeltaMs > 0, InvalidTimeoutPrevoteDeltaMs()); - require(newParams.timeoutPrecommitMs > 0, InvalidTimeoutPrecommitMs()); - require(newParams.timeoutPrecommitDeltaMs > 0, InvalidTimeoutPrecommitDeltaMs()); - require(newParams.timeoutRebroadcastMs > 0, InvalidTimeoutRebroadcastMs()); - - ProtocolConfigStorage storage $ = _getProtocolConfigStorage(); - $.consensusParams = newParams; - emit ConsensusParamsUpdated(newParams); - } - - /** - * @notice Updates only the blockGasLimit parameter - * @dev Only callable by controller when not paused - * @param newBlockGasLimit The new block gas limit (must be > 0) - */ - function updateBlockGasLimit(uint256 newBlockGasLimit) external override onlyController whenNotPaused { - require(newBlockGasLimit > 0, InvalidBlockGasLimit()); - - ProtocolConfigStorage storage $ = _getProtocolConfigStorage(); - $.feeParams.blockGasLimit = newBlockGasLimit; - emit FeeParamsUpdated($.feeParams); - } - - /** - * @notice Updates only the timeoutProposeMs parameter - * @dev Only callable by controller when not paused - * @param newTimeoutProposeMs The new timeout propose in milliseconds (must be > 0) - */ - function updateTimeoutProposeMs(uint16 newTimeoutProposeMs) external override onlyController whenNotPaused { - require(newTimeoutProposeMs > 0, InvalidTimeoutProposeMs()); - - ProtocolConfigStorage storage $ = _getProtocolConfigStorage(); - $.consensusParams.timeoutProposeMs = newTimeoutProposeMs; - emit ConsensusParamsUpdated($.consensusParams); - } -} diff --git a/contracts/src/protocol-config/interfaces/IProtocolConfig.sol b/contracts/src/protocol-config/interfaces/IProtocolConfig.sol deleted file mode 100644 index 6ef01df..0000000 --- a/contracts/src/protocol-config/interfaces/IProtocolConfig.sol +++ /dev/null @@ -1,94 +0,0 @@ -// Copyright 2025 Circle Internet Group, Inc. All rights reserved. -// -// SPDX-License-Identifier: Apache-2.0 -// -// Licensed under the Apache License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. -// You may obtain a copy of the License at -// -// http://www.apache.org/licenses/LICENSE-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// See the License for the specific language governing permissions and -// limitations under the License. -pragma solidity ^0.8.29; - -/** - * @title IProtocolConfig - */ -interface IProtocolConfig { - // ============ Structs ============ - - struct FeeParams { - uint64 alpha; - uint64 kRate; - uint64 inverseElasticityMultiplier; // target gas limit in basis points (e.g., 5000 for 50.00%, range: 0-10000) - uint256 minBaseFee; - uint256 maxBaseFee; - uint256 blockGasLimit; - } - - struct ConsensusParams { - uint16 timeoutProposeMs; - uint16 timeoutProposeDeltaMs; - uint16 timeoutPrevoteMs; - uint16 timeoutPrevoteDeltaMs; - uint16 timeoutPrecommitMs; - uint16 timeoutPrecommitDeltaMs; - uint16 timeoutRebroadcastMs; - /// @dev Target block time (setting to 0 disables the block time control mechanism) - uint16 targetBlockTimeMs; - } - - /* Events */ - - /// @dev Emitted each time the controller updates fee parameters. - event FeeParamsUpdated(FeeParams params); - - /// @dev Emitted each time the controller updates consensus parameters. - event ConsensusParamsUpdated(ConsensusParams params); - - /* READ-ONLY API */ - - /// @notice Returns the latest fee parameters. - function feeParams() external view returns (FeeParams memory params); - - /// @notice Returns the latest consensus parameters. - function consensusParams() external view returns (ConsensusParams memory params); - - /* MUTATIVE API */ - - /** - * @notice Propose a new set of fee parameters. - * @dev Access – `onlyController` in implementation. - * @dev Access - `whenNotPaused` in implementation. - * @param newParams Complete parameter bundle. - */ - function updateFeeParams(FeeParams calldata newParams) external; - - /** - * @notice Propose a new set of consensus parameters. - * @dev Access – `onlyController` in implementation. - * @dev Access - `whenNotPaused` in implementation. - * @param newParams Complete parameter bundle. - */ - function updateConsensusParams(ConsensusParams calldata newParams) external; - - /** - * @notice Update only the blockGasLimit in fee params. - * @dev Access – `onlyController` in implementation. - * @dev Access - `whenNotPaused` in implementation. - * @param newBlockGasLimit The new block gas limit (must be > 0). - */ - function updateBlockGasLimit(uint256 newBlockGasLimit) external; - - /** - * @notice Update only the timeoutProposeMs in consensus params. - * @dev Access – `onlyController` in implementation. - * @dev Access - `whenNotPaused` in implementation. - * @param newTimeoutProposeMs The new timeout for propose (must be > 0). - */ - function updateTimeoutProposeMs(uint16 newTimeoutProposeMs) external; -} diff --git a/contracts/src/protocol-config/roles/Controller.sol b/contracts/src/protocol-config/roles/Controller.sol deleted file mode 100644 index 306377b..0000000 --- a/contracts/src/protocol-config/roles/Controller.sol +++ /dev/null @@ -1,111 +0,0 @@ -// Copyright 2025 Circle Internet Group, Inc. All rights reserved. -// -// SPDX-License-Identifier: Apache-2.0 -// -// Licensed under the Apache License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. -// You may obtain a copy of the License at -// -// http://www.apache.org/licenses/LICENSE-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// See the License for the specific language governing permissions and -// limitations under the License. - -pragma solidity ^0.8.29; - -import {Ownable2StepUpgradeable} from "@openzeppelin/contracts-upgradeable/access/Ownable2StepUpgradeable.sol"; - -/** - * @title Controller - * @notice Base contract which allows children to manage controller role for protocol configuration updates - * @dev Uses ERC-7201 namespaced storage pattern for upgrade safety. - */ -abstract contract Controller is Ownable2StepUpgradeable { - // ============ Errors ============ - - /// @notice Thrown when controller address is zero - error ZeroControllerAddress(); - - /// @notice Thrown when caller is not the controller - error CallerIsNotController(); - - // ============ Storage (ERC-7201) ============ - - /// @custom:storage-location erc7201:arc.storage.ProtocolConfigController - struct ProtocolConfigControllerStorage { - /// @notice Current controller address (can call update functions) - address controller; - } - - // keccak256(abi.encode(uint256(keccak256("arc.storage.ProtocolConfigController")) - 1)) & ~bytes32(uint256(0xff)) - bytes32 private constant PROTOCOL_CONFIG_CONTROLLER_STORAGE_LOCATION = - 0x958f8fec699b51a1249f513eceda5429078000657f74abd1721bba363087af00; - - function _getProtocolConfigControllerStorage() internal pure returns (ProtocolConfigControllerStorage storage $) { - assembly { - $.slot := PROTOCOL_CONFIG_CONTROLLER_STORAGE_LOCATION - } - } - - /// @custom:oz-upgrades-unsafe-allow constructor - constructor() { - // Verify storage slot calculation is correct - assert( - PROTOCOL_CONFIG_CONTROLLER_STORAGE_LOCATION - == keccak256(abi.encode(uint256(keccak256("arc.storage.ProtocolConfigController")) - 1)) & ~bytes32(uint256(0xff)) - ); - } - - // ============ Events ============ - - /** - * @notice Emitted when controller is updated - * @param newController new controller address - */ - event ControllerUpdated(address indexed newController); - - // ============ Modifiers ============ - - /** - * @dev Modifier to restrict access to controller only - */ - modifier onlyController() { - _onlyController(); - _; - } - - // ============ Functions ============ - - /** - * @dev Internal function to check if caller is controller - */ - function _onlyController() internal view { - ProtocolConfigControllerStorage storage $ = _getProtocolConfigControllerStorage(); - require(msg.sender == $.controller, CallerIsNotController()); - } - - /** - * @notice Returns the current controller address - * @return The address of the current controller - */ - function controller() public view virtual returns (address) { - ProtocolConfigControllerStorage storage $ = _getProtocolConfigControllerStorage(); - return $.controller; - } - - /** - * @notice Updates the controller address - * @param newController The new controller address (cannot be zero address) - * @dev Only callable by owner - */ - function updateController(address newController) external virtual onlyOwner { - require(newController != address(0), ZeroControllerAddress()); - - ProtocolConfigControllerStorage storage $ = _getProtocolConfigControllerStorage(); - $.controller = newController; - emit ControllerUpdated(newController); - } -} diff --git a/contracts/src/proxy/AdminUpgradeableProxy.sol b/contracts/src/proxy/AdminUpgradeableProxy.sol deleted file mode 100644 index 21976e5..0000000 --- a/contracts/src/proxy/AdminUpgradeableProxy.sol +++ /dev/null @@ -1,100 +0,0 @@ -// Copyright 2025 Circle Internet Group, Inc. All rights reserved. -// -// SPDX-License-Identifier: Apache-2.0 -// -// Licensed under the Apache License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. -// You may obtain a copy of the License at -// -// http://www.apache.org/licenses/LICENSE-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// See the License for the specific language governing permissions and -// limitations under the License. -pragma solidity ^0.8.29; - -import {ERC1967Proxy} from "@openzeppelin/contracts/proxy/ERC1967/ERC1967Proxy.sol"; -import {ERC1967Utils} from "@openzeppelin/contracts/proxy/ERC1967/ERC1967Utils.sol"; - -/** - * @title AdminUpgradeableProxy - * @notice This contract combines an upgradeable proxy with an authorization - * mechanism for administrative tasks. - * - * @dev Forked from https://github.com/OpenZeppelin/openzeppelin-contracts/blob/7b74442c5e87ea51dde41c7f18a209fa5154f1a4/contracts/proxy/transparent/TransparentUpgradeableProxy.sol - * Modifications (7/31/2025): - * - Remove the dependency on ProxyAdmin contract. - * - Add public view functions admin() and implementation() using ERC1967Utils. - * - Remove overridden _fallback() implementation, removing the ifAdmin check before delegating to the implementation. - * - Add explicit public upgradeTo() and upgradeToAndCall() functions with onlyAdmin access. - */ -contract AdminUpgradeableProxy is ERC1967Proxy { - /** - * @dev Explicit admin gate: non-admins are forwarded via `_fallback()` while - * admins enter the function body. Keeping the branch local to the modifier - * (instead of relying on `_fallback()` never returning) makes the access - * control intent clear and resilient to future changes. - * - * forge-lint: `unwrapped-modifier-logic` is suppressed because this inline - * form best documents the behavior; wrapping it in a helper would satisfy - * the lint but obscure the rationale. - */ - /// forge-lint: disable-next-item(unwrapped-modifier-logic) - modifier onlyAdmin() { - if (msg.sender == ERC1967Utils.getAdmin()) { - _; - } else { - _fallback(); - } - } - - /** - * @dev Initializes an upgradeable proxy managed by `_admin`, backed by the implementation at `_logic`, and - * optionally initialized with `_data`. - */ - constructor(address _logic, address _admin, bytes memory _data) payable ERC1967Proxy(_logic, _data) { - // Set the storage value and emit an event for ERC-1967 compatibility - ERC1967Utils.changeAdmin(_admin); - } - - /** - * @dev Upgrade the implementation of the proxy. - * @dev Only the admin can call this function; other callers are delegated - */ - function upgradeTo(address newImplementation) external virtual onlyAdmin { - ERC1967Utils.upgradeToAndCall(newImplementation, ""); - } - - /** - * @dev Upgrade the implementation of the proxy and call a function on the new implementation. - * @dev Only the admin can call this function; other callers are delegated - */ - function upgradeToAndCall(address newImplementation, bytes calldata data) external payable virtual onlyAdmin { - ERC1967Utils.upgradeToAndCall(newImplementation, data); - } - - /** - * @dev Changes the admin of the proxy. - * - * Emits an {IERC1967-AdminChanged} event. - */ - function changeAdmin(address newAdmin) external virtual onlyAdmin { - ERC1967Utils.changeAdmin(newAdmin); - } - - /** - * @dev Returns the current implementation address. - */ - function implementation() external view virtual returns (address) { - return ERC1967Utils.getImplementation(); - } - - /** - * @dev Returns the current admin. - */ - function admin() external view virtual returns (address) { - return ERC1967Utils.getAdmin(); - } -} diff --git a/contracts/src/validator-manager/PermissionedValidatorManager.sol b/contracts/src/validator-manager/PermissionedValidatorManager.sol deleted file mode 100644 index d493e90..0000000 --- a/contracts/src/validator-manager/PermissionedValidatorManager.sol +++ /dev/null @@ -1,165 +0,0 @@ -// Copyright 2025 Circle Internet Group, Inc. All rights reserved. -// -// SPDX-License-Identifier: Apache-2.0 -// -// Licensed under the Apache License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. -// You may obtain a copy of the License at -// -// http://www.apache.org/licenses/LICENSE-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// See the License for the specific language governing permissions and -// limitations under the License. - -pragma solidity ^0.8.29; - -import {IPermissionedValidatorManager} from "./interfaces/IPermissionedValidatorManager.sol"; -import {IValidatorRegistry, Validator} from "./interfaces/IValidatorRegistry.sol"; -import {Controller} from "./roles/Controller.sol"; -import {ValidatorRegisterer} from "./roles/ValidatorRegisterer.sol"; -import {Pausable} from "../common/roles/Pausable.sol"; -import {Ownable2StepUpgradeable} from "@openzeppelin/contracts-upgradeable/access/Ownable2StepUpgradeable.sol"; - -/** - * @title PermissionedValidatorManager - * @notice Manages validator registration with role-based access control using a three-tier architecture: - * Owner → Controllers → ValidatorRegisterers → Validators - * @dev Implements {IPermissionedValidatorManager} with delegation and access control patterns. - * @dev Controller and ValidatorRegisterer roles use ERC-7201 storage pattern - */ -contract PermissionedValidatorManager is IPermissionedValidatorManager, Controller, ValidatorRegisterer, Pausable { - // ============ Errors ============ - - /// @notice Thrown when owner address is zero - error ZeroOwnerAddress(); - /// @notice Thrown when voting power exceeds controller limitation - error VotingPowerExceedsLimit(uint64 limit); - - // ============ Constants ============ - - /// @dev The underlying validator registry that this contract manages - /// @dev This contract should be set as the owner of the registry for proper access control - IValidatorRegistry public immutable REGISTRY; - /// @notice Default voting power assigned to newly registered validators - uint64 public constant DEFAULT_VOTING_POWER = 0; - - // ============ Events ============ - - /** - * @notice Emitted when the underlying Registry Ownership transfer has started. - */ - event RegistryOwnerTransferStarted(address indexed newOwner); - - /** - * @notice Emitted when the underlying Registry Ownership transfer has completed. - */ - event RegistryOwnerTransferCompleted(); - - /// @custom:oz-upgrades-unsafe-allow constructor - constructor(IValidatorRegistry validatorRegistry) { - REGISTRY = validatorRegistry; - _disableInitializers(); - } - - /** - * @notice Initialize the PermissionedValidatorManager (for proxy deployment) - * @param initialOwner The initial owner of the contract - * @param initialPauser The address allowed to pause/unpause - */ - function initialize(address initialOwner, address initialPauser) external initializer { - require(initialOwner != address(0), ZeroOwnerAddress()); - require(initialPauser != address(0), ZeroPauserAddress()); - - __Ownable2Step_init(); - _transferOwnership(initialOwner); - - PausableStorage storage $ = _getPausableStorage(); - $.pauser = initialPauser; - } - - // ============ IPermissionedValidatorManager Implementation ============ - /** - * @notice See {IPermissionedValidatorManager-registerValidator}. - */ - function registerValidator(bytes memory publicKey) external onlyValidatorRegisterer whenNotPaused returns (uint256) { - // Delegate to the underlying registry with a default voting power - return REGISTRY.registerValidator(publicKey, DEFAULT_VOTING_POWER); - } - - /** - * @notice See {IPermissionedValidatorManager-activateValidator}. - */ - function activateValidator() external onlyController whenNotPaused { - ControllerStorage storage $ = _getControllerStorage(); - // Get the registrationId that this controller is assigned to manage - uint256 registrationId = $.registrationOf[msg.sender]; - - // Delegate to the underlying registry with the controller's assigned validator - REGISTRY.activateValidator(registrationId); - } - - /** - * @notice See {IPermissionedValidatorManager-removeValidator}. - */ - function removeValidator() external onlyController whenNotPaused { - ControllerStorage storage $ = _getControllerStorage(); - // Get the registrationId that this controller is assigned to manage - uint256 registrationId = $.registrationOf[msg.sender]; - - // Delegate to the underlying registry with the controller's assigned validator - REGISTRY.removeValidator(registrationId); - } - - /** - * @notice See {IPermissionedValidatorManager-updateValidatorVotingPower}. - */ - function updateValidatorVotingPower(uint64 newVotingPower) external onlyController whenNotPaused { - ControllerStorage storage $ = _getControllerStorage(); - // Get the registrationId that this controller is assigned to manage - uint256 registrationId = $.registrationOf[msg.sender]; - - uint64 votingPowerLimit = $.votingPowerLimitOf[msg.sender]; - // Enforce per-controller voting power limitation - if (newVotingPower > votingPowerLimit) { - revert VotingPowerExceedsLimit(votingPowerLimit); - } - - // Delegate to the underlying registry with the controller's assigned validator - REGISTRY.updateValidatorVotingPower(registrationId, newVotingPower); - } - - /** - * @notice See {IPermissionedValidatorManager-getValidator}. - */ - function getValidator(address controller) external view returns (Validator memory) { - ControllerStorage storage $ = _getControllerStorage(); - // Get the registrationId that this controller is assigned to manage - uint256 registrationId = $.registrationOf[controller]; - - // Delegate to the underlying registry with the controller's assigned validator - return REGISTRY.getValidator(registrationId); - } - - /** - * @notice Updates the underlying Registry Owner - */ - function transferRegistryOwner(address newOwner) external onlyOwner { - require(newOwner != address(0), ZeroOwnerAddress()); - - Ownable2StepUpgradeable(address(REGISTRY)).transferOwnership(newOwner); - emit RegistryOwnerTransferStarted(newOwner); - } - - /** - * @notice Accepts ownership of the ValidatorRegistry - * @dev Call this after ValidatorRegistry.transferOwnership(thisContract) has been called - * @dev This contract must be the pending owner of the ValidatorRegistry - */ - function acceptRegistryOwnership() external onlyOwner { - Ownable2StepUpgradeable(address(REGISTRY)).acceptOwnership(); - emit RegistryOwnerTransferCompleted(); - } -} diff --git a/contracts/src/validator-manager/ValidatorRegistry.sol b/contracts/src/validator-manager/ValidatorRegistry.sol deleted file mode 100644 index be248d9..0000000 --- a/contracts/src/validator-manager/ValidatorRegistry.sol +++ /dev/null @@ -1,269 +0,0 @@ -// Copyright 2025 Circle Internet Group, Inc. All rights reserved. -// -// SPDX-License-Identifier: Apache-2.0 -// -// Licensed under the Apache License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. -// You may obtain a copy of the License at -// -// http://www.apache.org/licenses/LICENSE-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// See the License for the specific language governing permissions and -// limitations under the License. -pragma solidity ^0.8.29; - -import {IValidatorRegistry, Validator, ValidatorStatus} from "./interfaces/IValidatorRegistry.sol"; -import {Ownable2StepUpgradeable} from "@openzeppelin/contracts-upgradeable/access/Ownable2StepUpgradeable.sol"; -import {EnumerableSet} from "@openzeppelin/contracts/utils/structs/EnumerableSet.sol"; - -/** - * @title ValidatorRegistry - * @notice Registry for managing validator registrations - */ -contract ValidatorRegistry is IValidatorRegistry, Ownable2StepUpgradeable { - using EnumerableSet for EnumerableSet.UintSet; - - // ============ Errors ============ - - /// @notice Thrown when the validator public key is already registered - error ValidatorAlreadyRegistered(bytes32 publicKeyHash); - - /// @notice Thrown when a referenced registrationId is invalid - error InvalidRegistrationId(uint256 registrationId); - - /// @notice Thrown when a validator is not in the expected status for an operation - error InvalidValidatorStatus(uint256 registrationId, ValidatorStatus status); - - /// @notice Thrown for invalid public key formats - error InvalidPublicKeyFormat(); - - /// @notice Thrown for invalid voting power updates - error InvalidVotingPowerUpdate(); - - /// @notice Thrown when an update would leave the active validator set without any voting power - error InvalidValidatorSet(); - - // ============ Type Declarations ============ - - /// @custom:storage-location erc7201:arc.storage.ValidatorRegistry - struct ValidatorRegistryStorage { - /// @notice Maps registration IDs to validator data including status, public key, and voting power - mapping(uint256 => Validator) _validatorsByRegistrationId; - /// @notice Enumerable set to track active validator registrationIds - EnumerableSet.UintSet _activeValidatorRegistrations; - /// @notice To check for duplicate registered validator public keys - mapping(bytes32 => bool) _registeredPublicKeys; - /// @notice Counter tracking the next available registration ID for new validator registrations, starting from 1 - uint256 _nextRegistrationId; - } - - // ============ Constants ============ - - // Ed25519 public key length in bytes - uint256 constant ED25519_PUBLIC_KEY_LENGTH = 32; - - // keccak256(abi.encode(uint256(keccak256("arc.storage.ValidatorRegistry")) - 1)) & ~bytes32(uint256(0xff)); - bytes32 public constant VALIDATOR_REGISTRY_STORAGE_LOCATION = - 0xb58da0dce03316992faea3e12c60705b8ac05a309e27e3bc8421e5b271c9d200; - - /** - * @dev Constructor for implementation contract - disables initializers to prevent misuse - */ - /// @custom:oz-upgrades-unsafe-allow constructor - constructor() { - // Verify storage slot calculation is correct - assert( - VALIDATOR_REGISTRY_STORAGE_LOCATION - == keccak256(abi.encode(uint256(keccak256("arc.storage.ValidatorRegistry")) - 1)) - & ~bytes32(uint256(0xff)) - ); - - // Disable initializers on implementation contract - _disableInitializers(); - } - - // ============ External Functions ============ - - /** - * @inheritdoc IValidatorRegistry - */ - function registerValidator(bytes memory publicKey, uint64 votingPower) - external - onlyOwner - returns (uint256 registrationId) - { - // Validate public key length - require(publicKey.length == ED25519_PUBLIC_KEY_LENGTH, InvalidPublicKeyFormat()); - - ValidatorRegistryStorage storage $ = _getValidatorRegistryStorage(); - - bytes32 publicKeyHash; - assembly { - publicKeyHash := keccak256(add(publicKey, 0x20), mload(publicKey)) - } - // Check if public key is already registered - require(!$._registeredPublicKeys[publicKeyHash], ValidatorAlreadyRegistered(publicKeyHash)); - - registrationId = $._nextRegistrationId++; - - $._validatorsByRegistrationId[registrationId] = - Validator({status: ValidatorStatus.Registered, publicKey: publicKey, votingPower: votingPower}); - $._registeredPublicKeys[publicKeyHash] = true; - // Emit event for validator registration - emit ValidatorRegistered(registrationId, votingPower, publicKey); - return registrationId; - } - - /** - * @inheritdoc IValidatorRegistry - */ - function activateValidator(uint256 registrationId) external onlyOwner { - ValidatorRegistryStorage storage $ = _getValidatorRegistryStorage(); - Validator storage validatorInfo = $._validatorsByRegistrationId[registrationId]; - require(validatorInfo.status != ValidatorStatus.Unknown, InvalidRegistrationId(registrationId)); - require( - validatorInfo.status == ValidatorStatus.Registered, - InvalidValidatorStatus(registrationId, validatorInfo.status) - ); - - validatorInfo.status = ValidatorStatus.Active; - $._activeValidatorRegistrations.add(registrationId); - emit ValidatorActivated(registrationId, validatorInfo.votingPower); - } - - /** - * @inheritdoc IValidatorRegistry - */ - function removeValidator(uint256 registrationId) external onlyOwner { - ValidatorRegistryStorage storage $ = _getValidatorRegistryStorage(); - Validator storage validatorInfo = $._validatorsByRegistrationId[registrationId]; - - // Ensure validator is known - require(validatorInfo.status != ValidatorStatus.Unknown, InvalidRegistrationId(registrationId)); - - if (validatorInfo.status == ValidatorStatus.Active && validatorInfo.votingPower > 0) { - require(_countActiveValidatorsWithPositiveVotingPower($) > 1, InvalidValidatorSet()); - } - - bytes memory publicKey = validatorInfo.publicKey; - bytes32 publicKeyHash; - assembly { - publicKeyHash := keccak256(add(publicKey, 0x20), mload(publicKey)) - } - uint64 votingPower = validatorInfo.votingPower; - - // Prune state - $._activeValidatorRegistrations.remove(registrationId); - delete $._validatorsByRegistrationId[registrationId]; - delete $._registeredPublicKeys[publicKeyHash]; - - // Emit event - emit ValidatorRemoved(registrationId, votingPower); - } - - /** - * @inheritdoc IValidatorRegistry - * @dev Setting voting power to 0 effectively deactivates the validator without removing it from storage. - * The validator remains registered but will have no influence in consensus. - */ - function updateValidatorVotingPower(uint256 registrationId, uint64 newVotingPower) external onlyOwner { - ValidatorRegistryStorage storage $ = _getValidatorRegistryStorage(); - Validator storage validatorInfo = $._validatorsByRegistrationId[registrationId]; - - // Ensure validator is known - require(validatorInfo.status != ValidatorStatus.Unknown, InvalidRegistrationId(registrationId)); - - uint64 oldVotingPower = validatorInfo.votingPower; - require(oldVotingPower != newVotingPower, InvalidVotingPowerUpdate()); - - if (validatorInfo.status == ValidatorStatus.Active && oldVotingPower > 0 && newVotingPower == 0) { - require(_countActiveValidatorsWithPositiveVotingPower($) > 1, InvalidValidatorSet()); - } - - // Update voting power (setting to 0 effectively deactivates the validator) - validatorInfo.votingPower = newVotingPower; - - // Emit event - emit ValidatorVotingPowerUpdated(registrationId, oldVotingPower, newVotingPower); - } - - // ============ View Functions ============ - - /** - * @notice Retrieves validator information by registration ID - * @param registrationId The unique registration ID of the validator - * @return Validator memory struct containing validator details including status, public key, and voting power - */ - function getValidator(uint256 registrationId) external view returns (Validator memory) { - ValidatorRegistryStorage storage $ = _getValidatorRegistryStorage(); - return $._validatorsByRegistrationId[registrationId]; - } - - /** - * @notice Returns all active validators - * @dev This function is called by the consensus layer (malachite) to retrieve the current active validator set - */ - function getActiveValidatorSet() external view returns (Validator[] memory activeValidators) { - ValidatorRegistryStorage storage $ = _getValidatorRegistryStorage(); - - uint256 activeCount = $._activeValidatorRegistrations.length(); - activeValidators = new Validator[](activeCount); - - // Gas optimization: ++i increments directly without the temporary copy - for (uint256 i = 0; i < activeCount; ++i) { - uint256 registrationId = $._activeValidatorRegistrations.at(i); - activeValidators[i] = $._validatorsByRegistrationId[registrationId]; - } - - return activeValidators; - } - - /** - * @notice Returns count of active validators with voting power > 0 - * @return count Number of active validators with positive voting power - */ - function getActiveValidatorsWithPositiveVotingPowerCount() external view override returns (uint256 count) { - ValidatorRegistryStorage storage $ = _getValidatorRegistryStorage(); - return _countActiveValidatorsWithPositiveVotingPower($); - } - - /** - * @notice Returns the next registration ID that will be assigned - * @return The next available registration ID - */ - function getNextRegistrationId() external view returns (uint256) { - ValidatorRegistryStorage storage $ = _getValidatorRegistryStorage(); - return $._nextRegistrationId; - } - - // ============ Internal Functions ============ - - /// @dev Counts active validators with voting power > 0 by scanning the active registration set. - function _countActiveValidatorsWithPositiveVotingPower(ValidatorRegistryStorage storage $) - internal - view - returns (uint256 count) - { - uint256 registrationCount = $._activeValidatorRegistrations.length(); - - for (uint256 i = 0; i < registrationCount; ++i) { - uint256 activeRegistrationId = $._activeValidatorRegistrations.at(i); - if ($._validatorsByRegistrationId[activeRegistrationId].votingPower > 0) { - ++count; - } - } - } - - /** - * @dev Returns a storage pointer to the ValidatorRegistry storage struct using ERC-7201 pattern. - * This prevents storage collisions in upgradeable contracts by using a deterministic slot. - */ - function _getValidatorRegistryStorage() internal pure returns (ValidatorRegistryStorage storage $) { - assembly { - $.slot := VALIDATOR_REGISTRY_STORAGE_LOCATION - } - } -} diff --git a/contracts/src/validator-manager/interfaces/IPermissionedValidatorManager.sol b/contracts/src/validator-manager/interfaces/IPermissionedValidatorManager.sol deleted file mode 100644 index 07ebafe..0000000 --- a/contracts/src/validator-manager/interfaces/IPermissionedValidatorManager.sol +++ /dev/null @@ -1,56 +0,0 @@ -// Copyright 2025 Circle Internet Group, Inc. All rights reserved. -// -// SPDX-License-Identifier: Apache-2.0 -// -// Licensed under the Apache License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. -// You may obtain a copy of the License at -// -// http://www.apache.org/licenses/LICENSE-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// See the License for the specific language governing permissions and -// limitations under the License. - -pragma solidity ^0.8.29; - -import {Validator} from "./IValidatorRegistry.sol"; - -/** - * @dev PermissionedValidatorManager interface meant to be used as the owner of an - * {IValidatorRegistry} contract instance that only allows modifications to the validator set - * to be initiated by a fixed address. - */ -interface IPermissionedValidatorManager { - /** - * @notice Register validator. - * @param publicKey The Ed25519 public key of the validator. - * @return registrationId The unique identifier for the validator registration. - */ - function registerValidator(bytes memory publicKey) external returns (uint256 registrationId); - - /** - * @notice Activate validator. Only callable by the controller. - */ - function activateValidator() external; - - /** - * @notice Remove validator. Only callable by the controller. - */ - function removeValidator() external; - - /** - * @notice Update a validator voting power. Only callable by the controller. - * @param newVotingPower The new voting power for the validator. - */ - function updateValidatorVotingPower(uint64 newVotingPower) external; - - /** - * @notice Get validator information for the specified controller. - * @param controller The controller address to get validator information for. - * @return validator The validator struct containing status, public key, and voting power. - */ - function getValidator(address controller) external view returns (Validator memory validator); -} diff --git a/contracts/src/validator-manager/interfaces/IValidatorRegistry.sol b/contracts/src/validator-manager/interfaces/IValidatorRegistry.sol deleted file mode 100644 index 42ef2e6..0000000 --- a/contracts/src/validator-manager/interfaces/IValidatorRegistry.sol +++ /dev/null @@ -1,82 +0,0 @@ -// Copyright 2025 Circle Internet Group, Inc. All rights reserved. -// -// SPDX-License-Identifier: Apache-2.0 -// -// Licensed under the Apache License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. -// You may obtain a copy of the License at -// -// http://www.apache.org/licenses/LICENSE-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// See the License for the specific language governing permissions and -// limitations under the License. - -pragma solidity ^0.8.29; - -/// @notice Validator status. -enum ValidatorStatus { - Unknown, // Initial state or validator that hasn't been registered - Registered, // Validator has been registered but not yet active in the network - Active // Validator is currently active and participating in consensus - -} - -/** - * @notice Contains the status of a Validator. - * @param status The current status of the validator (Unknown, Registered, or Active) - * @param publicKey The public key of the validator - * @param votingPower The current voting power of the validator. - */ -struct Validator { - ValidatorStatus status; - bytes publicKey; - uint64 votingPower; -} - -/** - * @dev ValidatorRegistry interface - */ -interface IValidatorRegistry { - // ============ Events ============ - /// @notice Emitted when a validator is registered. - event ValidatorRegistered(uint256 indexed registrationId, uint64 votingPower, bytes publicKey); - - /// @notice Emitted when a validator is activated. - event ValidatorActivated(uint256 indexed registrationId, uint64 votingPower); - - /// @notice Emitted when a validator is removed. - event ValidatorRemoved(uint256 indexed registrationId, uint64 votingPower); - - /// @notice Emitted when a validator votingPower update is updated. - event ValidatorVotingPowerUpdated(uint256 indexed registrationId, uint64 oldVotingPower, uint64 newVotingPower); - - // ============ Functions ============ - function registerValidator(bytes memory publicKey, uint64 votingPower) external returns (uint256 registrationId); - - function activateValidator(uint256 registrationId) external; - - function removeValidator(uint256 registrationId) external; - - function updateValidatorVotingPower(uint256 registrationId, uint64 newVotingPower) external; - - /** - * @notice Returns the validator registration info, including the validator, public key, and voting power. - * @param registrationId The unique identifier for the validator registration - */ - function getValidator(uint256 registrationId) external view returns (Validator memory); - - /** - * @notice Returns all validators that are currently active - * @return activeValidators Array of active validator structs - */ - function getActiveValidatorSet() external view returns (Validator[] memory activeValidators); - - /** - * @notice Returns count of active validators with voting power > 0 - * @return count Number of active validators with positive voting power - */ - function getActiveValidatorsWithPositiveVotingPowerCount() external view returns (uint256 count); -} diff --git a/contracts/src/validator-manager/roles/Controller.sol b/contracts/src/validator-manager/roles/Controller.sol deleted file mode 100644 index 886b33b..0000000 --- a/contracts/src/validator-manager/roles/Controller.sol +++ /dev/null @@ -1,212 +0,0 @@ -// Copyright 2025 Circle Internet Group, Inc. All rights reserved. -// -// SPDX-License-Identifier: Apache-2.0 -// -// Licensed under the Apache License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. -// You may obtain a copy of the License at -// -// http://www.apache.org/licenses/LICENSE-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// See the License for the specific language governing permissions and -// limitations under the License. - -pragma solidity ^0.8.29; - -import {Ownable2StepUpgradeable} from "@openzeppelin/contracts-upgradeable/access/Ownable2StepUpgradeable.sol"; - -/** - * @title Controller - * @notice Base contract which allows children to adjust the weight, or remove, an individual validator that it controls. - * Multiple controllers can control a single validator. - * @dev Uses ERC-7201 storage pattern for upgradeable contracts - */ -abstract contract Controller is Ownable2StepUpgradeable { - // ============ Errors ============ - - /// @notice Thrown when controller address is zero - error ZeroControllerAddress(); - - /// @notice Thrown when caller is not a controller - error CallerIsNotController(); - - /// @notice Thrown when registration ID is zero - error RegistrationIdIsZero(); - - /// @notice Thrown when controller is already configured - error ControllerAlreadyConfigured(); - - /// @notice Thrown when controller is not configured - error ControllerNotConfigured(); - - // ============ Type Declarations ============ - - /// @custom:storage-location erc7201:arc.storage.PVMController - struct ControllerStorage { - /// @notice Records the registration ID for each controller address - mapping(address => uint256) registrationOf; - /// @notice Voting power limit each controller can set for their validator - mapping(address => uint64) votingPowerLimitOf; - } - - // ============ Constants ============ - - // keccak256(abi.encode(uint256(keccak256("arc.storage.PVMController")) - 1)) & ~bytes32(uint256(0xff)) - bytes32 public constant CONTROLLER_STORAGE_LOCATION = - 0xe90ec3add3e251bfbe914c9e482b511e91a3b187718c1dc10223f64a8a644a00; - - // ============ Constructor ============ - - /** - * @dev Constructor verifies the storage location calculation - */ - /// @custom:oz-upgrades-unsafe-allow constructor - constructor() { - // Verify storage slot calculation is correct - assert( - CONTROLLER_STORAGE_LOCATION - == keccak256(abi.encode(uint256(keccak256("arc.storage.PVMController")) - 1)) & ~bytes32(uint256(0xff)) - ); - } - - // ============ Events ============ - /** - * @notice Emitted when controller is configured - * @param controller controller address - * @param registrationId registrationId of the validator - * @param votingPowerLimit voting power limit the controller can assign - */ - event ControllerConfigured(address indexed controller, uint256 indexed registrationId, uint64 votingPowerLimit); - - /** - * @notice Emitted when controller is removed - * @param controller controller address removed - */ - event ControllerRemoved(address indexed controller); - - /** - * @notice Emitted when controller voting power cap is updated - * @param controller controller address - * @param newVotingPowerLimit new voting power limit for the controller - */ - event VotingPowerLimitUpdated(address indexed controller, uint64 newVotingPowerLimit); - - // ============ Modifiers ============ - /** - * @dev Throws if called by any account other than the controller. - */ - modifier onlyController() { - _onlyController(); - _; - } - - // ============ Functions ============ - - /** - * @dev Internal function to check if caller is controller - */ - function _onlyController() internal view { - ControllerStorage storage $ = _getControllerStorage(); - require($.registrationOf[msg.sender] != 0, CallerIsNotController()); - } - /** - * @notice Assigns a controller to manage a registrationId - * @param controller The address to grant controller permissions - * @param registrationId The registration ID associated with the controller - * @param votingPowerLimit The limitation of voting power the controller can set - */ - function configureController(address controller, uint256 registrationId, uint64 votingPowerLimit) external onlyOwner { - require(registrationId != 0, RegistrationIdIsZero()); - require(controller != address(0), ZeroControllerAddress()); - - ControllerStorage storage $ = _getControllerStorage(); - // Prevent configuring a controller that is already configured - require($.registrationOf[controller] == 0, ControllerAlreadyConfigured()); - - $.registrationOf[controller] = registrationId; - $.votingPowerLimitOf[controller] = votingPowerLimit; - emit ControllerConfigured(controller, registrationId, votingPowerLimit); - } - - /** - * @notice Remove a controller - * @param controller The address to revoke controller permissions - */ - function removeController(address controller) external onlyOwner { - require(controller != address(0), ZeroControllerAddress()); - - ControllerStorage storage $ = _getControllerStorage(); - require($.registrationOf[controller] != 0, ControllerNotConfigured()); - - delete $.registrationOf[controller]; - delete $.votingPowerLimitOf[controller]; - emit ControllerRemoved(controller); - } - - /** - * @notice Check if an address is a controller - * @param controller The address to check - * @return True if the address is a controller, false otherwise - */ - function isController(address controller) external view returns (bool) { - ControllerStorage storage $ = _getControllerStorage(); - return $.registrationOf[controller] != 0; - } - - /** - * @notice Get the registration ID for a configured controller. - * @dev Reverts with ControllerNotConfigured if the address is not a controller. - * Use {isController} for a non-reverting configuration check. - * @param controller The address to get the registration ID for - * @return The registration ID for the controller - */ - function getRegistrationId(address controller) external view returns (uint256) { - ControllerStorage storage $ = _getControllerStorage(); - uint256 registrationId = $.registrationOf[controller]; - require(registrationId != 0, ControllerNotConfigured()); - return registrationId; - } - - /** - * @notice Get the voting power limit for a configured controller. - * @dev Reverts with ControllerNotConfigured if the address is not a controller. - * Use {isController} for a non-reverting configuration check. - * @param controller The address to get the limit for - * @return The voting power limit for the controller - */ - function getVotingPowerLimit(address controller) external view returns (uint64) { - ControllerStorage storage $ = _getControllerStorage(); - require($.registrationOf[controller] != 0, ControllerNotConfigured()); - return $.votingPowerLimitOf[controller]; - } - - /** - * @notice Update the voting power limit for a configured controller - * @param controller The controller address to update - * @param newVotingPowerLimit The new voting power limit to set - */ - function updateVotingPowerLimit(address controller, uint64 newVotingPowerLimit) external onlyOwner { - require(controller != address(0), ZeroControllerAddress()); - - ControllerStorage storage $ = _getControllerStorage(); - require($.registrationOf[controller] != 0, ControllerNotConfigured()); - - // It's valid to lower the limit even if current validator's voting power is above it. - $.votingPowerLimitOf[controller] = newVotingPowerLimit; - emit VotingPowerLimitUpdated(controller, newVotingPowerLimit); - } - - // ============ Internal Functions ============ - - /** - * @dev Returns a storage pointer to the Controller storage struct using ERC-7201 pattern. - */ - function _getControllerStorage() internal pure returns (ControllerStorage storage $) { - assembly { - $.slot := CONTROLLER_STORAGE_LOCATION - } - } -} diff --git a/contracts/src/validator-manager/roles/ValidatorRegisterer.sol b/contracts/src/validator-manager/roles/ValidatorRegisterer.sol deleted file mode 100644 index d91bfb6..0000000 --- a/contracts/src/validator-manager/roles/ValidatorRegisterer.sol +++ /dev/null @@ -1,148 +0,0 @@ -// Copyright 2025 Circle Internet Group, Inc. All rights reserved. -// -// SPDX-License-Identifier: Apache-2.0 -// -// Licensed under the Apache License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. -// You may obtain a copy of the License at -// -// http://www.apache.org/licenses/LICENSE-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// See the License for the specific language governing permissions and -// limitations under the License. - -pragma solidity ^0.8.29; - -import {Ownable2StepUpgradeable} from "@openzeppelin/contracts-upgradeable/access/Ownable2StepUpgradeable.sol"; - -/** - * @title ValidatorRegisterer - * @notice Can register new validators - * @dev Uses ERC-7201 storage pattern for upgradeable contracts - */ -abstract contract ValidatorRegisterer is Ownable2StepUpgradeable { - // ============ Errors ============ - - /// @notice Thrown when validator registerer address is zero - error ZeroValidatorRegistererAddress(); - - /// @notice Thrown when caller is not a validator registerer - error CallerIsNotValidatorRegisterer(); - - /// @notice Thrown when validator registerer is already added - error ValidatorRegistererAlreadyAdded(); - - /// @notice Thrown when validator registerer is not added - error ValidatorRegistererNotAdded(); - // ============ Type Declarations ============ - - /// @custom:storage-location erc7201:arc.storage.PVMValidatorRegisterer - struct ValidatorRegistererStorage { - /// @notice Role with permission to register new validators - mapping(address => bool) validatorRegisterers; - } - - // ============ Constants ============ - - // keccak256(abi.encode(uint256(keccak256("arc.storage.PVMValidatorRegisterer")) - 1)) & ~bytes32(uint256(0xff)) - bytes32 public constant VALIDATOR_REGISTERER_STORAGE_LOCATION = - 0x36c39aeb5f498ae36546fc14573b003abf87227a5a2df6caec16ee566f1ad800; - - // ============ Constructor ============ - - /** - * @dev Constructor verifies the storage location calculation - */ - /// @custom:oz-upgrades-unsafe-allow constructor - constructor() { - // Verify storage slot calculation is correct - assert( - VALIDATOR_REGISTERER_STORAGE_LOCATION - == keccak256(abi.encode(uint256(keccak256("arc.storage.PVMValidatorRegisterer")) - 1)) & ~bytes32(uint256(0xff)) - ); - } - - // ============ Events ============ - /** - * @notice Emitted when validatorRegisterer is set - * @param validatorRegisterer validator registerer address - */ - event ValidatorRegistererAdded(address indexed validatorRegisterer); - - /** - * @notice Emitted when validatorRegisterer is removed - * @param validatorRegisterer validator registerer address removed - */ - event ValidatorRegistererRemoved(address indexed validatorRegisterer); - - // ============ Modifiers ============ - /** - * @dev Throws if called by any account other than a validator registerer. - */ - modifier onlyValidatorRegisterer() { - _onlyValidatorRegisterer(); - _; - } - - // ============ Functions ============ - - /** - * @dev Internal function to check if caller is validator registerer - */ - function _onlyValidatorRegisterer() internal view { - ValidatorRegistererStorage storage $ = _getValidatorRegistererStorage(); - require($.validatorRegisterers[msg.sender], CallerIsNotValidatorRegisterer()); - } - /** - * @notice Add a new validator registerer - * @param validatorRegisterer The address to grant validator registerer permissions - */ - function addValidatorRegisterer(address validatorRegisterer) external onlyOwner { - require(validatorRegisterer != address(0), ZeroValidatorRegistererAddress()); - - ValidatorRegistererStorage storage $ = _getValidatorRegistererStorage(); - // Prevent adding a validatorRegisterer that is already added - require($.validatorRegisterers[validatorRegisterer] == false, ValidatorRegistererAlreadyAdded()); - - $.validatorRegisterers[validatorRegisterer] = true; - emit ValidatorRegistererAdded(validatorRegisterer); - } - - /** - * @notice Remove a validator registerer - * @param validatorRegisterer The address to revoke validator registerer permissions - */ - function removeValidatorRegisterer(address validatorRegisterer) external onlyOwner { - require(validatorRegisterer != address(0), ZeroValidatorRegistererAddress()); - - ValidatorRegistererStorage storage $ = _getValidatorRegistererStorage(); - require($.validatorRegisterers[validatorRegisterer], ValidatorRegistererNotAdded()); - - $.validatorRegisterers[validatorRegisterer] = false; - emit ValidatorRegistererRemoved(validatorRegisterer); - } - - /** - * @notice Check if an address is a validator registerer - * @param validatorRegisterer The address to check - * @return True if the address is a validator registerer, false otherwise - */ - function isValidatorRegisterer(address validatorRegisterer) external view returns (bool) { - ValidatorRegistererStorage storage $ = _getValidatorRegistererStorage(); - return $.validatorRegisterers[validatorRegisterer]; - } - - // ============ Internal Functions ============ - - /** - * @dev Returns a storage pointer to the ValidatorRegisterer storage struct using ERC-7201 pattern. - */ - function _getValidatorRegistererStorage() internal pure returns (ValidatorRegistererStorage storage $) { - assembly { - $.slot := VALIDATOR_REGISTERER_STORAGE_LOCATION - } - } -} diff --git a/contracts/test/Denylist.t.sol b/contracts/test/Denylist.t.sol deleted file mode 100644 index 4912373..0000000 --- a/contracts/test/Denylist.t.sol +++ /dev/null @@ -1,274 +0,0 @@ -// Copyright 2025 Circle Internet Group, Inc. All rights reserved. -// -// SPDX-License-Identifier: Apache-2.0 -// -// Licensed under the Apache License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. -// You may obtain a copy of the License at -// -// http://www.apache.org/licenses/LICENSE-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// See the License for the specific language governing permissions and -// limitations under the License. - -pragma solidity ^0.8.29; - -import {Test} from "forge-std/Test.sol"; -import {Denylist} from "../src/Denylist.sol"; -import {AdminUpgradeableProxy} from "../src/proxy/AdminUpgradeableProxy.sol"; -import {Initializable} from "@openzeppelin/contracts-upgradeable/proxy/utils/Initializable.sol"; -import {OwnableUpgradeable} from "@openzeppelin/contracts-upgradeable/access/OwnableUpgradeable.sol"; - -contract DenylistTest is Test { - event Denylisted(address indexed account); - event UnDenylisted(address indexed account); - event DenylisterAdded(address indexed account); - event DenylisterRemoved(address indexed account); - - address owner = address(10); - address denylister1 = address(20); - address denylister2 = address(30); - address alice = address(40); - address bob = address(50); - address stranger = address(60); - - Denylist denylistImpl; - Denylist denylist; - - function setUp() public { - denylistImpl = new Denylist(); - bytes memory initData = abi.encodeWithSelector(Denylist.initialize.selector, owner); - AdminUpgradeableProxy proxy = new AdminUpgradeableProxy(address(denylistImpl), owner, initData); - denylist = Denylist(address(proxy)); - } - - // ============ Initialize ============ - - function test_Initialize_SetsOwner() public view { - assertEq(denylist.owner(), owner); - } - - function test_Initialize_RevertsForZeroOwner() public { - Denylist impl = new Denylist(); - bytes memory initData = abi.encodeWithSelector(Denylist.initialize.selector, address(0)); - vm.expectRevert(Denylist.ZeroAddress.selector); - new AdminUpgradeableProxy(address(impl), owner, initData); - } - - function test_Initialize_SubsequentCallsRevert() public { - vm.expectRevert(Initializable.InvalidInitialization.selector); - denylist.initialize(owner); - } - - // ============ Ownable2Step ============ - - function test_Ownable2Step_TransferRequiresAcceptance() public { - vm.prank(owner); - denylist.transferOwnership(alice); - assertEq(denylist.owner(), owner); - assertEq(denylist.pendingOwner(), alice); - - vm.prank(alice); - denylist.acceptOwnership(); - assertEq(denylist.owner(), alice); - assertEq(denylist.pendingOwner(), address(0)); - } - - function test_Ownable2Step_OnlyPendingOwnerCanAccept() public { - vm.prank(owner); - denylist.transferOwnership(alice); - vm.expectRevert( - abi.encodeWithSelector(OwnableUpgradeable.OwnableUnauthorizedAccount.selector, stranger) - ); - vm.prank(stranger); - denylist.acceptOwnership(); - } - - // ============ Denylist / UnDenylist (onlyDenylister) ============ - - function test_Denylist_OnlyDenylister_Success() public { - vm.prank(owner); - denylist.addDenylister(denylister1); - - address[] memory accounts = new address[](2); - accounts[0] = alice; - accounts[1] = bob; - - vm.prank(denylister1); - vm.expectEmit(true, true, true, true); - emit Denylisted(alice); - vm.expectEmit(true, true, true, true); - emit Denylisted(bob); - denylist.denylist(accounts); - - assertTrue(denylist.isDenylisted(alice)); - assertTrue(denylist.isDenylisted(bob)); - } - - function test_Denylist_RevertsForNonDenylister() public { - address[] memory accounts = new address[](1); - accounts[0] = alice; - - vm.prank(stranger); - vm.expectRevert(Denylist.CallerIsNotDenylister.selector); - denylist.denylist(accounts); - } - - function test_Denylist_RevertsWhenDenylistingOwner() public { - vm.prank(owner); - denylist.addDenylister(denylister1); - - address[] memory accounts = new address[](1); - accounts[0] = owner; - - vm.prank(denylister1); - vm.expectRevert(Denylist.CannotDenylistOwner.selector); - denylist.denylist(accounts); - } - - function test_Denylist_OwnerInBatch_Reverts() public { - vm.prank(owner); - denylist.addDenylister(denylister1); - - address[] memory accounts = new address[](3); - accounts[0] = alice; - accounts[1] = owner; - accounts[2] = bob; - - vm.prank(denylister1); - vm.expectRevert(Denylist.CannotDenylistOwner.selector); - denylist.denylist(accounts); - } - - function test_UnDenylist_OnlyDenylister_Success() public { - vm.prank(owner); - denylist.addDenylister(denylister1); - - address[] memory add = new address[](1); - add[0] = alice; - vm.prank(denylister1); - denylist.denylist(add); - - address[] memory remove = new address[](1); - remove[0] = alice; - vm.prank(denylister1); - vm.expectEmit(true, true, true, true); - emit UnDenylisted(alice); - denylist.unDenylist(remove); - - assertFalse(denylist.isDenylisted(alice)); - } - - function test_UnDenylist_RevertsForNonDenylister() public { - address[] memory accounts = new address[](1); - accounts[0] = alice; - - vm.prank(stranger); - vm.expectRevert(Denylist.CallerIsNotDenylister.selector); - denylist.unDenylist(accounts); - } - - function test_Denylist_Idempotent_NoDuplicateEvent() public { - vm.prank(owner); - denylist.addDenylister(denylister1); - - address[] memory accounts = new address[](1); - accounts[0] = alice; - - vm.prank(denylister1); - denylist.denylist(accounts); - vm.prank(denylister1); - denylist.denylist(accounts); // second time should not emit - - assertTrue(denylist.isDenylisted(alice)); - } - - // ============ Add / Remove denylisters (onlyOwner) ============ - - function test_AddDenylister_OnlyOwner_Success() public { - vm.prank(owner); - vm.expectEmit(true, true, true, true); - emit DenylisterAdded(denylister1); - denylist.addDenylister(denylister1); - - assertTrue(denylist.isDenylister(denylister1)); - } - - function test_AddDenylister_RevertsForNonOwner() public { - vm.prank(stranger); - vm.expectRevert(); - denylist.addDenylister(denylister1); - } - - function test_RemoveDenylister_OnlyOwner_Success() public { - vm.prank(owner); - denylist.addDenylister(denylister1); - vm.prank(owner); - vm.expectEmit(true, true, true, true); - emit DenylisterRemoved(denylister1); - denylist.removeDenylister(denylister1); - - assertFalse(denylist.isDenylister(denylister1)); - } - - function test_RemoveDenylister_RevertsForNonOwner() public { - vm.prank(owner); - denylist.addDenylister(denylister1); - vm.prank(stranger); - vm.expectRevert(); - denylist.removeDenylister(denylister1); - } - - function test_AddDenylister_RevertsForZeroAddress() public { - vm.prank(owner); - vm.expectRevert(Denylist.ZeroAddress.selector); - denylist.addDenylister(address(0)); - } - - function test_RemoveDenylister_RevertsForZeroAddress() public { - vm.prank(owner); - vm.expectRevert(Denylist.ZeroAddress.selector); - denylist.removeDenylister(address(0)); - } - - // ============ Storage layout (ERC-7201) ============ - - /// @dev Verifies the per-address slot formula: slot = keccak256(abi.encode(address, baseSlot)); value 1 = denylisted. - function test_StorageLayout_PerAddressSlot_MatchesFormula() public { - vm.prank(owner); - denylist.addDenylister(denylister1); - - address[] memory accounts = new address[](1); - accounts[0] = alice; - vm.prank(denylister1); - denylist.denylist(accounts); - - bytes32 baseSlot = denylist.DENYLIST_STORAGE_LOCATION(); - bytes32 expectedSlot = keccak256(abi.encode(alice, baseSlot)); - bytes32 value = vm.load(address(denylist), expectedSlot); - assertEq(value, bytes32(uint256(1)), "denylisted slot should be 1"); - - // Bob not denylisted -> slot should be 0 - bytes32 bobSlot = keccak256(abi.encode(bob, baseSlot)); - assertEq(vm.load(address(denylist), bobSlot), bytes32(0)); - } - - function test_StorageLayout_BaseSlot_MatchesDocumentedFormula() public view { - bytes32 expected = - keccak256(abi.encode(uint256(keccak256("arc.storage.Denylist.v1")) - 1)) & ~bytes32(uint256(0xff)); - assertEq(denylist.DENYLIST_STORAGE_LOCATION(), expected); - } - - // ============ View functions ============ - - function test_IsDenylisted_ReturnsFalseByDefault() public view { - assertFalse(denylist.isDenylisted(alice)); - } - - function test_IsDenylister_ReturnsFalseByDefault() public view { - assertFalse(denylist.isDenylister(denylister1)); - } -} diff --git a/contracts/test/batch/Multicall3From.t.sol b/contracts/test/batch/Multicall3From.t.sol deleted file mode 100644 index ca527d5..0000000 --- a/contracts/test/batch/Multicall3From.t.sol +++ /dev/null @@ -1,554 +0,0 @@ -// Copyright 2026 Circle Internet Group, Inc. All rights reserved. -// -// SPDX-License-Identifier: Apache-2.0 -// -// Licensed under the Apache License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. -// You may obtain a copy of the License at -// -// http://www.apache.org/licenses/LICENSE-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// See the License for the specific language governing permissions and -// limitations under the License. - -pragma solidity ^0.8.29; - -import {Test} from "forge-std/Test.sol"; - -import {Multicall3From} from "../../src/batch/Multicall3From.sol"; -import {IMulticall3From} from "../../src/batch/IMulticall3From.sol"; -import {Addresses} from "../../scripts/Addresses.sol"; - -/// @dev Mock that mimics the callFrom precompile: records every sender and target -/// passed to callFrom, then forwards the call to the real target via target.call(data). -/// Records are stored in arrays so tests can assert the core invariant (sender -/// preservation) across every call in a batch, not just the last one. -contract MockCallFrom { - address[] public senders; - address[] public targets; - - function callFrom(address sender, address target, bytes calldata data) - external - returns (bool success, bytes memory returnData) - { - senders.push(sender); - targets.push(target); - (success, returnData) = target.call(data); - } - - function callCount() external view returns (uint256) { - return senders.length; - } - - function getSender(uint256 i) external view returns (address) { - return senders[i]; - } - - function getTarget(uint256 i) external view returns (address) { - return targets[i]; - } -} - -/// @dev Mock callFrom that reverts unconditionally when the target is the -/// designated "revert target". Simulates framework-level reverts (e.g. -/// static context, sender-spoofing rejection) that happen before the -/// child EVM frame is constructed. -contract MockRevertingCallFrom { - address public revertTarget; - - constructor(address _revertTarget) { - revertTarget = _revertTarget; - } - - function callFrom(address sender, address target, bytes calldata data) - external - returns (bool success, bytes memory returnData) - { - if (target == revertTarget) { - revert("subcall precompiles cannot be invoked in static context"); - } - // Normal path: forward to target - (success, returnData) = target.call(data); - // Suppress unused variable warning - sender; - } -} - -/// @dev Simple target contract for testing. -contract MockTarget { - uint256 public value; - - function setValue(uint256 v) external { - value = v; - } - - function getValue() external view returns (uint256) { - return value; - } - - function alwaysReverts() external pure { - revert("MockTarget: always reverts"); - } - - error CustomError(uint256 code); - - function revertsWithCustomError(uint256 code) external pure { - revert CustomError(code); - } -} - -contract Multicall3FromTest is Test { - Multicall3From multicall; - MockCallFrom mockCallFrom; - MockTarget target; - - function setUp() public { - multicall = new Multicall3From(); - mockCallFrom = new MockCallFrom(); - target = new MockTarget(); - - // Place mock callFrom bytecode at the well-known precompile address. - // vm.etch copies only bytecode, not storage. Storage starts at zero-value - // defaults, which is acceptable because MockCallFrom uses dynamic arrays - // that start empty. - vm.etch(Addresses.CALL_FROM, address(mockCallFrom).code); - } - - // ======================== CALL_FROM constant ======================== - - function test_callFromConstant() public view { - assertEq(address(multicall.CALL_FROM()), Addresses.CALL_FROM); - } - - // ======================== aggregate3 ======================== - - function test_aggregate3_singleCallSuccess() public { - IMulticall3From.Call3[] memory calls = new IMulticall3From.Call3[](1); - calls[0] = IMulticall3From.Call3({ - target: address(target), - allowFailure: false, - callData: abi.encodeCall(MockTarget.setValue, (42)) - }); - - IMulticall3From.Result[] memory results = multicall.aggregate3(calls); - - assertEq(results.length, 1); - assertTrue(results[0].success); - assertEq(target.value(), 42); - } - - function test_aggregate3_preservesSender() public { - address caller = address(0xBEEF); - - IMulticall3From.Call3[] memory calls = new IMulticall3From.Call3[](1); - calls[0] = IMulticall3From.Call3({ - target: address(target), - allowFailure: false, - callData: abi.encodeCall(MockTarget.setValue, (1)) - }); - - vm.prank(caller); - multicall.aggregate3(calls); - - // Core invariant: the mock records the sender passed to callFrom. - // It must be the original caller, not the Multicall3From contract address. - MockCallFrom mock = MockCallFrom(Addresses.CALL_FROM); - assertEq(mock.callCount(), 1); - assertEq(mock.getSender(0), caller); - assertEq(mock.getTarget(0), address(target)); - } - - function test_aggregate3_multipleCalls() public { - MockTarget target2 = new MockTarget(); - - IMulticall3From.Call3[] memory calls = new IMulticall3From.Call3[](2); - calls[0] = IMulticall3From.Call3({ - target: address(target), - allowFailure: false, - callData: abi.encodeCall(MockTarget.setValue, (10)) - }); - calls[1] = IMulticall3From.Call3({ - target: address(target2), - allowFailure: false, - callData: abi.encodeCall(MockTarget.setValue, (20)) - }); - - IMulticall3From.Result[] memory results = multicall.aggregate3(calls); - - assertEq(results.length, 2); - assertTrue(results[0].success); - assertTrue(results[1].success); - assertEq(target.value(), 10); - assertEq(target2.value(), 20); - } - - function test_aggregate3_emptyBatch() public { - IMulticall3From.Call3[] memory calls = new IMulticall3From.Call3[](0); - - IMulticall3From.Result[] memory results = multicall.aggregate3(calls); - - assertEq(results.length, 0); - } - - function test_aggregate3_allowFailureTrue_callReverts() public { - IMulticall3From.Call3[] memory calls = new IMulticall3From.Call3[](2); - calls[0] = IMulticall3From.Call3({ - target: address(target), - allowFailure: true, - callData: abi.encodeCall(MockTarget.alwaysReverts, ()) - }); - calls[1] = IMulticall3From.Call3({ - target: address(target), - allowFailure: false, - callData: abi.encodeCall(MockTarget.setValue, (99)) - }); - - IMulticall3From.Result[] memory results = multicall.aggregate3(calls); - - assertEq(results.length, 2); - assertFalse(results[0].success); - assertTrue(results[1].success); - assertEq(target.value(), 99); - } - - function test_aggregate3_allowFailureFalse_callReverts() public { - IMulticall3From.Call3[] memory calls = new IMulticall3From.Call3[](1); - calls[0] = IMulticall3From.Call3({ - target: address(target), - allowFailure: false, - callData: abi.encodeCall(MockTarget.alwaysReverts, ()) - }); - - vm.expectRevert("MockTarget: always reverts"); - multicall.aggregate3(calls); - } - - function test_aggregate3_propagatesCustomError() public { - IMulticall3From.Call3[] memory calls = new IMulticall3From.Call3[](1); - calls[0] = IMulticall3From.Call3({ - target: address(target), - allowFailure: false, - callData: abi.encodeCall(MockTarget.revertsWithCustomError, (42)) - }); - - vm.expectRevert(abi.encodeWithSelector(MockTarget.CustomError.selector, 42)); - multicall.aggregate3(calls); - } - - function test_aggregate3_senderPreservedAcrossBatch() public { - address caller = address(0xCAFE); - MockTarget target2 = new MockTarget(); - - IMulticall3From.Call3[] memory calls = new IMulticall3From.Call3[](2); - calls[0] = IMulticall3From.Call3({ - target: address(target), - allowFailure: false, - callData: abi.encodeCall(MockTarget.setValue, (1)) - }); - calls[1] = IMulticall3From.Call3({ - target: address(target2), - allowFailure: false, - callData: abi.encodeCall(MockTarget.setValue, (2)) - }); - - vm.prank(caller); - multicall.aggregate3(calls); - - // Verify sender is preserved for every call in the batch, not just the last. - MockCallFrom mock = MockCallFrom(Addresses.CALL_FROM); - assertEq(mock.callCount(), 2); - assertEq(mock.getSender(0), caller); - assertEq(mock.getSender(1), caller); - assertEq(mock.getTarget(0), address(target)); - assertEq(mock.getTarget(1), address(target2)); - } - - function test_aggregate3_rejectsValue() public { - IMulticall3From.Call3[] memory calls = new IMulticall3From.Call3[](0); - - // aggregate3 is non-payable, sending value should revert - (bool success,) = address(multicall).call{value: 1 ether}( - abi.encodeCall(Multicall3From.aggregate3, (calls)) - ); - assertFalse(success); - } - - // ======================== aggregate ======================== - - function test_aggregate_singleCall() public { - IMulticall3From.Call[] memory calls = new IMulticall3From.Call[](1); - calls[0] = IMulticall3From.Call({ - target: address(target), - callData: abi.encodeCall(MockTarget.setValue, (77)) - }); - - (uint256 blockNumber, bytes[] memory returnData) = multicall.aggregate(calls); - - assertEq(blockNumber, block.number); - assertEq(returnData.length, 1); - assertEq(target.value(), 77); - } - - function test_aggregate_revertsOnFailure() public { - IMulticall3From.Call[] memory calls = new IMulticall3From.Call[](1); - calls[0] = IMulticall3From.Call({ - target: address(target), - callData: abi.encodeCall(MockTarget.alwaysReverts, ()) - }); - - vm.expectRevert("MockTarget: always reverts"); - multicall.aggregate(calls); - } - - function test_aggregate_preservesSender() public { - address caller = address(0xDEAD); - - IMulticall3From.Call[] memory calls = new IMulticall3From.Call[](1); - calls[0] = IMulticall3From.Call({ - target: address(target), - callData: abi.encodeCall(MockTarget.setValue, (5)) - }); - - vm.prank(caller); - multicall.aggregate(calls); - - MockCallFrom mock = MockCallFrom(Addresses.CALL_FROM); - assertEq(mock.callCount(), 1); - assertEq(mock.getSender(0), caller); - } - - // ======================== tryAggregate ======================== - - function test_tryAggregate_requireSuccessTrue_reverts() public { - IMulticall3From.Call[] memory calls = new IMulticall3From.Call[](1); - calls[0] = IMulticall3From.Call({ - target: address(target), - callData: abi.encodeCall(MockTarget.alwaysReverts, ()) - }); - - vm.expectRevert("MockTarget: always reverts"); - multicall.tryAggregate(true, calls); - } - - function test_tryAggregate_requireSuccessFalse_doesNotRevert() public { - IMulticall3From.Call[] memory calls = new IMulticall3From.Call[](2); - calls[0] = IMulticall3From.Call({ - target: address(target), - callData: abi.encodeCall(MockTarget.alwaysReverts, ()) - }); - calls[1] = IMulticall3From.Call({ - target: address(target), - callData: abi.encodeCall(MockTarget.setValue, (50)) - }); - - IMulticall3From.Result[] memory results = multicall.tryAggregate(false, calls); - - assertEq(results.length, 2); - assertFalse(results[0].success); - assertTrue(results[1].success); - assertEq(target.value(), 50); - } - - // ======================== blockAndAggregate ======================== - - function test_blockAndAggregate_returnsBlockInfo() public { - IMulticall3From.Call[] memory calls = new IMulticall3From.Call[](1); - calls[0] = IMulticall3From.Call({ - target: address(target), - callData: abi.encodeCall(MockTarget.setValue, (33)) - }); - - (uint256 blockNumber, bytes32 blockHash, IMulticall3From.Result[] memory results) = - multicall.blockAndAggregate(calls); - - assertEq(blockNumber, block.number); - assertEq(blockHash, blockhash(block.number)); - assertEq(results.length, 1); - assertTrue(results[0].success); - assertEq(target.value(), 33); - } - - function test_blockAndAggregate_revertsOnFailure() public { - IMulticall3From.Call[] memory calls = new IMulticall3From.Call[](1); - calls[0] = IMulticall3From.Call({ - target: address(target), - callData: abi.encodeCall(MockTarget.alwaysReverts, ()) - }); - - vm.expectRevert("MockTarget: always reverts"); - multicall.blockAndAggregate(calls); - } - - // ======================== tryBlockAndAggregate ======================== - - function test_tryBlockAndAggregate_requireSuccessFalse() public { - IMulticall3From.Call[] memory calls = new IMulticall3From.Call[](1); - calls[0] = IMulticall3From.Call({ - target: address(target), - callData: abi.encodeCall(MockTarget.alwaysReverts, ()) - }); - - (uint256 blockNumber, bytes32 blockHash, IMulticall3From.Result[] memory results) = - multicall.tryBlockAndAggregate(false, calls); - - assertEq(blockNumber, block.number); - assertEq(blockHash, blockhash(block.number)); - assertEq(results.length, 1); - assertFalse(results[0].success); - } - - function test_tryBlockAndAggregate_requireSuccessTrue_reverts() public { - IMulticall3From.Call[] memory calls = new IMulticall3From.Call[](1); - calls[0] = IMulticall3From.Call({ - target: address(target), - callData: abi.encodeCall(MockTarget.alwaysReverts, ()) - }); - - vm.expectRevert("MockTarget: always reverts"); - multicall.tryBlockAndAggregate(true, calls); - } - - // ======================== View helpers ======================== - - function test_getBlockNumber() public view { - assertEq(multicall.getBlockNumber(), block.number); - } - - function test_getCurrentBlockTimestamp() public view { - assertEq(multicall.getCurrentBlockTimestamp(), block.timestamp); - } - - function test_getCurrentBlockGasLimit() public view { - assertEq(multicall.getCurrentBlockGasLimit(), block.gaslimit); - } - - function test_getCurrentBlockCoinbase() public view { - assertEq(multicall.getCurrentBlockCoinbase(), block.coinbase); - } - - function test_getCurrentBlockDifficulty() public view { - assertEq(multicall.getCurrentBlockDifficulty(), block.prevrandao); - } - - function test_getBasefee() public view { - assertEq(multicall.getBasefee(), block.basefee); - } - - function test_getChainId() public view { - assertEq(multicall.getChainId(), block.chainid); - } - - function test_getBlockHash() public view { - assertEq(multicall.getBlockHash(block.number), blockhash(block.number)); - } - - function test_getLastBlockHash() public view { - assertEq(multicall.getLastBlockHash(), blockhash(block.number - 1)); - } - - function test_getEthBalance() public view { - assertEq(multicall.getEthBalance(address(this)), address(this).balance); - } - - // ======================== Framework-level revert capture ======================== - - bytes constant EXPECTED_PRECOMPILE_REVERT = - abi.encodeWithSignature("Error(string)", "subcall precompiles cannot be invoked in static context"); - - function test_aggregate3_allowFailure_capturesPrecompileRevert() public { - address revertTarget = address(0xDEAD); - MockRevertingCallFrom revertingMock = new MockRevertingCallFrom(revertTarget); - vm.etch(Addresses.CALL_FROM, address(revertingMock).code); - vm.store(Addresses.CALL_FROM, bytes32(0), bytes32(uint256(uint160(revertTarget)))); - - IMulticall3From.Call3[] memory calls = new IMulticall3From.Call3[](2); - calls[0] = IMulticall3From.Call3({ - target: revertTarget, - allowFailure: true, - callData: abi.encodeCall(MockTarget.setValue, (1)) - }); - calls[1] = IMulticall3From.Call3({ - target: address(target), - allowFailure: false, - callData: abi.encodeCall(MockTarget.setValue, (42)) - }); - - IMulticall3From.Result[] memory results = multicall.aggregate3(calls); - - assertEq(results.length, 2); - assertFalse(results[0].success); - assertEq(results[0].returnData, EXPECTED_PRECOMPILE_REVERT); - assertTrue(results[1].success); - assertEq(target.value(), 42); - } - - function test_tryAggregate_requireSuccessFalse_capturesPrecompileRevert() public { - address revertTarget = address(0xDEAD); - MockRevertingCallFrom revertingMock = new MockRevertingCallFrom(revertTarget); - vm.etch(Addresses.CALL_FROM, address(revertingMock).code); - vm.store(Addresses.CALL_FROM, bytes32(0), bytes32(uint256(uint160(revertTarget)))); - - IMulticall3From.Call[] memory calls = new IMulticall3From.Call[](2); - calls[0] = IMulticall3From.Call({ - target: revertTarget, - callData: abi.encodeCall(MockTarget.setValue, (1)) - }); - calls[1] = IMulticall3From.Call({ - target: address(target), - callData: abi.encodeCall(MockTarget.setValue, (99)) - }); - - IMulticall3From.Result[] memory results = multicall.tryAggregate(false, calls); - - assertEq(results.length, 2); - assertFalse(results[0].success); - assertEq(results[0].returnData, EXPECTED_PRECOMPILE_REVERT); - assertTrue(results[1].success); - assertEq(target.value(), 99); - } - - function test_aggregate3_allowFailureFalse_precompileRevert_stillReverts() public { - address revertTarget = address(0xDEAD); - MockRevertingCallFrom revertingMock = new MockRevertingCallFrom(revertTarget); - vm.etch(Addresses.CALL_FROM, address(revertingMock).code); - vm.store(Addresses.CALL_FROM, bytes32(0), bytes32(uint256(uint160(revertTarget)))); - - IMulticall3From.Call3[] memory calls = new IMulticall3From.Call3[](1); - calls[0] = IMulticall3From.Call3({ - target: revertTarget, - allowFailure: false, - callData: abi.encodeCall(MockTarget.setValue, (1)) - }); - - vm.expectRevert(EXPECTED_PRECOMPILE_REVERT); - multicall.aggregate3(calls); - } - - function test_tryBlockAndAggregate_requireSuccessFalse_capturesPrecompileRevert() public { - address revertTarget = address(0xDEAD); - MockRevertingCallFrom revertingMock = new MockRevertingCallFrom(revertTarget); - vm.etch(Addresses.CALL_FROM, address(revertingMock).code); - vm.store(Addresses.CALL_FROM, bytes32(0), bytes32(uint256(uint160(revertTarget)))); - - IMulticall3From.Call[] memory calls = new IMulticall3From.Call[](2); - calls[0] = IMulticall3From.Call({ - target: revertTarget, - callData: abi.encodeCall(MockTarget.setValue, (1)) - }); - calls[1] = IMulticall3From.Call({ - target: address(target), - callData: abi.encodeCall(MockTarget.setValue, (55)) - }); - - (,, IMulticall3From.Result[] memory results) = multicall.tryBlockAndAggregate(false, calls); - - assertEq(results.length, 2); - assertFalse(results[0].success); - assertEq(results[0].returnData, EXPECTED_PRECOMPILE_REVERT); - assertTrue(results[1].success); - assertEq(target.value(), 55); - } -} diff --git a/contracts/test/call-from/ICallFrom.t.sol b/contracts/test/call-from/ICallFrom.t.sol deleted file mode 100644 index 495a4a6..0000000 --- a/contracts/test/call-from/ICallFrom.t.sol +++ /dev/null @@ -1,33 +0,0 @@ -// Copyright 2026 Circle Internet Group, Inc. All rights reserved. -// -// SPDX-License-Identifier: Apache-2.0 -// -// Licensed under the Apache License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. -// You may obtain a copy of the License at -// -// http://www.apache.org/licenses/LICENSE-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// See the License for the specific language governing permissions and -// limitations under the License. - -pragma solidity ^0.8.29; - -import {Test} from "forge-std/Test.sol"; - -import {ICallFrom} from "../../src/call-from/ICallFrom.sol"; -import {Addresses} from "../../scripts/Addresses.sol"; - -contract ICallFromTest is Test { - function test_selector() public pure { - bytes4 expected = bytes4(keccak256("callFrom(address,address,bytes)")); - assertEq(ICallFrom.callFrom.selector, expected); - } - - function test_callFromAddressConstant() public pure { - assertEq(Addresses.CALL_FROM, 0x1800000000000000000000000000000000000003); - } -} diff --git a/contracts/test/memo/Memo.t.sol b/contracts/test/memo/Memo.t.sol deleted file mode 100644 index 8d3b89b..0000000 --- a/contracts/test/memo/Memo.t.sol +++ /dev/null @@ -1,174 +0,0 @@ -// Copyright 2026 Circle Internet Group, Inc. All rights reserved. -// -// SPDX-License-Identifier: Apache-2.0 -// -// Licensed under the Apache License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. -// You may obtain a copy of the License at -// -// http://www.apache.org/licenses/LICENSE-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// See the License for the specific language governing permissions and -// limitations under the License. - -pragma solidity ^0.8.29; - -import {Test} from "forge-std/Test.sol"; - -import {Memo as MemoContract} from "../../src/memo/Memo.sol"; -import {Addresses} from "../../scripts/Addresses.sol"; - -/// @dev Mock that simulates the callFrom precompile by forwarding the call to the target. -/// Records the sender argument so tests can verify caller preservation. -contract MockCallFrom { - address public lastSender; - - function callFrom(address sender, address target, bytes calldata data) - external - returns (bool success, bytes memory returnData) - { - lastSender = sender; - (success, returnData) = target.call(data); - } -} - -/// @dev Simple target contract that can succeed or revert. -contract MockTarget { - uint256 public value; - - function setValue(uint256 v) external { - value = v; - } - - function reverting() external pure { - revert("MockTarget: revert"); - } -} - -contract MemoTest is Test { - MemoContract public memoContract; - MockTarget public target; - address public caller; - - event BeforeMemo(uint256 indexed memoIndex); - event Memo( - address indexed sender, - address indexed target, - bytes32 callDataHash, - bytes32 indexed memoId, - bytes memo, - uint256 memoIndex - ); - - function setUp() public { - // Deploy mock and etch its bytecode at the CALL_FROM precompile address. - // vm.etch copies bytecode only — storage lives at the precompile address itself. - MockCallFrom mock = new MockCallFrom(); - vm.etch(Addresses.CALL_FROM, address(mock).code); - - memoContract = new MemoContract(); - target = new MockTarget(); - caller = makeAddr("caller"); - } - - function test_memo_success() public { - bytes memory data = abi.encodeCall(MockTarget.setValue, (42)); - bytes32 memoId = keccak256("memo-1"); - bytes memory memo = "payment for invoice 123"; - - vm.prank(caller); - vm.expectEmit(true, true, true, true, address(memoContract)); - emit Memo(caller, address(target), keccak256(data), memoId, memo, 0); - memoContract.memo(address(target), data, memoId, memo); - - assertEq(target.value(), 42); - assertEq(memoContract.memoIndex(), 1); - } - - function test_memo_memoIndexIncrements() public { - bytes memory data = abi.encodeCall(MockTarget.setValue, (1)); - bytes32 memoId = keccak256("memo"); - bytes32 dataHash = keccak256(data); - - for (uint256 i = 0; i < 3; i++) { - vm.prank(caller); - vm.expectEmit(true, true, true, true, address(memoContract)); - emit Memo(caller, address(target), dataHash, memoId, "", i); - memoContract.memo(address(target), data, memoId, ""); - } - - assertEq(memoContract.memoIndex(), 3); - } - - function test_memo_callReverts() public { - bytes memory data = abi.encodeCall(MockTarget.reverting, ()); - bytes32 memoId = keccak256("memo-fail"); - uint256 indexBefore = memoContract.memoIndex(); - - vm.prank(caller); - vm.expectRevert(); - memoContract.memo(address(target), data, memoId, "some memo"); - - assertEq(memoContract.memoIndex(), indexBefore); - } - - function test_memo_emitsBeforeMemo() public { - bytes memory data = abi.encodeCall(MockTarget.setValue, (1)); - - vm.prank(caller); - vm.expectEmit(true, false, false, false, address(memoContract)); - emit BeforeMemo(0); - vm.expectEmit(true, true, true, true, address(memoContract)); - emit Memo(caller, address(target), keccak256(data), bytes32(0), "", 0); - memoContract.memo(address(target), data, bytes32(0), ""); - } - - function test_memo_emptyMemo() public { - bytes memory data = abi.encodeCall(MockTarget.setValue, (99)); - bytes32 memoId = keccak256("empty"); - - vm.prank(caller); - vm.expectEmit(true, true, true, true, address(memoContract)); - emit Memo(caller, address(target), keccak256(data), memoId, "", 0); - memoContract.memo(address(target), data, memoId, ""); - - assertEq(target.value(), 99); - } - - function test_memo_passesMsgSenderToPrecompile() public { - bytes memory data = abi.encodeCall(MockTarget.setValue, (1)); - - vm.prank(caller); - memoContract.memo(address(target), data, bytes32(0), ""); - - // Verify the mock recorded the correct sender (the original msg.sender, not the contract) - assertEq(MockCallFrom(Addresses.CALL_FROM).lastSender(), caller); - } - - function test_memo_memoIndexNotIncrementedOnRevert() public { - bytes memory setData = abi.encodeCall(MockTarget.setValue, (1)); - bytes memory revertData = abi.encodeCall(MockTarget.reverting, ()); - bytes32 memoId = keccak256("mixed"); - - // First call succeeds — memoIndex 0 - vm.prank(caller); - memoContract.memo(address(target), setData, memoId, "first"); - assertEq(memoContract.memoIndex(), 1); - - // Second call reverts — memoIndex stays at 1 - vm.prank(caller); - vm.expectRevert(); - memoContract.memo(address(target), revertData, memoId, "fail"); - assertEq(memoContract.memoIndex(), 1); - - // Third call succeeds — memoIndex 1 - vm.prank(caller); - vm.expectEmit(true, true, true, true, address(memoContract)); - emit Memo(caller, address(target), keccak256(setData), memoId, "third", 1); - memoContract.memo(address(target), setData, memoId, "third"); - assertEq(memoContract.memoIndex(), 2); - } -} diff --git a/contracts/test/mock/CallHelper.t.sol b/contracts/test/mock/CallHelper.t.sol deleted file mode 100644 index fcf6f96..0000000 --- a/contracts/test/mock/CallHelper.t.sol +++ /dev/null @@ -1,143 +0,0 @@ -// Copyright 2025 Circle Internet Group, Inc. All rights reserved. -// -// SPDX-License-Identifier: Apache-2.0 -// -// Licensed under the Apache License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. -// You may obtain a copy of the License at -// -// http://www.apache.org/licenses/LICENSE-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// See the License for the specific language governing permissions and -// limitations under the License. - -pragma solidity ^0.8.29; - -import {Test} from "forge-std/Test.sol"; - -import {CallHelper} from "../../src/mocks/CallHelper.sol"; -import {IMulticall3} from "../../src/mocks/IMulticall3.sol"; -import {ArtifactHelper} from "../../scripts/ArtifactHelper.s.sol"; - -contract CallHelperTest is Test { - ArtifactHelper artifactHelper; - address eoa; - CallHelper A; - CallHelper B; - CallHelper C; - - function setUp() public { - artifactHelper = new ArtifactHelper(); - artifactHelper.loadManifest("assets/artifacts/manifest.json"); - ArtifactHelper.OneTimeAddressDeployment memory deployment = - artifactHelper.loadOneTimeAddressDeployment("Multicall3"); - artifactHelper.deployOneTimeAddressContract(deployment); - - eoa = makeAddr("EOA"); - A = new CallHelper(); - B = new CallHelper(); - C = new CallHelper(); - } - - function testGetSetValueAndStorage() public { - A.setStorage(0, 193); - assertEq(A.getStorage(0), 193); - } - - function testExecute() public { - // EOA -> A - vm.deal(eoa, 100); - vm.prank(eoa); - vm.expectEmit(address(A)); - emit CallHelper.ExecutionResult(true, hex""); - bytes memory data; - (bool success, bytes memory returnValue) = A.execute{value: 30}(address(A), data, 0); - assertTrue(success); - assertEq(returnValue, hex""); - assertEq(address(A).balance, 30); - } - - function testExecuteBatch() public { - // EOA -> A - // A -> B and C - vm.deal(address(A), 10); - vm.prank(eoa); - vm.expectEmit(true, true, false, false, address(A)); - emit CallHelper.ExecutionResult(false, hex""); - vm.expectEmit(true, true, true, false, address(A)); - emit CallHelper.ExecutionResult(true, hex""); - IMulticall3.Call3Value[] memory calls = new IMulticall3.Call3Value[](2); - calls[0] = IMulticall3.Call3Value(address(B), true, 0, abi.encodeWithSelector(B.revertWithString.selector, "B")); - calls[1] = IMulticall3.Call3Value(address(C), true, 10, hex""); - (IMulticall3.Result[] memory res) = A.executeBatch(calls); - assertEq(res[0].success, false); - assertEq(res[1].success, true); - } - - function testStaticCall() public { - // EOA -> A.staticCall - // A -> B.execute - // B 10 -> C - vm.deal(address(B), 10); - vm.prank(eoa); - vm.expectEmit(true, true, true, false, address(A)); - emit CallHelper.ExecutionResult(false, hex""); - (bool success, bytes memory returnValue) = - A.staticCall(address(B), abi.encodeWithSelector(B.execute.selector, address(C), 10)); - assertFalse(success); - assertEq(returnValue, hex""); - assertEq(address(C).balance, 0); - } - - function testDelegateCall() public { - // EOA -> A.delegateCall - // EOA -> A delegate A.receive - vm.prank(eoa); - vm.expectEmit(address(A)); - emit CallHelper.ExecutionResult(true, hex""); - bytes memory data; - (bool success, bytes memory returnValue) = A.delegateCall(address(A), data); - assertTrue(success); - assertEq(returnValue, hex""); - } - - function testDelegateCallSetValue() public { - // EOA -> A.delegateCall - // EOA -> A delegate B.setStorage - vm.prank(eoa); - vm.expectEmit(address(A)); - emit CallHelper.ExecutionResult(true, hex""); - (bool success, bytes memory returnValue) = - A.delegateCall(address(B), abi.encodeWithSelector(B.setStorage.selector, 0, 13)); - assertTrue(success); - assertEq(returnValue, hex""); - assertEq(A.getStorage(0), 13); - assertEq(B.getStorage(0), 0); - } - - function testBlockInfo() public view { - CallHelper.BlockInfo memory blockInfo = A.getBlockInfo(); - assertEq(blockInfo.coinbase, address(0)); - assertEq(blockInfo.timestamp, 1); - assertEq(blockInfo.number, 1); - assertEq(blockInfo.baseFee, 0); - assertEq(blockInfo.blobBaseFee, 1); - assertEq(blockInfo.prevRandao, 0); - assertEq(blockInfo.gasLimit, 1073741824); - } - - function testTxInfo() public { - vm.prank(eoa); - CallHelper.TransactionInfo memory txInfo = A.getTxInfo(); - assertEq(txInfo.gasPrice, 0); - assertEq(txInfo.origin, tx.origin); - } - - function testBlockHash() public view { - bytes32 hash = IMulticall3(address(0xcA11bde05977b3631167028862bE2a173976CA11)).getBlockHash(100); - assertEq(hash, bytes32(0)); - } -} diff --git a/contracts/test/protocol-config/ProtocolConfig.t.sol b/contracts/test/protocol-config/ProtocolConfig.t.sol deleted file mode 100644 index b78bd4e..0000000 --- a/contracts/test/protocol-config/ProtocolConfig.t.sol +++ /dev/null @@ -1,1386 +0,0 @@ -// Copyright 2025 Circle Internet Group, Inc. All rights reserved. -// -// SPDX-License-Identifier: Apache-2.0 -// -// Licensed under the Apache License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. -// You may obtain a copy of the License at -// -// http://www.apache.org/licenses/LICENSE-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// See the License for the specific language governing permissions and -// limitations under the License. - -pragma solidity ^0.8.29; - -import {Test} from "forge-std/Test.sol"; -import {Vm} from "forge-std/Vm.sol"; -import {console} from "forge-std/console.sol"; -import {ProtocolConfig} from "../../src/protocol-config/ProtocolConfig.sol"; -import {IProtocolConfig} from "../../src/protocol-config/interfaces/IProtocolConfig.sol"; -import {Controller} from "../../src/protocol-config/roles/Controller.sol"; -import {Pausable} from "../../src/common/roles/Pausable.sol"; -import {AdminUpgradeableProxy} from "../../src/proxy/AdminUpgradeableProxy.sol"; - -contract ProtocolConfigTest is Test { - ProtocolConfig public protocolConfig; - ProtocolConfig public implementation; - AdminUpgradeableProxy public proxy; - - address public owner; - address public controller; - address public pauser; - address public unauthorizedUser; - address public proxyOwner; - - // Storage slot constants (matching the contract) - bytes32 private constant PROTOCOL_CONFIG_STORAGE_LOCATION = - 0x668f09ce856848ead6cb1ddee963f15ef833cea8958030868f867aec84385200; - - // Events from contracts - event OwnershipTransferred(address indexed previousOwner, address indexed newOwner); - event ControllerUpdated(address indexed newController); - event PauserChanged(address indexed newAddress); - event FeeParamsUpdated(IProtocolConfig.FeeParams params); - event ConsensusParamsUpdated(IProtocolConfig.ConsensusParams params); - event Pause(); - event Unpause(); - - function setUp() public { - owner = makeAddr("owner"); - controller = makeAddr("controller"); - pauser = makeAddr("pauser"); - unauthorizedUser = makeAddr("unauthorizedUser"); - proxyOwner = makeAddr("proxyOwner"); - } - - // Helper function to deploy ProtocolConfig proxy with default values (simulating genesis initialization) - function deployProtocolConfig(address _owner, address _controller, address _pauser) - internal - returns (ProtocolConfig) - { - // Default fee parameters for testing - IProtocolConfig.FeeParams memory defaultFeeParams = IProtocolConfig.FeeParams({ - alpha: 50, - kRate: 75, - inverseElasticityMultiplier: 5000, - minBaseFee: 1000, - maxBaseFee: 2000, - blockGasLimit: 30000000 - }); - - // Default consensus parameters for testing - IProtocolConfig.ConsensusParams memory defaultConsensusParams = IProtocolConfig.ConsensusParams({ - timeoutProposeMs: 2000, - timeoutProposeDeltaMs: 200, - timeoutPrevoteMs: 1000, - timeoutPrevoteDeltaMs: 100, - timeoutPrecommitMs: 1000, - timeoutPrecommitDeltaMs: 100, - timeoutRebroadcastMs: 2000, - targetBlockTimeMs: 3000 - }); - - return deployProtocolConfig( - _owner, _controller, _pauser, defaultFeeParams, defaultConsensusParams - ); - } - - // Helper function to deploy ProtocolConfig proxy with custom parameters (simulating genesis initialization) - function deployProtocolConfig( - address _owner, - address _controller, - address _pauser, - IProtocolConfig.FeeParams memory _feeParams, - IProtocolConfig.ConsensusParams memory _consensusParams - ) internal returns (ProtocolConfig) { - // Deploy implementation contract - implementation = new ProtocolConfig(); - - // Deploy proxy without initialization (empty data) - proxy = new AdminUpgradeableProxy( - address(implementation), - proxyOwner, - "" // No initialization data - will be set through genesis-style storage manipulation - ); - - // Get the proxy as ProtocolConfig interface - ProtocolConfig deployedConfig = ProtocolConfig(address(proxy)); - - // Simulate genesis file initialization by directly setting storage using calculated indices - _simulateGenesisStorageInitialization( - deployedConfig, _owner, _controller, _pauser, _feeParams, _consensusParams - ); - - return deployedConfig; - } - - // Helper function to simulate genesis file initialization using calculated storage indices - function _simulateGenesisStorageInitialization( - ProtocolConfig config, - address _owner, - address _controller, - address _pauser, - IProtocolConfig.FeeParams memory _feeParams, - IProtocolConfig.ConsensusParams memory _consensusParams - ) internal { - // This simulates how genesis file would set storage slots directly - - // === Set Ownable storage (ERC-7201) === - // Owner is stored at ERC-7201 slot for "openzeppelin.storage.Ownable" - bytes32 ownableSlot = 0x9016d09d72d40fdae2fd8ceac6b6234c7706214fd39c1cd1e609a0528c199300; - vm.store(address(config), ownableSlot, bytes32(uint256(uint160(_owner)))); - - // === Set Controller storage (ERC-7201) === - // Controller: arc.storage.ProtocolConfigController - bytes32 controllerSlot = 0x958f8fec699b51a1249f513eceda5429078000657f74abd1721bba363087af00; - vm.store(address(config), controllerSlot, bytes32(uint256(uint160(_controller)))); - - // === Set Pausable storage (ERC-7201) === - // Pausable: arc.storage.Pausable - // struct PausableStorage { address pauser; bool paused; } - // Both packed into same slot: pauser (20 bytes) + paused (1 byte) = 21 bytes - bytes32 pausableSlot = 0x0642d7922329a434cf4fd17a3c95eb692c24fd95f9f94d0b55420a5d895f4a00; - bytes32 packedPausable = bytes32( - uint256(uint160(_pauser)) // pauser in bytes 0-19 - | (uint256(0) << 160) // paused = false in byte 20 - ); - vm.store(address(config), pausableSlot, packedPausable); - - // === Set ERC-7201 ProtocolConfig storage === - // Base slot: 0x668f09ce856848ead6cb1ddee963f15ef833cea8958030868f867aec84385200 - bytes32 baseSlot = PROTOCOL_CONFIG_STORAGE_LOCATION; - - // FeeParams struct layout: - // - alpha (uint64), kRate (uint64), inverseElasticityMultiplier (uint64) pack into one slot - // - minBaseFee (uint256) takes next slot - // - maxBaseFee (uint256) takes next slot - // - blockGasLimit (uint256) takes next slot - // - deprecated address placeholder (slot +4) takes next slot - // Then ConsensusParams struct takes next slots - - // Slot 0x668f09ce856848ead6cb1ddee963f15ef833cea8958030868f867aec84385200: packed alpha|kRate|inverseElasticityMultiplier - bytes32 packedSlot0 = bytes32( - (uint256(_feeParams.alpha)) | (uint256(_feeParams.kRate) << 64) - | (uint256(_feeParams.inverseElasticityMultiplier) << 128) - ); - vm.store(address(config), baseSlot, packedSlot0); - - // Slot 0x668f09ce856848ead6cb1ddee963f15ef833cea8958030868f867aec84385201: minBaseFee - vm.store(address(config), bytes32(uint256(baseSlot) + 1), bytes32(_feeParams.minBaseFee)); - - // Slot 0x668f09ce856848ead6cb1ddee963f15ef833cea8958030868f867aec84385202: maxBaseFee - vm.store(address(config), bytes32(uint256(baseSlot) + 2), bytes32(_feeParams.maxBaseFee)); - - // Slot 0x668f09ce856848ead6cb1ddee963f15ef833cea8958030868f867aec84385203: blockGasLimit - vm.store(address(config), bytes32(uint256(baseSlot) + 3), bytes32(_feeParams.blockGasLimit)); - - // ConsensusParams struct layout: - // - timeoutProposeMs (uint16), timeoutProposeDeltaMs (uint16), timeoutPrevoteMs (uint16), - // timeoutPrevoteDeltaMs (uint16), timeoutPrecommitMs (uint16), timeoutPrecommitDeltaMs (uint16), - // timeoutRebroadcastMs (uint16), targetBlockTimeMs (uint16) - // pack into one slot (8 * 16 = 128 bits, fits in one slot) - - // Slot 0x668f09ce856848ead6cb1ddee963f15ef833cea8958030868f867aec84385204: deprecated address placeholder - // placeholder (slot retained, type preserved as address, no migration). Tests leave at 0x0. - - // Slot 0x668f09ce856848ead6cb1ddee963f15ef833cea8958030868f867aec84385205: packed consensus params - bytes32 packedConsensusSlot = bytes32( - (uint256(_consensusParams.timeoutProposeMs)) | (uint256(_consensusParams.timeoutProposeDeltaMs) << 16) - | (uint256(_consensusParams.timeoutPrevoteMs) << 32) - | (uint256(_consensusParams.timeoutPrevoteDeltaMs) << 48) - | (uint256(_consensusParams.timeoutPrecommitMs) << 64) - | (uint256(_consensusParams.timeoutPrecommitDeltaMs) << 80) - | (uint256(_consensusParams.timeoutRebroadcastMs) << 96) - | (uint256(_consensusParams.targetBlockTimeMs) << 112) - ); - vm.store(address(config), bytes32(uint256(baseSlot) + 5), packedConsensusSlot); - } - - // ============================================================================ - // GENESIS FILE HELPER FUNCTIONS - // ============================================================================ - // Helper functions to generate genesis file allocation data - - /** - * @notice Demonstrates genesis file storage allocation for ProtocolConfig - * @dev Shows the exact key-value pairs needed in genesis alloc.storage field - */ - function logGenesisStorageExample( - address _owner, - address _controller, - address _pauser, - IProtocolConfig.FeeParams memory _feeParams, - IProtocolConfig.ConsensusParams memory _consensusParams - ) public pure { - // Log each storage slot individually to avoid stack too deep - - // Standard contract storage - console.log("=== Genesis Storage Allocation ==="); - console.log("// Owner (ERC-7201 slot for openzeppelin.storage.Ownable)"); - console.log( - '"0x9016d09d72d40fdae2fd8ceac6b6234c7706214fd39c1cd1e609a0528c199300": "0x%s",', _toHexStringAddress(_owner) - ); - - console.log("// Controller (slot 0)"); - console.log( - '"0x0000000000000000000000000000000000000000000000000000000000000000": "0x%s",', - _toHexStringAddress(_controller) - ); - - console.log("// Pauser (slot 1)"); - console.log( - '"0x0000000000000000000000000000000000000000000000000000000000000001": "0x%s",', - _toHexStringAddress(_pauser) - ); - - console.log("// Paused = false (slot 2)"); - console.log( - '"0x0000000000000000000000000000000000000000000000000000000000000002": "0x0000000000000000000000000000000000000000000000000000000000000000",' - ); - - // ERC-7201 storage - console.log("// Packed fee params (base slot)"); - bytes32 packedValue = bytes32( - (uint256(_feeParams.alpha)) | (uint256(_feeParams.kRate) << 64) - | (uint256(_feeParams.inverseElasticityMultiplier) << 128) - ); - console.log( - '"0x668f09ce856848ead6cb1ddee963f15ef833cea8958030868f867aec84385200": "0x%s",', - _toHexStringBytes32(packedValue) - ); - - console.log("// minBaseFee (base slot + 1)"); - console.log( - '"0x668f09ce856848ead6cb1ddee963f15ef833cea8958030868f867aec84385201": "0x%s",', - _toHexStringBytes32(bytes32(_feeParams.minBaseFee)) - ); - - console.log("// maxBaseFee (base slot + 2)"); - console.log( - '"0x668f09ce856848ead6cb1ddee963f15ef833cea8958030868f867aec84385202": "0x%s",', - _toHexStringBytes32(bytes32(_feeParams.maxBaseFee)) - ); - - console.log("// blockGasLimit (base slot + 3)"); - console.log( - '"0x668f09ce856848ead6cb1ddee963f15ef833cea8958030868f867aec84385203": "0x%s",', - _toHexStringBytes32(bytes32(_feeParams.blockGasLimit)) - ); - - // Slot +4 is the deprecated address placeholder — intentionally omitted from - // the example allocation (genesis leaves it zero). - - console.log("// consensus params (base slot + 5)"); - bytes32 packedConsensusValue = bytes32( - (uint256(_consensusParams.timeoutProposeMs)) | (uint256(_consensusParams.timeoutProposeDeltaMs) << 16) - | (uint256(_consensusParams.timeoutPrevoteMs) << 32) - | (uint256(_consensusParams.timeoutPrevoteDeltaMs) << 48) - | (uint256(_consensusParams.timeoutPrecommitMs) << 64) - | (uint256(_consensusParams.timeoutPrecommitDeltaMs) << 80) - | (uint256(_consensusParams.timeoutRebroadcastMs) << 96) - | (uint256(_consensusParams.targetBlockTimeMs) << 112) - ); - console.log( - '"0x668f09ce856848ead6cb1ddee963f15ef833cea8958030868f867aec84385205": "0x%s"', - _toHexStringBytes32(packedConsensusValue) - ); - } - - /** - * @notice Test that demonstrates genesis file allocation generation - */ - function test_generateGenesisAllocation() public { - // Example values for genesis file - address exampleOwner = makeAddr("exampleOwner"); - address exampleController = makeAddr("exampleController"); - address examplePauser = makeAddr("examplePauser"); - - IProtocolConfig.FeeParams memory exampleFeeParams = IProtocolConfig.FeeParams({ - alpha: 50, - kRate: 75, - inverseElasticityMultiplier: 5000, - minBaseFee: 1000000000, // 1 gwei - maxBaseFee: 500000000000, // 500 gwei - blockGasLimit: 30000000 - }); - - IProtocolConfig.ConsensusParams memory exampleConsensusParams = IProtocolConfig.ConsensusParams({ - timeoutProposeMs: 2000, - timeoutProposeDeltaMs: 200, - timeoutPrevoteMs: 1000, - timeoutPrevoteDeltaMs: 100, - timeoutPrecommitMs: 1000, - timeoutPrecommitDeltaMs: 100, - timeoutRebroadcastMs: 2000, - targetBlockTimeMs: 3000 - }); - - // Log the storage allocation for copy-paste into genesis file - logGenesisStorageExample( - exampleOwner, - exampleController, - examplePauser, - exampleFeeParams, - exampleConsensusParams - ); - } - - /** - * @notice Convert address to hex string (64 chars, padded) - */ - function _toHexStringAddress(address addr) internal pure returns (string memory) { - return _toHexStringBytes32(bytes32(uint256(uint160(addr)))); - } - - /** - * @notice Convert bytes32 to hex string (64 chars, no 0x prefix) - */ - function _toHexStringBytes32(bytes32 value) internal pure returns (string memory) { - bytes memory alphabet = "0123456789abcdef"; - bytes memory str = new bytes(64); - for (uint256 i = 0; i < 32; i++) { - str[i * 2] = alphabet[uint8(value[i] >> 4)]; - str[1 + i * 2] = alphabet[uint8(value[i] & 0x0f)]; - } - return string(str); - } - - // ============================================================================ - // DEPLOYMENT & STORAGE LAYOUT TESTS - // ============================================================================ - // Tests for contract deployment and storage layout verification after genesis initialization - - function test_contractDeploymentAndGenesisInitialization() public { - // Deploy with simulated genesis initialization - protocolConfig = deployProtocolConfig(owner, controller, pauser); - - // Verify role assignments were set correctly through genesis storage manipulation - assertEq(protocolConfig.owner(), owner); - assertEq(protocolConfig.controller(), controller); - assertEq(protocolConfig.pauser(), pauser); - assertFalse(protocolConfig.paused()); - - // Verify fee params are initialized to the default values from deployProtocolConfig - IProtocolConfig.FeeParams memory params = protocolConfig.feeParams(); - assertEq(params.alpha, 50); - assertEq(params.kRate, 75); - assertEq(params.inverseElasticityMultiplier, 5000); - assertEq(params.minBaseFee, 1000); - assertEq(params.maxBaseFee, 2000); - assertEq(params.blockGasLimit, 30000000); - - // Verify consensus params are initialized to the default values - IProtocolConfig.ConsensusParams memory consensusParams = protocolConfig.consensusParams(); - assertEq(consensusParams.timeoutProposeMs, 2000); - assertEq(consensusParams.timeoutProposeDeltaMs, 200); - assertEq(consensusParams.timeoutPrevoteMs, 1000); - assertEq(consensusParams.timeoutPrevoteDeltaMs, 100); - assertEq(consensusParams.timeoutPrecommitMs, 1000); - assertEq(consensusParams.timeoutPrecommitDeltaMs, 100); - assertEq(consensusParams.timeoutRebroadcastMs, 2000); - assertEq(consensusParams.targetBlockTimeMs, 3000); - } - - function test_implementationContractState() public { - // Test the actual implementation contract before genesis initialization - ProtocolConfig impl = new ProtocolConfig(); - - // Implementation should be uninitialized (upgradeable pattern) - assertEq(impl.owner(), address(0)); // No initialization on implementation - assertEq(impl.controller(), address(0)); // Not set - assertEq(impl.pauser(), address(0)); // Not set - assertFalse(impl.paused()); // Initial state - - // Storage should be empty (all zeros) - IProtocolConfig.FeeParams memory params = impl.feeParams(); - assertEq(params.alpha, 0); - assertEq(params.kRate, 0); - assertEq(params.inverseElasticityMultiplier, 0); - assertEq(params.minBaseFee, 0); - assertEq(params.maxBaseFee, 0); - assertEq(params.blockGasLimit, 0); - } - - function test_storageLayoutCalculation() public { - // Test that our storage layout calculations are correct by setting and reading specific values - protocolConfig = deployProtocolConfig(owner, controller, pauser); - - // Test extreme values to ensure correct slot packing/unpacking - IProtocolConfig.FeeParams memory extremeParams = IProtocolConfig.FeeParams({ - alpha: 100, // Max uint64 we allow - kRate: 99, // Near max - inverseElasticityMultiplier: type(uint64).max, // Actual max uint64 - minBaseFee: 1, // Min value - maxBaseFee: type(uint256).max, // Max uint256 - blockGasLimit: type(uint256).max // Max uint256 - }); - - // Deploy with extreme values - IProtocolConfig.ConsensusParams memory extremeConsensusParams = IProtocolConfig.ConsensusParams({ - timeoutProposeMs: 2000, - timeoutProposeDeltaMs: 200, - timeoutPrevoteMs: 1000, - timeoutPrevoteDeltaMs: 100, - timeoutPrecommitMs: 1000, - timeoutPrecommitDeltaMs: 100, - timeoutRebroadcastMs: 2000, - targetBlockTimeMs: 3000 - }); - ProtocolConfig extremeConfig = - deployProtocolConfig(owner, controller, pauser, extremeParams, extremeConsensusParams); - - // Verify extreme values are stored and retrieved correctly - IProtocolConfig.FeeParams memory retrievedParams = extremeConfig.feeParams(); - assertEq(retrievedParams.alpha, 100); - assertEq(retrievedParams.kRate, 99); - assertEq(retrievedParams.inverseElasticityMultiplier, type(uint64).max); - assertEq(retrievedParams.minBaseFee, 1); - assertEq(retrievedParams.maxBaseFee, type(uint256).max); - assertEq(retrievedParams.blockGasLimit, type(uint256).max); - } - - function test_directStorageAccess() public { - // Test that we can read the storage values directly to verify our layout calculations - protocolConfig = deployProtocolConfig(owner, controller, pauser); - - // Read the ERC-7201 storage directly - bytes32 baseSlot = PROTOCOL_CONFIG_STORAGE_LOCATION; - - // Read packed slot 0 (alpha, kRate, inverseElasticityMultiplier) - bytes32 packedSlot0 = vm.load(address(protocolConfig), baseSlot); - uint64 storedAlpha = uint64(uint256(packedSlot0)); - uint64 storedKRate = uint64(uint256(packedSlot0) >> 64); - uint64 storedInverseElasticityMultiplier = uint64(uint256(packedSlot0) >> 128); - - assertEq(storedAlpha, 50); - assertEq(storedKRate, 75); - assertEq(storedInverseElasticityMultiplier, 5000); - - // Read other slots - bytes32 minBaseFeeSlot = vm.load(address(protocolConfig), bytes32(uint256(baseSlot) + 1)); - bytes32 maxBaseFeeSlot = vm.load(address(protocolConfig), bytes32(uint256(baseSlot) + 2)); - bytes32 blockGasLimitSlot = vm.load(address(protocolConfig), bytes32(uint256(baseSlot) + 3)); - bytes32 reservedSlot4 = vm.load(address(protocolConfig), bytes32(uint256(baseSlot) + 4)); - bytes32 consensusParamsSlot = vm.load(address(protocolConfig), bytes32(uint256(baseSlot) + 5)); - - assertEq(uint256(minBaseFeeSlot), 1000); - assertEq(uint256(maxBaseFeeSlot), 2000); - assertEq(uint256(blockGasLimitSlot), 30000000); - // Deprecated address placeholder — helper leaves it zeroed. - assertEq(uint256(reservedSlot4), 0); - - // Verify consensus params are packed correctly - uint16 storedTimeoutProposeMs = uint16(uint256(consensusParamsSlot)); - uint16 storedTimeoutProposeDeltaMs = uint16(uint256(consensusParamsSlot) >> 16); - uint16 storedTimeoutPrevoteMs = uint16(uint256(consensusParamsSlot) >> 32); - uint16 storedTimeoutPrevoteDeltaMs = uint16(uint256(consensusParamsSlot) >> 48); - uint16 storedTimeoutPrecommitMs = uint16(uint256(consensusParamsSlot) >> 64); - uint16 storedTimeoutPrecommitDeltaMs = uint16(uint256(consensusParamsSlot) >> 80); - uint16 storedTimeoutRebroadcastMs = uint16(uint256(consensusParamsSlot) >> 96); - uint16 storedTargetBlockTimeMs = uint16(uint256(consensusParamsSlot) >> 112); - - assertEq(storedTimeoutProposeMs, 2000); - assertEq(storedTimeoutProposeDeltaMs, 200); - assertEq(storedTimeoutPrevoteMs, 1000); - assertEq(storedTimeoutPrevoteDeltaMs, 100); - assertEq(storedTimeoutPrecommitMs, 1000); - assertEq(storedTimeoutPrecommitDeltaMs, 100); - assertEq(storedTimeoutRebroadcastMs, 2000); - assertEq(storedTargetBlockTimeMs, 3000); - } - - // ============================================================================ - // FEE PARAMETERS VALIDATION TESTS - // ============================================================================ - // Tests for updateFeeParams validation logic and boundary conditions - - function test_updateFeeParams__ValidParams() public { - // Deploy contract - protocolConfig = deployProtocolConfig(owner, controller, pauser); - - IProtocolConfig.FeeParams memory newParams = IProtocolConfig.FeeParams({ - alpha: 50, - kRate: 75, - inverseElasticityMultiplier: 5000, - minBaseFee: 1000, - maxBaseFee: 2000, - blockGasLimit: 30000000 - }); - - // Should succeed when called by controller - vm.prank(controller); - vm.expectEmit(true, true, true, true); - emit FeeParamsUpdated(newParams); - protocolConfig.updateFeeParams(newParams); - - // Verify the params were updated - IProtocolConfig.FeeParams memory updatedParams = protocolConfig.feeParams(); - assertEq(updatedParams.alpha, 50); - assertEq(updatedParams.kRate, 75); - assertEq(updatedParams.inverseElasticityMultiplier, 5000); - assertEq(updatedParams.minBaseFee, 1000); - assertEq(updatedParams.maxBaseFee, 2000); - assertEq(updatedParams.blockGasLimit, 30000000); - } - - function test_updateFeeParams__InvalidAlpha() public { - // Deploy contract - protocolConfig = deployProtocolConfig(owner, controller, pauser); - - // Test validation: alpha value must be <= 100 - IProtocolConfig.FeeParams memory invalidParams = IProtocolConfig.FeeParams({ - alpha: 101, // Invalid: exceeds maximum allowed value of 100 - kRate: 50, - inverseElasticityMultiplier: 5000, - minBaseFee: 1000, - maxBaseFee: 2000, - blockGasLimit: 30000000 - }); - - // Should get specific validation error (not generic access control error) - vm.prank(controller); - vm.expectRevert(ProtocolConfig.InvalidAlpha.selector); - protocolConfig.updateFeeParams(invalidParams); - } - - function test_updateFeeParams__InvalidKRate() public { - // Deploy contract - protocolConfig = deployProtocolConfig(owner, controller, pauser); - - IProtocolConfig.FeeParams memory invalidParams = IProtocolConfig.FeeParams({ - alpha: 50, - kRate: 10001, // Invalid: > 10000 - inverseElasticityMultiplier: 5000, - minBaseFee: 1000, - maxBaseFee: 2000, - blockGasLimit: 30000000 - }); - - // Should revert with specific validation error - vm.prank(controller); - vm.expectRevert(ProtocolConfig.InvalidKRate.selector); - protocolConfig.updateFeeParams(invalidParams); - } - - function test_updateFeeParams__InvalidBaseFee() public { - // Deploy contract - protocolConfig = deployProtocolConfig(owner, controller, pauser); - - IProtocolConfig.FeeParams memory invalidParams = IProtocolConfig.FeeParams({ - alpha: 50, - kRate: 50, - inverseElasticityMultiplier: 5000, - minBaseFee: 2000, // Invalid: > maxBaseFee - maxBaseFee: 1000, - blockGasLimit: 30000000 - }); - - // Should revert with specific validation error - vm.prank(controller); - vm.expectRevert(ProtocolConfig.InvalidBaseFeeRange.selector); - protocolConfig.updateFeeParams(invalidParams); - } - - function test_updateFeeParams__InvalidBlockGasLimit() public { - // Deploy contract - protocolConfig = deployProtocolConfig(owner, controller, pauser); - - IProtocolConfig.FeeParams memory invalidParams = IProtocolConfig.FeeParams({ - alpha: 50, - kRate: 50, - inverseElasticityMultiplier: 5000, - minBaseFee: 1000, - maxBaseFee: 2000, - blockGasLimit: 0 // Invalid: must be > 0 - }); - - // Should revert with specific validation error - vm.prank(controller); - vm.expectRevert(ProtocolConfig.InvalidBlockGasLimit.selector); - protocolConfig.updateFeeParams(invalidParams); - } - - function test_updateFeeParams__InvalidElasticityMultiplier() public { - // Deploy contract - protocolConfig = deployProtocolConfig(owner, controller, pauser); - - IProtocolConfig.FeeParams memory invalidParams = IProtocolConfig.FeeParams({ - alpha: 50, - kRate: 50, - inverseElasticityMultiplier: 10001, // Invalid: must be <= 10000 - minBaseFee: 1000, - maxBaseFee: 2000, - blockGasLimit: 30000000 - }); - - // Should revert with specific validation error - vm.prank(controller); - vm.expectRevert(ProtocolConfig.InvalidInverseElasticityMultiplier.selector); - protocolConfig.updateFeeParams(invalidParams); - } - - function test_updateFeeParams__UnauthorizedUser() public { - // Deploy contract - protocolConfig = deployProtocolConfig(owner, controller, pauser); - - IProtocolConfig.FeeParams memory params = IProtocolConfig.FeeParams({ - alpha: 50, - kRate: 50, - inverseElasticityMultiplier: 5000, - minBaseFee: 1000, - maxBaseFee: 2000, - blockGasLimit: 30000000 - }); - - // Should revert when called by unauthorized user - vm.prank(unauthorizedUser); - vm.expectRevert(Controller.CallerIsNotController.selector); - protocolConfig.updateFeeParams(params); - } - - function test_updateBlockGasLimit__Succeeds() public { - protocolConfig = deployProtocolConfig(owner, controller, pauser); - - IProtocolConfig.FeeParams memory beforeParams = protocolConfig.feeParams(); - uint256 newBlockGasLimit = beforeParams.blockGasLimit + 1; - - IProtocolConfig.FeeParams memory expected = beforeParams; - expected.blockGasLimit = newBlockGasLimit; - - vm.prank(controller); - vm.expectEmit(true, true, true, true); - emit FeeParamsUpdated(expected); - protocolConfig.updateBlockGasLimit(newBlockGasLimit); - - IProtocolConfig.FeeParams memory afterParams = protocolConfig.feeParams(); - assertEq(afterParams.blockGasLimit, newBlockGasLimit); - assertEq(afterParams.alpha, beforeParams.alpha); - assertEq(afterParams.kRate, beforeParams.kRate); - assertEq(afterParams.inverseElasticityMultiplier, beforeParams.inverseElasticityMultiplier); - assertEq(afterParams.minBaseFee, beforeParams.minBaseFee); - assertEq(afterParams.maxBaseFee, beforeParams.maxBaseFee); - } - - function test_updateBlockGasLimit__RevertsZero() public { - protocolConfig = deployProtocolConfig(owner, controller, pauser); - vm.prank(controller); - vm.expectRevert(ProtocolConfig.InvalidBlockGasLimit.selector); - protocolConfig.updateBlockGasLimit(0); - } - - function test_updateBlockGasLimit__RevertsNotAuthorized() public { - protocolConfig = deployProtocolConfig(owner, controller, pauser); - vm.prank(unauthorizedUser); - vm.expectRevert(Controller.CallerIsNotController.selector); - protocolConfig.updateBlockGasLimit(40_000_000); - } - - function test_updateBlockGasLimit__RevertsWhenPaused() public { - protocolConfig = deployProtocolConfig(owner, controller, pauser); - - vm.prank(pauser); - protocolConfig.pause(); - - vm.prank(controller); - vm.expectRevert(Pausable.ContractPaused.selector); - protocolConfig.updateBlockGasLimit(40_000_000); - } - - // ============================================================================ - // CONSENSUS PARAMETERS VALIDATION TESTS - // ============================================================================ - // Tests for updateConsensusParams validation logic and boundary conditions - - function test_updateConsensusParams__ValidParams() public { - // Deploy contract - protocolConfig = deployProtocolConfig(owner, controller, pauser); - - IProtocolConfig.ConsensusParams memory newParams = IProtocolConfig.ConsensusParams({ - timeoutProposeMs: 3000, - timeoutProposeDeltaMs: 300, - timeoutPrevoteMs: 2000, - timeoutPrevoteDeltaMs: 200, - timeoutPrecommitMs: 2000, - timeoutPrecommitDeltaMs: 200, - timeoutRebroadcastMs: 4000, - targetBlockTimeMs: 6000 - }); - - // Should succeed when called by controller - vm.prank(controller); - vm.expectEmit(true, true, true, true); - emit ConsensusParamsUpdated(newParams); - protocolConfig.updateConsensusParams(newParams); - - // Verify the params were updated - IProtocolConfig.ConsensusParams memory updatedParams = protocolConfig.consensusParams(); - assertEq(updatedParams.timeoutProposeMs, 3000); - assertEq(updatedParams.timeoutProposeDeltaMs, 300); - assertEq(updatedParams.timeoutPrevoteMs, 2000); - assertEq(updatedParams.timeoutPrevoteDeltaMs, 200); - assertEq(updatedParams.timeoutPrecommitMs, 2000); - assertEq(updatedParams.timeoutPrecommitDeltaMs, 200); - assertEq(updatedParams.timeoutRebroadcastMs, 4000); - assertEq(updatedParams.targetBlockTimeMs, 6000); - } - - function test_updateConsensusParams__InvalidTimeoutProposeMs() public { - // Deploy contract - protocolConfig = deployProtocolConfig(owner, controller, pauser); - - IProtocolConfig.ConsensusParams memory invalidParams = IProtocolConfig.ConsensusParams({ - timeoutProposeMs: 0, // Invalid: must be > 0 - timeoutProposeDeltaMs: 200, - timeoutPrevoteMs: 1000, - timeoutPrevoteDeltaMs: 100, - timeoutPrecommitMs: 1000, - timeoutPrecommitDeltaMs: 100, - timeoutRebroadcastMs: 2000, - targetBlockTimeMs: 3000 - }); - - // Should revert with specific validation error - vm.prank(controller); - vm.expectRevert(ProtocolConfig.InvalidTimeoutProposeMs.selector); - protocolConfig.updateConsensusParams(invalidParams); - } - - function test_updateConsensusParams__InvalidTimeoutProposeDeltaMs() public { - // Deploy contract - protocolConfig = deployProtocolConfig(owner, controller, pauser); - - IProtocolConfig.ConsensusParams memory invalidParams = IProtocolConfig.ConsensusParams({ - timeoutProposeMs: 2000, - timeoutProposeDeltaMs: 0, // Invalid: must be > 0 - timeoutPrevoteMs: 1000, - timeoutPrevoteDeltaMs: 100, - timeoutPrecommitMs: 1000, - timeoutPrecommitDeltaMs: 100, - timeoutRebroadcastMs: 2000, - targetBlockTimeMs: 3000 - }); - - // Should revert with specific validation error - vm.prank(controller); - vm.expectRevert(ProtocolConfig.InvalidTimeoutProposeDeltaMs.selector); - protocolConfig.updateConsensusParams(invalidParams); - } - - function test_updateConsensusParams__InvalidTimeoutPrevoteMs() public { - // Deploy contract - protocolConfig = deployProtocolConfig(owner, controller, pauser); - - IProtocolConfig.ConsensusParams memory invalidParams = IProtocolConfig.ConsensusParams({ - timeoutProposeMs: 2000, - timeoutProposeDeltaMs: 200, - timeoutPrevoteMs: 0, // Invalid: must be > 0 - timeoutPrevoteDeltaMs: 100, - timeoutPrecommitMs: 1000, - timeoutPrecommitDeltaMs: 100, - timeoutRebroadcastMs: 2000, - targetBlockTimeMs: 3000 - }); - - // Should revert with specific validation error - vm.prank(controller); - vm.expectRevert(ProtocolConfig.InvalidTimeoutPrevoteMs.selector); - protocolConfig.updateConsensusParams(invalidParams); - } - - function test_updateConsensusParams__InvalidTimeoutPrevoteDeltaMs() public { - // Deploy contract - protocolConfig = deployProtocolConfig(owner, controller, pauser); - - IProtocolConfig.ConsensusParams memory invalidParams = IProtocolConfig.ConsensusParams({ - timeoutProposeMs: 2000, - timeoutProposeDeltaMs: 200, - timeoutPrevoteMs: 1000, - timeoutPrevoteDeltaMs: 0, // Invalid: must be > 0 - timeoutPrecommitMs: 1000, - timeoutPrecommitDeltaMs: 100, - timeoutRebroadcastMs: 2000, - targetBlockTimeMs: 3000 - }); - - // Should revert with specific validation error - vm.prank(controller); - vm.expectRevert(ProtocolConfig.InvalidTimeoutPrevoteDeltaMs.selector); - protocolConfig.updateConsensusParams(invalidParams); - } - - function test_updateConsensusParams__InvalidTimeoutPrecommitMs() public { - // Deploy contract - protocolConfig = deployProtocolConfig(owner, controller, pauser); - - IProtocolConfig.ConsensusParams memory invalidParams = IProtocolConfig.ConsensusParams({ - timeoutProposeMs: 2000, - timeoutProposeDeltaMs: 200, - timeoutPrevoteMs: 1000, - timeoutPrevoteDeltaMs: 100, - timeoutPrecommitMs: 0, // Invalid: must be > 0 - timeoutPrecommitDeltaMs: 100, - timeoutRebroadcastMs: 2000, - targetBlockTimeMs: 3000 - }); - - // Should revert with specific validation error - vm.prank(controller); - vm.expectRevert(ProtocolConfig.InvalidTimeoutPrecommitMs.selector); - protocolConfig.updateConsensusParams(invalidParams); - } - - function test_updateConsensusParams__InvalidTimeoutPrecommitDeltaMs() public { - // Deploy contract - protocolConfig = deployProtocolConfig(owner, controller, pauser); - - IProtocolConfig.ConsensusParams memory invalidParams = IProtocolConfig.ConsensusParams({ - timeoutProposeMs: 2000, - timeoutProposeDeltaMs: 200, - timeoutPrevoteMs: 1000, - timeoutPrevoteDeltaMs: 100, - timeoutPrecommitMs: 1000, - timeoutPrecommitDeltaMs: 0, // Invalid: must be > 0 - timeoutRebroadcastMs: 2000, - targetBlockTimeMs: 3000 - }); - - // Should revert with specific validation error - vm.prank(controller); - vm.expectRevert(ProtocolConfig.InvalidTimeoutPrecommitDeltaMs.selector); - protocolConfig.updateConsensusParams(invalidParams); - } - - function test_updateConsensusParams__InvalidTimeoutRebroadcastMs() public { - // Deploy contract - protocolConfig = deployProtocolConfig(owner, controller, pauser); - - IProtocolConfig.ConsensusParams memory invalidParams = IProtocolConfig.ConsensusParams({ - timeoutProposeMs: 2000, - timeoutProposeDeltaMs: 200, - timeoutPrevoteMs: 1000, - timeoutPrevoteDeltaMs: 100, - timeoutPrecommitMs: 1000, - timeoutPrecommitDeltaMs: 100, - timeoutRebroadcastMs: 0, // Invalid: must be > 0 - targetBlockTimeMs: 3000 - }); - - // Should revert with specific validation error - vm.prank(controller); - vm.expectRevert(ProtocolConfig.InvalidTimeoutRebroadcastMs.selector); - protocolConfig.updateConsensusParams(invalidParams); - } - - function test_updateConsensusParams__ZeroTargetBlockTimeMs() public { - // Deploy contract - protocolConfig = deployProtocolConfig(owner, controller, pauser); - - IProtocolConfig.ConsensusParams memory paramsWithZero = IProtocolConfig.ConsensusParams({ - timeoutProposeMs: 2000, - timeoutProposeDeltaMs: 200, - timeoutPrevoteMs: 1000, - timeoutPrevoteDeltaMs: 100, - timeoutPrecommitMs: 1000, - timeoutPrecommitDeltaMs: 100, - timeoutRebroadcastMs: 2000, - targetBlockTimeMs: 0 // 0 means disabled - }); - - vm.prank(controller); - protocolConfig.updateConsensusParams(paramsWithZero); - } - - function test_updateConsensusParams__UnauthorizedUser() public { - // Deploy contract - protocolConfig = deployProtocolConfig(owner, controller, pauser); - - IProtocolConfig.ConsensusParams memory params = IProtocolConfig.ConsensusParams({ - timeoutProposeMs: 3000, - timeoutProposeDeltaMs: 300, - timeoutPrevoteMs: 2000, - timeoutPrevoteDeltaMs: 200, - timeoutPrecommitMs: 2000, - timeoutPrecommitDeltaMs: 200, - timeoutRebroadcastMs: 4000, - targetBlockTimeMs: 6000 - }); - - // Should revert when called by unauthorized user - vm.prank(unauthorizedUser); - vm.expectRevert(Controller.CallerIsNotController.selector); - protocolConfig.updateConsensusParams(params); - } - - function test_updateTimeoutProposeMs__Valid() public { - protocolConfig = deployProtocolConfig(owner, controller, pauser); - - IProtocolConfig.ConsensusParams memory beforeParams = protocolConfig.consensusParams(); - uint16 newTimeout = beforeParams.timeoutProposeMs + 1; - - IProtocolConfig.ConsensusParams memory expected = beforeParams; - expected.timeoutProposeMs = newTimeout; - - vm.prank(controller); - vm.expectEmit(true, true, true, true); - emit ConsensusParamsUpdated(expected); - protocolConfig.updateTimeoutProposeMs(newTimeout); - - IProtocolConfig.ConsensusParams memory afterParams = protocolConfig.consensusParams(); - assertEq(afterParams.timeoutProposeMs, newTimeout); - assertEq(afterParams.timeoutProposeDeltaMs, beforeParams.timeoutProposeDeltaMs); - assertEq(afterParams.timeoutPrevoteMs, beforeParams.timeoutPrevoteMs); - assertEq(afterParams.timeoutPrevoteDeltaMs, beforeParams.timeoutPrevoteDeltaMs); - assertEq(afterParams.timeoutPrecommitMs, beforeParams.timeoutPrecommitMs); - assertEq(afterParams.timeoutPrecommitDeltaMs, beforeParams.timeoutPrecommitDeltaMs); - assertEq(afterParams.timeoutRebroadcastMs, beforeParams.timeoutRebroadcastMs); - assertEq(afterParams.targetBlockTimeMs, beforeParams.targetBlockTimeMs); - } - - function test_updateTimeoutProposeMs__RevertsZero() public { - protocolConfig = deployProtocolConfig(owner, controller, pauser); - vm.prank(controller); - vm.expectRevert(ProtocolConfig.InvalidTimeoutProposeMs.selector); - protocolConfig.updateTimeoutProposeMs(0); - } - - function test_updateTimeoutProposeMs__Unauthorized() public { - protocolConfig = deployProtocolConfig(owner, controller, pauser); - vm.prank(unauthorizedUser); - vm.expectRevert(Controller.CallerIsNotController.selector); - protocolConfig.updateTimeoutProposeMs(123); - } - - function test_updateTimeoutProposeMs__Paused() public { - protocolConfig = deployProtocolConfig(owner, controller, pauser); - - vm.prank(pauser); - protocolConfig.pause(); - - vm.prank(controller); - vm.expectRevert(Pausable.ContractPaused.selector); - protocolConfig.updateTimeoutProposeMs(123); - } - - // ============================================================================ - // PAUSE FUNCTIONALITY TESTS - // ============================================================================ - // Tests for pause/unpause behavior and whenNotPaused modifier - - function test_pauseAndUnpause() public { - // Deploy contract - protocolConfig = deployProtocolConfig(owner, controller, pauser); - - // Should allow pauser to pause - vm.prank(pauser); - vm.expectEmit(false, false, false, false); - emit Pause(); - protocolConfig.pause(); - - assertTrue(protocolConfig.paused()); - - // Operations should be blocked when paused - IProtocolConfig.FeeParams memory params = IProtocolConfig.FeeParams({ - alpha: 50, - kRate: 50, - inverseElasticityMultiplier: 5000, - minBaseFee: 1000, - maxBaseFee: 2000, - blockGasLimit: 30000000 - }); - - vm.prank(controller); - vm.expectRevert(Pausable.ContractPaused.selector); - protocolConfig.updateFeeParams(params); - - // Should allow pauser to unpause - vm.prank(pauser); - vm.expectEmit(false, false, false, false); - emit Unpause(); - protocolConfig.unpause(); - - assertFalse(protocolConfig.paused()); - - // Operations should work again after unpause - vm.prank(controller); - protocolConfig.updateFeeParams(params); - } - - function test_pauseAndUnpause_IdempotentNoDuplicateEvents() public { - protocolConfig = deployProtocolConfig(owner, controller, pauser); - - vm.prank(pauser); - protocolConfig.pause(); - assertTrue(protocolConfig.paused()); - - vm.recordLogs(); - vm.prank(pauser); - protocolConfig.pause(); - Vm.Log[] memory pauseLogs = vm.getRecordedLogs(); - assertEq(pauseLogs.length, 0, "repeated pause should not emit"); - assertTrue(protocolConfig.paused()); - - vm.prank(pauser); - protocolConfig.unpause(); - assertFalse(protocolConfig.paused()); - - vm.recordLogs(); - vm.prank(pauser); - protocolConfig.unpause(); - Vm.Log[] memory unpauseLogs = vm.getRecordedLogs(); - assertEq(unpauseLogs.length, 0, "repeated unpause should not emit"); - assertFalse(protocolConfig.paused()); - } - - // ============================================================================ - // ACCESS CONTROL TESTS - // ============================================================================ - // Tests for role-based access control (owner, controller, pauser) - - function test_accessControl_UnauthorizedUser() public { - protocolConfig = deployProtocolConfig(owner, controller, pauser); - - IProtocolConfig.FeeParams memory params = IProtocolConfig.FeeParams({ - alpha: 50, - kRate: 75, - inverseElasticityMultiplier: 5000, - minBaseFee: 1000, - maxBaseFee: 2000, - blockGasLimit: 30000000 - }); - - // Unauthorized user should still get controller error - vm.prank(unauthorizedUser); - vm.expectRevert(Controller.CallerIsNotController.selector); - protocolConfig.updateFeeParams(params); - - // Owner should also get controller error (only controller can call) - vm.prank(owner); - vm.expectRevert(Controller.CallerIsNotController.selector); - protocolConfig.updateFeeParams(params); - } - - // ============================================================================ - // VIEW FUNCTIONS & STATE TESTS - // ============================================================================ - // Tests for reading contract state and view function behavior - - function test_viewFunctions() public { - protocolConfig = deployProtocolConfig(owner, controller, pauser); - - // Test that view functions work and return the initialized default values - IProtocolConfig.FeeParams memory params = protocolConfig.feeParams(); - assertEq(params.alpha, 50); - assertEq(params.kRate, 75); - assertEq(params.inverseElasticityMultiplier, 5000); - assertEq(params.minBaseFee, 1000); - assertEq(params.maxBaseFee, 2000); - assertEq(params.blockGasLimit, 30000000); - - // Also verify initialized state - assertEq(protocolConfig.owner(), owner); - assertEq(protocolConfig.controller(), controller); - assertEq(protocolConfig.pauser(), pauser); - assertFalse(protocolConfig.paused()); - } - - function test_pauserAccessControl() public { - protocolConfig = deployProtocolConfig(owner, controller, pauser); - - // Unauthorized user should not be able to pause - vm.prank(unauthorizedUser); - vm.expectRevert(Pausable.CallerIsNotPauser.selector); - protocolConfig.pause(); - - // Controller should not be able to pause (only pauser can) - vm.prank(controller); - vm.expectRevert(Pausable.CallerIsNotPauser.selector); - protocolConfig.pause(); - - // Owner should not be able to pause (only pauser can) - vm.prank(owner); - vm.expectRevert(Pausable.CallerIsNotPauser.selector); - protocolConfig.pause(); - - // Pauser should be able to pause - vm.prank(pauser); - protocolConfig.pause(); - assertTrue(protocolConfig.paused()); - - // Pauser should be able to unpause - vm.prank(pauser); - protocolConfig.unpause(); - assertFalse(protocolConfig.paused()); - } - - function test_ownerAccessControl() public { - protocolConfig = deployProtocolConfig(owner, controller, pauser); - - address newController = makeAddr("newController"); - address newPauser = makeAddr("newPauser"); - - // Owner should be able to update controller - vm.prank(owner); - vm.expectEmit(true, true, true, true); - emit ControllerUpdated(newController); - protocolConfig.updateController(newController); - assertEq(protocolConfig.controller(), newController); - - // Owner should be able to update pauser - vm.prank(owner); - vm.expectEmit(true, true, true, true); - emit PauserChanged(newPauser); - protocolConfig.updatePauser(newPauser); - assertEq(protocolConfig.pauser(), newPauser); - - // Unauthorized user should not be able to update controller - vm.prank(unauthorizedUser); - vm.expectRevert(); - protocolConfig.updateController(controller); - - // Controller should not be able to update pauser - vm.prank(newController); - vm.expectRevert(); - protocolConfig.updatePauser(pauser); - } - - function test_updateController_ZeroAddressValidation() public { - protocolConfig = deployProtocolConfig(owner, controller, pauser); - - // Should revert when trying to set controller to zero address - vm.prank(owner); - vm.expectRevert(Controller.ZeroControllerAddress.selector); - protocolConfig.updateController(address(0)); - } - - function test_updatePauser_ZeroAddressValidation() public { - protocolConfig = deployProtocolConfig(owner, controller, pauser); - - // Should revert when trying to set pauser to zero address - vm.prank(owner); - vm.expectRevert(Pausable.ZeroPauserAddress.selector); - protocolConfig.updatePauser(address(0)); - } - - function test_contractStateTransitions() public { - protocolConfig = deployProtocolConfig(owner, controller, pauser); - - // Verify properly initialized state - assertEq(protocolConfig.owner(), owner); - assertEq(protocolConfig.controller(), controller); - assertEq(protocolConfig.pauser(), pauser); - assertFalse(protocolConfig.paused()); - - // Verify fee params are set to default values - IProtocolConfig.FeeParams memory params = protocolConfig.feeParams(); - assertEq(params.alpha, 50); - assertEq(params.blockGasLimit, 30000000); - - // Test pause state transition - vm.prank(pauser); - protocolConfig.pause(); - assertTrue(protocolConfig.paused()); - - vm.prank(pauser); - protocolConfig.unpause(); - assertFalse(protocolConfig.paused()); - } - - function test_extremeFeeParamsValues() public { - protocolConfig = deployProtocolConfig(owner, controller, pauser); - - // Test with extreme but valid values - should succeed - IProtocolConfig.FeeParams memory extremeParams = IProtocolConfig.FeeParams({ - alpha: 100, // Maximum valid value - kRate: 10000, // Maximum valid value - inverseElasticityMultiplier: 10000, - minBaseFee: 1, - maxBaseFee: type(uint256).max, - blockGasLimit: type(uint256).max - }); - - vm.prank(controller); - protocolConfig.updateFeeParams(extremeParams); - - // Verify the extreme values were set - IProtocolConfig.FeeParams memory setParams = protocolConfig.feeParams(); - assertEq(setParams.alpha, 100); - assertEq(setParams.kRate, 10000); - assertEq(setParams.inverseElasticityMultiplier, 10000); - - // Test edge case where minBaseFee equals maxBaseFee - should be valid - extremeParams.minBaseFee = 1000; - extremeParams.maxBaseFee = 1000; - - vm.prank(controller); - protocolConfig.updateFeeParams(extremeParams); - - // Test with invalid extreme values (alpha > 100) - IProtocolConfig.FeeParams memory invalidExtremeParams = IProtocolConfig.FeeParams({ - alpha: type(uint64).max, // Invalid: > 100 - kRate: 50, - inverseElasticityMultiplier: 10000, - minBaseFee: 1, - maxBaseFee: type(uint256).max, - blockGasLimit: type(uint256).max - }); - - vm.prank(controller); - vm.expectRevert(ProtocolConfig.InvalidAlpha.selector); - protocolConfig.updateFeeParams(invalidExtremeParams); - - // Test with invalid kRate > 10000 - invalidExtremeParams.alpha = 50; - invalidExtremeParams.kRate = type(uint64).max; // Invalid: > 10000 - - vm.prank(controller); - vm.expectRevert(ProtocolConfig.InvalidKRate.selector); - protocolConfig.updateFeeParams(invalidExtremeParams); - } - - function test_pauseBehaviorAccess() public { - protocolConfig = deployProtocolConfig(owner, controller, pauser); - - // Pauser should be able to pause - vm.prank(pauser); - protocolConfig.pause(); - assertTrue(protocolConfig.paused()); - - // Pauser should be able to unpause - vm.prank(pauser); - protocolConfig.unpause(); - assertFalse(protocolConfig.paused()); - - // Unauthorized user cannot pause - vm.prank(unauthorizedUser); - vm.expectRevert(Pausable.CallerIsNotPauser.selector); - protocolConfig.pause(); - - // Owner cannot pause (only pauser can) - vm.prank(owner); - vm.expectRevert(Pausable.CallerIsNotPauser.selector); - protocolConfig.pause(); - - // Controller cannot pause (only pauser can) - vm.prank(controller); - vm.expectRevert(Pausable.CallerIsNotPauser.selector); - protocolConfig.pause(); - } - - function test_whenNotPausedModifier() public { - protocolConfig = deployProtocolConfig(owner, controller, pauser); - - // The whenNotPaused modifier should work correctly - // Since the contract starts unpaused, operations should be allowed - - IProtocolConfig.FeeParams memory params = IProtocolConfig.FeeParams({ - alpha: 50, - kRate: 75, - inverseElasticityMultiplier: 5000, - minBaseFee: 1000, - maxBaseFee: 2000, - blockGasLimit: 30000000 - }); - - // Should succeed when unpaused - vm.prank(controller); - protocolConfig.updateFeeParams(params); - - // Now pause the contract - vm.prank(pauser); - protocolConfig.pause(); - assertTrue(protocolConfig.paused()); - - // Operations should fail when paused - vm.prank(controller); - vm.expectRevert(Pausable.ContractPaused.selector); - protocolConfig.updateFeeParams(params); - - // Unpause and operations should work again - vm.prank(pauser); - protocolConfig.unpause(); - assertFalse(protocolConfig.paused()); - - vm.prank(controller); - protocolConfig.updateFeeParams(params); - } - - // ============ Alpha and kRate Validation Tests ============ - - function test_alphaKRateValidation_BoundaryValues() public { - protocolConfig = deployProtocolConfig(owner, controller, pauser); - - // Test boundary values for alpha (0-100) and kRate (0-10000) should be valid - IProtocolConfig.FeeParams memory validParams; - - // Test alpha = 0, kRate = 0 (should pass validation and succeed) - validParams = IProtocolConfig.FeeParams({ - alpha: 0, - kRate: 0, - inverseElasticityMultiplier: 5000, - minBaseFee: 1000, - maxBaseFee: 2000, - blockGasLimit: 30000000 - }); - - vm.prank(controller); - protocolConfig.updateFeeParams(validParams); - - // Verify the boundary values were set - IProtocolConfig.FeeParams memory setParams = protocolConfig.feeParams(); - assertEq(setParams.alpha, 0); - assertEq(setParams.kRate, 0); - - // Test alpha = 100, kRate = 10000 (should pass validation and succeed) - validParams.alpha = 100; - validParams.kRate = 10000; - - vm.prank(controller); - protocolConfig.updateFeeParams(validParams); - - // Verify the maximum boundary values were set - IProtocolConfig.FeeParams memory maxParams = protocolConfig.feeParams(); - assertEq(maxParams.alpha, 100); - assertEq(maxParams.kRate, 10000); - } - - // ============================================================================ - // STORAGE & ERC-7201 TESTS - // ============================================================================ - // Tests for storage layout and ERC-7201 namespaced storage implementation - - function test_erc7201StorageSlotCalculation() public pure { - // Test the ERC-7201 storage slot calculation is correct - // Formula: keccak256(abi.encode(uint256(keccak256("namespace")) - 1)) & ~bytes32(uint256(0xff)) - - string memory namespace = "arc.storage.ProtocolConfig"; - bytes32 expectedSlot = keccak256(abi.encode(uint256(keccak256(bytes(namespace))) - 1)) & ~bytes32(uint256(0xff)); - - // The expected slot should match the constant in the contract - bytes32 contractSlot = 0x668f09ce856848ead6cb1ddee963f15ef833cea8958030868f867aec84385200; - - assertEq(expectedSlot, contractSlot, "ERC-7201 storage slot calculation mismatch"); - } - - function test_erc7201StorageLayout() public { - // Test the storage layout within our custom storage slot - ProtocolConfig newContract = new ProtocolConfig(); - - // The storage struct should have: - // - FeeParams feeParams (first field, multiple slots) - // - slot +4: deprecated address placeholder - // - ConsensusParams consensusParams - - // Test initial values are zero (default) - IProtocolConfig.FeeParams memory params = newContract.feeParams(); - assertEq(params.alpha, 0); - assertEq(params.kRate, 0); - assertEq(params.inverseElasticityMultiplier, 0); - assertEq(params.minBaseFee, 0); - assertEq(params.maxBaseFee, 0); - assertEq(params.blockGasLimit, 0); - } -} diff --git a/contracts/test/protocol-config/ProtocolConfigProxy.t.sol b/contracts/test/protocol-config/ProtocolConfigProxy.t.sol deleted file mode 100644 index 59f7a06..0000000 --- a/contracts/test/protocol-config/ProtocolConfigProxy.t.sol +++ /dev/null @@ -1,350 +0,0 @@ -// Copyright 2025 Circle Internet Group, Inc. All rights reserved. -// -// SPDX-License-Identifier: Apache-2.0 -// -// Licensed under the Apache License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. -// You may obtain a copy of the License at -// -// http://www.apache.org/licenses/LICENSE-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// See the License for the specific language governing permissions and -// limitations under the License. - -pragma solidity ^0.8.29; - -import {Test} from "forge-std/Test.sol"; -import {AdminUpgradeableProxy} from "../../src/proxy/AdminUpgradeableProxy.sol"; -import {ProtocolConfig} from "../../src/protocol-config/ProtocolConfig.sol"; -import {IProtocolConfig} from "../../src/protocol-config/interfaces/IProtocolConfig.sol"; - -/** - * @title ProtocolConfigProxyTest - * @dev Test suite for ProtocolConfig when deployed behind AdminUpgradeableProxy - * @dev Tests ProtocolConfig-specific functionality and business logic through proxy - */ -contract ProtocolConfigProxyTest is Test { - // ============ Constants ============ - bytes32 constant PROTOCOL_CONFIG_STORAGE_LOCATION = - 0x668f09ce856848ead6cb1ddee963f15ef833cea8958030868f867aec84385200; - - // ============ State Variables ============ - - AdminUpgradeableProxy public proxy; - ProtocolConfig public implementation; - ProtocolConfig public protocolConfig; - address public actualProxyAdmin; // The actual admin address read from proxy storage - - // Test role addresses - address public proxyAdminAddress; // The address we set as proxy admin - address public controller; // Controller role for ProtocolConfig - address public pauser; // Pauser role for ProtocolConfig - address public implementationOwner; // Owner of the ProtocolConfig implementation - - // Default test parameters - IProtocolConfig.FeeParams public defaultFeeParams = IProtocolConfig.FeeParams({ - alpha: 50, - kRate: 75, - inverseElasticityMultiplier: 5000, - minBaseFee: 1000, - maxBaseFee: 2000, - blockGasLimit: 30000000 - }); - - IProtocolConfig.ConsensusParams public defaultConsensusParams = IProtocolConfig.ConsensusParams({ - timeoutProposeMs: 2000, - timeoutProposeDeltaMs: 200, - timeoutPrevoteMs: 1000, - timeoutPrevoteDeltaMs: 100, - timeoutPrecommitMs: 1000, - timeoutPrecommitDeltaMs: 100, - timeoutRebroadcastMs: 2000, - targetBlockTimeMs: 3000 - }); - - // ============ Setup ============ - - function setUp() public { - // Create test addresses - proxyAdminAddress = makeAddr("proxyAdminAddress"); - controller = makeAddr("controller"); - pauser = makeAddr("pauser"); - implementationOwner = makeAddr("implementationOwner"); - - // Deploy implementation contract - implementation = new ProtocolConfig(); - - // Deploy proxy without initialization data (will be set via storage manipulation) - proxy = new AdminUpgradeableProxy( - address(implementation), - proxyAdminAddress, - "" // No initialization data - will be set through genesis-style storage manipulation - ); - - // Get proxy as ProtocolConfig interface - protocolConfig = ProtocolConfig(address(proxy)); - - // Simulate genesis file initialization by directly setting storage - _simulateGenesisStorageInitialization( - controller, pauser, defaultFeeParams, defaultConsensusParams - ); - - // Get the actual proxy admin address from ERC1967 storage - actualProxyAdmin = address( - uint160( - uint256(vm.load(address(proxy), 0xb53127684a568b3173ae13b9f8a6016e243e63b6e8ee1178d6a717850b5d6103)) - ) - ); - } - - // Helper function to simulate genesis file initialization using storage manipulation - function _simulateGenesisStorageInitialization( - address _controller, - address _pauser, - IProtocolConfig.FeeParams memory _feeParams, - IProtocolConfig.ConsensusParams memory _consensusParams - ) internal { - // === Set Controller storage === - // Controller.controller is in slot 0 (Ownable2StepUpgradeable uses ERC-7201 namespaced storage) - // Controller: ERC-7201 slot for arc.storage.ProtocolConfigController - bytes32 controllerSlot = 0x958f8fec699b51a1249f513eceda5429078000657f74abd1721bba363087af00; - vm.store(address(protocolConfig), controllerSlot, bytes32(uint256(uint160(_controller)))); - - // === Set Ownable owner in ERC-7201 storage === - // Calculate the correct ERC-7201 storage slot for Ownable owner - // Pattern: keccak256(abi.encode(uint256(keccak256("openzeppelin.storage.Ownable")) - 1)) & ~bytes32(uint256(0xff)) - bytes32 ownableStorageSlot = - keccak256(abi.encode(uint256(keccak256("openzeppelin.storage.Ownable")) - 1)) & ~bytes32(uint256(0xff)); - vm.store(address(protocolConfig), ownableStorageSlot, bytes32(uint256(uint160(_controller)))); - - // === Set Pauser storage === - // Slot 1: pauser (address, offset 0) + paused (bool, offset 20) - packed together - bytes32 packedPauserSlot = bytes32( - uint256(uint160(_pauser)) // pauser in bytes 0-19 - | (uint256(0) << 160) // paused = false in byte 20 (offset 20) - ); - // Pausable: ERC-7201 slot for arc.storage.Pausable - // struct PausableStorage { address pauser; bool paused; } - // Both packed into same slot: pauser (20 bytes) + paused (1 byte) - bytes32 pausableSlot = 0x0642d7922329a434cf4fd17a3c95eb692c24fd95f9f94d0b55420a5d895f4a00; - vm.store(address(protocolConfig), pausableSlot, packedPauserSlot); - // === Set ProtocolConfig ERC-7201 storage === - // FeeParams struct - packed in storage slots - // Slot 0: alpha (uint64) + kRate (uint64) + inverseElasticityMultiplier (uint64) + 8 bytes unused - bytes32 packedSlot0 = bytes32( - (uint256(_feeParams.alpha)) | (uint256(_feeParams.kRate) << 64) - | (uint256(_feeParams.inverseElasticityMultiplier) << 128) - ); - vm.store(address(protocolConfig), PROTOCOL_CONFIG_STORAGE_LOCATION, packedSlot0); - - // Slot 1: minBaseFee (uint256) - vm.store( - address(protocolConfig), - bytes32(uint256(PROTOCOL_CONFIG_STORAGE_LOCATION) + 1), - bytes32(uint256(_feeParams.minBaseFee)) - ); - - // Slot 2: maxBaseFee (uint256) - vm.store( - address(protocolConfig), - bytes32(uint256(PROTOCOL_CONFIG_STORAGE_LOCATION) + 2), - bytes32(uint256(_feeParams.maxBaseFee)) - ); - - // Slot 3: blockGasLimit (uint256) - vm.store( - address(protocolConfig), - bytes32(uint256(PROTOCOL_CONFIG_STORAGE_LOCATION) + 3), - bytes32(uint256(_feeParams.blockGasLimit)) - ); - - // Slot 4: deprecated address placeholder. Left zeroed. - - // ConsensusParams struct layout: - // - timeoutProposeMs (uint16), timeoutProposeDeltaMs (uint16), timeoutPrevoteMs (uint16), - // timeoutPrevoteDeltaMs (uint16), timeoutPrecommitMs (uint16), timeoutPrecommitDeltaMs (uint16), - // timeoutRebroadcastMs (uint16), targetBlockTimeMs (uint16) - // pack into one slot (8 * 16 = 128 bits, fits in one slot) - - // Slot 5: packed consensus params - bytes32 packedConsensusSlot = bytes32( - (uint256(_consensusParams.timeoutProposeMs)) | (uint256(_consensusParams.timeoutProposeDeltaMs) << 16) - | (uint256(_consensusParams.timeoutPrevoteMs) << 32) - | (uint256(_consensusParams.timeoutPrevoteDeltaMs) << 48) - | (uint256(_consensusParams.timeoutPrecommitMs) << 64) - | (uint256(_consensusParams.timeoutPrecommitDeltaMs) << 80) - | (uint256(_consensusParams.timeoutRebroadcastMs) << 96) - | (uint256(_consensusParams.targetBlockTimeMs) << 112) - ); - vm.store(address(protocolConfig), bytes32(uint256(PROTOCOL_CONFIG_STORAGE_LOCATION) + 5), packedConsensusSlot); - } - - // ============ ProtocolConfig Functionality Tests ============ - - function test_ProtocolConfigInitialization() public view { - // Verify ProtocolConfig is initialized correctly through proxy - IProtocolConfig.FeeParams memory params = protocolConfig.feeParams(); - assertEq(params.alpha, defaultFeeParams.alpha); - assertEq(params.kRate, defaultFeeParams.kRate); - assertEq(params.inverseElasticityMultiplier, defaultFeeParams.inverseElasticityMultiplier); - assertEq(params.minBaseFee, defaultFeeParams.minBaseFee); - assertEq(params.maxBaseFee, defaultFeeParams.maxBaseFee); - assertEq(params.blockGasLimit, defaultFeeParams.blockGasLimit); - - // Verify consensus params are initialized correctly - IProtocolConfig.ConsensusParams memory consensusParams = protocolConfig.consensusParams(); - assertEq(consensusParams.timeoutProposeMs, defaultConsensusParams.timeoutProposeMs); - assertEq(consensusParams.timeoutProposeDeltaMs, defaultConsensusParams.timeoutProposeDeltaMs); - assertEq(consensusParams.timeoutPrevoteMs, defaultConsensusParams.timeoutPrevoteMs); - assertEq(consensusParams.timeoutPrevoteDeltaMs, defaultConsensusParams.timeoutPrevoteDeltaMs); - assertEq(consensusParams.timeoutPrecommitMs, defaultConsensusParams.timeoutPrecommitMs); - assertEq(consensusParams.timeoutPrecommitDeltaMs, defaultConsensusParams.timeoutPrecommitDeltaMs); - assertEq(consensusParams.timeoutRebroadcastMs, defaultConsensusParams.timeoutRebroadcastMs); - assertEq(consensusParams.targetBlockTimeMs, defaultConsensusParams.targetBlockTimeMs); - } - - function test_UpdateFeeParamsViaProxy() public { - // Test updating fee parameters through the proxy - IProtocolConfig.FeeParams memory newParams = IProtocolConfig.FeeParams({ - alpha: 60, - kRate: 80, - inverseElasticityMultiplier: 3333, - minBaseFee: 1500, - maxBaseFee: 2500, - blockGasLimit: 35000000 - }); - - vm.prank(controller); - protocolConfig.updateFeeParams(newParams); - - IProtocolConfig.FeeParams memory updatedParams = protocolConfig.feeParams(); - assertEq(updatedParams.alpha, 60); - assertEq(updatedParams.kRate, 80); - assertEq(updatedParams.inverseElasticityMultiplier, 3333); - assertEq(updatedParams.minBaseFee, 1500); - assertEq(updatedParams.maxBaseFee, 2500); - assertEq(updatedParams.blockGasLimit, 35000000); - } - - function test_UpdateConsensusParamsViaProxy() public { - // Test updating consensus parameters through the proxy - IProtocolConfig.ConsensusParams memory newParams = IProtocolConfig.ConsensusParams({ - timeoutProposeMs: 3000, - timeoutProposeDeltaMs: 300, - timeoutPrevoteMs: 2000, - timeoutPrevoteDeltaMs: 200, - timeoutPrecommitMs: 2000, - timeoutPrecommitDeltaMs: 200, - timeoutRebroadcastMs: 4000, - targetBlockTimeMs: 6000 - }); - - vm.prank(controller); - protocolConfig.updateConsensusParams(newParams); - - IProtocolConfig.ConsensusParams memory updatedParams = protocolConfig.consensusParams(); - assertEq(updatedParams.timeoutProposeMs, 3000); - assertEq(updatedParams.timeoutProposeDeltaMs, 300); - assertEq(updatedParams.timeoutPrevoteMs, 2000); - assertEq(updatedParams.timeoutPrevoteDeltaMs, 200); - assertEq(updatedParams.timeoutPrecommitMs, 2000); - assertEq(updatedParams.timeoutPrecommitDeltaMs, 200); - assertEq(updatedParams.timeoutRebroadcastMs, 4000); - assertEq(updatedParams.targetBlockTimeMs, 6000); - } - - function test_AccessControlViaProxy() public { - // Test that access control works through the proxy - IProtocolConfig.FeeParams memory newParams = IProtocolConfig.FeeParams({ - alpha: 70, - kRate: 85, - inverseElasticityMultiplier: 2500, - minBaseFee: 2000, - maxBaseFee: 3000, - blockGasLimit: 40000000 - }); - - // Non-controller should not be able to update - vm.prank(implementationOwner); - vm.expectRevert(); // Should revert with access control error - protocolConfig.updateFeeParams(newParams); - - // Controller should be able to update - vm.prank(controller); - protocolConfig.updateFeeParams(newParams); // Should succeed - - IProtocolConfig.FeeParams memory updatedParams = protocolConfig.feeParams(); - assertEq(updatedParams.alpha, 70); - } - - // ============ ProtocolConfig Upgrade Tests ============ - - function test_ProtocolConfigUpgradeWithInitialization() public { - // Deploy a new ProtocolConfig implementation - ProtocolConfig newImplementation = new ProtocolConfig(); - - // Now that owner is properly set, test updateController functionality - // Controller is also the owner, so they can call updateController - vm.prank(controller); - protocolConfig.updateController(proxyAdminAddress); - - // Prepare initialization data for updateFeeParams (called by new controller = proxyAdminAddress) - IProtocolConfig.FeeParams memory upgradeParams = IProtocolConfig.FeeParams({ - alpha: 90, - kRate: 95, - inverseElasticityMultiplier: 2000, - minBaseFee: 3000, - maxBaseFee: 4000, - blockGasLimit: 45000000 - }); - - bytes memory initData = - abi.encodeWithSignature("updateFeeParams((uint64,uint64,uint64,uint256,uint256,uint256))", upgradeParams); - - // Perform upgrade with initialization - vm.prank(proxyAdminAddress); - proxy.upgradeToAndCall(address(newImplementation), initData); - - // Verify the upgrade succeeded and initialization was called - IProtocolConfig.FeeParams memory updatedParams = protocolConfig.feeParams(); - assertEq(updatedParams.alpha, 90); - assertEq(updatedParams.kRate, 95); - assertEq(updatedParams.inverseElasticityMultiplier, 2000); - } - - function test_ProtocolConfigStatePreservationAcrossUpgrade() public { - // Modify state through proxy - IProtocolConfig.FeeParams memory modifiedParams = IProtocolConfig.FeeParams({ - alpha: 77, - kRate: 88, - inverseElasticityMultiplier: 1666, - minBaseFee: 1111, - maxBaseFee: 2222, - blockGasLimit: 33333333 - }); - - vm.prank(controller); - protocolConfig.updateFeeParams(modifiedParams); - - // Verify state before upgrade - IProtocolConfig.FeeParams memory paramsBeforeUpgrade = protocolConfig.feeParams(); - assertEq(paramsBeforeUpgrade.alpha, 77); - - // Deploy new implementation and upgrade - ProtocolConfig newImplementation = new ProtocolConfig(); - vm.prank(proxyAdminAddress); - proxy.upgradeTo(address(newImplementation)); - - // Verify state is preserved after upgrade - IProtocolConfig.FeeParams memory paramsAfterUpgrade = protocolConfig.feeParams(); - assertEq(paramsAfterUpgrade.alpha, 77); - assertEq(paramsAfterUpgrade.kRate, 88); - assertEq(paramsAfterUpgrade.inverseElasticityMultiplier, 1666); - assertEq(paramsAfterUpgrade.minBaseFee, 1111); - assertEq(paramsAfterUpgrade.maxBaseFee, 2222); - assertEq(paramsAfterUpgrade.blockGasLimit, 33333333); - } -} diff --git a/contracts/test/proxy/AdminUpgradeableProxy.t.sol b/contracts/test/proxy/AdminUpgradeableProxy.t.sol deleted file mode 100644 index 5433258..0000000 --- a/contracts/test/proxy/AdminUpgradeableProxy.t.sol +++ /dev/null @@ -1,559 +0,0 @@ -// Copyright 2025 Circle Internet Group, Inc. All rights reserved. -// -// SPDX-License-Identifier: Apache-2.0 -// -// Licensed under the Apache License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. -// You may obtain a copy of the License at -// -// http://www.apache.org/licenses/LICENSE-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// See the License for the specific language governing permissions and -// limitations under the License. - -pragma solidity ^0.8.29; - -import {Test} from "forge-std/Test.sol"; -import {AdminUpgradeableProxy} from "../../src/proxy/AdminUpgradeableProxy.sol"; -import {TestImplementation} from "../../src/mocks/TestImplementation.sol"; -import {CallHelper} from "../../src/mocks/CallHelper.sol"; -import {ProtocolConfig} from "../../src/protocol-config/ProtocolConfig.sol"; -import {ERC1967Utils} from "@openzeppelin/contracts/proxy/ERC1967/ERC1967Utils.sol"; - -/** - * @title AdminUpgradeableProxyTest - * @dev Implementation-agnostic test suite for AdminUpgradeableProxy functionality - */ -contract AdminUpgradeableProxyTest is Test { - // ============ State Variables ============ - - AdminUpgradeableProxy public proxy; - TestImplementation public implementation; - TestImplementation public testContract; - address public actualProxyAdmin; // The actual admin address read from proxy storage - - // Test role addresses - address public proxyAdminAddress; // The address we set as proxy admin - address public implementationOwner; // Owner of the TestImplementation contract - address public user; // Regular user for testing - - // Default test parameters - uint256 public constant DEFAULT_VALUE = 42; - string public constant DEFAULT_NAME = "TestContract"; - - // ============ Setup ============ - - function setUp() public { - // Create test addresses - proxyAdminAddress = makeAddr("proxyAdminAddress"); - implementationOwner = makeAddr("implementationOwner"); - user = makeAddr("user"); - - // Deploy implementation contract - implementation = new TestImplementation(); - - // Deploy proxy without initialization - proxy = new AdminUpgradeableProxy( - address(implementation), - proxyAdminAddress, - "" // No initialization data - ); - - // Get proxy as TestImplementation interface - testContract = TestImplementation(payable(address(proxy))); - - // Initialize the contract through the proxy - testContract.initialize(implementationOwner, DEFAULT_VALUE, DEFAULT_NAME); - - // Get the actual proxy admin address from ERC1967 storage - actualProxyAdmin = address( - uint160( - uint256(vm.load(address(proxy), 0xb53127684a568b3173ae13b9f8a6016e243e63b6e8ee1178d6a717850b5d6103)) - ) - ); - } - - // ============ Basic Proxy Tests ============ - - function test_ProxyDeployment() public view { - // Verify proxy is deployed correctly - assertTrue(address(proxy) != address(0)); - assertTrue(address(testContract) == address(proxy)); - - // Verify proxy admin is set correctly using view function - assertEq(proxy.admin(), proxyAdminAddress); - assertTrue(actualProxyAdmin != address(0)); - assertEq(actualProxyAdmin, proxyAdminAddress); - - // Verify implementation is set correctly using view function - assertEq(proxy.implementation(), address(implementation)); - } - - function test_ProxyInitialization() public view { - // Verify TestImplementation is initialized correctly through proxy - assertTrue(testContract.isInitialized()); - assertEq(testContract.getOwner(), implementationOwner); - assertEq(testContract.getValue(), DEFAULT_VALUE); - assertEq(testContract.getName(), DEFAULT_NAME); - } - - function test_ProxyConstructorInitDataPath() public { - // fresh implementation - TestImplementation impl = new TestImplementation(); - - // prepare init data for delegatecall in constructor - bytes memory initData = abi.encodeWithSelector( - TestImplementation.initialize.selector, implementationOwner, DEFAULT_VALUE, DEFAULT_NAME - ); - - // deploy with init data - AdminUpgradeableProxy _proxy = new AdminUpgradeableProxy(address(impl), proxyAdminAddress, initData); - - // interact via the proxy as the implementation interface - TestImplementation _impl = TestImplementation(payable(address(_proxy))); - - // verify initialized state came from the constructor delegatecall - assertTrue(_impl.isInitialized()); - assertEq(_impl.getOwner(), implementationOwner); - assertEq(_impl.getValue(), DEFAULT_VALUE); - assertEq(_impl.getName(), DEFAULT_NAME); - } - - function test_ProxyFunctionality() public { - // Test that we can call implementation functions through the proxy - vm.prank(implementationOwner); - testContract.updateValue(100); - assertEq(testContract.getValue(), 100); - - vm.prank(implementationOwner); - testContract.updateName("Updated"); - assertEq(testContract.getName(), "Updated"); - } - - // ============ Upgrade Tests ============ - - function test_ProxyUpgrade() public { - // Deploy a new implementation - TestImplementation newImplementation = new TestImplementation(); - - // Verify initial state - assertEq(testContract.getValue(), DEFAULT_VALUE); - - // Upgrade through proxy admin - vm.prank(proxyAdminAddress); - proxy.upgradeTo(address(newImplementation)); - - // Verify upgrade occurred but state is preserved - assertEq(testContract.getValue(), DEFAULT_VALUE); - assertEq(testContract.getName(), DEFAULT_NAME); - assertTrue(testContract.isInitialized()); - } - - function test_ProxyUpgradeToAndCall() public { - // Deploy a new implementation - TestImplementation newImplementation = new TestImplementation(); - - // Upgrade and call with empty data - vm.prank(proxyAdminAddress); - proxy.upgradeToAndCall(address(newImplementation), ""); - - // Verify state is preserved - assertEq(testContract.getValue(), DEFAULT_VALUE); - } - - function test_ProxyUpgradeToAndCallWithData() public { - // Deploy a new implementation - TestImplementation newImplementation = new TestImplementation(); - - // Prepare call data for updateValue (only owner can call) - bytes memory initData = abi.encodeWithSignature("updateValue(uint256)", 999); - - // This should revert because the call is made as proxy admin, not as the implementation owner - vm.prank(proxyAdminAddress); - vm.expectRevert(); // Should revert because proxyAdminAddress is not the owner - proxy.upgradeToAndCall(address(newImplementation), initData); - } - - function test_ProxyUpgradeToAndCallSuccessful() public { - // Deploy a new implementation - TestImplementation newImplementation = new TestImplementation(); - - // Change the owner to proxyAdminAddress so the call can succeed - vm.prank(implementationOwner); - testContract.changeOwner(proxyAdminAddress); - - // Prepare call data for updateValue (now proxyAdminAddress is owner) - bytes memory initData = abi.encodeWithSignature("updateValue(uint256)", 999); - - // This should succeed now - vm.prank(proxyAdminAddress); - proxy.upgradeToAndCall(address(newImplementation), initData); - - // Verify state was updated - assertEq(testContract.getValue(), 999); - } - - function test_ProxyUpgradeToAndCallWithEther() public { - // Deploy a new implementation - TestImplementation newImplementation = new TestImplementation(); - - // Prepare call data for processWithValue (payable function) - bytes memory initData = abi.encodeWithSignature("processWithValue(bytes)", "test"); - - // Give the proxy admin some ETH - vm.deal(proxyAdminAddress, 1 ether); - - // This should succeed - calling a payable function with ETH - vm.prank(proxyAdminAddress); - proxy.upgradeToAndCall{value: 0.5 ether}(address(newImplementation), initData); - - // Verify ETH was sent to the implementation - assertEq(address(testContract).balance, 0.5 ether); - } - - function test_ProxyUpgradeToAndCallRejectsEtherWithEmptyData() public { - // Deploy a new implementation - TestImplementation newImplementation = new TestImplementation(); - - // Give the proxy admin some ETH - vm.deal(proxyAdminAddress, 1 ether); - - // Should revert when sending ETH with empty data - vm.prank(proxyAdminAddress); - vm.expectRevert(ERC1967Utils.ERC1967NonPayable.selector); // ERC1967NonPayable error from ERC1967Utils - proxy.upgradeToAndCall{value: 0.5 ether}(address(newImplementation), ""); - } - - // ============ Access Control Tests ============ - - function test_ProxyUpgradeToAndCallAccessControl() public { - // Deploy a new implementation - TestImplementation newImplementation = new TestImplementation(); - bytes memory emptyData = ""; - - // Test that non-admin cannot call upgradeToAndCall - // Should not revert with access control error because it falls back to implementation - vm.prank(user); - proxy.upgradeToAndCall(address(newImplementation), emptyData); - - // Test that admin can call upgradeToAndCall - vm.prank(proxyAdminAddress); - proxy.upgradeToAndCall(address(newImplementation), emptyData); // Should succeed - } - - // ============ Transparency Tests ============ - - function test_AdminCanCallImplementation() public { - // Proxy admin CANNOT call implementation functions (blocked by transparency) - vm.prank(address(actualProxyAdmin)); - testContract.getValue(); // Should not revert - - vm.prank(address(actualProxyAdmin)); - testContract.getName(); // Should not revert - } - - function test_ProxyFallback() public { - // Test that ProxyDeniedAdminAccess error is reverted for low-level calls through fallback - vm.prank(address(actualProxyAdmin)); - (bool success1,) = address(proxy).call(abi.encodeWithSignature("getValue()")); - assertTrue(success1); - - vm.prank(address(actualProxyAdmin)); - (bool success2,) = address(proxy).call(abi.encodeWithSignature("getName()")); - assertTrue(success2); - - // Verify non-admin can make low-level calls successfully - vm.prank(implementationOwner); - (bool success, bytes memory data) = address(proxy).call(abi.encodeWithSignature("getValue()")); - assertTrue(success); - assertTrue(data.length > 0); - } - - function test_NonAdminCanCallImplementation() public { - // Non-admin users should be able to call implementation functions - vm.prank(implementationOwner); - uint256 value = testContract.getValue(); - assertEq(value, DEFAULT_VALUE); - - vm.prank(user); - string memory name = testContract.getName(); - assertEq(name, DEFAULT_NAME); - - // Owner can modify state - vm.prank(implementationOwner); - testContract.updateValue(999); - assertEq(testContract.getValue(), 999); - } - - function test_NonAdminCallsAdminFunctionFallsBackToImplementation() public { - // Test that non-admin calls to admin functions fall back to implementation - // TestImplementation has a permissive fallback, so these calls succeed (proving delegation) - - address oldImpl = proxy.implementation(); - - vm.prank(user); - proxy.upgradeTo(address(implementation)); // This gets delegated to TestImplementation fallback - // Implementation should NOT change because it was delegated, not executed as admin function - assertEq(proxy.implementation(), oldImpl); // Proves it was delegated, not executed as admin - - vm.prank(implementationOwner); - proxy.changeAdmin(user); // This also gets delegated to TestImplementation fallback - // Admin should NOT change because it was delegated, not executed as admin function - assertEq(proxy.admin(), proxyAdminAddress); // Proves it was delegated, not executed as admin - } - - function test_FallbackBehaviorDemonstration() public { - // Demonstrate the difference between admin and non-admin behavior - - // 1. Non-admin calling existing implementation function should succeed (normal delegation) - vm.prank(user); - uint256 value = testContract.getValue(); // This exists on TestImplementation - assertEq(value, DEFAULT_VALUE); - - // 2. Admin calling admin function should execute as admin function (no delegation) - address newImpl = address(new TestImplementation()); - address oldImpl = proxy.implementation(); - - vm.prank(proxyAdminAddress); - proxy.upgradeTo(newImpl); // This executes as admin function - assertEq(proxy.implementation(), newImpl); // Implementation DOES change - - // 3. Non-admin calling admin function should delegate to implementation (no admin effect) - vm.prank(user); - proxy.upgradeTo(oldImpl); // This gets delegated to TestImplementation fallback - assertEq(proxy.implementation(), newImpl); // Implementation does NOT change (proves delegation) - - // 4. Demonstrate delegation vs admin execution - // Admin can change admin - vm.prank(proxyAdminAddress); - proxy.changeAdmin(user); - assertEq(proxy.admin(), user); // Admin DID change - - // Non-admin call gets delegated (no admin effect) - vm.prank(implementationOwner); - proxy.changeAdmin(proxyAdminAddress); // Gets delegated to implementation - assertEq(proxy.admin(), user); // Admin does NOT change (proves delegation) - } - - // ============ Storage Persistence Tests ============ - - function test_StoragePersistenceAcrossUpgrade() public { - // Modify state - vm.prank(implementationOwner); - testContract.updateValue(777); - vm.prank(implementationOwner); - testContract.updateName("Modified"); - - // Verify state before upgrade - assertEq(testContract.getValue(), 777); - assertEq(testContract.getName(), "Modified"); - - // Deploy new implementation and upgrade - TestImplementation newImplementation = new TestImplementation(); - vm.prank(proxyAdminAddress); - proxy.upgradeTo(address(newImplementation)); - - // Verify state persisted after upgrade - assertEq(testContract.getValue(), 777); - assertEq(testContract.getName(), "Modified"); - assertTrue(testContract.isInitialized()); - } - - // ============ Admin Management Tests ============ - - function test_ChangeAdmin() public { - address newAdmin = makeAddr("newAdmin"); - - // Only current admin can change admin - vm.prank(proxyAdminAddress); - proxy.changeAdmin(newAdmin); - - // Verify admin changed - address updatedAdmin = address( - uint160( - uint256(vm.load(address(proxy), 0xb53127684a568b3173ae13b9f8a6016e243e63b6e8ee1178d6a717850b5d6103)) - ) - ); - assertEq(updatedAdmin, newAdmin); - - // Old admin can still upgrade due to fallback - vm.prank(proxyAdminAddress); - proxy.upgradeTo(address(implementation)); - - // New admin can upgrade - TestImplementation newImplementation = new TestImplementation(); - vm.prank(newAdmin); - proxy.upgradeTo(address(newImplementation)); // Should succeed - } - - // ============ Edge Cases ============ - - function test_MultipleProxiesWithSameImplementation() public { - // Deploy another proxy with the same implementation - AdminUpgradeableProxy proxy2 = new AdminUpgradeableProxy(address(implementation), proxyAdminAddress, ""); - - TestImplementation testContract2 = TestImplementation(payable(address(proxy2))); - - // Initialize second proxy with different values - testContract2.initialize(user, 999, "SecondProxy"); - - // Verify they have independent state - assertEq(testContract.getValue(), DEFAULT_VALUE); - assertEq(testContract2.getValue(), 999); - assertEq(testContract.getName(), DEFAULT_NAME); - assertEq(testContract2.getName(), "SecondProxy"); - - // Verify they can be upgraded independently - TestImplementation newImplementation = new TestImplementation(); - vm.prank(proxyAdminAddress); - proxy.upgradeTo(address(newImplementation)); - - // First proxy upgraded, second still on old implementation - assertEq(testContract.getValue(), DEFAULT_VALUE); // State preserved - assertEq(testContract2.getValue(), 999); // State preserved - } - - function test_ImplementationViewFunction() public { - // Deploy implementations - TestImplementation impl1 = new TestImplementation(); - TestImplementation impl2 = new TestImplementation(); - - // Deploy proxy - AdminUpgradeableProxy testProxy = new AdminUpgradeableProxy(address(impl1), proxyAdminAddress, ""); - - // Verify implementation() returns correct address - assertEq(testProxy.implementation(), address(impl1)); - - // Non-admin should be able to call view function - vm.prank(implementationOwner); - assertEq(testProxy.implementation(), address(impl1)); - - // Admin should also be able to call view function - vm.prank(proxyAdminAddress); - assertEq(testProxy.implementation(), address(impl1)); - - // Upgrade and verify new implementation - vm.prank(proxyAdminAddress); - testProxy.upgradeTo(address(impl2)); - - assertEq(testProxy.implementation(), address(impl2)); - } - - function test_AdminViewFunction() public { - // Deploy implementation - TestImplementation impl = new TestImplementation(); - - // Deploy proxy - AdminUpgradeableProxy testProxy = new AdminUpgradeableProxy(address(impl), proxyAdminAddress, ""); - - // Verify admin() returns correct address - assertEq(testProxy.admin(), proxyAdminAddress); - - // Non-admin should be able to call view function - vm.prank(implementationOwner); - assertEq(testProxy.admin(), proxyAdminAddress); - - // Admin should also be able to call view function - vm.prank(proxyAdminAddress); - assertEq(testProxy.admin(), proxyAdminAddress); - - // Change admin and verify new admin - address newAdmin = makeAddr("newAdmin"); - vm.prank(proxyAdminAddress); - testProxy.changeAdmin(newAdmin); - - assertEq(testProxy.admin(), newAdmin); - } - - function test_ViewFunctionsAccessibility() public { - // Deploy implementation - TestImplementation impl = new TestImplementation(); - - // Deploy proxy - AdminUpgradeableProxy testProxy = new AdminUpgradeableProxy(address(impl), proxyAdminAddress, ""); - - // Test that anyone can call view functions - address randomUser = makeAddr("randomUser"); - - vm.prank(randomUser); - assertEq(testProxy.implementation(), address(impl)); - - vm.prank(randomUser); - assertEq(testProxy.admin(), proxyAdminAddress); - - // Verify these are truly view functions (don't change state) - address implBefore = testProxy.implementation(); - address adminBefore = testProxy.admin(); - - // Multiple calls should return same values - assertEq(testProxy.implementation(), implBefore); - assertEq(testProxy.admin(), adminBefore); - } - - function test_NativeTransferFailsWithoutPayableReceiveImplementation() public { - // Deploy a non-payable implementation (no receive/fallback) - ProtocolConfig impl = new ProtocolConfig(); - address proxyAdmin = makeAddr("protocolProxyAdmin"); - AdminUpgradeableProxy protocolProxy = new AdminUpgradeableProxy(address(impl), proxyAdmin, ""); - - // Fund a user and attempt to send ETH while calling a view function (rewardBeneficiary) - address sender = makeAddr("nativeSenderWithViewSelector"); - vm.deal(sender, 1 ether); - - vm.prank(sender); - (bool success, bytes memory revertData) = address(protocolProxy).call{value: 0.1 ether}(""); - - // Should fail because the function is non-payable and lacks receive/fallback payable handler - assertFalse(success, "native transfer with view selector should fail"); - assertEq(address(protocolProxy).balance, 0 ether, "proxy should not hold ETH"); - } - - function test_NativeTransferSucceedsWithPayableReceiveImplementation() public { - // Deploy an implementation that has a payable receive - CallHelper impl = new CallHelper(); - address proxyAdmin = makeAddr("helperProxyAdmin"); - AdminUpgradeableProxy helperProxy = new AdminUpgradeableProxy(address(impl), proxyAdmin, ""); - - // Fund a user and send ETH with empty calldata to the proxy - address sender = makeAddr("nativeSenderWithReceive"); - vm.deal(sender, 1 ether); - - vm.prank(sender); - (bool success,) = address(helperProxy).call{value: 0.2 ether}(""); - - // Call should succeed and ETH should be held by the proxy (delegatecall keeps balance on proxy) - assertTrue(success, "native transfer should succeed via payable receive"); - assertEq(address(helperProxy).balance, 0.2 ether, "proxy should hold transferred ETH"); - } - - function test_PayableVsNonPayableFunctionOnImplementation() public { - // Deploy an implementation that has both payable and non-payable entrypoints - CallHelper impl = new CallHelper(); - address proxyAdmin = makeAddr("helperProxyAdmin2"); - AdminUpgradeableProxy helperProxy = new AdminUpgradeableProxy(address(impl), proxyAdmin, ""); - - address sender = makeAddr("payableVsNonPayable"); - vm.deal(sender, 1 ether); - - // Payable function: setStorage is payable, should accept value - bytes memory payableCall = abi.encodeWithSelector(CallHelper.setStorage.selector, uint256(1), uint256(123)); - vm.prank(sender); - (bool payableSuccess,) = address(helperProxy).call{value: 0.15 ether}(payableCall); - assertTrue(payableSuccess, "payable function should accept ETH"); - assertEq(address(helperProxy).balance, 0.15 ether, "proxy should hold only accepted ETH"); - - // Non-payable function: getStorage is view (non-payable), should reject value - bytes memory nonPayableCall = abi.encodeWithSelector(CallHelper.getStorage.selector, uint256(1)); - vm.prank(sender); - (bool nonPayableSuccess, bytes memory revertData) = - address(helperProxy).call{value: 0.05 ether}(nonPayableCall); - - assertFalse(nonPayableSuccess, "non-payable function should reject ETH"); - - // Only the successful payable call's value should remain on the proxy - assertEq(address(helperProxy).balance, 0.15 ether, "proxy should hold only accepted ETH"); - } -} diff --git a/contracts/test/scripts/DenylistManagement.t.sol b/contracts/test/scripts/DenylistManagement.t.sol deleted file mode 100644 index 0c21ce8..0000000 --- a/contracts/test/scripts/DenylistManagement.t.sol +++ /dev/null @@ -1,199 +0,0 @@ -// Copyright 2026 Circle Internet Group, Inc. All rights reserved. -// -// SPDX-License-Identifier: Apache-2.0 -// -// Licensed under the Apache License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. -// You may obtain a copy of the License at -// -// http://www.apache.org/licenses/LICENSE-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// See the License for the specific language governing permissions and -// limitations under the License. - -pragma solidity ^0.8.29; - -import {Test} from "forge-std/Test.sol"; -import {Denylist} from "../../src/Denylist.sol"; -import {AdminUpgradeableProxy} from "../../src/proxy/AdminUpgradeableProxy.sol"; -import {DenylistManagement} from "../../scripts/DenylistManagement.s.sol"; - -contract DenylistManagementTest is Test { - DenylistManagement script; - Denylist denylist; - address proxyAddr; - - uint256 ownerPk = uint256(keccak256("OWNER_PK")); - address owner = vm.addr(ownerPk); - uint256 denylisterPk = uint256(keccak256("DENYLISTER_PK")); - address denylisterAddr = vm.addr(denylisterPk); - uint256 newOwnerPk = uint256(keccak256("NEW_OWNER_PK")); - address newOwner = vm.addr(newOwnerPk); - - uint256 proxyAdminPk = uint256(keccak256("PROXY_ADMIN_PK")); - address proxyAdminAddr = vm.addr(proxyAdminPk); - - address alice = address(40); - address bob = address(50); - - function setUp() public { - // Deploy Denylist behind proxy (distinct proxy admin and owner) - Denylist impl = new Denylist(); - bytes memory initData = abi.encodeWithSelector(Denylist.initialize.selector, owner); - AdminUpgradeableProxy proxy = new AdminUpgradeableProxy(address(impl), proxyAdminAddr, initData); - proxyAddr = address(proxy); - denylist = Denylist(proxyAddr); - - // Set DENYLIST_PROXY env var for the script - vm.setEnv("DENYLIST_PROXY", vm.toString(proxyAddr)); - - // Add a denylister via direct prank (setup prerequisite) - vm.prank(owner); - denylist.addDenylister(denylisterAddr); - - script = new DenylistManagement(); - } - - // ============ Print / View Functions ============ - - function test_PrintContractInfo_Succeeds() public view { - // Should not revert — exercises console.log paths - script.printContractInfo(); - } - - function test_PrintIsDenylisted_Succeeds() public view { - script.printIsDenylisted(alice); - } - - function test_PrintIsDenylister_Succeeds() public view { - script.printIsDenylister(denylisterAddr); - } - - function test_PrintBatchDenylistStatus_Succeeds() public view { - address[] memory accounts = new address[](2); - accounts[0] = alice; - accounts[1] = bob; - script.printBatchDenylistStatus(accounts); - } - - // ============ Denylister Management ============ - - function test_AddDenylister_Succeeds() public { - vm.setEnv("OWNER_KEY", vm.toString(ownerPk)); - - address newDenylister = address(100); - script.addDenylister(newDenylister); - - assertTrue(denylist.isDenylister(newDenylister)); - } - - function test_RemoveDenylister_Succeeds() public { - vm.setEnv("OWNER_KEY", vm.toString(ownerPk)); - - script.removeDenylister(denylisterAddr); - - assertFalse(denylist.isDenylister(denylisterAddr)); - } - - // ============ Denylist Operations ============ - - function test_Denylist_Succeeds() public { - vm.setEnv("DENYLISTER_KEY", vm.toString(denylisterPk)); - - address[] memory accounts = new address[](2); - accounts[0] = alice; - accounts[1] = bob; - script.denylist(accounts); - - assertTrue(denylist.isDenylisted(alice)); - assertTrue(denylist.isDenylisted(bob)); - } - - function test_Denylist_RevertsForEmptyAccounts() public { - vm.setEnv("DENYLISTER_KEY", vm.toString(denylisterPk)); - - address[] memory accounts = new address[](0); - - vm.expectRevert("accounts cannot be empty"); - script.denylist(accounts); - } - - function test_Denylist_RevertsForProxyAdmin() public { - vm.setEnv("DENYLISTER_KEY", vm.toString(denylisterPk)); - - address[] memory accounts = new address[](1); - accounts[0] = proxyAdminAddr; - - vm.expectRevert("cannot denylist proxy admin"); - script.denylist(accounts); - } - - function test_Denylist_RevertsForOwner() public { - vm.setEnv("DENYLISTER_KEY", vm.toString(denylisterPk)); - - address[] memory accounts = new address[](1); - accounts[0] = owner; - - vm.expectRevert("cannot denylist owner"); - script.denylist(accounts); - } - - function test_UnDenylist_Succeeds() public { - // First denylist alice - vm.prank(denylisterAddr); - address[] memory add = new address[](1); - add[0] = alice; - denylist.denylist(add); - assertTrue(denylist.isDenylisted(alice)); - - // Un-denylist via script - vm.setEnv("DENYLISTER_KEY", vm.toString(denylisterPk)); - address[] memory remove = new address[](1); - remove[0] = alice; - script.unDenylist(remove); - - assertFalse(denylist.isDenylisted(alice)); - } - - function test_UnDenylist_RevertsForEmptyAccounts() public { - vm.setEnv("DENYLISTER_KEY", vm.toString(denylisterPk)); - - address[] memory accounts = new address[](0); - - vm.expectRevert("accounts cannot be empty"); - script.unDenylist(accounts); - } - - // ============ Ownership Transfer ============ - - function test_TransferOwnership_Succeeds() public { - vm.setEnv("OWNER_KEY", vm.toString(ownerPk)); - - script.transferOwnership(newOwner); - - assertEq(denylist.owner(), owner); - assertEq(denylist.pendingOwner(), newOwner); - } - - function test_AcceptOwnership_Succeeds() public { - // Initiate transfer first - vm.prank(owner); - denylist.transferOwnership(newOwner); - - vm.setEnv("NEW_OWNER_KEY", vm.toString(newOwnerPk)); - script.acceptOwnership(); - - assertEq(denylist.owner(), newOwner); - assertEq(denylist.pendingOwner(), address(0)); - } - - function test_TransferOwnership_RevertsForZeroAddress() public { - vm.setEnv("OWNER_KEY", vm.toString(ownerPk)); - - vm.expectRevert("new owner cannot be zero address"); - script.transferOwnership(address(0)); - } -} diff --git a/contracts/test/scripts/DeployPermissionedValidatorManagement.t.sol b/contracts/test/scripts/DeployPermissionedValidatorManagement.t.sol deleted file mode 100644 index 12a2fbb..0000000 --- a/contracts/test/scripts/DeployPermissionedValidatorManagement.t.sol +++ /dev/null @@ -1,52 +0,0 @@ -// Copyright 2025 Circle Internet Group, Inc. All rights reserved. -// -// SPDX-License-Identifier: Apache-2.0 -// -// Licensed under the Apache License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. -// You may obtain a copy of the License at -// -// http://www.apache.org/licenses/LICENSE-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// See the License for the specific language governing permissions and -// limitations under the License. - -pragma solidity ^0.8.29; - -import {Test} from "forge-std/Test.sol"; -import {DeployPermissionedValidatorManager} from "../../scripts/DeployPermissionedValidatorManager.s.sol"; -import {PermissionedValidatorManager} from "../../src/validator-manager/PermissionedValidatorManager.sol"; -import {Addresses} from "../../scripts/Addresses.sol"; - -/** - * @title DeployPermissionedValidatorManagementTest - * @notice Test suite for DeployPermissionedValidatorManager deployment script - * @dev Tests the pure deploy() function with random inputs - no mocking needed! - */ -contract DeployPermissionedValidatorManagementTest is Test { - DeployPermissionedValidatorManager deployScript; - - function setUp() public { - deployScript = new DeployPermissionedValidatorManager(); - } - - - function test_Deploy_WithSpecificAddresses() public { - address testOwner = address(0x123); - address testPauser = address(0x789); - address testAdmin = address(0x456); - - (address implAddr, address proxyAddr) = deployScript.deploy(testOwner, testPauser, testAdmin); - - assertTrue(implAddr != address(0)); - assertTrue(proxyAddr != address(0)); - - PermissionedValidatorManager pvm = PermissionedValidatorManager(proxyAddr); - assertEq(pvm.owner(), testOwner); - assertEq(pvm.pauser(), testPauser); - assertEq(address(pvm.REGISTRY()), Addresses.VALIDATOR_REGISTRY); - } -} diff --git a/contracts/test/scripts/ProtocolConfigManagement.t.sol b/contracts/test/scripts/ProtocolConfigManagement.t.sol deleted file mode 100644 index ffdb44c..0000000 --- a/contracts/test/scripts/ProtocolConfigManagement.t.sol +++ /dev/null @@ -1,127 +0,0 @@ -// Copyright 2025 Circle Internet Group, Inc. All rights reserved. -// -// SPDX-License-Identifier: Apache-2.0 -// -// Licensed under the Apache License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. -// You may obtain a copy of the License at -// -// http://www.apache.org/licenses/LICENSE-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// See the License for the specific language governing permissions and -// limitations under the License. - -pragma solidity ^0.8.29; - -import {Test} from "forge-std/Test.sol"; -import {StdStorage, stdStorage} from "forge-std/StdStorage.sol"; -import {ProtocolConfig} from "../../src/protocol-config/ProtocolConfig.sol"; -import {IProtocolConfig} from "../../src/protocol-config/interfaces/IProtocolConfig.sol"; -import {ProtocolConfigManagement} from "../../scripts/ProtocolConfigManagement.s.sol"; - -contract ProtocolConfigManagementTest is Test { - using stdStorage for StdStorage; - - ProtocolConfig protocolConfig; - ProtocolConfigManagement script; - - // Keys - uint256 ownerPk = uint256(keccak256("OWNER_PK")); - address owner = vm.addr(ownerPk); - uint256 controllerPk = uint256(keccak256("CONTROLLER_PK")); - address controller = vm.addr(controllerPk); - - address constant PROTOCOL_CONFIG_ADDR = 0x3600000000000000000000000000000000000001; - - function setUp() public { - ProtocolConfig impl = new ProtocolConfig(); - script = new ProtocolConfigManagement(); - - // Etch implementation code to the well-known address the script uses - vm.etch(PROTOCOL_CONFIG_ADDR, address(impl).code); - - // Set controller at etched address (public variable) - stdstore.target(PROTOCOL_CONFIG_ADDR).sig("controller()").checked_write(controller); - - // Bind handle to etched address for reads - protocolConfig = ProtocolConfig(PROTOCOL_CONFIG_ADDR); - - // Seed initial fee params via controller so script preserves other fields - IProtocolConfig.FeeParams memory params = IProtocolConfig.FeeParams({ - alpha: 0, - kRate: 0, - minBaseFee: 1, - maxBaseFee: 10, - blockGasLimit: 30_000_000, - inverseElasticityMultiplier: 5000 - }); - vm.prank(controller); - protocolConfig.updateFeeParams(params); - - // Seed initial consensus params via controller - IProtocolConfig.ConsensusParams memory consensusParams = IProtocolConfig.ConsensusParams({ - timeoutProposeMs: 3000, - timeoutProposeDeltaMs: 500, - timeoutPrevoteMs: 1000, - timeoutPrevoteDeltaMs: 500, - timeoutPrecommitMs: 1000, - timeoutPrecommitDeltaMs: 500, - timeoutRebroadcastMs: 1000, - targetBlockTimeMs: 500 - }); - vm.prank(controller); - protocolConfig.updateConsensusParams(consensusParams); - } - - function test_UpdateBlockGasLimit_Succeeds() public { - vm.setEnv("CONTROLLER_KEY", vm.toString(controllerPk)); - - script.updateBlockGasLimit(40_000_000); - - IProtocolConfig.FeeParams memory _feeParams = protocolConfig.feeParams(); - assertEq(_feeParams.blockGasLimit, 40_000_000); - - // unchanged initial parameters - assertEq(_feeParams.minBaseFee, 1); - assertEq(_feeParams.maxBaseFee, 10); - assertEq(_feeParams.inverseElasticityMultiplier, 5000); - assertEq(_feeParams.alpha, 0); - assertEq(_feeParams.kRate, 0); - } - - function test_UpdateTargetBlockTime_Succeeds() public { - vm.setEnv("CONTROLLER_KEY", vm.toString(controllerPk)); - - script.updateTargetBlockTime(250); - - IProtocolConfig.ConsensusParams memory _consensusParams = protocolConfig.consensusParams(); - assertEq(_consensusParams.timeoutProposeMs, 3000); - assertEq(_consensusParams.timeoutProposeDeltaMs, 500); - assertEq(_consensusParams.timeoutPrevoteMs, 1000); - assertEq(_consensusParams.timeoutPrevoteDeltaMs, 500); - assertEq(_consensusParams.timeoutPrecommitMs, 1000); - assertEq(_consensusParams.timeoutPrecommitDeltaMs, 500); - assertEq(_consensusParams.timeoutRebroadcastMs, 1000); - assertEq(_consensusParams.targetBlockTimeMs, 250); - } - - function test_UpdateBaseFeeBounds_Succeeds() public { - vm.setEnv("CONTROLLER_KEY", vm.toString(controllerPk)); - - // Update only the base fee bounds - script.updateBaseFeeBounds(42, 1_000_000_000); - - IProtocolConfig.FeeParams memory _feeParams = protocolConfig.feeParams(); - assertEq(_feeParams.minBaseFee, 42); - assertEq(_feeParams.maxBaseFee, 1_000_000_000); - - // unchanged initial parameters - assertEq(_feeParams.blockGasLimit, 30_000_000); - assertEq(_feeParams.inverseElasticityMultiplier, 5000); - assertEq(_feeParams.alpha, 0); - assertEq(_feeParams.kRate, 0); - } -} diff --git a/contracts/test/scripts/ValidatorManagement.t.sol b/contracts/test/scripts/ValidatorManagement.t.sol deleted file mode 100644 index 24caa26..0000000 --- a/contracts/test/scripts/ValidatorManagement.t.sol +++ /dev/null @@ -1,265 +0,0 @@ -// Copyright 2025 Circle Internet Group, Inc. All rights reserved. -// -// SPDX-License-Identifier: Apache-2.0 -// -// Licensed under the Apache License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. -// You may obtain a copy of the License at -// -// http://www.apache.org/licenses/LICENSE-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// See the License for the specific language governing permissions and -// limitations under the License. - -pragma solidity ^0.8.29; - -import {TestUtils} from "../validator-manager/TestUtils.sol"; -import {ValidatorRegistry, Validator} from "../../src/validator-manager/ValidatorRegistry.sol"; -import {PermissionedValidatorManager} from "../../src/validator-manager/PermissionedValidatorManager.sol"; -import {ValidatorManagement} from "../../scripts/ValidatorManagement.s.sol"; - -contract ValidatorManagementTest is TestUtils { - ValidatorRegistry constant VALIDATOR_REGISTRY = ValidatorRegistry(0x3600000000000000000000000000000000000002); - PermissionedValidatorManager constant PERMISSIONED_VALIDATOR_MANAGER = - PermissionedValidatorManager(0x3600000000000000000000000000000000000003); - - ValidatorManagement validatorManagement; - - uint256 ownerPk = uint256(keccak256("OWNER_PK")); - address owner = vm.addr(ownerPk); - uint256 validatorRegistererPk = uint256(keccak256("VALIDATOR_REGISTERER_PK")); - address validatorRegisterer = vm.addr(validatorRegistererPk); - address initializedController = address(30); - // Use a high limit to allow tests (including unsafe update) to set large powers - uint64 controllerVotingPowerLimit = type(uint64).max; - - uint256 newControllerPk = uint256(keccak256("CONTROLLER_PK")); - address newController = vm.addr(newControllerPk); - - // Initialized validators - bytes validator1PublicKey = generateEd25519PublicKey(10); // 80 voting power - bytes validator2PublicKey = generateEd25519PublicKey(20); // 90 voting power - bytes validator3PublicKey = generateEd25519PublicKey(30); // 100 voting power - - // New validator - bytes newValidatorPublicKey = generateEd25519PublicKey(40); - - function setUp() public { - // Deploy contracts at temporary addresses first - ValidatorRegistry validatorRegistryImpl = new ValidatorRegistry(); - - // Use etch to move the ValidatorRegistry bytecode to expected address first - vm.etch(0x3600000000000000000000000000000000000002, address(validatorRegistryImpl).code); - - // Now deploy PermissionedValidatorManager with the correct registry address - PermissionedValidatorManager permissionedValidatorManagerImpl = - new PermissionedValidatorManager(VALIDATOR_REGISTRY); - - validatorManagement = new ValidatorManagement(); - - // Use etch to move PermissionedValidatorManager bytecode to expected address - vm.etch(0x3600000000000000000000000000000000000003, address(permissionedValidatorManagerImpl).code); - - // Set Ownable slots (simulating genesis initialization) - bytes32 ownableSlot = 0x9016d09d72d40fdae2fd8ceac6b6234c7706214fd39c1cd1e609a0528c199300; - // Set ValidatorRegistry owner to PERMISSIONED_VALIDATOR_MANAGER - vm.store( - address(VALIDATOR_REGISTRY), - ownableSlot, - bytes32(uint256(uint160(address(PERMISSIONED_VALIDATOR_MANAGER)))) - ); - // Set PermissionedValidatorManager owner to owner - vm.store( - address(PERMISSIONED_VALIDATOR_MANAGER), - ownableSlot, - bytes32(uint256(uint160(owner))) - ); - // Initialize ValidatorRegistry registrationId storage to start at 1 - bytes32 nextRegistrationIdSlot = bytes32(uint256(VALIDATOR_REGISTRY.VALIDATOR_REGISTRY_STORAGE_LOCATION()) + 4); - vm.store(address(VALIDATOR_REGISTRY), nextRegistrationIdSlot, bytes32(uint256(1))); - - // Configure some initial state - // 3 validators - vm.prank(owner); - PERMISSIONED_VALIDATOR_MANAGER.addValidatorRegisterer(validatorRegisterer); - - // Register keys - vm.startPrank(validatorRegisterer); - PERMISSIONED_VALIDATOR_MANAGER.registerValidator(validator1PublicKey); - PERMISSIONED_VALIDATOR_MANAGER.registerValidator(validator2PublicKey); - PERMISSIONED_VALIDATOR_MANAGER.registerValidator(validator3PublicKey); - vm.stopPrank(); - - // Set voting power and activate each validator - uint64[3] memory powers = [uint64(80), uint64(90), uint64(100)]; - for (uint256 i = 1; i <= 3; i++) { - vm.prank(owner); - PERMISSIONED_VALIDATOR_MANAGER.configureController(initializedController, i, controllerVotingPowerLimit); - - vm.startPrank(initializedController); - PERMISSIONED_VALIDATOR_MANAGER.updateValidatorVotingPower(powers[i - 1]); - PERMISSIONED_VALIDATOR_MANAGER.activateValidator(); - vm.stopPrank(); - - vm.prank(owner); - PERMISSIONED_VALIDATOR_MANAGER.removeController(initializedController); - } - } - - function test_PrintActiveValidatorSet() public view { - Validator[] memory _validators = validatorManagement.printActiveValidatorSet(); - - assertEq(_validators.length, 3); - assertEq(_validators[0].publicKey, validator1PublicKey); - assertEq(_validators[1].publicKey, validator2PublicKey); - assertEq(_validators[2].publicKey, validator3PublicKey); - - assertEq(_validators[0].votingPower, 80); - assertEq(_validators[1].votingPower, 90); - assertEq(_validators[2].votingPower, 100); - - assertEq(uint256(_validators[0].status), 2, "Status should be Active"); - assertEq(uint256(_validators[1].status), 2, "Status should be Active"); - assertEq(uint256(_validators[2].status), 2, "Status should be Active"); - } - - function test_RegisterValidator_Succeeds() public { - uint256 _registrationId = _registerValidator(); - - // Retrieve validator, and confirm that 0 voting power was registered - Validator memory _validator = VALIDATOR_REGISTRY.getValidator(_registrationId); - assertEq(_validator.votingPower, 0); // Voting power should be 0 - assertEq(_validator.publicKey, newValidatorPublicKey); - assertEq(uint256(_validator.status), 1); // Registered - } - - function test_RequirePublicKeyBasicSanity_AcceptsValidKey() public view { - validatorManagement.requirePublicKeyBasicSanity(newValidatorPublicKey); - } - - function test_RequirePublicKeyBasicSanity_RejectsWrongLengthKey() public { - // 31-byte key — one short of the required 32. - bytes memory tooShort = hex"c992c8696818bda11d628f38584022a6332c144b7f929b4d972bd39a23244a"; - vm.expectRevert("Public key must be 32 bytes"); - validatorManagement.requirePublicKeyBasicSanity(tooShort); - - // 33-byte key — one longer than the required 32. - bytes memory tooLong = - hex"c992c8696818bda11d628f38584022a6332c144b7f929b4d972bd39a23244aecbc"; - vm.expectRevert("Public key must be 32 bytes"); - validatorManagement.requirePublicKeyBasicSanity(tooLong); - } - - function test_RequirePublicKeyBasicSanity_RejectsAllZeroKey() public { - bytes memory allZero = new bytes(32); - vm.expectRevert("Public key must not be all zero"); - validatorManagement.requirePublicKeyBasicSanity(allZero); - } - - function test_ConfigureController_Succeeds() public { - uint256 _registrationId = _registerValidator(); - _configureController(_registrationId); - - // Verify controller is configured - assertTrue(PERMISSIONED_VALIDATOR_MANAGER.isController(newController), "Controller should be configured"); - } - - function test_ActivateValidator_Succeeds() public { - uint256 _registrationId = _registerValidator(); - _configureController(_registrationId); - _activateValidator(); - - // Verify validator is active - Validator memory _validator = VALIDATOR_REGISTRY.getValidator(_registrationId); - assertEq(uint256(_validator.status), 2, "Validator should be active"); - assertEq(_validator.publicKey, newValidatorPublicKey); - } - - function test_UpdateVotingPower_Succeeds() public { - uint256 _registrationId = _registerValidator(); - _configureController(_registrationId); - _activateValidator(); - - // Registry is initialized with 80 + 90 + 100 = 270 voting power - // X < (270 + X) / 3 - // 3X < 270 + X - // 2X < 270 - // --> X < 135 - _updateVotingPower(134); - - // Verify validator voting power is updated - Validator memory _validator = VALIDATOR_REGISTRY.getValidator(_registrationId); - assertEq(_validator.votingPower, 134); - } - - function test_UpdateVotingPower_FailsIfNewValidatorPowerIsCritical() public { - uint256 _registrationId = _registerValidator(); - _configureController(_registrationId); - _activateValidator(); - - vm.expectRevert("Highest voting power exceeds 1/3 of total voting power"); - _updateVotingPower(135); // See comment in test above - } - - function test_UpdateVotingPower_FailsIfExistingValidatorPowerIsCritical() public { - uint256 _registrationId = _registerValidator(); - _configureController(_registrationId); - _activateValidator(); - - // 80 + 90 + 100 = 270 total voting power - // Add a nominal power to the new validator ==> should fail, since the existing validators - // are considered critical - vm.expectRevert("Highest voting power exceeds 1/3 of total voting power"); - _updateVotingPower(10); // See comment in test above - } - - function test_UpdateVotingPowerUnsafe_Succeeds() public { - uint256 _registrationId = _registerValidator(); - _configureController(_registrationId); - _activateValidator(); - - _updateVotingPowerUnsafe(10000); - - // Verify validator voting power is updated - Validator memory _validator = VALIDATOR_REGISTRY.getValidator(_registrationId); - assertEq(_validator.votingPower, 10000); - } - - // Internal helpers - - function _registerValidator() internal returns (uint256 _registrationId) { - vm.setEnv("VALIDATOR_REGISTERER_KEY", vm.toString(validatorRegistererPk)); - vm.setEnv("VALIDATOR_PUBLIC_KEY_BYTES", vm.toString(newValidatorPublicKey)); - _registrationId = validatorManagement.registerValidator(); - } - - function _configureController(uint256 _registrationId) internal { - vm.setEnv("CONTROLLER_ADDRESS", vm.toString(newController)); - vm.setEnv("REGISTRATION_ID", vm.toString(_registrationId)); - vm.setEnv("PERMISSIONED_VALIDATOR_MANAGER_OWNER", vm.toString(ownerPk)); - vm.setEnv("CONTROLLER_VOTING_POWER_LIMIT", vm.toString(controllerVotingPowerLimit)); - - validatorManagement.configureController(); - } - - function _activateValidator() internal { - vm.setEnv("CONTROLLER_KEY", vm.toString(newControllerPk)); - - validatorManagement.activateValidator(); - } - - function _updateVotingPower(uint64 _newVotingPower) internal { - vm.setEnv("CONTROLLER_KEY", vm.toString(newControllerPk)); - - validatorManagement.updateVotingPower(_newVotingPower); - } - - function _updateVotingPowerUnsafe(uint64 _newVotingPower) internal { - vm.setEnv("CONTROLLER_KEY", vm.toString(newControllerPk)); - - validatorManagement.updateVotingPowerUnsafe(_newVotingPower); - } -} diff --git a/contracts/test/scripts/VerifyArtifacts.t.sol b/contracts/test/scripts/VerifyArtifacts.t.sol deleted file mode 100644 index 2dd2625..0000000 --- a/contracts/test/scripts/VerifyArtifacts.t.sol +++ /dev/null @@ -1,126 +0,0 @@ -// Copyright 2025 Circle Internet Group, Inc. All rights reserved. -// -// SPDX-License-Identifier: Apache-2.0 -// -// Licensed under the Apache License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. -// You may obtain a copy of the License at -// -// http://www.apache.org/licenses/LICENSE-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// See the License for the specific language governing permissions and -// limitations under the License. - -pragma solidity ^0.8.29; - -import {Test} from "forge-std/Test.sol"; - -import {ArtifactHelper} from "../../scripts/ArtifactHelper.s.sol"; - -contract VerifyArtifactTest is ArtifactHelper, Test { - uint256 ethereumFork; - bool verifyEthCodeHash = false; - - function setUp() public { - loadManifest("assets/artifacts/manifest.json"); - - string memory ethRpcUrl = vm.envOr("ETH_RPC_URL", string("")); - if (bytes(ethRpcUrl).length > 0) { - ethereumFork = vm.createFork(ethRpcUrl); - verifyEthCodeHash = true; - } - // To compare the codeHash with Ethereum mainnet, we need to set the chainId to 1. - vm.chainId(1); - } - - function getEthCodeHash(address addr) internal returns (bytes32) { - // Same as `cast codehash --rpc-url https://reth-ethereum.ithaca.xyz/rpc $addr` - vm.selectFork(ethereumFork); - string memory params = string.concat("[\"", vm.toString(addr), "\"]"); - bytes memory code = vm.rpc("eth_getCode", params); - return keccak256(code); - } - - function verifyDeterministicDeployedContract(DeterministicDeployment memory deployment) public { - address addr = deployDeterministicContract(deployment); - - bytes32 codeHash = keccak256(addr.code); - assertEq(codeHash, deployment.ethCodeHash, "deployed codehash mismatch"); - } - - function verifyOneTimeAddressContract(OneTimeAddressDeployment memory deployment) public { - // Forge env already provide same determinisic deployment proxy, remove it to deploy again. - vm.etch(deployment.addr, hex""); - vm.resetNonce(deployment.addr); - vm.resetNonce(deployment.deployer); - - vm.deal(deployment.deployer, deployment.deployerBalance); - vm.broadcastRawTransaction(deployment.rawTransaction); - - bytes32 codeHash = keccak256(deployment.addr.code); - assertEq(codeHash, deployment.ethCodeHash, "deployed codehash mismatch"); - } - - // TODO we could move to table test after foundry 1.3. - function testVerifyArtifacts() public { - for (uint256 i = 0; i < externalContracts.length; i++) { - string memory contractName = externalContracts[i]; - DeploymentType deploymentType = getDeploymentType(contractName); - - if (deploymentType == DeploymentType.deterministic) { - DeterministicDeployment memory deployment = loadDeterministicDeployment(contractName); - verifyDeterministicDeployedContract(deployment); - } else { - OneTimeAddressDeployment memory deployment = loadOneTimeAddressDeployment(contractName); - verifyOneTimeAddressContract(deployment); - } - } - } - - function testEthCodeHash() public { - vm.skip(!verifyEthCodeHash); - - for (uint256 i = 0; i < externalContracts.length; i++) { - string memory contractName = externalContracts[i]; - DeploymentType deploymentType = getDeploymentType(contractName); - - if (deploymentType == DeploymentType.deterministic) { - DeterministicDeployment memory deployment = loadDeterministicDeployment(contractName); - bytes32 ethCodeHash = getEthCodeHash(deployment.addr); - assertEq(ethCodeHash, deployment.addr.codehash, "code hash mismatch with Ethereum mainnet"); - } else { - OneTimeAddressDeployment memory deployment = loadOneTimeAddressDeployment(contractName); - bytes32 ethCodeHash = getEthCodeHash(deployment.addr); - assertEq(ethCodeHash, deployment.addr.codehash, "code hash mismatch with Ethereum mainnet"); - } - } - } - - function testDeterministicDeploymentProxyAddress() public view { - OneTimeAddressDeployment memory deployment = loadOneTimeAddressDeployment("DeterministicDeploymentProxy"); - assertEq(deployment.addr, DETERMINISTIC_DEPLOYMENT_PROXY, "deterministic deployment proxy address mismatch"); - } - - function testPermit2DomainSeparator() public { - DeterministicDeployment memory deployment = loadDeterministicDeployment("Permit2"); - - vm.chainId(1); - bytes32 domainSeparator = keccak256( - abi.encode( - keccak256("EIP712Domain(string name,uint256 chainId,address verifyingContract)"), - keccak256("Permit2"), - block.chainid, - deployment.addr - ) - ); - assertEq(block.chainid, 1, "chain ID mismatch"); - assertEq( - domainSeparator, - bytes32(0x866a5aba21966af95d6c7ab78eb2b2fc913915c28be3b9aa07cc04ff903e3f28), - "domain separator mismatch" - ); - } -} diff --git a/contracts/test/validator-manager/PermissionedValidatorManager.t.sol b/contracts/test/validator-manager/PermissionedValidatorManager.t.sol deleted file mode 100644 index 46c0bd5..0000000 --- a/contracts/test/validator-manager/PermissionedValidatorManager.t.sol +++ /dev/null @@ -1,871 +0,0 @@ -// Copyright 2025 Circle Internet Group, Inc. All rights reserved. -// -// SPDX-License-Identifier: Apache-2.0 -// -// Licensed under the Apache License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. -// You may obtain a copy of the License at -// -// http://www.apache.org/licenses/LICENSE-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// See the License for the specific language governing permissions and -// limitations under the License. -pragma solidity ^0.8.29; - -import {PermissionedValidatorManager} from "../../src/validator-manager/PermissionedValidatorManager.sol"; -import {ValidatorRegistry} from "../../src/validator-manager/ValidatorRegistry.sol"; -import {Validator, ValidatorStatus} from "../../src/validator-manager/interfaces/IValidatorRegistry.sol"; -import {Controller} from "../../src/validator-manager/roles/Controller.sol"; -import {Pausable} from "../../src/common/roles/Pausable.sol"; -import {TestUtils} from "./TestUtils.sol"; -import {OwnableUpgradeable} from "@openzeppelin/contracts-upgradeable/access/OwnableUpgradeable.sol"; -import {AdminUpgradeableProxy} from "../../src/proxy/AdminUpgradeableProxy.sol"; - -contract PermissionedValidatorManagerTest is TestUtils { - // Roles events - event ValidatorRegistererAdded(address indexed validatorRegisterer); - event ValidatorRegistererRemoved(address indexed validatorRegisterer); - event ControllerConfigured(address indexed controller, uint256 indexed registrationId, uint64 indexed votingPowerLimit); - event ControllerRemoved(address indexed controller); - event RegistryOwnerTransferStarted(address indexed newOwner); - event RegistryOwnerTransferCompleted(); - event Pause(); - event Unpause(); - - // Test constants - address owner = address(10); - address validatorRegisterer = address(20); - address controller1 = address(30); - address controller2 = address(40); - uint64 defaultVotingPower; - uint64 controllerVotingPowerLimit = 500; - address pauser = address(50); - bytes validator1PublicKey; - bytes validator2PublicKey; - address nonOwner = address(70); - address notRegisterer = address(80); - address notController = address(90); - - PermissionedValidatorManager permissionedValidatorManager; - ValidatorRegistry registry; - - function setUp() public { - // Initialize validator public keys - validator1PublicKey = generateEd25519PublicKey(50); - validator2PublicKey = generateEd25519PublicKey(60); - - vm.startPrank(owner); - - // Deploy registry (no constructor parameters for upgradeable pattern) - registry = new ValidatorRegistry(); - - // Set up ownership and initial state manually (simulating genesis initialization) - _setRegistryOwner(registry, owner); - - // Deploy PermissionedValidatorManager - permissionedValidatorManager = new PermissionedValidatorManager(registry); - - // Set owner manually using storage slot (simulating genesis initialization) - _setPvmOwner(permissionedValidatorManager, owner); - - // Transfer ownership of the registry to the PermissionedValidatorManager - registry.transferOwnership(address(permissionedValidatorManager)); - - vm.stopPrank(); - - // Accept ownership (required for Ownable2StepUpgradeable) - vm.prank(address(permissionedValidatorManager)); - registry.acceptOwnership(); - - assertEq(permissionedValidatorManager.owner(), owner); - assertEq(registry.owner(), address(permissionedValidatorManager)); - defaultVotingPower = permissionedValidatorManager.DEFAULT_VOTING_POWER(); - assertEq(defaultVotingPower, 0); - } - - // ============ Initialize Tests ============ - - function test_Initialize_ThroughProxy_Success() public { - // Deploy implementation - PermissionedValidatorManager impl = new PermissionedValidatorManager(registry); - address expectedOwner = address(999); - address expectedPauser = address(777); - address proxyAdmin = address(888); - - // Deploy proxy with initialize call - bytes memory initData = abi.encodeWithSignature("initialize(address,address)", expectedOwner, expectedPauser); - AdminUpgradeableProxy proxy = new AdminUpgradeableProxy(address(impl), proxyAdmin, initData); - PermissionedValidatorManager newPvm = PermissionedValidatorManager(address(proxy)); - - // Verify owner is set - assertEq(newPvm.owner(), expectedOwner); - assertEq(newPvm.pauser(), expectedPauser); - - // Verify pending owner is not set - assertEq(newPvm.pendingOwner(), address(0)); - - // Verify the new owner can perform owner actions - vm.prank(expectedOwner); - newPvm.addValidatorRegisterer(address(123)); - assertTrue(newPvm.isValidatorRegisterer(address(123))); - } - - function test_Initialize_ThroughProxy_CanOnlyBeCalledOnce() public { - // Deploy implementation and proxy - PermissionedValidatorManager impl = new PermissionedValidatorManager(registry); - address newOwner = address(100); - address newPauser = address(101); - address proxyAdmin = address(888); - - bytes memory initData = abi.encodeWithSignature("initialize(address,address)", newOwner, newPauser); - AdminUpgradeableProxy proxy = new AdminUpgradeableProxy(address(impl), proxyAdmin, initData); - PermissionedValidatorManager newPvm = PermissionedValidatorManager(address(proxy)); - - // Try to initialize again - should revert - vm.expectRevert(); - newPvm.initialize(address(200), address(201)); - } - - function test_Initialize_CannotBeCalledOnImplementation() public { - // Deploy implementation - _disableInitializers() is called in constructor - PermissionedValidatorManager impl = new PermissionedValidatorManager(registry); - - // Should revert because _disableInitializers() was called in constructor - vm.expectRevert(); - impl.initialize(owner, pauser); - } - - function test_Initialize_ThroughProxy_WithZeroAddress() public { - // Deploy implementation - PermissionedValidatorManager impl = new PermissionedValidatorManager(registry); - address proxyAdmin = address(888); - - // Initialize with zero owner through proxy - should revert during initialization - bytes memory initData = abi.encodeWithSignature("initialize(address,address)", address(0), owner); - vm.expectRevert(abi.encodeWithSelector(PermissionedValidatorManager.ZeroOwnerAddress.selector)); - new AdminUpgradeableProxy(address(impl), proxyAdmin, initData); - } - - function test_Initialize_ThroughProxy_WithZeroPauser() public { - PermissionedValidatorManager impl = new PermissionedValidatorManager(registry); - address proxyAdmin = address(888); - - bytes memory initData = abi.encodeWithSignature("initialize(address,address)", owner, address(0)); - vm.expectRevert(abi.encodeWithSelector(Pausable.ZeroPauserAddress.selector)); - new AdminUpgradeableProxy(address(impl), proxyAdmin, initData); - } - - // Tests - function test_InitialState() public view { - // Initially no validator registerers or controllers should be configured - assertFalse(permissionedValidatorManager.isValidatorRegisterer(validatorRegisterer)); - assertFalse(permissionedValidatorManager.isController(controller1)); - assertFalse(permissionedValidatorManager.isController(controller2)); - } - - // ============ Validator Management Tests ============ - - function test_RegisterValidator_Success() public { - vm.startPrank(owner); - - // First add a validator registerer - permissionedValidatorManager.addValidatorRegisterer(validatorRegisterer); - - vm.stopPrank(); - vm.startPrank(validatorRegisterer); - - bytes memory publicKey = validator1PublicKey; - - // Expect ValidatorRegistered event to be emitted - vm.expectEmit(true, false, false, true); - emit ValidatorRegistered(1, defaultVotingPower, publicKey); - - uint256 registrationId = permissionedValidatorManager.registerValidator(publicKey); - - assertEq(registrationId, 1); - Validator memory v = registry.getValidator(registrationId); - assertEq(uint8(v.status), uint8(ValidatorStatus.Registered)); - assertEq(v.votingPower, defaultVotingPower); - vm.stopPrank(); - } - - function test_RegisterValidator_OnlyValidatorRegisterer() public { - vm.startPrank(notRegisterer); - - bytes memory publicKey = validator1PublicKey; - - vm.expectRevert(); - permissionedValidatorManager.registerValidator(publicKey); - - vm.stopPrank(); - } - - - function test_ActivateValidator_Success() public { - vm.startPrank(owner); - - // Setup: Add validator registerer and configure controller - permissionedValidatorManager.addValidatorRegisterer(validatorRegisterer); - permissionedValidatorManager.configureController(controller1, 1, controllerVotingPowerLimit); - - vm.stopPrank(); - - // First register a validator with registration ID 1 - vm.startPrank(validatorRegisterer); - bytes memory publicKey = validator1PublicKey; - - uint256 registrationId = permissionedValidatorManager.registerValidator(publicKey); - assertEq(registrationId, 1); - vm.stopPrank(); - - // Now controller can activate their assigned validator - vm.startPrank(controller1); - - // Expect ValidatorActivated event to be emitted - vm.expectEmit(true, true, false, true); - emit ValidatorActivated(1, defaultVotingPower); - - permissionedValidatorManager.activateValidator(); - vm.stopPrank(); - } - - function test_ActivateValidator_OnlyController() public { - vm.startPrank(notController); - - vm.expectRevert(Controller.CallerIsNotController.selector); - permissionedValidatorManager.activateValidator(); - - vm.stopPrank(); - } - - function test_RemoveValidator_Success() public { - vm.startPrank(owner); - - // Setup: Add validator registerer and configure controller - permissionedValidatorManager.addValidatorRegisterer(validatorRegisterer); - permissionedValidatorManager.configureController(controller1, 1, controllerVotingPowerLimit); - - vm.stopPrank(); - - // First register a validator with registration ID 1 - vm.startPrank(validatorRegisterer); - bytes memory publicKey = validator1PublicKey; - - uint256 registrationId = permissionedValidatorManager.registerValidator(publicKey); - assertEq(registrationId, 1); - vm.stopPrank(); - - // Now controller can remove their assigned validator - vm.startPrank(controller1); - - // Expect ValidatorRemoved event to be emitted - vm.expectEmit(true, true, false, true); - emit ValidatorRemoved(1, defaultVotingPower); - - permissionedValidatorManager.removeValidator(); - vm.stopPrank(); - } - - function test_RemoveValidator_OnlyController() public { - vm.startPrank(notController); - - vm.expectRevert(Controller.CallerIsNotController.selector); - permissionedValidatorManager.removeValidator(); - - vm.stopPrank(); - } - - function test_UpdateValidatorVotingPower_Success() public { - vm.startPrank(owner); - - // Setup: Add validator registerer and configure controller - permissionedValidatorManager.addValidatorRegisterer(validatorRegisterer); - permissionedValidatorManager.configureController(controller1, 1, controllerVotingPowerLimit); - - vm.stopPrank(); - - // First register a validator with registration ID 1 - vm.startPrank(validatorRegisterer); - bytes memory publicKey = validator1PublicKey; - - uint256 registrationId = permissionedValidatorManager.registerValidator(publicKey); - assertEq(registrationId, 1); - vm.stopPrank(); - - // Now controller can update voting power for their assigned validator - vm.startPrank(controller1); - uint64 newVotingPower = 200; - - // Expect ValidatorVotingPowerUpdated event to be emitted - vm.expectEmit(true, true, false, true); - emit ValidatorVotingPowerUpdated(1, defaultVotingPower, newVotingPower); - - permissionedValidatorManager.updateValidatorVotingPower(newVotingPower); - vm.stopPrank(); - } - - function test_UpdateValidatorVotingPower_RevertsAboveLimit() public { - vm.startPrank(owner); - - // Setup: Add validator registerer and configure controller with a tight limit - uint64 votingPowerLimit = 150; - permissionedValidatorManager.addValidatorRegisterer(validatorRegisterer); - permissionedValidatorManager.configureController(controller1, 1, votingPowerLimit); - - vm.stopPrank(); - - // Register validator - vm.startPrank(validatorRegisterer); - permissionedValidatorManager.registerValidator(validator1PublicKey); - vm.stopPrank(); - - // Controller attempts to exceed limit - vm.startPrank(controller1); - vm.expectRevert(abi.encodeWithSelector(PermissionedValidatorManager.VotingPowerExceedsLimit.selector, votingPowerLimit)); - permissionedValidatorManager.updateValidatorVotingPower(votingPowerLimit + 1); - vm.stopPrank(); - } - - function test_UpdateValidatorVotingPower_OnlyController() public { - vm.startPrank(notController); - - uint64 newVotingPower = 200; - - vm.expectRevert(Controller.CallerIsNotController.selector); - permissionedValidatorManager.updateValidatorVotingPower(newVotingPower); - - vm.stopPrank(); - } - - function test_UpdateVotingPower_BeforeActivation() public { - vm.startPrank(owner); - - // Setup: Add validator registerer and configure controller - permissionedValidatorManager.addValidatorRegisterer(validatorRegisterer); - permissionedValidatorManager.configureController(controller1, 1, controllerVotingPowerLimit); - - vm.stopPrank(); - - // Register validator with default (zero) voting power - vm.startPrank(validatorRegisterer); - permissionedValidatorManager.registerValidator(validator1PublicKey); - vm.stopPrank(); - - // Controller updates voting power while validator is still Registered - uint64 newVotingPower = 123; - vm.startPrank(controller1); - permissionedValidatorManager.updateValidatorVotingPower(newVotingPower); - - // Validator should remain in Registered status with updated power - Validator memory validator = permissionedValidatorManager.getValidator(controller1); - assertEq(uint8(validator.status), uint8(ValidatorStatus.Registered)); - assertEq(validator.votingPower, newVotingPower); - - vm.stopPrank(); - - // Validator set should still be empty (not active yet) - Validator[] memory activeSet = registry.getActiveValidatorSet(); - assertEq(activeSet.length, 0); - } - - function test_UpdateValidatorVotingPower_WithZeroLimit_OnlyAllowsZero() public { - vm.startPrank(owner); - - // Setup controller with zero limit - permissionedValidatorManager.addValidatorRegisterer(validatorRegisterer); - permissionedValidatorManager.configureController(controller1, 1, 0); - - vm.stopPrank(); - - // Register validator - vm.startPrank(validatorRegisterer); - permissionedValidatorManager.registerValidator(validator1PublicKey); - vm.stopPrank(); - - // Attempt to set greater voting power should revert - vm.prank(controller1); - vm.expectRevert(abi.encodeWithSelector(PermissionedValidatorManager.VotingPowerExceedsLimit.selector, 0)); - permissionedValidatorManager.updateValidatorVotingPower(1); - - // increase the voting power limit - vm.prank(owner); - permissionedValidatorManager.updateVotingPowerLimit(controller1, 1); - - // should succeed now - vm.startPrank(controller1); - permissionedValidatorManager.updateValidatorVotingPower(1); - - // Setting zero should succeed as well - permissionedValidatorManager.updateValidatorVotingPower(0); - vm.stopPrank(); - } - - - function test_UpdateVotingPowerLimit_OnlyOwner() public { - vm.startPrank(owner); - permissionedValidatorManager.addValidatorRegisterer(validatorRegisterer); - permissionedValidatorManager.configureController(controller1, 1, controllerVotingPowerLimit); - vm.stopPrank(); - - vm.startPrank(controller1); - vm.expectRevert(abi.encodeWithSelector(OwnableUpgradeable.OwnableUnauthorizedAccount.selector, controller1)); - permissionedValidatorManager.updateVotingPowerLimit(controller1, 999); - vm.stopPrank(); - } - - function test_UpdateControllerVotingPowerLimit_CurrentPowerAboveNewLimit_Succeeds() public { - vm.startPrank(owner); - permissionedValidatorManager.addValidatorRegisterer(validatorRegisterer); - permissionedValidatorManager.configureController(controller1, 1, 200); - vm.stopPrank(); - - // Register and set voting power under initial limit - vm.startPrank(validatorRegisterer); - permissionedValidatorManager.registerValidator(validator1PublicKey); - vm.stopPrank(); - - vm.startPrank(controller1); - permissionedValidatorManager.updateValidatorVotingPower(150); - vm.stopPrank(); - - // Lower limit below current power; should NOT revert - vm.prank(owner); - permissionedValidatorManager.updateVotingPowerLimit(controller1, 100); - uint64 votingPowerLimit = permissionedValidatorManager.getVotingPowerLimit(controller1); - assertEq(votingPowerLimit, 100); - - // Attempt to keep power above new limit reverts - vm.startPrank(controller1); - vm.expectRevert(abi.encodeWithSelector(PermissionedValidatorManager.VotingPowerExceedsLimit.selector, 100)); - permissionedValidatorManager.updateValidatorVotingPower(120); - vm.stopPrank(); - - // Lowering within limit succeeds - vm.prank(controller1); - permissionedValidatorManager.updateValidatorVotingPower(90); - - uint64 newVotingPower = permissionedValidatorManager.getValidator(controller1).votingPower; - assertEq(newVotingPower, 90); - } - - // ============ Complete Validator Workflow Tests ============ - function test_CompleteValidatorWorkflow() public { - vm.startPrank(owner); - - // Setup: Add validator registerer and configure controllers - permissionedValidatorManager.addValidatorRegisterer(validatorRegisterer); - permissionedValidatorManager.configureController(controller1, 1, controllerVotingPowerLimit); - permissionedValidatorManager.configureController(controller2, 2, controllerVotingPowerLimit); - - vm.stopPrank(); - - // Step 1: Register validators - vm.startPrank(validatorRegisterer); - bytes memory publicKey = validator1PublicKey; - bytes memory secondaryPublicKey = validator2PublicKey; - - // Expect ValidatorRegistered event for controller1 validator - vm.expectEmit(true, false, false, true); - emit ValidatorRegistered(1, defaultVotingPower, publicKey); - - uint256 registrationId = permissionedValidatorManager.registerValidator(publicKey); - assertEq(registrationId, 1); - - // Register a second validator so removing controller1 validator is not the final active validator - uint256 secondaryRegistrationId = permissionedValidatorManager.registerValidator(secondaryPublicKey); - assertEq(secondaryRegistrationId, 2); - vm.stopPrank(); - - // Verify validator info after registration (Registered status) - vm.startPrank(controller1); - Validator memory validatorAfterRegistration = permissionedValidatorManager.getValidator(controller1); - assertEq(uint8(validatorAfterRegistration.status), uint8(ValidatorStatus.Registered)); - assertEq(validatorAfterRegistration.publicKey, publicKey); - assertEq(validatorAfterRegistration.votingPower, defaultVotingPower); - vm.stopPrank(); - - // Step 2: Activate validator (by controller) - vm.startPrank(controller1); - - // Expect ValidatorActivated event - vm.expectEmit(true, true, false, true); - emit ValidatorActivated(1, defaultVotingPower); - - permissionedValidatorManager.activateValidator(); - - // Verify validator info after activation (Active status) - Validator memory validatorAfterActivation = permissionedValidatorManager.getValidator(controller1); - assertEq(uint8(validatorAfterActivation.status), uint8(ValidatorStatus.Active)); - assertEq(validatorAfterActivation.publicKey, publicKey); - assertEq(validatorAfterActivation.votingPower, defaultVotingPower); - - vm.stopPrank(); - - // Step 3: Update voting power (by controller) - vm.startPrank(controller1); - uint64 newVotingPower = 200; - - // Expect ValidatorVotingPowerUpdated event - vm.expectEmit(true, true, false, true); - emit ValidatorVotingPowerUpdated(1, defaultVotingPower, newVotingPower); - - permissionedValidatorManager.updateValidatorVotingPower(newVotingPower); - - // Verify validator info after voting power update - Validator memory validatorAfterUpdate = permissionedValidatorManager.getValidator(controller1); - assertEq(uint8(validatorAfterUpdate.status), uint8(ValidatorStatus.Active)); - assertEq(validatorAfterUpdate.publicKey, publicKey); - assertEq(validatorAfterUpdate.votingPower, newVotingPower); - - vm.stopPrank(); - - // Activate and power-up controller2 validator so controller1 removal remains valid. - vm.startPrank(controller2); - permissionedValidatorManager.activateValidator(); - permissionedValidatorManager.updateValidatorVotingPower(100); - vm.stopPrank(); - - // Step 4: Remove validator (by controller) - vm.startPrank(controller1); - - // Expect ValidatorRemoved event - vm.expectEmit(true, true, false, true); - emit ValidatorRemoved(1, newVotingPower); - - permissionedValidatorManager.removeValidator(); - - // Verify validator was removed - Validator memory removedValidator = permissionedValidatorManager.getValidator(controller1); - assertEq(uint8(removedValidator.status), uint8(ValidatorStatus.Unknown)); - - vm.stopPrank(); - } - - function test_RemoveValidator_WhenRemovingLastActiveValidator_ShouldRevert() public { - vm.startPrank(owner); - - // Setup: Add validator registerer and configure controller - permissionedValidatorManager.addValidatorRegisterer(validatorRegisterer); - permissionedValidatorManager.configureController(controller1, 1, controllerVotingPowerLimit); - - vm.stopPrank(); - - // Register validator - vm.startPrank(validatorRegisterer); - permissionedValidatorManager.registerValidator(validator1PublicKey); - vm.stopPrank(); - - // Activate validator and give it non-zero voting power - vm.startPrank(controller1); - permissionedValidatorManager.activateValidator(); - permissionedValidatorManager.updateValidatorVotingPower(200); - - // Removing the only active validator with positive voting power should fail - vm.expectRevert(ValidatorRegistry.InvalidValidatorSet.selector); - permissionedValidatorManager.removeValidator(); - - // Zeroing voting power for the only active validator with positive voting power should also fail - vm.expectRevert(ValidatorRegistry.InvalidValidatorSet.selector); - permissionedValidatorManager.updateValidatorVotingPower(0); - vm.stopPrank(); - } - - function test_MultipleControllersAndRegisterers() public { - address controller3 = makeAddr("controller3"); - - vm.startPrank(owner); - - // Setup multiple roles - permissionedValidatorManager.addValidatorRegisterer(validatorRegisterer); - permissionedValidatorManager.configureController(controller1, 1, controllerVotingPowerLimit); // controller1 manages registrationId 1 - permissionedValidatorManager.configureController(controller2, 2, controllerVotingPowerLimit); // controller2 manages registrationId 2 - permissionedValidatorManager.configureController(controller3, 3, controllerVotingPowerLimit); // controller3 manages registrationId 3 - - // Verify setup - assertTrue(permissionedValidatorManager.isValidatorRegisterer(validatorRegisterer)); - assertTrue(permissionedValidatorManager.isController(controller1)); - assertTrue(permissionedValidatorManager.isController(controller2)); - assertTrue(permissionedValidatorManager.isController(controller3)); - - vm.stopPrank(); - - // Register multiple validators (sequential IDs 1, 2, and 3) - vm.startPrank(validatorRegisterer); - bytes memory publicKey1 = validator1PublicKey; - bytes memory publicKey2 = validator2PublicKey; - bytes memory publicKey3 = generateEd25519PublicKey(70); - - // Expect ValidatorRegistered events - vm.expectEmit(true, false, false, true); - emit ValidatorRegistered(1, defaultVotingPower, publicKey1); - - uint256 registrationId1 = permissionedValidatorManager.registerValidator(publicKey1); - - vm.expectEmit(true, false, false, true); - emit ValidatorRegistered(2, defaultVotingPower, publicKey2); - - uint256 registrationId2 = permissionedValidatorManager.registerValidator(publicKey2); - - vm.expectEmit(true, false, false, true); - emit ValidatorRegistered(3, defaultVotingPower, publicKey3); - - uint256 registrationId3 = permissionedValidatorManager.registerValidator(publicKey3); - - assertEq(registrationId1, 1); - assertEq(registrationId2, 2); - assertEq(registrationId3, 3); - vm.stopPrank(); - - // TEST: Verify controller1 can ONLY manage validator with registrationId 1 (access control) - vm.startPrank(controller1); - - // Expect ValidatorActivated event for validator1 (registrationId 1), NOT validator2 - vm.expectEmit(true, true, false, true); - emit ValidatorActivated(1, defaultVotingPower); - - permissionedValidatorManager.activateValidator(); - - // Expect ValidatorVotingPowerUpdated event for validator1, NOT validator2 - vm.expectEmit(true, true, false, true); - emit ValidatorVotingPowerUpdated(1, defaultVotingPower, 120); - - permissionedValidatorManager.updateValidatorVotingPower(120); - vm.stopPrank(); - - // TEST: Verify controller2 can ONLY manage validator with registrationId 2 (access control) - vm.startPrank(controller2); - - // Expect ValidatorActivated event for validator2 (registrationId 2), NOT validator1 - vm.expectEmit(true, true, false, true); - emit ValidatorActivated(2, defaultVotingPower); - - permissionedValidatorManager.activateValidator(); - - // Expect ValidatorVotingPowerUpdated event for validator2, NOT validator1 - vm.expectEmit(true, true, false, true); - emit ValidatorVotingPowerUpdated(2, defaultVotingPower, 180); - - permissionedValidatorManager.updateValidatorVotingPower(180); - vm.stopPrank(); - - // Activate and power-up validator3 so controller2 removal remains valid later - vm.startPrank(controller3); - - vm.expectEmit(true, true, false, true); - emit ValidatorActivated(3, defaultVotingPower); - permissionedValidatorManager.activateValidator(); - - vm.expectEmit(true, true, false, true); - emit ValidatorVotingPowerUpdated(3, defaultVotingPower, 210); - permissionedValidatorManager.updateValidatorVotingPower(210); - vm.stopPrank(); - - // FINAL VERIFICATION: Each controller can only remove their own validator - vm.startPrank(controller1); - - vm.expectEmit(true, true, false, true); - emit ValidatorRemoved(1, 120); // controller1 removes validator1, NOT validator2 - - permissionedValidatorManager.removeValidator(); - vm.stopPrank(); - - // At this point, validator1 is removed but validator2 should still be manageable by controller2 - vm.startPrank(controller2); - - vm.expectEmit(true, true, false, true); - emit ValidatorRemoved(2, 180); // controller2 removes validator2 - - permissionedValidatorManager.removeValidator(); - - // Validator2 should be removed successfully - Validator memory validator2AfterRemoval = permissionedValidatorManager.getValidator(controller2); - assertEq(uint8(validator2AfterRemoval.status), uint8(ValidatorStatus.Unknown)); - vm.stopPrank(); - } - - function test_TransferRegistryOwnership_OnlyOwner(address _randomCaller, address _randomNewOwner) public { - vm.assume(_randomNewOwner != address(0)); - vm.assume(_randomCaller != owner); - - vm.startPrank(_randomCaller); - vm.expectRevert(abi.encodeWithSelector(OwnableUpgradeable.OwnableUnauthorizedAccount.selector, _randomCaller)); - permissionedValidatorManager.transferRegistryOwner(_randomNewOwner); - vm.stopPrank(); - } - - function test_TransferRegistryOwnership_RevertsZeroAddress() public { - vm.startPrank(owner); - vm.expectRevert(PermissionedValidatorManager.ZeroOwnerAddress.selector); - permissionedValidatorManager.transferRegistryOwner(address(0)); - vm.stopPrank(); - } - - function test_TransferRegistryOwnership_Succeeds(address _newOwner) public { - vm.assume(_newOwner != address(0)); - - vm.startPrank(owner); - - // Check event: (topic1: newOwner) (topic2: unused) (topic3: unused) (data: empty) - vm.expectEmit(true, false, false, true); - emit RegistryOwnerTransferStarted(_newOwner); - permissionedValidatorManager.transferRegistryOwner(_newOwner); - vm.stopPrank(); - - // Check side effect - assertEq(registry.pendingOwner(), _newOwner); - } - - // ============ AcceptRegistryOwnership Tests ============ - - function test_AcceptRegistryOwnership_OnlyOwner(address _randomCaller) public { - vm.assume(_randomCaller != owner); - - // First transfer ownership to PVM - vm.prank(address(permissionedValidatorManager)); - registry.transferOwnership(address(permissionedValidatorManager)); - - // Try to accept as non-owner - vm.startPrank(_randomCaller); - vm.expectRevert(abi.encodeWithSelector(OwnableUpgradeable.OwnableUnauthorizedAccount.selector, _randomCaller)); - permissionedValidatorManager.acceptRegistryOwnership(); - vm.stopPrank(); - } - - function test_AcceptRegistryOwnership_RevertsIfNotPendingOwner() public { - // PVM is not the pending owner - address pendingOwner = registry.pendingOwner(); - - // Verify PVM is not pending owner - assertNotEq(pendingOwner, address(permissionedValidatorManager)); - - // Try to accept ownership when not pending owner - vm.startPrank(owner); - vm.expectRevert(); // Will revert from Ownable2StepUpgradeable - permissionedValidatorManager.acceptRegistryOwnership(); - vm.stopPrank(); - } - - function test_AcceptRegistryOwnership_WorksInMigrationScenario() public { - // Simulate the migration scenario from the script - // Step 1: Current registry owner transfers to new PVM - address currentRegistryOwner = registry.owner(); - - vm.prank(currentRegistryOwner); - registry.transferOwnership(address(permissionedValidatorManager)); - - // Verify pending state - assertEq(registry.pendingOwner(), address(permissionedValidatorManager)); - assertEq(registry.owner(), currentRegistryOwner); - - // Step 2: PVM owner accepts ownership via PVM contract - vm.startPrank(owner); // PVM owner - - // Expect RegistryOwnerTransferCompleted event - vm.expectEmit(); - emit RegistryOwnerTransferCompleted(); - - permissionedValidatorManager.acceptRegistryOwnership(); - vm.stopPrank(); - - // Verify final state - assertEq(registry.owner(), address(permissionedValidatorManager)); - assertEq(registry.pendingOwner(), address(0)); - - // Add validator registerer first - vm.prank(owner); - permissionedValidatorManager.addValidatorRegisterer(validatorRegisterer); - - // Verify PVM can now call registry functions - bytes memory newPublicKey = generateEd25519PublicKey(100); - - vm.prank(validatorRegisterer); - uint256 registrationId = permissionedValidatorManager.registerValidator(newPublicKey); - - // Verify registration succeeded - Validator memory validator = registry.getValidator(registrationId); - assertEq(validator.publicKey, newPublicKey); - assertEq(validator.votingPower, defaultVotingPower); - } - - // ============ Helper Functions ============ - - function _setPvmOwner(PermissionedValidatorManager pvm, address newOwner) internal { - // Ownable2StepUpgradeable uses ERC-7201 slot for "openzeppelin.storage.Ownable" - bytes32 ownableSlot = 0x9016d09d72d40fdae2fd8ceac6b6234c7706214fd39c1cd1e609a0528c199300; - vm.store(address(pvm), ownableSlot, bytes32(uint256(uint160(newOwner)))); - } - - function _setPauser(address newPauser) internal { - vm.prank(owner); - permissionedValidatorManager.updatePauser(newPauser); - } - - // ============ Pausable Tests ============ - - function test_Pause_RevertsForNonPauser() public { - _setPauser(pauser); - - vm.prank(notController); - vm.expectRevert(Pausable.CallerIsNotPauser.selector); - permissionedValidatorManager.pause(); - } - - function test_RegisterValidator_RevertsWhenPaused() public { - vm.startPrank(owner); - permissionedValidatorManager.addValidatorRegisterer(validatorRegisterer); - vm.stopPrank(); - - _setPauser(pauser); - vm.prank(pauser); - vm.expectEmit(); - emit Pause(); - permissionedValidatorManager.pause(); - assertTrue(permissionedValidatorManager.paused()); - - vm.prank(validatorRegisterer); - vm.expectRevert(Pausable.ContractPaused.selector); - permissionedValidatorManager.registerValidator(validator1PublicKey); - - // Unpause and ensure register succeeds again - vm.prank(pauser); - vm.expectEmit(); - emit Unpause(); - permissionedValidatorManager.unpause(); - assertFalse(permissionedValidatorManager.paused()); - - vm.prank(validatorRegisterer); - vm.expectEmit(true, false, false, true); - emit ValidatorRegistered(1, 0, validator1PublicKey); - uint256 registrationId = permissionedValidatorManager.registerValidator(validator1PublicKey); - assertEq(registrationId, 1); - } - - function test_ControllerActions_RevertWhenPaused() public { - vm.startPrank(owner); - permissionedValidatorManager.addValidatorRegisterer(validatorRegisterer); - permissionedValidatorManager.configureController(controller1, 1, controllerVotingPowerLimit); - vm.stopPrank(); - - vm.prank(validatorRegisterer); - permissionedValidatorManager.registerValidator(validator1PublicKey); - - _setPauser(pauser); - vm.prank(pauser); - permissionedValidatorManager.pause(); - - vm.prank(controller1); - vm.expectRevert(Pausable.ContractPaused.selector); - permissionedValidatorManager.activateValidator(); - - vm.prank(controller1); - vm.expectRevert(Pausable.ContractPaused.selector); - permissionedValidatorManager.updateValidatorVotingPower(200); - - vm.prank(controller1); - vm.expectRevert(Pausable.ContractPaused.selector); - permissionedValidatorManager.removeValidator(); - } -} diff --git a/contracts/test/validator-manager/TestUtils.sol b/contracts/test/validator-manager/TestUtils.sol deleted file mode 100644 index d7b1479..0000000 --- a/contracts/test/validator-manager/TestUtils.sol +++ /dev/null @@ -1,48 +0,0 @@ -// Copyright 2025 Circle Internet Group, Inc. All rights reserved. -// -// SPDX-License-Identifier: Apache-2.0 -// -// Licensed under the Apache License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. -// You may obtain a copy of the License at -// -// http://www.apache.org/licenses/LICENSE-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// See the License for the specific language governing permissions and -// limitations under the License. -pragma solidity ^0.8.29; - -import {Test} from "forge-std/Test.sol"; -import {ValidatorRegistry} from "../../src/validator-manager/ValidatorRegistry.sol"; - -contract TestUtils is Test { - // ValidatorRegistry events - event ValidatorRegistered(uint256 indexed registrationId, uint64 votingPower, bytes publicKey); - event ValidatorActivated(uint256 indexed registrationId, uint64 votingPower); - event ValidatorRemoved(uint256 indexed registrationId, uint64 votingPower); - event ValidatorVotingPowerUpdated(uint256 indexed registrationId, uint64 oldVotingPower, uint64 newVotingPower); - - /// @dev Helper function to generate a valid Ed25519 public key for testing - function generateEd25519PublicKey(uint256 seed) public pure returns (bytes memory publicKey) { - // Generate a deterministic 32-byte Ed25519 public key - bytes32 keyBytes = keccak256(abi.encodePacked("ed25519_test_key", seed)); - publicKey = abi.encodePacked(keyBytes); - } - - // Helper function to set owner for ValidatorRegistry (simulating genesis initialization) - function _setRegistryOwner(ValidatorRegistry registry, address newOwner) internal { - // ValidatorRegistry now uses Ownable2StepUpgradeable, so owner is stored in ERC-7201 slot - // ERC-7201 slot for "openzeppelin.storage.Ownable" - bytes32 ownableSlot = 0x9016d09d72d40fdae2fd8ceac6b6234c7706214fd39c1cd1e609a0528c199300; - vm.store(address(registry), ownableSlot, bytes32(uint256(uint160(newOwner)))); - - // Initialize the _nextRegistrationID in the ValidatorRegistry storage - // _nextRegistrationID is at offset 4 within the ValidatorRegistryStorage struct - bytes32 registryBaseSlot = registry.VALIDATOR_REGISTRY_STORAGE_LOCATION(); - bytes32 nextRegistrationIdSlot = bytes32(uint256(registryBaseSlot) + 4); - vm.store(address(registry), nextRegistrationIdSlot, bytes32(uint256(1))); // _nextRegistrationID = 1 - } -} diff --git a/contracts/test/validator-manager/ValidatorRegistry.t.sol b/contracts/test/validator-manager/ValidatorRegistry.t.sol deleted file mode 100644 index 70f8d36..0000000 --- a/contracts/test/validator-manager/ValidatorRegistry.t.sol +++ /dev/null @@ -1,985 +0,0 @@ -// Copyright 2025 Circle Internet Group, Inc. All rights reserved. -// -// SPDX-License-Identifier: Apache-2.0 -// -// Licensed under the Apache License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. -// You may obtain a copy of the License at -// -// http://www.apache.org/licenses/LICENSE-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// See the License for the specific language governing permissions and -// limitations under the License. -pragma solidity ^0.8.29; - -import {console} from "forge-std/Test.sol"; -import {ValidatorRegistry} from "../../src/validator-manager/ValidatorRegistry.sol"; -import {Validator, ValidatorStatus} from "../../src/validator-manager/interfaces/IValidatorRegistry.sol"; -import {AdminUpgradeableProxy} from "../../src/proxy/AdminUpgradeableProxy.sol"; -import {TestUtils} from "./TestUtils.sol"; - -contract ValidatorRegistryTest is TestUtils { - ValidatorRegistry public validatorRegistry; - ValidatorRegistry public implementation; - AdminUpgradeableProxy public proxy; - - address public owner; - address public proxyOwner; - - // Test constants - bytes validator1PublicKey; - uint64 votingPower1 = 150; - bytes validator2PublicKey; - uint64 votingPower2 = 250; - bytes validator3PublicKey; - uint64 votingPower3 = 350; - bytes validator4PublicKey; - uint64 votingPower4 = 450; - - // ============ Constants ============ - uint256 private constant GENESIS_VALIDATOR_COUNT = 3; - uint256 private constant EXPECTED_NEXT_REG_ID_AFTER_GENESIS = 4; - - // Storage layout constants - uint256 private constant NEXT_REG_ID_SLOT_OFFSET = 4; - uint256 private constant REGISTERED_VALIDATORS_SLOT_OFFSET = 3; - uint256 private constant ACTIVE_VALIDATORS_SLOT_OFFSET = 1; - - // ============ Setup ============ - function setUp() public { - owner = makeAddr("owner"); - proxyOwner = makeAddr("proxyOwner"); - - // Initialize validator public keys - validator1PublicKey = generateEd25519PublicKey(10); - validator2PublicKey = generateEd25519PublicKey(20); - validator3PublicKey = generateEd25519PublicKey(30); - validator4PublicKey = generateEd25519PublicKey(40); - } - - // ============ Tests ============ - - // ============ Storage Layout Tests ============ - /** - * @notice Test that the storage location constant follows ERC-7201 pattern and matches expected value - */ - function test_StorageLocation_ERC7201Pattern_ShouldMatchExpectedValue() public { - // Deploy ValidatorRegistry with initialValidators - Validator[] memory initialValidators = initializeValidatorSet(); - ValidatorRegistry registry = deployValidatorRegistry(owner, initialValidators); - - // Calculate expected storage location using ERC-7201 formula - // keccak256(abi.encode(uint256(keccak256("arc.storage.ValidatorRegistry")) - 1)) & ~bytes32(uint256(0xff)) - bytes32 namespace = keccak256("arc.storage.ValidatorRegistry"); - uint256 namespaceUint = uint256(namespace); - bytes32 expectedLocation = keccak256(abi.encode(namespaceUint - 1)) & ~bytes32(uint256(0xff)); - - // Verify that the contract constant matches the expected hardcoded value - assertEq( - registry.VALIDATOR_REGISTRY_STORAGE_LOCATION(), - expectedLocation, - "Storage location constant does not match expected hardcoded value" - ); - } - - /** - * @notice Test proxy deployment and admin functionality - */ - function test_ProxyDeployment_ShouldSetupCorrectly() public { - // Deploy ValidatorRegistry via proxy - ValidatorRegistry registry = deployGenesisValidatorRegistry(); - - // Verify proxy admin is set correctly - assertEq(proxy.admin(), proxyOwner, "Proxy admin should be set correctly"); - - // Verify proxy implementation is set correctly - assertEq(proxy.implementation(), address(implementation), "Proxy implementation should be set correctly"); - - // Verify the registry through proxy works correctly - assertEq(registry.owner(), owner, "Registry owner should be set through proxy"); - - // Verify we can interact with the registry through the proxy - Validator[] memory activeValidators = registry.getActiveValidatorSet(); - assertEq(activeValidators.length, GENESIS_VALIDATOR_COUNT, "Should have genesis validators through proxy"); - } - - /** - * @notice Test positive active voting power count initialization with mixed voting powers - */ - function test_GetActiveValidatorsWithPositiveVotingPowerCount_WithMixedValidators_ShouldSetCorrectCount() public { - Validator[] memory initialValidators = new Validator[](3); - initialValidators[0] = - Validator({status: ValidatorStatus.Active, publicKey: validator1PublicKey, votingPower: 100}); - initialValidators[1] = - Validator({status: ValidatorStatus.Active, publicKey: validator2PublicKey, votingPower: 0}); - initialValidators[2] = - Validator({status: ValidatorStatus.Active, publicKey: validator3PublicKey, votingPower: 300}); - - ValidatorRegistry registry = deployValidatorRegistry(owner, initialValidators); - uint256 positiveCount = registry.getActiveValidatorsWithPositiveVotingPowerCount(); - assertEq(positiveCount, 2, "Positive active voting power count should match initialized validators"); - } - - /** - * @notice Test the actual implementation contract before genesis initialization - */ - function test_ImplementationContractState_ShouldBeUninitialized() public { - // Test the actual implementation contract before genesis initialization - ValidatorRegistry impl = new ValidatorRegistry(); - - // Implementation should be uninitialized (upgradeable pattern) - assertEq(impl.owner(), address(0), "Implementation owner should be zero address (not initialized)"); - assertEq( - impl.getNextRegistrationId(), 0, "Implementation should have default nextRegistrationId (0, uninitialized)" - ); - - // Active validator set should be empty - Validator[] memory activeValidators = impl.getActiveValidatorSet(); - assertEq(activeValidators.length, 0, "Implementation should have no active validators"); - - // Test getting non-existent validator - Validator memory nonExistentValidator = impl.getValidator(1); - assertEq( - uint8(nonExistentValidator.status), - uint8(ValidatorStatus.Unknown), - "Implementation should return Unknown for non-existent validators" - ); - assertEq( - nonExistentValidator.votingPower, - 0, - "Implementation should return zero voting power for non-existent validators" - ); - assertEq( - nonExistentValidator.publicKey.length, - 0, - "Implementation should return empty public key for non-existent validators" - ); - } - - /** - * @notice Test function to demonstrate logGenesisStorageExample utility - */ - function test_GenesisStorageLogging_WithSampleValidators_ShouldLogCorrectFormat() public view { - // Create sample initial validators for logging - Validator[] memory sampleValidators = new Validator[](2); - - sampleValidators[0] = - Validator({status: ValidatorStatus.Active, publicKey: validator1PublicKey, votingPower: 100}); - - sampleValidators[1] = - Validator({status: ValidatorStatus.Active, publicKey: validator2PublicKey, votingPower: 200}); - - // Call the logging utility function - logGenesisStorageExample(owner, sampleValidators); - } - - // ============ Genesis Initialization Tests ============ - /** - * @notice Test function to demonstrate deployValidatorRegistry utility - */ - function test_GenesisDeployment_WithInitialValidators_ShouldInitializeCorrectly() public { - // Deploy ValidatorRegistry with initialValidators - Validator[] memory initialValidators = initializeValidatorSet(); - ValidatorRegistry registry = deployGenesisValidatorRegistry(); - - // Verify owner was set correctly - assertEq(registry.owner(), owner, "Owner should be set correctly"); - - // Verify validators were initialized correctly - for (uint256 i = 0; i < initialValidators.length; i++) { - uint256 registrationId = i + 1; - Validator memory storedValidator = registry.getValidator(registrationId); - - assertEq(uint8(storedValidator.status), uint8(ValidatorStatus.Active), "Validator should be Active"); - assertEq(storedValidator.votingPower, initialValidators[i].votingPower, "Voting power should match"); - assertEq(storedValidator.publicKey, initialValidators[i].publicKey, "Public key should match"); - } - - // Verify active validator set - Validator[] memory activeValidators = registry.getActiveValidatorSet(); - assertEq(activeValidators.length, GENESIS_VALIDATOR_COUNT, "Should have genesis validators"); - - // Verify next registration ID - assertEq( - registry.getNextRegistrationId(), - EXPECTED_NEXT_REG_ID_AFTER_GENESIS, - "Next registration ID should be correct" - ); - } - - // ============ View Function Tests ============ - /** - * @notice Test getValidator with non-existent ID returns default values - */ - function test_GetValidator_WithNonExistentID_ShouldReturnEmptyValidator() public { - ValidatorRegistry registry = deployCleanValidatorRegistry(); - - // Test getValidator with non-existent ID (edge case) - Validator memory nonExistentValidator = registry.getValidator(999); - - assertEq( - uint8(nonExistentValidator.status), - uint8(ValidatorStatus.Unknown), - "Non-existent validator should have Unknown status" - ); - assertEq(nonExistentValidator.votingPower, 0, "Non-existent validator voting power should be zero"); - assertEq(nonExistentValidator.publicKey.length, 0, "Non-existent validator public key should be empty"); - } - - /** - * @notice Test getActiveValidatorSet when no validators are active - */ - function test_GetActiveValidatorSet_WhenEmpty_ShouldReturnEmptyArray() public { - ValidatorRegistry registry = deployCleanValidatorRegistry(); - - // Get active validator set when registry is empty - Validator[] memory activeValidators = registry.getActiveValidatorSet(); - - assertEq(activeValidators.length, 0, "Active validator set should be empty"); - } - - /** - * @notice Test that view functions work when there are no active validators and activation remains possible - */ - function test_ViewFunctions_WhenNoActiveValidators_ShouldWork_AndActivationShouldStillBePossible() public { - ValidatorRegistry registry = deployCleanValidatorRegistry(); - - // View functions should still work in an empty/zero-active setup. - Validator[] memory activeValidatorsBefore = registry.getActiveValidatorSet(); - assertEq(activeValidatorsBefore.length, 0, "Active validator set should start empty"); - assertEq( - registry.getActiveValidatorsWithPositiveVotingPowerCount(), - 0, - "Positive active voting power count should be zero" - ); - assertEq(registry.getNextRegistrationId(), 1, "Next registration ID should start at 1"); - - // Register and activate a validator after starting from zero active validators. - vm.startPrank(owner); - uint256 registrationId = registry.registerValidator(validator4PublicKey, votingPower4); - registry.activateValidator(registrationId); - vm.stopPrank(); - - Validator[] memory activeValidatorsAfter = registry.getActiveValidatorSet(); - assertEq(activeValidatorsAfter.length, 1, "Activation should succeed from zero active validators"); - assertEq( - registry.getActiveValidatorsWithPositiveVotingPowerCount(), - 1, - "Positive active voting power count should update after activation" - ); - - Validator memory activatedValidator = registry.getValidator(registrationId); - assertEq(uint8(activatedValidator.status), uint8(ValidatorStatus.Active), "Validator should be active"); - } - - /** - * @notice Test getActiveValidatorSet cannot be emptied by removing all validators - */ - function test_GetActiveValidatorSet_WhenAttemptingToRemoveAll_ShouldKeepOneValidator() public { - ValidatorRegistry registry = deployGenesisValidatorRegistry(); - - vm.startPrank(owner); - - // Verify we start with genesis validators - Validator[] memory initialActiveValidators = registry.getActiveValidatorSet(); - assertEq(initialActiveValidators.length, GENESIS_VALIDATOR_COUNT, "Should start with genesis validators"); - - // Remove two validators successfully - for (uint256 i = 1; i < GENESIS_VALIDATOR_COUNT; i++) { - registry.removeValidator(i); - } - - // Removing the final active validator should fail - vm.expectRevert(ValidatorRegistry.InvalidValidatorSet.selector); - registry.removeValidator(GENESIS_VALIDATOR_COUNT); - - // Verify active validator set retains one validator - Validator[] memory finalActiveValidators = registry.getActiveValidatorSet(); - assertEq(finalActiveValidators.length, 1, "Active validator set should retain one validator"); - - vm.stopPrank(); - } - - // ============ Validator Registration Tests ============ - /** - * @notice Test successful validator registration - */ - function test_RegisterValidator_WithValidData_ShouldSucceedAndEmitEvent() public { - ValidatorRegistry registry = deployGenesisValidatorRegistry(); - - vm.prank(owner); - - // Expect ValidatorRegistered event - vm.expectEmit(true, false, false, true); - emit ValidatorRegistered(EXPECTED_NEXT_REG_ID_AFTER_GENESIS, votingPower4, validator4PublicKey); - - uint256 registrationId = registry.registerValidator(validator4PublicKey, votingPower4); - - assertEq(registrationId, EXPECTED_NEXT_REG_ID_AFTER_GENESIS, "Registration ID should be correct"); - - // Verify validator was stored correctly - Validator memory storedValidator = registry.getValidator(registrationId); - assertEq( - uint8(storedValidator.status), uint8(ValidatorStatus.Registered), "Validator should be in Registered status" - ); - assertEq(storedValidator.publicKey, validator4PublicKey, "Public key should match"); - assertEq(storedValidator.votingPower, votingPower4, "Voting power should match"); - } - - /** - * @notice Test registering validator that is already registered - */ - function test_RegisterValidator_WithAlreadyRegisteredValidator_ShouldRevert() public { - ValidatorRegistry registry = deployGenesisValidatorRegistry(); - - vm.startPrank(owner); - - // Registration should fail because validator1 public key is already in genesis - vm.expectRevert( - abi.encodeWithSelector( - ValidatorRegistry.ValidatorAlreadyRegistered.selector, keccak256(validator1PublicKey) - ) - ); - registry.registerValidator(validator1PublicKey, 200); - - vm.stopPrank(); - } - - function test_RegisterValidator_WithInvalidKeyFormat_ShouldRevert(bytes calldata _invalidKey, uint64 _votingPower) - public - { - // See: ValidatorRegistry.ED25519_PUBLIC_KEY_LENGTH; - vm.assume(_invalidKey.length != 32); - ValidatorRegistry _validatorRegistry = deployGenesisValidatorRegistry(); - - vm.startPrank(owner); - - vm.expectRevert(ValidatorRegistry.InvalidPublicKeyFormat.selector); - _validatorRegistry.registerValidator(_invalidKey, _votingPower); - - vm.stopPrank(); - } - - // ============ Access Control Tests ============ - /** - * @notice Test that non-owner cannot register validators - */ - function test_RegisterValidator_WithNonOwner_ShouldRevert() public { - ValidatorRegistry registry = deployGenesisValidatorRegistry(); - address nonOwner = address(0x456); - - vm.prank(nonOwner); - vm.expectRevert(abi.encodeWithSignature("OwnableUnauthorizedAccount(address)", nonOwner)); - registry.registerValidator(validator4PublicKey, votingPower4); - } - - /** - * @notice Test that non-owner cannot activate validators - */ - function test_ActivateValidator_WithNonOwner_ShouldRevert() public { - ValidatorRegistry registry = deployGenesisValidatorRegistry(); - address nonOwner = address(0x456); - - // First register a validator as owner - vm.prank(owner); - uint256 registrationId = registry.registerValidator(validator4PublicKey, votingPower4); - - // Try to activate as non-owner - vm.prank(nonOwner); - vm.expectRevert(abi.encodeWithSignature("OwnableUnauthorizedAccount(address)", nonOwner)); - registry.activateValidator(registrationId); - } - - /** - * @notice Test that non-owner cannot remove validators - */ - function test_RemoveValidator_WithNonOwner_ShouldRevert() public { - ValidatorRegistry registry = deployGenesisValidatorRegistry(); - address nonOwner = address(0x456); - - uint256 registrationIdToRemove = 1; // Genesis validator - - vm.prank(nonOwner); - vm.expectRevert(abi.encodeWithSignature("OwnableUnauthorizedAccount(address)", nonOwner)); - registry.removeValidator(registrationIdToRemove); - } - - /** - * @notice Test that non-owner cannot update validator voting power - */ - function test_UpdateValidatorVotingPower_WithNonOwner_ShouldRevert() public { - ValidatorRegistry registry = deployGenesisValidatorRegistry(); - address nonOwner = address(0x456); - - uint256 registrationId = 1; // Genesis validator - uint64 newVotingPower = 200; - - vm.prank(nonOwner); - vm.expectRevert(abi.encodeWithSignature("OwnableUnauthorizedAccount(address)", nonOwner)); - registry.updateValidatorVotingPower(registrationId, newVotingPower); - } - - // ============ Validator Activation Tests ============ - /** - * @notice Test successful validator activation - */ - function test_ActivateValidator_WithRegisteredValidator_ShouldSucceedAndEmitEvent() public { - ValidatorRegistry registry = deployGenesisValidatorRegistry(); - - vm.startPrank(owner); - - // First register a validator - uint256 registrationId = registry.registerValidator(validator4PublicKey, votingPower4); - assertEq(registrationId, EXPECTED_NEXT_REG_ID_AFTER_GENESIS, "Registration ID should be correct"); - - // Expect ValidatorActivated event - vm.expectEmit(true, true, false, true); - emit ValidatorActivated(registrationId, votingPower4); - - registry.activateValidator(registrationId); - - // Verify validator status changed to Active - Validator memory activatedValidator = registry.getValidator(registrationId); - assertEq(uint8(activatedValidator.status), uint8(ValidatorStatus.Active), "Validator should be Active"); - - // Verify validator appears in active set - Validator[] memory activeValidators = registry.getActiveValidatorSet(); - assertEq(activeValidators.length, GENESIS_VALIDATOR_COUNT + 1, "Should have genesis + 1 active validators"); - - vm.stopPrank(); - } - - /** - * @notice Test activating validator with invalid registration ID - */ - function test_ActivateValidator_WithInvalidRegistrationId_ShouldRevert() public { - ValidatorRegistry _validatorRegistry = deployGenesisValidatorRegistry(); - - vm.prank(owner); - - // Try to activate non-existent validator - vm.expectRevert(abi.encodeWithSelector(ValidatorRegistry.InvalidRegistrationId.selector, 999)); - _validatorRegistry.activateValidator(999); - } - - /** - * @notice Test activating validator that is already active - */ - function test_ActivateValidator_WithAlreadyActiveValidator_ShouldRevert() public { - ValidatorRegistry _validatorRegistry = deployGenesisValidatorRegistry(); - - vm.startPrank(owner); - - // Register a new validator - uint256 registrationId = _validatorRegistry.registerValidator(validator4PublicKey, votingPower4); - - // Activate the validator successfully - _validatorRegistry.activateValidator(registrationId); - - // Try to activate the same validator again (should fail) - vm.expectRevert( - abi.encodeWithSelector( - ValidatorRegistry.InvalidValidatorStatus.selector, registrationId, ValidatorStatus.Active - ) - ); - _validatorRegistry.activateValidator(registrationId); - - vm.stopPrank(); - } - - // ============ Validator Removal Tests ============ - /** - * @notice Test successful validator removal - */ - function test_RemoveValidator_WithActiveValidator_ShouldSucceedAndEmitEvent() public { - ValidatorRegistry registry = deployGenesisValidatorRegistry(); - - vm.startPrank(owner); - - // Verify validator is active - Validator[] memory activeValidators = registry.getActiveValidatorSet(); - assertEq(activeValidators.length, GENESIS_VALIDATOR_COUNT, "Should have genesis validators initially"); - - uint256 registrationIdToRemove = 1; - // Expect ValidatorRemoved event - vm.expectEmit(true, true, false, true); - emit ValidatorRemoved(registrationIdToRemove, votingPower1); - - registry.removeValidator(registrationIdToRemove); - - // Verify validator was removed - Validator memory removedValidator = registry.getValidator(registrationIdToRemove); - assertEq( - uint8(removedValidator.status), uint8(ValidatorStatus.Unknown), "Validator should be Unknown after removal" - ); - - // Verify active validator set now has 2 validators - Validator[] memory activeValidatorsAfter = registry.getActiveValidatorSet(); - assertEq( - activeValidatorsAfter.length, GENESIS_VALIDATOR_COUNT - 1, "Should have one less validator after removal" - ); - - vm.stopPrank(); - } - - /** - * @notice Test removing validator with invalid registration ID - */ - function test_RemoveValidator_WithInvalidRegistrationId_ShouldRevert() public { - ValidatorRegistry _validatorRegistry = deployGenesisValidatorRegistry(); - - vm.prank(owner); - - // Try to remove non-existent validator - vm.expectRevert(abi.encodeWithSelector(ValidatorRegistry.InvalidRegistrationId.selector, 999)); - _validatorRegistry.removeValidator(999); - } - - // ============ Voting Power Update Tests ============ - /** - * @notice Test successful voting power update with event emission and getValidator verification - */ - function test_UpdateValidatorVotingPower_WithValidNewPower_ShouldSucceedAndEmitEvent() public { - ValidatorRegistry registry = deployGenesisValidatorRegistry(); - - uint256 registrationId = 1; - uint64 newVotingPower = 200; - - // Update voting power and expect event - vm.expectEmit(true, true, false, true); - emit ValidatorVotingPowerUpdated(registrationId, votingPower1, newVotingPower); - - vm.prank(owner); - registry.updateValidatorVotingPower(registrationId, newVotingPower); - - // Verify the voting power was updated using getValidator - Validator memory updatedValidator = registry.getValidator(registrationId); - assertEq(updatedValidator.votingPower, newVotingPower, "Voting power should be updated to 200"); - - // Verify other fields remain unchanged - assertEq(uint8(updatedValidator.status), uint8(ValidatorStatus.Active), "Status should remain unchanged"); - assertEq(updatedValidator.publicKey, validator1PublicKey, "Public key should remain unchanged"); - } - - /** - * @notice Test voting power update to zero (effectively deactivates the validator) - */ - function test_UpdateValidatorVotingPower_WithZeroVotingPower_ShouldSucceedAndDeactivate() public { - ValidatorRegistry registry = deployGenesisValidatorRegistry(); - - uint256 registrationId = 1; - - // Update voting power to zero (should succeed and deactivate validator) - vm.expectEmit(true, true, false, true); - emit ValidatorVotingPowerUpdated(registrationId, votingPower1, 0); - - vm.prank(owner); - registry.updateValidatorVotingPower(registrationId, 0); - - // Verify voting power was updated to zero (validator is deactivated) - Validator memory validator = registry.getValidator(registrationId); - assertEq(validator.votingPower, 0, "Voting power should be updated to zero (deactivated)"); - assertEq(uint8(validator.status), uint8(ValidatorStatus.Active), "Status should remain active"); - } - - /** - * @notice Test updating voting power from zero to positive for an active validator - */ - function test_UpdateValidatorVotingPower_ZeroToPositiveForActiveValidator_ShouldSucceed() public { - ValidatorRegistry registry = deployGenesisValidatorRegistry(); - - vm.startPrank(owner); - - // Register a validator with zero voting power and activate it - uint256 registrationId = registry.registerValidator(validator4PublicKey, 0); - registry.activateValidator(registrationId); - - uint64 newVotingPower = 125; - vm.expectEmit(true, true, false, true); - emit ValidatorVotingPowerUpdated(registrationId, 0, newVotingPower); - registry.updateValidatorVotingPower(registrationId, newVotingPower); - - Validator memory updatedValidator = registry.getValidator(registrationId); - assertEq(updatedValidator.votingPower, newVotingPower, "Voting power should update from zero to positive"); - assertEq(uint8(updatedValidator.status), uint8(ValidatorStatus.Active), "Validator should remain active"); - - vm.stopPrank(); - } - - /** - * @notice Test updating voting power for non-existent validator - */ - function test_UpdateValidatorVotingPower_WithNonExistentValidator_ShouldRevert() public { - ValidatorRegistry _validatorRegistry = deployGenesisValidatorRegistry(); - - uint256 nonExistentId = 999; - - vm.prank(owner); - vm.expectRevert(abi.encodeWithSelector(ValidatorRegistry.InvalidRegistrationId.selector, nonExistentId)); - _validatorRegistry.updateValidatorVotingPower(nonExistentId, 100); - } - - /** - * @notice Test updating voting power for removed validator - */ - function test_UpdateValidatorVotingPower_WithRemovedValidator_ShouldRevert() public { - ValidatorRegistry _validatorRegistry = deployGenesisValidatorRegistry(); - - // Register and then remove a validator - vm.startPrank(owner); - uint256 registrationIdToRemove = 1; - _validatorRegistry.removeValidator(registrationIdToRemove); - - // Try to update voting power for removed validator (should fail) - vm.expectRevert( - abi.encodeWithSelector(ValidatorRegistry.InvalidRegistrationId.selector, registrationIdToRemove) - ); - _validatorRegistry.updateValidatorVotingPower(registrationIdToRemove, 200); - vm.stopPrank(); - } - - /** - * @notice Test updating voting power to the same value (should revert) - */ - function test_UpdateValidatorVotingPower_WithSameValue_ShouldRevert() public { - ValidatorRegistry _validatorRegistry = deployGenesisValidatorRegistry(); - - uint256 registrationId = 1; - - // Try to update voting power to the same value (should fail) - vm.prank(owner); - vm.expectRevert(ValidatorRegistry.InvalidVotingPowerUpdate.selector); - _validatorRegistry.updateValidatorVotingPower(registrationId, votingPower1); - - // Verify voting power remains unchanged - Validator memory validator = _validatorRegistry.getValidator(registrationId); - assertEq(validator.votingPower, votingPower1, "Voting power should remain unchanged after failed update"); - } - - /** - * @notice Test multiple voting power updates - */ - function test_UpdateValidatorVotingPower_WithMultipleUpdates_ShouldSucceedSequentially() public { - ValidatorRegistry registry = deployGenesisValidatorRegistry(); - - uint256 registrationId = 1; - - // Perform multiple updates - vm.startPrank(owner); - - // First update: 150 -> 200 - vm.expectEmit(true, true, false, true); - emit ValidatorVotingPowerUpdated(registrationId, votingPower1, 200); - registry.updateValidatorVotingPower(registrationId, 200); - - // Second update: 200 -> 150 - vm.expectEmit(true, true, false, true); - emit ValidatorVotingPowerUpdated(registrationId, 200, 150); - registry.updateValidatorVotingPower(registrationId, 150); - - // Third update: 150 -> 300 - vm.expectEmit(true, true, false, true); - emit ValidatorVotingPowerUpdated(registrationId, 150, 300); - registry.updateValidatorVotingPower(registrationId, 300); - - vm.stopPrank(); - - // Verify final voting power - Validator memory finalValidator = registry.getValidator(registrationId); - assertEq(finalValidator.votingPower, 300, "Final voting power should be 300"); - } - - /** - * @notice Test setting the last active validator with non-zero voting power to zero should revert - */ - function test_UpdateValidatorVotingPower_SetLastPositiveActiveVotingPowerToZero_ShouldRevert() public { - ValidatorRegistry registry = deployGenesisValidatorRegistry(); - - vm.startPrank(owner); - - // Reduce two active validators to zero voting power - registry.updateValidatorVotingPower(2, 0); - registry.updateValidatorVotingPower(3, 0); - - // Zeroing the last active validator with non-zero voting power should fail - vm.expectRevert(ValidatorRegistry.InvalidValidatorSet.selector); - registry.updateValidatorVotingPower(1, 0); - - vm.stopPrank(); - } - - // ============ Genesis Helper Functions ============ - // Helper function to deploy ValidatorRegistry with custom parameters (simulating genesis initialization) - function deployCleanValidatorRegistry() internal returns (ValidatorRegistry) { - // Deploy implementation contract - implementation = new ValidatorRegistry(); - - // Deploy proxy without initialization (empty data) - proxy = new AdminUpgradeableProxy( - address(implementation), - proxyOwner, - "" // No initialization data - will be set through genesis-style storage manipulation - ); - - // Get the proxy as ValidatorRegistry interface - ValidatorRegistry deployedRegistry = ValidatorRegistry(address(proxy)); - - // Simulate genesis file initialization by directly setting storage using calculated indices - _simulateGenesisStorageInitialization(deployedRegistry, owner, new Validator[](0)); - - assertEq( - deployedRegistry.getActiveValidatorsWithPositiveVotingPowerCount(), - 0, - "Positive active voting power count should be zero for empty validator set" - ); - - return deployedRegistry; - } - - function deployGenesisValidatorRegistry() internal returns (ValidatorRegistry) { - return deployValidatorRegistry(owner, initializeValidatorSet()); - } - - function deployValidatorRegistry(address _owner, Validator[] memory _initialValidators) - internal - returns (ValidatorRegistry) - { - // Deploy implementation contract - implementation = new ValidatorRegistry(); - - // Deploy proxy without initialization (empty data) - proxy = new AdminUpgradeableProxy( - address(implementation), - proxyOwner, - "" // No initialization data - will be set through genesis-style storage manipulation - ); - - // Get the proxy as ValidatorRegistry interface - ValidatorRegistry deployedRegistry = ValidatorRegistry(address(proxy)); - - // Simulate genesis file initialization by directly setting storage using calculated indices - _simulateGenesisStorageInitialization(deployedRegistry, _owner, _initialValidators); - - uint256 expectedPositiveVotingPowerCount = 0; - for (uint256 i = 0; i < _initialValidators.length; i++) { - if (_initialValidators[i].votingPower > 0) { - ++expectedPositiveVotingPowerCount; - } - } - assertEq( - deployedRegistry.getActiveValidatorsWithPositiveVotingPowerCount(), - expectedPositiveVotingPowerCount, - "Positive active voting power count should match initialized validators" - ); - - return deployedRegistry; - } - - function initializeValidatorSet() internal view returns (Validator[] memory) { - // Create initial validators for genesis (used by specific tests) - Validator[] memory genesisValidators = new Validator[](3); - genesisValidators[0] = Validator({ - status: ValidatorStatus.Active, // Will be overridden in genesis - publicKey: validator1PublicKey, - votingPower: votingPower1 - }); - genesisValidators[1] = - Validator({status: ValidatorStatus.Active, publicKey: validator2PublicKey, votingPower: votingPower2}); - genesisValidators[2] = - Validator({status: ValidatorStatus.Active, publicKey: validator3PublicKey, votingPower: votingPower3}); - return genesisValidators; - } - - // Helper function to simulate genesis file initialization using calculated storage indices - function _simulateGenesisStorageInitialization( - ValidatorRegistry registry, - address _owner, - Validator[] memory _initialValidators - ) internal { - // This simulates how genesis file would set storage slots directly - - // === Set Ownable storage (ERC-7201) === - // ValidatorRegistry now uses Ownable2StepUpgradeable, so owner is stored at ERC-7201 slot - bytes32 ownableSlot = 0x9016d09d72d40fdae2fd8ceac6b6234c7706214fd39c1cd1e609a0528c199300; - vm.store(address(registry), ownableSlot, bytes32(uint256(uint160(_owner)))); - - // === Set ERC-7201 ValidatorRegistry storage === - // Base slot: 0xb58da0dce03316992faea3e12c60705b8ac05a309e27e3bc8421e5b271c9d200 - bytes32 baseSlot = registry.VALIDATOR_REGISTRY_STORAGE_LOCATION(); - - // Set nextRegistrationID and initialization flag - _setRegistryMetadata(registry, baseSlot, _initialValidators.length); - - // Set initial validators data - for (uint256 i = 0; i < _initialValidators.length; i++) { - _setValidatorData(registry, baseSlot, i + 1, _initialValidators[i]); - } - } - - // Helper function to set registry metadata - function _setRegistryMetadata(ValidatorRegistry registry, bytes32 baseSlot, uint256 validatorCount) private { - // Set nextRegistrationID (base slot + NEXT_REG_ID_SLOT_OFFSET) - bytes32 nextRegSlot = bytes32(uint256(baseSlot) + NEXT_REG_ID_SLOT_OFFSET); - bytes32 valueToStore = bytes32(validatorCount + 1); - vm.store(address(registry), nextRegSlot, valueToStore); - - // Set up EnumerableSet for _activeValidatorRegistrations (base slot + ACTIVE_VALIDATORS_SLOT_OFFSET) - bytes32 activeSetSlot = bytes32(uint256(baseSlot) + ACTIVE_VALIDATORS_SLOT_OFFSET); - - // EnumerableSet internal structure: - // - _values array starts at keccak256(activeSetSlot) - // - _indexes mapping starts at activeSetSlot + 1 - - // Set the length of the _values array - vm.store(address(registry), activeSetSlot, bytes32(validatorCount)); - - // Set the values in the _values array and corresponding _indexes mapping - bytes32 valuesSlot = keccak256(abi.encode(activeSetSlot)); - bytes32 indexesSlot = bytes32(uint256(activeSetSlot) + 1); - - for (uint256 i = 0; i < validatorCount; i++) { - uint256 registrationId = i + 1; - - // Set value at index i in _values array - vm.store(address(registry), bytes32(uint256(valuesSlot) + i), bytes32(registrationId)); - - // Set index mapping: _indexes[registrationId] = i + 1 (1-based indexing) - bytes32 indexMappingSlot = keccak256(abi.encode(registrationId, indexesSlot)); - vm.store(address(registry), indexMappingSlot, bytes32(i + 1)); - } - } - - // Helper function to set individual validator data - function _setValidatorData( - ValidatorRegistry registry, - bytes32 baseSlot, - uint256 registrationId, - Validator memory validator - ) private { - // Validator mapping slot - bytes32 validatorSlot = keccak256(abi.encode(registrationId, baseSlot)); - - // Set validator struct fields according to Solidity struct packing: - // Slot 0: status (uint8) only - bytes32 packedSlot0 = bytes32(uint256(2)); // ValidatorStatus.Active = 2 - vm.store(address(registry), validatorSlot, packedSlot0); - - // Slot 1: publicKey (bytes) - Ed25519 keys are always 32 bytes - // For 32-byte bytes, stored as: length in slot 1, data in keccak256(slot 1) - vm.store(address(registry), bytes32(uint256(validatorSlot) + 1), bytes32(uint256(64) + 1)); // length * 2 + 1 = 32 * 2 + 1 = 65 - bytes32 dataSlot = keccak256(abi.encode(uint256(validatorSlot) + 1)); - vm.store(address(registry), dataSlot, bytes32(validator.publicKey)); - - // Slot 2: votingPower (uint64) - vm.store(address(registry), bytes32(uint256(validatorSlot) + 2), bytes32(uint256(validator.votingPower))); - - // Set registered public keys mapping (_registeredPublicKeys[keccak256(publicKey)] = true) - // _registeredPublicKeys is at REGISTERED_VALIDATORS_SLOT_OFFSET - bytes32 registeredPublicKeysSlot = bytes32(uint256(baseSlot) + REGISTERED_VALIDATORS_SLOT_OFFSET); - bytes32 publicKeyHash = keccak256(validator.publicKey); - bytes32 registeredSlot = keccak256(abi.encode(publicKeyHash, registeredPublicKeysSlot)); - - vm.store(address(registry), registeredSlot, bytes32(uint256(1))); - } - - // ============ Genesis Storage Logging Utility ============ - - /** - * @notice Utility function to log genesis storage allocation for ValidatorRegistry - * @param _owner The owner address of the ValidatorRegistry - * @param _initialValidators Array of initial validators for genesis - */ - function logGenesisStorageExample(address _owner, Validator[] memory _initialValidators) public pure { - // Log each storage slot individually to avoid stack too deep - - // Standard contract storage - console.log("=== ValidatorRegistry Genesis Storage Allocation ==="); - console.log("// Owner (ERC-7201 slot for openzeppelin.storage.Ownable)"); - console.log( - '"0x9016d09d72d40fdae2fd8ceac6b6234c7706214fd39c1cd1e609a0528c199300": "0x%s",', _toHexStringAddress(_owner) - ); - - // ValidatorRegistryStorage struct layout (ERC-7201): - // struct ValidatorRegistryStorage { - // mapping(uint256 => Validator) _validatorsByRegistrationId; // slot 0 (uses keccak256 hashing) - // EnumerableSet.UintSet _activeValidatorRegistrations; // slot 1-2 (2 slots: _values array + _positions mapping) - // mapping(bytes32 => bool) _registeredPublicKeys; // slot 3 (uses keccak256 hashing) - // uint256 _nextRegistrationID; // slot 4 (EnumerableSet uses 2 slots) - // } - console.log("// ValidatorRegistry Storage Location (ERC-7201)"); - console.log( - "// keccak256(abi.encode(uint256(keccak256(\"arc.storage.ValidatorRegistry\")) - 1)) & ~bytes32(uint256(0xff))" - ); - console.log("// Base storage slot: 0xb58da0dce03316992faea3e12c60705b8ac05a309e27e3bc8421e5b271c9d200"); - - // nextRegistrationID (base slot + 4) - uint256 nextRegId = _initialValidators.length + 1; - console.log("// nextRegistrationID (base slot + 4)"); - console.log( - '"0xb58da0dce03316992faea3e12c60705b8ac05a309e27e3bc8421e5b271c9d204": "0x%s",', - _toHexStringBytes32(bytes32(nextRegId)) - ); - - // Log initial validators data - for (uint256 i = 0; i < _initialValidators.length; i++) { - uint256 registrationId = i + 1; - - console.log("// Validator %d Data (registration ID %d)", i + 1, registrationId); - - // Validator mapping slot calculation: keccak256(registrationId . baseSlot) - bytes32 validatorSlot = keccak256( - abi.encode(registrationId, 0xb58da0dce03316992faea3e12c60705b8ac05a309e27e3bc8421e5b271c9d200) - ); - - // Status (Active = 2) - console.log( - '"0x%s": "0x0000000000000000000000000000000000000000000000000000000000000002",', - _toHexStringBytes32(validatorSlot) - ); - - // Public key (slot + 1) - first 32 bytes - bytes32 pubKeySlot = bytes32(uint256(validatorSlot) + 1); - if (_initialValidators[i].publicKey.length >= 32) { - bytes32 pubKeyValue = bytes32(_initialValidators[i].publicKey); - console.log('"0x%s": "0x%s",', _toHexStringBytes32(pubKeySlot), _toHexStringBytes32(pubKeyValue)); - } - - // Voting power (slot + 2) - bytes32 votingPowerSlot = bytes32(uint256(validatorSlot) + 2); - console.log( - '"0x%s": "0x%s",', - _toHexStringBytes32(votingPowerSlot), - _toHexStringBytes32(bytes32(uint256(_initialValidators[i].votingPower))) - ); - } - - console.log("=== End ValidatorRegistry Genesis Storage ==="); - } - - // ============ Hex String Helper Functions ============ - - /** - * @notice Convert an address to a hex string without 0x prefix - */ - function _toHexStringAddress(address addr) internal pure returns (string memory) { - return _toHexStringBytes32(bytes32(uint256(uint160(addr)))); - } - - /** - * @notice Convert bytes32 to a hex string without 0x prefix - */ - function _toHexStringBytes32(bytes32 value) internal pure returns (string memory) { - bytes memory buffer = new bytes(64); - bytes memory alphabet = "0123456789abcdef"; - - for (uint256 i = 0; i < 32; i++) { - buffer[i * 2] = alphabet[uint8(value[i] >> 4)]; - buffer[i * 2 + 1] = alphabet[uint8(value[i] & 0x0f)]; - } - - return string(buffer); - } -} diff --git a/contracts/test/validator-manager/ValidatorRegistryProxy.t.sol b/contracts/test/validator-manager/ValidatorRegistryProxy.t.sol deleted file mode 100644 index 69198e8..0000000 --- a/contracts/test/validator-manager/ValidatorRegistryProxy.t.sol +++ /dev/null @@ -1,431 +0,0 @@ -// Copyright 2025 Circle Internet Group, Inc. All rights reserved. -// -// SPDX-License-Identifier: Apache-2.0 -// -// Licensed under the Apache License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. -// You may obtain a copy of the License at -// -// http://www.apache.org/licenses/LICENSE-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// See the License for the specific language governing permissions and -// limitations under the License. - -pragma solidity ^0.8.29; - -import {AdminUpgradeableProxy} from "../../src/proxy/AdminUpgradeableProxy.sol"; -import {ValidatorRegistry} from "../../src/validator-manager/ValidatorRegistry.sol"; -import {Validator, ValidatorStatus} from "../../src/validator-manager/interfaces/IValidatorRegistry.sol"; -import {TestUtils} from "./TestUtils.sol"; - -/** - * @title ValidatorRegistryProxyTest - * @dev Test suite for ValidatorRegistry when deployed behind AdminUpgradeableProxy - * @dev Tests ValidatorRegistry-specific functionality and business logic through proxy - */ -contract ValidatorRegistryProxyTest is TestUtils { - // ============ Constants ============ - bytes32 private constant VALIDATOR_REGISTRY_STORAGE_LOCATION = - 0xb58da0dce03316992faea3e12c60705b8ac05a309e27e3bc8421e5b271c9d200; - - // ============ State Variables ============ - - AdminUpgradeableProxy public proxy; - ValidatorRegistry public implementation; - ValidatorRegistry public validatorRegistry; - address public actualProxyAdmin; // The actual admin address read from proxy storage - - // Test role addresses - address public proxyAdminAddress; // The address we set as proxy admin - address public registryOwner; // Owner role for ValidatorRegistry - address public implementationOwner; // Owner of the ValidatorRegistry implementation - - // Test validator data - bytes public validator1PublicKey; - bytes public validator2PublicKey; - bytes public validator3PublicKey; - uint64 public constant VOTING_POWER_1 = 100; - uint64 public constant VOTING_POWER_2 = 200; - uint64 public constant VOTING_POWER_3 = 300; - - // ============ Setup ============ - - function setUp() public { - // Create test addresses - proxyAdminAddress = makeAddr("proxyAdminAddress"); - registryOwner = makeAddr("registryOwner"); - implementationOwner = makeAddr("implementationOwner"); - - // Generate test validator public keys - validator1PublicKey = generateEd25519PublicKey(1); - validator2PublicKey = generateEd25519PublicKey(2); - validator3PublicKey = generateEd25519PublicKey(3); - - // Deploy implementation contract - implementation = new ValidatorRegistry(); - - // Deploy proxy without initialization data (will be set via storage manipulation) - proxy = new AdminUpgradeableProxy( - address(implementation), - proxyAdminAddress, - "" // No initialization data - will be set through genesis-style storage manipulation - ); - - // Get proxy as ValidatorRegistry interface - validatorRegistry = ValidatorRegistry(address(proxy)); - - // Simulate genesis file initialization by directly setting storage - _simulateGenesisStorageInitialization(registryOwner); - - // Get the actual proxy admin address from ERC1967 storage - actualProxyAdmin = address( - uint160( - uint256(vm.load(address(proxy), 0xb53127684a568b3173ae13b9f8a6016e243e63b6e8ee1178d6a717850b5d6103)) - ) - ); - } - - // Helper function to simulate genesis file initialization using storage manipulation - function _simulateGenesisStorageInitialization(address _owner) internal { - // === Set Ownable owner in ERC-7201 storage === - // Calculate the correct ERC-7201 storage slot for Ownable owner - // Pattern: keccak256(abi.encode(uint256(keccak256("openzeppelin.storage.Ownable")) - 1)) & ~bytes32(uint256(0xff)) - bytes32 ownableStorageSlot = - keccak256(abi.encode(uint256(keccak256("openzeppelin.storage.Ownable")) - 1)) & ~bytes32(uint256(0xff)); - vm.store(address(validatorRegistry), ownableStorageSlot, bytes32(uint256(uint160(_owner)))); - - // === Set ValidatorRegistry ERC-7201 storage === - // Initialize _nextRegistrationId to 1 (slot offset 4 in ValidatorRegistryStorage) - vm.store( - address(validatorRegistry), bytes32(uint256(VALIDATOR_REGISTRY_STORAGE_LOCATION) + 4), bytes32(uint256(1)) - ); - - // Note: Other mappings (_validatorsByRegistrationId, _registeredPublicKeys) and - // EnumerableSet (_activeValidatorRegistrations) are initialized empty by default - } - - // ============ ValidatorRegistry Functionality Tests ============ - - function test_ValidatorRegistryInitialization() public view { - // Verify ValidatorRegistry is initialized correctly through proxy - assertEq(validatorRegistry.getNextRegistrationId(), 1, "Next registration ID should be 1"); - - // Verify active validator set is empty initially - Validator[] memory activeValidators = validatorRegistry.getActiveValidatorSet(); - assertEq(activeValidators.length, 0, "Active validator set should be empty"); - } - - function test_GetActiveValidatorsWithPositiveVotingPowerCount_WithMixedActiveValidators_ShouldSetCorrectCount() - public - { - // Register validators with mixed voting powers and activate them - vm.startPrank(registryOwner); - uint256 regId1 = validatorRegistry.registerValidator(validator1PublicKey, VOTING_POWER_1); // positive - uint256 regId2 = validatorRegistry.registerValidator(validator2PublicKey, 0); // zero - uint256 regId3 = validatorRegistry.registerValidator(validator3PublicKey, VOTING_POWER_3); // positive - validatorRegistry.activateValidator(regId1); - validatorRegistry.activateValidator(regId2); - validatorRegistry.activateValidator(regId3); - vm.stopPrank(); - - // Count should reflect active validators with positive voting power. - assertEq( - validatorRegistry.getActiveValidatorsWithPositiveVotingPowerCount(), - 2, - "Positive active voting power count should match active validators" - ); - } - - function test_RegisterValidatorViaProxy() public { - // Test registering a validator through the proxy - vm.prank(registryOwner); - uint256 registrationId = validatorRegistry.registerValidator(validator1PublicKey, VOTING_POWER_1); - - assertEq(registrationId, 1, "First registration ID should be 1"); - assertEq(validatorRegistry.getNextRegistrationId(), 2, "Next registration ID should be 2"); - - // Verify validator data - Validator memory registeredValidator = validatorRegistry.getValidator(registrationId); - assertEq( - uint256(registeredValidator.status), uint256(ValidatorStatus.Registered), "Validator should be registered" - ); - assertEq(registeredValidator.publicKey, validator1PublicKey, "Public key should match"); - assertEq(registeredValidator.votingPower, VOTING_POWER_1, "Voting power should match"); - } - - function test_ActivateValidatorViaProxy() public { - // Register a validator first - vm.prank(registryOwner); - uint256 registrationId = validatorRegistry.registerValidator(validator1PublicKey, VOTING_POWER_1); - - // Activate the validator - vm.prank(registryOwner); - validatorRegistry.activateValidator(registrationId); - - // Verify validator is active - Validator memory activeValidator = validatorRegistry.getValidator(registrationId); - assertEq(uint256(activeValidator.status), uint256(ValidatorStatus.Active), "Validator should be active"); - - // Verify active validator set contains the validator - Validator[] memory activeValidators = validatorRegistry.getActiveValidatorSet(); - assertEq(activeValidators.length, 1, "Active validator set should contain 1 validator"); - assertEq(activeValidators[0].publicKey, validator1PublicKey, "Active validator public key should match"); - } - - function test_UpdateValidatorVotingPowerViaProxy() public { - // Register and activate a validator - vm.prank(registryOwner); - uint256 registrationId = validatorRegistry.registerValidator(validator1PublicKey, VOTING_POWER_1); - - vm.prank(registryOwner); - validatorRegistry.activateValidator(registrationId); - - // Update voting power - uint64 newVotingPower = 500; - vm.prank(registryOwner); - validatorRegistry.updateValidatorVotingPower(registrationId, newVotingPower); - - // Verify voting power updated - Validator memory updatedValidator = validatorRegistry.getValidator(registrationId); - assertEq(updatedValidator.votingPower, newVotingPower, "Voting power should be updated"); - } - - function test_RemoveValidatorViaProxy_WhenRemovingLastActiveValidator_ShouldRevert() public { - // Register and activate a validator - vm.prank(registryOwner); - uint256 registrationId = validatorRegistry.registerValidator(validator1PublicKey, VOTING_POWER_1); - - vm.prank(registryOwner); - validatorRegistry.activateValidator(registrationId); - - vm.prank(registryOwner); - uint256 registrationId2 = validatorRegistry.registerValidator(validator2PublicKey, VOTING_POWER_2); - - vm.prank(registryOwner); - validatorRegistry.activateValidator(registrationId2); - - // Verify validators are active initially - Validator[] memory activeValidatorsBefore = validatorRegistry.getActiveValidatorSet(); - assertEq(activeValidatorsBefore.length, 2, "Should have 2 active validators before removal"); - - // Removing one active validator should succeed - vm.prank(registryOwner); - validatorRegistry.removeValidator(registrationId); - - Validator[] memory activeValidatorsAfterFirstRemoval = validatorRegistry.getActiveValidatorSet(); - assertEq(activeValidatorsAfterFirstRemoval.length, 1, "Active validator set should contain 1 validator"); - - // Removing the last active validator should fail - vm.prank(registryOwner); - vm.expectRevert(ValidatorRegistry.InvalidValidatorSet.selector); - validatorRegistry.removeValidator(registrationId2); - - // Verify the remaining validator is unchanged after failed removal - Validator memory remainingValidator = validatorRegistry.getValidator(registrationId2); - assertEq( - uint256(remainingValidator.status), - uint256(ValidatorStatus.Active), - "Remaining validator should stay active after failed removal" - ); - - // Verify active validator set is unchanged - Validator[] memory activeValidatorsAfterFailedRemoval = validatorRegistry.getActiveValidatorSet(); - assertEq(activeValidatorsAfterFailedRemoval.length, 1, "Active validator set should still contain 1 validator"); - } - - function test_AccessControlViaProxy() public { - // Test that access control works through the proxy - address unauthorizedUser = makeAddr("unauthorizedUser"); - - // Non-owner should not be able to register validators - vm.prank(unauthorizedUser); - vm.expectRevert(); // Should revert with access control error - validatorRegistry.registerValidator(validator1PublicKey, VOTING_POWER_1); - - // Owner should be able to register validators - vm.prank(registryOwner); - uint256 registrationId = validatorRegistry.registerValidator(validator1PublicKey, VOTING_POWER_1); // Should succeed - assertEq(registrationId, 1, "Registration should succeed for owner"); - } - - // ============ ValidatorRegistry Upgrade Tests ============ - - function test_ValidatorRegistryUpgradeWithInitialization() public { - // Register some validators first - vm.startPrank(registryOwner); - validatorRegistry.registerValidator(validator1PublicKey, VOTING_POWER_1); - validatorRegistry.registerValidator(validator2PublicKey, VOTING_POWER_2); - vm.stopPrank(); - - // Deploy a new ValidatorRegistry implementation - ValidatorRegistry newImplementation = new ValidatorRegistry(); - - // Transfer ownership to proxy admin so they can make the initialization call - vm.prank(registryOwner); - validatorRegistry.transferOwnership(proxyAdminAddress); - - // Accept ownership as proxy admin - vm.prank(proxyAdminAddress); - validatorRegistry.acceptOwnership(); - - // Prepare initialization data for registerValidator (called by new owner = proxyAdminAddress) - bytes memory initData = - abi.encodeWithSignature("registerValidator(bytes,uint64)", validator3PublicKey, VOTING_POWER_3); - - // Perform upgrade with initialization - vm.prank(proxyAdminAddress); - proxy.upgradeToAndCall(address(newImplementation), initData); - - assertEq( - validatorRegistry.getActiveValidatorsWithPositiveVotingPowerCount(), - 0, - "Positive active voting power count should remain zero when no validators are active" - ); - - // Verify the upgrade succeeded and initialization was called - assertEq( - validatorRegistry.getNextRegistrationId(), - 4, - "Next registration ID should be 4 after upgrade initialization" - ); - - // Verify the new validator was registered during upgrade - Validator memory newValidator = validatorRegistry.getValidator(3); - assertEq(newValidator.publicKey, validator3PublicKey, "New validator should be registered during upgrade"); - assertEq(newValidator.votingPower, VOTING_POWER_3, "New validator voting power should match"); - } - - function test_ValidatorRegistryStatePreservationAcrossUpgrade() public { - // Register and activate validators, modify state through proxy - vm.startPrank(registryOwner); - uint256 regId1 = validatorRegistry.registerValidator(validator1PublicKey, VOTING_POWER_1); - uint256 regId2 = validatorRegistry.registerValidator(validator2PublicKey, VOTING_POWER_2); - validatorRegistry.activateValidator(regId1); - validatorRegistry.activateValidator(regId2); - - // Update voting power - validatorRegistry.updateValidatorVotingPower(regId1, 150); - vm.stopPrank(); - - // Verify state before upgrade - Validator memory validator1Before = validatorRegistry.getValidator(regId1); - Validator memory validator2Before = validatorRegistry.getValidator(regId2); - Validator[] memory activeValidatorsBefore = validatorRegistry.getActiveValidatorSet(); - uint256 nextRegIdBefore = validatorRegistry.getNextRegistrationId(); - - assertEq(validator1Before.votingPower, 150, "Validator 1 voting power should be updated before upgrade"); - assertEq( - uint256(validator1Before.status), - uint256(ValidatorStatus.Active), - "Validator 1 should be active before upgrade" - ); - assertEq( - uint256(validator2Before.status), - uint256(ValidatorStatus.Active), - "Validator 2 should be active before upgrade" - ); - assertEq(activeValidatorsBefore.length, 2, "Should have 2 active validators before upgrade"); - assertEq(nextRegIdBefore, 3, "Next registration ID should be 3 before upgrade"); - - // Deploy new implementation and upgrade - ValidatorRegistry newImplementation = new ValidatorRegistry(); - vm.prank(proxyAdminAddress); - proxy.upgradeTo(address(newImplementation)); - - // Verify state is preserved after upgrade - Validator memory validator1After = validatorRegistry.getValidator(regId1); - Validator memory validator2After = validatorRegistry.getValidator(regId2); - Validator[] memory activeValidatorsAfter = validatorRegistry.getActiveValidatorSet(); - uint256 nextRegIdAfter = validatorRegistry.getNextRegistrationId(); - - assertEq(validator1After.votingPower, 150, "Validator 1 voting power should be preserved after upgrade"); - assertEq( - uint256(validator1After.status), - uint256(ValidatorStatus.Active), - "Validator 1 should remain active after upgrade" - ); - assertEq( - uint256(validator2After.status), - uint256(ValidatorStatus.Active), - "Validator 2 should remain active after upgrade" - ); - assertEq(validator1After.publicKey, validator1PublicKey, "Validator 1 public key should be preserved"); - assertEq(validator2After.publicKey, validator2PublicKey, "Validator 2 public key should be preserved"); - assertEq(activeValidatorsAfter.length, 2, "Should have 2 active validators after upgrade"); - assertEq(nextRegIdAfter, 3, "Next registration ID should be preserved after upgrade"); - - vm.startPrank(registryOwner); - validatorRegistry.removeValidator(regId1); - Validator[] memory activeValidatorsAfterRemoval = validatorRegistry.getActiveValidatorSet(); - assertEq(activeValidatorsAfterRemoval.length, 1, "Should have 1 active validator after removing one"); - - vm.expectRevert(ValidatorRegistry.InvalidValidatorSet.selector); - validatorRegistry.removeValidator(regId2); - vm.stopPrank(); - } - - // ============ ValidatorRegistry-Specific Edge Cases ============ - - function test_MultipleValidatorRegistryProxies() public { - // Deploy another proxy with the same ValidatorRegistry implementation - AdminUpgradeableProxy proxy2 = new AdminUpgradeableProxy(address(implementation), proxyAdminAddress, ""); - - ValidatorRegistry validatorRegistry2 = ValidatorRegistry(address(proxy2)); - - // Initialize second proxy with different owner - address registryOwner2 = makeAddr("registryOwner2"); - - // Set owner for second proxy - bytes32 ownableStorageSlot = - keccak256(abi.encode(uint256(keccak256("openzeppelin.storage.Ownable")) - 1)) & ~bytes32(uint256(0xff)); - vm.store(address(validatorRegistry2), ownableStorageSlot, bytes32(uint256(uint160(registryOwner2)))); - - // Initialize _nextRegistrationId to 1 for second proxy - vm.store( - address(validatorRegistry2), bytes32(uint256(VALIDATOR_REGISTRY_STORAGE_LOCATION) + 4), bytes32(uint256(1)) - ); - - // Register different validators in each proxy - vm.prank(registryOwner); - uint256 regId1Proxy1 = validatorRegistry.registerValidator(validator1PublicKey, VOTING_POWER_1); - - vm.prank(registryOwner2); - uint256 regId1Proxy2 = validatorRegistry2.registerValidator(validator2PublicKey, VOTING_POWER_2); - - // Verify they have independent state - assertEq(regId1Proxy1, 1, "First proxy should have registration ID 1"); - assertEq(regId1Proxy2, 1, "Second proxy should also have registration ID 1"); - - Validator memory validator1Proxy1 = validatorRegistry.getValidator(regId1Proxy1); - Validator memory validator1Proxy2 = validatorRegistry2.getValidator(regId1Proxy2); - - assertEq(validator1Proxy1.publicKey, validator1PublicKey, "First proxy should have validator1 key"); - assertEq(validator1Proxy2.publicKey, validator2PublicKey, "Second proxy should have validator2 key"); - assertEq(validator1Proxy1.votingPower, VOTING_POWER_1, "First proxy should have voting power 1"); - assertEq(validator1Proxy2.votingPower, VOTING_POWER_2, "Second proxy should have voting power 2"); - - // Verify they can be upgraded independently - ValidatorRegistry newImplementation = new ValidatorRegistry(); - vm.prank(proxyAdminAddress); - proxy.upgradeTo(address(newImplementation)); - - // First proxy upgraded, second still on old implementation - // Both should preserve their independent state - Validator memory validator1Proxy1AfterUpgrade = validatorRegistry.getValidator(regId1Proxy1); - Validator memory validator1Proxy2AfterUpgrade = validatorRegistry2.getValidator(regId1Proxy2); - - assertEq( - validator1Proxy1AfterUpgrade.publicKey, - validator1PublicKey, - "First proxy should preserve state after upgrade" - ); - assertEq( - validator1Proxy2AfterUpgrade.publicKey, - validator2PublicKey, - "Second proxy should preserve independent state" - ); - } -} diff --git a/contracts/test/validator-manager/mocks/MockController.sol b/contracts/test/validator-manager/mocks/MockController.sol deleted file mode 100644 index 1efc813..0000000 --- a/contracts/test/validator-manager/mocks/MockController.sol +++ /dev/null @@ -1,24 +0,0 @@ -// Copyright 2025 Circle Internet Group, Inc. All rights reserved. -// -// SPDX-License-Identifier: Apache-2.0 -// -// Licensed under the Apache License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. -// You may obtain a copy of the License at -// -// http://www.apache.org/licenses/LICENSE-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// See the License for the specific language governing permissions and -// limitations under the License. -pragma solidity ^0.8.29; - -import {Controller} from "../../../src/validator-manager/roles/Controller.sol"; - -contract MockController is Controller { - constructor(address initialOwner) { - _transferOwnership(initialOwner); - } -} diff --git a/contracts/test/validator-manager/mocks/MockValidatorRegisterer.sol b/contracts/test/validator-manager/mocks/MockValidatorRegisterer.sol deleted file mode 100644 index 13e961c..0000000 --- a/contracts/test/validator-manager/mocks/MockValidatorRegisterer.sol +++ /dev/null @@ -1,24 +0,0 @@ -// Copyright 2025 Circle Internet Group, Inc. All rights reserved. -// -// SPDX-License-Identifier: Apache-2.0 -// -// Licensed under the Apache License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. -// You may obtain a copy of the License at -// -// http://www.apache.org/licenses/LICENSE-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// See the License for the specific language governing permissions and -// limitations under the License. -pragma solidity ^0.8.29; - -import {ValidatorRegisterer} from "../../../src/validator-manager/roles/ValidatorRegisterer.sol"; - -contract MockValidatorRegisterer is ValidatorRegisterer { - constructor(address initialOwner) { - _transferOwnership(initialOwner); - } -} diff --git a/contracts/test/validator-manager/roles/Controller.t.sol b/contracts/test/validator-manager/roles/Controller.t.sol deleted file mode 100644 index b1b3c51..0000000 --- a/contracts/test/validator-manager/roles/Controller.t.sol +++ /dev/null @@ -1,349 +0,0 @@ -// Copyright 2025 Circle Internet Group, Inc. All rights reserved. -// -// SPDX-License-Identifier: Apache-2.0 -// -// Licensed under the Apache License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. -// You may obtain a copy of the License at -// -// http://www.apache.org/licenses/LICENSE-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// See the License for the specific language governing permissions and -// limitations under the License. - -pragma solidity ^0.8.29; - -import {Test} from "forge-std/Test.sol"; -import {MockController} from "../mocks/MockController.sol"; -import {Controller} from "../../../src/validator-manager/roles/Controller.sol"; - -contract ControllerTest is Test { - // Test events - event ControllerConfigured(address indexed controller, uint256 indexed registrationId, uint64 votingPowerLimit); - event ControllerRemoved(address indexed controller); - event VotingPowerLimitUpdated(address indexed controller, uint64 newVotingPowerLimit); - - // Test constants - address owner = address(10); - address controllerAddr = address(20); - address nonOwner = address(30); - address anotherController = address(40); - uint64 defaultVotingPowerLimit = 500; - - MockController mockController; - - function setUp() public { - vm.startPrank(owner); - mockController = new MockController(owner); - - assertEq(mockController.owner(), owner); - vm.stopPrank(); - } - - // Tests - function test_InitialState() public view { - // Initially no controllers should be configured - assertFalse(mockController.isController(controllerAddr)); - assertFalse(mockController.isController(anotherController)); - assertFalse(mockController.isController(address(0))); - } - - function test_ConfigureController_Success() public { - vm.startPrank(owner); - - // Expect the event to be emitted with the provided registration ID - vm.expectEmit(true, true, true, true); - emit ControllerConfigured(controllerAddr, 1, defaultVotingPowerLimit); - - mockController.configureController(controllerAddr, 1, defaultVotingPowerLimit); - - // Verify controller is now configured - assertTrue(mockController.isController(controllerAddr)); - - // Verify controller has correct registration ID - assertEq(mockController.getRegistrationId(controllerAddr), 1); - // Verify controller has correct voting power limit - assertEq(mockController.getVotingPowerLimit(controllerAddr), defaultVotingPowerLimit); - - vm.stopPrank(); - } - - function test_UpdateVotingPowerLimit_Success() public { - vm.startPrank(owner); - mockController.configureController(controllerAddr, 1, defaultVotingPowerLimit); - - uint64 newVotingPowerLimit = 777; - vm.expectEmit(true, false, false, true); - emit VotingPowerLimitUpdated(controllerAddr, newVotingPowerLimit); - - mockController.updateVotingPowerLimit(controllerAddr, newVotingPowerLimit); - assertEq(mockController.getVotingPowerLimit(controllerAddr), newVotingPowerLimit); - vm.stopPrank(); - } - - function test_UpdateVotingPowerLimit_OnlyOwner() public { - vm.prank(owner); - mockController.configureController(controllerAddr, 1, defaultVotingPowerLimit); - - vm.startPrank(nonOwner); - vm.expectRevert(); - mockController.updateVotingPowerLimit(controllerAddr, 9); - vm.stopPrank(); - } - - function test_UpdateVotingPowerLimit_ControllerNotConfigured() public { - vm.startPrank(owner); - vm.expectRevert(Controller.ControllerNotConfigured.selector); - mockController.updateVotingPowerLimit(controllerAddr, 9); - vm.stopPrank(); - } - - function test_ConfigureController_OnlyOwner() public { - // Non-owner should not be able to configure controller - vm.startPrank(nonOwner); - vm.expectRevert(); - mockController.configureController(controllerAddr, 1, defaultVotingPowerLimit); - vm.stopPrank(); - - // Verify controller was not configured - assertFalse(mockController.isController(controllerAddr)); - - // Registration ID getter must revert for unconfigured addresses - vm.expectRevert(Controller.ControllerNotConfigured.selector); - mockController.getRegistrationId(controllerAddr); - } - - function test_ConfigureController_AlreadyConfigured() public { - vm.startPrank(owner); - - // Configure controller initially - mockController.configureController(controllerAddr, 1, defaultVotingPowerLimit); - assertTrue(mockController.isController(controllerAddr)); - - // Configure the same controller again (should revert) - vm.expectRevert(Controller.ControllerAlreadyConfigured.selector); - mockController.configureController(controllerAddr, 2, defaultVotingPowerLimit); - - vm.stopPrank(); - } - - function test_ConfigureController_MultipleRegistrationIDs() public { - vm.startPrank(owner); - - // Configure multiple controllers with different registration IDs - mockController.configureController(controllerAddr, 1, defaultVotingPowerLimit); - mockController.configureController(anotherController, 2, defaultVotingPowerLimit); - - assertTrue(mockController.isController(controllerAddr)); - assertTrue(mockController.isController(anotherController)); - - // Verify registration ID is correct - assertEq(mockController.getRegistrationId(controllerAddr), 1); - assertEq(mockController.getRegistrationId(anotherController), 2); - - vm.stopPrank(); - } - - function test_ConfigureController_ZeroAddress() public { - vm.startPrank(owner); - - // Should not be able to configure zero address (edge case) - vm.expectRevert(); - mockController.configureController(address(0), 1, defaultVotingPowerLimit); - - vm.stopPrank(); - } - - function test_ConfigureController_ZeroRegistrationID() public { - vm.startPrank(owner); - - vm.expectRevert(Controller.RegistrationIdIsZero.selector); - mockController.configureController(address(1), 0, defaultVotingPowerLimit); - - vm.stopPrank(); - } - - function test_RemoveController_Success() public { - vm.startPrank(owner); - - // First configure a controller - mockController.configureController(controllerAddr, 1, defaultVotingPowerLimit); - assertTrue(mockController.isController(controllerAddr)); - - // Now remove it - vm.expectEmit(true, true, true, true); - emit ControllerRemoved(controllerAddr); - - mockController.removeController(controllerAddr); - - // Verify controller is no longer configured - assertFalse(mockController.isController(controllerAddr)); - // Getters must revert for unconfigured addresses - vm.expectRevert(Controller.ControllerNotConfigured.selector); - mockController.getRegistrationId(controllerAddr); - vm.expectRevert(Controller.ControllerNotConfigured.selector); - mockController.getVotingPowerLimit(controllerAddr); - - vm.stopPrank(); - } - - function test_RemoveController_OnlyOwner() public { - vm.startPrank(owner); - mockController.configureController(controllerAddr, 1, defaultVotingPowerLimit); - vm.stopPrank(); - - // Non-owner should not be able to remove controller - vm.startPrank(nonOwner); - vm.expectRevert(); - mockController.removeController(controllerAddr); - vm.stopPrank(); - - // Verify controller is still configured - assertTrue(mockController.isController(controllerAddr)); - } - - function test_RemoveController_ZeroAddress() public { - vm.startPrank(owner); - - vm.expectRevert(); - mockController.removeController(address(0)); - - vm.stopPrank(); - } - - function test_RemoveController_NonExistent() public { - vm.startPrank(owner); - - // Should revert when trying to remove non-existent controller - vm.expectRevert(Controller.ControllerNotConfigured.selector); - mockController.removeController(controllerAddr); - - // Still should not be a controller - assertFalse(mockController.isController(controllerAddr)); - - vm.stopPrank(); - } - - function test_MultipleControllers() public { - vm.startPrank(owner); - - // Configure multiple controllers with different registration IDs - mockController.configureController(controllerAddr, 1, defaultVotingPowerLimit); - mockController.configureController(anotherController, 2, defaultVotingPowerLimit); - - // Both should be controllers - assertTrue(mockController.isController(controllerAddr)); - assertTrue(mockController.isController(anotherController)); - assertEq(mockController.getVotingPowerLimit(controllerAddr), defaultVotingPowerLimit); - assertEq(mockController.getVotingPowerLimit(anotherController), defaultVotingPowerLimit); - - // Remove one controller - mockController.removeController(controllerAddr); - - // Only the second one should remain - assertFalse(mockController.isController(controllerAddr)); - assertTrue(mockController.isController(anotherController)); - vm.expectRevert(Controller.ControllerNotConfigured.selector); - mockController.getVotingPowerLimit(controllerAddr); - assertEq(mockController.getVotingPowerLimit(anotherController), defaultVotingPowerLimit); - - vm.stopPrank(); - } - - function test_IsController_View() public { - // Test the view function with various addresses - assertFalse(mockController.isController(controllerAddr)); - assertFalse(mockController.isController(owner)); - assertFalse(mockController.isController(address(0))); - assertFalse(mockController.isController(address(0xdead))); - - // Configure a controller and test again - vm.startPrank(owner); - mockController.configureController(controllerAddr, 1, defaultVotingPowerLimit); - vm.stopPrank(); - - assertTrue(mockController.isController(controllerAddr)); - assertFalse(mockController.isController(owner)); - assertFalse(mockController.isController(anotherController)); - } - - function test_ConfigureController_EmitsCorrectEvent() public { - vm.startPrank(owner); - - // Test that the event is emitted with correct parameters - vm.expectEmit(true, false, false, true); - emit ControllerConfigured(controllerAddr, 1, defaultVotingPowerLimit); - - mockController.configureController(controllerAddr, 1, defaultVotingPowerLimit); - - vm.stopPrank(); - } - - function test_RemoveController_EmitsCorrectEvent() public { - vm.startPrank(owner); - - // Configure first - mockController.configureController(controllerAddr, 1, defaultVotingPowerLimit); - - // Test that the remove event is emitted with correct parameters - vm.expectEmit(true, false, false, true); - emit ControllerRemoved(controllerAddr); - - mockController.removeController(controllerAddr); - - vm.stopPrank(); - } - - function test_CompleteWorkflow() public { - vm.startPrank(owner); - - // Configure multiple controllers with different registration IDs - mockController.configureController(controllerAddr, 1, defaultVotingPowerLimit); - mockController.configureController(anotherController, 2, defaultVotingPowerLimit); - - // Verify both are configured - assertTrue(mockController.isController(controllerAddr)); - assertTrue(mockController.isController(anotherController)); - assertEq(mockController.getVotingPowerLimit(controllerAddr), defaultVotingPowerLimit); - assertEq(mockController.getVotingPowerLimit(anotherController), defaultVotingPowerLimit); - - // Verify registration IDs are correct - assertEq(mockController.getRegistrationId(controllerAddr), 1); - assertEq(mockController.getRegistrationId(anotherController), 2); - - // Remove one - mockController.removeController(controllerAddr); - assertFalse(mockController.isController(controllerAddr)); - assertTrue(mockController.isController(anotherController)); - - // Verify registration ID is cleared after removal - vm.expectRevert(Controller.ControllerNotConfigured.selector); - mockController.getRegistrationId(controllerAddr); - assertEq(mockController.getRegistrationId(anotherController), 2); - vm.expectRevert(Controller.ControllerNotConfigured.selector); - mockController.getVotingPowerLimit(controllerAddr); - assertEq(mockController.getVotingPowerLimit(anotherController), defaultVotingPowerLimit); - - // Re-configure the first one with a different registration ID - mockController.configureController(controllerAddr, 3, defaultVotingPowerLimit); - assertTrue(mockController.isController(controllerAddr)); - assertTrue(mockController.isController(anotherController)); - assertEq(mockController.getVotingPowerLimit(controllerAddr), defaultVotingPowerLimit); - assertEq(mockController.getVotingPowerLimit(anotherController), defaultVotingPowerLimit); - - // Verify the new registration ID - assertEq(mockController.getRegistrationId(controllerAddr), 3); - assertEq(mockController.getRegistrationId(anotherController), 2); - - // Remove both - mockController.removeController(controllerAddr); - mockController.removeController(anotherController); - assertFalse(mockController.isController(controllerAddr)); - assertFalse(mockController.isController(anotherController)); - - vm.stopPrank(); - } -} diff --git a/contracts/test/validator-manager/roles/ValidatorRegisterer.t.sol b/contracts/test/validator-manager/roles/ValidatorRegisterer.t.sol deleted file mode 100644 index 1e41411..0000000 --- a/contracts/test/validator-manager/roles/ValidatorRegisterer.t.sol +++ /dev/null @@ -1,252 +0,0 @@ -// Copyright 2025 Circle Internet Group, Inc. All rights reserved. -// -// SPDX-License-Identifier: Apache-2.0 -// -// Licensed under the Apache License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. -// You may obtain a copy of the License at -// -// http://www.apache.org/licenses/LICENSE-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// See the License for the specific language governing permissions and -// limitations under the License. -pragma solidity ^0.8.29; - -import {Test} from "forge-std/Test.sol"; -import {MockValidatorRegisterer} from "../mocks/MockValidatorRegisterer.sol"; -import {ValidatorRegisterer} from "../../../src/validator-manager/roles/ValidatorRegisterer.sol"; - -contract ValidatorRegistererTest is Test { - // Test events - event ValidatorRegistererAdded(address indexed validatorRegisterer); - event ValidatorRegistererRemoved(address indexed validatorRegisterer); - - // Test constants - address owner = address(10); - address validatorRegisterer = address(20); - address nonOwner = address(30); - address anotherRegisterer = address(40); - - MockValidatorRegisterer mockValidatorRegisterer; - - function setUp() public { - vm.startPrank(owner); - mockValidatorRegisterer = new MockValidatorRegisterer(owner); - - assertEq(mockValidatorRegisterer.owner(), owner); - vm.stopPrank(); - } - - // Tests - function test_InitialState() public view { - // Initially no validator registerers should be added - assertFalse(mockValidatorRegisterer.isValidatorRegisterer(validatorRegisterer)); - assertFalse(mockValidatorRegisterer.isValidatorRegisterer(anotherRegisterer)); - assertFalse(mockValidatorRegisterer.isValidatorRegisterer(address(0))); - } - - function test_AddValidatorRegisterer_Success() public { - vm.startPrank(owner); - - // Expect the event to be emitted - vm.expectEmit(true, true, true, true); - emit ValidatorRegistererAdded(validatorRegisterer); - - mockValidatorRegisterer.addValidatorRegisterer(validatorRegisterer); - - // Verify registerer is now added - assertTrue(mockValidatorRegisterer.isValidatorRegisterer(validatorRegisterer)); - - vm.stopPrank(); - } - - function test_AddValidatorRegisterer_OnlyOwner() public { - // Non-owner should not be able to add validator registerer - vm.startPrank(nonOwner); - vm.expectRevert(); - mockValidatorRegisterer.addValidatorRegisterer(validatorRegisterer); - vm.stopPrank(); - - // Verify registerer was not added - assertFalse(mockValidatorRegisterer.isValidatorRegisterer(validatorRegisterer)); - } - - function test_AddValidatorRegisterer_AlreadyAdded() public { - vm.startPrank(owner); - - // Add registerer initially - mockValidatorRegisterer.addValidatorRegisterer(validatorRegisterer); - assertTrue(mockValidatorRegisterer.isValidatorRegisterer(validatorRegisterer)); - - // Add the same registerer again (should revert) - vm.expectRevert(ValidatorRegisterer.ValidatorRegistererAlreadyAdded.selector); - mockValidatorRegisterer.addValidatorRegisterer(validatorRegisterer); - - vm.stopPrank(); - } - - function test_AddValidatorRegisterer_ZeroAddress() public { - vm.startPrank(owner); - - // Should not be able to add zero address - vm.expectRevert(); - mockValidatorRegisterer.addValidatorRegisterer(address(0)); - - vm.stopPrank(); - } - - function test_RemoveValidatorRegisterer_Success() public { - vm.startPrank(owner); - - // First add a validator registerer - mockValidatorRegisterer.addValidatorRegisterer(validatorRegisterer); - assertTrue(mockValidatorRegisterer.isValidatorRegisterer(validatorRegisterer)); - - // Now remove it - vm.expectEmit(true, true, true, true); - emit ValidatorRegistererRemoved(validatorRegisterer); - - mockValidatorRegisterer.removeValidatorRegisterer(validatorRegisterer); - - // Verify registerer is no longer added - assertFalse(mockValidatorRegisterer.isValidatorRegisterer(validatorRegisterer)); - - vm.stopPrank(); - } - - function test_RemoveValidatorRegisterer_OnlyOwner() public { - vm.startPrank(owner); - mockValidatorRegisterer.addValidatorRegisterer(validatorRegisterer); - vm.stopPrank(); - - // Non-owner should not be able to remove validator registerer - vm.startPrank(nonOwner); - vm.expectRevert(); - mockValidatorRegisterer.removeValidatorRegisterer(validatorRegisterer); - vm.stopPrank(); - - // Verify registerer is still added - assertTrue(mockValidatorRegisterer.isValidatorRegisterer(validatorRegisterer)); - } - - function test_RemoveValidatorRegisterer_NonExistent() public { - vm.startPrank(owner); - - // Should revert when trying to remove non-existent registerer - vm.expectRevert(ValidatorRegisterer.ValidatorRegistererNotAdded.selector); - mockValidatorRegisterer.removeValidatorRegisterer(validatorRegisterer); - - // Still should not be a registerer - assertFalse(mockValidatorRegisterer.isValidatorRegisterer(validatorRegisterer)); - - vm.stopPrank(); - } - - function test_RemoveValidatorRegisterer_ZeroAddress() public { - vm.startPrank(owner); - - // Should not be able to remove zero address - vm.expectRevert(); - mockValidatorRegisterer.removeValidatorRegisterer(address(0)); - - vm.stopPrank(); - } - - function test_MultipleValidatorRegisterers() public { - vm.startPrank(owner); - - // Add multiple registerers - mockValidatorRegisterer.addValidatorRegisterer(validatorRegisterer); - mockValidatorRegisterer.addValidatorRegisterer(anotherRegisterer); - - // Both should be registerers - assertTrue(mockValidatorRegisterer.isValidatorRegisterer(validatorRegisterer)); - assertTrue(mockValidatorRegisterer.isValidatorRegisterer(anotherRegisterer)); - - // Remove one registerer - mockValidatorRegisterer.removeValidatorRegisterer(validatorRegisterer); - - // Only the second one should remain - assertFalse(mockValidatorRegisterer.isValidatorRegisterer(validatorRegisterer)); - assertTrue(mockValidatorRegisterer.isValidatorRegisterer(anotherRegisterer)); - - vm.stopPrank(); - } - - function test_IsValidatorRegisterer_View() public { - // Test the view function with various addresses - assertFalse(mockValidatorRegisterer.isValidatorRegisterer(validatorRegisterer)); - assertFalse(mockValidatorRegisterer.isValidatorRegisterer(owner)); - assertFalse(mockValidatorRegisterer.isValidatorRegisterer(address(0))); - assertFalse(mockValidatorRegisterer.isValidatorRegisterer(address(0xdead))); - - // Add a registerer and test again - vm.startPrank(owner); - mockValidatorRegisterer.addValidatorRegisterer(validatorRegisterer); - vm.stopPrank(); - - assertTrue(mockValidatorRegisterer.isValidatorRegisterer(validatorRegisterer)); - assertFalse(mockValidatorRegisterer.isValidatorRegisterer(owner)); - assertFalse(mockValidatorRegisterer.isValidatorRegisterer(anotherRegisterer)); - } - - function test_AddValidatorRegisterer_EmitsCorrectEvent() public { - vm.startPrank(owner); - - // Test that the event is emitted with correct parameters - vm.expectEmit(true, false, false, true); - emit ValidatorRegistererAdded(validatorRegisterer); - - mockValidatorRegisterer.addValidatorRegisterer(validatorRegisterer); - - vm.stopPrank(); - } - - function test_RemoveValidatorRegisterer_EmitsCorrectEvent() public { - vm.startPrank(owner); - - // Add first - mockValidatorRegisterer.addValidatorRegisterer(validatorRegisterer); - - // Test that the remove event is emitted with correct parameters - vm.expectEmit(true, false, false, true); - emit ValidatorRegistererRemoved(validatorRegisterer); - - mockValidatorRegisterer.removeValidatorRegisterer(validatorRegisterer); - - vm.stopPrank(); - } - - function test_CompleteWorkflow() public { - vm.startPrank(owner); - - // Add multiple registerers - mockValidatorRegisterer.addValidatorRegisterer(validatorRegisterer); - mockValidatorRegisterer.addValidatorRegisterer(anotherRegisterer); - - // Verify both are added - assertTrue(mockValidatorRegisterer.isValidatorRegisterer(validatorRegisterer)); - assertTrue(mockValidatorRegisterer.isValidatorRegisterer(anotherRegisterer)); - - // Remove one - mockValidatorRegisterer.removeValidatorRegisterer(validatorRegisterer); - assertFalse(mockValidatorRegisterer.isValidatorRegisterer(validatorRegisterer)); - assertTrue(mockValidatorRegisterer.isValidatorRegisterer(anotherRegisterer)); - - // Re-add the first one - mockValidatorRegisterer.addValidatorRegisterer(validatorRegisterer); - assertTrue(mockValidatorRegisterer.isValidatorRegisterer(validatorRegisterer)); - assertTrue(mockValidatorRegisterer.isValidatorRegisterer(anotherRegisterer)); - - // Remove both - mockValidatorRegisterer.removeValidatorRegisterer(validatorRegisterer); - mockValidatorRegisterer.removeValidatorRegisterer(anotherRegisterer); - assertFalse(mockValidatorRegisterer.isValidatorRegisterer(validatorRegisterer)); - assertFalse(mockValidatorRegisterer.isValidatorRegisterer(anotherRegisterer)); - - vm.stopPrank(); - } -} diff --git a/crates/arc-eth-engine/Cargo.toml b/crates/arc-eth-engine/Cargo.toml new file mode 100644 index 0000000..b7df8a9 --- /dev/null +++ b/crates/arc-eth-engine/Cargo.toml @@ -0,0 +1,9 @@ +[package] +name = "arc-eth-engine" +version.workspace = true +edition.workspace = true +license.workspace = true +repository.workspace = true +description = "Placeholder package for Arc Engine API integration." +readme = "README.md" +include = ["Cargo.toml", "README.md", "src/lib.rs"] diff --git a/crates/arc-eth-engine/README.md b/crates/arc-eth-engine/README.md new file mode 100644 index 0000000..08892b2 --- /dev/null +++ b/crates/arc-eth-engine/README.md @@ -0,0 +1,11 @@ +# arc-eth-engine + +This crate name is reserved for the Arc node project. + +This published placeholder is not a functional library. The real +`arc-eth-engine` implementation is currently distributed from +https://github.com/circlefin/arc-node and is kept out of crates.io until the +Reth SDK dependencies Arc uses are available from crates.io at compatible +versions. + +Do not depend on version `0.0.0-placeholder` for runtime functionality. diff --git a/crates/arc-eth-engine/src/lib.rs b/crates/arc-eth-engine/src/lib.rs new file mode 100644 index 0000000..7913c79 --- /dev/null +++ b/crates/arc-eth-engine/src/lib.rs @@ -0,0 +1 @@ +#![doc = include_str!("../README.md")] diff --git a/crates/arc-evm-node/Cargo.toml b/crates/arc-evm-node/Cargo.toml new file mode 100644 index 0000000..15d3ffa --- /dev/null +++ b/crates/arc-evm-node/Cargo.toml @@ -0,0 +1,9 @@ +[package] +name = "arc-evm-node" +version.workspace = true +edition.workspace = true +license.workspace = true +repository.workspace = true +description = "Placeholder package for Arc EVM node wiring." +readme = "README.md" +include = ["Cargo.toml", "README.md", "src/lib.rs"] diff --git a/crates/arc-evm-node/README.md b/crates/arc-evm-node/README.md new file mode 100644 index 0000000..f23d265 --- /dev/null +++ b/crates/arc-evm-node/README.md @@ -0,0 +1,11 @@ +# arc-evm-node + +This crate name is reserved for the Arc node project. + +This published placeholder is not a functional library. The real +`arc-evm-node` implementation is currently distributed from +https://github.com/circlefin/arc-node and is kept out of crates.io until the +Reth SDK dependencies Arc uses are available from crates.io at compatible +versions. + +Do not depend on version `0.0.0-placeholder` for runtime functionality. diff --git a/crates/arc-evm-node/src/lib.rs b/crates/arc-evm-node/src/lib.rs new file mode 100644 index 0000000..7913c79 --- /dev/null +++ b/crates/arc-evm-node/src/lib.rs @@ -0,0 +1 @@ +#![doc = include_str!("../README.md")] diff --git a/crates/arc-evm/Cargo.toml b/crates/arc-evm/Cargo.toml new file mode 100644 index 0000000..125acf3 --- /dev/null +++ b/crates/arc-evm/Cargo.toml @@ -0,0 +1,9 @@ +[package] +name = "arc-evm" +version.workspace = true +edition.workspace = true +license.workspace = true +repository.workspace = true +description = "Placeholder package for Arc EVM support." +readme = "README.md" +include = ["Cargo.toml", "README.md", "src/lib.rs"] diff --git a/crates/arc-evm/README.md b/crates/arc-evm/README.md new file mode 100644 index 0000000..9c146e2 --- /dev/null +++ b/crates/arc-evm/README.md @@ -0,0 +1,10 @@ +# arc-evm + +This crate name is reserved for the Arc node project. + +This published placeholder is not a functional library. The real `arc-evm` +implementation is currently distributed from https://github.com/circlefin/arc-node +and is kept out of crates.io until the Reth SDK dependencies Arc uses are +available from crates.io at compatible versions. + +Do not depend on version `0.0.0-placeholder` for runtime functionality. diff --git a/crates/arc-evm/src/lib.rs b/crates/arc-evm/src/lib.rs new file mode 100644 index 0000000..7913c79 --- /dev/null +++ b/crates/arc-evm/src/lib.rs @@ -0,0 +1 @@ +#![doc = include_str!("../README.md")] diff --git a/crates/arc-execution-config/Cargo.toml b/crates/arc-execution-config/Cargo.toml new file mode 100644 index 0000000..fec4424 --- /dev/null +++ b/crates/arc-execution-config/Cargo.toml @@ -0,0 +1,9 @@ +[package] +name = "arc-execution-config" +version.workspace = true +edition.workspace = true +license.workspace = true +repository.workspace = true +description = "Placeholder package for Arc execution configuration." +readme = "README.md" +include = ["Cargo.toml", "README.md", "src/lib.rs"] diff --git a/crates/arc-execution-config/README.md b/crates/arc-execution-config/README.md new file mode 100644 index 0000000..60b1949 --- /dev/null +++ b/crates/arc-execution-config/README.md @@ -0,0 +1,11 @@ +# arc-execution-config + +This crate name is reserved for the Arc node project. + +This published placeholder is not a functional library. The real +`arc-execution-config` implementation is currently distributed from +https://github.com/circlefin/arc-node and is kept out of crates.io until the +Reth SDK dependencies Arc uses are available from crates.io at compatible +versions. + +Do not depend on version `0.0.0-placeholder` for runtime functionality. diff --git a/crates/arc-execution-config/src/lib.rs b/crates/arc-execution-config/src/lib.rs new file mode 100644 index 0000000..7913c79 --- /dev/null +++ b/crates/arc-execution-config/src/lib.rs @@ -0,0 +1 @@ +#![doc = include_str!("../README.md")] diff --git a/crates/arc-execution-payload/Cargo.toml b/crates/arc-execution-payload/Cargo.toml new file mode 100644 index 0000000..420309f --- /dev/null +++ b/crates/arc-execution-payload/Cargo.toml @@ -0,0 +1,9 @@ +[package] +name = "arc-execution-payload" +version.workspace = true +edition.workspace = true +license.workspace = true +repository.workspace = true +description = "Placeholder package for Arc execution payload support." +readme = "README.md" +include = ["Cargo.toml", "README.md", "src/lib.rs"] diff --git a/crates/arc-execution-payload/README.md b/crates/arc-execution-payload/README.md new file mode 100644 index 0000000..bf2b13c --- /dev/null +++ b/crates/arc-execution-payload/README.md @@ -0,0 +1,11 @@ +# arc-execution-payload + +This crate name is reserved for the Arc node project. + +This published placeholder is not a functional library. The real +`arc-execution-payload` implementation is currently distributed from +https://github.com/circlefin/arc-node and is kept out of crates.io until the +Reth SDK dependencies Arc uses are available from crates.io at compatible +versions. + +Do not depend on version `0.0.0-placeholder` for runtime functionality. diff --git a/crates/arc-execution-payload/src/lib.rs b/crates/arc-execution-payload/src/lib.rs new file mode 100644 index 0000000..7913c79 --- /dev/null +++ b/crates/arc-execution-payload/src/lib.rs @@ -0,0 +1 @@ +#![doc = include_str!("../README.md")] diff --git a/crates/arc-execution-txpool/Cargo.toml b/crates/arc-execution-txpool/Cargo.toml new file mode 100644 index 0000000..aa379f2 --- /dev/null +++ b/crates/arc-execution-txpool/Cargo.toml @@ -0,0 +1,9 @@ +[package] +name = "arc-execution-txpool" +version.workspace = true +edition.workspace = true +license.workspace = true +repository.workspace = true +description = "Placeholder package for Arc transaction pool support." +readme = "README.md" +include = ["Cargo.toml", "README.md", "src/lib.rs"] diff --git a/crates/arc-execution-txpool/README.md b/crates/arc-execution-txpool/README.md new file mode 100644 index 0000000..06341fc --- /dev/null +++ b/crates/arc-execution-txpool/README.md @@ -0,0 +1,11 @@ +# arc-execution-txpool + +This crate name is reserved for the Arc node project. + +This published placeholder is not a functional library. The real +`arc-execution-txpool` implementation is currently distributed from +https://github.com/circlefin/arc-node and is kept out of crates.io until the +Reth SDK dependencies Arc uses are available from crates.io at compatible +versions. + +Do not depend on version `0.0.0-placeholder` for runtime functionality. diff --git a/crates/arc-execution-txpool/src/lib.rs b/crates/arc-execution-txpool/src/lib.rs new file mode 100644 index 0000000..7913c79 --- /dev/null +++ b/crates/arc-execution-txpool/src/lib.rs @@ -0,0 +1 @@ +#![doc = include_str!("../README.md")] diff --git a/crates/arc-execution-validation/Cargo.toml b/crates/arc-execution-validation/Cargo.toml new file mode 100644 index 0000000..fd18544 --- /dev/null +++ b/crates/arc-execution-validation/Cargo.toml @@ -0,0 +1,9 @@ +[package] +name = "arc-execution-validation" +version.workspace = true +edition.workspace = true +license.workspace = true +repository.workspace = true +description = "Placeholder package for Arc execution validation." +readme = "README.md" +include = ["Cargo.toml", "README.md", "src/lib.rs"] diff --git a/crates/arc-execution-validation/README.md b/crates/arc-execution-validation/README.md new file mode 100644 index 0000000..d67432f --- /dev/null +++ b/crates/arc-execution-validation/README.md @@ -0,0 +1,11 @@ +# arc-execution-validation + +This crate name is reserved for the Arc node project. + +This published placeholder is not a functional library. The real +`arc-execution-validation` implementation is currently distributed from +https://github.com/circlefin/arc-node and is kept out of crates.io until the +Reth SDK dependencies Arc uses are available from crates.io at compatible +versions. + +Do not depend on version `0.0.0-placeholder` for runtime functionality. diff --git a/crates/arc-execution-validation/src/lib.rs b/crates/arc-execution-validation/src/lib.rs new file mode 100644 index 0000000..7913c79 --- /dev/null +++ b/crates/arc-execution-validation/src/lib.rs @@ -0,0 +1 @@ +#![doc = include_str!("../README.md")] diff --git a/crates/arc-node-consensus/Cargo.toml b/crates/arc-node-consensus/Cargo.toml new file mode 100644 index 0000000..2642c3d --- /dev/null +++ b/crates/arc-node-consensus/Cargo.toml @@ -0,0 +1,9 @@ +[package] +name = "arc-node-consensus" +version.workspace = true +edition.workspace = true +license.workspace = true +repository.workspace = true +description = "Placeholder package for the Arc consensus node." +readme = "README.md" +include = ["Cargo.toml", "README.md", "src/lib.rs"] diff --git a/crates/arc-node-consensus/README.md b/crates/arc-node-consensus/README.md new file mode 100644 index 0000000..7cf0fff --- /dev/null +++ b/crates/arc-node-consensus/README.md @@ -0,0 +1,11 @@ +# arc-node-consensus + +This crate name is reserved for the Arc node project. + +This published placeholder is not a functional library. The real +`arc-node-consensus` implementation is currently distributed from +https://github.com/circlefin/arc-node and is kept out of crates.io until the +Reth SDK dependency graph used by the Arc node is available from crates.io at +compatible versions. + +Do not depend on version `0.0.0-placeholder` for runtime functionality. diff --git a/crates/arc-node-consensus/src/lib.rs b/crates/arc-node-consensus/src/lib.rs new file mode 100644 index 0000000..7913c79 --- /dev/null +++ b/crates/arc-node-consensus/src/lib.rs @@ -0,0 +1 @@ +#![doc = include_str!("../README.md")] diff --git a/crates/arc-node-execution/Cargo.toml b/crates/arc-node-execution/Cargo.toml new file mode 100644 index 0000000..325f38b --- /dev/null +++ b/crates/arc-node-execution/Cargo.toml @@ -0,0 +1,9 @@ +[package] +name = "arc-node-execution" +version.workspace = true +edition.workspace = true +license.workspace = true +repository.workspace = true +description = "Placeholder package for the Arc execution node." +readme = "README.md" +include = ["Cargo.toml", "README.md", "src/lib.rs"] diff --git a/crates/arc-node-execution/README.md b/crates/arc-node-execution/README.md new file mode 100644 index 0000000..834bf2a --- /dev/null +++ b/crates/arc-node-execution/README.md @@ -0,0 +1,11 @@ +# arc-node-execution + +This crate name is reserved for the Arc node project. + +This published placeholder is not a functional library. The real +`arc-node-execution` implementation is currently distributed from +https://github.com/circlefin/arc-node and is kept out of crates.io until the +Reth SDK dependencies Arc uses are available from crates.io at compatible +versions. + +Do not depend on version `0.0.0-placeholder` for runtime functionality. diff --git a/crates/arc-node-execution/src/lib.rs b/crates/arc-node-execution/src/lib.rs new file mode 100644 index 0000000..7913c79 --- /dev/null +++ b/crates/arc-node-execution/src/lib.rs @@ -0,0 +1 @@ +#![doc = include_str!("../README.md")] diff --git a/crates/arc-precompiles/Cargo.toml b/crates/arc-precompiles/Cargo.toml new file mode 100644 index 0000000..ab2733a --- /dev/null +++ b/crates/arc-precompiles/Cargo.toml @@ -0,0 +1,9 @@ +[package] +name = "arc-precompiles" +version.workspace = true +edition.workspace = true +license.workspace = true +repository.workspace = true +description = "Placeholder package for Arc native precompiles." +readme = "README.md" +include = ["Cargo.toml", "README.md", "src/lib.rs"] diff --git a/crates/arc-precompiles/README.md b/crates/arc-precompiles/README.md new file mode 100644 index 0000000..8f6228e --- /dev/null +++ b/crates/arc-precompiles/README.md @@ -0,0 +1,11 @@ +# arc-precompiles + +This crate name is reserved for the Arc node project. + +This published placeholder is not a functional library. The real +`arc-precompiles` implementation is currently distributed from +https://github.com/circlefin/arc-node and is kept out of crates.io until the +Reth SDK dependencies Arc uses are available from crates.io at compatible +versions. + +Do not depend on version `0.0.0-placeholder` for runtime functionality. diff --git a/crates/arc-precompiles/src/lib.rs b/crates/arc-precompiles/src/lib.rs new file mode 100644 index 0000000..7913c79 --- /dev/null +++ b/crates/arc-precompiles/src/lib.rs @@ -0,0 +1 @@ +#![doc = include_str!("../README.md")] diff --git a/crates/consensus-db/Cargo.toml b/crates/consensus-db/Cargo.toml deleted file mode 100644 index 71efeff..0000000 --- a/crates/consensus-db/Cargo.toml +++ /dev/null @@ -1,38 +0,0 @@ -[package] -name = "arc-consensus-db" -version.workspace = true -edition.workspace = true -repository.workspace = true -license.workspace = true -rust-version.workspace = true -publish.workspace = true - -[features] -mock = ["dep:mockall"] - -[dependencies] -alloy-rpc-types-engine = { workspace = true, features = ["ssz"] } -arc-consensus-types.workspace = true -bytes.workspace = true -bytesize.workspace = true -ethereum_ssz = { workspace = true } -malachitebft-app-channel.workspace = true -malachitebft-core-types.workspace = true -malachitebft-proto.workspace = true -mockall = { workspace = true, optional = true } -prost.workspace = true -redb.workspace = true -thiserror.workspace = true -tokio.workspace = true -tracing.workspace = true - -[dev-dependencies] -alloy-primitives = { workspace = true, features = ["getrandom"] } -alloy-rpc-types-engine = { workspace = true, features = ["arbitrary"] } -arbitrary = { workspace = true } -arc-consensus-types = { workspace = true, features = ["arbitrary"] } -mockall = { workspace = true } -tempfile = { workspace = true } - -[lints] -workspace = true diff --git a/crates/consensus-db/src/decoder.rs b/crates/consensus-db/src/decoder.rs deleted file mode 100644 index 5984b26..0000000 --- a/crates/consensus-db/src/decoder.rs +++ /dev/null @@ -1,657 +0,0 @@ -// Copyright 2025 Circle Internet Group, Inc. All rights reserved. -// -// SPDX-License-Identifier: Apache-2.0 -// -// Licensed under the Apache License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. -// You may obtain a copy of the License at -// -// http://www.apache.org/licenses/LICENSE-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// See the License for the specific language governing permissions and -// limitations under the License. - -use alloy_rpc_types_engine::ExecutionPayloadV3; -use prost::Message; -use ssz::Decode; -use std::time::{Duration, UNIX_EPOCH}; - -use arc_consensus_types::block::ConsensusBlock; -use arc_consensus_types::codec::proto as proto_funcs; -use arc_consensus_types::evidence::{ - DoubleProposal, DoubleVote, StoredMisbehaviorEvidence, ValidatorEvidence, -}; -use arc_consensus_types::proposal_monitor::ProposalMonitor; -use arc_consensus_types::ssz::SszBlock; -use arc_consensus_types::{ - proto, Address, Height, ProposalParts, StoredCommitCertificate, ValueId, -}; -use malachitebft_app_channel::app::types::core::{Round, Validity}; -use malachitebft_proto::Protobuf; - -use crate::invalid_payloads::{InvalidPayload, StoredInvalidPayloads}; -use crate::versions::{ - CommitCertificateVersion, ConsensusBlockVersion, ExecutionPayloadVersion, - InvalidPayloadsVersion, MisbehaviorEvidenceVersion, ProposalMonitorDataVersion, - ProposalPartsVersion, -}; - -/// Error while decoding a value from the database. -#[derive(Debug, thiserror::Error)] -pub enum DecodeError { - /// The version of the data is not supported. - #[error( - "Unsupported version: {0}; please upgrade the database by running `malachite-app upgrade`" - )] - UnsupportedVersion(u8), - - /// The data is empty. - #[error("Empty version")] - EmptyVersion, - - /// The SSZ decoding error. - #[error("SSZ decoding error: `{0:?}`")] - Ssz(ssz::DecodeError), - - /// The Protobuf decoding error. - #[error("Protobuf decoding error: `{0:?}`")] - Protobuf(prost::DecodeError), - - /// The Protobuf error (local types). - #[error("Protobuf error: `{0:?}`")] - Proto(malachitebft_proto::Error), -} - -impl From for DecodeError { - fn from(err: ssz::DecodeError) -> Self { - DecodeError::Ssz(err) - } -} - -impl From for DecodeError { - fn from(err: prost::DecodeError) -> Self { - DecodeError::Protobuf(err) - } -} - -/// Decodes an execution payload from its byte representation. -pub fn decode_execution_payload(bytes: &[u8]) -> Result { - let version = bytes.first().ok_or(DecodeError::EmptyVersion)?; - - match ExecutionPayloadVersion::try_from(*version) { - Ok(ExecutionPayloadVersion::V3) => { - ExecutionPayloadV3::from_ssz_bytes(&bytes[1..]).map_err(DecodeError::Ssz) - } - Err(version) => Err(DecodeError::UnsupportedVersion(version)), - } -} - -/// Decodes a proposal parts from its byte representation. -pub fn decode_proposal_parts(bytes: &[u8]) -> Result { - let version = bytes.first().ok_or(DecodeError::EmptyVersion)?; - - match ProposalPartsVersion::try_from(*version) { - Ok(ProposalPartsVersion::V1) => { - let proto = proto::ProposalParts::decode(&bytes[1..])?; - proto_funcs::decode_proposal_parts(proto).map_err(DecodeError::Proto) - } - Err(version) => Err(DecodeError::UnsupportedVersion(version)), - } -} - -/// Decodes a commit certificate from its byte representation -pub fn decode_certificate(bytes: &[u8]) -> Result { - let version = bytes.first().ok_or(DecodeError::EmptyVersion)?; - - match CommitCertificateVersion::try_from(*version) { - Ok(CommitCertificateVersion::V1) => { - let proto = proto::store::CommitCertificate::decode(&bytes[1..])?; - proto_funcs::decode_store_commit_certificate(proto).map_err(DecodeError::Proto) - } - Err(version) => Err(DecodeError::UnsupportedVersion(version)), - } -} - -/// Decodes a block from its byte representation. -pub fn decode_block(bytes: &[u8]) -> Result { - let version = bytes.first().ok_or(DecodeError::EmptyVersion)?; - - match ConsensusBlockVersion::try_from(*version) { - Ok(ConsensusBlockVersion::V1) => { - let (height, round, valid_round, proposer, is_valid, execution_payload, signature) = - SszBlock::::from_ssz_bytes(&bytes[1..])?; - Ok(ConsensusBlock { - height: Height::new(height), - round: Round::from(round), - valid_round: Round::from(valid_round), - proposer: Address::from(proposer), - validity: Validity::from_bool(is_valid), - execution_payload, - signature: signature.map(|s| s.0), - }) - } - Err(version) => Err(DecodeError::UnsupportedVersion(version)), - } -} - -/// Decodes proposal monitor data from its byte representation. -pub fn decode_proposal_monitor_data(bytes: &[u8]) -> Result { - use arc_consensus_types::proposal_monitor::ProposalSuccessState; - - let version = bytes.first().ok_or(DecodeError::EmptyVersion)?; - - match ProposalMonitorDataVersion::try_from(*version) { - Ok(ProposalMonitorDataVersion::V1) => { - let proto_data = proto::ProtoProposalMonitorData::decode(&bytes[1..])?; - - let proposer = proto_data - .proposer - .ok_or_else(|| proto_error("Missing proposer in proposal monitor data")) - .and_then(|a| Address::from_proto(a).map_err(DecodeError::Proto))?; - - // SystemTime + Duration panics only on overflow past year ~30 billion — safe for millis from proto - #[allow(clippy::arithmetic_side_effects)] - let start_time = UNIX_EPOCH + Duration::from_millis(proto_data.start_time_ms); - - // Convert times from milliseconds; 0 = not present - let proposal_receive_time = if proto_data.receive_time_ms > 0 { - #[allow(clippy::arithmetic_side_effects)] - Some(UNIX_EPOCH + Duration::from_millis(proto_data.receive_time_ms)) - } else { - None - }; - - // Convert value_id from bytes (empty = not present) - let value_id = if proto_data.value_id.len() == 32 { - let mut hash = [0u8; 32]; - hash.copy_from_slice(&proto_data.value_id); - Some(ValueId::new(hash.into())) - } else { - None - }; - - // Convert successful_state back to Option - let successful = ProposalSuccessState::from(proto_data.successful_state); - - Ok(ProposalMonitor { - height: Height::new(proto_data.height), - proposer, - start_time, - proposal_receive_time, - value_id, - successful, - synced: proto_data.synced, - }) - } - Err(version) => Err(DecodeError::UnsupportedVersion(version)), - } -} - -/// Decodes misbehavior evidence from its byte representation. -pub fn decode_misbehavior_evidence(bytes: &[u8]) -> Result { - let version = bytes.first().ok_or(DecodeError::EmptyVersion)?; - - match MisbehaviorEvidenceVersion::try_from(*version) { - Ok(MisbehaviorEvidenceVersion::V1) => { - let proto_evidence = proto::ProtoMisbehaviorEvidence::decode(&bytes[1..])?; - decode_proto_misbehavior_evidence(proto_evidence) - } - Err(version) => Err(DecodeError::UnsupportedVersion(version)), - } -} - -/// Decode a proto misbehavior evidence into domain type. -fn decode_proto_misbehavior_evidence( - proto_evidence: proto::ProtoMisbehaviorEvidence, -) -> Result { - let validators = proto_evidence - .validators - .into_iter() - .map(decode_proto_validator_evidence) - .collect::, DecodeError>>()?; - - Ok(StoredMisbehaviorEvidence { - height: Height::new(proto_evidence.height), - validators, - }) -} - -/// Decode a proto validator evidence into domain type. -fn decode_proto_validator_evidence( - v: proto::ProtoValidatorEvidence, -) -> Result { - let address = v - .address - .ok_or_else(|| proto_error("Missing address in validator evidence")) - .and_then(|a| Address::from_proto(a).map_err(DecodeError::Proto))?; - - let double_votes = v - .double_votes - .into_iter() - .map(decode_proto_double_vote) - .collect::, DecodeError>>()?; - - let double_proposals = v - .double_proposals - .into_iter() - .map(decode_proto_double_proposal) - .collect::, DecodeError>>()?; - - Ok(ValidatorEvidence { - address, - double_votes, - double_proposals, - }) -} - -/// Decode a proto double vote into domain type. -fn decode_proto_double_vote(dv: proto::ProtoDoubleVote) -> Result { - let first = dv - .first - .ok_or_else(|| proto_error("Missing first vote in double vote")) - .and_then(|m| proto_funcs::decode_vote(m).map_err(DecodeError::Proto))?; - - let second = dv - .second - .ok_or_else(|| proto_error("Missing second vote in double vote")) - .and_then(|m| proto_funcs::decode_vote(m).map_err(DecodeError::Proto))?; - - Ok(DoubleVote { first, second }) -} - -/// Decode a proto double proposal into domain type. -fn decode_proto_double_proposal( - dp: proto::ProtoDoubleProposal, -) -> Result { - let first = dp - .first - .ok_or_else(|| proto_error("Missing first proposal in double proposal")) - .and_then(|m| proto_funcs::decode_signed_proposal(m).map_err(DecodeError::Proto))?; - - let second = dp - .second - .ok_or_else(|| proto_error("Missing second proposal in double proposal")) - .and_then(|m| proto_funcs::decode_signed_proposal(m).map_err(DecodeError::Proto))?; - - Ok(DoubleProposal { first, second }) -} - -/// Decodes invalid payloads from their byte representation. -pub fn decode_invalid_payloads(bytes: &[u8]) -> Result { - let version = bytes.first().ok_or(DecodeError::EmptyVersion)?; - - match InvalidPayloadsVersion::try_from(*version) { - Ok(InvalidPayloadsVersion::V1) => { - let proto = proto::ProtoInvalidPayloads::decode(&bytes[1..])?; - decode_proto_invalid_payloads(proto) - } - Err(version) => Err(DecodeError::UnsupportedVersion(version)), - } -} - -/// Decode proto invalid payloads into domain type. -fn decode_proto_invalid_payloads( - proto: proto::ProtoInvalidPayloads, -) -> Result { - let payloads = proto - .payloads - .into_iter() - .map(decode_proto_invalid_payload) - .collect::, DecodeError>>()?; - - Ok(StoredInvalidPayloads { - height: Height::new(proto.height), - payloads, - }) -} - -/// Decode a single proto invalid payload into domain type. -fn decode_proto_invalid_payload( - p: proto::ProtoInvalidPayload, -) -> Result { - let address = p - .proposer_address - .ok_or_else(|| proto_error("Missing proposer_address in invalid payload")) - .and_then(|a| Address::from_proto(a).map_err(DecodeError::Proto))?; - - let payload = p - .payload - .as_ref() - .map(|bytes| ExecutionPayloadV3::from_ssz_bytes(bytes).map_err(DecodeError::Ssz)) - .transpose()?; - - Ok(InvalidPayload { - height: Height::new(p.height), - round: Round::new(p.round), - proposer_address: address, - payload, - reason: p.reason, - }) -} - -fn proto_error(msg: &str) -> DecodeError { - DecodeError::Proto(malachitebft_proto::Error::Other(msg.to_string())) -} - -#[cfg(test)] -mod tests { - use super::*; - use crate::encoder; - use arbitrary::Unstructured; - use arc_consensus_types::proposal_monitor::{ProposalMonitor, ProposalSuccessState}; - use arc_consensus_types::{ - signing::Signature, Address, ArcContext, CommitCertificateType, Height, ProposalData, - ProposalFin, ProposalInit, ProposalPart, ProposalParts, StoredCommitCertificate, ValueId, - B256, - }; - use bytes::Bytes; - use malachitebft_app_channel::app::types::core::{ - CommitCertificate, CommitSignature, Round, Validity, - }; - - use std::time::{Duration, UNIX_EPOCH}; - - fn create_test_execution_payload() -> ExecutionPayloadV3 { - // Use arbitrary to generate a valid ExecutionPayloadV3 - Unstructured::new(&[0xab; 1024]) - .arbitrary::() - .unwrap() - } - - fn create_test_proposal_parts() -> ProposalParts { - let signature = Signature::from_bytes([0u8; 64]); - - let parts = vec![ - ProposalPart::Init(ProposalInit::new( - Height::new(1), - Round::new(0), - Round::Nil, - Address::new([0u8; 20]), - )), - ProposalPart::Data(ProposalData::new(Bytes::from_static(b"test data"))), - ProposalPart::Fin(ProposalFin::new(signature)), - ]; - - ProposalParts::new(parts).unwrap() - } - - fn create_test_commit_certificate() -> CommitCertificate { - let signature = Signature::from_bytes([0u8; 64]); - let address = Address::new([0u8; 20]); - let commit_sig = CommitSignature::new(address, signature); - - // Get a valid block hash from a test payload - let payload = create_test_execution_payload(); - let block_hash = payload.payload_inner.payload_inner.block_hash; - - CommitCertificate { - height: Height::new(1), - round: Round::new(0), - value_id: ValueId::new(block_hash), - commit_signatures: vec![commit_sig], - } - } - - fn create_test_consensus_block() -> ConsensusBlock { - let signature = Signature::from_bytes([0u8; 64]); - - ConsensusBlock { - height: Height::new(1), - round: Round::new(0), - valid_round: Round::Nil, - proposer: Address::new([0u8; 20]), - validity: Validity::Valid, - execution_payload: create_test_execution_payload(), - signature: Some(signature), - } - } - - #[test] - fn test_decode_execution_payload_valid() { - let payload = create_test_execution_payload(); - let encoded = encoder::encode_execution_payload(&payload); - let decoded = decode_execution_payload(&encoded).expect("should decode"); - assert_eq!(decoded, payload); - } - - #[test] - fn test_decode_execution_payload_empty() { - let result = decode_execution_payload(&[]); - assert!(matches!(result, Err(DecodeError::EmptyVersion))); - } - - #[test] - fn test_decode_execution_payload_unsupported_version() { - let mut bytes = vec![0xFF]; // Unsupported version - bytes.extend_from_slice(&[1, 2, 3]); - let result = decode_execution_payload(&bytes); - assert!(matches!(result, Err(DecodeError::UnsupportedVersion(0xFF)))); - } - - #[test] - fn test_decode_proposal_parts_valid() { - let parts = create_test_proposal_parts(); - let encoded = encoder::encode_proposal_parts(&parts).expect("should encode"); - let decoded = decode_proposal_parts(&encoded).expect("should decode"); - assert_eq!(decoded, parts); - } - - #[test] - fn test_decode_proposal_parts_empty() { - let result = decode_proposal_parts(&[]); - assert!(matches!(result, Err(DecodeError::EmptyVersion))); - } - - #[test] - fn test_decode_proposal_parts_unsupported_version() { - let mut bytes = vec![0x99]; // Unsupported version - bytes.extend_from_slice(&[1, 2, 3]); - let result = decode_proposal_parts(&bytes); - assert!(matches!(result, Err(DecodeError::UnsupportedVersion(0x99)))); - } - - #[test] - fn test_decode_certificate_valid() { - let cert = create_test_commit_certificate(); - - for tpe in [ - CommitCertificateType::Unknown, - CommitCertificateType::Minimal, - CommitCertificateType::Extended, - ] { - let cert = StoredCommitCertificate { - certificate: cert.clone(), - certificate_type: tpe, - proposer: Some(Address::new([0u8; 20])), - }; - - let encoded = encoder::encode_certificate(&cert).expect("should encode"); - let stored = decode_certificate(&encoded).expect("should decode"); - - assert_eq!(stored, cert); - } - } - - #[test] - fn test_decode_certificate_empty() { - let result = decode_certificate(&[]); - assert!(matches!(result, Err(DecodeError::EmptyVersion))); - } - - #[test] - fn test_decode_certificate_unsupported_version() { - let mut bytes = vec![0x42]; // Unsupported version - bytes.extend_from_slice(&[1, 2, 3]); - let result = decode_certificate(&bytes); - assert!(matches!(result, Err(DecodeError::UnsupportedVersion(0x42)))); - } - - #[test] - fn test_decode_block_valid() { - let block = create_test_consensus_block(); - let encoded = encoder::encode_block(&block); - let decoded = decode_block(&encoded).expect("should decode"); - assert_eq!(decoded, block); - } - - #[test] - fn test_decode_block_empty() { - let result = decode_block(&[]); - assert!(matches!(result, Err(DecodeError::EmptyVersion))); - } - - #[test] - fn test_decode_block_unsupported_version() { - let mut bytes = vec![0xAB]; // Unsupported version - bytes.extend_from_slice(&[1, 2, 3]); - let result = decode_block(&bytes); - assert!(matches!(result, Err(DecodeError::UnsupportedVersion(0xAB)))); - } - - fn create_test_proposal_monitor( - height: u64, - with_proposal: bool, - successful: ProposalSuccessState, - synced: bool, - ) -> ProposalMonitor { - #[allow(clippy::arithmetic_side_effects)] - let start_time = UNIX_EPOCH + Duration::from_secs(1000000); - let proposer = Address::new([0x42; 20]); - - let mut monitor = ProposalMonitor::new(Height::new(height), proposer, start_time); - - if with_proposal { - #[allow(clippy::arithmetic_side_effects)] - let receive_time = start_time + Duration::from_millis(150); - let value_id = ValueId::new(B256::repeat_byte(0xAB)); - monitor.proposal_receive_time = Some(receive_time); - monitor.value_id = Some(value_id); - } - - monitor.successful = successful; - monitor.synced = synced; - - monitor - } - - #[test] - fn test_decode_proposal_monitor_data_empty() { - let result = decode_proposal_monitor_data(&[]); - assert!(matches!(result, Err(DecodeError::EmptyVersion))); - } - - #[test] - fn test_decode_proposal_monitor_data_unsupported_version() { - let mut bytes = vec![0xFF]; // Unsupported version - bytes.extend_from_slice(&[1, 2, 3]); - let result = decode_proposal_monitor_data(&bytes); - assert!(matches!(result, Err(DecodeError::UnsupportedVersion(0xFF)))); - } - - #[test] - fn test_decode_proposal_monitor_data_valid_full() { - let monitor = - create_test_proposal_monitor(42, true, ProposalSuccessState::Successful, false); - let encoded = encoder::encode_proposal_monitor_data(&monitor).expect("should encode"); - let decoded = decode_proposal_monitor_data(&encoded).expect("should decode"); - - assert_eq!(decoded.height, monitor.height); - assert_eq!(decoded.proposer, monitor.proposer); - assert_eq!(decoded.value_id, monitor.value_id); - assert_eq!(decoded.successful, monitor.successful); - assert_eq!(decoded.synced, monitor.synced); - // Times are converted to/from milliseconds, so we compare at that precision - assert!( - decoded - .start_time - .duration_since(std::time::UNIX_EPOCH) - .unwrap() - .as_millis() - == monitor - .start_time - .duration_since(std::time::UNIX_EPOCH) - .unwrap() - .as_millis() - ); - assert!(decoded.proposal_receive_time.is_some()); - } - - #[test] - fn test_decode_proposal_monitor_data_valid_no_proposal() { - let monitor = - create_test_proposal_monitor(100, false, ProposalSuccessState::Unknown, false); - let encoded = encoder::encode_proposal_monitor_data(&monitor).expect("should encode"); - let decoded = decode_proposal_monitor_data(&encoded).expect("should decode"); - - assert_eq!(decoded.height, monitor.height); - assert_eq!(decoded.proposer, monitor.proposer); - assert!(decoded.proposal_receive_time.is_none()); - assert!(decoded.value_id.is_none()); - assert!(decoded.successful.is_unknown()); - assert!(!decoded.synced); - } - - #[test] - fn test_decode_proposal_monitor_data_synced() { - let monitor = - create_test_proposal_monitor(50, false, ProposalSuccessState::Unsuccessful, true); - let encoded = encoder::encode_proposal_monitor_data(&monitor).expect("should encode"); - let decoded = decode_proposal_monitor_data(&encoded).expect("should decode"); - - assert_eq!(decoded.height, monitor.height); - assert!(decoded.synced); - assert_eq!(decoded.successful, ProposalSuccessState::Unsuccessful); - } - - #[test] - fn test_decode_invalid_payloads_valid() { - let payload = create_test_execution_payload(); - let stored = StoredInvalidPayloads { - height: Height::new(5), - payloads: vec![InvalidPayload { - height: Height::new(5), - round: Round::new(0), - proposer_address: Address::new([1u8; 20]), - payload: Some(payload), - reason: "bad payload".to_string(), - }], - }; - let encoded = encoder::encode_invalid_payloads(&stored).expect("should encode"); - let decoded = decode_invalid_payloads(&encoded).expect("should decode"); - assert_eq!(decoded, stored); - } - - #[test] - fn test_decode_invalid_payloads_without_payload() { - let stored = StoredInvalidPayloads { - height: Height::new(3), - payloads: vec![InvalidPayload { - height: Height::new(3), - round: Round::new(0), - proposer_address: Address::new([5u8; 20]), - payload: None, - reason: "engine error".to_string(), - }], - }; - let encoded = encoder::encode_invalid_payloads(&stored).expect("should encode"); - let decoded = decode_invalid_payloads(&encoded).expect("should decode"); - assert_eq!(decoded, stored); - } - - #[test] - fn test_decode_invalid_payloads_empty() { - let result = decode_invalid_payloads(&[]); - assert!(matches!(result, Err(DecodeError::EmptyVersion))); - } - - #[test] - fn test_decode_invalid_payloads_unsupported_version() { - let mut bytes = vec![0xFE]; - bytes.extend_from_slice(&[1, 2, 3]); - let result = decode_invalid_payloads(&bytes); - assert!(matches!(result, Err(DecodeError::UnsupportedVersion(0xFE)))); - } -} diff --git a/crates/consensus-db/src/encoder.rs b/crates/consensus-db/src/encoder.rs deleted file mode 100644 index 20ac7c4..0000000 --- a/crates/consensus-db/src/encoder.rs +++ /dev/null @@ -1,416 +0,0 @@ -// Copyright 2025 Circle Internet Group, Inc. All rights reserved. -// -// SPDX-License-Identifier: Apache-2.0 -// -// Licensed under the Apache License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. -// You may obtain a copy of the License at -// -// http://www.apache.org/licenses/LICENSE-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// See the License for the specific language governing permissions and -// limitations under the License. - -use alloy_rpc_types_engine::ExecutionPayloadV3; -use bytes::Bytes; -use malachitebft_proto::Protobuf; -use prost::Message; -use ssz::Encode; -use std::time::UNIX_EPOCH; - -use arc_consensus_types::block::{block_as_ssz_data, ConsensusBlock}; -use arc_consensus_types::codec::proto as proto_funcs; -use arc_consensus_types::evidence::StoredMisbehaviorEvidence; -use arc_consensus_types::proposal_monitor::ProposalMonitor; -use arc_consensus_types::{proto, ProposalParts, StoredCommitCertificate}; - -use crate::invalid_payloads::StoredInvalidPayloads; -use crate::versions::{ - CommitCertificateVersion, ConsensusBlockVersion, ExecutionPayloadVersion, - InvalidPayloadsVersion, MisbehaviorEvidenceVersion, ProposalMonitorDataVersion, - ProposalPartsVersion, -}; - -// All encode_* functions prepend a 1-byte version tag to the serialized payload. -// The `1 + len` capacity calculation cannot overflow because any valid allocation -// already fits in usize and adding 1 byte stays within bounds. -pub fn encode_execution_payload(payload: &ExecutionPayloadV3) -> Vec { - let ssz_bytes = payload.as_ssz_bytes(); - #[allow(clippy::arithmetic_side_effects)] - let mut bytes = Vec::with_capacity(1 + ssz_bytes.len()); - bytes.push(ExecutionPayloadVersion::V3 as u8); - bytes.extend_from_slice(&ssz_bytes); - bytes -} - -pub fn encode_proposal_parts(parts: &ProposalParts) -> Result, malachitebft_proto::Error> { - let proto = proto_funcs::encode_proposal_parts(parts)?; - let proto_bytes = proto.encode_to_vec(); - // version byte + encoded protobuf - #[allow(clippy::arithmetic_side_effects)] - let mut bytes = Vec::with_capacity(1 + proto_bytes.len()); - bytes.push(ProposalPartsVersion::V1 as u8); - bytes.extend_from_slice(&proto_bytes); - Ok(bytes) -} - -/// Encodes a commit certificate into its byte representation -pub fn encode_certificate( - certificate: &StoredCommitCertificate, -) -> Result, malachitebft_proto::Error> { - let proto = proto_funcs::encode_store_commit_certificate(certificate)?; - let proto_bytes = proto.encode_to_vec(); - // version byte + encoded protobuf - #[allow(clippy::arithmetic_side_effects)] - let mut bytes = Vec::with_capacity(1 + proto_bytes.len()); - bytes.push(CommitCertificateVersion::V1 as u8); - bytes.extend_from_slice(&proto_bytes); - Ok(bytes) -} - -/// Encodes a block into its byte representation. -pub fn encode_block(block: &ConsensusBlock) -> Bytes { - let data = block_as_ssz_data(block); - let ssz_bytes = data.as_ssz_bytes(); - // version byte + encoded SSZ - #[allow(clippy::arithmetic_side_effects)] - let mut bytes = Vec::with_capacity(1 + ssz_bytes.len()); - bytes.push(ConsensusBlockVersion::V1 as u8); - bytes.extend_from_slice(&ssz_bytes); - Bytes::from(bytes) -} - -/// Encodes proposal monitor data into its byte representation. -pub fn encode_proposal_monitor_data( - data: &ProposalMonitor, -) -> Result, malachitebft_proto::Error> { - // SystemTime is converted to milliseconds since UNIX_EPOCH. - // u64 millis covers ~584 million years from epoch — truncation is unreachable. - let start_time_ms = { - let duration = data - .start_time - .duration_since(UNIX_EPOCH) - .unwrap_or_default(); - #[allow(clippy::cast_possible_truncation)] - let ms = duration.as_millis() as u64; - ms - }; - - let receive_time_ms = data - .proposal_receive_time - .map(|t| { - let duration = t.duration_since(UNIX_EPOCH).unwrap_or_default(); - #[allow(clippy::cast_possible_truncation)] - let ms = duration.as_millis() as u64; - ms - }) - .unwrap_or(0); - - // Convert Option to bytes (empty = not present) - let value_id_bytes = data - .value_id - .map(|v| Bytes::from(v.block_hash().to_vec())) - .unwrap_or_default(); - - let successful_state = data.successful.as_u32(); - - let proto_data = proto::ProtoProposalMonitorData { - height: data.height.as_u64(), - proposer: Some(data.proposer.to_proto()?), - start_time_ms, - receive_time_ms, - value_id: value_id_bytes, - successful_state, - synced: data.synced, - }; - - let proto_bytes = proto_data.encode_to_vec(); - // version byte + encoded protobuf - #[allow(clippy::arithmetic_side_effects)] - let mut bytes = Vec::with_capacity(1 + proto_bytes.len()); - bytes.push(ProposalMonitorDataVersion::V1 as u8); - bytes.extend_from_slice(&proto_bytes); - Ok(bytes) -} - -/// Encodes misbehavior evidence into its byte representation. -pub fn encode_misbehavior_evidence( - evidence: &StoredMisbehaviorEvidence, -) -> Result, malachitebft_proto::Error> { - let validators = evidence - .validators - .iter() - .map(|v| { - let double_votes = v - .double_votes - .iter() - .map(|dv| { - Ok(proto::ProtoDoubleVote { - first: Some(proto_funcs::encode_vote(&dv.first)?), - second: Some(proto_funcs::encode_vote(&dv.second)?), - }) - }) - .collect::, malachitebft_proto::Error>>()?; - - let double_proposals = v - .double_proposals - .iter() - .map(|dp| { - Ok(proto::ProtoDoubleProposal { - first: Some(proto_funcs::encode_signed_proposal(&dp.first)?), - second: Some(proto_funcs::encode_signed_proposal(&dp.second)?), - }) - }) - .collect::, malachitebft_proto::Error>>()?; - - Ok(proto::ProtoValidatorEvidence { - address: Some(v.address.to_proto()?), - double_votes, - double_proposals, - }) - }) - .collect::, malachitebft_proto::Error>>()?; - - let proto_evidence = proto::ProtoMisbehaviorEvidence { - height: evidence.height.as_u64(), - validators, - }; - - let proto_bytes = proto_evidence.encode_to_vec(); - // version byte + encoded protobuf - #[allow(clippy::arithmetic_side_effects)] - let mut bytes = Vec::with_capacity(1 + proto_bytes.len()); - bytes.push(MisbehaviorEvidenceVersion::V1 as u8); - bytes.extend_from_slice(&proto_bytes); - Ok(bytes) -} - -/// Encodes invalid payloads into their byte representation. -pub fn encode_invalid_payloads( - stored: &StoredInvalidPayloads, -) -> Result, malachitebft_proto::Error> { - let payloads = stored - .payloads - .iter() - .map(|p| { - Ok(proto::ProtoInvalidPayload { - height: p.height.as_u64(), - round: p.round.as_u32().ok_or_else(|| { - malachitebft_proto::Error::Other(format!( - "stored invalid payload {p} is missing the round", - )) - })?, - proposer_address: Some(p.proposer_address.to_proto()?), - payload: p.payload.as_ref().map(|pl| pl.as_ssz_bytes().into()), - reason: p.reason.clone(), - }) - }) - .collect::, malachitebft_proto::Error>>()?; - - let proto_payloads = proto::ProtoInvalidPayloads { - height: stored.height.as_u64(), - payloads, - }; - - let proto_bytes = proto_payloads.encode_to_vec(); - // version byte + encoded protobuf - #[allow(clippy::arithmetic_side_effects)] - let mut bytes = Vec::with_capacity(1 + proto_bytes.len()); - bytes.push(InvalidPayloadsVersion::V1 as u8); - bytes.extend_from_slice(&proto_bytes); - Ok(bytes) -} - -#[cfg(test)] -mod tests { - use super::*; - use crate::decoder; - use arbitrary::Unstructured; - use arc_consensus_types::{ - signing::Signature, Address, Height, ProposalData, ProposalFin, ProposalInit, ProposalPart, - ValueId, - }; - use arc_consensus_types::{ArcContext, CommitCertificate, CommitCertificateType}; - use malachitebft_app_channel::app::types::core::{CommitSignature, Round, Validity}; - - fn create_test_execution_payload() -> ExecutionPayloadV3 { - // Use arbitrary to generate a valid ExecutionPayloadV3 - Unstructured::new(&[0xab; 1024]) - .arbitrary::() - .unwrap() - } - - fn create_test_proposal_parts() -> ProposalParts { - let signature = Signature::from_bytes([0u8; 64]); - - let parts = vec![ - ProposalPart::Init(ProposalInit::new( - Height::new(1), - Round::new(0), - Round::Nil, - Address::new([0u8; 20]), - )), - ProposalPart::Data(ProposalData::new(Bytes::from_static(b"test data"))), - ProposalPart::Fin(ProposalFin::new(signature)), - ]; - - ProposalParts::new(parts).unwrap() - } - - fn create_test_commit_certificate() -> CommitCertificate { - let signature = Signature::from_bytes([0u8; 64]); - let address = Address::new([0u8; 20]); - let commit_sig = CommitSignature::new(address, signature); - - // Get a valid block hash from a test payload - let payload = create_test_execution_payload(); - let block_hash = payload.payload_inner.payload_inner.block_hash; - - CommitCertificate { - height: Height::new(1), - round: Round::new(0), - value_id: ValueId::new(block_hash), - commit_signatures: vec![commit_sig], - } - } - - fn create_test_consensus_block() -> ConsensusBlock { - let signature = Signature::from_bytes([0u8; 64]); - - ConsensusBlock { - height: Height::new(1), - round: Round::new(0), - valid_round: Round::Nil, - proposer: Address::new([0u8; 20]), - validity: Validity::Valid, - execution_payload: create_test_execution_payload(), - signature: Some(signature), - } - } - - #[test] - fn test_encode_execution_payload() { - let payload = create_test_execution_payload(); - let encoded = encode_execution_payload(&payload); - - // Check that version byte is correct - assert_eq!(encoded[0], ExecutionPayloadVersion::V3 as u8); - - // Check that decoding works without error - let decoded = decoder::decode_execution_payload(&encoded).expect("should decode"); - assert_eq!(decoded, payload); - } - - #[test] - fn test_encode_proposal_parts() { - let parts = create_test_proposal_parts(); - let encoded = encode_proposal_parts(&parts).expect("should encode"); - - // Check that version byte is correct - assert_eq!(encoded[0], ProposalPartsVersion::V1 as u8); - - // Check that decoding works without error - let decoded = decoder::decode_proposal_parts(&encoded).expect("should decode"); - assert_eq!(decoded, parts); - } - - #[test] - fn test_encode_certificate() { - let cert = create_test_commit_certificate(); - - for tpe in [ - CommitCertificateType::Unknown, - CommitCertificateType::Minimal, - CommitCertificateType::Extended, - ] { - let stored = StoredCommitCertificate { - certificate: cert.clone(), - certificate_type: tpe, - proposer: Some(Address::new([0u8; 20])), - }; - let encoded = encode_certificate(&stored).expect("should encode"); - - // Check that version byte is correct - assert_eq!(encoded[0], CommitCertificateVersion::V1 as u8); - - // Check that decoding works without error - let decoded = decoder::decode_certificate(&encoded).expect("should decode"); - - assert_eq!(decoded, stored); - } - } - - #[test] - fn test_encode_block() { - let block = create_test_consensus_block(); - let encoded = encode_block(&block); - - // Check that version byte is correct - assert_eq!(encoded[0], ConsensusBlockVersion::V1 as u8); - - // Check that decoding works without error - let decoded = decoder::decode_block(&encoded).expect("should decode"); - assert_eq!(decoded, block); - } - - #[test] - fn test_encode_invalid_payloads() { - use crate::invalid_payloads::InvalidPayload; - - let payload = create_test_execution_payload(); - let stored = StoredInvalidPayloads { - height: Height::new(5), - payloads: vec![InvalidPayload { - height: Height::new(5), - round: Round::new(0), - proposer_address: Address::new([1u8; 20]), - payload: Some(payload), - reason: "bad block".to_string(), - }], - }; - - let encoded = encode_invalid_payloads(&stored).expect("should encode"); - - assert_eq!(encoded[0], InvalidPayloadsVersion::V1 as u8,); - - let decoded = decoder::decode_invalid_payloads(&encoded).expect("should decode"); - assert_eq!(decoded, stored); - } - - #[test] - fn test_encode_invalid_payloads_without_payload() { - use crate::invalid_payloads::InvalidPayload; - - let stored = StoredInvalidPayloads { - height: Height::new(7), - payloads: vec![InvalidPayload { - height: Height::new(7), - round: Round::new(1), - proposer_address: Address::new([2u8; 20]), - payload: None, - reason: "engine error".to_string(), - }], - }; - - let encoded = encode_invalid_payloads(&stored).expect("should encode"); - - let decoded = decoder::decode_invalid_payloads(&encoded).expect("should decode"); - assert_eq!(decoded, stored); - } - - #[test] - fn test_encode_invalid_payloads_empty() { - let stored = StoredInvalidPayloads { - height: Height::new(1), - payloads: vec![], - }; - - let encoded = encode_invalid_payloads(&stored).expect("should encode"); - - let decoded = decoder::decode_invalid_payloads(&encoded).expect("should decode"); - assert_eq!(decoded, stored); - } -} diff --git a/crates/consensus-db/src/invalid_payloads.rs b/crates/consensus-db/src/invalid_payloads.rs deleted file mode 100644 index 66ca73d..0000000 --- a/crates/consensus-db/src/invalid_payloads.rs +++ /dev/null @@ -1,128 +0,0 @@ -// Copyright 2026 Circle Internet Group, Inc. All rights reserved. -// -// SPDX-License-Identifier: Apache-2.0 -// -// Licensed under the Apache License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. -// You may obtain a copy of the License at -// -// http://www.apache.org/licenses/LICENSE-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// See the License for the specific language governing permissions and -// limitations under the License. - -//! Types for storing and representing invalid proposals. - -use std::fmt; - -use alloy_rpc_types_engine::ExecutionPayloadV3; -use arc_consensus_types::{Address, Height, ProposalParts, Round}; - -use arc_consensus_types::block::ConsensusBlock; - -/// Invalid payloads collected during a height. -/// Stored in the database. -#[derive(Clone, Debug, PartialEq, Eq)] -pub struct StoredInvalidPayloads { - pub height: Height, - pub payloads: Vec, -} - -impl StoredInvalidPayloads { - /// Create an empty invalid-payloads struct for a given height. - pub fn empty(height: Height) -> Self { - Self { - height, - payloads: Vec::new(), - } - } - - /// Appends an invalid payload to this collection. - pub fn add_invalid_payload(&mut self, payload: InvalidPayload) { - self.payloads.push(payload); - } -} - -/// An invalid payload that was submitted to the network. -/// -/// An invalid payload is a payload that didn't pass the Engine API validation via -/// a call to `engine.newPayload`. -#[derive(Clone, Debug, PartialEq, Eq)] -pub struct InvalidPayload { - pub height: Height, - pub round: Round, - pub proposer_address: Address, - pub payload: Option, - pub reason: String, -} - -impl InvalidPayload { - /// Creates an invalid payload record from an assembled block, including a copy - /// of the execution payload. - /// - /// Use this when the full block is available (e.g. after successful assembly - /// but failed Engine API validation). - pub fn new_from_block(block: &ConsensusBlock, with_reason: &str) -> Self { - Self { - height: block.height, - round: block.round, - proposer_address: block.proposer, - payload: Some(block.execution_payload.clone()), - reason: with_reason.to_string(), - } - } - - /// Creates an invalid payload record from raw proposal parts, without an - /// execution payload. - /// - /// Use this when the block could not be assembled from its parts (e.g. SSZ - /// decoding failure), so no execution payload is available. - pub fn new_from_parts(parts: &ProposalParts, with_reason: &str) -> Self { - Self { - height: parts.height(), - round: parts.round(), - proposer_address: parts.proposer(), - payload: None, - reason: with_reason.to_string(), - } - } - - /// Creates an invalid payload record from individual fields, without an - /// execution payload. - /// - /// Use this when neither a [`ConsensusBlock`] nor [`ProposalParts`] is - /// available (e.g. when raw bytes failed SSZ decoding before a block could - /// be assembled). - pub fn new_without_payload( - height: Height, - round: Round, - proposer_address: Address, - reason: &str, - ) -> Self { - Self { - height, - round, - proposer_address, - payload: None, - reason: reason.to_string(), - } - } -} - -impl fmt::Display for InvalidPayload { - fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { - let block_hash: &dyn fmt::Display = match &self.payload { - Some(p) => &p.payload_inner.payload_inner.block_hash, - None => &"", - }; - - write!( - f, - "{{ height: {}, round: {}, proposer_address: {}, block_hash: {}, reason: {} }}", - self.height, self.round, self.proposer_address, block_hash, self.reason, - ) - } -} diff --git a/crates/consensus-db/src/keys.rs b/crates/consensus-db/src/keys.rs deleted file mode 100644 index d2b9368..0000000 --- a/crates/consensus-db/src/keys.rs +++ /dev/null @@ -1,143 +0,0 @@ -// Copyright 2025 Circle Internet Group, Inc. All rights reserved. -// -// SPDX-License-Identifier: Apache-2.0 -// -// Licensed under the Apache License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. -// You may obtain a copy of the License at -// -// http://www.apache.org/licenses/LICENSE-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// See the License for the specific language governing permissions and -// limitations under the License. - -use core::mem::size_of; - -use arc_consensus_types::{BlockHash, Height}; -use malachitebft_app_channel::app::types::core::Round; - -/// Key for pending proposal parts. -pub type PendingPartsKey = (HeightKey, RoundKey, BlockHashKey); - -/// Key for undecided blocks. -pub type UndecidedBlockKey = (HeightKey, RoundKey, BlockHashKey); - -/// Key representing a height. -#[derive(Copy, Clone, Debug)] -pub struct HeightKey; - -impl redb::Value for HeightKey { - type SelfType<'a> = Height; - type AsBytes<'a> = [u8; size_of::()]; - - fn fixed_width() -> Option { - Some(size_of::()) - } - - fn from_bytes<'a>(data: &'a [u8]) -> Self::SelfType<'a> - where - Self: 'a, - { - let height = ::from_bytes(data); - - Height::new(height) - } - - fn as_bytes<'a, 'b: 'a>(value: &'a Self::SelfType<'b>) -> Self::AsBytes<'a> - where - Self: 'a, - Self: 'b, - { - ::as_bytes(&value.as_u64()) - } - - fn type_name() -> redb::TypeName { - redb::TypeName::new("Height") - } -} - -impl redb::Key for HeightKey { - fn compare(data1: &[u8], data2: &[u8]) -> std::cmp::Ordering { - ::compare(data1, data2) - } -} - -/// Key representing a round. -#[derive(Copy, Clone, Debug)] -pub struct RoundKey; - -impl redb::Value for RoundKey { - type SelfType<'a> = Round; - type AsBytes<'a> = [u8; size_of::()]; - - fn fixed_width() -> Option { - Some(size_of::()) - } - - fn from_bytes<'a>(data: &'a [u8]) -> Self::SelfType<'a> - where - Self: 'a, - { - let round = ::from_bytes(data); - Round::from(round) - } - - fn as_bytes<'a, 'b: 'a>(value: &'a Self::SelfType<'b>) -> Self::AsBytes<'a> - where - Self: 'a, - Self: 'b, - { - ::as_bytes(&value.as_i64()) - } - - fn type_name() -> redb::TypeName { - redb::TypeName::new("Round") - } -} - -impl redb::Key for RoundKey { - fn compare(data1: &[u8], data2: &[u8]) -> std::cmp::Ordering { - ::compare(data1, data2) - } -} - -#[derive(Copy, Clone, Debug)] -pub struct BlockHashKey; - -impl redb::Value for BlockHashKey { - type SelfType<'a> = BlockHash; - type AsBytes<'a> = [u8; BlockHash::len_bytes()]; - - fn fixed_width() -> Option { - Some(BlockHash::len_bytes()) - } - - fn from_bytes<'a>(data: &'a [u8]) -> Self::SelfType<'a> - where - Self: 'a, - { - let bytes = as redb::Value>::from_bytes(data); - BlockHash::new(bytes) - } - - fn as_bytes<'a, 'b: 'a>(value: &'a Self::SelfType<'b>) -> Self::AsBytes<'a> - where - Self: 'a, - Self: 'b, - { - *value.as_ref() - } - - fn type_name() -> redb::TypeName { - redb::TypeName::new("BlockHash") - } -} - -impl redb::Key for BlockHashKey { - fn compare(data1: &[u8], data2: &[u8]) -> std::cmp::Ordering { - as redb::Key>::compare(data1, data2) - } -} diff --git a/crates/consensus-db/src/lib.rs b/crates/consensus-db/src/lib.rs deleted file mode 100644 index bb12a61..0000000 --- a/crates/consensus-db/src/lib.rs +++ /dev/null @@ -1,38 +0,0 @@ -// Copyright 2025 Circle Internet Group, Inc. All rights reserved. -// -// SPDX-License-Identifier: Apache-2.0 -// -// Licensed under the Apache License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. -// You may obtain a copy of the License at -// -// http://www.apache.org/licenses/LICENSE-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// See the License for the specific language governing permissions and -// limitations under the License. - -#![allow(clippy::result_large_err)] - -mod decoder; -mod encoder; - -pub mod keys; -pub mod migrations; -pub mod repositories; -pub mod services; -pub mod versions; - -mod store; -pub use store::{ - rollback_to_height, DbUpgrade, RollbackReport, Store, StoreError, CERTIFICATES_TABLE, - DECIDED_BLOCKS_TABLE, INVALID_PAYLOADS_TABLE, MISBEHAVIOR_EVIDENCE_TABLE, - PENDING_PROPOSAL_PARTS_TABLE, PROPOSAL_MONITOR_DATA_TABLE, ROLLBACK_BATCH_SIZE, - UNDECIDED_BLOCKS_TABLE, -}; - -mod metrics; -pub use metrics::DbMetrics; -pub mod invalid_payloads; diff --git a/crates/consensus-db/src/metrics.rs b/crates/consensus-db/src/metrics.rs deleted file mode 100644 index ca399fb..0000000 --- a/crates/consensus-db/src/metrics.rs +++ /dev/null @@ -1,211 +0,0 @@ -// Copyright 2025 Circle Internet Group, Inc. All rights reserved. -// -// SPDX-License-Identifier: Apache-2.0 -// -// Licensed under the Apache License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. -// You may obtain a copy of the License at -// -// http://www.apache.org/licenses/LICENSE-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// See the License for the specific language governing permissions and -// limitations under the License. - -use std::ops::Deref; -use std::sync::Arc; -use std::time::Duration; - -use malachitebft_app_channel::app::metrics; - -use metrics::prometheus::metrics::counter::Counter; -use metrics::prometheus::metrics::gauge::Gauge; -use metrics::prometheus::metrics::histogram::{exponential_buckets, Histogram}; -use metrics::SharedRegistry; - -/// Metrics for the database. -#[derive(Clone, Debug)] -pub struct DbMetrics(Arc); - -impl Deref for DbMetrics { - type Target = Inner; - - fn deref(&self) -> &Self::Target { - &self.0 - } -} - -/// Inner struct for database metrics. -#[derive(Debug)] -pub struct Inner { - /// Size of the database database (bytes) - size: Gauge, - - /// Amount of data written to the database (bytes) - write_bytes: Counter, - - /// Amount of data read from the database (bytes) - read_bytes: Counter, - - /// Amount of key data read from the database (bytes) - key_read_bytes: Counter, - - /// Total number of reads from the database - read_count: Counter, - - /// Total number of writes to the database - write_count: Counter, - - /// Total number of deletions to the database - delete_count: Counter, - - /// Time taken to read from the database (seconds) - read_time: Histogram, - - /// Time taken to write to the database (seconds) - write_time: Histogram, - - /// Time taken to delete from the database (seconds) - delete_time: Histogram, -} - -impl Inner { - /// Create a new `Inner` struct. - pub fn new() -> Self { - Self { - size: Gauge::default(), - write_bytes: Counter::default(), - read_bytes: Counter::default(), - key_read_bytes: Counter::default(), - read_count: Counter::default(), - write_count: Counter::default(), - delete_count: Counter::default(), - read_time: Histogram::new(exponential_buckets(0.001, 2.0, 10)), // Start from 1ms - write_time: Histogram::new(exponential_buckets(0.001, 2.0, 10)), - delete_time: Histogram::new(exponential_buckets(0.001, 2.0, 10)), - } - } -} - -impl Default for Inner { - fn default() -> Self { - Self::new() - } -} - -impl DbMetrics { - /// Create a new `DbMetrics` struct. - pub fn new() -> Self { - Self(Arc::new(Inner::new())) - } - - /// Register the metrics with the given registry. - pub fn register(registry: &SharedRegistry) -> Self { - let metrics = Self::new(); - - registry.with_prefix("arc_malachite_app_db", |registry| { - registry.register("size", "Size of the database (bytes)", metrics.size.clone()); - - registry.register( - "write_bytes", - "Amount of data written to the database (bytes)", - metrics.write_bytes.clone(), - ); - - registry.register( - "read_bytes", - "Amount of data read from the database (bytes)", - metrics.read_bytes.clone(), - ); - - registry.register( - "key_read_bytes", - "Amount of key data read from the database (bytes)", - metrics.key_read_bytes.clone(), - ); - - registry.register( - "read_count", - "Total number of reads from the database", - metrics.read_count.clone(), - ); - - registry.register( - "write_count", - "Total number of writes to the database", - metrics.write_count.clone(), - ); - - registry.register( - "delete_count", - "Total number of deletions to the database", - metrics.delete_count.clone(), - ); - - registry.register( - "read_time", - "Time taken to read bytes from the database (seconds)", - metrics.read_time.clone(), - ); - - registry.register( - "write_time", - "Time taken to write bytes to the database (seconds)", - metrics.write_time.clone(), - ); - - registry.register( - "delete_time", - "Time taken to delete bytes from the database (seconds)", - metrics.delete_time.clone(), - ); - }); - - metrics - } - - /// Set the size of the database, in bytes. - pub fn set_db_size(&self, size: u64) { - self.size.set(size as i64); - } - - /// Add the number of bytes written to the database. - pub fn add_write_bytes(&self, bytes: u64) { - self.write_bytes.inc_by(bytes); - self.write_count.inc(); - } - - /// Add the number of bytes read from the database. - pub fn add_read_bytes(&self, bytes: u64) { - self.read_bytes.inc_by(bytes); - self.read_count.inc(); - } - - /// Add the number of bytes read from the database (key only). - pub fn add_key_read_bytes(&self, bytes: u64) { - self.key_read_bytes.inc_by(bytes); - } - - /// Observe the time taken to read from the database. - pub fn observe_read_time(&self, duration: Duration) { - self.read_time.observe(duration.as_secs_f64()); - } - - /// Observe the time taken to write to the database. - pub fn observe_write_time(&self, duration: Duration) { - self.write_time.observe(duration.as_secs_f64()); - } - - /// Observe the time taken to delete from the database. - pub fn observe_delete_time(&self, duration: Duration) { - self.delete_time.observe(duration.as_secs_f64()); - } -} - -impl Default for DbMetrics { - fn default() -> Self { - Self::new() - } -} diff --git a/crates/consensus-db/src/migrations.rs b/crates/consensus-db/src/migrations.rs deleted file mode 100644 index a813145..0000000 --- a/crates/consensus-db/src/migrations.rs +++ /dev/null @@ -1,1568 +0,0 @@ -// Copyright 2025 Circle Internet Group, Inc. All rights reserved. -// -// SPDX-License-Identifier: Apache-2.0 -// -// Licensed under the Apache License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. -// You may obtain a copy of the License at -// -// http://www.apache.org/licenses/LICENSE-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// See the License for the specific language governing permissions and -// limitations under the License. - -use std::time::{Duration, Instant}; - -use redb::{ReadableTable, TableDefinition}; -use tracing::{debug, info, warn}; - -use crate::versions::SchemaVersion; - -use crate::decoder::DecodeError; -use crate::store::{ - StoreError, CERTIFICATES_TABLE, DECIDED_BLOCKS_TABLE, PENDING_PROPOSAL_PARTS_TABLE, - UNDECIDED_BLOCKS_TABLE, -}; -use crate::versions::DB_SCHEMA_VERSION; - -mod migrators; -use migrators::MigratorSet; - -/// Metadata table to track database schema version -pub const METADATA_TABLE: TableDefinition<&str, SchemaVersion> = TableDefinition::new("_metadata"); - -const SCHEMA_VERSION_KEY: &str = "schema_version"; - -/// Migration result with statistics -#[derive(Debug, Default)] -pub struct MigrationStats { - pub tables_migrated: usize, - pub records_scanned: usize, - pub records_upgraded: usize, - pub records_skipped: usize, - pub duration: Duration, -} - -impl MigrationStats { - // Migration counters are bounded by total DB records — overflow is not reachable. - #[allow(clippy::arithmetic_side_effects)] - fn merge(&mut self, other: MigrationStats) { - self.tables_migrated += other.tables_migrated; - self.records_scanned += other.records_scanned; - self.records_upgraded += other.records_upgraded; - self.records_skipped += other.records_skipped; - } -} - -/// Trait for migrating a specific data type from one version to another -pub trait Migrator: Send + Sync { - /// The name of this migrator (for logging) - fn name(&self) -> &str; - - /// Source version - fn source_version(&self) -> SchemaVersion; - - /// Target version - fn target_version(&self) -> SchemaVersion; - - /// Migrate a single record from old format to new format - fn migrate(&self, old_bytes: &[u8]) -> Result, StoreError>; - - /// Check if this record needs migration - fn needs_migration(&self, bytes: &[u8]) -> bool { - bytes - .first() - .map(|&v| v == self.source_version().as_u8()) - .unwrap_or(false) - } -} - -/// Migration coordinator -pub struct MigrationCoordinator { - db: redb::Database, -} - -impl MigrationCoordinator { - const BATCH_SIZE: usize = 1000; - - pub fn new(db: redb::Database) -> Self { - Self { db } - } - - pub fn into_db(self) -> redb::Database { - self.db - } - - /// Check current schema version - pub fn current_schema_version(&self) -> Result, StoreError> { - let tx = self.db.begin_read()?; - let table = tx.open_table(METADATA_TABLE)?; - Ok(table.get(SCHEMA_VERSION_KEY)?.map(|v| v.value())) - } - - /// Set schema version - fn set_schema_version(&self, version: SchemaVersion) -> Result<(), StoreError> { - let tx = self.db.begin_write()?; - { - let mut table = tx.open_table(METADATA_TABLE)?; - table.insert(SCHEMA_VERSION_KEY, version)?; - } - tx.commit()?; - Ok(()) - } - - /// Check if migration is needed - /// - /// Arguments: - /// - db_exists: true if the database file exists, false if it doesn't - /// - /// Returns: - /// - true if migration is needed, false if not - /// - Err if the database version is unsupported - pub fn needs_migration(&self, db_exists: bool) -> Result { - self.ensure_metadata_table_exists()?; - - let current = self.current_schema_version()?; - let target = DB_SCHEMA_VERSION; - - match current { - None if db_exists => { - info!("Database has no schema version, setting to v0"); - self.set_schema_version(SchemaVersion::V0)?; - Ok(true) - } - None => { - info!("New database created, setting to current version {target}"); - self.set_schema_version(target)?; - Ok(false) - } - Some(v) if v < target => { - info!("Database schema {v} is outdated, current is {target}"); - Ok(true) - } - Some(v) if v > target => { - warn!("Database schema {v} is newer than supported {target}"); - Err(StoreError::Decode(DecodeError::UnsupportedVersion( - v.as_u8(), - ))) - } - Some(_) => { - debug!("Database schema is up to date"); - Ok(false) - } - } - } - - /// Perform full migration - /// - /// Panics if the database schema version is not set. - pub fn migrate(&self) -> Result { - let start = Instant::now(); - let mut stats = MigrationStats::default(); - - let from_version = self - .current_schema_version()? - .expect("Database schema version should be set"); - let to_version = DB_SCHEMA_VERSION; - - info!( - from = %from_version, - to = %to_version, - "Starting database migration" - ); - - // Build migration chain - let migration_chain = self.build_migration_chain(from_version, to_version)?; - - if migration_chain.is_empty() { - info!("No migrations needed"); - return Ok(stats); - } - - // Migrate each table with appropriate strategy - for (schema_version, migrators) in migration_chain { - info!("Migrating to schema version {}", schema_version); - - stats.merge(self.migrate_certificates(migrators.certificate.as_ref(), false)?); - stats.merge(self.migrate_decided_blocks(migrators.decided_block.as_ref(), false)?); - stats.merge(self.migrate_undecided_blocks(migrators.undecided_block.as_ref(), false)?); - stats.merge(self.migrate_pending_parts(migrators.pending_parts.as_ref(), false)?); - - // Update schema version after successful migration - self.set_schema_version(schema_version)?; - info!("Successfully migrated to schema version {}", schema_version); - } - - stats.duration = start.elapsed(); - - Ok(stats) - } - - /// Scan what [`Self::migrate`] would apply without persisting: schema steps, per-table - /// migrator names, and record counts. Uses the same code path as migration (including - /// write transactions) but aborts each transaction instead of committing. - pub fn preview_migrate(&self) -> Result { - let start = Instant::now(); - let mut stats = MigrationStats::default(); - - let from_version = self.current_schema_version()?.ok_or_else(|| { - StoreError::Migration( - "database schema version is not set; cannot preview migration".to_owned(), - ) - })?; - let to_version = DB_SCHEMA_VERSION; - - info!( - from = %from_version, - to = %to_version, - "Dry-run: scanning pending database migrations (no commits)" - ); - - let migration_chain = self.build_migration_chain(from_version, to_version)?; - - if migration_chain.is_empty() { - info!("Dry-run: no schema steps in migration chain"); - stats.duration = start.elapsed(); - return Ok(stats); - } - - for (schema_version, migrators) in &migration_chain { - info!(schema_version = %schema_version, "Would migrate to schema version (dry-run)"); - info!( - certificates = migrators.certificate.name(), - decided_blocks = migrators.decided_block.name(), - undecided_blocks = migrators.undecided_block.name(), - pending_parts = migrators.pending_parts.name(), - "Pending migrators for this step" - ); - - stats.merge(self.migrate_certificates(migrators.certificate.as_ref(), true)?); - stats.merge(self.migrate_decided_blocks(migrators.decided_block.as_ref(), true)?); - stats.merge(self.migrate_undecided_blocks(migrators.undecided_block.as_ref(), true)?); - stats.merge(self.migrate_pending_parts(migrators.pending_parts.as_ref(), true)?); - } - - stats.duration = start.elapsed(); - - Ok(stats) - } - - /// Ensure the metadata table exists by creating it if necessary - fn ensure_metadata_table_exists(&self) -> Result<(), StoreError> { - let tx = self.db.begin_write()?; - { - let _ = tx.open_table(METADATA_TABLE)?; - } - tx.commit()?; - Ok(()) - } - - /// Build chain of migrations from source to target version - fn build_migration_chain( - &self, - from: SchemaVersion, - to: SchemaVersion, - ) -> Result, StoreError> { - let mut chain = Vec::new(); - - for version in from.next().as_u8()..=to.as_u8() { - let version = SchemaVersion::new(version); - let migrators = self.get_migrators_for_version(version)?; - chain.push((version, migrators)); - } - - Ok(chain) - } - - /// Get migrators for a specific target version - fn get_migrators_for_version(&self, version: SchemaVersion) -> Result { - match version { - SchemaVersion::V1 => Ok(MigratorSet::v1()), - // Future versions: - // Version::V2 => Ok(MigratorSet::v2()), - v => Err(StoreError::Decode(DecodeError::UnsupportedVersion( - v.as_u8(), - ))), - } - } - - /// Migrate certificates table. - /// - /// When `dry_run` is true, uses the same write transaction and iteration path as a real - /// migration but aborts each batch without persisting changes. - #[allow(clippy::arithmetic_side_effects)] // counter arithmetic bounded by DB record counts - fn migrate_certificates( - &self, - migrator: &dyn Migrator, - dry_run: bool, - ) -> Result { - info!(dry_run, "Processing certificates table"); - - let mut stats = MigrationStats::default(); - - // start from the min height - let mut next_height = if let Some((min_height, _)) = self - .db - .begin_read()? - .open_table(CERTIFICATES_TABLE)? - .first()? - { - min_height.value() - } else { - stats.tables_migrated += 1; - return Ok(stats); - }; - - loop { - let tx = self.db.begin_write()?; - let mut records_scanned = 0; - let records_upgraded; - { - let mut table = tx.open_table(CERTIFICATES_TABLE)?; - - // collect records to migrate in this batch - let mut batch = Vec::with_capacity(Self::BATCH_SIZE); - // iterate over BATCH_SIZE records - for entry in table.range(next_height..)?.take(Self::BATCH_SIZE) { - let (key, value) = entry?; - let bytes = value.value(); - records_scanned += 1; - - if migrator.needs_migration(&bytes[..]) { - let new_bytes = migrator.migrate(&bytes[..]).map_err(|e| { - StoreError::Migration(format!( - "Failed to migrate certificate at height {}: {}", - key.value(), - e - )) - })?; - - batch.push((key.value(), new_bytes)); - } - - next_height = key.value().increment(); - } - - records_upgraded = batch.len(); - if !dry_run { - for (key, value) in batch { - table.insert(key, value)?; - } - } - } - if dry_run { - tx.abort()?; - } else { - tx.commit()?; - } - debug!("{} records in batch", records_upgraded); - - stats.records_scanned += records_scanned; - stats.records_upgraded += records_upgraded; - - // if we scanned less than the batch size, we're done - if records_scanned < Self::BATCH_SIZE { - break; - } - } - - stats.records_skipped = stats.records_scanned - stats.records_upgraded; - stats.tables_migrated += 1; - - info!( - dry_run, - scanned = stats.records_scanned, - upgraded = stats.records_upgraded, - "Finished certificates table" - ); - - Ok(stats) - } - - /// Migrate decided blocks table. - /// - /// When `dry_run` is true, uses write transactions but aborts each batch without persisting. - #[allow(clippy::arithmetic_side_effects)] // counter arithmetic bounded by DB record counts - fn migrate_decided_blocks( - &self, - migrator: &dyn Migrator, - dry_run: bool, - ) -> Result { - info!(dry_run, "Processing decided blocks table"); - - let mut stats = MigrationStats::default(); - - // start from the min height - let mut next_height = if let Some((min_height, _)) = self - .db - .begin_read()? - .open_table(DECIDED_BLOCKS_TABLE)? - .first()? - { - min_height.value() - } else { - stats.tables_migrated += 1; - return Ok(stats); - }; - - loop { - let tx = self.db.begin_write()?; - let mut records_scanned = 0; - let records_upgraded; - { - let mut table = tx.open_table(DECIDED_BLOCKS_TABLE)?; - - // collect records to migrate in this batch - let mut batch = Vec::with_capacity(Self::BATCH_SIZE); - // iterate over BATCH_SIZE records - for entry in table.range(next_height..)?.take(Self::BATCH_SIZE) { - let (key, value) = entry?; - let bytes = value.value(); - records_scanned += 1; - - if migrator.needs_migration(&bytes[..]) { - let new_bytes = migrator.migrate(&bytes[..]).map_err(|e| { - StoreError::Migration(format!( - "Failed to migrate decided block at height {}: {}", - key.value(), - e - )) - })?; - - batch.push((key.value(), new_bytes)); - } - - next_height = key.value().increment(); - } - - records_upgraded = batch.len(); - if !dry_run { - for (key, value) in batch { - table.insert(key, value)?; - } - } - } - if dry_run { - tx.abort()?; - } else { - tx.commit()?; - } - debug!("{} records in batch", records_upgraded); - - stats.records_scanned += records_scanned; - stats.records_upgraded += records_upgraded; - - // if we scanned less than the batch size, we're done - if records_scanned < Self::BATCH_SIZE { - break; - } - } - - stats.records_skipped = stats.records_scanned - stats.records_upgraded; - stats.tables_migrated += 1; - - info!( - dry_run, - scanned = stats.records_scanned, - upgraded = stats.records_upgraded, - "Finished decided blocks table" - ); - - Ok(stats) - } - - /// Migrate undecided blocks table. - /// - /// When `dry_run` is true, opens a write transaction (creating the table if needed) but - /// aborts without applying updates. - #[allow(clippy::arithmetic_side_effects)] // counter arithmetic bounded by DB record counts - fn migrate_undecided_blocks( - &self, - migrator: &dyn Migrator, - dry_run: bool, - ) -> Result { - info!(dry_run, "Processing undecided blocks table"); - - let mut stats = MigrationStats::default(); - - let tx = self.db.begin_write()?; - { - let mut table = tx.open_table(UNDECIDED_BLOCKS_TABLE)?; - - // Collect all records to migrate - // This table is typically small (temporary blocks), so we can process it all at once - let mut to_migrate = Vec::new(); - for entry in table.iter()? { - let (key, value) = entry?; - let bytes = value.value(); - stats.records_scanned += 1; - - if migrator.needs_migration(&bytes[..]) { - let new_bytes = migrator.migrate(&bytes[..]).map_err(|e| { - StoreError::Migration(format!( - "Failed to migrate undecided block at {:?}: {}", - key.value(), - e - )) - })?; - - to_migrate.push((key.value(), new_bytes)); - } - } - - stats.records_upgraded = to_migrate.len(); - if !dry_run { - for (key, value) in to_migrate { - table.insert(key, value)?; - } - } - } - if dry_run { - tx.abort()?; - } else { - tx.commit()?; - } - - stats.records_skipped = stats.records_scanned - stats.records_upgraded; - stats.tables_migrated += 1; - - info!( - dry_run, - scanned = stats.records_scanned, - upgraded = stats.records_upgraded, - "Finished undecided blocks table" - ); - - Ok(stats) - } - - /// Migrate pending proposal parts table. - /// - /// When `dry_run` is true, opens a write transaction (creating the table if needed) but - /// aborts without applying updates. - #[allow(clippy::arithmetic_side_effects)] // counter arithmetic bounded by DB record counts - fn migrate_pending_parts( - &self, - migrator: &dyn Migrator, - dry_run: bool, - ) -> Result { - info!(dry_run, "Processing pending proposal parts table"); - - let mut stats = MigrationStats::default(); - - let tx = self.db.begin_write()?; - { - let mut table = tx.open_table(PENDING_PROPOSAL_PARTS_TABLE)?; - - // Collect all records to migrate - // This table is typically small (temporary data), so we can process it all at once - let mut to_migrate = Vec::new(); - for entry in table.iter()? { - let (key, value) = entry?; - let bytes = value.value(); - stats.records_scanned += 1; - - if migrator.needs_migration(&bytes[..]) { - let new_bytes = migrator.migrate(&bytes[..]).map_err(|e| { - StoreError::Migration(format!( - "Failed to migrate pending parts at {:?}: {}", - key.value(), - e - )) - })?; - - to_migrate.push((key.value(), new_bytes)); - } - } - - stats.records_upgraded = to_migrate.len(); - if !dry_run { - for (key, value) in to_migrate { - table.insert(key, value)?; - } - } - } - if dry_run { - tx.abort()?; - } else { - tx.commit()?; - } - - stats.records_skipped = stats.records_scanned - stats.records_upgraded; - stats.tables_migrated += 1; - - info!( - dry_run, - scanned = stats.records_scanned, - upgraded = stats.records_upgraded, - "Finished pending proposal parts table" - ); - - Ok(stats) - } -} - -#[cfg(test)] -mod tests { - use super::*; - use arc_consensus_types::{BlockHash, Height}; - use malachitebft_app_channel::app::types::core::Round; - use tempfile::tempdir; - - /// Helper to create a test database - fn create_test_db() -> (redb::Database, std::path::PathBuf) { - let dir = tempdir().unwrap(); - let db_path = dir.path().join("test.db"); - let db = redb::Database::builder() - .create(&db_path) - .expect("Failed to create test database"); - (db, db_path) - } - - /// Test migrator that adds a version byte if missing - struct TestMigrator { - from: SchemaVersion, - to: SchemaVersion, - } - - impl Migrator for TestMigrator { - fn name(&self) -> &str { - "TestMigrator" - } - - fn source_version(&self) -> SchemaVersion { - self.from - } - - fn target_version(&self) -> SchemaVersion { - self.to - } - - fn migrate(&self, old_bytes: &[u8]) -> Result, StoreError> { - if old_bytes.is_empty() { - return Err(StoreError::Decode(DecodeError::EmptyVersion)); - } - - // If already has target version, return as-is - if old_bytes[0] == self.to.as_u8() { - return Ok(old_bytes.to_vec()); - } - - // Otherwise, replace version byte - let mut result = Vec::with_capacity(old_bytes.len()); - result.push(self.to.as_u8()); - result.extend_from_slice(&old_bytes[1..]); - Ok(result) - } - - fn needs_migration(&self, bytes: &[u8]) -> bool { - bytes - .first() - .map(|&v| v == self.from.as_u8() || v == SchemaVersion::V0.as_u8()) - .unwrap_or(true) - } - } - - #[test] - fn test_metadata_table_operations() { - let (db, _path) = create_test_db(); - - // Create metadata table - let tx = db.begin_write().unwrap(); - let _ = tx.open_table(METADATA_TABLE).unwrap(); - tx.commit().unwrap(); - - let coordinator = MigrationCoordinator::new(db); - - // Initially no version - assert_eq!(coordinator.current_schema_version().unwrap(), None); - - // Set version - coordinator - .set_schema_version(SchemaVersion::new(1)) - .unwrap(); - assert_eq!( - coordinator.current_schema_version().unwrap(), - Some(SchemaVersion::new(1)) - ); - - // Update version - coordinator - .set_schema_version(SchemaVersion::new(2)) - .unwrap(); - assert_eq!( - coordinator.current_schema_version().unwrap(), - Some(SchemaVersion::new(2)) - ); - } - - #[test] - fn test_needs_migration_new_database() { - let (db, _path) = create_test_db(); - let coordinator = MigrationCoordinator::new(db); - - // New database should not need migration, it gets initialized with current version - // needs_migration() will create the metadata table if it doesn't exist - let needs = coordinator.needs_migration(false).unwrap(); - assert!( - !needs, - "New database should be initialized with current version" - ); - - // Version should be set to current - assert_eq!( - coordinator.current_schema_version().unwrap(), - Some(DB_SCHEMA_VERSION) - ); - } - - #[test] - fn test_needs_migration_outdated() { - let (db, _path) = create_test_db(); - let coordinator = MigrationCoordinator::new(db); - - // Set an old version - let old_version = DB_SCHEMA_VERSION.previous().expect("non-zero version"); - if old_version > SchemaVersion::V0 { - coordinator.set_schema_version(old_version).unwrap(); - - // Now check if migration is needed - let needs = coordinator.needs_migration(true).unwrap(); - assert!(needs, "Should need migration when version is outdated"); - } - } - - #[test] - fn test_needs_migration_newer_version() { - let (db, _path) = create_test_db(); - let coordinator = MigrationCoordinator::new(db); - - // Set a newer version - coordinator - .set_schema_version(DB_SCHEMA_VERSION.next()) - .unwrap(); - - // Should error on newer version - let result = coordinator.needs_migration(true); - assert!( - result.is_err(), - "Should error when database version is newer than supported" - ); - } - - #[test] - fn test_migration_stats() { - let mut stats1 = MigrationStats { - tables_migrated: 1, - records_scanned: 100, - records_upgraded: 50, - records_skipped: 50, - duration: Duration::from_secs(1), - }; - - let stats2 = MigrationStats { - tables_migrated: 2, - records_scanned: 200, - records_upgraded: 100, - records_skipped: 100, - duration: Duration::from_secs(2), - }; - - stats1.merge(stats2); - - assert_eq!(stats1.tables_migrated, 3); - assert_eq!(stats1.records_scanned, 300); - assert_eq!(stats1.records_upgraded, 150); - assert_eq!(stats1.records_skipped, 150); - } - - #[test] - fn test_migrator_needs_migration() { - let migrator = TestMigrator { - from: SchemaVersion::V0, - to: SchemaVersion::V1, - }; - - // No version byte -> needs migration - assert!(migrator.needs_migration(&[])); - - // Has from version -> needs migration - assert!(migrator.needs_migration(&[0, 1, 2, 3])); - - // Has target version -> doesn't need migration - assert!(!migrator.needs_migration(&[1, 1, 2, 3])); - - // Has different version -> doesn't need migration (not our job) - assert!(!migrator.needs_migration(&[2, 1, 2, 3])); - } - - #[test] - fn test_migrator_migrate() { - let migrator = TestMigrator { - from: SchemaVersion::V0, - to: SchemaVersion::V1, - }; - - // Empty bytes -> error - let result = migrator.migrate(&[]); - assert!(result.is_err()); - - // Migrate from version 0 - let data = vec![0, 10, 20, 30]; - let result = migrator.migrate(&data).unwrap(); - assert_eq!(result, vec![1, 10, 20, 30]); - - // Already at target version -> return as-is - let data = vec![1, 10, 20, 30]; - let result = migrator.migrate(&data).unwrap(); - assert_eq!(result, vec![1, 10, 20, 30]); - } - - #[test] - fn test_build_migration_chain() { - let (db, _path) = create_test_db(); - let coordinator = MigrationCoordinator::new(db); - - // Chain from 0 to current version - let chain = coordinator - .build_migration_chain(SchemaVersion::V0, DB_SCHEMA_VERSION) - .unwrap(); - assert_eq!(chain.len(), DB_SCHEMA_VERSION.as_u8() as usize); - - // Chain from current to current (no migrations) - let chain = coordinator - .build_migration_chain(DB_SCHEMA_VERSION, DB_SCHEMA_VERSION) - .unwrap(); - assert_eq!(chain.len(), 0); - } - - #[test] - fn test_preview_migrate_stats_match_migrate() { - let (db, _path) = create_test_db(); - let tx = db.begin_write().unwrap(); - { - let mut meta = tx.open_table(METADATA_TABLE).unwrap(); - meta.insert("schema_version", SchemaVersion::V0).unwrap(); - // Use V0 version byte (0x00) so records actually need migration. - let mut cert = tx.open_table(CERTIFICATES_TABLE).unwrap(); - cert.insert(Height::new(1), vec![0u8, 1, 2, 3]).unwrap(); - let mut dec = tx.open_table(DECIDED_BLOCKS_TABLE).unwrap(); - dec.insert(Height::new(1), vec![0u8, 4, 5, 6]).unwrap(); - } - tx.commit().unwrap(); - - let coordinator = MigrationCoordinator::new(db); - let preview = coordinator.preview_migrate().unwrap(); - - // Dry-run must not commit: schema version and record data must be unchanged. - assert_eq!( - coordinator.current_schema_version().unwrap(), - Some(SchemaVersion::V0), - "preview_migrate must not alter schema version" - ); - let tx = coordinator.db.begin_read().unwrap(); - let cert_table = tx.open_table(CERTIFICATES_TABLE).unwrap(); - assert_eq!( - cert_table.get(Height::new(1)).unwrap().unwrap().value()[0], - 0, - "preview_migrate must not alter record data" - ); - drop(cert_table); - drop(tx); - - let migrated = coordinator.migrate().unwrap(); - - assert_eq!(preview.records_scanned, migrated.records_scanned); - assert_eq!(preview.records_upgraded, migrated.records_upgraded); - assert_eq!(preview.records_skipped, migrated.records_skipped); - assert_eq!(preview.tables_migrated, migrated.tables_migrated); - } - - #[test] - fn test_preview_migrate_already_current() { - let (db, _path) = create_test_db(); - let coordinator = MigrationCoordinator::new(db); - - // Initialize DB at current schema version (simulates a node that is already up to date). - coordinator.needs_migration(false).unwrap(); - assert_eq!( - coordinator.current_schema_version().unwrap(), - Some(DB_SCHEMA_VERSION) - ); - - let stats = coordinator - .preview_migrate() - .expect("preview_migrate must not error when already at current version"); - - assert_eq!(stats.tables_migrated, 0); - assert_eq!(stats.records_scanned, 0); - assert_eq!(stats.records_upgraded, 0); - assert_eq!(stats.records_skipped, 0); - } - - #[test] - fn test_certificate_table_migration() { - let (db, _path) = create_test_db(); - - // Create tables - let tx = db.begin_write().unwrap(); - let _ = tx.open_table(CERTIFICATES_TABLE).unwrap(); - tx.commit().unwrap(); - - // Insert some test data with old version (0) - let tx = db.begin_write().unwrap(); - { - let mut table = tx.open_table(CERTIFICATES_TABLE).unwrap(); - table.insert(Height::new(1), vec![0, 1, 2, 3, 4]).unwrap(); - table.insert(Height::new(2), vec![0, 5, 6, 7, 8]).unwrap(); - } - tx.commit().unwrap(); - - let coordinator = MigrationCoordinator::new(db); - - // Run migration - let migrator = TestMigrator { - from: SchemaVersion::V0, - to: SchemaVersion::V1, - }; - let stats = coordinator.migrate_certificates(&migrator, false).unwrap(); - - assert_eq!(stats.records_scanned, 2); - assert_eq!(stats.records_upgraded, 2); - assert_eq!(stats.records_skipped, 0); - assert_eq!(stats.tables_migrated, 1); - - let db = coordinator.into_db(); - - // Verify data was migrated - let tx = db.begin_read().unwrap(); - let table = tx.open_table(CERTIFICATES_TABLE).unwrap(); - let value = table.get(Height::new(1)).unwrap().unwrap(); - assert_eq!(value.value(), vec![1, 1, 2, 3, 4]); - } - - #[test] - fn test_no_migration_when_already_upgraded() { - let (db, _path) = create_test_db(); - - // Create tables - let tx = db.begin_write().unwrap(); - let _ = tx.open_table(DECIDED_BLOCKS_TABLE).unwrap(); - tx.commit().unwrap(); - - // Insert data already at target version - let tx = db.begin_write().unwrap(); - { - let mut table = tx.open_table(DECIDED_BLOCKS_TABLE).unwrap(); - table.insert(Height::new(1), vec![1, 1, 2, 3, 4]).unwrap(); - table.insert(Height::new(2), vec![1, 5, 6, 7, 8]).unwrap(); - } - tx.commit().unwrap(); - - let coordinator = MigrationCoordinator::new(db); - - // Run migration - let migrator = TestMigrator { - from: SchemaVersion::V0, - to: SchemaVersion::V1, - }; - let stats = coordinator - .migrate_decided_blocks(&migrator, false) - .unwrap(); - - assert_eq!(stats.records_scanned, 2); - assert_eq!(stats.records_upgraded, 0); - assert_eq!(stats.records_skipped, 2); - } - - #[test] - fn test_mixed_versions_in_table() { - let (db, _path) = create_test_db(); - - // Create tables - let tx = db.begin_write().unwrap(); - let _ = tx.open_table(DECIDED_BLOCKS_TABLE).unwrap(); - tx.commit().unwrap(); - - // Insert mixed version data - let tx = db.begin_write().unwrap(); - { - let mut table = tx.open_table(DECIDED_BLOCKS_TABLE).unwrap(); - table.insert(Height::new(1), vec![0, 1, 2, 3]).unwrap(); // old version - table.insert(Height::new(2), vec![1, 5, 6, 7]).unwrap(); // new version - table.insert(Height::new(3), vec![0, 8, 9, 10]).unwrap(); // old version - } - tx.commit().unwrap(); - - let coordinator = MigrationCoordinator::new(db); - - // Run migration - let migrator = TestMigrator { - from: SchemaVersion::V0, - to: SchemaVersion::V1, - }; - let stats = coordinator - .migrate_decided_blocks(&migrator, false) - .unwrap(); - - assert_eq!(stats.records_scanned, 3); - assert_eq!(stats.records_upgraded, 2); - assert_eq!(stats.records_skipped, 1); - - let db = coordinator.into_db(); - - // Verify all data is now at version 1 - let tx = db.begin_read().unwrap(); - let table = tx.open_table(DECIDED_BLOCKS_TABLE).unwrap(); - - let value1 = table.get(Height::new(1)).unwrap().unwrap(); - assert_eq!(value1.value()[0], 1); - - let value2 = table.get(Height::new(2)).unwrap().unwrap(); - assert_eq!(value2.value()[0], 1); - - let value3 = table.get(Height::new(3)).unwrap().unwrap(); - assert_eq!(value3.value()[0], 1); - } - - #[test] - fn test_full_migration_flow() { - let (db, _path) = create_test_db(); - - // Create metadata table - let tx = db.begin_write().unwrap(); - let _ = tx.open_table(METADATA_TABLE).unwrap(); - tx.commit().unwrap(); - - let coordinator = MigrationCoordinator::new(db); - - // Check if migration is needed first (this initializes the schema version for a new database) - let needs = coordinator.needs_migration(false).unwrap(); - assert!(!needs, "New database should not need migration"); - - // Now migration should complete successfully with no data to migrate - let result = coordinator.migrate(); - - // Since we start at current version, no migration should be needed - assert!( - result.is_ok(), - "Migration should succeed: {:?}", - result.err() - ); - let stats = result.unwrap(); - assert_eq!( - stats.tables_migrated, 0, - "No tables should be migrated when already at current version" - ); - } - - #[test] - fn test_needs_migration_without_metadata_table() { - // Create old database without metadata table - let (db, _path) = create_test_db(); - let coordinator = MigrationCoordinator::new(db); - - // Should handle missing metadata table gracefully - coordinator - .ensure_metadata_table_exists() - .expect("Should create metadata table"); - - // Simulate old database that exists - let needs = coordinator - .needs_migration(true) - .expect("Should check migration status"); - assert!(needs, "Old database with no version should need migration"); - - // Verify it was set to v0 - let version = coordinator - .current_schema_version() - .expect("Should read version"); - assert_eq!( - version, - Some(SchemaVersion::V0), - "Should be set to v0 after check" - ); - } - - #[test] - fn test_metadata_table_exists_idempotent() { - let (db, _path) = create_test_db(); - let coordinator = MigrationCoordinator::new(db); - - // Call multiple times - should be idempotent - coordinator - .ensure_metadata_table_exists() - .expect("First call should succeed"); - coordinator - .ensure_metadata_table_exists() - .expect("Second call should succeed"); - coordinator - .ensure_metadata_table_exists() - .expect("Third call should succeed"); - - // Verify table is still accessible - assert!( - coordinator.current_schema_version().is_ok(), - "Table should be accessible after multiple ensure calls" - ); - } - - #[test] - #[allow(clippy::cast_possible_truncation)] - fn test_certificate_migration_batch_size_plus_one() { - let (db, _path) = create_test_db(); - - // Create tables - let tx = db.begin_write().unwrap(); - let _ = tx.open_table(CERTIFICATES_TABLE).unwrap(); - tx.commit().unwrap(); - - // Insert BATCH_SIZE + 1 records with old version (0) - // This tests that batch processing correctly handles the boundary - let num_records = MigrationCoordinator::BATCH_SIZE + 1; - let tx = db.begin_write().unwrap(); - { - let mut table = tx.open_table(CERTIFICATES_TABLE).unwrap(); - for i in 1..=num_records { - // Store height i with data [0, i_low, i_high, ...] where i is encoded as u16 - let mut data = vec![0]; // version 0 - data.push((i & 0xFF) as u8); // low byte of i - data.push(((i >> 8) & 0xFF) as u8); // high byte of i - table.insert(Height::new(i as u64), data).unwrap(); - } - } - tx.commit().unwrap(); - - // Verify we have BATCH_SIZE + 1 records before migration - let tx = db.begin_read().unwrap(); - let table = tx.open_table(CERTIFICATES_TABLE).unwrap(); - let count = table.iter().unwrap().count(); - assert_eq!( - count, num_records, - "Should have BATCH_SIZE+1 records before migration" - ); - drop(table); - drop(tx); - - let coordinator = MigrationCoordinator::new(db); - - // Run migration - let migrator = TestMigrator { - from: SchemaVersion::V0, - to: SchemaVersion::V1, - }; - let stats = coordinator.migrate_certificates(&migrator, false).unwrap(); - - // Verify all records were scanned and upgraded - assert_eq!( - stats.records_scanned, num_records, - "Should scan all BATCH_SIZE+1 records" - ); - assert_eq!( - stats.records_upgraded, num_records, - "Should upgrade all BATCH_SIZE+1 records" - ); - assert_eq!(stats.records_skipped, 0, "Should not skip any records"); - assert_eq!(stats.tables_migrated, 1); - - let db = coordinator.into_db(); - - // Verify all records were migrated correctly - let tx = db.begin_read().unwrap(); - let table = tx.open_table(CERTIFICATES_TABLE).unwrap(); - - for i in 1..=num_records { - let value = table.get(Height::new(i as u64)).unwrap().unwrap(); - let bytes = value.value(); - - // Should be version 1 now - assert_eq!( - bytes[0], 1, - "Record at height {} should be version 1 after migration", - i - ); - - // Data should be preserved (low byte, high byte) - assert_eq!( - bytes[1], - (i & 0xFF) as u8, - "Record at height {} should have correct low byte", - i - ); - assert_eq!( - bytes[2], - ((i >> 8) & 0xFF) as u8, - "Record at height {} should have correct high byte", - i - ); - } - - // Verify count is still correct - let count = table.iter().unwrap().count(); - assert_eq!( - count, num_records, - "Should still have BATCH_SIZE+1 records after migration" - ); - } - - #[test] - fn test_undecided_blocks_migration() { - let (db, _path) = create_test_db(); - - // Create tables - let tx = db.begin_write().unwrap(); - let _ = tx.open_table(UNDECIDED_BLOCKS_TABLE).unwrap(); - tx.commit().unwrap(); - - // Create test block hashes - let hash1 = BlockHash::from([1u8; 32]); - let hash2 = BlockHash::from([2u8; 32]); - let hash3 = BlockHash::from([3u8; 32]); - - // Insert some test data with old version (0) - // Using composite keys (height, round, block_hash) as per table definition - let tx = db.begin_write().unwrap(); - { - let mut table = tx.open_table(UNDECIDED_BLOCKS_TABLE).unwrap(); - table - .insert((Height::new(1), Round::new(0), hash1), vec![0, 1, 2, 3, 4]) - .unwrap(); - table - .insert((Height::new(1), Round::new(1), hash2), vec![0, 5, 6, 7, 8]) - .unwrap(); - table - .insert( - (Height::new(2), Round::new(0), hash3), - vec![0, 9, 10, 11, 12], - ) - .unwrap(); - } - tx.commit().unwrap(); - - let coordinator = MigrationCoordinator::new(db); - - // Run migration - let migrator = TestMigrator { - from: SchemaVersion::V0, - to: SchemaVersion::V1, - }; - let stats = coordinator - .migrate_undecided_blocks(&migrator, false) - .unwrap(); - - assert_eq!(stats.records_scanned, 3); - assert_eq!(stats.records_upgraded, 3); - assert_eq!(stats.records_skipped, 0); - assert_eq!(stats.tables_migrated, 1); - - let db = coordinator.into_db(); - - // Verify data was migrated - let tx = db.begin_read().unwrap(); - let table = tx.open_table(UNDECIDED_BLOCKS_TABLE).unwrap(); - let value = table - .get((Height::new(1), Round::new(0), hash1)) - .unwrap() - .unwrap(); - assert_eq!(value.value(), vec![1, 1, 2, 3, 4]); - - let value = table - .get((Height::new(1), Round::new(1), hash2)) - .unwrap() - .unwrap(); - assert_eq!(value.value(), vec![1, 5, 6, 7, 8]); - - let value = table - .get((Height::new(2), Round::new(0), hash3)) - .unwrap() - .unwrap(); - assert_eq!(value.value(), vec![1, 9, 10, 11, 12]); - } - - #[test] - fn test_undecided_blocks_migration_empty_table() { - let (db, _path) = create_test_db(); - - // Create empty table - let tx = db.begin_write().unwrap(); - let _ = tx.open_table(UNDECIDED_BLOCKS_TABLE).unwrap(); - tx.commit().unwrap(); - - let coordinator = MigrationCoordinator::new(db); - - // Run migration on empty table - let migrator = TestMigrator { - from: SchemaVersion::V0, - to: SchemaVersion::V1, - }; - let stats = coordinator - .migrate_undecided_blocks(&migrator, false) - .unwrap(); - - assert_eq!(stats.records_scanned, 0); - assert_eq!(stats.records_upgraded, 0); - assert_eq!(stats.records_skipped, 0); - assert_eq!(stats.tables_migrated, 1); - } - - #[test] - fn test_undecided_blocks_migration_mixed_versions() { - let (db, _path) = create_test_db(); - - // Create tables - let tx = db.begin_write().unwrap(); - let _ = tx.open_table(UNDECIDED_BLOCKS_TABLE).unwrap(); - tx.commit().unwrap(); - - // Create test block hashes - let hash1 = BlockHash::from([1u8; 32]); - let hash2 = BlockHash::from([2u8; 32]); - let hash3 = BlockHash::from([3u8; 32]); - - // Insert mixed version data - let tx = db.begin_write().unwrap(); - { - let mut table = tx.open_table(UNDECIDED_BLOCKS_TABLE).unwrap(); - table - .insert((Height::new(1), Round::new(0), hash1), vec![0, 1, 2, 3]) - .unwrap(); // old version - table - .insert((Height::new(1), Round::new(1), hash2), vec![1, 5, 6, 7]) - .unwrap(); // new version - table - .insert((Height::new(2), Round::new(0), hash3), vec![0, 8, 9, 10]) - .unwrap(); // old version - } - tx.commit().unwrap(); - - let coordinator = MigrationCoordinator::new(db); - - // Run migration - let migrator = TestMigrator { - from: SchemaVersion::V0, - to: SchemaVersion::V1, - }; - let stats = coordinator - .migrate_undecided_blocks(&migrator, false) - .unwrap(); - - assert_eq!(stats.records_scanned, 3); - assert_eq!(stats.records_upgraded, 2); - assert_eq!(stats.records_skipped, 1); - - let db = coordinator.into_db(); - - // Verify all data is now at version 1 - let tx = db.begin_read().unwrap(); - let table = tx.open_table(UNDECIDED_BLOCKS_TABLE).unwrap(); - - let value1 = table - .get((Height::new(1), Round::new(0), hash1)) - .unwrap() - .unwrap(); - assert_eq!(value1.value()[0], 1); - - let value2 = table - .get((Height::new(1), Round::new(1), hash2)) - .unwrap() - .unwrap(); - assert_eq!(value2.value()[0], 1); - - let value3 = table - .get((Height::new(2), Round::new(0), hash3)) - .unwrap() - .unwrap(); - assert_eq!(value3.value()[0], 1); - } - - #[test] - fn test_pending_parts_migration() { - let (db, _path) = create_test_db(); - - // Create tables - let tx = db.begin_write().unwrap(); - let _ = tx.open_table(PENDING_PROPOSAL_PARTS_TABLE).unwrap(); - tx.commit().unwrap(); - - // Create test block hashes - let hash1 = BlockHash::from([1u8; 32]); - let hash2 = BlockHash::from([2u8; 32]); - let hash3 = BlockHash::from([3u8; 32]); - let hash4 = BlockHash::from([4u8; 32]); - - // Insert some test data with old version (0) - // Using composite keys (height, round, block_hash) as per table definition - let tx = db.begin_write().unwrap(); - { - let mut table = tx.open_table(PENDING_PROPOSAL_PARTS_TABLE).unwrap(); - table - .insert((Height::new(1), Round::new(0), hash1), vec![0, 1, 2, 3, 4]) - .unwrap(); - table - .insert((Height::new(1), Round::new(0), hash2), vec![0, 5, 6, 7, 8]) - .unwrap(); - table - .insert( - (Height::new(1), Round::new(1), hash3), - vec![0, 9, 10, 11, 12], - ) - .unwrap(); - table - .insert( - (Height::new(2), Round::new(0), hash4), - vec![0, 13, 14, 15, 16], - ) - .unwrap(); - } - tx.commit().unwrap(); - - let coordinator = MigrationCoordinator::new(db); - - // Run migration - let migrator = TestMigrator { - from: SchemaVersion::V0, - to: SchemaVersion::V1, - }; - let stats = coordinator.migrate_pending_parts(&migrator, false).unwrap(); - - assert_eq!(stats.records_scanned, 4); - assert_eq!(stats.records_upgraded, 4); - assert_eq!(stats.records_skipped, 0); - assert_eq!(stats.tables_migrated, 1); - - let db = coordinator.into_db(); - - // Verify data was migrated - let tx = db.begin_read().unwrap(); - let table = tx.open_table(PENDING_PROPOSAL_PARTS_TABLE).unwrap(); - - let value = table - .get((Height::new(1), Round::new(0), hash1)) - .unwrap() - .unwrap(); - assert_eq!(value.value(), vec![1, 1, 2, 3, 4]); - - let value = table - .get((Height::new(1), Round::new(0), hash2)) - .unwrap() - .unwrap(); - assert_eq!(value.value(), vec![1, 5, 6, 7, 8]); - - let value = table - .get((Height::new(1), Round::new(1), hash3)) - .unwrap() - .unwrap(); - assert_eq!(value.value(), vec![1, 9, 10, 11, 12]); - - let value = table - .get((Height::new(2), Round::new(0), hash4)) - .unwrap() - .unwrap(); - assert_eq!(value.value(), vec![1, 13, 14, 15, 16]); - } - - #[test] - fn test_pending_parts_migration_empty_table() { - let (db, _path) = create_test_db(); - - // Create empty table - let tx = db.begin_write().unwrap(); - let _ = tx.open_table(PENDING_PROPOSAL_PARTS_TABLE).unwrap(); - tx.commit().unwrap(); - - let coordinator = MigrationCoordinator::new(db); - - // Run migration on empty table - let migrator = TestMigrator { - from: SchemaVersion::V0, - to: SchemaVersion::V1, - }; - let stats = coordinator.migrate_pending_parts(&migrator, false).unwrap(); - - assert_eq!(stats.records_scanned, 0); - assert_eq!(stats.records_upgraded, 0); - assert_eq!(stats.records_skipped, 0); - assert_eq!(stats.tables_migrated, 1); - } - - #[test] - fn test_pending_parts_migration_mixed_versions() { - let (db, _path) = create_test_db(); - - // Create tables - let tx = db.begin_write().unwrap(); - let _ = tx.open_table(PENDING_PROPOSAL_PARTS_TABLE).unwrap(); - tx.commit().unwrap(); - - // Create test block hashes - let hash1 = BlockHash::from([1u8; 32]); - let hash2 = BlockHash::from([2u8; 32]); - let hash3 = BlockHash::from([3u8; 32]); - let hash4 = BlockHash::from([4u8; 32]); - - // Insert mixed version data - let tx = db.begin_write().unwrap(); - { - let mut table = tx.open_table(PENDING_PROPOSAL_PARTS_TABLE).unwrap(); - table - .insert((Height::new(1), Round::new(0), hash1), vec![0, 1, 2, 3]) - .unwrap(); // old version - table - .insert((Height::new(1), Round::new(0), hash2), vec![1, 5, 6, 7]) - .unwrap(); // new version - table - .insert((Height::new(1), Round::new(1), hash3), vec![0, 8, 9, 10]) - .unwrap(); // old version - table - .insert((Height::new(2), Round::new(0), hash4), vec![1, 11, 12, 13]) - .unwrap(); // new version - } - tx.commit().unwrap(); - - let coordinator = MigrationCoordinator::new(db); - - // Run migration - let migrator = TestMigrator { - from: SchemaVersion::V0, - to: SchemaVersion::V1, - }; - let stats = coordinator.migrate_pending_parts(&migrator, false).unwrap(); - - assert_eq!(stats.records_scanned, 4); - assert_eq!(stats.records_upgraded, 2); - assert_eq!(stats.records_skipped, 2); - - let db = coordinator.into_db(); - - // Verify all data is now at version 1 - let tx = db.begin_read().unwrap(); - let table = tx.open_table(PENDING_PROPOSAL_PARTS_TABLE).unwrap(); - - let value1 = table - .get((Height::new(1), Round::new(0), hash1)) - .unwrap() - .unwrap(); - assert_eq!(value1.value()[0], 1); - - let value2 = table - .get((Height::new(1), Round::new(0), hash2)) - .unwrap() - .unwrap(); - assert_eq!(value2.value()[0], 1); - - let value3 = table - .get((Height::new(1), Round::new(1), hash3)) - .unwrap() - .unwrap(); - assert_eq!(value3.value()[0], 1); - - let value4 = table - .get((Height::new(2), Round::new(0), hash4)) - .unwrap() - .unwrap(); - assert_eq!(value4.value()[0], 1); - } -} diff --git a/crates/consensus-db/src/migrations/migrators.rs b/crates/consensus-db/src/migrations/migrators.rs deleted file mode 100644 index 959e530..0000000 --- a/crates/consensus-db/src/migrations/migrators.rs +++ /dev/null @@ -1,397 +0,0 @@ -// Copyright 2025 Circle Internet Group, Inc. All rights reserved. -// -// SPDX-License-Identifier: Apache-2.0 -// -// Licensed under the Apache License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. -// You may obtain a copy of the License at -// -// http://www.apache.org/licenses/LICENSE-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// See the License for the specific language governing permissions and -// limitations under the License. - -use crate::versions::SchemaVersion; - -use super::Migrator; -use crate::decoder::DecodeError; -use crate::store::StoreError; -use crate::versions::{ - CommitCertificateVersion, ConsensusBlockVersion, ExecutionPayloadVersion, ProposalPartsVersion, -}; - -/// Set of migrators for all tables at a specific schema version -pub struct MigratorSet { - pub certificate: Box, - pub decided_block: Box, - pub undecided_block: Box, - pub pending_parts: Box, -} - -impl MigratorSet { - /// Get migrators for schema version 1 - /// This handles the initial version where data already has version bytes - pub fn v1() -> Self { - Self { - certificate: Box::new(CommitCertificateMigrator0To1), - decided_block: Box::new(ExecutionPayloadMigrator0To3), - undecided_block: Box::new(ConsensusBlockMigrator0To1), - pending_parts: Box::new(ProposalPartsMigrator0To1), - } - } -} - -/// No-op migrator for commit certificates (v0 -> v1) -/// This is used when the database is first created or has data without version bytes -pub struct CommitCertificateMigrator0To1; - -impl Migrator for CommitCertificateMigrator0To1 { - fn name(&self) -> &str { - "CommitCertificate v0->v1" - } - - fn source_version(&self) -> SchemaVersion { - SchemaVersion::V0 - } - - fn target_version(&self) -> SchemaVersion { - SchemaVersion::new(CommitCertificateVersion::V1 as u8) - } - - fn migrate(&self, old_bytes: &[u8]) -> Result, StoreError> { - // For initial migration, data is already in correct format - // Just add version byte if missing - if old_bytes.is_empty() { - return Err(StoreError::Decode(DecodeError::EmptyVersion)); - } - - // Unconditionally prepend version byte. Otherwise, we might have some - // false positives when going from unversioned to versioned - // data. - #[allow(clippy::arithmetic_side_effects)] // 1 + slice.len() cannot overflow usize - let mut result = Vec::with_capacity(1 + old_bytes.len()); - result.push(self.target_version().as_u8()); - result.extend_from_slice(old_bytes); - Ok(result) - } - - fn needs_migration(&self, _bytes: &[u8]) -> bool { - true - } -} - -/// No-op migrator for execution payloads (v0 -> v3) -/// Note: We start at v3 for execution payloads since we use ExecutionPayloadV3 -pub struct ExecutionPayloadMigrator0To3; - -impl Migrator for ExecutionPayloadMigrator0To3 { - fn name(&self) -> &str { - "ExecutionPayload v0->v3" - } - - fn source_version(&self) -> SchemaVersion { - SchemaVersion::V0 - } - - fn target_version(&self) -> SchemaVersion { - SchemaVersion::new(ExecutionPayloadVersion::V3 as u8) - } - - fn migrate(&self, old_bytes: &[u8]) -> Result, StoreError> { - if old_bytes.is_empty() { - return Err(StoreError::Decode(DecodeError::EmptyVersion)); - } - - // Unconditionally prepend version byte. Otherwise, we might have some - // false positives when going from unversioned to versioned - // data. - #[allow(clippy::arithmetic_side_effects)] // 1 + slice.len() cannot overflow usize - let mut result = Vec::with_capacity(1 + old_bytes.len()); - result.push(self.target_version().as_u8()); - result.extend_from_slice(old_bytes); - Ok(result) - } - - fn needs_migration(&self, _bytes: &[u8]) -> bool { - true - } -} - -/// No-op migrator for consensus blocks (v0 -> v1) -pub struct ConsensusBlockMigrator0To1; - -impl Migrator for ConsensusBlockMigrator0To1 { - fn name(&self) -> &str { - "ConsensusBlock v0->v1" - } - - fn source_version(&self) -> SchemaVersion { - SchemaVersion::V0 - } - - fn target_version(&self) -> SchemaVersion { - SchemaVersion::new(ConsensusBlockVersion::V1 as u8) - } - - fn migrate(&self, old_bytes: &[u8]) -> Result, StoreError> { - if old_bytes.is_empty() { - return Err(StoreError::Decode(DecodeError::EmptyVersion)); - } - - // Unconditionally prepend version byte. Otherwise, we might have some - // false positives when going from unversioned to versioned - // data. - #[allow(clippy::arithmetic_side_effects)] // 1 + slice.len() cannot overflow usize - let mut result = Vec::with_capacity(1 + old_bytes.len()); - result.push(self.target_version().as_u8()); - result.extend_from_slice(old_bytes); - Ok(result) - } - - fn needs_migration(&self, _bytes: &[u8]) -> bool { - true - } -} - -/// No-op migrator for proposal parts (v0 -> v1) -pub struct ProposalPartsMigrator0To1; - -impl Migrator for ProposalPartsMigrator0To1 { - fn name(&self) -> &str { - "ProposalParts v0->v1" - } - - fn source_version(&self) -> SchemaVersion { - SchemaVersion::V0 - } - - fn target_version(&self) -> SchemaVersion { - SchemaVersion::new(ProposalPartsVersion::V1 as u8) - } - - fn migrate(&self, old_bytes: &[u8]) -> Result, StoreError> { - if old_bytes.is_empty() { - return Err(StoreError::Decode(DecodeError::EmptyVersion)); - } - - // Unconditionally prepend version byte. Otherwise, we might have some - // false positives when going from unversioned to versioned - // data. - #[allow(clippy::arithmetic_side_effects)] // 1 + slice.len() cannot overflow usize - let mut result = Vec::with_capacity(1 + old_bytes.len()); - result.push(self.target_version().as_u8()); - result.extend_from_slice(old_bytes); - Ok(result) - } - - fn needs_migration(&self, _bytes: &[u8]) -> bool { - true - } -} - -// Example of a real migration (v1 -> v2) for future use: -/* -pub struct CommitCertificateMigrator1To2; - -impl Migrator for CommitCertificateMigrator1To2 { - fn name(&self) -> &str { - "CommitCertificate v1->v2" - } - - fn source_version(&self) -> SchemaVersion { - SchemaVersion::new(CommitCertificateVersion::V1 as u8) - } - - fn target_version(&self) -> SchemaVersion { - SchemaVersion::new(CommitCertificateVersion::V2 as u8) - } - - fn migrate(&self, old_bytes: &[u8]) -> Result, StoreError> { - use crate::decoder::decode_certificate_v1; - use crate::encoder::encode_certificate_v2; - - // 1. Decode using v1 decoder - let cert = decode_certificate_v1(old_bytes)?; - - // 2. Transform data structure (e.g., add new fields, change format) - let updated_cert = transform_certificate_v1_to_v2(cert); - - // 3. Encode using v2 encoder - let new_bytes = encode_certificate_v2(&updated_cert)?; - - Ok(new_bytes) - } -} -*/ - -#[cfg(test)] -mod tests { - use super::*; - - #[test] - fn test_migrator_set_v1() { - let migrators = MigratorSet::v1(); - - // Verify all migrators are present - assert_eq!(migrators.certificate.name(), "CommitCertificate v0->v1"); - assert_eq!(migrators.decided_block.name(), "ExecutionPayload v0->v3"); - assert_eq!(migrators.undecided_block.name(), "ConsensusBlock v0->v1"); - assert_eq!(migrators.pending_parts.name(), "ProposalParts v0->v1"); - } - - #[test] - fn test_commit_certificate_migrator_versions() { - let migrator = CommitCertificateMigrator0To1; - - assert_eq!(migrator.source_version(), SchemaVersion::V0); - assert_eq!( - migrator.target_version(), - SchemaVersion::new(CommitCertificateVersion::V1 as u8) - ); - } - - #[test] - fn test_commit_certificate_migrator_migrate() { - let migrator = CommitCertificateMigrator0To1; - let target = CommitCertificateVersion::V1 as u8; - - // Empty bytes -> error - let result = migrator.migrate(&[]); - assert!(result.is_err()); - - let data = vec![5, 10, 20, 30]; - let result = migrator.migrate(&data).unwrap(); - assert_eq!(result[0], target); - assert_eq!(result.len(), 1 + data.len()); - } - - #[test] - fn test_execution_payload_migrator_versions() { - let migrator = ExecutionPayloadMigrator0To3; - - assert_eq!(migrator.source_version(), SchemaVersion::V0); - assert_eq!( - migrator.target_version(), - SchemaVersion::new(ExecutionPayloadVersion::V3 as u8) - ); - } - - #[test] - fn test_execution_payload_migrator_migrate() { - let migrator = ExecutionPayloadMigrator0To3; - let target = ExecutionPayloadVersion::V3 as u8; - - // Empty bytes -> error - let result = migrator.migrate(&[]); - assert!(result.is_err()); - - let data = vec![5, 10, 20, 30]; - let result = migrator.migrate(&data).unwrap(); - assert_eq!(result[0], target); - assert_eq!(result.len(), 1 + data.len()); - } - - #[test] - fn test_consensus_block_migrator_versions() { - let migrator = ConsensusBlockMigrator0To1; - - assert_eq!(migrator.source_version(), SchemaVersion::V0); - assert_eq!( - migrator.target_version(), - SchemaVersion::new(ConsensusBlockVersion::V1 as u8) - ); - } - - #[test] - fn test_consensus_block_migrator_migrate() { - let migrator = ConsensusBlockMigrator0To1; - let target = ConsensusBlockVersion::V1 as u8; - - // Empty bytes -> error - let result = migrator.migrate(&[]); - assert!(result.is_err()); - - let data = vec![5, 10, 20, 30]; - let result = migrator.migrate(&data).unwrap(); - assert_eq!(result[0], target); - assert_eq!(result.len(), 1 + data.len()); - } - - #[test] - fn test_proposal_parts_migrator_versions() { - let migrator = ProposalPartsMigrator0To1; - - assert_eq!(migrator.source_version(), SchemaVersion::V0); - assert_eq!( - migrator.target_version(), - SchemaVersion::new(ProposalPartsVersion::V1 as u8) - ); - } - - #[test] - fn test_proposal_parts_migrator_migrate() { - let migrator = ProposalPartsMigrator0To1; - let target = ProposalPartsVersion::V1 as u8; - - // Empty bytes -> error - let result = migrator.migrate(&[]); - assert!(result.is_err()); - - let data = vec![5, 10, 20, 30]; - let result = migrator.migrate(&data).unwrap(); - assert_eq!(result[0], target); - assert_eq!(result.len(), 1 + data.len()); - } - - #[test] - fn test_all_migrators_consistent() { - // All migrators should have source version 0 for initial migration - let migrators = MigratorSet::v1(); - - assert_eq!(migrators.certificate.source_version(), SchemaVersion::V0); - assert_eq!(migrators.decided_block.source_version(), SchemaVersion::V0); - assert_eq!( - migrators.undecided_block.source_version(), - SchemaVersion::V0 - ); - assert_eq!(migrators.pending_parts.source_version(), SchemaVersion::V0); - - // All should have proper target versions - assert_eq!( - migrators.certificate.target_version(), - SchemaVersion::new(CommitCertificateVersion::V1 as u8) - ); - assert_eq!( - migrators.decided_block.target_version(), - SchemaVersion::new(ExecutionPayloadVersion::V3 as u8) - ); - assert_eq!( - migrators.undecided_block.target_version(), - SchemaVersion::new(ConsensusBlockVersion::V1 as u8) - ); - assert_eq!( - migrators.pending_parts.target_version(), - SchemaVersion::new(ProposalPartsVersion::V1 as u8) - ); - } - - #[test] - fn test_migrator_replaces_version_byte() { - // Test that migration replaces the version byte but preserves rest of data - let old_version = 0; - let test_data = vec![old_version, 1, 2, 3, 4, 5, 6, 7, 8, 9]; - - let certificate_migrator = CommitCertificateMigrator0To1; - let result = certificate_migrator.migrate(&test_data).unwrap(); - - // First byte should be the new version - assert_eq!(result[0], CommitCertificateVersion::V1 as u8); - - // Rest should be the same - assert_eq!(result.len(), 1 + test_data.len()); - assert_eq!(&result[1..], &test_data[..]); - } -} diff --git a/crates/consensus-db/src/repositories/certificates.rs b/crates/consensus-db/src/repositories/certificates.rs deleted file mode 100644 index 4a521ce..0000000 --- a/crates/consensus-db/src/repositories/certificates.rs +++ /dev/null @@ -1,57 +0,0 @@ -// Copyright 2025 Circle Internet Group, Inc. All rights reserved. -// -// SPDX-License-Identifier: Apache-2.0 -// -// Licensed under the Apache License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. -// You may obtain a copy of the License at -// -// http://www.apache.org/licenses/LICENSE-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// See the License for the specific language governing permissions and -// limitations under the License. - -use arc_consensus_types::{Height, StoredCommitCertificate}; - -use crate::store::{Store, StoreError}; - -#[cfg_attr(any(test, feature = "mock"), mockall::automock(type Error = std::io::Error;))] -pub trait CertificatesRepository { - type Error: std::error::Error + Send + Sync + 'static; - - /// Get the height of the highest certificate stored. - async fn max_height(&self) -> Result, Self::Error>; - - /// Get the commit certificate at the given height. - async fn get(&self, height: Height) -> Result, Self::Error>; -} - -impl CertificatesRepository for &T -where - T: CertificatesRepository + ?Sized, -{ - type Error = T::Error; - - async fn max_height(&self) -> Result, Self::Error> { - (**self).max_height().await - } - - async fn get(&self, height: Height) -> Result, Self::Error> { - (**self).get(height).await - } -} - -impl CertificatesRepository for Store { - type Error = StoreError; - - async fn max_height(&self) -> Result, StoreError> { - self.max_height().await - } - - async fn get(&self, height: Height) -> Result, StoreError> { - self.get_certificate(Some(height)).await - } -} diff --git a/crates/consensus-db/src/repositories/decided_blocks.rs b/crates/consensus-db/src/repositories/decided_blocks.rs deleted file mode 100644 index aeb8ac9..0000000 --- a/crates/consensus-db/src/repositories/decided_blocks.rs +++ /dev/null @@ -1,76 +0,0 @@ -// Copyright 2025 Circle Internet Group, Inc. All rights reserved. -// -// SPDX-License-Identifier: Apache-2.0 -// -// Licensed under the Apache License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. -// You may obtain a copy of the License at -// -// http://www.apache.org/licenses/LICENSE-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// See the License for the specific language governing permissions and -// limitations under the License. - -use alloy_rpc_types_engine::ExecutionPayloadV3; -use arc_consensus_types::{Address, ArcContext, Height}; -use malachitebft_core_types::CommitCertificate; - -use crate::store::{Store, StoreError}; -use arc_consensus_types::block::DecidedBlock; - -#[cfg_attr(any(test, feature = "mock"), mockall::automock(type Error = std::io::Error;))] -pub trait DecidedBlocksRepository { - type Error: std::error::Error + Send + Sync + 'static; - - async fn get(&self, height: Height) -> Result, Self::Error>; - - async fn store( - &self, - certificate: CommitCertificate, - execution_payload: ExecutionPayloadV3, - proposer: Address, - ) -> Result<(), Self::Error>; -} - -impl DecidedBlocksRepository for &'_ T -where - T: DecidedBlocksRepository, -{ - type Error = T::Error; - - async fn get(&self, height: Height) -> Result, Self::Error> { - (*self).get(height).await - } - - async fn store( - &self, - certificate: CommitCertificate, - execution_payload: ExecutionPayloadV3, - proposer: Address, - ) -> Result<(), Self::Error> { - (*self) - .store(certificate, execution_payload, proposer) - .await - } -} - -impl DecidedBlocksRepository for Store { - type Error = StoreError; - - async fn get(&self, height: Height) -> Result, Self::Error> { - self.get_decided_block(height).await - } - - async fn store( - &self, - certificate: CommitCertificate, - execution_payload: ExecutionPayloadV3, - proposer: Address, - ) -> Result<(), Self::Error> { - self.store_decided_block(certificate, execution_payload, proposer) - .await - } -} diff --git a/crates/consensus-db/src/repositories/invalid_payloads.rs b/crates/consensus-db/src/repositories/invalid_payloads.rs deleted file mode 100644 index c1605b9..0000000 --- a/crates/consensus-db/src/repositories/invalid_payloads.rs +++ /dev/null @@ -1,52 +0,0 @@ -// Copyright 2026 Circle Internet Group, Inc. All rights reserved. -// -// SPDX-License-Identifier: Apache-2.0 -// -// Licensed under the Apache License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. -// You may obtain a copy of the License at -// -// http://www.apache.org/licenses/LICENSE-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// See the License for the specific language governing permissions and -// limitations under the License. - -use crate::invalid_payloads::InvalidPayload; -use crate::store::{Store, StoreError}; - -/// Repository for persisting invalid payload records. -/// -/// This trait abstracts the storage of [`InvalidPayload`] entries so that callers -/// (and their tests) are not coupled to the concrete [`Store`] type. -#[cfg_attr(any(test, feature = "mock"), mockall::automock(type Error = std::io::Error;))] -pub trait InvalidPayloadsRepository { - type Error: std::error::Error + Send + Sync + 'static; - - /// Appends an invalid-payload record to the store. - /// - /// Creates the underlying collection for the payload's height if one does not - /// already exist. - async fn append(&self, invalid_payload: InvalidPayload) -> Result<(), Self::Error>; -} - -impl InvalidPayloadsRepository for &'_ T -where - T: InvalidPayloadsRepository, -{ - type Error = T::Error; - - async fn append(&self, invalid_payload: InvalidPayload) -> Result<(), Self::Error> { - (*self).append(invalid_payload).await - } -} - -impl InvalidPayloadsRepository for Store { - type Error = StoreError; - - async fn append(&self, invalid_payload: InvalidPayload) -> Result<(), Self::Error> { - self.append_invalid_payload(invalid_payload).await - } -} diff --git a/crates/consensus-db/src/repositories/mod.rs b/crates/consensus-db/src/repositories/mod.rs deleted file mode 100644 index 96859b5..0000000 --- a/crates/consensus-db/src/repositories/mod.rs +++ /dev/null @@ -1,46 +0,0 @@ -// Copyright 2025 Circle Internet Group, Inc. All rights reserved. -// -// SPDX-License-Identifier: Apache-2.0 -// -// Licensed under the Apache License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. -// You may obtain a copy of the License at -// -// http://www.apache.org/licenses/LICENSE-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// See the License for the specific language governing permissions and -// limitations under the License. - -#![allow(async_fn_in_trait)] - -mod certificates; -pub use certificates::CertificatesRepository; - -mod decided_blocks; -pub use decided_blocks::DecidedBlocksRepository; - -mod undecided_blocks; -pub use undecided_blocks::UndecidedBlocksRepository; - -mod invalid_payloads; -pub use invalid_payloads::InvalidPayloadsRepository; - -mod payloads; -pub use payloads::PayloadsRepository; - -mod pending_proposals; -pub use pending_proposals::PendingProposalsRepository; - -#[cfg(any(test, feature = "mock"))] -#[allow(unused_imports)] -pub mod mocks { - pub use super::certificates::MockCertificatesRepository; - pub use super::decided_blocks::MockDecidedBlocksRepository; - pub use super::invalid_payloads::MockInvalidPayloadsRepository; - pub use super::payloads::MockPayloadsRepository; - pub use super::pending_proposals::MockPendingProposalsRepository; - pub use super::undecided_blocks::MockUndecidedBlocksRepository; -} diff --git a/crates/consensus-db/src/repositories/payloads.rs b/crates/consensus-db/src/repositories/payloads.rs deleted file mode 100644 index e9232f6..0000000 --- a/crates/consensus-db/src/repositories/payloads.rs +++ /dev/null @@ -1,46 +0,0 @@ -// Copyright 2025 Circle Internet Group, Inc. All rights reserved. -// -// SPDX-License-Identifier: Apache-2.0 -// -// Licensed under the Apache License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. -// You may obtain a copy of the License at -// -// http://www.apache.org/licenses/LICENSE-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// See the License for the specific language governing permissions and -// limitations under the License. - -use alloy_rpc_types_engine::ExecutionPayloadV3; -use arc_consensus_types::Height; - -use crate::store::{Store, StoreError}; - -#[cfg_attr(any(test, feature = "mock"), mockall::automock(type Error = std::io::Error;))] -pub trait PayloadsRepository { - type Error: std::error::Error + Send + Sync + 'static; - - async fn get(&self, height: Height) -> Result, Self::Error>; -} - -impl PayloadsRepository for &'_ T -where - T: PayloadsRepository, -{ - type Error = T::Error; - - async fn get(&self, height: Height) -> Result, Self::Error> { - (*self).get(height).await - } -} - -impl PayloadsRepository for Store { - type Error = StoreError; - - async fn get(&self, height: Height) -> Result, Self::Error> { - self.get_payload(height).await - } -} diff --git a/crates/consensus-db/src/repositories/pending_proposals.rs b/crates/consensus-db/src/repositories/pending_proposals.rs deleted file mode 100644 index 45ab98b..0000000 --- a/crates/consensus-db/src/repositories/pending_proposals.rs +++ /dev/null @@ -1,74 +0,0 @@ -// Copyright 2025 Circle Internet Group, Inc. All rights reserved. -// -// SPDX-License-Identifier: Apache-2.0 -// -// Licensed under the Apache License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. -// You may obtain a copy of the License at -// -// http://www.apache.org/licenses/LICENSE-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// See the License for the specific language governing permissions and -// limitations under the License. - -use arc_consensus_types::{BlockHash, Height, Round}; - -use crate::store::{Store, StoreError}; - -#[cfg_attr(any(test, feature = "mock"), mockall::automock(type Error = std::io::Error;))] -pub trait PendingProposalsRepository { - type Error: std::error::Error + Send + Sync + 'static; - - /// Enforce pending proposals limit on startup. - /// Clean up any excess proposals from previous runs. - /// Removes all proposals outside the valid range and trims to max_pending_proposals. - async fn enforce_limit( - &self, - max_pending_proposals: usize, - current_height: Height, - ) -> Result, Self::Error>; - - /// Return the total number of stored pending proposal parts. - async fn count(&self) -> Result; -} - -impl PendingProposalsRepository for &T -where - T: PendingProposalsRepository + ?Sized, -{ - type Error = T::Error; - - async fn enforce_limit( - &self, - max_pending_proposals: usize, - current_height: Height, - ) -> Result, Self::Error> { - (**self) - .enforce_limit(max_pending_proposals, current_height) - .await - } - - async fn count(&self) -> Result { - (**self).count().await - } -} - -impl PendingProposalsRepository for Store { - type Error = StoreError; - - async fn enforce_limit( - &self, - max_pending_proposals: usize, - current_height: Height, - ) -> Result, StoreError> { - self.enforce_pending_proposals_limit(max_pending_proposals, current_height) - .await - } - - async fn count(&self) -> Result { - self.get_pending_proposal_parts_count().await - } -} diff --git a/crates/consensus-db/src/repositories/undecided_blocks.rs b/crates/consensus-db/src/repositories/undecided_blocks.rs deleted file mode 100644 index 1b4105e..0000000 --- a/crates/consensus-db/src/repositories/undecided_blocks.rs +++ /dev/null @@ -1,125 +0,0 @@ -// Copyright 2025 Circle Internet Group, Inc. All rights reserved. -// -// SPDX-License-Identifier: Apache-2.0 -// -// Licensed under the Apache License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. -// You may obtain a copy of the License at -// -// http://www.apache.org/licenses/LICENSE-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// See the License for the specific language governing permissions and -// limitations under the License. - -use arc_consensus_types::{BlockHash, Height, Round}; - -use crate::store::{Store, StoreError}; -use arc_consensus_types::block::ConsensusBlock; - -#[cfg_attr(any(test, feature = "mock"), mockall::automock(type Error = std::io::Error;))] -pub trait UndecidedBlocksRepository { - type Error: std::error::Error + Send + Sync + 'static; - - /// Gets the undecided block with the given height, round, and block hash. - /// Returns `None` if no such block exists. - async fn get_by_round_and_hash( - &self, - height: Height, - round: Round, - block_hash: BlockHash, - ) -> Result, Self::Error>; - - /// Gets all undecided blocks for the given height and round. - async fn get_by_round( - &self, - height: Height, - round: Round, - ) -> Result, Self::Error>; - - /// Gets the undecided block with the given height and block hash. - /// Scans across all rounds, returning the first match found. - /// Returns `None` if no such block exists. - async fn get_by_hash( - &self, - height: Height, - block_hash: BlockHash, - ) -> Result, Self::Error>; - - /// Stores the undecided block. - async fn store_undecided_block(&self, block: ConsensusBlock) -> Result<(), Self::Error>; -} - -impl UndecidedBlocksRepository for &'_ T -where - T: UndecidedBlocksRepository, -{ - type Error = T::Error; - - async fn get_by_round_and_hash( - &self, - height: Height, - round: Round, - block_hash: BlockHash, - ) -> Result, Self::Error> { - (*self) - .get_by_round_and_hash(height, round, block_hash) - .await - } - - async fn get_by_round( - &self, - height: Height, - round: Round, - ) -> Result, Self::Error> { - (*self).get_by_round(height, round).await - } - - async fn get_by_hash( - &self, - height: Height, - block_hash: BlockHash, - ) -> Result, Self::Error> { - (*self).get_by_hash(height, block_hash).await - } - - async fn store_undecided_block(&self, block: ConsensusBlock) -> Result<(), Self::Error> { - (*self).store_undecided_block(block).await - } -} - -impl UndecidedBlocksRepository for Store { - type Error = StoreError; - - async fn get_by_round_and_hash( - &self, - height: Height, - round: Round, - block_hash: BlockHash, - ) -> Result, Self::Error> { - self.get_undecided_block(height, round, block_hash).await - } - - async fn get_by_round( - &self, - height: Height, - round: Round, - ) -> Result, Self::Error> { - self.get_undecided_blocks(height, round).await - } - - async fn get_by_hash( - &self, - height: Height, - block_hash: BlockHash, - ) -> Result, Self::Error> { - self.get_undecided_block_by_height_and_block_hash(height, block_hash) - .await - } - - async fn store_undecided_block(&self, block: ConsensusBlock) -> Result<(), Self::Error> { - self.store_undecided_block(block).await - } -} diff --git a/crates/consensus-db/src/services/mod.rs b/crates/consensus-db/src/services/mod.rs deleted file mode 100644 index 395bc01..0000000 --- a/crates/consensus-db/src/services/mod.rs +++ /dev/null @@ -1,25 +0,0 @@ -// Copyright 2025 Circle Internet Group, Inc. All rights reserved. -// -// SPDX-License-Identifier: Apache-2.0 -// -// Licensed under the Apache License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. -// You may obtain a copy of the License at -// -// http://www.apache.org/licenses/LICENSE-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// See the License for the specific language governing permissions and -// limitations under the License. - -#![allow(async_fn_in_trait)] - -mod pruning; -pub use pruning::{ProdPruningService, PruningService}; - -#[cfg(any(test, feature = "mock"))] -pub mod mocks { - pub use super::pruning::MockPruningService; -} diff --git a/crates/consensus-db/src/services/pruning.rs b/crates/consensus-db/src/services/pruning.rs deleted file mode 100644 index 936610e..0000000 --- a/crates/consensus-db/src/services/pruning.rs +++ /dev/null @@ -1,114 +0,0 @@ -// Copyright 2025 Circle Internet Group, Inc. All rights reserved. -// -// SPDX-License-Identifier: Apache-2.0 -// -// Licensed under the Apache License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. -// You may obtain a copy of the License at -// -// http://www.apache.org/licenses/LICENSE-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// See the License for the specific language governing permissions and -// limitations under the License. - -use arc_consensus_types::{Height, PruningConfig}; -use tracing::info; - -use crate::store::{Store, StoreError}; - -#[cfg_attr(any(test, feature = "mock"), mockall::automock(type Error = std::io::Error;))] -pub trait PruningService { - type Error: std::error::Error + Send + Sync + 'static; - - /// Prune decided blocks. - /// - /// # Important - /// Should be called regardless of whether pruning is enabled. - /// - /// As historical decided blocks are fetched from EL, we just keep a minimum number of blocks - /// in the DB to help with EL's amnesia upon recovery. - async fn prune_decided_blocks(&self) -> Result, Self::Error>; - - /// Prune historical certificates. - /// - /// # Important - /// This will only prune certificates if pruning is enabled. - /// - /// # Arguments - /// - `latest_height`: The latest committed height. Used to determine the effective retain height. - async fn prune_historical_certs( - &self, - latest_height: Height, - ) -> Result, Self::Error>; - - /// Clean up stale consensus data (undecided blocks and pending proposals) for committed heights. - /// - /// # Important - /// Should always be called when committing a block, regardless of pruning configuration. - /// - /// # Arguments - /// - `current_height`: All undecided/pending data with `height <= current_height` will be removed - async fn clean_stale_consensus_data(&self, current_height: Height) -> Result<(), Self::Error>; -} - -impl PruningService for &T -where - T: PruningService + ?Sized, -{ - type Error = T::Error; - - async fn prune_decided_blocks(&self) -> Result, Self::Error> { - T::prune_decided_blocks(*self).await - } - - async fn prune_historical_certs( - &self, - latest_height: Height, - ) -> Result, Self::Error> { - T::prune_historical_certs(*self, latest_height).await - } - - async fn clean_stale_consensus_data(&self, current_height: Height) -> Result<(), Self::Error> { - T::clean_stale_consensus_data(*self, current_height).await - } -} - -pub struct ProdPruningService<'a> { - store: &'a Store, - config: &'a PruningConfig, -} - -impl<'a> ProdPruningService<'a> { - pub fn new(store: &'a Store, config: &'a PruningConfig) -> Self { - Self { store, config } - } -} - -impl<'a> PruningService for ProdPruningService<'a> { - type Error = StoreError; - - async fn prune_decided_blocks(&self) -> Result, StoreError> { - self.store.prune_blocks().await - } - - async fn prune_historical_certs( - &self, - latest_height: Height, - ) -> Result, StoreError> { - if !self.config.enabled() { - return Ok(Vec::new()); - } - - let retain_height = self.config.effective_certificates_min_height(latest_height); - - info!(height = %latest_height, %retain_height, "Pruning historical data"); - self.store.prune_historical_certs(retain_height).await - } - - async fn clean_stale_consensus_data(&self, current_height: Height) -> Result<(), StoreError> { - self.store.clean_stale_consensus_data(current_height).await - } -} diff --git a/crates/consensus-db/src/store.rs b/crates/consensus-db/src/store.rs deleted file mode 100644 index cafb67a..0000000 --- a/crates/consensus-db/src/store.rs +++ /dev/null @@ -1,2916 +0,0 @@ -// Copyright 2025 Circle Internet Group, Inc. All rights reserved. -// -// SPDX-License-Identifier: Apache-2.0 -// -// Licensed under the Apache License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. -// You may obtain a copy of the License at -// -// http://www.apache.org/licenses/LICENSE-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// See the License for the specific language governing permissions and -// limitations under the License. - -use std::collections::BTreeMap; -use std::mem::size_of; -use std::ops::RangeBounds; -use std::path::{Path, PathBuf}; -use std::sync::Arc; -use std::time::{Duration, Instant}; - -use crate::invalid_payloads::InvalidPayload; - -use alloy_rpc_types_engine::ExecutionPayloadV3; -use bytesize::ByteSize; -use redb::{ReadableTable, ReadableTableMetadata, WriteTransaction}; -use thiserror::Error; -use tokio::task::JoinHandle; -use tracing::{debug, error, info, warn}; - -use arc_consensus_types::evidence::StoredMisbehaviorEvidence; -use arc_consensus_types::{ - Address, ArcContext, BlockHash, CommitCertificateType, Height, ProposalParts, - StoredCommitCertificate, B256, -}; -use malachitebft_app_channel::app::types::core::{CommitCertificate, Round}; -use malachitebft_core_types::Height as _; -use malachitebft_proto::Error as ProtoError; - -use crate::decoder::{ - decode_block, decode_certificate, decode_execution_payload, decode_invalid_payloads, - decode_misbehavior_evidence, decode_proposal_monitor_data, decode_proposal_parts, DecodeError, -}; -use crate::encoder::{ - encode_block, encode_certificate, encode_execution_payload, encode_invalid_payloads, - encode_misbehavior_evidence, encode_proposal_monitor_data, encode_proposal_parts, -}; -use crate::invalid_payloads::StoredInvalidPayloads; -use crate::keys::{HeightKey, PendingPartsKey, UndecidedBlockKey}; -use crate::metrics::DbMetrics; -use arc_consensus_types::block::{ConsensusBlock, DecidedBlock}; -use arc_consensus_types::proposal_monitor::ProposalMonitor; - -/// Store error. -#[derive(Debug, Error)] -pub enum StoreError { - #[error("Database error: {0}")] - Database(#[from] redb::DatabaseError), - - #[error("Database upgrade error: {0}")] - Upgrade(#[from] redb::UpgradeError), - - #[error("Storage error: {0}")] - Storage(#[from] redb::StorageError), - - #[error("Table error: {0}")] - Table(#[from] redb::TableError), - - #[error("Commit error: {0}")] - Commit(#[from] redb::CommitError), - - #[error("Transaction error: {0}")] - Transaction(#[from] redb::TransactionError), - - #[error("Failed to encode/decode Protobuf: {0}")] - Protobuf(#[from] ProtoError), - - #[error("Failed to decode: {0}")] - Decode(#[from] DecodeError), - - #[error("Failed to join on task: {0}")] - TaskJoin(#[from] tokio::task::JoinError), - - #[error("Migration error: {0}")] - Migration(String), - - #[error("{0}")] - Other(Box), -} - -impl StoreError { - pub fn other(error: E) -> Self - where - E: Into>, - { - StoreError::Other(error.into()) - } -} - -/// Per-table deletion counts from a rollback operation. -#[derive(Debug, Default)] -pub struct RollbackReport { - pub certificates: usize, - pub decided_blocks: usize, - pub invalid_payloads: usize, - pub misbehavior_evidence: usize, - pub proposal_monitor_data: usize, - pub undecided_blocks: usize, - pub pending_proposal_parts: usize, -} - -/// Default number of heights deleted per write transaction during rollback. -pub const ROLLBACK_BATCH_SIZE: u64 = 1000; - -/// Roll back all consensus tables to `target_height`, removing entries above it. -/// -/// When `dry_run` is true, entries are counted but the transaction is aborted -/// so the database remains unchanged. -/// -/// Deletions are batched in chunks of `batch_size` heights. Each batch is a -/// single write transaction across all 7 tables, so a crash mid-rollback leaves -/// the database consistent at some intermediate height. -/// -/// Operates directly on a `redb::Database` so CLI commands can call it without -/// constructing a full `Store`. -pub fn rollback_to_height( - db: &redb::Database, - target_height: Height, - batch_size: u64, - dry_run: bool, -) -> Result { - assert!(batch_size > 0, "batch_size must be > 0"); - - let mut report = RollbackReport::default(); - - let current_height = { - let tx = db.begin_read()?; - let table = tx.open_table(CERTIFICATES_TABLE)?; - match table.last()? { - Some((k, _)) => k.value(), - None => return Ok(report), - } - }; - - if current_height <= target_height { - return Ok(report); - } - - // Work from the tip downward so a crash leaves a contiguous prefix, not a gap. - - // Redb retain_in expects a range exclusive of the upper bound, so we increment - let stop = target_height.increment(); - let mut range_high = current_height.increment(); - loop { - if range_high == stop { - break; - } - let range_low = range_high.saturating_sub(batch_size).max(stop); - - // Round::Nil serializes to -1 and BlockHash::ZERO is all zeros, so these - // are the minimum (round, hash) values — the composite range captures all - // entries within the height range regardless of round or block hash. - let composite_start = (range_low, Round::Nil, BlockHash::ZERO); - let composite_end = (range_high, Round::Nil, BlockHash::ZERO); - - info!( - batch_start = %range_low, - batch_end = %range_high, - "Rolling back height batch" - ); - - let tx = db.begin_write()?; - { - let mut t = tx.open_table(CERTIFICATES_TABLE)?; - t.retain_in(range_low..range_high, |_, _| { - report.certificates = report.certificates.saturating_add(1); - dry_run - })?; - - let mut t = tx.open_table(DECIDED_BLOCKS_TABLE)?; - t.retain_in(range_low..range_high, |_, _| { - report.decided_blocks = report.decided_blocks.saturating_add(1); - dry_run - })?; - - let mut t = tx.open_table(INVALID_PAYLOADS_TABLE)?; - t.retain_in(range_low..range_high, |_, _| { - report.invalid_payloads = report.invalid_payloads.saturating_add(1); - dry_run - })?; - - let mut t = tx.open_table(MISBEHAVIOR_EVIDENCE_TABLE)?; - t.retain_in(range_low..range_high, |_, _| { - report.misbehavior_evidence = report.misbehavior_evidence.saturating_add(1); - dry_run - })?; - - let mut t = tx.open_table(PROPOSAL_MONITOR_DATA_TABLE)?; - t.retain_in(range_low..range_high, |_, _| { - report.proposal_monitor_data = report.proposal_monitor_data.saturating_add(1); - dry_run - })?; - - let mut t = tx.open_table(UNDECIDED_BLOCKS_TABLE)?; - t.retain_in(composite_start..composite_end, |_, _| { - report.undecided_blocks = report.undecided_blocks.saturating_add(1); - dry_run - })?; - - let mut t = tx.open_table(PENDING_PROPOSAL_PARTS_TABLE)?; - t.retain_in(composite_start..composite_end, |_, _| { - report.pending_proposal_parts = report.pending_proposal_parts.saturating_add(1); - dry_run - })?; - } - - if dry_run { - tx.abort()?; - } else { - tx.commit()?; - } - - info!( - updated_head = range_low.as_u64().saturating_sub(1), - dry_run = dry_run, - "Batch committed" - ); - range_high = range_low; - } - - Ok(report) -} - -pub const CERTIFICATES_TABLE: redb::TableDefinition> = - redb::TableDefinition::new("certificates"); - -pub const DECIDED_BLOCKS_TABLE: redb::TableDefinition> = - redb::TableDefinition::new("decided_blocks"); - -pub const UNDECIDED_BLOCKS_TABLE: redb::TableDefinition> = - redb::TableDefinition::new("undecided_blocks"); - -pub const PENDING_PROPOSAL_PARTS_TABLE: redb::TableDefinition> = - redb::TableDefinition::new("pending_proposal_parts"); - -pub const MISBEHAVIOR_EVIDENCE_TABLE: redb::TableDefinition> = - redb::TableDefinition::new("misbehavior_evidence"); - -pub const INVALID_PAYLOADS_TABLE: redb::TableDefinition> = - redb::TableDefinition::new("invalid_payloads"); - -pub const PROPOSAL_MONITOR_DATA_TABLE: redb::TableDefinition> = - redb::TableDefinition::new("proposal_monitor_data"); - -#[tracing::instrument(name = "db.monitor", skip_all)] -async fn monitor_db_size(path: PathBuf, metrics: DbMetrics, interval: Duration) { - let mut interval = tokio::time::interval(interval); - - loop { - // NOTE: The first tick completes immediately - interval.tick().await; - - match tokio::fs::metadata(&path).await { - Ok(metadata) => { - let size = metadata.len(); - metrics.set_db_size(size); - } - Err(e) => { - error!("Failed to get database size: {e}"); - } - } - } -} - -struct Db { - db: redb::Database, - path: PathBuf, - metrics: DbMetrics, -} - -impl Db { - /// Number of blocks `reth` is expected to forget upon recovery in the worst case. - const RETH_AMNESIA_HEIGHT_COUNT: u64 = 10_000; - /// Maximum number of records to prune per invocation. - const PRUNE_BATCH_LIMIT: usize = 300; - /// How many heights before pruning logs info messages. - const PRUNING_LOG_INFO_HEIGHTS: u64 = 100; - - #[tracing::instrument(name = "db", skip_all)] - fn new( - path: impl AsRef, - metrics: DbMetrics, - db_upgrade: DbUpgrade, - cache_size: ByteSize, - ) -> Result { - let path = path.as_ref().to_owned(); - let db_exists = path.exists(); - - #[allow(clippy::cast_possible_truncation)] // 32-bit targets won't have multi-GB caches - let cache_size_bytes = cache_size.as_u64() as usize; - let mut db = redb::Database::builder() - .set_cache_size(cache_size_bytes) - .set_repair_callback(|session| { - let status = session.progress() * 100.0; - info!("Database repair in progress: {status:.2}%"); - }) - .create(&path)?; - - info!(path = %path.display(), "Database opened"); - - // Upgrade the database file format to v3 for forward compatibility with redb 3.x. - // This is a one-time operation that makes the database file readable by redb 3.x. - if db.upgrade()? { - info!("Upgraded database to v3 file format"); - } else { - debug!("Database already in v3 file format"); - } - - // Perform schema migration if needed - let coordinator = crate::migrations::MigrationCoordinator::new(db); - - if db_upgrade == DbUpgrade::Skip { - info!("Skipping database schema upgrade as requested"); - } else if coordinator.needs_migration(db_exists)? { - info!("Database schema migration required"); - let stats = coordinator.migrate()?; - - info!( - tables = stats.tables_migrated, - scanned = stats.records_scanned, - upgraded = stats.records_upgraded, - skipped = stats.records_skipped, - duration = ?stats.duration, - "Database upgrade completed successfully" - ); - } - - // Retrieve the database from the coordinator after migration - let db = coordinator.into_db(); - - Ok(Self { db, path, metrics }) - } - - /// Spawn a background task to monitor the database size at regular intervals. - fn spawn_monitor(&self, interval: Duration) -> JoinHandle<()> { - tokio::task::spawn(monitor_db_size( - self.path.clone(), - self.metrics.clone(), - interval, - )) - } - - // Metric byte counters accumulate bounded DB record sizes — overflow is not reachable. - fn get_payload(&self, height: Height) -> Result, StoreError> { - let start = Instant::now(); - let mut read_bytes = 0; - - let tx = self.db.begin_read()?; - - let table = tx.open_table(DECIDED_BLOCKS_TABLE)?; - let payload = table.get(&height)?; - let payload = payload - .map(|value| { - let bytes = value.value(); - #[allow(clippy::arithmetic_side_effects)] - { - read_bytes += bytes.len(); - } - decode_execution_payload(&bytes) - }) - .transpose() - .map_err(StoreError::from)?; - - self.update_read_metrics(read_bytes, size_of::(), start.elapsed()); - - Ok(payload) - } - - /// Get the commit certificate for the given height. - /// - height: The height to get the certificate for. If None, get the latest certificate. - fn get_certificate( - &self, - height: Option, - ) -> Result, StoreError> { - let start = Instant::now(); - let mut read_bytes = 0; - - let tx = self.db.begin_read()?; - let table = tx.open_table(CERTIFICATES_TABLE)?; - - let bytes = if let Some(height) = height { - table.get(&height)?.map(|v| v.value()) - } else { - table.last()?.map(|(_, v)| v.value()) - }; - - let result = bytes.and_then(|bytes| { - #[allow(clippy::arithmetic_side_effects)] - { - read_bytes += bytes.len(); - } - decode_certificate(&bytes).ok() - }); - - self.update_read_metrics(read_bytes, size_of::(), start.elapsed()); - - Ok(result) - } - - /// Get the decided block for the given height. - fn get_decided_block(&self, height: Height) -> Result, StoreError> { - let start = Instant::now(); - let mut read_bytes = 0; - - let tx = self.db.begin_read()?; - - let payload = { - let table = tx.open_table(DECIDED_BLOCKS_TABLE)?; - let payload = table.get(&height)?.map(|value| value.value()); - payload - .map(|bytes| { - #[allow(clippy::arithmetic_side_effects)] - { - read_bytes += bytes.len(); - } - decode_execution_payload(&bytes) - }) - .transpose() - .map_err(StoreError::from)? - }; - - let certificate = { - let table = tx.open_table(CERTIFICATES_TABLE)?; - let value = table.get(&height)?; - value.and_then(|value| { - let bytes = value.value(); - #[allow(clippy::arithmetic_side_effects)] - { - read_bytes += bytes.len(); - } - decode_certificate(&bytes).ok() - }) - }; - - self.update_read_metrics(read_bytes, size_of::(), start.elapsed()); - - let decided_block = payload.zip(certificate).map(|(execution_payload, cert)| { - DecidedBlock::new(execution_payload, cert.certificate) - }); - - Ok(decided_block) - } - - /// Store the decided block and its certificate. - fn insert_decided_block( - &self, - decided_block: DecidedBlock, - proposer: Address, - ) -> Result<(), StoreError> { - let start = Instant::now(); - let mut write_bytes = 0; - - let height = decided_block.height(); - let tx = self.db.begin_write()?; - - { - let mut blocks = tx.open_table(DECIDED_BLOCKS_TABLE)?; - let block_bytes = encode_execution_payload(&decided_block.execution_payload); - #[allow(clippy::arithmetic_side_effects)] - { - write_bytes += block_bytes.len(); - } - blocks.insert(height, block_bytes)?; - } - - self.insert_certificate( - &tx, - decided_block.certificate, - CommitCertificateType::Minimal, - Some(proposer), - )?; - - tx.commit()?; - - self.update_write_metrics(write_bytes, start.elapsed()); - - Ok(()) - } - - /// Extend an existing commit certificate with additional precommit signatures collected during finalization. - fn extend_certificate( - &self, - certificate: CommitCertificate, - ) -> Result<(), StoreError> { - let height = certificate.height; - let tx = self.db.begin_write()?; - - // Check that a certificate already exists for this height and only allow insert if it does. - let existing = { - let table = tx.open_table(CERTIFICATES_TABLE)?; - let bytes = table.get(&height)?.map(|v| v.value()).ok_or_else(|| - StoreError::other(format!( - "Cannot extend certificate for height {height} because no existing certificate was found" - )))?; - - decode_certificate(&bytes).map_err(|e| { - StoreError::other(format!( - "Failed to decode existing certificate for height {height}: {e}" - )) - })? - }; - - // Check that the new certificate is a valid extension of the existing one: - // same height, same round, same value, superset of commit signatures. - - if certificate.height != existing.certificate.height { - return Err(StoreError::other(format!( - "Cannot extend certificate for height {height} because existing certificate has different height {}", - existing.certificate.height - ))); - } - - if certificate.round != existing.certificate.round { - return Err(StoreError::other(format!( - "Cannot extend certificate for height {height} because existing certificate has different round {}", - existing.certificate.round - ))); - } - - if certificate.value_id != existing.certificate.value_id { - return Err(StoreError::other(format!( - "Cannot extend certificate for height {height} because existing certificate has different value_id {}", - existing.certificate.value_id - ))); - } - - for signature in &existing.certificate.commit_signatures { - if !certificate.commit_signatures.contains(signature) { - return Err(StoreError::other(format!( - "Cannot extend certificate for height {height} because existing \ - commit signature from validator {} is missing in the new certificate", - signature.address - ))); - } - } - - self.insert_certificate( - &tx, - certificate, - CommitCertificateType::Extended, - existing.proposer, - )?; - - tx.commit()?; - - Ok(()) - } - - fn insert_certificate( - &self, - tx: &WriteTransaction, - certificate: CommitCertificate, - certificate_type: CommitCertificateType, - proposer: Option
, - ) -> Result<(), StoreError> { - let start = Instant::now(); - - let height = certificate.height; - - let stored = StoredCommitCertificate { - certificate, - certificate_type, - proposer, - }; - - let encoded_certificate = encode_certificate(&stored)?; - let write_bytes = encoded_certificate.len(); - - { - let mut certificates = tx.open_table(CERTIFICATES_TABLE)?; - certificates.insert(height, encoded_certificate)?; - } - self.update_write_metrics(write_bytes, start.elapsed()); - - Ok(()) - } - - /// Store misbehavior evidence for a given height. - fn insert_misbehavior_evidence( - &self, - evidence: StoredMisbehaviorEvidence, - ) -> Result<(), StoreError> { - let start = Instant::now(); - - let height = evidence.height; - let tx = self.db.begin_write()?; - - let encoded = encode_misbehavior_evidence(&evidence)?; - let write_bytes = encoded.len(); - - { - let mut table = tx.open_table(MISBEHAVIOR_EVIDENCE_TABLE)?; - table.insert(height, encoded)?; - } - - tx.commit()?; - - self.update_write_metrics(write_bytes, start.elapsed()); - - Ok(()) - } - - /// Store or update proposal monitor data for a given height. - fn insert_proposal_monitor_data(&self, data: ProposalMonitor) -> Result<(), StoreError> { - let start = Instant::now(); - - let height = data.height; - let tx = self.db.begin_write()?; - - let encoded = encode_proposal_monitor_data(&data)?; - let write_bytes = encoded.len(); - - { - let mut table = tx.open_table(PROPOSAL_MONITOR_DATA_TABLE)?; - table.insert(height, encoded)?; - } - - tx.commit()?; - - self.update_write_metrics(write_bytes, start.elapsed()); - - Ok(()) - } - - /// Get proposal monitor data for the given height. - /// - height: The height to get the data for. If None, get the latest. - fn get_proposal_monitor_data( - &self, - height: Option, - ) -> Result, StoreError> { - let start = Instant::now(); - let mut read_bytes = 0; - - let tx = self.db.begin_read()?; - let table = tx.open_table(PROPOSAL_MONITOR_DATA_TABLE)?; - - let bytes = if let Some(height) = height { - table.get(&height)?.map(|v| v.value()) - } else { - table.last()?.map(|(_, v)| v.value()) - }; - - let data = bytes - .map(|bytes| { - #[allow(clippy::arithmetic_side_effects)] - { - read_bytes += bytes.len(); - } - decode_proposal_monitor_data(&bytes) - }) - .transpose() - .map_err(StoreError::from)?; - - self.update_read_metrics(read_bytes, size_of::(), start.elapsed()); - - Ok(data) - } - - /// Get misbehavior evidence for the given height. - /// - height: The height when the evidence was collected. If None, use the latest height. - fn get_misbehavior_evidence( - &self, - height: Option, - ) -> Result, StoreError> { - let start = Instant::now(); - let mut read_bytes = 0; - - let Some(max_height) = self.max_height()? else { - // No blocks yet - return Ok(None); - }; - - // Determine the effective height to query - let height = match height { - Some(height) if height > max_height => return Ok(None), - Some(height) => height, - None => max_height, - }; - - let tx = self.db.begin_read()?; - let table = tx.open_table(MISBEHAVIOR_EVIDENCE_TABLE)?; - - let bytes = table.get(&height)?.map(|v| v.value()); - - let evidence = bytes - .map(|bytes| { - #[allow(clippy::arithmetic_side_effects)] - { - read_bytes += bytes.len(); - } - decode_misbehavior_evidence(&bytes) - }) - .transpose() - .map_err(StoreError::from)?; - - self.update_read_metrics(read_bytes, size_of::(), start.elapsed()); - - // Return empty evidence if no record found - Ok(Some(evidence.unwrap_or_else(|| { - StoredMisbehaviorEvidence::empty(height) - }))) - } - - /// Get invalid payloads for the given height. - /// - /// - height: The height to get the invalid payloads for. If None, use the latest height. - fn get_invalid_payloads( - &self, - height: Option, - ) -> Result, StoreError> { - let start = Instant::now(); - let mut read_bytes = 0; - - let Some(max_height) = self.max_height()? else { - // No blocks yet - return Ok(None); - }; - - // Determine the effective height to query - let height = match height { - Some(height) if height > max_height => return Ok(None), - Some(height) => height, - None => max_height, - }; - - let tx = self.db.begin_read()?; - let table = tx.open_table(INVALID_PAYLOADS_TABLE)?; - - let bytes = table.get(&height)?.map(|v| v.value()); - - let payloads = bytes - .map(|bytes| { - #[allow(clippy::arithmetic_side_effects)] - { - read_bytes += bytes.len(); - } - decode_invalid_payloads(&bytes) - }) - .transpose() - .map_err(StoreError::from)?; - - self.update_read_metrics(read_bytes, size_of::(), start.elapsed()); - - // Return empty payloads if no record found - Ok(Some( - payloads.unwrap_or_else(|| StoredInvalidPayloads::empty(height)), - )) - } - - /// Atomically appends an invalid payload to the stored collection for its - /// height, creating the collection if none exists yet. - /// - /// The read and write happen inside a single redb write transaction, so - /// concurrent appends for the same height cannot lose data. - fn append_invalid_payload(&self, invalid_payload: InvalidPayload) -> Result<(), StoreError> { - let start = Instant::now(); - let height = invalid_payload.height; - let tx = self.db.begin_write()?; - - let mut stored = { - let table = tx.open_table(INVALID_PAYLOADS_TABLE)?; - match table.get(&height)? { - Some(v) => decode_invalid_payloads(&v.value())?, - None => StoredInvalidPayloads { - height, - payloads: vec![], - }, - } - }; - - stored.add_invalid_payload(invalid_payload); - let encoded = encode_invalid_payloads(&stored)?; - let write_bytes = encoded.len(); - - { - let mut table = tx.open_table(INVALID_PAYLOADS_TABLE)?; - table.insert(height, encoded)?; - } - - tx.commit()?; - self.update_write_metrics(write_bytes, start.elapsed()); - Ok(()) - } - - /// Get the undecided block for the given height, round, and block hash. - #[tracing::instrument(skip(self))] - pub fn get_undecided_block( - &self, - height: Height, - round: Round, - block_hash: BlockHash, - ) -> Result, StoreError> { - let start = Instant::now(); - let mut read_bytes = 0; - - let tx = self.db.begin_read()?; - let table = tx.open_table(UNDECIDED_BLOCKS_TABLE)?; - - let value = if let Ok(Some(value)) = table.get(&(height, round, block_hash)) { - let bytes = value.value(); - #[allow(clippy::arithmetic_side_effects)] - { - read_bytes += bytes.len(); - } - - let block = decode_block(&bytes)?; - Some(block) - } else { - None - }; - - self.update_read_metrics( - read_bytes, - size_of::<(Height, Round, BlockHash)>(), - start.elapsed(), - ); - - Ok(value) - } - - #[tracing::instrument(skip(self))] - pub fn get_undecided_block_by_height_and_block_hash( - &self, - height: Height, - block_hash: BlockHash, - ) -> Result, StoreError> { - let start = Instant::now(); - let mut read_bytes = 0; - - let tx = self.db.begin_read()?; - let table = tx.open_table(UNDECIDED_BLOCKS_TABLE)?; - - // Iterate through all entries to find one that matches height and block_hash - for result in table.iter()? { - let (key, value) = result?; - let (key_height, _key_round, key_block_hash) = key.value(); - - if key_height == height && key_block_hash == block_hash { - let bytes = value.value(); - #[allow(clippy::arithmetic_side_effects)] - { - read_bytes += bytes.len(); - } - - let block = decode_block(&bytes)?; - - self.update_read_metrics( - read_bytes, - size_of::<(Height, BlockHash)>(), - start.elapsed(), - ); - - return Ok(Some(block)); - } - } - - self.update_read_metrics( - read_bytes, - size_of::<(Height, BlockHash)>(), - start.elapsed(), - ); - - Ok(None) - } - - /// Get all undecided blocks for a given height and round (sync version) - fn get_undecided_blocks( - &self, - height: Height, - round: Round, - ) -> Result, StoreError> { - let start = Instant::now(); - let mut read_bytes = 0; - - let tx = self.db.begin_read()?; - let table = tx.open_table(UNDECIDED_BLOCKS_TABLE)?; - - let mut blocks = Vec::new(); - - // Iterate through all entries that start with (height, round, *) - let range_start = (height, round, BlockHash::new([0; 32])); - #[allow(clippy::arithmetic_side_effects)] // round + 1 for range upper bound - let range_end = ( - height, - Round::from(round.as_i64() + 1), - BlockHash::new([0; 32]), - ); - - for result in table.range(range_start..range_end)? { - let (key, value) = result?; - let key_tuple = key.value(); - - // Only include entries that match exactly height and round - if key_tuple.0 == height && key_tuple.1 == round { - let bytes = value.value(); - #[allow(clippy::arithmetic_side_effects)] - { - read_bytes += bytes.len(); - } - - let proposal = decode_block(&bytes)?; - blocks.push(proposal); - } - } - - #[allow(clippy::arithmetic_side_effects)] - let key_bytes = size_of::<(Height, Round, BlockHash)>() * blocks.len(); - self.update_read_metrics(read_bytes, key_bytes, start.elapsed()); - - Ok(blocks) - } - - fn insert_undecided_block( - &self, - tx: &mut WriteTransaction, - block: ConsensusBlock, - ) -> Result<(), StoreError> { - let start = Instant::now(); - - let key = (block.height, block.round, block.block_hash()); - let value = encode_block(&block); - - { - let mut table = tx.open_table(UNDECIDED_BLOCKS_TABLE)?; - table.insert(key, value.to_vec())?; - } - - self.update_write_metrics(value.len(), start.elapsed()); - - Ok(()) - } - - fn remove_pending_proposal_parts( - &self, - tx: &mut WriteTransaction, - parts: ProposalParts, - ) -> Result<(), StoreError> { - let start = Instant::now(); - - let key = (parts.height(), parts.round(), B256::new(parts.hash())); - - { - let mut table = tx.open_table(PENDING_PROPOSAL_PARTS_TABLE)?; - table.remove(key)?; - } - - self.update_delete_metrics(start.elapsed()); - - Ok(()) - } - - fn height_range( - &self, - table: &Table, - range: impl RangeBounds, - limit: usize, - ) -> Result, StoreError> - where - Table: redb::ReadableTable>, - { - Ok(table - .range(range)? - .take(limit) - .flatten() - .map(|(key, _)| key.value()) - .collect::>()) - } - - fn get_pending_proposal_parts( - &self, - height: Height, - round: Round, - ) -> Result, StoreError> { - let start = Instant::now(); - let mut read_bytes = 0; - - let tx = self.db.begin_read()?; - let table = tx.open_table(PENDING_PROPOSAL_PARTS_TABLE)?; - - let mut proposals = Vec::new(); - for result in table.iter()? { - let (key, value) = result?; - let (h, r, _) = key.value(); - - if h == height && r == round { - let bytes = value.value(); - #[allow(clippy::arithmetic_side_effects)] - { - read_bytes += bytes.len(); - } - - let parts = decode_proposal_parts(&bytes)?; - proposals.push(parts); - } - } - - #[allow(clippy::arithmetic_side_effects)] - let key_bytes = size_of::<(Height, Round, BlockHash)>() * proposals.len(); - self.update_read_metrics(read_bytes, key_bytes, start.elapsed()); - - Ok(proposals) - } - - /// Return the total number of stored pending proposal parts. - fn get_pending_proposal_parts_count(&self) -> Result { - let start = Instant::now(); - let tx = self.db.begin_read()?; - let table = tx.open_table(PENDING_PROPOSAL_PARTS_TABLE)?; - - // redb returns u64; table won't exceed usize on any supported target - #[allow(clippy::cast_possible_truncation)] - let count = table.len()? as usize; - - self.update_read_metrics(0, 0, start.elapsed()); - - Ok(count) - } - - /// Return the number of stored pending proposal parts grouped by height. - /// Each entry in the returned vector contains the height and the count of - /// proposal parts currently stored in `PENDING_PROPOSAL_PARTS_TABLE` for that - /// height. - fn get_pending_proposal_parts_counts(&self) -> Result, StoreError> { - let start = Instant::now(); - let mut read_bytes = 0usize; - let mut counts = BTreeMap::new(); - let mut total_keys = 0usize; - - let tx = self.db.begin_read()?; - let table = tx.open_table(PENDING_PROPOSAL_PARTS_TABLE)?; - - for result in table.iter()? { - let (key, value) = result?; - let (height, _, _) = key.value(); - - let bytes = value.value(); - #[allow(clippy::arithmetic_side_effects)] - { - read_bytes += bytes.len(); - *counts.entry(height).or_insert(0) += 1; - total_keys += 1; - } - } - - #[allow(clippy::arithmetic_side_effects)] - let key_bytes = size_of::<(Height, Round, BlockHash)>() * total_keys; - self.update_read_metrics(read_bytes, key_bytes, start.elapsed()); - - Ok(counts.into_iter().collect()) - } - - fn insert_pending_proposal_parts( - &self, - parts: ProposalParts, - max_pending_parts: usize, - current_height: Height, - ) -> Result { - let start = Instant::now(); - - // Calculate max allowed height (inclusive). - // For current_height=10 and max_pending_parts=4, we allow heights 10, 11, 12, 13. - let max_allowed_height = current_height.increment_by( - max_pending_parts - .checked_sub(1) - .expect("max_pending_parts must be > 0") as u64, - ); - - // Do not insert if proposal is outside the allowed range (too far in the future) - if parts.height() > max_allowed_height { - return Ok(false); - } - - let mut inserted = false; - - let key = (parts.height(), parts.round(), B256::new(parts.hash())); - let value = encode_proposal_parts(&parts)?; - - // Insert the proposal if there is room in the table - let tx = self.db.begin_write()?; - { - let mut table = tx.open_table(PENDING_PROPOSAL_PARTS_TABLE)?; - - #[allow(clippy::cast_possible_truncation)] - let count = table.len()? as usize; - - if count < max_pending_parts { - table.insert(key, value.to_vec())?; - inserted = true; - } - } - tx.commit()?; - - if inserted { - self.update_write_metrics(value.len(), start.elapsed()); - } - - Ok(inserted) - } - - /// Enforce pending proposals limit on startup. - /// Called during Store::open to clean up any excess proposals from previous runs. - /// Removes all proposals outside the valid range and trims to max_pending_proposals. - fn enforce_pending_proposals_limit( - &self, - max_pending_proposals: usize, - current_height: Height, - ) -> Result, StoreError> { - let tx = self.db.begin_write()?; - - let removed = { - let mut table = tx.open_table(PENDING_PROPOSAL_PARTS_TABLE)?; - - // Calculate max allowed height (inclusive). - // For current_height=10 and max_pending_parts=4, we allow heights 10, 11, 12, 13. - let max_allowed_height = current_height.increment_by( - max_pending_proposals - .checked_sub(1) - .expect("max_pending_proposals must be > 0") as u64, - ); - - // Collect all keys, categorize as: stale, within_range, or too_far - // Keys are sorted by (height, round, hash) ascending - let (stale, within_range, too_far) = table - .iter()? - .filter_map(|result| result.ok().map(|(k, _)| k.value())) - .fold( - (vec![], vec![], vec![]), - |(mut stale, mut within, mut far), key| { - let (height, _, _) = key; - if height < current_height { - stale.push(key); - } else if height <= max_allowed_height { - within.push(key); - } else { - far.push(key); - } - (stale, within, far) - }, - ); - - // Determine keys to remove: - // - all stale entries from previous heights - // - all entries that are too far in the future - // - entries beyond max_pending_proposals within valid range (keep lowest, remove highest) - let mut keys_to_remove = stale; - keys_to_remove.extend(too_far); - - if within_range.len() > max_pending_proposals { - // within_range is sorted by (height, round, hash) ascending - // Keep first max_pending_proposals, remove the rest (highest heights/rounds) - keys_to_remove.extend(within_range.iter().skip(max_pending_proposals).copied()); - } - - if !keys_to_remove.is_empty() { - info!( - entries_to_remove = keys_to_remove.len(), - max_pending_proposals, - current_height = %current_height, - %max_allowed_height, - "Cleaning proposals on startup" - ); - - for key_to_remove in &keys_to_remove { - table.remove(key_to_remove)?; - } - } - - keys_to_remove - }; - - tx.commit()?; - - Ok(removed) - } - - /// Clean up undecided blocks and pending proposals for heights <= current_height. - /// This should always run when committing a block, regardless of pruning configuration. - fn clean_stale_consensus_data(&self, current_height: Height) -> Result<(), StoreError> { - let start = Instant::now(); - - let tx = self.db.begin_write()?; - - { - // Remove all undecided blocks with height <= current_height - let mut undecided = tx.open_table(UNDECIDED_BLOCKS_TABLE)?; - undecided.retain(|k, _| k.0 > current_height)?; - - // Remove all pending proposals with height <= current_height - let mut pending = tx.open_table(PENDING_PROPOSAL_PARTS_TABLE)?; - pending.retain(|k, _| k.0 > current_height)?; - } - - tx.commit()?; - - self.metrics.observe_delete_time(start.elapsed()); - - Ok(()) - } - - /// Prune up to PRUNE_BATCH_LIMIT historical certificates below retain_height. - /// This should only run when pruning is enabled. - fn prune_historical_certs(&self, retain_height: Height) -> Result, StoreError> { - let start = Instant::now(); - - let curr_height = self.max_height()?.unwrap_or_default(); - let log_info = curr_height.as_u64() % Self::PRUNING_LOG_INFO_HEIGHTS == 0; - - let keys = { - let tx_read = self.db.begin_read()?; - let certificates = tx_read.open_table(CERTIFICATES_TABLE)?; - self.height_range(&certificates, ..retain_height, Self::PRUNE_BATCH_LIMIT)? - }; - - if keys.is_empty() { - if log_info { - info!(%retain_height, %curr_height, "No historical certificates to prune in this batch"); - } else { - debug!(%retain_height, %curr_height, "No historical certificates to prune in this batch"); - } - self.update_delete_metrics(start.elapsed()); - return Ok(keys); - } - - // Remove collected keys within a short write transaction - let tx_write = self.db.begin_write()?; - { - let mut certificates = tx_write.open_table(CERTIFICATES_TABLE)?; - for h in &keys { - let _ = certificates.remove(h)?; - } - } - tx_write.commit()?; - - let first_pruned = keys.first().expect("'keys' should not be empty").as_u64(); - let last_pruned = keys.last().expect("'keys' should not be empty").as_u64(); - if log_info { - info!( - pruned_count = keys.len(), - %retain_height, - current_height = %curr_height, - %first_pruned, - %last_pruned, - "Pruned historical certificates batch" - ); - } else { - debug!( - pruned_count = keys.len(), - %retain_height, - current_height = %curr_height, - %first_pruned, - %last_pruned, - "Pruned historical certificates batch" - ); - } - - self.update_delete_metrics(start.elapsed()); - - Ok(keys) - } - - /// Prune up to PRUNE_BATCH_LIMIT blocks below (current_height - `EL_AMNESIA_HEIGHT_COUNT`). - /// This should run regardless of whether pruning is enabled. - fn prune_blocks(&self) -> Result, StoreError> { - let start = Instant::now(); - - let curr_height = self.max_height()?.unwrap_or_default(); - let log_info = curr_height.as_u64() % Self::PRUNING_LOG_INFO_HEIGHTS == 0; - let retain_height = curr_height.saturating_sub(Self::RETH_AMNESIA_HEIGHT_COUNT); - - let keys = { - let tx_read = self.db.begin_read()?; - let decided = tx_read.open_table(DECIDED_BLOCKS_TABLE)?; - self.height_range(&decided, ..retain_height, Self::PRUNE_BATCH_LIMIT)? - }; - - if keys.is_empty() { - if log_info { - info!(%retain_height, %curr_height, "No decided blocks to prune in this batch"); - } else { - debug!(%retain_height, %curr_height, "No decided blocks to prune in this batch"); - } - self.update_delete_metrics(start.elapsed()); - return Ok(keys); - } - - // Remove collected keys within a short write transaction - let tx_write = self.db.begin_write()?; - { - let mut decided = tx_write.open_table(DECIDED_BLOCKS_TABLE)?; - for h in &keys { - let _ = decided.remove(h)?; - } - } - tx_write.commit()?; - - let first_pruned = keys.first().expect("'keys' should not be empty").as_u64(); - let last_pruned = keys.last().expect("'keys' should not be empty").as_u64(); - - if log_info { - info!( - pruned_count = keys.len(), - %retain_height, - current_height = %curr_height, - %first_pruned, - %last_pruned, - "Pruned decided blocks batch" - ); - } else { - debug!( - pruned_count = keys.len(), - %retain_height, - current_height = %curr_height, - %first_pruned, - %last_pruned, - "Pruned decided blocks batch" - ); - } - - self.update_delete_metrics(start.elapsed()); - - Ok(keys) - } - - fn limit_height(&self, min: bool) -> Result, StoreError> { - let start = Instant::now(); - let tx = self.db.begin_read()?; - let table = tx.open_table(CERTIFICATES_TABLE)?; - - let maybe_key = if min { table.first()? } else { table.last()? }; - if let Some((key, block)) = maybe_key { - self.update_read_metrics(block.value().len(), size_of::(), start.elapsed()); - - return Ok(Some(key.value())); - } - - Ok(None) - } - - fn min_height(&self) -> Result, StoreError> { - self.limit_height(true) - } - - fn max_height(&self) -> Result, StoreError> { - self.limit_height(false) - } - - /// Create a savepoint in the database to ensure the allocator state table is up to date. - /// Doing this before shutting down the database can help avoid repair on next startup. - #[tracing::instrument(name = "db::savepoint", skip_all)] - fn savepoint(&self) { - if self.ensure_allocator_state_table().is_err() { - warn!("Failed to write allocator state table. Repair may be required at restart."); - } - } - - /// Make a new quick-repair commit to update the allocator state table - fn ensure_allocator_state_table(&self) -> Result<(), StoreError> { - debug!("Writing allocator state table"); - - let mut tx = self.db.begin_write()?; - tx.set_quick_repair(true); - tx.commit()?; - - Ok(()) - } - - fn create_tables(&self) -> Result<(), StoreError> { - let tx = self.db.begin_write()?; - - // Implicitly creates the tables if they do not exist yet - let _ = tx.open_table(CERTIFICATES_TABLE)?; - let _ = tx.open_table(DECIDED_BLOCKS_TABLE)?; - let _ = tx.open_table(UNDECIDED_BLOCKS_TABLE)?; - let _ = tx.open_table(PENDING_PROPOSAL_PARTS_TABLE)?; - let _ = tx.open_table(PROPOSAL_MONITOR_DATA_TABLE)?; - let _ = tx.open_table(MISBEHAVIOR_EVIDENCE_TABLE)?; - let _ = tx.open_table(INVALID_PAYLOADS_TABLE)?; - - tx.commit()?; - - Ok(()) - } - - fn update_read_metrics(&self, read_bytes: usize, key_read_bytes: usize, read_time: Duration) { - self.metrics.add_read_bytes(read_bytes as u64); - self.metrics.add_key_read_bytes(key_read_bytes as u64); - self.metrics.observe_read_time(read_time); - } - - fn update_write_metrics(&self, write_bytes: usize, write_time: Duration) { - self.metrics.add_write_bytes(write_bytes as u64); - self.metrics.observe_write_time(write_time); - } - - fn update_delete_metrics(&self, delete_time: Duration) { - self.metrics.observe_delete_time(delete_time); - } -} - -/// Whether to perform database schema upgrade on startup or skip it. -#[derive(Debug, Clone, Copy, Default, PartialEq, Eq)] -pub enum DbUpgrade { - /// Skip database schema upgrade on startup. - Skip, - - /// Perform database schema upgrade on startup if needed. - #[default] - Perform, -} - -/// Internal store for the application data. -#[derive(Clone)] -pub struct Store { - db: Arc, -} - -impl Store { - /// Open the store. - /// - path: The path to the store. - /// - metrics: The metrics to use for the store. - /// - skip_db_upgrade: Skip database schema upgrade on startup. - /// - cache_size: Cache size in bytes for the database page cache. - pub async fn open( - path: impl AsRef, - metrics: DbMetrics, - db_upgrade: DbUpgrade, - cache_size: ByteSize, - ) -> Result { - let path = path.as_ref().to_owned(); - tokio::task::spawn_blocking(move || { - let db = Db::new(path, metrics, db_upgrade, cache_size)?; - db.create_tables()?; - Ok(Self { db: Arc::new(db) }) - }) - .await? - } - - /// Spawn a background task to monitor the database size at regular intervals. - pub fn spawn_monitor(&self, interval: Duration) -> JoinHandle<()> { - self.db.spawn_monitor(interval) - } - - /// Get the minimum height in the DB. - pub async fn min_height(&self) -> Result, StoreError> { - let db = Arc::clone(&self.db); - tokio::task::spawn_blocking(move || db.min_height()).await? - } - - /// Get the maximum height in the DB. - pub async fn max_height(&self) -> Result, StoreError> { - let db = Arc::clone(&self.db); - tokio::task::spawn_blocking(move || db.max_height()).await? - } - - /// Get the execution layer payload for the given height. - /// - height: The height to get the EL payload for. - pub async fn get_payload( - &self, - height: Height, - ) -> Result, StoreError> { - let db = Arc::clone(&self.db); - - tokio::task::spawn_blocking(move || db.get_payload(height)).await? - } - - /// Get the commit certificate for the given height. - /// - height: The height to get the commit certificate for. If None, get the latest certificate. - pub async fn get_certificate( - &self, - height: Option, - ) -> Result, StoreError> { - let db = Arc::clone(&self.db); - tokio::task::spawn_blocking(move || db.get_certificate(height)).await? - } - - /// Get the decided block for the given height. - /// - height: The height to get the decided block for. - pub async fn get_decided_block( - &self, - height: Height, - ) -> Result, StoreError> { - let db = Arc::clone(&self.db); - tokio::task::spawn_blocking(move || db.get_decided_block(height)).await? - } - - /// Store the decided block. - /// - certificate: The certificate for the decided value. - /// - block: The block to store. - pub async fn store_decided_block( - &self, - certificate: CommitCertificate, - execution_payload: ExecutionPayloadV3, - proposer: Address, - ) -> Result<(), StoreError> { - let decided_block = DecidedBlock::new(execution_payload, certificate); - - let db = Arc::clone(&self.db); - tokio::task::spawn_blocking(move || db.insert_decided_block(decided_block, proposer)) - .await? - } - - /// Extend an existing commit certificate with additional precommit signatures collected during finalization. - /// - certificate: The extended certificate to store. - /// Must have the same height as the existing certificate and a superset of commit signatures. - pub async fn extend_certificate( - &self, - certificate: CommitCertificate, - ) -> Result<(), StoreError> { - let db = Arc::clone(&self.db); - tokio::task::spawn_blocking(move || db.extend_certificate(certificate)).await? - } - - /// Store misbehavior evidence for a given height. - /// - evidence: The misbehavior evidence to store. - pub async fn store_misbehavior_evidence( - &self, - evidence: StoredMisbehaviorEvidence, - ) -> Result<(), StoreError> { - let db = Arc::clone(&self.db); - tokio::task::spawn_blocking(move || db.insert_misbehavior_evidence(evidence)).await? - } - - /// Get the misbehavior evidence for the given height. - /// - height: The height to get the misbehavior evidence for. If None, use the latest height. - /// - /// Returns: - /// - `Some(evidence)` with actual or empty evidence for finalized heights - /// - `None` if the requested height was not yet finalized - pub async fn get_misbehavior_evidence( - &self, - height: Option, - ) -> Result, StoreError> { - let db = Arc::clone(&self.db); - tokio::task::spawn_blocking(move || db.get_misbehavior_evidence(height)).await? - } - - /// Store proposal monitor data for a given height. - /// - data: The proposal monitor data to store. - pub async fn store_proposal_monitor_data( - &self, - data: ProposalMonitor, - ) -> Result<(), StoreError> { - let db = Arc::clone(&self.db); - tokio::task::spawn_blocking(move || db.insert_proposal_monitor_data(data)).await? - } - - /// Get the proposal monitor data for the given height. - /// - height: The height to get the data for. If None, get the latest. - pub async fn get_proposal_monitor_data( - &self, - height: Option, - ) -> Result, StoreError> { - let db = Arc::clone(&self.db); - tokio::task::spawn_blocking(move || db.get_proposal_monitor_data(height)).await? - } - - /// Get invalid payloads for the given height. - /// - /// - height: The height to get the invalid payloads for. If None, use the latest height. - pub async fn get_invalid_payloads( - &self, - height: Option, - ) -> Result, StoreError> { - let db = Arc::clone(&self.db); - tokio::task::spawn_blocking(move || db.get_invalid_payloads(height)).await? - } - - /// Appends an invalid payload to the stored collection for its height, creating - /// the collection if none exists yet. - /// - /// The underlying read-modify-write runs inside a single - /// redb write transaction, so concurrent calls for the - /// same height are serialised by the database and cannot - /// lose data. - pub async fn append_invalid_payload( - &self, - invalid_payload: InvalidPayload, - ) -> Result<(), StoreError> { - let db = Arc::clone(&self.db); - tokio::task::spawn_blocking(move || db.append_invalid_payload(invalid_payload)).await? - } - - /// Store the undecided block. - /// - block: The block to store. - pub async fn store_undecided_block(&self, block: ConsensusBlock) -> Result<(), StoreError> { - let db = Arc::clone(&self.db); - tokio::task::spawn_blocking(move || -> Result<(), StoreError> { - let mut tx = db.db.begin_write()?; - db.insert_undecided_block(&mut tx, block)?; - tx.commit()?; - Ok(()) - }) - .await? - } - - /// Get the undecided block for the given height and round. - /// - height: The height to get the undecided block for. - /// - round: The round to get the undecided block for. - pub async fn get_undecided_block( - &self, - height: Height, - round: Round, - block_hash: BlockHash, - ) -> Result, StoreError> { - let db = Arc::clone(&self.db); - - tokio::task::spawn_blocking(move || db.get_undecided_block(height, round, block_hash)) - .await? - } - - /// Get the undecided block for the given height and block hash (ignoring round). - /// Returns the first undecided block found that matches the height and block hash. - /// - height: The height to get the undecided block for. - /// - block_hash: The block hash to get the undecided block for. - pub async fn get_undecided_block_by_height_and_block_hash( - &self, - height: Height, - block_hash: BlockHash, - ) -> Result, StoreError> { - let db = Arc::clone(&self.db); - - tokio::task::spawn_blocking(move || { - db.get_undecided_block_by_height_and_block_hash(height, block_hash) - }) - .await? - } - - /// Retrieves all undecided blocks for a given height and round. - /// Called by the application when starting a new round and existing blocks need to be replayed. - pub async fn get_undecided_blocks( - &self, - height: Height, - round: Round, - ) -> Result, StoreError> { - let db = Arc::clone(&self.db); - tokio::task::spawn_blocking(move || db.get_undecided_blocks(height, round)).await? - } - - /// Stores a pending proposal - /// Called by the application when receiving new proposals from peers. - /// - /// ## Returns - /// Whether or not the proposal was stored (may be rejected if limit is reached). - pub async fn store_pending_proposal_parts( - &self, - value: ProposalParts, - max_pending_proposals: usize, - current_height: Height, - ) -> Result { - let db = Arc::clone(&self.db); - tokio::task::spawn_blocking(move || { - db.insert_pending_proposal_parts(value, max_pending_proposals, current_height) - }) - .await? - } - - /// Enforce limits on the pending proposals table. - /// - /// Called at application at startup to clean up any excess proposals from previous runs. - /// Uses the provided current_height to determine which proposals to keep. - /// - /// ## Returns - /// A list of removed proposals, identified by their height, round, and block_hash. - pub async fn enforce_pending_proposals_limit( - &self, - max_pending_proposals: usize, - current_height: Height, - ) -> Result, StoreError> { - let db = Arc::clone(&self.db); - tokio::task::spawn_blocking(move || { - db.enforce_pending_proposals_limit(max_pending_proposals, current_height) - }) - .await? - } - - /// Retrieves all pending proposal parts for a given height and round. - /// Called by the application when starting a new round and existing proposals need to be replayed. - pub async fn get_pending_proposal_parts( - &self, - height: Height, - round: Round, - ) -> Result, StoreError> { - let db = Arc::clone(&self.db); - tokio::task::spawn_blocking(move || db.get_pending_proposal_parts(height, round)).await? - } - - /// Return the total number of stored pending proposal parts. - pub async fn get_pending_proposal_parts_count(&self) -> Result { - let db = Arc::clone(&self.db); - tokio::task::spawn_blocking(move || db.get_pending_proposal_parts_count()).await? - } - - /// Return the number of stored pending proposal parts grouped by height. - /// Each entry in the returned vector contains the height and the count of - /// proposal parts currently stored in `PENDING_PROPOSAL_PARTS_TABLE` for that - /// height. - pub async fn get_pending_proposal_parts_counts( - &self, - ) -> Result, StoreError> { - let db = Arc::clone(&self.db); - tokio::task::spawn_blocking(move || db.get_pending_proposal_parts_counts()).await? - } - - /// Atomically removes pending proposal parts and stores the undecided block. - /// This ensures that if the process fails, the parts are not lost. - pub async fn remove_pending_parts_and_store_undecided_block( - &self, - parts: ProposalParts, - block: ConsensusBlock, - ) -> Result<(), StoreError> { - let db = Arc::clone(&self.db); - tokio::task::spawn_blocking(move || { - let mut tx = db.db.begin_write()?; - db.remove_pending_proposal_parts(&mut tx, parts)?; - db.insert_undecided_block(&mut tx, block)?; - tx.commit()?; - Ok(()) - }) - .await? - } - - /// Clean up stale consensus data (undecided blocks and pending proposals) for committed heights. - /// Should always be called when committing a block, regardless of pruning configuration. - /// - current_height: All undecided/pending data with height <= current_height will be removed - pub async fn clean_stale_consensus_data( - &self, - current_height: Height, - ) -> Result<(), StoreError> { - let db = Arc::clone(&self.db); - tokio::task::spawn_blocking(move || db.clean_stale_consensus_data(current_height)).await? - } - - /// Prune historical certificates. - /// Should only be called when pruning is enabled. - /// - retain_height: The minimum height to retain for certificates. - pub async fn prune_historical_certs( - &self, - retain_height: Height, - ) -> Result, StoreError> { - let db = Arc::clone(&self.db); - tokio::task::spawn_blocking(move || db.prune_historical_certs(retain_height)).await? - } - - /// Prune decided blocks. - /// Should be called regardless of whether pruning is enabled. - /// As historical decided blocks are fetched from EL, we just keep a minimum number of blocks - /// in the DB to help with EL's amnesia upon recovery. - pub async fn prune_blocks(&self) -> Result, StoreError> { - let db = Arc::clone(&self.db); - tokio::task::spawn_blocking(move || db.prune_blocks()).await? - } - - /// Create a savepoint in the database to ensure the allocator state table is up to date. - /// Doing this before shutting down the database can help avoid repair on next startup. - pub fn savepoint(&self) { - info!("Creating database savepoint..."); - self.db.savepoint() - } -} - -#[cfg(test)] -mod tests { - use super::*; - use alloy_rpc_types_engine::ExecutionPayloadV3; - use arbitrary::Unstructured; - use arc_consensus_types::signing::Signature; - use arc_consensus_types::{ - Address, CommitSignature, ProposalData, ProposalFin, ProposalInit, ProposalPart, ValueId, - Vote, - }; - use bytes::Bytes; - use malachitebft_app_channel::app::types::core::Validity; - use tempfile::tempdir; - - const TEST_CACHE_SIZE: ByteSize = ByteSize::mib(64); - - async fn create_store() -> Store { - let dir = tempdir().unwrap(); - Store::open( - dir.path().join("db"), - DbMetrics::default(), - DbUpgrade::Skip, - TEST_CACHE_SIZE, - ) - .await - .unwrap() - } - - async fn create_test_proposal_parts( - height: Height, - round: Round, - proposer: Address, - ) -> ProposalParts { - // Create a dummy signature for testing - let signature = Signature::from_bytes([0u8; 64]); - - let parts = vec![ - ProposalPart::Init(ProposalInit::new(height, round, Round::Nil, proposer)), - ProposalPart::Data(ProposalData::new(Bytes::from_static(b"test data"))), - ProposalPart::Fin(ProposalFin::new(signature)), - ]; - - ProposalParts::new(parts).unwrap() - } - - fn arbitrary_payload() -> ExecutionPayloadV3 { - Unstructured::new(&[0xab; 1024]) - .arbitrary::() - .unwrap() - } - - #[tokio::test] - async fn test_store_and_get_decided_block() { - let store = create_store().await; - - let height = Height::new(1); - let round = Round::new(0); - let payload = arbitrary_payload(); - let block_hash = payload.payload_inner.payload_inner.block_hash; - let value_id = ValueId::new(block_hash); - let cert = CommitCertificate::::new(height, round, value_id, vec![]); - let block = ConsensusBlock { - height, - round, - valid_round: round, - proposer: Address::new([0u8; 20]), - validity: Validity::Valid, - execution_payload: payload, - signature: None, - }; - - store - .store_decided_block( - cert.clone(), - block.execution_payload.clone(), - block.proposer, - ) - .await - .unwrap(); - - let retrieved = store.get_decided_block(height).await.unwrap(); - - assert!(retrieved.is_some()); - let retrieved = retrieved.unwrap(); - assert_eq!(retrieved.execution_payload, block.execution_payload); - assert_eq!(retrieved.certificate.height, cert.height); - - let retrieved_payload = store.get_payload(height).await.unwrap(); - assert!(retrieved_payload.is_some()); - let retrieved_payload = retrieved_payload.unwrap(); - assert_eq!(retrieved.execution_payload, retrieved_payload); - } - - #[tokio::test] - async fn test_store_extended_certificate() { - use malachitebft_core_types::{NilOrVal, SignedMessage}; - - let store = create_store().await; - - let height = Height::new(1); - let round = Round::new(0); - let payload = arbitrary_payload(); - let block_hash = payload.payload_inner.payload_inner.block_hash; - let value_id = ValueId::new(block_hash); - - // Create signed precommits - let signature = Signature::from_bytes([0xab; 64]); - let addresses = [ - Address::new([1u8; 20]), - Address::new([2u8; 20]), - Address::new([3u8; 20]), - ]; - let commits: Vec<_> = addresses - .iter() - .map(|addr| { - let vote = Vote::new_precommit(height, round, NilOrVal::Val(value_id), *addr); - SignedMessage::new(vote, signature) - }) - .collect(); - - let cert = CommitCertificate::::new(height, round, value_id, commits); - - // First store a decided block to create the initial minimal certificate - store - .store_decided_block(cert.clone(), payload, Address::new([0u8; 20])) - .await - .unwrap(); - - let mut stored = store.get_certificate(Some(height)).await.unwrap().unwrap(); - - assert_eq!(stored.certificate_type, CommitCertificateType::Minimal); - assert_eq!(stored.certificate.commit_signatures.len(), 3); - - // Add one more signature to the certificate to simulate extension during finalization - let additional_commit_signature = { - let signature = Signature::from_bytes([0xcd; 64]); - let address = Address::new([4u8; 20]); - CommitSignature::new(address, signature) - }; - stored - .certificate - .commit_signatures - .push(additional_commit_signature); - - // Store the extended certificate - store - .extend_certificate(stored.certificate.clone()) - .await - .unwrap(); - - // Retrieve and verify - let retrieved = store.get_certificate(Some(height)).await.unwrap().unwrap(); - assert_eq!(retrieved.certificate_type, CommitCertificateType::Extended); - assert_eq!(retrieved.certificate.height, cert.height); - assert_eq!(retrieved.certificate.round, cert.round); - assert_eq!(retrieved.certificate.value_id, cert.value_id); - assert_eq!(retrieved.certificate.commit_signatures.len(), 4); - } - - #[tokio::test] - async fn test_extend_certificate_without_existing_fails() { - let store = create_store().await; - let height = Height::new(1); - let round = Round::new(0); - let value_id = ValueId::new(B256::random()); - let cert = CommitCertificate::::new(height, round, value_id, vec![]); - - let result = store.extend_certificate(cert).await; - assert!(result.is_err()); - } - - #[tokio::test] - async fn test_get_certificate_not_found() { - let store = create_store().await; - - // Height was never stored - let result = store.get_certificate(Some(Height::new(999))).await.unwrap(); - assert!(result.is_none()); - - let result = store.get_certificate(None).await.unwrap(); - assert!(result.is_none()); - } - - #[tokio::test] - async fn test_store_misbehavior_evidence() { - use arc_consensus_types::evidence::{ - DoubleVote, StoredMisbehaviorEvidence, ValidatorEvidence, - }; - use malachitebft_core_types::{NilOrVal, SignedMessage}; - - let store = create_store().await; - - let height = Height::new(5); - let round = Round::new(0); - let signature = Signature::from_bytes([0xab; 64]); - let address = Address::new([1u8; 20]); - - // Store a block at the height first (to establish max_height) - store_block_at_height(&store, height).await; - - // Create two conflicting signed votes - let value_id_1 = ValueId::new(BlockHash::repeat_byte(0x11)); - let value_id_2 = ValueId::new(BlockHash::repeat_byte(0x22)); - - let vote1 = Vote::new_precommit(height, round, NilOrVal::Val(value_id_1), address); - let vote2 = Vote::new_precommit(height, round, NilOrVal::Val(value_id_2), address); - - let signed_vote1 = SignedMessage::new(vote1, signature); - let signed_vote2 = SignedMessage::new(vote2, signature); - - let double_vote = DoubleVote { - first: signed_vote1, - second: signed_vote2, - }; - - let validator_evidence = ValidatorEvidence { - address, - double_votes: vec![double_vote], - double_proposals: vec![], - }; - - let evidence = StoredMisbehaviorEvidence { - height, - validators: vec![validator_evidence], - }; - - // Store the evidence - store - .store_misbehavior_evidence(evidence.clone()) - .await - .unwrap(); - - // Retrieve and verify - let retrieved = store - .get_misbehavior_evidence(Some(height)) - .await - .unwrap() - .unwrap(); - - assert_eq!(retrieved.height, evidence.height); - assert_eq!(retrieved.validators.len(), 1); - assert_eq!(retrieved.validators[0].address, address); - assert_eq!(retrieved.validators[0].double_votes.len(), 1); - assert!(retrieved.validators[0].double_proposals.is_empty()); - } - - #[tokio::test] - async fn test_get_misbehavior_evidence_not_found() { - let store = create_store().await; - - // Height was never stored - let result = store - .get_misbehavior_evidence(Some(Height::new(9999))) - .await - .unwrap(); - assert!(result.is_none()); - - let result = store.get_misbehavior_evidence(None).await.unwrap(); - assert!(result.is_none()); - } - - #[tokio::test] - async fn test_store_and_get_undecided_block() { - let store = create_store().await; - - let height = Height::new(2); - let round = Round::new(1); - let block = ConsensusBlock { - height, - round, - valid_round: round, - proposer: Address::new([0u8; 20]), - validity: Validity::Valid, - execution_payload: arbitrary_payload(), - signature: None, - }; - - store.store_undecided_block(block.clone()).await.unwrap(); - - let retrieved = store - .get_undecided_block(height, round, block.block_hash()) - .await - .unwrap(); - - assert!(retrieved.is_some()); - assert_eq!(retrieved.unwrap(), block); - } - - #[tokio::test] - #[allow(clippy::arithmetic_side_effects, clippy::cast_possible_truncation)] - async fn test_pending_proposal_parts_counts() { - // no proposal parts - let store = create_store().await; - let counts = store.get_pending_proposal_parts_counts().await.unwrap(); - assert!(counts.is_empty()); - - // proposal parts exist in multiple heights - let height_a = Height::new(3); - for r in 0..3 { - let round = Round::new(r); - let proposer = Address::new([r as u8; 20]); - let parts = create_test_proposal_parts(height_a, round, proposer).await; - let inserted = store - .store_pending_proposal_parts(parts, 100, Height::new(1)) - .await - .unwrap(); - - assert!(inserted, "Proposal parts should be inserted successfully"); - } - - let height_b = Height::new(7); - for r in 0..2 { - let round = Round::new(10 + r); - let proposer = Address::new([(10 + r) as u8; 20]); - let parts = create_test_proposal_parts(height_b, round, proposer).await; - let inserted = store - .store_pending_proposal_parts(parts, 100, Height::new(1)) - .await - .unwrap(); - - assert!(inserted, "Proposal parts should be inserted successfully"); - } - - let counts = store.get_pending_proposal_parts_counts().await.unwrap(); - assert_eq!(counts, vec![(height_a, 3), (height_b, 2)]); - } - - #[tokio::test] - async fn test_prune_historical_certs() { - let store = create_store().await; - - // Insert dummy values at heights 1 to 4. - store_blocks_at_heights(&store, &[1, 2, 3, 4]).await; - - let pruned = store.prune_historical_certs(Height::new(3)).await.unwrap(); - assert_eq!(pruned.len(), 2); - assert_eq!(pruned, vec![Height::new(1), Height::new(2)]); - - // Verify certificates are pruned but blocks remain. - for h in 1..=4 { - let height = Height::new(h); - let cert_exists = store.get_certificate(Some(height)).await.unwrap().is_some(); - let block_exists = store.get_payload(height).await.unwrap().is_some(); - - if h < 3 { - assert!(!cert_exists, "Certificate at height {} should be pruned", h); - assert!(block_exists, "Block at height {} should still exist", h); - } else { - assert!( - cert_exists, - "Certificate at height {} should be retained", - h - ); - assert!(block_exists, "Block at height {} should still exist", h); - } - } - - assert_eq!(store.min_height().await.unwrap().unwrap().as_u64(), 3); - assert_eq!(store.max_height().await.unwrap().unwrap().as_u64(), 4); - } - - #[tokio::test] - async fn test_prune_certs_then_blocks() { - let store = create_store().await; - - let retain_height = 50u64; - let heights_to_insert = vec![1, 29, 30, 49, 50, 51, 129, 130]; - let max_height = heights_to_insert.iter().max().unwrap().to_owned(); - let heights_to_retain = heights_to_insert - .iter() - .filter(|&h| *h >= retain_height) - .map(|&h| Height::new(h)) - .collect::>(); - let heights_to_prune = heights_to_insert - .iter() - .filter(|&h| *h < retain_height) - .map(|&h| Height::new(h)) - .collect::>(); - - store_blocks_at_heights(&store, &heights_to_insert).await; - - // Verify all blocks exist before pruning. - for h in heights_to_insert.iter() { - let height = Height::new(*h); - assert!( - store.get_payload(height).await.unwrap().is_some(), - "Block at height {} should exist before pruning", - h - ); - assert!( - store.get_certificate(Some(height)).await.unwrap().is_some(), - "Certificate at height {} should exist before pruning", - h - ); - } - - // Prune historical certificates up to retain_height. - let pruned_certs = store - .prune_historical_certs(Height::new(retain_height)) - .await - .unwrap(); - assert_eq!(pruned_certs, heights_to_prune); - - // After cert pruning, blocks are still there. - for h in heights_to_prune.iter() { - assert!( - store.get_certificate(Some(*h)).await.unwrap().is_none(), - "Certificate at height {} should be pruned (< retain_height)", - h - ); - assert!( - store.get_payload(*h).await.unwrap().is_some(), - "Block at height {} should still exist after cert pruning", - h - ); - } - for h in heights_to_retain.iter() { - assert!( - store.get_certificate(Some(*h)).await.unwrap().is_some(), - "Certificate at height {} should be retained (>= retain_height)", - h - ); - assert!( - store.get_payload(*h).await.unwrap().is_some(), - "Block at height {} should still exist", - h - ); - } - - assert_eq!( - store.min_height().await.unwrap().unwrap().as_u64(), - retain_height - ); - assert_eq!( - store.max_height().await.unwrap().unwrap().as_u64(), - max_height - ); - - let amnesia_retain_height = heights_to_insert - .iter() - .max() - .unwrap() - .to_owned() - .saturating_sub(Db::RETH_AMNESIA_HEIGHT_COUNT); - let amn_heights_to_retain: Vec = heights_to_insert - .iter() - .filter(|&h| *h >= amnesia_retain_height) - .map(|&h| Height::new(h)) - .collect(); - let amn_heights_to_prune: Vec = heights_to_insert - .iter() - .filter(|&h| *h < amnesia_retain_height) - .map(|&h| Height::new(h)) - .collect(); - - let pruned_blocks = store.prune_blocks().await.unwrap(); - assert_eq!(pruned_blocks, amn_heights_to_prune); - - // Verify blocks below 30 are pruned; 30 and above retained. - for h in amn_heights_to_prune.iter() { - assert!( - store.get_payload(*h).await.unwrap().is_none(), - "Block at height {} should be pruned (< 30)", - h - ); - } - for h in amn_heights_to_retain.iter() { - assert!( - store.get_payload(*h).await.unwrap().is_some(), - "Block at height {} should be retained (>= 30)", - h - ); - } - - // Certificates still define bounds. - assert_eq!( - store.min_height().await.unwrap().unwrap().as_u64(), - retain_height - ); - assert_eq!( - store.max_height().await.unwrap().unwrap().as_u64(), - max_height - ); - } - - #[tokio::test] - async fn test_prune_blocks() { - let store = create_store().await; - - let retain_height = 50u64; - let max_height = retain_height + Db::RETH_AMNESIA_HEIGHT_COUNT; - - let heights_to_insert = vec![ - retain_height.saturating_sub(2), - retain_height.saturating_sub(1), - retain_height, - retain_height + 1, - retain_height + 10, - max_height, - ]; - - let heights_to_retain = heights_to_insert - .iter() - .filter(|&h| *h >= retain_height) - .map(|&h| Height::new(h)) - .collect::>(); - let heights_to_prune = heights_to_insert - .iter() - .filter(|&h| *h < retain_height) - .map(|&h| Height::new(h)) - .collect::>(); - - store_blocks_at_heights(&store, &heights_to_insert).await; - - // Verify all blocks exist before pruning. - for &h in heights_to_insert.iter() { - let height = Height::new(h); - assert!( - store.get_payload(height).await.unwrap().is_some(), - "Block at height {} should exist before pruning", - h - ); - } - - let pruned = store.prune_blocks().await.unwrap(); - - assert_eq!(pruned.len(), heights_to_prune.len(), - "Expected {} blocks to be pruned, but {} were pruned. retain_height={}, RETH_AMNESIA_HEIGHT_COUNT={}", - heights_to_prune.len(), pruned.len(), retain_height, Db::RETH_AMNESIA_HEIGHT_COUNT); - assert_eq!(pruned, heights_to_prune); - - for height in heights_to_prune.iter() { - assert!( - store.get_payload(*height).await.unwrap().is_none(), - "Block at height {} should be pruned (< retain_height {})", - height, - retain_height, - ); - } - - for height in heights_to_retain.iter() { - assert!( - store.get_payload(*height).await.unwrap().is_some(), - "Block at height {} should be retained (>= retain_height {})", - height, - retain_height, - ); - } - - assert_eq!( - store.min_height().await.unwrap().unwrap().as_u64(), - *heights_to_insert.iter().min().unwrap() // min height is based on certificates, which are not pruned here - ); - assert_eq!( - store.max_height().await.unwrap().unwrap(), - Height::new(max_height) - ); - } - - #[tokio::test] - async fn test_prune_blocks_with_empty_store() { - let store = create_store().await; - - let pruned = store.prune_blocks().await.unwrap(); - assert!(pruned.is_empty()); - - assert_eq!(store.min_height().await.unwrap(), None); - assert_eq!(store.max_height().await.unwrap(), None); - } - - #[tokio::test] - async fn test_prune_blocks_with_few_blocks() { - let store = create_store().await; - - let max_test_height = Db::RETH_AMNESIA_HEIGHT_COUNT / 2; - - let heights = (1..=max_test_height).collect::>(); - store_blocks_at_heights(&store, &heights).await; - - let pruned = store.prune_blocks().await.unwrap(); - assert!( - pruned.is_empty(), - "No blocks should be pruned when max_height < RETH_AMNESIA_HEIGHT_COUNT (max_height={}, RETH_AMNESIA_HEIGHT_COUNT={})", - max_test_height, Db::RETH_AMNESIA_HEIGHT_COUNT - ); - - for h in 1..=max_test_height { - let height = Height::new(h); - assert!( - store.get_payload(height).await.unwrap().is_some(), - "Block at height {} should be retained", - h - ); - } - } - - #[tokio::test] - async fn test_prune_historical_certs_batch_cap() { - let store = create_store().await; - - let limit = Db::PRUNE_BATCH_LIMIT as u64; - let retain_height = limit + 50; // more than the limit - let curr_height = retain_height + 2; - - let mut heights = Vec::new(); - heights.extend(1..=curr_height); // insert some above retain_height - - store_blocks_at_heights(&store, &heights).await; - - let pruned1 = store - .prune_historical_certs(Height::new(retain_height)) - .await - .unwrap(); - assert_eq!(pruned1.len() as u64, limit); - let expected1 = (1..=limit).map(Height::new).collect::>(); - assert_eq!(expected1, pruned1); - - let pruned2 = store - .prune_historical_certs(Height::new(retain_height)) - .await - .unwrap(); - assert_eq!(pruned2.len() as u64, retain_height - 1 - limit); - let expected2 = ((limit + 1)..retain_height) - .map(Height::new) - .collect::>(); - assert_eq!(pruned2, expected2); - - assert_eq!( - store.min_height().await.unwrap().unwrap(), - Height::new(retain_height) - ); - assert_eq!( - store.max_height().await.unwrap().unwrap(), - Height::new(retain_height + 2) - ); - - for h in 1..retain_height { - let height = Height::new(h); - assert!( - store.get_payload(height).await.unwrap().is_some(), - "Block at height {} should still exist after cert pruning", - h - ); - } - } - - #[tokio::test] - async fn test_prune_blocks_batch_cap() { - let store = create_store().await; - - let limit = Db::PRUNE_BATCH_LIMIT as u64; - let curr_height = limit + Db::RETH_AMNESIA_HEIGHT_COUNT + 10; // more than one batch - let retain_height = curr_height - Db::RETH_AMNESIA_HEIGHT_COUNT; - - let mut heights = Vec::new(); - heights.extend(1..=curr_height); - - store_blocks_at_heights(&store, &heights).await; - - let pruned1 = store.prune_blocks().await.unwrap(); - assert_eq!(pruned1.len() as u64, limit); - let expected1 = (1..=limit).map(Height::new).collect::>(); - assert_eq!(pruned1, expected1); - - let pruned2 = store.prune_blocks().await.unwrap(); - assert_eq!(pruned2.len() as u64, retain_height - 1 - limit); - let expected2 = ((limit + 1)..retain_height) - .map(Height::new) - .collect::>(); - assert_eq!(pruned2, expected2); - - for h in 1..retain_height { - let height = Height::new(h); - assert!( - store.get_payload(height).await.unwrap().is_none(), - "Block at height {} should be pruned (< retain_height {})", - h, - retain_height - ); - } - for h in retain_height..=curr_height { - let height = Height::new(h); - assert!( - store.get_payload(height).await.unwrap().is_some(), - "Block at height {} should be retained (>= retain_height {})", - h, - retain_height - ); - } - - // Certificates still define bounds, unchanged by block pruning - assert_eq!(store.min_height().await.unwrap().unwrap(), Height::new(1)); - assert_eq!( - store.max_height().await.unwrap().unwrap(), - Height::new(curr_height) - ); - } - - async fn store_block_at_height(store: &Store, height: Height) { - let round = Round::new(0); - let payload = arbitrary_payload(); - let block_hash = payload.payload_inner.payload_inner.block_hash; - let value_id = ValueId::new(block_hash); - let cert = CommitCertificate::::new(height, round, value_id, vec![]); - let block = ConsensusBlock { - height, - round, - valid_round: round, - proposer: Address::new([0u8; 20]), - validity: Validity::Valid, - execution_payload: payload, - signature: None, - }; - store - .store_decided_block(cert, block.execution_payload, block.proposer) - .await - .unwrap(); - } - - /// Helper function to store blocks at multiple heights - async fn store_blocks_at_heights(store: &Store, heights: &[u64]) { - for &h in heights { - store_block_at_height(store, Height::new(h)).await; - } - } - - #[tokio::test] - async fn test_min_decided_value_height() { - let store = create_store().await; - - let heights = [Height::new(42), Height::new(10), Height::new(99)]; - - for &h in &heights { - store_block_at_height(&store, h).await; - } - - let min_height = store.min_height().await; - assert!(min_height.is_ok()); - assert_eq!(min_height.unwrap(), Some(Height::new(10))); - } - - #[tokio::test] - async fn test_max_decided_value_height() { - let store = create_store().await; - - let heights = [Height::new(2), Height::new(100), Height::new(99)]; - - for &h in &heights { - store_block_at_height(&store, h).await; - } - - let max_height = store.max_height().await; - assert!(max_height.is_ok()); - assert_eq!(max_height.unwrap(), Some(Height::new(100))); - } - - #[tokio::test] - async fn test_clean_stale_consensus_data() { - let store = create_store().await; - - // Create undecided blocks at height 2 for rounds 1 and 2 - let height = Height::new(2); - let payload = arbitrary_payload(); - for r in 1..=2 { - let round = Round::new(r as u32); - let block = ConsensusBlock { - height, - round, - valid_round: Round::Nil, - proposer: Address::new([r; 20]), - validity: Validity::Valid, - execution_payload: payload.clone(), - signature: None, - }; - store.store_undecided_block(block).await.unwrap(); - - // Check undecided blocks exist - let undecided_blocks = store.get_undecided_blocks(height, round).await.unwrap(); - assert!( - undecided_blocks.len() == 1, - "1 undecided block should exist at height {} and round {}", - height, - r - ); - } - - let height = Height::new(3); - // Create two pending proposal parts at height 3 for rounds 1 and 2 - for p in 1..=2 { - let proposer = Address::new([p; 20]); - let round = Round::new(p as u32); - let parts = create_test_proposal_parts(height, round, proposer).await; - let inserted = store - .store_pending_proposal_parts(parts, 100, Height::new(1)) - .await - .unwrap(); - assert!( - inserted, - "Proposal parts should be inserted successfully at height {} and round {}", - height, round - ); - - // Check pending parts exist - let pending_parts = store - .get_pending_proposal_parts(height, round) - .await - .unwrap(); - assert!( - pending_parts.len() == 1, - "1 pending part should exist at height {} and round {}", - height, - round - ); - } - - // Clean stale data up to height 2 - // Simulates decision at height 2, undecided blocks at height 2 should be removed, pending parts at height 3 should be kept - store - .clean_stale_consensus_data(Height::new(2)) - .await - .unwrap(); - - // Check undecided blocks are removed - for r in 1..=2 { - let undecided_blocks = store - .get_undecided_blocks(Height::new(2), Round::new(r as u32)) - .await - .unwrap(); - assert!( - undecided_blocks.is_empty(), - "Undecided block should be removed at height {} and round {}", - height, - r - ); - } - - // Check pending parts are kept - for r in 1..=2 { - let pending_parts = store - .get_pending_proposal_parts(Height::new(3), Round::new(r as u32)) - .await - .unwrap(); - assert!( - pending_parts.len() == 1, - "1 pending part should exist at height {} and round {}", - height, - r - ); - } - } - - async fn test_enforce_pending_limit_on_insert( - max_pending: usize, - current_height: u64, - proposals_to_insert: Vec<(u64, u32)>, - proposal_to_reject: (u64, u32), - ) { - let dir = tempdir().unwrap(); - let store = Store::open( - dir.path().join("db"), - DbMetrics::default(), - DbUpgrade::Skip, - TEST_CACHE_SIZE, - ) - .await - .unwrap(); - - let current_height = Height::new(current_height); - let proposer = Address::new([1; 20]); - - // Fill the table with proposals - for (h, r) in &proposals_to_insert { - let height = Height::new(*h); - let round = Round::new(*r); - let parts = create_test_proposal_parts(height, round, proposer).await; - let inserted = store - .store_pending_proposal_parts(parts, max_pending, current_height) - .await - .unwrap(); - assert!( - inserted, - "Proposal at height {h} and round {r} should be inserted", - ); - } - - // Verify we have max_pending entries - let counts = store.get_pending_proposal_parts_counts().await.unwrap(); - let total: usize = counts.iter().map(|(_, count)| count).sum(); - assert_eq!( - total, max_pending, - "Should have exactly {} entries", - max_pending - ); - - // Try to add one more proposal (should be rejected) - let height_reject = Height::new(proposal_to_reject.0); - let round_reject = Round::new(proposal_to_reject.1); - let parts_reject = create_test_proposal_parts(height_reject, round_reject, proposer).await; - let inserted = store - .store_pending_proposal_parts(parts_reject, max_pending, current_height) - .await - .unwrap(); - assert!( - !inserted, - "Proposal at height {} and round {} should be rejected", - proposal_to_reject.0, proposal_to_reject.1 - ); - - // Verify the extra proposal was NOT added (still max_pending entries) - let counts = store.get_pending_proposal_parts_counts().await.unwrap(); - let total: usize = counts.iter().map(|(_, count)| count).sum(); - assert_eq!( - total, max_pending, - "Should still have exactly {} entries, extra was rejected", - max_pending - ); - - // Verify that the rejected proposal is not in the table - let parts_at_reject = store - .get_pending_proposal_parts(height_reject, round_reject) - .await - .unwrap(); - assert_eq!( - parts_at_reject.len(), - 0, - "Proposal at height {} and round {} should not be stored", - proposal_to_reject.0, - proposal_to_reject.1 - ); - - // Verify all original proposals are still there - for (h, r) in &proposals_to_insert { - let height = Height::new(*h); - let round = Round::new(*r); - let parts = store - .get_pending_proposal_parts(height, round) - .await - .unwrap(); - assert_eq!( - parts.len(), - 1, - "Proposal at height {h} and round {r} should be stored", - ); - } - - drop(store); - dir.close().unwrap(); - } - - #[tokio::test] - async fn test_enforce_pending_limit_on_insert_cases() { - // Test with new proposal at height further in the future than max_pending - test_enforce_pending_limit_on_insert( - 4, - 10, - vec![(10, 0), (11, 0), (12, 0), (13, 0)], - (14, 0), - ) - .await; - - // Test with new proposal at height within max_pending range, rejected due to multiple proposals at the same height - test_enforce_pending_limit_on_insert( - 4, - 20, - vec![(20, 0), (20, 1), (21, 0), (22, 0)], - (23, 0), - ) - .await; - } - - #[allow(clippy::arithmetic_side_effects)] - async fn test_enforce_pending_limit_on_startup( - initial_proposals: Vec<(u64, u32)>, - current_height: u64, - max_pending: usize, - expected_remaining: Vec<(u64, u32)>, - ) { - let dir = tempdir().unwrap(); - let store = Store::open( - dir.path().join("db"), - DbMetrics::default(), - DbUpgrade::Skip, - TEST_CACHE_SIZE, - ) - .await - .unwrap(); - - let proposer = Address::new([1; 20]); - - // Insert initial proposals (without any limit enforcement) - for (h, r) in &initial_proposals { - let height = Height::new(*h); - let round = Round::new(*r); - let parts = create_test_proposal_parts(height, round, proposer).await; - // Use a very high max_pending to bypass limit during insertion - let inserted = store - .store_pending_proposal_parts(parts, 1000, Height::new(0)) - .await - .unwrap(); - assert!( - inserted, - "Proposal at height {h} and round {r} should be inserted", - ); - } - - // Verify all proposals were inserted - let counts = store.get_pending_proposal_parts_counts().await.unwrap(); - let total: usize = counts.iter().map(|(_, count)| count).sum(); - assert_eq!( - total, - initial_proposals.len(), - "Should have {} initial entries", - initial_proposals.len() - ); - - // Enforce limit on startup - let current_height = Height::new(current_height); - let removed = store - .enforce_pending_proposals_limit(max_pending, current_height) - .await - .unwrap(); - - // Verify the number of removed proposals matches expectation - let expected_removed_count = initial_proposals.len() - expected_remaining.len(); - assert_eq!( - removed.len(), - expected_removed_count, - "Should have removed {} entries", - expected_removed_count - ); - - // Verify the correct number of proposals remain - let counts = store.get_pending_proposal_parts_counts().await.unwrap(); - let total: usize = counts.iter().map(|(_, count)| count).sum(); - assert_eq!( - total, - expected_remaining.len(), - "Should have {} entries after cleanup", - expected_remaining.len() - ); - - // Verify that expected proposals are present - for (h, r) in &expected_remaining { - let height = Height::new(*h); - let round = Round::new(*r); - let parts = store - .get_pending_proposal_parts(height, round) - .await - .unwrap(); - assert_eq!( - parts.len(), - 1, - "Expected proposal at height {} round {} should be present", - h, - r - ); - } - - // Verify that non-expected proposals are absent - for (h, r) in &initial_proposals { - if !expected_remaining.contains(&(*h, *r)) { - let height = Height::new(*h); - let round = Round::new(*r); - let parts = store - .get_pending_proposal_parts(height, round) - .await - .unwrap(); - assert_eq!( - parts.len(), - 0, - "Unexpected proposal at height {} round {} should be removed", - h, - r - ); - } - } - - drop(store); - dir.close().unwrap(); - } - - #[tokio::test] - async fn test_enforce_pending_limit_on_startup_cases() { - // Remove stale entries (height < current_height) - // current_height=10 means processing height 10 - // Stale: height < 10 (so 8, 9) - // Keep (10, 0), (11, 0), (12, 0) - test_enforce_pending_limit_on_startup( - vec![(8, 0), (9, 0), (10, 0), (11, 0), (12, 0)], - 10, - 5, - vec![(10, 0), (11, 0), (12, 0)], - ) - .await; - - // Remove entries too far in the future - // current_height=10, max_pending=3 allows to keep (10, 0), (11, 0), (12, 0) - test_enforce_pending_limit_on_startup( - vec![(10, 0), (11, 0), (12, 0), (13, 0), (20, 0)], - 10, - 3, - vec![(10, 0), (11, 0), (12, 0)], - ) - .await; - - // Trim to max_pending when within range but exceeds limit - // current_height=10, max_pending=3 allows to keep (10, 0), (11, 0), (12, 0) - test_enforce_pending_limit_on_startup( - vec![(10, 0), (11, 0), (12, 0), (13, 0), (14, 0)], - 10, - 3, - vec![(10, 0), (11, 0), (12, 0)], - ) - .await; - - // Mixed scenario - stale, within range, and too far - // current_height=10, max_pending=3 allows to keep (10, 0), (11, 0), (12, 0) - test_enforce_pending_limit_on_startup( - vec![ - (5, 0), // stale (height < 10) - (9, 0), // stale (height < 10) - (10, 0), // within range - (11, 0), // within range - (12, 0), // within range - (13, 0), // too far (> 10 + 3 - 1 = 12) - (20, 0), // too far - ], - 10, - 3, - vec![(10, 0), (11, 0), (12, 0)], - ) - .await; - - // Same height, different rounds - // current_height=10, max_pending=3 allows to keep (10, 0), (10, 1), (10, 2) - test_enforce_pending_limit_on_startup( - vec![(10, 0), (10, 1), (10, 2), (10, 3), (10, 4)], - 10, - 3, - vec![(10, 0), (10, 1), (10, 2)], - ) - .await; - } -} diff --git a/crates/consensus-db/src/versions.rs b/crates/consensus-db/src/versions.rs deleted file mode 100644 index 8350174..0000000 --- a/crates/consensus-db/src/versions.rs +++ /dev/null @@ -1,232 +0,0 @@ -// Copyright 2025 Circle Internet Group, Inc. All rights reserved. -// -// SPDX-License-Identifier: Apache-2.0 -// -// Licensed under the Apache License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. -// You may obtain a copy of the License at -// -// http://www.apache.org/licenses/LICENSE-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// See the License for the specific language governing permissions and -// limitations under the License. - -use std::fmt; -use std::mem; - -/// Current database schema version -/// Increment this when making schema changes that require migration (any of the following enums are changed) -pub const DB_SCHEMA_VERSION: SchemaVersion = SchemaVersion::V1; - -/// Version type for various stored data -#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)] -#[repr(transparent)] -pub struct SchemaVersion(u8); - -impl SchemaVersion { - pub const V0: Self = Self(0x00); - pub const V1: Self = Self(0x01); - - pub const fn new(v: u8) -> Self { - Self(v) - } - - pub const fn as_u8(&self) -> u8 { - self.0 - } - - pub const fn next(&self) -> Self { - Self(self.0.checked_add(1).expect("schema version overflow")) - } - - pub fn previous(&self) -> Option { - self.0.checked_sub(1).map(Self::new) - } -} - -impl fmt::Display for SchemaVersion { - fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { - write!(f, "v{}", self.0) - } -} - -impl redb::Value for SchemaVersion { - type SelfType<'a> - = SchemaVersion - where - Self: 'a; - - type AsBytes<'a> - = [u8; 1] - where - Self: 'a; - - fn fixed_width() -> Option { - Some(mem::size_of::()) - } - - fn from_bytes<'a>(data: &'a [u8]) -> Self::SelfType<'a> - where - Self: 'a, - { - SchemaVersion(data[0]) - } - - fn as_bytes<'a, 'b: 'a>(value: &'a Self::SelfType<'b>) -> Self::AsBytes<'a> - where - Self: 'b, - { - [value.as_u8()] - } - - fn type_name() -> redb::TypeName { - redb::TypeName::new("SchemaVersion") - } -} - -/// Assert at compile time that `Version` always has the same size as `u8` (ie. 1 byte) -const _: () = assert!(mem::size_of::() == mem::size_of::()); - -/// Execution payload version -/// -/// Do not forget to increment the `DB_SCHEMA_VERSION` when changing this enum. -#[repr(u8)] -#[derive(Debug, Clone, Copy, PartialEq, Eq)] -pub enum ExecutionPayloadVersion { - V3 = 0x03, -} - -impl TryFrom for ExecutionPayloadVersion { - type Error = u8; - - fn try_from(value: u8) -> Result { - match value { - 0x03 => Ok(Self::V3), - _ => Err(value), - } - } -} - -/// Proposal parts version -/// -/// Do not forget to increment the `DB_SCHEMA_VERSION` when changing this enum. -#[repr(u8)] -#[derive(Debug, Clone, Copy, PartialEq, Eq)] -pub enum ProposalPartsVersion { - V1 = 0x01, -} - -impl TryFrom for ProposalPartsVersion { - type Error = u8; - - fn try_from(value: u8) -> Result { - match value { - 0x01 => Ok(Self::V1), - _ => Err(value), - } - } -} - -/// Consensus block version -/// -/// Do not forget to increment the `DB_SCHEMA_VERSION` when changing this enum. -#[repr(u8)] -#[derive(Debug, Clone, Copy, PartialEq, Eq)] -pub enum ConsensusBlockVersion { - V1 = 0x01, -} - -impl TryFrom for ConsensusBlockVersion { - type Error = u8; - - fn try_from(value: u8) -> Result { - match value { - 0x01 => Ok(Self::V1), - _ => Err(value), - } - } -} - -/// Commit certificate version -/// -/// Do not forget to increment the `DB_SCHEMA_VERSION` when changing this enum. -#[repr(u8)] -#[derive(Debug, Clone, Copy, PartialEq, Eq)] -pub enum CommitCertificateVersion { - V1 = 0x01, -} - -impl TryFrom for CommitCertificateVersion { - type Error = u8; - - fn try_from(value: u8) -> Result { - match value { - 0x01 => Ok(Self::V1), - _ => Err(value), - } - } -} - -/// Misbehavior evidence version -/// -/// Do not forget to increment the `DB_SCHEMA_VERSION` when changing this enum. -#[repr(u8)] -#[derive(Debug, Clone, Copy, PartialEq, Eq)] -pub enum MisbehaviorEvidenceVersion { - V1 = 0x01, -} - -impl TryFrom for MisbehaviorEvidenceVersion { - type Error = u8; - - fn try_from(value: u8) -> Result { - match value { - 0x01 => Ok(Self::V1), - _ => Err(value), - } - } -} - -/// Proposal monitor data version -/// -/// Do not forget to increment the `DB_SCHEMA_VERSION` when changing this enum. -#[repr(u8)] -#[derive(Debug, Clone, Copy, PartialEq, Eq)] -pub enum ProposalMonitorDataVersion { - V1 = 0x01, -} - -impl TryFrom for ProposalMonitorDataVersion { - type Error = u8; - - fn try_from(value: u8) -> Result { - match value { - 0x01 => Ok(Self::V1), - _ => Err(value), - } - } -} - -/// Invalid payloads version -/// -/// Do not forget to increment the `DB_SCHEMA_VERSION` -/// when changing this enum. -#[repr(u8)] -#[derive(Debug, Clone, Copy, PartialEq, Eq)] -pub enum InvalidPayloadsVersion { - V1 = 0x01, -} - -impl TryFrom for InvalidPayloadsVersion { - type Error = u8; - - fn try_from(value: u8) -> Result { - match value { - 0x01 => Ok(Self::V1), - _ => Err(value), - } - } -} diff --git a/crates/engine-bench/Cargo.toml b/crates/engine-bench/Cargo.toml deleted file mode 100644 index 5d8b06d..0000000 --- a/crates/engine-bench/Cargo.toml +++ /dev/null @@ -1,38 +0,0 @@ -[package] -name = "arc-engine-bench" -version.workspace = true -edition.workspace = true -repository.workspace = true -license.workspace = true -rust-version.workspace = true -publish.workspace = true - -[[bin]] -name = "arc-engine-bench" -path = "src/main.rs" - -[dependencies] -alloy-genesis = { workspace = true } -alloy-primitives = { workspace = true } -alloy-rpc-types-engine = { workspace = true, features = ["jwt", "serde"] } -arc-eth-engine = { workspace = true } -arc-execution-config = { workspace = true } -arc-version = { workspace = true } -chrono = { workspace = true } -clap = { workspace = true, features = ["derive", "env"] } -color-eyre = { workspace = true } -csv = { workspace = true } -eyre = { workspace = true } -reqwest = { workspace = true, features = ["json", "rustls-tls", "native-tls-vendored"] } -reth-cli = { workspace = true } -serde = { workspace = true, features = ["derive"] } -serde_json = { workspace = true } -tokio = { workspace = true, features = ["macros", "rt-multi-thread"] } -tracing = { workspace = true } -tracing-subscriber = { workspace = true, features = ["env-filter", "fmt"] } - -[dev-dependencies] -tempfile = { workspace = true } - -[lints] -workspace = true diff --git a/crates/engine-bench/README.md b/crates/engine-bench/README.md deleted file mode 100644 index e6aa6f0..0000000 --- a/crates/engine-bench/README.md +++ /dev/null @@ -1,201 +0,0 @@ -# Arc Engine Bench - -Inspired by [`reth-bench`](https://github.com/paradigmxyz/reth/tree/main/bin/reth-bench) from the -upstream Reth project. - -`arc-engine-bench` replays historical blocks into an Arc execution node and measures Engine API import -latency. The current benchmark mode, `new-payload-fcu`, submits each block with -`engine_newPayloadV4`, follows it with `engine_forkchoiceUpdatedV3`, and writes CSV artifacts for -per-block latency and aggregate throughput. - -## CLI - -| Command | Description | -| --- | --- | -| `arc-engine-bench prepare-payload` | Fetches a contiguous source block range and writes a local payload fixture directory with `genesis.json`, `metadata.json`, and `payloads.jsonl`. | -| `arc-engine-bench new-payload-fcu` | Replays a prepared payload fixture into a target execution node with `engine_newPayloadV4` followed by `engine_forkchoiceUpdatedV3`. | - -## What You Need - -- A running `arc-node-execution` instance to benchmark. This is the **target** node. -- A payload fixture directory containing `genesis.json`, `metadata.json`, and `payloads.jsonl` for - the block range you want to replay. -- A source RPC endpoint with the historical blocks you want to replay. This is only needed when you - run `prepare-payload` to create or refresh a fixture. -- The target must already be at block `FROM_BLOCK - 1`. The benchmark verifies that the target head - matches the fixture metadata before replay starts and exits if it does not. -- The target Engine API must be reachable via **IPC** or **authenticated RPC**. Pass either - `--engine-ipc ` or `--engine-rpc-url --jwt-secret ` (mutually exclusive). - -## Example Environment - -Copy this block, adjust it for your setup, then `source` it before running the commands below: - -`BENCH_DATADIR` is the directory where the target node snapshot lives. - -```bash -BENCH_DATADIR=datadir/bench-target -TARGET_ETH_RPC_URL=http://127.0.0.1:7545 -SOURCE_RPC_URL=http://127.0.0.1:8545 -HTTP_PORT=7545 -METRICS_PORT=19001 -FROM_BLOCK=1 -TO_BLOCK=3000 -PAYLOAD_DIR=target/engine-bench/payload-fixture -CHAIN=arc-localdev -# ipc -ENGINE_IPC="$BENCH_DATADIR/reth.ipc" -# rpc -ENGINE_RPC_URL=http://127.0.0.1:7551 -AUTHRPC_PORT=7551 -``` - -## Prepare the Target Node - -The target node must start at the parent of the first replayed block. - -- If `FROM_BLOCK=1`, you can start from a fresh datadir. -- If `FROM_BLOCK>1`, you need the target node at block `FROM_BLOCK - 1` before replay. In - practice, that means either: - - prepare a snapshot at the desired height, or - - sync the node past that height and unwind it back to `FROM_BLOCK - 1`. - -### 1. Create a datadir and JWT secret - -```bash -mkdir -p "$BENCH_DATADIR" -# Only needed for RPC transport: -openssl rand -hex 32 | tr -d '\n' > "$BENCH_DATADIR/jwt.hex" -chmod 600 "$BENCH_DATADIR/jwt.hex" -``` - -### 2. Unwind the target to the replay parent block - -Skip this step when `FROM_BLOCK=1`. If you synced the node past the replay start, stop it before running the unwind command: - -```bash -arc-node-execution stage unwind \ - --chain "$CHAIN" \ - --datadir "$BENCH_DATADIR" \ - to-block "$((FROM_BLOCK - 1))" -``` - -### 3. Start the target node - -**IPC transport:** - -```bash -arc-node-execution node \ - --chain "$CHAIN" \ - --datadir "$BENCH_DATADIR" \ - --dev \ - --disable-discovery \ - --http \ - --http.api=eth \ - --http.port "$HTTP_PORT" \ - --metrics 127.0.0.1:"$METRICS_PORT" \ - --auth-ipc \ - --auth-ipc.path "$ENGINE_IPC" \ - --arc.denylist.enabled -``` - -**RPC transport:** - -```bash -arc-node-execution node \ - --chain "$CHAIN" \ - --datadir "$BENCH_DATADIR" \ - --dev \ - --disable-discovery \ - --http \ - --http.api=eth \ - --http.port "$HTTP_PORT" \ - --metrics 127.0.0.1:"$METRICS_PORT" \ - --authrpc.addr=127.0.0.1 \ - --authrpc.port="$AUTHRPC_PORT" \ - --authrpc.jwtsecret="$BENCH_DATADIR/jwt.hex" \ - --arc.denylist.enabled -``` - -## Prepare the Payload Fixture - -Fetch source blocks `FROM_BLOCK..=TO_BLOCK` once and write them to a local fixture directory: - -```bash -arc-engine-bench prepare-payload \ - --chain "$CHAIN" \ - --source-rpc-url "$SOURCE_RPC_URL" \ - --from "$FROM_BLOCK" \ - --to "$TO_BLOCK" \ - --output-dir "$PAYLOAD_DIR" -``` - -Other flags: - -- `--chain ` sets the chain spec used to record genesis config. Accepts built-in - names (`arc-localdev`, `arc-devnet`, `arc-testnet`) or a path to a genesis JSON file. The default - is `arc-localdev`. -- `--eth-rpc-timeout-ms ` sets the timeout for source Ethereum RPC requests. The - default is `10000` ms. Batch requests use the larger of this value or 30 seconds. -- `--batch-size ` controls source RPC fetch batching. The default is `20`. - -The fixture directory contains: - -| File | Content | -| --- | --- | -| `genesis.json` | Chain genesis configuration (chain ID, hardfork activations, initial state). | -| `metadata.json` | Replay metadata including `from_block`, `to_block`, `payload_count`, and the expected parent block. | -| `payloads.jsonl` | One `ExecutionPayloadV3` JSON document per line, ordered by block number. | - -## Run `new-payload-fcu` - -Replay the prepared fixture into the target node: - -**IPC transport:** - -```bash -arc-engine-bench new-payload-fcu \ - --engine-ipc "$ENGINE_IPC" \ - --target-eth-rpc-url "$TARGET_ETH_RPC_URL" \ - --payload "$PAYLOAD_DIR" -``` - -**RPC transport:** - -```bash -arc-engine-bench new-payload-fcu \ - --engine-rpc-url "$ENGINE_RPC_URL" \ - --jwt-secret "$BENCH_DATADIR/jwt.hex" \ - --target-eth-rpc-url "$TARGET_ETH_RPC_URL" \ - --payload "$PAYLOAD_DIR" -``` - -Other flags: - -- `--output ` writes artifacts to an explicit directory. By default, output goes to - `target/engine-bench/new-payload-fcu-/`. -- `--eth-rpc-timeout-ms ` sets the timeout for target Ethereum RPC requests. The - default is `10000` ms. - -## Live Metrics - -From the repo root, start the monitoring stack: - -```bash -docker compose -f deployments/monitoring.yaml up -d -``` - -The bundled Prometheus config includes an `arc_engine_bench_target` scrape job that reads the -benchmark target from `host.docker.internal:19001`. - -Open Grafana at `http://127.0.0.1:3000`, then open the provisioned `Reth` dashboard and select the -`arc_engine_bench_target` instance. - -## Output Artifacts - -Each run writes to `target/engine-bench/-/` unless you pass `--output`. - -| File | Content | -| --- | --- | -| `combined_latency.csv` | One row per replayed block with block metadata, `new_payload_ms`, `fcu_ms`, `total_ms`, per-block throughput, and cumulative throughput. | -| `summary.csv` | One-row summary with sample count, total gas and txs, wall-clock time, average throughput, and latency percentiles. | diff --git a/crates/engine-bench/src/bench/context.rs b/crates/engine-bench/src/bench/context.rs deleted file mode 100644 index 6e6a2bc..0000000 --- a/crates/engine-bench/src/bench/context.rs +++ /dev/null @@ -1,250 +0,0 @@ -// Copyright 2026 Circle Internet Group, Inc. All rights reserved. -// -// SPDX-License-Identifier: Apache-2.0 -// -// Licensed under the Apache License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. -// You may obtain a copy of the License at -// -// http://www.apache.org/licenses/LICENSE-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// See the License for the specific language governing permissions and -// limitations under the License. - -use crate::cli::CommonArgs; -use arc_eth_engine::{ - engine::EngineAPI, ipc::engine_ipc::EngineIPC, rpc::engine_rpc::EngineRpc, - rpc::ethereum_rpc::EthereumRPC, -}; -use chrono::Utc; -use eyre::{bail, Context}; -use reqwest::Url; -use std::{ - fmt, fs, - path::{Path, PathBuf}, - time::Duration, -}; - -const ETH_BATCH_TIMEOUT_FLOOR: Duration = Duration::from_secs(30); - -/// Which Engine API transport was selected on the CLI. -pub(crate) enum EngineTransport { - Ipc(PathBuf), - Rpc { url: String, jwt_secret: PathBuf }, -} - -impl fmt::Display for EngineTransport { - fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { - match self { - Self::Ipc(path) => write!(f, "ipc:{}", path.display()), - Self::Rpc { url, .. } => write!(f, "rpc:{url}"), - } - } -} - -pub(crate) struct BenchContext { - transport: EngineTransport, - output_dir: PathBuf, - eth_rpc_timeout: Duration, -} - -impl fmt::Debug for BenchContext { - fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { - f.debug_struct("BenchContext") - .field("transport", &self.transport.to_string()) - .field("output_dir", &self.output_dir) - .field("eth_rpc_timeout", &self.eth_rpc_timeout) - .finish() - } -} - -impl BenchContext { - pub(crate) fn new(common: &CommonArgs, mode: &str) -> eyre::Result { - let transport = match (&common.engine_ipc, &common.engine_rpc_url) { - (Some(ipc), None) => EngineTransport::Ipc(ipc.clone()), - (None, Some(url)) => { - let jwt = common - .jwt_secret - .as_ref() - .ok_or_else(|| eyre::eyre!("--jwt-secret is required with --engine-rpc-url"))?; - if !jwt.exists() { - bail!("JWT secret file does not exist: {}", jwt.display()); - } - EngineTransport::Rpc { - url: url.clone(), - jwt_secret: jwt.clone(), - } - } - (None, None) => bail!( - "specify either --engine-ipc or --engine-rpc-url --jwt-secret " - ), - _ => unreachable!("clap group prevents both"), - }; - - Ok(Self { - transport, - output_dir: resolve_output_dir(common, mode)?, - eth_rpc_timeout: Duration::from_millis(common.eth_rpc_timeout_ms), - }) - } - - pub(crate) fn output_dir(&self) -> &Path { - &self.output_dir - } - - pub(crate) fn transport(&self) -> &EngineTransport { - &self.transport - } - - pub(crate) async fn engine(&self) -> eyre::Result> { - match &self.transport { - EngineTransport::Ipc(path) => { - let path_str = path.to_str().ok_or_else(|| { - eyre::eyre!( - "engine IPC socket path is not valid UTF-8: {}", - path.display() - ) - })?; - let ipc = EngineIPC::new(path_str) - .await - .wrap_err("failed to create engine IPC client")?; - Ok(Box::new(ipc) as Box) - } - EngineTransport::Rpc { url, jwt_secret } => { - let rpc = EngineRpc::new( - Url::parse(url).wrap_err("invalid engine RPC URL")?, - jwt_secret.as_path(), - ) - .wrap_err("failed to create engine RPC client")?; - Ok(Box::new(rpc) as Box) - } - } - } - - pub(crate) fn ethereum_rpc(&self, rpc_url: &str, role: &str) -> eyre::Result { - ethereum_rpc_client(rpc_url, role, self.eth_rpc_timeout) - } -} - -pub(crate) fn ethereum_rpc_client( - rpc_url: &str, - role: &str, - eth_rpc_timeout: Duration, -) -> eyre::Result { - EthereumRPC::new_with_timeouts( - Url::parse(rpc_url).wrap_err_with(|| format!("invalid {role} url"))?, - eth_rpc_timeout, - eth_rpc_timeout.max(ETH_BATCH_TIMEOUT_FLOOR), - ) - .wrap_err_with(|| format!("failed to create {role} client")) -} - -fn resolve_output_dir(common: &CommonArgs, mode: &str) -> eyre::Result { - let output = match &common.output { - Some(path) => path.clone(), - None => { - let timestamp = Utc::now().format("%Y%m%dT%H%M%SZ"); - PathBuf::from("target") - .join("engine-bench") - .join(format!("{mode}-{timestamp}")) - } - }; - fs::create_dir_all(&output) - .wrap_err_with(|| format!("failed to create benchmark output dir {}", output.display()))?; - Ok(output) -} - -#[cfg(test)] -mod tests { - use super::*; - use tempfile::TempDir; - - fn common_args_ipc() -> CommonArgs { - CommonArgs { - engine_ipc: Some(PathBuf::from("/tmp/reth.ipc")), - engine_rpc_url: None, - jwt_secret: None, - eth_rpc_timeout_ms: 10_000, - output: None, - } - } - - fn common_args_rpc(jwt_path: PathBuf) -> CommonArgs { - CommonArgs { - engine_ipc: None, - engine_rpc_url: Some("http://127.0.0.1:8551".to_string()), - jwt_secret: Some(jwt_path), - eth_rpc_timeout_ms: 10_000, - output: None, - } - } - - #[test] - fn resolve_output_dir_creates_explicit_output_directory() { - let temp_dir = TempDir::new().unwrap(); - let output_dir = temp_dir.path().join("bench-output"); - let mut args = common_args_ipc(); - args.output = Some(output_dir.clone()); - - let resolved = resolve_output_dir(&args, "new-payload-fcu").unwrap(); - - assert_eq!(resolved, output_dir); - assert!(resolved.is_dir()); - } - - #[test] - fn new_context_selects_ipc_transport() { - let args = common_args_ipc(); - let ctx = BenchContext::new(&args, "test").unwrap(); - assert!(matches!(ctx.transport(), EngineTransport::Ipc(_))); - } - - #[test] - fn new_context_selects_rpc_transport() { - let temp_dir = TempDir::new().unwrap(); - let jwt_path = temp_dir.path().join("jwt.hex"); - fs::write(&jwt_path, "secret").unwrap(); - - let args = common_args_rpc(jwt_path); - let ctx = BenchContext::new(&args, "test").unwrap(); - assert!(matches!(ctx.transport(), EngineTransport::Rpc { .. })); - } - - #[test] - fn new_context_errors_when_neither_transport_specified() { - let args = CommonArgs { - engine_ipc: None, - engine_rpc_url: None, - jwt_secret: None, - eth_rpc_timeout_ms: 10_000, - output: None, - }; - - let err = BenchContext::new(&args, "test").unwrap_err(); - assert!(err.to_string().contains("--engine-ipc")); - } - - #[test] - fn new_context_errors_when_rpc_without_jwt() { - let args = CommonArgs { - engine_ipc: None, - engine_rpc_url: Some("http://127.0.0.1:8551".to_string()), - jwt_secret: None, - eth_rpc_timeout_ms: 10_000, - output: None, - }; - - let err = BenchContext::new(&args, "test").unwrap_err(); - assert!(err.to_string().contains("--jwt-secret")); - } - - #[test] - fn new_context_errors_when_jwt_file_missing() { - let args = common_args_rpc(PathBuf::from("/tmp/nonexistent-jwt.hex")); - let err = BenchContext::new(&args, "test").unwrap_err(); - assert!(err.to_string().contains("does not exist")); - } -} diff --git a/crates/engine-bench/src/bench/fixture.rs b/crates/engine-bench/src/bench/fixture.rs deleted file mode 100644 index 44bad6a..0000000 --- a/crates/engine-bench/src/bench/fixture.rs +++ /dev/null @@ -1,542 +0,0 @@ -// Copyright 2026 Circle Internet Group, Inc. All rights reserved. -// -// SPDX-License-Identifier: Apache-2.0 -// -// Licensed under the Apache License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. -// You may obtain a copy of the License at -// -// http://www.apache.org/licenses/LICENSE-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// See the License for the specific language governing permissions and -// limitations under the License. - -use super::helpers::fmt_hash; -use alloy_genesis::Genesis; -use alloy_primitives::B256; -use alloy_rpc_types_engine::ExecutionPayloadV3; -use eyre::{bail, Context}; -use serde::{Deserialize, Serialize}; -use std::{ - fs::{self, File}, - io::{BufRead, BufReader, BufWriter, Write}, - path::{Path, PathBuf}, -}; - -pub(crate) const GENESIS_FILE_NAME: &str = "genesis.json"; -pub(crate) const METADATA_FILE_NAME: &str = "metadata.json"; -pub(crate) const PAYLOADS_FILE_NAME: &str = "payloads.jsonl"; - -#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] -pub(crate) struct ExpectedParentBlock { - pub block_number: u64, - pub block_hash: B256, -} - -#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] -pub(crate) struct PayloadFixtureMetadata { - pub from_block: u64, - pub to_block: u64, - pub payload_count: u64, - pub expected_parent: ExpectedParentBlock, -} - -impl PayloadFixtureMetadata { - pub(crate) fn validate(&self) -> eyre::Result<()> { - if self.from_block == 0 { - bail!("payload fixture from_block must be greater than 0"); - } - if self.from_block > self.to_block { - bail!("payload fixture from_block must be less than or equal to to_block"); - } - - let expected_count = self.to_block - self.from_block + 1; - if self.payload_count != expected_count { - bail!( - "payload fixture payload_count mismatch: expected {expected_count}, got {}", - self.payload_count - ); - } - - let expected_parent_block_number = self.from_block - 1; - if self.expected_parent.block_number != expected_parent_block_number { - bail!( - "payload fixture expected_parent block number mismatch: expected {expected_parent_block_number}, got {}", - self.expected_parent.block_number - ); - } - - Ok(()) - } -} - -pub(crate) struct PayloadFixtureWriter { - output_dir: PathBuf, - genesis_tmp_path: PathBuf, - metadata_tmp_path: PathBuf, - payloads_tmp_path: PathBuf, - payloads_writer: BufWriter, -} - -impl PayloadFixtureWriter { - pub(crate) fn new(output_dir: &Path) -> eyre::Result { - fs::create_dir_all(output_dir).wrap_err_with(|| { - format!( - "failed to create payload fixture directory {}", - output_dir.display() - ) - })?; - - let genesis_tmp_path = output_dir.join(format!("{GENESIS_FILE_NAME}.tmp")); - let metadata_tmp_path = output_dir.join(format!("{METADATA_FILE_NAME}.tmp")); - let payloads_tmp_path = output_dir.join(format!("{PAYLOADS_FILE_NAME}.tmp")); - remove_file_if_exists(&genesis_tmp_path)?; - remove_file_if_exists(&metadata_tmp_path)?; - remove_file_if_exists(&payloads_tmp_path)?; - - let payloads_writer = BufWriter::new( - File::create(&payloads_tmp_path) - .wrap_err_with(|| format!("failed to create {}", payloads_tmp_path.display()))?, - ); - - Ok(Self { - output_dir: output_dir.to_path_buf(), - genesis_tmp_path, - metadata_tmp_path, - payloads_tmp_path, - payloads_writer, - }) - } - - pub(crate) fn write_payload(&mut self, payload: &ExecutionPayloadV3) -> eyre::Result<()> { - serde_json::to_writer(&mut self.payloads_writer, payload) - .wrap_err("failed to serialize payload fixture entry")?; - self.payloads_writer - .write_all(b"\n") - .wrap_err("failed to write payload fixture newline")?; - Ok(()) - } - - pub(crate) fn finish( - self, - metadata: &PayloadFixtureMetadata, - genesis: &Genesis, - ) -> eyre::Result<()> { - metadata.validate()?; - - let Self { - output_dir, - genesis_tmp_path, - metadata_tmp_path, - payloads_tmp_path, - mut payloads_writer, - } = self; - - payloads_writer - .flush() - .wrap_err("failed to flush payload fixture stream")?; - drop(payloads_writer); - - write_json_file(&genesis_tmp_path, genesis, "genesis")?; - write_json_file(&metadata_tmp_path, metadata, "metadata")?; - - let genesis_path = genesis_path(&output_dir); - let metadata_path = metadata_path(&output_dir); - let payloads_path = payloads_path(&output_dir); - remove_file_if_exists(&genesis_path)?; - remove_file_if_exists(&metadata_path)?; - remove_file_if_exists(&payloads_path)?; - rename_into_place(&payloads_tmp_path, &payloads_path)?; - rename_into_place(&genesis_tmp_path, &genesis_path)?; - rename_into_place(&metadata_tmp_path, &metadata_path)?; - - Ok(()) - } -} - -pub(crate) struct PayloadFixture { - #[allow(dead_code)] - genesis: Genesis, - metadata: PayloadFixtureMetadata, - payload_reader: PayloadJsonlReader, - expected_next_block: u64, - expected_parent_hash: B256, - yielded: u64, - exhausted: bool, -} - -impl PayloadFixture { - pub(crate) fn open(payload_dir: &Path) -> eyre::Result { - let genesis = load_genesis(payload_dir)?; - let metadata = load_metadata(payload_dir)?; - let payloads_path = payloads_path(payload_dir); - if !payloads_path.is_file() { - bail!( - "payload fixture payloads file does not exist: {}", - payloads_path.display() - ); - } - - Ok(Self { - expected_next_block: metadata.from_block, - expected_parent_hash: metadata.expected_parent.block_hash, - payload_reader: PayloadJsonlReader::new(&payloads_path)?, - genesis, - metadata, - yielded: 0, - exhausted: false, - }) - } - - #[allow(dead_code)] - pub(crate) fn genesis(&self) -> &Genesis { - &self.genesis - } - - pub(crate) fn metadata(&self) -> &PayloadFixtureMetadata { - &self.metadata - } - - pub(crate) fn next_payload(&mut self) -> eyre::Result> { - if self.exhausted { - return Ok(None); - } - - if self.yielded == self.metadata.payload_count { - if let Some(extra_payload) = self.payload_reader.next_payload()? { - let extra_block = extra_payload.payload_inner.payload_inner.block_number; - bail!( - "payload fixture contains more payloads than expected: first extra block is {extra_block}" - ); - } - self.exhausted = true; - return Ok(None); - } - - let payload = self.payload_reader.next_payload()?.ok_or_else(|| { - eyre::eyre!( - "payload fixture ended early after {} payloads; expected {}", - self.yielded, - self.metadata.payload_count - ) - })?; - - let block = &payload.payload_inner.payload_inner; - if block.block_number != self.expected_next_block { - bail!( - "payload fixture block sequence mismatch: expected block {}, got {}", - self.expected_next_block, - block.block_number - ); - } - if block.parent_hash != self.expected_parent_hash { - bail!( - "payload fixture parent hash mismatch for block {}: expected parent {}, got {}", - block.block_number, - fmt_hash(self.expected_parent_hash), - fmt_hash(block.parent_hash), - ); - } - - self.yielded = self.yielded.saturating_add(1); - self.expected_next_block = self - .expected_next_block - .checked_add(1) - .ok_or_else(|| eyre::eyre!("payload fixture block number overflow"))?; - self.expected_parent_hash = block.block_hash; - - Ok(Some(payload)) - } -} - -pub(crate) fn genesis_path(payload_dir: &Path) -> PathBuf { - payload_dir.join(GENESIS_FILE_NAME) -} - -pub(crate) fn metadata_path(payload_dir: &Path) -> PathBuf { - payload_dir.join(METADATA_FILE_NAME) -} - -pub(crate) fn payloads_path(payload_dir: &Path) -> PathBuf { - payload_dir.join(PAYLOADS_FILE_NAME) -} - -fn load_genesis(payload_dir: &Path) -> eyre::Result { - let path = genesis_path(payload_dir); - load_json_file(&path) -} - -fn load_metadata(payload_dir: &Path) -> eyre::Result { - if !payload_dir.is_dir() { - bail!( - "payload fixture directory does not exist: {}", - payload_dir.display() - ); - } - let path = metadata_path(payload_dir); - let metadata: PayloadFixtureMetadata = load_json_file(&path)?; - metadata.validate()?; - Ok(metadata) -} - -fn load_json_file(path: &Path) -> eyre::Result { - if !path.is_file() { - bail!("payload fixture file does not exist: {}", path.display()); - } - let reader = BufReader::new( - File::open(path).wrap_err_with(|| format!("failed to open {}", path.display()))?, - ); - serde_json::from_reader(reader).wrap_err_with(|| format!("failed to parse {}", path.display())) -} - -fn write_json_file(path: &Path, value: &T, label: &str) -> eyre::Result<()> { - let mut writer = BufWriter::new( - File::create(path).wrap_err_with(|| format!("failed to create {}", path.display()))?, - ); - serde_json::to_writer_pretty(&mut writer, value) - .wrap_err_with(|| format!("failed to serialize payload fixture {label}"))?; - writer - .write_all(b"\n") - .wrap_err_with(|| format!("failed to write payload fixture {label} newline"))?; - writer - .flush() - .wrap_err_with(|| format!("failed to flush payload fixture {label}"))?; - Ok(()) -} - -fn rename_into_place(from: &Path, to: &Path) -> eyre::Result<()> { - fs::rename(from, to).wrap_err_with(|| { - format!( - "failed to move payload fixture file into place: {} -> {}", - from.display(), - to.display() - ) - }) -} - -fn remove_file_if_exists(path: &Path) -> eyre::Result<()> { - if path.exists() { - fs::remove_file(path).wrap_err_with(|| format!("failed to remove {}", path.display()))?; - } - Ok(()) -} - -struct PayloadJsonlReader { - path: PathBuf, - reader: BufReader, - line_number: usize, -} - -impl PayloadJsonlReader { - fn new(path: &Path) -> eyre::Result { - let file = - File::open(path).wrap_err_with(|| format!("failed to open {}", path.display()))?; - Ok(Self { - path: path.to_path_buf(), - reader: BufReader::new(file), - line_number: 0, - }) - } - - fn next_payload(&mut self) -> eyre::Result> { - let mut line = String::new(); - let bytes_read = self - .reader - .read_line(&mut line) - .wrap_err_with(|| format!("failed to read {}", self.path.display()))?; - if bytes_read == 0 { - return Ok(None); - } - - self.line_number += 1; - let line = line.trim_end_matches(&['\r', '\n'][..]); - if line.is_empty() { - bail!( - "payload fixture contains an empty line at {}:{}", - self.path.display(), - self.line_number - ); - } - - let payload = serde_json::from_str(line).wrap_err_with(|| { - format!( - "failed to parse payload fixture entry at {}:{}", - self.path.display(), - self.line_number - ) - })?; - Ok(Some(payload)) - } -} - -#[cfg(test)] -mod tests { - use super::*; - use serde_json::json; - use tempfile::TempDir; - - fn test_genesis() -> Genesis { - Genesis::default() - } - - fn zero_address() -> String { - format!("0x{}", "00".repeat(20)) - } - - fn zero_bloom() -> String { - format!("0x{}", "00".repeat(256)) - } - - fn hash_hex(value: u64) -> String { - format!("0x{value:064x}") - } - - fn payload(block_number: u64, parent_hash: u64, block_hash: u64) -> ExecutionPayloadV3 { - serde_json::from_value(json!({ - "parentHash": hash_hex(parent_hash), - "feeRecipient": zero_address(), - "stateRoot": hash_hex(1_000 + block_number), - "receiptsRoot": hash_hex(2_000 + block_number), - "logsBloom": zero_bloom(), - "prevRandao": hash_hex(3_000 + block_number), - "blockNumber": format!("0x{block_number:x}"), - "gasLimit": "0x1c9c380", - "gasUsed": format!("0x{:x}", block_number * 1_000), - "timestamp": format!("0x{:x}", 10_000 + block_number), - "extraData": "0x", - "baseFeePerGas": "0x1", - "blockHash": hash_hex(block_hash), - "transactions": [], - "withdrawals": [], - "blobGasUsed": "0x0", - "excessBlobGas": "0x0" - })) - .unwrap() - } - - fn metadata_for( - first_payload: &ExecutionPayloadV3, - to_block: u64, - payload_count: u64, - ) -> PayloadFixtureMetadata { - PayloadFixtureMetadata { - from_block: first_payload.payload_inner.payload_inner.block_number, - to_block, - payload_count, - expected_parent: ExpectedParentBlock { - block_number: first_payload.payload_inner.payload_inner.block_number - 1, - block_hash: first_payload.payload_inner.payload_inner.parent_hash, - }, - } - } - - #[test] - fn metadata_round_trips_through_json() { - let first_payload = payload(5, 44, 55); - let metadata = metadata_for(&first_payload, 6, 2); - - let encoded = serde_json::to_string(&metadata).unwrap(); - let decoded: PayloadFixtureMetadata = serde_json::from_str(&encoded).unwrap(); - - assert_eq!(decoded, metadata); - } - - #[test] - fn metadata_validation_rejects_count_mismatch() { - let first_payload = payload(5, 44, 55); - let mut metadata = metadata_for(&first_payload, 6, 2); - metadata.payload_count = 3; - - let err = metadata.validate().unwrap_err(); - - assert_eq!( - err.to_string(), - "payload fixture payload_count mismatch: expected 2, got 3" - ); - } - - #[test] - fn payload_fixture_writer_and_reader_round_trip() { - let temp_dir = TempDir::new().unwrap(); - let payload1 = payload(7, 66, 77); - let payload2 = payload(8, 77, 88); - let metadata = metadata_for(&payload1, 8, 2); - - let mut writer = PayloadFixtureWriter::new(temp_dir.path()).unwrap(); - writer.write_payload(&payload1).unwrap(); - writer.write_payload(&payload2).unwrap(); - writer.finish(&metadata, &test_genesis()).unwrap(); - - let mut fixture = PayloadFixture::open(temp_dir.path()).unwrap(); - - assert_eq!(fixture.metadata(), &metadata); - assert_eq!(fixture.next_payload().unwrap(), Some(payload1)); - assert_eq!(fixture.next_payload().unwrap(), Some(payload2)); - assert_eq!(fixture.next_payload().unwrap(), None); - } - - #[test] - fn payload_fixture_open_fails_when_files_are_missing() { - let temp_dir = TempDir::new().unwrap(); - - let err = match PayloadFixture::open(temp_dir.path()) { - Ok(_) => panic!("expected missing fixture files to fail"), - Err(err) => err, - }; - - assert_eq!( - err.to_string(), - format!( - "payload fixture file does not exist: {}", - temp_dir.path().join(GENESIS_FILE_NAME).display() - ) - ); - } - - #[test] - fn payload_fixture_rejects_block_gaps() { - let temp_dir = TempDir::new().unwrap(); - let payload1 = payload(10, 99, 100); - let payload3 = payload(12, 100, 101); - let metadata = metadata_for(&payload1, 11, 2); - - let mut writer = PayloadFixtureWriter::new(temp_dir.path()).unwrap(); - writer.write_payload(&payload1).unwrap(); - writer.write_payload(&payload3).unwrap(); - writer.finish(&metadata, &test_genesis()).unwrap(); - - let mut fixture = PayloadFixture::open(temp_dir.path()).unwrap(); - assert_eq!(fixture.next_payload().unwrap(), Some(payload1)); - - let err = fixture.next_payload().unwrap_err(); - assert_eq!( - err.to_string(), - "payload fixture block sequence mismatch: expected block 11, got 12" - ); - } - - #[test] - fn payload_fixture_rejects_extra_payloads() { - let temp_dir = TempDir::new().unwrap(); - let payload1 = payload(20, 199, 200); - let payload2 = payload(21, 200, 201); - let metadata = metadata_for(&payload1, 20, 1); - - let mut writer = PayloadFixtureWriter::new(temp_dir.path()).unwrap(); - writer.write_payload(&payload1).unwrap(); - writer.write_payload(&payload2).unwrap(); - writer.finish(&metadata, &test_genesis()).unwrap(); - - let mut fixture = PayloadFixture::open(temp_dir.path()).unwrap(); - assert_eq!(fixture.next_payload().unwrap(), Some(payload1)); - - let err = fixture.next_payload().unwrap_err(); - assert_eq!( - err.to_string(), - "payload fixture contains more payloads than expected: first extra block is 21" - ); - } -} diff --git a/crates/engine-bench/src/bench/helpers.rs b/crates/engine-bench/src/bench/helpers.rs deleted file mode 100644 index 3ab9e8b..0000000 --- a/crates/engine-bench/src/bench/helpers.rs +++ /dev/null @@ -1,27 +0,0 @@ -// Copyright 2026 Circle Internet Group, Inc. All rights reserved. -// -// SPDX-License-Identifier: Apache-2.0 -// -// Licensed under the Apache License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. -// You may obtain a copy of the License at -// -// http://www.apache.org/licenses/LICENSE-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// See the License for the specific language governing permissions and -// limitations under the License. - -use std::time::Duration; - -pub(crate) const SUMMARY_FILE_NAME: &str = "summary.csv"; - -pub(crate) fn duration_to_ms(duration: Duration) -> f64 { - duration.as_secs_f64() * 1_000.0 -} - -pub(crate) fn fmt_hash(value: T) -> String { - format!("{value:#x}") -} diff --git a/crates/engine-bench/src/bench/mod.rs b/crates/engine-bench/src/bench/mod.rs deleted file mode 100644 index 545223e..0000000 --- a/crates/engine-bench/src/bench/mod.rs +++ /dev/null @@ -1,31 +0,0 @@ -// Copyright 2026 Circle Internet Group, Inc. All rights reserved. -// -// SPDX-License-Identifier: Apache-2.0 -// -// Licensed under the Apache License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. -// You may obtain a copy of the License at -// -// http://www.apache.org/licenses/LICENSE-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// See the License for the specific language governing permissions and -// limitations under the License. - -use crate::cli::Command; - -mod context; -mod fixture; -mod helpers; -pub mod new_payload_fcu; -mod output; -mod prepare_payload; - -pub async fn run(command: Command) -> eyre::Result<()> { - match command { - Command::PreparePayload(args) => prepare_payload::run(args).await, - Command::NewPayloadFcu(args) => new_payload_fcu::run(args).await, - } -} diff --git a/crates/engine-bench/src/bench/new_payload_fcu.rs b/crates/engine-bench/src/bench/new_payload_fcu.rs deleted file mode 100644 index 5aa284c..0000000 --- a/crates/engine-bench/src/bench/new_payload_fcu.rs +++ /dev/null @@ -1,212 +0,0 @@ -// Copyright 2026 Circle Internet Group, Inc. All rights reserved. -// -// SPDX-License-Identifier: Apache-2.0 -// -// Licensed under the Apache License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. -// You may obtain a copy of the License at -// -// http://www.apache.org/licenses/LICENSE-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// See the License for the specific language governing permissions and -// limitations under the License. - -use super::{ - context::BenchContext, - fixture::{PayloadFixture, PayloadFixtureMetadata}, - helpers::{duration_to_ms, fmt_hash, SUMMARY_FILE_NAME}, - output::{ - build_summary, throughput_mgas_per_s, throughput_tx_per_s, write_csv, CombinedLatencyRow, - CsvWriter, COMBINED_LATENCY_FILE_NAME, - }, -}; -use crate::cli::NewPayloadFcuArgs; -use arc_eth_engine::{json_structures::ExecutionBlock, rpc::ethereum_rpc::EthereumRPC}; -use eyre::{bail, Context}; -use std::time::Instant; -use tracing::info; - -pub async fn run(args: NewPayloadFcuArgs) -> eyre::Result<()> { - let context = BenchContext::new(&args.common, "new-payload-fcu")?; - - info!( - payload_dir = %args.payload.display(), - target_eth_rpc_url = args.target_eth_rpc_url, - engine = %context.transport(), - output_dir = %context.output_dir().display(), - "running new-payload-fcu benchmark" - ); - - let target_eth_rpc = context.ethereum_rpc(&args.target_eth_rpc_url, "target eth rpc")?; - let engine = context.engine().await?; - let mut payload_fixture = PayloadFixture::open(args.payload.as_path())?; - let metadata = payload_fixture.metadata().clone(); - verify_target_start_state(&target_eth_rpc, &metadata).await?; - - info!( - from_block = metadata.from_block, - to_block = metadata.to_block, - payload_count = metadata.payload_count, - "starting payload replay" - ); - - let benchmark_started = Instant::now(); - let row_capacity = metadata.payload_count.min(usize::MAX as u64) as usize; - let mut rows = Vec::with_capacity(row_capacity); - let mut csv_writer = CsvWriter::new(&context.output_dir().join(COMBINED_LATENCY_FILE_NAME))?; - let mut cumulative_gas = 0_u64; - let mut cumulative_txs = 0_u64; - - while let Some(payload) = payload_fixture.next_payload()? { - let block_hash = payload.payload_inner.payload_inner.block_hash; - let parent_hash = payload.payload_inner.payload_inner.parent_hash; - let tx_count = payload.payload_inner.payload_inner.transactions.len() as u64; - let gas_used = payload.payload_inner.payload_inner.gas_used; - let block_number = payload.payload_inner.payload_inner.block_number; - - let start = Instant::now(); - let status = engine - .new_payload(&payload, Vec::new(), parent_hash) - .await - .wrap_err_with(|| format!("engine_newPayloadV4 failed for block {block_number}"))?; - let new_payload_latency = start.elapsed(); - - if !status.is_valid() { - bail!("engine_newPayloadV4 returned non-valid status for block {block_number}: {status:?}"); - } - - let fcu_result = engine - .forkchoice_updated(block_hash, None) - .await - .wrap_err_with(|| { - format!("engine_forkchoiceUpdatedV3 failed for block {block_number}") - })?; - let total_latency = start.elapsed(); - let fcu_latency = total_latency.saturating_sub(new_payload_latency); - - if !fcu_result.payload_status.is_valid() { - bail!( - "engine_forkchoiceUpdatedV3 returned non-valid status for block {block_number}: {:?}", - fcu_result.payload_status - ); - } - - cumulative_gas = cumulative_gas.saturating_add(gas_used); - cumulative_txs = cumulative_txs.saturating_add(tx_count); - let elapsed = benchmark_started.elapsed(); - - let row = CombinedLatencyRow { - block_number, - block_hash: fmt_hash(block_hash), - tx_count, - gas_used, - new_payload_ms: duration_to_ms(new_payload_latency), - fcu_ms: duration_to_ms(fcu_latency), - total_ms: duration_to_ms(total_latency), - elapsed_ms: duration_to_ms(elapsed), - mgas_per_s: throughput_mgas_per_s(gas_used, total_latency), - tx_per_s: throughput_tx_per_s(tx_count, total_latency), - cumulative_mgas_per_s: throughput_mgas_per_s(cumulative_gas, elapsed), - cumulative_tx_per_s: throughput_tx_per_s(cumulative_txs, elapsed), - }; - csv_writer.write_row(&row)?; - rows.push(row); - } - - csv_writer.finish()?; - let wall_clock = benchmark_started.elapsed(); - - let summary = build_summary("new-payload-fcu", &rows, wall_clock)?; - write_csv(&context.output_dir().join(SUMMARY_FILE_NAME), &[summary])?; - - info!( - samples = rows.len(), - wall_clock_ms = duration_to_ms(wall_clock), - output_dir = %context.output_dir().display(), - "new-payload-fcu benchmark complete" - ); - - Ok(()) -} - -async fn verify_target_start_state( - target_rpc: &EthereumRPC, - metadata: &PayloadFixtureMetadata, -) -> eyre::Result<()> { - let target_latest_block = target_rpc - .get_block_by_number("latest") - .await - .wrap_err("failed to fetch latest block from target node")? - .ok_or_else(|| eyre::eyre!("latest block not found on target node"))?; - - ensure_target_start_state(&target_latest_block, metadata)?; - - info!( - target_block_number = target_latest_block.block_number, - target_block_hash = %fmt_hash(target_latest_block.block_hash), - "verified target node replay start state" - ); - - Ok(()) -} - -fn ensure_target_start_state( - target_latest_block: &ExecutionBlock, - metadata: &PayloadFixtureMetadata, -) -> eyre::Result<()> { - if target_latest_block.block_number != metadata.expected_parent.block_number - || target_latest_block.block_hash != metadata.expected_parent.block_hash - { - bail!( - "target node is not at the expected replay start state: expected parent block {} ({}) before replaying block {}, but target latest is block {} ({})", - metadata.expected_parent.block_number, - fmt_hash(metadata.expected_parent.block_hash), - metadata.from_block, - target_latest_block.block_number, - fmt_hash(target_latest_block.block_hash), - ); - } - - Ok(()) -} - -#[cfg(test)] -mod tests { - use super::*; - use crate::bench::fixture::ExpectedParentBlock; - - #[test] - fn ensure_target_start_state_rejects_mismatch() { - let expected_hash = format!("0x{}", "01".repeat(32)).parse().unwrap(); - let actual_hash = format!("0x{}", "02".repeat(32)).parse().unwrap(); - let target_latest_block = ExecutionBlock { - block_hash: actual_hash, - block_number: 10, - parent_hash: format!("0x{}", "00".repeat(32)).parse().unwrap(), - timestamp: 123, - }; - let metadata = PayloadFixtureMetadata { - from_block: 11, - to_block: 12, - payload_count: 2, - expected_parent: ExpectedParentBlock { - block_number: 10, - block_hash: expected_hash, - }, - }; - - let err = ensure_target_start_state(&target_latest_block, &metadata).unwrap_err(); - - assert_eq!( - err.to_string(), - format!( - "target node is not at the expected replay start state: expected parent block 10 ({}) before replaying block 11, but target latest is block 10 ({})", - fmt_hash(expected_hash), - fmt_hash(actual_hash) - ) - ); - } -} diff --git a/crates/engine-bench/src/bench/output.rs b/crates/engine-bench/src/bench/output.rs deleted file mode 100644 index dca2ee9..0000000 --- a/crates/engine-bench/src/bench/output.rs +++ /dev/null @@ -1,313 +0,0 @@ -// Copyright 2026 Circle Internet Group, Inc. All rights reserved. -// -// SPDX-License-Identifier: Apache-2.0 -// -// Licensed under the Apache License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. -// You may obtain a copy of the License at -// -// http://www.apache.org/licenses/LICENSE-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// See the License for the specific language governing permissions and -// limitations under the License. - -use super::helpers::duration_to_ms; -use eyre::{bail, Context}; -use serde::Serialize; -use std::{ - cmp::Ordering, - path::{Path, PathBuf}, - time::Duration, -}; - -pub(crate) const COMBINED_LATENCY_FILE_NAME: &str = "combined_latency.csv"; - -#[derive(Debug, Clone, Serialize)] -pub(crate) struct CombinedLatencyRow { - pub block_number: u64, - pub block_hash: String, - pub tx_count: u64, - pub gas_used: u64, - pub new_payload_ms: f64, - pub fcu_ms: f64, - pub total_ms: f64, - pub elapsed_ms: f64, - pub mgas_per_s: f64, - pub tx_per_s: f64, - pub cumulative_mgas_per_s: f64, - pub cumulative_tx_per_s: f64, -} - -#[derive(Debug, Clone, Serialize)] -pub(crate) struct SummaryRow { - pub mode: String, - pub samples: u64, - pub total_gas: u64, - pub total_txs: u64, - pub wall_clock_ms: f64, - pub execution_ms: f64, - pub avg_total_ms: f64, - pub avg_new_payload_ms: Option, - pub avg_fcu_ms: Option, - pub avg_mgas_per_s: f64, - pub avg_tx_per_s: f64, - pub p50_new_payload_ms: Option, - pub p95_new_payload_ms: Option, - pub p99_new_payload_ms: Option, - pub p50_fcu_ms: Option, - pub p95_fcu_ms: Option, - pub p99_fcu_ms: Option, - pub p50_total_ms: f64, - pub p95_total_ms: f64, - pub p99_total_ms: f64, -} - -pub(crate) trait BenchmarkRow { - fn gas_used(&self) -> u64; - fn tx_count(&self) -> u64; - fn total_ms(&self) -> f64; - fn new_payload_ms(&self) -> Option { - None - } - fn fcu_ms(&self) -> Option { - None - } -} - -impl BenchmarkRow for CombinedLatencyRow { - fn gas_used(&self) -> u64 { - self.gas_used - } - - fn tx_count(&self) -> u64 { - self.tx_count - } - - fn total_ms(&self) -> f64 { - self.total_ms - } - - fn new_payload_ms(&self) -> Option { - Some(self.new_payload_ms) - } - - fn fcu_ms(&self) -> Option { - Some(self.fcu_ms) - } -} - -pub(crate) fn throughput_mgas_per_s(gas_used: u64, duration: Duration) -> f64 { - let seconds = duration.as_secs_f64(); - if seconds == 0.0 { - return 0.0; - } - gas_used as f64 / seconds / 1_000_000.0 -} - -pub(crate) fn throughput_tx_per_s(tx_count: u64, duration: Duration) -> f64 { - let seconds = duration.as_secs_f64(); - if seconds == 0.0 { - return 0.0; - } - tx_count as f64 / seconds -} - -pub(crate) struct CsvWriter { - writer: csv::Writer, - path: PathBuf, -} - -impl CsvWriter { - pub(crate) fn new(path: &Path) -> eyre::Result { - let writer = csv::Writer::from_path(path) - .wrap_err_with(|| format!("failed to create {}", path.display()))?; - Ok(Self { - writer, - path: path.to_path_buf(), - }) - } - - pub(crate) fn write_row(&mut self, row: &T) -> eyre::Result<()> { - self.writer - .serialize(row) - .wrap_err_with(|| format!("failed to write row to {}", self.path.display())) - } - - pub(crate) fn finish(mut self) -> eyre::Result<()> { - self.writer - .flush() - .wrap_err_with(|| format!("failed to flush {}", self.path.display())) - } -} - -pub(crate) fn write_csv(path: &Path, rows: &[T]) -> eyre::Result<()> { - let mut writer = CsvWriter::new(path)?; - for row in rows { - writer.write_row(row)?; - } - writer.finish() -} - -pub(crate) fn build_summary( - mode: &str, - rows: &[T], - wall_clock: Duration, -) -> eyre::Result { - if rows.is_empty() { - bail!("cannot build a summary for an empty benchmark result set"); - } - - let total_gas = rows.iter().map(BenchmarkRow::gas_used).sum::(); - let total_txs = rows.iter().map(BenchmarkRow::tx_count).sum::(); - let mut total_latencies = rows.iter().map(BenchmarkRow::total_ms).collect::>(); - let mut new_payload_latencies = rows - .iter() - .filter_map(BenchmarkRow::new_payload_ms) - .collect::>(); - let mut fcu_latencies = rows - .iter() - .filter_map(BenchmarkRow::fcu_ms) - .collect::>(); - let execution_ms = total_latencies.iter().sum::(); - - sort_f64(&mut total_latencies); - sort_f64(&mut new_payload_latencies); - sort_f64(&mut fcu_latencies); - - let pct_sorted_opt = |sorted: &[f64], q: f64| -> Option { - (!sorted.is_empty()).then(|| percentile_sorted(sorted, q)) - }; - - Ok(SummaryRow { - mode: mode.to_owned(), - samples: rows.len() as u64, - total_gas, - total_txs, - wall_clock_ms: duration_to_ms(wall_clock), - execution_ms, - avg_total_ms: execution_ms / rows.len() as f64, - avg_new_payload_ms: average(&new_payload_latencies), - avg_fcu_ms: average(&fcu_latencies), - avg_mgas_per_s: throughput_mgas_per_s(total_gas, wall_clock), - avg_tx_per_s: throughput_tx_per_s(total_txs, wall_clock), - p50_new_payload_ms: pct_sorted_opt(&new_payload_latencies, 0.50), - p95_new_payload_ms: pct_sorted_opt(&new_payload_latencies, 0.95), - p99_new_payload_ms: pct_sorted_opt(&new_payload_latencies, 0.99), - p50_fcu_ms: pct_sorted_opt(&fcu_latencies, 0.50), - p95_fcu_ms: pct_sorted_opt(&fcu_latencies, 0.95), - p99_fcu_ms: pct_sorted_opt(&fcu_latencies, 0.99), - p50_total_ms: percentile_sorted(&total_latencies, 0.50), - p95_total_ms: percentile_sorted(&total_latencies, 0.95), - p99_total_ms: percentile_sorted(&total_latencies, 0.99), - }) -} - -fn average(values: &[f64]) -> Option { - (!values.is_empty()).then(|| values.iter().sum::() / values.len() as f64) -} - -fn sort_f64(values: &mut [f64]) { - values.sort_by(|a, b| a.partial_cmp(b).unwrap_or(Ordering::Equal)); -} - -fn percentile_sorted(sorted: &[f64], quantile: f64) -> f64 { - if sorted.is_empty() { - return 0.0; - } - if sorted.len() == 1 { - return sorted[0]; - } - let rank = quantile.clamp(0.0, 1.0) * (sorted.len().saturating_sub(1)) as f64; - let lower_index = rank.floor() as usize; - let upper_index = rank.ceil() as usize; - if lower_index == upper_index { - sorted[lower_index] - } else { - sorted[lower_index] - + (sorted[upper_index] - sorted[lower_index]) * (rank - lower_index as f64) - } -} - -#[cfg(test)] -fn percentile(mut values: Vec, quantile: f64) -> f64 { - sort_f64(&mut values); - percentile_sorted(&values, quantile) -} - -#[cfg(test)] -mod tests { - use super::*; - - #[test] - fn percentile_interpolates_between_samples() { - let actual = percentile(vec![10.0, 20.0, 30.0, 40.0], 0.75); - assert_eq!(actual, 32.5); - } - - #[test] - fn summary_uses_wall_clock_for_average_throughput() { - let row = CombinedLatencyRow { - block_number: 1, - block_hash: "0x01".to_string(), - tx_count: 2, - gas_used: 2_000_000, - new_payload_ms: 100.0, - fcu_ms: 50.0, - total_ms: 150.0, - elapsed_ms: 150.0, - mgas_per_s: 0.0, - tx_per_s: 0.0, - cumulative_mgas_per_s: 0.0, - cumulative_tx_per_s: 0.0, - }; - let summary = build_summary("new-payload-fcu", &[row], Duration::from_millis(200)).unwrap(); - assert_eq!(summary.execution_ms, 150.0); - assert_eq!(summary.avg_total_ms, 150.0); - assert_eq!(summary.avg_new_payload_ms, Some(100.0)); - assert_eq!(summary.avg_fcu_ms, Some(50.0)); - assert_eq!(summary.avg_mgas_per_s, 10.0); - } - - #[test] - fn summary_includes_component_latency_percentiles() { - let rows = [ - CombinedLatencyRow { - block_number: 1, - block_hash: "0x01".to_string(), - tx_count: 1, - gas_used: 1_000_000, - new_payload_ms: 10.0, - fcu_ms: 1.0, - total_ms: 11.0, - elapsed_ms: 11.0, - mgas_per_s: 0.0, - tx_per_s: 0.0, - cumulative_mgas_per_s: 0.0, - cumulative_tx_per_s: 0.0, - }, - CombinedLatencyRow { - block_number: 2, - block_hash: "0x02".to_string(), - tx_count: 1, - gas_used: 1_000_000, - new_payload_ms: 20.0, - fcu_ms: 2.0, - total_ms: 22.0, - elapsed_ms: 33.0, - mgas_per_s: 0.0, - tx_per_s: 0.0, - cumulative_mgas_per_s: 0.0, - cumulative_tx_per_s: 0.0, - }, - ]; - - let summary = build_summary("new-payload-fcu", &rows, Duration::from_millis(33)).unwrap(); - assert_eq!(summary.p50_new_payload_ms, Some(15.0)); - assert_eq!(summary.p95_new_payload_ms, Some(19.5)); - assert_eq!(summary.p50_fcu_ms, Some(1.5)); - assert_eq!(summary.p95_fcu_ms, Some(1.95)); - } -} diff --git a/crates/engine-bench/src/bench/prepare_payload.rs b/crates/engine-bench/src/bench/prepare_payload.rs deleted file mode 100644 index 69c5920..0000000 --- a/crates/engine-bench/src/bench/prepare_payload.rs +++ /dev/null @@ -1,164 +0,0 @@ -// Copyright 2026 Circle Internet Group, Inc. All rights reserved. -// -// SPDX-License-Identifier: Apache-2.0 -// -// Licensed under the Apache License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. -// You may obtain a copy of the License at -// -// http://www.apache.org/licenses/LICENSE-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// See the License for the specific language governing permissions and -// limitations under the License. - -use super::{ - context::ethereum_rpc_client, - fixture::{ExpectedParentBlock, PayloadFixtureMetadata, PayloadFixtureWriter}, - helpers::fmt_hash, -}; -use crate::cli::PreparePayloadArgs; -use arc_eth_engine::{engine::EthereumAPI, rpc::ethereum_rpc::EthereumRPC}; -use arc_execution_config::chainspec::ArcChainSpecParser; -use eyre::{bail, Context}; -use reth_cli::chainspec::ChainSpecParser; -use std::time::Duration; -use tracing::info; - -pub async fn run(args: PreparePayloadArgs) -> eyre::Result<()> { - if args.from > args.to { - bail!("--from must be less than or equal to --to"); - } - if args.batch_size == 0 { - bail!("--batch-size must be greater than 0"); - } - - let chain_spec = ArcChainSpecParser::parse(&args.chain) - .wrap_err_with(|| format!("failed to parse chain spec: {}", args.chain))?; - let genesis = chain_spec.inner.genesis.clone(); - - info!( - chain = args.chain, - chain_id = genesis.config.chain_id, - source_rpc_url = args.source_rpc_url, - output_dir = %args.output_dir.display(), - from_block = args.from, - to_block = args.to, - "preparing payload fixture" - ); - - let source_rpc = ethereum_rpc_client( - &args.source_rpc_url, - "source rpc", - Duration::from_millis(args.eth_rpc_timeout_ms), - )?; - let expected_parent = fetch_expected_parent(&source_rpc, args.from).await?; - let payload_count = write_payload_fixture( - &source_rpc, - &args.output_dir, - &expected_parent, - &genesis, - args.from, - args.to, - args.batch_size, - ) - .await?; - - info!( - output_dir = %args.output_dir.display(), - payload_count, - "payload fixture prepared" - ); - - Ok(()) -} - -async fn fetch_expected_parent( - source_rpc: &EthereumRPC, - from_block: u64, -) -> eyre::Result { - let expected_parent_block_number = from_block - .checked_sub(1) - .ok_or_else(|| eyre::eyre!("from_block must be greater than 0"))?; - let expected_parent_block = source_rpc - .get_block_by_number(&format!("0x{expected_parent_block_number:x}")) - .await - .wrap_err_with(|| { - format!("failed to fetch source parent block {expected_parent_block_number}") - })? - .ok_or_else(|| { - eyre::eyre!("source parent block {expected_parent_block_number} not found") - })?; - - Ok(ExpectedParentBlock { - block_number: expected_parent_block.block_number, - block_hash: expected_parent_block.block_hash, - }) -} - -async fn write_payload_fixture( - source_rpc: &EthereumRPC, - output_dir: &std::path::Path, - expected_parent: &ExpectedParentBlock, - genesis: &alloy_genesis::Genesis, - from: u64, - to: u64, - batch_size: usize, -) -> eyre::Result { - let mut writer = PayloadFixtureWriter::new(output_dir)?; - let mut payload_count = 0_u64; - let mut expected_parent_hash = expected_parent.block_hash; - - for chunk_start in (from..=to).step_by(batch_size) { - let chunk_end = chunk_start - .saturating_add(batch_size as u64) - .saturating_sub(1) - .min(to); - let block_numbers = (chunk_start..=chunk_end) - .map(|block_number| format!("0x{block_number:x}")) - .collect::>(); - let chunk = - ::get_execution_payloads(source_rpc, &block_numbers) - .await - .wrap_err_with(|| { - format!("failed to fetch source blocks {chunk_start}..={chunk_end}") - })?; - - for (idx, maybe_payload) in chunk.into_iter().enumerate() { - let expected_block = chunk_start + idx as u64; - let payload = maybe_payload - .ok_or_else(|| eyre::eyre!("source block {expected_block} not found"))?; - let block = &payload.payload_inner.payload_inner; - if block.block_number != expected_block { - bail!( - "source payload sequence mismatch: expected block {expected_block}, got {}", - block.block_number - ); - } - if block.parent_hash != expected_parent_hash { - bail!( - "source payload parent hash mismatch for block {}: expected parent {}, got {}", - block.block_number, - fmt_hash(expected_parent_hash), - fmt_hash(block.parent_hash), - ); - } - - writer.write_payload(&payload)?; - expected_parent_hash = block.block_hash; - payload_count = payload_count.saturating_add(1); - } - } - - let metadata = PayloadFixtureMetadata { - from_block: from, - to_block: to, - payload_count, - expected_parent: expected_parent.clone(), - }; - writer.finish(&metadata, genesis)?; - - Ok(payload_count) -} diff --git a/crates/engine-bench/src/cli.rs b/crates/engine-bench/src/cli.rs deleted file mode 100644 index 11b31be..0000000 --- a/crates/engine-bench/src/cli.rs +++ /dev/null @@ -1,94 +0,0 @@ -// Copyright 2026 Circle Internet Group, Inc. All rights reserved. -// -// SPDX-License-Identifier: Apache-2.0 -// -// Licensed under the Apache License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. -// You may obtain a copy of the License at -// -// http://www.apache.org/licenses/LICENSE-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// See the License for the specific language governing permissions and -// limitations under the License. - -use clap::{Args, Parser, Subcommand}; -use std::path::PathBuf; - -#[derive(Debug, Parser)] -#[command( - name = "arc-engine-bench", - version = arc_version::SHORT_VERSION, - long_version = arc_version::LONG_VERSION, - about = "Benchmark Arc Engine API block import via newPayload + forkchoiceUpdated" -)] -pub struct Cli { - #[command(subcommand)] - pub command: Command, -} - -#[derive(Debug, Subcommand)] -pub enum Command { - /// Prepare a local payload fixture directory from historical source blocks. - PreparePayload(PreparePayloadArgs), - /// Replay historical blocks into a target Arc node with newPayload + forkchoiceUpdated. - NewPayloadFcu(NewPayloadFcuArgs), -} - -#[derive(Debug, Args, Clone)] -pub struct CommonArgs { - /// Engine API IPC socket path. Mutually exclusive with --engine-rpc-url / --jwt-secret. - #[arg(long, value_name = "ENGINE_IPC", group = "engine_transport")] - pub engine_ipc: Option, - /// Authenticated Engine API HTTP endpoint. Requires --jwt-secret. - #[arg(long, value_name = "ENGINE_RPC_URL", group = "engine_transport")] - pub engine_rpc_url: Option, - /// JWT secret used to authenticate Engine API requests (required with --engine-rpc-url). - #[arg(long = "jwt-secret", value_name = "PATH", requires = "engine_rpc_url")] - pub jwt_secret: Option, - /// Timeout for Ethereum JSON-RPC requests used by this command, in milliseconds (must be >= 1). - #[arg(long, value_name = "MILLISECONDS", default_value_t = 10_000, value_parser = clap::value_parser!(u64).range(1..))] - pub eth_rpc_timeout_ms: u64, - /// Output directory for CSV artifacts. Defaults to target/engine-bench/-. - #[arg(long, short, value_name = "OUTPUT_DIR")] - pub output: Option, -} - -#[derive(Debug, Args, Clone)] -pub struct PreparePayloadArgs { - /// Chain name or path to a genesis JSON file. Used to store the genesis config in the fixture. - #[arg(long, default_value = "arc-localdev")] - pub chain: String, - /// Read historical payloads from this source RPC endpoint. - #[arg(long, value_name = "SOURCE_RPC_URL")] - pub source_rpc_url: String, - /// First source block number to include in the fixture (must be >= 1). - #[arg(long, value_name = "FROM_BLOCK", value_parser = clap::value_parser!(u64).range(1..))] - pub from: u64, - /// Last source block number to include in the fixture, inclusive. - #[arg(long, value_name = "TO_BLOCK")] - pub to: u64, - /// Batch size for source RPC block fetching. - #[arg(long, value_name = "BATCH_SIZE", default_value_t = 20)] - pub batch_size: usize, - /// Timeout for source Ethereum JSON-RPC requests, in milliseconds (must be >= 1). - #[arg(long, value_name = "MILLISECONDS", default_value_t = 10_000, value_parser = clap::value_parser!(u64).range(1..))] - pub eth_rpc_timeout_ms: u64, - /// Output directory for the prepared payload fixture. - #[arg(long, value_name = "OUTPUT_DIR")] - pub output_dir: PathBuf, -} - -#[derive(Debug, Args, Clone)] -pub struct NewPayloadFcuArgs { - #[command(flatten)] - pub common: CommonArgs, - /// Regular RPC endpoint of the target Arc execution node. Used to verify the target head before replay starts. - #[arg(long, value_name = "TARGET_ETH_RPC_URL")] - pub target_eth_rpc_url: String, - /// Payload fixture directory containing genesis.json, metadata.json, and payloads.jsonl. - #[arg(long, value_name = "PAYLOAD_DIR")] - pub payload: PathBuf, -} diff --git a/crates/engine-bench/src/lib.rs b/crates/engine-bench/src/lib.rs deleted file mode 100644 index e4be6fe..0000000 --- a/crates/engine-bench/src/lib.rs +++ /dev/null @@ -1,27 +0,0 @@ -// Copyright 2026 Circle Internet Group, Inc. All rights reserved. -// -// SPDX-License-Identifier: Apache-2.0 -// -// Licensed under the Apache License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. -// You may obtain a copy of the License at -// -// http://www.apache.org/licenses/LICENSE-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// See the License for the specific language governing permissions and -// limitations under the License. - -#![allow(clippy::arithmetic_side_effects, clippy::cast_possible_truncation)] - -mod bench; -pub mod cli; -pub use bench::new_payload_fcu; - -use cli::Command; - -pub async fn run(command: Command) -> eyre::Result<()> { - bench::run(command).await -} diff --git a/crates/engine-bench/src/main.rs b/crates/engine-bench/src/main.rs deleted file mode 100644 index d2f4de2..0000000 --- a/crates/engine-bench/src/main.rs +++ /dev/null @@ -1,45 +0,0 @@ -// Copyright 2026 Circle Internet Group, Inc. All rights reserved. -// -// SPDX-License-Identifier: Apache-2.0 -// -// Licensed under the Apache License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. -// You may obtain a copy of the License at -// -// http://www.apache.org/licenses/LICENSE-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// See the License for the specific language governing permissions and -// limitations under the License. - -use arc_engine_bench::cli::Cli; -use clap::Parser; -use color_eyre::eyre::Context; -use std::io::IsTerminal; -use tracing_subscriber::EnvFilter; - -#[tokio::main] -async fn main() -> eyre::Result<()> { - color_eyre::install()?; - - let filter = EnvFilter::builder() - .with_default_directive(tracing::level_filters::LevelFilter::INFO.into()) - .from_env() - .wrap_err("failed to initialize tracing filter")?; - let subscriber = tracing_subscriber::fmt() - .with_env_filter(filter) - .with_ansi(std::io::stdout().is_terminal()) - .finish(); - tracing::subscriber::set_global_default(subscriber) - .wrap_err("failed to install tracing subscriber")?; - - tracing::info!( - version = arc_version::SHORT_VERSION, - commit = arc_version::GIT_COMMIT_HASH, - "arc-engine-bench starting" - ); - - arc_engine_bench::run(Cli::parse().command).await -} diff --git a/crates/eth-engine/Cargo.toml b/crates/eth-engine/Cargo.toml deleted file mode 100644 index 7fc0237..0000000 --- a/crates/eth-engine/Cargo.toml +++ /dev/null @@ -1,62 +0,0 @@ -[package] -name = "arc-eth-engine" -version.workspace = true -edition.workspace = true -repository.workspace = true -license.workspace = true -rust-version.workspace = true -publish.workspace = true - -[features] -mocks = ["dep:mockall"] - -[dependencies] - -# alloy crates -alloy-consensus = { workspace = true } -alloy-eips = { workspace = true } -alloy-genesis = { workspace = true } -alloy-primitives = { workspace = true } -alloy-rpc-types = { workspace = true } -alloy-rpc-types-engine = { workspace = true, features = ["jwt", "serde"] } -alloy-rpc-types-txpool = { workspace = true } -alloy-sol-macro = { workspace = true } -alloy-sol-types = { workspace = true } -arc-consensus-types = { workspace = true } -arc-execution-config = { workspace = true } -arc-shared = { workspace = true } - -async-trait = { workspace = true } -backon = { workspace = true } -ethereum_serde_utils = "0.8.0" -eyre = { workspace = true } -hex = { workspace = true } -jsonrpsee = { workspace = true, features = ["ws-client"] } -jsonrpsee-types = { workspace = true } -jsonwebtoken = { workspace = true } -malachitebft-core-types = { workspace = true } -mockall = { workspace = true, optional = true } -reqwest = { workspace = true, features = ["blocking", "json", "stream", "rustls-tls", "native-tls-vendored"] } -reth-chainspec = { workspace = true } -reth-ipc = { workspace = true } -serde = { workspace = true, features = ["derive"] } -serde_json = { workspace = true } -thiserror.workspace = true -tokio = { workspace = true, features = ["full"] } -tracing = { workspace = true } -uuid = { workspace = true, features = ["v4"] } - -[dev-dependencies] -alloy-rpc-types-eth = { workspace = true } -arc-evm-node = { workspace = true } -arc-execution-txpool = { workspace = true } -mockall = { workspace = true } -reth-node-builder = { workspace = true, features = ["test-utils"] } -reth-tasks = { workspace = true } -rstest = { workspace = true } -tempfile = "3.8" -url = { workspace = true } -wiremock = { workspace = true } - -[lints] -workspace = true diff --git a/crates/eth-engine/src/abi_utils.rs b/crates/eth-engine/src/abi_utils.rs deleted file mode 100644 index 5f9a5ab..0000000 --- a/crates/eth-engine/src/abi_utils.rs +++ /dev/null @@ -1,531 +0,0 @@ -// Copyright 2025 Circle Internet Group, Inc. All rights reserved. -// -// SPDX-License-Identifier: Apache-2.0 -// -// Licensed under the Apache License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. -// You may obtain a copy of the License at -// -// http://www.apache.org/licenses/LICENSE-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// See the License for the specific language governing permissions and -// limitations under the License. - -use std::time::Duration; - -use tracing::{debug, error}; - -use arc_consensus_types::signing::PublicKey; -use arc_consensus_types::{Address, ConsensusParams, Validator, ValidatorSet}; -use arc_shared::metrics::validator_set::record_skipped_validator; -use malachitebft_core_types::{LinearTimeouts, VotingPower}; - -use alloy_sol_macro::sol; -use alloy_sol_types::SolCall; - -// ABI types for decoding the return value of the `getActiveValidatorSet` and `consensusParams` functions. -sol! { - #[derive(PartialEq)] - enum ContractValidatorStatus { Unknown, Registered, Active } - struct ContractValidator { - ContractValidatorStatus status; - bytes publicKey; - uint64 votingPower; - } - - function getActiveValidatorSet() external view returns (ContractValidator[] memory activeValidators); - - struct ContractConsensusParams { - uint16 timeoutProposeMs; - uint16 timeoutProposeDeltaMs; - uint16 timeoutPrevoteMs; - uint16 timeoutPrevoteDeltaMs; - uint16 timeoutPrecommitMs; - uint16 timeoutPrecommitDeltaMs; - uint16 timeoutRebroadcastMs; - uint16 targetBlockTimeMs; - } - - function consensusParams() external view override returns (ContractConsensusParams memory); -} - -/// Decode validator set from ABI-encoded result. -/// -/// Validators with malformed public keys are skipped (logged at ERROR and counted in -/// `arc_validator_set_skipped_total`) rather than aborting the whole decode. The chain -/// keeps making progress on the surviving validators, even if they represent a small -/// fraction of the contract's advertised voting power — losing liveness while the -/// registry is corrupted is worse than running at reduced security until governance -/// removes the bad registrations. The empty-set case still errors, since consensus -/// cannot proceed with zero validators. -/// -/// **Operator alerting on `arc_validator_set_skipped_total` is load-bearing here.** -/// A non-zero rate is the only signal that the on-chain set has malformed keys and -/// the chain is operating with a degraded validator set; without an alert, the -/// degradation is silent. -/// -/// `PublicKey::from_bytes` is deterministic, so every node reaches the same filtered -/// set — there is no fork risk. -pub fn abi_decode_validator_set(result: Vec) -> eyre::Result { - // Decode the function's return payload exactly as the ABI defines it. - let active_validators: Vec = - getActiveValidatorSetCall::abi_decode_returns(&result)?; - - let mut validators: Vec = Vec::new(); - let mut skipped: usize = 0; - - for cv in &active_validators { - if cv.status != ContractValidatorStatus::Active || cv.votingPower == 0 { - continue; - } - - match try_decode_validator(cv) { - Ok(validator) => validators.push(validator), - Err(e) => { - error!( - public_key = %format!("0x{}", hex::encode(&cv.publicKey)), - "Skipping active validator with malformed public key: {e:#}", - ); - record_skipped_validator(); - skipped = skipped.saturating_add(1); - } - } - } - - if validators.is_empty() { - eyre::bail!( - "No active validators with valid public keys — validator set would be empty \ - ({skipped} active validator(s) skipped due to malformed public keys)" - ); - } - - debug!("ABI decoded validators:"); - for validator in &validators { - debug!( - " - address: {}, public key: {}, voting power: {}", - Address::from_public_key(&validator.public_key), - hex::encode(validator.public_key.as_bytes()), - validator.voting_power - ); - } - - Ok(ValidatorSet::new(validators)) -} - -/// Convert a single `ContractValidator` from the ABI payload into the domain `Validator` type. -fn try_decode_validator(cv: &ContractValidator) -> eyre::Result { - if cv.publicKey.len() != 32 { - eyre::bail!( - "Public key must be exactly 32 bytes, got {}", - cv.publicKey.len() - ); - } - let mut pk = [0u8; 32]; - pk.copy_from_slice(&cv.publicKey); - - let public_key = PublicKey::from_bytes(pk) - .map_err(|e| eyre::eyre!("Failed to decode public key bytes: {e}"))?; - let voting_power: VotingPower = cv.votingPower; - Ok(Validator::new(public_key, voting_power)) -} - -pub fn abi_decode_consensus_params(result: Vec) -> eyre::Result { - // Decode the function's return payload exactly as the ABI defines it. - let contract_params: ContractConsensusParams = - consensusParamsCall::abi_decode_returns(&result)?; - - let target_block_time = (contract_params.targetBlockTimeMs != 0) - .then(|| Duration::from_millis(contract_params.targetBlockTimeMs as u64)); - - // Map contract consensus params to use the domain type - let consensus_params = ConsensusParams::new( - target_block_time, - LinearTimeouts { - propose: Duration::from_millis(contract_params.timeoutProposeMs as u64), - propose_delta: Duration::from_millis(contract_params.timeoutProposeDeltaMs as u64), - prevote: Duration::from_millis(contract_params.timeoutPrevoteMs as u64), - prevote_delta: Duration::from_millis(contract_params.timeoutPrevoteDeltaMs as u64), - precommit: Duration::from_millis(contract_params.timeoutPrecommitMs as u64), - precommit_delta: Duration::from_millis(contract_params.timeoutPrecommitDeltaMs as u64), - rebroadcast: Duration::from_millis(contract_params.timeoutRebroadcastMs as u64), - }, - ); - - debug!("ABI decoded consensus params: {consensus_params:?}"); - Ok(consensus_params) -} - -#[cfg(test)] -mod tests { - use super::*; - use arc_consensus_types::Address; - use malachitebft_core_types::Validator as _; - - #[test] - fn test_decode_valset_abi() { - let mut pub_keys = [ - "c992c8696818bda11d628f38584022a6332c144b7f929b4d972bd39a23244aec", - "35121369a803f64463e1688af1ba5d963a40b7d71eeffadd8496f1d5b8d61d53", - "eda755457e2e7b8cb56956372611f5b5d37698eef26aa7fdb01616a6e7824f22", - ]; - let raw_valset = [ - "0000000000000000000000000000000000000000000000000000000000000020", - "0000000000000000000000000000000000000000000000000000000000000003", - "0000000000000000000000000000000000000000000000000000000000000060", - "0000000000000000000000000000000000000000000000000000000000000100", - "00000000000000000000000000000000000000000000000000000000000001a0", - "0000000000000000000000000000000000000000000000000000000000000002", - "0000000000000000000000000000000000000000000000000000000000000060", - "000000000000000000000000000000000000000000000000000000000000000f", - "0000000000000000000000000000000000000000000000000000000000000020", - pub_keys[0], - "0000000000000000000000000000000000000000000000000000000000000002", - "0000000000000000000000000000000000000000000000000000000000000060", - "000000000000000000000000000000000000000000000000000000000000000f", - "0000000000000000000000000000000000000000000000000000000000000020", - pub_keys[1], - "0000000000000000000000000000000000000000000000000000000000000002", - "0000000000000000000000000000000000000000000000000000000000000060", - "000000000000000000000000000000000000000000000000000000000000000f", - "0000000000000000000000000000000000000000000000000000000000000020", - pub_keys[2], - ]; - let result = hex::decode(raw_valset.concat()).unwrap(); - - let valset = abi_decode_validator_set(result).unwrap(); - // sort pub_keys by address - pub_keys.sort_unstable_by(|pk, pk2| { - let pk_bytes: [u8; 32] = hex::decode(pk).unwrap().try_into().unwrap(); - let pk2_bytes: [u8; 32] = hex::decode(pk2).unwrap().try_into().unwrap(); - let a1 = Address::from_public_key(&PublicKey::from_bytes(pk_bytes).unwrap()); - let a2 = Address::from_public_key(&PublicKey::from_bytes(pk2_bytes).unwrap()); - a1.cmp(&a2) - }); - - assert_eq!(valset.validators.len(), 3); - for (i, (v, expected_pk)) in valset.validators.iter().zip(&pub_keys).enumerate() { - let actual_pk = hex::encode(v.public_key().as_bytes()); - assert_eq!( - actual_pk, *expected_pk, - "Validator {i} has unexpected public key", - ); - assert_eq!( - v.voting_power(), - 15, - "Validator {i} has unexpected voting power", - ); - } - } - - /// A known-good ed25519 public key used to populate valid test validators. - const VALID_PK_A: &str = "c992c8696818bda11d628f38584022a6332c144b7f929b4d972bd39a23244aec"; - /// A second known-good ed25519 public key. - const VALID_PK_B: &str = "35121369a803f64463e1688af1ba5d963a40b7d71eeffadd8496f1d5b8d61d53"; - /// A third known-good ed25519 public key. - const VALID_PK_C: &str = "eda755457e2e7b8cb56956372611f5b5d37698eef26aa7fdb01616a6e7824f22"; - - /// Build an Active `ContractValidator` with the given 32-byte public key and voting power. - fn active_validator(pk_bytes: [u8; 32], voting_power: u64) -> ContractValidator { - ContractValidator { - status: ContractValidatorStatus::Active, - publicKey: pk_bytes.to_vec().into(), - votingPower: voting_power, - } - } - - fn pk(hex_str: &str) -> [u8; 32] { - hex::decode(hex_str).unwrap().try_into().unwrap() - } - - /// A 32-byte blob that `PublicKey::from_bytes` rejects as malformed. Derived from - /// `VALID_PK_A` by flipping bit 3 of byte 13 (`0x40` → `0x48`) — the same technique - /// used by `test_corrupted_public_key_leaves_set_empty_and_errors`. The result is - /// distinct from all `VALID_PK_*` constants so it can be combined with any of them - /// in the same test. - const MALFORMED_PK: &str = "c992c8696818bda11d628f38584822a6332c144b7f929b4d972bd39a23244aec"; - - fn malformed_pk_bytes() -> [u8; 32] { - let pk_bytes = pk(MALFORMED_PK); - assert!( - PublicKey::from_bytes(pk_bytes).is_err(), - "test precondition: MALFORMED_PK must be rejected by PublicKey::from_bytes", - ); - pk_bytes - } - - fn encode_validators(validators: Vec) -> Vec { - getActiveValidatorSetCall::abi_encode_returns(&validators) - } - - #[test] - fn test_skip_single_malformed_validator_in_set() { - // 3 active validators, middle one has a malformed public key. The decoder should - // drop the malformed validator and return a set of 2 with accurate voting power. - let validators = vec![ - active_validator(pk(VALID_PK_A), 10), - active_validator(malformed_pk_bytes(), 10), - active_validator(pk(VALID_PK_B), 10), - ]; - let encoded = encode_validators(validators); - - let valset = abi_decode_validator_set(encoded).expect("set should decode"); - - assert_eq!(valset.validators.len(), 2); - // Total voting power reflects only the retained validators; the malformed one is excluded. - assert_eq!(valset.total_voting_power(), 20); - } - - #[test] - fn test_majority_malformed_still_decodes_reduced_set() { - // 3 active validators with equal voting power; 2 are malformed. Even though malformed - // VP (2/3) far exceeds the BFT byzantine threshold, the surviving validator is - // returned — we prefer reduced security over a halted chain, and rely on operators - // alerting on `arc_validator_set_skipped_total` to drive recovery. - let validators = vec![ - active_validator(pk(VALID_PK_A), 10), - active_validator(malformed_pk_bytes(), 10), - active_validator(malformed_pk_bytes(), 10), - ]; - let encoded = encode_validators(validators); - - let valset = abi_decode_validator_set(encoded).expect("set should decode"); - - assert_eq!(valset.validators.len(), 1); - assert_eq!(valset.total_voting_power(), 10); - } - - #[test] - fn test_minority_malformed_decodes_reduced_set() { - // 4 active validators with equal voting power; 1 is malformed. Returns the reduced - // set with 3 validators and a recomputed total voting power. - let validators = vec![ - active_validator(pk(VALID_PK_A), 10), - active_validator(pk(VALID_PK_B), 10), - active_validator(pk(VALID_PK_C), 10), - active_validator(malformed_pk_bytes(), 10), - ]; - let encoded = encode_validators(validators); - - let valset = abi_decode_validator_set(encoded).expect("set should decode"); - - assert_eq!(valset.validators.len(), 3); - assert_eq!(valset.total_voting_power(), 30); - } - - #[test] - fn test_lopsided_voting_power_decodes_reduced_set() { - // Heavy-VP validator survives, light-VP one is malformed. Confirms that the - // returned total reflects only the survivors, not the contract's advertised total. - let validators = vec![ - active_validator(pk(VALID_PK_A), 90), - active_validator(malformed_pk_bytes(), 10), - ]; - let encoded = encode_validators(validators); - - let valset = abi_decode_validator_set(encoded).expect("set should decode"); - - assert_eq!(valset.validators.len(), 1); - assert_eq!(valset.total_voting_power(), 90); - } - - #[test] - fn test_corrupted_public_key_leaves_set_empty_and_errors() { - let pub_key = "c992c8696818bda11d628f38584022a6332c144b7f929b4d972bd39a23244aec"; - - let raw_valset = [ - "0000000000000000000000000000000000000000000000000000000000000020", - "0000000000000000000000000000000000000000000000000000000000000001", - "0000000000000000000000000000000000000000000000000000000000000020", - "0000000000000000000000000000000000000000000000000000000000000002", - "0000000000000000000000000000000000000000000000000000000000000060", - "0000000000000000000000000000000000000000000000000000000000000014", - "0000000000000000000000000000000000000000000000000000000000000020", - pub_key, - ]; - // First, try with valid pubkey - let mut bytes = hex::decode(raw_valset.concat()).unwrap(); - let valset = abi_decode_validator_set(bytes.clone()).unwrap(); - assert_eq!(valset.validators.len(), 1); - - let flip_byte_index = bytes.len() - 19; // Flip a bit to corrupt the public key - bytes[flip_byte_index] ^= 0b0000_1000; - - // With only one validator and that validator malformed, the filtered set is empty — - // abi_decode_validator_set returns an error rather than producing an empty ValidatorSet. - let err = abi_decode_validator_set(bytes) - .expect_err("decoding a sole malformed public key should fail"); - let msg = format!("{err:#}"); - assert!( - msg.contains("validator set would be empty"), - "unexpected error: {msg}", - ); - } - - #[test] - fn test_decode_consensus_params_abi() { - // Test data representing ABI-encoded consensus parameters - // Each uint16 is 32 bytes in ABI encoding (padded to 32 bytes) - let raw_consensus_params = [ - "00000000000000000000000000000000000000000000000000000000000003e8", // timeoutProposeMs = 1000 - "000000000000000000000000000000000000000000000000000000000000012c", // timeoutProposeDeltaMs = 300 - "00000000000000000000000000000000000000000000000000000000000007d0", // timeoutPrevoteMs = 2000 - "000000000000000000000000000000000000000000000000000000000000012c", // timeoutPrevoteDeltaMs = 300 - "0000000000000000000000000000000000000000000000000000000000000bb8", // timeoutPrecommitMs = 3000 - "000000000000000000000000000000000000000000000000000000000000012c", // timeoutPrecommitDeltaMs = 300 - "0000000000000000000000000000000000000000000000000000000000000fa0", // timeoutRebroadcastMs = 4000 - "0000000000000000000000000000000000000000000000000000000000001388", // targetBlockTimeMs = 5000 - ]; - let result = hex::decode(raw_consensus_params.concat()).unwrap(); - - let consensus_params = abi_decode_consensus_params(result).unwrap(); - - assert_eq!( - consensus_params.timeouts().propose, - Duration::from_millis(1000) - ); - assert_eq!( - consensus_params.timeouts().propose_delta, - Duration::from_millis(300) - ); - assert_eq!( - consensus_params.timeouts().prevote, - Duration::from_millis(2000) - ); - assert_eq!( - consensus_params.timeouts().prevote_delta, - Duration::from_millis(300) - ); - assert_eq!( - consensus_params.timeouts().precommit, - Duration::from_millis(3000) - ); - assert_eq!( - consensus_params.timeouts().precommit_delta, - Duration::from_millis(300) - ); - assert_eq!( - consensus_params.timeouts().rebroadcast, - Duration::from_millis(4000) - ); - assert_eq!( - consensus_params.target_block_time(), - Some(Duration::from_millis(500)) - ); - } - - #[test] - fn test_decode_consensus_params_with_zero_values_returns_default() { - // Test with all zero values - let raw_consensus_params = [ - "0000000000000000000000000000000000000000000000000000000000000000", // timeoutProposeMs = 0 - "0000000000000000000000000000000000000000000000000000000000000000", // timeoutProposeDeltaMs = 0 - "0000000000000000000000000000000000000000000000000000000000000000", // timeoutPrevoteMs = 0 - "0000000000000000000000000000000000000000000000000000000000000000", // timeoutPrevoteDeltaMs = 0 - "0000000000000000000000000000000000000000000000000000000000000000", // timeoutPrecommitMs = 0 - "0000000000000000000000000000000000000000000000000000000000000000", // timeoutPrecommitDeltaMs = 0 - "0000000000000000000000000000000000000000000000000000000000000000", // timeoutRebroadcastMs = 0 - "0000000000000000000000000000000000000000000000000000000000000000", // targetBlockTimeMs = 0 - ]; - let result = hex::decode(raw_consensus_params.concat()).unwrap(); - - let consensus_params = abi_decode_consensus_params(result).unwrap(); - let default = ConsensusParams::default(); - - assert_eq!( - consensus_params.timeouts().propose, - default.timeouts().propose - ); - assert_eq!( - consensus_params.timeouts().propose_delta, - default.timeouts().propose_delta - ); - assert_eq!( - consensus_params.timeouts().prevote, - default.timeouts().prevote - ); - assert_eq!( - consensus_params.timeouts().prevote_delta, - default.timeouts().prevote_delta - ); - assert_eq!( - consensus_params.timeouts().precommit, - default.timeouts().precommit - ); - assert_eq!( - consensus_params.timeouts().precommit_delta, - default.timeouts().precommit_delta - ); - assert_eq!( - consensus_params.timeouts().rebroadcast, - default.timeouts().rebroadcast - ); - // target_block_time should be None when 0 is provided - assert_eq!(consensus_params.target_block_time(), None); - } - - #[test] - fn test_decode_consensus_params_with_max_values_returns_default() { - // Test with maximum uint16 values (65535) - let raw_consensus_params = [ - "000000000000000000000000000000000000000000000000000000000000ffff", // timeoutProposeMs = 65535 - "000000000000000000000000000000000000000000000000000000000000ffff", // timeoutProposeDeltaMs = 65535 - "000000000000000000000000000000000000000000000000000000000000ffff", // timeoutPrevoteMs = 65535 - "000000000000000000000000000000000000000000000000000000000000ffff", // timeoutPrevoteDeltaMs = 65535 - "000000000000000000000000000000000000000000000000000000000000ffff", // timeoutPrecommitMs = 65535 - "000000000000000000000000000000000000000000000000000000000000ffff", // timeoutPrecommitDeltaMs = 65535 - "000000000000000000000000000000000000000000000000000000000000ffff", // timeoutRebroadcastMs = 65535 - "000000000000000000000000000000000000000000000000000000000000ffff", // targetBlockTimeMs = 65535 - ]; - let result = hex::decode(raw_consensus_params.concat()).unwrap(); - - let default = ConsensusParams::default(); - let consensus_params = abi_decode_consensus_params(result).unwrap(); - - assert_eq!( - consensus_params.timeouts().propose, - default.timeouts().propose - ); - assert_eq!( - consensus_params.timeouts().propose_delta, - default.timeouts().propose_delta - ); - assert_eq!( - consensus_params.timeouts().prevote, - default.timeouts().prevote - ); - assert_eq!( - consensus_params.timeouts().prevote_delta, - default.timeouts().prevote_delta - ); - assert_eq!( - consensus_params.timeouts().precommit, - default.timeouts().precommit - ); - assert_eq!( - consensus_params.timeouts().precommit_delta, - default.timeouts().precommit_delta - ); - assert_eq!( - consensus_params.timeouts().rebroadcast, - default.timeouts().rebroadcast - ); - assert_eq!( - consensus_params.target_block_time(), - default.target_block_time() - ); - } - - #[test] - fn test_decode_consensus_params_invalid_data() { - // Test with invalid data (too short) - let invalid_data = - hex::decode("0000000000000000000000000000000000000000000000000000000000000000") - .unwrap(); - - let result = abi_decode_consensus_params(invalid_data); - assert!(result.is_err(), "Expected error for invalid data"); - } -} diff --git a/crates/eth-engine/src/capabilities.rs b/crates/eth-engine/src/capabilities.rs deleted file mode 100644 index 0872f83..0000000 --- a/crates/eth-engine/src/capabilities.rs +++ /dev/null @@ -1,116 +0,0 @@ -// Copyright 2025 Circle Internet Group, Inc. All rights reserved. -// -// SPDX-License-Identifier: Apache-2.0 -// -// Licensed under the Apache License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. -// You may obtain a copy of the License at -// -// http://www.apache.org/licenses/LICENSE-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// See the License for the specific language governing permissions and -// limitations under the License. - -/// Engine capabilities -#[derive(Clone, Copy, Debug)] -pub struct EngineCapabilities { - pub new_payload_v1: bool, - pub new_payload_v2: bool, - pub new_payload_v3: bool, - pub new_payload_v4: bool, - pub forkchoice_updated_v1: bool, - pub forkchoice_updated_v2: bool, - pub forkchoice_updated_v3: bool, - pub get_payload_bodies_by_hash_v1: bool, - pub get_payload_bodies_by_range_v1: bool, - pub get_payload_v1: bool, - pub get_payload_v2: bool, - pub get_payload_v3: bool, - pub get_payload_v4: bool, - pub get_payload_v5: bool, - pub get_client_version_v1: bool, - pub get_blobs_v1: bool, -} - -impl EngineCapabilities { - /// Create EngineCapabilities from a set of capability strings - pub fn from_capabilities(capabilities: &std::collections::HashSet) -> Self { - use crate::constants::*; - - Self { - new_payload_v1: capabilities.contains(ENGINE_NEW_PAYLOAD_V1), - new_payload_v2: capabilities.contains(ENGINE_NEW_PAYLOAD_V2), - new_payload_v3: capabilities.contains(ENGINE_NEW_PAYLOAD_V3), - new_payload_v4: capabilities.contains(ENGINE_NEW_PAYLOAD_V4), - forkchoice_updated_v1: capabilities.contains(ENGINE_FORKCHOICE_UPDATED_V1), - forkchoice_updated_v2: capabilities.contains(ENGINE_FORKCHOICE_UPDATED_V2), - forkchoice_updated_v3: capabilities.contains(ENGINE_FORKCHOICE_UPDATED_V3), - get_payload_bodies_by_hash_v1: capabilities - .contains(ENGINE_GET_PAYLOAD_BODIES_BY_HASH_V1), - get_payload_bodies_by_range_v1: capabilities - .contains(ENGINE_GET_PAYLOAD_BODIES_BY_RANGE_V1), - get_payload_v1: capabilities.contains(ENGINE_GET_PAYLOAD_V1), - get_payload_v2: capabilities.contains(ENGINE_GET_PAYLOAD_V2), - get_payload_v3: capabilities.contains(ENGINE_GET_PAYLOAD_V3), - get_payload_v4: capabilities.contains(ENGINE_GET_PAYLOAD_V4), - get_payload_v5: capabilities.contains(ENGINE_GET_PAYLOAD_V5), - get_client_version_v1: capabilities.contains(ENGINE_GET_CLIENT_VERSION_V1), - get_blobs_v1: capabilities.contains(ENGINE_GET_BLOBS_V1), - } - } - - /// Create EngineCapabilities with all capabilities set to true - pub fn all() -> Self { - Self { - new_payload_v1: true, - new_payload_v2: true, - new_payload_v3: true, - new_payload_v4: true, - forkchoice_updated_v1: true, - forkchoice_updated_v2: true, - forkchoice_updated_v3: true, - get_payload_bodies_by_hash_v1: true, - get_payload_bodies_by_range_v1: true, - get_payload_v1: true, - get_payload_v2: true, - get_payload_v3: true, - get_payload_v4: true, - get_payload_v5: true, - get_client_version_v1: true, - get_blobs_v1: true, - } - } -} - -use crate::engine::EngineAPI; - -pub async fn check_capabilities(api: impl EngineAPI) -> eyre::Result<()> { - let caps = api.exchange_capabilities().await?; - - if !caps.forkchoice_updated_v3 { - eyre::bail!("Engine does not support forkchoiceUpdatedV3"); - } - - if !caps.get_payload_v4 { - eyre::bail!("Engine does not support getPayloadV4"); - } - - if !caps.new_payload_v4 { - eyre::bail!("Engine does not support newPayloadV4"); - } - - // V5 is only used when Osaka is active (decided per-block by `use_v5()`). - // Warn instead of bail so that chains without Osaka can start even if the - // EL hasn't been upgraded to advertise V5 yet (e.g. during rolling upgrades). - if !caps.get_payload_v5 { - tracing::warn!( - "Engine does not advertise getPayloadV5 — \ - Osaka blocks will fail until the EL is upgraded" - ); - } - - Ok(()) -} diff --git a/crates/eth-engine/src/constants.rs b/crates/eth-engine/src/constants.rs deleted file mode 100644 index 5adc090..0000000 --- a/crates/eth-engine/src/constants.rs +++ /dev/null @@ -1,120 +0,0 @@ -// Copyright 2025 Circle Internet Group, Inc. All rights reserved. -// -// SPDX-License-Identifier: Apache-2.0 -// -// Licensed under the Apache License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. -// You may obtain a copy of the License at -// -// http://www.apache.org/licenses/LICENSE-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// See the License for the specific language governing permissions and -// limitations under the License. - -use std::time::Duration; - -use alloy_primitives::{address, Address as AlloyAddress}; - -// Engine API method names -pub const ENGINE_NEW_PAYLOAD_V1: &str = "engine_newPayloadV1"; -pub const ENGINE_NEW_PAYLOAD_V2: &str = "engine_newPayloadV2"; -pub const ENGINE_NEW_PAYLOAD_V3: &str = "engine_newPayloadV3"; -pub const ENGINE_NEW_PAYLOAD_V4: &str = "engine_newPayloadV4"; - -pub const ENGINE_GET_PAYLOAD_V1: &str = "engine_getPayloadV1"; -pub const ENGINE_GET_PAYLOAD_V2: &str = "engine_getPayloadV2"; -pub const ENGINE_GET_PAYLOAD_V3: &str = "engine_getPayloadV3"; -pub const ENGINE_GET_PAYLOAD_V4: &str = "engine_getPayloadV4"; -pub const ENGINE_GET_PAYLOAD_V5: &str = "engine_getPayloadV5"; - -pub const ENGINE_FORKCHOICE_UPDATED_V1: &str = "engine_forkchoiceUpdatedV1"; -pub const ENGINE_FORKCHOICE_UPDATED_V2: &str = "engine_forkchoiceUpdatedV2"; -pub const ENGINE_FORKCHOICE_UPDATED_V3: &str = "engine_forkchoiceUpdatedV3"; - -pub const ENGINE_GET_PAYLOAD_BODIES_BY_HASH_V1: &str = "engine_getPayloadBodiesByHashV1"; -pub const ENGINE_GET_PAYLOAD_BODIES_BY_RANGE_V1: &str = "engine_getPayloadBodiesByRangeV1"; - -pub const ENGINE_EXCHANGE_CAPABILITIES: &str = "engine_exchangeCapabilities"; - -pub const ENGINE_GET_CLIENT_VERSION_V1: &str = "engine_getClientVersionV1"; - -pub const ENGINE_GET_BLOBS_V1: &str = "engine_getBlobsV1"; - -// Engine API timeouts -pub const ENGINE_NEW_PAYLOAD_TIMEOUT: Duration = Duration::from_secs(8); -pub const ENGINE_GET_PAYLOAD_TIMEOUT: Duration = Duration::from_secs(2); -pub const ENGINE_FORKCHOICE_UPDATED_TIMEOUT: Duration = Duration::from_secs(8); -// pub const ENGINE_GET_PAYLOAD_BODIES_TIMEOUT: Duration = Duration::from_secs(10); -pub const ENGINE_EXCHANGE_CAPABILITIES_TIMEOUT: Duration = Duration::from_secs(1); -// pub const ENGINE_GET_CLIENT_VERSION_TIMEOUT: Duration = Duration::from_secs(1); -// pub const ENGINE_GET_BLOBS_TIMEOUT: Duration = Duration::from_secs(1); - -// Ethereum API timeouts -pub const ETH_DEFAULT_TIMEOUT: Duration = Duration::from_secs(1); -pub const ETH_BATCH_REQUEST_TIMEOUT: Duration = Duration::from_secs(30); - -// IPC client-level timeout (upper-bound safety net set at jsonrpsee Client construction) -pub const IPC_CLIENT_TIMEOUT: Duration = Duration::from_secs(60); - -// Retry policy for eth_call requests (validator set, consensus params). -pub const ETH_CALL_RETRY: backon::FibonacciBuilder = backon::FibonacciBuilder::new() - .with_max_times(5) - .with_min_delay(Duration::from_millis(100)) - .with_max_delay(Duration::from_secs(1)); - -pub const INITIAL_RETRY_DELAY: Duration = Duration::from_secs(3); - -// Engine API retries for IPC -- No need to keep trying forever (already done when connecting to IPC socket). -pub const ENGINE_EXCHANGE_CAPABILITIES_RETRY_IPC: backon::FibonacciBuilder = - backon::FibonacciBuilder::new() - .with_min_delay(Duration::from_millis(100)) - .with_max_delay(Duration::from_secs(1)) - .with_max_times(30); - -// Retry policy for IPC Engine API calls (newPayload, forkchoiceUpdated, getPayload). -// All three are idempotent per the Engine API spec. -pub const ENGINE_API_RETRY_IPC: backon::FibonacciBuilder = backon::FibonacciBuilder::new() - .with_min_delay(Duration::from_millis(200)) - .with_max_delay(Duration::from_secs(2)) - .with_max_times(5); - -// Engine API retries for RPC -- First call to `reth`, keep retrying indefinitely. -pub const ENGINE_EXCHANGE_CAPABILITIES_RETRY_RPC: backon::ConstantBuilder = - backon::ConstantBuilder::new() - .with_delay(INITIAL_RETRY_DELAY) - .without_max_times(); - -// Engine API methods supported by this implementation -pub static NODE_CAPABILITIES: &[&str] = &[ - // ENGINE_NEW_PAYLOAD_V1, - // ENGINE_NEW_PAYLOAD_V2, - // ENGINE_NEW_PAYLOAD_V3, - ENGINE_NEW_PAYLOAD_V4, - // ENGINE_GET_PAYLOAD_V1, - // ENGINE_GET_PAYLOAD_V2, - // ENGINE_GET_PAYLOAD_V3, - ENGINE_GET_PAYLOAD_V4, - ENGINE_GET_PAYLOAD_V5, - // ENGINE_FORKCHOICE_UPDATED_V1, - // ENGINE_FORKCHOICE_UPDATED_V2, - ENGINE_FORKCHOICE_UPDATED_V3, - // ENGINE_GET_PAYLOAD_BODIES_BY_HASH_V1, - // ENGINE_GET_PAYLOAD_BODIES_BY_RANGE_V1, - // ENGINE_GET_CLIENT_VERSION_V1, - // ENGINE_GET_BLOBS_V1, -]; - -/// ProtocolConfig, ValidatorRegistry and PermissionedValidatorManager are -/// AdminUpgradeableProxy contracts. It means that their addresses will always -/// stay the same. The underlying contract addresses might change, but the -/// client code doesn't need to know about that. -/// -/// see scripts/genesis/addresses.ts -pub(crate) const VALIDATOR_REGISTRY_ADDRESS: AlloyAddress = - address!("0x3600000000000000000000000000000000000002"); - -pub(crate) const PROTOCOL_CONFIG_ADDRESS: AlloyAddress = - address!("0x3600000000000000000000000000000000000001"); diff --git a/crates/eth-engine/src/engine.rs b/crates/eth-engine/src/engine.rs deleted file mode 100644 index 180116a..0000000 --- a/crates/eth-engine/src/engine.rs +++ /dev/null @@ -1,875 +0,0 @@ -// Copyright 2025 Circle Internet Group, Inc. All rights reserved. -// -// SPDX-License-Identifier: Apache-2.0 -// -// Licensed under the Apache License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. -// You may obtain a copy of the License at -// -// http://www.apache.org/licenses/LICENSE-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// See the License for the specific language governing permissions and -// limitations under the License. - -use eyre::{eyre, Context}; -use reqwest::Url; -use std::{ - ops::Deref, - path::Path, - sync::{Arc, OnceLock}, - time::{SystemTime, UNIX_EPOCH}, -}; -use tokio::sync::watch; - -use async_trait::async_trait; -use tracing::debug; - -use alloy_rpc_types_engine::{ - ExecutionPayloadV3, ForkchoiceUpdated, PayloadAttributes, PayloadId as AlloyPayloadId, - PayloadStatus, PayloadStatusEnum, -}; -use alloy_rpc_types_txpool::{TxpoolInspect, TxpoolStatus}; - -use arc_consensus_types::{Address, BlockHash, ConsensusParams, ValidatorSet, B256}; - -use crate::capabilities::{check_capabilities, EngineCapabilities}; -use crate::ipc::{engine_ipc::EngineIPC, ethereum_ipc::EthereumIPC}; -use crate::json_structures::ExecutionBlock; -use crate::rpc::{engine_rpc::EngineRpc, ethereum_rpc::EthereumRPC}; - -/// A subscription-capable transport endpoint for the execution layer. -/// -/// Exposed so that callers (e.g. startup replay, RPC sync) can establish -/// auxiliary subscriptions without Engine needing to know about those concerns. -#[derive(Clone, Debug)] -pub enum SubscriptionEndpoint { - Ipc { socket_path: String }, - Ws { url: Url }, -} - -#[cfg_attr(any(test, feature = "mocks"), mockall::automock)] -#[async_trait] -pub trait EngineAPI: Send + Sync { - /// Exchange capabilities with the engine. - async fn exchange_capabilities(&self) -> eyre::Result; - /// Set the latest forkchoice state. - async fn forkchoice_updated( - &self, - head_block_hash: BlockHash, - maybe_payload_attributes: Option, - ) -> eyre::Result; - /// Get a payload by its ID. - /// When `use_v5` is true, uses `engine_getPayloadV5` (Osaka); otherwise uses V4. - async fn get_payload( - &self, - payload_id: AlloyPayloadId, - use_v5: bool, - ) -> eyre::Result; - /// Notify that a new payload has been created. - async fn new_payload( - &self, - execution_payload: &ExecutionPayloadV3, - versioned_hashes: Vec, - parent_block_hash: BlockHash, - ) -> eyre::Result; -} - -#[cfg_attr(any(test, feature = "mocks"), mockall::automock)] -#[async_trait] -pub trait EthereumAPI: Send + Sync { - /// Get the eth1 chain id of the given endpoint. - async fn get_chain_id(&self) -> eyre::Result; - /// Get the genesis block. - async fn get_genesis_block(&self) -> eyre::Result; - /// Get the active validator set at a specific block height. - async fn get_active_validator_set(&self, block_height: u64) -> eyre::Result; - /// Get the consensus parameters at a specific block height. - async fn get_consensus_params(&self, block_height: u64) -> eyre::Result; - /// Get a block by its number. - async fn get_block_by_number(&self, block_number: &str) - -> eyre::Result>; - /// Get multiple full payloads. - async fn get_execution_payloads( - &self, - block_numbers: &[String], - ) -> eyre::Result>>; - /// Get the status of the transaction pool. - async fn txpool_status(&self) -> eyre::Result; - /// Get the contents of the transaction pool. - async fn txpool_inspect(&self) -> eyre::Result; -} - -/// Function that checks whether Osaka is active at a given timestamp. -/// Used by the Engine to decide between `engine_getPayloadV4` and `engine_getPayloadV5`. -pub type IsOsakaActiveFn = Arc bool + Send + Sync>; - -/// Ethereum engine implementation. -/// Spec: https://github.com/ethereum/execution-apis/tree/main/src/engine -#[derive(Clone)] -pub struct Engine(Arc); - -impl Engine { - /// Create a new engine using IPC. - pub async fn new_ipc(execution_socket: &str, eth_socket: &str) -> eyre::Result { - let api = EngineIPC::new(execution_socket).await?; - let eth = EthereumIPC::new(eth_socket).await?; - - let api_disconnect = api.on_disconnect(); - let eth_disconnect = eth.on_disconnect(); - - let (disconnect_tx, disconnect_rx) = watch::channel(false); - tokio::spawn(async move { - tokio::select! { - _ = api_disconnect => {}, - _ = eth_disconnect => {}, - } - disconnect_tx.send(true).ok(); - }); - - let sub_endpoint = SubscriptionEndpoint::Ipc { - socket_path: eth_socket.to_owned(), - }; - Ok(Self(Arc::new(Inner::new( - Box::new(api), - Box::new(eth), - Some(sub_endpoint), - Some(disconnect_rx), - )))) - } - - /// Create a new engine using RPC. - /// - /// Probes the RPC server with `net_listening` to confirm it is - /// reachable before returning. - pub async fn new_rpc( - execution_endpoint: Url, - eth_endpoint: Url, - ws_endpoint: Option, - execution_jwt: &str, - ) -> eyre::Result { - let api = Box::new(EngineRpc::new( - execution_endpoint, - Path::new(execution_jwt), - )?); - let eth = Box::new(EthereumRPC::new(eth_endpoint)?); - - // Probe the RPC server to confirm it is reachable. - eth.check_connectivity().await?; - - let sub_endpoint = ws_endpoint.map(|url| SubscriptionEndpoint::Ws { url }); - Ok(Self(Arc::new(Inner::new(api, eth, sub_endpoint, None)))) - } - - /// Create a new engine with custom API implementations. - pub fn new(api: Box, eth: Box) -> Self { - Self(Arc::new(Inner::new(api, eth, None, None))) - } - - /// Resolves when either IPC connection to the EL closes. - /// - /// Stays pending forever for non-IPC engines (RPC, mock), so calling code can - /// unconditionally `select!` on this without special-casing the transport. - pub async fn wait_for_disconnect(&self) { - match &self.disconnect_rx { - // wait_for checks the current value first, so late subscribers see a prior disconnect. - Some(rx) => { - rx.clone().wait_for(|&v| v).await.ok(); - } - None => std::future::pending().await, - } - } - - /// Set the function that determines whether Osaka is active at a given timestamp. - /// - /// This should be called after construction once the chainspec is known. - /// The provided function should use the same chainspec as the EL so that - /// the V4/V5 decision always aligns. - pub fn set_is_osaka_active(&self, f: IsOsakaActiveFn) { - if self.is_osaka_active.set(f).is_err() { - tracing::warn!("Osaka activation function already set; ignoring duplicate call"); - } - } - - /// Configure the Osaka hardfork check by parsing the given genesis.json file. - /// - /// Builds an `ArcChainSpec` from the same file the EL uses, so the V4/V5 - /// decision always aligns — even when the file is patched at runtime - /// (e.g. nightly-upgrade tests that set a future `osakaTime`). - pub fn set_osaka_from_genesis_file(&self, genesis_path: &str) -> eyre::Result<()> { - use arc_execution_config::chainspec::ArcChainSpec; - use reth_chainspec::EthereumHardforks; - - let raw = std::fs::read_to_string(genesis_path) - .wrap_err_with(|| format!("Failed to read genesis file: {genesis_path}"))?; - let genesis: alloy_genesis::Genesis = serde_json::from_str(&raw) - .wrap_err_with(|| format!("Failed to parse genesis file: {genesis_path}"))?; - let chainspec = Arc::new(ArcChainSpec::from(genesis)); - let osaka_active_at_zero = chainspec.is_osaka_active_at_timestamp(0); - tracing::info!( - genesis_path, - osaka_active_at_zero, - "Osaka activation configured from genesis file" - ); - self.set_is_osaka_active(Arc::new(move |timestamp| { - chainspec.is_osaka_active_at_timestamp(timestamp) - })); - Ok(()) - } - - /// Configure the Osaka hardfork check from the static chainspec matching - /// the given chain ID. - /// - /// This is the fallback when `--genesis` is not provided. - pub fn set_osaka_from_chain_id(&self, chain_id: u64) { - use arc_execution_config::chainspec::bundled_chainspec_for_chain_id; - use reth_chainspec::EthereumHardforks; - - let Some(chainspec) = bundled_chainspec_for_chain_id(chain_id) else { - tracing::warn!( - chain_id, - "Unknown chain ID for Osaka activation; defaulting to V4 (Osaka disabled)" - ); - return; - }; - - tracing::info!( - chain_id, - "Osaka activation configured from static chainspec" - ); - self.set_is_osaka_active(Arc::new(move |timestamp| { - chainspec.is_osaka_active_at_timestamp(timestamp) - })); - } - - /// Returns the duration since the unix epoch. - pub fn timestamp_now() -> u64 { - SystemTime::now() - .duration_since(UNIX_EPOCH) - .expect("Clock is before UNIX epoch!") - .as_secs() - } - - /// Returns the subscription-capable endpoint for the execution layer, - /// if one was configured. `None` for test/mock engines or RPC without - /// `--execution-ws-endpoint`. - pub fn subscription_endpoint(&self) -> Option<&SubscriptionEndpoint> { - self.subscription_endpoint.as_ref() - } -} - -impl Deref for Engine { - type Target = Inner; - - fn deref(&self) -> &Self::Target { - &self.0 - } -} - -pub struct Inner { - /// Client for Engine API. - pub api: Box, - /// Client for Ethereum API. - pub eth: Box, - /// Subscription-capable endpoint for the execution layer. - subscription_endpoint: Option, - /// Optional function to check if Osaka is active at a given timestamp. - /// Set after construction via [`Engine::set_is_osaka_active`]. - /// When `None`, defaults to `false` (use V4). - is_osaka_active: OnceLock, - /// Set to `true` when either IPC connection to the EL drops. `None` for non-IPC engines. - disconnect_rx: Option>, -} - -impl Inner { - fn new( - api: Box, - eth: Box, - subscription_endpoint: Option, - disconnect_rx: Option>, - ) -> Self { - Self { - api, - eth, - subscription_endpoint, - is_osaka_active: OnceLock::new(), - disconnect_rx, - } - } - - /// Returns whether Osaka is active at the given timestamp. - fn use_v5(&self, timestamp: u64) -> bool { - self.is_osaka_active - .get() - .map(|f| f(timestamp)) - .unwrap_or(false) - } - - /// Check if the execution client supports the required methods. - pub async fn check_capabilities(&self) -> eyre::Result<()> { - check_capabilities(&self.api).await - } - - /// Set the latest forkchoice state. - pub async fn set_latest_forkchoice_state( - &self, - head_block_hash: BlockHash, - ) -> eyre::Result { - debug!("🟠 set_latest_forkchoice_state: {:?}", head_block_hash); - // Make this specific block the canonical chain head. - // - // EL Process: - // - Retrieves pre-validated block from "validated payloads pool" - // - Promotes the stored state to canonical state - // - Updates canonical chain head pointer - // - All other validated blocks for this height remain non-canonical - let ForkchoiceUpdated { - payload_status, - payload_id, - } = self.api.forkchoice_updated(head_block_hash, None).await?; - match (payload_status.status, payload_id) { - (PayloadStatusEnum::Valid, Some(payload_id)) => Err(eyre!( - "When setting latest forkchoice, payload ID should be None, got: {:?}", - payload_id - )), - (PayloadStatusEnum::Valid, None) => payload_status.latest_valid_hash.ok_or_else(|| { - eyre!("set_latest_forkchoice_state: Valid status must have latest_valid_hash") - }), - (PayloadStatusEnum::Syncing, _) if payload_status.latest_valid_hash.is_none() => { - // Engine API spec 8: SYNCING with null latestValidHash means the head - // references an unknown payload. Arc never hits this path because we - // always call new_payload before forkchoice_updated, so treat it as error. - Err(eyre!( - "set_latest_forkchoice_state: headBlockHash={:?} references an unknown payload or a payload that can't be validated", - head_block_hash - )) - } - (status, _) => Err(eyre!( - "set_latest_forkchoice_state: Invalid payload status: {}", - status - )), - } - } - - /// Build a new block. - /// - latest_block: The latest block to generate a new block on top of. - /// - timestamp: Unix timestamp for when the payload is expected to be executed. - /// It should be greater than or equal to that of forkchoiceState.headBlockHash. - pub async fn generate_block( - &self, - latest_block: &ExecutionBlock, - timestamp: u64, - suggested_fee_recipient: &Address, - ) -> eyre::Result { - debug!("🟠 Generating block on top of {}", latest_block.block_hash); - - let block_hash = latest_block.block_hash; - - let payload_attributes = PayloadAttributes { - timestamp, - - // Usually derived from the RANDAO mix (randomness accumulator) of the - // parent beacon block. The beacon chain generates this value using - // aggregated validator signatures over time. - // Its purpose is to expose the consensus layer’s randomness to the EVM. - // Arc, however, has neither a beacon chain, nor beacon blocks, so we - // set it to zero to indicate that Arc doesn't use it. - // NOTE: Smart contracts should therefore not rely on this value for - // randomness. - prev_randao: B256::ZERO, - - suggested_fee_recipient: suggested_fee_recipient.to_alloy_address(), - - // Cannot be None in V3. - withdrawals: Some(vec![]), - - // Cannot be None in V3. Arc has no beacon chain, so we use the - // execution block hash as parent_beacon_block_root. - parent_beacon_block_root: Some(block_hash), - }; - // Build a new block on top of parent_hash - // - // EL Process: - // - Sets working state to parent_hash - // - Collects transactions from mempool - // - Executes transactions → computes new state - // - Stores complete block in "built payloads pool" indexed by payload_id - // - // Returns payload_id (ticket to retrieve the built block) - let ForkchoiceUpdated { - payload_status, - payload_id, - } = self - .api - .forkchoice_updated(block_hash, Some(payload_attributes)) - .await - .wrap_err_with(|| { - format!("generate_block: forkchoice_updated failed; last_block {block_hash}") - })?; - - match payload_status.status { - PayloadStatusEnum::Valid => { - let Some(payload_id) = payload_id else { - return Err(eyre!( - "When generating new payload, payload ID must be Some, got: {:?}", - payload_id - )); - }; - if payload_status.latest_valid_hash != Some(block_hash) { - return Err(eyre!( - "When generating new payload, latest_valid_hash must be the parent block hash: {:?}, got: {:?}", - block_hash, - payload_status.latest_valid_hash - )); - } - - // Complete ExecutionPayloadV3 with all transactions and computed state - // See https://github.com/ethereum/consensus-specs/blob/v1.1.5/specs/merge/validator.md#block-proposal - let use_v5 = self.use_v5(timestamp); - match self.api.get_payload(payload_id, use_v5).await { - Ok(payload) => Ok(payload), - Err(e) => Err(e).wrap_err_with(|| { - format!("generate_block: get_payload failed; payload_id {payload_id}") - }), - } - } - PayloadStatusEnum::Syncing if payload_status.latest_valid_hash.is_none() => { - // Engine API spec 8: SYNCING with null latestValidHash means the head - // references an unknown payload. Arc never hits this path because we - // always call new_payload before forkchoice_updated, so treat it as error. - Err(eyre!( - "generate_block: headBlockHash={:?} references an unknown payload or a payload that can't be validated", - block_hash - )) - } - status => Err(eyre!("generate_block: Invalid payload status: {}", status)), - } - } - - /// Notify that a new block has been created. - /// - execution_payload: The execution payload - /// - versioned_hashes: The hashes of the blobs in the execution payload. - pub async fn notify_new_block( - &self, - execution_payload: &ExecutionPayloadV3, - versioned_hashes: Vec, - ) -> eyre::Result { - let parent_block_hash = execution_payload.payload_inner.payload_inner.parent_hash; - // Validate the block I just received. - // - // EL Process: - // - Sets working state to parent_hash - // - Executes transactions from execution_payload - // - Computes state → verifies against execution_payload's state_root - // - Stores validated block in "validated payloads pool" indexed by block_hash - // - Returns VALID/INVALID - // - // Hopefully skips re-execution and just moves the pre-computed state to "validated payloads pool" on a proposer. - self.api - .new_payload(execution_payload, versioned_hashes, parent_block_hash) - .await - } -} - -#[async_trait] -impl EngineAPI for &T -where - T: EngineAPI + ?Sized, -{ - async fn exchange_capabilities(&self) -> eyre::Result { - (**self).exchange_capabilities().await - } - - async fn forkchoice_updated( - &self, - head_block_hash: BlockHash, - maybe_payload_attributes: Option, - ) -> eyre::Result { - (**self) - .forkchoice_updated(head_block_hash, maybe_payload_attributes) - .await - } - - async fn get_payload( - &self, - payload_id: AlloyPayloadId, - use_v5: bool, - ) -> eyre::Result { - (**self).get_payload(payload_id, use_v5).await - } - - async fn new_payload( - &self, - execution_payload: &ExecutionPayloadV3, - versioned_hashes: Vec, - parent_block_hash: BlockHash, - ) -> eyre::Result { - (**self) - .new_payload(execution_payload, versioned_hashes, parent_block_hash) - .await - } -} - -#[async_trait] -impl EngineAPI for Box { - async fn exchange_capabilities(&self) -> eyre::Result { - (**self).exchange_capabilities().await - } - - async fn forkchoice_updated( - &self, - head_block_hash: BlockHash, - maybe_payload_attributes: Option, - ) -> eyre::Result { - (**self) - .forkchoice_updated(head_block_hash, maybe_payload_attributes) - .await - } - - async fn get_payload( - &self, - payload_id: AlloyPayloadId, - use_v5: bool, - ) -> eyre::Result { - (**self).get_payload(payload_id, use_v5).await - } - - async fn new_payload( - &self, - execution_payload: &ExecutionPayloadV3, - versioned_hashes: Vec, - parent_block_hash: BlockHash, - ) -> eyre::Result { - (**self) - .new_payload(execution_payload, versioned_hashes, parent_block_hash) - .await - } -} - -#[async_trait] -impl EthereumAPI for &T -where - T: EthereumAPI + ?Sized, -{ - async fn get_chain_id(&self) -> eyre::Result { - (**self).get_chain_id().await - } - - async fn get_genesis_block(&self) -> eyre::Result { - (**self).get_genesis_block().await - } - - async fn get_active_validator_set(&self, block_height: u64) -> eyre::Result { - (**self).get_active_validator_set(block_height).await - } - - async fn get_consensus_params(&self, block_height: u64) -> eyre::Result { - (**self).get_consensus_params(block_height).await - } - - async fn get_block_by_number( - &self, - block_number: &str, - ) -> eyre::Result> { - (**self).get_block_by_number(block_number).await - } - - async fn get_execution_payloads( - &self, - block_numbers: &[String], - ) -> eyre::Result>> { - (**self).get_execution_payloads(block_numbers).await - } - - async fn txpool_status(&self) -> eyre::Result { - (**self).txpool_status().await - } - - async fn txpool_inspect(&self) -> eyre::Result { - (**self).txpool_inspect().await - } -} - -#[async_trait] -impl EthereumAPI for Box { - async fn get_chain_id(&self) -> eyre::Result { - (**self).get_chain_id().await - } - - async fn get_genesis_block(&self) -> eyre::Result { - (**self).get_genesis_block().await - } - - async fn get_active_validator_set(&self, block_height: u64) -> eyre::Result { - (**self).get_active_validator_set(block_height).await - } - - async fn get_consensus_params(&self, block_height: u64) -> eyre::Result { - (**self).get_consensus_params(block_height).await - } - - async fn get_block_by_number( - &self, - block_number: &str, - ) -> eyre::Result> { - (**self).get_block_by_number(block_number).await - } - - async fn get_execution_payloads( - &self, - block_numbers: &[String], - ) -> eyre::Result>> { - (**self).get_execution_payloads(block_numbers).await - } - - async fn txpool_status(&self) -> eyre::Result { - (**self).txpool_status().await - } - - async fn txpool_inspect(&self) -> eyre::Result { - (**self).txpool_inspect().await - } -} - -#[cfg(test)] -mod tests { - use std::time::Duration; - - use arc_execution_config::chain_ids::*; - use rstest::rstest; - use tokio::time::timeout; - - use super::*; - - fn mock_engine() -> Engine { - Engine::new( - Box::new(MockEngineAPI::new()), - Box::new(MockEthereumAPI::new()), - ) - } - - fn engine_with_watch() -> (Engine, watch::Sender) { - let (tx, rx) = watch::channel(false); - let engine = Engine(Arc::new(Inner::new( - Box::new(MockEngineAPI::new()), - Box::new(MockEthereumAPI::new()), - None, - Some(rx), - ))); - (engine, tx) - } - - /// Bind a silent IPC server at `path`. Accepts one connection and holds it open - /// until the returned sender is dropped (or sends), then drops the stream. - async fn start_silent_ipc_server(path: &str) -> tokio::sync::oneshot::Sender<()> { - use tokio::net::UnixListener; - let listener = UnixListener::bind(path).unwrap(); - let (tx, rx) = tokio::sync::oneshot::channel::<()>(); - tokio::spawn(async move { - if let Ok((stream, _)) = listener.accept().await { - let _ = rx.await; - drop(stream); - } - }); - tx - } - - #[test] - fn test_timestamp_now() { - let now = Engine::timestamp_now(); - assert!(now > 0); - } - - #[rstest] - #[case::osaka_at_zero( - r#"{"config":{"chainId":1337,"osakaTime":0},"alloc":{}}"#, - &[(0, true), (9999, true)], - )] - #[case::future_osaka( - r#"{"config":{"chainId":1337,"osakaTime":5000},"alloc":{}}"#, - &[(0, false), (4999, false), (5000, true), (10000, true)], - )] - #[case::no_osaka( - r#"{"config":{"chainId":1337},"alloc":{}}"#, - &[(0, false), (u64::MAX, false)], - )] - fn test_set_osaka_from_genesis_file( - #[case] genesis_json: &str, - #[case] expectations: &[(u64, bool)], - ) { - let tmp = tempfile::NamedTempFile::new().unwrap(); - std::fs::write(tmp.path(), genesis_json).unwrap(); - - let engine = mock_engine(); - engine - .set_osaka_from_genesis_file(tmp.path().to_str().unwrap()) - .unwrap(); - - for &(timestamp, expected) in expectations { - assert_eq!( - engine.use_v5(timestamp), - expected, - "use_v5({timestamp}) should be {expected}" - ); - } - } - - #[rstest] - #[case::missing_file(None, "/nonexistent/genesis.json")] - #[case::invalid_json(Some("not json"), "")] - fn test_set_osaka_from_genesis_file_errors( - #[case] file_content: Option<&str>, - #[case] hardcoded_path: &str, - ) { - let tmp; - let path = match file_content { - Some(content) => { - tmp = tempfile::NamedTempFile::new().unwrap(); - std::fs::write(tmp.path(), content).unwrap(); - tmp.path().to_str().unwrap() - } - None => hardcoded_path, - }; - - let engine = mock_engine(); - assert!(engine.set_osaka_from_genesis_file(path).is_err()); - assert!(!engine.use_v5(0), "osaka should remain unset after error"); - } - - #[rstest] - #[case::localdev(LOCALDEV_CHAIN_ID, true)] - #[case::devnet(DEVNET_CHAIN_ID, false)] - #[case::testnet(TESTNET_CHAIN_ID, false)] - #[case::unknown(999999, false)] - fn test_set_osaka_from_chain_id(#[case] chain_id: u64, #[case] osaka_at_zero: bool) { - let engine = mock_engine(); - engine.set_osaka_from_chain_id(chain_id); - - assert_eq!( - engine.use_v5(0), - osaka_at_zero, - "use_v5(0) for chain_id {chain_id}" - ); - } - - #[test] - fn test_set_osaka_from_chain_id_mainnet() { - use arc_execution_config::chainspec::bundled_chainspec_for_chain_id; - - let engine = mock_engine(); - engine.set_osaka_from_chain_id(MAINNET_CHAIN_ID); - - let expected = bundled_chainspec_for_chain_id(MAINNET_CHAIN_ID).is_some(); - assert_eq!( - engine.use_v5(0), - expected, - "mainnet use_v5(0) must agree with bundle status" - ); - } - - /// Second call to `set_is_osaka_active` are silently ignored. - #[test] - fn test_set_is_osaka_active_subsequent_calls_are_ignored() { - let engine = mock_engine(); - engine.set_is_osaka_active(Arc::new(|_| true)); - engine.set_is_osaka_active(Arc::new(|_| false)); - assert!( - engine.use_v5(0), - "subsequent calls to set_is_osaka_active must be ignored" - ); - } - - #[test] - fn test_subscription_endpoint_none_for_mock() { - let engine = mock_engine(); - assert!(engine.subscription_endpoint().is_none()); - } - - #[tokio::test] - async fn wait_for_disconnect_never_resolves_for_mock_engine() { - let engine = mock_engine(); - let result = timeout(Duration::from_millis(10), engine.wait_for_disconnect()).await; - assert!(result.is_err(), "mock engine should never disconnect"); - } - - #[tokio::test] - async fn wait_for_disconnect_resolves_when_signalled() { - let (engine, tx) = engine_with_watch(); - tx.send(true).unwrap(); - timeout(Duration::from_millis(100), engine.wait_for_disconnect()) - .await - .expect("should resolve after send(true)"); - } - - #[tokio::test] - async fn wait_for_disconnect_resolves_when_sender_dropped() { - let (engine, tx) = engine_with_watch(); - drop(tx); // sender dropped without sending true — Err(RecvError) path - timeout(Duration::from_millis(10), engine.wait_for_disconnect()) - .await - .expect("should resolve when sender is dropped"); - } - - #[tokio::test] - async fn wait_for_disconnect_resolves_if_already_disconnected() { - let (engine, tx) = engine_with_watch(); - // Signal disconnect before anyone calls wait_for_disconnect. - tx.send(true).unwrap(); - drop(tx); - // Late subscriber must see the already-set state immediately. - timeout(Duration::from_millis(10), engine.wait_for_disconnect()) - .await - .expect("late subscriber should see already-disconnected state"); - } - - #[tokio::test] - async fn new_ipc_disconnect_fires_when_connection_closes() { - let tmp = tempfile::TempDir::new().unwrap(); - let engine_sock = tmp - .path() - .join("engine.sock") - .to_string_lossy() - .into_owned(); - let eth_sock = tmp.path().join("eth.sock").to_string_lossy().into_owned(); - - let close_engine = start_silent_ipc_server(&engine_sock).await; - let _close_eth = start_silent_ipc_server(ð_sock).await; - - let engine = Engine::new_ipc(&engine_sock, ð_sock) - .await - .expect("should connect to mock IPC servers"); - - // Drop closes the sender → receiver Err → stream dropped → client sees EOF - drop(close_engine); - - timeout(Duration::from_millis(500), engine.wait_for_disconnect()) - .await - .expect("disconnect should fire when engine IPC connection closes"); - } - - #[tokio::test] - async fn new_ipc_disconnect_fires_for_eth_socket() { - let tmp = tempfile::TempDir::new().unwrap(); - let engine_sock = tmp - .path() - .join("engine2.sock") - .to_string_lossy() - .into_owned(); - let eth_sock = tmp.path().join("eth2.sock").to_string_lossy().into_owned(); - - let _close_engine = start_silent_ipc_server(&engine_sock).await; - let close_eth = start_silent_ipc_server(ð_sock).await; - - let engine = Engine::new_ipc(&engine_sock, ð_sock) - .await - .expect("should connect to mock IPC servers"); - - drop(close_eth); - - timeout(Duration::from_millis(500), engine.wait_for_disconnect()) - .await - .expect("disconnect should fire when eth IPC connection closes"); - } -} diff --git a/crates/eth-engine/src/ipc/engine_ipc.rs b/crates/eth-engine/src/ipc/engine_ipc.rs deleted file mode 100644 index 7a65c0d..0000000 --- a/crates/eth-engine/src/ipc/engine_ipc.rs +++ /dev/null @@ -1,194 +0,0 @@ -// Copyright 2025 Circle Internet Group, Inc. All rights reserved. -// -// SPDX-License-Identifier: Apache-2.0 -// -// Licensed under the Apache License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. -// You may obtain a copy of the License at -// -// http://www.apache.org/licenses/LICENSE-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// See the License for the specific language governing permissions and -// limitations under the License. - -use alloy_rpc_types_engine::{ - ExecutionPayloadEnvelopeV4, ExecutionPayloadEnvelopeV5, ExecutionPayloadV3, ForkchoiceState, - ForkchoiceUpdated, PayloadAttributes, PayloadId as AlloyPayloadId, PayloadStatus, -}; -use async_trait::async_trait; -use backon::{Backoff, BackoffBuilder}; -use jsonrpsee::core::traits::ToRpcParams; -use jsonrpsee::rpc_params; -use serde::de::DeserializeOwned; -use std::time::Duration; - -use arc_consensus_types::{BlockHash, Bytes, B256}; - -use crate::capabilities::EngineCapabilities; -use crate::constants::*; -use crate::engine::EngineAPI; -use crate::ipc::ipc_builder::Ipc; - -/// Engine API client for connecting to Engine IPC via Unix Domain Socket. -pub struct EngineIPC { - ipc: Ipc, -} - -impl std::fmt::Display for EngineIPC { - fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { - write!(f, "EngineIPC:{}", self.ipc.socket_path()) - } -} - -impl EngineIPC { - /// Create a new `EngineIPC` struct given the IPC socket path. - pub async fn new(socket_path: &str) -> eyre::Result { - Self::new_with_timeout(socket_path, IPC_CLIENT_TIMEOUT).await - } - - /// Create a new `EngineIPC` struct with custom timeout. - pub async fn new_with_timeout(socket_path: &str, timeout: Duration) -> eyre::Result { - let ipc = Ipc::new_with_timeout(socket_path, timeout).await?; - Ok(Self { ipc }) - } - - /// Returns a future that resolves when the IPC connection closes. - pub fn on_disconnect(&self) -> impl std::future::Future + 'static { - let client = self.ipc.client_arc(); - async move { - let _ = client.on_disconnect().await; - } - } - - /// Send an RPC request to the Engine RPC endpoint via IPC. - pub async fn rpc_request( - &self, - method: &str, - params: impl ToRpcParams + Clone + Send, - timeout: Duration, - retry_policy: impl Backoff, - ) -> eyre::Result { - self.ipc - .rpc_request(method, params, timeout, retry_policy) - .await - } -} - -#[async_trait] -impl EngineAPI for EngineIPC { - /// Exchange capabilities with the Engine RPC endpoint. - async fn exchange_capabilities(&self) -> eyre::Result { - let capabilities: std::collections::HashSet = self - .rpc_request( - ENGINE_EXCHANGE_CAPABILITIES, - rpc_params![NODE_CAPABILITIES], - ENGINE_EXCHANGE_CAPABILITIES_TIMEOUT, - ENGINE_EXCHANGE_CAPABILITIES_RETRY_IPC.build(), - ) - .await?; - - Ok(EngineCapabilities::from_capabilities(&capabilities)) - } - - /// Notify that a fork choice has been updated, to set the head of the chain - /// - head_block_hash: The block hash of the head of the chain - /// - maybe_payload_attributes: Optional payload attributes for the next block - async fn forkchoice_updated( - &self, - head_block_hash: BlockHash, - maybe_payload_attributes: Option, - ) -> eyre::Result { - let forkchoice_state = ForkchoiceState { - head_block_hash, - safe_block_hash: head_block_hash, - finalized_block_hash: head_block_hash, - }; - - let params = if let Some(attrs) = maybe_payload_attributes { - rpc_params![forkchoice_state, attrs] - } else { - rpc_params![forkchoice_state] - }; - - self.rpc_request( - ENGINE_FORKCHOICE_UPDATED_V3, - params, - ENGINE_FORKCHOICE_UPDATED_TIMEOUT, - ENGINE_API_RETRY_IPC.build(), - ) - .await - } - - /// Get a payload by its ID. - /// Uses V5 (Osaka) when `use_v5` is true, otherwise V4. - async fn get_payload( - &self, - payload_id: AlloyPayloadId, - use_v5: bool, - ) -> eyre::Result { - if use_v5 { - let ExecutionPayloadEnvelopeV5 { - execution_payload, .. - } = self - .rpc_request( - ENGINE_GET_PAYLOAD_V5, - rpc_params![payload_id], - ENGINE_GET_PAYLOAD_TIMEOUT, - ENGINE_API_RETRY_IPC.build(), - ) - .await?; - Ok(execution_payload) - } else { - let ExecutionPayloadEnvelopeV4 { envelope_inner, .. } = self - .rpc_request( - ENGINE_GET_PAYLOAD_V4, - rpc_params![payload_id], - ENGINE_GET_PAYLOAD_TIMEOUT, - ENGINE_API_RETRY_IPC.build(), - ) - .await?; - Ok(envelope_inner.execution_payload) - } - } - - /// Notify that a new payload has been created. - /// - execution_payload: The execution payload to be included in the next block. - /// - versioned_hashes: The versioned hashes of the blobs in the execution payload. - /// - parent_block_hash: The hash of the parent block. - async fn new_payload( - &self, - execution_payload: &ExecutionPayloadV3, - versioned_hashes: Vec, - parent_block_hash: BlockHash, - ) -> eyre::Result { - let empty_execution_requests: Vec = Vec::new(); - let params = rpc_params![ - execution_payload, - versioned_hashes, - parent_block_hash, - empty_execution_requests - ]; - - self.rpc_request( - ENGINE_NEW_PAYLOAD_V4, - params, - ENGINE_NEW_PAYLOAD_TIMEOUT, - ENGINE_API_RETRY_IPC.build(), - ) - .await - } -} - -#[cfg(test)] -mod tests { - use super::*; - - #[tokio::test] - async fn test_new_with_invalid_socket() { - let result = EngineIPC::new("/nonexistent/socket").await; - assert!(result.is_err()); - } -} diff --git a/crates/eth-engine/src/ipc/ethereum_ipc.rs b/crates/eth-engine/src/ipc/ethereum_ipc.rs deleted file mode 100644 index 4376d1c..0000000 --- a/crates/eth-engine/src/ipc/ethereum_ipc.rs +++ /dev/null @@ -1,847 +0,0 @@ -// Copyright 2025 Circle Internet Group, Inc. All rights reserved. -// -// SPDX-License-Identifier: Apache-2.0 -// -// Licensed under the Apache License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. -// You may obtain a copy of the License at -// -// http://www.apache.org/licenses/LICENSE-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// See the License for the specific language governing permissions and -// limitations under the License. - -use std::time::Duration; - -use alloy_consensus::TxEnvelope; -use alloy_rpc_types::{Block, BlockNumberOrTag, TransactionRequest}; -use alloy_rpc_types_engine::ExecutionPayloadV3; -use alloy_rpc_types_txpool::{TxpoolInspect, TxpoolStatus}; -use alloy_sol_types::SolCall; -use async_trait::async_trait; -use backon::BackoffBuilder; -use eyre::Context; -use jsonrpsee::core::traits::ToRpcParams; -use jsonrpsee::rpc_params; -use serde::de::DeserializeOwned; -use tracing::{debug, trace}; - -use arc_consensus_types::{ConsensusParams, ValidatorSet}; - -use crate::abi_utils::{ - abi_decode_consensus_params, abi_decode_validator_set, consensusParamsCall, - getActiveValidatorSetCall, -}; -use crate::constants::{ - ETH_BATCH_REQUEST_TIMEOUT, ETH_CALL_RETRY, ETH_DEFAULT_TIMEOUT, IPC_CLIENT_TIMEOUT, - PROTOCOL_CONFIG_ADDRESS, VALIDATOR_REGISTRY_ADDRESS, -}; -use crate::engine::EthereumAPI; -use crate::ipc::ipc_builder::Ipc; -use crate::json_structures::*; -use crate::retry::NoRetry; - -/// Generate ABI parameters for IPC calls to get active validator set -fn abi_get_active_validator_set_params_ipc( - block_height: u64, -) -> eyre::Result<(TransactionRequest, BlockNumberOrTag)> { - // Encode the 4‑byte selector + (no args) - let calldata = getActiveValidatorSetCall {}.abi_encode(); - - let tx_request = TransactionRequest { - to: Some(alloy_primitives::TxKind::Call(VALIDATOR_REGISTRY_ADDRESS)), - input: alloy_primitives::Bytes::from(calldata).into(), - ..Default::default() - }; - - let block_number = BlockNumberOrTag::Number(block_height); - - Ok((tx_request, block_number)) -} - -/// Generate ABI parameters for IPC calls to get consensus params -fn abi_get_consensus_params_params_ipc( - block_height: u64, -) -> eyre::Result<(TransactionRequest, BlockNumberOrTag)> { - // Encode the 4‑byte selector + (no args) - let calldata = consensusParamsCall {}.abi_encode(); - - let tx_request = TransactionRequest { - to: Some(alloy_primitives::TxKind::Call(PROTOCOL_CONFIG_ADDRESS)), - input: alloy_primitives::Bytes::from(calldata).into(), - ..Default::default() - }; - - let block_number = BlockNumberOrTag::Number(block_height); - - Ok((tx_request, block_number)) -} - -/// IPC client for Ethereum server. -pub struct EthereumIPC { - ipc: Ipc, -} - -impl std::fmt::Display for EthereumIPC { - fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { - write!(f, "EthereumIPC:{}", self.ipc.socket_path()) - } -} - -impl EthereumIPC { - /// Create a new `EthereumIPC` struct given the IPC socket path. - pub async fn new(socket_path: &str) -> eyre::Result { - Self::new_with_timeout(socket_path, IPC_CLIENT_TIMEOUT).await - } - - /// Create a new `EthereumIPC` struct with custom timeout. - pub async fn new_with_timeout(socket_path: &str, timeout: Duration) -> eyre::Result { - let ipc = Ipc::new_with_timeout(socket_path, timeout).await?; - Ok(Self { ipc }) - } - - /// Returns a future that resolves when the IPC connection closes. - pub fn on_disconnect(&self) -> impl std::future::Future + 'static { - let client = self.ipc.client_arc(); - async move { - let _ = client.on_disconnect().await; - } - } - - /// Send an RPC request to the Ethereum RPC endpoint via IPC. - pub async fn rpc_request( - &self, - method: &str, - params: impl ToRpcParams + Clone + Send, - timeout: Duration, - ) -> eyre::Result { - self.ipc.rpc_request(method, params, timeout, NoRetry).await - } - - /// Get the eth1 chain id of the given endpoint. - pub async fn get_chain_id(&self) -> eyre::Result { - self.ipc - .build_rpc_request("eth_chainId", rpc_params!()) - .timeout(ETH_DEFAULT_TIMEOUT) - .retry(ETH_CALL_RETRY.build()) - .send() - .await - } - - /// Get the genesis block. - pub async fn get_genesis_block(&self) -> eyre::Result { - let block: Option = self - .rpc_request( - "eth_getBlockByNumber", - rpc_params!("0x0", false), - ETH_DEFAULT_TIMEOUT, - ) - .await?; - - block.ok_or_else(|| eyre::eyre!("Genesis block not found")) - } - - #[tracing::instrument(skip(self))] - pub async fn get_active_validator_set(&self, block_height: u64) -> eyre::Result { - let params = abi_get_active_validator_set_params_ipc(block_height)?; - debug!("eth_call params: {params:?}",); - - // Extract the transaction object and block number from the params tuple - let (tx_request, block_number) = params; - - let result: String = self - .ipc - .build_rpc_request("eth_call", rpc_params!(tx_request, block_number)) - .timeout(ETH_DEFAULT_TIMEOUT) - .retry(ETH_CALL_RETRY.build()) - .send() - .await?; - - trace!("eth_call result: {result}"); - let result = hex::decode(result.trim_start_matches("0x"))?; - - abi_decode_validator_set(result) - } - - #[tracing::instrument(skip(self))] - pub async fn get_consensus_params(&self, block_height: u64) -> eyre::Result { - let params = abi_get_consensus_params_params_ipc(block_height)?; - debug!("eth_call params: {params:?}",); - - // Extract the transaction object and block number from the params tuple - let (tx_request, block_number) = params; - - let result: String = self - .ipc - .build_rpc_request("eth_call", rpc_params!(tx_request, block_number)) - .timeout(ETH_DEFAULT_TIMEOUT) - .retry(ETH_CALL_RETRY.build()) - .send() - .await?; - - trace!("eth_call result: {result}"); - let result = hex::decode(result.trim_start_matches("0x"))?; - - abi_decode_consensus_params(result) - } - - /// Get a block by its number. - /// - block_number: The number of the block to get. - pub async fn get_block_by_number( - &self, - block_number: &str, - ) -> eyre::Result> { - let return_full_transaction_objects = false; - self.rpc_request( - "eth_getBlockByNumber", - rpc_params!(block_number, return_full_transaction_objects), - ETH_DEFAULT_TIMEOUT, - ) - .await - } - - /// Get a batch of full execution payloads. - async fn get_execution_payloads( - &self, - block_numbers: &[String], - ) -> eyre::Result>> { - if block_numbers.is_empty() { - return Ok(vec![]); - } - debug!("EthereumIPC: get_execution_payloads for block_numbers={block_numbers:?}"); - - let return_full_transaction_objects = true; - let params_list = block_numbers - .iter() - .map(|block_number| rpc_params![block_number, return_full_transaction_objects]) - .collect::>(); - let batch_blocks: Vec>> = self - .ipc - .batch_request( - "eth_getBlockByNumber", - ¶ms_list, - ETH_BATCH_REQUEST_TIMEOUT, - ) - .await - .wrap_err("Failed to send IPC batch request")?; - - let mut results = Vec::with_capacity(block_numbers.len()); - for (idx, block) in batch_blocks.into_iter().enumerate() { - match block { - Some(Some(b)) => { - let block_hash = b.header.hash; - let consensus_block = b.into_consensus().convert_transactions::(); - let execution_payload = - ExecutionPayloadV3::from_block_unchecked(block_hash, &consensus_block); - results.push(Some(execution_payload)); - } - Some(None) => { - debug!(idx=%idx, block_number=%block_numbers[idx], "No block found for request"); - results.push(None); - } - None => { - debug!(idx=%idx, block_number=%block_numbers[idx], "Request failed for block"); - results.push(None); - } - } - } - - Ok(results) - } - - /// Get the status of the transaction pool. - pub async fn txpool_status(&self) -> eyre::Result { - self.rpc_request("txpool_status", rpc_params!(), ETH_DEFAULT_TIMEOUT) - .await - } - - /// Get the contents of the transaction pool. - pub async fn txpool_inspect(&self) -> eyre::Result { - self.rpc_request("txpool_inspect", rpc_params!(), ETH_DEFAULT_TIMEOUT) - .await - } -} - -#[async_trait] -impl EthereumAPI for EthereumIPC { - /// Get the eth1 chain id of the given endpoint. - async fn get_chain_id(&self) -> eyre::Result { - self.get_chain_id() - .await - .wrap_err("EthereumIPC get_chain_id call failed") - } - - /// Get the genesis block. - async fn get_genesis_block(&self) -> eyre::Result { - self.get_genesis_block() - .await - .wrap_err("EthereumIPC get_genesis_block call failed") - } - - /// Get the active validator set at a specific block height. - async fn get_active_validator_set(&self, block_height: u64) -> eyre::Result { - self.get_active_validator_set(block_height) - .await - .wrap_err_with(|| { - format!( - "EthereumIPC get_active_validator_set call failed for height={block_height}" - ) - }) - } - - /// Get the consensus parameters at a specific block height. - async fn get_consensus_params(&self, block_height: u64) -> eyre::Result { - self.get_consensus_params(block_height).await - } - - /// Get a block by its number. - async fn get_block_by_number( - &self, - block_number: &str, - ) -> eyre::Result> { - self.get_block_by_number(block_number) - .await - .wrap_err_with(|| { - format!( - "EthereumIPC get_block_by_number call failed for block number={block_number}" - ) - }) - } - - /// Get a batch of full execution payloads. - async fn get_execution_payloads( - &self, - block_numbers: &[String], - ) -> eyre::Result>> { - self.get_execution_payloads(block_numbers) - .await - .wrap_err_with(|| { - format!("EthereumIPC get_execution_payloads call failed for block_numbers={block_numbers:?}") - }) - } - - /// Get the status of the transaction pool. - async fn txpool_status(&self) -> eyre::Result { - self.txpool_status().await - } - - /// Get the contents of the transaction pool. - async fn txpool_inspect(&self) -> eyre::Result { - self.txpool_inspect().await - } -} - -#[cfg(test)] -mod tests { - use super::*; - use alloy_consensus::Header as ConsensusHeader; - use alloy_primitives::B256; - use alloy_rpc_types_eth::{Block, Header}; - use std::{collections::HashMap, sync::Arc}; - use tempfile; - use tokio::io::{AsyncReadExt, AsyncWriteExt}; - use tokio::net::{UnixListener, UnixStream}; - use tokio::task::JoinHandle; - use tokio::time::{sleep, Duration}; - - #[test] - fn test_encode_get_valset_params_abi_ipc() { - let block_number = 4567; - let (tx_request, block_tag) = - abi_get_active_validator_set_params_ipc(block_number).unwrap(); - - // Check the transaction request has the expected address - if let Some(alloy_primitives::TxKind::Call(address)) = tx_request.to { - assert_eq!(address, VALIDATOR_REGISTRY_ADDRESS); - } else { - panic!("Expected Call transaction type"); - } - - // Check the block number - assert_eq!(block_tag, BlockNumberOrTag::Number(block_number)); - } - - #[test] - fn test_encode_get_consensus_params_abi_ipc() { - let block_number = 4567; - let (tx_request, block_tag) = abi_get_consensus_params_params_ipc(block_number).unwrap(); - - // Check the transaction request has the expected address - if let Some(alloy_primitives::TxKind::Call(address)) = tx_request.to { - assert_eq!(address, PROTOCOL_CONFIG_ADDRESS); - } else { - panic!("Expected Call transaction type"); - } - - // Check the block number - assert_eq!(block_tag, BlockNumberOrTag::Number(block_number)); - } - - fn create_test_block( - number: u64, - hash: &str, - parent_hash: &str, - timestamp: u64, - gas_used: u64, - ) -> Block { - let inner = ConsensusHeader { - number, - timestamp, - parent_hash: parent_hash.parse::().unwrap(), - gas_used, - ..Default::default() - }; - let header = Header { - hash: hash.parse::().unwrap(), - inner, - ..Default::default() - }; - Block { - header, - ..Default::default() - } - } - - async fn start_mock_ipc_server( - responses: HashMap>, - ) -> eyre::Result<(String, MockServerHandle)> { - let temp_dir = tempfile::tempdir()?; - let socket_path = temp_dir.path().join("mock_ipc.sock"); - let socket_path = socket_path.to_string_lossy().to_string(); - - let _ = std::fs::remove_file(&socket_path); // Just in case - - let listener = UnixListener::bind(&socket_path)?; - let responses = Arc::new(responses); - let server_jh = tokio::spawn(async move { - loop { - match listener.accept().await { - Ok((stream, _)) => { - let responses = responses.clone(); - tokio::spawn(async move { handle_ipc_connection(stream, responses).await }); - } - Err(e) => { - eprintln!("Failed to accept connection: {}", e); - break; - } - } - } - }); - - let handle = MockServerHandle { - _temp_dir_guard: temp_dir, - _server_jh: server_jh, - }; - - // Give the server time to breathe - sleep(Duration::from_millis(50)).await; - - Ok((socket_path, handle)) - } - - struct MockServerHandle { - _temp_dir_guard: tempfile::TempDir, - _server_jh: JoinHandle<()>, - } - - async fn handle_ipc_connection( - stream: UnixStream, - responses: Arc>>, - ) { - // Big buffer not to complicate this test infra with framing - let mut buffer = vec![0; 64 * 1024]; - let (mut reader, mut writer) = stream.into_split(); - - while let Ok(n) = reader.read(&mut buffer).await { - if n == 0 { - break; - } - - let request_str = String::from_utf8_lossy(&buffer[..n]); - let Ok(value) = serde_json::from_str::(&request_str) else { - continue; - }; - - let response = if value.is_array() { - // Handle batch request - let batch_request = value.as_array().unwrap(); - let mut batch_response = Vec::new(); - - for request in batch_request { - let response = handle_single_request(request, &responses); - batch_response.push(response); - } - // Simulate out-of-order responses - batch_response.reverse(); - - serde_json::Value::Array(batch_response) - } else { - // Handle single request - handle_single_request(&value, &responses) - }; - - let response_str = serde_json::to_string(&response).unwrap() + "\n"; - - writer.write_all(response_str.as_bytes()).await.unwrap(); - writer.flush().await.unwrap(); - } - } - - fn handle_single_request( - request: &serde_json::Value, - responses: &HashMap>, - ) -> serde_json::Value { - let Some(method) = request.get("method").and_then(|m| m.as_str()) else { - return serde_json::json!({ - "jsonrpc": "2.0", - "id": null, - "error": {"code": -32600, "message": "Invalid Request"} - }); - }; - if method != "eth_getBlockByNumber" { - return serde_json::json!({ - "jsonrpc": "2.0", - "id": request.get("id").unwrap_or(&serde_json::Value::Null), - "error": {"code": -32601, "message": "Method not found"} - }); - }; - let Some(params) = request.get("params").and_then(|p| p.as_array()) else { - return serde_json::json!({ - "jsonrpc": "2.0", - "id": request.get("id").unwrap_or(&serde_json::Value::Null), - "error": {"code": -32602, "message": "Invalid params"} - }); - }; - if params.is_empty() { - return serde_json::json!({ - "jsonrpc": "2.0", - "id": request.get("id").unwrap_or(&serde_json::Value::Null), - "error": {"code": -32602, "message": "Invalid params"} - }); - }; - let Some(block_number) = params[0].as_str() else { - return serde_json::json!({ - "jsonrpc": "2.0", - "id": request.get("id").unwrap_or(&serde_json::Value::Null), - "error": {"code": -32602, "message": "Invalid params"} - }); - }; - let Some(result) = responses.get(block_number) else { - // Block number not found in our test data - return null - return serde_json::json!({ - "jsonrpc": "2.0", - "id": request.get("id").unwrap_or(&serde_json::Value::Null), - "result": null - }); - }; - let result = result.to_owned(); - serde_json::json!({ - "jsonrpc": "2.0", - "id": request.get("id").unwrap_or(&serde_json::Value::Null), - "result": result - }) - } - - #[tokio::test] - async fn test_ethereum_ipc_get_genesis_block_success() { - let genesis_hash = "0xaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa"; - let genesis_block = create_test_block( - 0, - genesis_hash, - "0x0000000000000000000000000000000000000000000000000000000000000000", - 0, - 0, - ); - - let mut responses = HashMap::new(); - responses.insert("0x0".to_string(), Some(genesis_block)); - - let (socket_path, _handle) = start_mock_ipc_server(responses).await.unwrap(); - let ethereum_ipc = EthereumIPC::new(&socket_path).await.unwrap(); - - let result = ethereum_ipc.get_genesis_block().await.unwrap(); - assert_eq!(result.block_hash, genesis_hash.parse::().unwrap()); - assert_eq!(result.timestamp, 0); - } - - #[tokio::test] - async fn test_ethereum_ipc_get_genesis_block_not_found() { - let responses = HashMap::new(); // No blocks available - - let (socket_path, _handle) = start_mock_ipc_server(responses).await.unwrap(); - let ethereum_ipc = EthereumIPC::new(&socket_path).await.unwrap(); - - let err = ethereum_ipc.get_genesis_block().await.unwrap_err(); - assert!(err.to_string().contains("Genesis block not found")); - } - - #[tokio::test] - async fn test_ethereum_ipc_batch_payloads_success() { - let block1 = create_test_block( - 1, - "0x1234567890123456789012345678901234567890123456789012345678901234", - "0x0000000000000000000000000000000000000000000000000000000000000000", - 0x64, - 0, - ); - let block2 = create_test_block( - 2, - "0x2345678901234567890123456789012345678901234567890123456789012345", - "0x1234567890123456789012345678901234567890123456789012345678901234", - 0xc8, - 26000, - ); - - let mut responses = HashMap::new(); - responses.insert("0x1".to_string(), Some(block1)); - responses.insert("0x2".to_string(), Some(block2)); - - let (socket_path, _handle) = start_mock_ipc_server(responses).await.unwrap(); - let ethereum_ipc = EthereumIPC::new(&socket_path).await.unwrap(); - - let block_numbers = vec!["0x1".to_string(), "0x2".to_string()]; - let result = ethereum_ipc - .get_execution_payloads(&block_numbers) - .await - .unwrap(); - - assert_eq!(result.len(), 2); - assert!(result[0].is_some()); - assert!(result[1].is_some()); - - let payload1 = result[0].as_ref().unwrap(); - let payload2 = result[1].as_ref().unwrap(); - - assert_eq!(payload1.payload_inner.payload_inner.block_number, 1); - assert_eq!(payload1.payload_inner.payload_inner.gas_used, 0); - assert_eq!(payload2.payload_inner.payload_inner.block_number, 2); - assert_eq!(payload2.payload_inner.payload_inner.gas_used, 26000); - } - - #[tokio::test] - async fn test_ethereum_ipc_batch_payloads_partial_failure() { - let block1 = create_test_block( - 1, - "0x1234567890123456789012345678901234567890123456789012345678901234", - "0x0000000000000000000000000000000000000000000000000000000000000000", - 0x64, - 33344, - ); - - let mut responses = HashMap::new(); - responses.insert("0x1".to_string(), Some(block1)); - responses.insert("0x999".to_string(), None); // Missing block - - let (socket_path, _handle) = start_mock_ipc_server(responses).await.unwrap(); - let ethereum_ipc = EthereumIPC::new(&socket_path).await.unwrap(); - - let block_numbers = vec!["0x1".to_string(), "0x999".to_string()]; - let result = ethereum_ipc - .get_execution_payloads(&block_numbers) - .await - .unwrap(); - - assert_eq!(result.len(), 2); - assert!(result[0].is_some()); - assert!(result[1].is_none()); - - let payload = result[0].as_ref().unwrap(); - assert_eq!(payload.payload_inner.payload_inner.block_number, 1); - assert_eq!(payload.payload_inner.payload_inner.gas_used, 33344); - } - - #[tokio::test] - async fn test_ethereum_ipc_batch_payloads_empty_request() { - let responses = HashMap::new(); - let (socket_path, _handle) = start_mock_ipc_server(responses).await.unwrap(); - let ethereum_ipc = EthereumIPC::new(&socket_path).await.unwrap(); - - let result = ethereum_ipc.get_execution_payloads(&[]).await.unwrap(); - assert_eq!(result.len(), 0); - } - - #[tokio::test] - async fn test_ethereum_ipc_batch_payloads_all_missing() { - let responses = HashMap::new(); // No blocks available - - let (socket_path, _handle) = start_mock_ipc_server(responses).await.unwrap(); - let ethereum_ipc = EthereumIPC::new(&socket_path).await.unwrap(); - - let block_numbers = vec!["0x1".to_string(), "0x2".to_string()]; - let result = ethereum_ipc - .get_execution_payloads(&block_numbers) - .await - .unwrap(); - - assert_eq!(result.len(), 2); - assert!(result[0].is_none()); - assert!(result[1].is_none()); - } - - #[tokio::test] - async fn test_ethereum_ipc_batch_payloads_connection_error() { - let temp_dir = tempfile::tempdir().unwrap(); - let socket_path = temp_dir.path().join("non_existent_socket.sock"); - let socket_path = socket_path.to_string_lossy().to_string(); - - let result = EthereumIPC::new(&socket_path).await; - assert!(result.is_err()); - } - - #[tokio::test] - async fn test_ethereum_ipc_batch_payloads_single_block() { - let block1 = create_test_block( - 1, - "0x1234567890123456789012345678901234567890123456789012345678901234", - "0x0000000000000000000000000000000000000000000000000000000000000000", - 0x64, - 12345, - ); - - let mut responses = HashMap::new(); - responses.insert("0x1".to_string(), Some(block1)); - - let (socket_path, _handle) = start_mock_ipc_server(responses).await.unwrap(); - let ethereum_ipc = EthereumIPC::new(&socket_path).await.unwrap(); - - let block_numbers = vec!["0x1".to_string()]; - let result = ethereum_ipc - .get_execution_payloads(&block_numbers) - .await - .unwrap(); - - assert_eq!(result.len(), 1); - assert!(result[0].is_some()); - - let payload = result[0].as_ref().unwrap(); - assert_eq!(payload.payload_inner.payload_inner.block_number, 1); - assert_eq!(payload.payload_inner.payload_inner.gas_used, 12345); - } - - #[tokio::test] - async fn test_ethereum_ipc_batch_payloads_response_length_validation() { - let block1 = create_test_block( - 1, - "0x1234567890123456789012345678901234567890123456789012345678901234", - "0x0000000000000000000000000000000000000000000000000000000000000000", - 0x64, - 12345, - ); - let mut responses = HashMap::new(); - responses.insert("0x1".to_string(), Some(block1)); - - let (socket_path, _handle) = start_mock_ipc_server(responses).await.unwrap(); - let ethereum_ipc = EthereumIPC::new(&socket_path).await.unwrap(); - - let block_numbers = vec!["0x1".to_string()]; - let result = ethereum_ipc - .get_execution_payloads(&block_numbers) - .await - .unwrap(); - - assert_eq!(result.len(), 1); - assert!(result[0].is_some()); - } - - #[tokio::test] - async fn test_ethereum_ipc_batch_payloads_invalid_block_data() { - let mut responses = HashMap::new(); - // None simulates invalid/malformed block data - responses.insert("0x1".to_string(), None); - - let (socket_path, _handle) = start_mock_ipc_server(responses).await.unwrap(); - let ethereum_ipc = EthereumIPC::new(&socket_path).await.unwrap(); - - let block_numbers = vec!["0x1".to_string()]; - let result = ethereum_ipc - .get_execution_payloads(&block_numbers) - .await - .unwrap(); - - assert_eq!(result.len(), 1); - assert!(result[0].is_none()); - } - - #[tokio::test] - async fn test_ethereum_ipc_batch_payloads_mixed_success_and_errors() { - let mut responses = HashMap::new(); - - // Block 0x1: Success - let block1 = create_test_block( - 1, - "0x1111111111111111111111111111111111111111111111111111111111111111", - "0x0000000000000000000000000000000000000000000000000000000000000000", - 0x64, - 12345, - ); - responses.insert("0x1".to_string(), Some(block1)); - - // Block 0x2: No response (simulates JSON-RPC error) - - // Block 0x3: Success - let block3 = create_test_block( - 3, - "0x3333333333333333333333333333333333333333333333333333333333333333", - "0x2222222222222222222222222222222222222222222222222222222222222222", - 0xc8, - 26000, - ); - responses.insert("0x3".to_string(), Some(block3)); - - // Block 0x4: Block not found - responses.insert("0x4".to_string(), None); - - let (socket_path, _handle) = start_mock_ipc_server(responses).await.unwrap(); - let ethereum_ipc = EthereumIPC::new(&socket_path).await.unwrap(); - - let block_numbers = vec![ - "0x1".to_string(), - "0x2".to_string(), - "0x3".to_string(), - "0x4".to_string(), - ]; - let result = ethereum_ipc.get_execution_payloads(&block_numbers).await; - - assert!( - result.is_ok(), - "The batch should succeed even though some requests failed" - ); - let payloads = result.unwrap(); - assert_eq!(payloads.len(), 4); - - assert!(payloads[0].is_some(), "Block 1 should succeed"); - assert_eq!( - payloads[0] - .as_ref() - .unwrap() - .payload_inner - .payload_inner - .block_number, - 1 - ); - - assert!( - payloads[1].is_none(), - "Block 2 should be None (request failed/no response)" - ); - - assert!(payloads[2].is_some(), "Block 3 should succeed"); - assert_eq!( - payloads[2] - .as_ref() - .unwrap() - .payload_inner - .payload_inner - .block_number, - 3 - ); - - assert!( - payloads[3].is_none(), - "Block 4 should be None (block not found)" - ); - } -} diff --git a/crates/eth-engine/src/ipc/ipc_builder.rs b/crates/eth-engine/src/ipc/ipc_builder.rs deleted file mode 100644 index d52acc8..0000000 --- a/crates/eth-engine/src/ipc/ipc_builder.rs +++ /dev/null @@ -1,251 +0,0 @@ -// Copyright 2025 Circle Internet Group, Inc. All rights reserved. -// -// SPDX-License-Identifier: Apache-2.0 -// -// Licensed under the Apache License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. -// You may obtain a copy of the License at -// -// http://www.apache.org/licenses/LICENSE-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// See the License for the specific language governing permissions and -// limitations under the License. - -use std::sync::Arc; -use std::time::Duration; - -use backon::{Backoff, Retryable}; -use eyre::{eyre, Context}; -use jsonrpsee::{ - async_client::Client, - core::{ - client::{ClientT, Error as RpcClientError}, - params::BatchRequestBuilder, - traits::ToRpcParams, - }, -}; -use reth_ipc::client::IpcClientBuilder; -use serde::de::DeserializeOwned; -use tracing::{debug, info, warn}; - -use crate::constants::IPC_CLIENT_TIMEOUT; -use crate::rpc::EngineApiRpcError; - -/// Common IPC client functionality for connecting to IPC endpoints via Unix Domain Socket. -#[derive(Debug)] -pub struct Ipc { - client: Arc, - socket_path: String, -} - -impl std::fmt::Display for Ipc { - fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { - write!(f, "IPC:{}", self.socket_path) - } -} - -impl Ipc { - /// Create a new `IPC` struct with custom timeout. Defaults to 60 seconds. - pub async fn new_with_timeout(socket_path: &str, timeout: Duration) -> eyre::Result { - let client = IpcClientBuilder::default() - .request_timeout(timeout) - .build(socket_path) - .await?; - - info!("🟢 Connected to IPC: {}", socket_path); - - Ok(Self { - client: Arc::new(client), - socket_path: socket_path.to_string(), - }) - } - - /// Build an RPC request to the IPC endpoint. - pub fn build_rpc_request<'a, 'b, P>( - &'a self, - method: &'b str, - params: P, - ) -> RpcRequestBuilder<'a, P> - where - 'b: 'a, - P: ToRpcParams + Clone + Send, - { - RpcRequestBuilder::new(&self.client, method, params) - } - - /// Send an RPC request to the IPC endpoint. - pub async fn rpc_request( - &self, - method: &str, - params: impl ToRpcParams + Clone + Send, - timeout: Duration, - retry_policy: impl Backoff, - ) -> eyre::Result { - self.build_rpc_request(method, params) - .timeout(timeout) - .retry(retry_policy) - .send() - .await - } - - /// Send a batch of RPC requests. - pub async fn batch_request( - &self, - method: &str, - params_list: &[impl ToRpcParams + Clone + Send], - timeout: Duration, - ) -> eyre::Result>> { - if params_list.is_empty() { - return Ok(vec![]); - } - - let mut batch_builder = BatchRequestBuilder::new(); - for params in params_list { - batch_builder - .insert(method, params.clone()) - .map_err(|e| eyre!("Failed to insert request into batch: {e}"))?; - } - - let batch_response = - tokio::time::timeout(timeout, self.client.batch_request(batch_builder)) - .await - .map_err(|_| eyre!("IPC batch request timed out after {timeout:?}"))? - .wrap_err("IPC batch request failed")?; - - let mut results = Vec::with_capacity(params_list.len()); - for (idx, batch_entry) in batch_response.into_iter().enumerate() { - match batch_entry { - Ok(response) => { - results.push(Some(response)); - } - Err(error) => { - debug!(idx=%idx, error=?error, "IPC batch request failed for entry"); - results.push(None); - } - } - } - - // Ensure we have the expected number of results - if results.len() != params_list.len() { - return Err(eyre!( - "Batch response length mismatch: expected {}, got {}", - params_list.len(), - results.len() - )); - } - - Ok(results) - } - - /// Get the socket path this client is connected to - pub fn socket_path(&self) -> &str { - &self.socket_path - } - - /// Returns a cloned `Arc` reference to the underlying client. - /// - /// Callers can use this to monitor connection lifetime independently of the - /// `Ipc` owner (e.g. `arc_client.on_disconnect().await`). - pub fn client_arc(&self) -> Arc { - self.client.clone() - } -} - -use crate::retry::NoRetry; - -/// A builder for creating and sending an IPC request. -pub struct RpcRequestBuilder<'a, P, B: Backoff = NoRetry> { - client: &'a Client, - method: &'a str, - params: P, - retry_policy: B, - timeout: Duration, -} - -impl<'a, P> RpcRequestBuilder<'a, P> { - /// Creates a new builder instance. - pub fn new(client: &'a Client, method: &'a str, params: P) -> Self { - Self { - client, - method, - params, - retry_policy: NoRetry, - timeout: IPC_CLIENT_TIMEOUT, - } - } -} - -impl<'a, P, B: Backoff> RpcRequestBuilder<'a, P, B> { - /// Sets the per-call timeout for the request. - pub fn timeout(mut self, timeout: Duration) -> Self { - self.timeout = timeout; - self - } - - /// Sets the retry policy for the request. - /// This consumes the current builder and returns a new one with the specified policy. - pub fn retry(self, policy: NB) -> RpcRequestBuilder<'a, P, NB> { - RpcRequestBuilder { - client: self.client, - method: self.method, - params: self.params, - retry_policy: policy, - timeout: self.timeout, - } - } - - /// Builds and sends the JSON-RPC request. - pub async fn send(self) -> eyre::Result - where - D: DeserializeOwned, - P: ToRpcParams + Clone + Send, - { - let timeout = self.timeout; - let method = self.method; - - // Closure that sends the request and processes the response. - // This will be retried according to the retry policy. - let send_once = || async { - tokio::time::timeout( - timeout, - self.client - .request::(self.method, self.params.clone()), - ) - .await - .map_err(|_| eyre!("IPC request {method} timed out after {timeout:?}"))? - .map_err(|e| match e { - RpcClientError::Call(err) => { - let engine_rpc_error = EngineApiRpcError::from(err); - eyre::Report::new(engine_rpc_error) - .wrap_err(format!("IPC request {} failed", self.method)) - } - other => { - eyre::Report::new(other).wrap_err(format!("IPC request {} failed", self.method)) - } - }) - }; - - // Use `backon::Retryable` to execute the closure with the given retry policy. - // If the policy is `NoRetry`, it runs exactly once. - send_once - .retry(self.retry_policy) - .notify(|e, dur| { - warn!("IPC request failed: {e}, retrying in {dur:?}"); - }) - .await - } -} - -#[cfg(test)] -mod tests { - use super::*; - - #[tokio::test] - async fn test_new_with_invalid_socket() { - let result = Ipc::new_with_timeout("/nonexistent/socket", Duration::from_secs(60)).await; - assert!(result.is_err()); - } -} diff --git a/crates/eth-engine/src/ipc/mod.rs b/crates/eth-engine/src/ipc/mod.rs deleted file mode 100644 index 845cb1b..0000000 --- a/crates/eth-engine/src/ipc/mod.rs +++ /dev/null @@ -1,20 +0,0 @@ -// Copyright 2025 Circle Internet Group, Inc. All rights reserved. -// -// SPDX-License-Identifier: Apache-2.0 -// -// Licensed under the Apache License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. -// You may obtain a copy of the License at -// -// http://www.apache.org/licenses/LICENSE-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// See the License for the specific language governing permissions and -// limitations under the License. - -pub mod engine_ipc; -pub mod ethereum_ipc; - -mod ipc_builder; diff --git a/crates/eth-engine/src/json_structures.rs b/crates/eth-engine/src/json_structures.rs deleted file mode 100644 index c107407..0000000 --- a/crates/eth-engine/src/json_structures.rs +++ /dev/null @@ -1,32 +0,0 @@ -// Copyright 2025 Circle Internet Group, Inc. All rights reserved. -// -// SPDX-License-Identifier: Apache-2.0 -// -// Licensed under the Apache License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. -// You may obtain a copy of the License at -// -// http://www.apache.org/licenses/LICENSE-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// See the License for the specific language governing permissions and -// limitations under the License. - -use alloy_primitives::BlockTimestamp; -use arc_consensus_types::{BlockHash, BlockNumber}; -use serde::{Deserialize, Serialize}; - -/// JSON execution block. -#[derive(Clone, Copy, Debug, PartialEq, Serialize, Deserialize)] -#[serde(rename_all = "camelCase")] -pub struct ExecutionBlock { - #[serde(rename = "hash")] - pub block_hash: BlockHash, - #[serde(rename = "number", with = "serde_utils::u64_hex_be")] - pub block_number: BlockNumber, - pub parent_hash: BlockHash, - #[serde(with = "serde_utils::u64_hex_be")] - pub timestamp: BlockTimestamp, -} diff --git a/crates/eth-engine/src/lib.rs b/crates/eth-engine/src/lib.rs deleted file mode 100644 index 9734b2b..0000000 --- a/crates/eth-engine/src/lib.rs +++ /dev/null @@ -1,34 +0,0 @@ -// Copyright 2025 Circle Internet Group, Inc. All rights reserved. -// -// SPDX-License-Identifier: Apache-2.0 -// -// Licensed under the Apache License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. -// You may obtain a copy of the License at -// -// http://www.apache.org/licenses/LICENSE-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// See the License for the specific language governing permissions and -// limitations under the License. - -pub mod capabilities; -pub mod engine; -pub mod ipc; -pub mod json_structures; -pub mod persistence_meter; -pub mod retry; -pub mod rpc; - -mod abi_utils; - -mod constants; -pub use constants::INITIAL_RETRY_DELAY; - -#[cfg(any(test, feature = "mocks"))] -pub mod mocks { - pub use crate::engine::{MockEngineAPI, MockEthereumAPI}; - pub use crate::persistence_meter::MockPersistenceMeter; -} diff --git a/crates/eth-engine/src/persistence_meter.rs b/crates/eth-engine/src/persistence_meter.rs deleted file mode 100644 index eb93cf3..0000000 --- a/crates/eth-engine/src/persistence_meter.rs +++ /dev/null @@ -1,787 +0,0 @@ -// Copyright 2026 Circle Internet Group, Inc. All rights reserved. -// -// SPDX-License-Identifier: Apache-2.0 -// -// Licensed under the Apache License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. -// You may obtain a copy of the License at -// -// http://www.apache.org/licenses/LICENSE-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// See the License for the specific language governing permissions and -// limitations under the License. - -use std::sync::atomic::{AtomicU64, AtomicU8, Ordering}; -use std::sync::Arc; -use std::time::{Duration, Instant}; - -use alloy_eips::BlockNumHash; -use async_trait::async_trait; -use eyre::{eyre, Context}; -use jsonrpsee::{ - async_client::Client, - core::client::SubscriptionClientT, - rpc_params, - ws_client::{PingConfig, WsClientBuilder}, -}; -use reth_ipc::client::IpcClientBuilder; -use tokio::sync::Notify; -use tracing::{debug, info, warn}; - -use crate::engine::{EthereumAPI, SubscriptionEndpoint}; - -const SUBSCRIBE_METHOD: &str = "reth_subscribePersistedBlock"; -const UNSUBSCRIBE_METHOD: &str = "reth_unsubscribePersistedBlock"; -/// Timeout for the initial WebSocket/IPC connection attempt. -const CONNECT_TIMEOUT: Duration = Duration::from_secs(10); -/// Timeout for individual RPC requests on the subscription connection. -const REQUEST_TIMEOUT: Duration = Duration::from_secs(5); -/// Initial delay between reconnection attempts (doubles on each failure, capped). -const RECONNECT_BACKOFF: Duration = Duration::from_secs(1); -/// WebSocket ping interval for detecting stale connections. -const WS_PING_INTERVAL: Duration = Duration::from_secs(30); -/// Cap on exponential reconnect backoff. -const MAX_RECONNECT_BACKOFF: Duration = Duration::from_secs(60); - -/// Tracks execution layer block persistence and applies backpressure when the -/// EL falls behind. -/// -/// Callers invoke [`wait_for_persisted_block`] after submitting a block to the -/// EL. The call returns immediately if the EL has already persisted within the -/// configured threshold, or blocks until persistence catches up. Returns `Err` -/// on timeout. -#[cfg_attr(any(test, feature = "mocks"), mockall::automock)] -#[async_trait] -pub trait PersistenceMeter: Send + Sync { - /// Block until the canonical-minus-persisted gap for `block_number` falls - /// below the configured threshold. Returns immediately if already satisfied. - /// Returns `Err` on timeout - async fn wait_for_persisted_block( - &self, - block_number: u64, - timeout: Duration, - ) -> eyre::Result<()>; - - /// Set a known persisted block height so early wait calls don't block - /// unnecessarily. Only reliable at startup before blocks accumulate. - fn seed(&self, _block_number: u64) {} -} - -#[async_trait] -impl PersistenceMeter for &T { - async fn wait_for_persisted_block( - &self, - block_number: u64, - timeout: Duration, - ) -> eyre::Result<()> { - (**self) - .wait_for_persisted_block(block_number, timeout) - .await - } - - fn seed(&self, block_number: u64) { - (**self).seed(block_number); - } -} -pub struct NoopPersistenceMeter; - -#[async_trait] -impl PersistenceMeter for NoopPersistenceMeter { - async fn wait_for_persisted_block( - &self, - _block_number: u64, - _timeout: Duration, - ) -> eyre::Result<()> { - Ok(()) - } -} - -/// Statuses that the persistence meter internally transitions between -/// -/// Subscription live and counter reliable. Backpressure enforced. -const SUBSCRIPTION_STATUS_ACTIVE: u8 = 0; -/// No live subscription. Backpressure suspended. -const SUBSCRIPTION_STATUS_RECONNECTING: u8 = 1; -/// Subscription live but no notification received yet. Backpressure suspended. -const SUBSCRIPTION_STATUS_CONNECTED: u8 = 2; - -struct SharedState { - /// Highest block number the EL has confirmed as persisted to disk. - last_persisted_block: AtomicU64, - /// Wakes waiters when `last_persisted_block` advances or connection state changes. - notify: Notify, - /// Subscription lifecycle state. Backpressure is only enforced when - /// [`SUBSCRIPTION_STATUS_ACTIVE`]. - subscription_status: AtomicU8, -} - -/// Meter block ingestion throughput using the execution layer's persisted block subscription. -/// -/// A background task maintains the subscription connection, updates an atomic -/// counter on each notification, and wakes any waiters via [`Notify`]. This -/// allows [`wait_for_persisted_block`] to return immediately when the -/// canonical-minus-persisted gap is already below the configured threshold, -/// and multiple callers can wait concurrently without contending on a lock. -/// -/// In the background, a task will maintain the connection, reconnecting as needed. -/// When reconnecting, the internal [`subscription_status`] will transition to [`SUBSCRIPTION_STATUS_RECONNECTING`] -/// which disables backpressure. Upon the first received notification, it will transition back to [`SUBSCRIPTION_STATUS_ACTIVE`], -/// applying backpressure again. -/// -/// Seeding the meter with an initial height value will transition it to -/// [`SUBSCRIPTION_STATUS_ACTIVE`] only if the subscription is already -/// [`SUBSCRIPTION_STATUS_CONNECTED`]. If reconnecting, the seed updates the -/// counter but backpressure remains suspended until the subscription is live. -pub struct PersistedBlockMeter { - shared: Arc, - /// Backpressure threshold. Backpressure applies when the - /// canonical-minus-persisted gap reaches this value. - persistence_backpressure_threshold: u64, - /// Background subscription reader; aborted on drop. - _background: tokio::task::JoinHandle<()>, -} - -impl PersistedBlockMeter { - async fn new(endpoint: &SubscriptionEndpoint, persistence_backpressure_threshold: u64) -> Self { - let initial_connection = connect_and_subscribe(endpoint).await.ok(); - let status = if initial_connection.is_some() { - info!("Persistence meter: subscription connected"); - SUBSCRIPTION_STATUS_CONNECTED - } else { - warn!("Persistence meter: initial connection failed; retrying in background"); - SUBSCRIPTION_STATUS_RECONNECTING - }; - - let shared = Arc::new(SharedState { - last_persisted_block: AtomicU64::new(0), - notify: Notify::new(), - subscription_status: AtomicU8::new(status), - }); - - let background = tokio::spawn(background_reader( - endpoint.clone(), - Arc::clone(&shared), - initial_connection, - )); - - Self { - shared, - persistence_backpressure_threshold, - _background: background, - } - } -} - -#[cfg(test)] -impl PersistedBlockMeter { - fn test_instance(persistence_backpressure_threshold: u64) -> Self { - let shared = Arc::new(SharedState { - last_persisted_block: AtomicU64::new(0), - notify: Notify::new(), - subscription_status: AtomicU8::new(SUBSCRIPTION_STATUS_ACTIVE), - }); - Self { - shared, - persistence_backpressure_threshold, - _background: tokio::spawn(std::future::pending()), - } - } -} - -impl Drop for PersistedBlockMeter { - fn drop(&mut self) { - self._background.abort(); - } -} - -#[async_trait] -impl PersistenceMeter for PersistedBlockMeter { - fn seed(&self, block_number: u64) { - self.shared - .last_persisted_block - .fetch_max(block_number, Ordering::Release); - - // Only transition to ACTIVE if there's a functioning connection - // This is to prevent the situation where the connection can never be - // established, yet back pressure would still be applied. - let activated = self - .shared - .subscription_status - .compare_exchange( - SUBSCRIPTION_STATUS_CONNECTED, - SUBSCRIPTION_STATUS_ACTIVE, - Ordering::Release, - Ordering::Relaxed, - ) - .is_ok(); - self.shared.notify.notify_waiters(); - if activated { - info!( - persisted_block = block_number, - "Persistence meter: seeded and active" - ); - } else { - info!( - persisted_block = block_number, - "Persistence meter: seeded (backpressure deferred until subscription is live)" - ); - } - } - - async fn wait_for_persisted_block( - &self, - block_number: u64, - timeout: Duration, - ) -> eyre::Result<()> { - let started_at = Instant::now(); - - loop { - // Register for notification *before* checking state to avoid - // missing an update between the checks and the await. - let notified = self.shared.notify.notified(); - - // Only enforce backpressure when ACTIVE. - let status = self.shared.subscription_status.load(Ordering::Acquire); - if status != SUBSCRIPTION_STATUS_ACTIVE { - debug!( - requested_block = block_number, - status, "Persistence backpressure skipped: subscription not active" - ); - return Ok(()); - } - - let current = self.shared.last_persisted_block.load(Ordering::Acquire); - let gap = block_number.saturating_sub(current); - // If we're below the configured threshold, proceed. - if gap < self.persistence_backpressure_threshold { - debug!( - requested_block = block_number, - persisted_block = current, - "Persistence backpressure satisfied: EL is keeping up" - ); - return Ok(()); - } - - // Else, wait until timeout is reached - let remaining = timeout - .checked_sub(started_at.elapsed()) - .unwrap_or(Duration::ZERO); - - if remaining.is_zero() { - return Err(eyre!( - "Persistence backpressure timed out: requested block {block_number}, \ - persisted block {current}" - )); - } - - debug!( - requested_block = block_number, - persisted_block = current, - "Persistence backpressure: waiting for execution layer to catch up" - ); - - if tokio::time::timeout(remaining, notified).await.is_err() { - let current = self.shared.last_persisted_block.load(Ordering::Acquire); - return Err(eyre!( - "Persistence backpressure timed out: requested block {block_number}, \ - persisted block {current}" - )); - } - } - } -} - -/// Background task that reads persisted block notifications from the execution -/// layer and updates the shared atomic counter. Reconnects automatically on error. -/// -/// If `initial_connection` is `None`, the task begins by connecting via `reconnect()`. -async fn background_reader( - endpoint: SubscriptionEndpoint, - shared: Arc, - initial_connection: Option<(Client, jsonrpsee::core::client::Subscription)>, -) { - let (mut client, mut subscription) = match initial_connection { - Some(conn) => conn, - None => reconnect(&endpoint).await, - }; - - loop { - match subscription.next().await { - Some(Ok(block)) => { - // If this is the first notification received since re-establishing - // a connection, transition to ACTIVE - let prev_status = shared - .subscription_status - .swap(SUBSCRIPTION_STATUS_ACTIVE, Ordering::Release); - if prev_status != SUBSCRIPTION_STATUS_ACTIVE { - info!( - persisted_block = block.number, - "Persistence meter: subscription now active" - ); - } - let prev = shared - .last_persisted_block - .fetch_max(block.number, Ordering::Release); - // Only notify if we've advanced, in case they arrive out-of-order - if block.number > prev { - shared.notify.notify_waiters(); - } - debug!( - persisted_block = block.number, - "Persistence meter: received persisted block notification" - ); - } - Some(Err(error)) => { - warn!( - %error, - "Persistence meter: subscription errored; reconnecting" - ); - shared - .subscription_status - .store(SUBSCRIPTION_STATUS_RECONNECTING, Ordering::Release); - shared.notify.notify_waiters(); - drop(subscription); - drop(client); - (client, subscription) = reconnect(&endpoint).await; - } - None => { - warn!("Persistence meter: subscription closed; reconnecting"); - shared - .subscription_status - .store(SUBSCRIPTION_STATUS_RECONNECTING, Ordering::Release); - shared.notify.notify_waiters(); - drop(subscription); - drop(client); - (client, subscription) = reconnect(&endpoint).await; - } - } - } -} - -/// Retry the subscription connection with exponential backoff (1s → 60s cap). -/// -/// Retries forever. This runs in the background task, not in the caller's -/// loop, so it never directly blocks block processing. -async fn reconnect( - endpoint: &SubscriptionEndpoint, -) -> (Client, jsonrpsee::core::client::Subscription) { - let mut backoff = RECONNECT_BACKOFF; - loop { - match connect_and_subscribe(endpoint).await { - Ok((client, subscription)) => { - info!("Persistence meter: reconnected to persisted block subscription"); - return (client, subscription); - } - Err(error) => { - warn!( - %error, - backoff = ?backoff, - "Persistence meter: reconnection failed; retrying" - ); - tokio::time::sleep(backoff).await; - // backoff <= MAX_RECONNECT_BACKOFF (60s); *2 fits in Duration - #[allow(clippy::arithmetic_side_effects)] - { - backoff = (backoff * 2).min(MAX_RECONNECT_BACKOFF); - } - } - } - } -} - -/// Establish a fresh connection and subscribe to persisted block -/// notifications. Used both for the initial connection in -/// [`PersistedBlockMeter::new`] and for reconnection attempts in -/// [`reconnect`]. -async fn connect_and_subscribe( - endpoint: &SubscriptionEndpoint, -) -> eyre::Result<(Client, jsonrpsee::core::client::Subscription)> { - let client = connect_client(endpoint).await?; - let subscription = client - .subscribe(SUBSCRIBE_METHOD, rpc_params![], UNSUBSCRIBE_METHOD) - .await - .wrap_err("Failed to subscribe to persisted block notifications")?; - - Ok((client, subscription)) -} - -/// Open a JSON-RPC client to the execution layer. -/// -/// Dispatches on [`SubscriptionEndpoint`]: IPC connects to a Unix socket, -/// WS connects over WebSocket with 'keep-alive' ping. -async fn connect_client(endpoint: &SubscriptionEndpoint) -> eyre::Result { - match endpoint { - SubscriptionEndpoint::Ipc { socket_path } => IpcClientBuilder::default() - .request_timeout(REQUEST_TIMEOUT) - .build(socket_path) - .await - .wrap_err_with(|| format!("Failed to connect to IPC socket {socket_path}")), - SubscriptionEndpoint::Ws { url } => { - // 30s * 2 = 60s — fits in Duration - #[allow(clippy::arithmetic_side_effects)] - let ws_inactive_limit = WS_PING_INTERVAL * 2; - WsClientBuilder::default() - .request_timeout(REQUEST_TIMEOUT) - .connection_timeout(CONNECT_TIMEOUT) - .enable_ws_ping( - PingConfig::new() - .ping_interval(WS_PING_INTERVAL) - .inactive_limit(ws_inactive_limit), - ) - .build(url) - .await - .wrap_err_with(|| format!("Failed to connect to WebSocket endpoint {url}")) - } - } -} - -/// Create a persistence meter. The initial connection is attempted eagerly; -/// if it fails, the background task retries automatically. -/// -/// The meter starts without a known persisted height. Call -/// [`PersistenceMeter::seed`] after creation if a reliable starting -/// height is available (e.g. at startup via `eth_getBlockByNumber`). -pub async fn create( - endpoint: &SubscriptionEndpoint, - persistence_backpressure_threshold: u64, -) -> Box { - let m = PersistedBlockMeter::new(endpoint, persistence_backpressure_threshold).await; - info!( - backpressure_threshold = persistence_backpressure_threshold, - "Persistence meter: active" - ); - Box::new(m) -} - -/// Create a persistence meter with graceful degradation. Falls back to a no-op -/// meter (no backpressure) when: -/// - `enabled` is false -/// - no subscription endpoint is configured -/// -/// The meter starts without a known persisted height. Call -/// [`PersistenceMeter::seed`] after creation if a reliable starting -/// height is available (e.g. at startup via `eth_getBlockByNumber`). -pub async fn create_with_fallback( - enabled: bool, - endpoint: Option<&SubscriptionEndpoint>, - persistence_backpressure_threshold: u64, -) -> Box { - if !enabled { - info!("Persistence meter: disabled, --execution-persistence-backpressure not set"); - return Box::new(NoopPersistenceMeter); - } - let Some(endpoint) = endpoint else { - info!("Persistence meter: disabled, no subscription endpoint configured"); - return Box::new(NoopPersistenceMeter); - }; - create(endpoint, persistence_backpressure_threshold).await -} - -/// Seed the meter with the EL's current block height. Only reliable at -/// startup before blocks accumulate, since `eth_getBlockByNumber("latest")` -/// may return un-persisted heights during steady-state operation. -pub async fn seed_from_latest_block(meter: &dyn PersistenceMeter, eth: &dyn EthereumAPI) { - match eth.get_block_by_number("latest").await { - Ok(Some(block)) => { - meter.seed(block.block_number); - } - Ok(None) => { - warn!("Persistence meter: seed skipped: Ethereum API returned no latest block"); - } - Err(error) => { - warn!( - %error, - "Persistence meter: seed skipped: failed to fetch latest Ethereum block" - ); - } - } -} - -#[cfg(test)] -mod tests { - use super::*; - use crate::engine::MockEthereumAPI; - use eyre::eyre; - - const TEST_TIMEOUT: Duration = Duration::from_secs(5); - const SHORT_TIMEOUT: Duration = Duration::from_millis(50); - use reqwest::Url; - use std::sync::atomic::Ordering; - - #[tokio::test] - async fn noop_meter_always_succeeds() { - let meter = NoopPersistenceMeter; - assert!(meter - .wait_for_persisted_block(100, TEST_TIMEOUT) - .await - .is_ok()); - assert!(meter - .wait_for_persisted_block(0, TEST_TIMEOUT) - .await - .is_ok()); - assert!(meter - .wait_for_persisted_block(u64::MAX, TEST_TIMEOUT) - .await - .is_ok()); - } - - #[tokio::test] - async fn create_with_fallback_returns_noop_when_disabled() { - let endpoint = SubscriptionEndpoint::Ws { - url: Url::parse("ws://localhost:8546").unwrap(), - }; - let meter = create_with_fallback(false, Some(&endpoint), 100).await; - assert!(meter - .wait_for_persisted_block(1000, TEST_TIMEOUT) - .await - .is_ok()); - } - - #[tokio::test] - async fn create_with_fallback_returns_noop_when_no_endpoint() { - let meter = create_with_fallback(true, None, 100).await; - assert!(meter - .wait_for_persisted_block(1000, TEST_TIMEOUT) - .await - .is_ok()); - } - - #[tokio::test] - async fn create_with_fallback_suspends_backpressure_on_connection_failure() { - let endpoint = SubscriptionEndpoint::Ws { - url: Url::parse("ws://127.0.0.1:1").unwrap(), // closed port → fail-fast ECONNREFUSED - }; - let meter = create_with_fallback(true, Some(&endpoint), 5).await; - assert!(meter - .wait_for_persisted_block(1000, TEST_TIMEOUT) - .await - .is_ok()); - } - - #[tokio::test] - async fn create_retries_on_unreachable_endpoint() { - let endpoint = SubscriptionEndpoint::Ws { - url: Url::parse("ws://127.0.0.1:1").unwrap(), - }; - let meter = create(&endpoint, 5).await; - // Initial connection failed — backpressure suspended (not ACTIVE) - assert!(meter - .wait_for_persisted_block(1000, TEST_TIMEOUT) - .await - .is_ok()); - } - - #[tokio::test] - async fn wait_returns_immediately_when_already_persisted() { - let meter = PersistedBlockMeter::test_instance(5); - meter - .shared - .last_persisted_block - .store(100, Ordering::Release); - - // gap=0 (saturating) < threshold=5 - assert!(meter - .wait_for_persisted_block(10, TEST_TIMEOUT) - .await - .is_ok()); - } - - #[tokio::test] - async fn wait_returns_immediately_when_threshold_covers_gap() { - let meter = PersistedBlockMeter::test_instance(10); - meter - .shared - .last_persisted_block - .store(0, Ordering::Release); - - // gap=5 < threshold=10 - assert!(meter - .wait_for_persisted_block(5, TEST_TIMEOUT) - .await - .is_ok()); - } - - #[tokio::test] - async fn wait_enforces_backpressure_at_exact_threshold() { - let meter = PersistedBlockMeter::test_instance(16); - meter - .shared - .last_persisted_block - .store(84, Ordering::Release); - - // gap=16 equals threshold=16, so backpressure must apply. - let result = meter.wait_for_persisted_block(100, SHORT_TIMEOUT).await; - assert!(result.is_err()); - } - - #[tokio::test] - async fn wait_times_out_when_not_persisted() { - let meter = PersistedBlockMeter::test_instance(0); - meter - .shared - .last_persisted_block - .store(5, Ordering::Release); - - // target=100, persisted=5 — will never catch up - let result = meter.wait_for_persisted_block(100, SHORT_TIMEOUT).await; - assert!(result.is_err()); - } - - #[tokio::test] - async fn wait_skips_backpressure_when_reconnecting() { - let meter = PersistedBlockMeter::test_instance(0); - // persisted=0, target=100 — would normally block and time out - meter - .shared - .subscription_status - .store(SUBSCRIPTION_STATUS_RECONNECTING, Ordering::Release); - assert!(meter - .wait_for_persisted_block(100, TEST_TIMEOUT) - .await - .is_ok()); - } - - #[tokio::test] - async fn wait_enforces_backpressure_when_active() { - let meter = PersistedBlockMeter::test_instance(0); - meter - .shared - .last_persisted_block - .store(5, Ordering::Release); - // Status is ACTIVE (default), persisted=5, target=100 — should block and time out - let result = meter.wait_for_persisted_block(100, SHORT_TIMEOUT).await; - assert!(result.is_err()); - } - - #[tokio::test] - async fn seed_transitions_connected_to_active() { - let meter = PersistedBlockMeter::test_instance(0); - meter - .shared - .subscription_status - .store(SUBSCRIPTION_STATUS_CONNECTED, Ordering::Release); - meter.seed(100); - assert_eq!( - meter.shared.subscription_status.load(Ordering::Acquire), - SUBSCRIPTION_STATUS_ACTIVE - ); - } - - #[tokio::test] - async fn seed_does_not_activate_when_reconnecting() { - let meter = PersistedBlockMeter::test_instance(0); - meter - .shared - .subscription_status - .store(SUBSCRIPTION_STATUS_RECONNECTING, Ordering::Release); - meter.seed(100); - assert_eq!( - meter.shared.subscription_status.load(Ordering::Acquire), - SUBSCRIPTION_STATUS_RECONNECTING - ); - assert_eq!( - meter.shared.last_persisted_block.load(Ordering::Acquire), - 100 - ); - } - - #[tokio::test] - async fn wait_skips_backpressure_when_connected_but_not_active() { - let meter = PersistedBlockMeter::test_instance(0); - meter - .shared - .subscription_status - .store(SUBSCRIPTION_STATUS_CONNECTED, Ordering::Release); - assert!(meter - .wait_for_persisted_block(100, TEST_TIMEOUT) - .await - .is_ok()); - } - - #[tokio::test] - async fn wait_wakes_up_on_notify() { - let meter = PersistedBlockMeter::test_instance(2); - meter - .shared - .last_persisted_block - .store(0, Ordering::Release); - - let shared = Arc::clone(&meter.shared); - tokio::spawn(async move { - tokio::time::sleep(Duration::from_millis(10)).await; - shared.last_persisted_block.store(50, Ordering::Release); - shared.notify.notify_waiters(); - }); - - assert!(meter - .wait_for_persisted_block(51, TEST_TIMEOUT) - .await - .is_ok()); - } - - #[tokio::test] - async fn seed_updates_persisted_block() { - let meter = PersistedBlockMeter::test_instance(5); - assert_eq!(meter.shared.last_persisted_block.load(Ordering::Acquire), 0); - - meter.seed(42); - assert_eq!( - meter.shared.last_persisted_block.load(Ordering::Acquire), - 42 - ); - - // seed should only increase, never decrease - meter.seed(10); - assert_eq!( - meter.shared.last_persisted_block.load(Ordering::Acquire), - 42 - ); - } - - #[tokio::test] - async fn seed_from_latest_block_updates_meter() { - let meter = PersistedBlockMeter::test_instance(5); - - let mut eth = MockEthereumAPI::new(); - eth.expect_get_block_by_number().return_once(|_| { - Ok(Some(crate::json_structures::ExecutionBlock { - block_number: 99, - block_hash: Default::default(), - parent_hash: Default::default(), - timestamp: 0, - })) - }); - - seed_from_latest_block(&meter, ð).await; - assert_eq!( - meter.shared.last_persisted_block.load(Ordering::Acquire), - 99 - ); - } - - #[tokio::test] - async fn seed_from_latest_block_handles_no_block() { - let meter = PersistedBlockMeter::test_instance(5); - - let mut eth = MockEthereumAPI::new(); - eth.expect_get_block_by_number().return_once(|_| Ok(None)); - - seed_from_latest_block(&meter, ð).await; - assert_eq!(meter.shared.last_persisted_block.load(Ordering::Acquire), 0); - } - - #[tokio::test] - async fn seed_from_latest_block_handles_error() { - let meter = PersistedBlockMeter::test_instance(5); - - let mut eth = MockEthereumAPI::new(); - eth.expect_get_block_by_number() - .return_once(|_| Err(eyre!("connection refused"))); - - seed_from_latest_block(&meter, ð).await; - assert_eq!(meter.shared.last_persisted_block.load(Ordering::Acquire), 0); - } -} diff --git a/crates/eth-engine/src/retry.rs b/crates/eth-engine/src/retry.rs deleted file mode 100644 index e529775..0000000 --- a/crates/eth-engine/src/retry.rs +++ /dev/null @@ -1,60 +0,0 @@ -// Copyright 2026 Circle Internet Group, Inc. All rights reserved. -// -// SPDX-License-Identifier: Apache-2.0 -// -// Licensed under the Apache License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. -// You may obtain a copy of the License at -// -// http://www.apache.org/licenses/LICENSE-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// See the License for the specific language governing permissions and -// limitations under the License. - -use std::time::Duration; - -// A backoff policy that never retries, used as the default. -#[derive(Clone, Default)] -pub struct NoRetry; - -impl Iterator for NoRetry { - type Item = Duration; - - fn next(&mut self) -> Option { - // Never yields a duration, so the operation is attempted only once. - None - } -} - -#[cfg(test)] -mod tests { - use super::*; - use backon::Retryable; - use std::sync::atomic::{AtomicUsize, Ordering}; - - #[tokio::test] - async fn test_no_retry() { - let attempts = AtomicUsize::new(0); - let operation = || async { - attempts.fetch_add(1, Ordering::SeqCst); - Err::<(), _>("operation failed") - }; - - let result = operation - .retry(NoRetry) - .notify(|e, dur| { - panic!( - "This should not be printed, as NoRetry does not retry. Error: {e}, Duration: {dur:?}", - ); - }) - .await; - - assert!(result.is_err()); - - // Ensure the operation was attempted only once. - assert_eq!(attempts.load(Ordering::SeqCst), 1); - } -} diff --git a/crates/eth-engine/src/rpc/auth.rs b/crates/eth-engine/src/rpc/auth.rs deleted file mode 100644 index 2577f60..0000000 --- a/crates/eth-engine/src/rpc/auth.rs +++ /dev/null @@ -1,80 +0,0 @@ -// Copyright 2025 Circle Internet Group, Inc. All rights reserved. -// -// SPDX-License-Identifier: Apache-2.0 -// -// Licensed under the Apache License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. -// You may obtain a copy of the License at -// -// http://www.apache.org/licenses/LICENSE-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// See the License for the specific language governing permissions and -// limitations under the License. - -use std::path::Path; - -use alloy_rpc_types_engine::{Claims, JwtSecret}; -use eyre::Ok; -use jsonwebtoken::{encode, get_current_timestamp, Algorithm, EncodingKey, Header}; - -/// Default algorithm used for JWT token signing. -const DEFAULT_ALGORITHM: Algorithm = Algorithm::HS256; - -/// Contains the JWT secret and claims parameters. -pub struct Auth { - key: EncodingKey, -} - -impl Auth { - /// Create a new `Auth` struct given the JWT secret. - pub fn new(secret: JwtSecret) -> Self { - Self { - key: EncodingKey::from_secret(secret.as_bytes()), - } - } - - /// Create a new `Auth` struct given the path to the file containing the hex - /// encoded jwt key. - pub fn new_from_path(jwt_path: &Path) -> eyre::Result { - Ok(Self::new(JwtSecret::from_file(jwt_path)?)) - } - - /// Generate a JWT token with `claims.iat` set to current time. - pub fn generate_token(&self) -> eyre::Result { - let claims = self.generate_claims_at_timestamp(); - self.generate_token_with_claims(&claims) - } - - /// Generate a JWT token with the given claims. - fn generate_token_with_claims(&self, claims: &Claims) -> eyre::Result { - let header = Header::new(DEFAULT_ALGORITHM); - Ok(encode(&header, claims, &self.key)?) - } - - /// Generate a `Claims` struct with `iat` set to current time - fn generate_claims_at_timestamp(&self) -> Claims { - Claims { - iat: get_current_timestamp(), - exp: None, - } - } -} - -#[cfg(test)] -mod tests { - use super::*; - - #[test] - fn test_roundtrip() { - let secret = JwtSecret::random(); - let auth = Auth::new(secret); - let claims = auth.generate_claims_at_timestamp(); - let token = auth.generate_token_with_claims(&claims).unwrap(); - - let res = secret.validate(token.as_str()); - assert!(res.is_ok()); - } -} diff --git a/crates/eth-engine/src/rpc/engine_rpc.rs b/crates/eth-engine/src/rpc/engine_rpc.rs deleted file mode 100644 index c4387a3..0000000 --- a/crates/eth-engine/src/rpc/engine_rpc.rs +++ /dev/null @@ -1,259 +0,0 @@ -// Copyright 2025 Circle Internet Group, Inc. All rights reserved. -// -// SPDX-License-Identifier: Apache-2.0 -// -// Licensed under the Apache License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. -// You may obtain a copy of the License at -// -// http://www.apache.org/licenses/LICENSE-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// See the License for the specific language governing permissions and -// limitations under the License. - -use std::collections::HashSet; -use std::path::Path; -use std::time::Duration; - -use alloy_rpc_types_engine::{ - ExecutionPayloadEnvelopeV4, ExecutionPayloadEnvelopeV5, ExecutionPayloadV3, ForkchoiceState, - ForkchoiceUpdated, PayloadAttributes, PayloadId as AlloyPayloadId, PayloadStatus, -}; -use async_trait::async_trait; -use backon::{Backoff, BackoffBuilder}; -use eyre::Context; -use reqwest::{Client, Url}; -use serde::de::DeserializeOwned; -use serde_json::json; -use tracing::debug; - -use arc_consensus_types::{BlockHash, Bytes, B256}; - -use crate::capabilities::EngineCapabilities; -use crate::constants::*; -use crate::engine::EngineAPI; -use crate::retry::NoRetry; -use crate::rpc::auth::Auth; -use crate::rpc::request_builder::RpcRequestBuilder; - -/// RPC client for connecting to Engine RPC endpoint with JWT authentication. -pub struct EngineRpc { - client: Client, - url: Url, - auth: Auth, -} - -impl std::fmt::Display for EngineRpc { - fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { - write!(f, "{}", self.url) - } -} - -impl EngineRpc { - /// Create a new `EngineRpc` struct given the URL and JWT path. - pub fn new(url: Url, jwt_path: &Path) -> eyre::Result { - Ok(Self { - client: Client::builder().build()?, - url, - auth: Auth::new_from_path(jwt_path).wrap_err("Failed to load configuration file")?, - }) - } - - /// Building an RPC request to the Ethereum server. - /// - method: The method to call. - fn build_rpc_request<'a, 'b>(&'a self, method: &'b str) -> RpcRequestBuilder<'a> - where - 'b: 'a, - { - RpcRequestBuilder::new(&self.client, &self.url, method) - } - - /// Send an RPC request to the Ethereum server. - /// - method: The method to call. - /// - params: The parameters to pass to the method. - /// - timeout: The timeout for the request. - /// - retry_policy: The retry policy for the request. - pub async fn rpc_request( - &self, - method: &str, - params: serde_json::Value, - timeout: Duration, - retry_policy: impl Backoff, - ) -> eyre::Result { - self.build_rpc_request(method) - .params(params) - .timeout(timeout) - .retry(retry_policy) - .bearer_auth(self.auth.generate_token()?) - .send() - .await - } - - /// Exchange capabilities with the Engine RPC endpoint. - pub async fn exchange_capabilities(&self) -> eyre::Result { - let capabilities: HashSet = self - .rpc_request( - ENGINE_EXCHANGE_CAPABILITIES, - json!([NODE_CAPABILITIES]), - ENGINE_EXCHANGE_CAPABILITIES_TIMEOUT, - ENGINE_EXCHANGE_CAPABILITIES_RETRY_RPC.build(), - ) - .await?; - debug!("🟠 EngineAPI: exchange_capabilities: {:?}", capabilities); - Ok(EngineCapabilities::from_capabilities(&capabilities)) - } - - /// Notify that a fork choice has been updated, to set the head of the chain - /// - head_block_hash: The block hash of the head of the chain - /// - maybe_payload_attributes: Optional payload attributes for the next block - pub async fn forkchoice_updated( - &self, - head_block_hash: BlockHash, - maybe_payload_attributes: Option, - ) -> eyre::Result { - let forkchoice_state = ForkchoiceState { - head_block_hash, - safe_block_hash: head_block_hash, - finalized_block_hash: head_block_hash, - }; - let res = self - .rpc_request( - ENGINE_FORKCHOICE_UPDATED_V3, - json!([forkchoice_state, maybe_payload_attributes]), - ENGINE_FORKCHOICE_UPDATED_TIMEOUT, - NoRetry, - ) - .await; - debug!( - "🟠 EngineAPI: forkchoice_updated{}: {:?}", - maybe_payload_attributes - .map(|_| " with payload attributes") - .unwrap_or(""), - head_block_hash - ); - res - } - - /// Get a payload by its ID. - /// Uses V5 (Osaka) when `use_v5` is true, otherwise V4. - pub async fn get_payload( - &self, - payload_id: AlloyPayloadId, - use_v5: bool, - ) -> eyre::Result { - let execution_payload = if use_v5 { - let ExecutionPayloadEnvelopeV5 { - execution_payload, .. - } = self - .rpc_request( - ENGINE_GET_PAYLOAD_V5, - json!([payload_id]), - ENGINE_GET_PAYLOAD_TIMEOUT, - NoRetry, - ) - .await?; - execution_payload - } else { - let ExecutionPayloadEnvelopeV4 { envelope_inner, .. } = self - .rpc_request( - ENGINE_GET_PAYLOAD_V4, - json!([payload_id]), - ENGINE_GET_PAYLOAD_TIMEOUT, - NoRetry, - ) - .await?; - envelope_inner.execution_payload - }; - debug!( - "🟠 EngineAPI: get_payload: {:?}", - execution_payload.payload_inner.payload_inner.block_hash, - ); - Ok(execution_payload) - } - - /// Notify that a new payload has been created. - /// - execution_payload: The execution payload to be included in the next block. - /// - versioned_hashes: The versioned hashes of the blobs in the execution payload. - /// - parent_block_hash: The hash of the parent block. - pub async fn new_payload( - &self, - execution_payload: &ExecutionPayloadV3, - versioned_hashes: Vec, - parent_block_hash: BlockHash, - ) -> eyre::Result { - let empty_execution_requests: Vec = Vec::new(); - let params = json!([ - execution_payload, - versioned_hashes, - parent_block_hash, - empty_execution_requests - ]); - let res = self - .rpc_request( - ENGINE_NEW_PAYLOAD_V4, - params, - ENGINE_NEW_PAYLOAD_TIMEOUT, - NoRetry, - ) - .await; - - let block_hash = execution_payload.payload_inner.payload_inner.block_hash; - debug!("🟠 EngineAPI: new_payload: {:?}", block_hash); - - res - } -} - -#[async_trait] -impl EngineAPI for EngineRpc { - /// Exchange capabilities with the engine. - async fn exchange_capabilities(&self) -> eyre::Result { - EngineRpc::exchange_capabilities(self) - .await - .wrap_err("EngineRpc exchange_capabilities call failed") - } - - /// Set the latest forkchoice state. - async fn forkchoice_updated( - &self, - head_block_hash: BlockHash, - maybe_payload_attributes: Option, - ) -> eyre::Result { - EngineRpc::forkchoice_updated(self, head_block_hash, maybe_payload_attributes.clone()) - .await - .wrap_err_with(|| { - format!( - "EngineRpc forkchoice_updated call failed; block_hash {:?}, payload_attributes {:?}", - head_block_hash, - maybe_payload_attributes, - ) - }) - } - - /// Get a payload by its ID. - async fn get_payload( - &self, - payload_id: AlloyPayloadId, - use_v5: bool, - ) -> eyre::Result { - EngineRpc::get_payload(self, payload_id, use_v5) - .await - .wrap_err_with(|| format!("EngineRpc get_payload call failed; payload_id {payload_id}")) - } - - /// Notify that a new payload has been created. - async fn new_payload( - &self, - execution_payload: &ExecutionPayloadV3, - versioned_hashes: Vec, - parent_block_hash: BlockHash, - ) -> eyre::Result { - let payload_hash = execution_payload.payload_inner.payload_inner.block_hash; - EngineRpc::new_payload(self, execution_payload, versioned_hashes, parent_block_hash) - .await - .wrap_err_with(|| format!("EngineRpc new_payload call failed; block_hash {payload_hash}, parent_block_hash {parent_block_hash}")) - } -} diff --git a/crates/eth-engine/src/rpc/errors.rs b/crates/eth-engine/src/rpc/errors.rs deleted file mode 100644 index 553b6d5..0000000 --- a/crates/eth-engine/src/rpc/errors.rs +++ /dev/null @@ -1,208 +0,0 @@ -// Copyright 2026 Circle Internet Group, Inc. All rights reserved. -// -// SPDX-License-Identifier: Apache-2.0 -// -// Licensed under the Apache License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. -// You may obtain a copy of the License at -// -// http://www.apache.org/licenses/LICENSE-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// See the License for the specific language governing permissions and -// limitations under the License. - -use std::fmt; - -use crate::rpc::json_structs::JsonError; -use eyre::Report; -use jsonrpsee_types::error::ErrorObject; -use thiserror::Error; - -/// Error codes taken from reth's code. -/// See https://github.com/paradigmxyz/reth/blob/7345e1e5b5b88e53c4b3f3152078653507d0d26f/crates/rpc/rpc-engine-api/src/error.rs -/// -/// Code used by reth for EngineApiError::UnknownPayload. -const UNKNOWN_PAYLOAD_CODE: i32 = -38001; - -/// Code used by reth for EngineApiError::UnsupportedFork. -const UNSUPPORTED_FORK_CODE: i32 = -38005; - -/// Represents an error returned by the Engine API RPC. -/// Mirrors the structure of the error that reth returns. -#[derive(Debug, Clone, Error)] -pub struct EngineApiRpcError { - code: i32, - message: String, - // To create this field, reth wraps the Display output of the JSON-RPC error - // inside a struct, converts that struct into a String, and then inserts that - // String into the JSON-RPC error's `data` field. - // As a result, whatever ends up in the `data` field can be retrieved as a plain - // String. - data: Option, -} - -impl EngineApiRpcError { - /// Creates a new `EngineApiRpcError`. - pub fn new(code: i32, message: &str, data: Option<&str>) -> Self { - Self { - code, - message: message.into(), - data: data.map(|d| d.into()), - } - } - - /// Classifies the error into a specific kind. - pub fn kind(&self) -> EngineRpcErrorKind { - match self.code { - UNKNOWN_PAYLOAD_CODE => EngineRpcErrorKind::UnknownPayload, - UNSUPPORTED_FORK_CODE => EngineRpcErrorKind::UnsupportedFork, - _ => EngineRpcErrorKind::Other, - } - } - - /// Checks if the error is of kind `UnknownPayload`. - pub fn is_unknown_payload(&self) -> bool { - self.kind() == EngineRpcErrorKind::UnknownPayload - } - - /// Checks if the error is of kind `UnsupportedFork`. - pub fn is_unsupported_fork(&self) -> bool { - self.kind() == EngineRpcErrorKind::UnsupportedFork - } -} - -impl fmt::Display for EngineApiRpcError { - fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { - match &self.data { - Some(data) => write!(f, "Code {}: {}: {}", self.code, self.message, data), - None => write!(f, "Code {}: {}", self.code, self.message), - } - } -} - -/// Convert a `JsonError` object to our engine api error type. -impl From for EngineApiRpcError { - fn from(err: JsonError) -> Self { - Self { - code: err.code, - message: err.message, - data: err.data.map(|d| d.to_string()), - } - } -} - -/// Convert a jsonrpsee `ErrorObject` into our Engine api error type. -impl From> for EngineApiRpcError { - fn from(err: ErrorObject<'_>) -> Self { - Self { - code: err.code(), - message: err.message().to_owned(), - data: err.data().map(|d| d.get().to_owned()), - } - } -} - -/// Converts an `eyre::Report` into an `EngineApiRpcError` if possible. -impl TryFrom<&Report> for EngineApiRpcError { - type Error = String; - fn try_from(err: &Report) -> Result { - for cause in err.chain() { - if let Some(engine_api_error) = cause.downcast_ref::() { - return Ok(engine_api_error.clone()); - } - } - Err("error object isn't of type EngineApiRpcError".to_string()) - } -} - -/// Converts an `eyre::Report` into an `EngineApiRpcError` if possible. -impl TryFrom for EngineApiRpcError { - type Error = String; - fn try_from(err: Report) -> Result { - EngineApiRpcError::try_from(&err) - } -} - -/// Classification of Engine API JSON-RPC errors we want to match specifically. -/// Can easily be extended with additional kinds. -#[derive(Debug, Clone, Copy, PartialEq, Eq)] -pub enum EngineRpcErrorKind { - /// reth thinks that the payload does not exist or is not available locally. - UnknownPayload, - /// reth does not support the requested Engine API version for the current fork. - UnsupportedFork, - /// Any JSON-RPC error we are not interested in. - Other, -} - -#[cfg(test)] -mod tests { - use super::*; - use eyre::{eyre, Report}; - use serde_json::json; - - #[test] - fn display_includes_data() { - let err = EngineApiRpcError::new(42, "some error", Some("root cause")); - assert_eq!(err.to_string(), "Code 42: some error: root cause"); - } - - #[test] - fn display_omits_data() { - let err = EngineApiRpcError::new(42, "some error", None); - assert_eq!(err.to_string(), "Code 42: some error"); - } - - #[test] - fn unknown_payload_error() { - let unknown = EngineApiRpcError::new(UNKNOWN_PAYLOAD_CODE, "Unknown payload", None); - assert_eq!(unknown.kind(), EngineRpcErrorKind::UnknownPayload); - assert!(unknown.is_unknown_payload()); - - let other = EngineApiRpcError::new(-32603, "Internal error", None); - assert_eq!(other.kind(), EngineRpcErrorKind::Other); - assert!(!other.is_unknown_payload()); - } - - #[test] - fn try_from_report_ok() { - let original = EngineApiRpcError::new(42, "some error", Some("root cause")); - let report = Report::new(original.clone()); - - let extracted = EngineApiRpcError::try_from(&report).expect("should be EngineApiRpcError"); - assert_eq!(extracted.to_string(), original.to_string()); - } - - #[test] - fn try_from_report_err() { - let report = eyre!("some other error"); - assert!(EngineApiRpcError::try_from(&report).is_err()); - } - - #[test] - fn from_json_error() { - let json_err = JsonError { - code: 42, - message: "some error".to_string(), - data: Some(json!("root cause")), - }; - - let err = EngineApiRpcError::from(json_err); - assert_eq!(err.to_string(), "Code 42: some error: \"root cause\""); - assert_eq!(err.kind(), EngineRpcErrorKind::Other); - } - - #[test] - fn from_error_object() { - use jsonrpsee_types::error::ErrorObjectOwned; - - let owned = ErrorObjectOwned::owned(42, "some error", Some("root cause")); - let err = EngineApiRpcError::from(owned); - - assert_eq!(err.to_string(), "Code 42: some error: \"root cause\""); - assert_eq!(err.kind(), EngineRpcErrorKind::Other); - } -} diff --git a/crates/eth-engine/src/rpc/ethereum_rpc.rs b/crates/eth-engine/src/rpc/ethereum_rpc.rs deleted file mode 100644 index f0a5d16..0000000 --- a/crates/eth-engine/src/rpc/ethereum_rpc.rs +++ /dev/null @@ -1,796 +0,0 @@ -// Copyright 2025 Circle Internet Group, Inc. All rights reserved. -// -// SPDX-License-Identifier: Apache-2.0 -// -// Licensed under the Apache License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. -// You may obtain a copy of the License at -// -// http://www.apache.org/licenses/LICENSE-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// See the License for the specific language governing permissions and -// limitations under the License. - -use std::time::Duration; - -use alloy_consensus::TxEnvelope; -use alloy_rpc_types::Block; -use alloy_rpc_types_engine::ExecutionPayloadV3; -use alloy_rpc_types_txpool::{TxpoolInspect, TxpoolStatus}; -use alloy_sol_types::SolCall; -use async_trait::async_trait; -use backon::BackoffBuilder; -use eyre::Context; -use reqwest::{Client, Url}; -use serde::de::DeserializeOwned; -use serde_json::{from_value, json, Value}; -use tracing::{debug, trace}; - -use arc_consensus_types::{ConsensusParams, ValidatorSet}; - -use crate::abi_utils::{ - abi_decode_consensus_params, abi_decode_validator_set, consensusParamsCall, - getActiveValidatorSetCall, -}; -use crate::constants::{ - ETH_BATCH_REQUEST_TIMEOUT, ETH_CALL_RETRY, ETH_DEFAULT_TIMEOUT, PROTOCOL_CONFIG_ADDRESS, - VALIDATOR_REGISTRY_ADDRESS, -}; -use crate::engine::EthereumAPI; -use crate::json_structures::ExecutionBlock; -use crate::rpc::request_builder::RpcRequestBuilder; - -/// Generate ABI parameters for RPC calls to get active validator set -fn abi_get_active_validator_set_params_rpc(block_height: u64) -> eyre::Result { - // Encode the 4‑byte selector + (no args) - let calldata = getActiveValidatorSetCall {}.abi_encode(); - let data = format!("0x{}", hex::encode(calldata)); - - Ok(serde_json::json!([{ - "to": format!("{:#x}", VALIDATOR_REGISTRY_ADDRESS), - "data": data - }, format!("0x{:x}", block_height)])) -} - -/// Generate ABI parameters for RPC calls to get consensus params -fn abi_get_consensus_params_params_rpc(block_height: u64) -> eyre::Result { - let calldata = consensusParamsCall {}.abi_encode(); - let data = format!("0x{}", hex::encode(calldata)); - - Ok(serde_json::json!([{ - "to": format!("{:#x}", PROTOCOL_CONFIG_ADDRESS), - "data": data - }, format!("0x{:x}", block_height)])) -} - -/// RPC client for Ethereum server. -pub struct EthereumRPC { - client: Client, - url: Url, - default_timeout: Duration, - batch_request_timeout: Duration, -} - -impl EthereumRPC { - /// Create a new `EthereumRPC` struct given the URL. - pub fn new(url: Url) -> eyre::Result { - Self::new_with_timeouts(url, ETH_DEFAULT_TIMEOUT, ETH_BATCH_REQUEST_TIMEOUT) - } - - /// Create a new `EthereumRPC` struct given the URL and request timeouts. - pub fn new_with_timeouts( - url: Url, - default_timeout: Duration, - batch_request_timeout: Duration, - ) -> eyre::Result { - Ok(Self { - client: Client::builder().build()?, - url, - default_timeout, - batch_request_timeout, - }) - } - - /// Building an RPC request to the Ethereum server. - /// - method: The method to call. - pub fn build_rpc_request<'a, 'b>(&'a self, method: &'b str) -> RpcRequestBuilder<'a> - where - 'b: 'a, - { - RpcRequestBuilder::new(&self.client, &self.url, method) - } - - /// Send an RPC request to the Ethereum server. - /// - method: The method to call. - /// - params: The parameters to pass to the method. - /// - timeout: The timeout for the request. - pub async fn rpc_request( - &self, - method: &str, - params: Value, - timeout: Duration, - ) -> eyre::Result { - self.build_rpc_request(method) - .params(params) - .timeout(timeout) - .send() - .await - } - - /// Probe the RPC server with `net_listening` to confirm - /// it is accepting requests. - pub async fn check_connectivity(&self) -> eyre::Result { - self.rpc_request("net_listening", json!([]), self.default_timeout) - .await - } - - /// Get the eth1 chain id of the given endpoint. - pub async fn get_chain_id(&self) -> eyre::Result { - self.build_rpc_request("eth_chainId") - .params(json!([])) - .timeout(self.default_timeout) - .retry(ETH_CALL_RETRY.build()) - .send() - .await - } - - /// Get the genesis block. - pub async fn get_genesis_block(&self) -> eyre::Result { - let block: Option = self - .rpc_request( - "eth_getBlockByNumber", - json!(["0x0", false]), - self.default_timeout, - ) - .await?; - - block.ok_or_else(|| eyre::eyre!("Genesis block not found")) - } - - #[tracing::instrument(skip(self))] - pub async fn get_active_validator_set(&self, block_height: u64) -> eyre::Result { - let params = abi_get_active_validator_set_params_rpc(block_height)?; - debug!("eth_call params: {params}"); - - let result: String = self - .build_rpc_request("eth_call") - .params(params) - .timeout(self.default_timeout) - .retry(ETH_CALL_RETRY.build()) - .send() - .await - .wrap_err_with(|| { - format!( - "eth_call request for active validator set failed for height={block_height}" - ) - })?; - - trace!("eth_call result: {result}"); - - let result = hex::decode(result.trim_start_matches("0x"))?; - abi_decode_validator_set(result) - } - - #[tracing::instrument(skip(self))] - pub async fn get_consensus_params(&self, block_height: u64) -> eyre::Result { - let params = abi_get_consensus_params_params_rpc(block_height)?; - debug!("eth_call params: {params}"); - - let result: String = self - .build_rpc_request("eth_call") - .params(params) - .timeout(self.default_timeout) - .retry(ETH_CALL_RETRY.build()) - .send() - .await - .wrap_err_with(|| { - format!("eth_call request for consensus params failed for height={block_height}") - })?; - - trace!("eth_call result: {result}"); - - let result = hex::decode(result.trim_start_matches("0x"))?; - abi_decode_consensus_params(result) - } - - /// Get a block by its number. - /// - block_number: The number of the block to get. - pub async fn get_block_by_number( - &self, - block_number: &str, - ) -> eyre::Result> { - let return_full_transaction_objects = false; - let params = json!([block_number, return_full_transaction_objects]); - self.rpc_request("eth_getBlockByNumber", params, self.default_timeout) - .await - } - - /// Get a batch of full execution payloads. - async fn get_execution_payloads( - &self, - block_numbers: &[String], - ) -> eyre::Result>> { - if block_numbers.is_empty() { - return Ok(vec![]); - } - debug!("EthereumRPC: get_execution_payloads for block_numbers={block_numbers:?}"); - - // Build JSON-RPC batch request for full blocks with transactions - let return_full_transaction_objects = true; - let batch_requests = block_numbers - .iter() - .enumerate() - .map(|(id, block_number)| { - json!({ - "jsonrpc": "2.0", - "method": "eth_getBlockByNumber", - "params": [block_number, return_full_transaction_objects], - "id": id - }) - }) - .collect::>(); - - // Send batch request - let response = self - .client - .post(self.url.clone()) - .json(&batch_requests) - .timeout(self.batch_request_timeout) - .send() - .await - .wrap_err("Failed to send RPC batch request")?; - - let batch_responses: Vec = response - .json() - .await - .wrap_err("Failed to parse batch response")?; - - // Parse results maintaining order and convert to execution payloads - let mut results = vec![None; block_numbers.len()]; - let mut processed_ids = std::collections::HashSet::new(); - - for response in batch_responses { - let (Some(id), Some(result)) = ( - response.get("id").and_then(|v| v.as_u64()), - response.get("result"), - ) else { - if let Some(error) = response.get("error") { - debug!("RPC batch response error: {}", error); - } - continue; - }; - - #[allow(clippy::cast_possible_truncation)] // bounded by block_numbers.len() below - let id = id as usize; - if id >= block_numbers.len() { - debug!( - "Invalid response ID {} for batch of size {}", - id, - block_numbers.len() - ); - continue; - } - - processed_ids.insert(id); - - if result.is_null() { - debug!(id=%id, block_number=%block_numbers[id], "No block found for request"); - continue; - } - - // Parse as full block with transactions - match from_value::(result.clone()) { - Ok(block) => { - let block_hash = block.header.hash; - let consensus_block = - block.into_consensus().convert_transactions::(); - let execution_payload = - ExecutionPayloadV3::from_block_unchecked(block_hash, &consensus_block); - - if let Some(entry) = results.get_mut(id) { - *entry = Some(execution_payload); - } - } - Err(e) => { - debug!(id=%id, error=%e, "Failed to parse block for request"); - } - } - } - - for (idx, block_number) in block_numbers.iter().enumerate() { - if !processed_ids.contains(&idx) { - debug!(idx=%idx, block_number=%block_number, "No response received for request"); - } - } - - Ok(results) - } - - /// Get the status of the transaction pool. - pub async fn txpool_status(&self) -> eyre::Result { - self.rpc_request("txpool_status", json!([]), self.default_timeout) - .await - } - - /// Get the contents of the transaction pool. - pub async fn txpool_inspect(&self) -> eyre::Result { - self.rpc_request("txpool_inspect", json!([]), self.default_timeout) - .await - } -} - -#[async_trait] -impl EthereumAPI for EthereumRPC { - /// Get the eth1 chain id of the given endpoint. - async fn get_chain_id(&self) -> eyre::Result { - self.get_chain_id() - .await - .wrap_err("EthereumRPC get_chain_id call failed") - } - - /// Get the genesis block. - async fn get_genesis_block(&self) -> eyre::Result { - self.get_genesis_block() - .await - .wrap_err("EthereumRPC get_genesis_block call failed") - } - - /// Get the active validator set at a specific block height. - async fn get_active_validator_set(&self, block_height: u64) -> eyre::Result { - self.get_active_validator_set(block_height) - .await - .wrap_err_with(|| { - format!( - "EthereumRPC get_active_validator_set call failed for height={block_height}" - ) - }) - } - - /// Get the consensus parameters at a specific block height. - async fn get_consensus_params(&self, block_height: u64) -> eyre::Result { - self.get_consensus_params(block_height).await - } - - /// Get a block by its number. - async fn get_block_by_number( - &self, - block_number: &str, - ) -> eyre::Result> { - self.get_block_by_number(block_number) - .await - .wrap_err_with(|| { - format!( - "EthereumRPC get_block_by_number call failed for block number={block_number}" - ) - }) - } - - /// Get a batch of full execution payloads. - async fn get_execution_payloads( - &self, - block_numbers: &[String], - ) -> eyre::Result>> { - self.get_execution_payloads(block_numbers) - .await - .wrap_err_with(|| { - format!( - "EthereumRPC get_execution_payloads call failed for block numbers={block_numbers:?}" - ) - }) - } - - /// Get the status of the transaction pool. - async fn txpool_status(&self) -> eyre::Result { - self.txpool_status() - .await - .wrap_err("EthereumRPC txpool_status call failed") - } - - /// Get the contents of the transaction pool. - async fn txpool_inspect(&self) -> eyre::Result { - self.txpool_inspect() - .await - .wrap_err("EthereumRPC txpool_inspect call failed") - } -} - -#[cfg(test)] -mod tests { - use super::*; - use alloy_consensus::Header as ConsensusHeader; - use alloy_primitives::B256; - use alloy_rpc_types_eth::{Block, Header}; - use serde_json::json; - use wiremock::matchers::{body_json, method, path}; - use wiremock::{Mock, MockServer, ResponseTemplate}; - - fn create_test_block( - number: u64, - hash: &str, - parent_hash: &str, - timestamp: u64, - gas_used: u64, - ) -> serde_json::Value { - let inner = ConsensusHeader { - number, - timestamp, - parent_hash: parent_hash.parse::().unwrap(), - gas_used, - ..Default::default() - }; - let header = Header { - hash: hash.parse::().unwrap(), - inner, - ..Default::default() - }; - let block: Block<()> = Block { - header, - ..Default::default() - }; - - serde_json::to_value(block).unwrap() - } - - #[test] - fn test_encode_get_valset_params_abi_rpc() { - let block_number = 4567; - let params = abi_get_active_validator_set_params_rpc(block_number).unwrap(); - - let expected = serde_json::json!([ - { - "to": VALIDATOR_REGISTRY_ADDRESS, - "data": "0x24408a68" - }, - "0x11d7" - ]); - assert_eq!(params, expected); - } - - #[test] - fn test_encode_get_consensus_params_abi_rpc() { - let block_number = 4567; - let params = abi_get_consensus_params_params_rpc(block_number).unwrap(); - - let expected = serde_json::json!([ - { - "to": PROTOCOL_CONFIG_ADDRESS, - "data": "0x9fd02a36" - }, - "0x11d7" - ]); - assert_eq!(params, expected); - } - - #[tokio::test] - async fn test_ethereum_rpc_get_genesis_block_success() { - let server = MockServer::start().await; - - let genesis_hash = "0xaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa"; - let genesis_block = create_test_block( - 0, - genesis_hash, - "0x0000000000000000000000000000000000000000000000000000000000000000", - 0, - 0, - ); - - Mock::given(method("POST")) - .and(path("/")) - .respond_with(ResponseTemplate::new(200).set_body_json(json!({ - "jsonrpc": "2.0", - "id": "1", - "result": genesis_block - }))) - .mount(&server) - .await; - - let url = Url::parse(&server.uri()).unwrap(); - let ethereum_rpc = EthereumRPC::new(url).unwrap(); - - let result = ethereum_rpc.get_genesis_block().await.unwrap(); - assert_eq!(result.block_hash, genesis_hash.parse::().unwrap()); - assert_eq!(result.timestamp, 0); - } - - #[tokio::test] - async fn test_ethereum_rpc_get_genesis_block_not_found() { - let server = MockServer::start().await; - - Mock::given(method("POST")) - .and(path("/")) - .respond_with(ResponseTemplate::new(200).set_body_json(json!({ - "jsonrpc": "2.0", - "id": "1", - "result": null - }))) - .mount(&server) - .await; - - let url = Url::parse(&server.uri()).unwrap(); - let ethereum_rpc = EthereumRPC::new(url).unwrap(); - - let err = ethereum_rpc.get_genesis_block().await.unwrap_err(); - assert!(err.to_string().contains("Genesis block not found")); - } - - #[tokio::test] - async fn test_ethereum_rpc_batch_payloads_success() { - let server = MockServer::start().await; - - let block1 = create_test_block( - 1, - "0x1234567890123456789012345678901234567890123456789012345678901234", - "0x0000000000000000000000000000000000000000000000000000000000000000", - 0x64, - 12345, - ); - let block2 = create_test_block( - 2, - "0x2345678901234567890123456789012345678901234567890123456789012345", - "0x1234567890123456789012345678901234567890123456789012345678901234", - 0xc8, - 26000, - ); - - Mock::given(method("POST")) - .and(path("/")) - .and(body_json(json!([ - { - "jsonrpc": "2.0", - "method": "eth_getBlockByNumber", - "params": ["0x1", true], - "id": 0 - }, - { - "jsonrpc": "2.0", - "method": "eth_getBlockByNumber", - "params": ["0x2", true], - "id": 1 - } - ]))) - // Reverse order to test matching by ID - .respond_with(ResponseTemplate::new(200).set_body_json(json!([ - { - "jsonrpc": "2.0", - "id": 1, - "result": block2 - }, - { - "jsonrpc": "2.0", - "id": 0, - "result": block1 - } - ]))) - .mount(&server) - .await; - - let url = Url::parse(&server.uri()).unwrap(); - let ethereum_rpc = EthereumRPC::new(url).unwrap(); - - let block_numbers = vec!["0x1".to_string(), "0x2".to_string()]; - let result = ethereum_rpc - .get_execution_payloads(&block_numbers) - .await - .unwrap(); - - assert_eq!(result.len(), 2); - assert!(result[0].is_some()); - assert!(result[1].is_some()); - - let payload1 = result[0].as_ref().unwrap(); - let payload2 = result[1].as_ref().unwrap(); - - assert_eq!(payload1.payload_inner.payload_inner.block_number, 1); - assert_eq!(payload1.payload_inner.payload_inner.gas_used, 12345); - assert_eq!(payload2.payload_inner.payload_inner.block_number, 2); - assert_eq!(payload2.payload_inner.payload_inner.gas_used, 26000); - } - - #[tokio::test] - async fn test_ethereum_rpc_batch_payloads_partial_failure() { - let server = MockServer::start().await; - - let block1 = create_test_block( - 5, - "0x1234567890123456789012345678901234567890123456789012345678901234", - "0x0000000000000000000000000000000000000000000000000000000000000000", - 0x64, - 33344, - ); - - Mock::given(method("POST")) - .and(path("/")) - .and(body_json(json!([ - { - "jsonrpc": "2.0", - "method": "eth_getBlockByNumber", - "params": ["0x1", true], - "id": 0 - }, - { - "jsonrpc": "2.0", - "method": "eth_getBlockByNumber", - "params": ["0x999", true], - "id": 1 - } - ]))) - .respond_with(ResponseTemplate::new(200).set_body_json(json!([ - { - "jsonrpc": "2.0", - "id": 0, - "result": block1 - }, - { - "jsonrpc": "2.0", - "id": 1, - "result": null - } - ]))) - .mount(&server) - .await; - - let url = Url::parse(&server.uri()).unwrap(); - let ethereum_rpc = EthereumRPC::new(url).unwrap(); - - let block_numbers = vec!["0x1".to_string(), "0x999".to_string()]; - let result = ethereum_rpc - .get_execution_payloads(&block_numbers) - .await - .unwrap(); - - assert_eq!(result.len(), 2); - assert!(result[0].is_some()); - - let payload = result[0].as_ref().unwrap(); - - assert_eq!(payload.payload_inner.payload_inner.block_number, 5); - assert_eq!(payload.payload_inner.payload_inner.gas_used, 33344); - - assert!(result[1].is_none()); - } - - #[tokio::test] - async fn test_ethereum_rpc_batch_payloads_response_length_validation() { - let server = MockServer::start().await; - - Mock::given(method("POST")) - .and(path("/")) - .and(body_json(json!([ - { - "jsonrpc": "2.0", - "method": "eth_getBlockByNumber", - "params": ["0x1", true], - "id": 0 - } - ]))) - .respond_with(ResponseTemplate::new(200).set_body_json(json!([ - { - "jsonrpc": "2.0", - "id": 10, // Invalid ID - too large - "result": { - "number": "0x1", - "hash": "0x1234567890123456789012345678901234567890123456789012345678901234" - } - } - ]))) - .mount(&server) - .await; - - let url = Url::parse(&server.uri()).unwrap(); - let ethereum_rpc = EthereumRPC::new(url).unwrap(); - - let block_numbers = vec!["0x1".to_string()]; - let result = ethereum_rpc - .get_execution_payloads(&block_numbers) - .await - .unwrap(); - - assert_eq!(result.len(), 1); - assert!(result[0].is_none()); - } - - #[tokio::test] - async fn test_ethereum_rpc_batch_payloads_invalid_block_data() { - let server = MockServer::start().await; - - Mock::given(method("POST")) - .and(path("/")) - .and(body_json(json!([ - { - "jsonrpc": "2.0", - "method": "eth_getBlockByNumber", - "params": ["0x1", true], - "id": 0 - } - ]))) - .respond_with(ResponseTemplate::new(200).set_body_json(json!([ - { - "jsonrpc": "2.0", - "id": 0, - "result": { - "number": "0x1", - "hash": "invalid_hash", - "parentHash": "0x0000000000000000000000000000000000000000000000000000000000000000", - "timestamp": "0x64" - // Missing many required fields - } - } - ]))) - .mount(&server) - .await; - - let url = Url::parse(&server.uri()).unwrap(); - let ethereum_rpc = EthereumRPC::new(url).unwrap(); - - let block_numbers = vec!["0x1".to_string()]; - let result = ethereum_rpc - .get_execution_payloads(&block_numbers) - .await - .unwrap(); - - assert_eq!(result.len(), 1); - assert!(result[0].is_none()); - } - - #[tokio::test] - async fn test_ethereum_rpc_batch_payloads_network_error() { - // Use invalid URL to trigger network error - let url = Url::parse("http://invalid-host:8545").unwrap(); - let ethereum_rpc = EthereumRPC::new(url).unwrap(); - - let block_numbers = vec!["0x1".to_string()]; - let result = ethereum_rpc.get_execution_payloads(&block_numbers).await; - - assert!(result.is_err()); - let error_msg = result.unwrap_err().to_string(); - assert!(error_msg.contains("Failed to send RPC batch request")); - } - - #[tokio::test] - async fn test_ethereum_rpc_batch_payloads_out_of_range_id() { - let server = MockServer::start().await; - - let valid_block = create_test_block( - 1, - "0x1234567890123456789012345678901234567890123456789012345678901234", - "0x0000000000000000000000000000000000000000000000000000000000000000", - 0x64, - 0, - ); - - Mock::given(method("POST")) - .and(path("/")) - .and(body_json(json!([ - { - "jsonrpc": "2.0", - "method": "eth_getBlockByNumber", - "params": ["0x1", true], - "id": 0 - } - ]))) - .respond_with(ResponseTemplate::new(200).set_body_json(json!([ - { - "jsonrpc": "2.0", - "id": 10, // Out of range - "result": valid_block - } - ]))) - .mount(&server) - .await; - - let url = Url::parse(&server.uri()).unwrap(); - let ethereum_rpc = EthereumRPC::new(url).unwrap(); - - let block_numbers = vec!["0x1".to_string()]; - let result = ethereum_rpc - .get_execution_payloads(&block_numbers) - .await - .unwrap(); - - assert_eq!(result.len(), 1); - assert!(result[0].is_none()); // id was out of range - } -} diff --git a/crates/eth-engine/src/rpc/json_structs.rs b/crates/eth-engine/src/rpc/json_structs.rs deleted file mode 100644 index 5813935..0000000 --- a/crates/eth-engine/src/rpc/json_structs.rs +++ /dev/null @@ -1,47 +0,0 @@ -// Copyright 2025 Circle Internet Group, Inc. All rights reserved. -// -// SPDX-License-Identifier: Apache-2.0 -// -// Licensed under the Apache License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. -// You may obtain a copy of the License at -// -// http://www.apache.org/licenses/LICENSE-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// See the License for the specific language governing permissions and -// limitations under the License. - -use serde::{Deserialize, Serialize}; - -/// JSON request body for RPC calls. -#[derive(Debug, PartialEq, Serialize, Deserialize)] -#[serde(rename_all = "camelCase")] -pub struct JsonRequestBody<'a> { - pub jsonrpc: &'a str, - pub method: &'a str, - pub params: serde_json::Value, - pub id: serde_json::Value, -} - -/// JSON error response. -#[derive(Debug, PartialEq, Serialize, Deserialize)] -pub struct JsonError { - pub code: i32, - pub message: String, - pub data: Option, -} - -/// JSON response. -#[derive(Debug, PartialEq, Serialize, Deserialize)] -#[serde(rename_all = "camelCase")] -pub struct JsonResponseBody { - pub jsonrpc: String, - #[serde(default)] - pub error: Option, - #[serde(default)] - pub result: serde_json::Value, - pub id: serde_json::Value, -} diff --git a/crates/eth-engine/src/rpc/mod.rs b/crates/eth-engine/src/rpc/mod.rs deleted file mode 100644 index 3fd1606..0000000 --- a/crates/eth-engine/src/rpc/mod.rs +++ /dev/null @@ -1,25 +0,0 @@ -// Copyright 2025 Circle Internet Group, Inc. All rights reserved. -// -// SPDX-License-Identifier: Apache-2.0 -// -// Licensed under the Apache License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. -// You may obtain a copy of the License at -// -// http://www.apache.org/licenses/LICENSE-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// See the License for the specific language governing permissions and -// limitations under the License. - -pub mod engine_rpc; -pub mod ethereum_rpc; - -mod auth; -mod errors; -mod json_structs; -mod request_builder; - -pub use errors::EngineApiRpcError; diff --git a/crates/eth-engine/src/rpc/request_builder.rs b/crates/eth-engine/src/rpc/request_builder.rs deleted file mode 100644 index 6ae2217..0000000 --- a/crates/eth-engine/src/rpc/request_builder.rs +++ /dev/null @@ -1,145 +0,0 @@ -// Copyright 2025 Circle Internet Group, Inc. All rights reserved. -// -// SPDX-License-Identifier: Apache-2.0 -// -// Licensed under the Apache License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. -// You may obtain a copy of the License at -// -// http://www.apache.org/licenses/LICENSE-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// See the License for the specific language governing permissions and -// limitations under the License. - -use std::time::Duration; - -use backon::{Backoff, Retryable}; -use reqwest::header::CONTENT_TYPE; -use reqwest::{Client, Url}; -use serde::de::DeserializeOwned; -use serde_json::{json, Value}; -use tracing::warn; - -use crate::retry::NoRetry; -use crate::rpc::errors::EngineApiRpcError; -use crate::rpc::json_structs::{JsonRequestBody, JsonResponseBody}; - -/// A builder for creating and sending a JSON-RPC request. -pub struct RpcRequestBuilder<'a, B: Backoff = NoRetry> { - client: &'a Client, - url: &'a Url, - method: &'a str, - params: Option, - timeout: Option, - bearer_auth: Option, - retry_policy: B, -} - -impl<'a> RpcRequestBuilder<'a> { - /// Creates a new builder instance. - pub fn new(client: &'a Client, url: &'a Url, method: &'a str) -> Self { - Self { - client, - url, - method, - params: None, - timeout: None, - bearer_auth: None, - retry_policy: NoRetry, - } - } -} - -impl<'a, B: Backoff> RpcRequestBuilder<'a, B> { - /// Sets the parameters for the request. - pub fn params(mut self, params: serde_json::Value) -> Self { - self.params = Some(params); - self - } - - /// Sets a timeout for the request. - pub fn timeout(mut self, timeout: Duration) -> Self { - self.timeout = Some(timeout); - self - } - - /// Sets the Bearer token for authorization. - pub fn bearer_auth>(mut self, token: S) -> Self { - self.bearer_auth = Some(token.into()); - self - } - - /// Sets the retry policy for the request. - /// This consumes the current builder and returns a new one with the specified policy. - pub fn retry(self, policy: NB) -> RpcRequestBuilder<'a, NB> { - RpcRequestBuilder { - client: self.client, - url: self.url, - method: self.method, - params: self.params, - timeout: self.timeout, - bearer_auth: self.bearer_auth, - retry_policy: policy, - } - } - - /// Builds and sends the JSON-RPC request. - pub async fn send(self) -> eyre::Result - where - D: DeserializeOwned, - { - // Use provided params or default to an empty JSON array - let params = self.params.unwrap_or_else(|| json!([])); - - let request_body = JsonRequestBody { - jsonrpc: "2.0", - method: self.method, - params, - id: Value::from(uuid::Uuid::new_v4().to_string()), - }; - - // Closure that sends the request and processes the response. - // This will be retried according to the retry policy. - let send_once = || async { - let mut request_builder = self - .client - .post(self.url.clone()) - .header(CONTENT_TYPE, "application/json") - .json(&request_body); - - // Apply timeout if one was provided - if let Some(timeout) = self.timeout { - request_builder = request_builder.timeout(timeout); - } - - // Apply Bearer token if one was provided - if let Some(token) = &self.bearer_auth { - request_builder = request_builder.bearer_auth(token); - } - - // Send the request - let response = request_builder.send().await?.error_for_status()?; - let response_body: JsonResponseBody = response.json().await?; - - match (response_body.result, response_body.error) { - (result, None) => serde_json::from_value(result).map_err(Into::into), - (_, Some(error)) => { - let engine_rpc_error = EngineApiRpcError::from(error); - Err(eyre::Report::new(engine_rpc_error).wrap_err("JSON-RPC request failed")) - } - } - }; - - // Use `backon::Retryable` to execute the closure with the given retry policy. - // If the policy is `NoRetry`, it runs exactly once. - send_once - .retry(self.retry_policy) - .notify(|e, dur| { - warn!("RPC request failed: {e}, retrying in {dur:?}"); - }) - .await - } -} diff --git a/crates/eth-engine/tests/integration.rs b/crates/eth-engine/tests/integration.rs deleted file mode 100644 index 3ae93fe..0000000 --- a/crates/eth-engine/tests/integration.rs +++ /dev/null @@ -1,277 +0,0 @@ -// Copyright 2025 Circle Internet Group, Inc. All rights reserved. -// -// SPDX-License-Identifier: Apache-2.0 -// -// Licensed under the Apache License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. -// You may obtain a copy of the License at -// -// http://www.apache.org/licenses/LICENSE-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// See the License for the specific language governing permissions and -// limitations under the License. - -#![allow( - clippy::arithmetic_side_effects, - clippy::cast_possible_truncation, - clippy::unwrap_used -)] - -use std::path::Path; -use std::time::Duration; - -use jsonrpsee::rpc_params; -use serde_json::{json, Value}; -use tempfile::TempDir; -use url::Url; - -use alloy_rpc_types_engine::JwtSecret; -use reth_node_builder::{NodeBuilder, NodeConfig}; -use reth_tasks::TaskExecutor; - -use arc_consensus_types::Address; -use arc_eth_engine::ipc::engine_ipc::EngineIPC; -use arc_eth_engine::retry::NoRetry; -use arc_eth_engine::rpc::EngineApiRpcError; -use arc_eth_engine::{engine::Engine, rpc::engine_rpc::EngineRpc}; -use arc_evm_node::node::{ArcNode, ArcRpcConfig}; -use arc_evm_node::ARC_RPC_MAX_BATCH_ENTRIES_DEFAULT; -use arc_execution_config::addresses_denylist::AddressesDenylistConfig; -use arc_execution_config::chainspec::ArcChainSpec; -use arc_execution_config::chainspec::LOCAL_DEV; -use arc_execution_txpool::InvalidTxListConfig; - -/// Common test suite for engine implementations -async fn test_engine_common(engine: &Engine, initial_block_number: &str) { - // Test getting chain ID - let chain_id = engine - .eth - .get_chain_id() - .await - .expect("Failed to get chain ID"); - assert_eq!(chain_id, "0x539"); // 1337 in hex - - // Test getting genesis block - let genesis_block = engine - .eth - .get_genesis_block() - .await - .expect("Failed to get genesis block"); - assert!( - !genesis_block.block_hash.is_zero(), - "Genesis hash should not be zero" - ); - - // Test getting a block - let block = engine - .eth - .get_block_by_number(initial_block_number) - .await - .expect("Failed to get block"); - assert!(block.is_some(), "Block should exist"); - - // Test get active validator set - let validator_set = engine - .eth - .get_active_validator_set(0) - .await - .expect("Failed to get active validator set"); - assert!( - !validator_set.validators.is_empty(), - "Validator set should not be empty" - ); - - // Test get consensus params - let consensus_params = engine - .eth - .get_consensus_params(0) - .await - .expect("Failed to get consensus params"); - assert!( - consensus_params.timeouts().propose > Duration::ZERO, - "Consensus params should have valid timeout_propose" - ); - - // Test exchange capabilities - let capabilities = engine.check_capabilities().await; - assert!( - capabilities.is_ok(), - "Failed to check capabilities: {:?}", - capabilities - ); - - // Test generate new block - let fee_recipient = Address::repeat_byte(0xBE); - let block = engine - .generate_block(&block.unwrap(), Engine::timestamp_now() + 1, &fee_recipient) - .await - .expect("Failed to generate new block"); - assert!(block.payload_inner.payload_inner.block_hash.len() == 32); - - // Test notify new block - let status = engine - .notify_new_block(&block, Vec::new()) - .await - .expect("Failed to notify new block"); - assert!( - status.status.is_valid(), - "Block validation failed: {:?}", - status - ); - - // Test set latest forkchoice state - let head_block_hash = engine - .set_latest_forkchoice_state(block.payload_inner.payload_inner.block_hash) - .await - .expect("Failed to set latest forkchoice state"); - assert!(head_block_hash.len() == 32); -} - -#[tokio::test] -async fn test_engine() { - let executor = TaskExecutor::test(); - - let chain_spec = LOCAL_DEV.clone(); - - let temp_dir = TempDir::new().expect("Failed to create temp dir"); - let ipc_path = temp_dir.path().join("reth.ipc"); - let auth_ipc_path = temp_dir.path().join("auth.ipc"); - let jwt_path = temp_dir.path().join("jwtsecret"); - JwtSecret::try_create_random(jwt_path.as_path()).expect("Failed to create JWT secret"); - - // Create node config with ArcChainSpec type - let mut node_config: NodeConfig = NodeConfig::new(chain_spec); - node_config.rpc.ipcpath = ipc_path.to_string_lossy().to_string(); - node_config.rpc.auth_ipc = true; - node_config.rpc.auth_ipc_path = auth_ipc_path.to_string_lossy().to_string(); - node_config.rpc.http = true; - node_config.rpc.ws = false; - node_config.rpc.auth_jwtsecret = Some(jwt_path.clone()); - let node_config = node_config.set_dev(true); - - // Build and start the reth node with Arc types - let arc_node = ArcNode::new( - ArcRpcConfig::default(), - InvalidTxListConfig::default(), - AddressesDenylistConfig::default(), - None, - true, - true, - false, - 160 * 1024 * 1024, - ARC_RPC_MAX_BATCH_ENTRIES_DEFAULT, - std::time::Duration::from_secs(0), // disable rebroadcast in integration tests - ); - let node_handle = NodeBuilder::new(node_config) - .testing_node(executor) - .node(arc_node) - .launch() - .await - .expect("Failed to launch reth node"); - - // Spawn the node in a background task - let node_task = tokio::spawn(async move { - println!("Node task started, waiting for exit..."); - node_handle - .node_exit_future - .await - .expect("Node exited unexpectedly"); - }); - - // Test IPC engine - { - let engine = Engine::new_ipc(auth_ipc_path.to_str().unwrap(), ipc_path.to_str().unwrap()) - .await - .expect("Failed to connect to IPC socket"); - - // Configure Osaka activation from the localdev chainspec (chain_id = 1337) - engine.set_osaka_from_chain_id(1337); - - // Test transaction pool status (IPC-specific test) - let txpool_status = engine - .eth - .txpool_status() - .await - .expect("Failed to get txpool status"); - assert_eq!(txpool_status.pending, 0); - assert_eq!(txpool_status.queued, 0); - - // Test transaction pool inspect (IPC-specific test) - let txpool_inspect = engine - .eth - .txpool_inspect() - .await - .expect("Failed to get txpool inspect"); - assert!(txpool_inspect.pending.is_empty()); - assert!(txpool_inspect.queued.is_empty()); - - // Run common engine tests - test_engine_common(&engine, "0x0").await; - - // Test IPC error handling - let ipc_client = - EngineIPC::new_with_timeout(auth_ipc_path.to_str().unwrap(), Duration::from_secs(5)) - .await - .expect("Failed to build IPC client for error test"); - let params = rpc_params!(1u64); - let err = ipc_client - .rpc_request::( - "engine_newPayloadV4", - params, - Duration::from_secs(5), - NoRetry, - ) - .await - .expect_err("malformed IPC payload should error"); - assert!( - err.downcast_ref::().is_some(), - "IPC error should downcast to EngineApiRpcError" - ); - } - - // Test RPC engine - { - let engine = Engine::new_rpc( - Url::parse("http://localhost:8551").unwrap(), - Url::parse("http://localhost:8545").unwrap(), - None, - jwt_path.to_str().unwrap(), - ) - .await - .expect("Failed to create RPC engine client"); - - // Configure Osaka activation from the localdev chainspec (chain_id = 1337) - engine.set_osaka_from_chain_id(1337); - - // Run common engine tests - test_engine_common(&engine, "latest").await; - - // Test RPC error handling - let rpc_client = EngineRpc::new( - Url::parse("http://localhost:8551").unwrap(), - Path::new(jwt_path.to_str().unwrap()), - ) - .expect("Failed to build RPC client for error test"); - - let params = json!(1u64); - let err = rpc_client - .rpc_request::( - "engine_newPayloadV4", - params, - Duration::from_secs(5), - NoRetry, - ) - .await - .expect_err("malformed RPC payload should error"); - assert!( - err.downcast_ref::().is_some(), - "RPC error should downcast to EngineApiRpcError" - ); - } - - // Shutdown the node at the end of the test - node_task.abort(); -} diff --git a/crates/evm-node/Cargo.toml b/crates/evm-node/Cargo.toml deleted file mode 100644 index becb2d7..0000000 --- a/crates/evm-node/Cargo.toml +++ /dev/null @@ -1,69 +0,0 @@ -[package] -name = "arc-evm-node" -version.workspace = true -edition.workspace = true -readme.workspace = true -license.workspace = true -exclude.workspace = true -rust-version.workspace = true -publish.workspace = true -repository.workspace = true - -[features] -arbitrary = ["alloy-rpc-types-engine/arbitrary"] -integration = ["arc-evm/integration"] - -[dependencies] -alloy-consensus = { workspace = true, features = ["serde"] } -alloy-network.workspace = true -alloy-primitives.workspace = true -alloy-rpc-types-engine.workspace = true -alloy-rpc-types-eth.workspace = true -arc-consensus-types.workspace = true -arc-evm.workspace = true -arc-execution-config.workspace = true -arc-execution-payload.workspace = true -arc-execution-txpool.workspace = true -arc-execution-validation.workspace = true -arc-version.workspace = true -async-trait.workspace = true -backon.workspace = true -eyre.workspace = true -jsonrpsee.workspace = true -reqwest.workspace = true -reth-chainspec.workspace = true -reth-engine-primitives.workspace = true -reth-ethereum = { workspace = true, features = ["node-api", "node", "provider", "network", "evm", "pool", "cli", "js-tracer"] } -reth-ethereum-engine-primitives.workspace = true -reth-ethereum-payload-builder.workspace = true -reth-ethereum-primitives.workspace = true -reth-evm.workspace = true -reth-network.workspace = true -reth-node-api.workspace = true -reth-node-builder.workspace = true -reth-payload-primitives.workspace = true -reth-primitives-traits.workspace = true -reth-provider.workspace = true -reth-rpc.workspace = true -reth-rpc-api.workspace = true -reth-rpc-builder.workspace = true -reth-rpc-eth-api.workspace = true -reth-rpc-eth-types.workspace = true -reth-rpc-server-types.workspace = true -reth-tracing.workspace = true -reth-transaction-pool.workspace = true -revm.workspace = true -serde = { workspace = true, features = ["derive"] } -serde_json.workspace = true -serde_with.workspace = true -thiserror.workspace = true -tokio.workspace = true -tower.workspace = true -tracing.workspace = true - -[dev-dependencies] -arc-execution-config = { path = "../execution-config", features = ["test-utils"] } -revm-primitives.workspace = true - -[lints] -workspace = true diff --git a/crates/evm-node/src/engine.rs b/crates/evm-node/src/engine.rs deleted file mode 100644 index 16be2c0..0000000 --- a/crates/evm-node/src/engine.rs +++ /dev/null @@ -1,322 +0,0 @@ -// Copyright 2025 Circle Internet Group, Inc. All rights reserved. -// -// SPDX-License-Identifier: Apache-2.0 -// -// Licensed under the Apache License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. -// You may obtain a copy of the License at -// -// http://www.apache.org/licenses/LICENSE-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// See the License for the specific language governing permissions and -// limitations under the License. - -//! Arc validator -//! fork from https://github.com/paradigmxyz/reth/blob/main/crates/ethereum/node/src/engine.rs -//! - customize validate_payload_attributes_against_header to relax the timestamp constraint. - -use alloy_consensus::BlockHeader; -use alloy_rpc_types_engine::ExecutionData; -pub use alloy_rpc_types_engine::{ - ExecutionPayloadEnvelopeV2, ExecutionPayloadEnvelopeV3, ExecutionPayloadEnvelopeV4, - ExecutionPayloadV1, -}; -use reth_chainspec::{EthChainSpec, EthereumHardforks}; -use reth_engine_primitives::{EngineApiValidator, PayloadValidator}; -use reth_ethereum_engine_primitives::EthPayloadAttributes; -use reth_ethereum_payload_builder::EthereumExecutionPayloadValidator; -use reth_ethereum_primitives::Block; -use reth_node_api::PayloadTypes; -use reth_payload_primitives::InvalidPayloadAttributesError; -use reth_payload_primitives::PayloadAttributes; -use reth_payload_primitives::{ - validate_execution_requests, validate_version_specific_fields, EngineApiMessageVersion, - EngineObjectValidationError, NewPayloadError, PayloadOrAttributes, -}; -use reth_primitives_traits::Block as BlockTr; -use reth_primitives_traits::RecoveredBlock; -use std::sync::Arc; - -#[derive(Debug, Clone)] -pub struct ArcEngineValidator { - inner: EthereumExecutionPayloadValidator, -} - -impl ArcEngineValidator { - /// Instantiates a new validator. - pub const fn new(chain_spec: Arc) -> Self { - Self { - inner: EthereumExecutionPayloadValidator::new(chain_spec), - } - } - - /// Returns the chain spec used by the validator. - #[inline] - fn chain_spec(&self) -> &ChainSpec { - self.inner.chain_spec() - } -} - -/// Type that validates an [`ExecutionPayload`]. -impl PayloadValidator for ArcEngineValidator -where - ChainSpec: EthChainSpec + EthereumHardforks + 'static, - Types: PayloadTypes, - Types::PayloadAttributes: PayloadAttributes, -{ - type Block = Block; - - fn convert_payload_to_block( - &self, - payload: ExecutionData, - ) -> Result, NewPayloadError> { - if payload - .sidecar - .versioned_hashes() - .is_some_and(|v| !v.is_empty()) - || payload.payload.blob_gas_used().is_some_and(|g| g > 0) - { - return Err(NewPayloadError::Other( - "Blob transactions are not supported".into(), - )); - } - - self.inner - .ensure_well_formed_payload(payload) - .map_err(Into::into) - } - - fn ensure_well_formed_payload( - &self, - payload: ExecutionData, - ) -> Result, NewPayloadError> { - let sealed_block = - >::convert_payload_to_block(self, payload)?; - sealed_block - .try_recover() - .map_err(|e| NewPayloadError::Other(e.into())) - } - - fn validate_payload_attributes_against_header( - &self, - attr: &Types::PayloadAttributes, - header: &::Header, - ) -> Result<(), InvalidPayloadAttributesError> { - // NOTE(romac): Here we relax the check to allow the payload attributes timestamp - // to be greater than or equal to the header's timestamp. - if attr.timestamp() < header.timestamp() { - return Err(InvalidPayloadAttributesError::InvalidTimestamp); - } - Ok(()) - } -} - -/// Type that validates the payloads processed by the engine. -impl EngineApiValidator for ArcEngineValidator -where - ChainSpec: EthChainSpec + EthereumHardforks + 'static, - Types: PayloadTypes, -{ - fn validate_version_specific_fields( - &self, - version: EngineApiMessageVersion, - payload_or_attrs: PayloadOrAttributes<'_, Types::ExecutionData, EthPayloadAttributes>, - ) -> Result<(), EngineObjectValidationError> { - payload_or_attrs - .execution_requests() - .map(|requests| validate_execution_requests(requests)) - .transpose()?; - - validate_version_specific_fields(self.chain_spec(), version, payload_or_attrs) - } - - fn ensure_well_formed_attributes( - &self, - version: EngineApiMessageVersion, - attributes: &EthPayloadAttributes, - ) -> Result<(), EngineObjectValidationError> { - validate_version_specific_fields( - self.chain_spec(), - version, - PayloadOrAttributes::::PayloadAttributes( - attributes, - ), - ) - } -} - -#[cfg(test)] -mod tests { - use super::*; - use alloy_primitives::{hex, Address, Bytes, B256, U256}; - use alloy_rpc_types_engine::{ - CancunPayloadFields, ExecutionPayload, ExecutionPayloadSidecar, ExecutionPayloadV1, - ExecutionPayloadV2, ExecutionPayloadV3, PraguePayloadFields, - }; - use reth_chainspec::ChainSpecBuilder; - use reth_ethereum::primitives::Header; - use reth_ethereum_engine_primitives::{EthEngineTypes, EthPayloadTypes}; - use revm_primitives::hex::FromHex; - - fn create_validator() -> ArcEngineValidator { - let chain_spec = Arc::new(ChainSpecBuilder::mainnet().build()); - ArcEngineValidator::new(chain_spec) - } - - fn create_test_payload_v3(blob_gas_used: u64) -> ExecutionPayloadV3 { - ExecutionPayloadV3 { - payload_inner: ExecutionPayloadV2 { - payload_inner: ExecutionPayloadV1 { - base_fee_per_gas: U256::from(7u64), - block_number: 0xa946u64, - block_hash: hex!("a5ddd3f286f429458a39cafc13ffe89295a7efa8eb363cf89a1a4887dbcf272b").into(), - logs_bloom: hex!("00200004000000000000000080000000000200000000000000000000000000000000200000000000000000000000000000000000800000000200000000000000000000000000000000000008000000200000000000000000000001000000000000000000000000000000800000000000000000000100000000000030000000000000000040000000000000000000000000000000000800080080404000000000000008000000000008200000000000200000000000000000000000000000000000000002000000000000000000000000000000000000001000000000000000000000000000000000000000000000000000000000000100000000000000000000").into(), - extra_data: hex!("d883010d03846765746888676f312e32312e31856c696e7578").into(), - gas_limit: 0x1c9c380, - gas_used: 0x1f4a9, - timestamp: 0x651f35b8, - fee_recipient: hex!("f97e180c050e5ab072211ad2c213eb5aee4df134").into(), - parent_hash: hex!("a5ddd3f286f429458a39cafc13ffe89295a7efa8eb363cf89a1a4887dbcf272b").into(), - prev_randao: hex!("a5ddd3f286f429458a39cafc13ffe89295a7efa8eb363cf89a1a4887dbcf272b").into(), - receipts_root: hex!("a5ddd3f286f429458a39cafc13ffe89295a7efa8eb363cf89a1a4887dbcf272b").into(), - state_root: hex!("a5ddd3f286f429458a39cafc13ffe89295a7efa8eb363cf89a1a4887dbcf272b").into(), - transactions: vec![Bytes::from_hex("0x1234").unwrap()], - }, - withdrawals: vec![], - }, - blob_gas_used, - excess_blob_gas: 0x0, - } - } - - #[test] - fn validate_payload_attributes_against_header_valid() { - let validator = create_validator(); - - let parent_header = Header { - timestamp: 1000, - ..Default::default() - }; - - // Equal timestamp - let attributes_eq = EthPayloadAttributes { - timestamp: 1000, - prev_randao: B256::ZERO, - suggested_fee_recipient: Address::ZERO, - withdrawals: None, - parent_beacon_block_root: None, - }; - - assert!( as PayloadValidator>::validate_payload_attributes_against_header( - &validator, - &attributes_eq, - &parent_header, - ).is_ok()); - - // Greater timestamp - let attributes_gt = EthPayloadAttributes { - timestamp: 1001, - prev_randao: B256::ZERO, - suggested_fee_recipient: Address::ZERO, - withdrawals: None, - parent_beacon_block_root: None, - }; - - assert!( as PayloadValidator>::validate_payload_attributes_against_header( - &validator, - &attributes_gt, - &parent_header, - ).is_ok()); - } - - #[test] - fn validate_payload_attributes_against_header_invalid() { - let validator = create_validator(); - - let parent_header = Header { - timestamp: 1000, - ..Default::default() - }; - - let attributes = EthPayloadAttributes { - timestamp: 999, - prev_randao: B256::ZERO, - suggested_fee_recipient: Address::ZERO, - withdrawals: None, - parent_beacon_block_root: None, - }; - - assert!(matches!( as PayloadValidator>::validate_payload_attributes_against_header( - &validator, - &attributes, - &parent_header, - ), Err(InvalidPayloadAttributesError::InvalidTimestamp))); - } - - #[test] - fn ensure_well_formed_payload_rejects_blob_versioned_hashes() { - let validator = create_validator(); - let new_payload = create_test_payload_v3(0); - - let result = - >::ensure_well_formed_payload( - &validator, - ExecutionData { - payload: ExecutionPayload::V3(new_payload), - sidecar: ExecutionPayloadSidecar::v4( - CancunPayloadFields::new( - hex!( - "a5ddd3f286f429458a39cafc13ffe89295a7efa8eb363cf89a1a4887dbcf272b" - ) - .into(), - vec![B256::from(hex!( - "a5ddd3f286f429458a39cafc13ffe89295a7efa8eb363cf89a1a4887dbcf272b" - ))], - ), - PraguePayloadFields::default(), - ), - }, - ); - - match result { - Err(NewPayloadError::Other(msg)) => { - assert_eq!(msg.to_string(), "Blob transactions are not supported"); - } - _ => panic!("Unexpected result: {:?}", result), - } - } - - #[test] - fn ensure_well_formed_payload_rejects_blob_gas_used() { - let validator = create_validator(); - let new_payload = create_test_payload_v3(10); - - let result = - >::ensure_well_formed_payload( - &validator, - ExecutionData { - payload: ExecutionPayload::V3(new_payload), - sidecar: ExecutionPayloadSidecar::v4( - CancunPayloadFields::new( - hex!( - "a5ddd3f286f429458a39cafc13ffe89295a7efa8eb363cf89a1a4887dbcf272b" - ) - .into(), - vec![], - ), - PraguePayloadFields::default(), - ), - }, - ); - - match result { - Err(NewPayloadError::Other(msg)) => { - assert_eq!(msg.to_string(), "Blob transactions are not supported"); - } - _ => panic!("Unexpected result: {:?}", result), - } - } -} diff --git a/crates/evm-node/src/lib.rs b/crates/evm-node/src/lib.rs deleted file mode 100644 index 8a90742..0000000 --- a/crates/evm-node/src/lib.rs +++ /dev/null @@ -1,31 +0,0 @@ -// Copyright 2026 Circle Internet Group, Inc. All rights reserved. -// -// SPDX-License-Identifier: Apache-2.0 -// -// Licensed under the Apache License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. -// You may obtain a copy of the License at -// -// http://www.apache.org/licenses/LICENSE-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// See the License for the specific language governing permissions and -// limitations under the License. - -//! Arc EVM Node -//! -//! Implements the core EVM traits that bind all of the execution layer -//! functionality together. - -pub mod engine; -pub mod node; -pub mod payload; -pub mod rebroadcast; -pub mod rpc; -pub mod rpc_middleware; - -// Re-export commonly used types -pub use engine::ArcEngineValidator; -pub use rpc_middleware::{ArcRpcLayer, ARC_RPC_MAX_BATCH_ENTRIES_DEFAULT}; diff --git a/crates/evm-node/src/node.rs b/crates/evm-node/src/node.rs deleted file mode 100644 index 8bb65fc..0000000 --- a/crates/evm-node/src/node.rs +++ /dev/null @@ -1,872 +0,0 @@ -// Copyright 2025 Circle Internet Group, Inc. All rights reserved. -// -// SPDX-License-Identifier: Apache-2.0 -// -// Licensed under the Apache License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. -// You may obtain a copy of the License at -// -// http://www.apache.org/licenses/LICENSE-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// See the License for the specific language governing permissions and -// limitations under the License. - -//! Arc Node types config. -//! Fork from https://github.com/paradigmxyz/reth/blob/v1.7.0/crates/ethereum/node/src/node.rs -//! Reference to EthereumNode and add our customization -//! - inject the EVM customization in ArcExecutorBuilder -//! - inject our consensus ArcConsensus in ArcConsensusBuilder -//! - inject ArcEngineValidatorBuilder in ArcEngineValidatorBuilder - -use crate::payload::ArcLocalPayloadAttributesBuilder; -use alloy_network::Ethereum; -use alloy_rpc_types_engine::ExecutionData; -use arc_evm::{ArcEvmConfig, ArcEvmFactory}; -use arc_execution_validation::ArcConsensus; -use reth_chainspec::{EthereumHardforks, Hardforks}; -use reth_engine_primitives::EngineTypes; -use reth_ethereum::{node::EthEngineTypes, node::EthEvmConfig}; -use reth_ethereum_engine_primitives::{ - EthBuiltPayload, EthPayloadAttributes, EthPayloadBuilderAttributes, -}; -use reth_ethereum_primitives::EthPrimitives; -use reth_evm::{ConfigureEvm, EvmFactory, EvmFactoryFor, NextBlockEnvAttributes}; -use reth_network::{primitives::BasicNetworkPrimitives, NetworkHandle, PeersInfo}; -use reth_node_api::{ - AddOnsContext, FullNodeComponents, HeaderTy, NodeAddOns, PayloadAttributesBuilder, - PrimitivesTy, TxTy, -}; -use reth_node_builder::{ - components::{ - BasicPayloadServiceBuilder, ComponentsBuilder, ConsensusBuilder, ExecutorBuilder, - NetworkBuilder, - }, - node::{FullNodeTypes, NodeTypes}, - rpc::{ - BasicEngineApiBuilder, BasicEngineValidatorBuilder, EngineApiBuilder, EngineValidatorAddOn, - EngineValidatorBuilder, EthApiBuilder, EthApiCtx, PayloadValidatorBuilder, RethRpcAddOns, - RpcAddOns, RpcHandle, - }, - BuilderContext, DebugNode, Node, NodeAdapter, -}; -use reth_payload_primitives::PayloadTypes; -use reth_provider::{providers::ProviderFactoryBuilder, EthStorage}; -use reth_rpc::{ - eth::core::{EthApiFor, EthRpcConverterFor}, - ValidationApi, -}; -use reth_rpc_api::servers::BlockSubmissionValidationApiServer; -use reth_rpc_builder::{config::RethRpcServerConfig, middleware::RethRpcMiddleware}; -use reth_rpc_eth_api::{ - helpers::{ - config::{EthConfigApiServer, EthConfigHandler}, - pending_block::BuildPendingEnv, - }, - RpcConvert, RpcTypes, SignableTxRequest, -}; -use reth_rpc_eth_types::{error::FromEvmError, EthApiError}; -use reth_rpc_server_types::RethRpcModule; -use reth_tracing::tracing::{info, warn}; -use reth_transaction_pool::{PoolPooledTx, PoolTransaction, TransactionPool}; -use revm::context::TxEnv; -use std::{default::Default, marker::PhantomData, sync::Arc}; - -use arc_execution_config::addresses_denylist::AddressesDenylistConfig; -use arc_execution_config::chainspec::ArcChainSpec; -use arc_execution_payload::payload::ArcNetworkPayloadBuilderBuilder; -use arc_execution_txpool::{ArcPoolBuilder, InvalidTxList, InvalidTxListConfig}; - -// FIXME use the ethereum chain spec temporary, we need to define Arc chain spec -// original traits for ChainSpec in this file `Hardforks + EthereumHardforks + EthExecutorSpec` - -use crate::rpc_middleware::{ArcRpcLayer, ARC_RPC_MAX_BATCH_ENTRIES_DEFAULT}; -use crate::ArcEngineValidator; - -/// Type configuration for a regular Arc node. -#[derive(Debug, Clone)] -#[non_exhaustive] -pub struct ArcNode { - pub rpc_cfg: ArcRpcConfig, - pub invalid_tx_list_cfg: InvalidTxListConfig, - pub addresses_denylist_config: AddressesDenylistConfig, - /// Custom payload builder loop time limit in milliseconds. When set, used instead of Reth's builder.deadline. - pub payload_builder_deadline_ms: Option, - /// When true, `on_missing_payload` waits for the in-flight build instead of - /// racing an empty block. - pub wait_for_payload: bool, - /// When true (default), pending-tx RPCs are restricted - /// (`eth_subscribe("newPendingTransactions")`, `eth_newPendingTransactionFilter`, - /// `eth_getBlockByNumber("pending")`, and `eth_getTransactionBySenderAndNonce`). - /// When false, all requests are forwarded. - /// CLI users opt out of the default via `--arc.expose-pending-txs`. - /// `--public-api` also forces this to `true` (and conflicts with `--arc.expose-pending-txs`). - pub filter_pending_txs: bool, - /// When true, raw transaction submission RPCs accept pre-EIP-155 - /// (replay-unprotected) transactions. Defaults to false, matching Geth. - /// P2P-received transactions and transactions included in blocks by other - /// validators are unaffected. - pub allow_unprotected_txs: bool, - /// Maximum batch response size in bytes, mirrors `--rpc.max-response-size`. - pub max_response_body_size: u32, - /// Maximum number of entries permitted in a JSON-RPC batch request. - pub max_batch_entries: usize, - /// Interval between tx rebroadcast rounds. Zero disables rebroadcast. - pub rebroadcast_interval: std::time::Duration, -} - -impl Default for ArcNode { - fn default() -> Self { - Self { - rpc_cfg: ArcRpcConfig::default(), - invalid_tx_list_cfg: InvalidTxListConfig::default(), - addresses_denylist_config: AddressesDenylistConfig::default(), - payload_builder_deadline_ms: None, - wait_for_payload: true, - filter_pending_txs: true, - allow_unprotected_txs: false, - max_response_body_size: 160 * 1024 * 1024, - max_batch_entries: ARC_RPC_MAX_BATCH_ENTRIES_DEFAULT, - rebroadcast_interval: crate::rebroadcast::DEFAULT_REBROADCAST_INTERVAL, - } - } -} - -impl ArcNode { - /// Creates a new `ArcNode`. - #[allow(clippy::too_many_arguments)] - pub fn new( - rpc_cfg: ArcRpcConfig, - invalid_tx_list_cfg: InvalidTxListConfig, - addresses_denylist_config: AddressesDenylistConfig, - payload_builder_deadline_ms: Option, - wait_for_payload: bool, - filter_pending_txs: bool, - allow_unprotected_txs: bool, - max_response_body_size: u32, - max_batch_entries: usize, - rebroadcast_interval: std::time::Duration, - ) -> Self { - Self { - rpc_cfg, - invalid_tx_list_cfg, - addresses_denylist_config, - payload_builder_deadline_ms, - wait_for_payload, - filter_pending_txs, - allow_unprotected_txs, - max_response_body_size, - max_batch_entries, - rebroadcast_interval, - } - } - - /// Returns a [`ComponentsBuilder`] configured for a regular Arc node. - pub fn components( - invalid_tx_list_cfg: &InvalidTxListConfig, - addresses_denylist_config: &AddressesDenylistConfig, - payload_builder_deadline_ms: Option, - wait_for_payload: bool, - rebroadcast_interval: std::time::Duration, - ) -> ComponentsBuilder< - Node, - ArcPoolBuilder, - BasicPayloadServiceBuilder, - ArcNetworkBuilder, - ArcExecutorBuilder, - ArcConsensusBuilder, - > - where - Node: FullNodeTypes>, - ::Payload: PayloadTypes< - BuiltPayload = EthBuiltPayload, - PayloadAttributes = EthPayloadAttributes, - PayloadBuilderAttributes = EthPayloadBuilderAttributes, - >, - { - let invalid_tx_list_opt = if invalid_tx_list_cfg.enabled { - info!( - capacity = invalid_tx_list_cfg.capacity, - "Invalid tx list is enabled; initializing" - ); - Some(InvalidTxList::new(invalid_tx_list_cfg.capacity)) - } else { - info!("Invalid tx list is disabled"); - None - }; - - let pb_builder = ArcNetworkPayloadBuilderBuilder::new( - invalid_tx_list_opt.clone(), - payload_builder_deadline_ms, - wait_for_payload, - ); - ComponentsBuilder::default() - .node_types::() - .pool(ArcPoolBuilder::new( - invalid_tx_list_opt, - addresses_denylist_config.clone(), - )) - .executor(ArcExecutorBuilder::default()) - .payload(BasicPayloadServiceBuilder::new(pb_builder)) - .network(ArcNetworkBuilder::default().with_rebroadcast_interval(rebroadcast_interval)) - .consensus(ArcConsensusBuilder::default()) - } - - /// Instantiates the [`ProviderFactoryBuilder`] for an Arc node. - pub fn provider_factory_builder() -> ProviderFactoryBuilder { - ProviderFactoryBuilder::default() - } -} - -impl NodeTypes for ArcNode { - type Primitives = EthPrimitives; - type ChainSpec = ArcChainSpec; - type Storage = EthStorage; - type Payload = EthEngineTypes; -} - -/// Builds [`EthApi`](reth_rpc::EthApi) for Arc. -#[derive(Debug)] -pub struct ArcEthApiBuilder(PhantomData); - -impl Default for ArcEthApiBuilder { - fn default() -> Self { - Self(Default::default()) - } -} - -impl EthApiBuilder for ArcEthApiBuilder -where - N: FullNodeComponents< - Types: NodeTypes, - Evm: ConfigureEvm>>, - >, - NetworkT: RpcTypes>>, - EthRpcConverterFor: RpcConvert< - Primitives = PrimitivesTy, - Error = EthApiError, - Network = NetworkT, - Evm = N::Evm, - >, - EthApiError: FromEvmError, -{ - type EthApi = EthApiFor; - - async fn build_eth_api(self, ctx: EthApiCtx<'_, N>) -> eyre::Result { - Ok(ctx - .eth_api_builder() - .map_converter(|r| r.with_network()) - .build()) - } -} - -/// Configuration for the ARC RPC namespace. -#[derive(Debug, Clone, Default)] -pub struct ArcRpcConfig { - /// Whether the ARC namespace is enabled. - pub enabled: bool, - /// Optional upstream `malachite-app` RPC URL. - pub upstream_url: Option, -} - -impl ArcRpcConfig { - pub fn new(enabled: bool, upstream_url: Option) -> Self { - Self { - enabled, - upstream_url, - } - } -} - -/// Add-ons for Arc -#[derive(Debug)] -pub struct ArcAddOns< - N: FullNodeComponents, - EthB: EthApiBuilder, - PVB, - EB = BasicEngineApiBuilder, - EVB = BasicEngineValidatorBuilder, - RpcMiddleware = ArcRpcLayer, -> { - inner: RpcAddOns, - arc_rpc: ArcRpcConfig, -} - -impl ArcAddOns -where - N: FullNodeComponents, - EthB: EthApiBuilder, -{ - /// Creates a new instance from the inner `RpcAddOns`. - pub fn new( - inner: RpcAddOns, - arc_rpc: ArcRpcConfig, - ) -> Self { - Self { inner, arc_rpc } - } -} - -impl Default for ArcAddOns -where - N: FullNodeComponents< - Types: NodeTypes< - ChainSpec = ArcChainSpec, - Payload: EngineTypes - + PayloadTypes, - Primitives = EthPrimitives, - >, - >, - ArcEthApiBuilder: EthApiBuilder, -{ - fn default() -> Self { - let addons = RpcAddOns::new( - ArcEthApiBuilder::default(), - ArcEngineValidatorBuilder::default(), - BasicEngineApiBuilder::default(), - BasicEngineValidatorBuilder::default(), - Default::default(), - ); - Self::new(addons, ArcRpcConfig::default()) - } -} - -impl ArcAddOns -where - N: FullNodeComponents, - EthB: EthApiBuilder, -{ - /// Replace the engine API builder. - pub fn with_engine_api( - self, - engine_api_builder: T, - ) -> ArcAddOns - where - T: Send, - { - let Self { inner, arc_rpc } = self; - ArcAddOns::new(inner.with_engine_api(engine_api_builder), arc_rpc) - } - - /// Replace the payload validator builder. - pub fn with_payload_validator( - self, - payload_validator_builder: T, - ) -> ArcAddOns { - let Self { inner, arc_rpc } = self; - ArcAddOns::new( - inner.with_payload_validator(payload_validator_builder), - arc_rpc, - ) - } - - /// Sets rpc middleware - pub fn with_rpc_middleware(self, rpc_middleware: T) -> ArcAddOns - where - T: Send, - { - let Self { inner, arc_rpc } = self; - ArcAddOns::new(inner.with_rpc_middleware(rpc_middleware), arc_rpc) - } - - /// Sets the tokio runtime for the RPC servers. - /// - /// Caution: This runtime must not be created from within asynchronous context. - pub fn with_tokio_runtime(self, tokio_runtime: Option) -> Self { - let Self { inner, arc_rpc } = self; - Self::new(inner.with_tokio_runtime(tokio_runtime), arc_rpc) - } - - /// Replace entire ARC RPC config. - pub fn with_arc_rpc_config(mut self, cfg: ArcRpcConfig) -> Self { - self.arc_rpc = cfg; - self - } -} - -impl NodeAddOns - for ArcAddOns -where - N: FullNodeComponents< - Types: NodeTypes< - ChainSpec = ArcChainSpec, - Primitives = EthPrimitives, - Payload: EngineTypes, - >, - Evm: ConfigureEvm, - >, - EthB: EthApiBuilder, - PVB: Send, - EB: EngineApiBuilder, - EVB: EngineValidatorBuilder, - EthApiError: FromEvmError, - EvmFactoryFor: EvmFactory, - RpcMiddleware: RethRpcMiddleware, -{ - type Handle = RpcHandle; - - async fn launch_add_ons( - self, - ctx: reth_node_api::AddOnsContext<'_, N>, - ) -> eyre::Result { - let validation_api = ValidationApi::<_, _, ::Payload>::new( - ctx.node.provider().clone(), - Arc::new(ctx.node.consensus().clone()), - ctx.node.evm_config().clone(), - ctx.config.rpc.flashbots_config(), - Box::new(ctx.node.task_executor().clone()), - Arc::new(ArcEngineValidator::new(ctx.config.chain.clone())), - ); - - let eth_config = - EthConfigHandler::new(ctx.node.provider().clone(), ctx.node.evm_config().clone()); - - self.inner - .launch_add_ons_with(ctx, move |container| { - container.modules.merge_if_module_configured( - RethRpcModule::Flashbots, - validation_api.into_rpc(), - )?; - - container - .modules - .merge_if_module_configured(RethRpcModule::Eth, eth_config.into_rpc())?; - - if self.arc_rpc.enabled { - if let Ok(arc_module) = - crate::rpc::arc::build_arc_rpc_module(self.arc_rpc.upstream_url.clone()) - { - container.modules.merge_configured(arc_module)?; - } - } - - Ok(()) - }) - .await - } -} - -impl RethRpcAddOns for ArcAddOns -where - N: FullNodeComponents< - Types: NodeTypes< - ChainSpec = ArcChainSpec, - Primitives = EthPrimitives, - Payload: EngineTypes, - >, - Evm: ConfigureEvm, - >, - EthB: EthApiBuilder, - PVB: PayloadValidatorBuilder, - EB: EngineApiBuilder, - EVB: EngineValidatorBuilder, - EthApiError: FromEvmError, - EvmFactoryFor: EvmFactory, -{ - type EthApi = EthB::EthApi; - - fn hooks_mut(&mut self) -> &mut reth_node_builder::rpc::RpcHooks { - self.inner.hooks_mut() - } -} - -impl EngineValidatorAddOn - for ArcAddOns -where - N: FullNodeComponents< - Types: NodeTypes< - ChainSpec = ArcChainSpec, - Primitives = EthPrimitives, - Payload: EngineTypes, - >, - Evm: ConfigureEvm, - >, - EthB: EthApiBuilder, - PVB: Send, - EB: EngineApiBuilder, - EVB: EngineValidatorBuilder, - EthApiError: FromEvmError, - EvmFactoryFor: EvmFactory, - RpcMiddleware: Send, -{ - type ValidatorBuilder = EVB; - - fn engine_validator_builder(&self) -> Self::ValidatorBuilder { - self.inner.engine_validator_builder() - } -} - -impl Node for ArcNode -where - N: FullNodeTypes, -{ - type ComponentsBuilder = ComponentsBuilder< - N, - ArcPoolBuilder, - BasicPayloadServiceBuilder, - ArcNetworkBuilder, - ArcExecutorBuilder, - ArcConsensusBuilder, - >; - - type AddOns = ArcAddOns< - NodeAdapter, - ArcEthApiBuilder, - ArcEngineValidatorBuilder, - BasicEngineApiBuilder, - BasicEngineValidatorBuilder, - ArcRpcLayer, - >; - - fn components_builder(&self) -> Self::ComponentsBuilder { - Self::components( - &self.invalid_tx_list_cfg, - &self.addresses_denylist_config, - self.payload_builder_deadline_ms, - self.wait_for_payload, - self.rebroadcast_interval, - ) - } - - fn add_ons(&self) -> Self::AddOns { - ArcAddOns::default() - .with_arc_rpc_config(self.rpc_cfg.clone()) - .with_rpc_middleware(ArcRpcLayer::new( - self.filter_pending_txs, - self.allow_unprotected_txs, - self.max_response_body_size as usize, - self.max_batch_entries, - )) - } -} - -impl> DebugNode for ArcNode { - type RpcBlock = alloy_rpc_types_eth::Block; - - fn rpc_to_primitive_block(rpc_block: Self::RpcBlock) -> reth_ethereum_primitives::Block { - rpc_block.into_consensus().convert_transactions() - } - - fn local_payload_attributes_builder( - chain_spec: &Self::ChainSpec, - ) -> impl PayloadAttributesBuilder<::PayloadAttributes> { - ArcLocalPayloadAttributesBuilder::new(Arc::new(chain_spec.clone())) - } -} - -/// A regular Arc evm and executor builder. -#[derive(Clone, Debug, Default)] -#[non_exhaustive] -pub struct ArcExecutorBuilder; - -impl ExecutorBuilder for ArcExecutorBuilder -where - Types: NodeTypes, - Node: FullNodeTypes, -{ - type EVM = ArcEvmConfig; - - async fn build_evm(self, ctx: &BuilderContext) -> eyre::Result { - let evm_config = EthEvmConfig::new_with_evm_factory( - ctx.chain_spec().clone(), - ArcEvmFactory::new(ctx.chain_spec().clone()), - ); - Ok(ArcEvmConfig::new(evm_config)) - } -} - -/// Arc network builder with optional tx rebroadcast. -#[derive(Debug, Clone, Copy)] -pub struct ArcNetworkBuilder { - rebroadcast_interval: std::time::Duration, -} - -impl Default for ArcNetworkBuilder { - fn default() -> Self { - Self { - rebroadcast_interval: crate::rebroadcast::DEFAULT_REBROADCAST_INTERVAL, - } - } -} - -impl ArcNetworkBuilder { - pub fn with_rebroadcast_interval(mut self, interval: std::time::Duration) -> Self { - self.rebroadcast_interval = interval; - self - } -} - -impl NetworkBuilder for ArcNetworkBuilder -where - Node: FullNodeTypes>, - Pool: TransactionPool>> - + Unpin - + 'static, -{ - type Network = - NetworkHandle, PoolPooledTx>>; - - async fn build_network( - self, - ctx: &BuilderContext, - pool: Pool, - ) -> eyre::Result { - let network = ctx.network_builder().await?; - let rebroadcast_pool = if self.rebroadcast_interval.is_zero() { - None - } else { - Some(pool.clone()) - }; - let handle = ctx.start_network(network, pool); - info!(target: "reth::cli", enode=%handle.local_node_record(), "P2P networking initialized"); - - if let Some(pool) = rebroadcast_pool { - if let Some(txns_handle) = handle.transactions_handle().await { - let rebroadcaster = crate::rebroadcast::TxRebroadcaster::new( - pool, - txns_handle, - self.rebroadcast_interval, - ); - ctx.task_executor() - .spawn_task(Box::pin(rebroadcaster.run())); - info!( - target: "arc::txpool::rebroadcast", - interval_secs = self.rebroadcast_interval.as_secs(), - "Transaction rebroadcast task started" - ); - } else { - warn!( - target: "arc::txpool::rebroadcast", - "Transaction rebroadcast disabled: no transactions handle available" - ); - } - } - - Ok(handle) - } -} - -/// A basic Arc consensus builder. -#[derive(Debug, Default, Clone, Copy)] -pub struct ArcConsensusBuilder { - // TODO add closure to modify consensus -} - -impl ConsensusBuilder for ArcConsensusBuilder -where - Node: FullNodeTypes>, -{ - type Consensus = Arc::ChainSpec>>; - - async fn build_consensus(self, ctx: &BuilderContext) -> eyre::Result { - Ok(Arc::new(ArcConsensus::new(ctx.chain_spec()))) - } -} - -/// Builder for [`ArcEngineValidator`]. -#[derive(Debug, Default, Clone)] -#[non_exhaustive] -pub struct ArcEngineValidatorBuilder; - -impl PayloadValidatorBuilder for ArcEngineValidatorBuilder -where - Types: NodeTypes< - ChainSpec: Hardforks + EthereumHardforks + Clone + 'static, - Payload: EngineTypes - + PayloadTypes, - Primitives = EthPrimitives, - >, - Node: FullNodeComponents, -{ - type Validator = ArcEngineValidator; - - async fn build(self, ctx: &AddOnsContext<'_, Node>) -> eyre::Result { - Ok(ArcEngineValidator::new(ctx.config.chain.clone())) - } -} - -#[cfg(test)] -mod tests { - use super::*; - - #[test] - fn arc_rpc_config_construction() { - let disabled = ArcRpcConfig::default(); - assert!(!disabled.enabled); - assert!(disabled.upstream_url.is_none()); - - let enabled = ArcRpcConfig::new(true, Some("http://example".into())); - assert!(enabled.enabled); - assert_eq!(enabled.upstream_url.as_deref(), Some("http://example")); - } - - #[test] - fn invalid_tx_list_config_default() { - let cfg = InvalidTxListConfig::default(); - assert!(cfg.enabled); - assert_eq!(cfg.capacity, 100_000); - } - - #[test] - fn invalid_tx_list_config_custom() { - let cfg = InvalidTxListConfig::new(true, 50_000); - assert!(cfg.enabled); - assert_eq!(cfg.capacity, 50_000); - } - - #[test] - fn arc_node_construction_with_invalid_tx_list() { - let rpc_cfg = ArcRpcConfig::default(); - let invalid_tx_list_cfg = InvalidTxListConfig::new(true, 25_000); - let node = ArcNode::new( - rpc_cfg, - invalid_tx_list_cfg, - AddressesDenylistConfig::default(), - None, - true, - true, - false, - 160 * 1024 * 1024, - ARC_RPC_MAX_BATCH_ENTRIES_DEFAULT, - crate::rebroadcast::DEFAULT_REBROADCAST_INTERVAL, - ); - - assert!(!node.rpc_cfg.enabled); - assert!(node.invalid_tx_list_cfg.enabled); - assert_eq!(node.invalid_tx_list_cfg.capacity, 25_000); - assert!(!node.addresses_denylist_config.is_enabled()); - } - - #[test] - fn arc_node_construction_with_addresses_denylist_config() { - use alloy_primitives::{address, b256}; - let addresses_cfg = AddressesDenylistConfig::try_new( - true, - Some(address!("0x3600000000000000000000000000000000000001")), - Some(b256!( - "0x0000000000000000000000000000000000000000000000000000000000000001" - )), - Vec::new(), - ) - .unwrap(); - let node = ArcNode::new( - ArcRpcConfig::default(), - InvalidTxListConfig::default(), - addresses_cfg.clone(), - None, - true, - true, - false, - 160 * 1024 * 1024, - ARC_RPC_MAX_BATCH_ENTRIES_DEFAULT, - crate::rebroadcast::DEFAULT_REBROADCAST_INTERVAL, - ); - assert!(node.addresses_denylist_config.is_enabled()); - if let AddressesDenylistConfig::Enabled { - contract_address, .. - } = &node.addresses_denylist_config - { - assert_eq!( - *contract_address, - address!("0x3600000000000000000000000000000000000001") - ); - } else { - panic!("expected Enabled variant"); - } - } - - #[test] - fn arc_node_default_has_pending_txs_filter_enabled() { - let node = ArcNode::default(); - assert!( - node.filter_pending_txs, - "Default ArcNode should have pending txs filter enabled" - ); - } - - #[test] - fn arc_node_construction_with_pending_tx_filter_disabled() { - let node = ArcNode::new( - ArcRpcConfig::default(), - InvalidTxListConfig::default(), - AddressesDenylistConfig::default(), - None, - true, - false, - false, - 160 * 1024 * 1024, - ARC_RPC_MAX_BATCH_ENTRIES_DEFAULT, - crate::rebroadcast::DEFAULT_REBROADCAST_INTERVAL, - ); - assert!(!node.filter_pending_txs); - } - - #[test] - fn arc_node_default_wait_for_payload_enabled() { - let node = ArcNode::default(); - assert!( - node.wait_for_payload, - "Default ArcNode should have wait_for_payload enabled" - ); - } - - #[test] - fn arc_node_construction_with_wait_for_payload_disabled() { - let node = ArcNode::new( - ArcRpcConfig::default(), - InvalidTxListConfig::default(), - AddressesDenylistConfig::default(), - None, - false, - false, - false, - 160 * 1024 * 1024, - ARC_RPC_MAX_BATCH_ENTRIES_DEFAULT, - crate::rebroadcast::DEFAULT_REBROADCAST_INTERVAL, - ); - assert!(!node.wait_for_payload); - } - - #[test] - fn arc_node_default_rebroadcast_interval() { - let node = ArcNode::default(); - assert_eq!( - node.rebroadcast_interval, - crate::rebroadcast::DEFAULT_REBROADCAST_INTERVAL - ); - } - - #[test] - fn arc_node_construction_with_rebroadcast_disabled() { - let node = ArcNode::new( - ArcRpcConfig::default(), - InvalidTxListConfig::default(), - AddressesDenylistConfig::default(), - None, - true, - true, - false, - 160 * 1024 * 1024, - ARC_RPC_MAX_BATCH_ENTRIES_DEFAULT, - std::time::Duration::ZERO, - ); - assert!(node.rebroadcast_interval.is_zero()); - } - - #[test] - fn arc_network_builder_default_interval() { - let builder = ArcNetworkBuilder::default(); - assert_eq!( - builder.rebroadcast_interval, - crate::rebroadcast::DEFAULT_REBROADCAST_INTERVAL - ); - } - - #[test] - fn arc_network_builder_with_zero_disables() { - let builder = - ArcNetworkBuilder::default().with_rebroadcast_interval(std::time::Duration::ZERO); - assert!(builder.rebroadcast_interval.is_zero()); - } -} diff --git a/crates/evm-node/src/payload.rs b/crates/evm-node/src/payload.rs deleted file mode 100644 index a31b66f..0000000 --- a/crates/evm-node/src/payload.rs +++ /dev/null @@ -1,138 +0,0 @@ -// Copyright 2026 Circle Internet Group, Inc. All rights reserved. -// -// SPDX-License-Identifier: Apache-2.0 -// -// Licensed under the Apache License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. -// You may obtain a copy of the License at -// -// http://www.apache.org/licenses/LICENSE-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// See the License for the specific language governing permissions and -// limitations under the License. - -//! Arc payload attributes builder for dev-mode local mining. -//! Fork from https://github.com/paradigmxyz/reth/blob/v1.11.3/crates/engine/local/src/payload.rs -//! - Uses `max(parent.timestamp, wall_clock)` instead of `max(parent.timestamp + 1, wall_clock)` -//! to allow equal timestamps, matching Arc's relaxed validation. - -use alloy_consensus::BlockHeader; -use alloy_primitives::B256; -use reth_chainspec::{EthChainSpec, EthereumHardforks}; -use reth_ethereum_engine_primitives::EthPayloadAttributes; -use reth_payload_primitives::PayloadAttributesBuilder; -use reth_primitives_traits::SealedHeader; -use std::sync::Arc; - -/// Payload attributes builder for Arc's dev-mode miner. -/// -/// Unlike upstream's `LocalPayloadAttributesBuilder` which enforces strictly -/// increasing timestamps (`parent + 1`), this uses `max(wall_clock, parent.timestamp)` -/// to allow equal timestamps — matching Arc's sub-second block production. -#[derive(Debug)] -pub struct ArcLocalPayloadAttributesBuilder { - chain_spec: Arc, -} - -impl ArcLocalPayloadAttributesBuilder { - /// Creates a new instance of the builder. - pub const fn new(chain_spec: Arc) -> Self { - Self { chain_spec } - } -} - -impl PayloadAttributesBuilder - for ArcLocalPayloadAttributesBuilder -where - ChainSpec: EthChainSpec + EthereumHardforks + 'static, -{ - fn build(&self, parent: &SealedHeader) -> EthPayloadAttributes { - let timestamp = std::cmp::max( - parent.timestamp(), - std::time::SystemTime::now() - .duration_since(std::time::UNIX_EPOCH) - .expect("Clock is before UNIX epoch") - .as_secs(), - ); - - EthPayloadAttributes { - timestamp, - prev_randao: B256::random(), - // Mock CL uses genesis coinbase as suggested fee recipient - suggested_fee_recipient: self.chain_spec.genesis_header().beneficiary(), - withdrawals: self - .chain_spec - .is_shanghai_active_at_timestamp(timestamp) - .then(Default::default), - parent_beacon_block_root: self - .chain_spec - .is_cancun_active_at_timestamp(timestamp) - .then(B256::random), - } - } -} - -#[cfg(test)] -mod tests { - use super::*; - use reth_chainspec::ChainSpecBuilder; - use reth_ethereum::primitives::Header; - use reth_primitives_traits::SealedHeader; - - fn builder() -> ArcLocalPayloadAttributesBuilder { - ArcLocalPayloadAttributesBuilder::new(Arc::new(ChainSpecBuilder::mainnet().build())) - } - - #[test] - fn timestamp_uses_parent_when_ahead_of_clock() { - let b = builder(); - let parent = SealedHeader::seal_slow(Header { - timestamp: u64::MAX / 2, - ..Default::default() - }); - let attrs = b.build(&parent); - assert_eq!(attrs.timestamp, u64::MAX / 2); - } - - #[test] - fn timestamp_equal_to_parent_not_incremented() { - let b = builder(); - let future = std::time::SystemTime::now() - .duration_since(std::time::UNIX_EPOCH) - .unwrap() - .as_secs() - + 10; - let parent = SealedHeader::seal_slow(Header { - timestamp: future, - ..Default::default() - }); - let attrs = b.build(&parent); - assert_eq!(attrs.timestamp, future); - } - - #[test] - fn timestamp_uses_wall_clock_when_parent_is_old() { - let b = builder(); - let parent = SealedHeader::seal_slow(Header { - timestamp: 0, - ..Default::default() - }); - let attrs = b.build(&parent); - let now = std::time::SystemTime::now() - .duration_since(std::time::UNIX_EPOCH) - .unwrap() - .as_secs(); - assert!(attrs.timestamp >= now.saturating_sub(1)); - } - - #[test] - fn withdrawals_populated_for_shanghai() { - let b = builder(); - let parent = SealedHeader::seal_slow(Header::default()); - let attrs = b.build(&parent); - assert_eq!(attrs.withdrawals, Some(vec![])); // empty array - } -} diff --git a/crates/evm-node/src/rebroadcast.rs b/crates/evm-node/src/rebroadcast.rs deleted file mode 100644 index 910f866..0000000 --- a/crates/evm-node/src/rebroadcast.rs +++ /dev/null @@ -1,211 +0,0 @@ -// Copyright 2026 Circle Internet Group, Inc. All rights reserved. -// -// SPDX-License-Identifier: Apache-2.0 -// -// Licensed under the Apache License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. -// You may obtain a copy of the License at -// -// http://www.apache.org/licenses/LICENSE-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// See the License for the specific language governing permissions and -// limitations under the License. - -//! Periodic transaction rebroadcast. -//! -//! Reth announces each transaction to peers exactly once when it enters the pool. -//! If that single gossip attempt is missed (peer busy, queue full, multi-hop drop), -//! the transaction sits in the local pool forever — valid but invisible to validators. -//! -//! This module adds a background task that periodically broadcasts pending -//! transactions to all peers using `PropagationMode::Forced`, which ignores -//! per-peer LRU seen-caches. This ensures transactions reach validators even -//! when the initial announcement was received but the cache entry hasn't been -//! evicted — critical on permissioned chains with low transaction volume. - -use reth_network::primitives::NetworkPrimitives; -use reth_network::transactions::TransactionsHandle; -use reth_transaction_pool::{PoolTransaction, TransactionPool}; -use std::time::Duration; -use tracing::debug; - -pub const DEFAULT_REBROADCAST_INTERVAL: Duration = Duration::from_secs(60); - -/// Maximum number of transactions to broadcast per rebroadcast round. -pub const MAX_REBROADCAST: usize = 4096; - -/// Background task that periodically broadcasts pending pool transactions -/// using `PropagationMode::Forced` to bypass per-peer LRU seen-caches. -pub struct TxRebroadcaster { - pool: Pool, - transactions_handle: TransactionsHandle, - interval: Duration, -} - -impl TxRebroadcaster -where - Pool: TransactionPool + 'static, - N: NetworkPrimitives< - BroadcastedTransaction = ::Consensus, - >, -{ - pub fn new(pool: Pool, transactions_handle: TransactionsHandle, interval: Duration) -> Self { - Self { - pool, - transactions_handle, - interval, - } - } - - /// Runs the rebroadcast loop until the task is cancelled. - pub async fn run(self) { - let mut interval = tokio::time::interval(self.interval); - interval.set_missed_tick_behavior(tokio::time::MissedTickBehavior::Skip); - interval.tick().await; - - loop { - interval.tick().await; - self.rebroadcast_pending(); - } - } - - fn rebroadcast_pending(&self) { - let txs = collect_pending_txs(&self.pool); - - if txs.is_empty() { - return; - } - - let count = txs.len(); - - // broadcast_transactions uses PropagationMode::Forced, which ignores - // per-peer LRU seen-caches — every connected peer receives the announcement. - self.transactions_handle.broadcast_transactions(txs); - - debug!( - target: "arc::txpool::rebroadcast", - count, - "Broadcast pending transactions (Forced mode)" - ); - } -} - -/// Collects up to [`MAX_REBROADCAST`] consensus-format transactions from the pending set. -/// -/// When the pool exceeds `MAX_REBROADCAST`, only the first `MAX_REBROADCAST` transactions -/// by internal pool ordering (sender-id, then nonce) are broadcast. -pub(crate) fn collect_pending_txs( - pool: &Pool, -) -> Vec<::Consensus> { - pool.pending_transactions_max(MAX_REBROADCAST) - .into_iter() - .map(|tx| tx.transaction.clone_into_consensus().into_inner()) - .collect() -} - -#[cfg(test)] -mod tests { - use super::*; - use alloy_primitives::{B256, U256}; - use reth_ethereum::node::EthEvmConfig; - use reth_provider::test_utils::{ExtendedAccount, MockEthProvider}; - use reth_transaction_pool::{ - blobstore::InMemoryBlobStore, test_utils::MockTransaction, - validate::EthTransactionValidatorBuilder, CoinbaseTipOrdering, Pool, PoolTransaction, - TransactionPool, - }; - - fn create_test_pool( - provider: &MockEthProvider, - ) -> Pool< - reth_transaction_pool::validate::EthTransactionValidator< - MockEthProvider, - MockTransaction, - EthEvmConfig, - >, - CoinbaseTipOrdering, - InMemoryBlobStore, - > { - provider.add_block(B256::ZERO, reth_ethereum_primitives::Block::default()); - let blob_store = InMemoryBlobStore::default(); - let validator = - EthTransactionValidatorBuilder::new(provider.clone(), EthEvmConfig::mainnet()) - .build(blob_store.clone()); - Pool::new( - validator, - CoinbaseTipOrdering::default(), - blob_store, - Default::default(), - ) - } - - fn funded_tx(provider: &MockEthProvider, nonce: u64) -> MockTransaction { - let tx = MockTransaction::legacy() - .with_gas_limit(21_000) - .with_gas_price(1_000_000_000) - .with_value(U256::from(1000)) - .with_nonce(nonce); - provider.add_account(tx.sender(), ExtendedAccount::new(nonce, U256::MAX)); - tx - } - - #[test] - fn collect_pending_txs_empty_pool() { - let provider = MockEthProvider::default(); - let pool = create_test_pool(&provider); - let txs = collect_pending_txs(&pool); - assert!(txs.is_empty()); - } - - #[tokio::test] - async fn collect_pending_txs_returns_correct_hashes() { - let provider = MockEthProvider::default(); - let pool = create_test_pool(&provider); - - let tx = funded_tx(&provider, 0); - let expected_hash = *tx.hash(); - pool.add_external_transaction(tx).await.unwrap(); - - let txs = collect_pending_txs(&pool); - assert_eq!(txs.len(), 1); - assert_eq!(*txs[0].tx_hash(), expected_hash); - } - - #[tokio::test] - async fn collect_pending_txs_multiple() { - let provider = MockEthProvider::default(); - let pool = create_test_pool(&provider); - - let tx1 = funded_tx(&provider, 0); - let tx2 = funded_tx(&provider, 0); - assert_ne!( - tx1.sender(), - tx2.sender(), - "different senders for independent nonces" - ); - - pool.add_external_transaction(tx1).await.unwrap(); - pool.add_external_transaction(tx2).await.unwrap(); - - let txs = collect_pending_txs(&pool); - assert_eq!(txs.len(), 2); - } - - #[tokio::test] - async fn collect_pending_txs_truncates_at_max() { - let provider = MockEthProvider::default(); - let pool = create_test_pool(&provider); - - for i in 0..(MAX_REBROADCAST + 100) { - let nonce = (i / 100) as u64; - let tx = funded_tx(&provider, nonce); - pool.add_external_transaction(tx).await.unwrap(); - } - - let collected = collect_pending_txs(&pool); - assert_eq!(collected.len(), MAX_REBROADCAST); - } -} diff --git a/crates/evm-node/src/rpc/arc.rs b/crates/evm-node/src/rpc/arc.rs deleted file mode 100644 index 7991fa8..0000000 --- a/crates/evm-node/src/rpc/arc.rs +++ /dev/null @@ -1,67 +0,0 @@ -// Copyright 2025 Circle Internet Group, Inc. All rights reserved. -// -// SPDX-License-Identifier: Apache-2.0 -// -// Licensed under the Apache License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. -// You may obtain a copy of the License at -// -// http://www.apache.org/licenses/LICENSE-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// See the License for the specific language governing permissions and -// limitations under the License. - -//! Top level file of `reth` node API extension RPCs - -use crate::rpc::common::ARC_DEFAULT_BASE_URL; -use crate::rpc::get_certificate::{ - rpc_get_certificate, CertificateSource, HttpCertificateSource, RpcCommitCertificate, -}; -use crate::rpc::get_version::{rpc_get_version, RpcVersionInfo}; -use async_trait::async_trait; -use jsonrpsee::{core::RpcResult, proc_macros::rpc, RpcModule}; - -pub type DefaultArcApiImpl = ArcApiImpl; - -/// ARC namespace exposing RPC methods. Full reference: `crates/node/src/rpc/openapi/openapi.yaml`. -#[rpc(server, namespace = "arc")] -#[async_trait] -pub trait ArcApi { - /// Returns version information for the execution layer - #[method(name = "getVersion")] - fn version(&self) -> RpcResult; - - #[method(name = "getCertificate")] - async fn get_certificate(&self, height: u64) -> RpcResult; -} - -pub struct ArcApiImpl { - certificate_source: S, -} - -impl ArcApiImpl { - pub fn new(certificate_source: S) -> Self { - Self { certificate_source } - } -} - -#[async_trait] -impl ArcApiServer for ArcApiImpl { - async fn get_certificate(&self, height: u64) -> RpcResult { - rpc_get_certificate(&self.certificate_source, height).await - } - fn version(&self) -> RpcResult { - rpc_get_version() - } -} - -pub fn build_arc_rpc_module( - base_url: Option, -) -> eyre::Result>> { - let base = base_url.unwrap_or_else(|| ARC_DEFAULT_BASE_URL.to_string()); - let certificate_source = HttpCertificateSource::new(base)?; - Ok(ArcApiImpl::new(certificate_source).into_rpc()) -} diff --git a/crates/evm-node/src/rpc/common.rs b/crates/evm-node/src/rpc/common.rs deleted file mode 100644 index a6e8827..0000000 --- a/crates/evm-node/src/rpc/common.rs +++ /dev/null @@ -1,30 +0,0 @@ -// Copyright 2025 Circle Internet Group, Inc. All rights reserved. -// -// SPDX-License-Identifier: Apache-2.0 -// -// Licensed under the Apache License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. -// You may obtain a copy of the License at -// -// http://www.apache.org/licenses/LICENSE-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// See the License for the specific language governing permissions and -// limitations under the License. - -use jsonrpsee::types::{ErrorCode, ErrorObjectOwned}; - -pub const ARC_DEFAULT_BASE_URL: &str = "http://127.0.0.1:31000"; - -pub fn invalid_params(msg: impl Into) -> ErrorObjectOwned { - ErrorObjectOwned::owned(ErrorCode::InvalidParams.code(), msg.into(), None::<()>) -} - -pub mod codes { - /// Artifact not found (e.g., certificate missing upstream). - pub const NOT_FOUND: i32 = -32004; - /// Upstream service unreachable (TCP connect failures). - pub const UPSTREAM_UNREACHABLE: i32 = -32005; -} diff --git a/crates/evm-node/src/rpc/get_certificate.rs b/crates/evm-node/src/rpc/get_certificate.rs deleted file mode 100644 index 901f536..0000000 --- a/crates/evm-node/src/rpc/get_certificate.rs +++ /dev/null @@ -1,288 +0,0 @@ -// Copyright 2025 Circle Internet Group, Inc. All rights reserved. -// -// SPDX-License-Identifier: Apache-2.0 -// -// Licensed under the Apache License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. -// You may obtain a copy of the License at -// -// http://www.apache.org/licenses/LICENSE-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// See the License for the specific language governing permissions and -// limitations under the License. - -//! getCertificate RPC API implementation - -use crate::rpc::common::{codes, invalid_params}; -pub use arc_consensus_types::commit_http::HttpCommitCertificate as RpcCommitCertificate; -use async_trait::async_trait; -use backon::{ExponentialBuilder, Retryable}; -use jsonrpsee::{ - core::RpcResult, - types::{ErrorCode, ErrorObjectOwned}, -}; -use reqwest::StatusCode; -use std::time::Duration; -use tracing::error; - -/// Default maximum retry attempts for upstream certificate HTTP fetches. -const HTTP_MAX_RETRIES: usize = 3; - -/// Core logic for the `arc_getCertificate` method: validates params, performs fetch, maps errors. -pub async fn rpc_get_certificate( - source: &dyn CertificateSource, - height: u64, -) -> RpcResult { - if height < 1 { - return Err(invalid_params(format!( - "height must be >= 1, got {}", - height - ))); - } - match source.fetch(height).await { - Ok(Some(cert)) => Ok(cert), - Ok(None) | Err(FetchError::NotFound) => Err(ErrorObjectOwned::owned( - codes::NOT_FOUND, - "Certificate not found", - None::<()>, - )), - Err(FetchError::Timeout) => Err(ErrorObjectOwned::owned( - ErrorCode::InternalError.code(), - "Timeout fetching certificate", - None::<()>, - )), - Err(FetchError::Connect) => Err(ErrorObjectOwned::owned( - codes::UPSTREAM_UNREACHABLE, - "Upstream certificate server unreachable", - None::<()>, - )), - Err(FetchError::Network(status)) => Err(ErrorObjectOwned::owned( - ErrorCode::InternalError.code(), - format!("Upstream HTTP error: {status}"), - None::<()>, - )), - Err(FetchError::Decode(e)) => Err(ErrorObjectOwned::owned( - ErrorCode::InternalError.code(), - format!("Failed to decode upstream response: {e}"), - None::<()>, - )), - } -} - -#[async_trait] -pub trait CertificateSource: Send + Sync { - async fn fetch(&self, height: u64) -> Result, FetchError>; -} - -#[derive(thiserror::Error, Debug)] -pub enum FetchError { - #[error("not found")] - NotFound, - #[error("timeout")] - Timeout, - #[error("connect error")] - Connect, - #[error("network status {0}")] - Network(StatusCode), - #[error("decode error: {0}")] - Decode(String), -} - -#[derive(Clone)] -pub struct HttpCertificateSource { - client: reqwest::Client, - base_url: String, - max_retries: usize, -} - -impl HttpCertificateSource { - pub fn new(base_url: impl Into) -> eyre::Result { - let client = reqwest::Client::builder() - .timeout(Duration::from_secs(2)) - .build() - .map_err(|e| eyre::eyre!("Failed to build HTTP client: {}", e))?; - Ok(Self { - client, - base_url: base_url.into(), - max_retries: HTTP_MAX_RETRIES, - }) - } -} - -#[async_trait] -impl CertificateSource for HttpCertificateSource { - async fn fetch(&self, height: u64) -> Result, FetchError> { - let url = format!( - "{}/commit?height={}", - self.base_url.trim_end_matches('/'), - height - ); - let builder = ExponentialBuilder::default() - .with_min_delay(Duration::from_millis(50)) - .with_max_delay(Duration::from_secs(5)) - .with_max_times(self.max_retries); - let operation = || async { - let resp = match self.client.get(&url).send().await { - Ok(r) => r, - Err(e) if e.is_timeout() => { - error!(height=?height, "Timeout fetching certificate"); - return Err(FetchError::Timeout); - } - Err(e) if e.is_connect() => { - error!(height=?height, "Connect error fetching certificate"); - return Err(FetchError::Connect); - } - Err(e) => { - error!(height=%height, error=?e, "General network error fetching certificate"); - return Err(FetchError::Network(StatusCode::INTERNAL_SERVER_ERROR)); - } - }; - let status = resp.status(); - if status == StatusCode::NOT_FOUND { - return Ok(None); - } - if !status.is_success() { - error!(height=%height, status=?status, "Upstream non-success status"); - return Err(FetchError::Network(status)); - } - let bytes = resp - .bytes() - .await - .map_err(|e| FetchError::Decode(e.to_string()))?; - let cert = serde_json::from_slice::(&bytes) - .map_err(|e| FetchError::Decode(e.to_string()))?; - Ok(Some(cert)) - }; - operation - .retry(builder) - .sleep(tokio::time::sleep) - .when(|e: &FetchError| { - matches!( - e, - FetchError::Timeout | FetchError::Connect | FetchError::Network(_) - ) - }) - .await - } -} - -#[cfg(test)] -mod tests { - use super::*; - use crate::rpc::{ - arc::{ArcApiImpl, ArcApiServer}, - common::codes::{NOT_FOUND, UPSTREAM_UNREACHABLE}, - get_certificate::{CertificateSource, RpcCommitCertificate}, - }; - use arc_consensus_types::{BlockHash, ValueId}; - use async_trait::async_trait; - use jsonrpsee::types::ErrorCode; - use reqwest::StatusCode; - struct MockCertSource; - #[async_trait] - impl CertificateSource for MockCertSource { - async fn fetch(&self, h: u64) -> Result, FetchError> { - Ok(Some(RpcCommitCertificate { - height: h, - round: 3, - block_hash: ValueId::new(BlockHash::ZERO), - signatures: vec![], - })) - } - } - - #[tokio::test] - async fn rejects_invalid_height() { - let api = ArcApiImpl::new(MockCertSource); - let err = ArcApiServer::get_certificate(&api, 0).await.unwrap_err(); - assert!(err.message().contains("height must be")); - } - - #[tokio::test] - async fn not_found_maps_to_error() { - struct EmptySource; - #[async_trait] - impl CertificateSource for EmptySource { - async fn fetch(&self, _h: u64) -> Result, FetchError> { - Ok(None) - } - } - let api = ArcApiImpl::new(EmptySource); - let err = ArcApiServer::get_certificate(&api, 5).await.unwrap_err(); - assert_eq!(err.code(), NOT_FOUND); - } - struct ConnectFail; - #[async_trait] - impl CertificateSource for ConnectFail { - async fn fetch(&self, _h: u64) -> Result, FetchError> { - Err(FetchError::Connect) - } - } - - #[tokio::test] - async fn connect_failure_maps_to_custom_code() { - let api = ArcApiImpl::new(ConnectFail); - let err = ArcApiServer::get_certificate(&api, 10).await.unwrap_err(); - assert_eq!(err.code(), UPSTREAM_UNREACHABLE); - assert!(err.message().contains("unreachable")); - } - - #[tokio::test] - async fn returns_real_cert_from_source() { - let api = ArcApiImpl::new(MockCertSource); - let cert = ArcApiServer::get_certificate(&api, 7).await.unwrap(); - assert_eq!(cert.height, 7); - assert_eq!(cert.round, 3); - } - struct TimeoutSource; - #[async_trait] - impl CertificateSource for TimeoutSource { - async fn fetch(&self, _h: u64) -> Result, FetchError> { - Err(FetchError::Timeout) - } - } - - #[tokio::test] - async fn timeout_maps_to_internal_error() { - let api = ArcApiImpl::new(TimeoutSource); - let err = ArcApiServer::get_certificate(&api, 9).await.unwrap_err(); - assert_eq!(err.code(), ErrorCode::InternalError.code()); - assert!(err.message().contains("Timeout")); - } - - struct DecodeFailSource; - #[async_trait] - impl CertificateSource for DecodeFailSource { - async fn fetch(&self, _h: u64) -> Result, FetchError> { - Err(FetchError::Decode("boom".into())) - } - } - - #[tokio::test] - async fn decode_error_maps_to_internal_error() { - let api = ArcApiImpl::new(DecodeFailSource); - let err = ArcApiServer::get_certificate(&api, 11).await.unwrap_err(); - assert_eq!(err.code(), ErrorCode::InternalError.code()); - assert!(err.message().contains("Failed to decode")); - } - - struct NetworkFailSource; - #[async_trait] - impl CertificateSource for NetworkFailSource { - async fn fetch(&self, _h: u64) -> Result, FetchError> { - Err(FetchError::Network(StatusCode::INTERNAL_SERVER_ERROR)) - } - } - - #[tokio::test] - async fn network_status_maps_to_internal_error() { - let api = ArcApiImpl::new(NetworkFailSource); - let err = ArcApiServer::get_certificate(&api, 13).await.unwrap_err(); - assert_eq!(err.code(), ErrorCode::InternalError.code()); - assert!(err.message().contains("Upstream HTTP error")); - assert!(err.message().contains("500")); - } -} diff --git a/crates/evm-node/src/rpc/get_version.rs b/crates/evm-node/src/rpc/get_version.rs deleted file mode 100644 index afc6bc5..0000000 --- a/crates/evm-node/src/rpc/get_version.rs +++ /dev/null @@ -1,43 +0,0 @@ -// Copyright 2025 Circle Internet Group, Inc. All rights reserved. -// -// SPDX-License-Identifier: Apache-2.0 -// -// Licensed under the Apache License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. -// You may obtain a copy of the License at -// -// http://www.apache.org/licenses/LICENSE-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// See the License for the specific language governing permissions and -// limitations under the License. - -//! getVersion RPC API implementation - -use jsonrpsee::core::RpcResult; -use serde::Serialize; - -/// Version information returned by the RPC -#[derive(Debug, Clone, Serialize)] -pub struct RpcVersionInfo { - /// Git version (tag or short commit hash) - pub git_version: String, - /// Full git commit hash - pub git_commit: String, - /// Short git commit hash - pub git_short_hash: String, - /// Cargo package version - pub cargo_version: String, -} - -/// Core logic for the `version` RPC method -pub fn rpc_get_version() -> RpcResult { - Ok(RpcVersionInfo { - git_version: arc_version::GIT_VERSION.to_string(), - git_commit: arc_version::GIT_COMMIT_HASH.to_string(), - git_short_hash: arc_version::GIT_SHORT_HASH.to_string(), - cargo_version: arc_version::SHORT_VERSION.to_string(), - }) -} diff --git a/crates/evm-node/src/rpc/mod.rs b/crates/evm-node/src/rpc/mod.rs deleted file mode 100644 index 3af763b..0000000 --- a/crates/evm-node/src/rpc/mod.rs +++ /dev/null @@ -1,22 +0,0 @@ -// Copyright 2025 Circle Internet Group, Inc. All rights reserved. -// -// SPDX-License-Identifier: Apache-2.0 -// -// Licensed under the Apache License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. -// You may obtain a copy of the License at -// -// http://www.apache.org/licenses/LICENSE-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// See the License for the specific language governing permissions and -// limitations under the License. - -//! Custom RPC modules for Arc - -pub mod arc; -pub mod common; -pub mod get_certificate; -pub mod get_version; diff --git a/crates/evm-node/src/rpc/openapi/openapi.yaml b/crates/evm-node/src/rpc/openapi/openapi.yaml deleted file mode 100644 index 411cc77..0000000 --- a/crates/evm-node/src/rpc/openapi/openapi.yaml +++ /dev/null @@ -1,218 +0,0 @@ -# Copyright 2026 Circle Internet Group, Inc. All rights reserved. -# -# SPDX-License-Identifier: Apache-2.0 -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. - -openapi: 3.0.0 -info: - title: Arc RPC - version: 0.1.0 - description: | - Hand-maintained OpenAPI summary of custom ARC RPC methods. - Implementation layout: - - Namespace glue: `crates/node/src/rpc/arc.rs` - - Certificate method logic: `crates/node/src/rpc/get_certificate.rs` - - Shared constants/errors: `crates/node/src/rpc/common.rs` - Each future method will follow the same pattern: its own file + delegation from `arc.rs`. -servers: - - url: http://localhost:8545 - description: Local node JSON-RPC -tags: - - name: ARC - description: ARC custom consensus RPC methods on `reth` -paths: - /: - post: - tags: [ARC] - summary: JSON-RPC endpoint for ARC methods - operationId: arc_rpc - description: | - Single JSON-RPC 2.0 POST endpoint. Methods currently implemented under the `arc` namespace: - - 1. arc_getCertificate(height: u64) -> RpcCommitCertificate - Returns the consensus commit certificate for the specified height if available. - Error Mapping (error.code -> meaning): - -32602 Invalid params (height < 1) - -32004 Not found (no certificate upstream) - -32005 Upstream unreachable (connect errors) - -32603 Internal (timeout / decode / generic upstream HTTP status) - Curl Example: - curl -s -X POST -H 'Content-Type: application/json' \ - --data '{"jsonrpc":"2.0","method":"arc_getCertificate","params":[7],"id":1}' \ - http://127.0.0.1:8545 - - 2. arc_getVersion() -> RpcVersionInfo - Returns version/build information for the node execution layer. - Fields: - git_version: Git tag or short commit hash - git_commit: Full git commit hash - git_short_hash: Short git commit hash - cargo_version: Cargo package version - Curl Example: - curl -s -X POST -H 'Content-Type: application/json' \ - --data '{"jsonrpc":"2.0","method":"arc_getVersion","params":[],"id":1}' \ - http://127.0.0.1:8545 - - NOTE: This path '/' is shared by all JSON-RPC methods; separate request/response schemas are provided for clarity. - requestBody: - required: true - content: - application/json: - schema: - oneOf: - - $ref: '#/components/schemas/JsonRpcRequest_arc_getCertificate' - - $ref: '#/components/schemas/JsonRpcRequest_arc_getVersion' - responses: - '200': - description: JSON-RPC success or error response for any listed method - content: - application/json: - schema: - oneOf: - - $ref: '#/components/schemas/JsonRpcSuccess_arc_getCertificate' - - $ref: '#/components/schemas/JsonRpcSuccess_arc_getVersion' - - $ref: '#/components/schemas/JsonRpcError' -components: - schemas: - ErrorObject: - type: object - required: [code, message] - properties: - code: - type: integer - message: - type: string - JsonRpcRequest_arc_getCertificate: - type: object - required: [jsonrpc, method, params, id] - properties: - jsonrpc: - type: string - enum: ["2.0"] - method: - type: string - enum: ["arc_getCertificate"] - params: - type: array - minItems: 1 - maxItems: 1 - items: - type: integer - minimum: 1 - format: int64 - id: - oneOf: - - type: integer - - type: string - JsonRpcSuccess_arc_getCertificate: - type: object - required: [jsonrpc, id, result] - properties: - jsonrpc: - type: string - enum: ["2.0"] - id: - oneOf: - - type: integer - - type: string - result: - $ref: '#/components/schemas/RpcCommitCertificate' - JsonRpcRequest_arc_getVersion: - type: object - required: [jsonrpc, method, params, id] - properties: - jsonrpc: - type: string - enum: ["2.0"] - method: - type: string - enum: ["arc_getVersion"] - params: - type: array - maxItems: 0 - id: - oneOf: - - type: integer - - type: string - JsonRpcSuccess_arc_getVersion: - type: object - required: [jsonrpc, id, result] - properties: - jsonrpc: - type: string - enum: ["2.0"] - id: - oneOf: - - type: integer - - type: string - result: - $ref: '#/components/schemas/RpcVersionInfo' - JsonRpcError: - type: object - required: [jsonrpc, id, error] - properties: - jsonrpc: - type: string - enum: ["2.0"] - id: - oneOf: - - type: integer - - type: string - error: - $ref: '#/components/schemas/ErrorObject' - Address: - type: string - pattern: '^0x[a-fA-F0-9]{40}$' - ValueId: - type: string - pattern: '^0x[a-fA-F0-9]{64}$' - RpcCommitSignature: - type: object - required: [address, signature] - properties: - address: - $ref: '#/components/schemas/Address' - signature: - type: string - description: Base64-encoded bytes - RpcCommitCertificate: - type: object - required: [height, round, block_hash, signatures] - properties: - height: - type: integer - minimum: 1 - format: uint64 - round: - type: integer - minimum: 0 - format: int64 - block_hash: - $ref: '#/components/schemas/ValueId' - signatures: - type: array - items: - $ref: '#/components/schemas/RpcCommitSignature' - RpcVersionInfo: - type: object - required: [git_version, git_commit, git_short_hash, cargo_version] - properties: - git_version: - type: string - git_commit: - type: string - git_short_hash: - type: string - cargo_version: - type: string diff --git a/crates/evm-node/src/rpc_middleware.rs b/crates/evm-node/src/rpc_middleware.rs deleted file mode 100644 index 0491e3b..0000000 --- a/crates/evm-node/src/rpc_middleware.rs +++ /dev/null @@ -1,1479 +0,0 @@ -// Copyright 2026 Circle Internet Group, Inc. All rights reserved. -// -// SPDX-License-Identifier: Apache-2.0 -// -// Licensed under the Apache License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. -// You may obtain a copy of the License at -// -// http://www.apache.org/licenses/LICENSE-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// See the License for the specific language governing permissions and -// limitations under the License. - -use alloy_consensus::TxEnvelope; -use alloy_network::eip2718::Decodable2718; -use alloy_primitives::Bytes; -use jsonrpsee::{ - core::middleware::{layer::Either, Batch, BatchEntry, Notification, RpcServiceT}, - types::{ErrorObject, ErrorObjectOwned, Id, Request, ResponsePayload}, - BatchResponseBuilder, MethodResponse, -}; -use std::future::Future; -use tower::Layer; - -const ETH_SUBSCRIBE_METHOD: &str = "eth_subscribe"; -const PENDING_TX_SUBSCRIPTION_TYPE: &str = "newPendingTransactions"; -const ETH_NEW_PENDING_TX_FILTER_METHOD: &str = "eth_newPendingTransactionFilter"; -const ETH_GET_BLOCK_BY_NUMBER_METHOD: &str = "eth_getBlockByNumber"; -const PENDING_BLOCK_TAG: &str = "pending"; -const ETH_GET_TX_BY_SENDER_AND_NONCE_METHOD: &str = "eth_getTransactionBySenderAndNonce"; -const ETH_SEND_RAW_TRANSACTION_METHOD: &str = "eth_sendRawTransaction"; -const ETH_SEND_RAW_TRANSACTION_SYNC_METHOD: &str = "eth_sendRawTransactionSync"; -const PENDING_TX_SUBSCRIPTION_ERROR_CODE: i32 = -32001; -const BATCH_TOO_LARGE_ERROR_CODE: i32 = -32600; -const UNPROTECTED_TX_ERROR_CODE: i32 = -32000; -const UNPROTECTED_TX_ERROR_MSG: &str = - "only replay-protected (EIP-155) transactions allowed over RPC"; - -/// Default maximum number of entries permitted in a JSON-RPC batch request. -pub const ARC_RPC_MAX_BATCH_ENTRIES_DEFAULT: usize = 100; - -/// Config for the Arc-specific RPC middleware stack. -#[derive(Clone, Debug)] -pub struct ArcRpcLayer { - /// When true (default), `eth_subscribe("newPendingTransactions")`, - /// `eth_newPendingTransactionFilter`, `eth_getBlockByNumber("pending")`, - /// and `eth_getTransactionBySenderAndNonce` are blocked. - /// When false, the filter is bypassed and these are allowed. - /// CLI users opt out of the default via `--arc.expose-pending-txs`. - pub filter_pending_txs: bool, - /// When true, raw transaction submission RPCs accept pre-EIP-155 - /// (replay-unprotected) transactions. Defaults to false, matching Geth. - /// P2P-received transactions and transactions included in blocks by other - /// validators are not affected. - pub allow_unprotected_txs: bool, - /// Mirrors `--rpc.max-response-size` from the server config (in bytes). - pub max_response_body_size: usize, - /// Maximum number of entries permitted in a JSON-RPC batch request. - pub max_batch_entries: usize, -} - -impl Default for ArcRpcLayer { - fn default() -> Self { - Self { - filter_pending_txs: true, - allow_unprotected_txs: false, - max_response_body_size: usize::MAX, - max_batch_entries: ARC_RPC_MAX_BATCH_ENTRIES_DEFAULT, - } - } -} - -impl ArcRpcLayer { - pub fn new( - filter_pending_txs: bool, - allow_unprotected_txs: bool, - max_response_body_size: usize, - max_batch_entries: usize, - ) -> Self { - Self { - filter_pending_txs, - allow_unprotected_txs, - max_response_body_size, - max_batch_entries, - } - } -} - -// S: Clone is required because the middleware clones the inner service in its -// `call` implementation. -impl Layer for ArcRpcLayer -where - S: Clone, -{ - type Service = BatchSizeLimitMiddleware< - RejectUnprotectedTxsMiddleware, S>>, - >; - - fn layer(&self, inner: S) -> Self::Service { - let pending_layer = if self.filter_pending_txs { - Either::Left(NoPendingTransactionsRpcMiddleware { - service: inner, - max_response_body_size: self.max_response_body_size, - }) - } else { - Either::Right(inner) - }; - let service = RejectUnprotectedTxsMiddleware { - service: pending_layer, - allow_unprotected_txs: self.allow_unprotected_txs, - max_response_body_size: self.max_response_body_size, - }; - BatchSizeLimitMiddleware { - service, - max_entries: self.max_batch_entries, - } - } -} - -/// RPC middleware that rejects JSON-RPC batches above `max_entries` before any -/// per-entry handler runs. -#[derive(Clone, Debug)] -pub struct BatchSizeLimitMiddleware { - service: S, - max_entries: usize, -} - -impl BatchSizeLimitMiddleware { - pub fn new(service: S, max_entries: usize) -> Self { - Self { - service, - max_entries, - } - } -} - -impl RpcServiceT for BatchSizeLimitMiddleware -where - S: RpcServiceT< - MethodResponse = MethodResponse, - NotificationResponse = MethodResponse, - BatchResponse = MethodResponse, - > + Send - + Sync - + Clone - + 'static, -{ - type MethodResponse = S::MethodResponse; - type NotificationResponse = S::NotificationResponse; - type BatchResponse = S::BatchResponse; - - fn call<'a>(&self, req: Request<'a>) -> impl Future + Send + 'a { - self.service.call(req) - } - - fn batch<'a>(&self, req: Batch<'a>) -> impl Future + Send + 'a { - let service = self.service.clone(); - let max_entries = self.max_entries; - async move { - if req.len() > max_entries { - let err = ErrorObjectOwned::owned::<()>( - BATCH_TOO_LARGE_ERROR_CODE, - format!("batch size {} exceeds limit of {}", req.len(), max_entries), - None, - ); - return MethodResponse::error(Id::Null, err); - } - service.batch(req).await - } - } - - fn notification<'a>( - &self, - n: Notification<'a>, - ) -> impl Future + Send + 'a { - self.service.notification(n) - } -} - -/// RPC middleware that prevents websocket subscriptions and HTTP filters for pending transactions. -#[derive(Clone, Debug)] -pub struct NoPendingTransactionsRpcMiddleware { - service: S, - max_response_body_size: usize, -} - -impl NoPendingTransactionsRpcMiddleware { - /// Creates a new instance of the middleware. - pub fn new(service: S) -> Self { - Self { - service, - max_response_body_size: usize::MAX, - } - } -} - -impl RpcServiceT for NoPendingTransactionsRpcMiddleware -where - S: RpcServiceT< - MethodResponse = MethodResponse, - NotificationResponse = MethodResponse, - BatchResponse = MethodResponse, - > + Send - + Sync - + Clone - + 'static, -{ - type MethodResponse = S::MethodResponse; - type NotificationResponse = S::NotificationResponse; - type BatchResponse = S::BatchResponse; - - fn call<'a>(&self, req: Request<'a>) -> impl Future + Send + 'a { - let service = self.service.clone(); - async move { intercept_or_forward(&service, req).await } - } - - fn batch<'a>(&self, req: Batch<'a>) -> impl Future + Send + 'a { - let service = self.service.clone(); - let max_response_body_size = self.max_response_body_size; - async move { - let mut builder = BatchResponseBuilder::new_with_limit(max_response_body_size); - let mut got_notif = false; - for entry in req { - match entry { - Ok(BatchEntry::Call(request)) => { - let response = intercept_or_forward(&service, request).await; - if let Err(too_large) = builder.append(response) { - return too_large; - } - } - Ok(BatchEntry::Notification(notification)) => { - got_notif = true; - service.notification(notification).await; - } - Err(err) => { - let (error, id) = err.into_parts(); - if let Err(too_large) = builder.append(MethodResponse::error(id, error)) { - return too_large; - } - } - } - } - if builder.is_empty() && got_notif { - return MethodResponse::notification(); - } - MethodResponse::from_batch(builder.finish()) - } - } - - fn notification<'a>( - &self, - n: Notification<'a>, - ) -> impl Future + Send + 'a { - self.service.notification(n) - } -} - -/// Intercepts pending-tx RPCs (error or null) or forwards to the inner service. -async fn intercept_or_forward<'a, S>(service: &S, req: Request<'a>) -> MethodResponse -where - S: RpcServiceT + Send + Sync, -{ - if let Err(err) = error_if_pending_tx_rpc(&req) { - return MethodResponse::error(req.id(), err); - } - if is_pool_pending_tx_lookup(&req) || is_pending_block_query(&req) { - return null_response(&req); - } - service.call(req).await -} - -/// RPC middleware that rejects raw transaction submission calls carrying a -/// pre-EIP-155 (replay-unprotected) transaction. -/// -/// Only the RPC submission path is gated — peer-gossiped transactions and -/// transactions included in blocks by other validators still flow through the -/// txpool and execution layers unchanged. -#[derive(Clone, Debug)] -pub struct RejectUnprotectedTxsMiddleware { - service: S, - allow_unprotected_txs: bool, - max_response_body_size: usize, -} - -impl RejectUnprotectedTxsMiddleware { - pub fn new(service: S) -> Self { - Self { - service, - allow_unprotected_txs: false, - max_response_body_size: usize::MAX, - } - } -} - -impl RpcServiceT for RejectUnprotectedTxsMiddleware -where - S: RpcServiceT< - MethodResponse = MethodResponse, - NotificationResponse = MethodResponse, - BatchResponse = MethodResponse, - > + Send - + Sync - + Clone - + 'static, -{ - type MethodResponse = S::MethodResponse; - type NotificationResponse = S::NotificationResponse; - type BatchResponse = S::BatchResponse; - - fn call<'a>(&self, req: Request<'a>) -> impl Future + Send + 'a { - let service = self.service.clone(); - let allow_unprotected_txs = self.allow_unprotected_txs; - async move { - if !allow_unprotected_txs { - if let Err(err) = error_if_unprotected_send_raw_tx(&req) { - return MethodResponse::error(req.id(), err); - } - } - service.call(req).await - } - } - - fn batch<'a>(&self, req: Batch<'a>) -> impl Future + Send + 'a { - let service = self.service.clone(); - let allow_unprotected_txs = self.allow_unprotected_txs; - let max_response_body_size = self.max_response_body_size; - // Build the batch response here so unprotected-tx rejection can be partial; - // forwarded entries still use `service.call()`, preserving inner middleware. - async move { - let mut builder = BatchResponseBuilder::new_with_limit(max_response_body_size); - let mut got_notif = false; - for entry in req { - match entry { - Ok(BatchEntry::Call(request)) => { - let response = if !allow_unprotected_txs { - if let Err(err) = error_if_unprotected_send_raw_tx(&request) { - MethodResponse::error(request.id(), err) - } else { - service.call(request).await - } - } else { - service.call(request).await - }; - if let Err(too_large) = builder.append(response) { - return too_large; - } - } - Ok(BatchEntry::Notification(notification)) => { - got_notif = true; - service.notification(notification).await; - } - Err(err) => { - let (error, id) = err.into_parts(); - if let Err(too_large) = builder.append(MethodResponse::error(id, error)) { - return too_large; - } - } - } - } - if builder.is_empty() && got_notif { - return MethodResponse::notification(); - } - MethodResponse::from_batch(builder.finish()) - } - } - - fn notification<'a>( - &self, - n: Notification<'a>, - ) -> impl Future + Send + 'a { - self.service.notification(n) - } -} - -/// Returns an error if the request is a raw transaction submission call with a -/// pre-EIP-155 (replay-unprotected) payload. -/// -/// Malformed payloads (bad hex, undecodable RLP) are forwarded unchanged so -/// the inner RPC handler can return its own precise error. -fn error_if_unprotected_send_raw_tx<'a>(req: &Request<'a>) -> Result<(), ErrorObject<'a>> { - if !is_raw_transaction_submission(req.method_name()) { - return Ok(()); - } - #[derive(serde::Deserialize)] - struct SendRawTransactionParams { - bytes: Bytes, - } - - let bytes = if req.params().is_object() { - let Ok(params) = req.params().parse::() else { - return Ok(()); - }; - params.bytes - } else { - let Ok((bytes,)) = req.params().parse::<(Bytes,)>() else { - return Ok(()); - }; - bytes - }; - let Ok(envelope) = TxEnvelope::decode_2718_exact(bytes.as_ref()) else { - return Ok(()); - }; - if envelope.is_replay_protected() { - return Ok(()); - } - Err(ErrorObjectOwned::owned::<()>( - UNPROTECTED_TX_ERROR_CODE, - UNPROTECTED_TX_ERROR_MSG, - None, - )) -} - -fn is_raw_transaction_submission(method: &str) -> bool { - method == ETH_SEND_RAW_TRANSACTION_METHOD || method == ETH_SEND_RAW_TRANSACTION_SYNC_METHOD -} - -/// Returns an error if the request is a pending-tx RPC (subscription or filter) that would leak pending transaction data. -fn error_if_pending_tx_rpc<'a>(req: &Request<'a>) -> Result<(), ErrorObject<'a>> { - if req.method_name() == ETH_NEW_PENDING_TX_FILTER_METHOD { - let error = ErrorObjectOwned::owned::<()>( - PENDING_TX_SUBSCRIPTION_ERROR_CODE, - "Pending transaction filters are not allowed", - None, - ); - return Err(error); - } - - if req.method_name() == ETH_SUBSCRIBE_METHOD { - // Parse parameters to check if it's for newPendingTransactions - if let Ok(Some(subscription_type)) = req.params().sequence().optional_next::() { - if subscription_type == PENDING_TX_SUBSCRIPTION_TYPE { - let error = ErrorObjectOwned::owned::<()>( - PENDING_TX_SUBSCRIPTION_ERROR_CODE, - "Subscriptions to pending transactions are not allowed", - None, - ); - return Err(error); - } - } - } - Ok(()) -} - -/// Returns true if the request queries the transaction pool directly for pending tx data. -fn is_pool_pending_tx_lookup(req: &Request<'_>) -> bool { - req.method_name() == ETH_GET_TX_BY_SENDER_AND_NONCE_METHOD -} - -/// Returns true if the request is `eth_getBlockByNumber("pending", ...)`. -/// -/// The consensus engine may briefly expose a pending block via `provider().pending_block()` -/// even when `--rpc.pending-block=none` is set. Intercepting at the middleware layer -/// guarantees a consistent `null` response regardless of consensus-engine state. -fn is_pending_block_query(req: &Request<'_>) -> bool { - if req.method_name() != ETH_GET_BLOCK_BY_NUMBER_METHOD { - return false; - } - if let Ok(Some(block_tag)) = req.params().sequence().optional_next::() { - return block_tag == PENDING_BLOCK_TAG; - } - false -} - -/// Builds a JSON-RPC success response containing `null`. -fn null_response(req: &Request<'_>) -> MethodResponse { - let payload = ResponsePayload::success(serde_json::Value::Null); - MethodResponse::response(req.id(), payload.into(), usize::MAX) -} - -#[cfg(test)] -mod tests { - use super::*; - use jsonrpsee::{ - types::{Id, ResponsePayload}, - BatchResponseBuilder, - }; - use serde_json::value::RawValue; - use std::borrow::Cow; - - /// Mock RPC service that always returns a success response - #[derive(Clone, Debug)] - struct MockRpcService; - - impl RpcServiceT for MockRpcService { - type MethodResponse = MethodResponse; - type NotificationResponse = MethodResponse; - type BatchResponse = MethodResponse; - - // Silence clippy false positive, see - #[allow(clippy::manual_async_fn)] - fn call<'a>( - &self, - req: Request<'a>, - ) -> impl Future + Send + 'a { - async move { - let payload = - ResponsePayload::success(serde_json::Value::String("success".to_string())); - MethodResponse::response(req.id(), payload.into(), usize::MAX) - } - } - - // Silence clippy false positive, see - #[allow(clippy::manual_async_fn)] - fn batch<'a>( - &self, - req: Batch<'a>, - ) -> impl Future + Send + 'a { - let service = self.clone(); - async move { - let mut response = BatchResponseBuilder::new_with_limit(usize::MAX); - for r in req { - match r { - Ok(BatchEntry::Call(request)) => { - let payload = ResponsePayload::success(serde_json::Value::String( - "success".to_string(), - )); - response - .append(MethodResponse::response( - request.id(), - payload.into(), - usize::MAX, - )) - .unwrap(); - } - Ok(BatchEntry::Notification(notification)) => { - response - .append(service.notification(notification).await) - .unwrap(); - } - Err(err) => { - let (error, id) = err.into_parts(); - response.append(MethodResponse::error(id, error)).unwrap(); - } - } - } - MethodResponse::from_batch(response.finish()) - } - } - - // Silence clippy false positive, see - #[allow(clippy::manual_async_fn)] - fn notification<'a>( - &self, - _n: Notification<'a>, - ) -> impl Future + Send + 'a { - async move { - let payload = ResponsePayload::success(serde_json::Value::String( - "notification_success".to_string(), - )); - MethodResponse::response(Id::Number(0), payload.into(), usize::MAX) - } - } - } - - fn create_request_with_params( - method: &str, - params: Box, - id: u64, - ) -> Request<'static> { - Request::owned(method.to_string(), Some(params), Id::Number(id)) - } - - // ── Middleware active (default) ───────────────────────────────────── - // - // When active, the middleware intercepts pending-state RPCs: - // subscriptions, filters, and block queries. - // The binary default is middleware ON; users opt out via - // --arc.expose-pending-txs on trusted/internal nodes. - - // -- pending txs: blocked -- - - #[tokio::test] - async fn test_enabled_blocks_pending_tx_subscription() { - let middleware = NoPendingTransactionsRpcMiddleware::new(MockRpcService); - let params = RawValue::from_string(r#"["newPendingTransactions"]"#.to_string()).unwrap(); - let request = create_request_with_params(ETH_SUBSCRIBE_METHOD, params, 1); - let response = middleware.call(request).await; - - assert!( - response.as_error_code().is_some(), - "filter_pending_txs=true should block newPendingTransactions subscription" - ); - assert_eq!( - response.as_error_code().unwrap(), - PENDING_TX_SUBSCRIPTION_ERROR_CODE - ); - } - - #[tokio::test] - async fn test_enabled_blocks_pending_tx_filter() { - let middleware = NoPendingTransactionsRpcMiddleware::new(MockRpcService); - let params = RawValue::from_string(r#"[]"#.to_string()).unwrap(); - let request = create_request_with_params(ETH_NEW_PENDING_TX_FILTER_METHOD, params, 1); - let response = middleware.call(request).await; - - assert!( - response.as_error_code().is_some(), - "filter_pending_txs=true should block eth_newPendingTransactionFilter" - ); - assert_eq!( - response.as_error_code().unwrap(), - PENDING_TX_SUBSCRIPTION_ERROR_CODE - ); - } - - // -- allowed subscriptions and methods -- - - #[tokio::test] - async fn test_enabled_allows_non_pending_subscriptions() { - let middleware = NoPendingTransactionsRpcMiddleware::new(MockRpcService); - let cases: &[(&str, &str)] = &[ - (r#"["newHeads"]"#, "newHeads"), - (r#"["logs"]"#, "logs"), - (r#"["syncing"]"#, "syncing"), - (r#"["NewPendingTransactions"]"#, "wrong-case pendingTx"), - ("[]", "empty params"), - ("[123]", "non-string params"), - ]; - for (params_json, label) in cases { - let params = RawValue::from_string(params_json.to_string()).unwrap(); - let request = create_request_with_params(ETH_SUBSCRIBE_METHOD, params, 1); - let response = middleware.call(request).await; - assert!( - response.as_error_code().is_none(), - "filter_pending_txs=true should allow {label}" - ); - } - } - - #[tokio::test] - async fn test_enabled_allows_non_pending_methods() { - let middleware = NoPendingTransactionsRpcMiddleware::new(MockRpcService); - let methods = &[ - "eth_blockNumber", - "eth_getBalance", - "eth_getTransactionByHash", - "eth_call", - "eth_newBlockFilter", - "net_version", - ]; - for method in methods { - let params = RawValue::from_string("[]".to_string()).unwrap(); - let request = create_request_with_params(method, params, 1); - let response = middleware.call(request).await; - assert!( - response.as_error_code().is_none(), - "filter_pending_txs=true should allow {method}" - ); - } - } - - #[tokio::test] - async fn test_enabled_allows_notifications() { - let middleware = NoPendingTransactionsRpcMiddleware::new(MockRpcService); - let notification_params = Some(Cow::Owned( - RawValue::from_string(r#"{"subscription":"0x1","result":"0x123"}"#.to_string()) - .unwrap(), - )); - let notification = - Notification::new(Cow::Borrowed("eth_subscription"), notification_params); - let response = middleware.notification(notification).await; - - assert!( - response.as_error_code().is_none(), - "filter_pending_txs=true should allow notifications" - ); - } - - // -- pending block: intercepted -- - // - // eth_getBlockByNumber("pending") returns null (success, not error). - // Other block tags ("latest", "0x1", etc.) pass through unchanged. - - #[tokio::test] - async fn test_enabled_pending_block_returns_null() { - let middleware = NoPendingTransactionsRpcMiddleware::new(MockRpcService); - let params = RawValue::from_string(r#"["pending", false]"#.to_string()).unwrap(); - let request = create_request_with_params(ETH_GET_BLOCK_BY_NUMBER_METHOD, params, 1); - let response = middleware.call(request).await; - - assert!( - response.as_error_code().is_none(), - "filter_pending_txs=true should return success (not error) for pending block" - ); - let json: serde_json::Value = serde_json::from_str(response.into_json().get()).unwrap(); - assert!( - json["result"].is_null(), - "filter_pending_txs=true should return null for pending block" - ); - } - - #[tokio::test] - async fn test_enabled_pending_block_full_txs_returns_null() { - let middleware = NoPendingTransactionsRpcMiddleware::new(MockRpcService); - let params = RawValue::from_string(r#"["pending", true]"#.to_string()).unwrap(); - let request = create_request_with_params(ETH_GET_BLOCK_BY_NUMBER_METHOD, params, 2); - let response = middleware.call(request).await; - - let json: serde_json::Value = serde_json::from_str(response.into_json().get()).unwrap(); - assert!( - json["result"].is_null(), - "filter_pending_txs=true should return null for pending block with full txs" - ); - } - - #[tokio::test] - async fn test_enabled_latest_block_passes_through() { - let middleware = NoPendingTransactionsRpcMiddleware::new(MockRpcService); - let params = RawValue::from_string(r#"["latest", false]"#.to_string()).unwrap(); - let request = create_request_with_params(ETH_GET_BLOCK_BY_NUMBER_METHOD, params, 3); - let response = middleware.call(request).await; - - assert!( - response.as_error_code().is_none(), - "filter_pending_txs=true should allow getBlockByNumber(\"latest\")" - ); - } - - #[tokio::test] - async fn test_enabled_numbered_block_passes_through() { - let middleware = NoPendingTransactionsRpcMiddleware::new(MockRpcService); - let params = RawValue::from_string(r#"["0x1", false]"#.to_string()).unwrap(); - let request = create_request_with_params(ETH_GET_BLOCK_BY_NUMBER_METHOD, params, 4); - let response = middleware.call(request).await; - - assert!( - response.as_error_code().is_none(), - "filter_pending_txs=true should allow getBlockByNumber(\"0x1\")" - ); - } - - // -- pool pending tx lookup: intercepted -- - // - // eth_getTransactionBySenderAndNonce returns null (success, not error) - // because it directly queries the pool for pending tx contents. - - #[tokio::test] - async fn test_enabled_pool_lookup_returns_null() { - let middleware = NoPendingTransactionsRpcMiddleware::new(MockRpcService); - let params = RawValue::from_string("[]".to_string()).unwrap(); - let request = create_request_with_params(ETH_GET_TX_BY_SENDER_AND_NONCE_METHOD, params, 1); - let response = middleware.call(request).await; - - assert!( - response.as_error_code().is_none(), - "should return success, not error" - ); - let json: serde_json::Value = serde_json::from_str(response.into_json().get()).unwrap(); - assert!( - json["result"].is_null(), - "should return null for pool lookup" - ); - } - - #[tokio::test] - async fn test_enabled_pool_lookup_with_params_returns_null() { - let middleware = NoPendingTransactionsRpcMiddleware::new(MockRpcService); - let params = RawValue::from_string( - r#"["0x1234567890abcdef1234567890abcdef12345678", "0x0"]"#.to_string(), - ) - .unwrap(); - let request = create_request_with_params(ETH_GET_TX_BY_SENDER_AND_NONCE_METHOD, params, 2); - let response = middleware.call(request).await; - - assert!( - response.as_error_code().is_none(), - "should return success, not error" - ); - let json: serde_json::Value = serde_json::from_str(response.into_json().get()).unwrap(); - assert!( - json["result"].is_null(), - "should return null for pool lookup with params" - ); - } - - // -- batch requests -- - // - // Batch entries are intercepted per-entry, consistent with the single-request path: - // subscriptions/filters → error, pool lookups/pending block → null. - - #[tokio::test] - async fn test_enabled_batch_blocks_pending_tx_subscription() { - let middleware = NoPendingTransactionsRpcMiddleware::new(MockRpcService); - let batch = Batch::from(vec![ - Ok(BatchEntry::Call(create_request_with_params( - "eth_blockNumber", - RawValue::from_string("[]".to_string()).unwrap(), - 1, - ))), - Ok(BatchEntry::Call(create_request_with_params( - ETH_SUBSCRIBE_METHOD, - RawValue::from_string(r#"["newPendingTransactions"]"#.to_string()).unwrap(), - 2, - ))), - ]); - let response = middleware.batch(batch).await; - let json = response.into_json(); - let responses: Vec = serde_json::from_str(json.get()).unwrap(); - - assert!(responses[0].get("result").is_some()); - assert!(responses[1].get("error").is_some()); - let error_code = responses[1]["error"]["code"].as_i64().unwrap(); - assert_eq!(error_code, PENDING_TX_SUBSCRIPTION_ERROR_CODE as i64); - } - - #[tokio::test] - async fn test_enabled_batch_blocks_pending_tx_filter() { - let middleware = NoPendingTransactionsRpcMiddleware::new(MockRpcService); - let batch = Batch::from(vec![ - Ok(BatchEntry::Call(create_request_with_params( - "eth_blockNumber", - RawValue::from_string("[]".to_string()).unwrap(), - 1, - ))), - Ok(BatchEntry::Call(create_request_with_params( - ETH_NEW_PENDING_TX_FILTER_METHOD, - RawValue::from_string(r#"[]"#.to_string()).unwrap(), - 2, - ))), - ]); - let response = middleware.batch(batch).await; - let json = response.into_json(); - let responses: Vec = serde_json::from_str(json.get()).unwrap(); - - assert!(responses[0].get("result").is_some()); - assert_eq!( - responses[1]["error"]["code"].as_i64().unwrap(), - PENDING_TX_SUBSCRIPTION_ERROR_CODE as i64 - ); - } - - #[tokio::test] - async fn test_enabled_batch_pending_block_returns_null() { - let middleware = NoPendingTransactionsRpcMiddleware::new(MockRpcService); - let batch = Batch::from(vec![ - Ok(BatchEntry::Call(create_request_with_params( - "eth_blockNumber", - RawValue::from_string("[]".to_string()).unwrap(), - 1, - ))), - Ok(BatchEntry::Call(create_request_with_params( - ETH_GET_BLOCK_BY_NUMBER_METHOD, - RawValue::from_string(r#"["pending", false]"#.to_string()).unwrap(), - 2, - ))), - ]); - let response = middleware.batch(batch).await; - let json = response.into_json(); - let responses: Vec = serde_json::from_str(json.get()).unwrap(); - - assert_eq!(responses.len(), 2); - assert_eq!(responses[0]["result"], "success"); - assert!( - responses[1]["result"].is_null(), - "batch pending block should return null" - ); - } - - #[tokio::test] - async fn test_enabled_batch_pool_lookup_returns_null() { - let middleware = NoPendingTransactionsRpcMiddleware::new(MockRpcService); - let batch = Batch::from(vec![ - Ok(BatchEntry::Call(create_request_with_params( - "eth_blockNumber", - RawValue::from_string("[]".to_string()).unwrap(), - 1, - ))), - Ok(BatchEntry::Call(create_request_with_params( - ETH_GET_TX_BY_SENDER_AND_NONCE_METHOD, - RawValue::from_string( - r#"["0x1234567890abcdef1234567890abcdef12345678", "0x0"]"#.to_string(), - ) - .unwrap(), - 2, - ))), - ]); - let response = middleware.batch(batch).await; - let json = response.into_json(); - let responses: Vec = serde_json::from_str(json.get()).unwrap(); - - assert_eq!(responses.len(), 2); - assert_eq!(responses[0]["result"], "success"); - assert!( - responses[1]["result"].is_null(), - "batch pool lookup should return null" - ); - } - - #[tokio::test] - async fn test_enabled_batch_mixed_interceptions() { - let middleware = NoPendingTransactionsRpcMiddleware::new(MockRpcService); - let batch = Batch::from(vec![ - // 0: normal method → success - Ok(BatchEntry::Call(create_request_with_params( - "eth_blockNumber", - RawValue::from_string("[]".to_string()).unwrap(), - 1, - ))), - // 1: pool lookup → null - Ok(BatchEntry::Call(create_request_with_params( - ETH_GET_TX_BY_SENDER_AND_NONCE_METHOD, - RawValue::from_string("[]".to_string()).unwrap(), - 2, - ))), - // 2: pending block → null - Ok(BatchEntry::Call(create_request_with_params( - ETH_GET_BLOCK_BY_NUMBER_METHOD, - RawValue::from_string(r#"["pending", false]"#.to_string()).unwrap(), - 3, - ))), - // 3: pending tx subscription → error - Ok(BatchEntry::Call(create_request_with_params( - ETH_SUBSCRIBE_METHOD, - RawValue::from_string(r#"["newPendingTransactions"]"#.to_string()).unwrap(), - 4, - ))), - ]); - let response = middleware.batch(batch).await; - let json = response.into_json(); - let responses: Vec = serde_json::from_str(json.get()).unwrap(); - - assert_eq!(responses.len(), 4); - assert_eq!( - responses[0]["result"], "success", - "normal method should succeed" - ); - assert!( - responses[1]["result"].is_null(), - "pool lookup should return null" - ); - assert!( - responses[2]["result"].is_null(), - "pending block should return null" - ); - assert!( - responses[3].get("error").is_some(), - "pending tx subscription should error" - ); - } - - #[tokio::test] - async fn test_enabled_batch_notification_excluded_from_response() { - let middleware = NoPendingTransactionsRpcMiddleware::new(MockRpcService); - let batch = Batch::from(vec![ - Ok(BatchEntry::Call(create_request_with_params( - "eth_blockNumber", - RawValue::from_string("[]".to_string()).unwrap(), - 1, - ))), - Ok(BatchEntry::Notification(Notification::new( - Cow::Borrowed("eth_subscription"), - Some(Cow::Owned( - RawValue::from_string(r#"{"subscription":"0x1","result":"0x123"}"#.to_string()) - .unwrap(), - )), - ))), - Ok(BatchEntry::Call(create_request_with_params( - "eth_chainId", - RawValue::from_string("[]".to_string()).unwrap(), - 2, - ))), - ]); - let response = middleware.batch(batch).await; - let json = response.into_json(); - let responses: Vec = serde_json::from_str(json.get()).unwrap(); - - assert_eq!( - responses.len(), - 2, - "notification must not produce a response entry" - ); - assert_eq!(responses[0]["result"], "success"); - assert_eq!(responses[1]["result"], "success"); - } - - // ── Middleware disabled (--arc.expose-pending-txs) ────────────────── - // - // The middleware is bypassed entirely. All requests pass through. - - #[tokio::test] - async fn test_disabled_allows_pending_tx_subscription() { - let layer = ArcRpcLayer { - filter_pending_txs: false, - ..Default::default() - }; - let middleware = layer.layer(MockRpcService); - let params = RawValue::from_string(r#"["newPendingTransactions"]"#.to_string()).unwrap(); - let request = create_request_with_params(ETH_SUBSCRIBE_METHOD, params, 99); - let response = middleware.call(request).await; - - assert!( - response.as_error_code().is_none(), - "filter_pending_txs=false should allow newPendingTransactions" - ); - let json: serde_json::Value = serde_json::from_str(response.into_json().get()).unwrap(); - assert_eq!( - json["result"], "success", - "filter_pending_txs=false should forward to inner service" - ); - } - - #[tokio::test] - async fn test_disabled_allows_pending_tx_filter() { - let layer = ArcRpcLayer { - filter_pending_txs: false, - ..Default::default() - }; - let middleware = layer.layer(MockRpcService); - let params = RawValue::from_string(r#"[]"#.to_string()).unwrap(); - let request = create_request_with_params(ETH_NEW_PENDING_TX_FILTER_METHOD, params, 101); - let response = middleware.call(request).await; - assert!( - response.as_error_code().is_none(), - "filter_pending_txs=false should allow newPendingTransactionFilter" - ); - let json: serde_json::Value = serde_json::from_str(response.into_json().get()).unwrap(); - assert_eq!( - json["result"], "success", - "filter_pending_txs=false should forward to inner service" - ); - } - - #[tokio::test] - async fn test_disabled_allows_pool_lookup() { - let layer = ArcRpcLayer { - filter_pending_txs: false, - ..Default::default() - }; - let middleware = layer.layer(MockRpcService); - let params = RawValue::from_string( - r#"["0x1234567890abcdef1234567890abcdef12345678", "0x0"]"#.to_string(), - ) - .unwrap(); - let request = - create_request_with_params(ETH_GET_TX_BY_SENDER_AND_NONCE_METHOD, params, 102); - let response = middleware.call(request).await; - assert!( - response.as_error_code().is_none(), - "filter_pending_txs=false should allow eth_getTransactionBySenderAndNonce" - ); - let json: serde_json::Value = serde_json::from_str(response.into_json().get()).unwrap(); - assert_eq!( - json["result"], "success", - "filter_pending_txs=false should forward to inner service" - ); - } - - #[tokio::test] - async fn test_disabled_allows_pending_block() { - let layer = ArcRpcLayer { - filter_pending_txs: false, - ..Default::default() - }; - let middleware = layer.layer(MockRpcService); - let params = RawValue::from_string(r#"["pending", false]"#.to_string()).unwrap(); - let request = create_request_with_params(ETH_GET_BLOCK_BY_NUMBER_METHOD, params, 1); - let response = middleware.call(request).await; - - assert!( - response.as_error_code().is_none(), - "filter_pending_txs=false should allow getBlockByNumber(\"pending\")" - ); - let json: serde_json::Value = serde_json::from_str(response.into_json().get()).unwrap(); - assert_eq!( - json["result"], "success", - "filter_pending_txs=false should forward to inner service" - ); - } - - // ── ArcRpcLayer::default() ────────────────────────────────────────── - - #[test] - fn test_arc_rpc_layer_default_has_filter_enabled() { - let layer = ArcRpcLayer::default(); - assert!( - layer.filter_pending_txs, - "Default ArcRpcLayer should have filter enabled (opt out via --arc.expose-pending-txs)" - ); - } - - #[test] - fn test_arc_rpc_layer_default_max_batch_entries() { - let layer = ArcRpcLayer::default(); - assert_eq!(layer.max_batch_entries, ARC_RPC_MAX_BATCH_ENTRIES_DEFAULT); - } - - // ── BatchSizeLimitMiddleware ──────────────────────────────────────── - - fn make_call_entry(method: &str, id: u64) -> BatchEntry<'static> { - BatchEntry::Call(create_request_with_params( - method, - RawValue::from_string("[]".to_string()).unwrap(), - id, - )) - } - - #[tokio::test] - async fn test_batch_size_limit_at_limit_passes_through() { - let middleware = BatchSizeLimitMiddleware::new(MockRpcService, 3); - let batch = Batch::from(vec![ - Ok(make_call_entry("eth_blockNumber", 1)), - Ok(make_call_entry("eth_blockNumber", 2)), - Ok(make_call_entry("eth_blockNumber", 3)), - ]); - let response = middleware.batch(batch).await; - let responses: Vec = - serde_json::from_str(response.into_json().get()).unwrap(); - assert_eq!(responses.len(), 3); - for r in &responses { - assert_eq!(r["result"], "success"); - } - } - - #[tokio::test] - async fn test_batch_size_limit_below_limit_passes_through() { - let middleware = BatchSizeLimitMiddleware::new(MockRpcService, 10); - let batch = Batch::from(vec![ - Ok(make_call_entry("eth_blockNumber", 1)), - Ok(make_call_entry("eth_chainId", 2)), - ]); - let response = middleware.batch(batch).await; - let responses: Vec = - serde_json::from_str(response.into_json().get()).unwrap(); - assert_eq!(responses.len(), 2); - } - - #[tokio::test] - async fn test_batch_size_limit_above_limit_rejected() { - let middleware = BatchSizeLimitMiddleware::new(MockRpcService, 2); - let batch = Batch::from(vec![ - Ok(make_call_entry("eth_blockNumber", 1)), - Ok(make_call_entry("eth_blockNumber", 2)), - Ok(make_call_entry("eth_blockNumber", 3)), - ]); - let response = middleware.batch(batch).await; - assert_eq!( - response.as_error_code(), - Some(BATCH_TOO_LARGE_ERROR_CODE), - "oversized batch should be rejected with BATCH_TOO_LARGE_ERROR_CODE" - ); - let json: serde_json::Value = serde_json::from_str(response.into_json().get()).unwrap(); - assert!(json["id"].is_null(), "batch rejection should use Id::Null"); - let message = json["error"]["message"].as_str().unwrap_or(""); - assert!( - message.contains("batch size 3") && message.contains("limit of 2"), - "error message should name the offending size and limit; got: {message}" - ); - } - - #[tokio::test] - async fn test_batch_size_limit_single_call_passes_through() { - let middleware = BatchSizeLimitMiddleware::new(MockRpcService, 1); - let params = RawValue::from_string("[]".to_string()).unwrap(); - let request = create_request_with_params("eth_blockNumber", params, 1); - let response = middleware.call(request).await; - let json: serde_json::Value = serde_json::from_str(response.into_json().get()).unwrap(); - assert_eq!(json["result"], "success"); - } - - #[tokio::test] - async fn test_batch_size_limit_composes_with_pending_filter() { - // ArcRpcLayer composes BatchSizeLimit ∘ NoPendingTransactions. Verify that - // an oversized batch is rejected before the pending-tx filter iterates entries. - let layer = ArcRpcLayer { - filter_pending_txs: true, - max_batch_entries: 2, - ..Default::default() - }; - let middleware = layer.layer(MockRpcService); - let batch = Batch::from(vec![ - Ok(make_call_entry("eth_blockNumber", 1)), - Ok(make_call_entry("eth_blockNumber", 2)), - Ok(make_call_entry("eth_blockNumber", 3)), - ]); - let response = middleware.batch(batch).await; - assert_eq!(response.as_error_code(), Some(BATCH_TOO_LARGE_ERROR_CODE)); - } - - #[tokio::test] - async fn test_batch_size_limit_applies_when_pending_filter_disabled() { - // The batch cap must apply even when --arc.expose-pending-txs disables - // the pending-tx filter (the other half of the composition). - let layer = ArcRpcLayer { - filter_pending_txs: false, - max_batch_entries: 2, - ..Default::default() - }; - let middleware = layer.layer(MockRpcService); - let batch = Batch::from(vec![ - Ok(make_call_entry("eth_blockNumber", 1)), - Ok(make_call_entry("eth_blockNumber", 2)), - Ok(make_call_entry("eth_blockNumber", 3)), - ]); - let response = middleware.batch(batch).await; - assert_eq!(response.as_error_code(), Some(BATCH_TOO_LARGE_ERROR_CODE)); - } - - // ── RejectUnprotectedTxsMiddleware ────────────────────────────────── - - use alloy_consensus::{SignableTransaction, TxLegacy}; - use alloy_network::eip2718::Encodable2718; - use alloy_primitives::{hex, Address, Signature, TxKind, U256}; - - /// Dummy non-zero signature scalars. Sufficient for envelope decoding; we - /// never recover the signer or verify the signature in middleware. - fn dummy_signature() -> Signature { - Signature::new(U256::from(1u64), U256::from(1u64), false) - } - - fn legacy_tx(chain_id: Option) -> TxLegacy { - TxLegacy { - chain_id, - nonce: 0, - gas_price: 1_000_000_000, - gas_limit: 21_000, - to: TxKind::Call(Address::ZERO), - value: U256::ZERO, - input: Default::default(), - } - } - - fn encode_legacy_bytes(chain_id: Option) -> Vec { - let tx = legacy_tx(chain_id); - let signed = tx.into_signed(dummy_signature()); - let envelope: TxEnvelope = signed.into(); - envelope.encoded_2718() - } - - fn encode_legacy_raw(chain_id: Option) -> String { - format!("0x{}", hex::encode(encode_legacy_bytes(chain_id))) - } - - fn raw_tx_request_with_params(method: &str, params_json: String, id: u64) -> Request<'static> { - let params = RawValue::from_string(params_json).unwrap(); - create_request_with_params(method, params, id) - } - - fn send_raw_tx_request_with_params(params_json: String, id: u64) -> Request<'static> { - raw_tx_request_with_params(ETH_SEND_RAW_TRANSACTION_METHOD, params_json, id) - } - - fn send_raw_tx_request(raw_hex: &str, id: u64) -> Request<'static> { - send_raw_tx_request_with_params(format!("[\"{raw_hex}\"]"), id) - } - - fn send_raw_tx_sync_request(raw_hex: &str, id: u64) -> Request<'static> { - raw_tx_request_with_params( - ETH_SEND_RAW_TRANSACTION_SYNC_METHOD, - format!("[\"{raw_hex}\"]"), - id, - ) - } - - fn send_raw_tx_object_request(raw_hex: &str, id: u64) -> Request<'static> { - send_raw_tx_request_with_params(format!("{{\"bytes\":\"{raw_hex}\"}}"), id) - } - - fn send_raw_tx_bytes_request(bytes: &[u8], id: u64) -> Request<'static> { - let bytes_json = serde_json::to_string(bytes).unwrap(); - send_raw_tx_request_with_params(format!("[{bytes_json}]"), id) - } - - fn send_raw_tx_object_bytes_request(bytes: &[u8], id: u64) -> Request<'static> { - let bytes_json = serde_json::to_string(bytes).unwrap(); - send_raw_tx_request_with_params(format!("{{\"bytes\":{bytes_json}}}"), id) - } - - fn assert_unprotected_tx_rejected(response: MethodResponse) { - assert_eq!( - response.as_error_code(), - Some(UNPROTECTED_TX_ERROR_CODE), - "pre-EIP-155 tx must be rejected with UNPROTECTED_TX_ERROR_CODE" - ); - let json: serde_json::Value = serde_json::from_str(response.into_json().get()).unwrap(); - assert_eq!(json["error"]["message"], UNPROTECTED_TX_ERROR_MSG); - } - - #[tokio::test] - async fn test_rejects_pre_eip155_send_raw_transaction() { - let middleware = RejectUnprotectedTxsMiddleware::new(MockRpcService); - let request = send_raw_tx_request(&encode_legacy_raw(None), 1); - let response = middleware.call(request).await; - - assert_unprotected_tx_rejected(response); - } - - #[tokio::test] - async fn test_rejects_pre_eip155_send_raw_transaction_sync() { - let middleware = RejectUnprotectedTxsMiddleware::new(MockRpcService); - let request = send_raw_tx_sync_request(&encode_legacy_raw(None), 1); - let response = middleware.call(request).await; - - assert_unprotected_tx_rejected(response); - } - - #[tokio::test] - async fn test_rejects_pre_eip155_send_raw_transaction_object_hex_params() { - let middleware = RejectUnprotectedTxsMiddleware::new(MockRpcService); - let request = send_raw_tx_object_request(&encode_legacy_raw(None), 1); - let response = middleware.call(request).await; - - assert_unprotected_tx_rejected(response); - } - - #[tokio::test] - async fn test_rejects_pre_eip155_send_raw_transaction_positional_bytes_params() { - let middleware = RejectUnprotectedTxsMiddleware::new(MockRpcService); - let bytes = encode_legacy_bytes(None); - let request = send_raw_tx_bytes_request(&bytes, 1); - let response = middleware.call(request).await; - - assert_unprotected_tx_rejected(response); - } - - #[tokio::test] - async fn test_rejects_pre_eip155_send_raw_transaction_object_bytes_params() { - let middleware = RejectUnprotectedTxsMiddleware::new(MockRpcService); - let bytes = encode_legacy_bytes(None); - let request = send_raw_tx_object_bytes_request(&bytes, 1); - let response = middleware.call(request).await; - - assert_unprotected_tx_rejected(response); - } - - #[tokio::test] - async fn test_allows_eip155_send_raw_transaction() { - let middleware = RejectUnprotectedTxsMiddleware::new(MockRpcService); - let request = send_raw_tx_request(&encode_legacy_raw(Some(1337)), 2); - let response = middleware.call(request).await; - - assert!( - response.as_error_code().is_none(), - "EIP-155-protected legacy tx must be forwarded to the inner service" - ); - let json: serde_json::Value = serde_json::from_str(response.into_json().get()).unwrap(); - assert_eq!(json["result"], "success"); - } - - #[tokio::test] - async fn test_malformed_raw_tx_forwarded() { - // Bad hex / undecodable RLP must reach the inner handler so it can - // return its own precise error — middleware must not pre-empt. - let middleware = RejectUnprotectedTxsMiddleware::new(MockRpcService); - let request = send_raw_tx_request("0xdeadbeef", 4); - let response = middleware.call(request).await; - - assert!( - response.as_error_code().is_none(), - "undecodable raw tx must be forwarded to inner handler" - ); - } - - #[tokio::test] - async fn test_raw_tx_with_trailing_bytes_forwarded() { - // Trailing bytes make the payload malformed. The middleware must not - // convert that into its replay-protection error. - let middleware = RejectUnprotectedTxsMiddleware::new(MockRpcService); - let mut bytes = encode_legacy_bytes(None); - bytes.push(0); - let raw_hex = format!("0x{}", hex::encode(bytes)); - let request = send_raw_tx_request(&raw_hex, 5); - let response = middleware.call(request).await; - - assert!( - response.as_error_code().is_none(), - "raw tx with trailing bytes must be forwarded to inner handler" - ); - let json: serde_json::Value = serde_json::from_str(response.into_json().get()).unwrap(); - assert_eq!(json["result"], "success"); - } - - #[tokio::test] - async fn test_batch_partial_reject() { - // A mixed batch: only the unprotected entry must be rejected; the - // protected and unrelated entries must reach the inner handler. - let middleware = RejectUnprotectedTxsMiddleware::new(MockRpcService); - let unprotected = send_raw_tx_request(&encode_legacy_raw(None), 1); - let protected = send_raw_tx_request(&encode_legacy_raw(Some(1337)), 2); - let other = create_request_with_params( - "eth_blockNumber", - RawValue::from_string("[]".to_string()).unwrap(), - 3, - ); - let batch = Batch::from(vec![ - Ok(BatchEntry::Call(unprotected)), - Ok(BatchEntry::Call(protected)), - Ok(BatchEntry::Call(other)), - ]); - let response = middleware.batch(batch).await; - let responses: Vec = - serde_json::from_str(response.into_json().get()).unwrap(); - - assert_eq!(responses.len(), 3); - assert_eq!(responses[0]["error"]["code"], UNPROTECTED_TX_ERROR_CODE); - assert_eq!(responses[0]["error"]["message"], UNPROTECTED_TX_ERROR_MSG); - assert_eq!(responses[1]["result"], "success"); - assert_eq!(responses[2]["result"], "success"); - } - - #[tokio::test] - async fn test_batch_partial_rejects_send_raw_transaction_sync() { - let middleware = RejectUnprotectedTxsMiddleware::new(MockRpcService); - let unprotected = send_raw_tx_sync_request(&encode_legacy_raw(None), 1); - let other = create_request_with_params( - "eth_blockNumber", - RawValue::from_string("[]".to_string()).unwrap(), - 2, - ); - let batch = Batch::from(vec![ - Ok(BatchEntry::Call(unprotected)), - Ok(BatchEntry::Call(other)), - ]); - let response = middleware.batch(batch).await; - let responses: Vec = - serde_json::from_str(response.into_json().get()).unwrap(); - - assert_eq!(responses.len(), 2); - assert_eq!(responses[0]["error"]["code"], UNPROTECTED_TX_ERROR_CODE); - assert_eq!(responses[0]["error"]["message"], UNPROTECTED_TX_ERROR_MSG); - assert_eq!(responses[1]["result"], "success"); - } - - #[tokio::test] - async fn test_default_layer_rejects_unprotected_and_pending_entries_in_batch() { - let layer = ArcRpcLayer::default(); - let middleware = layer.layer(MockRpcService); - let unprotected = send_raw_tx_request(&encode_legacy_raw(None), 1); - let pending_subscription = create_request_with_params( - ETH_SUBSCRIBE_METHOD, - RawValue::from_string(r#"["newPendingTransactions"]"#.to_string()).unwrap(), - 2, - ); - let other = create_request_with_params( - "eth_blockNumber", - RawValue::from_string("[]".to_string()).unwrap(), - 3, - ); - let batch = Batch::from(vec![ - Ok(BatchEntry::Call(unprotected)), - Ok(BatchEntry::Call(pending_subscription)), - Ok(BatchEntry::Call(other)), - ]); - let response = middleware.batch(batch).await; - let responses: Vec = - serde_json::from_str(response.into_json().get()).unwrap(); - - assert_eq!(responses.len(), 3); - assert_eq!(responses[0]["error"]["code"], UNPROTECTED_TX_ERROR_CODE); - assert_eq!(responses[0]["error"]["message"], UNPROTECTED_TX_ERROR_MSG); - assert_eq!( - responses[1]["error"]["code"], - PENDING_TX_SUBSCRIPTION_ERROR_CODE - ); - assert_eq!(responses[2]["result"], "success"); - } - - #[tokio::test] - async fn test_flag_off_via_layer_allows_unprotected() { - // With --arc.rpc.allow-unprotected-txs, the rejecting middleware is - // bypassed entirely and pre-EIP-155 txs reach the inner service. - let layer = ArcRpcLayer { - allow_unprotected_txs: true, - ..Default::default() - }; - let middleware = layer.layer(MockRpcService); - let request = send_raw_tx_request(&encode_legacy_raw(None), 10); - let response = middleware.call(request).await; - - assert!( - response.as_error_code().is_none(), - "allow_unprotected_txs=true must let pre-EIP-155 txs through" - ); - let json: serde_json::Value = serde_json::from_str(response.into_json().get()).unwrap(); - assert_eq!(json["result"], "success"); - } -} diff --git a/crates/evm-specs-tests/Cargo.toml b/crates/evm-specs-tests/Cargo.toml deleted file mode 100644 index eddb88a..0000000 --- a/crates/evm-specs-tests/Cargo.toml +++ /dev/null @@ -1,40 +0,0 @@ -[package] -name = "arc-evm-specs-tests" -version = "0.1.0" -edition.workspace = true -license.workspace = true -publish.workspace = true - -[[bin]] -name = "arc-evm-specs-tests" -path = "src/main.rs" - -[dependencies] -alloy-primitives = { workspace = true, features = ["std", "rlp"] } -alloy-rlp = { workspace = true } - -alloy-trie = { workspace = true, features = ["ethereum"] } - -arc-evm = { workspace = true } -arc-execution-config = { workspace = true, features = ["test-utils"] } -arc-version = { workspace = true } - -clap = { workspace = true, features = ["derive"] } - -hash-db = { workspace = true } -plain_hasher = { workspace = true } -reth-evm = { workspace = true } - -revm = { workspace = true, features = ["std", "serde"] } -revm-context-interface = { workspace = true } -revm-database = { workspace = true, features = ["std"] } -revm-inspector = { workspace = true, features = ["tracer"] } -revm-primitives = { workspace = true } -revm-statetest-types = { workspace = true } -serde = { workspace = true, features = ["derive"] } -serde_json = { workspace = true, features = ["std"] } -thiserror = { workspace = true } -triehash = { workspace = true } - -[lints] -workspace = true diff --git a/crates/evm-specs-tests/README.md b/crates/evm-specs-tests/README.md deleted file mode 100644 index 503f211..0000000 --- a/crates/evm-specs-tests/README.md +++ /dev/null @@ -1,179 +0,0 @@ -# arc-evm-specs-tests - -`arc-evm-specs-tests` is a fixture-consumer binary for `arc-execution-specs`. - -Its purpose is: -- execute Ethereum state-test fixtures through the ARC EVM execution path -- expose that execution via a CLI binary that `arc-execution-specs` can call through `consume direct` - -## How It Simulates ARC EVM Execution - -`arc-evm-specs-tests` does not implement a second EVM. It is a harness that drives the existing ARC execution layer from `arc-evm` (`crates/evm`). - -The runner implementation and design are sourced from upstream `revm`: - -- source: -- statetest runner shape: [`revme/src/cmd/statetest/runner.rs`](https://github.com/bluealloy/revm/blob/main/bins/revme/src/cmd/statetest/runner.rs) -- root helper shape: [`revme/src/cmd/statetest/merkle_trie.rs`](https://github.com/bluealloy/revm/blob/main/bins/revme/src/cmd/statetest/merkle_trie.rs) - -Execution flow: -1. `arc-evm-specs-tests` command entrypoint loads fixtures (`statetest` command). -2. It builds an `ArcEvmFactory` from `arc-evm`. -3. For each fixture test case, it builds env/tx input and calls `factory.create_evm(...)`. -4. It executes with `evm.transact_commit(tx)`. -5. It validates logs hash and state root against fixture expectations and returns normalized error classes/kinds. - -That means the transaction execution engine is ARC EVM code (from `crates/evm/src/evm.rs`), while `arc-evm-specs-tests` is the harness around it. - -## Execution Mode - -Current adapter setup uses `LOCAL_DEV`, which means ARC localdev executes with the ARC hardforks active in that chain spec. - -The fixture fork name still selects the Ethereum `cfg.spec`, but the underlying executor is ARC localdev. - -This is an ARC-mode harness, not a pure Ethereum fork-isolated runner: -- the fixture fork still chooses the REVM `cfg.spec` -- the chain-level execution context is still ARC `LOCAL_DEV` -- ARC localdev behavior and ARC hardfork activation can therefore influence results even when the fixture fork is pre-ARC or pre-feature - -Interpretation rule: -- use this runner to measure how ARC localdev behaves when driven by Ethereum statetest fixtures -- do not interpret the results as “stock Ethereum execution for that fixture fork in isolation” - -If you need pure Ethereum fork-isolated validation, that should be a separate runner mode with a different chain-spec contract. - - -## Temporary Upstream Workarounds - -This crate currently uses a two-pass fixture parse because `revm-statetest-types` does not yet expose all fields needed by ARC's harness flow. - -- Verified against: `revm-statetest-types = 14.x` -- Workaround details: - - extract `config.chainid` from raw JSON before typed deserialization - - sanitize unsupported fields (`receipt`, `state` -> `postState`) before deserializing into `TestSuite` -- Removal condition: delete this workaround once upstream typed deserialization includes `config.chainid` and fixture fields needed for direct `TestSuite` parsing. - -## How `arc-execution-specs` Consumes It - -`arc-execution-specs` runs this binary through: - -```bash -uv --project packages/testing run --python 3.12 consume direct \ - --bin $HOME/crcl/arc-node-project-name/target/release/arc-evm-specs-tests \ - --input $HOME/crcl/arc-execution-specs/fixtures/state_tests/for_prague \ - -m state_test -q -``` - -Integration contract: -- producer side (arc-node repo): build `arc-evm-specs-tests` binary and keep Rust health (`cargo test`, `cargo build`) -- consumer side (`arc-execution-specs`): drive fixtures, collect pass/fail output, and report compatibility metrics - -## Build - -```bash -cd $HOME/crcl/arc-node-project-name -cargo build --release -p arc-evm-specs-tests -``` - -Binary: - -```text -$HOME/crcl/arc-node-project-name/target/release/arc-evm-specs-tests -``` - -## Consume Prague State Tests - -```bash -cd $HOME/crcl/arc-execution-specs -uv --project packages/testing run --python 3.12 consume direct \ - --bin $HOME/crcl/arc-node-project-name/target/release/arc-evm-specs-tests \ - --input $HOME/crcl/arc-execution-specs/fixtures/state_tests/for_prague \ - -m state_test -q -``` - -## Focused Retest Examples - -`extcodehash_via_call`: - -```bash -cd $HOME/crcl/arc-execution-specs -uv --project packages/testing run --python 3.12 consume direct \ - --bin $HOME/crcl/arc-node-project-name/target/release/arc-evm-specs-tests \ - --input $HOME/crcl/arc-execution-specs/fixtures/state_tests/for_prague \ - -m state_test -k "test_extcodehash_via_call and fork_Prague" --maxfail=1 -ra -``` - -`precompile_absence`: - -```bash -cd $HOME/crcl/arc-execution-specs -uv --project packages/testing run --python 3.12 consume direct \ - --bin $HOME/crcl/arc-node-project-name/target/release/arc-evm-specs-tests \ - --input $HOME/crcl/arc-execution-specs/fixtures/state_tests/for_prague \ - -m state_test -k "test_precompile_absence and fork_Prague" --maxfail=1 -ra -``` - -## Error Classes in Adaptor Output - -`arc-evm-specs-tests` emits normalized tags in failure details: - -- `error_class=HARNESS_PRECONDITION` -- `error_class=EXECUTION_MISMATCH` -- `error_kind=TX_ENV_BUILD_FAILED` -- `error_kind=UNEXPECTED_EXCEPTION` -- `error_kind=UNEXPECTED_SUCCESS` -- `error_kind=WRONG_EXCEPTION` -- `error_kind=LOGS_HASH_MISMATCH` -- `error_kind=STATE_ROOT_MISMATCH` - -This is intended to be stable and reusable across suites, rather than requiring per-test string mapping. - -## Output Metadata - -Consume-direct JSON output includes structured variant metadata: - -- `data_index`: index into fixture `transaction.data` -- `gas_index`: index into fixture `transaction.gasLimit` -- `value_index`: index into fixture `transaction.value` - -The `variantId` string is still emitted for compatibility with existing -consume-direct tooling. Its suffix remains in the historical `d..._g..._v...` -format even though the structured JSON fields use the clearer names above. - -## Reporting Artifacts - -`consume direct` exposes multiple reporting layers, and they operate at -different granularities: - -- `report_consume.html` is the pytest HTML report. Its primary unit is the - collected pytest item, which is suite-level from the fixture consumer's - point of view. -- the aggregate JSON written to stdout by `arc-evm-specs-tests` is the - fixture-consumer contract consumed by `arc-execution-specs`; it summarizes - each executed result as `name` / `variantId` / `pass` / `error`, with the - optional JSON outcome attached alongside it. -- `per_test_outcomes.jsonl` is the ARC runner's variant-level artifact. Each - line is one concrete fixture variant emitted through `--json-outcome`. - -That means the HTML report is useful for high-level triage, but it does not -model fixture-internal transaction variants as first-class rows. The variant -detail lives in the runner output artifacts. - -Practical interpretation: - -- use `report_consume.html` to answer "which pytest cases failed?" -- use the aggregate JSON output to answer "what did the fixture consumer - return for this run as a whole?" -- use `per_test_outcomes.jsonl` to answer "which exact fixture variant failed, - and what were its state root, logs root, gas used, and normalized error?" - -The stable link between those two views is the runner's formatted test ID: - -- pytest failure details surface the full variant identifier -- per-test JSON lines emit the same identifier in the `test` field -- `variantId` carries the same concrete variant identity in aggregate - consume-direct JSON output - -This is why `format_test_id(...)` intentionally remains stable even though the -structured metadata fields were renamed to `data_index`, `gas_index`, and -`value_index`: the reporting contract depends on a stable concrete variant key. diff --git a/crates/evm-specs-tests/src/adapter.rs b/crates/evm-specs-tests/src/adapter.rs deleted file mode 100644 index e99fc82..0000000 --- a/crates/evm-specs-tests/src/adapter.rs +++ /dev/null @@ -1,329 +0,0 @@ -// Copyright 2026 Circle Internet Group, Inc. All rights reserved. -// -// SPDX-License-Identifier: Apache-2.0 -// -// Licensed under the Apache License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. -// You may obtain a copy of the License at -// -// http://www.apache.org/licenses/LICENSE-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// See the License for the specific language governing permissions and -// limitations under the License. - -use std::collections::HashMap; -use std::sync::Arc; - -use arc_evm::ArcEvmFactory; -use arc_execution_config::chainspec::{ArcChainSpec, LOCAL_DEV}; -use reth_evm::EvmEnv; -use revm::context::CfgEnv; -use revm_statetest_types::{SpecName, TestUnit}; - -use crate::error::EvmSpecsTestError; - -/// Extract a test_name → chain_id map from raw JSON before revm-statetest-types -/// deserialization. -/// -/// EEST StateFixture JSON has `"config": {"chainid": "0x01"}` at the test-unit -/// level. `revm-statetest-types` v14 does NOT parse this field (it silently -/// drops unknown fields). We parse the raw JSON as a generic map to extract -/// `config.chainid` per test name, then let revm-statetest-types handle the -/// execution-relevant fields. -/// -/// TODO(arc-evm-specs-tests): remove this raw JSON extraction once `revm-statetest-types` -/// natively deserializes `config.chainid` for EEST fixtures. -/// Verified workaround requirement against `revm-statetest-types = 14.x`. -pub fn extract_chain_ids( - raw_json: &serde_json::Value, -) -> Result, EvmSpecsTestError> { - let mut map = HashMap::new(); - if let Some(obj) = raw_json.as_object() { - for (test_name, test_value) in obj { - if let Some(raw_chain_id) = test_value - .get("config") - .and_then(|c| c.get("chainid")) - .and_then(|v| v.as_str()) - { - let chain_id = parse_chain_id(raw_chain_id).ok_or_else(|| { - EvmSpecsTestError::MalformedChainId { - test_name: test_name.clone(), - raw_value: raw_chain_id.to_string(), - } - })?; - map.insert(test_name.clone(), chain_id); - } - } - } - Ok(map) -} - -fn parse_chain_id(raw: &str) -> Option { - let trimmed = raw.trim(); - if trimmed.is_empty() { - return None; - } - - if let Some(hex) = trimmed - .strip_prefix("0x") - .or_else(|| trimmed.strip_prefix("0X")) - { - return u64::from_str_radix(hex, 16).ok(); - } - - trimmed.parse::().ok() -} - -/// Resolve chain ID for a test with precedence: -/// 1. config.chainid (from raw JSON map — EEST source of truth) -/// 2. env.current_chain_id (revm-statetest-types, older reference format) -/// 3. Error (do NOT silently default) -pub fn resolve_chain_id( - test_name: &str, - chain_id_map: &HashMap, - unit: &TestUnit, -) -> Result { - if let Some(&id) = chain_id_map.get(test_name) { - return Ok(id); - } - if let Some(id) = unit.env.current_chain_id { - return id - .try_into() - .map_err(|_| EvmSpecsTestError::MissingChainId { - test_name: test_name.to_string(), - }); - } - Err(EvmSpecsTestError::MissingChainId { - test_name: test_name.to_string(), - }) -} - -/// Build the default ARC chain spec used for statetest execution. -/// -/// This runner always executes against ARC localdev with all ARC hardforks -/// active. Ethereum fixture fork names still choose the `cfg.spec`, but the -/// underlying ARC executor is the full local ARC chain configuration. -/// -/// This is intentional ARC-mode execution, not pure Ethereum fork isolation. -/// The fixture chooses the EVM spec id; the chain-level execution context -/// remains ARC `LOCAL_DEV`. -pub fn build_default_arc_chain_spec() -> Arc { - LOCAL_DEV.clone() -} - -/// Build the ArcEvmFactory from a chain spec. -/// -/// Note: ArcEvmFactory::new takes a single arg (chain_spec). -/// The struct is #[non_exhaustive] so the API may expand in the future. -pub fn build_evm_factory(chain_spec: Arc) -> ArcEvmFactory { - ArcEvmFactory::new(chain_spec) -} - -/// Build CfgEnv + BlockEnv from a TestUnit for a given SpecName + chain_id. -/// -/// Chain ID is resolved externally via `resolve_chain_id()` before calling -/// this function, so it takes chain_id as an explicit parameter. -pub fn build_evm_env( - unit: &TestUnit, - spec_name: &SpecName, - chain_id: u64, -) -> Result { - let spec_id = spec_name.to_spec_id(); - let mut cfg = CfgEnv::new().with_spec_and_mainnet_gas_params(spec_id); - - cfg.chain_id = chain_id; - - let block = unit.block_env(&mut cfg); - - Ok(EvmEnv { - cfg_env: cfg, - block_env: block, - }) -} - -/// Phase 1: Only standard Ethereum forks are supported. -/// `SpecName::Unknown` (custom ARC names) is unsupported. -pub fn is_supported_spec(spec_name: &SpecName) -> bool { - !matches!(spec_name, SpecName::Unknown) -} - -#[cfg(test)] -mod tests { - use super::*; - use alloy_primitives::{Address, U256}; - use revm_primitives::hardfork::SpecId; - use revm_statetest_types::{Env, TestUnit, TransactionParts}; - use std::collections::BTreeMap; - - fn unit_with_chain_id(chain_id: Option) -> TestUnit { - TestUnit { - info: None, - env: Env { - current_chain_id: chain_id, - current_coinbase: Address::ZERO, - current_difficulty: U256::ZERO, - current_gas_limit: U256::from(30_000_000), - current_number: U256::from(1), - current_timestamp: U256::from(1), - current_base_fee: Some(U256::ZERO), - previous_hash: None, - current_random: None, - current_beacon_root: None, - current_withdrawals_root: None, - current_excess_blob_gas: None, - }, - pre: alloy_primitives::map::HashMap::default(), - post: BTreeMap::default(), - transaction: TransactionParts { - tx_type: None, - data: vec![], - gas_limit: vec![], - gas_price: None, - nonce: U256::ZERO, - secret_key: Default::default(), - sender: None, - to: None, - value: vec![], - max_fee_per_gas: None, - max_priority_fee_per_gas: None, - initcodes: None, - access_lists: vec![], - authorization_list: None, - blob_versioned_hashes: vec![], - max_fee_per_blob_gas: None, - }, - out: None, - } - } - - #[test] - fn extracts_hex_and_decimal_chain_ids() { - let json = serde_json::json!({ - "hex_case": { "config": { "chainid": "0x10" } }, - "dec_case": { "config": { "chainid": "10" } } - }); - let ids = extract_chain_ids(&json).expect("chain ids should parse"); - assert_eq!(ids.get("hex_case"), Some(&16)); - assert_eq!(ids.get("dec_case"), Some(&10)); - } - - #[test] - fn rejects_malformed_chain_id_with_context() { - let json = serde_json::json!({ - "bad_case": { "config": { "chainid": "xyz" } } - }); - let err = extract_chain_ids(&json).expect_err("invalid chain id should fail"); - assert!(matches!( - err, - EvmSpecsTestError::MalformedChainId { test_name, raw_value } - if test_name == "bad_case" && raw_value == "xyz" - )); - } - - #[test] - fn extract_chain_ids_ignores_missing_or_non_string_chain_ids() { - let json = serde_json::json!({ - "missing": { "config": {} }, - "non_string": { "config": { "chainid": 1 } }, - "good": { "config": { "chainid": "0X2a" } } - }); - - let ids = extract_chain_ids(&json).expect("chain ids should parse"); - - assert_eq!(ids.len(), 1); - assert_eq!(ids.get("good"), Some(&42)); - } - - #[test] - fn supported_spec_rejects_unknown_only() { - assert!(is_supported_spec(&SpecName::Prague)); - assert!(!is_supported_spec(&SpecName::Unknown)); - } - - #[test] - fn resolve_chain_id_prefers_explicit_map_over_env() { - let unit = unit_with_chain_id(Some(U256::from(7))); - let ids = HashMap::from([(String::from("fixture"), 42_u64)]); - - let resolved = resolve_chain_id("fixture", &ids, &unit).expect("chain id should resolve"); - - assert_eq!(resolved, 42); - } - - #[test] - fn resolve_chain_id_falls_back_to_env_chain_id() { - let unit = unit_with_chain_id(Some(U256::from(7))); - - let resolved = resolve_chain_id("fixture", &HashMap::default(), &unit) - .expect("chain id should resolve"); - - assert_eq!(resolved, 7); - } - - #[test] - fn resolve_chain_id_errors_when_no_sources_exist() { - let unit = unit_with_chain_id(None); - - let err = resolve_chain_id("fixture", &HashMap::default(), &unit) - .expect_err("missing chain id should fail"); - - assert!(matches!( - err, - EvmSpecsTestError::MissingChainId { test_name } if test_name == "fixture" - )); - } - - #[test] - fn build_default_arc_chain_spec_returns_local_dev() { - let chain_spec = build_default_arc_chain_spec(); - - assert_eq!(chain_spec.inner.chain.id(), LOCAL_DEV.inner.chain.id()); - } - - #[test] - fn build_evm_env_sets_requested_chain_id_and_spec() { - let unit = unit_with_chain_id(Some(U256::from(1))); - - let env = build_evm_env(&unit, &SpecName::Prague, 5042).expect("env should build"); - - assert_eq!(env.cfg_env.chain_id, 5042); - assert_eq!(env.cfg_env.spec, SpecId::PRAGUE); - assert_eq!(env.block_env.number.to::(), 1); - } - - #[test] - fn build_evm_env_gas_params_track_spec_across_forks() { - let unit = unit_with_chain_id(Some(U256::from(1))); - - let frontier = build_evm_env(&unit, &SpecName::Frontier, 1).expect("frontier env builds"); - let istanbul = build_evm_env(&unit, &SpecName::Istanbul, 1).expect("istanbul env builds"); - let prague = build_evm_env(&unit, &SpecName::Prague, 1).expect("prague env builds"); - - assert_eq!(frontier.cfg_env.spec, SpecId::FRONTIER); - assert_eq!(istanbul.cfg_env.spec, SpecId::ISTANBUL); - assert_eq!(prague.cfg_env.spec, SpecId::PRAGUE); - - // Pre-fix bug: gas_params were stuck on the default (PRAGUE) regardless of - // requested spec, causing pre-Prague fixtures to bill modern SSTORE/SLOAD - // gas. The fix routes through with_spec_and_mainnet_gas_params so the table - // matches the spec. - assert_ne!(frontier.cfg_env.gas_params, prague.cfg_env.gas_params); - assert_ne!(istanbul.cfg_env.gas_params, prague.cfg_env.gas_params); - - let reference_frontier = CfgEnv::new().with_spec_and_mainnet_gas_params(SpecId::FRONTIER); - let reference_istanbul = CfgEnv::new().with_spec_and_mainnet_gas_params(SpecId::ISTANBUL); - assert_eq!(frontier.cfg_env.gas_params, reference_frontier.gas_params); - assert_eq!(istanbul.cfg_env.gas_params, reference_istanbul.gas_params); - } - - #[test] - fn build_evm_factory_uses_supplied_chain_spec() { - let chain_spec = build_default_arc_chain_spec(); - let _factory = build_evm_factory(chain_spec); - - // Construction succeeds with the supplied chain spec. - } -} diff --git a/crates/evm-specs-tests/src/cmd/mod.rs b/crates/evm-specs-tests/src/cmd/mod.rs deleted file mode 100644 index 9d7d1d9..0000000 --- a/crates/evm-specs-tests/src/cmd/mod.rs +++ /dev/null @@ -1,17 +0,0 @@ -// Copyright 2026 Circle Internet Group, Inc. All rights reserved. -// -// SPDX-License-Identifier: Apache-2.0 -// -// Licensed under the Apache License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. -// You may obtain a copy of the License at -// -// http://www.apache.org/licenses/LICENSE-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// See the License for the specific language governing permissions and -// limitations under the License. - -pub mod statetest; diff --git a/crates/evm-specs-tests/src/cmd/statetest.rs b/crates/evm-specs-tests/src/cmd/statetest.rs deleted file mode 100644 index 1b0d723..0000000 --- a/crates/evm-specs-tests/src/cmd/statetest.rs +++ /dev/null @@ -1,58 +0,0 @@ -// Copyright 2026 Circle Internet Group, Inc. All rights reserved. -// -// SPDX-License-Identifier: Apache-2.0 -// -// Licensed under the Apache License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. -// You may obtain a copy of the License at -// -// http://www.apache.org/licenses/LICENSE-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// See the License for the specific language governing permissions and -// limitations under the License. - -use std::path::PathBuf; - -use crate::error::EvmSpecsTestError; -use crate::result::RunStatus; - -/// Thin CLI wrapper over the ARC-backed statetest runner harness. -pub fn run( - path: PathBuf, - filter_name: Option, - strict_exit: bool, - trace: bool, - json_outcome: bool, -) -> Result { - crate::runner::run(path, filter_name, strict_exit, trace, json_outcome) -} - -#[cfg(test)] -mod tests { - use super::run; - use crate::error::EvmSpecsTestError; - use std::time::{SystemTime, UNIX_EPOCH}; - - #[test] - fn run_forwards_runner_no_json_files_error() { - let nonce = SystemTime::now() - .duration_since(UNIX_EPOCH) - .expect("system time should be after epoch") - .as_nanos(); - let root = std::env::temp_dir().join(format!("arc_evm_specs_cmd_empty_{nonce}")); - std::fs::create_dir_all(&root).expect("temp dir should be created"); - - let err = run(root.clone(), None, false, false, false) - .expect_err("empty directory should fail through wrapper"); - - assert!(matches!( - err, - EvmSpecsTestError::NoJsonFiles { path } if path == root.display().to_string() - )); - - std::fs::remove_dir_all(root).expect("temp dir should be removed"); - } -} diff --git a/crates/evm-specs-tests/src/error.rs b/crates/evm-specs-tests/src/error.rs deleted file mode 100644 index 32ad0a3..0000000 --- a/crates/evm-specs-tests/src/error.rs +++ /dev/null @@ -1,147 +0,0 @@ -// Copyright 2026 Circle Internet Group, Inc. All rights reserved. -// -// SPDX-License-Identifier: Apache-2.0 -// -// Licensed under the Apache License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. -// You may obtain a copy of the License at -// -// http://www.apache.org/licenses/LICENSE-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// See the License for the specific language governing permissions and -// limitations under the License. - -use thiserror::Error; - -/// Errors that can occur during arc-evm-specs-tests execution. -#[derive(Debug, Error)] -pub enum EvmSpecsTestError { - #[error("IO error: {0}")] - Io(#[from] std::io::Error), - - #[error("JSON parse error: {path}: {source}")] - JsonParse { - path: String, - source: serde_json::Error, - }, - - #[error("Test error in '{name}': {kind}")] - TestFailure { name: String, kind: TestErrorKind }, - #[error("No JSON fixture files found at: {path}")] - NoJsonFiles { path: String }, - #[error("Missing chain_id for test '{test_name}': neither config.chainid nor env.current_chain_id is available")] - MissingChainId { test_name: String }, - #[error("Malformed config.chainid for test '{test_name}': '{raw_value}' (expected decimal like '1' or hex like '0x1')")] - MalformedChainId { - test_name: String, - raw_value: String, - }, - #[error("Runner queue mutex poisoned")] - RunnerQueuePoisoned, - #[error("Failed to spawn runner worker thread: {0}")] - WorkerSpawn(std::io::Error), - #[error("Runner worker thread panicked")] - WorkerPanic, -} - -/// Specific kinds of test failures. -#[derive(Debug, Error)] -pub enum TestErrorKind { - #[error("EVM execution error: error_class={error_class}; error_kind={error_kind}; {detail}")] - EvmError { - error_class: &'static str, - error_kind: &'static str, - detail: String, - }, -} - -impl TestErrorKind { - pub fn evm( - error_class: &'static str, - error_kind: &'static str, - detail: impl Into, - ) -> Self { - Self::EvmError { - error_class, - error_kind, - detail: detail.into(), - } - } -} - -#[cfg(test)] -mod tests { - use super::*; - - #[test] - fn evm_error_kind_formats_with_class_kind_and_detail() { - let err = TestErrorKind::evm("EXECUTION_MISMATCH", "WRONG_EXCEPTION", "details"); - - assert_eq!( - err.to_string(), - "EVM execution error: error_class=EXECUTION_MISMATCH; error_kind=WRONG_EXCEPTION; details" - ); - } - - #[test] - fn top_level_error_wraps_test_failure_context() { - let err = EvmSpecsTestError::TestFailure { - name: "fixture/Prague/d0_g0_v0".to_string(), - kind: TestErrorKind::evm("EXECUTION_MISMATCH", "STATE_ROOT_MISMATCH", "nope"), - }; - - let rendered = err.to_string(); - assert!(rendered.contains("Test error in 'fixture/Prague/d0_g0_v0'")); - assert!(rendered.contains("error_class=EXECUTION_MISMATCH")); - assert!(rendered.contains("error_kind=STATE_ROOT_MISMATCH")); - } - - #[test] - fn missing_chain_id_error_mentions_expected_sources() { - let err = EvmSpecsTestError::MissingChainId { - test_name: "fixture".to_string(), - }; - - assert_eq!( - err.to_string(), - "Missing chain_id for test 'fixture': neither config.chainid nor env.current_chain_id is available" - ); - } - - #[test] - fn error_variants_render_expected_messages() { - let io_err = EvmSpecsTestError::Io(std::io::Error::other("disk")); - assert!(io_err.to_string().contains("IO error: disk")); - - let json_err = EvmSpecsTestError::JsonParse { - path: "fixture.json".to_string(), - source: serde_json::from_str::("{").expect_err("invalid json"), - }; - assert!(json_err - .to_string() - .contains("JSON parse error: fixture.json")); - - let malformed = EvmSpecsTestError::MalformedChainId { - test_name: "fixture".to_string(), - raw_value: "xyz".to_string(), - }; - assert!(malformed.to_string().contains("Malformed config.chainid")); - - assert_eq!( - EvmSpecsTestError::RunnerQueuePoisoned.to_string(), - "Runner queue mutex poisoned" - ); - assert_eq!( - EvmSpecsTestError::WorkerPanic.to_string(), - "Runner worker thread panicked" - ); - - let spawn = EvmSpecsTestError::WorkerSpawn(std::io::Error::other("thread")); - assert!(spawn - .to_string() - .contains("Failed to spawn runner worker thread: thread")); - } -} diff --git a/crates/evm-specs-tests/src/exception_match.rs b/crates/evm-specs-tests/src/exception_match.rs deleted file mode 100644 index 61dfc9e..0000000 --- a/crates/evm-specs-tests/src/exception_match.rs +++ /dev/null @@ -1,457 +0,0 @@ -// Copyright 2026 Circle Internet Group, Inc. All rights reserved. -// -// SPDX-License-Identifier: Apache-2.0 -// -// Licensed under the Apache License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. -// You may obtain a copy of the License at -// -// http://www.apache.org/licenses/LICENSE-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// See the License for the specific language governing permissions and -// limitations under the License. - -//! Temporary exception-bucket matching helpers. -//! -//! This module is intentionally stricter than upstream `revme` today. -//! `revme` currently treats any execution error as satisfying -//! `expectException`, and during tx env construction it also accepts: -//! -//! ```ignore -//! let tx = match test.tx_env(&unit) { -//! Ok(tx) => tx, -//! Err(_) if test.expect_exception.is_some() => continue, -//! Err(e) => ... -//! }; -//! ``` -//! -//! See `bins/revme/src/cmd/statetest/runner.rs` in the `bluealloy/revm` -//! repository. -//! -//! We are not following that behavior yet because this stage of the work is -//! focused on debugging expectation mismatches across different error buckets. -//! For that reason, this file normalizes and classifies error strings so the -//! runner can distinguish "wrong exception bucket" from "some exception -//! happened". -//! -//! Once that debugging stage is complete, we should remove this stricter -//! bucket-matching behavior and follow `revme`'s exception handling semantics. - -use std::borrow::Cow; - -#[derive(Clone, Copy, Debug, Eq, PartialEq)] -enum ExceptionCategory { - IntrinsicGasTooLow, - IntrinsicGasBelowFloorGasCost, - InsufficientAccountFunds, - GasLimitPriceProductOverflow, - SenderNotEoa, - Type3TxBlobCountExceeded, - Type1TxPreFork, - Type2TxPreFork, - Type3TxInvalidBlobVersionedHash, - Type3TxZeroBlobs, - Type4EmptyAuthorizationList, - Type4TxPreFork, - InsufficientMaxFeePerGas, - InsufficientMaxFeePerBlobGas, - InitcodeSizeExceeded, - GasLimitExceedsMaximum, - PriorityGreaterThanMaxFeePerGas, - GasAllowanceExceeded, -} - -pub(crate) fn exception_matches(expected: &str, actual: &str) -> bool { - let actual_norm = normalize_exception_phrase(actual); - if actual_norm.is_empty() { - return false; - } - - let actual_category = actual_exception_category(&actual_norm); - expected - .split('|') - .map(str::trim) - .filter(|candidate| !candidate.is_empty()) - .any(|candidate| expected_alternative_matches(candidate, &actual_norm, actual_category)) -} - -pub(crate) fn normalize_exception_phrase(value: &str) -> String { - let mut normalized = String::with_capacity(value.len()); - let mut prev_was_alnum = false; - let mut prev_was_lower = false; - let mut prev_was_digit = false; - - for ch in value.chars() { - if !ch.is_ascii_alphanumeric() { - normalized.push(' '); - prev_was_alnum = false; - prev_was_lower = false; - prev_was_digit = false; - continue; - } - - let is_upper = ch.is_ascii_uppercase(); - let is_lower = ch.is_ascii_lowercase(); - let is_digit = ch.is_ascii_digit(); - - if prev_was_alnum - && ((prev_was_lower && is_upper) - || (prev_was_digit && !is_digit) - || (!prev_was_digit && is_digit)) - { - normalized.push(' '); - } - - normalized.push(ch.to_ascii_lowercase()); - prev_was_alnum = true; - prev_was_lower = is_lower; - prev_was_digit = is_digit; - } - - normalized.split_whitespace().collect::>().join(" ") -} - -pub(crate) fn tx_env_actual_exception(error: &str) -> Option<&str> { - let got_marker = "got Some(\""; - let start = error.find(got_marker)?.checked_add(got_marker.len())?; - let rest = &error[start..]; - let end = rest.find("\")")?; - Some(&rest[..end]) -} - -pub(crate) fn tx_env_exception_matches(expected: &str, actual: &str) -> bool { - if exception_matches(expected, actual) { - return true; - } - - let expected_norm = normalize_exception_phrase(expected); - let actual_norm = normalize_exception_phrase(actual); - - let expected_key = normalize_expected_key(&expected_norm); - matches!( - (expected_key.as_ref(), actual_norm.as_str()), - ("type 3 tx contract creation", "invalid transaction type") - | ("type 4 tx contract creation", "invalid transaction type") - ) -} - -fn normalize_expected_key(expected_norm: &str) -> Cow<'_, str> { - let trimmed = expected_norm - .trim_start_matches("transaction exception ") - .trim_start_matches("exception ") - .trim(); - Cow::Borrowed(trimmed) -} - -fn expected_alternative_matches( - expected: &str, - actual_norm: &str, - actual_category: Option, -) -> bool { - let expected_norm = normalize_exception_phrase(expected); - if expected_norm.is_empty() { - return false; - } - - if let Some(expected_category) = expected_exception_category(&expected_norm) { - return actual_category.is_some_and(|actual| actual == expected_category); - } - - if expected_norm == actual_norm { - return true; - } - if expected_norm.split_whitespace().count() < 2 { - return false; - } - - let bounded_actual = format!(" {actual_norm} "); - let bounded_expected = format!(" {expected_norm} "); - bounded_actual.contains(&bounded_expected) -} - -fn expected_exception_category(expected_norm: &str) -> Option { - match normalize_expected_key(expected_norm).as_ref() { - "intrinsic gas too low" => Some(ExceptionCategory::IntrinsicGasTooLow), - "intrinsic gas below floor gas cost" => { - Some(ExceptionCategory::IntrinsicGasBelowFloorGasCost) - } - "insufficient account funds" => Some(ExceptionCategory::InsufficientAccountFunds), - "gaslimit price product overflow" => Some(ExceptionCategory::GasLimitPriceProductOverflow), - "sender not eoa" => Some(ExceptionCategory::SenderNotEoa), - "type 3 tx max blob gas allowance exceeded" | "type 3 tx blob count exceeded" => { - Some(ExceptionCategory::Type3TxBlobCountExceeded) - } - "type 1 tx pre fork" => Some(ExceptionCategory::Type1TxPreFork), - "type 2 tx pre fork" => Some(ExceptionCategory::Type2TxPreFork), - "type 3 tx invalid blob versioned hash" => { - Some(ExceptionCategory::Type3TxInvalidBlobVersionedHash) - } - "type 3 tx zero blobs" => Some(ExceptionCategory::Type3TxZeroBlobs), - "type 4 empty authorization list" => Some(ExceptionCategory::Type4EmptyAuthorizationList), - "type 4 tx pre fork" => Some(ExceptionCategory::Type4TxPreFork), - "insufficient max fee per gas" => Some(ExceptionCategory::InsufficientMaxFeePerGas), - "insufficient max fee per blob gas" => { - Some(ExceptionCategory::InsufficientMaxFeePerBlobGas) - } - "initcode size exceeded" => Some(ExceptionCategory::InitcodeSizeExceeded), - "gas limit exceeds maximum" => Some(ExceptionCategory::GasLimitExceedsMaximum), - "priority greater than max fee per gas" => { - Some(ExceptionCategory::PriorityGreaterThanMaxFeePerGas) - } - "gas allowance exceeded" => Some(ExceptionCategory::GasAllowanceExceeded), - _ => None, - } -} - -fn actual_exception_category(actual_norm: &str) -> Option { - actual_exception_rules() - .iter() - .find(|rule| rule.matches(actual_norm)) - .map(|rule| rule.category) -} - -struct ActualExceptionRule { - category: ExceptionCategory, - required_substrings: &'static [&'static str], -} - -impl ActualExceptionRule { - fn matches(&self, actual_norm: &str) -> bool { - self.required_substrings - .iter() - .all(|needle| actual_norm.contains(needle)) - } -} - -fn actual_exception_rules() -> &'static [ActualExceptionRule] { - &[ - ActualExceptionRule { - category: ExceptionCategory::IntrinsicGasTooLow, - required_substrings: &["call gas cost", "exceeds the gas limit"], - }, - ActualExceptionRule { - category: ExceptionCategory::IntrinsicGasBelowFloorGasCost, - required_substrings: &["gas floor", "exceeds the gas limit"], - }, - ActualExceptionRule { - category: ExceptionCategory::InsufficientAccountFunds, - required_substrings: &["lack of funds", "for max fee"], - }, - ActualExceptionRule { - category: ExceptionCategory::GasLimitPriceProductOverflow, - required_substrings: &["overflow payment in transaction"], - }, - ActualExceptionRule { - category: ExceptionCategory::SenderNotEoa, - required_substrings: &["senders with deployed code"], - }, - ActualExceptionRule { - category: ExceptionCategory::Type3TxBlobCountExceeded, - required_substrings: &["too many blobs"], - }, - ActualExceptionRule { - category: ExceptionCategory::Type1TxPreFork, - required_substrings: &["eip 2930", "not supported"], - }, - ActualExceptionRule { - category: ExceptionCategory::Type2TxPreFork, - required_substrings: &["eip 1559", "not supported"], - }, - ActualExceptionRule { - category: ExceptionCategory::Type3TxInvalidBlobVersionedHash, - required_substrings: &["blob version not supported"], - }, - ActualExceptionRule { - category: ExceptionCategory::Type3TxZeroBlobs, - required_substrings: &["empty blobs"], - }, - ActualExceptionRule { - category: ExceptionCategory::Type4EmptyAuthorizationList, - required_substrings: &["empty authorization list"], - }, - ActualExceptionRule { - category: ExceptionCategory::Type4TxPreFork, - required_substrings: &["eip 7702", "not supported"], - }, - ActualExceptionRule { - category: ExceptionCategory::InsufficientMaxFeePerGas, - required_substrings: &["gas price is less than basefee"], - }, - ActualExceptionRule { - category: ExceptionCategory::InsufficientMaxFeePerBlobGas, - required_substrings: &["blob gas price", "greater than max fee per blob gas"], - }, - ActualExceptionRule { - category: ExceptionCategory::InitcodeSizeExceeded, - required_substrings: &["create initcode size limit"], - }, - ActualExceptionRule { - category: ExceptionCategory::GasLimitExceedsMaximum, - required_substrings: &["transaction gas limit", "greater than the cap"], - }, - ActualExceptionRule { - category: ExceptionCategory::PriorityGreaterThanMaxFeePerGas, - required_substrings: &["priority fee is greater than max fee"], - }, - ActualExceptionRule { - category: ExceptionCategory::GasAllowanceExceeded, - required_substrings: &["caller gas limit exceeds the block gas limit"], - }, - ] -} - -#[cfg(test)] -mod tests { - use super::*; - - #[test] - fn exception_matching_requires_the_expected_error() { - assert!(exception_matches( - "TR_TypeNotSupported", - "transaction validation error: tr type not supported" - )); - assert!(!exception_matches( - "TR_TypeNotSupported", - "transaction validation error: nonce too low" - )); - } - - #[test] - fn exception_matching_rejects_substring_word_matches() { - assert!(!exception_matches("gas", "IntrinsicGasTooLow")); - } - - #[test] - fn exception_matching_accepts_expected_alternatives() { - assert!(exception_matches( - "TransactionException.INSUFFICIENT_ACCOUNT_FUNDS|TransactionException.INTRINSIC_GAS_TOO_LOW", - "transaction validation error: call gas cost (53000) exceeds the gas limit (21000)" - )); - } - - #[test] - fn exception_matching_keeps_unrecognized_exact_alternative() { - assert!(exception_matches( - "TransactionException.INTRINSIC_GAS_TOO_LOW|custom exact mismatch text", - "custom exact mismatch text" - )); - } - - #[test] - fn exception_matching_maps_high_volume_transaction_aliases() { - let cases = [ - ( - "TransactionException.INTRINSIC_GAS_TOO_LOW", - "transaction validation error: call gas cost (53000) exceeds the gas limit (21000)", - ), - ( - "TransactionException.INSUFFICIENT_ACCOUNT_FUNDS", - "transaction validation error: lack of funds (100) for max fee (101)", - ), - ( - "TransactionException.INSUFFICIENT_ACCOUNT_FUNDS|TransactionException.GASLIMIT_PRICE_PRODUCT_OVERFLOW", - "transaction validation error: overflow payment in transaction", - ), - ( - "TransactionException.INTRINSIC_GAS_BELOW_FLOOR_GAS_COST", - "transaction validation error: gas floor (53000) exceeds the gas limit (21000)", - ), - ( - "TransactionException.SENDER_NOT_EOA", - "transaction validation error: reject transactions from senders with deployed code", - ), - ( - "TransactionException.TYPE_2_TX_PRE_FORK", - "transaction validation error: Eip1559 is not supported", - ), - ( - "TransactionException.TYPE_1_TX_PRE_FORK", - "transaction validation error: Eip2930 is not supported", - ), - ( - "TransactionException.TYPE_4_TX_PRE_FORK", - "transaction validation error: Eip7702 is not supported", - ), - ( - "TransactionException.TYPE_3_TX_INVALID_BLOB_VERSIONED_HASH", - "transaction validation error: blob version not supported", - ), - ( - "TransactionException.TYPE_3_TX_MAX_BLOB_GAS_ALLOWANCE_EXCEEDED|TransactionException.TYPE_3_TX_BLOB_COUNT_EXCEEDED", - "transaction validation error: too many blobs, have 7, max 6", - ), - ( - "TransactionException.INSUFFICIENT_MAX_FEE_PER_GAS", - "transaction validation error: gas price is less than basefee", - ), - ( - "TransactionException.INITCODE_SIZE_EXCEEDED", - "transaction validation error: create initcode size limit exceeded", - ), - ( - "TransactionException.GAS_LIMIT_EXCEEDS_MAXIMUM", - "transaction validation error: transaction gas limit 30000001 greater than the cap", - ), - ( - "TransactionException.PRIORITY_GREATER_THAN_MAX_FEE_PER_GAS", - "transaction validation error: priority fee is greater than max fee", - ), - ( - "TransactionException.TYPE_3_TX_ZERO_BLOBS", - "transaction validation error: empty blobs are not allowed", - ), - ( - "TransactionException.TYPE_4_EMPTY_AUTHORIZATION_LIST", - "transaction validation error: empty authorization list", - ), - ( - "TransactionException.INSUFFICIENT_MAX_FEE_PER_BLOB_GAS", - "transaction validation error: blob gas price 5 greater than max fee per blob gas 4", - ), - ( - "TransactionException.GAS_ALLOWANCE_EXCEEDED", - "transaction validation error: caller gas limit exceeds the block gas limit", - ), - ]; - - for (expected, actual) in cases { - assert!( - exception_matches(expected, actual), - "expected {expected} to match {actual}" - ); - } - } - - #[test] - fn normalize_exception_phrase_splits_camel_case_and_digits() { - assert_eq!( - normalize_exception_phrase("IntrinsicGasTooLow123"), - "intrinsic gas too low 123" - ); - } - - #[test] - fn tx_env_actual_exception_extracts_inner_message() { - assert_eq!( - tx_env_actual_exception( - "unexpected exception: got Some(\"Invalid transaction type\"), expected Some(\"TransactionException.TYPE_3_TX_CONTRACT_CREATION\")" - ), - Some("Invalid transaction type") - ); - } - - #[test] - fn tx_env_exception_matches_contract_creation_aliases() { - assert!(tx_env_exception_matches( - "TransactionException.TYPE_3_TX_CONTRACT_CREATION", - "Invalid transaction type" - )); - assert!(tx_env_exception_matches( - "TransactionException.TYPE_4_TX_CONTRACT_CREATION", - "Invalid transaction type" - )); - } -} diff --git a/crates/evm-specs-tests/src/fixture_sanitizer.rs b/crates/evm-specs-tests/src/fixture_sanitizer.rs deleted file mode 100644 index 3cc04ba..0000000 --- a/crates/evm-specs-tests/src/fixture_sanitizer.rs +++ /dev/null @@ -1,154 +0,0 @@ -// Copyright 2026 Circle Internet Group, Inc. All rights reserved. -// -// SPDX-License-Identifier: Apache-2.0 -// -// Licensed under the Apache License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. -// You may obtain a copy of the License at -// -// http://www.apache.org/licenses/LICENSE-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// See the License for the specific language governing permissions and -// limitations under the License. - -/// Narrow compatibility shim for fixture fields that current typed upstream -/// parsing does not accept yet. -/// -/// This intentionally rewrites only the supported top-level `state` field and -/// direct `post.[]` entries. Nested objects are left untouched so schema -/// drift surfaces instead of being recursively mutated into shape. -pub(crate) fn strip_unsupported_fields(value: &mut serde_json::Value) { - let Some(root) = value.as_object_mut() else { - return; - }; - - for test_unit in root.values_mut() { - let Some(test_map) = test_unit.as_object_mut() else { - continue; - }; - - if !test_map.contains_key("postState") - && let Some(state) = test_map.remove("state") - { - test_map.insert("postState".to_string(), state); - } - - if let Some(post) = test_map.get_mut("post") { - strip_unsupported_fields_in_post(post); - } - } -} - -fn strip_unsupported_fields_in_post(post: &mut serde_json::Value) { - let Some(forks) = post.as_object_mut() else { - return; - }; - - for entries in forks.values_mut() { - let Some(items) = entries.as_array_mut() else { - continue; - }; - - for item in items { - let Some(map) = item.as_object_mut() else { - continue; - }; - - if !map.contains_key("postState") - && let Some(state) = map.remove("state") - { - map.insert("postState".to_string(), state); - } - map.remove("receipt"); - } - } -} - -#[cfg(test)] -mod tests { - use super::strip_unsupported_fields; - - #[test] - fn strip_unsupported_fields_removes_receipt_and_sets_post_state() { - let mut value = serde_json::json!({ - "suite": { - "receipt": { "status": "0x1" }, - "state": { "0x1": { "balance": "0x01" } }, - "post": { - "Cancun": [ - { - "receipt": { "status": "0x1" } - } - ] - } - } - }); - strip_unsupported_fields(&mut value); - let suite = value - .get("suite") - .and_then(serde_json::Value::as_object) - .unwrap(); - assert!(suite.contains_key("receipt")); - assert!(!suite.contains_key("state")); - assert!(suite.contains_key("postState")); - let post_entry = suite - .get("post") - .and_then(|v| v.get("Cancun")) - .and_then(serde_json::Value::as_array) - .and_then(|arr| arr.first()) - .and_then(serde_json::Value::as_object) - .unwrap(); - assert!(!post_entry.contains_key("receipt")); - } - - #[test] - fn strip_unsupported_fields_preserves_existing_post_state() { - let mut value = serde_json::json!({ - "suite": { - "state": { "old": true }, - "postState": { "kept": true }, - "post": { - "Prague": [ - { - "receipt": { "status": "0x1" }, - "nested": { - "receipt": { "status": "0x1" } - } - } - ] - } - } - }); - - strip_unsupported_fields(&mut value); - let suite = value.get("suite").unwrap(); - - assert_eq!( - suite.get("postState").unwrap(), - &serde_json::json!({ "kept": true }) - ); - assert_eq!( - suite.get("state").unwrap(), - &serde_json::json!({ "old": true }) - ); - let post_entry = suite - .get("post") - .and_then(|v| v.get("Prague")) - .and_then(serde_json::Value::as_array) - .and_then(|arr| arr.first()) - .and_then(serde_json::Value::as_object) - .unwrap(); - assert!(!post_entry.contains_key("receipt")); - assert!(suite - .get("post") - .and_then(|v| v.get("Prague")) - .and_then(serde_json::Value::as_array) - .and_then(|arr| arr.first()) - .and_then(|entry| entry.get("nested")) - .and_then(serde_json::Value::as_object) - .is_some_and(|nested| nested.contains_key("receipt"))); - } -} diff --git a/crates/evm-specs-tests/src/lib.rs b/crates/evm-specs-tests/src/lib.rs deleted file mode 100644 index a4d93dd..0000000 --- a/crates/evm-specs-tests/src/lib.rs +++ /dev/null @@ -1,24 +0,0 @@ -// Copyright 2026 Circle Internet Group, Inc. All rights reserved. -// -// SPDX-License-Identifier: Apache-2.0 -// -// Licensed under the Apache License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. -// You may obtain a copy of the License at -// -// http://www.apache.org/licenses/LICENSE-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// See the License for the specific language governing permissions and -// limitations under the License. - -pub mod adapter; -pub mod cmd; -pub mod error; -pub mod exception_match; -pub mod fixture_sanitizer; -pub mod result; -pub mod roots; -pub mod runner; diff --git a/crates/evm-specs-tests/src/main.rs b/crates/evm-specs-tests/src/main.rs deleted file mode 100644 index 27023a7..0000000 --- a/crates/evm-specs-tests/src/main.rs +++ /dev/null @@ -1,159 +0,0 @@ -// Copyright 2026 Circle Internet Group, Inc. All rights reserved. -// -// SPDX-License-Identifier: Apache-2.0 -// -// Licensed under the Apache License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. -// You may obtain a copy of the License at -// -// http://www.apache.org/licenses/LICENSE-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// See the License for the specific language governing permissions and -// limitations under the License. - -use arc_evm_specs_tests::result::RunStatus; -use clap::{Parser, Subcommand}; - -#[derive(Parser)] -#[command( - name = "arc-evm-specs-tests", - version = arc_version::SHORT_VERSION, - long_version = arc_version::LONG_VERSION, - about = "ARC EVM specs state-test runner" -)] -struct Cli { - #[command(subcommand)] - command: Commands, -} - -#[derive(Subcommand)] -enum Commands { - /// Run state tests from EEST fixtures - Statetest { - /// Path to a JSON fixture file or directory - path: std::path::PathBuf, - - /// Run only the test with this name - #[arg(long)] - run: Option, - - /// Rerun failing tests with EIP-3155 trace output on stderr - #[arg(long)] - trace: bool, - - /// Emit upstream-style per-test JSON outcome fields alongside name/pass/error - #[arg(long)] - json_outcome: bool, - - /// Exit with non-zero code if any test fails - #[arg(long)] - strict_exit: bool, - }, -} - -fn main() { - if emit_compat_version_if_requested() { - return; - } - - let cli = Cli::parse(); - - match cli.command { - Commands::Statetest { - path, - run: filter_name, - trace, - json_outcome, - strict_exit, - } => match arc_evm_specs_tests::cmd::statetest::run( - path, - filter_name, - strict_exit, - trace, - json_outcome, - ) { - Ok(RunStatus::Success) => {} - Ok(status) => std::process::exit(status as i32), - Err(e) => { - eprintln!("Fatal error: {e}"); - std::process::exit(2); - } - }, - } -} - -fn emit_compat_version_if_requested() -> bool { - emit_compat_version_if_requested_from_args(std::env::args().skip(1)) -} - -fn emit_compat_version_if_requested_from_args(mut args: impl Iterator) -> bool { - let Some(first) = args.next() else { - return false; - }; - - if args.next().is_some() { - return false; - } - - if matches!(first.as_str(), "-v" | "--version" | "version") { - println!("{}", detailed_version()); - return true; - } - - false -} - -fn detailed_version() -> String { - format!( - "arc-evm-specs-tests {}\n{}", - arc_version::SHORT_VERSION, - arc_version::LONG_VERSION - ) -} - -#[cfg(test)] -mod tests { - use super::{detailed_version, emit_compat_version_if_requested_from_args}; - - #[test] - fn compat_version_accepts_single_version_flags() { - assert!(emit_compat_version_if_requested_from_args( - ["-v".to_string()].into_iter() - )); - assert!(emit_compat_version_if_requested_from_args( - ["--version".to_string()].into_iter() - )); - assert!(emit_compat_version_if_requested_from_args( - ["version".to_string()].into_iter() - )); - } - - #[test] - fn compat_version_rejects_other_shapes() { - assert!(!emit_compat_version_if_requested_from_args( - std::iter::empty() - )); - assert!(!emit_compat_version_if_requested_from_args( - ["statetest".to_string()].into_iter() - )); - assert!(!emit_compat_version_if_requested_from_args( - ["--version".to_string(), "extra".to_string()].into_iter() - )); - } - - #[test] - fn detailed_version_uses_arc_build_metadata() { - let version = detailed_version(); - - assert!(version.starts_with("arc-evm-specs-tests ")); - assert!(version.contains(arc_version::SHORT_VERSION)); - assert!(version.contains("Version:")); - assert!(version.contains("Commit SHA:")); - assert!(version.contains("Build Timestamp:")); - assert!(version.contains("Build Profile:")); - assert!(version.contains("Platform:")); - } -} diff --git a/crates/evm-specs-tests/src/result.rs b/crates/evm-specs-tests/src/result.rs deleted file mode 100644 index e899207..0000000 --- a/crates/evm-specs-tests/src/result.rs +++ /dev/null @@ -1,328 +0,0 @@ -// Copyright 2026 Circle Internet Group, Inc. All rights reserved. -// -// SPDX-License-Identifier: Apache-2.0 -// -// Licensed under the Apache License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. -// You may obtain a copy of the License at -// -// http://www.apache.org/licenses/LICENSE-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// See the License for the specific language governing permissions and -// limitations under the License. -//! Result-model types for the ARC state-test runner. -//! -//! This module owns the JSON-facing data structures emitted by the runner: -//! `TestResult` for aggregate stdout output, `JsonOutcome` for per-variant -//! execution metadata, and `TestSummary` / `RunStatus` for run-level reporting. -//! -//! Artifact mapping: -//! - `TestResult`: aggregate JSON written to stdout and consumed by -//! `arc-execution-specs` -//! - `JsonOutcome`: embedded into `TestResult` and also emitted as per-variant -//! lines in `per_test_outcomes.jsonl` -//! - `TestSummary`: CLI stderr summary text for the overall run -//! - `RunStatus`: process/report status used to derive command exit behavior -//! -//! The pytest HTML report is generated on the Python side and does not -//! serialize these Rust types directly. - -use alloy_primitives::{Bytes, B256}; -use serde::Serialize; -/// A single test result, serialized to JSON stdout. -#[derive(Debug, Clone, Serialize)] -pub struct TestResult { - /// Consumer-facing fixture identity. This may be fixture-level for - /// compatibility with existing consume-direct integrations. - pub name: String, - /// Stable unique identifier for a concrete test variant when available. - /// - /// The `variantId` string currently keeps the historical `d{}_g{}_v{}` - /// suffix inherited from the upstream `revm` statetest runner shape - /// (`bins/revme/src/cmd/statetest/runner.rs`). Structured JSON fields use - /// the clearer `data_index` / `gas_index` / `value_index` names, while the - /// per-test stderr artifact still carries legacy `d` / `g` / `v` aliases - /// for compatibility with existing revm-style parsers. - #[serde(rename = "variantId", skip_serializing_if = "Option::is_none")] - pub variant_id: Option, - pub pass: bool, - #[serde(skip_serializing_if = "String::is_empty")] - pub error: String, - #[serde(rename = "stateRoot", skip_serializing_if = "Option::is_none")] - pub state_root: Option, - #[serde(rename = "logsRoot", skip_serializing_if = "Option::is_none")] - pub logs_root: Option, - #[serde(skip_serializing_if = "Option::is_none")] - pub output: Option, - #[serde(rename = "gasUsed", skip_serializing_if = "Option::is_none")] - pub gas_used: Option, - #[serde(rename = "errorMsg", skip_serializing_if = "Option::is_none")] - pub error_msg: Option, - #[serde(rename = "evmResult", skip_serializing_if = "Option::is_none")] - pub evm_result: Option, - #[serde(rename = "postLogsHash", skip_serializing_if = "Option::is_none")] - pub post_logs_hash: Option, - #[serde(skip_serializing_if = "Option::is_none")] - pub fork: Option, - #[serde(skip_serializing_if = "Option::is_none")] - pub test: Option, - #[serde(skip_serializing_if = "Option::is_none")] - /// Index into fixture `transaction.data`. - pub data_index: Option, - #[serde(skip_serializing_if = "Option::is_none")] - /// Index into fixture `transaction.gasLimit`. - pub gas_index: Option, - #[serde(skip_serializing_if = "Option::is_none")] - /// Index into fixture `transaction.value`. - pub value_index: Option, -} - -impl TestResult { - pub fn passed(name: String) -> Self { - Self { - name, - variant_id: None, - pass: true, - error: String::new(), - state_root: None, - logs_root: None, - output: None, - gas_used: None, - error_msg: None, - evm_result: None, - post_logs_hash: None, - fork: None, - test: None, - data_index: None, - gas_index: None, - value_index: None, - } - } - - pub fn failed(name: String, error: String) -> Self { - Self { - name, - variant_id: None, - pass: false, - error, - state_root: None, - logs_root: None, - output: None, - gas_used: None, - error_msg: None, - evm_result: None, - post_logs_hash: None, - fork: None, - test: None, - data_index: None, - gas_index: None, - value_index: None, - } - } - - pub fn with_json_outcome(mut self, outcome: JsonOutcome) -> Self { - self.state_root = Some(outcome.state_root); - self.logs_root = Some(outcome.logs_root); - self.output = Some(outcome.output); - self.gas_used = Some(outcome.gas_used); - self.error_msg = Some(outcome.error_msg); - self.evm_result = Some(outcome.evm_result); - self.post_logs_hash = Some(outcome.post_logs_hash); - self.fork = Some(outcome.fork); - self.test = Some(outcome.test); - self.data_index = Some(outcome.data_index); - self.gas_index = Some(outcome.gas_index); - self.value_index = Some(outcome.value_index); - self - } - - pub fn with_variant_id(mut self, variant_id: impl Into) -> Self { - self.variant_id = Some(variant_id.into()); - self - } -} - -#[derive(Debug, Clone)] -pub struct JsonOutcome { - pub state_root: B256, - pub logs_root: B256, - pub output: Bytes, - pub gas_used: u64, - pub error_msg: String, - pub evm_result: String, - pub post_logs_hash: B256, - pub fork: String, - pub test: String, - /// Index into fixture `transaction.data`. - pub data_index: usize, - /// Index into fixture `transaction.gasLimit`. - pub gas_index: usize, - /// Index into fixture `transaction.value`. - pub value_index: usize, -} - -/// Summary printed to stderr after all tests. -#[derive(Debug, Default)] -pub struct TestSummary { - pub files_processed: usize, - pub tests_total: usize, - pub tests_passed: usize, - pub tests_failed: usize, - pub tests_skipped_by_spec: usize, -} - -impl std::fmt::Display for TestSummary { - fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { - writeln!(f, "--- arc-evm-specs-tests summary ---")?; - writeln!(f, "files processed: {}", self.files_processed)?; - writeln!(f, "tests total: {}", self.tests_total)?; - writeln!(f, "tests passed: {}", self.tests_passed)?; - writeln!(f, "tests failed: {}", self.tests_failed)?; - writeln!(f, "tests skipped (spec): {}", self.tests_skipped_by_spec) - } -} - -impl TestSummary { - pub fn add_files_processed(&mut self, delta: usize) { - self.files_processed = self - .files_processed - .checked_add(delta) - .expect("files_processed overflow"); - } - - pub fn add_tests_total(&mut self, delta: usize) { - self.tests_total = self - .tests_total - .checked_add(delta) - .expect("tests_total overflow"); - } - - pub fn add_tests_passed(&mut self, delta: usize) { - self.tests_passed = self - .tests_passed - .checked_add(delta) - .expect("tests_passed overflow"); - } - - pub fn add_tests_failed(&mut self, delta: usize) { - self.tests_failed = self - .tests_failed - .checked_add(delta) - .expect("tests_failed overflow"); - } - - pub fn add_tests_skipped_by_spec(&mut self, delta: usize) { - self.tests_skipped_by_spec = self - .tests_skipped_by_spec - .checked_add(delta) - .expect("tests_skipped_by_spec overflow"); - } -} - -#[derive(Debug, Clone, Copy, PartialEq, Eq)] -pub enum RunStatus { - Success = 0, - TestsFailed = 1, - FatalFileErrors = 2, -} - -#[cfg(test)] -mod tests { - use super::*; - use alloy_primitives::{bytes, B256}; - - #[test] - fn passed_result_has_empty_error() { - let result = TestResult::passed("fixture/Prague/d0_g0_v0".to_string()); - - assert!(result.pass); - assert_eq!(result.name, "fixture/Prague/d0_g0_v0"); - assert!(result.error.is_empty()); - } - - #[test] - fn failed_result_preserves_error() { - let result = TestResult::failed("fixture".to_string(), "boom".to_string()); - - assert!(!result.pass); - assert_eq!(result.name, "fixture"); - assert_eq!(result.error, "boom"); - } - - #[test] - fn json_outcome_fields_serialize_only_when_present() { - let result = TestResult::passed("fixture".to_string()) - .with_variant_id("fixture/Berlin/d0_g0_v0") - .with_json_outcome(JsonOutcome { - state_root: B256::ZERO, - logs_root: B256::ZERO, - output: bytes!("01"), - gas_used: 21000, - error_msg: String::new(), - evm_result: "Success: Stop".to_string(), - post_logs_hash: B256::ZERO, - fork: "BERLIN".to_string(), - test: "fixture/Berlin/d0_g0_v0".to_string(), - data_index: 0, - gas_index: 0, - value_index: 0, - }); - - let json = serde_json::to_value(result).unwrap(); - assert_eq!(json.get("name").unwrap(), "fixture"); - assert_eq!(json.get("variantId").unwrap(), "fixture/Berlin/d0_g0_v0"); - assert_eq!(json.get("gasUsed").unwrap(), 21000); - assert_eq!(json.get("fork").unwrap(), "BERLIN"); - assert!(json.get("stateRoot").is_some()); - assert_eq!(json.get("data_index").unwrap(), 0); - assert_eq!(json.get("gas_index").unwrap(), 0); - assert_eq!(json.get("value_index").unwrap(), 0); - } - - #[test] - fn summary_display_includes_all_counts() { - let summary = TestSummary { - files_processed: 2, - tests_total: 7, - tests_passed: 5, - tests_failed: 1, - tests_skipped_by_spec: 1, - }; - - let rendered = summary.to_string(); - - assert!(rendered.contains("--- arc-evm-specs-tests summary ---")); - assert!(rendered.contains("files processed: 2")); - assert!(rendered.contains("tests total: 7")); - assert!(rendered.contains("tests passed: 5")); - assert!(rendered.contains("tests failed: 1")); - assert!(rendered.contains("tests skipped (spec): 1")); - } - - #[test] - fn summary_adders_accumulate_counts() { - let mut summary = TestSummary::default(); - - summary.add_files_processed(2); - summary.add_tests_total(7); - summary.add_tests_passed(5); - summary.add_tests_failed(1); - summary.add_tests_skipped_by_spec(1); - - assert_eq!(summary.files_processed, 2); - assert_eq!(summary.tests_total, 7); - assert_eq!(summary.tests_passed, 5); - assert_eq!(summary.tests_failed, 1); - assert_eq!(summary.tests_skipped_by_spec, 1); - } - - #[test] - fn run_status_exit_codes_are_stable() { - assert_eq!(RunStatus::Success as i32, 0); - assert_eq!(RunStatus::TestsFailed as i32, 1); - assert_eq!(RunStatus::FatalFileErrors as i32, 2); - } -} diff --git a/crates/evm-specs-tests/src/roots.rs b/crates/evm-specs-tests/src/roots.rs deleted file mode 100644 index 440a32b..0000000 --- a/crates/evm-specs-tests/src/roots.rs +++ /dev/null @@ -1,375 +0,0 @@ -// Copyright 2026 Circle Internet Group, Inc. All rights reserved. -// -// SPDX-License-Identifier: Apache-2.0 -// -// Licensed under the Apache License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. -// You may obtain a copy of the License at -// -// http://www.apache.org/licenses/LICENSE-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// See the License for the specific language governing permissions and -// limitations under the License. - -use std::convert::Infallible; - -use alloy_primitives::{address, keccak256, Address, B256, U256}; -use alloy_rlp::{RlpEncodable, RlpMaxEncodedLen}; -use alloy_trie::{ - root::{state_root_unhashed, storage_root_unhashed}, - TrieAccount as FixtureTrieAccount, EMPTY_ROOT_HASH, -}; -use hash_db::Hasher; -use plain_hasher::PlainHasher; -use revm::{ - context::result::{EVMError, ExecutionResult, HaltReason, InvalidTransaction}, - database::{bal::EvmDatabaseError, EmptyDB, PlainAccount, State}, -}; -use revm_primitives::Log; -use revm_statetest_types::AccountInfo as FixtureAccountInfo; -use triehash::sec_trie_root; - -pub struct TestValidationResult { - pub logs_root: B256, - pub state_root: B256, -} - -const ARC_NATIVE_COIN_AUTHORITY: Address = address!("1800000000000000000000000000000000000000"); -const ARC_NATIVE_COIN_CONTROL: Address = address!("1800000000000000000000000000000000000001"); -const ARC_SYSTEM_ACCOUNTING: Address = address!("1800000000000000000000000000000000000002"); -const ARC_CALL_FROM: Address = address!("1800000000000000000000000000000000000003"); -const ARC_PQ: Address = address!("1800000000000000000000000000000000000004"); - -const ARC_SYSTEM_ADDRESSES: &[Address] = &[ - ARC_NATIVE_COIN_AUTHORITY, - ARC_NATIVE_COIN_CONTROL, - ARC_SYSTEM_ACCOUNTING, - ARC_CALL_FROM, - ARC_PQ, -]; - -#[inline] -fn is_arc_system_address(address: &Address) -> bool { - ARC_SYSTEM_ADDRESSES.iter().any(|a| a == address) -} - -pub fn compute_test_roots( - exec_result: &Result< - ExecutionResult, - EVMError, InvalidTransaction>, - >, - db: &State, -) -> TestValidationResult { - TestValidationResult { - logs_root: compute_logs_hash( - filter_validation_logs( - exec_result - .as_ref() - .map(|result| result.logs()) - .unwrap_or_default(), - ) - .as_slice(), - ), - state_root: state_merkle_trie_root( - db.cache - .trie_account() - .into_iter() - .filter(|(address, _)| !is_arc_system_address(address)), - ), - } -} - -pub fn compute_logs_hash(logs: &[Log]) -> B256 { - let mut encoded = Vec::new(); - alloy_rlp::encode_list(logs, &mut encoded); - keccak256(&encoded) -} - -pub fn filter_validation_logs(logs: &[Log]) -> Vec { - logs.iter() - .filter(|log| log.address != ARC_NATIVE_COIN_AUTHORITY) - .cloned() - .collect() -} - -pub fn state_merkle_trie_root<'a>( - accounts: impl IntoIterator, -) -> B256 { - trie_root( - accounts - .into_iter() - .filter(|(_, account)| !is_empty_plain_account(account)) - .map(|(address, account)| { - ( - address, - alloy_rlp::encode_fixed_size(&TrieAccount::new(account)), - ) - }), - ) -} - -#[derive(RlpEncodable, RlpMaxEncodedLen)] -struct TrieAccount { - nonce: u64, - balance: U256, - root_hash: B256, - code_hash: B256, -} - -impl TrieAccount { - fn new(account: &PlainAccount) -> Self { - Self { - nonce: account.info.nonce, - balance: account.info.balance, - root_hash: sec_trie_root::( - account - .storage - .iter() - .filter(|(_slot, value)| !value.is_zero()) - .map(|(slot, value)| { - ( - slot.to_be_bytes::<32>(), - alloy_rlp::encode_fixed_size(value), - ) - }), - ), - code_hash: account.info.code_hash, - } - } -} - -fn is_empty_plain_account(account: &PlainAccount) -> bool { - account.info.is_empty() && account.storage.values().all(|value| value.is_zero()) -} - -#[inline] -fn trie_root(input: I) -> B256 -where - I: IntoIterator, - A: AsRef<[u8]>, - B: AsRef<[u8]>, -{ - sec_trie_root::(input) -} - -#[derive(Default, Debug, Clone, PartialEq, Eq, Hash)] -struct KeccakHasher; - -impl Hasher for KeccakHasher { - type Out = B256; - type StdHasher = PlainHasher; - const LENGTH: usize = 32; - - fn hash(x: &[u8]) -> Self::Out { - keccak256(x) - } -} - -pub fn compute_state_root_from_fixture_accounts( - accounts: &alloy_primitives::map::HashMap, -) -> B256 { - state_root_unhashed(accounts.iter().map(|(address, account)| { - let storage_root = if account.storage.is_empty() { - EMPTY_ROOT_HASH - } else { - storage_root_unhashed( - account - .storage - .iter() - .map(|(slot, value)| (B256::from(slot.to_be_bytes::<32>()), *value)), - ) - }; - ( - *address, - FixtureTrieAccount { - nonce: account.nonce, - balance: account.balance, - storage_root, - code_hash: keccak256(&account.code), - }, - ) - })) -} - -#[cfg(test)] -mod tests { - use super::*; - use alloy_primitives::{address, Bytes}; - use revm::state::AccountInfo; - use revm_database::states::CacheState; - - #[test] - fn logs_hash_changes_when_log_payload_changes() { - let baseline_log = Log { - address: address!("1000000000000000000000000000000000000001"), - data: revm_primitives::LogData::new_unchecked( - vec![B256::ZERO], - Bytes::from(vec![1, 2]), - ), - }; - let changed_log = Log { - address: baseline_log.address, - data: revm_primitives::LogData::new_unchecked( - vec![B256::repeat_byte(0x11)], - Bytes::from(vec![1, 2, 3]), - ), - }; - - let baseline_hash = compute_logs_hash(std::slice::from_ref(&baseline_log)); - assert_eq!( - baseline_hash, - compute_logs_hash(std::slice::from_ref(&baseline_log)) - ); - assert_ne!( - baseline_hash, - compute_logs_hash(std::slice::from_ref(&changed_log)) - ); - } - - #[test] - fn filter_validation_logs_excludes_arc_authority_log() { - let authority_log = Log { - address: ARC_NATIVE_COIN_AUTHORITY, - data: revm_primitives::LogData::new_unchecked(vec![B256::ZERO], Bytes::from(vec![1])), - }; - let user_log = Log { - address: address!("1000000000000000000000000000000000000001"), - data: revm_primitives::LogData::new_unchecked( - vec![B256::repeat_byte(0x11)], - Bytes::from(vec![2, 3]), - ), - }; - - assert_eq!( - filter_validation_logs(&[authority_log, user_log.clone()]), - vec![user_log] - ); - } - - #[test] - fn revm_state_root_ignores_zero_storage_slots() { - let address = address!("2000000000000000000000000000000000000002"); - let mut cache = CacheState::new(true); - cache.insert_account_with_storage( - address, - AccountInfo::default(), - std::collections::HashMap::from_iter([ - (U256::from(1), U256::ZERO), - (U256::from(2), U256::from(22)), - ]), - ); - - let root = state_merkle_trie_root(cache.trie_account()); - - let expected = - compute_state_root_from_fixture_accounts(&alloy_primitives::map::HashMap::from_iter([ - ( - address, - FixtureAccountInfo { - balance: U256::ZERO, - code: Bytes::default(), - nonce: 0, - storage: alloy_primitives::map::HashMap::from_iter([( - U256::from(2), - U256::from(22), - )]), - }, - ), - ])); - - assert_eq!(root, expected); - } - - #[test] - fn state_root_ignores_empty_accounts() { - let address = address!("3000000000000000000000000000000000000003"); - let mut cache = CacheState::new(true); - cache.insert_account(address, AccountInfo::default()); - - assert_eq!( - state_merkle_trie_root(cache.trie_account()), - EMPTY_ROOT_HASH - ); - } - - #[test] - fn arc_system_addresses_recognized() { - for arc in ARC_SYSTEM_ADDRESSES { - assert!( - is_arc_system_address(arc), - "{arc} should be recognized as ARC system address" - ); - } - - for non_arc in [ - Address::ZERO, - address!("1800000000000000000000000000000000000005"), - address!("1000000000000000000000000000000000000001"), - ] { - assert!( - !is_arc_system_address(&non_arc), - "{non_arc} should not be recognized as ARC system address" - ); - } - } - - #[test] - fn state_root_filter_excludes_arc_system_accounts_with_state() { - let user_address = address!("4000000000000000000000000000000000000004"); - let user_info = AccountInfo { - balance: U256::from(42), - nonce: 7, - ..Default::default() - }; - - let mut cache_with_arc = CacheState::new(true); - for arc in ARC_SYSTEM_ADDRESSES { - cache_with_arc.insert_account( - *arc, - AccountInfo { - balance: U256::from(999), - nonce: 1, - ..Default::default() - }, - ); - } - cache_with_arc.insert_account(user_address, user_info.clone()); - - let filtered_root = state_merkle_trie_root( - cache_with_arc - .trie_account() - .into_iter() - .filter(|(address, _)| !is_arc_system_address(address)), - ); - - let mut cache_user_only = CacheState::new(true); - cache_user_only.insert_account(user_address, user_info); - let expected_root = state_merkle_trie_root(cache_user_only.trie_account()); - - assert_eq!(filtered_root, expected_root); - assert_ne!(filtered_root, EMPTY_ROOT_HASH); - } - - #[test] - fn fixture_state_root_matches_expected_fixture_accounts() { - let mut accounts = alloy_primitives::map::HashMap::default(); - accounts.insert( - address!("1000000000000000000000000000000000000001"), - FixtureAccountInfo { - balance: U256::from(7), - code: Bytes::default(), - nonce: 2, - storage: alloy_primitives::map::HashMap::from_iter([ - (U256::from(1), U256::from(11)), - (U256::from(2), U256::from(22)), - ]), - }, - ); - - let root = compute_state_root_from_fixture_accounts(&accounts); - assert_ne!(root, B256::ZERO); - } -} diff --git a/crates/evm-specs-tests/src/runner.rs b/crates/evm-specs-tests/src/runner.rs deleted file mode 100644 index c145f3f..0000000 --- a/crates/evm-specs-tests/src/runner.rs +++ /dev/null @@ -1,1854 +0,0 @@ -// Copyright 2026 Circle Internet Group, Inc. All rights reserved. -// -// SPDX-License-Identifier: Apache-2.0 -// -// Licensed under the Apache License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. -// You may obtain a copy of the License at -// -// http://www.apache.org/licenses/LICENSE-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// See the License for the specific language governing permissions and -// limitations under the License. - -//! ARC-backed statetest runner and harness. -//! -//! This module owns both per-suite execution and file-level orchestration, -//! closer to upstream `revme`, while preserving ARC execution semantics and -//! the structured consume-direct output contract. -//! -//! Important execution-mode note: -//! - fixture fork names still choose the REVM `cfg.spec` -//! - the executor chain spec is always ARC `LOCAL_DEV` -//! - this runner therefore measures ARC localdev behavior under Ethereum -//! fixture inputs, not pure Ethereum fork-isolated execution - -use std::{ - collections::{BTreeSet, HashMap}, - io::stderr, - path::{Path, PathBuf}, - sync::{ - atomic::{AtomicUsize, Ordering}, - Arc, Mutex, - }, -}; - -use alloy_primitives::{address, Address, Bytes, B256}; -use arc_evm::ArcEvmFactory; -use reth_evm::{Evm, EvmEnv, EvmFactory}; -use revm::{context::CfgEnv, database::State}; -use revm_inspector::{inspectors::TracerEip3155, InspectCommitEvm}; -use revm_primitives::hardfork::SpecId; -use revm_statetest_types::{SpecName, Test, TestSuite, TestUnit}; - -use crate::adapter::{ - build_default_arc_chain_spec, build_evm_env, build_evm_factory, extract_chain_ids, - is_supported_spec, resolve_chain_id, -}; -use crate::error::{EvmSpecsTestError, TestErrorKind}; -use crate::exception_match::{ - exception_matches, tx_env_actual_exception, tx_env_exception_matches, -}; -use crate::fixture_sanitizer::strip_unsupported_fields; -use crate::result::{JsonOutcome, TestResult, TestSummary}; -use crate::roots::{ - compute_state_root_from_fixture_accounts, compute_test_roots, state_merkle_trie_root, - TestValidationResult, -}; - -const ARC_NATIVE_COIN_AUTHORITY: Address = address!("1800000000000000000000000000000000000000"); -const ARC_NATIVE_COIN_CONTROL: Address = address!("1800000000000000000000000000000000000001"); - -const SIGNAL_ARC_NATIVE_COIN_AUTHORITY_LOG_PRESENT: &str = "arc_native_coin_authority_log_present"; -const SIGNAL_ARC_NATIVE_COIN_CONTROL_STATE_TOUCHED: &str = "arc_native_coin_control_state_touched"; -const SIGNAL_ARC_SYSTEM_ACCOUNT_TOUCHED: &str = "arc_system_account_touched"; -const SIGNAL_PRECOMPILE_ADDRESS_TOUCHED: &str = "precompile_address_touched"; -const SIGNAL_COINBASE_TOUCHED: &str = "coinbase_touched"; -const SIGNAL_FIXTURE_ORACLE_ROOT_MATCHES_FIXTURE_HASH: &str = - "fixture_oracle_root_matches_fixture_hash"; - -#[derive(Default)] -struct FileRunOutput { - results: Vec, - summary: TestSummary, - fatal_file_errors: usize, -} - -struct RunnerConfig { - filter_name: Option, - trace: bool, - json_outcome: bool, -} - -#[derive(Clone)] -struct RunnerState { - completed_files: Arc, - queue: Arc)>>, - total_files: usize, -} - -impl RunnerState { - fn new(files: Vec) -> Self { - let total_files = files.len(); - Self { - completed_files: Arc::new(AtomicUsize::new(0)), - queue: Arc::new(Mutex::new((0, files))), - total_files, - } - } - - fn next_file(&self) -> Result, EvmSpecsTestError> { - let (next_idx, files) = &mut *self - .queue - .lock() - .map_err(|_| EvmSpecsTestError::RunnerQueuePoisoned)?; - let index = *next_idx; - let Some(path) = files.get(index).cloned() else { - return Ok(None); - }; - *next_idx = index.checked_add(1).expect("runner queue index overflow"); - Ok(Some((index, path))) - } -} - -struct TestExecutionContext<'a> { - factory: &'a ArcEvmFactory, - cache_state: &'a revm_database::states::CacheState, - evm_env: &'a EvmEnv, - unit: &'a TestUnit, - test: &'a Test, - test_id: &'a str, -} - -struct DebugContext<'a> { - factory: &'a ArcEvmFactory, - cache_state: &'a revm_database::states::CacheState, - evm_env: &'a EvmEnv, - unit: &'a TestUnit, - test: &'a Test, - test_id: &'a str, - error: &'a EvmSpecsTestError, -} - -struct TestExecutionReport { - error: Option, - json_outcome: Option, -} - -struct LoadedFixtureFile { - suite: TestSuite, - chain_id_map: HashMap, -} - -struct TestUnitExecution<'a, 'b> { - name: &'a str, - unit: &'a TestUnit, - factory: &'a ArcEvmFactory, - chain_id_map: &'a HashMap, - filter_name: Option<&'a str>, - trace: bool, - json_outcome: bool, - summary: &'b mut TestSummary, - results: &'b mut Vec, -} - -pub fn run( - path: PathBuf, - filter_name: Option, - strict_exit: bool, - trace: bool, - json_outcome: bool, -) -> Result { - use crate::result::RunStatus; - - let json_files = find_json_files(&path)?; - if json_files.is_empty() { - return Err(EvmSpecsTestError::NoJsonFiles { - path: path.display().to_string(), - }); - } - - let state = RunnerState::new(json_files); - let config = Arc::new(RunnerConfig { - filter_name, - trace, - json_outcome, - }); - let num_threads = determine_thread_count(state.total_files, trace); - - let mut handles = Vec::with_capacity(num_threads); - for worker_id in 0..num_threads { - let state = state.clone(); - let config = Arc::clone(&config); - let thread = std::thread::Builder::new() - .name(format!("arc-evm-specs-tests-runner-{worker_id}")) - .spawn(move || run_file_worker(state, config)) - .map_err(EvmSpecsTestError::WorkerSpawn)?; - handles.push(thread); - } - - let mut indexed_outputs = Vec::new(); - for handle in handles { - let output = handle - .join() - .map_err(|_| EvmSpecsTestError::WorkerPanic)??; - indexed_outputs.extend(output); - } - indexed_outputs.sort_by_key(|(index, _)| *index); - - let mut all_results = Vec::new(); - let mut total_summary = TestSummary::default(); - let mut fatal_file_errors = 0usize; - - for (_, output) in indexed_outputs { - all_results.extend(output.results); - fatal_file_errors = fatal_file_errors - .checked_add(output.fatal_file_errors) - .expect("fatal_file_errors overflow"); - total_summary.add_files_processed(output.summary.files_processed); - total_summary.add_tests_total(output.summary.tests_total); - total_summary.add_tests_passed(output.summary.tests_passed); - total_summary.add_tests_failed(output.summary.tests_failed); - total_summary.add_tests_skipped_by_spec(output.summary.tests_skipped_by_spec); - } - - let json_output = - serde_json::to_string_pretty(&all_results).expect("Failed to serialize results"); - println!("{json_output}"); - eprintln!("{total_summary}"); - - if fatal_file_errors > 0 { - return Ok(RunStatus::FatalFileErrors); - } - if strict_exit && total_summary.tests_failed > 0 { - return Ok(RunStatus::TestsFailed); - } - - Ok(RunStatus::Success) -} - -/// Execute all tests in a deserialized TestSuite. -/// -/// `chain_id_map` is built from raw JSON via `extract_chain_ids()` before -/// calling this function (two-pass parse pattern). -/// -/// Returns a vec of TestResult (one per test variant) and updates the summary. -pub fn execute_test_suite( - suite: &TestSuite, - factory: &ArcEvmFactory, - chain_id_map: &HashMap, - filter_name: Option<&str>, - trace: bool, - json_outcome: bool, -) -> (Vec, TestSummary) { - let mut summary = TestSummary::default(); - let mut results = Vec::new(); - - for (name, unit) in &suite.0 { - execute_test_unit(TestUnitExecution { - name, - unit, - factory, - chain_id_map, - filter_name, - trace, - json_outcome, - summary: &mut summary, - results: &mut results, - }); - } - - (results, summary) -} - -fn run_file_worker( - state: RunnerState, - config: Arc, -) -> Result, EvmSpecsTestError> { - let mut outputs = Vec::new(); - - while let Some((index, file)) = state.next_file()? { - let output = process_fixture_file( - &file, - config.filter_name.as_deref(), - config.trace, - config.json_outcome, - &state.completed_files, - state.total_files, - ); - outputs.push((index, output)); - } - - Ok(outputs) -} - -fn process_fixture_file( - file: &Path, - filter_name: Option<&str>, - trace: bool, - json_outcome: bool, - completed_files: &AtomicUsize, - total_files: usize, -) -> FileRunOutput { - let loaded_fixture = match load_fixture_file(file) { - Ok(loaded_fixture) => loaded_fixture, - Err(error) => { - report_progress(completed_files, total_files); - return file_error_result(file, error); - } - }; - - let chain_spec = build_default_arc_chain_spec(); - let factory = build_evm_factory(chain_spec); - let (results, mut summary) = execute_test_suite( - &loaded_fixture.suite, - &factory, - &loaded_fixture.chain_id_map, - filter_name, - trace, - json_outcome, - ); - summary.files_processed = 1; - - report_progress(completed_files, total_files); - - FileRunOutput { - results, - summary, - fatal_file_errors: 0, - } -} - -fn load_fixture_file(file: &Path) -> Result { - let bytes = std::fs::read(file).map_err(|e| format!("read error: {e}"))?; - let raw_json: serde_json::Value = serde_json::from_slice(&bytes).map_err(|source| { - EvmSpecsTestError::JsonParse { - path: file.display().to_string(), - source, - } - .to_string() - })?; - - if !looks_like_statetest_fixture(&raw_json) { - return Err(format!( - "unsupported or malformed statetest fixture shape in {}", - file.display() - )); - } - - let chain_id_map = extract_chain_ids(&raw_json).map_err(|e| e.to_string())?; - let mut sanitized_json = raw_json; - strip_unsupported_fields(&mut sanitized_json); - let suite = serde_json::from_value(sanitized_json).map_err(|source| { - EvmSpecsTestError::JsonParse { - path: file.display().to_string(), - source, - } - .to_string() - })?; - - Ok(LoadedFixtureFile { - suite, - chain_id_map, - }) -} - -fn execute_test_unit(exec: TestUnitExecution<'_, '_>) { - let TestUnitExecution { - name, - unit, - factory, - chain_id_map, - filter_name, - trace, - json_outcome, - summary, - results, - } = exec; - let cache_state = unit.state(); - - let chain_id = - match resolve_test_unit_chain_id(name, unit, chain_id_map, filter_name, summary, results) { - Some(chain_id) => chain_id, - None => return, - }; - - for (spec_name, tests) in &unit.post { - execute_spec_tests( - name, - unit, - factory, - &cache_state, - chain_id, - spec_name, - tests, - filter_name, - trace, - json_outcome, - summary, - results, - ); - } -} - -fn resolve_test_unit_chain_id( - name: &str, - unit: &TestUnit, - chain_id_map: &HashMap, - filter_name: Option<&str>, - summary: &mut TestSummary, - results: &mut Vec, -) -> Option { - match resolve_chain_id(name, chain_id_map, unit) { - Ok(chain_id) => Some(chain_id), - Err(error) => { - let rendered_error = error.to_string(); - for (spec_name, tests) in &unit.post { - push_failures_for_spec_variants( - name, - spec_name, - tests, - filter_name, - &rendered_error, - summary, - results, - ); - } - None - } - } -} - -#[allow(clippy::too_many_arguments)] -fn execute_spec_tests( - name: &str, - unit: &TestUnit, - factory: &ArcEvmFactory, - cache_state: &revm_database::states::CacheState, - chain_id: u64, - spec_name: &SpecName, - tests: &[Test], - filter_name: Option<&str>, - trace: bool, - json_outcome: bool, - summary: &mut TestSummary, - results: &mut Vec, -) { - if !is_supported_spec(spec_name) { - record_skipped_spec_tests(name, spec_name, tests, filter_name, summary); - return; - } - - let evm_env = match build_spec_evm_env(unit, spec_name, chain_id) { - Ok(evm_env) => evm_env, - Err(error) => { - push_failures_for_spec_variants( - name, - spec_name, - tests, - filter_name, - &error, - summary, - results, - ); - return; - } - }; - - for test in tests { - execute_spec_test(TestCaseExecution { - name, - unit, - factory, - cache_state, - evm_env: &evm_env, - spec_name, - test, - filter_name, - trace, - json_outcome, - summary, - results, - }); - } -} - -fn build_spec_evm_env( - unit: &TestUnit, - spec_name: &SpecName, - chain_id: u64, -) -> Result { - let mut evm_env = build_evm_env(unit, spec_name, chain_id).map_err(|e| e.to_string())?; - configure_blob_limits(&mut evm_env.cfg_env); - Ok(evm_env) -} - -struct TestCaseExecution<'a, 'b> { - name: &'a str, - unit: &'a TestUnit, - factory: &'a ArcEvmFactory, - cache_state: &'a revm_database::states::CacheState, - evm_env: &'a EvmEnv, - spec_name: &'a SpecName, - test: &'a Test, - filter_name: Option<&'a str>, - trace: bool, - json_outcome: bool, - summary: &'b mut TestSummary, - results: &'b mut Vec, -} - -fn execute_spec_test(exec: TestCaseExecution<'_, '_>) { - let test_id = format_test_id(exec.name, exec.spec_name, &exec.test.indexes); - if let Some(filter) = exec.filter_name - && !matches_filter(filter, &test_id, exec.name, exec.spec_name) - { - return; - } - - let result_name = output_name_for_filter(exec.filter_name, &test_id, exec.name, exec.spec_name); - exec.summary.add_tests_total(1); - - let ctx = TestExecutionContext { - factory: exec.factory, - cache_state: exec.cache_state, - evm_env: exec.evm_env, - unit: exec.unit, - test: exec.test, - test_id: &test_id, - }; - - let report = execute_single_test(ctx, exec.json_outcome); - push_test_execution_result(exec, test_id, result_name, report); -} - -fn push_test_execution_result( - exec: TestCaseExecution<'_, '_>, - test_id: String, - result_name: String, - report: TestExecutionReport, -) { - match report { - TestExecutionReport { - error: None, - json_outcome, - } => { - exec.summary.add_tests_passed(1); - if let Some(outcome) = json_outcome.as_ref() { - emit_json_outcome_line(outcome, true); - } - let result = TestResult::passed(result_name).with_variant_id(test_id); - exec.results.push(match json_outcome { - Some(outcome) => result.with_json_outcome(outcome), - None => result, - }); - } - TestExecutionReport { - error: Some(error), - json_outcome, - } => { - if exec.trace { - debug_failed_test(DebugContext { - factory: exec.factory, - cache_state: exec.cache_state, - evm_env: exec.evm_env, - unit: exec.unit, - test: exec.test, - test_id: &test_id, - error: &error, - }); - } - exec.summary.add_tests_failed(1); - if let Some(outcome) = json_outcome.as_ref() { - emit_json_outcome_line(outcome, false); - } - let result = - TestResult::failed(result_name, error.to_string()).with_variant_id(test_id); - exec.results.push(match json_outcome { - Some(outcome) => result.with_json_outcome(outcome), - None => result, - }); - } - } -} - -fn emit_json_outcome_line(outcome: &JsonOutcome, pass: bool) { - eprintln!("{}", build_json_outcome_report(outcome, pass)); -} - -fn build_json_outcome_report(outcome: &JsonOutcome, pass: bool) -> serde_json::Value { - serde_json::json!({ - "stateRoot": outcome.state_root, - "logsRoot": outcome.logs_root, - "output": outcome.output, - "gasUsed": outcome.gas_used, - "pass": pass, - "errorMsg": outcome.error_msg, - "evmResult": outcome.evm_result, - "postLogsHash": outcome.post_logs_hash, - "fork": outcome.fork, - "test": outcome.test, - // Keep the legacy aliases alongside the readable names so existing - // revm-style parsers do not silently break when consume-direct starts - // persisting `per_test_outcomes.jsonl`. - "d": outcome.data_index, - "g": outcome.gas_index, - "v": outcome.value_index, - "data_index": outcome.data_index, - "gas_index": outcome.gas_index, - "value_index": outcome.value_index, - }) -} - -fn record_skipped_spec_tests( - name: &str, - spec_name: &SpecName, - tests: &[Test], - filter_name: Option<&str>, - summary: &mut TestSummary, -) { - if let Some(filter) = filter_name { - for test in tests { - let test_id = format_test_id(name, spec_name, &test.indexes); - if matches_filter(filter, &test_id, name, spec_name) { - summary.add_tests_skipped_by_spec(1); - } - } - return; - } - summary.add_tests_skipped_by_spec(tests.len()); -} - -fn push_failures_for_spec_variants( - name: &str, - spec_name: &SpecName, - tests: &[Test], - filter_name: Option<&str>, - error: &str, - summary: &mut TestSummary, - results: &mut Vec, -) { - for test in tests { - let test_id = format_test_id(name, spec_name, &test.indexes); - if let Some(filter) = filter_name - && !matches_filter(filter, &test_id, name, spec_name) - { - continue; - } - let result_name = output_name_for_filter(filter_name, &test_id, name, spec_name); - summary.add_tests_total(1); - summary.add_tests_failed(1); - results.push(TestResult::failed(result_name, error.to_string()).with_variant_id(test_id)); - } -} - -fn configure_blob_limits(cfg: &mut CfgEnv) { - if cfg.spec.is_enabled_in(SpecId::OSAKA) { - cfg.set_max_blobs_per_tx(6); - } else if cfg.spec.is_enabled_in(SpecId::PRAGUE) { - cfg.set_max_blobs_per_tx(9); - } else { - cfg.set_max_blobs_per_tx(6); - } -} - -/// Execute a single test variant and validate results. -fn execute_single_test(ctx: TestExecutionContext<'_>, json_outcome: bool) -> TestExecutionReport { - let tx = match ctx.test.tx_env(ctx.unit) { - Ok(tx) => tx, - Err(err) => match handle_tx_env_error(&ctx, &err.to_string()) { - Ok(()) => { - return TestExecutionReport { - error: None, - json_outcome: None, - }; - } - Err(error) => { - return TestExecutionReport { - error: Some(error), - json_outcome: None, - }; - } - }, - }; - - let state = State::builder() - .with_cached_prestate(ctx.cache_state.clone()) - .with_bundle_update() - .build(); - - let mut evm = ctx.factory.create_evm(state, ctx.evm_env.clone()); - let exec_result = evm.transact_commit(tx); - let db = &*evm.db_mut(); - - let validation = compute_test_roots(&exec_result, db); - let error = evaluate_evm_execution(&ctx, ctx.unit.out.as_ref(), &exec_result, db, &validation); - let json_outcome = json_outcome.then(|| { - build_json_outcome( - &ctx, - &exec_result, - &validation, - error.as_ref().map(std::string::ToString::to_string), - ) - }); - - TestExecutionReport { - error, - json_outcome, - } -} - -fn handle_tx_env_error(ctx: &TestExecutionContext<'_>, err: &str) -> Result<(), EvmSpecsTestError> { - let actual_exception = tx_env_actual_exception(err).unwrap_or(err); - - if let Some(expected) = &ctx.test.expect_exception { - if tx_env_exception_matches(expected, actual_exception) { - return Ok(()); - } - - return Err(EvmSpecsTestError::TestFailure { - name: ctx.test_id.to_string(), - kind: TestErrorKind::evm( - "EXECUTION_MISMATCH", - "WRONG_EXCEPTION", - format!("expected_exception={expected}, got_exception={actual_exception}"), - ), - }); - } - - Err(EvmSpecsTestError::TestFailure { - name: ctx.test_id.to_string(), - kind: TestErrorKind::evm( - "HARNESS_PRECONDITION", - "TX_ENV_BUILD_FAILED", - err.to_string(), - ), - }) -} - -fn validate_expected_exception( - ctx: &TestExecutionContext<'_>, - exec_result: &Result< - revm::context::result::ExecutionResult, - revm::context::result::EVMError< - revm::database::bal::EvmDatabaseError, - revm::context::result::InvalidTransaction, - >, - >, -) -> Result { - match (&ctx.test.expect_exception, exec_result) { - (None, Err(e)) => Err(EvmSpecsTestError::TestFailure { - name: ctx.test_id.to_string(), - kind: TestErrorKind::evm( - "EXECUTION_MISMATCH", - "UNEXPECTED_EXCEPTION", - format!("expected_exception=None, got_exception={}", e), - ), - }), - (Some(expected), Ok(_)) => Err(EvmSpecsTestError::TestFailure { - name: ctx.test_id.to_string(), - kind: TestErrorKind::evm( - "EXECUTION_MISMATCH", - "UNEXPECTED_SUCCESS", - format!("expected_exception={expected}, got_exception=None"), - ), - }), - (Some(expected), Err(actual)) => { - let actual = actual.to_string(); - if exception_matches(expected, &actual) { - Ok(true) - } else { - Err(EvmSpecsTestError::TestFailure { - name: ctx.test_id.to_string(), - kind: TestErrorKind::evm( - "EXECUTION_MISMATCH", - "WRONG_EXCEPTION", - format!("expected_exception={expected}, got_exception={actual}"), - ), - }) - } - } - (None, Ok(_)) => Ok(false), - } -} - -fn validate_output( - ctx: &TestExecutionContext<'_>, - expected_output: Option<&Bytes>, - actual_result: &revm::context::result::ExecutionResult, -) -> Result<(), EvmSpecsTestError> { - if let Some((expected, actual)) = expected_output.zip(actual_result.output()) - && expected != actual - { - return Err(EvmSpecsTestError::TestFailure { - name: ctx.test_id.to_string(), - kind: TestErrorKind::evm( - "EXECUTION_MISMATCH", - "UNEXPECTED_OUTPUT", - format!("expected_output={expected:?}, got_output={actual:?}"), - ), - }); - } - - Ok(()) -} - -fn evaluate_evm_execution( - ctx: &TestExecutionContext<'_>, - expected_output: Option<&Bytes>, - exec_result: &Result< - revm::context::result::ExecutionResult, - revm::context::result::EVMError< - revm::database::bal::EvmDatabaseError, - revm::context::result::InvalidTransaction, - >, - >, - db: &State, - validation: &TestValidationResult, -) -> Option { - let logs = exec_result - .as_ref() - .map(|result| result.logs()) - .unwrap_or_default(); - - match validate_expected_exception(ctx, exec_result) { - Ok(true) => return None, - Ok(false) => {} - Err(error) => return Some(error), - } - - if let Ok(result) = exec_result - && let Err(error) = validate_output(ctx, expected_output, result) - { - return Some(error); - } - - if validation.logs_root != ctx.test.logs { - let logs_preview = summarize_logs(logs); - let signals = summarize_arc_signals(logs, db, None, ctx.unit.env.current_coinbase); - return Some(EvmSpecsTestError::TestFailure { - name: ctx.test_id.to_string(), - kind: TestErrorKind::evm( - "EXECUTION_MISMATCH", - "LOGS_HASH_MISMATCH", - format!( - "expected={}, got={}; logs_count={}, {}; signals={signals}", - ctx.test.logs, - validation.logs_root, - logs.len(), - logs_preview - ), - ), - }); - } - - if validation.state_root != ctx.test.hash { - let diagnostic = build_state_root_diagnostic(ctx, db, logs); - return Some(EvmSpecsTestError::TestFailure { - name: ctx.test_id.to_string(), - kind: TestErrorKind::evm( - "EXECUTION_MISMATCH", - "STATE_ROOT_MISMATCH", - format!( - "expected={}, got={}; diagnostic: {diagnostic}", - ctx.test.hash, validation.state_root - ), - ), - }); - } - - None -} - -fn build_state_root_diagnostic( - ctx: &TestExecutionContext<'_>, - db: &State, - logs: &[revm_primitives::Log], -) -> String { - let coinbase = ctx.unit.env.current_coinbase; - let coinbase_delta = db - .cache - .accounts - .get(&coinbase) - .and_then(|account| account.account.as_ref()) - .map(|account| { - format!( - "nonce={},balance={}", - account.info.nonce, account.info.balance - ) - }) - .unwrap_or_else(|| "missing".to_string()); - let touched_accounts = summarize_touched_accounts(db); - let actual_trie_accounts: BTreeSet<_> = db - .cache - .trie_account() - .into_iter() - .map(|(address, _)| address) - .collect(); - let filtered_arc_system_root = state_merkle_trie_root( - db.cache - .trie_account() - .into_iter() - .filter(|(address, _)| *address != ARC_NATIVE_COIN_CONTROL), - ); - let filtered_arc_system_and_coinbase_root = state_merkle_trie_root( - db.cache - .trie_account() - .into_iter() - .filter(|(address, _)| *address != ARC_NATIVE_COIN_CONTROL && *address != coinbase), - ); - - if ctx.test.post_state.is_empty() { - let signals = summarize_arc_signals(logs, db, None, coinbase); - return format!( - "fixture postState unavailable; actual_trie_accounts={}, filtered_arc_system_root={filtered_arc_system_root}, filtered_arc_system_and_coinbase_root={filtered_arc_system_and_coinbase_root}, fixture_hash={}, coinbase={}, coinbase_delta={coinbase_delta}, touched_accounts={touched_accounts}, signals={signals}", - actual_trie_accounts.len(), - ctx.test.hash, - coinbase - ); - } - - let oracle_root = compute_state_root_from_fixture_accounts(&ctx.test.post_state); - let expected_trie_accounts: BTreeSet<_> = ctx.test.post_state.keys().copied().collect(); - let extra_actual_accounts = actual_trie_accounts - .difference(&expected_trie_accounts) - .take(6) - .map(|address| address.to_string()) - .collect::>() - .join("|"); - let missing_expected_accounts = expected_trie_accounts - .difference(&actual_trie_accounts) - .take(6) - .map(|address| address.to_string()) - .collect::>() - .join("|"); - let signals = summarize_arc_signals(logs, db, Some((oracle_root, ctx.test.hash)), coinbase); - - format!( - "fixture_postState_root={oracle_root}, actual_trie_accounts={}, expected_trie_accounts={}, extra_actual_accounts={}, missing_expected_accounts={}, filtered_arc_system_root={filtered_arc_system_root}, filtered_arc_system_and_coinbase_root={filtered_arc_system_and_coinbase_root}, fixture_hash={}, coinbase={}, coinbase_delta={coinbase_delta}, touched_accounts={touched_accounts}, signals={signals}", - actual_trie_accounts.len(), - expected_trie_accounts.len(), - if extra_actual_accounts.is_empty() { "none" } else { &extra_actual_accounts }, - if missing_expected_accounts.is_empty() { "none" } else { &missing_expected_accounts }, - ctx.test.hash, - coinbase - ) -} - -fn build_json_outcome( - ctx: &TestExecutionContext<'_>, - exec_result: &Result< - revm::context::result::ExecutionResult, - revm::context::result::EVMError< - revm::database::bal::EvmDatabaseError, - revm::context::result::InvalidTransaction, - >, - >, - validation: &TestValidationResult, - error: Option, -) -> JsonOutcome { - JsonOutcome { - state_root: validation.state_root, - logs_root: validation.logs_root, - output: exec_result - .as_ref() - .ok() - .and_then(|result| result.output().cloned()) - .unwrap_or_default(), - gas_used: exec_result - .as_ref() - .ok() - .map(|result| result.gas_used()) - .unwrap_or_default(), - error_msg: error.unwrap_or_default(), - evm_result: format_evm_result(exec_result), - post_logs_hash: validation.logs_root, - fork: format!("{:?}", ctx.evm_env.cfg_env.spec), - test: ctx.test_id.to_string(), - data_index: ctx.test.indexes.data, - gas_index: ctx.test.indexes.gas, - value_index: ctx.test.indexes.value, - } -} - -fn format_evm_result( - exec_result: &Result< - revm::context::result::ExecutionResult, - revm::context::result::EVMError< - revm::database::bal::EvmDatabaseError, - revm::context::result::InvalidTransaction, - >, - >, -) -> String { - match exec_result { - Ok(result) => match result { - revm::context::result::ExecutionResult::Success { reason, .. } => { - format!("Success: {reason:?}") - } - revm::context::result::ExecutionResult::Revert { .. } => "Revert".to_string(), - revm::context::result::ExecutionResult::Halt { reason, .. } => { - format!("Halt: {reason:?}") - } - }, - Err(error) => error.to_string(), - } -} - -/// Format the stable identifier for a concrete fixture variant. -/// -/// This ID ties together the suite-level pytest HTML report and the -/// variant-level `per_test_outcomes.jsonl` artifact emitted during -/// `consume direct`, so keep it stable unless the downstream reporting -/// contract is updated in lockstep. -fn format_test_id( - name: &str, - spec: &SpecName, - indexes: &revm_statetest_types::TxPartIndices, -) -> String { - // Keep the historical `d{}_g{}_v{}` suffix inherited from the upstream - // `revm` statetest runner shape (`bins/revme/src/cmd/statetest/runner.rs`). - format!( - "{name}/{spec:?}/d{}_g{}_v{}", - indexes.data, indexes.gas, indexes.value - ) -} - -fn summarize_logs(logs: &[revm_primitives::Log]) -> String { - if logs.is_empty() { - return "first_log=none".to_string(); - } - - let first = &logs[0]; - let topics_len = first.data.topics().len(); - let data_len = first.data.data.len(); - format!( - "first_log=address={},topics={},data_len={}", - first.address, topics_len, data_len - ) -} - -fn summarize_arc_signals( - logs: &[revm_primitives::Log], - db: &State, - fixture_roots: Option<(B256, B256)>, - coinbase: Address, -) -> String { - let mut signals = Vec::new(); - - if logs - .iter() - .any(|log| log.address == ARC_NATIVE_COIN_AUTHORITY) - { - signals.push(SIGNAL_ARC_NATIVE_COIN_AUTHORITY_LOG_PRESENT.to_string()); - } - - if db.cache.accounts.contains_key(&ARC_NATIVE_COIN_CONTROL) { - signals.push(format!( - "{SIGNAL_ARC_NATIVE_COIN_CONTROL_STATE_TOUCHED}={ARC_NATIVE_COIN_CONTROL}" - )); - signals.push(format!( - "{SIGNAL_ARC_SYSTEM_ACCOUNT_TOUCHED}={ARC_NATIVE_COIN_CONTROL}" - )); - } - - if db.cache.accounts.contains_key(&coinbase) { - signals.push(format!("{SIGNAL_COINBASE_TOUCHED}={coinbase}")); - } - - if let Some(precompile_address) = first_touched_precompile_address(db) { - signals.push(format!( - "{SIGNAL_PRECOMPILE_ADDRESS_TOUCHED}={precompile_address}" - )); - } - - if let Some((oracle_root, fixture_hash)) = fixture_roots - && oracle_root == fixture_hash - { - signals.push(SIGNAL_FIXTURE_ORACLE_ROOT_MATCHES_FIXTURE_HASH.to_string()); - } - - if signals.is_empty() { - "none".to_string() - } else { - signals.join("|") - } -} - -fn summarize_touched_accounts(db: &State) -> String { - let mut entries: Vec<_> = db.cache.accounts.iter().collect(); - entries.sort_by_key(|(address, _)| *address); - - let preview = entries - .into_iter() - .take(4) - .filter_map(|(address, account)| account.account.as_ref().map(|plain| (address, plain))) - .map(|(address, account)| { - format!( - "{address}:nonce={},balance={},code_hash={},storage_slots={},selfdestructed={}", - account.info.nonce, - account.info.balance, - account.info.code_hash, - account.storage.len(), - false - ) - }) - .collect::>() - .join("|"); - - if preview.is_empty() { - "none".to_string() - } else { - preview - } -} - -fn first_touched_precompile_address(db: &State) -> Option
{ - let mut addresses: Vec<_> = db.cache.accounts.keys().copied().collect(); - addresses.sort(); - addresses - .into_iter() - .find(|address| is_precompile_address(*address)) -} - -fn is_precompile_address(address: Address) -> bool { - let bytes = address.as_slice(); - if !bytes[..18].iter().all(|byte| *byte == 0) { - return false; - } - - let index = u16::from_be_bytes([bytes[18], bytes[19]]); - matches!(index, 0x0001..=0x0011 | 0x0100) -} - -fn matches_filter(filter: &str, test_id: &str, fixture_name: &str, _spec: &SpecName) -> bool { - let filter_norm = normalize_fixture_name(filter); - let fixture_norm = normalize_fixture_name(fixture_name); - filter == test_id - || filter == fixture_name - || filter_norm == fixture_norm - || is_consume_state_filter_for_fixture(filter, fixture_name) -} - -fn output_name_for_filter( - filter_name: Option<&str>, - test_id: &str, - fixture_name: &str, - _spec: &SpecName, -) -> String { - match filter_name { - Some(filter) if filter == fixture_name => test_id.to_string(), - Some(filter) if normalize_fixture_name(filter) == normalize_fixture_name(fixture_name) => { - test_id.to_string() - } - Some(filter) if is_consume_state_filter_for_fixture(filter, fixture_name) => { - filter.to_string() - } - _ => fixture_name.to_string(), - } -} - -fn is_consume_state_filter_for_fixture(filter: &str, fixture_name: &str) -> bool { - let Some(suffix) = filter.strip_prefix(fixture_name) else { - return false; - }; - suffix.starts_with("[fork_") && suffix.ends_with("-state_test]") -} - -fn normalize_fixture_name(name: &str) -> &str { - name.strip_prefix("./").unwrap_or(name) -} - -fn debug_failed_test(ctx: DebugContext<'_>) { - eprintln!("\nTraces:"); - - let tx = match ctx.test.tx_env(ctx.unit) { - Ok(tx) => tx, - Err(err) => { - eprintln!("Unable to rebuild tx for trace rerun: {err}"); - eprintln!( - "\nTest name: {:?} failed before trace rerun:\n{}", - ctx.test_id, ctx.error - ); - return; - } - }; - - let state = State::builder() - .with_cached_prestate(ctx.cache_state.clone()) - .with_bundle_update() - .build(); - - let tracer = TracerEip3155::buffered(stderr()).without_summary(); - let mut evm = ctx - .factory - .create_evm_with_inspector(state, ctx.evm_env.clone(), tracer); - let exec_result = evm.inner.inspect_tx_commit(tx.clone()); - - eprintln!("\nExecution result: {exec_result:#?}"); - eprintln!("\nExpected exception: {:?}", ctx.test.expect_exception); - eprintln!("\nState before:\n{}", ctx.cache_state.pretty_print()); - eprintln!("\nState after:\n{}", evm.db_mut().cache.pretty_print()); - eprintln!("\nSpecification: {:?}", ctx.evm_env.cfg_env.spec); - eprintln!("\nTx: {tx:#?}"); - eprintln!("Block: {:#?}", ctx.evm_env.block_env); - eprintln!("Cfg: {:#?}", ctx.evm_env.cfg_env); - eprintln!("\nTest name: {:?} failed:\n{}", ctx.test_id, ctx.error); -} - -fn determine_thread_count(total_files: usize, trace: bool) -> usize { - if trace { - return 1; - } - std::thread::available_parallelism() - .map(|count| count.get().min(total_files)) - .unwrap_or(1) - .max(1) -} - -fn find_json_files(path: &Path) -> Result, EvmSpecsTestError> { - let mut files = Vec::new(); - if path.is_file() { - if path.extension().is_some_and(|ext| ext == "json") { - files.push(path.to_path_buf()); - } - } else if path.is_dir() { - collect_json_files_recursive(path, &mut files)?; - } - Ok(files) -} - -fn collect_json_files_recursive( - dir: &Path, - files: &mut Vec, -) -> Result<(), EvmSpecsTestError> { - for entry in std::fs::read_dir(dir)? { - let entry = entry?; - let path = entry.path(); - if path.is_dir() { - collect_json_files_recursive(&path, files)?; - } else if path.extension().is_some_and(|ext| ext == "json") { - files.push(path); - } - } - Ok(()) -} - -fn file_error_result(file: &Path, error: String) -> FileRunOutput { - let summary = TestSummary { - files_processed: 1, - ..TestSummary::default() - }; - - FileRunOutput { - results: vec![TestResult::failed( - format!("__file_error__/{}", file.display()), - error, - )], - summary, - fatal_file_errors: 1, - } -} - -fn looks_like_statetest_fixture(value: &serde_json::Value) -> bool { - let Some(obj) = value.as_object() else { - return false; - }; - obj.values().any(|entry| { - entry.is_object() - && entry.get("env").is_some() - && entry.get("post").is_some() - && entry.get("pre").is_some() - }) -} - -fn report_progress(completed_files: &AtomicUsize, total_files: usize) { - let done = completed_files - .fetch_add(1, Ordering::Relaxed) - .checked_add(1) - .expect("completed_files overflow"); - if done == 1 || done == total_files || done.is_multiple_of(100) { - eprintln!("[arc-evm-specs-tests] processed {done}/{total_files} fixture files"); - } -} - -#[cfg(test)] -mod tests { - use super::*; - use alloy_primitives::{bytes, LogData}; - use reth_evm::EvmEnv; - use revm::context::result::{ - EVMError, ExecutionResult, HaltReason, InvalidTransaction, OutOfGasError, Output, - SuccessReason, - }; - use revm::database::{EmptyDB, State}; - use revm::state::AccountInfo; - use revm_database::states::CacheState; - use revm_statetest_types::{Env, Test, TestSuite, TransactionParts}; - use std::{ - collections::BTreeMap, - time::{SystemTime, UNIX_EPOCH}, - }; - - fn touched_state(addresses: &[Address]) -> State { - let mut cache = CacheState::new(true); - for address in addresses { - cache.insert_account(*address, AccountInfo::default()); - } - State::builder() - .with_cached_prestate(cache) - .with_bundle_update() - .build() - } - - fn log_at(address: Address) -> revm_primitives::Log { - revm_primitives::Log { - address, - data: LogData::new_unchecked(vec![B256::ZERO], bytes!("01")), - } - } - - fn fixture_test(expect_exception: Option<&str>) -> Test { - serde_json::from_value(serde_json::json!({ - "expectException": expect_exception, - "indexes": { - "data": 0, - "gas": 0, - "value": 0 - }, - "hash": format!("{:#066x}", B256::ZERO), - "postState": {}, - "logs": format!("{:#066x}", B256::ZERO) - })) - .expect("test fixture should deserialize") - } - - fn fixture_unit(spec_name: SpecName) -> TestUnit { - let mut post = BTreeMap::new(); - post.insert(spec_name, vec![fixture_test(None)]); - - TestUnit { - info: None, - env: Env { - current_chain_id: Some(alloy_primitives::U256::from(1)), - current_coinbase: Address::ZERO, - current_difficulty: alloy_primitives::U256::ZERO, - current_gas_limit: alloy_primitives::U256::from(30_000_000), - current_number: alloy_primitives::U256::from(1), - current_timestamp: alloy_primitives::U256::from(1), - current_base_fee: Some(alloy_primitives::U256::ZERO), - previous_hash: None, - current_random: None, - current_beacon_root: None, - current_withdrawals_root: None, - current_excess_blob_gas: None, - }, - pre: alloy_primitives::map::HashMap::default(), - post, - transaction: TransactionParts { - tx_type: None, - data: vec![bytes!("")], - gas_limit: vec![alloy_primitives::U256::from(21_000)], - gas_price: Some(alloy_primitives::U256::from(1)), - nonce: alloy_primitives::U256::ZERO, - secret_key: B256::ZERO, - sender: Some(Address::ZERO), - to: Some(Address::ZERO), - value: vec![alloy_primitives::U256::ZERO], - max_fee_per_gas: None, - max_priority_fee_per_gas: None, - initcodes: None, - access_lists: vec![], - authorization_list: None, - blob_versioned_hashes: vec![], - max_fee_per_blob_gas: None, - }, - out: None, - } - } - - fn fixture_context<'a>( - unit: &'a TestUnit, - test: &'a Test, - evm_env: &'a EvmEnv, - test_id: &'a str, - ) -> TestExecutionContext<'a> { - let factory = - crate::adapter::build_evm_factory(crate::adapter::build_default_arc_chain_spec()); - let cache_state = CacheState::new(true); - - TestExecutionContext { - factory: Box::leak(Box::new(factory)), - cache_state: Box::leak(Box::new(cache_state)), - evm_env, - unit, - test, - test_id, - } - } - - fn call_result(output: Bytes) -> ExecutionResult { - ExecutionResult::Success { - reason: SuccessReason::Return, - gas_used: 21_000, - gas_refunded: 0, - logs: Vec::new(), - output: Output::Call(output), - } - } - - fn revert_result(output: Bytes) -> ExecutionResult { - ExecutionResult::Revert { - gas_used: 21_000, - output, - } - } - - #[test] - fn detects_arc_authority_log_signals() { - let signals = summarize_arc_signals( - &[log_at(ARC_NATIVE_COIN_AUTHORITY)], - &touched_state(&[]), - None, - Address::ZERO, - ); - - assert!(signals.contains(SIGNAL_ARC_NATIVE_COIN_AUTHORITY_LOG_PRESENT)); - } - - #[test] - fn detects_native_coin_control_touched_signals() { - let state = touched_state(&[ARC_NATIVE_COIN_CONTROL]); - let signals = summarize_arc_signals(&[], &state, None, Address::ZERO); - - assert!(signals.contains(SIGNAL_ARC_NATIVE_COIN_CONTROL_STATE_TOUCHED)); - assert!(signals.contains(SIGNAL_ARC_SYSTEM_ACCOUNT_TOUCHED)); - } - - #[test] - fn detects_touched_precompile_signal() { - let state = touched_state(&[address!("000000000000000000000000000000000000000a")]); - let signals = summarize_arc_signals(&[], &state, None, Address::ZERO); - - assert!(signals - .contains("precompile_address_touched=0x000000000000000000000000000000000000000A")); - } - - #[test] - fn ignores_non_precompile_low_address_signal() { - let state = touched_state(&[address!("000000000000000000000000000000000000006a")]); - let signals = summarize_arc_signals(&[], &state, None, Address::ZERO); - - assert!(!signals.contains(SIGNAL_PRECOMPILE_ADDRESS_TOUCHED)); - } - - #[test] - fn detects_p256_precompile_signal() { - let state = touched_state(&[address!("0000000000000000000000000000000000000100")]); - let signals = summarize_arc_signals(&[], &state, None, Address::ZERO); - - assert!(signals - .contains("precompile_address_touched=0x0000000000000000000000000000000000000100")); - } - - #[test] - fn detects_fixture_oracle_match_signal() { - let root = B256::repeat_byte(0x11); - let signals = - summarize_arc_signals(&[], &touched_state(&[]), Some((root, root)), Address::ZERO); - - assert!(signals.contains(SIGNAL_FIXTURE_ORACLE_ROOT_MATCHES_FIXTURE_HASH)); - } - - #[test] - fn filter_matching_does_not_use_substrings() { - let fixture_name = "foo_bar_1"; - let test_id = "foo_bar_1/Prague/d0_g0_v0"; - - assert!(matches_filter( - fixture_name, - test_id, - fixture_name, - &SpecName::Prague - )); - assert!(!matches_filter( - "foo_bar_10", - test_id, - fixture_name, - &SpecName::Prague - )); - } - - #[test] - fn output_name_prefers_fixture_filter_variants() { - let test_id = "fixture/Prague/d0_g0_v0"; - let fixture_name = "./fixture"; - - assert_eq!( - output_name_for_filter(Some("fixture"), test_id, fixture_name, &SpecName::Prague), - test_id - ); - assert_eq!( - output_name_for_filter( - Some("fixture[fork_Prague-state_test]"), - test_id, - "fixture", - &SpecName::Prague - ), - "fixture[fork_Prague-state_test]" - ); - assert_eq!( - output_name_for_filter(None, test_id, fixture_name, &SpecName::Prague), - fixture_name - ); - } - - #[test] - fn summarize_logs_and_touched_accounts_produce_previews() { - let log_summary = summarize_logs(&[log_at(ARC_NATIVE_COIN_AUTHORITY)]); - assert!(log_summary.contains("first_log=address=")); - assert!(log_summary.contains("topics=1")); - - let state = touched_state(&[ARC_NATIVE_COIN_CONTROL]); - let touched_summary = summarize_touched_accounts(&state); - assert!(touched_summary.contains("0x1800000000000000000000000000000000000001")); - assert!(touched_summary.contains("storage_slots=0")); - } - - #[test] - fn execute_test_suite_records_missing_chain_id_as_failures() { - let mut unit = fixture_unit(SpecName::Prague); - unit.env.current_chain_id = None; - let suite = TestSuite(BTreeMap::from([(String::from("fixture"), unit)])); - let factory = - crate::adapter::build_evm_factory(crate::adapter::build_default_arc_chain_spec()); - - let (results, summary) = - execute_test_suite(&suite, &factory, &HashMap::new(), None, false, false); - - assert_eq!(summary.tests_total, 1); - assert_eq!(summary.tests_failed, 1); - assert_eq!(results.len(), 1); - assert!(results[0].error.contains("Missing chain_id")); - } - - #[test] - fn execute_test_suite_skips_unknown_specs() { - let suite = TestSuite(BTreeMap::from([( - String::from("fixture"), - fixture_unit(SpecName::Unknown), - )])); - let factory = - crate::adapter::build_evm_factory(crate::adapter::build_default_arc_chain_spec()); - - let (results, summary) = - execute_test_suite(&suite, &factory, &HashMap::new(), None, false, false); - - assert!(results.is_empty()); - assert_eq!(summary.tests_total, 0); - assert_eq!(summary.tests_failed, 0); - assert_eq!(summary.tests_skipped_by_spec, 1); - } - - #[test] - fn execute_test_suite_reports_tx_env_build_failures() { - let mut unit = fixture_unit(SpecName::Prague); - unit.transaction.to = None; - unit.transaction.max_fee_per_blob_gas = Some(alloy_primitives::U256::from(1)); - let suite = TestSuite(BTreeMap::from([(String::from("fixture"), unit)])); - let factory = - crate::adapter::build_evm_factory(crate::adapter::build_default_arc_chain_spec()); - - let (results, summary) = - execute_test_suite(&suite, &factory, &HashMap::new(), None, false, false); - - assert_eq!(summary.tests_total, 1); - assert_eq!(summary.tests_failed, 1); - assert_eq!(results.len(), 1); - assert!(results[0].error.contains("HARNESS_PRECONDITION")); - assert!(results[0].error.contains("TX_ENV_BUILD_FAILED")); - } - - #[test] - fn handle_tx_env_error_accepts_fixture_declared_exception() { - let unit = fixture_unit(SpecName::Prague); - let test = fixture_test(Some("PriorityGreaterThanMaxFeePerGas")); - let evm_env = crate::adapter::build_evm_env(&unit, &SpecName::Prague, 1) - .expect("test env should build"); - let ctx = fixture_context(&unit, &test, &evm_env, "fixture/Prague/d0_g0_v0"); - - let result = handle_tx_env_error( - &ctx, - "tx env build failed: got Some(\"priority fee is greater than max fee\")", - ); - - assert!(result.is_ok()); - } - - #[test] - fn handle_tx_env_error_reports_harness_precondition_without_expected_exception() { - let unit = fixture_unit(SpecName::Prague); - let test = fixture_test(None); - let evm_env = crate::adapter::build_evm_env(&unit, &SpecName::Prague, 1) - .expect("test env should build"); - let ctx = fixture_context(&unit, &test, &evm_env, "fixture/Prague/d0_g0_v0"); - - let error = handle_tx_env_error(&ctx, "tx env build failed: missing to").unwrap_err(); - let rendered = error.to_string(); - - assert!(rendered.contains("HARNESS_PRECONDITION")); - assert!(rendered.contains("TX_ENV_BUILD_FAILED")); - assert!(rendered.contains("missing to")); - } - - #[test] - fn validate_expected_exception_distinguishes_expected_and_unexpected_outcomes() { - let unit = fixture_unit(SpecName::Prague); - let evm_env = crate::adapter::build_evm_env(&unit, &SpecName::Prague, 1) - .expect("test env should build"); - - let matching_test = fixture_test(Some("PriorityGreaterThanMaxFeePerGas")); - let matching_ctx = - fixture_context(&unit, &matching_test, &evm_env, "fixture/Prague/d0_g0_v0"); - let matching_error = Err(EVMError::Transaction( - InvalidTransaction::PriorityFeeGreaterThanMaxFee, - )); - assert!(validate_expected_exception(&matching_ctx, &matching_error) - .expect("expected exception should match")); - - let unexpected_success_test = fixture_test(Some("PriorityGreaterThanMaxFeePerGas")); - let unexpected_success_ctx = fixture_context( - &unit, - &unexpected_success_test, - &evm_env, - "fixture/Prague/d0_g0_v0", - ); - let unexpected_success = - validate_expected_exception(&unexpected_success_ctx, &Ok(call_result(bytes!("01")))) - .unwrap_err() - .to_string(); - assert!(unexpected_success.contains("UNEXPECTED_SUCCESS")); - - let unexpected_exception_test = fixture_test(None); - let unexpected_exception_ctx = fixture_context( - &unit, - &unexpected_exception_test, - &evm_env, - "fixture/Prague/d0_g0_v0", - ); - let unexpected_exception = validate_expected_exception( - &unexpected_exception_ctx, - &Err(EVMError::Transaction( - InvalidTransaction::PriorityFeeGreaterThanMaxFee, - )), - ) - .unwrap_err() - .to_string(); - assert!(unexpected_exception.contains("UNEXPECTED_EXCEPTION")); - } - - #[test] - fn validate_output_flags_only_mismatched_call_data() { - let mut unit = fixture_unit(SpecName::Prague); - unit.out = Some(bytes!("0102")); - let test = fixture_test(None); - let evm_env = crate::adapter::build_evm_env(&unit, &SpecName::Prague, 1) - .expect("test env should build"); - let ctx = fixture_context(&unit, &test, &evm_env, "fixture/Prague/d0_g0_v0"); - - validate_output(&ctx, unit.out.as_ref(), &call_result(bytes!("0102"))) - .expect("matching output should pass"); - - let error = validate_output(&ctx, unit.out.as_ref(), &call_result(bytes!("03"))) - .unwrap_err() - .to_string(); - assert!(error.contains("UNEXPECTED_OUTPUT")); - - validate_output( - &ctx, - unit.out.as_ref(), - &ExecutionResult::Halt { - reason: HaltReason::OutOfGas(OutOfGasError::Basic), - gas_used: 21_000, - }, - ) - .expect("halted executions do not have output to compare"); - } - - #[test] - fn configure_blob_limits_tracks_fork_specific_limits() { - let mut pre_prague = CfgEnv::default(); - pre_prague.spec = SpecId::CANCUN; - configure_blob_limits(&mut pre_prague); - assert_eq!(pre_prague.max_blobs_per_tx, Some(6)); - - let mut prague = CfgEnv::default(); - prague.spec = SpecId::PRAGUE; - configure_blob_limits(&mut prague); - assert_eq!(prague.max_blobs_per_tx, Some(9)); - - let mut osaka = CfgEnv::default(); - osaka.spec = SpecId::OSAKA; - configure_blob_limits(&mut osaka); - assert_eq!(osaka.max_blobs_per_tx, Some(6)); - } - - #[test] - fn format_evm_result_preserves_result_kind_for_json_consumers() { - assert_eq!( - format_evm_result(&Ok(call_result(bytes!("010203")))), - "Success: Return" - ); - assert_eq!( - format_evm_result(&Ok(revert_result(bytes!("deadbeef")))), - "Revert" - ); - assert_eq!( - format_evm_result(&Ok(ExecutionResult::Halt { - reason: HaltReason::OutOfGas(OutOfGasError::Basic), - gas_used: 21_000, - })), - "Halt: OutOfGas(Basic)" - ); - assert_eq!( - format_evm_result(&Err(EVMError::Transaction( - InvalidTransaction::PriorityFeeGreaterThanMaxFee, - ))), - "transaction validation error: priority fee is greater than max fee" - ); - } - - #[test] - fn json_outcome_report_preserves_legacy_aliases_and_readable_index_names() { - let outcome = JsonOutcome { - state_root: B256::repeat_byte(0x11), - logs_root: B256::repeat_byte(0x22), - output: bytes!("0102"), - gas_used: 21_000, - error_msg: "boom".to_string(), - evm_result: "Success: Return".to_string(), - post_logs_hash: B256::repeat_byte(0x33), - fork: "BERLIN".to_string(), - test: "fixture/Berlin/d0_g0_v0".to_string(), - data_index: 0, - gas_index: 1, - value_index: 2, - }; - - let report = build_json_outcome_report(&outcome, false); - - assert_eq!(report.get("pass").unwrap(), false); - assert_eq!(report.get("gasUsed").unwrap(), 21_000); - assert_eq!(report.get("fork").unwrap(), "BERLIN"); - assert_eq!(report.get("errorMsg").unwrap(), "boom"); - assert_eq!(report.get("d").unwrap(), 0); - assert_eq!(report.get("g").unwrap(), 1); - assert_eq!(report.get("v").unwrap(), 2); - assert_eq!(report.get("data_index").unwrap(), 0); - assert_eq!(report.get("gas_index").unwrap(), 1); - assert_eq!(report.get("value_index").unwrap(), 2); - } - - #[test] - fn file_errors_are_counted_in_summary() { - let output = file_error_result(Path::new("/tmp/bad.json"), "boom".to_string()); - - assert_eq!(output.summary.tests_total, 0); - assert_eq!(output.summary.tests_failed, 0); - assert_eq!(output.fatal_file_errors, 1); - } - - #[test] - fn malformed_fixture_shape_becomes_file_error() { - let nonce = SystemTime::now() - .duration_since(UNIX_EPOCH) - .unwrap() - .as_nanos(); - let file = std::env::temp_dir().join(format!("arc_evm_specs_tests_bad_shape_{nonce}.json")); - std::fs::write( - &file, - serde_json::json!({ - "my_test": { - "env": {}, - "post": {} - } - }) - .to_string(), - ) - .unwrap(); - - let completed = AtomicUsize::new(0); - let output = process_fixture_file(&file, None, false, false, &completed, 1); - - assert_eq!(output.fatal_file_errors, 1); - assert_eq!(output.summary.files_processed, 1); - assert_eq!(output.summary.tests_total, 0); - assert_eq!(output.results.len(), 1); - assert!(output.results[0] - .error - .contains("unsupported or malformed statetest fixture shape")); - - std::fs::remove_file(file).unwrap(); - } - - #[test] - fn looks_like_statetest_fixture_checks_required_keys() { - let good = serde_json::json!({ - "my_test": { - "env": {}, - "post": {}, - "pre": {} - } - }); - let bad = serde_json::json!({ - "config": { "name": "not-a-fixture" } - }); - assert!(looks_like_statetest_fixture(&good)); - assert!(!looks_like_statetest_fixture(&bad)); - } - - #[test] - fn find_json_files_recurses_and_filters_extensions() { - let nonce = SystemTime::now() - .duration_since(UNIX_EPOCH) - .unwrap() - .as_nanos(); - let root = std::env::temp_dir().join(format!("arc_evm_specs_tests_find_json_{nonce}")); - let nested = root.join("nested"); - std::fs::create_dir_all(&nested).unwrap(); - - let a = root.join("a.json"); - let b = nested.join("b.json"); - let c = nested.join("c.txt"); - std::fs::write(&a, "{}").unwrap(); - std::fs::write(&b, "{}").unwrap(); - std::fs::write(&c, "ignore").unwrap(); - - let mut files = find_json_files(&root).unwrap(); - files.sort(); - - assert_eq!(files, vec![a, b]); - std::fs::remove_dir_all(&root).unwrap(); - } - - #[test] - fn run_returns_no_json_files_for_empty_directory() { - let nonce = SystemTime::now() - .duration_since(UNIX_EPOCH) - .unwrap() - .as_nanos(); - let root = std::env::temp_dir().join(format!("arc_evm_specs_tests_empty_{nonce}")); - std::fs::create_dir_all(&root).unwrap(); - - let err = - run(root.clone(), None, false, false, false).expect_err("empty directory should fail"); - assert!(matches!( - err, - EvmSpecsTestError::NoJsonFiles { path } if path == root.display().to_string() - )); - - std::fs::remove_dir_all(root).unwrap(); - } -} diff --git a/crates/evm/Cargo.toml b/crates/evm/Cargo.toml deleted file mode 100644 index 9b1844d..0000000 --- a/crates/evm/Cargo.toml +++ /dev/null @@ -1,63 +0,0 @@ -[package] -name = "arc-evm" -version.workspace = true -edition.workspace = true -readme.workspace = true -license.workspace = true -exclude.workspace = true -rust-version.workspace = true -publish.workspace = true -repository.workspace = true - -[features] -arbitrary = [ - "alloy-consensus/arbitrary", - "alloy-eips/arbitrary", - "alloy-primitives/arbitrary", - "revm-primitives/arbitrary", - "revm/arbitrary", - "reth-ethereum/arbitrary", -] -integration = ["arc-precompiles/integration"] - -[dependencies] -# alloy -alloy-consensus = { workspace = true, features = ["serde"] } -alloy-eips.workspace = true -alloy-evm.workspace = true -alloy-primitives.workspace = true -alloy-rpc-types-engine.workspace = true -alloy-sol-types.workspace = true - -# arc -arc-execution-config.workspace = true -arc-precompiles.workspace = true - -# reth -reth-chainspec.workspace = true -reth-ethereum = { workspace = true, features = ["node-api", "provider", "evm"] } -reth-ethereum-primitives.workspace = true -reth-evm.workspace = true -reth-node-api.workspace = true -reth-primitives-traits.workspace = true - -# revm -revm.workspace = true -revm-context-interface.workspace = true -revm-interpreter.workspace = true -revm-primitives.workspace = true - -# misc -thiserror.workspace = true -tracing.workspace = true - -[dev-dependencies] -alloy-genesis.workspace = true -alloy-rpc-types-trace.workspace = true -arc-execution-config = { workspace = true, features = ["test-utils"] } -arc-precompiles.workspace = true -revm-inspectors.workspace = true -rstest.workspace = true - -[lints] -workspace = true diff --git a/crates/evm/src/assembler.rs b/crates/evm/src/assembler.rs deleted file mode 100644 index 702f2ac..0000000 --- a/crates/evm/src/assembler.rs +++ /dev/null @@ -1,121 +0,0 @@ -// Copyright 2025 Circle Internet Group, Inc. All rights reserved. -// -// SPDX-License-Identifier: Apache-2.0 -// -// Licensed under the Apache License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. -// You may obtain a copy of the License at -// -// http://www.apache.org/licenses/LICENSE-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// See the License for the specific language governing permissions and -// limitations under the License. - -extern crate alloc; - -use alloc::sync::Arc; -use alloy_consensus::Block; -use alloy_evm::{block::BlockExecutorFactory, eth::EthBlockExecutionCtx}; -use arc_execution_config::{chainspec::ArcChainSpec, gas_fee::encode_base_fee_to_bytes}; -use reth_chainspec::{EthChainSpec, EthereumHardforks}; -use reth_ethereum::evm::EthBlockAssembler; -use reth_ethereum_primitives::{Receipt, TransactionSigned}; -use reth_evm::execute::{BlockAssembler, BlockAssemblerInput, BlockExecutionError}; -use revm::context::Block as RevmBlockContext; -use revm_primitives::B256; - -use arc_precompiles::system_accounting::{ - compute_gas_values_storage_slot, unpack_gas_values_from_storage, SYSTEM_ACCOUNTING_ADDRESS, -}; - -#[derive(Debug, Clone)] -pub struct ArcBlockAssembler { - chain_spec: Arc, -} - -impl ArcBlockAssembler { - pub fn new(chain_spec: Arc) -> Self { - Self { chain_spec } - } -} - -impl BlockAssembler for ArcBlockAssembler -where - F: for<'a> BlockExecutorFactory< - ExecutionCtx<'a> = EthBlockExecutionCtx<'a>, - Transaction = TransactionSigned, - Receipt = Receipt, - >, - ChainSpec: EthChainSpec + EthereumHardforks, -{ - type Block = Block; - - fn assemble_block( - &self, - mut input: BlockAssemblerInput<'_, '_, F, ::Header>, - ) -> Result { - let assembler = EthBlockAssembler::new(self.chain_spec.clone()); - - // Loading the gas values from the system accounting contract and insert to the extra data. - let block_number_result = - input - .evm_env - .block_env() - .number() - .try_into() - .inspect_err(|err| { - tracing::warn!("Failed to convert block number to u64: {}", err); - }); - - if let Ok(block_number) = block_number_result { - let slot = compute_gas_values_storage_slot(block_number); - - // If the state changed, read the new value from bundle_state. - let mut value = - if let Some(account) = input.bundle_state.account(&SYSTEM_ACCOUNTING_ADDRESS) { - account.storage_slot(slot.into()) - } else { - None - }; - - // Read from state provider if the state is not changed. - if value.is_none() { - value = input - .state_provider - .storage(SYSTEM_ACCOUNTING_ADDRESS, slot) - .unwrap_or(None) - } - - if let Some(value) = value { - let gas_values = unpack_gas_values_from_storage(B256::from(value)); - if gas_values.nextBaseFee != 0 { - input.execution_ctx.extra_data = - encode_base_fee_to_bytes(gas_values.nextBaseFee); - } - } else { - tracing::warn!("Gas value not found for block number: {}", block_number); - } - } - - assembler.assemble_block(input) - } -} - -#[cfg(test)] -mod tests { - use super::*; - use alloc::sync::Arc; - use arc_execution_config::chainspec::LOCAL_DEV; - - #[test] - fn block_assembler_creation() { - let chain_spec = LOCAL_DEV.clone(); - let assembler = ArcBlockAssembler::new(chain_spec.clone()); - - // Verify the inner assembler's chain_spec points to the same - assert!(Arc::ptr_eq(&assembler.chain_spec, &chain_spec)); - } -} diff --git a/crates/evm/src/evm.rs b/crates/evm/src/evm.rs deleted file mode 100644 index ce4324b..0000000 --- a/crates/evm/src/evm.rs +++ /dev/null @@ -1,8797 +0,0 @@ -// Copyright 2025 Circle Internet Group, Inc. All rights reserved. -// -// SPDX-License-Identifier: Apache-2.0 -// -// Licensed under the Apache License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. -// You may obtain a copy of the License at -// -// http://www.apache.org/licenses/LICENSE-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// See the License for the specific language governing permissions and -// limitations under the License. - -extern crate alloc; - -use crate::assembler::ArcBlockAssembler; -use crate::executor::ArcBlockExecutor; -use crate::frame_result::{create_frame_result, create_oog_frame_result, BeforeFrameInitResult}; -use crate::log::{create_eip7708_transfer_log, create_native_transfer_log}; -use alloc::sync::Arc; -use alloy_evm::eth::EthEvmContext; -use alloy_evm::{ - block::{BlockExecutorFactory, BlockExecutorFor}, - eth::EthBlockExecutionCtx, - precompiles::PrecompilesMap, - Evm as AlloyEvmTrait, EvmFactory, -}; -use alloy_rpc_types_engine::ExecutionData; -use arc_execution_config::hardforks::{ArcHardfork, ArcHardforkFlags}; -use arc_execution_config::native_coin_control::{ - compute_is_blocklisted_storage_slot, is_blocklisted_status, -}; -use arc_precompiles::helpers::{ - ERR_BLOCKED_ADDRESS, ERR_SELFDESTRUCTED_BALANCE_INCREASED, ERR_ZERO_ADDRESS, - PRECOMPILE_SLOAD_GAS_COST, -}; -use arc_precompiles::NATIVE_COIN_CONTROL_ADDRESS; -use core::fmt::Debug; -use reth_ethereum::evm::revm::context::block::BlockEnv; -use reth_ethereum::evm::revm::primitives::U256; -use reth_ethereum::{ - evm::{ - primitives::{Database, EvmEnv, InspectorFor, NextBlockEnvAttributes}, - revm::{ - context::{Context, ContextTr, JournalTr, TxEnv}, - context_interface::result::{EVMError, HaltReason}, - db::State, - inspector::{Inspector, NoOpInspector}, - interpreter::interpreter::EthInterpreter, - primitives::hardfork::SpecId, - }, - EthEvmConfig, - }, - node::api::ConfigureEvm, - primitives::{Header, SealedBlock, SealedHeader}, - Receipt, TransactionSigned, -}; -use reth_evm::execute::BlockBuilder; -use reth_evm::{ConfigureEngineEvm, EvmEnvFor, ExecutableTxIterator, ExecutionCtxFor}; -use reth_primitives_traits::NodePrimitives; -use revm::bytecode::opcode::SELFDESTRUCT; -use revm::bytecode::Bytecode; -use revm::context_interface::result::ResultAndState; -use revm::handler::evm::{ContextDbError, FrameInitResult}; -use revm::handler::instructions::InstructionProvider; -use revm::handler::{EvmTr, FrameInitOrResult, FrameResult, FrameTr, Handler, ItemOrResult}; -use revm::inspector::{InspectorEvmTr, InspectorHandler, JournalExt}; -use revm::state::AccountInfo; -use revm::Database as RevmDatabase; -use revm::ExecuteEvm; -use revm::{ - context::{ - result::{ExecResultAndState, ExecutionResult, InvalidTransaction}, - ContextSetters, Evm as RevmEvm, - }, - handler::{instructions::EthInstructions, EthFrame, PrecompileProvider, SystemCallTx}, - interpreter::{CallOutcome, Gas, InstructionResult, InterpreterResult}, - state::EvmState, - InspectEvm, SystemCallEvm, -}; -use revm_context_interface::journaled_state::{JournalCheckpoint, JournalLoadError}; -use revm_context_interface::{FrameStack, Transaction}; -use revm_interpreter::interpreter_action::FrameInit; -use revm_interpreter::{CallScheme, CreateScheme, FrameInput, Instruction}; -use revm_primitives::{Address, Bytes}; -use std::collections::{HashMap, HashSet}; - -use crate::handler::ArcEvmHandler; -use crate::opcode::{ - arc_network_selfdestruct_zero4, arc_network_selfdestruct_zero5, arc_network_selfdestruct_zero7, -}; -use crate::subcall::{SubcallContinuation, SubcallRegistry}; -use arc_execution_config::chainspec::{ArcChainSpec, BlockGasLimitProvider}; -use arc_execution_config::protocol_config::{expected_gas_limit, retrieve_fee_params}; -use arc_precompiles::call_from::{CallFromPrecompile, CALL_FROM_ADDRESS}; -use arc_precompiles::precompile_provider::ArcPrecompileProvider; -use arc_precompiles::subcall::SubcallPrecompile; -use revm::interpreter::interpreter_action::CallInputs; - -/// Flat gas cost charged for rejected subcall dispatches (unauthorized caller, wrong scheme, -/// value attached, sender spoofing, init_subcall errors). Charged by `init_subcall_revert` -/// calls. Prevents zero-cost probing of subcall precompile addresses. -const SUBCALL_DISPATCH_COST: u64 = 100; - -/// Construct a revert `FrameResult` for a subcall precompile rejection. -fn init_subcall_revert(message: &str, call_inputs: &CallInputs) -> FrameResult { - let revert_bytes = arc_precompiles::helpers::revert_message_to_bytes(message); - let mut gas = Gas::new(call_inputs.gas_limit); - // Charge a flat dispatch cost. If the caller doesn't have enough gas, consume all of it. - if !gas.record_cost(SUBCALL_DISPATCH_COST) { - gas.spend_all(); - } - let result = InterpreterResult::new(InstructionResult::Revert, revert_bytes, gas); - FrameResult::Call(CallOutcome { - result, - memory_offset: call_inputs.return_memory_offset.clone(), - was_precompile_called: true, - precompile_call_logs: Default::default(), - }) -} - -/// Construct an OOG `FrameResult` for a subcall that exceeded its gas budget. -fn subcall_oog(gas: Gas, return_memory_offset: std::ops::Range) -> FrameResult { - FrameResult::Call(CallOutcome { - result: InterpreterResult::new(InstructionResult::OutOfGas, Bytes::new(), gas), - memory_offset: return_memory_offset, - was_precompile_called: true, - precompile_call_logs: Default::default(), - }) -} - -/// Reject a subcall in static context. Mirrors `InstructionResult::CallNotAllowedInsideStatic` -/// halt semantics and the precompile-helper `check_staticcall` path: consume all caller gas. -fn init_subcall_static_revert(call_inputs: &CallInputs) -> FrameResult { - let revert_bytes = arc_precompiles::helpers::revert_message_to_bytes( - "subcall precompiles cannot be invoked in static context", - ); - let mut gas = Gas::new(call_inputs.gas_limit); - gas.spend_all(); - let result = InterpreterResult::new(InstructionResult::Revert, revert_bytes, gas); - FrameResult::Call(CallOutcome { - result, - memory_offset: call_inputs.return_memory_offset.clone(), - was_precompile_called: true, - precompile_call_logs: Default::default(), - }) -} - -/// Loads an account with code, recording its EIP-2929 access cost on `gas`. -/// Skips the DB load when the account is cold and `gas` can't cover it, -/// preventing warming as a side-effect of an intentional OOG. -/// Returns `None` on OOG (caller should `spend_all` and return). -fn load_account_with_code_metered( - journal: &mut J, - address: Address, - gas: &mut Gas, -) -> Result, ::Error> { - let skip_cold_load = gas.remaining() < revm_interpreter::gas::COLD_ACCOUNT_ACCESS_COST; - - match journal.load_account_info_skip_cold_load(address, true, skip_cold_load) { - Ok(info) => { - let cost = if info.is_cold { - revm_interpreter::gas::COLD_ACCOUNT_ACCESS_COST - } else { - revm_interpreter::gas::WARM_STORAGE_READ_COST - }; - if !gas.record_cost(cost) { - return Ok(None); - } - Ok(Some(info.account.into_owned())) - } - Err(JournalLoadError::ColdLoadSkipped) => Ok(None), - Err(JournalLoadError::DBError(err)) => Err(err), - } -} - -#[derive(Debug)] -pub struct ArcEvm> { - /// Inner EVM type. - pub inner: RevmEvm, - pub inspect: bool, - /// Feature flags for Arc hardforks active at the current block. - pub hardfork_flags: ArcHardforkFlags, - /// Registry of subcall-capable precompiles. - subcall_registry: Arc, - /// Active subcall continuations, keyed by the precompile call's depth. - subcall_continuations: HashMap, -} - -/// ArcEvm implementation, wrapping an inner revm EVM instance to apply handler -/// 1. Hook frame_init to add the NativeCoinTransferred event log. -/// 2. Add the blocklist check for each frame. -/// 3. Check static context to the precompiles. -impl ArcEvm, P> { - /// Create a new Arc EVM. - #[allow(clippy::too_many_arguments)] - pub fn new( - ctx: CTX, - inspector: INSP, - precompiles: P, - instruction: EthInstructions, - inspect: bool, - hardfork_flags: ArcHardforkFlags, - subcall_registry: Arc, - ) -> Self { - Self { - inner: RevmEvm { - ctx, - inspector, - instruction, - precompiles, - frame_stack: FrameStack::new(), - }, - inspect, - hardfork_flags, - subcall_registry, - subcall_continuations: HashMap::new(), - } - } -} - -/// Extracts transfer parameters (from, to, amount) from a Call frame input. -/// Returns None if the call scheme doesn't involve a value transfer. -fn extract_call_transfer_params( - inputs: &revm_interpreter::CallInputs, -) -> Option<(Address, Address, U256)> { - match inputs.scheme { - CallScheme::DelegateCall | CallScheme::StaticCall | CallScheme::CallCode => None, - CallScheme::Call => Some(( - inputs.transfer_from(), - inputs.transfer_to(), - inputs.transfer_value().unwrap_or(U256::ZERO), - )), - } -} - -fn frame_gas_limit(frame_input: &FrameInit) -> u64 { - match &frame_input.frame_input { - FrameInput::Call(inputs) => inputs.gas_limit, - FrameInput::Create(inputs) => inputs.gas_limit(), - FrameInput::Empty => 0, - } -} - -/// Creates a revert that charges the actual SLOAD gas cost when metered, or OOGs if the -/// frame's gas budget is insufficient. -fn metered_revert( - frame_input: &FrameInit, - meter_sloads: bool, - gas_cost: u64, - reason: &str, -) -> BeforeFrameInitResult { - let gas_spent = if meter_sloads { gas_cost } else { 0 }; - if gas_spent > 0 && frame_gas_limit(frame_input) < gas_spent { - BeforeFrameInitResult::Reverted(create_oog_frame_result(frame_input)) - } else { - BeforeFrameInitResult::Reverted(create_frame_result(frame_input, reason, gas_spent)) - } -} - -/// Defensive revert for when a subcall interception fires on a non-Call frame. -/// This should never happen (the caller in `frame_init` checks for Call), but -/// avoids panicking in production. -fn non_call_frame_revert(frame_input: &FrameInit) -> Result, E> { - Ok(ItemOrResult::Result(create_frame_result( - frame_input, - "internal error: subcall interception on non-Call frame", - 0, - ))) -} - -/// Resolves any `SharedBuffer` call input to concrete `Bytes`. -/// -/// # Warning -/// -/// Resolve the buffer before calling any child frames that could overwrite it. -/// -/// Precompiles receive `CallInputs` but cannot dereference `SharedBuffer` references -/// because they lack access to the EVM context. This must be called before dispatching -/// to a subcall precompile's `init_subcall` method. -fn resolve_shared_buffer(ctx: &impl ContextTr, frame_input: &mut FrameInit) { - if let FrameInput::Call(ref mut inputs) = frame_input.frame_input { - if let revm::interpreter::interpreter_action::CallInput::SharedBuffer(_) = &inputs.input { - let resolved = inputs.input.bytes(ctx); - inputs.input = revm::interpreter::interpreter_action::CallInput::Bytes(resolved); - } - } -} - -/// Returns whether a transfer log should be emitted for the given frame init result. -/// -/// A log is kept when the call/create completed successfully. -/// CREATE results that succeed but have no address (nonce overflow) are excluded because -/// value was not actually transferred. -fn should_emit_transfer_log_for_result(result: &FrameResult) -> bool { - match result { - FrameResult::Create(create_outcome) => { - create_outcome.instruction_result().is_ok() && create_outcome.address.is_some() - } - FrameResult::Call(call_outcome) => call_outcome.instruction_result().is_ok(), - } -} - -/// Returns whether a transfer log should be emitted for the given frame init outcome. -/// -/// For `Item` (pending execution), always emit — REVM's internal frame checkpoint will -/// revert it if the frame later fails. For `Result` (synchronous completion), delegate -/// to [`should_emit_transfer_log_for_result`]. -fn should_emit_transfer_log(frame_res: &ItemOrResult) -> bool { - match frame_res { - ItemOrResult::Item(_) => true, - ItemOrResult::Result(result) => should_emit_transfer_log_for_result(result), - } -} - -/// Returns `true` if `frame_input` targets a precompile address. -/// -/// Only CALL frames can target precompiles; CREATE never does. -fn is_precompile_call( - frame_input: &FrameInit, - precompiles: &impl PrecompileProvider, -) -> bool { - match &frame_input.frame_input { - FrameInput::Call(inputs) => precompiles.contains(&inputs.bytecode_address), - _ => false, - } -} - -/// Outcome of [`ArcEvm::checked_frame_init`]. -/// -/// Returning owned data (rather than a `FrameInitResult` reference) resolves borrow-checker -/// conflicts: the caller can reborrow individual fields of `self` after the method returns. -enum FrameInitOutcome { - /// Frame was pushed onto the stack; the execution loop will drive it. - Pushed, - /// Frame completed immediately (e.g. empty bytecode, allowlist rejection). - Immediate(FrameResult), -} - -/// Mirrors `Evm::frame_init` from revm-handler, but takes individual field references instead -/// of `&mut self`. This enables borrow splitting at the call site: the returned reference borrows -/// only `frame_stack`, leaving `ctx` and other fields accessible. -/// -/// See [`revm::handler::EvmTr::frame_init`] for the original. -fn init_frame<'fs, CTX, P>( - frame_stack: &'fs mut FrameStack>, - ctx: &mut CTX, - precompiles: &mut P, - frame_input: FrameInit, -) -> Result>, ContextDbError> -where - CTX: ContextTr, - P: PrecompileProvider, -{ - let is_first_init = frame_stack.index().is_none(); - let new_frame = if is_first_init { - frame_stack.start_init() - } else { - frame_stack.get_next() - }; - - let res = EthFrame::init_with_context(new_frame, ctx, precompiles, frame_input)?; - - Ok(res.map_frame(|token| { - if is_first_init { - unsafe { frame_stack.end_init(token) }; - } else { - unsafe { frame_stack.push(token) }; - } - frame_stack.get() - })) -} - -/// ArcEvm implementation for customized operations. -impl ArcEvm { - /// Checks if an address is blocklisted by reading from the native coin control precompile storage. - /// - /// Returns `(is_blocklisted, is_cold)`. The `is_cold` flag, together with the `sload_cost` - /// and `metered_revert` plumbing it feeds, is retained as dormant infrastructure: blocklist - /// SLOADs are currently unmetered (callers pass `meter_sloads = false` and the cost is - /// discarded), but the wiring is kept so re-enabling metering is a single-flag flip rather - /// than a re-architecture. - /// - /// Note: This calls `journal.sload()` directly, bypassing the interpreter, so revm does not - /// automatically meter gas for these reads. - fn is_address_blocklisted( - &mut self, - address: Address, - ) -> Result<(bool, bool), ContextDbError> { - let storage_slot = compute_is_blocklisted_storage_slot(address).into(); - - // Read blocklist status: non-zero value means blocklisted - let state_load = self - .inner - .ctx - .journal_mut() - .sload(NATIVE_COIN_CONTROL_ADDRESS, storage_slot)?; - - Ok((is_blocklisted_status(state_load.data), state_load.is_cold)) - } - - fn sload_cost(&self, is_cold: bool) -> u64 { - if self.hardfork_flags.is_active(ArcHardfork::Zero6) { - if is_cold { - revm_interpreter::gas::COLD_SLOAD_COST - } else { - revm_interpreter::gas::WARM_STORAGE_READ_COST - } - } else { - PRECOMPILE_SLOAD_GAS_COST - } - } - - /// Extracts transfer parameters (from, to, amount) from a Create frame input. - /// - /// For value-carrying CREATE2 frames, this also normalizes the frame scheme to - /// `CreateScheme::Custom { address }` after deriving the address, so revm reuses - /// the precomputed address during frame initialization instead of hashing initcode again. - /// - /// Returns None if value is zero or scheme is already Custom. - fn extract_and_normalize_create_transfer_params( - &mut self, - inputs: &mut revm_interpreter::CreateInputs, - depth: usize, - ) -> Result, ContextDbError> { - if inputs.value().is_zero() { - return Ok(None); - } - - match inputs.scheme() { - CreateScheme::Create => { - let nonce = if depth == 0 { - // First frame: use transaction nonce directly - self.inner.ctx.tx().nonce() - } else { - // Nested frame: look up nonce in journal - self.inner - .ctx - .journal_mut() - .load_account(inputs.caller())? - .info - .nonce - }; - Ok(Some(( - inputs.caller(), - inputs.created_address(nonce), - inputs.value(), - ))) - } - CreateScheme::Create2 { salt: _ } => { - // Nonce doesn't matter for CREATE2 - let created_address = inputs.created_address(0); - // Arc needs the created address before revm initializes the create frame - // for blocklist checks and transfer logs. Normalize the frame to a custom - // create so revm reuses the precomputed address instead of hashing the - // initcode a second time. This can be removed after upgrading to a - // revm release containing bluealloy/revm#3664. - inputs.set_scheme(CreateScheme::Custom { - address: created_address, - }); - Ok(Some((inputs.caller(), created_address, inputs.value()))) - } - CreateScheme::Custom { address: _ } => Ok(None), - } - } - - /// Checks blocklist status for transfer participants and returns appropriate result. - fn check_blocklist_and_create_log( - &mut self, - from: Address, - to: Address, - amount: U256, - frame_input: &FrameInit, - ) -> Result> { - // Meter SLOAD gas on revert for nested frames (depth > 0). - // Currently SLOAD gas metering is disabled — blocklist checks are unmetered. - let meter_sloads = false; - - // Zero5: reject CALL/CREATE value transfers involving the zero address. - // This prevents accidental burn/mint semantics at the EVM execution layer. - // - // Note: this check only applies to CALL/CREATE frame value transfers — it does NOT - // affect the NativeCoinAuthority precompile, which legitimately uses Address::ZERO in - // ERC-20 Transfer events for mint (from=0x0) and burn (to=0x0). The precompile operates - // via direct journal balance mutations within its own frame, never triggering frame_init. - if self.hardfork_flags.is_active(ArcHardfork::Zero5) - && (from == Address::ZERO || to == Address::ZERO) - { - return Ok(BeforeFrameInitResult::Reverted(create_frame_result( - frame_input, - ERR_ZERO_ADDRESS, - 0, - ))); - } - - let (from_blocklisted, from_is_cold) = self.is_address_blocklisted(from)?; - let from_sload_cost = self.sload_cost(from_is_cold); - - if from_blocklisted { - return Ok(metered_revert( - frame_input, - meter_sloads, - from_sload_cost, - ERR_BLOCKED_ADDRESS, - )); - } - - let (to_blocklisted, to_is_cold) = self.is_address_blocklisted(to)?; - let to_sload_cost = self.sload_cost(to_is_cold); - - // Both are PRECOMPILE_SLOAD_GAS_COST (2,100); sum fits in u64 - #[allow(clippy::arithmetic_side_effects)] - let total_sload_cost = from_sload_cost + to_sload_cost; - - if to_blocklisted { - return Ok(metered_revert( - frame_input, - meter_sloads, - total_sload_cost, - ERR_BLOCKED_ADDRESS, - )); - } - if self.hardfork_flags.is_active(ArcHardfork::Zero5) { - // Probe inside a checkpoint so this unmetered selfdestructed-target check does not - // leave a previously-cold account warm for the later path. - let target_is_selfdestructed = { - let journal = self.inner.journal_mut(); - if self.hardfork_flags.is_active(ArcHardfork::Zero7) { - let checkpoint = journal.checkpoint(); - let result = journal - .load_account(to) - .map(|target_account| target_account.is_selfdestructed()); - journal.checkpoint_revert(checkpoint); - result? - } else { - journal - .load_account(to) - .map(|target_account| target_account.is_selfdestructed())? - } - }; - - if target_is_selfdestructed { - return Ok(metered_revert( - frame_input, - meter_sloads, - total_sload_cost, - ERR_SELFDESTRUCTED_BALANCE_INCREASED, - )); - } - } - - if self.hardfork_flags.is_active(ArcHardfork::Zero5) { - if from == to { - // EIP-7708: self-transfers do not emit a log - Ok(BeforeFrameInitResult::Checked(total_sload_cost)) - } else { - Ok(BeforeFrameInitResult::Log( - create_eip7708_transfer_log(from, to, amount), - total_sload_cost, - )) - } - } else { - Ok(BeforeFrameInitResult::Log( - create_native_transfer_log(from, to, amount), - total_sload_cost, - )) - } - } - - pub(crate) fn before_frame_init( - &mut self, - frame_input: &mut FrameInit, - ) -> Result> { - // Extract transfer parameters based on frame type - let transfer_params = match &mut frame_input.frame_input { - FrameInput::Empty => None, - FrameInput::Create(inputs) => { - self.extract_and_normalize_create_transfer_params(inputs, frame_input.depth)? - } - FrameInput::Call(inputs) => extract_call_transfer_params(inputs), - }; - - // Process transfer if present and non-zero - match transfer_params { - Some((from, to, amount)) if !amount.is_zero() => { - self.check_blocklist_and_create_log(from, to, amount, frame_input) - } - _ => Ok(BeforeFrameInitResult::None), - } - } -} - -// Implement EvmTr for ArcEvm -// ref: op-revm v12.0.1 implementation https://github.com/bluealloy/revm/blob/v97/crates/op-revm/src/evm.rs#L95 -impl EvmTr for ArcEvm -where - CTX: ContextTr, - I: InstructionProvider, - P: PrecompileProvider, -{ - type Context = CTX; - type Instructions = I; - type Precompiles = P; - type Frame = EthFrame; - - #[inline] - fn all( - &self, - ) -> ( - &Self::Context, - &Self::Instructions, - &Self::Precompiles, - &FrameStack, - ) { - self.inner.all() - } - - #[inline] - fn all_mut( - &mut self, - ) -> ( - &mut Self::Context, - &mut Self::Instructions, - &mut Self::Precompiles, - &mut FrameStack, - ) { - self.inner.all_mut() - } - - /// Initializes the frame for the given frame input. Frame is pushed to the frame stack. - #[inline] - fn frame_init( - &mut self, - mut frame_input: ::FrameInit, - ) -> Result, ContextDbError> { - // Subcall precompiles must be checked first: they reject value transfers, so - // the parent's `before_frame_init` is a no-op (no transfer to check). Running - // `checked_frame_init` for the parent would incorrectly push a frame for the - // precompile address onto the stack. - if let FrameInput::Call(ref call_inputs) = frame_input.frame_input { - if let Some((precompile, allowed_callers)) = - self.subcall_registry.get(&call_inputs.target_address) - { - if !allowed_callers.is_allowed(&call_inputs.caller) { - return Ok(ItemOrResult::Result(init_subcall_revert( - "unauthorized caller", - call_inputs, - ))); - } - - // Clone precompile to release the immutable borrow on self.subcall_registry - // before taking &mut self for before_frame_init. - let precompile = precompile.clone(); - - // Defense-in-depth: run before_frame_init on the parent frame even though - // subcall precompiles currently reject value transfers (making this a no-op). - // If a future subcall precompile allows value, the log needs to be handled here. - match self.before_frame_init(&mut frame_input)? { - BeforeFrameInitResult::Reverted(res) => { - return Ok(ItemOrResult::Result(res)); - } - BeforeFrameInitResult::Log(..) - | BeforeFrameInitResult::Checked(_) - | BeforeFrameInitResult::None => {} - } - - return self.init_subcall(frame_input, precompile); - } - } - - // Standard path: blocklist checks, revm frame init, transfer log. - match self.checked_frame_init(frame_input)? { - FrameInitOutcome::Pushed => Ok(ItemOrResult::Item(self.inner.frame_stack.get())), - FrameInitOutcome::Immediate(result) => Ok(ItemOrResult::Result(result)), - } - } - - /// Run the frame from the top of the stack. Returns the frame init or result. - #[inline] - fn frame_run(&mut self) -> Result, ContextDbError> { - self.inner.frame_run() - } - - /// Returns the result of the frame to the caller. Frame is popped from the frame stack. - /// - /// Overrides the default revm behavior to intercept subcall continuations: - /// when a child frame completes and a continuation exists at `depth - 1`, - /// we finalize the subcall via `complete_subcall` instead of returning the raw child result. - #[inline] - fn frame_return_result( - &mut self, - result: ::FrameResult, - ) -> Result::FrameResult>, ContextDbError> { - let frame_was_finished = self.inner.frame_stack.get().is_finished(); - - // Capture the finished frame's depth before popping — needed for subcall continuation - // lookup when the pop leaves the stack empty (direct EOA -> precompile calls). - let finished_depth = self.inner.frame_stack.get().depth; - - // Pop the finished frame (revm default behavior) - if frame_was_finished { - self.inner.frame_stack.pop(); - } - - let stack_empty = self.inner.frame_stack.index().is_none(); - - // Check for a subcall continuation, but ONLY when a frame actually finished execution. - // - // When `frame_was_finished` is true, this result came from a child frame that ran to - // completion. The continuation was stored at the precompile's depth (finished_depth - 1). - // - // When `frame_was_finished` is false, this is an immediate result from `frame_init` - // (e.g., init_subcall already ran complete_subcall for a CallTooDeep). - // The stack top is the still-running parent frame, and `finished_depth` is the parent's - // depth — not the child's. Looking up `finished_depth - 1` would match the grandparent's - // continuation, corrupting state. - if frame_was_finished { - let continuation_key = finished_depth.checked_sub(1); - if let Some(key) = continuation_key { - if let Some(continuation) = self.subcall_continuations.remove(&key) { - let final_result = self.complete_subcall(result, continuation)?; - if stack_empty { - // Direct EOA -> precompile: no parent frame to propagate to. - return Ok(Some(final_result)); - } - // Propagate to the parent frame - self.inner - .frame_stack - .get() - .return_result::<_, ContextDbError>( - &mut self.inner.ctx, - final_result, - )?; - return Ok(None); - } - } - } - - // If stack is empty, this is the top-level result — return it - if stack_empty { - return Ok(Some(result)); - } - - // Propagate to the parent frame. This covers: - // - Normal (non-subcall) frame returns - // - Immediate results from frame_init (complete_subcall already ran) - self.inner - .frame_stack - .get() - .return_result::<_, ContextDbError>(&mut self.inner.ctx, result)?; - Ok(None) - } -} - -/// Subcall interception methods and shared frame-stack helpers. -/// -/// These are separated from the `EvmTr` impl because they are private helper methods, -/// not trait methods. They share the same trait bounds as the `EvmTr` impl. -impl ArcEvm -where - CTX: ContextTr, - I: InstructionProvider, - P: PrecompileProvider, -{ - /// Runs `before_frame_init` (blocklist checks, transfer log), then initializes the - /// frame via revm's standard machinery and emits the transfer log on success. - /// - /// For Zero5+ precompile CALLs, the EIP-7708 Transfer log is pushed before frame init - /// (wrapped in a journal checkpoint) so it precedes precompile-emitted logs. For all - /// other cases, the log is pushed after frame init only on success. - /// - /// Returns an owned [`FrameInitOutcome`] so the caller can reborrow `self` fields - /// after the call without borrow-checker conflicts. - fn checked_frame_init( - &mut self, - mut frame_input: FrameInit, - ) -> Result> { - let (maybe_log, _sload_gas) = match self.before_frame_init(&mut frame_input)? { - BeforeFrameInitResult::Reverted(res) => { - return Ok(FrameInitOutcome::Immediate(res)); - } - BeforeFrameInitResult::Log(log, gas) => (Some(log), gas), - BeforeFrameInitResult::Checked(gas) => (None, gas), - BeforeFrameInitResult::None => (None, 0), - }; - - // Log emission strategy for the EIP-7708 Transfer log: - // - // **Zero5+ precompile CALL**: Push the Transfer log BEFORE init_frame so it - // precedes any logs the precompile emits (EIP-7708 requires the native Transfer - // log to appear before precompile-emitted logs). The precompile runs synchronously - // inside init_with_context and returns a Result, so a journal checkpoint can - // correctly commit/revert based on the outcome. - // - // **All other cases** (non-precompile CALLs/CREATEs at any hardfork, and pre-Zero5 - // precompile CALLs): Execute first, then push the log only if the result indicates - // success. For non-precompile frames that return Item (pending execution), REVM's - // internal frame checkpoint already covers this log — if the frame later reverts, - // logs.truncate(log_i) removes it automatically. - let is_precompile = is_precompile_call(&frame_input, &self.inner.precompiles); - - let frame_res = if is_precompile && self.hardfork_flags.is_active(ArcHardfork::Zero5) { - // Zero5+ precompile path: push the Transfer log BEFORE init_frame so it - // precedes any logs the precompile emits, wrapped in a journal checkpoint so - // the log is reverted if the precompile fails. - let log_checkpoint = if let Some(log) = maybe_log { - let cp = self.inner.ctx.journal_mut().checkpoint(); - self.inner.ctx.journal_mut().log(log); - Some(cp) - } else { - None - }; - - let res = init_frame( - &mut self.inner.frame_stack, - &mut self.inner.ctx, - &mut self.inner.precompiles, - frame_input, - )?; - - if let Some(cp) = log_checkpoint { - if should_emit_transfer_log(&res) { - self.inner.ctx.journal_mut().checkpoint_commit(); - } else { - self.inner.ctx.journal_mut().checkpoint_revert(cp); - } - } - - res - } else { - // Common path: execute first, then push the log only if successful. - let res = init_frame( - &mut self.inner.frame_stack, - &mut self.inner.ctx, - &mut self.inner.precompiles, - frame_input, - )?; - - if let Some(log) = maybe_log { - if should_emit_transfer_log(&res) { - self.inner.ctx.journal_mut().log(log); - } - } - - res - }; - - match frame_res { - ItemOrResult::Item(_) => Ok(FrameInitOutcome::Pushed), - ItemOrResult::Result(result) => Ok(FrameInitOutcome::Immediate(result)), - } - } - - /// Intercept a call to a subcall-capable precompile and initialize the child frame. - /// - /// Decodes the precompile input via [`SubcallPrecompile::init_subcall`], stores a - /// continuation, and initializes the child frame via [`checked_frame_init`] so that it - /// goes through `before_frame_init` hooks (blocklist checks, transfer log). - fn init_subcall( - &mut self, - mut frame_input: ::FrameInit, - precompile: Arc, - ) -> Result>, ContextDbError> { - let call_inputs = match &frame_input.frame_input { - FrameInput::Call(inputs) => inputs.as_ref(), - _ => return non_call_frame_revert(&frame_input), - }; - - // Reject non-CALL schemes (DELEGATECALL, STATICCALL, CALLCODE). - // Subcall precompiles only support the CALL scheme — other schemes have - // incompatible semantics (e.g. DELEGATECALL runs code in the caller's - // context, STATICCALL prohibits state changes). - if call_inputs.scheme != CallScheme::Call { - return Ok(ItemOrResult::Result(init_subcall_revert( - "subcall precompiles only support CALL scheme", - call_inputs, - ))); - } - - // Reject static context — even when the scheme is CALL, `is_static` can be true - // if this call is nested inside a STATICCALL frame higher in the call stack. - // Consume all gas to match upstream `CallNotAllowedInsideStatic` halt semantics - // and the standard precompile-helper `check_staticcall` path. - if call_inputs.is_static { - return Ok(ItemOrResult::Result(init_subcall_static_revert( - call_inputs, - ))); - } - - // Reject calls with value attached. The subcall framework intercepts frame_init, - // bypassing revm's init_with_context for the precompile call itself, so the value - // transfer from caller → precompile address is never executed. Forwarding value to - // the child would require explicit transfer logic that is not yet implemented. - if call_inputs.transfers_value() { - return Ok(ItemOrResult::Result(init_subcall_revert( - "subcall precompiles do not support value transfers", - call_inputs, - ))); - } - - // Resolve SharedBuffer → Bytes before handing inputs to the precompile, which - // doesn't have access to the EVM context needed to dereference shared memory. - resolve_shared_buffer(&self.inner.ctx, &mut frame_input); - - // Re-extract call_inputs after the mutable borrow in resolve_shared_buffer. - let call_inputs = match &frame_input.frame_input { - FrameInput::Call(inputs) => inputs.as_ref(), - _ => return non_call_frame_revert(&frame_input), - }; - - let init_result = match precompile.init_subcall(call_inputs) { - Ok(result) => result, - Err(err) => { - return Ok(ItemOrResult::Result(init_subcall_revert( - &err.to_string(), - call_inputs, - ))); - } - }; - - // Prevent sender spoofing by contracts: if the precompile changes the caller - // (e.g. callFrom), the new caller must be tx.origin (the signing EOA). - if init_result.child_inputs.caller != call_inputs.caller - && init_result.child_inputs.caller != self.inner.ctx.tx().caller() - { - return Ok(ItemOrResult::Result(init_subcall_revert( - "sender spoofing requires tx.origin as sender", - call_inputs, - ))); - } - - let return_memory_offset = call_inputs.return_memory_offset.clone(); - - // Pre-load the child's caller and target accounts into the journal. The normal EVM - // execution path has these already loaded (caller is the executing frame, target was - // loaded by the CALL opcode handler), but we're constructing a synthetic child frame - // with a potentially-unseen spoofed sender and arbitrary target. - // - // Both must be present in the journal state map before `make_call_frame` calls - // `transfer_loaded`, which panics on missing accounts. The target is loaded with - // code to support EIP-7702 delegation resolution below. - // - // These loads happen BEFORE the checkpoint so that warmups persist regardless of - // outcome (OOG, child revert, or parent rejection). This matches normal EVM CALL - // semantics where target warming is a side-effect of the CALL opcode, not the - // called code. - // - // Note: when caller==target, `load_account(caller)` warms the address first, so - // `target_load.is_cold` is false and only the warm cost (100) is charged. - let mut child_inputs = init_result.child_inputs; - self.inner - .ctx - .journal_mut() - .load_account(child_inputs.caller)?; - - // Track gas for the subcall overhead: ABI decode + target access + delegation. - // Access costs are charged explicitly because our pre-loads warm the accounts, - // so revm's internal CALL handler won't charge them. - let mut gas = Gas::new(call_inputs.gas_limit); - if !gas.record_cost(init_result.gas_overhead) { - gas.spend_all(); - return Ok(ItemOrResult::Result(subcall_oog( - gas, - call_inputs.return_memory_offset.clone(), - ))); - } - - let Some(target) = load_account_with_code_metered( - self.inner.ctx.journal_mut(), - child_inputs.target_address, - &mut gas, - )? - else { - gas.spend_all(); - return Ok(ItemOrResult::Result(subcall_oog( - gas, - call_inputs.return_memory_offset.clone(), - ))); - }; - - // Resolve EIP-7702 delegation: if the target has a delegation designator, - // load the delegate's code so the child frame executes correct bytecode. - if let Some(Bytecode::Eip7702(delegation)) = &target.code { - let delegate_address = delegation.address(); - - let Some(delegate) = load_account_with_code_metered( - self.inner.ctx.journal_mut(), - delegate_address, - &mut gas, - )? - else { - gas.spend_all(); - return Ok(ItemOrResult::Result(subcall_oog( - gas, - call_inputs.return_memory_offset.clone(), - ))); - }; - - if let Some(code) = delegate.code { - child_inputs.known_bytecode = Some((delegate.code_hash, code)); - } - } - - // Take a journal checkpoint AFTER account loading so warmups always persist, - // but BEFORE child dispatch so we can revert the child's committed state if - // `complete_subcall` rejects a successful child. - // - // `checkpoint()` increments journal depth, which would create a depth gap visible - // to tracing inspectors (causing `push_trace` to panic with "Disconnected trace"). - // We immediately call `checkpoint_commit()` to restore depth. The checkpoint value - // remains valid for a later revert — see `complete_subcall` for the revert protocol. - let checkpoint = self.inner.ctx.journal_mut().checkpoint(); - self.inner.ctx.journal_mut().checkpoint_commit(); - - let depth = frame_input.depth; - let gas_limit = call_inputs.gas_limit; - - // Recalculate child gas with EIP-150 (63/64ths) applied to the gas remaining - // after total overhead. This overwrites the gas_limit set by the trait's - // `init_subcall`, which only accounted for the ABI decode overhead. - // gas.remaining() / 64 <= gas.remaining(), so the subtraction cannot underflow. - #[allow(clippy::arithmetic_side_effects)] - let child_gas_limit = gas.remaining() - (gas.remaining() / 64); - child_inputs.gas_limit = child_gas_limit; - - let child_frame_input = FrameInit { - // Call depth is bounded by the EVM stack limit (1024) - #[allow(clippy::arithmetic_side_effects)] - depth: depth + 1, - memory: frame_input.memory, - frame_input: FrameInput::Call(child_inputs), - }; - - // Initialize the child frame through checked_frame_init so that - // before_frame_init hooks (blocklist, transfer log) run on the - // child. This returns owned FrameInitOutcome, releasing &mut self. - match self.checked_frame_init(child_frame_input)? { - // Child frame needs execution — store the continuation so - // frame_return_result can finalize it when the child completes. - FrameInitOutcome::Pushed => { - self.subcall_continuations.insert( - depth, - SubcallContinuation { - precompile, - gas_limit, - init_subcall_gas_overhead: gas.spent(), - return_memory_offset, - continuation_data: init_result.continuation_data, - checkpoint, - }, - ); - Ok(ItemOrResult::Item(self.inner.frame_stack.get())) - } - // Child completed immediately (e.g. rejected by allowlist, empty bytecode). - // complete_subcall must run now since frame_return_result won't see this child. - FrameInitOutcome::Immediate(child_result) => { - let continuation = SubcallContinuation { - precompile, - gas_limit, - init_subcall_gas_overhead: gas.spent(), - return_memory_offset, - continuation_data: init_result.continuation_data, - checkpoint, - }; - let final_result = self.complete_subcall(child_result, continuation)?; - Ok(ItemOrResult::Result(final_result)) - } - } - } - - /// The child frame has completed. Finalize the precompile result via - /// [`SubcallPrecompile::complete_subcall`]. - /// - /// Computes the final `FrameResult` from the child outcome and continuation state. - /// The caller is responsible for propagating or returning this result. - /// - /// If the child succeeded but `complete_subcall` signals failure, the child's committed - /// state is reverted using the checkpoint stored in the continuation. - fn complete_subcall( - &mut self, - child_result: FrameResult, - continuation: SubcallContinuation, - ) -> Result> { - let child_gas = child_result.gas(); - - // Classify the child frame's execution result. - // - // `child_succeeded`: the child returned normally (Stop, Return, SelfDestruct). - // Used to gate SSTORE refund forwarding — reverted children's refunds are invalid. - // - // `child_halted`: the child hit an error (OOG, StackUnderflow, etc.) — anything - // that is NOT ok and NOT a revert. - // In normal EVM CALL semantics, a halted child consumes the entire gas allocation - // including the retained 1/64th (revm's `is_ok_or_revert()` check gates - // `erase_cost`). We mirror that here: when the child halted, the full gas_limit - // is reported as spent. - let (child_succeeded, child_halted) = match &child_result { - FrameResult::Call(outcome) => { - let r = outcome.result.result; - (r.is_ok(), !r.is_ok_or_revert()) - } - _ => (false, true), - }; - - let completion_result = continuation - .precompile - .complete_subcall(continuation.continuation_data, &child_result); - - // Extract completion gas before consuming the result in the match below. - let completion_gas = match &completion_result { - Ok(result) => result.gas_overhead, - Err(_) => 0, // Error case already consumes all gas - }; - - // Total gas consumed by the precompile. - // - // Normal case: init_subcall overhead + child execution cost + completion overhead. - // The remainder (gas_limit - gas_used) includes the retained 1/64th implicitly, - // since it was never forwarded to the child. - // - // Child halted (OOG, StackUnderflow, etc.): standard CALL semantics burn the full - // allocation including the retained 1/64th. Still compute the metered cost with - // completion overhead; if completion cannot fit in the retained gas, the subcall - // becomes OOG below instead of silently discarding that cost. - let metered_gas_used = continuation - .init_subcall_gas_overhead - .checked_add(child_gas.spent()) - .expect("gas overflow: init_subcall_overhead + child_gas.spent()") - .checked_add(completion_gas) - .expect("gas overflow: metered_gas_used + completion_gas"); - let gas_used = if child_halted { - continuation.gas_limit.max(metered_gas_used) - } else { - metered_gas_used - }; - - let mut gas = Gas::new(continuation.gas_limit); - if !gas.record_cost(gas_used) { - gas.spend_all(); - - // If the child succeeded, its state was committed before completion - // accounting discovered the OOG. Roll it back through the same checkpoint - // protocol used for explicit completion failures. - if child_succeeded { - self.revert_subcall_checkpoint(continuation.checkpoint); - } - - return Ok(subcall_oog(gas, continuation.return_memory_offset)); - } - - match completion_result { - Ok(result) if result.success => { - // Only forward SSTORE refunds when the child frame itself succeeded. - // A reverted child's refunds are invalid — they correspond to state - // changes that were rolled back. - if child_succeeded { - gas.record_refund(child_gas.refunded()); - } - - Ok(FrameResult::Call(CallOutcome { - result: InterpreterResult::new(InstructionResult::Return, result.output, gas), - memory_offset: continuation.return_memory_offset, - was_precompile_called: true, - precompile_call_logs: Default::default(), - })) - } - completion_failure => { - let output = match completion_failure { - Ok(result) => result.output, - Err(_) => { - gas.spend_all(); - Bytes::new() - } - }; - - // If the child succeeded, revert its committed state using the - // checkpoint taken before child dispatch. - if child_succeeded { - self.revert_subcall_checkpoint(continuation.checkpoint); - } - - Ok(FrameResult::Call(CallOutcome { - result: InterpreterResult::new(InstructionResult::Revert, output, gas), - memory_offset: continuation.return_memory_offset, - was_precompile_called: true, - precompile_call_logs: Default::default(), - })) - } - } - } - - /// Revert journal state to a checkpoint that was taken without a depth increment. - /// - /// The checkpoint was originally taken via `checkpoint()` + immediate `checkpoint_commit()` - /// (to neutralize the depth side-effect for tracing compatibility). To revert: - /// 1. Call `checkpoint()` to increment depth (so `checkpoint_revert` can decrement it back) - /// 2. Call `checkpoint_revert(saved_cp)` which decrements depth and rolls back state - /// - /// Net depth change: 0. The dummy checkpoint from step 1 is discarded — we revert to - /// the saved position which predates the child's execution. - fn revert_subcall_checkpoint(&mut self, saved_cp: JournalCheckpoint) { - let depth_before = self.inner.ctx.journal_mut().depth(); - let _dummy = self.inner.ctx.journal_mut().checkpoint(); - self.inner.ctx.journal_mut().checkpoint_revert(saved_cp); - debug_assert_eq!( - self.inner.ctx.journal_mut().depth(), - depth_before, - "revert_subcall_checkpoint must not change journal depth" - ); - } -} - -// Implement InspectorEvmTr for ArcEvm -// ref: op-revm v12.0.1 implementation https://github.com/bluealloy/revm/blob/v97/crates/op-revm/src/evm.rs#L59 -impl InspectorEvmTr for ArcEvm -where - CTX: ContextTr + ContextSetters, - I: InstructionProvider, - P: PrecompileProvider, - INSP: Inspector, -{ - type Inspector = INSP; - - #[inline] - fn all_inspector( - &self, - ) -> ( - &Self::Context, - &Self::Instructions, - &Self::Precompiles, - &FrameStack, - &Self::Inspector, - ) { - self.inner.all_inspector() - } - - #[inline] - fn all_mut_inspector( - &mut self, - ) -> ( - &mut Self::Context, - &mut Self::Instructions, - &mut Self::Precompiles, - &mut FrameStack, - &mut Self::Inspector, - ) { - self.inner.all_mut_inspector() - } - - /// Override `inspect_frame_init` to make subcall precompiles transparent in traces. - /// - /// For subcall precompiles (e.g. CallFrom): uses [`SubcallPrecompile::trace_child_call`] - /// to obtain the child's `CallInputs`, then passes them to [`ArcEvm::inspect_frame_init_impl`] - /// so the trace node shows the logical child call - /// (spoofed_sender → target) instead of the precompile address. - /// - /// For non-subcall calls: delegates to the default `inspect_frame_init_impl(frame_init, None)` - /// (equivalent to upstream revm's `inspect_frame_init`). - fn inspect_frame_init( - &mut self, - mut frame_init: ::FrameInit, - ) -> Result, ContextDbError> { - // Check if this targets a subcall precompile. - let is_subcall = matches!( - &frame_init.frame_input, - FrameInput::Call(c) if self.subcall_registry.get(&c.target_address).is_some() - ); - - if !is_subcall { - // Pass `None`, so inspect_frame_init_impl behaves like upstream revm. - // We cannot call self.inner.inspect_frame_init because it calls InnerEvm::frame_init - // instead of ArcEvm::frame_init. - return self.inspect_frame_init_impl(frame_init, None); - } - - // Resolve SharedBuffer so trace_child_call can read the calldata bytes. - resolve_shared_buffer(&self.inner.ctx, &mut frame_init); - - let FrameInput::Call(call_inputs) = &frame_init.frame_input else { - debug_assert!(false, "is_subcall matched but frame_input is not Call"); - return self.inspect_frame_init_impl(frame_init, None); - }; - - // Fall back to the original input if trace_child_call returns None - // (e.g. malformed calldata that will fail during actual execution). - let trace_frame_input = self - .subcall_registry - .get(&call_inputs.target_address) - .and_then(|(precompile, _)| precompile.trace_child_call(call_inputs)) - .map(|inputs| FrameInput::Call(Box::new(inputs))) - .unwrap_or_else(|| frame_init.frame_input.clone()); - - self.inspect_frame_init_impl(frame_init, Some(trace_frame_input)) - } -} - -/// Mirrors the default [`InspectorEvmTr::inspect_frame_init`] from revm-inspector v15.0.0 -/// (`revm::inspector::traits`), but routes through `ArcEvm::frame_init` (not the inner -/// `InnerEvm::frame_init`) so Arc-specific logic (blocklist checks, EIP-7708 logs, subcall -/// routing) is always applied. -/// -/// When `trace_override` is `Some`, `frame_start`/`frame_end` use the provided input for -/// the inspector trace identity (e.g. showing the logical child call instead of the -/// precompile address). Inspector `call()` mutations are isolated from execution. -/// -/// When `trace_override` is `None`, `frame_start` receives `&mut frame_init.frame_input` -/// directly, matching upstream semantics where inspector `call()` mutations flow through -/// to execution. -/// -/// Keep in sync with: -/// (revm crate v34.0.0 — verify this function if upgrading revm) -impl ArcEvm -where - CTX: ContextTr + ContextSetters, - I: InstructionProvider, - P: PrecompileProvider, - INSP: Inspector, -{ - /// Resolve the trace identity for `frame_start`/`frame_end`. - /// - /// When `trace_override` is `Some`, `frame_start` uses a separate identity (subcall - /// precompile transparency — inspector mutations isolated from execution). - /// When `None`, `frame_start` mutates `frame_init.frame_input` directly so inspector - /// `call()` mutations flow through to execution (upstream semantics). - /// - /// Returns `Ok(trace_input)` to continue execution, or `Err(result)` for early exit - /// when `frame_start` produces a result (e.g. inspector-driven revert). - fn frame_start_with_trace( - &mut self, - frame_init: &mut FrameInit, - trace_override: Option, - ) -> Result> { - use revm::inspector::handler::{frame_end, frame_start}; - - let (ctx, inspector) = self.ctx_inspector(); - match trace_override { - Some(mut trace_input) => { - if let Some(mut output) = frame_start(ctx, inspector, &mut trace_input) { - frame_end(ctx, inspector, &trace_input, &mut output); - return Err(Box::new(output)); - } - Ok(trace_input) - } - // This branch mirrors upstream revm's inspect_frame_init. - None => { - if let Some(mut output) = frame_start(ctx, inspector, &mut frame_init.frame_input) { - frame_end(ctx, inspector, &frame_init.frame_input, &mut output); - return Err(Box::new(output)); - } - // Clone after frame_start may have mutated it — frame_init is consumed - // by move below, but frame_end still needs the trace input. - Ok(frame_init.frame_input.clone()) - } - } - } - - fn inspect_frame_init_impl( - &mut self, - mut frame_init: FrameInit, - trace_override: Option, - ) -> Result>, ContextDbError> { - use revm::inspector::handler::frame_end; - use revm::inspector::InspectorFrame; - - let trace_input_for_end = match self.frame_start_with_trace(&mut frame_init, trace_override) - { - Ok(input) => input, - Err(output) => return Ok(ItemOrResult::Result(*output)), - }; - - let (ctx, _) = self.ctx_inspector(); - let logs_i = ctx.journal().logs().len(); - if let ItemOrResult::Result(mut output) = self.frame_init(frame_init)? { - let (ctx, inspector) = self.ctx_inspector(); - // for precompiles send logs to inspector. - if let FrameResult::Call(CallOutcome { - was_precompile_called, - precompile_call_logs, - .. - }) = &mut output - { - if *was_precompile_called { - let logs = ctx.journal_mut().logs()[logs_i..].to_vec(); - for log in logs.iter().chain(precompile_call_logs.iter()).cloned() { - inspector.log(ctx, log); - } - } - } - frame_end(ctx, inspector, &trace_input_for_end, &mut output); - return Ok(ItemOrResult::Result(output)); - } - - // if it is new frame, initialize the interpreter. - let (ctx, inspector, frame) = self.ctx_inspector_frame(); - if let Some(frame) = frame.eth_frame() { - let interp = &mut frame.interpreter; - inspector.initialize_interp(interp, ctx); - }; - Ok(ItemOrResult::Item(frame)) - } -} - -impl ExecuteEvm - for ArcEvm< - EthEvmContext, - INSP, - EthInstructions>, - PRECOMPILES, - > -where - DB: Database, - INSP: Inspector, EthInterpreter>, - PRECOMPILES: PrecompileProvider, Output = InterpreterResult>, -{ - type ExecutionResult = ExecutionResult; - type State = EvmState; - type Error = - EVMError<< as ContextTr>::Db as RevmDatabase>::Error, InvalidTransaction>; - type Tx = as ContextTr>::Tx; - type Block = as ContextTr>::Block; - - fn set_block(&mut self, block: Self::Block) { - self.inner.set_block(block); - } - - fn transact_one(&mut self, tx: Self::Tx) -> Result { - self.inner.ctx.set_tx(tx); - let result = ArcEvmHandler::<_, _>::new(self.hardfork_flags).run(self); - debug_assert!( - self.subcall_continuations.is_empty(), - "stale subcall continuations after transaction" - ); - self.subcall_continuations.clear(); - result - } - - fn finalize(&mut self) -> Self::State { - self.inner.journal_mut().finalize() - } - - fn replay( - &mut self, - ) -> Result, Self::Error> { - let execution_result = - ArcEvmHandler::<_, Self::Error>::new(self.hardfork_flags).run(self)?; - debug_assert!( - self.subcall_continuations.is_empty(), - "stale subcall continuations after replay" - ); - self.subcall_continuations.clear(); - Ok(ExecResultAndState::new(execution_result, self.finalize())) - } -} - -impl InspectEvm - for ArcEvm< - EthEvmContext, - INSP, - EthInstructions>, - PRECOMPILES, - > -where - DB: Database, - INSP: Inspector, EthInterpreter>, - PRECOMPILES: PrecompileProvider, Output = InterpreterResult>, -{ - type Inspector = INSP; - - fn set_inspector(&mut self, inspector: Self::Inspector) { - self.inner.set_inspector(inspector); - } - - fn inspect_one_tx(&mut self, tx: Self::Tx) -> Result { - self.inner.ctx.set_tx(tx); - let result = ArcEvmHandler::<_, _>::new(self.hardfork_flags).inspect_run(self); - debug_assert!( - self.subcall_continuations.is_empty(), - "stale subcall continuations after inspect" - ); - self.subcall_continuations.clear(); - result - } -} - -/// implement AlloyEvmTrait for ArcEvm -impl AlloyEvmTrait - for ArcEvm< - EthEvmContext, - INSP, - EthInstructions>, - PRECOMPILE, - > -where - DB: Database, - INSP: Inspector, EthInterpreter>, - PRECOMPILE: PrecompileProvider, Output = InterpreterResult>, -{ - type DB = DB; - type Tx = TxEnv; - type Error = EVMError; - type HaltReason = HaltReason; - type Spec = SpecId; - type BlockEnv = BlockEnv; - type Precompiles = PRECOMPILE; - type Inspector = INSP; - - fn block(&self) -> &BlockEnv { - &self.inner.ctx.block - } - - fn chain_id(&self) -> u64 { - self.inner.ctx.cfg.chain_id - } - - fn transact_raw( - &mut self, - tx: Self::Tx, - ) -> Result, Self::Error> { - if self.inspect { - InspectEvm::inspect_tx(self, tx) - } else { - ExecuteEvm::transact(self, tx) - } - } - - fn transact_system_call( - &mut self, - caller: Address, - contract: Address, - data: Bytes, - ) -> Result, Self::Error> { - debug_assert!( - self.subcall_continuations.is_empty(), - "system calls must not start with active Arc subcall continuations" - ); - debug_assert!( - self.subcall_registry.get(&contract).is_none(), - "system-call target {contract:?} is an Arc subcall precompile; explicitly decide whether this target is allowed" - ); - - if !self.hardfork_flags.is_active(ArcHardfork::Zero7) { - return self.inner.system_call_with_caller(caller, contract, data); - } - - self.inner - .ctx - .journal_mut() - .load_account(NATIVE_COIN_CONTROL_ADDRESS)?; - - self.inner - .ctx - .set_tx(TxEnv::new_system_tx_with_caller(caller, contract, data)); - - let result = - ArcEvmHandler::<_, Self::Error>::new(self.hardfork_flags).run_system_call(self); - debug_assert!( - self.subcall_continuations.is_empty(), - "stale subcall continuations after system call" - ); - self.subcall_continuations.clear(); - - let result = result?; - let state = self.inner.journal_mut().finalize(); - Ok(ResultAndState::new(result, state)) - } - - fn finish(self) -> (Self::DB, EvmEnv) { - let Context { - block: block_env, - cfg: cfg_env, - journaled_state, - .. - } = self.inner.ctx; - - (journaled_state.database, EvmEnv { block_env, cfg_env }) - } - - fn set_inspector_enabled(&mut self, enabled: bool) { - self.inspect = enabled; - } - - /// Provides immutable references to the database, inspector and precompiles. - fn components(&self) -> (&Self::DB, &Self::Inspector, &Self::Precompiles) { - ( - &self.inner.ctx.journaled_state.database, - &self.inner.inspector, - &self.inner.precompiles, - ) - } - - /// Provides mutable references to the database, inspector and precompiles. - fn components_mut(&mut self) -> (&mut Self::DB, &mut Self::Inspector, &mut Self::Precompiles) { - ( - &mut self.inner.ctx.journaled_state.database, - &mut self.inner.inspector, - &mut self.inner.precompiles, - ) - } -} - -#[derive(Debug, Clone)] -#[non_exhaustive] -pub struct ArcEvmFactory { - chain_spec: Arc, -} - -impl ArcEvmFactory { - pub fn new(chain_spec: Arc) -> Self { - Self { chain_spec } - } - - fn get_hardfork_flags(&self, block_env: &BlockEnv) -> ArcHardforkFlags { - // `BlockEnv` carries `number`/`timestamp` as U256, but Arc consensus pins both - // to u64 ranges, so these conversions can only fail on malformed inputs. - let number: u64 = block_env - .number - .try_into() - .expect("Failed to convert block number to u64"); - let timestamp: u64 = block_env - .timestamp - .try_into() - .expect("Failed to convert block timestamp to u64"); - self.chain_spec.get_hardfork_flags(number, timestamp) - } - - /// Builds the subcall registry for the given hardfork flags. - /// - /// Registers subcall-capable precompiles gated on their activation hardfork. - /// When the hardfork is not active, the registry is empty and subcall addresses - /// are handled as normal (non-subcall) precompile calls. - fn build_subcall_registry(&self, hardfork_flags: ArcHardforkFlags) -> Arc { - use arc_execution_config::call_from::{MEMO_ADDRESS, MULTICALL3_FROM_ADDRESS}; - - use crate::subcall::AllowedCallers; - - let mut registry = SubcallRegistry::new(); - - if hardfork_flags.is_active(ArcHardfork::Zero7) { - registry.register( - CALL_FROM_ADDRESS, - Arc::new(CallFromPrecompile), - AllowedCallers::Only(HashSet::from([MEMO_ADDRESS, MULTICALL3_FROM_ADDRESS])), - ); - } - - Arc::new(registry) - } -} - -impl EvmFactory for ArcEvmFactory { - type Evm>> = ArcEvm< - EthEvmContext, - I, - EthInstructions>, - Self::Precompiles, - >; - type Tx = TxEnv; - type Error = EVMError; - type HaltReason = HaltReason; - type Context = EthEvmContext; - type Spec = SpecId; - type BlockEnv = BlockEnv; - type Precompiles = PrecompilesMap; - - fn create_evm(&self, db: DB, input: EvmEnv) -> Self::Evm { - let spec = input.cfg_env.spec; - let hardfork_flags = self.get_hardfork_flags(&input.block_env); - - let ctx = Self::Context::new(db, spec) - .with_cfg(input.cfg_env) - .with_block(input.block_env); - let mut instruction = EthInstructions::new_mainnet_with_spec(spec); - let precompiles = ArcPrecompileProvider::create_precompiles_map(spec, hardfork_flags); - let inspector = NoOpInspector {}; - let subcall_registry = self.build_subcall_registry(hardfork_flags); - - if hardfork_flags.is_active(ArcHardfork::Zero7) { - instruction.insert_instruction( - SELFDESTRUCT, - Instruction::new(arc_network_selfdestruct_zero7, 5000), - ); - } else if hardfork_flags.is_active(ArcHardfork::Zero5) { - instruction.insert_instruction( - SELFDESTRUCT, - Instruction::new(arc_network_selfdestruct_zero5, 5000), - ); - } else { - instruction.insert_instruction( - SELFDESTRUCT, - Instruction::new(arc_network_selfdestruct_zero4, 5000), - ); - } - - ArcEvm::new( - ctx, - inspector, - precompiles, - instruction, - false, - hardfork_flags, - subcall_registry, - ) - } - - fn create_evm_with_inspector, EthInterpreter>>( - &self, - db: DB, - input: EvmEnv, - inspector: I, - ) -> Self::Evm { - let spec = input.cfg_env.spec; - let hardfork_flags = self.get_hardfork_flags(&input.block_env); - - let ctx = Self::Context::new(db, spec) - .with_cfg(input.cfg_env) - .with_block(input.block_env); - let mut instruction = EthInstructions::new_mainnet_with_spec(spec); - let precompiles = ArcPrecompileProvider::create_precompiles_map(spec, hardfork_flags); - let subcall_registry = self.build_subcall_registry(hardfork_flags); - - if hardfork_flags.is_active(ArcHardfork::Zero7) { - instruction.insert_instruction( - SELFDESTRUCT, - Instruction::new(arc_network_selfdestruct_zero7, 5000), - ); - } else if hardfork_flags.is_active(ArcHardfork::Zero5) { - instruction.insert_instruction( - SELFDESTRUCT, - Instruction::new(arc_network_selfdestruct_zero5, 5000), - ); - } else { - instruction.insert_instruction( - SELFDESTRUCT, - Instruction::new(arc_network_selfdestruct_zero4, 5000), - ); - } - - ArcEvm::new( - ctx, - inspector, - precompiles, - instruction, - true, - hardfork_flags, - subcall_registry, - ) - } -} - -/// Custom EVM configuration for Arc -#[derive(Debug, Clone)] -pub struct ArcEvmConfig { - pub(crate) inner: EthEvmConfig, - pub(crate) evm_factory_instance: ArcEvmFactory, - pub(crate) block_assembler: ArcBlockAssembler, -} - -impl ArcEvmConfig { - /// Create a new Arc EVM configuration - pub fn new(inner: EthEvmConfig) -> Self { - let chain_spec = inner.chain_spec().clone(); - let evm_factory_instance = inner.executor_factory.evm_factory().clone(); - Self { - inner, - evm_factory_instance, - block_assembler: ArcBlockAssembler::new(chain_spec.clone()), - } - } -} - -impl BlockExecutorFactory for ArcEvmConfig { - type EvmFactory = ArcEvmFactory; - type ExecutionCtx<'a> = EthBlockExecutionCtx<'a>; - type Transaction = TransactionSigned; - type Receipt = Receipt; - - fn evm_factory(&self) -> &Self::EvmFactory { - &self.evm_factory_instance - } - - fn create_executor<'a, DB, I>( - &'a self, - evm: ::Evm<&'a mut State, I>, - ctx: EthBlockExecutionCtx<'a>, - ) -> impl BlockExecutorFor<'a, Self, DB, I> - where - DB: Database + 'a, - I: InspectorFor> + 'a, - { - ArcBlockExecutor::new( - evm, - ctx, - self.inner.chain_spec(), - self.inner.executor_factory.receipt_builder(), - ) - } -} - -impl ConfigureEvm for ArcEvmConfig { - type Primitives = ::Primitives; - type Error = ::Error; - type NextBlockEnvCtx = ::NextBlockEnvCtx; - type BlockExecutorFactory = Self; - type BlockAssembler = ArcBlockAssembler; - - fn block_executor_factory(&self) -> &Self::BlockExecutorFactory { - self - } - - fn block_assembler(&self) -> &Self::BlockAssembler { - &self.block_assembler - } - - fn evm_env(&self, header: &Header) -> Result, Self::Error> { - self.inner.evm_env(header) - } - - fn builder_for_next_block<'a, DB: Database + 'a>( - &'a self, - db: &'a mut State, - parent: &'a SealedHeader<::BlockHeader>, - attributes: Self::NextBlockEnvCtx, - ) -> Result< - impl BlockBuilder< - Primitives = Self::Primitives, - Executor: BlockExecutorFor<'a, Self::BlockExecutorFactory, DB>, - >, - Self::Error, - > { - // Query the ProtocolConfig contract for the reward beneficiary using system call - let mut attributes = attributes.clone(); - - // Create EVM environment for the system call - let mut system_evm = self.inner.evm_with_env( - &mut *db, - self.inner - .next_evm_env(parent, &attributes) - .inspect_err(|err| { - tracing::error!(error = ?err, "Failed to create EVM environment"); - })?, - ); - - // Override the gas limit with the gas limit from the ProtocolConfig contract. - // ADR-0003: use chainspec bounds; fall back to chainspec default when ProtocolConfig - // is unavailable or returns an out-of-bounds value. - let chain_spec = self.inner.chain_spec().as_ref(); - let fee_params = retrieve_fee_params(&mut system_evm).inspect_err(|err| { - tracing::warn!(error = ?err, "Failed to get fee params from ProtocolConfig, using default gas limit"); - }).ok(); - let next_block_height = parent.number.checked_add(1).expect("block number overflow"); - - let gas_limit_config = chain_spec.block_gas_limit_config(next_block_height); - attributes.gas_limit = expected_gas_limit(fee_params.as_ref(), &gas_limit_config); - - let evm_env = self.next_evm_env(parent, &attributes)?; - let evm = self.evm_with_env(db, evm_env); - let ctx = self.context_for_next_block(parent, attributes)?; - - Ok(self.create_block_builder(evm, parent, ctx)) - } - - fn next_evm_env( - &self, - parent: &Header, - attributes: &NextBlockEnvAttributes, - ) -> Result { - self.inner.next_evm_env(parent, attributes) - } - - fn context_for_block<'a>( - &self, - block: &'a SealedBlock, - ) -> Result, Self::Error> { - self.inner.context_for_block(block) - } - - fn context_for_next_block( - &self, - parent: &SealedHeader, - attributes: Self::NextBlockEnvCtx, - ) -> Result, Self::Error> { - let mut ctx = self.inner.context_for_next_block(parent, attributes)?; - // Clearing extra_data as the executor will supply it after execution. - ctx.extra_data = Default::default(); - Ok(ctx) - } -} - -impl ConfigureEngineEvm for ArcEvmConfig { - fn evm_env_for_payload(&self, payload: &ExecutionData) -> Result, Self::Error> { - self.inner.evm_env_for_payload(payload) - } - - fn context_for_payload<'a>( - &self, - payload: &'a ExecutionData, - ) -> Result, Self::Error> { - self.inner.context_for_payload(payload) - } - - fn tx_iterator_for_payload( - &self, - payload: &ExecutionData, - ) -> Result, Self::Error> { - self.inner.tx_iterator_for_payload(payload) - } -} - -#[cfg(test)] -mod tests { - use super::*; - use crate::frame_result::BeforeFrameInitResult; - - use crate::log::NativeCoinTransferred; - use crate::log::Transfer; - use alloy_consensus::Block; - use alloy_primitives::{address, Bytes, B256, U256}; - use alloy_rpc_types_engine::ExecutionData; - use alloy_sol_types::SolEvent; - use arc_execution_config::chainspec::{DEVNET, LOCAL_DEV, TESTNET}; - use arc_precompiles::precompile_provider::ArcPrecompileProvider; - use arc_precompiles::{ - native_coin_control, NATIVE_COIN_AUTHORITY_ADDRESS, NATIVE_COIN_CONTROL_ADDRESS, - }; - use reth_chainspec::{EthChainSpec, ForkCondition}; - use reth_ethereum::evm::revm::{ - context::CfgEnv, - db::{CacheDB, EmptyDB}, - primitives::keccak256, - }; - use reth_ethereum::evm::revm_spec_by_timestamp_and_block_number; - use reth_ethereum_primitives::TransactionSigned; - use reth_evm::{eth::EthEvmContext, precompiles::PrecompilesMap}; - use reth_node_api::ConfigureEvm; - use revm::context::ContextTr; - use revm::interpreter::{ - interpreter_action::{ - CallInput, CallInputs, CallScheme, CallValue, CreateInputs, FrameInit, FrameInput, - }, - InstructionResult, SharedMemory, - }; - use revm::{ - bytecode::{opcode, Bytecode}, - context::Context, - database::InMemoryDB, - handler::instructions::EthInstructions, - inspector::NoOpInspector, - }; - use revm_context_interface::journaled_state::account::JournaledAccountTr; - use revm_interpreter::interpreter::EthInterpreter; - use revm_interpreter::CreateScheme; - use revm_primitives::hardfork::SpecId; - use rstest::rstest; - - struct TestCase { - name: &'static str, - frame_input: FrameInput, - expected_log: Option, - } - - const ADDRESS_A: Address = address!("1000000000000000000000000000000000000001"); - const ADDRESS_B: Address = address!("2000000000000000000000000000000000000002"); - - type TestEvm = ArcEvm< - EthEvmContext, - NoOpInspector, - EthInstructions>, - PrecompilesMap, - >; - - fn is_account_cold(evm: &TestEvm, address: Address) -> bool { - let journal = evm.inner.ctx.journal(); - let transaction_id = journal.inner.transaction_id; - let account_is_cold = journal - .inner - .state - .get(&address) - .is_none_or(|account| account.is_cold_transaction_id(transaction_id)); - - account_is_cold && journal.inner.warm_addresses.is_cold(&address) - } - - fn create_arc_evm(chain_spec: Arc, db: InMemoryDB) -> TestEvm { - let spec = revm_spec_by_timestamp_and_block_number(&chain_spec, 0, 0); - let hardfork_flags = chain_spec.get_hardfork_flags(0, 0); - let cfg_env = CfgEnv::new() - .with_chain_id(chain_spec.chain_id()) - .with_spec_and_mainnet_gas_params(spec); - let block_env = BlockEnv::default(); - - let ctx = EthEvmContext::new(db, spec) - .with_cfg(cfg_env) - .with_block(block_env); - let mut instruction = EthInstructions::new_mainnet_with_spec(spec); - let precompiles = ArcPrecompileProvider::create_precompiles_map(spec, hardfork_flags); - let inspector = NoOpInspector {}; - - if hardfork_flags.is_active(ArcHardfork::Zero7) { - instruction.insert_instruction( - SELFDESTRUCT, - revm_interpreter::Instruction::new(arc_network_selfdestruct_zero7, 5000), - ); - } else if hardfork_flags.is_active(ArcHardfork::Zero5) { - instruction.insert_instruction( - SELFDESTRUCT, - revm_interpreter::Instruction::new(arc_network_selfdestruct_zero5, 5000), - ); - } else { - instruction.insert_instruction( - SELFDESTRUCT, - revm_interpreter::Instruction::new(arc_network_selfdestruct_zero4, 5000), - ); - } - - let factory = ArcEvmFactory::new(chain_spec); - let subcall_registry = factory.build_subcall_registry(hardfork_flags); - - ArcEvm::new( - ctx, - inspector, - precompiles, - instruction, - false, - hardfork_flags, - subcall_registry, - ) - } - - fn evm_env_with_spec(spec: SpecId) -> EvmEnv { - let cfg_env = CfgEnv::new() - .with_chain_id(LOCAL_DEV.chain_id()) - .with_spec_and_mainnet_gas_params(spec); - - EvmEnv { - block_env: BlockEnv::default(), - cfg_env, - } - } - - #[test] - fn factory_uses_input_spec_for_instruction_tables() { - let factory = ArcEvmFactory::new(LOCAL_DEV.clone()); - let evm = factory.create_evm(InMemoryDB::default(), evm_env_with_spec(SpecId::TANGERINE)); - - assert_eq!(evm.inner.instruction.spec, SpecId::TANGERINE); - assert_eq!( - evm.inner.instruction.instruction_table[opcode::DELEGATECALL as usize].static_gas(), - 700 - ); - - let evm = factory.create_evm_with_inspector( - InMemoryDB::default(), - evm_env_with_spec(SpecId::TANGERINE), - NoOpInspector {}, - ); - - assert_eq!(evm.inner.instruction.spec, SpecId::TANGERINE); - assert_eq!( - evm.inner.instruction.instruction_table[opcode::DELEGATECALL as usize].static_gas(), - 700 - ); - } - - fn create_db(accounts: &[(Address, u64)]) -> InMemoryDB { - let mut db = InMemoryDB::default(); - for (address, balance) in accounts { - db.insert_account_info( - *address, - revm::state::AccountInfo { - balance: U256::from(*balance), - nonce: 0, - code_hash: alloy_primitives::KECCAK256_EMPTY, - code: None, - account_id: None, - }, - ); - } - db - } - - fn call_input( - scheme: CallScheme, - value: U256, - caller: Address, - target: Address, - ) -> Box { - Box::new(CallInputs { - scheme, - target_address: target, - bytecode_address: address!("2000000000000000000000000000000000000002"), - known_bytecode: None, - value: CallValue::Transfer(value), - input: CallInput::Bytes(Bytes::new()), - gas_limit: 100_000, - is_static: false, - caller, - return_memory_offset: 0..1, - }) - } - - fn create_input(scheme: CreateScheme, value: U256, caller: Address) -> Box { - Box::new(CreateInputs::new( - caller, - scheme, - value, - Bytes::new(), - 100_000, - )) - } - - /// Builds bytecode that executes `CALL(gas, target, value, 0, 0, 0, 0)`. - /// - /// Equivalent assembly (stack grows left-to-right, CALL pops 7 args): - /// ```text - /// PUSH1 0 ; retLength - /// PUSH1 0 ; retOffset - /// PUSH1 0 ; argsLength - /// PUSH1 0 ; argsOffset - /// PUSH32 ; value to transfer - /// PUSH20 ; target address - /// GAS ; forward all remaining gas - /// CALL ; CALL(gas, target, value, argsOffset, argsLength, retOffset, retLength) - /// POP ; discard success flag - /// STOP - /// ``` - #[allow(clippy::vec_init_then_push)] - fn call_with_value_bytecode(target: Address, value: U256) -> Bytecode { - let mut bytecode = Vec::new(); - - // retLength, retOffset, argsLength, argsOffset — all zero - bytecode.push(opcode::PUSH1); - bytecode.push(0); - bytecode.push(opcode::PUSH1); - bytecode.push(0); - bytecode.push(opcode::PUSH1); - bytecode.push(0); - bytecode.push(opcode::PUSH1); - bytecode.push(0); - - // value - bytecode.push(opcode::PUSH32); - bytecode.extend_from_slice(&value.to_be_bytes::<32>()); - - // target address - bytecode.push(opcode::PUSH20); - bytecode.extend_from_slice(target.as_slice()); - - // gas (all remaining), CALL, discard result, stop - bytecode.push(opcode::GAS); - bytecode.push(opcode::CALL); - bytecode.push(opcode::POP); - bytecode.push(opcode::STOP); - - Bytecode::new_legacy(bytecode.into()) - } - - /// Builds bytecode that copies `init_code` into memory then executes - /// `CREATE(value, 0, init_code.len())`. - /// - /// Equivalent assembly: - /// ```text - /// ;; Copy init_code into memory byte-by-byte - /// PUSH1 ; for each byte of init_code... - /// PUSH1 ; memory offset - /// MSTORE8 ; memory[index] = byte - /// ... - /// ;; Execute CREATE - /// PUSH1 ; init_code length - /// PUSH1 0 ; memory offset - /// PUSH32 ; value to endow the new contract - /// CREATE ; CREATE(value, offset, length) - /// POP ; discard new contract address - /// STOP - /// ``` - #[allow(clippy::vec_init_then_push)] - fn create_with_value_bytecode(init_code: &[u8], value: U256) -> Bytecode { - let mut bytecode = Vec::new(); - - // Copy init_code into memory byte-by-byte: memory[i] = init_code[i] - for (i, byte) in init_code.iter().enumerate() { - bytecode.push(opcode::PUSH1); - bytecode.push(*byte); - bytecode.push(opcode::PUSH1); - bytecode.push(i as u8); - bytecode.push(opcode::MSTORE8); - } - - // length, offset - bytecode.push(opcode::PUSH1); - bytecode.push(init_code.len() as u8); - bytecode.push(opcode::PUSH1); - bytecode.push(0); - - // value - bytecode.push(opcode::PUSH32); - bytecode.extend_from_slice(&value.to_be_bytes::<32>()); - - // CREATE, discard result, stop - bytecode.push(opcode::CREATE); - bytecode.push(opcode::POP); - bytecode.push(opcode::STOP); - - Bytecode::new_legacy(bytecode.into()) - } - - fn create_test_evm( - db: InMemoryDB, - hardfork_flags: ArcHardforkFlags, - ) -> ArcEvm< - EthEvmContext, - NoOpInspector, - EthInstructions>, - PrecompilesMap, - > { - let spec = SpecId::PRAGUE; - let precompiles = ArcPrecompileProvider::create_precompiles_map(spec, hardfork_flags); - create_test_evm_with_precompiles(db, hardfork_flags, precompiles) - } - - fn create_test_evm_with_precompiles( - db: InMemoryDB, - hardfork_flags: ArcHardforkFlags, - precompiles: PrecompilesMap, - ) -> ArcEvm< - EthEvmContext, - NoOpInspector, - EthInstructions>, - PrecompilesMap, - > { - let spec = SpecId::PRAGUE; - let ctx = Context::new(db, spec); - let instruction = EthInstructions::new_mainnet_with_spec(spec); - - let subcall_registry = Arc::new(SubcallRegistry::default()); - ArcEvm::new( - ctx, - NoOpInspector {}, - precompiles, - instruction, - false, - hardfork_flags, - subcall_registry, - ) - } - - #[test] - fn test_builder_for_next_block_fallback_behavior() { - use alloy_primitives::B256; - - // Create test setup - let chain_spec = LOCAL_DEV.clone(); - let inner_config = EthEvmConfig::new_with_evm_factory( - chain_spec.clone(), - ArcEvmFactory::new(chain_spec.clone()), - ); - let evm_config = ArcEvmConfig::new(inner_config); - let mut db = State::builder().build(); - let parent_header = Header { - number: 1, - gas_limit: 30_000_000, - gas_used: 21_000, - base_fee_per_gas: Some(1_000_000_000), // 1 gwei - timestamp: 1000, - ..Default::default() - }; - let sealed_parent = SealedHeader::new(parent_header, B256::ZERO); - - let attributes = NextBlockEnvAttributes { - timestamp: 1001, - prev_randao: B256::ZERO, - suggested_fee_recipient: Address::repeat_byte(0x42), - gas_limit: 30_000_000, - parent_beacon_block_root: None, - withdrawals: None, - extra_data: Default::default(), - }; - - let result = evm_config.builder_for_next_block(&mut db, &sealed_parent, attributes); - assert!( - result.is_ok(), - "builder_for_next_block should succeed when ProtocolConfig is absent" - ); - } - - #[test] - fn test_system_call_preserves_system_call_semantics() { - let chain_spec = LOCAL_DEV.clone(); - let system_contract = Address::repeat_byte(0x21); - let return_value = U256::from(42); - - // PUSH1 0x2a PUSH1 0x00 MSTORE PUSH1 0x20 PUSH1 0x00 RETURN - let runtime = Bytecode::new_raw(Bytes::from(vec![ - 0x60, 0x2a, 0x60, 0x00, 0x52, 0x60, 0x20, 0x60, 0x00, 0xf3, - ])); - - let mut db = create_db(&[]); - db.insert_account_info( - system_contract, - revm::state::AccountInfo { - balance: U256::ZERO, - nonce: 1, - code_hash: keccak256(runtime.bytecode()), - code: Some(runtime), - account_id: None, - }, - ); - let mut evm = create_arc_evm(chain_spec, db); - - let result = alloy_evm::Evm::transact_system_call( - &mut evm, - Address::ZERO, - system_contract, - Bytes::new(), - ) - .expect("system call should execute"); - - assert!(result.result.is_success(), "system call should succeed"); - assert_eq!( - result.result.logs().len(), - 0, - "zero-value system call should emit no logs" - ); - assert_eq!( - result - .result - .output() - .expect("successful system call should return output") - .as_ref(), - &return_value.to_be_bytes::<32>() - ); - } - - #[test] - fn test_zero7_system_call_uses_arc_frame_init_for_nested_blocklist_checks() { - let chain_spec = LOCAL_DEV.clone(); - let system_contract = Address::repeat_byte(0x21); - let blocklisted_target = Address::repeat_byte(0x22); - let transfer_amount = U256::from(1); - let runtime = call_with_value_bytecode(blocklisted_target, transfer_amount); - - let mut db = create_db(&[]); - db.insert_account_info( - system_contract, - revm::state::AccountInfo { - balance: U256::from(10), - nonce: 1, - code_hash: keccak256(runtime.bytecode()), - code: Some(runtime), - account_id: None, - }, - ); - let slot = native_coin_control::compute_is_blocklisted_storage_slot(blocklisted_target); - db.insert_account_storage(NATIVE_COIN_CONTROL_ADDRESS, slot.into(), U256::from(1)) - .expect("insert blocklist storage"); - let mut evm = create_arc_evm(chain_spec, db); - - let result = alloy_evm::Evm::transact_system_call( - &mut evm, - Address::ZERO, - system_contract, - Bytes::new(), - ) - .expect("system call should execute"); - - assert!( - result.result.is_success(), - "parent system call should succeed after catching failed child CALL" - ); - assert_eq!( - result.result.logs().len(), - 0, - "blocked child value transfer should not emit an EIP-7708 log" - ); - - let target_balance = result - .state - .get(&blocklisted_target) - .map(|account| account.info.balance) - .unwrap_or(U256::ZERO); - assert_eq!( - target_balance, - U256::ZERO, - "nested CALL to blocklisted target should be rejected before value transfer" - ); - } - - #[test] - fn test_pre_zero7_system_call_preserves_nested_call_blocklist_bypass() { - use arc_execution_config::{chainspec::localdev_with_hardforks, hardforks::ArcHardfork}; - use reth_chainspec::ForkCondition; - - let chain_spec = localdev_with_hardforks(&[ - (ArcHardfork::Zero3, ForkCondition::Block(0)), - (ArcHardfork::Zero4, ForkCondition::Block(0)), - (ArcHardfork::Zero5, ForkCondition::Block(0)), - (ArcHardfork::Zero6, ForkCondition::Block(0)), - ]); - let system_contract = Address::repeat_byte(0x21); - let blocklisted_target = Address::repeat_byte(0x22); - let transfer_amount = U256::from(1); - let runtime = call_with_value_bytecode(blocklisted_target, transfer_amount); - - let mut db = create_db(&[]); - db.insert_account_info( - system_contract, - revm::state::AccountInfo { - balance: U256::from(10), - nonce: 1, - code_hash: keccak256(runtime.bytecode()), - code: Some(runtime), - account_id: None, - }, - ); - let slot = native_coin_control::compute_is_blocklisted_storage_slot(blocklisted_target); - db.insert_account_storage(NATIVE_COIN_CONTROL_ADDRESS, slot.into(), U256::from(1)) - .expect("insert blocklist storage"); - let mut evm = create_arc_evm(chain_spec, db); - - let result = alloy_evm::Evm::transact_system_call( - &mut evm, - Address::ZERO, - system_contract, - Bytes::new(), - ) - .expect("system call should execute"); - - assert!( - result.result.is_success(), - "pre-Zero7 parent system call should succeed" - ); - - let target_balance = result - .state - .get(&blocklisted_target) - .map(|account| account.info.balance) - .unwrap_or(U256::ZERO); - assert_eq!( - target_balance, transfer_amount, - "pre-Zero7 nested CALL to blocklisted target should preserve old blocklist bypass" - ); - } - - #[test] - fn test_system_call_preloads_native_coin_control_for_selfdestruct_blocklist() { - let chain_spec = LOCAL_DEV.clone(); - let system_contract = Address::repeat_byte(0x21); - let blocklisted_target = Address::repeat_byte(0x22); - - // PUSH20 SELFDESTRUCT - let mut runtime = vec![opcode::PUSH20]; - runtime.extend_from_slice(blocklisted_target.as_slice()); - runtime.push(SELFDESTRUCT); - let runtime = Bytecode::new_raw(Bytes::from(runtime)); - - let mut db = create_db(&[]); - db.insert_account_info( - system_contract, - revm::state::AccountInfo { - balance: U256::from(1), - nonce: 1, - code_hash: keccak256(runtime.bytecode()), - code: Some(runtime), - account_id: None, - }, - ); - let slot = native_coin_control::compute_is_blocklisted_storage_slot(blocklisted_target); - db.insert_account_storage(NATIVE_COIN_CONTROL_ADDRESS, slot.into(), U256::from(1)) - .expect("insert blocklist storage"); - let mut evm = create_arc_evm(chain_spec, db); - - let result = alloy_evm::Evm::transact_system_call( - &mut evm, - Address::ZERO, - system_contract, - Bytes::new(), - ) - .expect("system call should execute"); - - let ExecutionResult::Revert { output, .. } = result.result else { - panic!("system call selfdestruct to blocklisted target should revert"); - }; - assert_eq!( - output, - arc_precompiles::helpers::revert_message_to_bytes(ERR_BLOCKED_ADDRESS), - ); - } - - #[test] - fn test_pre_zero7_system_call_preserves_selfdestruct_blocklist_fail_open() { - use arc_execution_config::{chainspec::localdev_with_hardforks, hardforks::ArcHardfork}; - use reth_chainspec::ForkCondition; - - let chain_spec = localdev_with_hardforks(&[ - (ArcHardfork::Zero3, ForkCondition::Block(0)), - (ArcHardfork::Zero4, ForkCondition::Block(0)), - (ArcHardfork::Zero5, ForkCondition::Block(0)), - (ArcHardfork::Zero6, ForkCondition::Block(0)), - ]); - let system_contract = Address::repeat_byte(0x21); - let blocklisted_target = Address::repeat_byte(0x22); - - // PUSH20 SELFDESTRUCT - let mut runtime = vec![opcode::PUSH20]; - runtime.extend_from_slice(blocklisted_target.as_slice()); - runtime.push(SELFDESTRUCT); - let runtime = Bytecode::new_raw(Bytes::from(runtime)); - - let mut db = create_db(&[]); - db.insert_account_info( - system_contract, - revm::state::AccountInfo { - balance: U256::from(1), - nonce: 1, - code_hash: keccak256(runtime.bytecode()), - code: Some(runtime), - account_id: None, - }, - ); - let slot = native_coin_control::compute_is_blocklisted_storage_slot(blocklisted_target); - db.insert_account_storage(NATIVE_COIN_CONTROL_ADDRESS, slot.into(), U256::from(1)) - .expect("insert blocklist storage"); - let mut evm = create_arc_evm(chain_spec, db); - - let result = alloy_evm::Evm::transact_system_call( - &mut evm, - Address::ZERO, - system_contract, - Bytes::new(), - ) - .expect("system call should execute"); - - assert!( - result.result.is_success(), - "pre-Zero7 system call should preserve old fail-open behavior" - ); - } - - /// Under Zero5, Arc self-emits EIP-7708 Transfer logs for CALL/CREATE value transfers. - #[test] - fn test_transact_one_eip7708_log_under_zero5() { - use alloy_primitives::B256; - use arc_execution_config::{chainspec::localdev_with_hardforks, hardforks::ArcHardfork}; - use revm::handler::SYSTEM_ADDRESS; - use revm_primitives::TxKind; - - let chain_spec = localdev_with_hardforks(&[ - (ArcHardfork::Zero3, ForkCondition::Block(0)), - (ArcHardfork::Zero4, ForkCondition::Block(0)), - (ArcHardfork::Zero5, ForkCondition::Block(0)), - ]); - let sender = Address::repeat_byte(0x01); - let receiver = Address::repeat_byte(0x02); - let amount = U256::from(100); - let tx = TxEnv { - caller: sender, - kind: TxKind::Call(receiver), - value: amount, - gas_limit: 21_000, - gas_price: 0, - chain_id: Some(chain_spec.chain_id()), - ..Default::default() - }; - let db = create_db(&[(sender, 1000)]); - let mut evm = create_arc_evm(chain_spec.clone(), db); - - let result = evm.transact_one(tx).expect("transact_one should succeed"); - - assert!( - matches!(result, ExecutionResult::Success { .. }), - "transact_one execution should succeed" - ); - let logs = match &result { - ExecutionResult::Success { logs, .. } => logs, - _ => panic!("Expected Success result"), - }; - assert_eq!(logs.len(), 1, "Zero5: expect 1 EIP-7708 Transfer log"); - assert_eq!( - logs[0].address, SYSTEM_ADDRESS, - "Log should be from EIP-7708 system address" - ); - // Verify full log content: 3 topics (event sig, from, to) + amount data - assert_eq!( - logs[0].topics().len(), - 3, - "EIP-7708 Transfer log should have 3 topics" - ); - assert_eq!( - logs[0].topics()[1], - B256::left_padding_from(sender.as_slice()), - "topic[1] should be sender address" - ); - assert_eq!( - logs[0].topics()[2], - B256::left_padding_from(receiver.as_slice()), - "topic[2] should be receiver address" - ); - assert_eq!( - logs[0].data.data.as_ref(), - &amount.to_be_bytes::<32>(), - "log data should encode the transfer amount" - ); - } - - /// Under Zero5, replay emits EIP-7708 Transfer logs. - #[test] - fn test_replay_eip7708_log_under_zero5() { - use alloy_primitives::B256; - use arc_execution_config::{chainspec::localdev_with_hardforks, hardforks::ArcHardfork}; - use revm::handler::SYSTEM_ADDRESS; - use revm_primitives::TxKind; - - let chain_spec = localdev_with_hardforks(&[ - (ArcHardfork::Zero3, ForkCondition::Block(0)), - (ArcHardfork::Zero4, ForkCondition::Block(0)), - (ArcHardfork::Zero5, ForkCondition::Block(0)), - ]); - let sender = Address::repeat_byte(0x01); - let receiver = Address::repeat_byte(0x02); - let amount = U256::from(100); - let tx = TxEnv { - caller: sender, - kind: TxKind::Call(receiver), - value: amount, - gas_limit: 21_000, - gas_price: 0, - chain_id: Some(chain_spec.chain_id()), - ..Default::default() - }; - let db = create_db(&[(sender, 1000)]); - let mut evm = create_arc_evm(chain_spec.clone(), db); - - evm.inner.ctx.set_tx(tx); - let replay_result = evm.replay().expect("replay should succeed"); - - assert!( - matches!(replay_result.result, ExecutionResult::Success { .. }), - "replay execution should succeed" - ); - let replay_logs = match &replay_result.result { - ExecutionResult::Success { logs, .. } => logs, - _ => panic!("Expected Success result"), - }; - assert_eq!( - replay_logs.len(), - 1, - "Zero5: expect 1 EIP-7708 Transfer log in replay" - ); - assert_eq!( - replay_logs[0].address, SYSTEM_ADDRESS, - "Log should be from EIP-7708 system address" - ); - // Verify full log content - assert_eq!( - replay_logs[0].topics()[1], - B256::left_padding_from(sender.as_slice()), - "topic[1] should be sender address" - ); - assert_eq!( - replay_logs[0].topics()[2], - B256::left_padding_from(receiver.as_slice()), - "topic[2] should be receiver address" - ); - assert_eq!( - replay_logs[0].data.data.as_ref(), - &amount.to_be_bytes::<32>(), - "log data should encode the transfer amount" - ); - } - - /// Zero-value CALL under Zero5 emits no log (transfer amount check short-circuits). - #[test] - fn test_zero5_zero_value_call_no_log() { - let db = CacheDB::new(EmptyDB::default()); - let flags = ArcHardforkFlags::with(&[ArcHardfork::Zero5]); - let mut evm = create_test_evm(db, flags); - - evm.ctx_mut() - .journal_mut() - .load_account(NATIVE_COIN_CONTROL_ADDRESS) - .unwrap(); - - let mut frame = FrameInit { - frame_input: FrameInput::Call(call_input( - CallScheme::Call, - U256::ZERO, - ADDRESS_A, - ADDRESS_B, - )), - memory: SharedMemory::default(), - depth: 1, - }; - - let result = evm.before_frame_init(&mut frame).unwrap(); - assert!( - matches!(result, BeforeFrameInitResult::None), - "Zero-value call under Zero5 should return None (no log, no blocklist checks)" - ); - } - - #[test] - fn test_capture_transfer_events() { - let test_cases = vec![ - TestCase { - name: "call with value", - frame_input: FrameInput::Call(call_input( - CallScheme::Call, - U256::from(1), - ADDRESS_A, - ADDRESS_B, - )), - expected_log: Some(NativeCoinTransferred { - from: ADDRESS_A, - to: ADDRESS_B, - amount: U256::from(1), - }), - }, - TestCase { - name: "call with no value", - frame_input: FrameInput::Call(call_input( - CallScheme::Call, - U256::ZERO, - ADDRESS_A, - ADDRESS_B, - )), - expected_log: None, - }, - TestCase { - name: "create with value", - frame_input: FrameInput::Create(create_input( - CreateScheme::Create, - U256::from(1), - ADDRESS_A, - )), - expected_log: Some(NativeCoinTransferred { - from: ADDRESS_A, - to: ADDRESS_A.create(0), - amount: U256::from(1), - }), - }, - TestCase { - name: "create with no value", - frame_input: FrameInput::Create(create_input( - CreateScheme::Create, - U256::ZERO, - ADDRESS_A, - )), - expected_log: None, - }, - TestCase { - name: "create2 with value", - frame_input: FrameInput::Create(create_input( - CreateScheme::Create2 { - salt: U256::from(123), - }, - U256::from(1), - ADDRESS_A, - )), - expected_log: Some(NativeCoinTransferred { - from: ADDRESS_A, - to: ADDRESS_A.create2(U256::from(123).to_be_bytes(), keccak256(Bytes::new())), - amount: U256::from(1), - }), - }, - TestCase { - name: "create2 with no value", - frame_input: FrameInput::Create(create_input( - CreateScheme::Create2 { - salt: U256::from(123), - }, - U256::ZERO, - ADDRESS_A, - )), - expected_log: None, - }, - ]; - - // Test pre-Zero5 hardforks only — Zero5 enables EIP-7708 which emits different logs. - for hardfork in [ArcHardfork::Zero3, ArcHardfork::Zero4] { - for test in &test_cases { - let mut frame = FrameInit { - frame_input: test.frame_input.clone(), - memory: SharedMemory::default(), - depth: 0, - }; - - let db = CacheDB::new(EmptyDB::default()); - let mut evm = create_test_evm(db, ArcHardforkFlags::with(&[hardfork])); - - // Load native coin control account - evm.ctx_mut() - .journal_mut() - .load_account(NATIVE_COIN_CONTROL_ADDRESS) - .unwrap(); - - let transfer_result = evm.before_frame_init(&mut frame).unwrap(); - - // No early return should occur for basic tests - assert!( - !matches!(transfer_result, BeforeFrameInitResult::Reverted(_)), - "{} (hardfork: {:?}): unexpected blocklist violation", - test.name, - hardfork - ); - - let log_opt = match transfer_result { - BeforeFrameInitResult::Log(log, _gas) => Some(log), - _ => None, - }; - - assert_eq!( - log_opt.is_some(), - test.expected_log.is_some(), - "{} (hardfork: {:?}): unexpected log result", - test.name, - hardfork - ); - - if let Some(log) = log_opt { - assert_eq!( - log.address, NATIVE_COIN_AUTHORITY_ADDRESS, - "{} (hardfork: {:?}): wrong log address", - test.name, hardfork - ); - - let log_data = NativeCoinTransferred::decode_log(&log); - assert!( - log_data.is_ok(), - "{} (hardfork: {:?}): failed to decode log", - test.name, - hardfork - ); - assert_eq!( - &log_data.unwrap().data, - test.expected_log.as_ref().unwrap(), - "Native send event mismatch" - ); - } - } - } - } - - #[test] - fn test_create2_with_value_normalizes_to_custom_address() { - let db = CacheDB::new(EmptyDB::default()); - let mut evm = create_test_evm(db, ArcHardforkFlags::with(&[ArcHardfork::Zero5])); - evm.ctx_mut() - .journal_mut() - .load_account(NATIVE_COIN_CONTROL_ADDRESS) - .unwrap(); - - let salt = U256::from(123); - let init_code = Bytes::from(vec![0x60, 0x00, 0x60, 0x00, 0xF3]); - let expected_address = ADDRESS_A.create2(salt.to_be_bytes(), keccak256(&init_code)); - let mut frame = FrameInit { - frame_input: FrameInput::Create(Box::new(CreateInputs::new( - ADDRESS_A, - CreateScheme::Create2 { salt }, - U256::from(1), - init_code, - 100_000, - ))), - memory: SharedMemory::default(), - depth: 0, - }; - - let transfer_result = evm.before_frame_init(&mut frame).unwrap(); - assert!( - matches!(transfer_result, BeforeFrameInitResult::Log(_, _)), - "CREATE2 with value should produce a transfer log, got {transfer_result:?}" - ); - - let FrameInput::Create(inputs) = &frame.frame_input else { - panic!("expected CREATE frame"); - }; - assert_eq!( - inputs.scheme(), - CreateScheme::Custom { - address: expected_address, - }, - "CREATE2 should be normalized to the precomputed custom address" - ); - } - - #[test] - fn test_create2_with_value_rejects_blocklisted_created_address() { - let db = CacheDB::new(EmptyDB::default()); - let mut evm = create_test_evm(db, ArcHardforkFlags::with(&[ArcHardfork::Zero5])); - evm.ctx_mut() - .journal_mut() - .load_account(NATIVE_COIN_CONTROL_ADDRESS) - .unwrap(); - - let salt = U256::from(456); - let init_code = Bytes::from(vec![0x60, 0x00, 0x60, 0x00, 0xF3]); - let expected_address = ADDRESS_A.create2(salt.to_be_bytes(), keccak256(&init_code)); - let storage_slot = - native_coin_control::compute_is_blocklisted_storage_slot(expected_address); - evm.ctx_mut() - .journal_mut() - .sstore( - NATIVE_COIN_CONTROL_ADDRESS, - storage_slot.into(), - U256::from(1), - ) - .unwrap(); - - let mut frame = FrameInit { - frame_input: FrameInput::Create(Box::new(CreateInputs::new( - ADDRESS_A, - CreateScheme::Create2 { salt }, - U256::from(1), - init_code, - 100_000, - ))), - memory: SharedMemory::default(), - depth: 0, - }; - - let transfer_result = evm.before_frame_init(&mut frame).unwrap(); - assert!( - matches!(transfer_result, BeforeFrameInitResult::Reverted(_)), - "CREATE2 to blocklisted created address should revert, got {transfer_result:?}" - ); - - let FrameInput::Create(inputs) = &frame.frame_input else { - panic!("expected CREATE frame"); - }; - assert_eq!( - inputs.scheme(), - CreateScheme::Custom { - address: expected_address, - }, - "rejected CREATE2 should still carry the precomputed custom address" - ); - } - - struct BlocklistTestCase { - name: &'static str, - frame_input: FrameInput, - sender_blocklisted: bool, - recipient_blocklisted: bool, - expected_reverted: bool, - expect_context_db_error: bool, - } - - fn run_blocklist_test_case( - test_case: &BlocklistTestCase, - hardfork: ArcHardfork, - sender: Address, - recipient: Address, - ) { - println!( - "Running blocklist test case: {} (hardfork: {:?})", - test_case.name, hardfork - ); - - let mut frame = FrameInit { - frame_input: test_case.frame_input.clone(), - memory: SharedMemory::default(), - depth: 0, - }; - - let db = CacheDB::new(EmptyDB::default()); - let mut evm = create_test_evm(db, ArcHardforkFlags::with(&[hardfork])); - - // Set transaction nonce for CREATE address calculation - evm.ctx_mut().tx.nonce = 0; - - if test_case.expect_context_db_error { - // Intentionally do NOT load the account into the journal - // This will cause is_address_blocklisted to fail (sload panics when account is not loaded) - let transfer_result = std::panic::catch_unwind(std::panic::AssertUnwindSafe(|| { - evm.before_frame_init(&mut frame) - })); - - assert!( - transfer_result.is_err(), - "{} (hardfork: {:?}): expected panic when account is not loaded but got {:?}", - test_case.name, - hardfork, - transfer_result - ); - return; - } - - // Load native coin control account - evm.ctx_mut() - .journal_mut() - .load_account(NATIVE_COIN_CONTROL_ADDRESS) - .unwrap(); - - // Set sender blocklist status - if test_case.sender_blocklisted { - let storage_slot = native_coin_control::compute_is_blocklisted_storage_slot(sender); - evm.ctx_mut() - .journal_mut() - .sstore( - NATIVE_COIN_CONTROL_ADDRESS, - storage_slot.into(), - U256::from(1), // Non-zero means blocklisted - ) - .unwrap(); - } - - // Set recipient blocklist status - if test_case.recipient_blocklisted { - let storage_slot = native_coin_control::compute_is_blocklisted_storage_slot(recipient); - evm.ctx_mut() - .journal_mut() - .sstore( - NATIVE_COIN_CONTROL_ADDRESS, - storage_slot.into(), - U256::from(1), // Non-zero means blocklisted - ) - .unwrap(); - } - - let transfer_result = evm.before_frame_init(&mut frame).unwrap(); - - if test_case.expected_reverted { - assert!( - matches!(transfer_result, BeforeFrameInitResult::Reverted(_)), - "{} (hardfork: {:?}): expected blocklist violation but got {:?}", - test_case.name, - hardfork, - transfer_result - ); - if let BeforeFrameInitResult::Reverted(reverted) = transfer_result { - assert_eq!( - reverted.gas().spent(), - 0, - "{} (hardfork: {:?}): depth-0 reverts should have zero gas spent", - test_case.name, - hardfork - ); - } - } else { - assert!( - !matches!(transfer_result, BeforeFrameInitResult::Reverted(_)), - "{} (hardfork: {:?}): unexpected blocklist violation", - test_case.name, - hardfork - ); - } - } - - #[test] - fn test_capture_transfer_events_with_blocklist() { - let sender = address!("A000000000000000000000000000000000000001"); - let recipient = address!("B000000000000000000000000000000000000002"); - - let test_cases = vec![ - BlocklistTestCase { - name: "call_with_value_sender_blocklisted", - frame_input: FrameInput::Call(call_input( - CallScheme::Call, - U256::from(100), - sender, - recipient, - )), - sender_blocklisted: true, - recipient_blocklisted: false, - expected_reverted: true, - expect_context_db_error: false, - }, - BlocklistTestCase { - name: "call_with_value_recipient_blocklisted", - frame_input: FrameInput::Call(call_input( - CallScheme::Call, - U256::from(100), - sender, - recipient, - )), - sender_blocklisted: false, - recipient_blocklisted: true, - expected_reverted: true, - expect_context_db_error: false, - }, - BlocklistTestCase { - name: "call_with_value_both_blocklisted", - frame_input: FrameInput::Call(call_input( - CallScheme::Call, - U256::from(100), - sender, - recipient, - )), - sender_blocklisted: true, - recipient_blocklisted: true, - expected_reverted: true, - expect_context_db_error: false, - }, - BlocklistTestCase { - name: "call_with_value_neither_blocklisted", - frame_input: FrameInput::Call(call_input( - CallScheme::Call, - U256::from(100), - sender, - recipient, - )), - sender_blocklisted: false, - recipient_blocklisted: false, - expected_reverted: false, - expect_context_db_error: false, - }, - BlocklistTestCase { - name: "call_zero_value_sender_blocklisted_bypassed", - frame_input: FrameInput::Call(call_input( - CallScheme::Call, - U256::ZERO, - sender, - recipient, - )), - sender_blocklisted: true, - recipient_blocklisted: false, - expected_reverted: false, // Zero value bypasses blocklist - expect_context_db_error: false, - }, - BlocklistTestCase { - name: "call_zero_value_recipient_blocklisted_bypassed", - frame_input: FrameInput::Call(call_input( - CallScheme::Call, - U256::ZERO, - sender, - recipient, - )), - sender_blocklisted: false, - recipient_blocklisted: true, - expected_reverted: false, // Zero value bypasses blocklist - expect_context_db_error: false, - }, - BlocklistTestCase { - name: "delegatecall_sender_blocklisted_bypassed", - frame_input: FrameInput::Call(call_input( - CallScheme::DelegateCall, - U256::from(100), - sender, - recipient, - )), - sender_blocklisted: true, - recipient_blocklisted: false, - expected_reverted: false, // DelegateCall doesn't transfer value - expect_context_db_error: false, - }, - BlocklistTestCase { - name: "staticcall_recipient_blocklisted_bypassed", - frame_input: FrameInput::Call(call_input( - CallScheme::StaticCall, - U256::from(100), - sender, - recipient, - )), - sender_blocklisted: false, - recipient_blocklisted: true, - expected_reverted: false, // StaticCall doesn't transfer value - expect_context_db_error: false, - }, - BlocklistTestCase { - name: "callcode_sender_blocklisted_bypassed", - frame_input: FrameInput::Call(call_input( - CallScheme::CallCode, - U256::from(100), - sender, - recipient, - )), - sender_blocklisted: true, - recipient_blocklisted: false, - expected_reverted: false, // CallCode doesn't transfer value - expect_context_db_error: false, - }, - BlocklistTestCase { - name: "create_with_value_sender_blocklisted", - frame_input: FrameInput::Create(create_input( - CreateScheme::Create, - U256::from(100), - sender, - )), - sender_blocklisted: true, - recipient_blocklisted: false, - expected_reverted: true, - expect_context_db_error: false, - }, - BlocklistTestCase { - name: "create2_with_value_sender_blocklisted", - frame_input: FrameInput::Create(create_input( - CreateScheme::Create2 { - salt: U256::from(456), - }, - U256::from(100), - sender, - )), - sender_blocklisted: true, - recipient_blocklisted: false, - expected_reverted: true, - expect_context_db_error: false, - }, - BlocklistTestCase { - name: "create_zero_value_sender_blocklisted_bypassed", - frame_input: FrameInput::Create(create_input( - CreateScheme::Create, - U256::ZERO, - sender, - )), - sender_blocklisted: true, - recipient_blocklisted: false, - expected_reverted: false, // Zero value bypasses blocklist - expect_context_db_error: false, - }, - BlocklistTestCase { - name: "create2_zero_value_sender_blocklisted_bypassed", - frame_input: FrameInput::Create(create_input( - CreateScheme::Create2 { - salt: U256::from(789), - }, - U256::ZERO, - sender, - )), - sender_blocklisted: true, - recipient_blocklisted: false, - expected_reverted: false, // Zero value bypasses blocklist - expect_context_db_error: false, - }, - BlocklistTestCase { - name: "call_with_value_context_db_error", - frame_input: FrameInput::Call(call_input( - CallScheme::Call, - U256::from(100), - sender, - recipient, - )), - sender_blocklisted: false, - recipient_blocklisted: false, - expected_reverted: false, // Not used - this test expects ContextDbError - expect_context_db_error: true, - }, - ]; - - for hardfork in [ArcHardfork::Zero4, ArcHardfork::Zero5, ArcHardfork::Zero6] { - for test_case in &test_cases { - run_blocklist_test_case(test_case, hardfork, sender, recipient); - } - } - } - - /// Guards against the static_gas regression introduced during the revm v29→v32 migration. - /// revm v32 moved SELFDESTRUCT's 5000 base gas (EIP-150) from the instruction handler to - /// the instruction table's static_gas field. Since Arc overrides the instruction entry via - /// insert_instruction, the static_gas must be set explicitly or it silently drops to 0. - #[test] - fn selfdestruct_static_gas_is_5000() { - let db = InMemoryDB::default(); - let evm = create_arc_evm(LOCAL_DEV.clone(), db); - let static_gas = - evm.inner.instruction.instruction_table[SELFDESTRUCT as usize].static_gas(); - assert_eq!( - static_gas, 5000, - "SELFDESTRUCT static gas must be 5000 (EIP-150)" - ); - } - - /// Verifies that `create_arc_evm` dispatches the pre-Zero5 SELFDESTRUCT variant when - /// only Zero3+Zero4 are active. Pre-Zero5 allows SELFDESTRUCT to the zero address - /// (unlike Zero5) and emits `NativeCoinTransferred` (unlike EIP-7708). - #[test] - fn create_arc_evm_dispatches_selfdestruct_zero4() { - use arc_execution_config::{chainspec::localdev_with_hardforks, hardforks::ArcHardfork}; - use revm_primitives::TxKind; - - let chain_spec = localdev_with_hardforks(&[ - (ArcHardfork::Zero3, ForkCondition::Block(0)), - (ArcHardfork::Zero4, ForkCondition::Block(0)), - ]); - let sender = Address::repeat_byte(0x11); - let contract = Address::repeat_byte(0xBB); - - // Bytecode: PUSH20 0x00..00 SELFDESTRUCT (target = zero address) - let mut code = vec![opcode::PUSH20]; - code.extend_from_slice(Address::ZERO.as_slice()); - code.push(SELFDESTRUCT); - let runtime = Bytecode::new_raw(Bytes::from(code.clone())); - - let mut db = create_db(&[(sender, 1000)]); - db.insert_account_info( - contract, - revm::state::AccountInfo { - balance: U256::from(500), - nonce: 1, - code_hash: keccak256(&code), - code: Some(runtime), - account_id: None, - }, - ); - - let mut evm = create_arc_evm(chain_spec.clone(), db); - let tx = TxEnv { - caller: sender, - kind: TxKind::Call(contract), - value: U256::ZERO, - gas_limit: 100_000, - gas_price: 0, - chain_id: Some(chain_spec.chain_id()), - ..Default::default() - }; - - let result = evm.transact_one(tx).expect("transaction should execute"); - // Zero4 allows SELFDESTRUCT to the zero address — not a revert. - assert!( - result.is_success(), - "Zero4 SELFDESTRUCT to zero address should succeed, got {:?}", - result - ); - // Zero4 emits the custom NativeCoinTransferred log, not EIP-7708. - let logs = result.logs(); - assert!( - !logs.is_empty(), - "Zero4 SELFDESTRUCT with nonzero balance should emit a transfer log" - ); - assert_ne!( - logs[0].address, - revm::handler::SYSTEM_ADDRESS, - "Zero4 should emit NativeCoinTransferred, not an EIP-7708 Transfer log" - ); - } - - /// Transaction-level regression test: a Zero5 SELFDESTRUCT with nonzero balance - /// must produce exactly one EIP-7708 Transfer log in the final receipt. - #[test] - fn test_zero5_selfdestruct_emits_eip7708_log_in_receipt() { - use alloy_primitives::b256; - use arc_execution_config::{chainspec::localdev_with_hardforks, hardforks::ArcHardfork}; - use revm::handler::SYSTEM_ADDRESS; - use revm_primitives::TxKind; - - let chain_spec = localdev_with_hardforks(&[ - (ArcHardfork::Zero3, ForkCondition::Block(0)), - (ArcHardfork::Zero4, ForkCondition::Block(0)), - (ArcHardfork::Zero5, ForkCondition::Block(0)), - ]); - - let sender = Address::repeat_byte(0x11); - let contract = Address::repeat_byte(0xBB); - let beneficiary = Address::repeat_byte(0xCC); - let contract_balance = U256::from(500); - - // Bytecode: PUSH20 SELFDESTRUCT - let mut code = vec![opcode::PUSH20]; - code.extend_from_slice(beneficiary.as_slice()); - code.push(SELFDESTRUCT); - let runtime = Bytecode::new_raw(Bytes::from(code.clone())); - - let mut db = create_db(&[(sender, 1000)]); - db.insert_account_info( - contract, - revm::state::AccountInfo { - balance: contract_balance, - nonce: 1, - code_hash: keccak256(&code), - code: Some(runtime), - account_id: None, - }, - ); - - let mut evm = create_arc_evm(chain_spec.clone(), db); - let tx = TxEnv { - caller: sender, - kind: TxKind::Call(contract), - value: U256::ZERO, - gas_limit: 100_000, - gas_price: 0, - chain_id: Some(chain_spec.chain_id()), - ..Default::default() - }; - - let result = evm.transact_one(tx).expect("transaction should execute"); - assert!( - result.is_success(), - "Zero5 SELFDESTRUCT to non-zero beneficiary should succeed, got {:?}", - result - ); - - let logs = result.logs(); - assert_eq!( - logs.len(), - 1, - "Zero5 SELFDESTRUCT should emit exactly one EIP-7708 Transfer log, got {}", - logs.len() - ); - - let log = &logs[0]; - assert_eq!( - log.address, SYSTEM_ADDRESS, - "Log should come from the EIP-7708 system address" - ); - - let topics = log.data.topics(); - assert_eq!(topics.len(), 3, "Transfer log should have 3 topics"); - // topic0: Transfer(address,address,uint256) selector - assert_eq!( - topics[0], - b256!("ddf252ad1be2c89b69c2b068fc378daa952ba7f163c4a11628f55a4df523b3ef"), - ); - // topic1: from = contract - assert_eq!(topics[1], B256::left_padding_from(contract.as_slice())); - // topic2: to = beneficiary - assert_eq!(topics[2], B256::left_padding_from(beneficiary.as_slice())); - // data: amount = contract balance - assert_eq!( - log.data.data.as_ref(), - &contract_balance.to_be_bytes::<32>() - ); - } - - #[test] - fn transfer_to_selfdestruct_account() { - let db = InMemoryDB::default(); - let mut evm = create_test_evm( - db, - ArcHardforkFlags::with(&[ArcHardfork::Zero4, ArcHardfork::Zero5]), - ); - - // Load native coin control account - let spec_id = evm.ctx().cfg.spec; - let journal = evm.ctx_mut().journal_mut(); - journal.load_account(NATIVE_COIN_CONTROL_ADDRESS).unwrap(); - - // 1. Prepare ADDRESS_B as a destructed account. - journal - .load_account_mut_optional_code(ADDRESS_A, false) - .expect("load ADDRESS_A") - .set_balance(U256::from(100)); - journal.load_account(ADDRESS_B).expect("load ADDRESS_B"); - journal - .create_account_checkpoint(ADDRESS_A, ADDRESS_B, U256::from(100), spec_id) - .unwrap(); - journal - .selfdestruct(ADDRESS_B, ADDRESS_A, false) - .expect("selfdestruct"); - - // 2. Prepare frame to transfer balance from ADDRESS_A to ADDRESS_B - let mut frame = FrameInit { - frame_input: FrameInput::Call(Box::new(CallInputs { - scheme: CallScheme::Call, - target_address: ADDRESS_B, - bytecode_address: ADDRESS_A, - known_bytecode: None, - value: CallValue::Transfer(U256::from(100)), - input: CallInput::Bytes(Bytes::new()), - gas_limit: 100_000, - caller: ADDRESS_A, - is_static: false, - return_memory_offset: 0..0, - })), - memory: SharedMemory::default(), - depth: 0, - }; - - assert!( - !is_account_cold(&evm, ADDRESS_B), - "selfdestructed target should be warm after setup" - ); - - // 3. Should revert on transferring to destructed account - let result = evm.before_frame_init(&mut frame); - assert!( - matches!(result, Ok(BeforeFrameInitResult::Reverted(_))), - "expect revert on transferring to destructed account" - ); - - assert!( - !is_account_cold(&evm, ADDRESS_B), - "checkpointed selfdestruct probe should not make an already-warm target cold" - ); - } - - #[rstest] - #[case::zero7_enabled( - &[ArcHardfork::Zero4, ArcHardfork::Zero5, ArcHardfork::Zero6, ArcHardfork::Zero7], - true - )] - #[case::zero6_enabled( - &[ArcHardfork::Zero4, ArcHardfork::Zero5, ArcHardfork::Zero6], - false - )] - fn transfer_to_cold_target_preserves_cold_status( - #[case] hardforks: &[ArcHardfork], - #[case] expected_target_is_cold: bool, - ) { - let db = create_db(&[(ADDRESS_A, 1000), (ADDRESS_B, 0)]); - let mut evm = create_test_evm(db, ArcHardforkFlags::with(hardforks)); - - evm.ctx_mut() - .journal_mut() - .load_account(NATIVE_COIN_CONTROL_ADDRESS) - .unwrap(); - - assert!( - is_account_cold(&evm, ADDRESS_B), - "target should start cold before frame init" - ); - - let mut frame = FrameInit { - frame_input: FrameInput::Call(call_input( - CallScheme::Call, - U256::from(100), - ADDRESS_A, - ADDRESS_B, - )), - memory: SharedMemory::default(), - depth: 0, - }; - - let result = evm.before_frame_init(&mut frame).unwrap(); - assert!( - matches!(result, BeforeFrameInitResult::Log(_, _)), - "normal value transfer should still produce a transfer log, got {result:?}" - ); - - assert_eq!( - is_account_cold(&evm, ADDRESS_B), - expected_target_is_cold, - "unexpected target warmth after selfdestruct probe for hardforks {hardforks:?}" - ); - } - - #[rstest] - #[case::zero7_enabled( - &[ArcHardfork::Zero4, ArcHardfork::Zero5, ArcHardfork::Zero6, ArcHardfork::Zero7], - true - )] - #[case::zero6_enabled( - &[ArcHardfork::Zero4, ArcHardfork::Zero5, ArcHardfork::Zero6], - false - )] - fn create_to_cold_target_preserves_cold_status( - #[case] hardforks: &[ArcHardfork], - #[case] expected_target_is_cold: bool, - ) { - let db = create_db(&[(ADDRESS_A, 1000)]); - let mut evm = create_test_evm(db, ArcHardforkFlags::with(hardforks)); - - evm.ctx_mut() - .journal_mut() - .load_account(NATIVE_COIN_CONTROL_ADDRESS) - .unwrap(); - - let amount = U256::from(1); - let target_address = ADDRESS_A.create(0); - - assert!( - is_account_cold(&evm, target_address), - "target should start cold before frame init" - ); - - let mut frame = FrameInit { - frame_input: FrameInput::Create(Box::new(CreateInputs::new( - ADDRESS_A, - CreateScheme::Create, - amount, - Bytes::from(vec![0x60, 0x00, 0x60, 0x00, 0xF3]), // dummy contract byte code - 100_000, - ))), - memory: SharedMemory::default(), - depth: 0, - }; - - let result = evm.before_frame_init(&mut frame).unwrap(); - assert!( - matches!(result, BeforeFrameInitResult::Log(_, _)), - "normal value transfer should still produce a transfer log, got {result:?}" - ); - if let BeforeFrameInitResult::Log(log, _gas) = result { - let t = Transfer::decode_log(&log).expect("decode EIP-7708 Transfer log"); - assert_eq!(t.data.to, target_address); - assert_eq!(t.data.amount, amount) - } - - assert_eq!( - is_account_cold(&evm, target_address), - expected_target_is_cold, - "unexpected target warmth after selfdestruct probe for hardforks {hardforks:?}" - ); - } - - #[test] - fn test_chain_spec_resolves_to_expected_spec_id() { - let cases: &[(&str, &Arc, SpecId)] = &[ - ("localdev", &LOCAL_DEV, SpecId::OSAKA), - ("devnet", &DEVNET, SpecId::PRAGUE), - ("testnet", &TESTNET, SpecId::PRAGUE), - ]; - - for (label, chain_spec, expected) in cases { - let spec = revm_spec_by_timestamp_and_block_number(chain_spec, 0, 0); - assert_eq!(spec, *expected, "{label} should resolve to {expected:?}"); - } - } - - // These two tests exercise the extra_data contract that the executor's base fee - // validation depends on, namely, applying strict validation against extra_data contents. - // - // Reth provides a tx_count_hint, which can distinguish between various paths (payload execution vs. building vs. an eth_call type context) - // though simply validating against the presence of the extra_data is more explicit. - // To do this though, we need to clear the extra_data vs. blindly copying it over from a parent - // when building context for the next block. - #[test] - fn context_for_payload_preserves_extra_data() { - let chain_spec = LOCAL_DEV.clone(); - let evm_config = ArcEvmConfig::new(EthEvmConfig::new_with_evm_factory( - chain_spec.clone(), - ArcEvmFactory::new(chain_spec.clone()), - )); - - let block: Block = Block { - header: Header { - extra_data: arc_execution_config::gas_fee::encode_base_fee_to_bytes(12345), - ..Default::default() - }, - ..Default::default() - }; - let payload = ExecutionData::from_block_unchecked(B256::ZERO, &block); - - let ctx = evm_config - .context_for_payload(&payload) - .expect("context_for_payload must succeed"); - - assert!( - !ctx.extra_data.is_empty(), - "context_for_payload must preserve extra_data from the payload — \ - the executor relies on it being non-empty to trigger base fee validation" - ); - } - - #[test] - fn context_for_next_block_clears_extra_data() { - let chain_spec = LOCAL_DEV.clone(); - let evm_config = ArcEvmConfig::new(EthEvmConfig::new_with_evm_factory( - chain_spec.clone(), - ArcEvmFactory::new(chain_spec.clone()), - )); - - // Use a parent with non-empty extra_data to confirm we clear it. - let parent = SealedHeader::new( - Header { - extra_data: arc_execution_config::gas_fee::encode_base_fee_to_bytes(12345), - ..Default::default() - }, - B256::ZERO, - ); - let attributes = NextBlockEnvAttributes { - timestamp: 1, - prev_randao: B256::ZERO, - suggested_fee_recipient: Address::ZERO, - gas_limit: 30_000_000, - parent_beacon_block_root: None, - withdrawals: None, - extra_data: Default::default(), - }; - - let ctx = evm_config - .context_for_next_block(&parent, attributes) - .expect("context_for_next_block must succeed"); - - assert!( - ctx.extra_data.is_empty(), - "context_for_next_block must clear extra_data so the executor's \ - base fee validation gate does not fire during block building or simulation" - ); - } - - // ======================================================================== - // Subcall integration tests - // ======================================================================== - - mod build_subcall_registry { - use super::*; - use arc_execution_config::call_from::{MEMO_ADDRESS, MULTICALL3_FROM_ADDRESS}; - use arc_precompiles::call_from::CALL_FROM_ADDRESS; - - fn factory() -> ArcEvmFactory { - ArcEvmFactory::new(LOCAL_DEV.clone()) - } - - #[test] - fn registry_populated_when_zero7_active() { - let flags = ArcHardforkFlags::with(&[ArcHardfork::Zero7]); - let registry = factory().build_subcall_registry(flags); - - assert!( - registry.get(&CALL_FROM_ADDRESS).is_some(), - "CallFrom should be registered when Zero7 is active" - ); - } - - #[test] - fn registry_empty_when_zero7_not_active() { - let flags = ArcHardforkFlags::with(&[ - ArcHardfork::Zero3, - ArcHardfork::Zero4, - ArcHardfork::Zero5, - ArcHardfork::Zero6, - ]); - let registry = factory().build_subcall_registry(flags); - - assert!( - registry.get(&CALL_FROM_ADDRESS).is_none(), - "CallFrom should not be registered when Zero7 is inactive" - ); - } - - #[test] - fn registry_empty_with_no_hardforks() { - let flags = ArcHardforkFlags::default(); - let registry = factory().build_subcall_registry(flags); - - assert!( - registry.get(&CALL_FROM_ADDRESS).is_none(), - "CallFrom should not be registered with no hardforks active" - ); - } - - #[test] - fn allowed_callers_include_memo_and_multicall3_from() { - let flags = ArcHardforkFlags::with(&[ArcHardfork::Zero7]); - let registry = factory().build_subcall_registry(flags); - - let (_precompile, allowed_callers) = registry - .get(&CALL_FROM_ADDRESS) - .expect("should be registered"); - - assert!( - allowed_callers.is_allowed(&MEMO_ADDRESS), - "Memo should be an allowed caller" - ); - assert!( - allowed_callers.is_allowed(&MULTICALL3_FROM_ADDRESS), - "Multicall3From should be an allowed caller" - ); - } - - #[test] - fn arbitrary_address_not_allowed() { - let flags = ArcHardforkFlags::with(&[ArcHardfork::Zero7]); - let registry = factory().build_subcall_registry(flags); - - let (_precompile, allowed_callers) = registry - .get(&CALL_FROM_ADDRESS) - .expect("should be registered"); - - let rando = address!("deadbeefdeadbeefdeadbeefdeadbeefdeadbeef"); - assert!( - !allowed_callers.is_allowed(&rando), - "arbitrary address should not be an allowed caller" - ); - } - - #[test] - fn registry_follows_block_derived_hardfork_flags() { - use arc_execution_config::chainspec::localdev_with_hardforks; - - let spec = localdev_with_hardforks(&[ - (ArcHardfork::Zero3, ForkCondition::Block(0)), - (ArcHardfork::Zero4, ForkCondition::Block(0)), - (ArcHardfork::Zero5, ForkCondition::Block(0)), - (ArcHardfork::Zero6, ForkCondition::Block(0)), - (ArcHardfork::Zero7, ForkCondition::Block(10)), - ]); - let factory = ArcEvmFactory::new(spec.clone()); - - let pre = factory.build_subcall_registry(spec.get_hardfork_flags(9, 0)); - assert!( - pre.get(&CALL_FROM_ADDRESS).is_none(), - "block 9: Zero7 not yet active" - ); - - let post = factory.build_subcall_registry(spec.get_hardfork_flags(10, 0)); - assert!( - post.get(&CALL_FROM_ADDRESS).is_some(), - "block 10: Zero7 active" - ); - } - } - - mod subcall_tests { - use super::*; - use crate::subcall_test::SUBCALL_TEST_ADDRESS; - use alloy_primitives::{address, keccak256, Bytes, U256}; - use alloy_sol_types::SolType; - use arc_execution_config::chainspec::LOCAL_DEV; - use revm::context_interface::result::ExecutionResult; - use revm::database::InMemoryDB; - use revm::state::{AccountInfo, Bytecode}; - use revm_primitives::TxKind; - - // ----- EVM bytecode helpers ----- - - use revm::bytecode::opcode::*; - - /// Appends the return-returndata epilogue: copy all returndata to memory[0..] and RETURN. - fn append_return_returndata(code: &mut Vec) { - #[rustfmt::skip] - code.extend_from_slice(&[ - // RETURNDATACOPY(destOffset=0, srcOffset=0, size=RETURNDATASIZE) - RETURNDATASIZE, - PUSH1, 0x00, // srcOffset=0 - PUSH1, 0x00, // destOffset=0 - RETURNDATACOPY, - // RETURN(offset=0, size=RETURNDATASIZE) - RETURNDATASIZE, - PUSH1, 0x00, // offset=0 - RETURN, - ]); - } - - /// Returns EVM bytecode that reads calldata[0..32], multiplies by 2, and returns it. - fn echo_double_bytecode() -> Bytes { - #[rustfmt::skip] - let code = vec![ - PUSH1, 0x00, // offset 0 - CALLDATALOAD, // stack: calldata[0..32] - PUSH1, 0x02, // multiplier - MUL, // stack: value * 2 - PUSH1, 0x00, // memory offset - MSTORE, // memory[0..32] = result - PUSH1, 0x20, // return size = 32 - PUSH1, 0x00, // return offset = 0 - RETURN, - ]; - Bytes::from(code) - } - - /// Returns EVM bytecode that always reverts with empty data. - fn reverting_bytecode() -> Bytes { - #[rustfmt::skip] - let code = vec![ - PUSH1, 0x00, - PUSH1, 0x00, - REVERT, - ]; - Bytes::from(code) - } - - /// Returns EVM bytecode that always reverts with the given 4-byte payload. - fn reverting_with_payload_bytecode(payload: [u8; 4]) -> Bytes { - let mut code = vec![PUSH4]; - code.extend_from_slice(&payload); - #[rustfmt::skip] - code.extend_from_slice(&[ - PUSH1, 0x00, - MSTORE, - PUSH1, 0x04, - PUSH1, 0x1c, - REVERT, - ]); - Bytes::from(code) - } - - /// Returns EVM bytecode that returns msg.sender as a 32-byte word. - fn return_caller_bytecode() -> Bytes { - #[rustfmt::skip] - let code = vec![ - CALLER, // stack: msg.sender - PUSH1, 0x00, // memory offset - MSTORE, // memory[0..32] = msg.sender - PUSH1, 0x20, // return size = 32 - PUSH1, 0x00, // return offset = 0 - RETURN, - ]; - Bytes::from(code) - } - - /// Returns EVM bytecode that forwards all calldata to `target` via CALL, then - /// returns the returndata unchanged. - fn wrapper_call_bytecode(target: Address) -> Bytes { - #[rustfmt::skip] - let mut code = vec![ - // CALLDATACOPY(destOffset=0, srcOffset=0, size=CALLDATASIZE) - CALLDATASIZE, - PUSH1, 0x00, // srcOffset=0 - PUSH1, 0x00, // destOffset=0 - CALLDATACOPY, - // CALL(gas, target, value=0, argsOff=0, argsLen=CALLDATASIZE, retOff=0, retLen=0) - PUSH1, 0x00, // retLen=0 - PUSH1, 0x00, // retOffset=0 - CALLDATASIZE, // argsLen - PUSH1, 0x00, // argsOffset=0 - PUSH1, 0x00, // value=0 - PUSH20, // target address follows - ]; - code.extend_from_slice(target.as_slice()); - code.extend_from_slice(&[GAS, CALL, POP]); - append_return_returndata(&mut code); - Bytes::from(code) - } - - /// Returns EVM bytecode that forwards all calldata to `target` via STATICCALL, - /// then returns the returndata unchanged. - fn static_call_wrapper_bytecode(target: Address) -> Bytes { - #[rustfmt::skip] - let mut code = vec![ - // CALLDATACOPY(destOffset=0, srcOffset=0, size=CALLDATASIZE) - CALLDATASIZE, - PUSH1, 0x00, // srcOffset=0 - PUSH1, 0x00, // destOffset=0 - CALLDATACOPY, - // STATICCALL(gas, target, argsOff=0, argsLen=CALLDATASIZE, retOff=0, retLen=0) - PUSH1, 0x00, // retLen=0 - PUSH1, 0x00, // retOffset=0 - CALLDATASIZE, // argsLen - PUSH1, 0x00, // argsOffset=0 - PUSH20, // target address follows - ]; - code.extend_from_slice(target.as_slice()); - code.extend_from_slice(&[GAS, STATICCALL, POP]); - append_return_returndata(&mut code); - Bytes::from(code) - } - - // ----- Encoding helpers ----- - - /// ABI-encodes `(address target, bytes calldata, bytes memo)` for the subcall test - /// precompile. - fn encode_subcall_test_input(target: Address, calldata: &[u8]) -> Bytes { - type Input = ( - alloy_sol_types::sol_data::Address, - alloy_sol_types::sol_data::Bytes, - ); - Bytes::from(Input::abi_encode(&(target, calldata.to_vec()))) - } - - /// Decodes an `Error(string)` revert reason from raw bytes. - /// Returns the contained message string, or panics if the format doesn't match. - fn decode_revert_reason(data: &[u8]) -> String { - const REVERT_SELECTOR: [u8; 4] = [0x08, 0xc3, 0x79, 0xa0]; - assert!( - data.len() >= 4 && data[..4] == REVERT_SELECTOR, - "expected Error(string) revert selector, got {data:?}" - ); - ::abi_decode(&data[4..]) - .expect("should decode revert reason string") - } - - // ----- Setup helpers ----- - - type NoOpTestEvm = ArcEvm< - EthEvmContext, - revm::inspector::NoOpInspector, - EthInstructions>, - PrecompilesMap, - >; - - /// Creates an ArcEvm backed by an in-memory DB with accounts and deployed contracts. - /// - /// `accounts`: list of `(address, balance)` pairs for EOAs. - /// `contracts`: list of `(address, bytecode)` pairs for contract accounts. - /// Creates an ArcEvm with both SubcallTestPrecompile (unrestricted) and - /// CallFromPrecompile (with the given allowlist) registered. - fn setup_test_evm( - accounts: &[(Address, U256)], - contracts: &[(Address, Bytes)], - call_from_allowlist: &[Address], - ) -> NoOpTestEvm { - use crate::subcall::AllowedCallers; - use crate::subcall_test::{SubcallTestPrecompile, SUBCALL_TEST_ADDRESS}; - - let chain_spec = LOCAL_DEV.clone(); - let mut db = InMemoryDB::default(); - - for (addr, balance) in accounts { - db.insert_account_info( - *addr, - AccountInfo { - balance: *balance, - nonce: 0, - code_hash: alloy_primitives::KECCAK256_EMPTY, - code: None, - account_id: None, - }, - ); - } - - for (addr, code) in contracts { - db.insert_account_info( - *addr, - AccountInfo { - balance: U256::ZERO, - nonce: 1, - code_hash: keccak256(code), - code: Some(Bytecode::new_raw(code.clone())), - account_id: None, - }, - ); - } - - let spec = - reth_ethereum::evm::revm_spec_by_timestamp_and_block_number(&chain_spec, 0, 0); - let hardfork_flags = chain_spec.get_hardfork_flags(0, 0); - let mut cfg_env = revm::context::CfgEnv::new() - .with_chain_id(chain_spec.chain_id()) - .with_spec_and_mainnet_gas_params(spec); - // Disable EIP-7825 tx gas limit cap so tests can use arbitrary gas limits. - cfg_env.tx_gas_limit_cap = Some(u64::MAX); - let ctx = EthEvmContext::new(db, spec) - .with_cfg(cfg_env) - .with_block(BlockEnv::default()); - let precompiles = ArcPrecompileProvider::create_precompiles_map(spec, hardfork_flags); - let mut instruction = EthInstructions::new_mainnet_with_spec(spec); - if hardfork_flags.is_active(ArcHardfork::Zero7) { - instruction.insert_instruction( - SELFDESTRUCT, - revm_interpreter::Instruction::new(arc_network_selfdestruct_zero7, 5000), - ); - } else if hardfork_flags.is_active(ArcHardfork::Zero5) { - instruction.insert_instruction( - SELFDESTRUCT, - revm_interpreter::Instruction::new(arc_network_selfdestruct_zero5, 5000), - ); - } - - let mut registry = SubcallRegistry::new(); - registry.register( - SUBCALL_TEST_ADDRESS, - Arc::new(SubcallTestPrecompile), - AllowedCallers::Unrestricted, - ); - let allowed_callers = - AllowedCallers::Only(HashSet::from_iter(call_from_allowlist.iter().copied())); - registry.register( - CALL_FROM_ADDRESS, - Arc::new(CallFromPrecompile), - allowed_callers, - ); - - ArcEvm::new( - ctx, - revm::inspector::NoOpInspector {}, - precompiles, - instruction, - false, - hardfork_flags, - Arc::new(registry), - ) - } - - fn insert_eip7702_account(evm: &mut NoOpTestEvm, address: Address, delegate: Address) { - use revm::bytecode::eip7702::Eip7702Bytecode; - - let eip7702_code = Bytecode::Eip7702(Arc::new(Eip7702Bytecode::new(delegate))); - evm.inner.ctx.journal_mut().db_mut().insert_account_info( - address, - AccountInfo { - balance: U256::ZERO, - nonce: 1, - code_hash: keccak256(eip7702_code.bytes_slice()), - code: Some(eip7702_code), - account_id: None, - }, - ); - } - - // ----- Test addresses ----- - - const EOA: Address = address!("e000000000000000000000000000000000000001"); - const WRAPPER: Address = address!("c000000000000000000000000000000000000001"); - const ECHO_CONTRACT: Address = address!("c000000000000000000000000000000000000002"); - const REVERT_CONTRACT: Address = address!("c000000000000000000000000000000000000003"); - const CALLER_CONTRACT: Address = address!("c000000000000000000000000000000000000004"); - const WRAPPER_INNER: Address = address!("c000000000000000000000000000000000000005"); - const SPOOFED_SENDER: Address = address!("a000000000000000000000000000000000000001"); - - // ----- Integration tests ----- - - /// EOA → wrapper → subcall_test_precompile → echo_double(42) - /// Asserts: success, output contains echo_double(42) = 84. - #[test] - fn test_subcall_happy_path() { - let wrapper_code = wrapper_call_bytecode(SUBCALL_TEST_ADDRESS); - let echo_code = echo_double_bytecode(); - - let mut evm = setup_test_evm( - &[(EOA, U256::from(1_000_000))], - &[(WRAPPER, wrapper_code), (ECHO_CONTRACT, echo_code)], - &[], - ); - - let inner_calldata = U256::from(42).to_be_bytes::<32>().to_vec(); - let subcall_input = encode_subcall_test_input(ECHO_CONTRACT, &inner_calldata); - - let tx = TxEnv { - caller: EOA, - kind: TxKind::Call(WRAPPER), - value: U256::ZERO, - gas_limit: 1_000_000, - gas_price: 0, - chain_id: Some(LOCAL_DEV.chain_id()), - data: subcall_input, - ..Default::default() - }; - - let result = evm.transact_one(tx).expect("transact_one should succeed"); - match &result { - ExecutionResult::Success { output, .. } => { - let data = output.data(); - assert!(!data.is_empty(), "should have non-empty output"); - - // The test precompile ABI-encodes the child output as `bytes`. - // Decode the outer `bytes` wrapper to get the raw child return data. - let child_output = - ::abi_decode(data) - .expect("should decode bytes wrapper"); - // echo_double(42) → 84 - let expected = U256::from(84).to_be_bytes::<32>(); - assert_eq!(child_output.as_ref(), &expected, "result mismatch"); - } - other => panic!("expected Success, got {other:?}"), - }; - } - - /// EOA → wrapper → subcall_test_precompile → reverting contract - /// The wrapper catches the revert, so the top-level tx succeeds. - /// The wrapper's returndata is the precompile's revert output (empty). - #[test] - fn test_subcall_child_reverts() { - let wrapper_code = wrapper_call_bytecode(SUBCALL_TEST_ADDRESS); - let revert_code = reverting_bytecode(); - - let mut evm = setup_test_evm( - &[(EOA, U256::from(1_000_000))], - &[(WRAPPER, wrapper_code), (REVERT_CONTRACT, revert_code)], - &[], - ); - - let subcall_input = encode_subcall_test_input(REVERT_CONTRACT, &[]); - - let tx = TxEnv { - caller: EOA, - kind: TxKind::Call(WRAPPER), - value: U256::ZERO, - gas_limit: 1_000_000, - gas_price: 0, - chain_id: Some(LOCAL_DEV.chain_id()), - data: subcall_input, - ..Default::default() - }; - - let result = evm.transact_one(tx).expect("transact_one should succeed"); - match &result { - // The wrapper forwards returndata from the precompile. When the child - // reverts, the test precompile signals failure (success=false) with - // empty output, so the wrapper sees a REVERT with empty data. - ExecutionResult::Success { output, .. } => { - assert!( - output.data().is_empty(), - "wrapper should return empty data on child revert" - ); - } - other => panic!("expected Success (wrapper catches revert), got {other:?}"), - }; - } - - /// EOA → wrapper → subcall_test_precompile → return_caller contract - /// Asserts: the child sees msg.sender as the wrapper address (not the precompile). - #[test] - fn test_subcall_caller_is_wrapper_not_precompile() { - let wrapper_code = wrapper_call_bytecode(SUBCALL_TEST_ADDRESS); - let caller_code = return_caller_bytecode(); - - let mut evm = setup_test_evm( - &[(EOA, U256::from(1_000_000))], - &[(WRAPPER, wrapper_code), (CALLER_CONTRACT, caller_code)], - &[], - ); - - let subcall_input = encode_subcall_test_input(CALLER_CONTRACT, &[]); - - let tx = TxEnv { - caller: EOA, - kind: TxKind::Call(WRAPPER), - value: U256::ZERO, - gas_limit: 1_000_000, - gas_price: 0, - chain_id: Some(LOCAL_DEV.chain_id()), - data: subcall_input, - ..Default::default() - }; - - let result = evm.transact_one(tx).expect("transact_one should succeed"); - match &result { - ExecutionResult::Success { output, .. } => { - // The test precompile ABI-encodes the child output as `bytes`. - // The child (return_caller) returns msg.sender left-padded to 32 bytes. - // The subcall test precompile passes `caller = inputs.caller` to the - // child, so msg.sender should be the WRAPPER. - let child_output = - ::abi_decode(output.data()) - .expect("should decode bytes wrapper"); - let returned_address = Address::from_slice(&child_output[12..32]); - assert_eq!( - returned_address, WRAPPER, - "child should see msg.sender = wrapper, not the precompile address" - ); - } - other => panic!("expected Success, got {other:?}"), - }; - } - - /// EOA → subcall_test_precompile directly (no wrapper) - /// Asserts: success, output contains echo_double(21) = 42. - #[test] - fn test_subcall_direct_eoa_call() { - let echo_code = echo_double_bytecode(); - - let mut evm = setup_test_evm( - &[(EOA, U256::from(1_000_000))], - &[(ECHO_CONTRACT, echo_code)], - &[], - ); - - let inner_calldata = U256::from(21).to_be_bytes::<32>().to_vec(); - let subcall_input = encode_subcall_test_input(ECHO_CONTRACT, &inner_calldata); - - let tx = TxEnv { - caller: EOA, - kind: TxKind::Call(SUBCALL_TEST_ADDRESS), - value: U256::ZERO, - gas_limit: 1_000_000, - gas_price: 0, - chain_id: Some(LOCAL_DEV.chain_id()), - data: subcall_input, - ..Default::default() - }; - - let result = evm.transact_one(tx).expect("transact_one should succeed"); - match &result { - ExecutionResult::Success { output, .. } => { - let child_output = - ::abi_decode(output.data()) - .expect("should decode bytes wrapper"); - // echo_double(21) → 42 - let expected = U256::from(42).to_be_bytes::<32>(); - assert_eq!(child_output.as_ref(), &expected); - } - other => panic!("expected Success, got {other:?}"), - }; - } - - /// EOA → wrapper_outer → subcall_test → wrapper_inner → subcall_test → echo_double(42) - /// Asserts: the inner echo_double(42) = 84 result propagates through both subcall - /// layers, verifying correct frame-stack ordering for nested subcalls. - #[test] - fn test_subcall_reentrant() { - let echo_code = echo_double_bytecode(); - // wrapper_inner calls subcall_test_precompile - let wrapper_inner_code = wrapper_call_bytecode(SUBCALL_TEST_ADDRESS); - // wrapper_outer also calls subcall_test_precompile - let wrapper_outer_code = wrapper_call_bytecode(SUBCALL_TEST_ADDRESS); - - let mut evm = setup_test_evm( - &[(EOA, U256::from(1_000_000))], - &[ - (WRAPPER, wrapper_outer_code), - (WRAPPER_INNER, wrapper_inner_code), - (ECHO_CONTRACT, echo_code), - ], - &[], - ); - - // Inner subcall: wrapper_inner → subcall_test → echo_double(42) - let inner_calldata = U256::from(42).to_be_bytes::<32>().to_vec(); - let inner_subcall_input = encode_subcall_test_input(ECHO_CONTRACT, &inner_calldata); - - // Outer subcall: wrapper_outer → subcall_test → wrapper_inner(inner_subcall_input) - let outer_subcall_input = - encode_subcall_test_input(WRAPPER_INNER, inner_subcall_input.as_ref()); - - let tx = TxEnv { - caller: EOA, - kind: TxKind::Call(WRAPPER), - value: U256::ZERO, - gas_limit: 5_000_000, - gas_price: 0, - chain_id: Some(LOCAL_DEV.chain_id()), - data: outer_subcall_input, - ..Default::default() - }; - - let result = evm.transact_one(tx).expect("transact_one should succeed"); - match &result { - ExecutionResult::Success { output, .. } => { - // The return chain is: - // echo_double(42) → raw 84 (32 bytes) - // inner subcall_test → ABI-encode as `bytes` (wrapper around raw 84) - // wrapper_inner → forwards inner subcall_test output as-is - // outer subcall_test → ABI-encode as `bytes` (wrapper around inner output) - // wrapper_outer → forwards outer subcall_test output as-is - // - // Peel the outer `bytes` wrapper: - let inner_output = - ::abi_decode(output.data()) - .expect("should decode outer bytes wrapper"); - // Peel the inner `bytes` wrapper: - let echo_result = - ::abi_decode(&inner_output) - .expect("should decode inner bytes wrapper"); - // echo_double(42) → 84 - let expected = U256::from(84).to_be_bytes::<32>(); - assert_eq!( - echo_result.as_ref(), - &expected, - "inner echo_double(42) should propagate through nested subcalls as 84" - ); - } - other => panic!("expected Success, got {other:?}"), - }; - } - - /// AllowedCallers enforcement: if the caller is not in the allowed set, the call - /// should revert. - #[test] - fn test_subcall_unauthorized_caller_rejected() { - use crate::subcall::AllowedCallers; - use std::collections::HashSet; - - let wrapper_code = wrapper_call_bytecode(SUBCALL_TEST_ADDRESS); - let echo_code = echo_double_bytecode(); - - // Build a custom EVM where the test precompile only allows a specific address - let authorized_caller = address!("a000000000000000000000000000000000000099"); - let chain_spec = LOCAL_DEV.clone(); - let mut db = InMemoryDB::default(); - - // Insert accounts - db.insert_account_info( - EOA, - AccountInfo { - balance: U256::from(1_000_000u64), - nonce: 0, - code_hash: alloy_primitives::KECCAK256_EMPTY, - code: None, - account_id: None, - }, - ); - for (addr, code) in [(WRAPPER, &wrapper_code), (ECHO_CONTRACT, &echo_code)] { - db.insert_account_info( - addr, - AccountInfo { - balance: U256::ZERO, - nonce: 1, - code_hash: keccak256(code), - code: Some(Bytecode::new_raw(code.clone())), - account_id: None, - }, - ); - } - - let spec = - reth_ethereum::evm::revm_spec_by_timestamp_and_block_number(&chain_spec, 0, 0); - let hardfork_flags = chain_spec.get_hardfork_flags(0, 0); - let mut cfg_env = revm::context::CfgEnv::new() - .with_chain_id(chain_spec.chain_id()) - .with_spec_and_mainnet_gas_params(spec); - // Disable EIP-7825 tx gas limit cap so tests can use arbitrary gas limits. - cfg_env.tx_gas_limit_cap = Some(u64::MAX); - let ctx = EthEvmContext::new(db, spec) - .with_cfg(cfg_env) - .with_block(BlockEnv::default()); - let precompiles = ArcPrecompileProvider::create_precompiles_map(spec, hardfork_flags); - let mut instruction = EthInstructions::new_mainnet_with_spec(spec); - if hardfork_flags.is_active(ArcHardfork::Zero7) { - instruction.insert_instruction( - SELFDESTRUCT, - revm_interpreter::Instruction::new(arc_network_selfdestruct_zero7, 5000), - ); - } else if hardfork_flags.is_active(ArcHardfork::Zero5) { - instruction.insert_instruction( - SELFDESTRUCT, - revm_interpreter::Instruction::new(arc_network_selfdestruct_zero5, 5000), - ); - } - - // Custom registry: restrict subcall_test to `authorized_caller` only - let mut registry = SubcallRegistry::new(); - registry.register( - SUBCALL_TEST_ADDRESS, - Arc::new(crate::subcall_test::SubcallTestPrecompile), - AllowedCallers::Only(HashSet::from([authorized_caller])), - ); - - let mut evm = ArcEvm::new( - ctx, - revm::inspector::NoOpInspector {}, - precompiles, - instruction, - false, - hardfork_flags, - Arc::new(registry), - ); - - let inner_calldata = U256::from(42).to_be_bytes::<32>().to_vec(); - let subcall_input = encode_subcall_test_input(ECHO_CONTRACT, &inner_calldata); - - let tx = TxEnv { - caller: EOA, - kind: TxKind::Call(WRAPPER), - value: U256::ZERO, - gas_limit: 1_000_000, - gas_price: 0, - chain_id: Some(LOCAL_DEV.chain_id()), - data: subcall_input, - ..Default::default() - }; - - let result = evm.transact_one(tx).expect("transact_one should succeed"); - match &result { - // The wrapper catches the precompile's revert, so the tx succeeds. - // The returndata is the revert output (Error(string) ABI-encoded). - ExecutionResult::Success { output, .. } => { - let reason = decode_revert_reason(output.data()); - assert_eq!(reason, "unauthorized caller"); - } - other => panic!("expected Success (wrapper catches revert), got {other:?}"), - }; - } - - /// Subcall precompiles must reject static-context invocations. - #[test] - fn test_subcall_static_context_rejected() { - let static_wrapper_code = static_call_wrapper_bytecode(SUBCALL_TEST_ADDRESS); - let echo_code = echo_double_bytecode(); - - let mut evm = setup_test_evm( - &[(EOA, U256::from(1_000_000))], - &[(WRAPPER, static_wrapper_code), (ECHO_CONTRACT, echo_code)], - &[], - ); - - let inner_calldata = U256::from(42).to_be_bytes::<32>().to_vec(); - let subcall_input = encode_subcall_test_input(ECHO_CONTRACT, &inner_calldata); - - let tx = TxEnv { - caller: EOA, - kind: TxKind::Call(WRAPPER), - value: U256::ZERO, - gas_limit: 1_000_000, - gas_price: 0, - chain_id: Some(LOCAL_DEV.chain_id()), - data: subcall_input, - ..Default::default() - }; - - let result = evm.transact_one(tx).expect("transact_one should succeed"); - match &result { - // The wrapper catches the precompile's revert, so the tx succeeds. - // The returndata is the revert output (Error(string) ABI-encoded). - // STATICCALL sets scheme=StaticCall, so the scheme check fires before - // the is_static check. - ExecutionResult::Success { output, .. } => { - let reason = decode_revert_reason(output.data()); - assert_eq!(reason, "subcall precompiles only support CALL scheme"); - } - other => panic!("expected Success (wrapper catches revert), got {other:?}"), - }; - } - - // ================================================================ - // CallFrom precompile tests - // ================================================================ - - /// ABI-encodes the raw parameters for `callFrom(address sender, address target, bytes data)`. - /// Uses the sol!-generated type to ensure encoding matches `abi_decode`. - fn encode_call_from_input(sender: Address, target: Address, data: &[u8]) -> Bytes { - use alloy_sol_types::SolCall; - use arc_precompiles::call_from::ICallFrom; - let call = ICallFrom::callFromCall { - sender, - target, - data: data.to_vec().into(), - }; - // abi_encode includes the 4-byte function selector, matching standard - // Solidity call syntax and what abi_decode expects. - Bytes::from(call.abi_encode()) - } - - /// Decodes ABI-encoded `(bool success, bytes returnData)` from CallFrom output. - fn decode_call_from_output(data: &[u8]) -> (bool, Vec) { - use alloy_sol_types::SolCall; - use arc_precompiles::call_from::ICallFrom; - let ret = ICallFrom::callFromCall::abi_decode_returns(data) - .expect("failed to ABI-decode callFrom return"); - (ret.success, ret.returnData.to_vec()) - } - - /// Returns EVM bytecode that forwards all calldata to `target` via CALL, - /// forwarding the received CALLVALUE, then returns the returndata unchanged. - fn wrapper_call_with_value_bytecode(target: Address) -> Bytes { - #[rustfmt::skip] - let mut code = vec![ - // CALLDATACOPY(destOffset=0, srcOffset=0, size=CALLDATASIZE) - CALLDATASIZE, - PUSH1, 0x00, // srcOffset=0 - PUSH1, 0x00, // destOffset=0 - CALLDATACOPY, - // CALL(gas, target, value=CALLVALUE, argsOff=0, argsLen=CALLDATASIZE, retOff=0, retLen=0) - PUSH1, 0x00, // retLen=0 - PUSH1, 0x00, // retOffset=0 - CALLDATASIZE, // argsLen - PUSH1, 0x00, // argsOffset=0 - CALLVALUE, // value - PUSH20, // target address follows - ]; - code.extend_from_slice(target.as_slice()); - code.extend_from_slice(&[GAS, CALL, POP]); - append_return_returndata(&mut code); - Bytes::from(code) - } - - /// Returns EVM bytecode that forwards all calldata to `target` via DELEGATECALL, - /// then returns the returndata unchanged. - fn delegatecall_wrapper_bytecode(target: Address) -> Bytes { - #[rustfmt::skip] - let mut code = vec![ - // CALLDATACOPY(destOffset=0, srcOffset=0, size=CALLDATASIZE) - CALLDATASIZE, - PUSH1, 0x00, // srcOffset=0 - PUSH1, 0x00, // destOffset=0 - CALLDATACOPY, - // DELEGATECALL(gas, target, argsOff=0, argsLen=CALLDATASIZE, retOff=0, retLen=0) - PUSH1, 0x00, // retLen=0 - PUSH1, 0x00, // retOffset=0 - CALLDATASIZE, // argsLen - PUSH1, 0x00, // argsOffset=0 - PUSH20, // target address follows - ]; - code.extend_from_slice(target.as_slice()); - code.extend_from_slice(&[GAS, DELEGATECALL, POP]); - append_return_returndata(&mut code); - Bytes::from(code) - } - - /// Contract A (allowlisted) calls CallFrom(sender=EOA, target=B, data). - /// B returns successfully → A receives B's return data. - #[test] - fn test_call_from_happy_path() { - let contract_a_code = wrapper_call_bytecode(CALL_FROM_ADDRESS); - let contract_b_code = echo_double_bytecode(); - const CONTRACT_A: Address = WRAPPER; - const CONTRACT_B: Address = ECHO_CONTRACT; - - let mut evm = setup_test_evm( - &[(EOA, U256::from(1_000_000))], - &[(CONTRACT_A, contract_a_code), (CONTRACT_B, contract_b_code)], - &[CONTRACT_A], - ); - - // A calls CallFrom(sender=EOA, target=B, data=abi(42)) - // The wrapper forwards its own msg.sender (EOA) as the sender parameter. - let inner_calldata = U256::from(42).to_be_bytes::<32>().to_vec(); - let call_from_input = encode_call_from_input(EOA, CONTRACT_B, &inner_calldata); - - let tx = TxEnv { - caller: EOA, - kind: TxKind::Call(CONTRACT_A), - value: U256::ZERO, - gas_limit: 1_000_000, - gas_price: 0, - chain_id: Some(LOCAL_DEV.chain_id()), - data: call_from_input, - ..Default::default() - }; - - let result = evm.transact_one(tx).expect("transact_one should succeed"); - match &result { - ExecutionResult::Success { output, .. } => { - let (success, return_data) = decode_call_from_output(output.data()); - assert!(success, "callFrom should report child success"); - let expected = U256::from(84).to_be_bytes::<32>(); - assert_eq!( - return_data, expected, - "returnData should be echo_double(42) = 84" - ); - } - other => panic!("expected Success, got {other:?}"), - } - } - - /// Verify that the child frame's gas consumption is propagated to the parent. - /// - /// Compares gas_used for two transactions through the same wrapper: - /// 1. callFrom → echo_double (trivial work, ~20 gas in child) - /// 2. callFrom → gas_burner (infinite loop, consumes all child gas) - /// - /// Both targets are pre-deployed contracts (same cold-access cost pattern). - /// If child gas is properly propagated, the gas_burner transaction must use - /// substantially more gas. If gas were NOT propagated (the old bug), both - /// would show nearly identical gas_used. - #[test] - fn test_call_from_gas_propagated() { - let contract_a_code = wrapper_call_bytecode(CALL_FROM_ADDRESS); - let contract_b_code = echo_double_bytecode(); - let gas_burner_code = gas_burner_bytecode(); - const CONTRACT_A: Address = WRAPPER; - const CONTRACT_B: Address = ECHO_CONTRACT; - const GAS_BURNER: Address = address!("c000000000000000000000000000000000000006"); - - let mut evm = setup_test_evm( - &[(EOA, U256::from(1_000_000))], - &[ - (CONTRACT_A, contract_a_code), - (CONTRACT_B, contract_b_code), - (GAS_BURNER, gas_burner_code), - ], - &[CONTRACT_A], - ); - - // Transaction 1: callFrom targeting echo_double (trivial child work) - let inner_calldata = U256::from(42).to_be_bytes::<32>().to_vec(); - let call_from_input_echo = encode_call_from_input(EOA, CONTRACT_B, &inner_calldata); - - let tx_echo = TxEnv { - caller: EOA, - kind: TxKind::Call(CONTRACT_A), - value: U256::ZERO, - gas_limit: 100_000, - gas_price: 0, - chain_id: Some(LOCAL_DEV.chain_id()), - data: call_from_input_echo, - ..Default::default() - }; - - let result_echo = evm - .transact_one(tx_echo) - .expect("transact_one should succeed"); - let gas_used_echo = match &result_echo { - ExecutionResult::Success { gas_used, .. } => *gas_used, - other => panic!("expected Success, got {other:?}"), - }; - - // Transaction 2: callFrom targeting gas_burner (infinite loop, consumes all gas) - let call_from_input_burner = encode_call_from_input(EOA, GAS_BURNER, &[]); - - let tx_burner = TxEnv { - caller: EOA, - kind: TxKind::Call(CONTRACT_A), - value: U256::ZERO, - gas_limit: 100_000, - gas_price: 0, - nonce: 1, - chain_id: Some(LOCAL_DEV.chain_id()), - data: call_from_input_burner, - ..Default::default() - }; - - let result_burner = evm - .transact_one(tx_burner) - .expect("transact_one should succeed"); - let gas_used_burner = match &result_burner { - ExecutionResult::Success { gas_used, .. } => *gas_used, - other => panic!("expected Success, got {other:?}"), - }; - - // The gas_burner call should use substantially more gas than echo_double. - // With 100k gas_limit, the burner should consume nearly all of it (~90k+), - // while echo_double uses only ~26k (base tx + wrapper + calldata + trivial child). - assert!( - gas_used_burner > gas_used_echo * 2, - "callFrom to gas_burner ({gas_used_burner}) should use much more gas \ - than callFrom to echo_double ({gas_used_echo})" - ); - } - - /// Contract A (allowlisted) calls CallFrom(sender=EOA, target=B, data). - /// B reverts → CallFrom propagates the revert, wrapper catches it. - #[test] - fn test_call_from_child_reverts() { - let contract_a_code = wrapper_call_bytecode(CALL_FROM_ADDRESS); - let contract_b_code = reverting_bytecode(); - const CONTRACT_A: Address = WRAPPER; - const CONTRACT_B: Address = REVERT_CONTRACT; - - let mut evm = setup_test_evm( - &[(EOA, U256::from(1_000_000))], - &[(CONTRACT_A, contract_a_code), (CONTRACT_B, contract_b_code)], - &[CONTRACT_A], - ); - - let call_from_input = encode_call_from_input(EOA, CONTRACT_B, &[]); - - let tx = TxEnv { - caller: EOA, - kind: TxKind::Call(CONTRACT_A), - value: U256::ZERO, - gas_limit: 1_000_000, - gas_price: 0, - chain_id: Some(LOCAL_DEV.chain_id()), - data: call_from_input, - ..Default::default() - }; - - // CallFrom always returns successfully with ABI-encoded (bool, bytes). - // The child reverted, so success=false and returnData contains the revert output. - let result = evm.transact_one(tx).expect("transact_one should succeed"); - match &result { - ExecutionResult::Success { output, .. } => { - let (success, _return_data) = decode_call_from_output(output.data()); - assert!(!success, "callFrom should report child failure"); - } - other => panic!("expected Success, got {other:?}"), - } - } - - /// EOA directly calls CallFrom precompile. Since the default registry has an empty - /// allowlist for CallFrom, this should revert. - #[test] - fn test_call_from_unauthorized_caller_rejected() { - let echo_code = echo_double_bytecode(); - const CONTRACT_B: Address = ECHO_CONTRACT; - - // Empty allowlist — no one can call CallFrom - let mut evm = setup_test_evm( - &[(EOA, U256::from(1_000_000))], - &[(CONTRACT_B, echo_code)], - &[], - ); - - let inner_calldata = U256::from(42).to_be_bytes::<32>().to_vec(); - let call_from_input = encode_call_from_input(EOA, CONTRACT_B, &inner_calldata); - - let tx = TxEnv { - caller: EOA, - kind: TxKind::Call(CALL_FROM_ADDRESS), - value: U256::ZERO, - gas_limit: 1_000_000, - gas_price: 0, - chain_id: Some(LOCAL_DEV.chain_id()), - data: call_from_input, - ..Default::default() - }; - - let result = evm.transact_one(tx).expect("transact_one should succeed"); - assert!( - matches!(result, ExecutionResult::Revert { .. }), - "direct call from non-allowlisted address should revert" - ); - } - - /// DELEGATECALL to CallFrom should revert — only CALL scheme is supported. - #[test] - fn test_call_from_delegatecall_rejected() { - let contract_a_code = delegatecall_wrapper_bytecode(CALL_FROM_ADDRESS); - let contract_b_code = echo_double_bytecode(); - const CONTRACT_A: Address = WRAPPER; - const CONTRACT_B: Address = ECHO_CONTRACT; - - let mut evm = setup_test_evm( - &[(EOA, U256::from(1_000_000))], - &[(CONTRACT_A, contract_a_code), (CONTRACT_B, contract_b_code)], - &[CONTRACT_A], - ); - - let inner_calldata = U256::from(42).to_be_bytes::<32>().to_vec(); - let call_from_input = encode_call_from_input(EOA, CONTRACT_B, &inner_calldata); - - let tx = TxEnv { - caller: EOA, - kind: TxKind::Call(CONTRACT_A), - value: U256::ZERO, - gas_limit: 1_000_000, - gas_price: 0, - chain_id: Some(LOCAL_DEV.chain_id()), - data: call_from_input, - ..Default::default() - }; - - // The wrapper uses DELEGATECALL → CallFrom. The scheme check rejects it with a - // revert. The wrapper catches the revert and returns the revert data. - let result = evm.transact_one(tx).expect("transact_one should succeed"); - match &result { - ExecutionResult::Success { output, .. } => { - // The output is revert data from the scheme rejection. - // If echo_double had actually run, output would contain value 84. - let echo_result = U256::from(84).to_be_bytes::<32>(); - assert_ne!( - output.data().as_ref(), - &echo_result, - "DELEGATECALL should not have reached the child contract" - ); - } - other => panic!("expected Success (wrapper catches revert), got {other:?}"), - } - } - - /// Contract A (allowlisted) calls CallFrom with value attached. - /// The framework rejects this with a revert. Asserts no value is lost: - /// EOA balance unchanged (minus nothing — gas_price=0), A and B at zero. - #[test] - fn test_call_from_value_rejected() { - let contract_a_code = wrapper_call_with_value_bytecode(CALL_FROM_ADDRESS); - let contract_b_code = echo_double_bytecode(); - const CONTRACT_A: Address = WRAPPER; - const CONTRACT_B: Address = ECHO_CONTRACT; - let transfer_value = U256::from(1_000); - let eoa_balance = U256::from(100_000); - - let mut evm = setup_test_evm( - &[(EOA, eoa_balance)], - &[(CONTRACT_A, contract_a_code), (CONTRACT_B, contract_b_code)], - &[CONTRACT_A], - ); - - let inner_calldata = U256::from(7).to_be_bytes::<32>().to_vec(); - let call_from_input = encode_call_from_input(EOA, CONTRACT_B, &inner_calldata); - - evm.inner.ctx.set_tx(TxEnv { - caller: EOA, - kind: TxKind::Call(CONTRACT_A), - value: transfer_value, - gas_limit: 1_000_000, - gas_price: 0, - chain_id: Some(LOCAL_DEV.chain_id()), - data: call_from_input, - ..Default::default() - }); - - let result_and_state = evm.replay().expect("replay should succeed"); - - // The wrapper catches the inner revert via CALL, so the top-level tx succeeds. - // But the child contract should NOT have been reached. - match &result_and_state.result { - ExecutionResult::Success { output, .. } => { - let echo_result = U256::from(14).to_be_bytes::<32>(); - assert_ne!( - output.data().as_ref(), - &echo_result, - "child contract should not have been reached" - ); - } - other => panic!("expected Success (wrapper catches revert), got {other:?}"), - } - - let state = &result_and_state.state; - - // EOA sent value to CONTRACT_A via the outer CALL, but the inner callFrom - // reverted. The wrapper's CALL to the precompile returned 0 (failure), - // but the wrapper itself doesn't revert — it just returns the revert data. - // The outer EOA → CONTRACT_A transfer still happened. - let eoa_account = state.get(&EOA).expect("EOA should be in state"); - assert_eq!( - eoa_account.info.balance, - eoa_balance - transfer_value, - "EOA should be debited by the outer transfer to CONTRACT_A" - ); - - // CONTRACT_A received value from EOA but the inner callFrom reverted, - // so the value stays in CONTRACT_A. - let a_account = state - .get(&CONTRACT_A) - .expect("contract A should be in state"); - assert_eq!( - a_account.info.balance, transfer_value, - "contract A should hold the value (inner call reverted)" - ); - - // CONTRACT_B should not have received anything. - let b_balance = state - .get(&CONTRACT_B) - .map(|a| a.info.balance) - .unwrap_or(U256::ZERO); - assert_eq!(b_balance, U256::ZERO, "contract B should have zero balance"); - } - - /// STATICCALL → INNER → CALL CallFrom propagates `is_static=true` to the inner - /// CALL frame (scheme stays `Call`). The subcall framework must reject it and - /// consume all caller gas, matching upstream `CallNotAllowedInsideStatic` halt - /// semantics and the standard precompile `check_staticcall` helper. - #[test] - fn test_call_from_is_static_propagated_consumes_all_gas() { - let outer_code = static_call_wrapper_bytecode(WRAPPER_INNER); - let inner_code = wrapper_call_bytecode(CALL_FROM_ADDRESS); - let echo_code = echo_double_bytecode(); - - let mut evm = setup_test_evm( - &[(EOA, U256::from(10_000_000))], - &[ - (WRAPPER, outer_code), - (WRAPPER_INNER, inner_code), - (ECHO_CONTRACT, echo_code), - ], - &[WRAPPER_INNER], - ); - - let inner_calldata = U256::from(42).to_be_bytes::<32>().to_vec(); - let call_from_input = encode_call_from_input(EOA, ECHO_CONTRACT, &inner_calldata); - - let gas_limit: u64 = 1_000_000; - let tx = TxEnv { - caller: EOA, - kind: TxKind::Call(WRAPPER), - value: U256::ZERO, - gas_limit, - gas_price: 0, - chain_id: Some(LOCAL_DEV.chain_id()), - data: call_from_input, - ..Default::default() - }; - - let result = evm.transact_one(tx).expect("transact_one should succeed"); - let (output, gas_used) = match &result { - ExecutionResult::Success { - output, gas_used, .. - } => (output, *gas_used), - other => panic!("expected Success (wrapper catches revert), got {other:?}"), - }; - - assert_eq!( - decode_revert_reason(output.data()), - "subcall precompiles cannot be invoked in static context", - ); - - // The inner CALL forwards (almost) all remaining gas to CallFrom; the static - // rejection consumes all of it. EIP-150 retains 1/64 of the caller's gas at - // each CALL/STATICCALL boundary; with two nested boundaries the unavoidable - // upper bound on retained gas is 1/64 + (1/64)·(63/64) ≈ 3.1%, so gas_used - // must exceed ~96.9% of the limit. The 95% threshold leaves a thin buffer - // for wrapper bookkeeping; tightening it on a future cost change is desirable - // — a regression to the old flat-100-gas behavior would drop gas_used by an - // order of magnitude (to ~5% of the limit). - assert!( - gas_used > gas_limit * 95 / 100, - "expected gas_used > 95% of limit (all gas consumed by static rejection); \ - got gas_used={gas_used} of gas_limit={gas_limit}", - ); - } - - /// Nested callFrom targeting the CallFrom precompile address itself. The child frame - /// calls CALL_FROM_ADDRESS via `init_with_context` (not our `frame_init`), so the - /// subcall interception never fires. Since CALL_FROM_ADDRESS has no deployed code, - /// revm returns an immediate empty success. This verifies the framework handles - /// the code-less-address case correctly — the child "succeeds" with empty output, - /// and echo_double is never invoked. - #[test] - fn test_call_from_target_codeless_precompile_address() { - let contract_a_code = wrapper_call_bytecode(CALL_FROM_ADDRESS); - let contract_b_code = echo_double_bytecode(); - const CONTRACT_A: Address = WRAPPER; - const CONTRACT_B: Address = ECHO_CONTRACT; - - let mut evm = setup_test_evm( - &[(EOA, U256::from(1_000_000))], - &[(CONTRACT_A, contract_a_code), (CONTRACT_B, contract_b_code)], - &[CONTRACT_A], - ); - - let inner_calldata = U256::from(7).to_be_bytes::<32>().to_vec(); - let inner_call_from = encode_call_from_input(EOA, CONTRACT_B, &inner_calldata); - - // Outer callFrom targets CALL_FROM_ADDRESS. The child frame calls it via - // init_with_context, which sees no code and returns immediate Stop. - let outer_call_from = encode_call_from_input(EOA, CALL_FROM_ADDRESS, &inner_call_from); - - let tx = TxEnv { - caller: EOA, - kind: TxKind::Call(CONTRACT_A), - value: U256::ZERO, - gas_limit: 1_000_000, - gas_price: 0, - chain_id: Some(LOCAL_DEV.chain_id()), - data: outer_call_from, - ..Default::default() - }; - - // CALL_FROM_ADDRESS has no deployed code → init_with_context returns - // immediate Stop. The outer callFrom returns (true, ). - let result = evm.transact_one(tx).expect("transact_one should succeed"); - match &result { - ExecutionResult::Success { output, .. } => { - let (success, return_data) = decode_call_from_output(output.data()); - assert!( - success, - "outer callFrom should report child success (code-less address)" - ); - assert!( - return_data.is_empty(), - "return data should be empty (CALL_FROM_ADDRESS has no deployed code)" - ); - } - other => panic!("expected Success, got {other:?}"), - } - } - - /// CallFrom targets another subcall precompile address that has genesis stub - /// bytecode (`0x01` = ADD opcode). The child frame bypasses the subcall registry - /// (dispatched via `EthFrame::init_with_context`), executes the stub ADD opcode - /// which stack-underflows, and reverts. The outer callFrom reports child failure. - /// - /// This mirrors production behavior where precompile addresses have `0x01` stub - /// code in genesis. A subcall precompile's `init_subcall` child target must be a - /// regular contract or standard precompile — targeting another subcall precompile - /// address silently reverts via the stub bytecode instead of going through the - /// subcall registry. - #[test] - fn test_call_from_target_subcall_precompile_with_stub_bytecode() { - let contract_a_code = wrapper_call_bytecode(CALL_FROM_ADDRESS); - const CONTRACT_A: Address = WRAPPER; - - // Deploy stub bytecode (0x01 = ADD) at the subcall test precompile address, - // simulating the genesis allocation for custom precompile addresses. - let stub_bytecode = Bytes::from(vec![0x01]); - - let mut evm = setup_test_evm( - &[(EOA, U256::from(1_000_000))], - &[ - (CONTRACT_A, contract_a_code), - (SUBCALL_TEST_ADDRESS, stub_bytecode), - ], - &[CONTRACT_A], - ); - - // callFrom(sender=EOA, target=SUBCALL_TEST_ADDRESS, data=) - let inner_calldata = U256::from(42).to_be_bytes::<32>().to_vec(); - let call_from_input = - encode_call_from_input(EOA, SUBCALL_TEST_ADDRESS, &inner_calldata); - - let tx = TxEnv { - caller: EOA, - kind: TxKind::Call(CONTRACT_A), - value: U256::ZERO, - gas_limit: 1_000_000, - gas_price: 0, - chain_id: Some(LOCAL_DEV.chain_id()), - data: call_from_input, - ..Default::default() - }; - - // The child frame targets SUBCALL_TEST_ADDRESS, but it is dispatched via - // init_with_context (not the subcall registry). It finds the 0x01 stub code, - // executes ADD with an empty stack, stack-underflows, and reverts. - let result = evm.transact_one(tx).expect("transact_one should succeed"); - match &result { - ExecutionResult::Success { output, .. } => { - let (success, _return_data) = decode_call_from_output(output.data()); - assert!( - !success, - "child targeting subcall precompile stub bytecode should revert" - ); - } - other => panic!("expected Success (wrapper catches revert), got {other:?}"), - } - } - - /// Returns EVM bytecode that increments a counter in storage slot 0, then - /// forwards all calldata to `target` via CALL (value=0). On return, copies - /// returndata to memory and returns it. - fn counting_wrapper_bytecode(target: Address) -> Bytes { - #[rustfmt::skip] - let mut code = vec![ - // counter = SLOAD(0) + 1; SSTORE(0, counter) - PUSH1, 0x00, // slot 0 - SLOAD, // load current counter - PUSH1, 0x01, - ADD, // counter + 1 - PUSH1, 0x00, // slot 0 - SSTORE, // store updated counter - - // CALLDATACOPY(destOffset=0, srcOffset=0, size=CALLDATASIZE) - CALLDATASIZE, - PUSH1, 0x00, // srcOffset=0 - PUSH1, 0x00, // destOffset=0 - CALLDATACOPY, - - // CALL(gas, target, value=0, argsOff=0, argsLen=CALLDATASIZE, retOff=0, retLen=0) - PUSH1, 0x00, // retLen=0 - PUSH1, 0x00, // retOffset=0 - CALLDATASIZE, // argsLen - PUSH1, 0x00, // argsOffset=0 - PUSH1, 0x00, // value=0 - PUSH20, // target address follows - ]; - code.extend_from_slice(target.as_slice()); - code.extend_from_slice(&[GAS, CALL, POP]); - append_return_returndata(&mut code); - Bytes::from(code) - } - - /// Indirect recursion: CONTRACT_A → callFrom → CONTRACT_A → callFrom → ... until the - /// EVM call stack depth limit (1024) is hit. CONTRACT_A increments a storage counter - /// on each invocation so we can observe the actual recursion depth. - #[test] - fn test_call_from_recursive_depth_limit() { - let contract_a_code = counting_wrapper_bytecode(CALL_FROM_ADDRESS); - let contract_b_code = echo_double_bytecode(); - const CONTRACT_A: Address = WRAPPER; - const CONTRACT_B: Address = ECHO_CONTRACT; - - let mut evm = setup_test_evm( - &[(EOA, U256::from(1_000_000))], - &[(CONTRACT_A, contract_a_code), (CONTRACT_B, contract_b_code)], - &[CONTRACT_A], - ); - - // Build a deeply nested payload: 1000 layers of callFrom(EOA, CONTRACT_A, ) - // wrapping a base case of callFrom(EOA, CONTRACT_B, abi(7)). Each layer causes - // CONTRACT_A to be invoked and increment its counter. The depth limit (1024) - // should cut off recursion before we exhaust all 1000 layers. - let base_calldata = U256::from(7).to_be_bytes::<32>().to_vec(); - let mut payload = encode_call_from_input(EOA, CONTRACT_B, &base_calldata); - for _ in 0..1000 { - payload = encode_call_from_input(EOA, CONTRACT_A, &payload); - } - - // With EIP-150 63/64ths gas forwarding, gas shrinks geometrically at each level. - // Use u64::MAX so that gas is never the limiting factor — the 1024 depth limit - // should be what stops recursion. - evm.inner.ctx.set_tx(TxEnv { - caller: EOA, - kind: TxKind::Call(CONTRACT_A), - value: U256::ZERO, - gas_limit: u64::MAX, - gas_price: 0, - chain_id: Some(LOCAL_DEV.chain_id()), - data: payload, - ..Default::default() - }); - - let result_and_state = evm.replay().expect("replay should succeed"); - - // The recursion eventually hits CallTooDeep. Each level's wrapper catches - // the revert from the level below, so the top-level tx succeeds. - assert!( - matches!(result_and_state.result, ExecutionResult::Success { .. }), - "recursive callFrom should succeed (wrapper catches depth-limit revert)" - ); - - // Read the counter from CONTRACT_A's storage slot 0 to observe recursion depth. - // Each cycle: CONTRACT_A at depth D does CALL → intercepted at D+1 → child - // CONTRACT_A created at D+2. So each invocation consumes 2 depth levels. - let state = &result_and_state.state; - let a_account = state - .get(&CONTRACT_A) - .expect("contract A should be in state"); - let counter = a_account - .storage - .get(&U256::ZERO) - .map(|slot| slot.present_value) - .unwrap_or(U256::ZERO); - - // Each recursion cycle uses 2 depth levels (one for the intercepted callFrom frame, - // one for the child CONTRACT_A frame). With a limit of 1024 and initial depth 0, - // CONTRACT_A runs at depths 0, 2, 4, ..., 1024 — that's 513 invocations. - assert_eq!( - counter, - U256::from(513), - "expected 513 invocations (depths 0, 2, 4, ..., 1024)" - ); - } - - /// Returns EVM bytecode that consumes all gas via an infinite loop. - fn gas_burner_bytecode() -> Bytes { - #[rustfmt::skip] - let code = vec![ - // JUMPDEST at offset 0; JUMP back to 0 - JUMPDEST, // offset 0 - PUSH1, 0x00, // jump target = 0 - JUMP, - ]; - Bytes::from(code) - } - - /// Returns EVM bytecode that makes two CALLs to `target` with different payloads, - /// discards the first result, and returns the second call's returndata. - /// - /// Calldata layout: `[len1: 32 bytes][payload1: len1 bytes][payload2: rest]` - fn double_call_wrapper_bytecode(target: Address) -> Bytes { - #[rustfmt::skip] - let mut code = vec![ - // len1 = calldata[0..32] - PUSH1, 0x00, - CALLDATALOAD, // [len1] - - // Copy payload1 to mem[0x00..] - DUP1, // [len1, len1] - PUSH1, 0x20, // [0x20, len1, len1] - PUSH1, 0x00, // [0x00, 0x20, len1, len1] - CALLDATACOPY, // mem[0x00..len1] = calldata[0x20..0x20+len1]; stack: [len1] - - // CALL 1(gas, target, 0, 0, len1, 0, 0) — result discarded - PUSH1, 0x00, // retLen - PUSH1, 0x00, // retOff - DUP3, // argsLen = len1 - PUSH1, 0x00, // argsOff - PUSH1, 0x00, // value - PUSH20, - ]; - code.extend_from_slice(target.as_slice()); - #[rustfmt::skip] - code.extend_from_slice(&[ - GAS, - CALL, // [success, len1] - POP, // [len1] - - // src2 = 0x20 + len1; len2 = CALLDATASIZE - src2 - PUSH1, 0x20, - ADD, // [src2] - CALLDATASIZE, // [CDS, src2] - DUP2, // [src2, CDS, src2] - SWAP1, // [CDS, src2, src2] - SUB, // [len2, src2] - - // Copy payload2 to mem[0x00..] - DUP1, // [len2, len2, src2] - DUP3, // [src2, len2, len2, src2] - PUSH1, 0x00, // [0x00, src2, len2, len2, src2] - CALLDATACOPY, // mem[0x00..len2] = calldata[src2..]; stack: [len2, src2] - SWAP1, // [src2, len2] - POP, // [len2] - - // CALL 2(gas, target, 0, 0, len2, 0, 0) - PUSH1, 0x00, // retLen - PUSH1, 0x00, // retOff - DUP3, // argsLen = len2 - PUSH1, 0x00, // argsOff - PUSH1, 0x00, // value - PUSH20, - ]); - code.extend_from_slice(target.as_slice()); - code.extend_from_slice(&[GAS, CALL, POP, POP]); // discard success and len2 - append_return_returndata(&mut code); - Bytes::from(code) - } - - /// callFrom targeting an EOA (no bytecode). init_with_context returns an immediate - /// success (Stop) for empty code. Verifies the subcall framework handles - /// immediate success results correctly (continuation consumed, no stale state). - #[test] - fn test_call_from_target_eoa() { - let contract_a_code = wrapper_call_bytecode(CALL_FROM_ADDRESS); - const CONTRACT_A: Address = WRAPPER; - const TARGET_EOA: Address = address!("e000000000000000000000000000000000000002"); - - let mut evm = setup_test_evm( - &[(EOA, U256::from(1_000_000)), (TARGET_EOA, U256::ZERO)], - &[(CONTRACT_A, contract_a_code)], - &[CONTRACT_A], - ); - - let call_from_input = encode_call_from_input(EOA, TARGET_EOA, &[0xab; 4]); - - let tx = TxEnv { - caller: EOA, - kind: TxKind::Call(CONTRACT_A), - value: U256::ZERO, - gas_limit: 1_000_000, - gas_price: 0, - chain_id: Some(LOCAL_DEV.chain_id()), - data: call_from_input, - ..Default::default() - }; - - // The child frame targets an EOA (no code) — init_with_context returns - // immediate success with empty output. - let result = evm.transact_one(tx).expect("transact_one should succeed"); - assert!( - matches!(result, ExecutionResult::Success { .. }), - "callFrom targeting an EOA should succeed, got {result:?}" - ); - } - - /// callFrom targeting the ecrecover precompile (address 0x01). The child frame - /// is handled as a standard precompile by revm's init_with_context, returning - /// an immediate result. Verifies interop between subcall and standard precompiles. - #[test] - fn test_call_from_target_standard_precompile() { - let contract_a_code = wrapper_call_bytecode(CALL_FROM_ADDRESS); - const CONTRACT_A: Address = WRAPPER; - const ECRECOVER: Address = address!("0000000000000000000000000000000000000001"); - - let mut evm = setup_test_evm( - &[(EOA, U256::from(1_000_000))], - &[(CONTRACT_A, contract_a_code)], - &[CONTRACT_A], - ); - - // Provide invalid ecrecover input — the precompile will return empty/error, - // but the call itself should not panic or leave stale state. - let call_from_input = encode_call_from_input(EOA, ECRECOVER, &[0x00; 128]); - - let tx = TxEnv { - caller: EOA, - kind: TxKind::Call(CONTRACT_A), - value: U256::ZERO, - gas_limit: 1_000_000, - gas_price: 0, - chain_id: Some(LOCAL_DEV.chain_id()), - data: call_from_input, - ..Default::default() - }; - - // The wrapper catches any inner failure. The key property: no panics, - // no stale continuations, transaction completes cleanly. - let result = evm.transact_one(tx).expect("transact_one should succeed"); - assert!( - matches!(result, ExecutionResult::Success { .. }), - "callFrom targeting ecrecover should complete cleanly, got {result:?}" - ); - } - - /// callFrom child runs out of gas (OOG). The child is an infinite loop that - /// consumes all forwarded gas. complete_subcall should see the halt and propagate - /// failure to the wrapper. - #[test] - fn test_call_from_child_oog() { - let contract_a_code = wrapper_call_bytecode(CALL_FROM_ADDRESS); - let gas_burner_code = gas_burner_bytecode(); - const CONTRACT_A: Address = WRAPPER; - const GAS_BURNER: Address = address!("c000000000000000000000000000000000000006"); - - let mut evm = setup_test_evm( - &[(EOA, U256::from(1_000_000))], - &[(CONTRACT_A, contract_a_code), (GAS_BURNER, gas_burner_code)], - &[CONTRACT_A], - ); - - let call_from_input = encode_call_from_input(EOA, GAS_BURNER, &[]); - - let tx = TxEnv { - caller: EOA, - kind: TxKind::Call(CONTRACT_A), - value: U256::ZERO, - gas_limit: 1_000_000, - gas_price: 0, - chain_id: Some(LOCAL_DEV.chain_id()), - data: call_from_input, - ..Default::default() - }; - - // The child burns all gas and halts with OOG. The child's own checkpoint is - // reverted by process_next_action. The wrapper catches the failure via CALL - // return value (0), so the top-level tx succeeds. - let result = evm.transact_one(tx).expect("transact_one should succeed"); - assert!( - matches!(result, ExecutionResult::Success { .. }), - "wrapper should catch OOG child failure, got {result:?}" - ); - } - - /// Child OOG should burn the full subcall gas allocation (including the retained - /// 1/64th), matching EVM CALL semantics. - /// - /// Compares a callFrom-to-gas-burner (child OOGs) against a direct - /// wrapper-to-gas-burner CALL (standard EVM OOG). Both wrappers are structurally - /// identical (same opcodes, same calldata size), so the wrapper-level gas overhead - /// is the same. Any difference in gas_used is due to the subcall precompile's - /// extra layer (init overhead + second 63/64ths split). - /// - /// The key assertion: the callFrom OOG path must consume *at least* as much gas - /// as the direct CALL OOG path. If the subcall leaked the 1/64th back, the - /// callFrom path would consume *less* gas than the direct CALL (because the - /// leaked 1/64th would be returned to the wrapper). - #[test] - fn test_call_from_child_oog_burns_full_allocation() { - // Wrapper A: calls CallFrom precompile → CallFrom calls gas burner (child OOGs) - let wrapper_via_callfrom = wrapper_call_bytecode(CALL_FROM_ADDRESS); - // Wrapper B: calls gas burner directly (standard EVM CALL, child OOGs) - let wrapper_direct = wrapper_call_bytecode(GAS_BURNER); - let gas_burner_code = gas_burner_bytecode(); - const WRAPPER_VIA_CALLFROM: Address = WRAPPER; - const WRAPPER_DIRECT: Address = address!("c000000000000000000000000000000000000009"); - const GAS_BURNER: Address = address!("c000000000000000000000000000000000000006"); - - let mut evm = setup_test_evm( - &[(EOA, U256::from(1_000_000))], - &[ - (WRAPPER_VIA_CALLFROM, wrapper_via_callfrom), - (WRAPPER_DIRECT, wrapper_direct), - (GAS_BURNER, gas_burner_code), - ], - &[WRAPPER_VIA_CALLFROM], - ); - - let gas_limit: u64 = 500_000; - - // Tx 1: callFrom → gas burner (child OOGs inside subcall framework) - let call_from_input = encode_call_from_input(EOA, GAS_BURNER, &[]); - let tx_callfrom = TxEnv { - caller: EOA, - kind: TxKind::Call(WRAPPER_VIA_CALLFROM), - value: U256::ZERO, - gas_limit, - gas_price: 0, - chain_id: Some(LOCAL_DEV.chain_id()), - data: call_from_input, - ..Default::default() - }; - let result_callfrom = evm.transact_one(tx_callfrom).expect("callfrom tx"); - let gas_used_callfrom = match &result_callfrom { - ExecutionResult::Success { gas_used, .. } => *gas_used, - other => panic!("expected Success (wrapper catches OOG), got {other:?}"), - }; - - // Tx 2: direct CALL → gas burner (standard EVM OOG, no subcall layer) - let tx_direct = TxEnv { - caller: EOA, - kind: TxKind::Call(WRAPPER_DIRECT), - value: U256::ZERO, - gas_limit, - gas_price: 0, - nonce: 1, - chain_id: Some(LOCAL_DEV.chain_id()), - data: Bytes::new(), - ..Default::default() - }; - let result_direct = evm.transact_one(tx_direct).expect("direct tx"); - let gas_used_direct = match &result_direct { - ExecutionResult::Success { gas_used, .. } => *gas_used, - other => panic!("expected Success, got {other:?}"), - }; - - // Both wrappers forward the same amount of gas G to their CALL target. - // In the direct path, the child receives G and OOGs — the wrapper's CALL - // reports G spent. In the callFrom path, the subcall precompile deducts - // init overhead, then forwards (G - overhead) * 63/64 to the child. The - // child OOGs and burns that. With correct gas accounting the subcall reports - // the full G as spent (overhead + child + retained 1/64th), matching the - // direct path. - // - // If the subcall leaked the retained 1/64th back to the wrapper, - // gas_used_callfrom would be less than gas_used_direct — that's the bug - // this test catches. - assert!( - gas_used_callfrom >= gas_used_direct, - "callFrom OOG ({gas_used_callfrom}) should consume at least as much gas \ - as direct CALL OOG ({gas_used_direct})" - ); - } - - /// Two sequential callFrom calls from the same wrapper in one transaction, each - /// with different parameters. Verifies that continuations keyed by depth don't - /// collide — each call stores and consumes its own continuation independently. - /// The wrapper discards the first result and returns the second call's returndata. - #[test] - fn test_call_from_sequential_calls() { - let contract_a_code = double_call_wrapper_bytecode(CALL_FROM_ADDRESS); - let contract_b_code = echo_double_bytecode(); - const CONTRACT_A: Address = WRAPPER; - const CONTRACT_B: Address = ECHO_CONTRACT; - - let mut evm = setup_test_evm( - &[(EOA, U256::from(1_000_000))], - &[(CONTRACT_A, contract_a_code), (CONTRACT_B, contract_b_code)], - &[CONTRACT_A], - ); - - // Call 1: echo_double(7) = 14 (result discarded by wrapper) - let payload1 = - encode_call_from_input(EOA, CONTRACT_B, &U256::from(7).to_be_bytes::<32>()); - // Call 2: echo_double(42) = 84 (result returned by wrapper) - let payload2 = - encode_call_from_input(EOA, CONTRACT_B, &U256::from(42).to_be_bytes::<32>()); - - // Calldata layout: [len1: 32 bytes][payload1][payload2] - let len1 = U256::from(payload1.len()); - let mut calldata = len1.to_be_bytes::<32>().to_vec(); - calldata.extend_from_slice(&payload1); - calldata.extend_from_slice(&payload2); - - let tx = TxEnv { - caller: EOA, - kind: TxKind::Call(CONTRACT_A), - value: U256::ZERO, - gas_limit: 1_000_000, - gas_price: 0, - chain_id: Some(LOCAL_DEV.chain_id()), - data: Bytes::from(calldata), - ..Default::default() - }; - - let result = evm.transact_one(tx).expect("transact_one should succeed"); - match &result { - ExecutionResult::Success { output, .. } => { - // Wrapper returns the second call's returndata (ABI-encoded callFrom output) - let (success, return_data) = decode_call_from_output(output.data()); - assert!(success, "second callFrom should report child success"); - - let value = U256::from_be_slice(&return_data); - assert_eq!(value, U256::from(84), "echo_double(42) should return 84"); - } - other => panic!("expected Success, got {other:?}"), - } - } - - /// STATICCALL to CallFrom should revert — static context is not supported. - #[test] - fn test_call_from_staticcall_rejected() { - let contract_a_code = static_call_wrapper_bytecode(CALL_FROM_ADDRESS); - let contract_b_code = echo_double_bytecode(); - const CONTRACT_A: Address = WRAPPER; - const CONTRACT_B: Address = ECHO_CONTRACT; - - let mut evm = setup_test_evm( - &[(EOA, U256::from(1_000_000))], - &[(CONTRACT_A, contract_a_code), (CONTRACT_B, contract_b_code)], - &[CONTRACT_A], - ); - - let inner_calldata = U256::from(42).to_be_bytes::<32>().to_vec(); - let call_from_input = encode_call_from_input(EOA, CONTRACT_B, &inner_calldata); - - let tx = TxEnv { - caller: EOA, - kind: TxKind::Call(CONTRACT_A), - value: U256::ZERO, - gas_limit: 1_000_000, - gas_price: 0, - chain_id: Some(LOCAL_DEV.chain_id()), - data: call_from_input, - ..Default::default() - }; - - // The wrapper uses STATICCALL → CallFrom. The scheme check rejects it with a - // revert. The wrapper catches the revert and returns the revert data. - let result = evm.transact_one(tx).expect("transact_one should succeed"); - match &result { - ExecutionResult::Success { output, .. } => { - // The output is revert data from the scheme rejection. - // If echo_double had actually run, output would contain value 84. - // Instead, the data should NOT contain the echo result, confirming - // the subcall never reached the child contract. - let echo_result = U256::from(84).to_be_bytes::<32>(); - assert_ne!( - output.data().as_ref(), - &echo_result, - "STATICCALL should not have reached the child contract" - ); - } - other => panic!("expected Success (wrapper catches revert), got {other:?}"), - } - } - - // ----- Tracing / Inspector tests ----- - - /// Creates an ArcEvm identical to `setup_test_evm` but with a generic inspector - /// and the `inspect` flag set to `true` so `inspect_one_tx` uses the inspector path. - fn setup_test_evm_with_inspector< - INSP: revm::inspector::Inspector, EthInterpreter>, - >( - accounts: &[(Address, U256)], - contracts: &[(Address, Bytes)], - call_from_allowlist: &[Address], - inspector: INSP, - ) -> ArcEvm< - EthEvmContext, - INSP, - EthInstructions>, - PrecompilesMap, - > { - use crate::subcall::AllowedCallers; - use crate::subcall_test::{SubcallTestPrecompile, SUBCALL_TEST_ADDRESS}; - - let chain_spec = LOCAL_DEV.clone(); - let mut db = InMemoryDB::default(); - - for (addr, balance) in accounts { - db.insert_account_info( - *addr, - AccountInfo { - balance: *balance, - nonce: 0, - code_hash: alloy_primitives::KECCAK256_EMPTY, - code: None, - account_id: None, - }, - ); - } - - for (addr, code) in contracts { - db.insert_account_info( - *addr, - AccountInfo { - balance: U256::ZERO, - nonce: 1, - code_hash: keccak256(code), - code: Some(Bytecode::new_raw(code.clone())), - account_id: None, - }, - ); - } - - let spec = - reth_ethereum::evm::revm_spec_by_timestamp_and_block_number(&chain_spec, 0, 0); - let hardfork_flags = chain_spec.get_hardfork_flags(0, 0); - let mut cfg_env = revm::context::CfgEnv::new() - .with_chain_id(chain_spec.chain_id()) - .with_spec_and_mainnet_gas_params(spec); - cfg_env.tx_gas_limit_cap = Some(u64::MAX); - let ctx = EthEvmContext::new(db, spec) - .with_cfg(cfg_env) - .with_block(BlockEnv::default()); - let precompiles = ArcPrecompileProvider::create_precompiles_map(spec, hardfork_flags); - let mut instruction = EthInstructions::new_mainnet_with_spec(spec); - if hardfork_flags.is_active(ArcHardfork::Zero7) { - instruction.insert_instruction( - SELFDESTRUCT, - revm_interpreter::Instruction::new(arc_network_selfdestruct_zero7, 5000), - ); - } else if hardfork_flags.is_active(ArcHardfork::Zero5) { - instruction.insert_instruction( - SELFDESTRUCT, - revm_interpreter::Instruction::new(arc_network_selfdestruct_zero5, 5000), - ); - } - - let mut registry = SubcallRegistry::new(); - registry.register( - SUBCALL_TEST_ADDRESS, - Arc::new(SubcallTestPrecompile), - AllowedCallers::Unrestricted, - ); - let allowed_callers = - AllowedCallers::Only(HashSet::from_iter(call_from_allowlist.iter().copied())); - registry.register( - CALL_FROM_ADDRESS, - Arc::new(CallFromPrecompile), - allowed_callers, - ); - - ArcEvm::new( - ctx, - inspector, - precompiles, - instruction, - true, // enable inspect path - hardfork_flags, - Arc::new(registry), - ) - } - - /// Verifies that `debug_traceTransaction` with `callTracer` produces a transparent - /// trace for subcall precompiles: the CallFrom precompile is invisible, and the trace - /// shows the logical child call (spoofed_sender → target) with inner frames nested. - /// - /// Call chain: EOA → wrapper → CallFrom → forwarder → echo_double(42) - /// - /// Expected trace (precompile is transparent): - /// CALL: EOA → wrapper - /// └─ CALL: EOA → forwarder (the spoofed child call) - /// └─ CALL: forwarder → echo (forwarder calls echo_double) - /// - /// Also serves as a regression test for the original "Disconnected trace" panic - /// (checkpoint depth gap). - #[test] - fn test_call_from_nested_call_with_tracing_inspector() { - use alloy_rpc_types_trace::geth::call::CallConfig; - use revm_inspectors::tracing::{TracingInspector, TracingInspectorConfig}; - - // forwarder: a contract that forwards calldata to echo_double via CALL - let forwarder_code = wrapper_call_bytecode(ECHO_CONTRACT); - let echo_code = echo_double_bytecode(); - let wrapper_code = wrapper_call_bytecode(CALL_FROM_ADDRESS); - const CONTRACT_WRAPPER: Address = WRAPPER; - const CONTRACT_FORWARDER: Address = - address!("c000000000000000000000000000000000000003"); - const CONTRACT_ECHO: Address = ECHO_CONTRACT; - - let call_config = CallConfig { - with_log: Some(true), - only_top_call: Some(false), - }; - let inspector = - TracingInspector::new(TracingInspectorConfig::from_geth_call_config(&call_config)); - - let mut evm = setup_test_evm_with_inspector( - &[(EOA, U256::from(1_000_000))], - &[ - (CONTRACT_WRAPPER, wrapper_code), - (CONTRACT_FORWARDER, forwarder_code), - (CONTRACT_ECHO, echo_code), - ], - &[CONTRACT_WRAPPER], // wrapper is allowed to call CallFrom - inspector, - ); - - // Inner calldata: the forwarder will forward this to echo_double - let inner_calldata = U256::from(42).to_be_bytes::<32>().to_vec(); - // CallFrom(sender=EOA, target=forwarder, data=inner_calldata) - let call_from_input = encode_call_from_input(EOA, CONTRACT_FORWARDER, &inner_calldata); - - let tx = TxEnv { - caller: EOA, - kind: TxKind::Call(CONTRACT_WRAPPER), - value: U256::ZERO, - gas_limit: 1_000_000, - gas_price: 0, - chain_id: Some(LOCAL_DEV.chain_id()), - data: call_from_input, - ..Default::default() - }; - - // Must use inspect_one_tx (not transact_one) because transact_one uses - // run_exec_loop which never calls inspector.call/call_end. The inspect - // path (inspect_run_exec_loop → inspect_frame_init → frame_start) is - // what triggers the TracingInspector's depth tracking. - let result = evm - .inspect_one_tx(tx) - .expect("inspect_one_tx should succeed"); - let gas_used = match &result { - ExecutionResult::Success { gas_used, .. } => *gas_used, - other => panic!("expected Success, got {other:?}"), - }; - - // Convert trace to geth call format and verify structure - let frame = evm - .inner - .inspector - .with_transaction_gas_limit(1_000_000) - .into_geth_builder() - .geth_call_traces(call_config, gas_used); - - // Top-level frame: EOA → wrapper - assert_eq!(frame.from, EOA, "top frame: from should be EOA"); - assert_eq!( - frame.to, - Some(CONTRACT_WRAPPER), - "top frame: to should be wrapper" - ); - - // The wrapper's only call should be the transparent child call - // (spoofed_sender → forwarder), not the precompile call. - assert_eq!( - frame.calls.len(), - 1, - "wrapper should have exactly 1 child call (the transparent subcall)" - ); - let child = &frame.calls[0]; - assert_eq!( - child.from, EOA, - "child frame: from should be the spoofed sender (EOA)" - ); - assert_eq!( - child.to, - Some(CONTRACT_FORWARDER), - "child frame: to should be the forwarder" - ); - - // The forwarder calls echo_double — this should appear as a nested call. - assert_eq!( - child.calls.len(), - 1, - "forwarder should have exactly 1 nested call (to echo_double)" - ); - let grandchild = &child.calls[0]; - assert_eq!( - grandchild.from, CONTRACT_FORWARDER, - "grandchild frame: from should be forwarder" - ); - assert_eq!( - grandchild.to, - Some(CONTRACT_ECHO), - "grandchild frame: to should be echo contract" - ); - } - - /// When CallFrom receives malformed calldata, `trace_child_call` returns `None`. - /// The trace should fall back to showing the precompile address, not the logical - /// child call. The execution itself reverts (ABI decode failure in `init_subcall`). - #[test] - fn test_call_from_malformed_calldata_trace_shows_precompile() { - use alloy_rpc_types_trace::geth::call::CallConfig; - use revm_inspectors::tracing::{TracingInspector, TracingInspectorConfig}; - - let wrapper_code = wrapper_call_bytecode(CALL_FROM_ADDRESS); - - let call_config = CallConfig { - with_log: Some(true), - only_top_call: Some(false), - }; - let inspector = - TracingInspector::new(TracingInspectorConfig::from_geth_call_config(&call_config)); - - let mut evm = setup_test_evm_with_inspector( - &[(EOA, U256::from(1_000_000))], - &[(WRAPPER, wrapper_code)], - &[WRAPPER], - inspector, - ); - - // Send garbage calldata — not a valid callFrom ABI encoding. - let garbage_data = Bytes::from(vec![0xde, 0xad, 0xbe, 0xef]); - - let tx = TxEnv { - caller: EOA, - kind: TxKind::Call(WRAPPER), - value: U256::ZERO, - gas_limit: 1_000_000, - gas_price: 0, - chain_id: Some(LOCAL_DEV.chain_id()), - data: garbage_data, - ..Default::default() - }; - - let result = evm - .inspect_one_tx(tx) - .expect("inspect_one_tx should succeed"); - let gas_used = match &result { - ExecutionResult::Success { gas_used, .. } => *gas_used, - other => panic!("expected Success, got {other:?}"), - }; - - let frame = evm - .inner - .inspector - .into_geth_builder() - .geth_call_traces(call_config, gas_used); - - // Top-level: EOA → wrapper - assert_eq!(frame.from, EOA); - assert_eq!(frame.to, Some(WRAPPER)); - - // The child call should show the precompile address (fallback behavior), - // since trace_child_call returned None for the malformed input. - assert_eq!( - frame.calls.len(), - 1, - "wrapper should have exactly 1 child call" - ); - let child = &frame.calls[0]; - assert_eq!( - child.from, WRAPPER, - "malformed calldata: trace should show precompile address (fallback)" - ); - assert_eq!( - child.to, - Some(CALL_FROM_ADDRESS), - "malformed calldata: trace should show precompile address (fallback)" - ); - } - - /// When CallFrom targets an EOA (no bytecode), the child call completes - /// immediately without spawning an interpreter frame. This exercises the - /// `ItemOrResult::Result` branch in `inspect_frame_init_impl`. - #[test] - fn test_call_from_eoa_target_trace() { - use alloy_rpc_types_trace::geth::call::CallConfig; - use revm_inspectors::tracing::{TracingInspector, TracingInspectorConfig}; - - let wrapper_code = wrapper_call_bytecode(CALL_FROM_ADDRESS); - const TARGET_EOA: Address = address!("e000000000000000000000000000000000000099"); - - let call_config = CallConfig { - with_log: Some(true), - only_top_call: Some(false), - }; - let inspector = - TracingInspector::new(TracingInspectorConfig::from_geth_call_config(&call_config)); - - let mut evm = setup_test_evm_with_inspector( - &[(EOA, U256::from(1_000_000)), (TARGET_EOA, U256::ZERO)], - &[(WRAPPER, wrapper_code)], - &[WRAPPER], - inspector, - ); - - let inner_calldata = vec![]; - let call_from_input = encode_call_from_input(EOA, TARGET_EOA, &inner_calldata); - - let tx = TxEnv { - caller: EOA, - kind: TxKind::Call(WRAPPER), - value: U256::ZERO, - gas_limit: 1_000_000, - gas_price: 0, - chain_id: Some(LOCAL_DEV.chain_id()), - data: call_from_input, - ..Default::default() - }; - - let result = evm - .inspect_one_tx(tx) - .expect("inspect_one_tx should succeed"); - let gas_used = match &result { - ExecutionResult::Success { gas_used, .. } => *gas_used, - other => panic!("expected Success, got {other:?}"), - }; - - let frame = evm - .inner - .inspector - .into_geth_builder() - .geth_call_traces(call_config, gas_used); - - // Top-level: EOA → wrapper - assert_eq!(frame.from, EOA); - assert_eq!(frame.to, Some(WRAPPER)); - - // The child call should show the logical call (EOA → TARGET_EOA), - // transparently — even though the EOA has no code and completes immediately. - assert_eq!( - frame.calls.len(), - 1, - "wrapper should have exactly 1 child call" - ); - let child = &frame.calls[0]; - assert_eq!( - child.from, EOA, - "child frame: from should be the spoofed sender" - ); - assert_eq!( - child.to, - Some(TARGET_EOA), - "child frame: to should be the EOA target" - ); - // EOA target has no code, so no nested calls. - assert_eq!( - child.calls.len(), - 0, - "EOA target should have no nested calls" - ); - } - - // ----- Gas accounting tests ----- - - /// Verifies CallFromPrecompile::init_subcall applies EIP-150 63/64ths gas forwarding. - /// Direct unit test — no EVM execution, just the init_subcall calculation. - #[test] - fn test_call_from_init_subcall_gas_calculation() { - use alloy_sol_types::SolCall; - use arc_precompiles::call_from::{abi_decode_gas, ICallFrom}; - use arc_precompiles::subcall::SubcallPrecompile; - - let precompile = CallFromPrecompile; - let gas_limit: u64 = 100_000; - let child_data: Vec = vec![0x42]; - - let calldata = ICallFrom::callFromCall { - sender: EOA, - target: ECHO_CONTRACT, - data: child_data.clone().into(), - } - .abi_encode(); - - let inputs = CallInputs { - scheme: CallScheme::Call, - target_address: CALL_FROM_ADDRESS, - bytecode_address: CALL_FROM_ADDRESS, - known_bytecode: None, - value: CallValue::Transfer(U256::ZERO), - input: CallInput::Bytes(Bytes::from(calldata)), - gas_limit, - is_static: false, - caller: WRAPPER, - return_memory_offset: 0..0, - }; - - let result = precompile - .init_subcall(&inputs) - .expect("init_subcall should succeed"); - - let overhead = abi_decode_gas(child_data.len()); - let expected_available = gas_limit - overhead; - let expected_child_gas = expected_available - (expected_available / 64); - - assert_eq!( - result.child_inputs.gas_limit, expected_child_gas, - "child gas_limit should be (gas_limit - overhead) * 63/64" - ); - assert_eq!(result.gas_overhead, overhead); - } - - /// init_subcall should error when gas_limit is less than the ABI decode overhead. - #[test] - fn test_call_from_init_subcall_insufficient_gas() { - use alloy_sol_types::SolCall; - use arc_precompiles::call_from::{abi_decode_gas, ICallFrom}; - use arc_precompiles::subcall::SubcallPrecompile; - - let precompile = CallFromPrecompile; - let child_data: Vec = vec![0xAB; 64]; // 2 words of data - - let calldata = ICallFrom::callFromCall { - sender: EOA, - target: ECHO_CONTRACT, - data: child_data.clone().into(), - } - .abi_encode(); - - let inputs = CallInputs { - scheme: CallScheme::Call, - target_address: CALL_FROM_ADDRESS, - bytecode_address: CALL_FROM_ADDRESS, - known_bytecode: None, - value: CallValue::Transfer(U256::ZERO), - input: CallInput::Bytes(Bytes::from(calldata)), - gas_limit: abi_decode_gas(child_data.len()) - 1, // Not enough - is_static: false, - caller: WRAPPER, - return_memory_offset: 0..0, - }; - - let result = precompile.init_subcall(&inputs); - assert!( - result.is_err(), - "init_subcall should fail with insufficient gas" - ); - } - - /// Bytecode: SSTORE(slot=0, value=1) then SSTORE(slot=0, value=0) to trigger a refund, - /// then RETURN empty. The SSTORE 1→0 transition earns a gas refund. - fn sstore_refund_bytecode() -> Bytes { - #[rustfmt::skip] - let code = vec![ - PUSH1, 0x01, PUSH1, 0x00, SSTORE, // SSTORE(0, 1) - PUSH1, 0x00, PUSH1, 0x00, SSTORE, // SSTORE(0, 0) — refund - PUSH1, 0x00, PUSH1, 0x00, RETURN, // RETURN(0, 0) - ]; - Bytes::from(code) - } - - /// Bytecode: SSTORE(slot=0, value=1) then SSTORE(slot=0, value=0) to trigger a refund, - /// then REVERT. - fn sstore_refund_then_revert_bytecode() -> Bytes { - #[rustfmt::skip] - let code = vec![ - PUSH1, 0x01, PUSH1, 0x00, SSTORE, // SSTORE(0, 1) - PUSH1, 0x00, PUSH1, 0x00, SSTORE, // SSTORE(0, 0) — refund - PUSH1, 0x00, PUSH1, 0x00, REVERT, // REVERT(0, 0) - ]; - Bytes::from(code) - } - - /// SSTORE refunds are forwarded on child success but NOT on child revert. - #[test] - fn test_call_from_gas_refund_on_success_not_revert() { - let contract_a_code = wrapper_call_bytecode(CALL_FROM_ADDRESS); - let refund_success_code = sstore_refund_bytecode(); - let refund_revert_code = sstore_refund_then_revert_bytecode(); - const CONTRACT_A: Address = WRAPPER; - const REFUND_SUCCESS: Address = address!("c000000000000000000000000000000000000007"); - const REFUND_REVERT: Address = address!("c000000000000000000000000000000000000008"); - - let mut evm = setup_test_evm( - &[(EOA, U256::from(1_000_000))], - &[ - (CONTRACT_A, contract_a_code), - (REFUND_SUCCESS, refund_success_code), - (REFUND_REVERT, refund_revert_code), - ], - &[CONTRACT_A], - ); - - // Tx 1: child succeeds with SSTORE refund - let call_from_success = encode_call_from_input(EOA, REFUND_SUCCESS, &[]); - let tx_success = TxEnv { - caller: EOA, - kind: TxKind::Call(CONTRACT_A), - value: U256::ZERO, - gas_limit: 1_000_000, - gas_price: 0, - chain_id: Some(LOCAL_DEV.chain_id()), - data: call_from_success, - ..Default::default() - }; - let result_success = evm.transact_one(tx_success).expect("success tx"); - let gas_used_success = match &result_success { - ExecutionResult::Success { gas_used, .. } => *gas_used, - other => panic!("expected Success, got {other:?}"), - }; - - // Tx 2: child reverts after earning SSTORE refund (refund should be discarded) - let call_from_revert = encode_call_from_input(EOA, REFUND_REVERT, &[]); - let tx_revert = TxEnv { - caller: EOA, - kind: TxKind::Call(CONTRACT_A), - value: U256::ZERO, - gas_limit: 1_000_000, - gas_price: 0, - nonce: 1, - chain_id: Some(LOCAL_DEV.chain_id()), - data: call_from_revert, - ..Default::default() - }; - let result_revert = evm.transact_one(tx_revert).expect("revert tx"); - let gas_used_revert = match &result_revert { - ExecutionResult::Success { gas_used, .. } => *gas_used, - other => panic!("expected Success (wrapper catches revert), got {other:?}"), - }; - - // The success path should use LESS gas than the revert path because the - // SSTORE refund is applied on success but discarded on revert. - assert!( - gas_used_success < gas_used_revert, - "success ({gas_used_success}) should use less gas than revert ({gas_used_revert}) \ - due to SSTORE refund being forwarded only on success" - ); - } - - // ================================================================ - // EIP-7702 delegation tests - // ================================================================ - - /// CallFrom targeting an EIP-7702 delegated account executes the delegate's code. - /// DELEGATED_EOA has delegation → ECHO_CONTRACT, so calling it runs echo_double. - #[test] - fn test_call_from_eip7702_delegation() { - const DELEGATED_EOA: Address = address!("d000000000000000000000000000000000000001"); - - let contract_a_code = wrapper_call_bytecode(CALL_FROM_ADDRESS); - let echo_code = echo_double_bytecode(); - - let mut evm = setup_test_evm( - &[(EOA, U256::from(1_000_000))], - &[(WRAPPER, contract_a_code), (ECHO_CONTRACT, echo_code)], - &[WRAPPER], - ); - - // Insert EIP-7702 delegated account: DELEGATED_EOA delegates to ECHO_CONTRACT - insert_eip7702_account(&mut evm, DELEGATED_EOA, ECHO_CONTRACT); - - // EOA → WRAPPER → CallFrom(sender=EOA, target=DELEGATED_EOA, data=abi(42)) - // DELEGATED_EOA has delegation to ECHO_CONTRACT, so echo_double(42) = 84 - let inner_calldata = U256::from(42).to_be_bytes::<32>().to_vec(); - let call_from_input = encode_call_from_input(EOA, DELEGATED_EOA, &inner_calldata); - - let tx = TxEnv { - caller: EOA, - kind: TxKind::Call(WRAPPER), - value: U256::ZERO, - gas_limit: 1_000_000, - gas_price: 0, - chain_id: Some(LOCAL_DEV.chain_id()), - data: call_from_input, - ..Default::default() - }; - - let result = evm.transact_one(tx).expect("transact_one should succeed"); - match &result { - ExecutionResult::Success { output, .. } => { - let (success, return_data) = decode_call_from_output(output.data()); - assert!(success, "callFrom should report child success"); - let expected = U256::from(84).to_be_bytes::<32>(); - assert_eq!( - return_data, expected, - "should get echo_double(42) = 84 via delegation" - ); - } - other => panic!("expected Success, got {other:?}"), - } - } - - /// SubcallTestPrecompile targeting an EIP-7702 delegated account executes the - /// delegate's code. DELEGATED_EOA delegates to ECHO_CONTRACT, so the subcall - /// test precompile calling it should run echo_double. - #[test] - fn test_subcall_eip7702_delegation() { - use crate::subcall_test::SUBCALL_TEST_ADDRESS; - - const DELEGATED_EOA: Address = address!("d000000000000000000000000000000000000001"); - - let wrapper_code = wrapper_call_bytecode(SUBCALL_TEST_ADDRESS); - let echo_code = echo_double_bytecode(); - - let mut evm = setup_test_evm( - &[(EOA, U256::from(1_000_000))], - &[(WRAPPER, wrapper_code), (ECHO_CONTRACT, echo_code)], - &[], - ); - - // Insert EIP-7702 delegated account: DELEGATED_EOA delegates to ECHO_CONTRACT - insert_eip7702_account(&mut evm, DELEGATED_EOA, ECHO_CONTRACT); - - // EOA → WRAPPER → SubcallTest(target=DELEGATED_EOA, data=abi(21)) - let inner_calldata = U256::from(21).to_be_bytes::<32>().to_vec(); - let subcall_input = encode_subcall_test_input(DELEGATED_EOA, &inner_calldata); - - let tx = TxEnv { - caller: EOA, - kind: TxKind::Call(WRAPPER), - value: U256::ZERO, - gas_limit: 1_000_000, - gas_price: 0, - chain_id: Some(LOCAL_DEV.chain_id()), - data: subcall_input, - ..Default::default() - }; - - let result = evm.transact_one(tx).expect("transact_one should succeed"); - match &result { - ExecutionResult::Success { output, .. } => { - let child_output = - ::abi_decode(output.data()) - .expect("should decode bytes wrapper"); - // echo_double(21) → 42 - let expected = U256::from(42).to_be_bytes::<32>(); - assert_eq!( - child_output.as_ref(), - &expected, - "should get echo_double(21) = 42 via delegation" - ); - } - other => panic!("expected Success, got {other:?}"), - } - } - - /// CallFrom targeting an EIP-7702 delegated account whose delegate reverts. - /// DELEGATED_EOA delegates to REVERT_CONTRACT — the revert should propagate - /// correctly through CallFrom's (success=false) return value. - #[test] - fn test_call_from_eip7702_delegation_to_reverting_contract() { - const DELEGATED_EOA: Address = address!("d000000000000000000000000000000000000001"); - const REVERT_PAYLOAD: [u8; 4] = [0xca, 0xfe, 0xba, 0xbe]; - - let contract_a_code = wrapper_call_bytecode(CALL_FROM_ADDRESS); - let revert_code = reverting_with_payload_bytecode(REVERT_PAYLOAD); - - let mut evm = setup_test_evm( - &[(EOA, U256::from(1_000_000))], - &[(WRAPPER, contract_a_code), (REVERT_CONTRACT, revert_code)], - &[WRAPPER], - ); - - // Insert EIP-7702 delegated account: DELEGATED_EOA delegates to REVERT_CONTRACT - insert_eip7702_account(&mut evm, DELEGATED_EOA, REVERT_CONTRACT); - - // EOA → WRAPPER → CallFrom(sender=EOA, target=DELEGATED_EOA, data=) - let call_from_input = encode_call_from_input(EOA, DELEGATED_EOA, &[]); - - let tx = TxEnv { - caller: EOA, - kind: TxKind::Call(WRAPPER), - value: U256::ZERO, - gas_limit: 1_000_000, - gas_price: 0, - chain_id: Some(LOCAL_DEV.chain_id()), - data: call_from_input, - ..Default::default() - }; - - let result = evm.transact_one(tx).expect("transact_one should succeed"); - match &result { - ExecutionResult::Success { output, .. } => { - let (success, return_data) = decode_call_from_output(output.data()); - assert!( - !success, - "callFrom should report child failure (delegate reverts)" - ); - assert_eq!( - return_data.as_slice(), - REVERT_PAYLOAD.as_slice(), - "callFrom should propagate the delegate's revert payload" - ); - } - other => panic!("expected Success, got {other:?}"), - } - } - - /// If EIP-7702 target resolution succeeds but the cold delegate load is OOG, - /// the delegate must stay cold. Otherwise an attacker could warm the delegate as - /// a side-effect of an insufficient-gas subcall. - #[test] - fn test_call_from_eip7702_delegate_oog_does_not_warm_delegate() { - use alloy_sol_types::SolCall; - use arc_precompiles::call_from::{abi_decode_gas, ICallFrom}; - use revm_interpreter::gas::COLD_ACCOUNT_ACCESS_COST; - - const DELEGATED_EOA: Address = address!("d000000000000000000000000000000000000001"); - - let echo_code = echo_double_bytecode(); - let mut evm = setup_test_evm( - &[(EOA, U256::from(1_000_000))], - &[(ECHO_CONTRACT, echo_code)], - &[WRAPPER], - ); - insert_eip7702_account(&mut evm, DELEGATED_EOA, ECHO_CONTRACT); - - evm.ctx_mut().journal_mut().clear(); - evm.inner.ctx.set_tx(TxEnv { - caller: EOA, - ..Default::default() - }); - - let child_data: Vec = Vec::new(); - let calldata = ICallFrom::callFromCall { - sender: EOA, - target: DELEGATED_EOA, - data: child_data.clone().into(), - } - .abi_encode(); - - let gas_limit = abi_decode_gas(child_data.len()) - .checked_add(COLD_ACCOUNT_ACCESS_COST) - .and_then(|gas| gas.checked_add(COLD_ACCOUNT_ACCESS_COST)) - .and_then(|gas| gas.checked_sub(1)) - .expect("test gas calculation should not overflow"); - let call_inputs = CallInputs { - scheme: CallScheme::Call, - target_address: CALL_FROM_ADDRESS, - bytecode_address: CALL_FROM_ADDRESS, - known_bytecode: None, - value: CallValue::Transfer(U256::ZERO), - input: CallInput::Bytes(Bytes::from(calldata)), - gas_limit, - is_static: false, - caller: WRAPPER, - return_memory_offset: 0..0, - }; - - let frame_input = FrameInit { - frame_input: FrameInput::Call(Box::new(call_inputs)), - memory: SharedMemory::default(), - depth: 1, - }; - - let precompile: Arc = - Arc::new(CallFromPrecompile); - let result = evm - .init_subcall(frame_input, precompile) - .expect("init_subcall should return OOG, not DB error"); - - match result { - ItemOrResult::Result(FrameResult::Call(outcome)) => { - assert_eq!(outcome.result.result, InstructionResult::OutOfGas); - assert_eq!( - outcome.result.gas.spent(), - gas_limit, - "OOG should consume the whole subcall gas budget" - ); - } - other => panic!("expected immediate OOG result, got {other:?}"), - } - assert!( - evm.subcall_continuations.is_empty(), - "OOG before child dispatch should not store any continuation" - ); - - let target_probe = evm - .ctx_mut() - .journal_mut() - .load_account_info_skip_cold_load(DELEGATED_EOA, true, true); - assert!( - target_probe.is_ok(), - "target account should be warm after successful target resolution" - ); - - let delegate_probe = evm - .ctx_mut() - .journal_mut() - .load_account_info_skip_cold_load(ECHO_CONTRACT, true, true); - assert!( - matches!(delegate_probe, Err(JournalLoadError::ColdLoadSkipped)), - "delegate account should remain cold when delegate resolution is OOG" - ); - } - - // ================================================================ - // tx.origin sender validation tests - // ================================================================ - - /// EOA → WRAPPER → callFrom(sender=SPOOFED_SENDER, target=ECHO, data) - /// SPOOFED_SENDER is neither tx.origin (EOA) nor the actual caller (WRAPPER), - /// so the sender validation rejects it. - #[test] - fn test_call_from_contract_sender_spoofing_rejected() { - let contract_a_code = wrapper_call_bytecode(CALL_FROM_ADDRESS); - let contract_b_code = echo_double_bytecode(); - const CONTRACT_A: Address = WRAPPER; - const CONTRACT_B: Address = ECHO_CONTRACT; - - let mut evm = setup_test_evm( - &[(EOA, U256::from(1_000_000))], - &[(CONTRACT_A, contract_a_code), (CONTRACT_B, contract_b_code)], - &[CONTRACT_A], - ); - - let inner_calldata = U256::from(42).to_be_bytes::<32>().to_vec(); - let call_from_input = - encode_call_from_input(SPOOFED_SENDER, CONTRACT_B, &inner_calldata); - - let tx = TxEnv { - caller: EOA, - kind: TxKind::Call(CONTRACT_A), - value: U256::ZERO, - gas_limit: 1_000_000, - gas_price: 0, - chain_id: Some(LOCAL_DEV.chain_id()), - data: call_from_input, - ..Default::default() - }; - - let result = evm.transact_one(tx).expect("transact_one should succeed"); - match &result { - ExecutionResult::Success { output, .. } => { - let reason = decode_revert_reason(output.data()); - assert_eq!(reason, "sender spoofing requires tx.origin as sender"); - } - other => panic!("expected Success (wrapper catches revert), got {other:?}"), - } - } - - /// complete_subcall error should consume all gas allocated to the subcall. - #[test] - fn test_complete_subcall_error_consumes_all_gas() { - use crate::subcall::AllowedCallers; - use crate::subcall_test::{ - FailingCompleteSubcallPrecompile, SubcallTestPrecompile, - FAILING_COMPLETE_SUBCALL_ADDRESS, SUBCALL_TEST_ADDRESS, - }; - - let wrapper_code = wrapper_call_bytecode(FAILING_COMPLETE_SUBCALL_ADDRESS); - let echo_code = echo_double_bytecode(); - let normal_wrapper_code = wrapper_call_bytecode(SUBCALL_TEST_ADDRESS); - const FAILING_WRAPPER: Address = WRAPPER; - const NORMAL_WRAPPER: Address = address!("c000000000000000000000000000000000000010"); - - let chain_spec = LOCAL_DEV.clone(); - let mut db = InMemoryDB::default(); - db.insert_account_info( - EOA, - AccountInfo { - balance: U256::from(1_000_000), - nonce: 0, - code_hash: alloy_primitives::KECCAK256_EMPTY, - code: None, - account_id: None, - }, - ); - for (addr, code) in [ - (FAILING_WRAPPER, wrapper_code), - (NORMAL_WRAPPER, normal_wrapper_code), - (ECHO_CONTRACT, echo_code), - ] { - db.insert_account_info( - addr, - AccountInfo { - balance: U256::ZERO, - nonce: 1, - code_hash: keccak256(&code), - code: Some(Bytecode::new_raw(code)), - account_id: None, - }, - ); - } - - let spec = - reth_ethereum::evm::revm_spec_by_timestamp_and_block_number(&chain_spec, 0, 0); - let hardfork_flags = chain_spec.get_hardfork_flags(0, 0); - let mut cfg_env = revm::context::CfgEnv::new() - .with_chain_id(chain_spec.chain_id()) - .with_spec_and_mainnet_gas_params(spec); - // Disable EIP-7825 tx gas limit cap so tests can use arbitrary gas limits. - cfg_env.tx_gas_limit_cap = Some(u64::MAX); - let ctx = EthEvmContext::new(db, spec) - .with_cfg(cfg_env) - .with_block(BlockEnv::default()); - let precompiles = ArcPrecompileProvider::create_precompiles_map(spec, hardfork_flags); - let instruction = EthInstructions::new_mainnet_with_spec(spec); - - let mut registry = SubcallRegistry::new(); - registry.register( - FAILING_COMPLETE_SUBCALL_ADDRESS, - Arc::new(FailingCompleteSubcallPrecompile), - AllowedCallers::Unrestricted, - ); - registry.register( - SUBCALL_TEST_ADDRESS, - Arc::new(SubcallTestPrecompile), - AllowedCallers::Unrestricted, - ); - - let mut evm = ArcEvm::new( - ctx, - revm::inspector::NoOpInspector {}, - precompiles, - instruction, - false, - hardfork_flags, - Arc::new(registry), - ); - - // Tx 1: call via FailingCompleteSubcallPrecompile — complete_subcall errors, should consume more gas - let inner_calldata = U256::from(42).to_be_bytes::<32>().to_vec(); - let input = encode_subcall_test_input(ECHO_CONTRACT, &inner_calldata); - let tx_failing = TxEnv { - caller: EOA, - kind: TxKind::Call(FAILING_WRAPPER), - value: U256::ZERO, - gas_limit: 1_000_000, - gas_price: 0, - chain_id: Some(LOCAL_DEV.chain_id()), - data: input.clone(), - ..Default::default() - }; - - let result_failing = evm.transact_one(tx_failing).expect("tx should succeed"); - let gas_used_failing = match &result_failing { - ExecutionResult::Success { gas_used, .. } => *gas_used, - other => panic!("expected Success (wrapper catches revert), got {other:?}"), - }; - - // Tx 2: call via normal SubcallTestPrecompile — complete_subcall succeeds, uses less gas - let tx_normal = TxEnv { - caller: EOA, - kind: TxKind::Call(NORMAL_WRAPPER), - value: U256::ZERO, - gas_limit: 1_000_000, - gas_price: 0, - nonce: 1, - chain_id: Some(LOCAL_DEV.chain_id()), - data: input, - ..Default::default() - }; - - let result_normal = evm.transact_one(tx_normal).expect("tx should succeed"); - let gas_used_normal = match &result_normal { - ExecutionResult::Success { gas_used, .. } => *gas_used, - other => panic!("expected Success, got {other:?}"), - }; - - // complete_subcall error consumes ALL gas allocated to the subcall, while a successful - // complete_subcall returns unused gas. So the failing path should use more gas. - assert!( - gas_used_failing > gas_used_normal, - "complete_subcall error ({gas_used_failing}) should consume more gas than \ - normal path ({gas_used_normal}) because all subcall gas is consumed" - ); - } - - /// Bytecode: SSTORE(slot=0, value=42) then RETURN. Used to verify state revert. - fn sstore_42_bytecode() -> Bytes { - #[rustfmt::skip] - let code = vec![ - PUSH1, 42, // value = 42 - PUSH1, 0x00, // slot = 0 - SSTORE, // SSTORE(0, 42) - PUSH1, 0x00, - PUSH1, 0x00, - RETURN, // RETURN(0, 0) - ]; - Bytes::from(code) - } - - /// Bytecode: SSTORE(slot=0, value=42), burn most remaining gas, then RETURN. - /// - /// Used to force completion accounting to exceed the caller's retained gas while - /// keeping the child frame successful. - fn sstore_42_then_drain_gas_bytecode() -> Bytes { - #[rustfmt::skip] - let code = vec![ - PUSH1, 42, // value = 42 - PUSH1, 0x00, // slot = 0 - SSTORE, // SSTORE(0, 42) - JUMPDEST, // offset 5 - GAS, - PUSH2, 0x00, 0x40, // stop once gasleft <= 64 - LT, // 64 < gasleft - PUSH1, 0x05, - JUMPI, - PUSH1, 0x00, - PUSH1, 0x00, - RETURN, // RETURN(0, 0) - ]; - Bytes::from(code) - } - - /// Bytecode: burn most remaining gas, then REVERT. - /// - /// Used to force completion accounting to exceed the caller's retained gas while - /// keeping the child result in the EVM revert class (not a halt). - fn drain_gas_then_revert_bytecode() -> Bytes { - #[rustfmt::skip] - let code = vec![ - JUMPDEST, // offset 0 - GAS, - PUSH2, 0x00, 0x40, // stop once gasleft <= 64 - LT, // 64 < gasleft - PUSH1, 0x00, - JUMPI, - PUSH1, 0x00, - PUSH1, 0x00, - REVERT, // REVERT(0, 0) - ]; - Bytes::from(code) - } - - /// Control: the gas-draining child bytecode must be capable of persisting its - /// SSTORE when completion accounting does not OOG. - #[test] - fn test_sstore_drain_child_persists_without_excessive_completion_gas() { - let sstore_drain_code = sstore_42_then_drain_gas_bytecode(); - const STORAGE_CONTRACT: Address = address!("c000000000000000000000000000000000000021"); - - let mut evm = setup_test_evm( - &[(EOA, U256::from(1_000_000))], - &[(STORAGE_CONTRACT, sstore_drain_code)], - &[], - ); - - let input = encode_subcall_test_input(STORAGE_CONTRACT, &[]); - evm.inner.ctx.set_tx(TxEnv { - caller: EOA, - kind: TxKind::Call(SUBCALL_TEST_ADDRESS), - value: U256::ZERO, - gas_limit: 100_000, - gas_price: 0, - chain_id: Some(LOCAL_DEV.chain_id()), - data: input, - ..Default::default() - }); - - let result_and_state = evm.replay().expect("replay should succeed"); - assert!( - result_and_state.result.is_success(), - "normal completion should let the direct subcall tx succeed: {:?}", - result_and_state.result, - ); - - let storage_account = result_and_state - .state - .get(&STORAGE_CONTRACT) - .expect("storage contract should have a state diff"); - let slot_value = storage_account - .storage - .get(&U256::ZERO) - .map(|s| s.present_value) - .unwrap_or(U256::ZERO); - assert_eq!( - slot_value, - U256::from(42), - "control child bytecode should persist SSTORE(0, 42)" - ); - } - - /// Completion accounting that exceeds the subcall gas limit must make the - /// subcall fail with OOG and revert successful child state changes. - #[test] - fn test_complete_subcall_completion_oog_reverts_successful_child_state() { - use crate::subcall::AllowedCallers; - use crate::subcall_test::{ - ExcessiveCompleteSubcallPrecompile, EXCESSIVE_COMPLETE_SUBCALL_ADDRESS, - }; - - let wrapper_code = wrapper_call_bytecode(EXCESSIVE_COMPLETE_SUBCALL_ADDRESS); - let sstore_drain_code = sstore_42_then_drain_gas_bytecode(); - const CONTRACT_WRAPPER: Address = WRAPPER; - const STORAGE_CONTRACT: Address = address!("c000000000000000000000000000000000000021"); - - let mut evm = setup_test_evm( - &[(EOA, U256::from(1_000_000))], - &[ - (CONTRACT_WRAPPER, wrapper_code), - (STORAGE_CONTRACT, sstore_drain_code), - ], - &[], - ); - - let mut registry = SubcallRegistry::new(); - registry.register( - EXCESSIVE_COMPLETE_SUBCALL_ADDRESS, - Arc::new(ExcessiveCompleteSubcallPrecompile), - AllowedCallers::Unrestricted, - ); - evm.subcall_registry = Arc::new(registry); - - let input = encode_subcall_test_input(STORAGE_CONTRACT, &[]); - evm.inner.ctx.set_tx(TxEnv { - caller: EOA, - kind: TxKind::Call(CONTRACT_WRAPPER), - value: U256::ZERO, - gas_limit: 100_000, - gas_price: 0, - chain_id: Some(LOCAL_DEV.chain_id()), - data: input, - ..Default::default() - }); - - let result_and_state = evm.replay().expect("replay should succeed"); - assert!( - result_and_state.result.is_success(), - "wrapper should succeed after catching the inner OOG: {:?}", - result_and_state.result, - ); - - let ExecutionResult::Success { output, .. } = &result_and_state.result else { - panic!("expected successful wrapper result"); - }; - assert!( - output.data().is_empty(), - "inner completion OOG should not return successful subcall returndata" - ); - - let storage_account = result_and_state - .state - .get(&STORAGE_CONTRACT) - .expect("storage contract should appear in state diff (child did execute)"); - let slot_value = storage_account - .storage - .get(&U256::ZERO) - .map(|s| s.present_value) - .unwrap_or(U256::ZERO); - assert_eq!( - slot_value, - U256::ZERO, - "child's SSTORE(0, 42) should have been reverted after completion OOG" - ); - } - - /// Completion accounting that exceeds the subcall gas limit must OOG even - /// when the child result is a revert rather than a halt. - #[test] - fn test_complete_subcall_child_revert_excessive_completion_gas_oogs() { - use crate::subcall::AllowedCallers; - use crate::subcall_test::{ - ExcessiveCompleteSubcallPrecompile, EXCESSIVE_COMPLETE_SUBCALL_ADDRESS, - }; - - const REVERT_DRAINER: Address = address!("c000000000000000000000000000000000000022"); - let revert_drainer_code = drain_gas_then_revert_bytecode(); - - let mut evm = setup_test_evm( - &[(EOA, U256::from(1_000_000))], - &[(REVERT_DRAINER, revert_drainer_code)], - &[], - ); - - let mut registry = SubcallRegistry::new(); - registry.register( - EXCESSIVE_COMPLETE_SUBCALL_ADDRESS, - Arc::new(ExcessiveCompleteSubcallPrecompile), - AllowedCallers::Unrestricted, - ); - evm.subcall_registry = Arc::new(registry); - - let input = encode_subcall_test_input(REVERT_DRAINER, &[]); - let tx = TxEnv { - caller: EOA, - kind: TxKind::Call(EXCESSIVE_COMPLETE_SUBCALL_ADDRESS), - value: U256::ZERO, - gas_limit: 100_000, - gas_price: 0, - chain_id: Some(LOCAL_DEV.chain_id()), - data: input, - ..Default::default() - }; - - let result = evm.transact_one(tx).expect("tx should execute"); - match result { - ExecutionResult::Halt { reason, .. } => { - assert!( - matches!(reason, HaltReason::OutOfGas(_)), - "excessive child-revert completion gas should OOG" - ); - } - other => panic!("expected direct subcall tx to halt OOG, got {other:?}"), - } - } - - /// Completion gas must also be accounted when the child halts. Calling the - /// subcall precompile directly lets the top-level result distinguish OOG from - /// a normal completion revert. - #[test] - fn test_complete_subcall_child_halt_excessive_completion_gas_oogs() { - use crate::subcall::AllowedCallers; - use crate::subcall_test::{ - ExcessiveCompleteSubcallPrecompile, EXCESSIVE_COMPLETE_SUBCALL_ADDRESS, - }; - - const GAS_BURNER: Address = address!("c000000000000000000000000000000000000006"); - let gas_burner_code = gas_burner_bytecode(); - - let mut evm = setup_test_evm( - &[(EOA, U256::from(1_000_000))], - &[(GAS_BURNER, gas_burner_code)], - &[], - ); - - let mut registry = SubcallRegistry::new(); - registry.register( - EXCESSIVE_COMPLETE_SUBCALL_ADDRESS, - Arc::new(ExcessiveCompleteSubcallPrecompile), - AllowedCallers::Unrestricted, - ); - evm.subcall_registry = Arc::new(registry); - - let input = encode_subcall_test_input(GAS_BURNER, &[]); - let tx = TxEnv { - caller: EOA, - kind: TxKind::Call(EXCESSIVE_COMPLETE_SUBCALL_ADDRESS), - value: U256::ZERO, - gas_limit: 100_000, - gas_price: 0, - chain_id: Some(LOCAL_DEV.chain_id()), - data: input, - ..Default::default() - }; - - let result = evm.transact_one(tx).expect("tx should execute"); - match result { - ExecutionResult::Halt { reason, .. } => { - assert!( - matches!(reason, HaltReason::OutOfGas(_)), - "excessive child-halt completion gas should OOG" - ); - } - other => panic!("expected direct subcall tx to halt OOG, got {other:?}"), - } - } - - /// Nonzero completion gas on a halted child should preserve the existing - /// full-allocation burn behavior when the completion gas fits in retained gas. - #[test] - fn test_complete_subcall_child_halt_completion_gas_fits_reverts() { - use crate::subcall::AllowedCallers; - use crate::subcall_test::{ - CostlyCompleteSubcallPrecompile, COSTLY_COMPLETE_SUBCALL_ADDRESS, - }; - - const GAS_BURNER: Address = address!("c000000000000000000000000000000000000006"); - let gas_burner_code = gas_burner_bytecode(); - - let mut evm = setup_test_evm( - &[(EOA, U256::from(1_000_000))], - &[(GAS_BURNER, gas_burner_code)], - &[], - ); - - let mut registry = SubcallRegistry::new(); - registry.register( - COSTLY_COMPLETE_SUBCALL_ADDRESS, - Arc::new(CostlyCompleteSubcallPrecompile), - AllowedCallers::Unrestricted, - ); - evm.subcall_registry = Arc::new(registry); - - let input = encode_subcall_test_input(GAS_BURNER, &[]); - let gas_limit = 100_000; - let tx = TxEnv { - caller: EOA, - kind: TxKind::Call(COSTLY_COMPLETE_SUBCALL_ADDRESS), - value: U256::ZERO, - gas_limit, - gas_price: 0, - chain_id: Some(LOCAL_DEV.chain_id()), - data: input, - ..Default::default() - }; - - let result = evm.transact_one(tx).expect("tx should execute"); - match result { - ExecutionResult::Revert { gas_used, output } => { - assert_eq!( - gas_used, gas_limit, - "halted child with fitting completion gas should burn the full subcall allocation" - ); - assert!( - output.is_empty(), - "halted child should return empty revert data" - ); - } - other => panic!("expected direct subcall tx to revert, got {other:?}"), - } - } - - /// Exact gas accounting boundary: gas_used == gas_limit succeeds, but - /// gas_used == gas_limit + 1 OOGs. - #[test] - fn test_complete_subcall_completion_gas_exact_boundary() { - use arc_precompiles::subcall::{ - SubcallCompletionResult, SubcallContinuationData, SubcallError, SubcallInitResult, - SubcallPrecompile, - }; - - #[derive(Debug)] - struct FixedCompleteSubcallPrecompile { - completion_gas: u64, - success: bool, - } - - impl SubcallPrecompile for FixedCompleteSubcallPrecompile { - fn init_subcall( - &self, - _inputs: &CallInputs, - ) -> Result { - unreachable!("test calls complete_subcall directly") - } - - fn complete_subcall( - &self, - _continuation_data: SubcallContinuationData, - _child_result: &FrameResult, - ) -> Result { - Ok(SubcallCompletionResult { - output: Bytes::new(), - success: self.success, - gas_overhead: self.completion_gas, - }) - } - } - - fn run_with_gas_limit( - gas_limit: u64, - child_instruction_result: InstructionResult, - child_gas_spent: u64, - completion_success: bool, - ) -> FrameResult { - let mut evm = setup_test_evm(&[(EOA, U256::from(1_000_000))], &[], &[]); - let checkpoint = evm.inner.ctx.journal_mut().checkpoint(); - evm.inner.ctx.journal_mut().checkpoint_commit(); - - let mut child_gas = Gas::new(10); - assert!(child_gas.record_cost(child_gas_spent)); - let child_result = FrameResult::Call(CallOutcome { - result: InterpreterResult::new( - child_instruction_result, - Bytes::new(), - child_gas, - ), - memory_offset: 0..0, - was_precompile_called: false, - precompile_call_logs: Default::default(), - }); - - evm.complete_subcall( - child_result, - SubcallContinuation { - precompile: Arc::new(FixedCompleteSubcallPrecompile { - completion_gas: 5, - success: completion_success, - }), - gas_limit, - init_subcall_gas_overhead: 3, - return_memory_offset: 0..0, - continuation_data: SubcallContinuationData { - state: Box::new(()), - }, - checkpoint, - }, - ) - .expect("complete_subcall should not hit db errors") - } - - let FrameResult::Call(exact_outcome) = - run_with_gas_limit(12, InstructionResult::Return, 4, true) - else { - panic!("expected call outcome for exact-fit case"); - }; - assert_eq!( - exact_outcome.result.result, - InstructionResult::Return, - "exact gas fit should not OOG" - ); - assert_eq!( - exact_outcome.result.gas.spent(), - 12, - "exact gas fit should spend the metered gas" - ); - - let FrameResult::Call(oog_outcome) = - run_with_gas_limit(11, InstructionResult::Return, 4, true) - else { - panic!("expected call outcome for OOG case"); - }; - assert_eq!( - oog_outcome.result.result, - InstructionResult::OutOfGas, - "one gas below metered cost should OOG" - ); - assert_eq!( - oog_outcome.result.gas.spent(), - 11, - "OOG should spend the full subcall gas limit" - ); - - let FrameResult::Call(halted_exact_outcome) = - run_with_gas_limit(18, InstructionResult::OutOfGas, 10, false) - else { - panic!("expected call outcome for halted exact-fit case"); - }; - assert_eq!( - halted_exact_outcome.result.result, - InstructionResult::Revert, - "halted child with exact gas fit should not OOG" - ); - assert_eq!( - halted_exact_outcome.result.gas.spent(), - 18, - "halted exact gas fit should spend the metered gas" - ); - - let FrameResult::Call(halted_oog_outcome) = - run_with_gas_limit(17, InstructionResult::OutOfGas, 10, false) - else { - panic!("expected call outcome for halted OOG case"); - }; - assert_eq!( - halted_oog_outcome.result.result, - InstructionResult::OutOfGas, - "halted child one gas below metered cost should OOG" - ); - assert_eq!( - halted_oog_outcome.result.gas.spent(), - 17, - "halted OOG should spend the full subcall gas limit" - ); - } - - /// CS-ARC-PC-026: When `complete_subcall` rejects a successful child (returns - /// `success: false`), the child's committed state changes must be reverted. - #[test] - fn test_complete_subcall_rejects_successful_child_reverts_state() { - use crate::subcall::AllowedCallers; - use crate::subcall_test::{ - RejectingCompleteSubcallPrecompile, REJECTING_COMPLETE_SUBCALL_ADDRESS, - }; - - let wrapper_code = wrapper_call_bytecode(REJECTING_COMPLETE_SUBCALL_ADDRESS); - let sstore_code = sstore_42_bytecode(); - const CONTRACT_WRAPPER: Address = WRAPPER; - const STORAGE_CONTRACT: Address = address!("c000000000000000000000000000000000000020"); - - let chain_spec = LOCAL_DEV.clone(); - let mut db = InMemoryDB::default(); - db.insert_account_info( - EOA, - AccountInfo { - balance: U256::from(1_000_000), - nonce: 0, - code_hash: alloy_primitives::KECCAK256_EMPTY, - code: None, - account_id: None, - }, - ); - for (addr, code) in [ - (CONTRACT_WRAPPER, wrapper_code), - (STORAGE_CONTRACT, sstore_code), - ] { - db.insert_account_info( - addr, - AccountInfo { - balance: U256::ZERO, - nonce: 1, - code_hash: keccak256(&code), - code: Some(Bytecode::new_raw(code)), - account_id: None, - }, - ); - } - - let spec = - reth_ethereum::evm::revm_spec_by_timestamp_and_block_number(&chain_spec, 0, 0); - let hardfork_flags = chain_spec.get_hardfork_flags(0, 0); - let mut cfg_env = revm::context::CfgEnv::new() - .with_chain_id(chain_spec.chain_id()) - .with_spec_and_mainnet_gas_params(spec); - cfg_env.tx_gas_limit_cap = Some(u64::MAX); - let ctx = EthEvmContext::new(db, spec) - .with_cfg(cfg_env) - .with_block(BlockEnv::default()); - let precompiles = ArcPrecompileProvider::create_precompiles_map(spec, hardfork_flags); - let instruction = EthInstructions::new_mainnet_with_spec(spec); - - let mut registry = SubcallRegistry::new(); - registry.register( - REJECTING_COMPLETE_SUBCALL_ADDRESS, - Arc::new(RejectingCompleteSubcallPrecompile), - AllowedCallers::Unrestricted, - ); - - let mut evm = ArcEvm::new( - ctx, - revm::inspector::NoOpInspector {}, - precompiles, - instruction, - false, - hardfork_flags, - Arc::new(registry), - ); - - // Call wrapper → RejectingCompleteSubcallPrecompile → STORAGE_CONTRACT - // The child (STORAGE_CONTRACT) writes SSTORE(0, 42) and succeeds. - // But complete_subcall returns success: false, so the child's state must revert. - let input = encode_subcall_test_input(STORAGE_CONTRACT, &[]); - evm.inner.ctx.set_tx(TxEnv { - caller: EOA, - kind: TxKind::Call(CONTRACT_WRAPPER), - value: U256::ZERO, - gas_limit: 1_000_000, - gas_price: 0, - chain_id: Some(LOCAL_DEV.chain_id()), - data: input, - ..Default::default() - }); - - let result_and_state = evm.replay().expect("replay should succeed"); - - // The wrapper catches the revert from the precompile, so outer tx succeeds. - assert!( - result_and_state.result.is_success(), - "wrapper should succeed (it catches inner revert): {:?}", - result_and_state.result, - ); - - // The child's SSTORE(0, 42) must have been reverted. Check that STORAGE_CONTRACT - // either has no entry in the state diff or has slot 0 unchanged. - let storage_account = result_and_state.state.get(&STORAGE_CONTRACT); - if let Some(account) = storage_account { - let slot_value = account - .storage - .get(&U256::ZERO) - .map(|s| s.present_value) - .unwrap_or(U256::ZERO); - assert_eq!( - slot_value, - U256::ZERO, - "child's SSTORE(0, 42) should have been reverted, but slot 0 = {slot_value}" - ); - } - // If STORAGE_CONTRACT isn't in state at all, that's also correct (no writes persisted) - } - - /// CS-ARC-PC-026: Same as above but for the `Err` path from `complete_subcall`. - #[test] - fn test_complete_subcall_error_reverts_successful_child_state() { - use crate::subcall::AllowedCallers; - use crate::subcall_test::{ - FailingCompleteSubcallPrecompile, FAILING_COMPLETE_SUBCALL_ADDRESS, - }; - - let wrapper_code = wrapper_call_bytecode(FAILING_COMPLETE_SUBCALL_ADDRESS); - let sstore_code = sstore_42_bytecode(); - const CONTRACT_WRAPPER: Address = WRAPPER; - const STORAGE_CONTRACT: Address = address!("c000000000000000000000000000000000000020"); - - let chain_spec = LOCAL_DEV.clone(); - let mut db = InMemoryDB::default(); - db.insert_account_info( - EOA, - AccountInfo { - balance: U256::from(1_000_000), - nonce: 0, - code_hash: alloy_primitives::KECCAK256_EMPTY, - code: None, - account_id: None, - }, - ); - for (addr, code) in [ - (CONTRACT_WRAPPER, wrapper_code), - (STORAGE_CONTRACT, sstore_code), - ] { - db.insert_account_info( - addr, - AccountInfo { - balance: U256::ZERO, - nonce: 1, - code_hash: keccak256(&code), - code: Some(Bytecode::new_raw(code)), - account_id: None, - }, - ); - } - - let spec = - reth_ethereum::evm::revm_spec_by_timestamp_and_block_number(&chain_spec, 0, 0); - let hardfork_flags = chain_spec.get_hardfork_flags(0, 0); - let mut cfg_env = revm::context::CfgEnv::new() - .with_chain_id(chain_spec.chain_id()) - .with_spec_and_mainnet_gas_params(spec); - cfg_env.tx_gas_limit_cap = Some(u64::MAX); - let ctx = EthEvmContext::new(db, spec) - .with_cfg(cfg_env) - .with_block(BlockEnv::default()); - let precompiles = ArcPrecompileProvider::create_precompiles_map(spec, hardfork_flags); - let instruction = EthInstructions::new_mainnet_with_spec(spec); - - let mut registry = SubcallRegistry::new(); - registry.register( - FAILING_COMPLETE_SUBCALL_ADDRESS, - Arc::new(FailingCompleteSubcallPrecompile), - AllowedCallers::Unrestricted, - ); - - let mut evm = ArcEvm::new( - ctx, - revm::inspector::NoOpInspector {}, - precompiles, - instruction, - false, - hardfork_flags, - Arc::new(registry), - ); - - let input = encode_subcall_test_input(STORAGE_CONTRACT, &[]); - evm.inner.ctx.set_tx(TxEnv { - caller: EOA, - kind: TxKind::Call(CONTRACT_WRAPPER), - value: U256::ZERO, - gas_limit: 1_000_000, - gas_price: 0, - chain_id: Some(LOCAL_DEV.chain_id()), - data: input, - ..Default::default() - }); - - let result_and_state = evm.replay().expect("replay should succeed"); - - assert!( - result_and_state.result.is_success(), - "wrapper should succeed (it catches inner revert): {:?}", - result_and_state.result, - ); - - let storage_account = result_and_state.state.get(&STORAGE_CONTRACT); - if let Some(account) = storage_account { - let slot_value = account - .storage - .get(&U256::ZERO) - .map(|s| s.present_value) - .unwrap_or(U256::ZERO); - assert_eq!( - slot_value, - U256::ZERO, - "child's SSTORE(0, 42) should have been reverted, but slot 0 = {slot_value}" - ); - } - } - - /// Regression test: after a rejected subcall completes (including the revert path), - /// the child target address remains warm. A second subcall to the same target in the - /// same transaction should use WARM_STORAGE_READ_COST, not COLD_ACCOUNT_ACCESS_COST. - /// - /// Uses a double-call wrapper that invokes the rejecting precompile twice. Compares - /// two runs: one targeting the same contract both times (second call warm), one - /// targeting two different contracts (both calls cold). The gas delta should equal - /// COLD_ACCOUNT_ACCESS_COST - WARM_STORAGE_READ_COST = 2500. - #[test] - fn test_rejected_subcall_target_stays_warm() { - use crate::subcall::AllowedCallers; - use crate::subcall_test::{ - RejectingCompleteSubcallPrecompile, REJECTING_COMPLETE_SUBCALL_ADDRESS, - }; - use revm_interpreter::gas::{COLD_ACCOUNT_ACCESS_COST, WARM_STORAGE_READ_COST}; - - let double_wrapper = double_call_wrapper_bytecode(REJECTING_COMPLETE_SUBCALL_ADDRESS); - let sstore_code = sstore_42_bytecode(); - const DOUBLE_WRAPPER: Address = address!("c000000000000000000000000000000000000030"); - const STORAGE_A: Address = address!("c000000000000000000000000000000000000020"); - const STORAGE_B: Address = address!("c000000000000000000000000000000000000021"); - - let chain_spec = LOCAL_DEV.clone(); - let spec = - reth_ethereum::evm::revm_spec_by_timestamp_and_block_number(&chain_spec, 0, 0); - let hardfork_flags = chain_spec.get_hardfork_flags(0, 0); - - let input_a = encode_subcall_test_input(STORAGE_A, &[]); - let input_b = encode_subcall_test_input(STORAGE_B, &[]); - - let run_double = |payload1: &[u8], payload2: &[u8]| -> u64 { - let mut db = InMemoryDB::default(); - db.insert_account_info( - EOA, - AccountInfo { - balance: U256::from(1_000_000), - nonce: 0, - code_hash: alloy_primitives::KECCAK256_EMPTY, - code: None, - account_id: None, - }, - ); - for (addr, code) in [ - (DOUBLE_WRAPPER, double_wrapper.clone()), - (STORAGE_A, sstore_code.clone()), - (STORAGE_B, sstore_code.clone()), - ] { - db.insert_account_info( - addr, - AccountInfo { - balance: U256::ZERO, - nonce: 1, - code_hash: keccak256(&code), - code: Some(Bytecode::new_raw(code)), - account_id: None, - }, - ); - } - let mut cfg_env = revm::context::CfgEnv::new() - .with_chain_id(chain_spec.chain_id()) - .with_spec_and_mainnet_gas_params(spec); - cfg_env.tx_gas_limit_cap = Some(u64::MAX); - let ctx = EthEvmContext::new(db, spec) - .with_cfg(cfg_env) - .with_block(BlockEnv::default()); - let mut registry = SubcallRegistry::new(); - registry.register( - REJECTING_COMPLETE_SUBCALL_ADDRESS, - Arc::new(RejectingCompleteSubcallPrecompile), - AllowedCallers::Unrestricted, - ); - let mut evm = ArcEvm::new( - ctx, - revm::inspector::NoOpInspector {}, - ArcPrecompileProvider::create_precompiles_map(spec, hardfork_flags), - EthInstructions::new_mainnet_with_spec(spec), - false, - hardfork_flags, - Arc::new(registry), - ); - - let mut calldata = Vec::new(); - calldata.extend_from_slice(&U256::from(payload1.len()).to_be_bytes::<32>()); - calldata.extend_from_slice(payload1); - calldata.extend_from_slice(payload2); - - evm.inner.ctx.set_tx(TxEnv { - caller: EOA, - kind: TxKind::Call(DOUBLE_WRAPPER), - value: U256::ZERO, - gas_limit: 1_000_000, - gas_price: 0, - chain_id: Some(chain_spec.chain_id()), - data: Bytes::from(calldata), - ..Default::default() - }); - let result = evm.replay().expect("replay should succeed"); - assert!(result.result.is_success(), "double wrapper should succeed"); - result.result.gas_used() - }; - - // Both-cold: target A then target B (different addresses, both cold) - let gas_both_cold = run_double(&input_a, &input_b); - // Same-target: target A then target A (second access is warm) - let gas_second_warm = run_double(&input_a, &input_a); - - // The only difference between the two runs is that in the second case, - // STORAGE_A is already warm from the first subcall. Delta = COLD - WARM. - let savings = gas_both_cold - gas_second_warm; - let expected_savings = COLD_ACCOUNT_ACCESS_COST - WARM_STORAGE_READ_COST; - assert_eq!( - savings, expected_savings, - "second subcall to same target should save exactly {expected_savings} gas \ - (cold={COLD_ACCOUNT_ACCESS_COST} - warm={WARM_STORAGE_READ_COST}), \ - got {savings} (gas_both_cold={gas_both_cold}, gas_second_warm={gas_second_warm})" - ); - } - - /// complete_subcall gas overhead (ABI encoding) should be included in total gas consumed. - /// - /// Proves the framework charges the reported `gas_overhead` from `complete_subcall`. - /// - /// Compares SubcallTestPrecompile (gas_overhead=0) against CostlyCompleteSubcallPrecompile - /// (gas_overhead=500). Both have identical init_subcall logic, so the gas delta is - /// exactly the difference in reported completion gas. - #[test] - fn test_complete_subcall_gas_overhead_is_charged() { - use crate::subcall::AllowedCallers; - use crate::subcall_test::{ - CostlyCompleteSubcallPrecompile, SubcallTestPrecompile, - COSTLY_COMPLETE_GAS_OVERHEAD, COSTLY_COMPLETE_SUBCALL_ADDRESS, - SUBCALL_TEST_ADDRESS, - }; - - let zero_wrapper_code = wrapper_call_bytecode(SUBCALL_TEST_ADDRESS); - let costly_wrapper_code = wrapper_call_bytecode(COSTLY_COMPLETE_SUBCALL_ADDRESS); - let echo_code = echo_double_bytecode(); - const ZERO_WRAPPER: Address = WRAPPER; - const COSTLY_WRAPPER: Address = address!("c000000000000000000000000000000000000011"); - - let chain_spec = LOCAL_DEV.clone(); - let mut db = InMemoryDB::default(); - db.insert_account_info( - EOA, - AccountInfo { - balance: U256::from(1_000_000), - nonce: 0, - code_hash: alloy_primitives::KECCAK256_EMPTY, - code: None, - account_id: None, - }, - ); - for (addr, code) in [ - (ZERO_WRAPPER, zero_wrapper_code), - (COSTLY_WRAPPER, costly_wrapper_code), - (ECHO_CONTRACT, echo_code), - ] { - db.insert_account_info( - addr, - AccountInfo { - balance: U256::ZERO, - nonce: 1, - code_hash: keccak256(&code), - code: Some(Bytecode::new_raw(code)), - account_id: None, - }, - ); - } - - let spec = - reth_ethereum::evm::revm_spec_by_timestamp_and_block_number(&chain_spec, 0, 0); - let hardfork_flags = chain_spec.get_hardfork_flags(0, 0); - let mut cfg_env = revm::context::CfgEnv::new() - .with_chain_id(chain_spec.chain_id()) - .with_spec_and_mainnet_gas_params(spec); - cfg_env.tx_gas_limit_cap = Some(u64::MAX); - let ctx = EthEvmContext::new(db, spec) - .with_cfg(cfg_env) - .with_block(BlockEnv::default()); - let precompiles = ArcPrecompileProvider::create_precompiles_map(spec, hardfork_flags); - let instruction = EthInstructions::new_mainnet_with_spec(spec); - - let mut registry = SubcallRegistry::new(); - registry.register( - SUBCALL_TEST_ADDRESS, - Arc::new(SubcallTestPrecompile), - AllowedCallers::Unrestricted, - ); - registry.register( - COSTLY_COMPLETE_SUBCALL_ADDRESS, - Arc::new(CostlyCompleteSubcallPrecompile), - AllowedCallers::Unrestricted, - ); - - let mut evm = ArcEvm::new( - ctx, - revm::inspector::NoOpInspector {}, - precompiles, - instruction, - false, - hardfork_flags, - Arc::new(registry), - ); - - let inner_calldata = U256::from(42).to_be_bytes::<32>().to_vec(); - - // Tx 1: SubcallTestPrecompile — gas_overhead = 0 - let input_zero = encode_subcall_test_input(ECHO_CONTRACT, &inner_calldata); - let tx_zero = TxEnv { - caller: EOA, - kind: TxKind::Call(ZERO_WRAPPER), - value: U256::ZERO, - gas_limit: 1_000_000, - gas_price: 0, - chain_id: Some(LOCAL_DEV.chain_id()), - data: input_zero, - ..Default::default() - }; - - let result_zero = evm.transact_one(tx_zero).expect("tx should succeed"); - let gas_used_zero = match &result_zero { - ExecutionResult::Success { gas_used, .. } => *gas_used, - other => panic!("expected Success, got {other:?}"), - }; - - // Tx 2: CostlyCompleteSubcallPrecompile — gas_overhead = 500 - let input_costly = encode_subcall_test_input(ECHO_CONTRACT, &inner_calldata); - let tx_costly = TxEnv { - caller: EOA, - kind: TxKind::Call(COSTLY_WRAPPER), - value: U256::ZERO, - gas_limit: 1_000_000, - gas_price: 0, - nonce: 1, - chain_id: Some(LOCAL_DEV.chain_id()), - data: input_costly, - ..Default::default() - }; - - let result_costly = evm.transact_one(tx_costly).expect("tx should succeed"); - let gas_used_costly = match &result_costly { - ExecutionResult::Success { gas_used, .. } => *gas_used, - other => panic!("expected Success, got {other:?}"), - }; - - // The only difference between the two precompiles is gas_overhead (0 vs 500). - // The delta must be exactly COSTLY_COMPLETE_GAS_OVERHEAD. - assert_eq!( - gas_used_costly - gas_used_zero, - COSTLY_COMPLETE_GAS_OVERHEAD, - "gas delta should equal the reported completion gas_overhead ({COSTLY_COMPLETE_GAS_OVERHEAD})" - ); - } - - /// Completion gas is charged even when the child reverts (not halts). - /// Uses the same CostlyCompleteSubcallPrecompile pair, targeting a reverting contract. - #[test] - fn test_complete_subcall_gas_overhead_charged_on_child_revert() { - use crate::subcall::AllowedCallers; - use crate::subcall_test::{ - CostlyCompleteSubcallPrecompile, SubcallTestPrecompile, - COSTLY_COMPLETE_GAS_OVERHEAD, COSTLY_COMPLETE_SUBCALL_ADDRESS, - SUBCALL_TEST_ADDRESS, - }; - - let zero_wrapper_code = wrapper_call_bytecode(SUBCALL_TEST_ADDRESS); - let costly_wrapper_code = wrapper_call_bytecode(COSTLY_COMPLETE_SUBCALL_ADDRESS); - let revert_code = reverting_bytecode(); - const ZERO_WRAPPER: Address = WRAPPER; - const COSTLY_WRAPPER: Address = address!("c000000000000000000000000000000000000011"); - const REVERTING: Address = address!("c000000000000000000000000000000000000012"); - - let chain_spec = LOCAL_DEV.clone(); - let mut db = InMemoryDB::default(); - db.insert_account_info( - EOA, - AccountInfo { - balance: U256::from(1_000_000), - nonce: 0, - code_hash: alloy_primitives::KECCAK256_EMPTY, - code: None, - account_id: None, - }, - ); - for (addr, code) in [ - (ZERO_WRAPPER, zero_wrapper_code), - (COSTLY_WRAPPER, costly_wrapper_code), - (REVERTING, revert_code), - ] { - db.insert_account_info( - addr, - AccountInfo { - balance: U256::ZERO, - nonce: 1, - code_hash: keccak256(&code), - code: Some(Bytecode::new_raw(code)), - account_id: None, - }, - ); - } - - let spec = - reth_ethereum::evm::revm_spec_by_timestamp_and_block_number(&chain_spec, 0, 0); - let hardfork_flags = chain_spec.get_hardfork_flags(0, 0); - let mut cfg_env = revm::context::CfgEnv::new() - .with_chain_id(chain_spec.chain_id()) - .with_spec_and_mainnet_gas_params(spec); - cfg_env.tx_gas_limit_cap = Some(u64::MAX); - let ctx = EthEvmContext::new(db, spec) - .with_cfg(cfg_env) - .with_block(BlockEnv::default()); - let precompiles = ArcPrecompileProvider::create_precompiles_map(spec, hardfork_flags); - let instruction = EthInstructions::new_mainnet_with_spec(spec); - - let mut registry = SubcallRegistry::new(); - registry.register( - SUBCALL_TEST_ADDRESS, - Arc::new(SubcallTestPrecompile), - AllowedCallers::Unrestricted, - ); - registry.register( - COSTLY_COMPLETE_SUBCALL_ADDRESS, - Arc::new(CostlyCompleteSubcallPrecompile), - AllowedCallers::Unrestricted, - ); - - let mut evm = ArcEvm::new( - ctx, - revm::inspector::NoOpInspector {}, - precompiles, - instruction, - false, - hardfork_flags, - Arc::new(registry), - ); - - // Target a reverting contract — child reverts, but does NOT halt. - let inner_calldata: Vec = vec![]; - - // Tx 1: SubcallTestPrecompile (gas_overhead=0) with reverting child - let input_zero = encode_subcall_test_input(REVERTING, &inner_calldata); - let tx_zero = TxEnv { - caller: EOA, - kind: TxKind::Call(ZERO_WRAPPER), - value: U256::ZERO, - gas_limit: 1_000_000, - gas_price: 0, - chain_id: Some(LOCAL_DEV.chain_id()), - data: input_zero, - ..Default::default() - }; - - let result_zero = evm.transact_one(tx_zero).expect("tx should succeed"); - let gas_used_zero = match &result_zero { - ExecutionResult::Success { gas_used, .. } => *gas_used, - other => panic!("expected Success (wrapper catches revert), got {other:?}"), - }; - - // Tx 2: CostlyCompleteSubcallPrecompile (gas_overhead=500) with reverting child - let input_costly = encode_subcall_test_input(REVERTING, &inner_calldata); - let tx_costly = TxEnv { - caller: EOA, - kind: TxKind::Call(COSTLY_WRAPPER), - value: U256::ZERO, - gas_limit: 1_000_000, - gas_price: 0, - nonce: 1, - chain_id: Some(LOCAL_DEV.chain_id()), - data: input_costly, - ..Default::default() - }; - - let result_costly = evm.transact_one(tx_costly).expect("tx should succeed"); - let gas_used_costly = match &result_costly { - ExecutionResult::Success { gas_used, .. } => *gas_used, - other => panic!("expected Success (wrapper catches revert), got {other:?}"), - }; - - // Even with a reverting child, completion gas is charged. - assert_eq!( - gas_used_costly - gas_used_zero, - COSTLY_COMPLETE_GAS_OVERHEAD, - "completion gas_overhead must be charged even when child reverts" - ); - } - - /// Unit test: CallFrom complete_subcall reports correct gas_overhead for various data sizes. - #[test] - fn test_call_from_complete_subcall_gas_overhead() { - use arc_precompiles::call_from::abi_encode_gas; - use arc_precompiles::subcall::{SubcallContinuationData, SubcallPrecompile}; - - let precompile = CallFromPrecompile; - - // Helper: create a FrameResult::Call with given output bytes - let make_child_result = |output: Bytes| -> FrameResult { - FrameResult::Call(CallOutcome { - result: InterpreterResult::new( - InstructionResult::Return, - output, - Gas::new(100_000), - ), - memory_offset: 0..0, - was_precompile_called: false, - precompile_call_logs: Default::default(), - }) - }; - - let continuation_data = SubcallContinuationData { - state: Box::new(()), - }; - - // Empty output - let result = precompile - .complete_subcall(continuation_data, &make_child_result(Bytes::new())) - .expect("complete_subcall should succeed"); - assert_eq!(result.gas_overhead, abi_encode_gas(0)); - - // 32 bytes output - let continuation_data = SubcallContinuationData { - state: Box::new(()), - }; - let result = precompile - .complete_subcall( - continuation_data, - &make_child_result(Bytes::from(vec![0u8; 32])), - ) - .expect("complete_subcall should succeed"); - assert_eq!(result.gas_overhead, abi_encode_gas(32)); - - // 33 bytes output (rounds up to 2 words) - let continuation_data = SubcallContinuationData { - state: Box::new(()), - }; - let result = precompile - .complete_subcall( - continuation_data, - &make_child_result(Bytes::from(vec![0u8; 33])), - ) - .expect("complete_subcall should succeed"); - assert_eq!(result.gas_overhead, abi_encode_gas(33)); - // 33 bytes = 2 words → base + 2*COPY - assert_eq!(result.gas_overhead, 100 + 2 * 3); - } - - // These tests verify that `init_subcall` correctly charges EIP-2929 account - // access costs for the child target address. - - /// Integration test: CallFrom targeting a cold account should cost more gas - /// than targeting a warm one (pre-warmed via access_list). - /// - /// Uses two separate EVM instances so each transaction starts with a fresh - /// journal — reusing one EVM would leave addresses warm from the first tx. - #[test] - fn test_call_from_cold_target_costs_more_gas_than_warm() { - let contract_a_code = wrapper_call_bytecode(CALL_FROM_ADDRESS); - let contract_b_code = echo_double_bytecode(); - const CONTRACT_A: Address = WRAPPER; - const CONTRACT_B: Address = ECHO_CONTRACT; - - let inner_calldata = U256::from(42).to_be_bytes::<32>().to_vec(); - let call_from_input = encode_call_from_input(EOA, CONTRACT_B, &inner_calldata); - - // Cold target: fresh EVM, no access_list - let mut evm_cold = setup_test_evm( - &[(EOA, U256::from(1_000_000))], - &[ - (CONTRACT_A, contract_a_code.clone()), - (CONTRACT_B, contract_b_code.clone()), - ], - &[CONTRACT_A], - ); - let tx_cold = TxEnv { - tx_type: 1, // EIP-2930 - caller: EOA, - kind: TxKind::Call(CONTRACT_A), - value: U256::ZERO, - gas_limit: 200_000, - gas_price: 0, - chain_id: Some(LOCAL_DEV.chain_id()), - data: call_from_input.clone(), - access_list: Default::default(), - ..Default::default() - }; - let result_cold = evm_cold - .transact_one(tx_cold) - .expect("cold tx should succeed"); - let gas_cold = match &result_cold { - ExecutionResult::Success { gas_used, .. } => *gas_used, - other => panic!("expected Success, got {other:?}"), - }; - - // Warm target: fresh EVM, pre-warm CONTRACT_B via access_list. - // tx_type must be EIP-2930 (1) so revm processes the access_list warmup; - // the default Legacy type skips access list handling entirely. - let mut evm_warm = setup_test_evm( - &[(EOA, U256::from(1_000_000))], - &[(CONTRACT_A, contract_a_code), (CONTRACT_B, contract_b_code)], - &[CONTRACT_A], - ); - let tx_warm = TxEnv { - tx_type: 1, // EIP-2930 - caller: EOA, - kind: TxKind::Call(CONTRACT_A), - value: U256::ZERO, - gas_limit: 200_000, - gas_price: 0, - chain_id: Some(LOCAL_DEV.chain_id()), - data: call_from_input, - access_list: vec![alloy_eips::eip2930::AccessListItem { - address: CONTRACT_B, - storage_keys: vec![], - }] - .into(), - ..Default::default() - }; - let result_warm = evm_warm - .transact_one(tx_warm) - .expect("warm tx should succeed"); - let gas_warm = match &result_warm { - ExecutionResult::Success { gas_used, .. } => *gas_used, - other => panic!("expected Success, got {other:?}"), - }; - - // Both txs are EIP-2930; the only difference is the access_list entry. - // Delta = COLD_ACCOUNT_ACCESS_COST (2600) − ACCESS_LIST_ADDRESS_COST (2400) - // - WARM_STORAGE_READ_COST (100) = 100. - assert_eq!( - gas_cold - gas_warm, - 100, - "cold/warm gas delta should be exactly 100 (COLD_ACCOUNT_ACCESS_COST 2600 \ - - ACCESS_LIST_ADDRESS_COST 2400 - WARM_STORAGE_READ_COST 100)" - ); - } - - /// Unit test: init_subcall with a cold target should set - /// init_subcall_gas_overhead = abi_decode_gas + COLD_ACCOUNT_ACCESS_COST. - #[test] - fn test_call_from_cold_target_recalculates_child_gas() { - use alloy_sol_types::SolCall; - use arc_precompiles::call_from::{abi_decode_gas, ICallFrom}; - use revm_interpreter::gas::COLD_ACCOUNT_ACCESS_COST; - - let echo_code = echo_double_bytecode(); - let mut evm = setup_test_evm( - &[(EOA, U256::from(1_000_000))], - &[(ECHO_CONTRACT, echo_code)], - &[WRAPPER], - ); - - // Clear journal state and load required accounts - evm.ctx_mut().journal_mut().clear(); - evm.ctx_mut() - .journal_mut() - .load_account(NATIVE_COIN_CONTROL_ADDRESS) - .unwrap(); - - // tx.origin must match the spoofed sender for the origin check. - evm.inner.ctx.set_tx(TxEnv { - caller: EOA, - ..Default::default() - }); - - let child_data: Vec = vec![0x42]; - let calldata = ICallFrom::callFromCall { - sender: EOA, - target: ECHO_CONTRACT, - data: child_data.clone().into(), - } - .abi_encode(); - - let gas_limit: u64 = 100_000; - let call_inputs = CallInputs { - scheme: CallScheme::Call, - target_address: CALL_FROM_ADDRESS, - bytecode_address: CALL_FROM_ADDRESS, - known_bytecode: None, - value: CallValue::Transfer(U256::ZERO), - input: CallInput::Bytes(Bytes::from(calldata)), - gas_limit, - is_static: false, - caller: WRAPPER, - return_memory_offset: 0..0, - }; - - let frame_input = FrameInit { - frame_input: FrameInput::Call(Box::new(call_inputs)), - memory: SharedMemory::default(), - depth: 1, - }; - - let precompile: Arc = - Arc::new(CallFromPrecompile); - let result = evm - .init_subcall(frame_input, precompile) - .expect("init_subcall should succeed"); - assert!( - matches!(result, ItemOrResult::Item(_)), - "expected child frame, got immediate result" - ); - - let continuation = evm - .subcall_continuations - .get(&1) - .expect("continuation should be stored at depth 1"); - - let expected_overhead = abi_decode_gas(child_data.len()) + COLD_ACCOUNT_ACCESS_COST; - assert_eq!( - continuation.init_subcall_gas_overhead, - expected_overhead, - "overhead should be abi_decode ({}) + cold access ({COLD_ACCOUNT_ACCESS_COST}), \ - got {}", - abi_decode_gas(child_data.len()), - continuation.init_subcall_gas_overhead - ); - } - - /// Unit test: when the target is already warm, init_subcall should use - /// WARM_STORAGE_READ_COST (100) instead of COLD_ACCOUNT_ACCESS_COST (2600). - #[test] - fn test_call_from_warm_target_uses_warm_cost() { - use alloy_sol_types::SolCall; - use arc_precompiles::call_from::{abi_decode_gas, ICallFrom}; - use revm_interpreter::gas::WARM_STORAGE_READ_COST; - - let echo_code = echo_double_bytecode(); - let mut evm = setup_test_evm( - &[(EOA, U256::from(1_000_000))], - &[(ECHO_CONTRACT, echo_code)], - &[WRAPPER], - ); - - // Clear journal state and load required accounts - evm.ctx_mut().journal_mut().clear(); - evm.ctx_mut() - .journal_mut() - .load_account(NATIVE_COIN_CONTROL_ADDRESS) - .unwrap(); - - // Pre-warm the target account - evm.ctx_mut() - .journal_mut() - .load_account(ECHO_CONTRACT) - .unwrap(); - - evm.inner.ctx.set_tx(TxEnv { - caller: EOA, - ..Default::default() - }); - - let child_data: Vec = vec![0x42]; - let calldata = ICallFrom::callFromCall { - sender: EOA, - target: ECHO_CONTRACT, - data: child_data.clone().into(), - } - .abi_encode(); - - let gas_limit: u64 = 100_000; - let call_inputs = CallInputs { - scheme: CallScheme::Call, - target_address: CALL_FROM_ADDRESS, - bytecode_address: CALL_FROM_ADDRESS, - known_bytecode: None, - value: CallValue::Transfer(U256::ZERO), - input: CallInput::Bytes(Bytes::from(calldata)), - gas_limit, - is_static: false, - caller: WRAPPER, - return_memory_offset: 0..0, - }; - - let frame_input = FrameInit { - frame_input: FrameInput::Call(Box::new(call_inputs)), - memory: SharedMemory::default(), - depth: 1, - }; - - let precompile: Arc = - Arc::new(CallFromPrecompile); - let result = evm - .init_subcall(frame_input, precompile) - .expect("init_subcall should succeed"); - assert!( - matches!(result, ItemOrResult::Item(_)), - "expected child frame, got immediate result" - ); - - let continuation = evm - .subcall_continuations - .get(&1) - .expect("continuation should be stored at depth 1"); - - let expected_overhead = abi_decode_gas(child_data.len()) + WARM_STORAGE_READ_COST; - assert_eq!( - continuation.init_subcall_gas_overhead, - expected_overhead, - "overhead should be abi_decode ({}) + warm read ({WARM_STORAGE_READ_COST}), \ - got {}", - abi_decode_gas(child_data.len()), - continuation.init_subcall_gas_overhead - ); - } - - /// When caller == target, `load_account(caller)` warms the address before - /// `load_account(target)`, so only the warm access cost (100) is charged. - #[test] - fn test_call_from_caller_equals_target_uses_warm_cost() { - use alloy_sol_types::SolCall; - use arc_precompiles::call_from::{abi_decode_gas, ICallFrom}; - use revm_interpreter::gas::WARM_STORAGE_READ_COST; - - let echo_code = echo_double_bytecode(); - // ECHO_CONTRACT is both the sender and target — give it balance and code. - let mut evm = setup_test_evm( - &[(ECHO_CONTRACT, U256::from(10_000_000))], - &[(ECHO_CONTRACT, echo_code)], - &[WRAPPER], - ); - - evm.ctx_mut().journal_mut().clear(); - evm.ctx_mut() - .journal_mut() - .load_account(NATIVE_COIN_CONTROL_ADDRESS) - .unwrap(); - - evm.inner.ctx.set_tx(TxEnv { - caller: ECHO_CONTRACT, - ..Default::default() - }); - - let child_data: Vec = vec![0x42]; - let calldata = ICallFrom::callFromCall { - sender: ECHO_CONTRACT, - target: ECHO_CONTRACT, - data: child_data.clone().into(), - } - .abi_encode(); - - let gas_limit: u64 = 100_000; - let call_inputs = CallInputs { - scheme: CallScheme::Call, - target_address: CALL_FROM_ADDRESS, - bytecode_address: CALL_FROM_ADDRESS, - known_bytecode: None, - value: CallValue::Transfer(U256::ZERO), - input: CallInput::Bytes(Bytes::from(calldata)), - gas_limit, - is_static: false, - caller: WRAPPER, - return_memory_offset: 0..0, - }; - - let frame_input = FrameInit { - frame_input: FrameInput::Call(Box::new(call_inputs)), - memory: SharedMemory::default(), - depth: 1, - }; - - let precompile: Arc = - Arc::new(CallFromPrecompile); - let result = evm - .init_subcall(frame_input, precompile) - .expect("init_subcall should succeed"); - assert!( - matches!(result, ItemOrResult::Item(_)), - "expected child frame, got immediate result" - ); - - let continuation = evm - .subcall_continuations - .get(&1) - .expect("continuation should be stored at depth 1"); - - let expected_overhead = abi_decode_gas(child_data.len()) + WARM_STORAGE_READ_COST; - assert_eq!( - continuation.init_subcall_gas_overhead, - expected_overhead, - "caller==target: overhead should be abi_decode ({}) + warm read \ - ({WARM_STORAGE_READ_COST}), got {}", - abi_decode_gas(child_data.len()), - continuation.init_subcall_gas_overhead - ); - } - - /// Unit test: when gas_limit is just below abi_decode + COLD_ACCOUNT_ACCESS_COST, - /// init_subcall should OOG. - #[test] - fn test_call_from_oog_with_cold_account_access() { - use alloy_sol_types::SolCall; - use arc_precompiles::call_from::{abi_decode_gas, ICallFrom}; - use revm_interpreter::gas::COLD_ACCOUNT_ACCESS_COST; - - let echo_code = echo_double_bytecode(); - let mut evm = setup_test_evm( - &[(EOA, U256::from(1_000_000))], - &[(ECHO_CONTRACT, echo_code)], - &[WRAPPER], - ); - - evm.ctx_mut().journal_mut().clear(); - evm.ctx_mut() - .journal_mut() - .load_account(NATIVE_COIN_CONTROL_ADDRESS) - .unwrap(); - - evm.inner.ctx.set_tx(TxEnv { - caller: EOA, - ..Default::default() - }); - - let child_data: Vec = vec![0x42]; - let calldata = ICallFrom::callFromCall { - sender: EOA, - target: ECHO_CONTRACT, - data: child_data.clone().into(), - } - .abi_encode(); - - // Gas is enough for ABI decode but NOT enough for ABI decode + cold access - let insufficient_gas = abi_decode_gas(child_data.len()) + COLD_ACCOUNT_ACCESS_COST - 1; - let call_inputs = CallInputs { - scheme: CallScheme::Call, - target_address: CALL_FROM_ADDRESS, - bytecode_address: CALL_FROM_ADDRESS, - known_bytecode: None, - value: CallValue::Transfer(U256::ZERO), - input: CallInput::Bytes(Bytes::from(calldata)), - gas_limit: insufficient_gas, - is_static: false, - caller: WRAPPER, - return_memory_offset: 0..0, - }; - - let frame_input = FrameInit { - frame_input: FrameInput::Call(Box::new(call_inputs)), - memory: SharedMemory::default(), - depth: 1, - }; - - let precompile: Arc = - Arc::new(CallFromPrecompile); - let result = evm - .init_subcall(frame_input, precompile) - .expect("init_subcall should not return db error"); - - match result { - ItemOrResult::Result(FrameResult::Call(outcome)) => { - assert_eq!( - outcome.result.result, - InstructionResult::OutOfGas, - "should OOG when gas is insufficient for account access cost" - ); - assert_eq!( - outcome.result.gas.spent(), - insufficient_gas, - "OOG should consume all allocated gas" - ); - assert!( - !evm.subcall_continuations.contains_key(&1), - "continuation should be removed after OOG" - ); - } - ItemOrResult::Result(other) => { - panic!("expected Call result, got {other:?}"); - } - ItemOrResult::Item(_) => { - panic!( - "expected OutOfGas when gas_limit ({insufficient_gas}) < \ - abi_decode ({}) + cold access ({COLD_ACCOUNT_ACCESS_COST})", - abi_decode_gas(child_data.len()) - ); - } - } - } - - /// Boundary: gas_limit == abi_decode + COLD_ACCOUNT_ACCESS_COST should succeed - /// with child_gas_limit = 0 (child will OOG when it runs, but init_subcall itself - /// should not reject it). - #[test] - fn test_call_from_exact_overhead_gas_succeeds_with_zero_child_gas() { - use alloy_sol_types::SolCall; - use arc_precompiles::call_from::{abi_decode_gas, ICallFrom}; - use revm_interpreter::gas::COLD_ACCOUNT_ACCESS_COST; - - let echo_code = echo_double_bytecode(); - let mut evm = setup_test_evm( - &[(EOA, U256::from(1_000_000))], - &[(ECHO_CONTRACT, echo_code)], - &[WRAPPER], - ); - - evm.ctx_mut().journal_mut().clear(); - evm.ctx_mut() - .journal_mut() - .load_account(NATIVE_COIN_CONTROL_ADDRESS) - .unwrap(); - - evm.inner.ctx.set_tx(TxEnv { - caller: EOA, - ..Default::default() - }); - - let child_data: Vec = vec![0x42]; - let calldata = ICallFrom::callFromCall { - sender: EOA, - target: ECHO_CONTRACT, - data: child_data.clone().into(), - } - .abi_encode(); - - let exact_gas = abi_decode_gas(child_data.len()) + COLD_ACCOUNT_ACCESS_COST; - let call_inputs = CallInputs { - scheme: CallScheme::Call, - target_address: CALL_FROM_ADDRESS, - bytecode_address: CALL_FROM_ADDRESS, - known_bytecode: None, - value: CallValue::Transfer(U256::ZERO), - input: CallInput::Bytes(Bytes::from(calldata)), - gas_limit: exact_gas, - is_static: false, - caller: WRAPPER, - return_memory_offset: 0..0, - }; - - let frame_input = FrameInit { - frame_input: FrameInput::Call(Box::new(call_inputs)), - memory: SharedMemory::default(), - depth: 1, - }; - - let precompile: Arc = - Arc::new(CallFromPrecompile); - let result = evm - .init_subcall(frame_input, precompile) - .expect("init_subcall should not return db error"); - - assert!( - matches!(result, ItemOrResult::Item(_)), - "exact overhead gas should push a child frame, not OOG" - ); - - let continuation = evm - .subcall_continuations - .get(&1) - .expect("continuation should exist at depth 1"); - assert_eq!( - continuation.init_subcall_gas_overhead, exact_gas, - "overhead should equal the full gas budget" - ); - } - } - fn create_test_evm_with_spec( - db: InMemoryDB, - hardfork_flags: ArcHardforkFlags, - spec: SpecId, - ) -> ArcEvm< - EthEvmContext, - NoOpInspector, - EthInstructions>, - PrecompilesMap, - > { - let ctx = Context::new(db, spec); - let instruction = EthInstructions::new_mainnet_with_spec(spec); - let precompiles = ArcPrecompileProvider::create_precompiles_map(spec, hardfork_flags); - ArcEvm::new( - ctx, - NoOpInspector {}, - precompiles, - instruction, - false, - hardfork_flags, - Arc::new(SubcallRegistry::default()), - ) - } - - #[test] - fn test_zero5_emits_eip7708_transfer_log() { - use revm::handler::SYSTEM_ADDRESS; - - let db = CacheDB::new(EmptyDB::default()); - let flags = ArcHardforkFlags::with(&[ArcHardfork::Zero5]); - let mut evm = create_test_evm(db, flags); - - evm.ctx_mut() - .journal_mut() - .load_account(NATIVE_COIN_CONTROL_ADDRESS) - .unwrap(); - - let mut frame = FrameInit { - frame_input: FrameInput::Call(call_input( - CallScheme::Call, - U256::from(100), - ADDRESS_A, - ADDRESS_B, - )), - memory: SharedMemory::default(), - depth: 1, - }; - - let result = evm.before_frame_init(&mut frame).unwrap(); - match result { - BeforeFrameInitResult::Log(log, gas) => { - assert!(gas > 0, "Should have SLOAD gas cost"); - assert_eq!( - log.address, SYSTEM_ADDRESS, - "Zero5 should emit EIP-7708 Transfer log from system address" - ); - } - other => panic!( - "Expected Log result with EIP-7708 Transfer under Zero5, got {:?}", - other - ), - } - } - - #[test] - fn test_zero5_self_transfer_no_log() { - let db = CacheDB::new(EmptyDB::default()); - let flags = ArcHardforkFlags::with(&[ArcHardfork::Zero5]); - let mut evm = create_test_evm(db, flags); - - evm.ctx_mut() - .journal_mut() - .load_account(NATIVE_COIN_CONTROL_ADDRESS) - .unwrap(); - - // Self-transfer: from == to - let mut frame = FrameInit { - frame_input: FrameInput::Call(call_input( - CallScheme::Call, - U256::from(100), - ADDRESS_A, - ADDRESS_A, - )), - memory: SharedMemory::default(), - depth: 1, - }; - - let result = evm.before_frame_init(&mut frame).unwrap(); - match result { - BeforeFrameInitResult::Checked(gas) => { - assert!(gas > 0, "Should have SLOAD gas cost"); - } - other => panic!( - "Expected Checked result for self-transfer under Zero5, got {:?}", - other - ), - } - } - - #[test] - fn test_pre_zero5_still_emits_custom_log() { - let db = CacheDB::new(EmptyDB::default()); - let flags = ArcHardforkFlags::with(&[ArcHardfork::Zero4]); - let mut evm = create_test_evm(db, flags); - - evm.ctx_mut() - .journal_mut() - .load_account(NATIVE_COIN_CONTROL_ADDRESS) - .unwrap(); - - let mut frame = FrameInit { - frame_input: FrameInput::Call(call_input( - CallScheme::Call, - U256::from(100), - ADDRESS_A, - ADDRESS_B, - )), - memory: SharedMemory::default(), - depth: 1, - }; - - let result = evm.before_frame_init(&mut frame).unwrap(); - match result { - BeforeFrameInitResult::Log(log, _gas) => { - assert_eq!(log.address, NATIVE_COIN_AUTHORITY_ADDRESS); - } - other => panic!("Expected Log result pre-Zero5, got {:?}", other), - } - } - - /// Verifies that AMSTERDAM SpecId enables EIP-7708 (is_enabled_in returns true). - /// Once REVM is upgraded to a version with EIP-7708 journal support, the journal's - /// `transfer` method will emit Transfer logs when SpecId >= AMSTERDAM. - #[test] - fn test_amsterdam_spec_enables_eip7708() { - // AMSTERDAM is after PRAGUE in the SpecId ordering - assert!( - SpecId::AMSTERDAM.is_enabled_in(SpecId::AMSTERDAM), - "AMSTERDAM should be enabled in AMSTERDAM" - ); - assert!( - !SpecId::PRAGUE.is_enabled_in(SpecId::AMSTERDAM), - "PRAGUE should NOT be enabled in AMSTERDAM (AMSTERDAM comes after PRAGUE)" - ); - - // Verify that an EVM can be created with AMSTERDAM spec (for future use) - let db = create_db(&[(ADDRESS_A, 1000)]); - let flags = ArcHardforkFlags::with(&[ArcHardfork::Zero5]); - let evm = create_test_evm_with_spec(db, flags, SpecId::AMSTERDAM); - assert_eq!( - evm.inner.ctx.cfg.spec, - SpecId::AMSTERDAM, - "EVM should be configured with AMSTERDAM spec" - ); - } - - /// Verifies that Zero5 emits EIP-7708 Transfer logs regardless of SpecId. - /// Arc self-implements EIP-7708 log emission, so PRAGUE vs AMSTERDAM doesn't matter. - #[test] - fn test_zero5_emits_eip7708_regardless_of_spec() { - use revm::handler::SYSTEM_ADDRESS; - - // Zero5 + PRAGUE: Arc emits EIP-7708 Transfer logs itself - let db = CacheDB::new(EmptyDB::default()); - let flags = ArcHardforkFlags::with(&[ArcHardfork::Zero5]); - let mut evm = create_test_evm_with_spec(db, flags, SpecId::PRAGUE); - - evm.ctx_mut() - .journal_mut() - .load_account(NATIVE_COIN_CONTROL_ADDRESS) - .unwrap(); - - let mut frame = FrameInit { - frame_input: FrameInput::Call(call_input( - CallScheme::Call, - U256::from(100), - ADDRESS_A, - ADDRESS_B, - )), - memory: SharedMemory::default(), - depth: 1, - }; - - let result = evm.before_frame_init(&mut frame).unwrap(); - match result { - BeforeFrameInitResult::Log(log, _gas) => { - assert_eq!( - log.address, SYSTEM_ADDRESS, - "Zero5 + PRAGUE: should emit EIP-7708 Transfer log" - ); - } - other => panic!( - "Expected Log result with EIP-7708 Transfer, got {:?}", - other - ), - } - } - - /// Zero5: CALL with value to Address::ZERO should revert. - #[test] - fn test_zero5_call_to_zero_address_reverts() { - let db = CacheDB::new(EmptyDB::default()); - let flags = ArcHardforkFlags::with(&[ArcHardfork::Zero5]); - let mut evm = create_test_evm(db, flags); - - evm.ctx_mut() - .journal_mut() - .load_account(NATIVE_COIN_CONTROL_ADDRESS) - .unwrap(); - - let mut frame = FrameInit { - frame_input: FrameInput::Call(call_input( - CallScheme::Call, - U256::from(100), - ADDRESS_A, - Address::ZERO, - )), - memory: SharedMemory::default(), - depth: 1, - }; - - let result = evm.before_frame_init(&mut frame).unwrap(); - assert!( - matches!(result, BeforeFrameInitResult::Reverted(_)), - "Zero5 should revert CALL with value to zero address, got {:?}", - result, - ); - } - - /// Zero5: CALL from Address::ZERO with value should revert. - #[test] - fn test_zero5_call_from_zero_address_reverts() { - let db = CacheDB::new(EmptyDB::default()); - let flags = ArcHardforkFlags::with(&[ArcHardfork::Zero5]); - let mut evm = create_test_evm(db, flags); - - evm.ctx_mut() - .journal_mut() - .load_account(NATIVE_COIN_CONTROL_ADDRESS) - .unwrap(); - - let mut frame = FrameInit { - frame_input: FrameInput::Call(call_input( - CallScheme::Call, - U256::from(100), - Address::ZERO, - ADDRESS_B, - )), - memory: SharedMemory::default(), - depth: 1, - }; - - let result = evm.before_frame_init(&mut frame).unwrap(); - assert!( - matches!(result, BeforeFrameInitResult::Reverted(_)), - "Zero5 should revert CALL with value from zero address, got {:?}", - result, - ); - } - - /// Pre-Zero5: CALL to Address::ZERO is NOT blocked (backwards compatible). - #[test] - fn test_pre_zero5_call_to_zero_address_allowed() { - let db = CacheDB::new(EmptyDB::default()); - let flags = ArcHardforkFlags::with(&[ArcHardfork::Zero4]); - let mut evm = create_test_evm(db, flags); - - evm.ctx_mut() - .journal_mut() - .load_account(NATIVE_COIN_CONTROL_ADDRESS) - .unwrap(); - - let mut frame = FrameInit { - frame_input: FrameInput::Call(call_input( - CallScheme::Call, - U256::from(100), - ADDRESS_A, - Address::ZERO, - )), - memory: SharedMemory::default(), - depth: 1, - }; - - let result = evm.before_frame_init(&mut frame).unwrap(); - assert!( - matches!(result, BeforeFrameInitResult::Log(_, _)), - "Pre-Zero5 should allow CALL to zero address, got {:?}", - result, - ); - } - - /// Regression test for phantom EIP-7708 logs: - /// an inner value-transferring CALL that reverts must not leave a Transfer log behind. - #[test] - fn test_zero5_reverted_call_with_value_emits_no_eip7708_log() { - use arc_execution_config::{chainspec::localdev_with_hardforks, hardforks::ArcHardfork}; - use revm_primitives::TxKind; - - let chain_spec = localdev_with_hardforks(&[ - (ArcHardfork::Zero3, ForkCondition::Block(0)), - (ArcHardfork::Zero4, ForkCondition::Block(0)), - (ArcHardfork::Zero5, ForkCondition::Block(0)), - ]); - let sender = Address::repeat_byte(0x11); - let caller_contract = Address::repeat_byte(0x22); - let reverting_contract = Address::repeat_byte(0x33); - let amount = U256::from(100); - - // Runtime bytecode: PUSH1 0x00 PUSH1 0x00 REVERT - let revert_runtime: Bytes = vec![0x60, 0x00, 0x60, 0x00, 0xfd].into(); - let caller_runtime = call_with_value_bytecode(reverting_contract, amount); - - let mut db = create_db(&[(sender, 1000)]); - db.insert_account_info( - caller_contract, - revm::state::AccountInfo { - balance: U256::from(1000), - nonce: 1, - code_hash: keccak256(caller_runtime.bytecode()), - code: Some(caller_runtime), - account_id: None, - }, - ); - db.insert_account_info( - reverting_contract, - revm::state::AccountInfo { - balance: U256::ZERO, - nonce: 1, - code_hash: keccak256(&revert_runtime), - code: Some(Bytecode::new_raw(revert_runtime)), - account_id: None, - }, - ); - - let mut evm = create_arc_evm(chain_spec.clone(), db); - let tx = TxEnv { - caller: sender, - kind: TxKind::Call(caller_contract), - value: U256::ZERO, - gas_limit: 100_000, - gas_price: 0, - chain_id: Some(chain_spec.chain_id()), - ..Default::default() - }; - - let result = evm.transact_one(tx).expect("nested CALL should execute"); - assert!( - result.is_success(), - "Outer transaction should succeed; only the inner CALL should revert, got {:?}", - result - ); - assert_eq!( - result.logs().len(), - 0, - "Reverted inner CALL with value must not leave an EIP-7708 Transfer log behind" - ); - } - - /// Regression test for phantom EIP-7708 logs: - /// an inner CREATE with endowment whose initcode reverts must not leave a Transfer log behind. - #[test] - fn test_zero5_reverted_create_with_value_emits_no_eip7708_log() { - use arc_execution_config::{chainspec::localdev_with_hardforks, hardforks::ArcHardfork}; - use revm_primitives::TxKind; - - let chain_spec = localdev_with_hardforks(&[ - (ArcHardfork::Zero3, ForkCondition::Block(0)), - (ArcHardfork::Zero4, ForkCondition::Block(0)), - (ArcHardfork::Zero5, ForkCondition::Block(0)), - ]); - let sender = Address::repeat_byte(0x33); - let factory_contract = Address::repeat_byte(0x44); - let amount = U256::from(100); - - // Initcode: PUSH1 0x00 PUSH1 0x00 REVERT - let revert_initcode = vec![0x60, 0x00, 0x60, 0x00, 0xfd]; - let factory_runtime = create_with_value_bytecode(&revert_initcode, amount); - - let mut db = create_db(&[(sender, 1000)]); - db.insert_account_info( - factory_contract, - revm::state::AccountInfo { - balance: U256::from(1000), - nonce: 1, - code_hash: keccak256(factory_runtime.bytecode()), - code: Some(factory_runtime), - account_id: None, - }, - ); - let mut evm = create_arc_evm(chain_spec.clone(), db); - let tx = TxEnv { - caller: sender, - kind: TxKind::Call(factory_contract), - value: U256::ZERO, - gas_limit: 120_000, - gas_price: 0, - chain_id: Some(chain_spec.chain_id()), - ..Default::default() - }; - - let result = evm.transact_one(tx).expect("nested CREATE should execute"); - assert!( - result.is_success(), - "Outer transaction should succeed; only the inner CREATE should revert, got {:?}", - result - ); - assert_eq!( - result.logs().len(), - 0, - "Reverted inner CREATE with value must not leave an EIP-7708 Transfer log behind" - ); - } - - /// Zero5: EIP-7708 Transfer log must precede precompile logs in the journal. - /// - /// When a CALL with value targets a precompile that emits its own logs, the - /// EIP-7708 Transfer log from the value transfer must appear before any logs - /// produced by the precompile execution. - #[test] - fn test_zero5_transfer_log_precedes_precompile_log() { - use alloy_primitives::{Log as PrimLog, LogData}; - use reth_evm::precompiles::DynPrecompile; - use revm::handler::SYSTEM_ADDRESS; - use revm::precompile::{PrecompileId, PrecompileOutput}; - - // A distinctive address for the mock precompile, not overlapping with real ones. - const MOCK_PRECOMPILE: Address = address!("ff00000000000000000000000000000000000099"); - - // A distinctive log address so we can tell precompile logs from transfer logs. - const MOCK_LOG_ADDRESS: Address = address!("aa00000000000000000000000000000000000001"); - - // Build a PrecompilesMap that includes the mock precompile. - let spec = SpecId::PRAGUE; - let flags = ArcHardforkFlags::with(&[ArcHardfork::Zero5]); - let mut precompile_map = ArcPrecompileProvider::create_precompiles_map(spec, flags); - precompile_map.set_precompile_lookup(move |address: &Address| { - if *address == MOCK_PRECOMPILE { - Some(DynPrecompile::new_stateful( - PrecompileId::Custom("MOCK_LOG_EMITTER".into()), - move |mut input| { - // Emit a log via the journal so it appears in the journal log list. - input.internals.log(PrimLog { - address: MOCK_LOG_ADDRESS, - data: LogData::new_unchecked(vec![], Bytes::new()), - }); - Ok(PrecompileOutput::new(0, Bytes::new())) - }, - )) - } else { - None - } - }); - - // Build the ArcEvm with our custom precompile map. - let db = create_db(&[(ADDRESS_A, 10_000)]); - let mut evm = create_test_evm_with_precompiles(db, flags, precompile_map); - - // Warm-load accounts so journal state is populated for transfer_loaded. - evm.ctx_mut() - .journal_mut() - .load_account(NATIVE_COIN_CONTROL_ADDRESS) - .unwrap(); - evm.ctx_mut().journal_mut().load_account(ADDRESS_A).unwrap(); - evm.ctx_mut() - .journal_mut() - .load_account(MOCK_PRECOMPILE) - .unwrap(); - - // CALL with value from ADDRESS_A to MOCK_PRECOMPILE. - // bytecode_address must equal the precompile address so PrecompilesMap::run finds it. - let frame = FrameInit { - frame_input: FrameInput::Call(Box::new(CallInputs { - scheme: CallScheme::Call, - target_address: MOCK_PRECOMPILE, - bytecode_address: MOCK_PRECOMPILE, - known_bytecode: None, - value: CallValue::Transfer(U256::from(100)), - input: CallInput::Bytes(Bytes::new()), - gas_limit: 500_000, - is_static: false, - caller: ADDRESS_A, - return_memory_offset: 0..0, - })), - memory: SharedMemory::default(), - depth: 1, - }; - - // Record the log count before frame_init. - let logs_before = evm.ctx().journal().logs().len(); - - let result = evm.frame_init(frame); - assert!(result.is_ok(), "frame_init should succeed"); - - let ctx = evm.ctx(); - let logs = ctx.journal().logs(); - let new_logs = &logs[logs_before..]; - - assert!( - new_logs.len() >= 2, - "Expected at least 2 logs (transfer + precompile), got {}", - new_logs.len() - ); - - // First log must be the EIP-7708 Transfer log from the value transfer. - assert_eq!( - new_logs[0].address, SYSTEM_ADDRESS, - "First log should be the EIP-7708 Transfer log from system address, got {:?}", - new_logs[0].address - ); - - // Second log must be the mock precompile's log. - assert_eq!( - new_logs[1].address, MOCK_LOG_ADDRESS, - "Second log should be the mock precompile log, got {:?}", - new_logs[1].address - ); - } - - /// Regression test: a CALL with value to a precompile that reverts must roll back - /// the EIP-7708 Transfer log via Arc's checkpoint_revert (the precompile path at - /// line 507, distinct from the non-precompile path tested by - /// `test_zero5_reverted_call_with_value_emits_no_eip7708_log`). - #[test] - fn test_zero5_reverted_precompile_call_with_value_emits_no_eip7708_log() { - use reth_evm::precompiles::DynPrecompile; - use revm::precompile::{PrecompileError, PrecompileId}; - - const MOCK_PRECOMPILE: Address = address!("ff00000000000000000000000000000000000099"); - - let spec = SpecId::PRAGUE; - let flags = ArcHardforkFlags::with(&[ArcHardfork::Zero5]); - let mut precompile_map = ArcPrecompileProvider::create_precompiles_map(spec, flags); - precompile_map.set_precompile_lookup(move |address: &Address| { - if *address == MOCK_PRECOMPILE { - Some(DynPrecompile::new_stateful( - PrecompileId::Custom("MOCK_REVERTER".into()), - move |_input| Err(PrecompileError::other("authorization failed")), - )) - } else { - None - } - }); - - let db = create_db(&[(ADDRESS_A, 10_000)]); - let mut evm = create_test_evm_with_precompiles(db, flags, precompile_map); - - evm.ctx_mut() - .journal_mut() - .load_account(NATIVE_COIN_CONTROL_ADDRESS) - .unwrap(); - evm.ctx_mut().journal_mut().load_account(ADDRESS_A).unwrap(); - evm.ctx_mut() - .journal_mut() - .load_account(MOCK_PRECOMPILE) - .unwrap(); - - let logs_before = evm.ctx().journal().logs().len(); - - let frame = FrameInit { - frame_input: FrameInput::Call(Box::new(CallInputs { - scheme: CallScheme::Call, - target_address: MOCK_PRECOMPILE, - bytecode_address: MOCK_PRECOMPILE, - known_bytecode: None, - value: CallValue::Transfer(U256::from(100)), - input: CallInput::Bytes(Bytes::new()), - gas_limit: 500_000, - is_static: false, - caller: ADDRESS_A, - return_memory_offset: 0..0, - })), - memory: SharedMemory::default(), - depth: 1, - }; - - let result = evm.frame_init(frame); - assert!( - result.is_ok(), - "frame_init should succeed (precompile failure is a Result, not an Err)" - ); - - let ctx = evm.ctx(); - let new_logs = &ctx.journal().logs()[logs_before..]; - assert_eq!( - new_logs.len(), - 0, - "Reverting precompile CALL with value must not leave an EIP-7708 Transfer log behind, got {} logs", - new_logs.len() - ); - } - - /// ----- Revm upgrade checklist ----- - /// Guard against silent drift when upgrading revm. If this test fails, the revm - /// dependency version has changed and the forked/mirrored functions below must be - /// reviewed for upstream behavioral changes: - /// - /// 1. [`ArcEvm::inspect_frame_init_impl`] + [`ArcEvm::frame_start_with_trace`] in - /// `crates/evm/src/evm.rs` - /// — mirrors `InspectorEvmTr::inspect_frame_init` from `revm-inspector` - /// — - /// - /// 2. [`init_frame`] in `crates/evm/src/evm.rs` - /// — mirrors `Evm::frame_init` from `revm-handler` (borrow-split variant) - /// — see [`revm::handler::EvmTr::frame_init`] - /// - /// 3. `arc_network_selfdestruct_impl` in `crates/evm/src/opcode.rs` - /// — forked from `revm/crates/interpreter/src/instructions/host.rs` (SELFDESTRUCT) - /// — - /// - /// 4. `istanbul_sstore_cost` logic in `crates/precompiles/src/helpers.rs` - /// — mirrors revm's `istanbul_sstore_cost` gas calculation - #[test] - fn revm_version_check() { - const EXPECTED_REVM_VERSION: &str = "34.0.0"; - let workspace_toml = include_str!("../../../Cargo.toml"); - let expected = format!("revm = {{ version = \"{EXPECTED_REVM_VERSION}\""); - assert!( - workspace_toml.contains(&expected), - "revm version has changed from {EXPECTED_REVM_VERSION}. \ - Review all forked/mirrored revm functions listed in this test's doc comment." - ); - } -} diff --git a/crates/evm/src/executor.rs b/crates/evm/src/executor.rs deleted file mode 100644 index 87eb283..0000000 --- a/crates/evm/src/executor.rs +++ /dev/null @@ -1,1239 +0,0 @@ -// Copyright 2025 Circle Internet Group, Inc. All rights reserved. -// -// SPDX-License-Identifier: Apache-2.0 -// -// Licensed under the Apache License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. -// You may obtain a copy of the License at -// -// http://www.apache.org/licenses/LICENSE-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// See the License for the specific language governing permissions and -// limitations under the License. - -use alloy_evm::block::ExecutableTx; -use alloy_evm::block::TxResult; -use alloy_evm::eth::receipt_builder::ReceiptBuilder; -use reth_chainspec::EthChainSpec; -use reth_chainspec::Hardforks; -use reth_ethereum::{ - evm::{ - primitives::{ - execute::{BlockExecutionError, BlockExecutor}, - Database, OnStateHook, - }, - revm::db::State, - }, - provider::BlockExecutionResult, -}; -use reth_evm::eth::EthBlockExecutionCtx; -use revm::{context::Block, context_interface::result::ResultAndState}; - -use alloy_consensus::transaction::Transaction; -use alloy_consensus::transaction::TransactionEnvelope; -use alloy_consensus::TxReceipt; -use alloy_eips::eip2718::Encodable2718; -use alloy_eips::eip7685::Requests; -use alloy_evm::block::BlockValidationError; -use alloy_evm::block::InternalBlockExecutionError; -use alloy_evm::block::StateChangeSource; -use alloy_evm::block::SystemCaller; -use alloy_evm::eth::receipt_builder::ReceiptBuilderCtx; -use alloy_evm::eth::spec::EthExecutorSpec; -use alloy_evm::Evm; -use alloy_evm::FromRecoveredTx; -use alloy_evm::FromTxWithEncoded; -use alloy_evm::RecoveredTx; -use alloy_primitives::{Address, Log}; -use arc_execution_config::chainspec::{BaseFeeConfigProvider, BlockGasLimitProvider}; -use arc_execution_config::gas_fee::{ - self, arc_calc_next_block_base_fee, decode_base_fee_from_bytes, -}; -use arc_execution_config::hardforks::{is_arc_fork_active, ArcHardfork}; -use arc_execution_config::native_coin_control::{ - compute_is_blocklisted_storage_slot, is_blocklisted_status, -}; -use arc_execution_config::protocol_config; -use arc_precompiles::helpers::ERR_BLOCKED_ADDRESS; -use arc_precompiles::system_accounting; -use reth_evm::block::StateChangePostBlockSource; -use revm::DatabaseCommit; - -const ERR_BLOCKLIST_READ_FAILED: &str = "Failed to read beneficiary blocklist status"; -const ERR_BLOCK_NUMBER_CONVERSION_FAILED: &str = "Failed to convert block number to u64"; -const ERR_BLOCK_TIMESTAMP_CONVERSION_FAILED: &str = "Failed to convert block timestamp to u64"; - -/// Result of executing an Arc transaction. -#[derive(Debug)] -pub struct ArcTxResult { - /// Result of the transaction execution. - pub result: ResultAndState, - /// Blob gas used by the transaction. - pub blob_gas_used: u64, - /// Type of the transaction. - pub tx_type: T, -} - -impl TxResult for ArcTxResult { - type HaltReason = H; - - fn result(&self) -> &ResultAndState { - &self.result - } -} - -/// Custom block executor for Arc -/// -/// This functionality is mostly forked from: https://github.com/alloy-rs/evm/blob/v0.23.2/crates/evm/src/eth/block.rs -/// with modifications to support Arc-specific functionality. -pub struct ArcBlockExecutor<'a, Evm, Spec, R: ReceiptBuilder> { - /// Context for block execution. - pub ctx: EthBlockExecutionCtx<'a>, - /// Chain spec. - chain_spec: Spec, - /// Inner EVM. - evm: Evm, - /// Utility to call system smart contracts. - system_caller: SystemCaller, - /// Receipt builder. - receipt_builder: R, - /// Receipts of executed transactions. - receipts: Vec, - /// Total gas used by transactions in this block. - gas_used: u64, - /// Total blob gas used by transactions in this block. - blob_gas_used: u64, -} - -impl<'a, Evm, Spec, R> ArcBlockExecutor<'a, Evm, Spec, R> -where - Spec: Clone, - R: ReceiptBuilder, - Evm: alloy_evm::Evm, -{ - /// Creates a new [`ArcBlockExecutor`] - pub fn new(evm: Evm, ctx: EthBlockExecutionCtx<'a>, spec: Spec, receipt_builder: R) -> Self { - Self { - chain_spec: spec.clone(), - evm, - ctx, - receipts: Vec::new(), - gas_used: 0, - blob_gas_used: 0, - system_caller: SystemCaller::new(spec.clone()), - receipt_builder, - } - } - - /// Current block number as `u64`. - fn block_number_u64(&self) -> Result { - let block_number = self.evm.block().number(); - block_number.try_into().map_err(|err| { - tracing::error!( - error = %err, - block_number = %block_number, - "Failed to convert block number to u64" - ); - BlockExecutionError::msg(ERR_BLOCK_NUMBER_CONVERSION_FAILED) - }) - } - - /// Current block timestamp as `u64`. - /// - /// Needed alongside [`Self::block_number_u64`] because Arc hardforks may activate - /// either by block or by timestamp (e.g. testnet Zero5/Zero6, all networks' Zero7+); - /// runtime hardfork gating must consult both dimensions via - /// [`arc_execution_config::hardforks::is_arc_fork_active`]. - fn block_timestamp_u64(&self) -> Result { - let block_timestamp = self.evm.block().timestamp(); - block_timestamp.try_into().map_err(|err| { - tracing::error!( - error = %err, - block_timestamp = %block_timestamp, - "Failed to convert block timestamp to u64" - ); - BlockExecutionError::msg(ERR_BLOCK_TIMESTAMP_CONVERSION_FAILED) - }) - } -} - -fn validate_beneficiary_not_blocklisted( - db: &mut DB, - header_beneficiary: Address, - block_number: u64, -) -> Result<(), BlockExecutionError> { - let is_blocklisted = db - .storage( - arc_precompiles::NATIVE_COIN_CONTROL_ADDRESS, - compute_is_blocklisted_storage_slot(header_beneficiary).into(), - ) - .map(is_blocklisted_status) - .map_err(|error| { - let reason = format!("NativeCoinControl blocklist storage read failed: {error}"); - tracing::error!( - error = %reason, - header_beneficiary = %header_beneficiary, - block_number = block_number, - "Blocklist status read failed for block beneficiary" - ); - BlockValidationError::msg(ERR_BLOCKLIST_READ_FAILED) - })?; - - if is_blocklisted { - tracing::warn!( - header_beneficiary = %header_beneficiary, - block_number = block_number, - "Block beneficiary is blocklisted" - ); - return Err(BlockValidationError::msg(ERR_BLOCKED_ADDRESS).into()); - } - - Ok(()) -} - -impl<'db, DB, E, Spec, R> ArcBlockExecutor<'_, E, Spec, R> -where - DB: Database + 'db, - E: Evm< - DB = &'db mut State, - Tx: FromRecoveredTx + FromTxWithEncoded, - >, - Spec: - EthExecutorSpec + Hardforks + EthChainSpec + BlockGasLimitProvider + BaseFeeConfigProvider, - R: ReceiptBuilder>, -{ - /// Validates that block `extra_data` encodes the same next base fee that this executor - /// computed for the current block. - fn validate_extra_data_base_fee( - &self, - block_number: u64, - expected_next_base_fee: u64, - ) -> Result<(), BlockExecutionError> { - let extra_data_base_fee = decode_base_fee_from_bytes(&self.ctx.extra_data); - if extra_data_base_fee != Some(expected_next_base_fee) { - return Err(BlockExecutionError::Validation(BlockValidationError::Other( - format!( - "extra_data base fee mismatch at block {block_number}: computed nextBaseFee={expected_next_base_fee}, extra_data={extra_data_base_fee:?}" - ) - .into(), - ))); - } - Ok(()) - } - - /// Computes `GasValues` using the pre-Zero5 path - fn compute_gas_values_legacy( - &mut self, - block_number: u64, - fee_params: Option, - ) -> Result { - let Some(fee_params) = fee_params else { - return Ok(system_accounting::GasValues { - gasUsed: self.gas_used, - gasUsedSmoothed: self.gas_used, - nextBaseFee: 0, - }); - }; - - let parent_block_number = block_number.saturating_sub(1); - let parent_gas_values = - system_accounting::retrieve_gas_values(parent_block_number, &mut self.evm).map_err( - |e| BlockExecutionError::Internal(InternalBlockExecutionError::Other(Box::new(e))), - )?; - - let calculated_smoothed_gas_used = gas_fee::determine_ema_parent_gas_used( - parent_gas_values.gasUsedSmoothed, - self.gas_used, - fee_params.alpha, - ); - - let mut next_base_fee: u64 = 0; - if let Some(smoothed_gas_used) = calculated_smoothed_gas_used { - let raw = arc_calc_next_block_base_fee( - smoothed_gas_used, - self.evm.block().gas_limit(), - self.evm.block().basefee(), - fee_params.kRate, - fee_params.inverseElasticityMultiplier, - ); - next_base_fee = protocol_config::determine_bounded_base_fee(&fee_params, raw); - } - - let smoothed_gas_used = calculated_smoothed_gas_used.unwrap_or(self.gas_used); - - Ok(system_accounting::GasValues { - gasUsed: self.gas_used, - gasUsedSmoothed: smoothed_gas_used, - nextBaseFee: next_base_fee, - }) - } - - /// Computes `GasValues` using the ADR-0004 spec (Zero5+). - /// - /// Validates the on-chain `FeeParams` against the chainspec `BaseFeeConfig` bounds, - /// substituting per-field defaults for any out-of-range value. If ProtocolConfig is - /// unavailable, falls back to each field's `default`. Applies EMA smoothing, computes - /// the next base fee, optionally applies the ProtocolConfig `minBaseFee`/`maxBaseFee` - /// clamp, then applies the chainspec absolute bounds clamp. - fn compute_gas_values( - &mut self, - block_number: u64, - fee_params: Option, - ) -> Result { - if fee_params.is_none() { - tracing::warn!( - block_number, - "ProtocolConfig unavailable post-Zero5; computing next_base_fee with chainspec defaults" - ); - } - - let base_fee_config = self - .chain_spec - .base_fee_config(block_number.checked_add(1).expect("block number overflow")); - let calc = base_fee_config.resolve_calc_params(fee_params.as_ref()); - - let parent_block_number = block_number.saturating_sub(1); - let parent_gas_values = - system_accounting::retrieve_gas_values(parent_block_number, &mut self.evm).map_err( - |e| { - tracing::warn!( - error = %e, - block_number, - "Failed to retrieve parent gas values from SystemAccounting" - ); - BlockExecutionError::Internal(InternalBlockExecutionError::Other(Box::new(e))) - }, - )?; - - let smoothed_gas_used = gas_fee::determine_ema_parent_gas_used( - parent_gas_values.gasUsedSmoothed, - self.gas_used, - calc.alpha, - ) - .unwrap_or(self.gas_used); - - let raw_next_base_fee = arc_calc_next_block_base_fee( - smoothed_gas_used, - self.evm.block().gas_limit(), - self.evm.block().basefee(), - calc.k_rate, - calc.inverse_elasticity_multiplier, - ); - - // Apply ProtocolConfig's own minBaseFee/maxBaseFee clamp if available. - let clamped = match fee_params.as_ref() { - Some(fp) => protocol_config::determine_bounded_base_fee(fp, raw_next_base_fee), - None => raw_next_base_fee, - }; - - let next_base_fee = base_fee_config.clamp_absolute(clamped); - - Ok(system_accounting::GasValues { - gasUsed: self.gas_used, - gasUsedSmoothed: smoothed_gas_used, - nextBaseFee: next_base_fee, - }) - } -} - -impl<'db, DB, E, Spec, R> BlockExecutor for ArcBlockExecutor<'_, E, Spec, R> -where - DB: Database + 'db, - E: Evm< - DB = &'db mut State, - Tx: FromRecoveredTx + FromTxWithEncoded, - >, - Spec: - EthExecutorSpec + Hardforks + EthChainSpec + BlockGasLimitProvider + BaseFeeConfigProvider, - R: ReceiptBuilder>, -{ - type Transaction = R::Transaction; - type Receipt = R::Receipt; - type Evm = E; - type Result = ArcTxResult::TxType>; - - fn apply_pre_execution_changes(&mut self) -> Result<(), BlockExecutionError> { - // Spurious Dragon hardfork is enabled - self.evm.db_mut().set_state_clear_flag(true); - - // Zero5+ pre-execution checks: beneficiary blocklist, gas limit validation - let block_number = self.block_number_u64()?; - let block_timestamp = self.block_timestamp_u64()?; - - if is_arc_fork_active( - &self.chain_spec, - ArcHardfork::Zero5, - block_number, - block_timestamp, - ) { - // EIP-2935: persist parent block hash in history storage contract. - // Internally gates on Prague activation and is a no-op at block 0 (genesis). - self.system_caller - .apply_blockhashes_contract_call(self.ctx.parent_hash, &mut self.evm)?; - - let beneficiary = self.evm.block().beneficiary(); - validate_beneficiary_not_blocklisted(self.evm.db_mut(), beneficiary, block_number)?; - - // ADR-0003: Stateful gas limit validation against ProtocolConfig (Zero5+) - let block_gas_limit = self.evm.block().gas_limit(); - let fee_params = protocol_config::retrieve_fee_params(&mut self.evm).inspect_err(|err| { - tracing::warn!(error = ?err, block_number, "Failed to get fee params from ProtocolConfig for gas limit validation"); - }).ok(); - - let gas_limit_config = self.chain_spec.block_gas_limit_config(block_number); - let expected = - protocol_config::expected_gas_limit(fee_params.as_ref(), &gas_limit_config); - - if block_gas_limit != expected { - return Err(BlockExecutionError::Validation( - BlockValidationError::Other( - format!( - "block gas limit {block_gas_limit} does not match expected {expected}" - ) - .into(), - ), - )); - } - } - - Ok(()) - } - - fn execute_transaction_without_commit( - &mut self, - tx: impl ExecutableTx, - ) -> Result { - let (tx_env, tx) = tx.into_parts(); - - // The sum of the transaction's gas limit, Tg, and the gas utilized in this block prior, - // must be no greater than the block's gasLimit. - let block_available_gas = self - .evm - .block() - .gas_limit() - .checked_sub(self.gas_used) - .expect("gas_used must not exceed block gas_limit"); - - if tx.tx().gas_limit() > block_available_gas { - return Err( - BlockValidationError::TransactionGasLimitMoreThanAvailableBlockGas { - transaction_gas_limit: tx.tx().gas_limit(), - block_available_gas, - } - .into(), - ); - } - - // Execute transaction. - let result = self - .evm - .transact(tx_env) - .map_err(|err| BlockExecutionError::evm(err, tx.tx().trie_hash()))?; - - Ok(ArcTxResult { - result, - blob_gas_used: tx.tx().blob_gas_used().unwrap_or_default(), - tx_type: tx.tx().tx_type(), - }) - } - - fn commit_transaction(&mut self, output: Self::Result) -> Result { - let ArcTxResult { - result: ResultAndState { result, state }, - blob_gas_used, - tx_type, - } = output; - - self.system_caller - .on_state(StateChangeSource::Transaction(self.receipts.len()), &state); - - let gas_used = result.gas_used(); - - // append gas used - self.gas_used = self - .gas_used - .checked_add(gas_used) - .expect("cumulative gas overflow"); - - // Cancun is always active for arc - self.blob_gas_used = self.blob_gas_used.saturating_add(blob_gas_used); - - // Push transaction changeset and calculate header bloom filter for receipt. - self.receipts - .push(self.receipt_builder.build_receipt(ReceiptBuilderCtx { - tx_type, - evm: &self.evm, - result, - state: &state, - cumulative_gas_used: self.gas_used, - })); - - // Commit the state changes. - self.evm.db_mut().commit(state); - - Ok(gas_used) - } - - fn finish( - mut self, - ) -> Result<(Self::Evm, BlockExecutionResult), BlockExecutionError> { - // EIP-6110 not activated - let requests = Requests::default(); - - let block_number = self.block_number_u64()?; - let block_timestamp = self.block_timestamp_u64()?; - - // At the end of the block, call a system contract (precompile) to persist gas accounting - // state: raw gas used, smoothed gas used, and the next block's base fee. - let fee_params = protocol_config::retrieve_fee_params(&mut self.evm) - .inspect_err(|e| { - tracing::error!( - error = %e, - block_number, - "Failed to retrieve fee params from ProtocolConfig" - ); - }) - .ok(); - let is_zero5 = is_arc_fork_active( - &self.chain_spec, - ArcHardfork::Zero5, - block_number, - block_timestamp, - ); - let gas_values = if is_zero5 { - // ADR-0004 implementation: compute gas values within local bounds - self.compute_gas_values(block_number, fee_params)? - } else { - self.compute_gas_values_legacy(block_number, fee_params)? - }; - - // ADR-004: enforce extra_data matches what is computed, but only when executing an - // existing payload (extra_data already set by consensus). During block building the - // executor writes extra_data itself, so it is empty at this point — skip validation. - if is_zero5 && !self.ctx.extra_data.is_empty() { - self.validate_extra_data_base_fee(block_number, gas_values.nextBaseFee)?; - } - - let state = system_accounting::store_gas_values(block_number, gas_values, &mut self.evm) - .map_err(|e| { - tracing::error!(error = %e, "Failed to store gas values to SystemAccounting"); - BlockExecutionError::Internal(InternalBlockExecutionError::Other(Box::new(e))) - })?; - - // BalanceIncrements is semantically imprecise (this is a storage write, not a balance - // change), but it's the least-wrong variant available in the upstream enum, and functionally - // equivalent to others. - self.system_caller.on_state( - StateChangeSource::PostBlock(StateChangePostBlockSource::BalanceIncrements), - &state, - ); - - Ok(( - self.evm, - BlockExecutionResult { - receipts: self.receipts, - requests, - gas_used: self.gas_used, - blob_gas_used: self.blob_gas_used, - }, - )) - } - - fn receipts(&self) -> &[Self::Receipt] { - &self.receipts - } - - fn set_state_hook(&mut self, hook: Option>) { - self.system_caller.with_state_hook(hook); - } - - fn evm_mut(&mut self) -> &mut Self::Evm { - &mut self.evm - } - - fn evm(&self) -> &Self::Evm { - &self.evm - } -} - -#[cfg(test)] -mod tests { - extern crate alloc; - - use super::*; - - use alloy_genesis::Genesis; - use alloy_primitives::address; - use alloy_primitives::map::HashMap; - use alloy_primitives::B256 as AlloyB256; - use alloy_primitives::KECCAK256_EMPTY; - use reth_chainspec::{EthChainSpec, ForkCondition}; - use reth_evm::ConfigureEvm; - use reth_evm::EvmEnv; - - use revm::{ - context::{BlockEnv, CfgEnv}, - database::InMemoryDB, - state::{AccountInfo, Bytecode}, - }; - use revm_primitives::ruint::aliases::U256; - use revm_primitives::{hardfork::SpecId, keccak256}; - use revm_primitives::{StorageKey, StorageValue}; - - use arc_execution_config::chainspec::{ - localdev_with_hardforks, ArcChainSpec, BaseFeeConfigProvider, LOCAL_DEV, - }; - - // Build env from localdev genesis so ProtocolConfig is available - pub fn insert_alloc_into_db(db: &mut InMemoryDB, genesis: &Genesis) { - for addr in genesis.alloc.keys() { - let data = genesis.alloc.get(addr).unwrap().clone(); - match data.code.clone() { - Some(code) => db.insert_account_info( - *addr, - AccountInfo { - balance: data.balance, - nonce: data.nonce.unwrap_or_default(), - code_hash: keccak256(&code), - code: Some(Bytecode::new_raw(code)), - account_id: None, - }, - ), - None => db.insert_account_info( - *addr, - AccountInfo { - balance: data.balance, - nonce: data.nonce.unwrap_or_default(), - code_hash: KECCAK256_EMPTY, - code: None, - account_id: None, - }, - ), - } - for (k, v) in data.storage_slots() { - db.insert_account_storage(*addr, k.into(), v) - .expect("insert storage"); - } - } - } - - // Input values used only for pre-Zero5 patch_fee_params calls — not used as expected outputs. - const GENESIS_K_RATE: u64 = 200; - const GENESIS_INVERSE_ELASTICITY_MULTIPLIER: u64 = 5000; - const GAS_USED: u64 = 100_000; - - fn get_mock_block_env() -> BlockEnv { - BlockEnv { - basefee: 10000, - gas_limit: 30000000, - ..Default::default() - } - } - - /// Helper function to create a block execution context - fn get_mock_execution_ctx<'a>() -> reth_evm::eth::EthBlockExecutionCtx<'a> { - reth_evm::eth::EthBlockExecutionCtx { - parent_hash: AlloyB256::ZERO, - parent_beacon_block_root: None, - ommers: &[], - withdrawals: None, - extra_data: Default::default(), - tx_count_hint: None, - } - } - - /// Helper function to create an ArcEvmConfig - fn create_evm_config(chain_spec: alloc::sync::Arc) -> crate::evm::ArcEvmConfig { - crate::evm::ArcEvmConfig::new(reth_ethereum::evm::EthEvmConfig::new_with_evm_factory( - chain_spec.clone(), - crate::evm::ArcEvmFactory::new(chain_spec), - )) - } - - fn mark_address_as_blocklisted(db: &mut InMemoryDB, beneficiary: Address) { - let storage_slot = compute_is_blocklisted_storage_slot(beneficiary).into(); - db.insert_account_storage( - arc_precompiles::NATIVE_COIN_CONTROL_ADDRESS, - storage_slot, - StorageValue::from(1u64), - ) - .expect("Insert storage"); - } - - /// Runs the executor finish() and returns the gas values stored in the precompile - fn run_executor_finish_and_query_gas_values( - chain_spec: alloc::sync::Arc, - block_env: &BlockEnv, - db: &mut InMemoryDB, - ) -> system_accounting::GasValues { - // Build EVM env manually (mirrors tests/common.rs pattern) - let cfg_env = CfgEnv::new() - .with_chain_id(chain_spec.chain_id()) - .with_spec_and_mainnet_gas_params(SpecId::PRAGUE); - let evm_env = EvmEnv { - cfg_env, - block_env: block_env.clone(), - }; - - let evm_config = - crate::evm::ArcEvmConfig::new(reth_ethereum::evm::EthEvmConfig::new_with_evm_factory( - chain_spec.clone(), - crate::evm::ArcEvmFactory::new(chain_spec.clone()), - )); - - let mut state = reth_ethereum::evm::revm::db::State::builder() - .with_database(db) // or `state.set_db(db)` depending on your version - .build(); - - let evm = evm_config.evm_with_env(&mut state, evm_env); - let ctx = reth_evm::eth::EthBlockExecutionCtx { - parent_hash: AlloyB256::ZERO, - parent_beacon_block_root: None, - ommers: &[], - withdrawals: None, - extra_data: Default::default(), - tx_count_hint: None, - }; - - let mut executor = ArcBlockExecutor::new( - evm, - ctx, - chain_spec.clone(), - evm_config.inner.executor_factory.receipt_builder(), - ); - executor.gas_used = GAS_USED; - - let (mut evm_after, _result) = executor.finish().expect("finish()"); - let current_block_number = 0u64; // block env default number in our test - arc_precompiles::system_accounting::retrieve_gas_values( - current_block_number, - &mut evm_after, - ) - .expect("retrieve") - } - - #[test] - fn test_executor_stores_smoothed_gas_used_according_to_protocol_config() { - let block_env = get_mock_block_env(); - - let chain_spec = LOCAL_DEV.clone(); - - let mut db = InMemoryDB::default(); - insert_alloc_into_db(&mut db, chain_spec.genesis()); - - let stored = - run_executor_finish_and_query_gas_values(chain_spec.clone(), &block_env, &mut db); - let block_env = get_mock_block_env(); - - assert_eq!(stored.gasUsed, GAS_USED); - let defaults = chain_spec.base_fee_config(1).resolve_calc_params(None); - let expected_smoothed = GAS_USED * defaults.alpha / 100u64; - assert_eq!(stored.gasUsedSmoothed, expected_smoothed); - let expected_next_base_fee = arc_calc_next_block_base_fee( - expected_smoothed, - block_env.gas_limit, - block_env.basefee, - defaults.k_rate, - defaults.inverse_elasticity_multiplier, - ); - assert_eq!(stored.nextBaseFee, expected_next_base_fee); - } - - #[test] - fn test_executor_stores_raw_gas_used_if_protocol_config_is_not_available() { - let block_env = get_mock_block_env(); - - // Brick the protocol config contract by overwriting the implementation slot - // Implementation slot: 0x360894a13ba1a3210667c828492db98dca3e2076cc3735a920a3ca505d382bbc - fn patch_protocol_config_to_invalid_impl(db: &mut InMemoryDB) { - db.replace_account_storage( - address!("3600000000000000000000000000000000000001"), - HashMap::from_iter([( - StorageKey::from_str_radix( - "360894a13ba1a3210667c828492db98dca3e2076cc3735a920a3ca505d382bbc", - 16, - ) - .unwrap(), - StorageValue::from(0u64), - )]), - ) - .expect("Replace storage"); - } - - // ADR-0004 (Zero5+): When ProtocolConfig is unavailable, the executor uses - // each field's default from the chainspec BaseFeeConfig - let chain_spec = LOCAL_DEV.clone(); // Zero5 active at block 0 - - let mut db = InMemoryDB::default(); - insert_alloc_into_db(&mut db, chain_spec.genesis()); - patch_protocol_config_to_invalid_impl(&mut db); - let stored = - run_executor_finish_and_query_gas_values(chain_spec.clone(), &block_env, &mut db); - assert_eq!(stored.gasUsed, GAS_USED); - - // EMA smoothing and fee calculation use each field's default from the chainspec BaseFeeConfig. - let defaults = chain_spec.base_fee_config(1).resolve_calc_params(None); - let expected_smoothed = GAS_USED * defaults.alpha / 100u64; - assert_eq!(stored.gasUsedSmoothed, expected_smoothed); - let expected_next_base_fee = arc_calc_next_block_base_fee( - expected_smoothed, - block_env.gas_limit, - block_env.basefee, - defaults.k_rate, - defaults.inverse_elasticity_multiplier, - ); - assert_eq!(stored.nextBaseFee, expected_next_base_fee); - assert_ne!( - stored.nextBaseFee, 0, - "ADR-004 fallback must produce a non-zero base fee" - ); - } - - /// Packs `(alpha, k_rate, inverse_elasticity_multiplier)` into the single storage word that - /// ProtocolConfig stores at the ERC-7201 base slot. - /// - /// Layout (from `scripts/genesis/ProtocolConfig.ts`): - /// bits [0,63] – alpha - /// bits [64,127] – kRate - /// bits [128,191] – inverseElasticityMultiplier - fn pack_fee_params_slot( - alpha: u64, - k_rate: u64, - inverse_elasticity_multiplier: u64, - ) -> StorageValue { - U256::from(alpha) - | (U256::from(k_rate) << 64) - | (U256::from(inverse_elasticity_multiplier) << 128) - } - - /// ERC-7201 base slot for ProtocolConfig storage. - const PROTOCOL_CONFIG_FEE_PARAMS_SLOT: &str = - "668f09ce856848ead6cb1ddee963f15ef833cea8958030868f867aec84385200"; - - /// Overwrites the packed fee-params slot in ProtocolConfig storage with the given values, - /// leaving minBaseFee/maxBaseFee/blockGasLimit untouched. - fn patch_fee_params( - db: &mut InMemoryDB, - alpha: u64, - k_rate: u64, - inverse_elasticity_multiplier: u64, - ) { - let slot = - StorageKey::from_str_radix(PROTOCOL_CONFIG_FEE_PARAMS_SLOT, 16).expect("valid hex"); - db.insert_account_storage( - protocol_config::PROTOCOL_CONFIG_ADDRESS, - slot, - pack_fee_params_slot(alpha, k_rate, inverse_elasticity_multiplier), - ) - .expect("insert storage"); - } - - #[test] - fn test_zero4_invalid_alpha_stores_zero_next_base_fee() { - let block_env = get_mock_block_env(); - let chain_spec = localdev_with_hardforks(&[(ArcHardfork::Zero3, ForkCondition::Block(0))]); - - let mut db = InMemoryDB::default(); - insert_alloc_into_db(&mut db, chain_spec.genesis()); - patch_fee_params( - &mut db, - 255, - GENESIS_K_RATE, - GENESIS_INVERSE_ELASTICITY_MULTIPLIER, - ); - - let stored = run_executor_finish_and_query_gas_values(chain_spec, &block_env, &mut db); - - assert_eq!(stored.gasUsed, GAS_USED); - assert_eq!(stored.gasUsedSmoothed, GAS_USED); - assert_eq!(stored.nextBaseFee, 0); - } - - #[test] - fn test_zero5_executor_out_of_range_alpha_uses_default() { - // alpha=255 exceeds alpha.max for localdev; zero5 will substitute alpha.default - let block_env = get_mock_block_env(); - let chain_spec = LOCAL_DEV.clone(); - - let defaults = chain_spec.base_fee_config(1).resolve_calc_params(None); - - let mut db = InMemoryDB::default(); - insert_alloc_into_db(&mut db, chain_spec.genesis()); - patch_fee_params( - &mut db, - 255, - defaults.k_rate, - defaults.inverse_elasticity_multiplier, - ); - - let stored = run_executor_finish_and_query_gas_values(chain_spec, &block_env, &mut db); - - // alpha=255 is out of range; alpha.default is used - let expected_smoothed = GAS_USED * defaults.alpha / 100u64; - assert_eq!(stored.gasUsedSmoothed, expected_smoothed); - let expected_next_base_fee = arc_calc_next_block_base_fee( - expected_smoothed, - block_env.gas_limit, - block_env.basefee, - defaults.k_rate, - defaults.inverse_elasticity_multiplier, - ); - assert_eq!(stored.nextBaseFee, expected_next_base_fee); - } - - #[test] - fn test_zero5_executor_out_of_range_k_rate_uses_default() { - let block_env = get_mock_block_env(); - let chain_spec = LOCAL_DEV.clone(); - - let defaults = chain_spec.base_fee_config(1).resolve_calc_params(None); - - let mut db = InMemoryDB::default(); - insert_alloc_into_db(&mut db, chain_spec.genesis()); - patch_fee_params( - &mut db, - defaults.alpha, - 20000, - defaults.inverse_elasticity_multiplier, - ); - - let stored = run_executor_finish_and_query_gas_values(chain_spec, &block_env, &mut db); - - let expected_smoothed = GAS_USED * defaults.alpha / 100u64; - // k_rate=20000 is out of range; k_rate.default must be used. - let expected_next_base_fee = arc_calc_next_block_base_fee( - expected_smoothed, - block_env.gas_limit, - block_env.basefee, - defaults.k_rate, - defaults.inverse_elasticity_multiplier, - ); - assert_eq!(stored.nextBaseFee, expected_next_base_fee); - } - - #[test] - fn test_zero5_executor_out_of_range_elasticity_multiplier_uses_default() { - let block_env = get_mock_block_env(); - let chain_spec = LOCAL_DEV.clone(); - - let defaults = chain_spec.base_fee_config(1).resolve_calc_params(None); - - let mut db = InMemoryDB::default(); - insert_alloc_into_db(&mut db, chain_spec.genesis()); - patch_fee_params(&mut db, defaults.alpha, defaults.k_rate, 0); - - let stored = run_executor_finish_and_query_gas_values(chain_spec, &block_env, &mut db); - - let expected_smoothed = GAS_USED * defaults.alpha / 100u64; - // inverse_elasticity_multiplier=0 is below min; inverse_elasticity_multiplier.default must be used. - let expected_next_base_fee = arc_calc_next_block_base_fee( - expected_smoothed, - block_env.gas_limit, - block_env.basefee, - defaults.k_rate, - defaults.inverse_elasticity_multiplier, - ); - assert_eq!(stored.nextBaseFee, expected_next_base_fee); - } - - #[test] - fn test_zero5_executor_in_range_params_pass_through() { - // All params are within bounds; the on-chain values must be used as-is (no substitution). - let block_env = get_mock_block_env(); - let chain_spec = LOCAL_DEV.clone(); - - // alpha=50 (in [0,100]), k_rate=500 (in [0,10000]), inverse_elasticity_multiplier=3000 (in [1,10000]) - const CUSTOM_ALPHA: u64 = 50; - const CUSTOM_K_RATE: u64 = 500; - const CUSTOM_ELASTICITY: u64 = 3000; - - let mut db = InMemoryDB::default(); - insert_alloc_into_db(&mut db, chain_spec.genesis()); - patch_fee_params(&mut db, CUSTOM_ALPHA, CUSTOM_K_RATE, CUSTOM_ELASTICITY); - - let stored = run_executor_finish_and_query_gas_values(chain_spec, &block_env, &mut db); - - let expected_smoothed = GAS_USED * CUSTOM_ALPHA / 100u64; - assert_eq!(stored.gasUsedSmoothed, expected_smoothed); - let expected_next_base_fee = arc_calc_next_block_base_fee( - expected_smoothed, - block_env.gas_limit, - block_env.basefee, - CUSTOM_K_RATE, - CUSTOM_ELASTICITY, - ); - assert_eq!(stored.nextBaseFee, expected_next_base_fee); - } - - #[test] - fn test_zero5_executor_payload_rejects_mismatched_extra_data_base_fee() { - let block_env = get_mock_block_env(); - let chain_spec = LOCAL_DEV.clone(); - - let mut db = InMemoryDB::default(); - insert_alloc_into_db(&mut db, chain_spec.genesis()); - - let cfg_env = CfgEnv::new() - .with_chain_id(chain_spec.chain_id()) - .with_spec_and_mainnet_gas_params(SpecId::PRAGUE); - let evm_env = EvmEnv { - cfg_env, - block_env: block_env.clone(), - }; - - let evm_config = - crate::evm::ArcEvmConfig::new(reth_ethereum::evm::EthEvmConfig::new_with_evm_factory( - chain_spec.clone(), - crate::evm::ArcEvmFactory::new(chain_spec.clone()), - )); - - let mut state = reth_ethereum::evm::revm::db::State::builder() - .with_database(&mut db) - .build(); - let evm = evm_config.evm_with_env(&mut state, evm_env); - - let mut ctx = get_mock_execution_ctx(); - // Non-empty extra_data signals payload execution (consensus set the value); wrong on purpose - ctx.extra_data = arc_execution_config::gas_fee::encode_base_fee_to_bytes(1); - - let mut executor = ArcBlockExecutor::new( - evm, - ctx, - chain_spec, - evm_config.inner.executor_factory.receipt_builder(), - ); - executor.gas_used = GAS_USED; - - let err = executor - .finish() - .expect_err("Zero5 payload with mismatched extra_data must be rejected"); - let err_msg = err.to_string(); - assert!( - err_msg.contains("extra_data base fee mismatch"), - "unexpected error: {err_msg}" - ); - } - - #[test] - fn test_validate_beneficiary_not_blocklisted_rejects_blocklisted_address() { - let mut db = InMemoryDB::default(); - let blocklisted_beneficiary = address!("0000000000000000000000000000000000000bad"); - mark_address_as_blocklisted(&mut db, blocklisted_beneficiary); - - let err = validate_beneficiary_not_blocklisted(&mut db, blocklisted_beneficiary, 10) - .expect_err("Blocklisted beneficiary should be rejected"); - match err { - BlockExecutionError::Validation(validation_err) => { - let err_msg = validation_err.to_string(); - assert!( - err_msg.contains(ERR_BLOCKED_ADDRESS), - "Expected validation error containing '{}', got: {}", - ERR_BLOCKED_ADDRESS, - err_msg - ); - } - other => panic!("Expected BlockExecutionError::Validation, got {:?}", other), - } - } - - #[test] - fn test_beneficiary_validation_skipped_before_zero5() { - // Test that beneficiary validation is skipped for blocks before Zero5 hardfork - let chain_spec = localdev_with_hardforks(&[(ArcHardfork::Zero4, ForkCondition::Block(0))]); - - let mut db = InMemoryDB::default(); - insert_alloc_into_db(&mut db, chain_spec.genesis()); - - let evm_config = create_evm_config(chain_spec.clone()); - - // Use a wrong beneficiary - should still pass because we're before Zero5 - let wrong_beneficiary = address!("0000000000000000000000000000000000000bad"); - - let mut block_env = get_mock_block_env(); - block_env.number = U256::from(0); // Before Zero5 - block_env.beneficiary = wrong_beneficiary; - - let cfg_env = CfgEnv::new() - .with_chain_id(chain_spec.chain_id()) - .with_spec_and_mainnet_gas_params(SpecId::PRAGUE); - let evm_env = EvmEnv { cfg_env, block_env }; - - let mut state = State::builder().with_database(db).build(); - let evm = evm_config.evm_with_env(&mut state, evm_env); - - let ctx = get_mock_execution_ctx(); - - let mut executor = ArcBlockExecutor::new( - evm, - ctx, - chain_spec.as_ref(), - evm_config.inner.executor_factory.receipt_builder(), - ); - - // This should succeed because validation is skipped before Zero5 - let result = executor.apply_pre_execution_changes(); - assert!( - result.is_ok(), - "Beneficiary validation should be skipped before Zero5 hardfork" - ); - } - - #[test] - fn test_beneficiary_validation_fails_when_proposer_beneficiary_is_blocklisted() { - let chain_spec = LOCAL_DEV.clone(); - - let mut db = InMemoryDB::default(); - insert_alloc_into_db(&mut db, chain_spec.genesis()); - - let blocklisted_beneficiary = address!("0000000000000000000000000000000000000bad"); - mark_address_as_blocklisted(&mut db, blocklisted_beneficiary); - let storage_slot = compute_is_blocklisted_storage_slot(blocklisted_beneficiary).into(); - let blocklist_status = ::storage( - &mut db, - arc_precompiles::NATIVE_COIN_CONTROL_ADDRESS, - storage_slot, - ) - .expect("Read blocklist storage"); - assert_eq!( - blocklist_status, - StorageValue::from(1u64), - "Beneficiary should be blocklisted in NativeCoinControl storage" - ); - - let evm_config = create_evm_config(chain_spec.clone()); - - let mut block_env = get_mock_block_env(); - block_env.number = U256::from(10); - block_env.beneficiary = blocklisted_beneficiary; - - let cfg_env = CfgEnv::new() - .with_chain_id(chain_spec.chain_id()) - .with_spec_and_mainnet_gas_params(SpecId::PRAGUE); - let evm_env = EvmEnv { cfg_env, block_env }; - - let mut state = State::builder().with_database(db).build(); - let evm = evm_config.evm_with_env(&mut state, evm_env); - let ctx = get_mock_execution_ctx(); - let mut executor = ArcBlockExecutor::new( - evm, - ctx, - chain_spec.as_ref(), - evm_config.inner.executor_factory.receipt_builder(), - ); - - let result = executor.apply_pre_execution_changes(); - match result { - Err(BlockExecutionError::Validation(err)) => { - let err_msg = err.to_string(); - assert!( - err_msg.contains(ERR_BLOCKED_ADDRESS), - "Expected validation error containing '{}', got: {}", - ERR_BLOCKED_ADDRESS, - err_msg - ); - } - other => panic!( - "Expected BlockExecutionError::Validation containing '{}', got: {:?}", - ERR_BLOCKED_ADDRESS, other - ), - } - } - - #[derive(Debug, thiserror::Error)] - #[error("forced blocklist storage read failure")] - struct ForcedBlocklistReadError; - impl revm::database_interface::DBErrorMarker for ForcedBlocklistReadError {} - - #[derive(Debug)] - struct BlocklistReadFailingDb { - inner: InMemoryDB, - } - - impl BlocklistReadFailingDb { - fn new(inner: InMemoryDB) -> Self { - Self { inner } - } - } - - impl revm::Database for BlocklistReadFailingDb { - type Error = ForcedBlocklistReadError; - - fn basic(&mut self, address: Address) -> Result, Self::Error> { - ::basic(&mut self.inner, address) - .map_err(|infallible: core::convert::Infallible| match infallible {}) - } - - fn code_by_hash(&mut self, code_hash: AlloyB256) -> Result { - ::code_by_hash(&mut self.inner, code_hash) - .map_err(|infallible: core::convert::Infallible| match infallible {}) - } - - fn storage( - &mut self, - address: Address, - index: StorageKey, - ) -> Result { - if address == arc_precompiles::NATIVE_COIN_CONTROL_ADDRESS { - return Err(ForcedBlocklistReadError); - } - ::storage(&mut self.inner, address, index) - .map_err(|infallible: core::convert::Infallible| match infallible {}) - } - - fn block_hash(&mut self, number: u64) -> Result { - ::block_hash(&mut self.inner, number) - .map_err(|infallible: core::convert::Infallible| match infallible {}) - } - } - - #[test] - fn test_beneficiary_validation_fails_when_blocklist_read_fails() { - let chain_spec = LOCAL_DEV.clone(); - - let mut base_db = InMemoryDB::default(); - insert_alloc_into_db(&mut base_db, chain_spec.genesis()); - - let db = BlocklistReadFailingDb::new(base_db); - let evm_config = create_evm_config(chain_spec.clone()); - let beneficiary = address!("0000000000000000000000000000000000000bad"); - - let mut block_env = get_mock_block_env(); - block_env.number = U256::from(10); - block_env.beneficiary = beneficiary; - - let cfg_env = CfgEnv::new() - .with_chain_id(chain_spec.chain_id()) - .with_spec_and_mainnet_gas_params(SpecId::PRAGUE); - let evm_env = EvmEnv { cfg_env, block_env }; - - let mut state = State::builder().with_database(db).build(); - let evm = evm_config.evm_with_env(&mut state, evm_env); - let ctx = get_mock_execution_ctx(); - let mut executor = ArcBlockExecutor::new( - evm, - ctx, - chain_spec.as_ref(), - evm_config.inner.executor_factory.receipt_builder(), - ); - - let result = executor.apply_pre_execution_changes(); - match result { - Err(BlockExecutionError::Validation(validation_err)) => { - let err_msg = validation_err.to_string(); - assert!( - err_msg.contains(ERR_BLOCKLIST_READ_FAILED), - "Expected validation error containing '{}', got: {}", - ERR_BLOCKLIST_READ_FAILED, - err_msg - ); - } - other => panic!( - "Expected BlockExecutionError::Validation containing '{}', got: {:?}", - ERR_BLOCKLIST_READ_FAILED, other - ), - } - } -} diff --git a/crates/evm/src/frame_result.rs b/crates/evm/src/frame_result.rs deleted file mode 100644 index 58565c0..0000000 --- a/crates/evm/src/frame_result.rs +++ /dev/null @@ -1,161 +0,0 @@ -// Copyright 2025 Circle Internet Group, Inc. All rights reserved. -// -// SPDX-License-Identifier: Apache-2.0 -// -// Licensed under the Apache License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. -// You may obtain a copy of the License at -// -// http://www.apache.org/licenses/LICENSE-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// See the License for the specific language governing permissions and -// limitations under the License. - -//! Frame result utilities for Arc EVM handler - -use arc_precompiles::helpers::revert_message_to_bytes; -use reth_ethereum::primitives::Log; -use revm::{ - handler::FrameResult, - interpreter::{ - interpreter_action::{FrameInit, FrameInput}, - CallOutcome, CreateOutcome, Gas, InstructionResult, InterpreterResult, - }, -}; - -/// Result of processing a native transfer with blocklist validation -#[derive(Debug)] -pub enum BeforeFrameInitResult { - /// Transfer is valid, emit this log. The `u64` is the total SLOAD gas cost - /// for blocklist checks (warm/cold-aware for Zero6+). - Log(Log, u64), - /// Blocklist passed, but no log to emit (e.g., Zero5 self-transfer). The `u64` is SLOAD gas. - Checked(u64), - /// Transfer reverted due to blocklist violation - Reverted(FrameResult), - /// No transfer to process (zero amount or non-value operation). No SLOADs performed. - None, -} - -/// Creates a new FrameResult for out-of-gas during blocklist checks. -/// This is used when a nested frame doesn't have enough gas to cover the SLOAD costs -/// for blocklist verification. -pub fn create_oog_frame_result(frame_init: &FrameInit) -> FrameResult { - match &frame_init.frame_input { - FrameInput::Call(call_input) => { - // All gas is consumed on OOG - let mut gas_counter = Gas::new(call_input.gas_limit); - let _ = gas_counter.record_cost(call_input.gas_limit); - - let interpreter_result = InterpreterResult::new( - InstructionResult::OutOfGas, - Default::default(), - gas_counter, - ); - FrameResult::Call(CallOutcome { - result: interpreter_result, - memory_offset: call_input.return_memory_offset.clone(), - was_precompile_called: false, - precompile_call_logs: Default::default(), - }) - } - FrameInput::Create(create_input) => { - // All gas is consumed on OOG - let mut gas_counter = Gas::new(create_input.gas_limit()); - let _ = gas_counter.record_cost(create_input.gas_limit()); - - let interpreter_result = InterpreterResult::new( - InstructionResult::OutOfGas, - Default::default(), - gas_counter, - ); - FrameResult::Create(CreateOutcome { - result: interpreter_result, - address: None, - }) - } - FrameInput::Empty => unreachable!(), - } -} - -/// Creates a new FrameResult for a given frame init and error message. -pub fn create_frame_result( - frame_init: &FrameInit, - error_message: &str, - gas_spent: u64, -) -> FrameResult { - let revert_data = revert_message_to_bytes(error_message); - - match &frame_init.frame_input { - FrameInput::Call(call_input) => { - let mut gas_counter = Gas::new(call_input.gas_limit); - gas_counter.set_spent(gas_spent); - - let interpreter_result = - InterpreterResult::new(InstructionResult::Revert, revert_data, gas_counter); - FrameResult::Call(CallOutcome { - result: interpreter_result, - memory_offset: call_input.return_memory_offset.clone(), - was_precompile_called: false, - precompile_call_logs: Default::default(), - }) - } - FrameInput::Create(create_input) => { - let mut gas_counter = Gas::new(create_input.gas_limit()); - gas_counter.set_spent(gas_spent); - - let interpreter_result = - InterpreterResult::new(InstructionResult::Revert, revert_data, gas_counter); - FrameResult::Create(CreateOutcome { - result: interpreter_result, - address: None, - }) - } - FrameInput::Empty => unreachable!(), - } -} - -#[cfg(test)] -mod tests { - use super::*; - use alloy_primitives::{Address, Bytes, U256}; - use arc_precompiles::helpers::ERR_BLOCKED_ADDRESS; - use revm::interpreter::{ - interpreter_action::{CreateInputs, FrameInit, FrameInput}, - SharedMemory, - }; - use revm_interpreter::CreateScheme; - - #[test] - fn test_create_frame_result_returns_none_address_for_create() { - let create_inputs = CreateInputs::new( - Address::repeat_byte(0x42), - CreateScheme::Create, - U256::ZERO, - Bytes::from(vec![0x60, 0x00, 0x60, 0x00, 0xF3]), // minimal bytecode - 100_000, - ); - - let frame_init = FrameInit { - depth: 0, - memory: SharedMemory::default(), - frame_input: FrameInput::Create(Box::new(create_inputs)), - }; - - let result = create_frame_result(&frame_init, ERR_BLOCKED_ADDRESS, 0); - - match result { - FrameResult::Create(outcome) => { - assert!( - outcome.address.is_none(), - "Create outcome should have None address for blocklisted frame" - ); - assert_eq!(outcome.result.result, InstructionResult::Revert); - } - _ => panic!("Expected FrameResult::Create variant"), - } - } -} diff --git a/crates/evm/src/handler.rs b/crates/evm/src/handler.rs deleted file mode 100644 index 1bb5869..0000000 --- a/crates/evm/src/handler.rs +++ /dev/null @@ -1,739 +0,0 @@ -// Copyright 2025 Circle Internet Group, Inc. All rights reserved. -// -// SPDX-License-Identifier: Apache-2.0 -// -// Licensed under the Apache License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. -// You may obtain a copy of the License at -// -// http://www.apache.org/licenses/LICENSE-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// See the License for the specific language governing permissions and -// limitations under the License. - -use alloy_primitives::{Address, U256}; -use arc_execution_config::hardforks::ArcHardforkFlags; -use arc_execution_config::native_coin_control::{ - compute_is_blocklisted_storage_slot, is_blocklisted_status, -}; -use arc_precompiles::helpers::ERR_BLOCKED_ADDRESS; -use arc_precompiles::NATIVE_COIN_CONTROL_ADDRESS; -use revm::inspector::{Inspector, InspectorEvmTr, InspectorHandler}; -use revm::{ - context::{ContextTr, JournalTr, Transaction}, - context_interface::result::InvalidTransaction, - context_interface::{result::HaltReason, Block}, - handler::{EthFrame, EvmTr, EvmTrError, FrameTr, Handler, MainnetHandler}, - interpreter::{interpreter::EthInterpreter, InitialAndFloorGas}, - state::EvmState, -}; -use revm_primitives::TxKind; - -// Handler Implementation -// See: https://github.com/bluealloy/revm/blob/v97/examples/erc20_gas/src/handler.rs#L19 -pub struct ArcEvmHandler { - mainnet: MainnetHandler>, - /// Feature flags for Arc hardforks active at the current block. - /// Retained for future hardfork-gated handler behavior (e.g. new precompiles). - #[allow(dead_code)] - hardfork_flags: ArcHardforkFlags, -} - -impl ArcEvmHandler { - pub fn new(hardfork_flags: ArcHardforkFlags) -> Self { - Self { - mainnet: MainnetHandler::default(), - hardfork_flags, - } - } -} - -impl Handler for ArcEvmHandler -where - EVM: EvmTr< - Context: ContextTr>, - Frame = EthFrame, - >, - ERROR: EvmTrError, -{ - type Evm = EVM; - type Error = ERROR; - type HaltReason = HaltReason; - - #[inline] - fn pre_execution(&self, evm: &mut Self::Evm) -> Result { - let ctx = evm.ctx(); - let tx = ctx.tx(); - let caller = tx.caller(); - let tx_kind = tx.kind(); - let tx_value = tx.value(); - - evm.ctx_mut() - .journal_mut() - .load_account(NATIVE_COIN_CONTROL_ADDRESS)?; - self.check_blocklist(evm, caller, &tx_kind, tx_value)?; - - self.mainnet.pre_execution(evm) - } - - #[inline] - fn reward_beneficiary( - &self, - evm: &mut Self::Evm, - exec_result: &mut <::Frame as FrameTr>::FrameResult, - ) -> Result<(), Self::Error> { - // Redirect both base fee and priority fee to the specified beneficiary - // This overrides the default EIP-1559 behavior which burns the base fee - let ctx = evm.ctx(); - let beneficiary = ctx.block().beneficiary(); - let basefee = ctx.block().basefee() as u128; - let effective_gas_price = ctx.tx().effective_gas_price(basefee); - let gas_used = exec_result.gas().used(); - - // u128 * u64 fits in U256 (max 192 bits). - #[allow(clippy::arithmetic_side_effects)] - let total_fee_amount = U256::from(effective_gas_price) * U256::from(gas_used); - - // Transfer the total fee to the beneficiary (both base fee and priority fee) - evm.ctx_mut() - .journal_mut() - .balance_incr(beneficiary, total_fee_amount) - .map_err(From::from) - } - - /// Returns base intrinsic gas from the mainnet handler. - /// - /// Blocklist SLOADs are unmetered — no extra gas is added for blocklist checks. - #[inline] - fn validate_initial_tx_gas( - &self, - evm: &mut Self::Evm, - ) -> Result { - self.mainnet.validate_initial_tx_gas(evm) - } -} - -// Implementing InspectorHandler for ArcEvmHandler. -impl InspectorHandler for ArcEvmHandler -where - EVM: InspectorEvmTr< - Context: ContextTr>, - Frame = EthFrame, - Inspector: Inspector<<::Evm as EvmTr>::Context, EthInterpreter>, - >, - ERROR: EvmTrError, -{ - type IT = EthInterpreter; -} - -impl ArcEvmHandler -where - EVM: EvmTr< - Context: ContextTr>, - Frame = EthFrame, - >, - ERROR: EvmTrError, -{ - fn check_blocklist( - &self, - evm: &mut EVM, - caller: Address, - tx_kind: &TxKind, - tx_value: U256, - ) -> Result<(), ERROR> { - if self.is_address_blocklisted(evm, caller)? { - return Err(InvalidTransaction::Str(ERR_BLOCKED_ADDRESS.into()).into()); - } - if let TxKind::Call(to_address) = tx_kind { - if !tx_value.is_zero() && self.is_address_blocklisted(evm, *to_address)? { - return Err(InvalidTransaction::Str(ERR_BLOCKED_ADDRESS.into()).into()); - } - } - Ok(()) - } - - /// Checks if an address is blocklisted by reading from the native coin control precompile storage. - fn is_address_blocklisted(&self, evm: &mut EVM, address: Address) -> Result { - let storage_slot = compute_is_blocklisted_storage_slot(address).into(); - let journal = evm.ctx_mut().journal_mut(); - - let state_load = journal.sload(NATIVE_COIN_CONTROL_ADDRESS, storage_slot)?; - Ok(is_blocklisted_status(state_load.data)) - } -} - -#[cfg(test)] -mod tests { - use super::*; - use alloy_primitives::{address, U256}; - use arc_execution_config::hardforks::ArcHardfork; - use arc_precompiles::NATIVE_COIN_CONTROL_ADDRESS; - use reth_ethereum::evm::revm::db::{CacheDB, EmptyDB}; - use revm::{ - context::Context, - context_interface::result::{EVMError, InvalidTransaction}, - database::EmptyDBTyped, - handler::FrameResult, - interpreter::{CallOutcome, Gas, InstructionResult, InterpreterResult}, - MainBuilder, MainContext, - }; - use std::convert::Infallible; - - #[test] - fn test_reward_beneficiary_basic_functionality() { - let beneficiary = address!("3000000000000000000000000000000000000003"); - let caller = address!("4000000000000000000000000000000000000004"); - let gas_price = 10u128; // 10 wei per gas - let gas_used = 21000u64; // standard transaction gas - - let db: CacheDB> = CacheDB::new(EmptyDB::default()); - let mut evm = Context::mainnet().with_db(db).build_mainnet(); - - // Set up the block environment with our beneficiary - evm.block.beneficiary = beneficiary; - evm.block.basefee = 7; // 7 wei base fee - - // Set up transaction environment - evm.tx.caller = caller; - evm.tx.gas_price = gas_price; - evm.tx.gas_priority_fee = Some(3); // priority fee = gas_price - basefee = 10 - 7 = 3 - - // Create a call outcome with gas usage - let interpreter_result = InterpreterResult::new( - InstructionResult::Return, - alloy_primitives::Bytes::new(), - Gas::new_spent(gas_used), - ); - let call_outcome = CallOutcome::new(interpreter_result, 0..0); - let mut exec_result = FrameResult::Call(call_outcome); - - // Get initial beneficiary balance - let initial_balance = evm - .journaled_state - .load_account(beneficiary) - .unwrap() - .info - .balance; - - // Test the reward_beneficiary function - let handler: ArcEvmHandler<_, EVMError> = - ArcEvmHandler::new(ArcHardforkFlags::default()); - let result = handler.reward_beneficiary(&mut evm, &mut exec_result); - - // Assert success - assert!(result.is_ok(), "reward_beneficiary should succeed"); - - // Calculate expected fee (total fee = effective_gas_price * gas_used) - // effective_gas_price = gas_price = 10 wei - let expected_fee = U256::from(gas_price * gas_used as u128); - - // Check that beneficiary balance increased by the expected fee amount - let final_balance = evm - .journaled_state - .load_account(beneficiary) - .unwrap() - .info - .balance; - let balance_increase = final_balance - initial_balance; - - assert_eq!( - balance_increase, expected_fee, - "Beneficiary should receive the full transaction fee (base fee + priority fee)" - ); - } - - #[test] - fn test_reward_beneficiary_eip1559_full_fee_redirect() { - let beneficiary = address!("5000000000000000000000000000000000000005"); - let caller = address!("6000000000000000000000000000000000000006"); - let base_fee = 50u64; - let max_fee_per_gas = 100u128; - let max_priority_fee_per_gas = 20u128; - let gas_used = 30000u64; - - let db: CacheDB> = CacheDB::new(EmptyDB::default()); - let mut evm = Context::mainnet().with_db(db).build_mainnet(); - - // Set up EIP-1559 transaction - evm.block.beneficiary = beneficiary; - evm.block.basefee = base_fee; - evm.tx.caller = caller; - evm.tx.gas_price = max_fee_per_gas; - evm.tx.gas_priority_fee = Some(max_priority_fee_per_gas); - - // Create call outcome - let interpreter_result = InterpreterResult::new( - InstructionResult::Return, - alloy_primitives::Bytes::new(), - Gas::new_spent(gas_used), - ); - let call_outcome = CallOutcome::new(interpreter_result, 0..0); - let mut exec_result = FrameResult::Call(call_outcome); - - let initial_balance = evm - .journaled_state - .load_account(beneficiary) - .unwrap() - .info - .balance; - - // Test reward_beneficiary - let handler: ArcEvmHandler<_, EVMError> = - ArcEvmHandler::new(ArcHardforkFlags::default()); - let result = handler.reward_beneficiary(&mut evm, &mut exec_result); - - assert!(result.is_ok(), "reward_beneficiary should succeed"); - - // The effective gas price is what revm actually calculates - // In this test setup, it appears to be using gas_price (100) directly - let expected_total_fee = U256::from(max_fee_per_gas * gas_used as u128); - - let final_balance = evm - .journaled_state - .load_account(beneficiary) - .unwrap() - .info - .balance; - let balance_increase = final_balance - initial_balance; - - assert_eq!( - balance_increase, expected_total_fee, - "Beneficiary should receive the full effective gas price (including base fee), not just priority fee" - ); - - // Verify this is MORE than just the priority fee - let priority_fee_only = U256::from(max_priority_fee_per_gas * gas_used as u128); - assert!( - balance_increase > priority_fee_only, - "Fee redirect should include base fee, not just priority fee" - ); - } - - #[test] - fn test_reward_beneficiary_zero_gas_used() { - let beneficiary = address!("7000000000000000000000000000000000000007"); - let caller = address!("8000000000000000000000000000000000000008"); - - let db: CacheDB> = CacheDB::new(EmptyDB::default()); - let mut evm = Context::mainnet().with_db(db).build_mainnet(); - - evm.block.beneficiary = beneficiary; - evm.block.basefee = 10; - evm.tx.caller = caller; - evm.tx.gas_price = 20; - - // Create call outcome with zero gas used - let interpreter_result = InterpreterResult::new( - InstructionResult::Return, - alloy_primitives::Bytes::new(), - Gas::new_spent(0), // Zero gas used - ); - let call_outcome = CallOutcome::new(interpreter_result, 0..0); - let mut exec_result = FrameResult::Call(call_outcome); - - let initial_balance = evm - .journaled_state - .load_account(beneficiary) - .unwrap() - .info - .balance; - - let handler: ArcEvmHandler<_, EVMError> = - ArcEvmHandler::new(ArcHardforkFlags::default()); - let result = handler.reward_beneficiary(&mut evm, &mut exec_result); - - assert!( - result.is_ok(), - "reward_beneficiary should succeed even with zero gas" - ); - - let final_balance = evm - .journaled_state - .load_account(beneficiary) - .unwrap() - .info - .balance; - let balance_increase = final_balance - initial_balance; - - assert_eq!( - balance_increase, - U256::ZERO, - "No fee should be rewarded when zero gas is used" - ); - } - - #[test] - fn test_reward_beneficiary_different_beneficiaries() { - let beneficiary1 = address!("9000000000000000000000000000000000000009"); - let beneficiary2 = address!("A000000000000000000000000000000000000000"); - let caller = address!("B000000000000000000000000000000000000000"); - let gas_used = 25000u64; - let gas_price = 15u128; - - let db: CacheDB> = CacheDB::new(EmptyDB::default()); - let mut evm = Context::mainnet().with_db(db).build_mainnet(); - - evm.tx.caller = caller; - evm.tx.gas_price = gas_price; - evm.block.basefee = 5; - - // Create call outcome - let interpreter_result = InterpreterResult::new( - InstructionResult::Return, - alloy_primitives::Bytes::new(), - Gas::new_spent(gas_used), - ); - let call_outcome = CallOutcome::new(interpreter_result, 0..0); - let mut exec_result = FrameResult::Call(call_outcome); - - let handler: ArcEvmHandler<_, EVMError> = - ArcEvmHandler::new(ArcHardforkFlags::default()); - - // Test with first beneficiary - evm.block.beneficiary = beneficiary1; - let initial_balance1 = evm - .journaled_state - .load_account(beneficiary1) - .unwrap() - .info - .balance; - let initial_balance2 = evm - .journaled_state - .load_account(beneficiary2) - .unwrap() - .info - .balance; - - let result = handler.reward_beneficiary(&mut evm, &mut exec_result); - assert!(result.is_ok()); - - let balance1_after = evm - .journaled_state - .load_account(beneficiary1) - .unwrap() - .info - .balance; - let balance2_after = evm - .journaled_state - .load_account(beneficiary2) - .unwrap() - .info - .balance; - - let expected_fee = U256::from(gas_price * gas_used as u128); - - // Beneficiary1 should receive the fee - assert_eq!( - balance1_after - initial_balance1, - expected_fee, - "First beneficiary should receive the fee" - ); - - // Beneficiary2 should not receive anything - assert_eq!( - balance2_after, initial_balance2, - "Second beneficiary should not receive any fee" - ); - - // Now test with second beneficiary (simulate next transaction) - evm.block.beneficiary = beneficiary2; - let result = handler.reward_beneficiary(&mut evm, &mut exec_result); - assert!(result.is_ok()); - - let balance2_final = evm - .journaled_state - .load_account(beneficiary2) - .unwrap() - .info - .balance; - - // Beneficiary2 should now receive the fee - assert_eq!( - balance2_final - balance2_after, - expected_fee, - "Second beneficiary should receive the fee when set as beneficiary" - ); - } - - #[test] - fn test_reward_beneficiary_large_values_no_overflow() { - let beneficiary = address!("1100000000000000000000000000000000000011"); - let caller = address!("2200000000000000000000000000000000000022"); - // Values that would overflow u128 when multiplied: (u128::MAX / 1000) * 2000 > u128::MAX - let gas_price = u128::MAX / 1000; - let gas_used = 2000u64; - - let db: CacheDB> = CacheDB::new(EmptyDB::default()); - let mut evm = Context::mainnet().with_db(db).build_mainnet(); - - evm.block.beneficiary = beneficiary; - evm.block.basefee = 0; - evm.tx.caller = caller; - evm.tx.gas_price = gas_price; - - let interpreter_result = InterpreterResult::new( - InstructionResult::Return, - alloy_primitives::Bytes::new(), - Gas::new_spent(gas_used), - ); - let call_outcome = CallOutcome::new(interpreter_result, 0..0); - let mut exec_result = FrameResult::Call(call_outcome); - - let initial_balance = evm - .journaled_state - .load_account(beneficiary) - .unwrap() - .info - .balance; - - let handler: ArcEvmHandler<_, EVMError> = - ArcEvmHandler::new(ArcHardforkFlags::default()); - let result = handler.reward_beneficiary(&mut evm, &mut exec_result); - - assert!( - result.is_ok(), - "reward_beneficiary should succeed with large values" - ); - - let expected_fee = U256::from(gas_price) * U256::from(gas_used); - - let final_balance = evm - .journaled_state - .load_account(beneficiary) - .unwrap() - .info - .balance; - let balance_increase = final_balance - initial_balance; - - assert_eq!( - balance_increase, expected_fee, - "Beneficiary should receive correct fee even with large values that would overflow u128" - ); - } - - #[derive(Debug)] - struct BlocklistTestCase { - name: &'static str, - caller: Address, - recipient: Option
, // None for Create transactions - value: U256, - caller_blocklisted: bool, - recipient_blocklisted: bool, - expected_result: BlocklistExpectedResult, - } - - #[derive(Debug, PartialEq)] - enum BlocklistExpectedResult { - Error, // Should return "address is blocklisted" error - Success, // Should return Ok(gas_value) - } - - #[test] - fn test_pre_execution_blocklist_scenarios() { - let test_cases = vec![ - BlocklistTestCase { - name: "caller_blocked", - caller: address!("C000000000000000000000000000000000000001"), - recipient: Some(address!("D000000000000000000000000000000000000002")), - value: U256::from(1000), - caller_blocklisted: true, - recipient_blocklisted: false, - expected_result: BlocklistExpectedResult::Error, - }, - BlocklistTestCase { - name: "recipient_blocked_with_value", - caller: address!("E000000000000000000000000000000000000001"), - recipient: Some(address!("F000000000000000000000000000000000000002")), - value: U256::from(1000), - caller_blocklisted: false, - recipient_blocklisted: true, - expected_result: BlocklistExpectedResult::Error, - }, - BlocklistTestCase { - name: "recipient_blocked_zero_value_allowed", - caller: address!("a000000000000000000000000000000000000001"), - recipient: Some(address!("b000000000000000000000000000000000000002")), - value: U256::ZERO, - caller_blocklisted: false, - recipient_blocklisted: true, - expected_result: BlocklistExpectedResult::Success, - }, - BlocklistTestCase { - name: "create_transaction_caller_blocked", - caller: address!("c000000000000000000000000000000000000001"), - recipient: None, // Create transaction - value: U256::from(1000), - caller_blocklisted: true, - recipient_blocklisted: false, - expected_result: BlocklistExpectedResult::Error, - }, - BlocklistTestCase { - name: "unblocklisted_addresses", - caller: address!("d000000000000000000000000000000000000001"), - recipient: Some(address!("e000000000000000000000000000000000000002")), - value: U256::from(1000), - caller_blocklisted: false, - recipient_blocklisted: false, - expected_result: BlocklistExpectedResult::Success, - }, - BlocklistTestCase { - name: "storage_default_behavior", - caller: address!("f000000000000000000000000000000000000001"), - recipient: Some(address!("1000000000000000000000000000000000000002")), - value: U256::from(1000), - caller_blocklisted: false, - recipient_blocklisted: false, - expected_result: BlocklistExpectedResult::Success, - }, - ]; - - for test_case in test_cases { - println!("Running test case: {}", test_case.name); - - // Set up EVM context - let db: CacheDB> = CacheDB::new(EmptyDB::default()); - let mut evm = Context::mainnet().with_db(db).build_mainnet(); - - // Configure transaction - evm.tx.caller = test_case.caller; - evm.tx.kind = match test_case.recipient { - Some(addr) => TxKind::Call(addr), - None => TxKind::Create, - }; - evm.tx.value = test_case.value; - evm.tx.gas_limit = 21000u64; - evm.tx.gas_price = 1; - - // Set up caller balance - evm.journaled_state.load_account(test_case.caller).unwrap(); - let initial_caller_balance = U256::from(100_000); - evm.journaled_state - .balance_incr(test_case.caller, initial_caller_balance) - .unwrap(); - - // Load native coin control account - evm.journaled_state - .load_account(NATIVE_COIN_CONTROL_ADDRESS) - .unwrap(); - - // Set caller blocklist status - if test_case.caller_blocklisted { - let storage_slot = compute_is_blocklisted_storage_slot(test_case.caller).into(); - evm.journaled_state - .sstore(NATIVE_COIN_CONTROL_ADDRESS, storage_slot, U256::from(1)) - .unwrap(); - } - - // Set recipient blocklist status - if let Some(recipient_addr) = test_case.recipient { - if test_case.recipient_blocklisted { - let storage_slot = compute_is_blocklisted_storage_slot(recipient_addr).into(); - evm.journaled_state - .sstore(NATIVE_COIN_CONTROL_ADDRESS, storage_slot, U256::from(1)) - .unwrap(); - } - } - - let handler: ArcEvmHandler<_, EVMError> = - ArcEvmHandler::new(ArcHardforkFlags::default()); - let result = handler.pre_execution(&mut evm); - - // Check caller balance after pre_execution to verify correct gas deduction behavior - let final_caller_balance = evm - .journaled_state - .load_account(test_case.caller) - .unwrap() - .info - .balance; - - match test_case.expected_result { - BlocklistExpectedResult::Error => { - // Verify that the error is an InvalidTransaction - let err = result.expect_err(&format!( - "Test case '{}' should return an error", - test_case.name - )); - match err { - EVMError::Transaction(InvalidTransaction::Str(msg)) => { - assert!( - msg.contains(ERR_BLOCKED_ADDRESS), - "Test case '{}' error should contain blocklisted message, got: {}", - test_case.name, - msg - ); - } - _ => { - panic!( - "Test case '{}' should return EVMError::Transaction(InvalidTransaction::Str), got: {:?}", - test_case.name, - err - ); - } - } - - // Verify that no gas fees were deducted when pre_execution fails - assert_eq!( - final_caller_balance, initial_caller_balance, - "Test case '{}': Caller balance should not change when pre_execution fails (no gas should be deducted)", - test_case.name - ); - } - BlocklistExpectedResult::Success => { - assert!( - result.is_ok(), - "Test case '{}' should return success", - test_case.name - ); - // Note: In test environment, mainnet.pre_execution returns 0 - assert_eq!( - result.unwrap(), - 0, - "Test case '{}' should return expected gas value", - test_case.name - ); - - // For successful pre_execution, mainnet.pre_execution is called which deducts gas - let expected_gas_deduction = evm.tx.gas_limit as u128 * evm.tx.gas_price; - let expected_final_balance = - initial_caller_balance.saturating_sub(U256::from(expected_gas_deduction)); - assert_eq!( - final_caller_balance, expected_final_balance, - "Test case '{}': Caller balance should be reduced by gas cost ({}) when pre_execution succeeds and calls mainnet handler", - test_case.name, expected_gas_deduction - ); - } - } - } - } - - #[test] - fn test_validate_initial_tx_gas_no_blocklist_surcharge() { - // Blocklist SLOADs are unmetered — native value transfer costs exactly 21,000 gas - let caller = address!("1000000000000000000000000000000000000001"); - let recipient = address!("2000000000000000000000000000000000000002"); - - let db: CacheDB> = CacheDB::new(EmptyDB::default()); - let mut evm = Context::mainnet().with_db(db).build_mainnet(); - - evm.tx.caller = caller; - evm.tx.kind = TxKind::Call(recipient); - evm.tx.value = U256::from(1000); - evm.tx.gas_limit = 100_000u64; - evm.tx.gas_price = 1; - - // Zero6 active — still no extra gas for blocklist checks - let handler: ArcEvmHandler<_, EVMError> = - ArcEvmHandler::new(ArcHardforkFlags::with(&[ArcHardfork::Zero6])); - let result = handler.validate_initial_tx_gas(&mut evm); - - assert!(result.is_ok(), "validate_initial_tx_gas should succeed"); - assert_eq!( - result.unwrap().initial_gas, - 21000, - "Native value transfer should cost exactly 21,000 gas (no blocklist surcharge)" - ); - } -} diff --git a/crates/evm/src/lib.rs b/crates/evm/src/lib.rs deleted file mode 100644 index e924fb9..0000000 --- a/crates/evm/src/lib.rs +++ /dev/null @@ -1,39 +0,0 @@ -// Copyright 2026 Circle Internet Group, Inc. All rights reserved. -// -// SPDX-License-Identifier: Apache-2.0 -// -// Licensed under the Apache License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. -// You may obtain a copy of the License at -// -// http://www.apache.org/licenses/LICENSE-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// See the License for the specific language governing permissions and -// limitations under the License. - -#![cfg_attr( - test, - allow(clippy::arithmetic_side_effects, clippy::cast_possible_truncation) -)] - -//! Arc EVM configuration, executor, and handler. -//! -//! This crate contains the EVM customization for Arc Network: block assembler, -//! block executor, handler, opcode overrides, and frame result utilities. - -pub mod assembler; -pub mod evm; -pub mod executor; -pub mod frame_result; -pub mod handler; -mod log; -pub mod opcode; -pub mod subcall; -#[cfg(test)] -mod subcall_test; - -// Re-export commonly used types -pub use evm::{ArcEvm, ArcEvmConfig, ArcEvmFactory}; diff --git a/crates/evm/src/log.rs b/crates/evm/src/log.rs deleted file mode 100644 index 21a725b..0000000 --- a/crates/evm/src/log.rs +++ /dev/null @@ -1,95 +0,0 @@ -// Copyright 2025 Circle Internet Group, Inc. All rights reserved. -// -// SPDX-License-Identifier: Apache-2.0 -// -// Licensed under the Apache License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. -// You may obtain a copy of the License at -// -// http://www.apache.org/licenses/LICENSE-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// See the License for the specific language governing permissions and -// limitations under the License. - -use alloy_primitives::{Address, U256}; -use alloy_sol_types::{sol, SolEvent}; -use arc_precompiles::NATIVE_COIN_AUTHORITY_ADDRESS; -use reth_ethereum::primitives::Log; -use revm::handler::SYSTEM_ADDRESS; - -sol! { - #[derive(Debug, PartialEq, Eq)] - event NativeCoinTransferred(address indexed from, address indexed to, uint256 amount); -} - -// Creates a log for native coin transfers -pub(crate) fn create_native_transfer_log(from: Address, to: Address, amount: U256) -> Log { - let log_data = NativeCoinTransferred { from, to, amount }.encode_log_data(); - - Log { - address: NATIVE_COIN_AUTHORITY_ADDRESS, - data: log_data, - } -} - -sol! { - #[derive(Debug, PartialEq, Eq)] - event Transfer(address indexed from, address indexed to, uint256 amount); -} - -/// Creates an EIP-7708 ERC-20 Transfer log for native coin transfers. -/// -/// Constructs the log manually to match the exact format REVM will use when upgraded -/// (via `eip7708_transfer_log`), rather than using `SolEvent::encode_log_data()`. -pub(crate) fn create_eip7708_transfer_log(from: Address, to: Address, amount: U256) -> Log { - let log_data = Transfer { from, to, amount }.encode_log_data(); - - Log { - address: SYSTEM_ADDRESS, - data: log_data, - } -} - -#[cfg(test)] -mod tests { - use super::*; - use alloy_primitives::{address, b256, B256}; - use rstest::rstest; - - const ALICE: Address = address!("0x000000000000000000000000000000000000A11c"); - const BOB: Address = address!("0x0000000000000000000000000000000000000B0b"); - - #[rstest] - #[case::typical_transfer( - ALICE, - BOB, - U256::from(1_000_000_000_000_000_000u128), // 1 USDC (18 decimals) - )] - #[case::zero_amount(ALICE, BOB, U256::ZERO)] - #[case::max_amount(ALICE, BOB, U256::MAX)] - #[case::same_address(ALICE, ALICE, U256::from(42))] - #[case::zero_address_from(Address::ZERO, BOB, U256::from(1))] - #[case::zero_address_to(ALICE, Address::ZERO, U256::from(1))] - fn eip7708_log_structure(#[case] from: Address, #[case] to: Address, #[case] amount: U256) { - let log = create_eip7708_transfer_log(from, to, amount); - - // Emitted from EIP-7708 system address - assert_eq!(log.address, SYSTEM_ADDRESS); - - // 3 topics: event signature, indexed from, indexed to - let topics = log.data.topics(); - assert_eq!(topics.len(), 3); - assert_eq!( - topics[0], - b256!("ddf252ad1be2c89b69c2b068fc378daa952ba7f163c4a11628f55a4df523b3ef") - ); - assert_eq!(topics[1], B256::left_padding_from(from.as_slice())); - assert_eq!(topics[2], B256::left_padding_from(to.as_slice())); - - // Data encodes amount as big-endian uint256 - assert_eq!(log.data.data.as_ref(), &amount.to_be_bytes::<32>()); - } -} diff --git a/crates/evm/src/opcode.rs b/crates/evm/src/opcode.rs deleted file mode 100644 index f263e09..0000000 --- a/crates/evm/src/opcode.rs +++ /dev/null @@ -1,1483 +0,0 @@ -// Copyright 2025 Circle Internet Group, Inc. All rights reserved. -// -// SPDX-License-Identifier: Apache-2.0 -// -// Licensed under the Apache License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. -// You may obtain a copy of the License at -// -// http://www.apache.org/licenses/LICENSE-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// See the License for the specific language governing permissions and -// limitations under the License. - -use crate::log::{create_eip7708_transfer_log, create_native_transfer_log}; -use alloy_evm::eth::EthEvmContext; -use arc_execution_config::native_coin_control::{ - compute_is_blocklisted_storage_slot, is_blocklisted_status, -}; -use arc_precompiles::helpers::{ - self, ERR_BLOCKED_ADDRESS, ERR_SELFDESTRUCTED_BALANCE_INCREASED, ERR_ZERO_ADDRESS, -}; -use arc_precompiles::native_coin_control; -use revm_context_interface::{ContextTr, JournalTr}; - -/// Controls which transfer log is emitted by the SELFDESTRUCT instruction. -enum TransferLogMode { - /// Emit the custom NativeCoinTransferred log (pre-Zero5). - NativeCoinTransferred, - /// Emit an EIP-7708 ERC-20 Transfer log (Zero5+). - Eip7708Transfer, -} - -#[derive(Clone, Copy)] -enum BlocklistReadPolicy { - /// SLOAD failures are treated as "not blocklisted" for backward compatibility. - FailOpen, - /// SLOAD failures propagate to the caller; an unexpected cold NCC account is loaded and retried. - FailClosed, -} - -use reth_ethereum::evm::primitives::Database; -use reth_evm::revm::{ - context_interface::{host::LoadError, Host}, - interpreter::{ - gas, - instructions::utility::IntoAddress, - interpreter_action::InterpreterAction, - interpreter_types::{InputsTr, RuntimeFlag, StackTr}, - popn, require_non_staticcall, InstructionContext, InstructionResult, InterpreterTypes, - }, - primitives::hardfork::SpecId, -}; -use revm::interpreter::interpreter_types::LoopControl; -use revm_interpreter::StateLoad; - -// Overridden SELFDESTRUCT that applies Arc Network-specific functionality - -// Forked from: https://github.com/bluealloy/revm/blob/v97/crates/interpreter/src/instructions/host.rs#L387, -// with the following modifications: -// - Add event emissions for non-zero native value transfers -// - Add blocklist checks on host address and target -// - Disallow selfdestruct if target == addr, and amount is non-zero -fn arc_network_selfdestruct_impl( - mut context: InstructionContext<'_, EthEvmContext, WIRE>, - check_target_destructed: bool, - log_mode: Option, - blocklist_read_policy: BlocklistReadPolicy, -) { - require_non_staticcall!(context.interpreter); - popn!([target], context.interpreter); - let target = target.into_address(); - let spec = context.interpreter.runtime_flag.spec_id(); - let cold_load_gas = context.host.gas_params().selfdestruct_cold_cost(); - let skip_cold_load = context.interpreter.gas.remaining() < cold_load_gas; - - // MODIFIED CODE - let addr = context.interpreter.input.target_address(); - let addr_balance = context.host.balance(addr); - let is_cold = match addr_balance.clone() { - Some(balance) if !balance.is_zero() => { - // Zero5: reject SELFDESTRUCT to zero address (prevents burn-like semantics) - if matches!(log_mode, Some(TransferLogMode::Eip7708Transfer)) - && target == alloy_primitives::Address::ZERO - { - context - .interpreter - .bytecode - .set_action(InterpreterAction::new_return( - InstructionResult::Revert, - helpers::revert_message_to_bytes(ERR_ZERO_ADDRESS), - context.interpreter.gas, - )); - return; - } - - // Checks the source and target account is valid or not. - let Ok(is_target_cold) = check_selfdestruct_accounts( - &mut context, - addr, - target, - skip_cold_load, - check_target_destructed, - blocklist_read_policy, - ) else { - // The next action is set in the check_selfdestruct_accounts. - return; - }; - - is_target_cold - } - None => { - context - .interpreter - .halt(InstructionResult::FatalExternalError); - return; - } - _ => None, - }; - - let res = match context - .host - .selfdestruct( - context.interpreter.input.target_address(), - target, - skip_cold_load, - ) - .map(|res| StateLoad { - data: res.data, - is_cold: is_cold.unwrap_or(res.is_cold), - }) { - Ok(res) => res, - Err(LoadError::ColdLoadSkipped) => return context.interpreter.halt_oog(), - Err(LoadError::DBError) => return context.interpreter.halt_fatal(), - }; - - // Emit the transfer log after host.selfdestruct() succeeds, matching REVM's ordering - // where the log is emitted after the balance is zeroed and transferred in the journal. - // The balance was captured before selfdestruct zeroed it. - if let Some(balance) = addr_balance { - if !balance.is_zero() { - match log_mode { - Some(TransferLogMode::NativeCoinTransferred) => { - context - .host - .log(create_native_transfer_log(addr, target, balance.data)); - } - Some(TransferLogMode::Eip7708Transfer) => { - context - .host - .log(create_eip7708_transfer_log(addr, target, balance.data)); - } - None => {} - } - } - } - // END MODIFIED CODE - - // EIP-161: State trie clearing (invariant-preserving alternative) - let should_charge_topup = if spec.is_enabled_in(SpecId::SPURIOUS_DRAGON) { - res.had_value && !res.target_exists - } else { - !res.target_exists - }; - - gas!( - context.interpreter, - context - .host - .gas_params() - .selfdestruct_cost(should_charge_topup, res.is_cold) - ); - - if !res.previously_destroyed { - context - .interpreter - .gas - .record_refund(context.host.gas_params().selfdestruct_refund()); - } - - context.interpreter.halt(InstructionResult::SelfDestruct); -} - -/// Pre-Zero5 variant: does not check target destructed status and emits NativeCoinTransferred logs. -pub(crate) fn arc_network_selfdestruct_zero4( - context: InstructionContext<'_, EthEvmContext, WIRE>, -) { - arc_network_selfdestruct_impl( - context, - false, - Some(TransferLogMode::NativeCoinTransferred), - BlocklistReadPolicy::FailOpen, - ); -} - -/// Zero5 and Zero6: checks target destructed status and emits EIP-7708 Transfer logs. -pub(crate) fn arc_network_selfdestruct_zero5( - context: InstructionContext<'_, EthEvmContext, WIRE>, -) { - arc_network_selfdestruct_impl( - context, - true, - Some(TransferLogMode::Eip7708Transfer), - BlocklistReadPolicy::FailOpen, - ); -} - -/// Zero7+: fail closed on blocklist read failures. -pub(crate) fn arc_network_selfdestruct_zero7( - context: InstructionContext<'_, EthEvmContext, WIRE>, -) { - arc_network_selfdestruct_impl( - context, - true, - Some(TransferLogMode::Eip7708Transfer), - BlocklistReadPolicy::FailClosed, - ); -} - -/// Checks whether a given account is currently on the blocklist -fn is_blocklisted( - context: &mut InstructionContext<'_, H, WIRE>, - account: alloy_primitives::Address, - blocklist_read_policy: BlocklistReadPolicy, -) -> Result { - let slot = compute_is_blocklisted_storage_slot(account).into(); - match blocklist_read_policy { - BlocklistReadPolicy::FailOpen => Ok(context - .host - .sload(native_coin_control::NATIVE_COIN_CONTROL_ADDRESS, slot) - .is_some_and(|state_load| is_blocklisted_status(state_load.data))), - BlocklistReadPolicy::FailClosed => { - let state_load = match context.host.sload_skip_cold_load( - native_coin_control::NATIVE_COIN_CONTROL_ADDRESS, - slot, - false, - ) { - Ok(state_load) => state_load, - Err(LoadError::ColdLoadSkipped) => { - let native_coin_control_was_cold = { - let native_coin_control_load = - context.host.load_account_info_skip_cold_load( - native_coin_control::NATIVE_COIN_CONTROL_ADDRESS, - false, - false, - )?; - native_coin_control_load.is_cold - }; - debug_assert!( - !native_coin_control_was_cold, - "NativeCoinControl should be preloaded before Zero7 SELFDESTRUCT blocklist reads" - ); - // The account is now loaded and warm; the retry should succeed. - // If it doesn't, the `?` propagates the load error to the caller. - context.host.sload_skip_cold_load( - native_coin_control::NATIVE_COIN_CONTROL_ADDRESS, - slot, - false, - )? - } - Err(LoadError::DBError) => return Err(LoadError::DBError), - }; - - Ok(is_blocklisted_status(state_load.data)) - } - } -} - -/// Checks the source and target addresses are valid or not, and return target is cold or warm if the target account is loaded. -/// -/// Checking rules -/// - source and target is not the same -/// - source and target are not blocklisted. -/// - target is not self-destructed. -/// -/// - returns Ok(None) pass the check, and we did not load the target account. -/// - returns Ok(bool) pass the check, return `true` if the account is cold, `false` if it is warm. -/// - returns Err(()) if the source or target account is invalid. The error should update in the context.interpreter. -/// - returns Err(()) if a fail-closed blocklist load fails; the interpreter is halted fatally. -fn check_selfdestruct_accounts( - context: &mut InstructionContext<'_, EthEvmContext, WIRE>, - source: alloy_primitives::Address, - target: alloy_primitives::Address, - skip_cold_load: bool, - check_target_destructed: bool, - blocklist_read_policy: BlocklistReadPolicy, -) -> Result, ()> { - // Disallow selfdestruct if target == source - if source == target { - context.interpreter.halt(InstructionResult::Revert); - return Err(()); - } - - // Check if either account is blocklisted - let target_blocklisted = match is_blocklisted(context, target, blocklist_read_policy) { - Ok(is_blocklisted) => is_blocklisted, - Err(err) => { - tracing::error!( - address = %target, - err = ?err, - "blocklist read failed for selfdestruct target" - ); - context.interpreter.halt_fatal(); - return Err(()); - } - }; - if target_blocklisted { - context - .interpreter - .bytecode - .set_action(InterpreterAction::new_return( - InstructionResult::Revert, - helpers::revert_message_to_bytes(ERR_BLOCKED_ADDRESS), - context.interpreter.gas, - )); - return Err(()); - } - - let source_blocklisted = match is_blocklisted(context, source, blocklist_read_policy) { - Ok(is_blocklisted) => is_blocklisted, - Err(err) => { - tracing::error!( - address = %source, - err = ?err, - "blocklist read failed for selfdestruct source" - ); - context.interpreter.halt_fatal(); - return Err(()); - } - }; - if source_blocklisted { - context - .interpreter - .bytecode - .set_action(InterpreterAction::new_return( - InstructionResult::Revert, - helpers::revert_message_to_bytes(ERR_BLOCKED_ADDRESS), - context.interpreter.gas, - )); - return Err(()); - } - - // Skip the selfdestruct, early return. - if !check_target_destructed { - return Ok(None); - } - - // We cannot call JournalInner here to skip a cold load. - // Additionally, `load_account_mut_skip_cold_load` will panic if a `LoadError::ColdLoadSkipped` occurs. - // Therefore, we use `warm_addresses.check_is_cold` to check the cold status directly. - if context - .host - .journal_mut() - .warm_addresses - .check_is_cold::(&target, skip_cold_load) - .is_err() - { - context.interpreter.halt_oog(); - return Err(()); - } - - // Load target account and check if it is desctructed. - match context.host.journal_mut().load_account(target) { - Ok(acc) => { - if acc.is_selfdestructed() { - context - .interpreter - .bytecode - .set_action(InterpreterAction::new_return( - InstructionResult::Revert, - helpers::revert_message_to_bytes(ERR_SELFDESTRUCTED_BALANCE_INCREASED), - context.interpreter.gas, - )); - return Err(()); - } - Ok(Some(acc.is_cold)) - } - Err(e) => { - // Follow the original error handling on Host::selfdestruct for Context, - tracing::error!("load account failed: {:?}", e); - context.interpreter.halt_fatal(); - Err(()) - } - } -} - -#[cfg(test)] -mod tests { - use super::*; - use alloy_primitives::{address, Address, U256}; - use alloy_sol_types::SolEvent; - use reth_ethereum::evm::revm::db::{EmptyDB, InMemoryDB}; - use reth_ethereum::evm::revm::{ - context::{Context, ContextTr, JournalTr}, - interpreter::{interpreter::EthInterpreter, Interpreter}, - }; - use revm::state::AccountStatus; - use revm::DatabaseCommit; - use revm_context_interface::journaled_state::account::JournaledAccountTr; - use revm_interpreter::{Gas, InterpreterResult, SelfDestructResult}; - - const ACCOUNT: Address = address!("0x1000000000000000000000000000000000000001"); - const TARGET: Address = address!("0x2000000000000000000000000000000000000002"); - const STATIC_GAS_COST: u64 = 5000; - - /// Helper struct to create different AccountStatus combinations for testing. - struct State {} - - impl State { - /// AccountStatus is empty. - fn loaded() -> AccountStatus { - AccountStatus::empty() - } - - /// AccountStatus is loaded as not existing. - fn loaded_new() -> AccountStatus { - AccountStatus::LoadedAsNotExisting - } - - /// Returns AccountStatus for a newly touched account that did not previously exist. - /// This is represented by `AccountStatus::Touched | AccountStatus::LoadedAsNotExisting`, - /// indicating the account was created during this transaction and its changes must be persisted. - fn touch_new() -> AccountStatus { - AccountStatus::Touched | AccountStatus::LoadedAsNotExisting - } - - /// Returns AccountStatus for a newly created account (CREATE, CREATE2). - /// This is represented by `AccountStatus::Touched | AccountStatus::Created | AccountStatus::CreatedLocal`, - /// indicating the account was created during this transaction and its changes must be persisted. - fn touch_created() -> AccountStatus { - AccountStatus::Touched | AccountStatus::Created | AccountStatus::CreatedLocal - } - - /// Returns AccountStatus for a newly destructed account. - /// This is represented by `AccountStatus::Touched | AccountStatus::Created | AccountStatus::CreatedLocal | AccountStatus::SelfDestructed | AccountStatus::SelfDestructedLocal`, - /// indicating the account was created then destroyed during this transaction and its changes must be persisted. - fn touch_destructed() -> AccountStatus { - AccountStatus::Touched - | AccountStatus::Created - | AccountStatus::CreatedLocal - | AccountStatus::SelfDestructed - | AccountStatus::SelfDestructedLocal - } - - /// Similar to `touch_destructed`, but the account was not existing before. - fn touch_new_destructed() -> AccountStatus { - AccountStatus::LoadedAsNotExisting | Self::touch_destructed() - } - - /// Returns AccountStatus for a account which was destructed before current transaction. - /// This is represented by `AccountStatus::Touched | AccountStatus::Created | AccountStatus::SelfDestructed`, - fn destructed_before() -> AccountStatus { - AccountStatus::Touched | AccountStatus::Created | AccountStatus::SelfDestructed - } - - /// Similar to `destructed_before`, but the account was not existing before. - fn destructed_new_before() -> AccountStatus { - AccountStatus::LoadedAsNotExisting | Self::destructed_before() - } - } - - /// Wrapper for the host context used in selfdestruct/account lifecycle state tests. - struct HostTestEnv { - spec: SpecId, - host: EthEvmContext, - } - - impl HostTestEnv { - fn new(db: DB) -> Self { - Self::new_with_spec(db, SpecId::PRAGUE) - } - - fn new_with_spec(db: DB, spec: SpecId) -> Self { - let host = Context::new(db, spec); - Self { host, spec } - } - - /// Sets the balance of an account in the host context. - fn set_account_balance(&mut self, account: Address, balance: U256) { - self.host - .journal_mut() - .load_account_mut_optional_code(account, false) - .expect("load account") - .set_balance(balance); - } - - /// Set account to blocklist - fn set_blocklist(&mut self, account: Address) { - self.host - .journal_mut() - .load_account(native_coin_control::NATIVE_COIN_CONTROL_ADDRESS) - .expect("load account to state"); - - // Blocklist the target address - let storage_slot = native_coin_control::compute_is_blocklisted_storage_slot(account); - self.host - .journal_mut() - .sstore( - native_coin_control::NATIVE_COIN_CONTROL_ADDRESS, - storage_slot.into(), - U256::from(1), - ) - .expect("sstore set blocklist"); - } - - /// Simulates the creation of a new contract account. - /// This does not assign contract code, but initializes the account status only. - fn simulate_create_account(&mut self, caller: Address, account: Address, balance: U256) { - let spec_id = self.host.cfg.spec; - self.host - .journal_mut() - .load_account(caller) - .expect("load account"); - self.host - .journal_mut() - .load_account(account) - .expect("load account"); - self.host - .journal_mut() - .create_account_checkpoint(caller, account, balance, spec_id) - .expect("create account checkpoint"); - } - - /// Wrapper for journal selfdestruct, not include the Arc modifications. - /// This is only for testing the account status. - fn journal_selfdestruct( - &mut self, - account: Address, - target: Address, - ) -> StateLoad { - self.host - .journal_mut() - .load_account(account) - .expect("load account"); - self.host - .journal_mut() - .selfdestruct(account, target, false) - .expect("selfdestruct") - } - - /// Simulates a call to `arc_network_selfdestruct_impl` for testing. - /// Executes SELFDESTRUCT from `account` to `target` using the arc logic, - /// Returns the resulting `InterpreterResult`. - fn simulate_arc_selfdestruct( - &mut self, - account: Address, - target: Address, - check_target_destructed: bool, - ) -> InterpreterResult { - self.simulate_arc_selfdestruct_full( - account, - target, - check_target_destructed, - Some(TransferLogMode::NativeCoinTransferred), - None, - ) - } - - /// Like `simulate_arc_selfdestruct` but with a custom gas limit. - /// Used to trigger ColdLoadSkipped when gas is below cold load cost after static cost. - fn simulate_arc_selfdestruct_with_gas( - &mut self, - account: Address, - target: Address, - check_target_destructed: bool, - initial_gas_limit: Option, - ) -> InterpreterResult { - self.simulate_arc_selfdestruct_full( - account, - target, - check_target_destructed, - Some(TransferLogMode::NativeCoinTransferred), - initial_gas_limit, - ) - } - - /// Full selfdestruct simulation with all parameters. - fn simulate_arc_selfdestruct_full( - &mut self, - account: Address, - target: Address, - check_target_destructed: bool, - log_mode: Option, - initial_gas_limit: Option, - ) -> InterpreterResult { - self.simulate_arc_selfdestruct_full_with_preload( - account, - target, - check_target_destructed, - log_mode, - initial_gas_limit, - true, - BlocklistReadPolicy::FailClosed, - ) - } - - fn simulate_arc_selfdestruct_without_native_coin_control_preload( - &mut self, - account: Address, - target: Address, - check_target_destructed: bool, - blocklist_read_policy: BlocklistReadPolicy, - ) -> InterpreterResult { - self.simulate_arc_selfdestruct_full_with_preload( - account, - target, - check_target_destructed, - Some(TransferLogMode::NativeCoinTransferred), - None, - false, - blocklist_read_policy, - ) - } - - #[allow(clippy::too_many_arguments)] - fn simulate_arc_selfdestruct_full_with_preload( - &mut self, - account: Address, - target: Address, - check_target_destructed: bool, - log_mode: Option, - initial_gas_limit: Option, - preload_native_coin_control: bool, - blocklist_read_policy: BlocklistReadPolicy, - ) -> InterpreterResult { - if preload_native_coin_control { - self.host - .journal_mut() - .load_account(native_coin_control::NATIVE_COIN_CONTROL_ADDRESS) - .expect("load account to state"); - } - - // Builds an interpreter instance with the SELFDESTRUCT target already on the stack. - let mut interpreter = Interpreter::::default(); - if let Some(initial_gas_limit) = initial_gas_limit { - interpreter.gas = Gas::new(initial_gas_limit); - } - interpreter.runtime_flag.spec_id = self.spec; - - interpreter.input.target_address = account; - interpreter.input.caller_address = account; - let success = interpreter - .stack - .push(U256::from_be_slice(target.into_word().as_ref())); - if !success { - panic!("push target to stack failed"); - } - - // Deduct the static cost of selfdestruct first to simulate the full op cost. - assert!(interpreter.gas.record_cost(STATIC_GAS_COST)); - - // Prepare context and execute. - let context = InstructionContext { - interpreter: &mut interpreter, - host: &mut self.host, - }; - arc_network_selfdestruct_impl( - context, - check_target_destructed, - log_mode, - blocklist_read_policy, - ); - - // The selfdestruct should halt and return a Return action. - let next_action = interpreter.take_next_action(); - match next_action { - InterpreterAction::Return(result, ..) => result, - _ => panic!("Expected Return action"), - } - } - } - - impl HostTestEnv { - /// Drop the data of transction context. - fn commit_tx(&mut self) { - self.host.journal_mut().commit_tx(); - } - - /// Set the changes to the database and drop execution context. - fn commit_block(&mut self) { - let state = self.host.journal_mut().finalize(); - self.host.db_mut().commit(state); - } - } - - /// Asserts that the account's status and balance match the expected values. - /// Defined as a macro so assertions keep the test's original line number for accurate error reporting. - /// - /// Note: Invokes `load_account`, which causes the account to become warm. - /// Avoid using when a cold account is required. - macro_rules! assert_account_matches { - ($env:ident, $account:expr, $expected_status:expr, $expected_balance:expr) => { - assert_account_matches!($env, $account, $expected_status, $expected_balance, ""); - }; - ($env:ident, $account:expr, $expected_status:expr, $expected_balance:expr, $($msg:tt)+) => { - let acc = $env.host - .journal_mut() - .load_account($account) - .expect("load account"); - assert_eq!( - (acc.status, acc.data.info.balance), - ($expected_status, $expected_balance), - $($msg)+ - ); - }; - } - - #[test] - fn demo_account_selfdestruct_status_change() { - let amount = U256::from(42); - let mut env = HostTestEnv::new(InMemoryDB::default()); - - assert_account_matches!(env, ACCOUNT, State::loaded_new(), U256::ZERO); - - // 1. Init ACCOUNT with balance, and commit to cache DB. - env.set_account_balance(ACCOUNT, amount); - assert_account_matches!(env, ACCOUNT, State::touch_new(), amount); - env.commit_block(); - - // 2. Verify the account status should be clear after finalize - assert_account_matches!(env, ACCOUNT, State::loaded(), amount); - env.commit_block(); - - // 3. Selfdestruct ACCOUNT, transferring its entire balance to TARGET. - let result = env.journal_selfdestruct(ACCOUNT, TARGET); - assert_eq!( - result, - StateLoad { - data: SelfDestructResult { - had_value: true, - target_exists: false, - previously_destroyed: false - }, - is_cold: true, - } - ); - // selfdestruct does not touch the account, we need to touch it manually. - env.host.journal_mut().touch_account(ACCOUNT); - assert_account_matches!(env, ACCOUNT, AccountStatus::Touched, U256::ZERO); - assert_account_matches!(env, TARGET, State::touch_new(), amount); - env.commit_block(); - - // 4. Create account first then selfdestruct ACCOUNT to create the destructed locally account. - env.simulate_create_account(TARGET, ACCOUNT, amount); - assert_account_matches!(env, ACCOUNT, State::touch_created(), amount); - assert_account_matches!(env, TARGET, State::loaded(), U256::ZERO); - assert_eq!( - env.journal_selfdestruct(ACCOUNT, TARGET), - StateLoad { - data: SelfDestructResult { - had_value: true, - target_exists: false, - previously_destroyed: false - }, - is_cold: false, - } - ); - assert_account_matches!(env, ACCOUNT, State::touch_destructed(), U256::ZERO); - assert_account_matches!(env, TARGET, AccountStatus::Touched, amount); - - // 5. selfdestruct again in the same transaction - assert_eq!( - env.journal_selfdestruct(ACCOUNT, TARGET), - StateLoad { - data: SelfDestructResult { - had_value: false, - target_exists: true, - previously_destroyed: true, - }, - is_cold: false, - } - ); - - // 6. Simulate another transaction started in the same block. - // The ACCOUNT should be destroyed and local flags are removed. - env.commit_tx(); - assert_account_matches!(env, ACCOUNT, State::destructed_before(), U256::ZERO); - - // 7. Create again, and destruct again - env.simulate_create_account(TARGET, ACCOUNT, amount); - env.journal_selfdestruct(ACCOUNT, TARGET); - assert_account_matches!(env, ACCOUNT, State::touch_destructed(), U256::ZERO); - assert_account_matches!(env, TARGET, AccountStatus::Touched, amount); - } - - #[test] - fn selfdestruct_emits_event_when_balance_non_zero() { - let amount = U256::from(42); - - for check_target_destruct_locally in [true, false] { - let mut env = HostTestEnv::new(EmptyDB::new()); - env.set_account_balance(ACCOUNT, amount); - - let res = env.simulate_arc_selfdestruct(ACCOUNT, TARGET, check_target_destruct_locally); - assert_eq!(res.result, InstructionResult::SelfDestruct); - assert_eq!(res.gas.spent(), 32600u64); // 5000 static + 25000 new account + 2600 cold account - assert_eq!(res.gas.refunded(), 0i64); - - // Verify log. - let logs = env.host.journal_mut().take_logs(); - assert_eq!(logs.len(), 1, "exactly one transfer event expected"); - let decoded = - crate::log::NativeCoinTransferred::decode_log(&logs[0]).expect("decode log"); - assert_eq!(decoded.data.from, ACCOUNT); - assert_eq!(decoded.data.to, TARGET); - assert_eq!(decoded.data.amount, amount); - } - } - - #[test] - fn selfdestruct_no_event_emitted_when_balance_zero() { - for check_target_destruct_locally in [true, false] { - let mut env = HostTestEnv::new(EmptyDB::new()); - - let res = env.simulate_arc_selfdestruct(ACCOUNT, TARGET, check_target_destruct_locally); - assert_eq!(res.result, InstructionResult::SelfDestruct); - assert_eq!(res.gas.spent(), 7600u64); // 5000 static cost + 2600 cold target, no transfer to create new account - assert_eq!(res.gas.refunded(), 0i64); - - assert!( - env.host.journal_mut().take_logs().is_empty(), - "no transfer event expected" - ); - } - } - - /// Under Zero5, selfdestruct with non-zero balance emits an EIP-7708 Transfer log. - #[test] - fn selfdestruct_zero5_emits_eip7708_transfer_log() { - let amount = U256::from(42); - let mut env = HostTestEnv::new(EmptyDB::new()); - env.set_account_balance(ACCOUNT, amount); - - let res = env.simulate_arc_selfdestruct_full( - ACCOUNT, - TARGET, - true, - Some(TransferLogMode::Eip7708Transfer), - None, - ); - assert_eq!(res.result, InstructionResult::SelfDestruct); - - // Verify EIP-7708 Transfer log was emitted - let logs = env.host.journal_mut().take_logs(); - assert_eq!( - logs.len(), - 1, - "Zero5: exactly one EIP-7708 Transfer log expected from selfdestruct" - ); - assert_eq!( - logs[0].address, - revm::handler::SYSTEM_ADDRESS, - "Log should be from EIP-7708 system address" - ); - - // Verify balance was still transferred - assert_account_matches!(env, ACCOUNT, State::touch_new(), U256::ZERO); - assert_account_matches!(env, TARGET, State::touch_new(), amount); - } - - /// Zero5: SELFDESTRUCT to Address::ZERO with non-zero balance should revert. - #[test] - fn selfdestruct_zero5_to_zero_address_reverts() { - let amount = U256::from(42); - let mut env = HostTestEnv::new(EmptyDB::new()); - env.set_account_balance(ACCOUNT, amount); - - let res = env.simulate_arc_selfdestruct_full( - ACCOUNT, - Address::ZERO, - true, - Some(TransferLogMode::Eip7708Transfer), - None, - ); - assert_eq!( - res.result, - InstructionResult::Revert, - "Zero5: SELFDESTRUCT to zero address should revert" - ); - - // Balance should NOT have been transferred (account was touched during setup) - assert_account_matches!(env, ACCOUNT, State::touch_new(), amount); - } - - /// Pre-Zero5: SELFDESTRUCT to Address::ZERO is not blocked (existing behavior). - #[test] - fn selfdestruct_pre_zero5_to_zero_address_allowed() { - let amount = U256::from(42); - let mut env = HostTestEnv::new(EmptyDB::new()); - env.set_account_balance(ACCOUNT, amount); - - let res = env.simulate_arc_selfdestruct_full( - ACCOUNT, - Address::ZERO, - false, - Some(TransferLogMode::NativeCoinTransferred), - None, - ); - // Pre-Zero5 variants use NativeCoinTransferred and don't block zero address - assert_eq!( - res.result, - InstructionResult::SelfDestruct, - "Pre-Zero5: SELFDESTRUCT to zero address should succeed" - ); - } - - #[test] - fn selfdestruct_cold_load_skipped_halts_oog() { - // Gas limit so that after static cost, remaining < cold load cost (2600) → skip_cold_load = true. - // Target is cold (not loaded), so host.selfdestruct returns ColdLoadSkipped. - let initial_gas = STATIC_GAS_COST + 100; - - for check_target_destruct_locally in [true, false] { - let mut env = HostTestEnv::new(EmptyDB::new()); - env.set_account_balance(ACCOUNT, U256::from(1)); - - let res = env.simulate_arc_selfdestruct_with_gas( - ACCOUNT, - TARGET, - check_target_destruct_locally, - Some(initial_gas), - ); - - assert_eq!( - res.result, - InstructionResult::OutOfGas, - "ColdLoadSkipped from host.selfdestruct should halt with OutOfGas" - ); - assert!( - res.gas.spent() <= initial_gas, - "gas spent should not exceed initial limit" - ); - } - } - - #[test] - #[cfg_attr( - debug_assertions, - should_panic(expected = "NativeCoinControl should be preloaded") - )] - fn selfdestruct_zero7_recovers_or_debug_asserts_when_native_coin_control_is_not_loaded() { - let check_target_destruct_locally_values: &[bool] = if cfg!(debug_assertions) { - &[true] - } else { - &[true, false] - }; - - for check_target_destruct_locally in check_target_destruct_locally_values { - let mut db = InMemoryDB::default(); - let slot = native_coin_control::compute_is_blocklisted_storage_slot(TARGET); - db.insert_account_storage( - native_coin_control::NATIVE_COIN_CONTROL_ADDRESS, - slot.into(), - U256::from(1), - ) - .expect("insert blocklist storage"); - let mut env = HostTestEnv::new(db); - env.set_account_balance(ACCOUNT, U256::from(1)); - - let res = env.simulate_arc_selfdestruct_without_native_coin_control_preload( - ACCOUNT, - TARGET, - *check_target_destruct_locally, - BlocklistReadPolicy::FailClosed, - ); - - #[cfg(debug_assertions)] - let _ = res; - - #[cfg(not(debug_assertions))] - { - assert_eq!(res.result, InstructionResult::Revert); - assert_eq!(res.gas.spent(), STATIC_GAS_COST); - assert_eq!(res.gas.refunded(), 0); - assert_account_matches!(env, ACCOUNT, State::touch_new(), U256::from(1)); - assert_account_matches!(env, TARGET, State::loaded_new(), U256::ZERO); - } - } - } - - #[test] - fn selfdestruct_pre_zero7_preserves_fail_open_without_native_coin_control_loaded() { - for check_target_destruct_locally in [true, false] { - let mut db = InMemoryDB::default(); - let slot = native_coin_control::compute_is_blocklisted_storage_slot(TARGET); - db.insert_account_storage( - native_coin_control::NATIVE_COIN_CONTROL_ADDRESS, - slot.into(), - U256::from(1), - ) - .expect("insert blocklist storage"); - let mut env = HostTestEnv::new(db); - env.set_account_balance(ACCOUNT, U256::from(1)); - - let res = env.simulate_arc_selfdestruct_without_native_coin_control_preload( - ACCOUNT, - TARGET, - check_target_destruct_locally, - BlocklistReadPolicy::FailOpen, - ); - - assert_eq!(res.result, InstructionResult::SelfDestruct); - assert_account_matches!(env, ACCOUNT, State::touch_new(), U256::ZERO); - assert_account_matches!(env, TARGET, State::touch_new(), U256::from(1)); - } - } - - // Regression tests against existing behavior - - #[test] - fn selfdestruct_refund_recorded_pre_london() { - for check_target_destruct_locally in [true, false] { - let mut env = HostTestEnv::new_with_spec(EmptyDB::new(), SpecId::ISTANBUL); - - let res = env.simulate_arc_selfdestruct(ACCOUNT, TARGET, check_target_destruct_locally); - assert_eq!(res.result, InstructionResult::SelfDestruct); - assert_eq!(res.gas.spent(), STATIC_GAS_COST); - assert_eq!(res.gas.refunded(), gas::SELFDESTRUCT_REFUND); - } - } - - #[test] - fn selfdestruct_no_refund_after_london() { - for check_target_destruct_locally in [true, false] { - let mut env = HostTestEnv::new_with_spec(EmptyDB::new(), SpecId::LONDON); - - let res = env.simulate_arc_selfdestruct(ACCOUNT, TARGET, check_target_destruct_locally); - assert_eq!(res.result, InstructionResult::SelfDestruct); - assert_eq!(res.gas.spent(), 7600u64); // 5000 static cost + 2600 cold target - assert_eq!(res.gas.refunded(), 0i64); - } - } - - #[test] - fn selfdestruct_transfers_balance() { - let initial_account_balance = U256::from(123u64); - let initial_target_balance = U256::from(456u64); - - for check_target_destruct_locally in [true, false] { - let mut env = HostTestEnv::new(EmptyDB::new()); - - // Set balances - env.set_account_balance(ACCOUNT, initial_account_balance); - env.set_account_balance(TARGET, initial_target_balance); - - let res = env.simulate_arc_selfdestruct(ACCOUNT, TARGET, check_target_destruct_locally); - assert_eq!(res.result, InstructionResult::SelfDestruct); - assert_eq!(res.gas.spent(), STATIC_GAS_COST); - assert_eq!(res.gas.refunded(), 0i64); - - // Verify balances - assert_account_matches!(env, ACCOUNT, State::touch_new(), U256::ZERO); - assert_account_matches!( - env, - TARGET, - State::touch_new(), - initial_target_balance + initial_account_balance - ); - } - } - - #[test] - fn selfdestruct_rejects_transfers_to_self() { - let initial_account_balance = U256::from(123u64); - - for check_target_destruct_locally in [true, false] { - let mut env = HostTestEnv::new(EmptyDB::new()); - - // Set balances - env.set_account_balance(ACCOUNT, initial_account_balance); - - let res = - env.simulate_arc_selfdestruct(ACCOUNT, ACCOUNT, check_target_destruct_locally); - assert_eq!(res.result, InstructionResult::Revert); - assert_eq!(res.gas.spent(), STATIC_GAS_COST); - assert_eq!(res.gas.refunded(), 0i64); - - // Verify balance is unchanged - assert_account_matches!(env, ACCOUNT, State::touch_new(), initial_account_balance); - } - } - - #[test] - fn selfdestruct_blocked_when_target_is_blocklisted() { - let initial_account_balance = U256::from(456u64); - let initial_target_balance = U256::from(789u64); - - for check_target_destruct_locally in [true, false] { - let mut env = HostTestEnv::new(EmptyDB::new()); - - // Set balances - env.set_account_balance(ACCOUNT, initial_account_balance); - env.set_account_balance(TARGET, initial_target_balance); - - // Blocklist the target address - env.set_blocklist(TARGET); - - let res = env.simulate_arc_selfdestruct(ACCOUNT, TARGET, check_target_destruct_locally); - assert_eq!( - res.result, - InstructionResult::Revert, - "Selfdestruct should revert when target is blocklisted" - ); - assert_eq!(res.gas.spent(), STATIC_GAS_COST); - assert_eq!(res.gas.refunded(), 0i64); - - // Verify balances are unchanged - assert_account_matches!(env, ACCOUNT, State::touch_new(), initial_account_balance); - assert_account_matches!(env, TARGET, State::touch_new(), initial_target_balance); - } - } - - #[test] - fn selfdestruct_blocked_when_selfdestructing_address_is_blocklisted() { - let initial_account_balance = U256::from(321u64); - let initial_target_balance = U256::from(654u64); - - for check_target_destruct_locally in [true, false] { - let mut env = HostTestEnv::new(EmptyDB::new()); - - // Set balances - env.set_account_balance(ACCOUNT, initial_account_balance); - env.set_account_balance(TARGET, initial_target_balance); - - // Blocklist the selfdestructing address (ACCOUNT) - env.set_blocklist(ACCOUNT); - - // Run interpreter - let res = env.simulate_arc_selfdestruct(ACCOUNT, TARGET, check_target_destruct_locally); - - // Verify the operation was reverted by checking the action set on bytecode - assert_eq!( - res.result, - InstructionResult::Revert, - "Selfdestruct should revert when selfdestructing address is blocklisted" - ); - assert_eq!(res.gas.spent(), STATIC_GAS_COST); - assert_eq!(res.gas.refunded(), 0i64); - - // Verify balances are unchanged - assert_account_matches!(env, ACCOUNT, State::touch_new(), initial_account_balance); - assert_account_matches!(env, TARGET, State::touch_new(), initial_target_balance); - } - } - - // transfer to a destructed account - - #[test] - fn selfdestruct_transfer_to_not_destructed_account() { - let amount = U256::from(234); - - for check_target_destruct_locally in [true, false] { - // Prepare host context with caller balance. - let mut env = HostTestEnv::new(EmptyDB::new()); - env.set_account_balance(ACCOUNT, amount); - - // Destruct `ACCOUNT`, transfer to empty, cold account `TARGET` - // After Cancun, the account will not be destructed if it was not created on the same transaction. - let res = env.simulate_arc_selfdestruct(ACCOUNT, TARGET, check_target_destruct_locally); - assert_eq!(res.result, InstructionResult::SelfDestruct); - assert_eq!(res.gas.spent(), 32600u64); - assert_eq!(res.gas.refunded(), 0i64); - assert_account_matches!(env, ACCOUNT, State::touch_new(), U256::ZERO); - assert_account_matches!(env, TARGET, State::touch_new(), amount); - - // Destruct `TARGET`, transfer to warm, not destructed account `ACCOUNT` - let res = env.simulate_arc_selfdestruct(TARGET, ACCOUNT, check_target_destruct_locally); - assert_eq!(res.result, InstructionResult::SelfDestruct); - assert_eq!(res.gas.spent(), 30000u64); // // 5000 static + 25000 new account, warm target - assert_eq!(res.gas.refunded(), 0i64); - assert_account_matches!(env, ACCOUNT, State::touch_new(), amount); - assert_account_matches!(env, TARGET, State::touch_new(), U256::ZERO); - } - } - - struct TransferToDestructedTestCase { - check_target_destruct_locally: bool, - locally: bool, - amount: U256, - expect_state_after_selfdestructed: (AccountStatus, AccountStatus), - expect_state_after_committed: (AccountStatus, AccountStatus), - } - - impl TransferToDestructedTestCase { - fn expect_revert(&self) -> bool { - self.check_target_destruct_locally && !self.amount.is_zero() - } - } - - #[test] - fn selfdestruct_transfer_to_destructed_account() { - let sender = address!("0x3000000000000000000000000000000000000002"); - - let test_cases = [ - TransferToDestructedTestCase { - check_target_destruct_locally: true, - locally: true, - amount: U256::ZERO, - expect_state_after_selfdestructed: ( - State::touch_new(), - State::touch_new_destructed(), - ), - expect_state_after_committed: (State::loaded(), State::loaded_new()), - }, - TransferToDestructedTestCase { - check_target_destruct_locally: false, - locally: true, - amount: U256::ZERO, - expect_state_after_selfdestructed: ( - State::touch_new(), - State::touch_new_destructed(), - ), - expect_state_after_committed: (State::loaded(), State::loaded_new()), - }, - TransferToDestructedTestCase { - check_target_destruct_locally: true, - locally: true, - amount: U256::from(234), - expect_state_after_selfdestructed: ( - State::touch_new(), - State::touch_new_destructed(), - ), - expect_state_after_committed: (State::loaded(), State::loaded_new()), - }, - TransferToDestructedTestCase { - check_target_destruct_locally: false, - locally: true, - amount: U256::from(234), - expect_state_after_selfdestructed: ( - State::touch_new(), - State::touch_new_destructed(), - ), - expect_state_after_committed: (State::loaded(), State::loaded_new()), - }, - TransferToDestructedTestCase { - check_target_destruct_locally: true, - locally: false, - amount: U256::ZERO, - expect_state_after_selfdestructed: ( - State::touch_new(), - State::destructed_new_before(), - ), - expect_state_after_committed: (State::loaded(), State::loaded_new()), - }, - TransferToDestructedTestCase { - check_target_destruct_locally: false, - locally: false, - amount: U256::ZERO, - expect_state_after_selfdestructed: ( - State::touch_new(), - State::destructed_new_before(), - ), - expect_state_after_committed: (State::loaded(), State::loaded_new()), - }, - TransferToDestructedTestCase { - check_target_destruct_locally: true, - locally: false, - amount: U256::from(234), - expect_state_after_selfdestructed: ( - State::touch_new(), - State::destructed_new_before(), - ), - expect_state_after_committed: (State::loaded(), State::loaded_new()), - }, - TransferToDestructedTestCase { - check_target_destruct_locally: false, - locally: false, - amount: U256::from(234), - expect_state_after_selfdestructed: ( - State::touch_new(), - State::destructed_new_before(), - ), - expect_state_after_committed: (State::loaded(), State::loaded_new()), - }, - ]; - - for (index, tc) in test_cases.iter().enumerate() { - let desc = format!( - "index={index}, check_target_destruct_locally={}, locally={}, amount={}", - tc.check_target_destruct_locally, tc.locally, tc.amount - ); - let mut env = HostTestEnv::new(InMemoryDB::default()); - - // Prepare a destruct locally account TARGET, don't expect it will fail here. - env.set_account_balance(sender, tc.amount); - env.simulate_create_account(sender, TARGET, tc.amount); - assert_account_matches!(env, sender, State::touch_new(), U256::ZERO, "({desc})"); - let res = - env.simulate_arc_selfdestruct(TARGET, ACCOUNT, tc.check_target_destruct_locally); - assert_eq!(res.result, InstructionResult::SelfDestruct, "({desc})"); - assert_account_matches!( - env, - TARGET, - State::touch_new_destructed(), - U256::ZERO, - "({desc})" - ); - assert_account_matches!(env, ACCOUNT, State::touch_new(), tc.amount, "({desc})"); - - // If the test case requires a new transaction context, commit the current transaction. - if !tc.locally { - env.commit_tx(); - } - - // Destruct ACCOUNT, transfer balance to local destructed account `TARGET` - let res = - env.simulate_arc_selfdestruct(ACCOUNT, TARGET, tc.check_target_destruct_locally); - - let (account_amount, target_amount) = if tc.expect_revert() { - assert_eq!( - res.result, - InstructionResult::Revert, - "({desc}) not reverted", - ); - assert_eq!(res.gas.spent(), STATIC_GAS_COST, "({desc})"); - assert_eq!(res.gas.refunded(), 0i64, "({desc})"); - (tc.amount, U256::ZERO) - } else { - assert_eq!( - res.result, - InstructionResult::SelfDestruct, - "({desc}) not selfdestructed", - ); - (U256::ZERO, tc.amount) - }; - assert_account_matches!( - env, - ACCOUNT, - tc.expect_state_after_selfdestructed.0, - account_amount, - "({desc}) ACCOUNT state mismatch after selfdestruct", - ); - assert_account_matches!( - env, - TARGET, - tc.expect_state_after_selfdestructed.1, - target_amount, - "({desc}) TARGET state mismatch after selfdestruct", - ); - - // Commit the block changes to the database. - env.commit_block(); - let (account_amount, target_amount) = if tc.expect_revert() { - (tc.amount, U256::ZERO) - } else { - (U256::ZERO, U256::ZERO) - }; - assert_account_matches!( - env, - ACCOUNT, - tc.expect_state_after_committed.0, - account_amount, - "({desc}) ACCOUNT state mismatch after commit block", - ); - assert_account_matches!( - env, - TARGET, - tc.expect_state_after_committed.1, - target_amount, - "({desc}) TARGET state mismatch after commit block", - ); - } - } - - // B8: Verify Arc's SELFDESTRUCT restrictions make REVM's delayed burn log unreachable. - // - // REVM emits Selfdestruct(address,uint256) logs at end-of-transaction for self-destructed - // accounts that still have balance (e.g., account A selfdestructs, then receives ETH from - // account B's selfdestruct in the same transaction). Arc does not implement delayed burn - // logs because its restrictions prevent the scenarios that would trigger them: - // 1. SELFDESTRUCT to self is always rejected (revert) - // 2. SELFDESTRUCT to Address::ZERO is rejected under Zero5 - // 3. SELFDESTRUCT to an already-destructed target is rejected - // These tests verify each restriction holds under Zero5's EIP-7708 log mode. - - /// Zero5: SELFDESTRUCT to self is rejected, so a self-destructed account cannot - /// re-receive its own balance (the primary delayed burn scenario). - #[test] - fn selfdestruct_zero5_to_self_rejected() { - let amount = U256::from(100); - let mut env = HostTestEnv::new(EmptyDB::new()); - env.set_account_balance(ACCOUNT, amount); - - let res = env.simulate_arc_selfdestruct_full( - ACCOUNT, - ACCOUNT, - true, - Some(TransferLogMode::Eip7708Transfer), - None, - ); - assert_eq!( - res.result, - InstructionResult::Revert, - "Zero5: SELFDESTRUCT to self should revert" - ); - - // Balance unchanged — no state modification - assert_account_matches!(env, ACCOUNT, State::touch_new(), amount); - - // No logs emitted - let logs = env.host.journal_mut().take_logs(); - assert!( - logs.is_empty(), - "Zero5: SELFDESTRUCT to self should emit no logs" - ); - } - - /// Zero5: SELFDESTRUCT to an already-destructed target is rejected, preventing - /// cross-selfdestruct balance accumulation that would require delayed burn logs. - #[test] - fn selfdestruct_zero5_to_destructed_target_rejected() { - let amount_a = U256::from(100); - let amount_b = U256::from(200); - let mut env = HostTestEnv::new(EmptyDB::new()); - - // Create two accounts and selfdestruct ACCOUNT → TARGET first. - env.simulate_create_account(ACCOUNT, ACCOUNT, U256::ZERO); - env.set_account_balance(ACCOUNT, amount_a); - env.simulate_create_account(TARGET, TARGET, U256::ZERO); - env.set_account_balance(TARGET, amount_b); - - // First: ACCOUNT selfdestructs to TARGET (succeeds) - let res1 = env.simulate_arc_selfdestruct_full( - ACCOUNT, - TARGET, - true, - Some(TransferLogMode::Eip7708Transfer), - None, - ); - assert_eq!( - res1.result, - InstructionResult::SelfDestruct, - "First selfdestruct should succeed" - ); - - // TARGET now has balance: amount_b + amount_a - // TARGET selfdestructs back to ACCOUNT — but ACCOUNT is now destructed. - // Arc's check_selfdestruct_accounts rejects this. - let res2 = env.simulate_arc_selfdestruct_full( - TARGET, - ACCOUNT, - true, // check_target_destructed = true (Zero5+ behavior) - Some(TransferLogMode::Eip7708Transfer), - None, - ); - assert_eq!( - res2.result, - InstructionResult::Revert, - "Zero5: SELFDESTRUCT to already-destructed target should revert" - ); - } - - /// Zero5: SELFDESTRUCT to Address::ZERO with zero balance succeeds but produces no log. - /// (The zero-address check only triggers when balance is non-zero.) - #[test] - fn selfdestruct_zero5_to_zero_address_zero_balance_succeeds() { - let mut env = HostTestEnv::new(EmptyDB::new()); - // ACCOUNT has zero balance - env.set_account_balance(ACCOUNT, U256::ZERO); - - let res = env.simulate_arc_selfdestruct_full( - ACCOUNT, - Address::ZERO, - true, - Some(TransferLogMode::Eip7708Transfer), - None, - ); - // Zero balance → the non-zero branch is skipped entirely → proceeds to host.selfdestruct - assert_eq!( - res.result, - InstructionResult::SelfDestruct, - "Zero5: SELFDESTRUCT to zero address with zero balance should succeed" - ); - - let logs = env.host.journal_mut().take_logs(); - assert!( - logs.is_empty(), - "Zero5: SELFDESTRUCT with zero balance should emit no logs" - ); - } -} diff --git a/crates/evm/src/subcall.rs b/crates/evm/src/subcall.rs deleted file mode 100644 index ccea548..0000000 --- a/crates/evm/src/subcall.rs +++ /dev/null @@ -1,133 +0,0 @@ -// Copyright 2026 Circle Internet Group, Inc. All rights reserved. -// -// SPDX-License-Identifier: Apache-2.0 -// -// Licensed under the Apache License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. -// You may obtain a copy of the License at -// -// http://www.apache.org/licenses/LICENSE-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// See the License for the specific language governing permissions and -// limitations under the License. - -//! Subcall registry and continuation storage for `ArcEvm`. -//! -//! The [`SubcallPrecompile`] trait and related types are defined in `arc-precompiles`. -//! This module provides the registry that maps precompile addresses to implementations, -//! and the continuation type stored on `ArcEvm` between `init_subcall` and `complete_subcall`. - -use alloy_primitives::Address; -use arc_precompiles::subcall::{SubcallContinuationData, SubcallPrecompile}; -use revm_context_interface::journaled_state::JournalCheckpoint; -use std::collections::{HashMap, HashSet}; -use std::sync::Arc; - -/// State stored between `init_subcall` and `complete_subcall`, keyed by the precompile call's depth. -/// -/// Contains a trait object (`dyn SubcallPrecompile`) which doesn't implement `Debug`, -/// so we provide a manual implementation. -pub struct SubcallContinuation { - /// The subcall precompile implementation that handles `complete_subcall`. - pub(crate) precompile: Arc, - /// The original call's gas limit (budget allocated by the parent frame). - pub(crate) gas_limit: u64, - /// Gas consumed by `init_subcall` (ABI decoding, validation, and EIP-2929 account access). - pub(crate) init_subcall_gas_overhead: u64, - /// The original call's return memory offset. - pub(crate) return_memory_offset: std::ops::Range, - /// Opaque state carried from `init_subcall` to `complete_subcall`. - pub(crate) continuation_data: SubcallContinuationData, - /// Journal checkpoint taken before child dispatch. Used to revert the child's - /// committed state if `complete_subcall` rejects a successful child. - pub(crate) checkpoint: JournalCheckpoint, -} - -/// Specifies which addresses are authorized to call a subcall precompile. -#[derive(Debug, Clone)] -pub enum AllowedCallers { - /// Any address may call this precompile. - Unrestricted, - /// Only the specified addresses may call this precompile. - Only(HashSet
), -} - -impl AllowedCallers { - /// Returns `true` if the given caller is authorized. - pub fn is_allowed(&self, caller: &Address) -> bool { - match self { - Self::Unrestricted => true, - Self::Only(set) => set.contains(caller), - } - } -} - -/// A registered subcall precompile with its caller restrictions. -#[derive(Clone)] -struct SubcallRegistration { - /// The subcall precompile implementation - precompile: Arc, - /// Hardcoded set of addresses allowed to call this subcall precompile. - /// Enforced in frame_init before calling `init_subcall`. - allowed_callers: AllowedCallers, -} - -/// Registry of subcall-capable precompiles. -/// -/// Maps precompile addresses to their implementations and caller restrictions. -/// Built by `ArcEvmFactory` and shared (via `Arc`) across all EVM instances for a block. -#[derive(Default, Clone)] -pub struct SubcallRegistry { - precompiles: HashMap, -} - -impl SubcallRegistry { - /// Creates a new empty registry. - pub fn new() -> Self { - Self::default() - } - - /// Registers a subcall precompile at the given address. - pub fn register( - &mut self, - address: Address, - precompile: Arc, - allowed_callers: AllowedCallers, - ) { - self.precompiles.insert( - address, - SubcallRegistration { - precompile, - allowed_callers, - }, - ); - } - - /// Looks up a subcall precompile by address. - /// - /// Returns `None` if the address is not a registered subcall precompile. - pub fn get(&self, address: &Address) -> Option<(&Arc, &AllowedCallers)> { - self.precompiles - .get(address) - .map(|r| (&r.precompile, &r.allowed_callers)) - } -} - -impl std::fmt::Debug for SubcallContinuation { - fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { - f.debug_struct("SubcallContinuation") - .field("return_memory_offset", &self.return_memory_offset) - .finish_non_exhaustive() - } -} - -impl std::fmt::Debug for SubcallRegistry { - fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { - f.debug_struct("SubcallRegistry") - .field("addresses", &self.precompiles.keys().collect::>()) - .finish() - } -} diff --git a/crates/evm/src/subcall_test.rs b/crates/evm/src/subcall_test.rs deleted file mode 100644 index ff6684e..0000000 --- a/crates/evm/src/subcall_test.rs +++ /dev/null @@ -1,296 +0,0 @@ -// Copyright 2026 Circle Internet Group, Inc. All rights reserved. -// -// SPDX-License-Identifier: Apache-2.0 -// -// Licensed under the Apache License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. -// You may obtain a copy of the License at -// -// http://www.apache.org/licenses/LICENSE-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// See the License for the specific language governing permissions and -// limitations under the License. - -//! Test precompile for subcall integration tests. -//! -//! This precompile demonstrates the two-phase subcall pattern: -//! - `init_subcall`: ABI-decode `(address target, bytes calldata)` from input -//! - `complete_subcall`: On child success, ABI-encode child output as `bytes` return -//! -//! Only available under `#[cfg(test)]`. - -use alloy_primitives::{address, Address, Bytes}; -use alloy_sol_types::{sol_data, SolType, SolValue}; -use arc_precompiles::subcall::{ - SubcallCompletionResult, SubcallContinuationData, SubcallError, SubcallInitResult, - SubcallPrecompile, -}; -use revm::handler::FrameResult; -use revm::interpreter::interpreter_action::{CallInput, CallInputs, CallScheme, CallValue}; -use revm::interpreter::Gas; - -/// Address of the subcall test precompile: `0x1800...0099` -pub const SUBCALL_TEST_ADDRESS: Address = address!("1800000000000000000000000000000000000099"); - -// ABI types for init_subcall input decoding: (address target, bytes calldata) -type InitSubcallInput = (sol_data::Address, sol_data::Bytes); - -/// Test subcall precompile for integration testing. -#[derive(Debug)] -pub struct SubcallTestPrecompile; - -impl SubcallPrecompile for SubcallTestPrecompile { - fn init_subcall(&self, inputs: &CallInputs) -> Result { - let input_bytes = match &inputs.input { - CallInput::Bytes(b) => b.clone(), - CallInput::SharedBuffer(_) => { - return Err(SubcallError::AbiDecodeError( - "unexpected shared buffer input".into(), - )); - } - }; - - let decoded = ::abi_decode(&input_bytes) - .map_err(|e| SubcallError::AbiDecodeError(format!("subcall_test: {e}")))?; - - let (target, calldata) = decoded; - - // hardcoded limit for testing purposes - let child_gas = Gas::new(100_000); - - let child_inputs = Box::new(CallInputs { - scheme: CallScheme::Call, - target_address: target, - bytecode_address: target, - known_bytecode: None, - value: CallValue::Transfer(alloy_primitives::U256::ZERO), - input: CallInput::Bytes(calldata), - gas_limit: child_gas.remaining(), - is_static: inputs.is_static, - caller: inputs.caller, - return_memory_offset: 0..0, - }); - - Ok(SubcallInitResult { - child_inputs, - continuation_data: SubcallContinuationData { - state: Box::new(()), - }, - // Test precompile — not gas-precise; see CallFromPrecompile for real accounting - gas_overhead: 0, - }) - } - - fn complete_subcall( - &self, - _continuation_data: SubcallContinuationData, - child_result: &FrameResult, - ) -> Result { - let outcome = match child_result { - FrameResult::Call(outcome) => outcome, - _ => return Err(SubcallError::UnexpectedFrameResult), - }; - - let child_success = outcome.result.result.is_ok(); - let child_output = &outcome.result.output; - - if !child_success { - // Child failed — signal that the precompile should revert - return Ok(SubcallCompletionResult { - output: if outcome.result.result.is_revert() { - child_output.clone() - } else { - Bytes::new() - }, - success: false, - gas_overhead: 0, - }); - } - - // ABI-encode the child output as `bytes` return type - let encoded_output = Bytes::from(child_output.clone().abi_encode()); - - Ok(SubcallCompletionResult { - output: encoded_output, - success: true, - gas_overhead: 0, - }) - } -} - -/// Address for the CostlyCompleteSubcallPrecompile test precompile. -pub const COSTLY_COMPLETE_SUBCALL_ADDRESS: Address = - address!("1800000000000000000000000000000000000096"); - -/// Known gas_overhead reported by CostlyCompleteSubcallPrecompile for testing. -pub const COSTLY_COMPLETE_GAS_OVERHEAD: u64 = 500; - -/// Test precompile identical to SubcallTestPrecompile but reports a known nonzero gas_overhead. -/// Used to verify the framework actually charges the reported completion gas. -#[derive(Debug)] -pub struct CostlyCompleteSubcallPrecompile; - -impl SubcallPrecompile for CostlyCompleteSubcallPrecompile { - fn init_subcall(&self, inputs: &CallInputs) -> Result { - SubcallTestPrecompile.init_subcall(inputs) - } - - fn complete_subcall( - &self, - continuation_data: SubcallContinuationData, - child_result: &FrameResult, - ) -> Result { - let mut result = SubcallTestPrecompile.complete_subcall(continuation_data, child_result)?; - result.gas_overhead = COSTLY_COMPLETE_GAS_OVERHEAD; - Ok(result) - } -} - -/// Address for the ExcessiveCompleteSubcallPrecompile test precompile. -pub const EXCESSIVE_COMPLETE_SUBCALL_ADDRESS: Address = - address!("1800000000000000000000000000000000000095"); - -/// Large gas_overhead reported by ExcessiveCompleteSubcallPrecompile for OOG tests. -pub const EXCESSIVE_COMPLETE_GAS_OVERHEAD: u64 = 5_000; - -/// Test precompile that behaves like SubcallTestPrecompile but reports a large -/// completion gas overhead. -#[derive(Debug)] -pub struct ExcessiveCompleteSubcallPrecompile; - -impl SubcallPrecompile for ExcessiveCompleteSubcallPrecompile { - fn init_subcall(&self, inputs: &CallInputs) -> Result { - SubcallTestPrecompile.init_subcall(inputs) - } - - fn complete_subcall( - &self, - continuation_data: SubcallContinuationData, - child_result: &FrameResult, - ) -> Result { - let mut result = SubcallTestPrecompile.complete_subcall(continuation_data, child_result)?; - result.gas_overhead = EXCESSIVE_COMPLETE_GAS_OVERHEAD; - Ok(result) - } -} - -/// Address for the FailingCompleteSubcallPrecompile test precompile. -pub const FAILING_COMPLETE_SUBCALL_ADDRESS: Address = - address!("1800000000000000000000000000000000000098"); - -/// Test precompile whose complete_subcall always errors — used to verify all-gas-consumed behavior. -#[derive(Debug)] -pub struct FailingCompleteSubcallPrecompile; - -impl SubcallPrecompile for FailingCompleteSubcallPrecompile { - fn init_subcall(&self, inputs: &CallInputs) -> Result { - let input_bytes = match &inputs.input { - CallInput::Bytes(b) => b.clone(), - CallInput::SharedBuffer(_) => { - return Err(SubcallError::AbiDecodeError( - "unexpected shared buffer".into(), - )); - } - }; - - let decoded = ::abi_decode(&input_bytes) - .map_err(|e| SubcallError::AbiDecodeError(format!("failing_complete_subcall: {e}")))?; - let (target, calldata) = decoded; - - Ok(SubcallInitResult { - child_inputs: Box::new(CallInputs { - scheme: CallScheme::Call, - target_address: target, - bytecode_address: target, - known_bytecode: None, - value: CallValue::Transfer(alloy_primitives::U256::ZERO), - input: CallInput::Bytes(calldata), - gas_limit: 50_000, - is_static: false, - caller: inputs.caller, - return_memory_offset: 0..0, - }), - continuation_data: SubcallContinuationData { - state: Box::new(()), - }, - gas_overhead: 0, - }) - } - - fn complete_subcall( - &self, - _continuation_data: SubcallContinuationData, - child_result: &FrameResult, - ) -> Result { - // Assert child succeeded - let child_ok = matches!(child_result, FrameResult::Call(o) if o.result.result.is_ok()); - assert!(child_ok, "expected successful child, got: {child_result:?}"); - Err(SubcallError::InternalError( - "intentional complete_subcall failure".into(), - )) - } -} - -/// Address for the RejectingCompleteSubcallPrecompile test precompile. -pub const REJECTING_COMPLETE_SUBCALL_ADDRESS: Address = - address!("1800000000000000000000000000000000000097"); - -/// Test precompile whose complete_subcall returns `success: false` when the child succeeds. -/// Used to verify that the child's committed state is reverted. -#[derive(Debug)] -pub struct RejectingCompleteSubcallPrecompile; - -impl SubcallPrecompile for RejectingCompleteSubcallPrecompile { - fn init_subcall(&self, inputs: &CallInputs) -> Result { - let input_bytes = match &inputs.input { - CallInput::Bytes(b) => b.clone(), - CallInput::SharedBuffer(_) => { - return Err(SubcallError::AbiDecodeError( - "unexpected shared buffer".into(), - )); - } - }; - - let decoded = ::abi_decode(&input_bytes).map_err(|e| { - SubcallError::AbiDecodeError(format!("rejecting_complete_subcall: {e}")) - })?; - let (target, calldata) = decoded; - - Ok(SubcallInitResult { - child_inputs: Box::new(CallInputs { - scheme: CallScheme::Call, - target_address: target, - bytecode_address: target, - known_bytecode: None, - value: CallValue::Transfer(alloy_primitives::U256::ZERO), - input: CallInput::Bytes(calldata), - gas_limit: 50_000, - is_static: false, - caller: inputs.caller, - return_memory_offset: 0..0, - }), - continuation_data: SubcallContinuationData { - state: Box::new(()), - }, - gas_overhead: 0, - }) - } - - fn complete_subcall( - &self, - _continuation_data: SubcallContinuationData, - child_result: &FrameResult, - ) -> Result { - // Assert child succeeded - let child_ok = matches!(child_result, FrameResult::Call(o) if o.result.result.is_ok()); - assert!(child_ok, "expected successful child, got: {child_result:?}"); - Ok(SubcallCompletionResult { - output: Bytes::new(), - success: false, - gas_overhead: 0, - }) - } -} diff --git a/crates/execution-config/Cargo.toml b/crates/execution-config/Cargo.toml deleted file mode 100644 index e88daef..0000000 --- a/crates/execution-config/Cargo.toml +++ /dev/null @@ -1,56 +0,0 @@ -[package] -name = "arc-execution-config" -version.workspace = true -edition.workspace = true -readme.workspace = true -license.workspace = true -exclude.workspace = true -rust-version.workspace = true -publish.workspace = true -repository.workspace = true - -[features] -arbitrary = ["alloy-serde/arbitrary"] -test-utils = [] - -[dependencies] -# alloy -alloy-eips.workspace = true -alloy-evm.workspace = true -alloy-genesis.workspace = true -alloy-primitives.workspace = true -alloy-serde.workspace = true -alloy-sol-types.workspace = true - -# arc -arc-shared.workspace = true - -# misc -eyre.workspace = true -itertools.workspace = true -once_cell.workspace = true - -# reth -reth-chainspec.workspace = true -reth-cli.workspace = true -reth-cli-commands.workspace = true -reth-ethereum-forks.workspace = true -reth-ethereum-primitives.workspace = true -reth-evm.workspace = true -reth-network-peers.workspace = true -reth-node-core.workspace = true -reth-primitives-traits.workspace = true - -# revm -revm.workspace = true -revm-primitives.workspace = true -serde = { workspace = true, features = ["derive"] } -serde_json = { workspace = true, default-features = false, features = ["alloc"] } -thiserror.workspace = true - -[dev-dependencies] -clap = { workspace = true, features = ["derive"] } -reth-ethereum = { workspace = true, features = ["evm"] } - -[lints] -workspace = true diff --git a/crates/execution-config/src/addresses_denylist.rs b/crates/execution-config/src/addresses_denylist.rs deleted file mode 100644 index 365726e..0000000 --- a/crates/execution-config/src/addresses_denylist.rs +++ /dev/null @@ -1,214 +0,0 @@ -// Copyright 2025 Circle Internet Group, Inc. All rights reserved. -// -// SPDX-License-Identifier: Apache-2.0 -// -// Licensed under the Apache License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. -// You may obtain a copy of the License at -// -// http://www.apache.org/licenses/LICENSE-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// See the License for the specific language governing permissions and -// limitations under the License. - -//! Configuration for the addresses denylist. -//! -//! Used by mempool validation and Revm pre-flight when integrated. -//! This module defines the config type and validation only; no chain reads. - -use alloy_primitives::{address, b256, Address, B256}; -use itertools::Itertools; -use thiserror::Error; - -/// Revert message when a transaction involves a denylisted address. -pub const ERR_DENYLISTED_ADDRESS: &str = "Address is denylisted"; - -/// Default Denylist contract address when deployed in genesis (localdev). -/// Matches the deterministic address used by the genesis builder (`scripts/genesis/addresses.ts`). -/// Node CLI can use this as the default for `--arc.denylist.address` when the contract is deployed in genesis. -/// -/// Address derived via deterministic CREATE2 salt search: cast create2 with --seed keccak256("Denylist.v1"), -/// first match with prefix 0x360. Reproduce: `make mine-denylist-salt INIT_CODE_HASH=` -pub const DEFAULT_DENYLIST_ADDRESS: Address = - address!("0x36059b615370eB999e8eC0c9401835B407834221"); - -/// ERC-7201 base storage slot for the Denylist contract (arc.storage.Denylist.v1). -/// Matches the slot used by the genesis builder (`scripts/genesis/Denylist.ts`). -/// Node CLI can use this as the default for `--arc.denylist.storage-slot` when the contract is deployed in genesis. -pub const DEFAULT_DENYLIST_ERC7201_BASE_SLOT: B256 = - b256!("0x1d7e1388d3ae56f3d9c18b1ce8d2b3b1a238a0edf682d2053af5d8a1d2f12f00"); - -/// Computes the ERC-7201 storage slot for `address` in the Denylist contract's denylisted mapping. -/// Matches the formula: `keccak256(abi.encode(address, base_slot))`. -#[inline] -#[must_use] -pub fn compute_denylist_storage_slot(address: Address, base_slot: B256) -> B256 { - use alloy_primitives::keccak256; - use alloy_sol_types::SolValue; - - let encoded = (address, base_slot).abi_encode(); - B256::from(keccak256(encoded.as_slice()).0) -} - -/// Error when building [`AddressesDenylistConfig`] with `enabled` but missing address or slot. -#[derive(Debug, Error)] -pub enum AddressesDenylistConfigError { - #[error("denylist is enabled but address is not set")] - MissingContractAddress, - #[error("denylist is enabled but storage slot is not set")] - MissingStorageSlot, -} - -/// Configuration for the addresses denylist. -/// -/// Invalid states (enabled without address or slot) are unrepresentable. -#[derive(Default, Debug, Clone)] -pub enum AddressesDenylistConfig { - /// Denylist checks disabled. - #[default] - Disabled, - /// Denylist checks enabled. All fields required. - Enabled { - /// Denylist contract address. - contract_address: Address, - /// ERC-7201 base storage slot for the denylist. - storage_slot: B256, - /// Addresses to exclude from denylist checks (e.g. ops recovery). - /// Stored deduplicated for fast lookup. - addresses_exclusions: Vec
, - }, -} - -impl AddressesDenylistConfig { - /// Build config. When `enabled` is true, `contract_address` and `storage_slot` must be set. - /// Deduplicates exclusions. - pub fn try_new( - enabled: bool, - contract_address: Option
, - storage_slot: Option, - addresses_exclusions: Vec
, - ) -> Result { - if enabled { - let contract_address = - contract_address.ok_or(AddressesDenylistConfigError::MissingContractAddress)?; - let storage_slot = - storage_slot.ok_or(AddressesDenylistConfigError::MissingStorageSlot)?; - let addresses_exclusions = addresses_exclusions.into_iter().unique().collect(); - Ok(Self::Enabled { - contract_address, - storage_slot, - addresses_exclusions, - }) - } else { - Ok(Self::Disabled) - } - } - - /// Returns true if denylist checks are enabled. - #[inline] - pub fn is_enabled(&self) -> bool { - matches!(self, Self::Enabled { .. }) - } - - /// Returns true if the given address is in the address exclusions set. - /// When disabled, returns false (no exclusions apply). - #[inline] - pub fn is_address_excluded(&self, addr: &Address) -> bool { - match self { - Self::Disabled => false, - Self::Enabled { - addresses_exclusions, - .. - } => addresses_exclusions.iter().any(|a| a == addr), - } - } -} - -#[cfg(test)] -mod tests { - use super::*; - use alloy_primitives::address; - - #[test] - fn default_is_disabled() { - let cfg = AddressesDenylistConfig::default(); - assert!(!cfg.is_enabled()); - } - - #[test] - fn try_new_enabled_with_both_succeeds() { - let addr = address!("0x3600000000000000000000000000000000000001"); - let slot = B256::from([1u8; 32]); - let cfg = - AddressesDenylistConfig::try_new(true, Some(addr), Some(slot), Vec::new()).unwrap(); - assert!(cfg.is_enabled()); - if let AddressesDenylistConfig::Enabled { - contract_address, - storage_slot, - .. - } = &cfg - { - assert_eq!(*contract_address, addr); - assert_eq!(*storage_slot, slot); - } else { - panic!("expected Enabled variant"); - } - } - - #[test] - fn try_new_enabled_without_address_fails() { - let err = - AddressesDenylistConfig::try_new(true, None, Some(B256::ZERO), Vec::new()).unwrap_err(); - assert!(matches!( - err, - AddressesDenylistConfigError::MissingContractAddress - )); - } - - #[test] - fn try_new_enabled_without_slot_fails() { - let err = AddressesDenylistConfig::try_new(true, Some(Address::ZERO), None, Vec::new()) - .unwrap_err(); - assert!(matches!( - err, - AddressesDenylistConfigError::MissingStorageSlot - )); - } - - #[test] - fn try_new_disabled_accepts_none() { - let cfg = AddressesDenylistConfig::try_new(false, None, None, Vec::new()).unwrap(); - assert!(!cfg.is_enabled()); - } - - #[test] - fn exclusions_deduplicated() { - let addr = address!("0x3600000000000000000000000000000000000001"); - let slot = B256::from([1u8; 32]); - let addrs = vec![addr, addr]; - let cfg = AddressesDenylistConfig::try_new(true, Some(addr), Some(slot), addrs).unwrap(); - if let AddressesDenylistConfig::Enabled { - addresses_exclusions, - .. - } = &cfg - { - assert_eq!(addresses_exclusions.len(), 1); - } else { - panic!("expected Enabled variant"); - } - } - - #[test] - fn is_address_excluded() { - let addr1 = address!("0x3600000000000000000000000000000000000001"); - let addr2 = address!("0x3600000000000000000000000000000000000002"); - let slot = B256::from([1u8; 32]); - let cfg = - AddressesDenylistConfig::try_new(true, Some(addr1), Some(slot), vec![addr1]).unwrap(); - assert!(cfg.is_address_excluded(&addr1)); - assert!(!cfg.is_address_excluded(&addr2)); - } -} diff --git a/crates/execution-config/src/call_from.rs b/crates/execution-config/src/call_from.rs deleted file mode 100644 index a0878db..0000000 --- a/crates/execution-config/src/call_from.rs +++ /dev/null @@ -1,27 +0,0 @@ -// Copyright 2026 Circle Internet Group, Inc. All rights reserved. -// -// SPDX-License-Identifier: Apache-2.0 -// -// Licensed under the Apache License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. -// You may obtain a copy of the License at -// -// http://www.apache.org/licenses/LICENSE-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// See the License for the specific language governing permissions and -// limitations under the License. - -//! Addresses of contracts authorized to call the CallFrom precompile. -//! -//! These are CREATE2-precomputed addresses deployed with a zero salt. - -use alloy_primitives::{address, Address}; - -/// Address of the `Memo` contract (CREATE2-deployed, zero salt). -pub const MEMO_ADDRESS: Address = address!("5294E9927c3306DcBaDb03fe70b92e01cCede505"); - -/// Address of the `Multicall3From` contract (CREATE2-deployed, zero salt). -pub const MULTICALL3_FROM_ADDRESS: Address = address!("522fAf9A91c41c443c66765030741e4AaCe147D0"); diff --git a/crates/execution-config/src/chainspec.rs b/crates/execution-config/src/chainspec.rs deleted file mode 100644 index ba1c6d9..0000000 --- a/crates/execution-config/src/chainspec.rs +++ /dev/null @@ -1,1933 +0,0 @@ -// Copyright 2025 Circle Internet Group, Inc. All rights reserved. -// -// SPDX-License-Identifier: Apache-2.0 -// -// Licensed under the Apache License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. -// You may obtain a copy of the License at -// -// http://www.apache.org/licenses/LICENSE-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// See the License for the specific language governing permissions and -// limitations under the License. - -use alloy_eips::eip1559::DEFAULT_ELASTICITY_MULTIPLIER; -use alloy_eips::eip7840::BlobParams; -use alloy_evm::eth::spec::EthExecutorSpec; -use alloy_genesis::Genesis; -#[cfg(any(feature = "test-utils", test))] -use alloy_primitives::{address, b256}; -use alloy_primitives::{Address, U256}; -use eyre::Result; -use once_cell::sync::Lazy as LazyLock; -use reth_chainspec::{ - BaseFeeParams, Chain, ChainSpec, DepositContract, EthChainSpec, EthereumHardfork, - EthereumHardforks, ForkCondition, ForkFilter, ForkId, Hardfork, Hardforks, Head, -}; -use reth_cli::chainspec::{parse_genesis, ChainSpecParser}; -use reth_ethereum_primitives::EthPrimitives; -use reth_network_peers::NodeRecord; -use reth_primitives_traits::NodePrimitives; -use revm_primitives::B256; -use std::sync::Arc; - -#[cfg(any(feature = "test-utils", test))] -use crate::hardforks::ArcHardfork; -#[cfg(any(feature = "test-utils", test))] -use crate::native_coin_control::compute_is_blocklisted_storage_slot; -use crate::{ - gas_fee::decode_base_fee_from_bytes, - hardforks::{ - ArcGenesisInfo, ArcHardforkFlags, ARC_DEVNET_HARDFORKS, ARC_LOCALDEV_HARDFORKS, - ARC_MAINNET_HARDFORKS, ARC_TESTNET_HARDFORKS, - }, -}; - -use crate::chain_ids::*; - -const ARC_SUPPORTED: &[&str] = &["arc-mainnet", "arc-testnet", "arc-localdev", "arc-devnet"]; -const ARC_BASE_FEE_MAX_CHANGE_DENOMINATOR: u128 = 50; // 1/50 = 2% - -#[derive(Debug, Clone, Default)] -#[non_exhaustive] -pub struct ArcChainSpecParser; - -impl ChainSpecParser for ArcChainSpecParser { - type ChainSpec = ArcChainSpec; - - const SUPPORTED_CHAINS: &'static [&'static str] = ARC_SUPPORTED; - - fn parse(s: &str) -> Result> { - match s { - "arc-localdev" => Ok(LOCAL_DEV.clone()), - "arc-devnet" => Ok(DEVNET.clone()), - "arc-testnet" => Ok(TESTNET.clone()), - "arc-mainnet" => Ok(MAINNET.clone()), - _ => { - let genesis = parse_genesis(s)?; - Ok(Arc::new(ArcChainSpec::from(genesis))) - } - } - } -} - -/// Block gas limit configuration -/// -/// Use [`BlockGasLimitConfig::new`] to construct. -#[derive(Debug, Clone, Copy, PartialEq, Eq)] -pub struct BlockGasLimitConfig { - min: u64, - max: u64, - default: u64, -} - -impl BlockGasLimitConfig { - /// Creates a new `BlockGasLimitConfig`. - /// - /// # Panics - /// Panics if `min > default` or `default > max`. - pub fn new(min: u64, max: u64, default: u64) -> Self { - assert!( - min <= default && default <= max, - "invalid block gas limit config: min ({min}) <= default ({default}) <= max ({max})" - ); - Self { min, max, default } - } - - pub fn min(&self) -> u64 { - self.min - } - - pub fn max(&self) -> u64 { - self.max - } - - pub fn default(&self) -> u64 { - self.default - } -} - -/// Provides block gas limit configuration at a given block height. -/// -pub trait BlockGasLimitProvider { - /// Returns the block gas limit config for the given block height. - fn block_gas_limit_config(&self, block_height: u64) -> BlockGasLimitConfig; -} - -impl BlockGasLimitProvider for Arc { - fn block_gas_limit_config(&self, block_height: u64) -> BlockGasLimitConfig { - (**self).block_gas_limit_config(block_height) - } -} - -impl BlockGasLimitProvider for &T { - fn block_gas_limit_config(&self, block_height: u64) -> BlockGasLimitConfig { - (**self).block_gas_limit_config(block_height) - } -} - -/// A bounded parameter with a minimum, default, and maximum value. -/// -/// Used by [`BaseFeeConfig`] to validate on-chain values sourced from ProtocolConfig and -/// substitute the default when the on-chain value is out of the `[min, max]` range. -#[derive(Debug, Clone, Copy, PartialEq, Eq)] -pub struct BoundedParam { - min: T, - default: T, - max: T, -} - -impl BoundedParam { - /// Validates that min <= default <= max. - pub const fn new(min: u64, default: u64, max: u64) -> Self { - assert!( - min <= default && default <= max, - "invalid BoundedParam: must satisfy min <= default <= max" - ); - Self { min, default, max } - } -} - -impl BoundedParam { - /// Returns `on_chain` if it is within `[min, max]`; otherwise returns `default`. - pub fn resolve(&self, on_chain: T) -> T { - if on_chain >= self.min && on_chain <= self.max { - on_chain - } else { - self.default - } - } -} - -/// Resolved base fee calculation parameters (after bounds-checking). -/// -/// All values are in basis points unless noted (e.g. `k_rate = 200` means 2%). -#[derive(Debug, Clone, Copy, PartialEq, Eq)] -pub struct BaseFeeCalcParams { - /// Exponential smoothing factor [0, 100]. 0 = no smoothing, 100 = raw gas used. - pub alpha: u64, - /// Max base fee change rate per block in basis points (200 = 2%). - pub k_rate: u64, - /// Target gas utilisation in basis points (5000 = 50%). - pub inverse_elasticity_multiplier: u64, -} - -/// Complete base fee configuration for a network (ADR-0004). -/// -/// Each calculation parameter field holds its own `[min, default, max]` bounds via -/// [`BoundedParam`]: if the on-chain value falls outside the range for that field, -/// `default` is used instead. -/// -/// `absolute_min_base_fee` and `absolute_max_base_fee` clamp the *output* after -/// both the computation and the ProtocolConfig's own `minBaseFee`/`maxBaseFee` clamp. -/// -/// Use [`BaseFeeConfig::new`] to construct; direct struct literal construction is only -/// available inside this module (fields are private outside `chainspec`). -#[derive(Debug, Clone, Copy, PartialEq, Eq)] -pub struct BaseFeeConfig { - pub alpha: BoundedParam, - pub k_rate: BoundedParam, - pub inverse_elasticity_multiplier: BoundedParam, - /// Absolute floor on the computed base fee output. - pub absolute_min_base_fee: u64, - /// Absolute ceiling on the computed base fee output. - pub absolute_max_base_fee: u64, -} - -impl BaseFeeConfig { - /// Validates that min / max are appropriately ordered. - pub const fn new( - alpha: BoundedParam, - k_rate: BoundedParam, - inverse_elasticity_multiplier: BoundedParam, - absolute_min_base_fee: u64, - absolute_max_base_fee: u64, - ) -> Self { - assert!( - absolute_min_base_fee <= absolute_max_base_fee, - "invalid BaseFeeConfig: absolute_min_base_fee must be <= absolute_max_base_fee" - ); - Self { - alpha, - k_rate, - inverse_elasticity_multiplier, - absolute_min_base_fee, - absolute_max_base_fee, - } - } - - /// Resolves `BaseFeeCalcParams` from an optional on-chain `FeeParams`. - /// - /// If `fee_params` is `None`, returns the defaults for each field. - /// Otherwise validates each field independently and substitutes the default for any - /// field that is out of the `[min, max]` range. - pub fn resolve_calc_params( - &self, - fee_params: Option<&crate::protocol_config::IProtocolConfig::FeeParams>, - ) -> BaseFeeCalcParams { - match fee_params { - None => BaseFeeCalcParams { - alpha: self.alpha.default, - k_rate: self.k_rate.default, - inverse_elasticity_multiplier: self.inverse_elasticity_multiplier.default, - }, - Some(fp) => BaseFeeCalcParams { - alpha: self.alpha.resolve(fp.alpha), - k_rate: self.k_rate.resolve(fp.kRate), - inverse_elasticity_multiplier: self - .inverse_elasticity_multiplier - .resolve(fp.inverseElasticityMultiplier), - }, - } - } - - /// Clamps `base_fee` to `[absolute_min_base_fee, absolute_max_base_fee]`. - pub fn clamp_absolute(&self, base_fee: u64) -> u64 { - base_fee.clamp(self.absolute_min_base_fee, self.absolute_max_base_fee) - } -} - -/// Provides base fee configuration at a given block height. -pub trait BaseFeeConfigProvider { - fn base_fee_config(&self, block_height: u64) -> BaseFeeConfig; -} - -impl BaseFeeConfigProvider for Arc { - fn base_fee_config(&self, block_height: u64) -> BaseFeeConfig { - (**self).base_fee_config(block_height) - } -} - -impl BaseFeeConfigProvider for &T { - fn base_fee_config(&self, block_height: u64) -> BaseFeeConfig { - (**self).base_fee_config(block_height) - } -} - -#[derive(Debug, Clone, PartialEq, Eq)] -pub struct ArcChainSpec { - pub inner: ChainSpec, -} - -impl ArcChainSpec { - pub fn new(inner: ChainSpec) -> Self { - Self { inner } - } - - /// Get the hardfork flags for a given (block height, timestamp). - /// - /// Returns feature flags indicating which Arc hardforks are active at the given - /// head. Both inputs are required because Arc hardfork schedules are - /// network-specific and may use either block heights or timestamps. - pub fn get_hardfork_flags(&self, height: u64, timestamp: u64) -> ArcHardforkFlags { - ArcHardforkFlags::from_chain_hardforks(&self.inner.hardforks, height, timestamp) - } -} - -impl BlockGasLimitProvider for ArcChainSpec { - fn block_gas_limit_config(&self, _block_height: u64) -> BlockGasLimitConfig { - let (min, max) = match self.chain().id() { - MAINNET_CHAIN_ID => (10_000_000, 200_000_000), - TESTNET_CHAIN_ID => (10_000_000, 200_000_000), - _ => (1_000_000, 1_000_000_000), - }; - BlockGasLimitConfig::new(min, max, 30_000_000) - } -} - -const BASE_FEE_CONFIG_MAINNET: BaseFeeConfig = BaseFeeConfig::new( - BoundedParam::new(1, 20, 100), - BoundedParam::new(1, 200, 1_000), - BoundedParam::new(1, 5000, 9_000), - 1, - 20_000_000_000_000, // 20,000 gwei -); - -const BASE_FEE_CONFIG_TESTNET: BaseFeeConfig = BaseFeeConfig::new( - BoundedParam::new(1, 20, 100), - BoundedParam::new(1, 200, 1_000), - BoundedParam::new(1, 5000, 9_000), - 1, - 20_000_000_000_000, // 20,000 gwei -); - -const BASE_FEE_CONFIG_DEFAULT: BaseFeeConfig = BaseFeeConfig::new( - BoundedParam::new(1, 20, 100), - BoundedParam::new(1, 200, 10_000), - BoundedParam::new(1, 5000, 10_000), - 1, - u64::MAX - 1, -); - -impl BaseFeeConfigProvider for ArcChainSpec { - // While the same config is used for all blockheights, it is available to ease future hardfork transitions - fn base_fee_config(&self, _block_height: u64) -> BaseFeeConfig { - match self.chain().id() { - MAINNET_CHAIN_ID => BASE_FEE_CONFIG_MAINNET, - TESTNET_CHAIN_ID => BASE_FEE_CONFIG_TESTNET, - _ => BASE_FEE_CONFIG_DEFAULT, - } - } -} - -/// ERC-7201 namespaced storage slots for ProtocolConfig (proxy at 0x3600..0001). -/// Base: keccak256(abi.encode(uint256(keccak256("arc.storage.ProtocolConfig")) - 1)) & ~0xff -#[cfg(any(feature = "test-utils", test))] -const PROTOCOL_CONFIG_BLOCK_GAS_LIMIT_SLOT: B256 = - b256!("668f09ce856848ead6cb1ddee963f15ef833cea8958030868f867aec84385203"); -/// ERC-1967 implementation slot on the proxy. -#[cfg(any(feature = "test-utils", test))] -const PROXY_IMPLEMENTATION_SLOT: B256 = - b256!("360894a13ba1a3210667c828492db98dca3e2076cc3735a920a3ca505d382bbc"); - -/// Creates a custom localdev chain spec for testing with specific hardfork activations. -/// -/// This starts with base Ethereum forks and adds only the specified Arc hardforks. -/// Each entry pairs a hardfork with the [`ForkCondition`] that activates it — use -/// `ForkCondition::Block(n)` for block-gated forks and `ForkCondition::Timestamp(t)` -/// for timestamp-gated ones. -/// -/// # Example -/// ```ignore -/// use arc_execution_config::chainspec::localdev_with_hardforks; -/// use arc_execution_config::hardforks::ArcHardfork; -/// use reth_chainspec::ForkCondition; -/// -/// // Create a chain spec with Zero3 and Zero4 active at genesis -/// let spec = localdev_with_hardforks(&[ -/// (ArcHardfork::Zero3, ForkCondition::Block(0)), -/// (ArcHardfork::Zero4, ForkCondition::Block(0)), -/// ]); -/// -/// // Test Zero7 activating at a future timestamp -/// let spec = localdev_with_hardforks(&[ -/// (ArcHardfork::Zero3, ForkCondition::Block(0)), -/// (ArcHardfork::Zero7, ForkCondition::Timestamp(1_800_000_000)), -/// ]); -/// ``` -#[cfg(any(feature = "test-utils", test))] -pub fn localdev_with_hardforks(hardforks: &[(ArcHardfork, ForkCondition)]) -> Arc { - use crate::hardforks::BASE_FORKS; - - let genesis: Genesis = - serde_json::from_str(include_str!("../../../assets/localdev/genesis.json")) - .expect("Can't deserialize localdev genesis json"); - - let mut inner = ChainSpec::from_genesis(genesis); - inner.hardforks = BASE_FORKS.clone(); - - for &(hardfork, condition) in hardforks { - // Match to access the constant value for .boxed() — needed for the 'static lifetime. - match hardfork { - ArcHardfork::Zero3 => inner - .hardforks - .insert(ArcHardfork::Zero3.boxed(), condition), - ArcHardfork::Zero4 => inner - .hardforks - .insert(ArcHardfork::Zero4.boxed(), condition), - ArcHardfork::Zero5 => inner - .hardforks - .insert(ArcHardfork::Zero5.boxed(), condition), - ArcHardfork::Zero6 => inner - .hardforks - .insert(ArcHardfork::Zero6.boxed(), condition), - ArcHardfork::Zero7 => inner - .hardforks - .insert(ArcHardfork::Zero7.boxed(), condition), - }; - } - - Arc::new(ArcChainSpec::new(inner)) -} - -/// Creates a localdev chain spec with an address pre-blocklisted in NativeCoinControl. -#[cfg(any(feature = "test-utils", test))] -pub fn localdev_with_storage_override(blocklisted_address: Option
) -> Arc { - let mut genesis: Genesis = - serde_json::from_str(include_str!("../../../assets/localdev/genesis.json")) - .expect("Can't deserialize localdev genesis json"); - - if let Some(blocklisted_address) = blocklisted_address { - const BLOCKLISTED_STATUS: B256 = - b256!("0000000000000000000000000000000000000000000000000000000000000001"); - - let native_coin_control_address = address!("0x1800000000000000000000000000000000000001"); - let native_coin_control_account = genesis - .alloc - .get_mut(&native_coin_control_address) - .expect("LOCAL_DEV genesis missing NativeCoinControl account"); - let native_coin_control_storage = native_coin_control_account - .storage - .get_or_insert_with(Default::default); - native_coin_control_storage.insert( - compute_is_blocklisted_storage_slot(blocklisted_address), - BLOCKLISTED_STATUS, - ); - } - - let mut inner = ChainSpec::from_genesis(genesis); - inner.hardforks = ARC_LOCALDEV_HARDFORKS.clone(); - Arc::new(ArcChainSpec::new(inner)) -} - -/// Creates a localdev chain spec with addresses pre-denylisted in the Denylist contract. -#[cfg(any(feature = "test-utils", test))] -pub fn localdev_with_denylisted_addresses( - denylisted_addresses: impl IntoIterator, -) -> Arc { - use crate::addresses_denylist::{ - compute_denylist_storage_slot, DEFAULT_DENYLIST_ADDRESS, DEFAULT_DENYLIST_ERC7201_BASE_SLOT, - }; - - const DENYLISTED_STATUS: B256 = - b256!("0000000000000000000000000000000000000000000000000000000000000001"); - - let mut genesis: Genesis = - serde_json::from_str(include_str!("../../../assets/localdev/genesis.json")) - .expect("Can't deserialize localdev genesis json"); - - let denylist_account = genesis - .alloc - .get_mut(&DEFAULT_DENYLIST_ADDRESS) - .expect("LOCAL_DEV genesis missing Denylist account"); - let storage = denylist_account - .storage - .get_or_insert_with(Default::default); - for addr in denylisted_addresses { - storage.insert( - compute_denylist_storage_slot(addr, DEFAULT_DENYLIST_ERC7201_BASE_SLOT), - DENYLISTED_STATUS, - ); - } - - let mut inner = ChainSpec::from_genesis(genesis); - inner.hardforks = ARC_LOCALDEV_HARDFORKS.clone(); - Arc::new(ArcChainSpec::new(inner)) -} - -/// Creates a localdev chain spec with a custom blockGasLimit in ProtocolConfig storage. -#[cfg(any(feature = "test-utils", test))] -pub fn localdev_with_block_gas_limit(block_gas_limit: u64) -> Arc { - localdev_with_protocol_config_overrides(&[( - PROTOCOL_CONFIG_BLOCK_GAS_LIMIT_SLOT, - U256::from(block_gas_limit).into(), - )]) -} - -/// Creates a localdev chain spec where ProtocolConfig reverts on any call. -/// Achieved by zeroing the ERC-1967 implementation slot on the proxy. -#[cfg(any(feature = "test-utils", test))] -pub fn localdev_with_protocol_config_reverts() -> Arc { - localdev_with_protocol_config_overrides(&[(PROXY_IMPLEMENTATION_SLOT, B256::ZERO)]) -} - -/// Creates a localdev chain spec with arbitrary storage overrides on the -/// ProtocolConfig proxy account. -#[cfg(any(feature = "test-utils", test))] -fn localdev_with_protocol_config_overrides(overrides: &[(B256, B256)]) -> Arc { - let mut genesis: Genesis = - serde_json::from_str(include_str!("../../../assets/localdev/genesis.json")) - .expect("Can't deserialize localdev genesis json"); - - let protocol_config_account = genesis - .alloc - .get_mut(&crate::protocol_config::PROTOCOL_CONFIG_ADDRESS) - .expect("LOCAL_DEV genesis missing ProtocolConfig account"); - - let storage = protocol_config_account - .storage - .get_or_insert_with(Default::default); - - for &(slot, value) in overrides { - storage.insert(slot, value); - } - - let mut inner = ChainSpec::from_genesis(genesis); - inner.hardforks = ARC_LOCALDEV_HARDFORKS.clone(); - Arc::new(ArcChainSpec::new(inner)) -} - -// localdev chain spec. -pub static LOCAL_DEV: LazyLock> = LazyLock::new(|| { - let genesis: Genesis = - serde_json::from_str(include_str!("../../../assets/localdev/genesis.json")) - .expect("Can't deserialize localdev genesis json"); - let mut inner = ChainSpec::from_genesis(genesis); - inner.hardforks = ARC_LOCALDEV_HARDFORKS.clone(); - ArcChainSpec::new(inner).into() -}); - -pub static DEVNET: LazyLock> = LazyLock::new(|| { - let genesis: Genesis = - serde_json::from_str(include_str!("../../../assets/devnet/genesis.json")) - .expect("Can't deserialize Devnet genesis json"); - let mut inner = ChainSpec::from_genesis(genesis); - inner.hardforks = ARC_DEVNET_HARDFORKS.clone(); - ArcChainSpec::new(inner).into() -}); - -pub static TESTNET: LazyLock> = LazyLock::new(|| { - let genesis: Genesis = - serde_json::from_str(include_str!("../../../assets/testnet/genesis.json")) - .expect("Can't deserialize Testnet genesis json"); - let mut inner = ChainSpec::from_genesis(genesis); - inner.hardforks = ARC_TESTNET_HARDFORKS.clone(); - ArcChainSpec::new(inner).into() -}); - -pub static MAINNET: LazyLock> = LazyLock::new(|| { - let genesis: Genesis = - serde_json::from_str(include_str!("../../../assets/mainnet/genesis.json")) - .expect("Can't deserialize Mainnet genesis json"); - let mut inner = ChainSpec::from_genesis(genesis); - inner.hardforks = ARC_MAINNET_HARDFORKS.clone(); - ArcChainSpec::new(inner).into() -}); - -/// Returns the bundled chainspec for a known Arc chain ID, or `None` if the -/// chain ID is unknown. -pub fn bundled_chainspec_for_chain_id(chain_id: u64) -> Option> { - match chain_id { - LOCALDEV_CHAIN_ID => Some(LOCAL_DEV.clone()), - MAINNET_CHAIN_ID => Some(MAINNET.clone()), - DEVNET_CHAIN_ID => Some(DEVNET.clone()), - TESTNET_CHAIN_ID => Some(TESTNET.clone()), - _ => None, - } -} - -impl From for ArcChainSpec { - fn from(inner: ChainSpec) -> Self { - Self::new(inner) - } -} - -impl From for ArcChainSpec { - fn from(genesis: Genesis) -> Self { - let mut inner = ChainSpec::from_genesis(genesis); - - // For mainnet, devnet, and testnet, we don't read the fork configuration from - // genesis. Patch the hardfork table from the predefined value instead. - // - // Localdev is intentionally NOT hardcoded here so that genesis.json controls - // hardfork activation — the nightly-upgrade test patches genesis.json with jq - // and relies on the node reading those values. The named network "arc-localdev" - // (LOCAL_DEV static) still uses ARC_LOCALDEV_HARDFORKS directly. - match inner.chain().id() { - MAINNET_CHAIN_ID => { - inner.hardforks = ARC_MAINNET_HARDFORKS.clone(); - } - DEVNET_CHAIN_ID => { - inner.hardforks = ARC_DEVNET_HARDFORKS.clone(); - } - TESTNET_CHAIN_ID => { - inner.hardforks = ARC_TESTNET_HARDFORKS.clone(); - } - _ => { - if let Some(extra) = - ArcGenesisInfo::extract_from(&inner.genesis().config.extra_fields) - { - for (hardfork, condition) in extra.get_hardfork_conditions() { - inner.hardforks.insert(hardfork, condition); - } - } - } - } - Self::new(inner) - } -} - -impl EthChainSpec for ArcChainSpec { - type Header = ::BlockHeader; - - fn chain(&self) -> Chain { - self.inner.chain() - } - - // Do not use this function, use `calc_next_block_base_fee` directly instead. - fn base_fee_params_at_timestamp(&self, _timestamp: u64) -> BaseFeeParams { - BaseFeeParams::new( - ARC_BASE_FEE_MAX_CHANGE_DENOMINATOR, - DEFAULT_ELASTICITY_MULTIPLIER as u128, - ) - } - - fn blob_params_at_timestamp(&self, timestamp: u64) -> Option { - self.inner.blob_params_at_timestamp(timestamp) - } - - fn deposit_contract(&self) -> Option<&DepositContract> { - None - } - - fn genesis_hash(&self) -> B256 { - self.inner.genesis_hash() - } - - fn prune_delete_limit(&self) -> usize { - self.inner.prune_delete_limit() - } - - fn display_hardforks(&self) -> Box { - Box::new(self.inner.display_hardforks()) - } - - fn genesis_header(&self) -> &Self::Header { - self.inner.genesis_header() - } - - fn genesis(&self) -> &Genesis { - self.inner.genesis() - } - - fn bootnodes(&self) -> Option> { - self.inner.bootnodes() - } - - fn final_paris_total_difficulty(&self) -> Option { - self.inner.final_paris_total_difficulty() - } - - fn chain_id(&self) -> u64 { - self.chain().id() - } - - fn is_optimism(&self) -> bool { - false - } - - fn is_ethereum(&self) -> bool { - false - } - - fn next_block_base_fee(&self, parent: &Self::Header, _target_timestamp: u64) -> Option { - let child_number = parent.number.saturating_add(1); - let base_fee_config = self.base_fee_config(child_number); - if let Some(base_fee) = decode_base_fee_from_bytes(&parent.extra_data) { - Some(base_fee) - } else { - // Fallback that should never be hit once Zero5 is activated: use field defaults - // from BaseFeeConfig since no ProtocolConfig data is available. - let calc = base_fee_config.resolve_calc_params(None); - let raw = crate::gas_fee::arc_calc_next_block_base_fee( - parent.gas_used, - parent.gas_limit, - parent.base_fee_per_gas.unwrap_or_default(), - calc.k_rate, - calc.inverse_elasticity_multiplier, - ); - Some(base_fee_config.clamp_absolute(raw)) - } - } -} - -impl EthereumHardforks for ArcChainSpec { - fn ethereum_fork_activation(&self, fork: EthereumHardfork) -> ForkCondition { - self.inner.ethereum_fork_activation(fork) - } - - fn is_ethereum_fork_active_at_timestamp(&self, fork: EthereumHardfork, timestamp: u64) -> bool { - self.ethereum_fork_activation(fork) - .active_at_timestamp(timestamp) - } - - fn is_ethereum_fork_active_at_block(&self, fork: EthereumHardfork, block_number: u64) -> bool { - self.ethereum_fork_activation(fork) - .active_at_block(block_number) - } - - fn is_homestead_active_at_block(&self, block_number: u64) -> bool { - self.is_ethereum_fork_active_at_block(EthereumHardfork::Homestead, block_number) - } - - fn is_tangerine_whistle_active_at_block(&self, block_number: u64) -> bool { - self.is_ethereum_fork_active_at_block(EthereumHardfork::Tangerine, block_number) - } - - fn is_spurious_dragon_active_at_block(&self, block_number: u64) -> bool { - self.is_ethereum_fork_active_at_block(EthereumHardfork::SpuriousDragon, block_number) - } - - fn is_byzantium_active_at_block(&self, block_number: u64) -> bool { - self.is_ethereum_fork_active_at_block(EthereumHardfork::Byzantium, block_number) - } - - fn is_constantinople_active_at_block(&self, block_number: u64) -> bool { - self.is_ethereum_fork_active_at_block(EthereumHardfork::Constantinople, block_number) - } - - fn is_petersburg_active_at_block(&self, block_number: u64) -> bool { - self.is_ethereum_fork_active_at_block(EthereumHardfork::Petersburg, block_number) - } - - fn is_istanbul_active_at_block(&self, block_number: u64) -> bool { - self.is_ethereum_fork_active_at_block(EthereumHardfork::Istanbul, block_number) - } - - fn is_berlin_active_at_block(&self, block_number: u64) -> bool { - self.is_ethereum_fork_active_at_block(EthereumHardfork::Berlin, block_number) - } - - fn is_london_active_at_block(&self, block_number: u64) -> bool { - self.is_ethereum_fork_active_at_block(EthereumHardfork::London, block_number) - } - - fn is_paris_active_at_block(&self, block_number: u64) -> bool { - self.is_ethereum_fork_active_at_block(EthereumHardfork::Paris, block_number) - } - - fn is_shanghai_active_at_timestamp(&self, timestamp: u64) -> bool { - self.is_ethereum_fork_active_at_timestamp(EthereumHardfork::Shanghai, timestamp) - } - - fn is_cancun_active_at_timestamp(&self, timestamp: u64) -> bool { - self.is_ethereum_fork_active_at_timestamp(EthereumHardfork::Cancun, timestamp) - } - - fn is_prague_active_at_timestamp(&self, timestamp: u64) -> bool { - self.is_ethereum_fork_active_at_timestamp(EthereumHardfork::Prague, timestamp) - } - - fn is_osaka_active_at_timestamp(&self, timestamp: u64) -> bool { - self.is_ethereum_fork_active_at_timestamp(EthereumHardfork::Osaka, timestamp) - } - - fn is_amsterdam_active_at_timestamp(&self, timestamp: u64) -> bool { - self.is_ethereum_fork_active_at_timestamp(EthereumHardfork::Amsterdam, timestamp) - } - - fn is_bpo1_active_at_timestamp(&self, timestamp: u64) -> bool { - self.is_ethereum_fork_active_at_timestamp(EthereumHardfork::Bpo1, timestamp) - } - - fn is_bpo2_active_at_timestamp(&self, timestamp: u64) -> bool { - self.is_ethereum_fork_active_at_timestamp(EthereumHardfork::Bpo2, timestamp) - } - - fn is_bpo3_active_at_timestamp(&self, timestamp: u64) -> bool { - self.is_ethereum_fork_active_at_timestamp(EthereumHardfork::Bpo3, timestamp) - } - - fn is_bpo4_active_at_timestamp(&self, timestamp: u64) -> bool { - self.is_ethereum_fork_active_at_timestamp(EthereumHardfork::Bpo4, timestamp) - } - - fn is_bpo5_active_at_timestamp(&self, timestamp: u64) -> bool { - self.is_ethereum_fork_active_at_timestamp(EthereumHardfork::Bpo5, timestamp) - } -} - -impl Hardforks for ArcChainSpec { - fn fork(&self, fork: H) -> ForkCondition { - self.inner.fork(fork) - } - - fn forks_iter(&self) -> impl Iterator { - self.inner.forks_iter() - } - - fn fork_id(&self, head: &Head) -> ForkId { - self.inner.fork_id(head) - } - - fn latest_fork_id(&self) -> ForkId { - self.inner.latest_fork_id() - } - - fn fork_filter(&self, head: Head) -> ForkFilter { - self.inner.fork_filter(head) - } - - fn is_fork_active_at_timestamp(&self, fork: H, timestamp: u64) -> bool { - self.fork(fork).active_at_timestamp(timestamp) - } - - fn is_fork_active_at_block(&self, fork: H, block_number: u64) -> bool { - self.fork(fork).active_at_block(block_number) - } -} - -impl EthExecutorSpec for ArcChainSpec { - fn deposit_contract_address(&self) -> Option
{ - None - } -} - -// Test Arc LocalDev chain spec parsing -#[cfg(test)] -mod tests { - use super::*; - - use crate::chain_ids::{ - DEVNET_CHAIN_ID, LOCALDEV_CHAIN_ID, MAINNET_CHAIN_ID, TESTNET_CHAIN_ID, - }; - use crate::hardforks::{ - ARC_OSAKA_HARDFORK_TIMESTAMP_ACTIVATION_DEVNET, - ARC_OSAKA_HARDFORK_TIMESTAMP_ACTIVATION_TESTNET, - ARC_ZERO3_HARDFORK_BLOCK_ACTIVATION_DEVNET, ARC_ZERO3_HARDFORK_BLOCK_ACTIVATION_TESTNET, - ARC_ZERO4_HARDFORK_BLOCK_ACTIVATION_DEVNET, ARC_ZERO4_HARDFORK_BLOCK_ACTIVATION_TESTNET, - ARC_ZERO5_HARDFORK_BLOCK_ACTIVATION_DEVNET, - ARC_ZERO5_HARDFORK_TIMESTAMP_ACTIVATION_TESTNET, - ARC_ZERO6_HARDFORK_BLOCK_ACTIVATION_DEVNET, - ARC_ZERO6_HARDFORK_TIMESTAMP_ACTIVATION_TESTNET, BASE_FORKS, - }; - - fn assert_arc_chainspec_evm_hardforks(spec: &ArcChainSpec) { - // ---- Block-gated forks (chronological) ---- - // No helper function for Frontier - assert!(spec.is_homestead_active_at_block(0)); - assert!(spec.is_tangerine_whistle_active_at_block(0)); - assert!(spec.is_spurious_dragon_active_at_block(0)); - assert!(spec.is_byzantium_active_at_block(0)); - assert!(spec.is_constantinople_active_at_block(0)); - assert!(spec.is_petersburg_active_at_block(0)); - assert!(spec.is_istanbul_active_at_block(0)); - - assert!(spec.is_ethereum_fork_active_at_block(EthereumHardfork::MuirGlacier, 0)); - assert!(spec.is_berlin_active_at_block(0)); - assert!(spec.is_london_active_at_block(0)); - assert!(spec.is_ethereum_fork_active_at_block(EthereumHardfork::ArrowGlacier, 0)); - assert!(spec.is_ethereum_fork_active_at_block(EthereumHardfork::GrayGlacier, 0)); - assert!(spec.is_paris_active_at_block(0)); - - // ---- Timestamp-gated forks (chronological) ---- - assert!(spec.is_shanghai_active_at_timestamp(0)); - assert!(spec.is_cancun_active_at_timestamp(0)); - assert!(spec.is_prague_active_at_timestamp(0)); - - // Sanity - assert!(!spec.is_ethereum()); - assert!(!spec.is_optimism()); - - // Forks beyond osaka - assert!(!spec.is_amsterdam_active_at_timestamp(0)); - assert!(!spec.is_bpo1_active_at_timestamp(0)); - assert!(!spec.is_bpo2_active_at_timestamp(0)); - assert!(!spec.is_bpo3_active_at_timestamp(0)); - assert!(!spec.is_bpo4_active_at_timestamp(0)); - assert!(!spec.is_bpo5_active_at_timestamp(0)); - - // Verify each fork is supported - let supported_hardforks = [ - EthereumHardfork::Frontier, - EthereumHardfork::Homestead, - EthereumHardfork::Tangerine, - EthereumHardfork::SpuriousDragon, - EthereumHardfork::Byzantium, - EthereumHardfork::Constantinople, - EthereumHardfork::Petersburg, - EthereumHardfork::Istanbul, - EthereumHardfork::MuirGlacier, - EthereumHardfork::Berlin, - EthereumHardfork::London, - EthereumHardfork::ArrowGlacier, - EthereumHardfork::GrayGlacier, - EthereumHardfork::Paris, - EthereumHardfork::Shanghai, - EthereumHardfork::Cancun, - EthereumHardfork::Prague, - ]; - - for fork in supported_hardforks { - let cond = spec.ethereum_fork_activation(fork); - if cond.active_at_block(0) { - assert!( - cond.active_at_block(0), - "Fork {:?} not block-active at 0", - fork - ); - } else if cond.active_at_timestamp(0) { - assert!( - cond.active_at_timestamp(0), - "Fork {:?} not ts-active at 0", - fork - ); - } else { - panic!( - "Fork {:?} has neither block nor timestamp active at 0 (cond: {:?})", - fork, cond - ); - } - } - - // Empty deposit contract - assert!(spec.deposit_contract().is_none()); - assert!(spec.deposit_contract_address().is_none()); - - // BaseFeeParams - let base_fee_params = spec.base_fee_params_at_timestamp(0); - assert_eq!( - base_fee_params.max_change_denominator, - ARC_BASE_FEE_MAX_CHANGE_DENOMINATOR - ); - assert_eq!( - base_fee_params.elasticity_multiplier, - DEFAULT_ELASTICITY_MULTIPLIER as u128 - ); - - // Bootnodes - assert_eq!(spec.bootnodes(), spec.inner.bootnodes()); - - // Blob params - assert_eq!( - spec.blob_params_at_timestamp(0), - spec.inner.blob_params_at_timestamp(0) - ); - - // Genesis - assert_eq!(spec.genesis_hash(), spec.inner.genesis_hash()); - assert_eq!(spec.genesis(), spec.inner.genesis()); - - // Misc - assert!(spec.final_paris_total_difficulty().is_none()); - } - - #[test] - fn test_load_genesis_localdev() { - let spec = ArcChainSpecParser::parse("../../assets/localdev/genesis.json") - .expect("Failed to parse arc-localdev"); - assert_eq!(spec.chain().id(), LOCALDEV_CHAIN_ID); - assert_arc_chainspec_evm_hardforks(&spec); - assert_eq!(spec.forks_iter().count(), 23); - assert!(spec.is_osaka_active_at_timestamp(0)); - - // verify zero3 hardfork block - assert!(!spec.is_fork_active_at_timestamp(ArcHardfork::Zero3, 1762732800 - 1)); - assert!( - spec.is_fork_active_at_block(ArcHardfork::Zero3, 0), - "Zero3 should be active at block 0 in hardfork.rs, and load by chainspec" - ); - // verify zero4 hardfork block - assert!( - spec.is_fork_active_at_block(ArcHardfork::Zero4, 0), - "Zero4 should be active at block 0 in hardfork.rs, and load by chainspec" - ); - // verify zero5 hardfork block - assert!( - spec.is_fork_active_at_block(ArcHardfork::Zero5, 0), - "Zero5 should be active at block 0 in hardfork.rs, and load by chainspec" - ); - // verify zero6 hardfork block - assert!( - spec.is_fork_active_at_block(ArcHardfork::Zero6, 0), - "Zero6 should be active at block 0 in hardfork.rs, and load by chainspec" - ); - // Zero7 activates by timestamp (Arc convention from Zero7 onward). - assert!( - spec.is_fork_active_at_timestamp(ArcHardfork::Zero7, 0), - "Zero7 should be active at timestamp 0 in hardfork.rs, and load by chainspec" - ); - let flags = spec.get_hardfork_flags(0, 0); - assert!(flags.is_active(ArcHardfork::Zero3)); - assert!(flags.is_active(ArcHardfork::Zero4)); - assert!(flags.is_active(ArcHardfork::Zero5)); - assert!(flags.is_active(ArcHardfork::Zero6)); - assert!(flags.is_active(ArcHardfork::Zero7)); - } - - #[test] - fn test_arc_localdev_chainspec() { - let spec = ArcChainSpecParser::parse("arc-localdev").expect("Failed to parse arc-localdev"); - assert_eq!(spec.chain().id(), LOCALDEV_CHAIN_ID); - assert_arc_chainspec_evm_hardforks(&spec); - assert!(spec.is_osaka_active_at_timestamp(0)); - assert_eq!(spec.forks_iter().count(), 23); - - // verify zero3 hardfork block - assert!(!spec.is_fork_active_at_timestamp(ArcHardfork::Zero3, 1762732800)); - assert!(spec.is_fork_active_at_block(ArcHardfork::Zero3, 0)); - // verify zero4 hardfork block - assert!(!spec.is_fork_active_at_timestamp(ArcHardfork::Zero4, 1762732800)); - assert!(spec.is_fork_active_at_block(ArcHardfork::Zero4, 0)); - // verify zero5 hardfork block - assert!(!spec.is_fork_active_at_timestamp(ArcHardfork::Zero5, 1762732800)); - assert!(spec.is_fork_active_at_block(ArcHardfork::Zero5, 0)); - // verify zero6 hardfork block - assert!(!spec.is_fork_active_at_timestamp(ArcHardfork::Zero6, 1762732800)); - assert!(spec.is_fork_active_at_block(ArcHardfork::Zero6, 0)); - // Zero7 activates by timestamp (Arc convention from Zero7 onward). - assert!(spec.is_fork_active_at_timestamp(ArcHardfork::Zero7, 0)); - assert!(!spec.is_fork_active_at_block(ArcHardfork::Zero7, 0)); - let flags = spec.get_hardfork_flags(0, 0); - assert!(flags.is_active(ArcHardfork::Zero3)); - assert!(flags.is_active(ArcHardfork::Zero4)); - assert!(flags.is_active(ArcHardfork::Zero5)); - assert!(flags.is_active(ArcHardfork::Zero6)); - assert!(flags.is_active(ArcHardfork::Zero7)); - assert_eq!( - spec.display_hardforks().to_string(), - r#"Pre-merge hard forks (block based): -- Frontier @0 -- Homestead @0 -- Tangerine @0 -- SpuriousDragon @0 -- Byzantium @0 -- Constantinople @0 -- Petersburg @0 -- Istanbul @0 -- MuirGlacier @0 -- Berlin @0 -- London @0 -- ArrowGlacier @0 -- GrayGlacier @0 -- Zero3 @0 -- Zero4 @0 -- Zero5 @0 -- Zero6 @0 -Merge hard forks: -- Paris @0 (network is known to be merged) -Post-merge hard forks (timestamp based): -- Shanghai @0 blob: (target: 6, max: 9, fraction: 5007716) -- Cancun @0 blob: (target: 6, max: 9, fraction: 5007716) -- Prague @0 blob: (target: 6, max: 9, fraction: 5007716) -- Osaka @0 blob: (target: 6, max: 9, fraction: 5007716) -- Zero7 @0 blob: (target: 6, max: 9, fraction: 5007716)"# - ); - } - - #[test] - fn test_arc_mainnet_chainspec() { - let spec = ArcChainSpecParser::parse("arc-mainnet").expect("Failed to parse arc-mainnet"); - assert_eq!(spec.chain().id(), MAINNET_CHAIN_ID); - - // Pin the genesis hash to catch any unintended drift in - // assets/mainnet/genesis.json. Update only when a deliberate respin - // happens (e.g. revised admin set, additional prefund, hardfork shift). - assert_eq!( - spec.genesis_hash().to_string(), - "0x09944e07412986bb417fd0006c89ffb71ee523d68ce2017ec2dabc944c42edad", - "the genesis hash of assets/mainnet/genesis.json changed unexpectedly" - ); - - assert_arc_chainspec_evm_hardforks(&spec); - assert!(spec.is_osaka_active_at_timestamp(0)); - assert_eq!(spec.forks_iter().count(), 22); - - // Mainnet launches at Zero6: Zero3..Zero6 active at block 0; Zero7 is not scheduled on mainnet. - let flags = spec.get_hardfork_flags(0, 0); - assert!(flags.is_active(ArcHardfork::Zero3)); - assert!(flags.is_active(ArcHardfork::Zero4)); - assert!(flags.is_active(ArcHardfork::Zero5)); - assert!(flags.is_active(ArcHardfork::Zero6)); - assert!(!flags.is_active(ArcHardfork::Zero7)); - - assert_eq!( - spec.display_hardforks().to_string(), - r#"Pre-merge hard forks (block based): -- Frontier @0 -- Homestead @0 -- Tangerine @0 -- SpuriousDragon @0 -- Byzantium @0 -- Constantinople @0 -- Petersburg @0 -- Istanbul @0 -- MuirGlacier @0 -- Berlin @0 -- London @0 -- ArrowGlacier @0 -- GrayGlacier @0 -- Zero3 @0 -- Zero4 @0 -- Zero5 @0 -- Zero6 @0 -Merge hard forks: -- Paris @0 (network is known to be merged) -Post-merge hard forks (timestamp based): -- Shanghai @0 blob: (target: 6, max: 9, fraction: 5007716) -- Cancun @0 blob: (target: 6, max: 9, fraction: 5007716) -- Prague @0 blob: (target: 6, max: 9, fraction: 5007716) -- Osaka @0 blob: (target: 6, max: 9, fraction: 5007716)"# - ); - } - - #[test] - fn test_bundled_chainspec_for_chain_id() { - // Round-trip: looking up a chain ID must return the matching spec — - // guards against a regression like the helper returning Some(LOCAL_DEV) - // for DEVNET_CHAIN_ID. - assert_eq!( - bundled_chainspec_for_chain_id(LOCALDEV_CHAIN_ID) - .expect("localdev bundled") - .chain() - .id(), - LOCALDEV_CHAIN_ID - ); - assert_eq!( - bundled_chainspec_for_chain_id(DEVNET_CHAIN_ID) - .expect("devnet bundled") - .chain() - .id(), - DEVNET_CHAIN_ID - ); - assert_eq!( - bundled_chainspec_for_chain_id(TESTNET_CHAIN_ID) - .expect("testnet bundled") - .chain() - .id(), - TESTNET_CHAIN_ID - ); - assert!(bundled_chainspec_for_chain_id(999_999).is_none()); - - assert_eq!( - bundled_chainspec_for_chain_id(MAINNET_CHAIN_ID) - .expect("mainnet bundled") - .chain() - .id(), - MAINNET_CHAIN_ID - ); - } - - /// Expected activations are pinned here. - #[test] - fn test_mainnet_chainspec_paths_agree() { - use alloy_genesis::Genesis; - - let from_parser = ArcChainSpecParser::parse("arc-mainnet").expect("named parser path"); - let from_helper = bundled_chainspec_for_chain_id(MAINNET_CHAIN_ID).expect("helper path"); - let from_genesis = ArcChainSpec::from( - serde_json::from_str::(&format!( - r#"{{ "config": {{ "chainId": {} }}, "alloc": {{}} }}"#, - MAINNET_CHAIN_ID - )) - .expect("synthetic mainnet genesis parses"), - ); - - assert!( - Arc::ptr_eq(&from_parser, &from_helper), - "parser and helper must return the same MAINNET Arc" - ); - - let expected_active: &[(ArcHardfork, bool)] = &[ - (ArcHardfork::Zero3, true), - (ArcHardfork::Zero4, true), - (ArcHardfork::Zero5, true), - (ArcHardfork::Zero6, true), - (ArcHardfork::Zero7, false), // deferred — not active at launch - ]; - let paths: [(&str, &ArcChainSpec); 3] = [ - ("parser", &from_parser), - ("helper", &from_helper), - ("From", &from_genesis), - ]; - for (label, spec) in paths { - for &(fork, want) in expected_active { - assert_eq!( - spec.get_hardfork_flags(0, 0).is_active(fork), - want, - "{label}: {fork:?} active={want}" - ); - } - assert!( - spec.is_osaka_active_at_timestamp(0), - "{label}: Osaka must be active at timestamp 0" - ); - } - } - - #[test] - fn test_arc_devnet_chainspec() { - let spec = ArcChainSpecParser::parse("arc-devnet").expect("Failed to parse arc-devnet"); - assert_eq!(spec.chain().id(), DEVNET_CHAIN_ID); - - // Verify the genesis hash for devnet. The hash may changed when we reset the devnet. - // Otherwise, the genesis hash should be the same. - assert_eq!( - spec.genesis_hash().to_string(), - "0x41c417868fee948f58602b01a84ce0ddb5ffe2184f7e9ab43b9c8d7e5eb47067", - "the genesis hash of assets/devnet/genesis.json changed unexpectedly" - ); - assert_eq!(spec.forks_iter().count(), 23); - assert_arc_chainspec_evm_hardforks(&spec); - assert!(!spec.is_osaka_active_at_timestamp(0)); - assert!(spec.is_osaka_active_at_timestamp(ARC_OSAKA_HARDFORK_TIMESTAMP_ACTIVATION_DEVNET)); - - let flags_before = - spec.get_hardfork_flags(ARC_ZERO3_HARDFORK_BLOCK_ACTIVATION_DEVNET - 1, 0); - assert!(!flags_before.is_active(ArcHardfork::Zero3)); - assert!(!flags_before.is_active(ArcHardfork::Zero4)); - - let flags_at = spec.get_hardfork_flags(ARC_ZERO3_HARDFORK_BLOCK_ACTIVATION_DEVNET, 0); - assert!(flags_at.is_active(ArcHardfork::Zero3)); - assert!(!flags_at.is_active(ArcHardfork::Zero4)); - - let flags_before_zero4 = - spec.get_hardfork_flags(ARC_ZERO4_HARDFORK_BLOCK_ACTIVATION_DEVNET - 1, 0); - assert!(flags_before_zero4.is_active(ArcHardfork::Zero3)); - assert!(!flags_before_zero4.is_active(ArcHardfork::Zero4)); - - let flags_at_zero4 = spec.get_hardfork_flags(ARC_ZERO4_HARDFORK_BLOCK_ACTIVATION_DEVNET, 0); - assert!(flags_at_zero4.is_active(ArcHardfork::Zero3)); - assert!(flags_at_zero4.is_active(ArcHardfork::Zero4)); - assert!(!flags_at_zero4.is_active(ArcHardfork::Zero5)); - - let flags_before_zero5 = - spec.get_hardfork_flags(ARC_ZERO5_HARDFORK_BLOCK_ACTIVATION_DEVNET - 1, 0); - assert!(flags_before_zero5.is_active(ArcHardfork::Zero3)); - assert!(flags_before_zero5.is_active(ArcHardfork::Zero4)); - assert!(!flags_before_zero5.is_active(ArcHardfork::Zero5)); - - let flags_at_zero5 = spec.get_hardfork_flags(ARC_ZERO5_HARDFORK_BLOCK_ACTIVATION_DEVNET, 0); - assert!(flags_at_zero5.is_active(ArcHardfork::Zero3)); - assert!(flags_at_zero5.is_active(ArcHardfork::Zero4)); - assert!(flags_at_zero5.is_active(ArcHardfork::Zero5)); - assert!(!flags_at_zero5.is_active(ArcHardfork::Zero6)); - - let flags_before_zero6 = - spec.get_hardfork_flags(ARC_ZERO6_HARDFORK_BLOCK_ACTIVATION_DEVNET - 1, 0); - assert!(flags_before_zero6.is_active(ArcHardfork::Zero3)); - assert!(flags_before_zero6.is_active(ArcHardfork::Zero4)); - assert!(flags_before_zero6.is_active(ArcHardfork::Zero5)); - assert!(!flags_before_zero6.is_active(ArcHardfork::Zero6)); - - let flags_at_zero6 = spec.get_hardfork_flags(ARC_ZERO6_HARDFORK_BLOCK_ACTIVATION_DEVNET, 0); - assert!(flags_at_zero6.is_active(ArcHardfork::Zero3)); - assert!(flags_at_zero6.is_active(ArcHardfork::Zero4)); - assert!(flags_at_zero6.is_active(ArcHardfork::Zero5)); - assert!(flags_at_zero6.is_active(ArcHardfork::Zero6)); - - assert_eq!( - spec.display_hardforks().to_string(), - r#"Pre-merge hard forks (block based): -- Frontier @0 -- Homestead @0 -- Tangerine @0 -- SpuriousDragon @0 -- Byzantium @0 -- Constantinople @0 -- Petersburg @0 -- Istanbul @0 -- MuirGlacier @0 -- Berlin @0 -- London @0 -- ArrowGlacier @0 -- GrayGlacier @0 -- Zero3 @7437594 -- Zero4 @19491165 -- Zero5 @32371192 -- Zero6 @40033853 -Merge hard forks: -- Paris @0 (network is known to be merged) -Post-merge hard forks (timestamp based): -- Shanghai @0 blob: (target: 6, max: 9, fraction: 5007716) -- Cancun @0 blob: (target: 6, max: 9, fraction: 5007716) -- Prague @0 blob: (target: 6, max: 9, fraction: 5007716) -- Osaka @1775483400 blob: (target: 6, max: 9, fraction: 5007716) -- Zero7 @1780495200 blob: (target: 6, max: 9, fraction: 5007716)"# - ); - assert_eq!( - spec.fork(ArcHardfork::Zero3), - ForkCondition::Block(ARC_ZERO3_HARDFORK_BLOCK_ACTIVATION_DEVNET) - ); - assert_eq!( - spec.fork(ArcHardfork::Zero4), - ForkCondition::Block(ARC_ZERO4_HARDFORK_BLOCK_ACTIVATION_DEVNET) - ); - assert_eq!( - spec.fork(ArcHardfork::Zero5), - ForkCondition::Block(ARC_ZERO5_HARDFORK_BLOCK_ACTIVATION_DEVNET) - ); - assert_eq!( - spec.fork(ArcHardfork::Zero6), - ForkCondition::Block(ARC_ZERO6_HARDFORK_BLOCK_ACTIVATION_DEVNET) - ); - } - - #[test] - fn test_arc_testnet_chainspec() { - let spec = ArcChainSpecParser::parse("arc-testnet").expect("Failed to parse arc-testnet"); - assert_eq!(spec.chain().id(), TESTNET_CHAIN_ID); - - // Verify the genesis hash for testnet. The genesis hash should be the same. - assert_eq!( - spec.genesis_hash().to_string(), - "0xe20e653af4441e8c6088e172b129d56420139824400477287b46e7101ae2bb1f", - "the genesis hash of assets/testnet/genesis.json changed unexpectedly" - ); - assert_arc_chainspec_evm_hardforks(&spec); - assert!(!spec.is_osaka_active_at_timestamp(0)); - assert!(spec.is_osaka_active_at_timestamp(ARC_OSAKA_HARDFORK_TIMESTAMP_ACTIVATION_TESTNET)); - assert_eq!(spec.forks_iter().count(), 23); - - // Zero3 - let flags_before_zero3 = - spec.get_hardfork_flags(ARC_ZERO3_HARDFORK_BLOCK_ACTIVATION_TESTNET - 1, 0); - assert!(!flags_before_zero3.is_active(ArcHardfork::Zero3)); - assert!(!flags_before_zero3.is_active(ArcHardfork::Zero4)); - - let flags_at_zero3 = - spec.get_hardfork_flags(ARC_ZERO3_HARDFORK_BLOCK_ACTIVATION_TESTNET, 0); - assert!(flags_at_zero3.is_active(ArcHardfork::Zero3)); - assert!(!flags_at_zero3.is_active(ArcHardfork::Zero4)); - assert_eq!( - spec.fork(ArcHardfork::Zero3), - ForkCondition::Block(ARC_ZERO3_HARDFORK_BLOCK_ACTIVATION_TESTNET) - ); - - // Zero4 - let flags_before_zero4 = - spec.get_hardfork_flags(ARC_ZERO4_HARDFORK_BLOCK_ACTIVATION_TESTNET - 1, 0); - assert!(!flags_before_zero4.is_active(ArcHardfork::Zero4)); - - let flags_at_zero4 = - spec.get_hardfork_flags(ARC_ZERO4_HARDFORK_BLOCK_ACTIVATION_TESTNET, 0); - assert!(flags_at_zero4.is_active(ArcHardfork::Zero4)); - assert_eq!( - spec.fork(ArcHardfork::Zero4), - ForkCondition::Block(ARC_ZERO4_HARDFORK_BLOCK_ACTIVATION_TESTNET) - ); - - // Zero5 — activates by timestamp on testnet. Use a block past Zero4's activation - // so Zero4 still reads as active in the snapshot. - let post_zero4_block = ARC_ZERO4_HARDFORK_BLOCK_ACTIVATION_TESTNET + 1; - let flags_before_zero5 = spec.get_hardfork_flags( - post_zero4_block, - ARC_ZERO5_HARDFORK_TIMESTAMP_ACTIVATION_TESTNET - 1, - ); - assert!(flags_before_zero5.is_active(ArcHardfork::Zero4)); - assert!(!flags_before_zero5.is_active(ArcHardfork::Zero5)); - - let flags_at_zero5 = spec.get_hardfork_flags( - post_zero4_block, - ARC_ZERO5_HARDFORK_TIMESTAMP_ACTIVATION_TESTNET, - ); - assert!(flags_at_zero5.is_active(ArcHardfork::Zero4)); - assert!(flags_at_zero5.is_active(ArcHardfork::Zero5)); - assert_eq!( - spec.fork(ArcHardfork::Zero5), - ForkCondition::Timestamp(ARC_ZERO5_HARDFORK_TIMESTAMP_ACTIVATION_TESTNET) - ); - - // Zero6 — activates by timestamp on testnet (same timestamp as Zero5 by current schedule). - let flags_before_zero6 = spec.get_hardfork_flags( - post_zero4_block, - ARC_ZERO6_HARDFORK_TIMESTAMP_ACTIVATION_TESTNET - 1, - ); - assert!(!flags_before_zero6.is_active(ArcHardfork::Zero6)); - - let flags_at_zero6 = spec.get_hardfork_flags( - post_zero4_block, - ARC_ZERO6_HARDFORK_TIMESTAMP_ACTIVATION_TESTNET, - ); - assert!(flags_at_zero6.is_active(ArcHardfork::Zero5)); - assert!(flags_at_zero6.is_active(ArcHardfork::Zero6)); - assert_eq!( - spec.fork(ArcHardfork::Zero6), - ForkCondition::Timestamp(ARC_ZERO6_HARDFORK_TIMESTAMP_ACTIVATION_TESTNET) - ); - - assert_eq!( - spec.fork(EthereumHardfork::Osaka), - ForkCondition::Timestamp(ARC_OSAKA_HARDFORK_TIMESTAMP_ACTIVATION_TESTNET) - ); - - assert_eq!( - spec.display_hardforks().to_string(), - r#"Pre-merge hard forks (block based): -- Frontier @0 -- Homestead @0 -- Tangerine @0 -- SpuriousDragon @0 -- Byzantium @0 -- Constantinople @0 -- Petersburg @0 -- Istanbul @0 -- MuirGlacier @0 -- Berlin @0 -- London @0 -- ArrowGlacier @0 -- GrayGlacier @0 -- Zero3 @11172019 -- Zero4 @26148086 -Merge hard forks: -- Paris @0 (network is known to be merged) -Post-merge hard forks (timestamp based): -- Shanghai @0 blob: (target: 6, max: 9, fraction: 5007716) -- Cancun @0 blob: (target: 6, max: 9, fraction: 5007716) -- Prague @0 blob: (target: 6, max: 9, fraction: 5007716) -- Osaka @1779890400 blob: (target: 6, max: 9, fraction: 5007716) -- Zero5 @1779894517 blob: (target: 6, max: 9, fraction: 5007716) -- Zero6 @1779894517 blob: (target: 6, max: 9, fraction: 5007716) -- Zero7 @1781791200 blob: (target: 6, max: 9, fraction: 5007716)"# - ); - } - - #[test] - fn test_gas_limit_config_localdev() { - let spec = ArcChainSpecParser::parse("arc-localdev").expect("Failed to parse arc-localdev"); - let config = spec.block_gas_limit_config(0); - assert_eq!( - config, - BlockGasLimitConfig::new(1_000_000, 1_000_000_000, 30_000_000) - ); - } - - #[test] - fn test_gas_limit_config_testnet() { - let spec = ArcChainSpecParser::parse("arc-testnet").expect("Failed to parse arc-testnet"); - let config = spec.block_gas_limit_config(0); - assert_eq!( - config, - BlockGasLimitConfig::new(10_000_000, 200_000_000, 30_000_000) - ); - } - - #[test] - fn test_gas_limit_config_devnet() { - let spec = ArcChainSpecParser::parse("arc-devnet").expect("Failed to parse arc-devnet"); - let config = spec.block_gas_limit_config(0); - assert_eq!( - config, - BlockGasLimitConfig::new(1_000_000, 1_000_000_000, 30_000_000) - ); - } - - #[test] - fn test_gas_limit_config_mainnet() { - // Mainnet has no parseable chainspec name (genesis.json is gitignored), so build - // a synthetic spec by cloning localdev and overriding the chain id. - let mut spec = - (*ArcChainSpecParser::parse("arc-localdev").expect("localdev parses")).clone(); - spec.inner.chain = Chain::from_id(MAINNET_CHAIN_ID); - let config = spec.block_gas_limit_config(0); - assert_eq!( - config, - BlockGasLimitConfig::new(10_000_000, 200_000_000, 30_000_000) - ); - } - - #[test] - fn test_base_fee_config_mainnet() { - let mut spec = - (*ArcChainSpecParser::parse("arc-localdev").expect("localdev parses")).clone(); - spec.inner.chain = Chain::from_id(MAINNET_CHAIN_ID); - let cfg = spec.base_fee_config(0); - - assert_eq!(cfg.absolute_min_base_fee, 1); - assert_eq!(cfg.absolute_max_base_fee, 20_000_000_000_000); // 20,000 gwei - assert_eq!(cfg.alpha, BoundedParam::new(1, 20, 100)); - assert_eq!(cfg.k_rate, BoundedParam::new(1, 200, 1_000)); - assert_eq!( - cfg.inverse_elasticity_multiplier, - BoundedParam::new(1, 5000, 9_000) - ); - } - - /// Exercises the named-chain arms of the match (the - /// alternative path to the `arc-mainnet` / `arc-devnet` / `arc-testnet` - /// parser, used when someone passes `--chain `). - #[test] - fn test_from_genesis_named_chain_ids_apply_predefined_hardforks() { - use alloy_genesis::Genesis; - fn parse_with_chain_id(chain_id: u64) -> ArcChainSpec { - let s = format!( - r#"{{ "config": {{ "chainId": {} }}, "alloc": {{}} }}"#, - chain_id - ); - let genesis: Genesis = serde_json::from_str(&s).expect("parse genesis"); - ArcChainSpec::from(genesis) - } - - // Mainnet - let spec = parse_with_chain_id(MAINNET_CHAIN_ID); - let flags = spec.get_hardfork_flags(0, 0); - assert!(flags.is_active(ArcHardfork::Zero3)); - assert!(flags.is_active(ArcHardfork::Zero4)); - assert!(flags.is_active(ArcHardfork::Zero5)); - assert!(flags.is_active(ArcHardfork::Zero6)); - assert!(!flags.is_active(ArcHardfork::Zero7)); - assert!(spec.is_osaka_active_at_timestamp(0)); - - // Devnet - let spec = parse_with_chain_id(DEVNET_CHAIN_ID); - assert!(spec - .get_hardfork_flags(ARC_ZERO3_HARDFORK_BLOCK_ACTIVATION_DEVNET, 0) - .is_active(ArcHardfork::Zero3)); - assert!(!spec.get_hardfork_flags(0, 0).is_active(ArcHardfork::Zero3)); - - // Testnet - let spec = parse_with_chain_id(TESTNET_CHAIN_ID); - assert!(spec - .get_hardfork_flags(ARC_ZERO3_HARDFORK_BLOCK_ACTIVATION_TESTNET, 0) - .is_active(ArcHardfork::Zero3)); - assert!(!spec.get_hardfork_flags(0, 0).is_active(ArcHardfork::Zero3)); - } - - /// Simulates the nightly-upgrade scenario: genesis.json with a future osakaTime. - /// Verifies that From correctly reads osakaTime and activates Osaka - /// only at the specified timestamp. - #[test] - fn test_from_genesis_with_future_osaka_time() { - use alloy_genesis::Genesis; - - let s = r#"{ - "config": { - "chainId": 1337, - "zero3Block": 0, "zero4Block": 0, "zero5Block": 100, - "osakaTime": 9999 - }, - "alloc": {} - }"#; - let genesis: Genesis = serde_json::from_str(s).expect("Failed to parse genesis"); - let spec = ArcChainSpec::from(genesis); - - // Osaka NOT active before timestamp 9999 - assert!(!spec.is_osaka_active_at_timestamp(0)); - assert!(!spec.is_osaka_active_at_timestamp(9998)); - // Osaka active at timestamp 9999 - assert!(spec.is_osaka_active_at_timestamp(9999)); - assert!(spec.is_osaka_active_at_timestamp(10000)); - - // zero5 block-based activation also works - assert!(!spec.is_fork_active_at_block(ArcHardfork::Zero5, 99)); - assert!(spec.is_fork_active_at_block(ArcHardfork::Zero5, 100)); - } - - #[test] - #[should_panic(expected = "invalid block gas limit config")] - fn test_gas_limit_config_default_below_min_panics() { - BlockGasLimitConfig::new(10, 100, 5); - } - - #[test] - #[should_panic(expected = "invalid block gas limit config")] - fn test_gas_limit_config_default_above_max_panics() { - BlockGasLimitConfig::new(10, 100, 200); - } - - // --- BaseFeeConfig / BaseFeeCalcParams unit tests --- - - fn make_config() -> BaseFeeConfig { - BaseFeeConfig::new( - BoundedParam::new(1, 20, 100), - BoundedParam::new(1, 200, 10_000), - BoundedParam::new(1, 5000, 10_000), - 1, - u64::MAX - 1, - ) - } - - fn make_fee_params( - alpha: u64, - k_rate: u64, - inverse_elasticity_multiplier: u64, - ) -> crate::protocol_config::IProtocolConfig::FeeParams { - crate::protocol_config::IProtocolConfig::FeeParams { - alpha, - kRate: k_rate, - inverseElasticityMultiplier: inverse_elasticity_multiplier, - minBaseFee: alloy_primitives::U256::from(1u64), - maxBaseFee: alloy_primitives::U256::from(u64::MAX), - blockGasLimit: alloy_primitives::U256::from(30_000_000u64), - } - } - - #[test] - fn test_resolve_calc_params_none_returns_default() { - let config = make_config(); - let calc = config.resolve_calc_params(None); - assert_eq!(calc.alpha, config.alpha.default); - assert_eq!(calc.k_rate, config.k_rate.default); - assert_eq!( - calc.inverse_elasticity_multiplier, - config.inverse_elasticity_multiplier.default - ); - } - - #[test] - fn test_resolve_calc_params_in_range_passes_through() { - let config = make_config(); - // All values within bounds but different from defaults - let fp = make_fee_params(50, 500, 3000); - let calc = config.resolve_calc_params(Some(&fp)); - assert_eq!(calc.alpha, 50); - assert_eq!(calc.k_rate, 500); - assert_eq!(calc.inverse_elasticity_multiplier, 3000); - } - - #[test] - fn test_resolve_calc_params_at_min_boundary_passes_through() { - let config = make_config(); - let fp = make_fee_params( - config.alpha.min, - config.k_rate.min, - config.inverse_elasticity_multiplier.min, - ); - let calc = config.resolve_calc_params(Some(&fp)); - assert_eq!(calc.alpha, config.alpha.min); - assert_eq!(calc.k_rate, config.k_rate.min); - assert_eq!( - calc.inverse_elasticity_multiplier, - config.inverse_elasticity_multiplier.min - ); - } - - #[test] - fn test_resolve_calc_params_at_max_boundary_passes_through() { - let config = make_config(); - let fp = make_fee_params( - config.alpha.max, - config.k_rate.max, - config.inverse_elasticity_multiplier.max, - ); - let calc = config.resolve_calc_params(Some(&fp)); - assert_eq!(calc.alpha, config.alpha.max); - assert_eq!(calc.k_rate, config.k_rate.max); - assert_eq!( - calc.inverse_elasticity_multiplier, - config.inverse_elasticity_multiplier.max - ); - } - - #[test] - fn test_resolve_calc_params_alpha_above_max_uses_default() { - let config = make_config(); - let fp = make_fee_params( - config.alpha.max + 1, - config.k_rate.default + 1, - config.inverse_elasticity_multiplier.default + 1, - ); - let calc = config.resolve_calc_params(Some(&fp)); - assert_eq!(calc.alpha, config.alpha.default); - // Unchanged - assert_eq!(calc.k_rate, config.k_rate.default + 1); - assert_eq!( - calc.inverse_elasticity_multiplier, - config.inverse_elasticity_multiplier.default + 1 - ); - } - - #[test] - fn test_resolve_calc_params_k_rate_above_max_uses_default() { - let config = make_config(); - let fp = make_fee_params( - config.alpha.default + 1, - config.k_rate.max + 1, - config.inverse_elasticity_multiplier.default + 1, - ); - let calc = config.resolve_calc_params(Some(&fp)); - assert_eq!(calc.k_rate, config.k_rate.default); - // Unchanged - assert_eq!(calc.alpha, config.alpha.default + 1); - assert_eq!( - calc.inverse_elasticity_multiplier, - config.inverse_elasticity_multiplier.default + 1 - ) - } - - #[test] - fn test_resolve_calc_params_elasticity_above_max_uses_default() { - let config = make_config(); - let fp = make_fee_params( - config.alpha.default + 1, - config.k_rate.default + 1, - config.inverse_elasticity_multiplier.max + 1, - ); - let calc = config.resolve_calc_params(Some(&fp)); - assert_eq!( - calc.inverse_elasticity_multiplier, - config.inverse_elasticity_multiplier.default - ); - assert_eq!(calc.k_rate, config.k_rate.default + 1); - assert_eq!(calc.alpha, config.alpha.default + 1); - } - - #[test] - fn test_resolve_calc_params_alpha_below_min_uses_default() { - let config = make_config(); - let fp = make_fee_params( - config.alpha.min - 1, - config.k_rate.default + 1, - config.inverse_elasticity_multiplier.default + 1, - ); - let calc = config.resolve_calc_params(Some(&fp)); - assert_eq!( - calc.alpha, - config.alpha.default // default - ); - // Unchanged - assert_eq!(calc.k_rate, config.k_rate.default + 1); - assert_eq!( - calc.inverse_elasticity_multiplier, - config.inverse_elasticity_multiplier.default + 1 - ); - } - - #[test] - fn test_resolve_calc_params_k_rate_below_min_uses_default() { - let config = make_config(); - let fp = make_fee_params( - config.alpha.default, - config.k_rate.min - 1, - config.inverse_elasticity_multiplier.default, - ); - let calc = config.resolve_calc_params(Some(&fp)); - assert_eq!( - calc.k_rate, - config.k_rate.default // default - ); - } - - #[test] - fn test_resolve_calc_params_elasticity_below_min_uses_default() { - let config = make_config(); - let fp = make_fee_params( - config.alpha.default, - config.k_rate.default, - config.inverse_elasticity_multiplier.min - 1, - ); - let calc = config.resolve_calc_params(Some(&fp)); - assert_eq!( - calc.inverse_elasticity_multiplier, - config.inverse_elasticity_multiplier.default // default - ); - } - - #[test] - fn test_clamp_absolute() { - let config = BaseFeeConfig::new( - BoundedParam::new(0, 20, 100), - BoundedParam::new(0, 200, 10_000), - BoundedParam::new(1, 5000, 10_000), - 100, - 1000, - ); - assert_eq!(config.clamp_absolute(0), 100); - assert_eq!(config.clamp_absolute(99), 100); - assert_eq!(config.clamp_absolute(500), 500); - assert_eq!(config.clamp_absolute(1000), 1000); - assert_eq!(config.clamp_absolute(1001), 1000); - } - - #[test] - #[should_panic(expected = "invalid BaseFeeConfig")] - fn test_base_fee_config_inverted_absolute_bounds_panics() { - BaseFeeConfig::new( - BoundedParam::new(0, 20, 100), - BoundedParam::new(0, 200, 10_000), - BoundedParam::new(1, 5000, 10_000), - 1000, // min > max — should panic - 100, - ); - } - - #[test] - #[should_panic(expected = "invalid BoundedParam")] - fn test_bounded_param_inverted_bounds_panics() { - BoundedParam::new(100u64, 20, 50); - } - - static MOCK_ARC_HARDFORKS: LazyLock<[(Box, ForkCondition); 6]> = - LazyLock::new(|| { - [ - (ArcHardfork::Zero3.boxed(), ForkCondition::Block(0)), - (ArcHardfork::Zero4.boxed(), ForkCondition::Block(10)), - ( - EthereumHardfork::Osaka.boxed(), - ForkCondition::Timestamp(1779244750), - ), - ( - ArcHardfork::Zero5.boxed(), - ForkCondition::Timestamp(1779244760), - ), - ( - ArcHardfork::Zero6.boxed(), - ForkCondition::Timestamp(1779244770), - ), - ( - ArcHardfork::Zero7.boxed(), - ForkCondition::Timestamp(1779244780), - ), - ] - }); - - #[test] - fn test_arc_hardfork_ids() { - use reth_chainspec::Hardforks; - - let genesis: Genesis = - serde_json::from_str(include_str!("../../../assets/devnet/genesis.json")) - .expect("Can't deserialize Devnet genesis json"); - - let mut prev_head = Head { - number: 0, - timestamp: 0, - ..Default::default() - }; - let make_spec = |i: usize| -> ArcChainSpec { - let mut inner = ChainSpec::from_genesis(genesis.clone()); - inner.hardforks = BASE_FORKS.clone(); - for (hardfork, cond) in MOCK_ARC_HARDFORKS[0..i + 1].iter() { - inner.hardforks.insert(hardfork, *cond); - } - ArcChainSpec::new(inner) - }; - let mut prev_spec = make_spec(0); - let mut prev_hardfork = MOCK_ARC_HARDFORKS[0].0.clone(); - - for i in 1..MOCK_ARC_HARDFORKS.len() { - let hardfork = MOCK_ARC_HARDFORKS[i].0.clone(); - let spec = make_spec(i); - // simulate the next head according to the current fork condition - let head = match MOCK_ARC_HARDFORKS[i].1 { - ForkCondition::Block(block) => Head { - timestamp: prev_head.timestamp, - number: block, - ..Default::default() - }, - ForkCondition::TTD { - fork_block: Some(block), - .. - } => Head { - number: block, - ..Default::default() - }, - ForkCondition::Timestamp(timestamp) => Head { - number: prev_head.number, - timestamp, - ..Default::default() - }, - _ => panic!("unexpected fork condition"), - }; - let msg = format!( - "[iter={i}, {hardfork:?}, prev_head=({},{}), head=({},{})]", - prev_head.number, prev_head.timestamp, head.number, head.timestamp - ); - println!("{}", msg); - - let prev_filter = prev_spec.fork_filter(prev_head); - let mut filter = spec.fork_filter(prev_head); - - // make sure when we add the next hardfork, it could still valid for previos version. - let prev_fork_id = prev_filter.current(); - let fork_id = filter.current(); - - assert_eq!( - prev_fork_id.hash, fork_id.hash, - "[{msg}] fork hash should be the same when add a new hardfork", - ); - assert_eq!( - filter.validate(prev_fork_id), - Ok(()), - "[{msg}] fork id for the prev verions should validate by new version" - ); - assert_eq!( - prev_filter.validate(fork_id), - Ok(()), - "[{msg}] fork id for the new verions should validate by previous version" - ); - - // fork_id() use a different compute path, verify the value is the same as filter - assert_eq!( - prev_spec.fork_id(&prev_head), - prev_fork_id, - "[{msg}] computed fork id should be the same as it from previous filter" - ); - assert_eq!( - spec.fork_id(&prev_head), - fork_id, - "[{msg}] spec.fork_id(&prev_head) mismatched" - ); - let next_fork_id = spec.fork_id(&head); - - // Verify the fork ID by hardfork - assert_eq!( - prev_spec.inner.hardfork_fork_id(prev_hardfork.clone()), - Some(prev_fork_id), - "[{msg}] computed fork id by hardfork mismatched for previous spec" - ); - assert_eq!( - spec.inner.hardfork_fork_id(prev_hardfork.clone()), - Some(fork_id) - ); - assert_eq!( - spec.inner.hardfork_fork_id(hardfork.clone()), - Some(next_fork_id) - ); - - // Set the new head. - let transition: Option = filter.set_head(head); - assert!( - transition.is_some(), - "[{msg}] transition should be happened on next head" - ); - - // Verify the fork ID is the same as filter on next head - assert_eq!( - spec.fork_id(&head), - filter.current(), - "[{msg}] spec.fork_id(&head) mismatched on next head" - ); - assert_eq!( - next_fork_id, - filter.current(), - "[{msg}] computed fork ID mismatched on next head" - ); - - (prev_head, prev_spec, prev_hardfork) = (head, spec, hardfork); - } - } -} diff --git a/crates/execution-config/src/defaults.rs b/crates/execution-config/src/defaults.rs deleted file mode 100644 index bbed0d3..0000000 --- a/crates/execution-config/src/defaults.rs +++ /dev/null @@ -1,131 +0,0 @@ -// Copyright 2026 Circle Internet Group, Inc. All rights reserved. -// -// SPDX-License-Identifier: Apache-2.0 -// -// Licensed under the Apache License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. -// You may obtain a copy of the License at -// -// http://www.apache.org/licenses/LICENSE-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// See the License for the specific language governing permissions and -// limitations under the License. - -//! Default configuration for Arc Network node. -//! -//! This module provides default values for various node components including -//! snapshot download URLs for quick node bootstrapping, and RPC connection limits. - -use reth_cli_commands::download::DownloadDefaults; -use reth_node_core::args::DefaultRpcServerArgs; -use std::borrow::Cow; - -// FIXME: Update this to the actual snapshot URL. -/// Default snapshot URL for Arc Network testnet (chain ID 5042002). -pub(crate) const DEFAULT_DOWNLOAD_URL: &str = "https://snapshots.arc.network/5042002"; - -/// Max simultaneous RPC connections (HTTP + WS pooled). Bounds WS subscription fan-out memory. -pub const RPC_MAX_CONNECTIONS: u32 = 250; - -/// Max subscriptions per RPC connection. Real-world clients multiplex ~5 over one WS socket. -pub const RPC_MAX_SUBSCRIPTIONS_PER_CONNECTION: u32 = 32; - -fn init_download_urls() { - let download_defaults = DownloadDefaults { - available_snapshots: vec![ - // FIXME: Update this to the actual snapshot URL. - Cow::Borrowed("https://snapshots.arc.network/5042002 (testnet)"), - Cow::Borrowed("https://snapshots.arc.network/5042001 (devnet)"), - ], - default_base_url: Cow::Borrowed(DEFAULT_DOWNLOAD_URL), - default_chain_aware_base_url: None, - long_help: None, - }; - let _ = download_defaults.try_init(); -} - -fn init_rpc_defaults() { - let _ = DefaultRpcServerArgs::default() - .with_rpc_max_connections(RPC_MAX_CONNECTIONS.into()) - .with_rpc_max_subscriptions_per_connection(RPC_MAX_SUBSCRIPTIONS_PER_CONNECTION.into()) - .try_init(); -} - -/// Register Arc defaults with Reth. Must run before CLI parsing. -pub fn init_defaults() { - init_download_urls(); - init_rpc_defaults(); -} - -#[cfg(test)] -mod tests { - use super::*; - use clap::{Args, Parser}; - use reth_node_core::args::RpcServerArgs; - - #[derive(Parser)] - struct CommandParser { - #[command(flatten)] - args: T, - } - - // Reth's RpcServerArgs read defaults from a process-global OnceLock, so - // every test in this binary must init first to be order-independent. - fn ensure_initialized() { - init_defaults(); - } - - #[test] - fn rpc_defaults_match_arc_constants_when_no_override() { - ensure_initialized(); - let args = CommandParser::::parse_from(["arc-node-execution"]).args; - assert_eq!(args.rpc_max_connections.get(), RPC_MAX_CONNECTIONS); - assert_eq!( - args.rpc_max_subscriptions_per_connection.get(), - RPC_MAX_SUBSCRIPTIONS_PER_CONNECTION, - ); - } - - #[test] - fn rpc_max_connections_cli_override_wins() { - ensure_initialized(); - let args = CommandParser::::parse_from([ - "arc-node-execution", - "--rpc.max-connections", - "777", - ]) - .args; - assert_eq!(args.rpc_max_connections.get(), 777); - } - - #[test] - fn rpc_max_subscriptions_per_connection_cli_override_wins() { - ensure_initialized(); - let args = CommandParser::::parse_from([ - "arc-node-execution", - "--rpc.max-subscriptions-per-connection", - "1024", - ]) - .args; - assert_eq!(args.rpc_max_subscriptions_per_connection.get(), 1024); - } - - #[test] - fn rpc_overrides_are_independent() { - ensure_initialized(); - let args = CommandParser::::parse_from([ - "arc-node-execution", - "--rpc.max-connections", - "500", - ]) - .args; - assert_eq!(args.rpc_max_connections.get(), 500); - assert_eq!( - args.rpc_max_subscriptions_per_connection.get(), - RPC_MAX_SUBSCRIPTIONS_PER_CONNECTION, - ); - } -} diff --git a/crates/execution-config/src/follow.rs b/crates/execution-config/src/follow.rs deleted file mode 100644 index 30acc85..0000000 --- a/crates/execution-config/src/follow.rs +++ /dev/null @@ -1,107 +0,0 @@ -// Copyright 2026 Circle Internet Group, Inc. All rights reserved. -// -// SPDX-License-Identifier: Apache-2.0 -// -// Licensed under the Apache License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. -// You may obtain a copy of the License at -// -// http://www.apache.org/licenses/LICENSE-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// See the License for the specific language governing permissions and -// limitations under the License. - -//! RPC node configuration for the Arc network. - -use eyre::{eyre, Result}; -use reth_network_peers::TrustedPeer; - -use arc_shared::chain_ids::{LOCALDEV_CHAIN_ID, TESTNET_CHAIN_ID}; - -/// Returns the WebSocket URL for the given chain ID. -pub fn ws_url_for_chain_id(chain_id: u64) -> Result { - let url = match chain_id { - TESTNET_CHAIN_ID => "wss://rpc.quicknode.testnet.arc.network", - LOCALDEV_CHAIN_ID => "ws://localhost:8546", - _ => return Err(eyre!("Unsupported chain for follow mode: {}", chain_id)), - }; - Ok(url.to_string()) -} - -/// Returns the trusted peers (enode URLs) for the given chain ID. -/// -/// Currently returns an empty list for all chains. Trusted peer discovery -/// is not needed when running with `--rpc.forwarder` (the recommended -/// setup). If devp2p backfill is needed in the future, add real enode IDs -/// here. -pub fn trusted_peers_for_chain_id(chain_id: u64) -> Result> { - match chain_id { - TESTNET_CHAIN_ID | LOCALDEV_CHAIN_ID => Ok(Vec::new()), - _ => Err(eyre!("Unsupported chain for follow mode: {}", chain_id)), - } -} - -#[cfg(test)] -mod tests { - use super::*; - use arc_shared::chain_ids::DEVNET_CHAIN_ID; - - #[test] - fn test_ws_url_for_chain_id_localdev() { - let url = ws_url_for_chain_id(LOCALDEV_CHAIN_ID).unwrap(); - assert_eq!(url, "ws://localhost:8546"); - } - - #[test] - fn test_ws_url_for_chain_id_devnet() { - let result = ws_url_for_chain_id(DEVNET_CHAIN_ID); - assert!(result.is_err()); - } - - #[test] - fn test_ws_url_for_chain_id_testnet() { - let url = ws_url_for_chain_id(TESTNET_CHAIN_ID).unwrap(); - assert_eq!(url, "wss://rpc.quicknode.testnet.arc.network"); - } - - #[test] - fn test_ws_url_for_chain_id_unsupported() { - let result = ws_url_for_chain_id(999); - assert!(result.is_err()); - assert_eq!( - result.unwrap_err().to_string(), - "Unsupported chain for follow mode: 999" - ); - } - - #[test] - fn test_trusted_peers_for_chain_id_localdev() { - let peers = trusted_peers_for_chain_id(LOCALDEV_CHAIN_ID).unwrap(); - assert_eq!(peers.len(), 0); - } - - #[test] - fn test_trusted_peers_for_chain_id_devnet() { - let result = trusted_peers_for_chain_id(DEVNET_CHAIN_ID); - assert!(result.is_err()); - } - - #[test] - fn test_trusted_peers_for_chain_id_testnet() { - let peers = trusted_peers_for_chain_id(TESTNET_CHAIN_ID).unwrap(); - assert_eq!(peers.len(), 0); - } - - #[test] - fn test_trusted_peers_for_chain_id_unsupported() { - let result = trusted_peers_for_chain_id(999); - assert!(result.is_err()); - assert_eq!( - result.unwrap_err().to_string(), - "Unsupported chain for follow mode: 999" - ); - } -} diff --git a/crates/execution-config/src/gas_fee.rs b/crates/execution-config/src/gas_fee.rs deleted file mode 100644 index 37b944c..0000000 --- a/crates/execution-config/src/gas_fee.rs +++ /dev/null @@ -1,390 +0,0 @@ -// Copyright 2025 Circle Internet Group, Inc. All rights reserved. -// -// SPDX-License-Identifier: Apache-2.0 -// -// Licensed under the Apache License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. -// You may obtain a copy of the License at -// -// http://www.apache.org/licenses/LICENSE-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// See the License for the specific language governing permissions and -// limitations under the License. - -use alloy_primitives::Bytes; - -const ALPHA_MAX: u128 = 100u128; - -/// Computes the parent gas used value using Exponential Moving Average (EMA) smoothing. -/// This is fed into an EIP-1559 style base fee calculation. -pub fn determine_ema_parent_gas_used( - smoothed_parent_gas_used: u64, - raw_block_gas_used: u64, - alpha: u64, -) -> Option { - // Extraordinarily unlikely to overflow, but just in case - let a = alpha as u128; - if a > ALPHA_MAX { - return None; - } - - let raw = raw_block_gas_used as u128; - let smoothed = smoothed_parent_gas_used as u128; - - // (1-α) * G[t-1] + α * G[t] - // α is expressed as an integer value [0, 100] - // a <= ALPHA_MAX is guaranteed by the guard above. - #[allow(clippy::arithmetic_side_effects)] - let complement = ALPHA_MAX - a; - let left = complement.checked_mul(smoothed)?; - let right = a.checked_mul(raw)?; - let together = left.checked_add(right)?; - - // Floor (truncation) - u64::try_from(together / ALPHA_MAX).ok() -} - -/// Encode the base fee to bytes. -pub fn encode_base_fee_to_bytes(base_fee: u64) -> Bytes { - let bytes: [u8; 8] = base_fee.to_be_bytes(); - bytes.into() -} - -/// Decode the base fee from bytes. -pub fn decode_base_fee_from_bytes(extra_data: &Bytes) -> Option { - if extra_data.len() != 8 { - return None; - } - let bytes: [u8; 8] = extra_data.as_ref().try_into().ok()?; - Some(u64::from_be_bytes(bytes)) -} - -const ARC_BASE_FEE_FIXED_POINT_SCALE: u128 = 10_000; - -// This is copied from alloy_eips::eip1559::calc_next_block_base_fee, and use fixed point for -// max_change_denominator and elasticity_multiplier to calculate the gas_target. -pub fn arc_calc_next_block_base_fee( - gas_used: u64, - gas_limit: u64, - base_fee: u64, - k_rate: u64, // 2500 => 25% - inverse_elasticity_multiplier: u64, // 7500 => 75% -) -> u64 { - // All u64×u64 products fit in u128 without overflow. - #[allow(clippy::arithmetic_side_effects)] - let gas_target_u128 = - gas_limit as u128 * inverse_elasticity_multiplier as u128 / ARC_BASE_FEE_FIXED_POINT_SCALE; - let gas_target = u64::try_from(gas_target_u128).unwrap_or(u64::MAX); - - if gas_target == 0 || k_rate == 0 { - return base_fee; - } - - // k_rate != 0 checked above; gas_target (u64) × 10_000 fits in u128 - #[allow(clippy::arithmetic_side_effects)] - let denominator = gas_target as u128 * ARC_BASE_FEE_FIXED_POINT_SCALE / k_rate as u128; - - if denominator == 0 { - return base_fee; - } - - match gas_used.cmp(&gas_target) { - // If the gas used in the current block is equal to the gas target, the base fee remains the - // same (no increase). - core::cmp::Ordering::Equal => base_fee, - // If the gas used in the current block is greater than the gas target, calculate a new - // increased base fee. - core::cmp::Ordering::Greater => { - // Calculate the increase in base fee based on the formula defined by EIP-1559. - // gas_used > gas_target in this arm, so subtraction is safe - #[allow(clippy::arithmetic_side_effects)] - let increase = base_fee as u128 * (gas_used - gas_target) as u128 / denominator; - let increase = u64::try_from(increase).unwrap_or(u64::MAX); - // Ensure a minimum increase of 1. - base_fee.saturating_add(core::cmp::max(1, increase)) - } - // If the gas used in the current block is less than the gas target, calculate a new - // decreased base fee. - core::cmp::Ordering::Less => { - // Calculate the decrease in base fee based on the formula defined by EIP-1559. - // gas_target > gas_used in this arm, so subtraction is safe - #[allow(clippy::arithmetic_side_effects)] - let decrease = base_fee as u128 * (gas_target - gas_used) as u128 / denominator; - let decrease = u64::try_from(decrease).unwrap_or(u64::MAX); - base_fee.saturating_sub(decrease) - } - } -} - -#[cfg(test)] -mod tests { - use alloy_eips::eip1559::DEFAULT_BASE_FEE_MAX_CHANGE_DENOMINATOR; - use alloy_eips::eip1559::DEFAULT_ELASTICITY_MULTIPLIER; - use alloy_primitives::{hex, Bytes}; - use reth_chainspec::BaseFeeParams; - - use crate::gas_fee::ARC_BASE_FEE_FIXED_POINT_SCALE; - - const ARC_BASE_FEE_K_RATE: u64 = 200; - const ARC_BASE_FEE_INVERSE_ELASTICITY_MULTIPLIER: u64 = 7500; - - use super::{ - arc_calc_next_block_base_fee, decode_base_fee_from_bytes, determine_ema_parent_gas_used, - encode_base_fee_to_bytes, - }; - - struct Case { - name: &'static str, - smoothed_parent_gas_used: u64, - raw_block_gas_used: u64, - alpha: u64, - expected: Option, - } - - #[test] - fn determine_ema_parent_gas_used_table() { - let max = u64::MAX; - let _ = max; // keep for readability in cases below - let cases: &[Case] = &[ - Case { - name: "alpha > 100 -> None", - smoothed_parent_gas_used: 100, - raw_block_gas_used: 200, - alpha: 101, - expected: None, - }, - Case { - name: "alpha == 0 returns parent gas used (no change)", - smoothed_parent_gas_used: 123_456, - raw_block_gas_used: 987_654, - alpha: 0, - expected: Some(123_456), - }, - Case { - name: "alpha == 100 returns raw block gas used (no smoothing)", - smoothed_parent_gas_used: 111, - raw_block_gas_used: 222, - alpha: 100, - expected: Some(222), - }, - Case { - name: "50% average simple", - smoothed_parent_gas_used: 100, - raw_block_gas_used: 200, - alpha: 50, - expected: Some(150), - }, - Case { - name: "floor with single division (alpha=33, 1 & 2) => 1", - smoothed_parent_gas_used: 1, - raw_block_gas_used: 2, - alpha: 33, - expected: Some(1), - }, - Case { - name: "50% of (1,3) floors to 2", - smoothed_parent_gas_used: 1, - raw_block_gas_used: 3, - alpha: 50, - expected: Some(2), - }, - // Wider accumulator behavior: these succeed (Some) rather than overflow to None. - Case { - name: "alpha=100 with very large raw succeeds (u128 accumulator)", - smoothed_parent_gas_used: 0, - raw_block_gas_used: (max / 100) + 1, - alpha: 100, - expected: Some((max / 100) + 1), - }, - Case { - name: "alpha=0 with very large smoothed succeeds (u128 accumulator)", - smoothed_parent_gas_used: (max / 100) + 1, - raw_block_gas_used: 0, - alpha: 0, - expected: Some((max / 100) + 1), - }, - ]; - - for tc in cases { - let got = determine_ema_parent_gas_used( - tc.smoothed_parent_gas_used, - tc.raw_block_gas_used, - tc.alpha, - ); - match tc.expected { - Some(expected) => assert_eq!(got, Some(expected), "{}", tc.name), - None => assert!(got.is_none(), "{}", tc.name), - } - } - } - - #[test] - fn encode_decode_base_fee_roundtrip() { - for base_fee in [0, 1, 77999931, u64::MAX] { - let bytes = encode_base_fee_to_bytes(base_fee); - let recovered = decode_base_fee_from_bytes(&bytes); - - assert_eq!(recovered, Some(base_fee), "{}", base_fee); - } - } - - #[test] - fn decode_base_fee() { - for (bytes, expected) in [ - (Bytes::from_static(&hex!("")), None), - (Bytes::from_static(&hex!("0000000000000000")), Some(0_u64)), - (Bytes::from_static(&hex!("000000000000000000")), None), - ( - Bytes::from_static(&hex!("00000000000000000000000000000000")), - None, - ), - (Bytes::from_static(b"ff"), None), - ( - Bytes::from_static(&hex!("ffffffffffffffff")), - Some(u64::MAX), - ), - (Bytes::from_static(&hex!("ffffffffffffffffff")), None), - ( - Bytes::from_static(&hex!("ffffffffffffffffffffffffffffffffffffffffff")), - None, - ), - ( - Bytes::from_static(&hex!("42681199dd51b2e2")), - Some(4785093956621939426_u64), - ), - ( - Bytes::from_static(&hex!( - "42681199dd51b27f81ea319045dbb740f92740ce7f069cbcd3f25d97261969a4" - )), - None, - ), - ] { - let recovered = decode_base_fee_from_bytes(&bytes); - assert_eq!( - recovered, expected, - "recovered: {recovered:?}, expected: {expected:?}, bytes: {bytes:?}", - ); - } - } - - #[test] - fn test_calc_next_block_base_fee() { - // gas_used, gas_limit, base_fee, expected_base_fee - let sample_blocks_info: [(u64, u64, u64, u64); _] = [ - (1000, 10000, 1000000, 982667), - (1000, u64::MAX, 1000000, 980001), - (3, 3, u64::MAX, u64::MAX), - (3, 3, 451123121293128, 455634352506059), - (100000, 300_000, u64::MAX, 18241780250668334375), - (0, 30_000_000, 0, 0), - (3, 3, 0, 1), - (3, 3, 1, 2), - (3, 3, 7, 8), - (0, 30_000_000, 160_000_000_000, 156800000000), - (1, 30_000_000, 160_000_000_000, 156800000143), - (2, 30_000_000, 160_000_000_000, 156800000285), - (3, 30_000_000, 160_000_000_000, 156800000427), - (14_999_998, 30_000_000, 160_000_000_000, 158933333049), - (14_999_999, 30_000_000, 160_000_000_000, 158933333192), - (15_000_000, 30_000_000, 160_000_000_000, 158933333334), - (15_000_001, 30_000_000, 160_000_000_000, 158933333476), - (15_000_002, 30_000_000, 160_000_000_000, 158933333618), - (15_000_003, 30_000_000, 160_000_000_000, 158933333760), - (30_000_000, 30_000_000, 160_000_000_000, 161066666666), - (29_999_999, 30_000_000, 160_000_000_000, 161066666524), - (29_999_998, 30_000_000, 160_000_000_000, 161066666382), - (1, 3, 160_000_000_000, 158400000000), - (2, 3, 160_000_000_000, 160000000000), - (3, 3, 160_000_000_000, 161600000000), - (0, 30_000_000, 1, 1), - ]; - - for (gas_used, gas_limit, base_fee, _) in sample_blocks_info { - if gas_used > gas_limit / 2 && base_fee == u64::MAX { - continue; // eip1559 can not handle this - } - let eip1559_next_base_fee = BaseFeeParams::new( - DEFAULT_BASE_FEE_MAX_CHANGE_DENOMINATOR as u128, - DEFAULT_ELASTICITY_MULTIPLIER as u128, - ) - .next_block_base_fee(gas_used, gas_limit, base_fee); - - // use kRate 12.5% and inverse elasticity multiplier 50% to compute the same value as eip1559 - #[allow(clippy::cast_possible_truncation)] // 10_000u128 fits in u64 - let next_base_fee = arc_calc_next_block_base_fee( - gas_used, - gas_limit, - base_fee, - ARC_BASE_FEE_FIXED_POINT_SCALE as u64 / DEFAULT_BASE_FEE_MAX_CHANGE_DENOMINATOR, - ARC_BASE_FEE_FIXED_POINT_SCALE as u64 / DEFAULT_ELASTICITY_MULTIPLIER, - ); - - assert_eq!( - eip1559_next_base_fee, next_base_fee, - "cmp with eip1559 gas used={gas_used}, gas limit={gas_limit}, base fee={base_fee}" - ); - } - - for (gas_used, gas_limit, base_fee, expect_base_fee) in sample_blocks_info { - let next_base_fee = arc_calc_next_block_base_fee( - gas_used, - gas_limit, - base_fee, - ARC_BASE_FEE_K_RATE, - ARC_BASE_FEE_INVERSE_ELASTICITY_MULTIPLIER, - ); - assert_eq!( - expect_base_fee, next_base_fee, - "gas used={gas_used}, gas limit={gas_limit}, base fee={base_fee}" - ); - } - } - - #[test] - fn base_fee_should_greater_than_zero() { - // For eip1559, the base fee could be zero if max_change_denominator is 1. - for ( - denominator, - elasticity_multiplier, - gas_used, - gas_limit, - base_fee, - expect_next_base_fee, - ) in [ - (8, 2, 0, 30_000_000, 1, 1), - (2, 2, 0, 30_000_000, 1, 1), - (1, 2, 0, 30_000_000, 1, 0), - (8, 2, 0, 30_000_000, 0, 0), - ] { - assert_eq!( - BaseFeeParams::new(denominator, elasticity_multiplier as u128) - .next_block_base_fee(gas_used, gas_limit, base_fee), - expect_next_base_fee, - "gas_used={gas_used}, gas_limit={gas_limit}, base_fee={base_fee}, denominator={denominator}, elasticity_multiplier={elasticity_multiplier}", - ) - } - - // We add a lower bound to make sure it will not be zero. - for (k_rate, iem, gas_used, gas_limit, base_fee, expect_next_base_fee) in [ - (200, 7500, 0, 30_000_000, 1, 1), // the base fee should not be zero in normal case. - (200, 5000, 0, 30_000_000, 1, 1), // the base fee should not be zero in normal case. - (9000, 0, 1, 30_000_000, 10000, 10000), // if gas_target is zero, the base fee should be the same - (10000, 10000, 0, 30_000_000, 1, 0), // 100% decrease, but the result should be 1 - (10000, 10000, 0, 30_000_000, 10000, 0), // 100% decrease, but the result should be 1, not 0 - (0, 1, 0, 30_000_000, 8888, 8888), // zero kRate, the base fee should be the same - (200, 5000, 0, 30_000_000, 0, 0), // keep zero value - (0, 0, 0, 3, 1, 1), - (0, 0, 1, 3, 1, 1), - ] { - let next_base_fee = - arc_calc_next_block_base_fee(gas_used, gas_limit, base_fee, k_rate, iem); - assert_eq!( - next_base_fee, expect_next_base_fee, - "gas_used={gas_used}, gas_limit={gas_limit}, base_fee={base_fee}, k_rate={k_rate}, inverse_elasticity_multiplier={iem}", - ); - } - } -} diff --git a/crates/execution-config/src/hardforks.rs b/crates/execution-config/src/hardforks.rs deleted file mode 100644 index 71fbd08..0000000 --- a/crates/execution-config/src/hardforks.rs +++ /dev/null @@ -1,859 +0,0 @@ -// Copyright 2025 Circle Internet Group, Inc. All rights reserved. -// -// SPDX-License-Identifier: Apache-2.0 -// -// Licensed under the Apache License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. -// You may obtain a copy of the License at -// -// http://www.apache.org/licenses/LICENSE-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// See the License for the specific language governing permissions and -// limitations under the License. - -extern crate alloc; - -use alloc::vec; -use alloy_primitives::U256; -use alloy_serde::OtherFields; -use once_cell::sync::Lazy as LazyLock; -use reth_chainspec::hardfork; -use reth_ethereum_forks::{ChainHardforks, EthereumHardfork, ForkCondition, Hardfork}; - -// ref: [OpHardfork](https://docs.rs/alloy-op-hardforks/latest/alloy_op_hardforks/enum.OpHardfork.html) -hardfork!( - #[derive(serde::Serialize, serde::Deserialize, Default)] - ArcHardfork { - Zero3, // v0.3 hardfork, align to Ethereum Prague - Zero4, // v0.4 hardfork, align to Ethereum Prague - Zero5, // v0.5 hardfork, align to Ethereum Prague - #[default] - Zero6, // v0.6 hardfork - Zero7, // v0.7 hardfork — batch (Multicall3From) and memo contracts - } -); - -// define our extra genesis info (hardfork table) -// ref: [OpGenesisInfo](https://docs.rs/op-alloy-rpc-types/0.19.0/op_alloy_rpc_types/struct.OpGenesisInfo.html) -#[derive(Default, Debug, Clone, Copy, Eq, PartialEq, serde::Serialize, serde::Deserialize)] -#[serde(rename_all = "camelCase")] -pub struct ArcGenesisInfo { - /// v0.3 hardfork block - pub zero_3_block: Option, - /// v0.4 hardfork block - pub zero_4_block: Option, - /// v0.5 hardfork block - pub zero_5_block: Option, - /// v0.6 hardfork block - pub zero_6_block: Option, - /// v0.7 hardfork timestamp for genesis-configured chains. - /// - /// Built-in network schedules are defined in `ARC_*_HARDFORKS` and may activate - /// earlier Arc hardforks by timestamp, e.g. testnet Zero5/Zero6. - pub zero_7_time: Option, -} - -impl ArcGenesisInfo { - /// Extract the Arc-specific genesis info from a genesis file. - pub fn extract_from(others: &OtherFields) -> Option { - Self::try_from(others).ok() - } - - pub fn get_hardfork_conditions(&self) -> Vec<(ArcHardfork, ForkCondition)> { - let mut hardforks = Vec::new(); - for (fork_block, hardfork) in [ - (self.zero_3_block, ArcHardfork::Zero3), - (self.zero_4_block, ArcHardfork::Zero4), - (self.zero_5_block, ArcHardfork::Zero5), - (self.zero_6_block, ArcHardfork::Zero6), - ] { - if let Some(fork_block) = fork_block { - hardforks.push((hardfork, ForkCondition::Block(fork_block))); - } - } - // Zero7+ activate by timestamp (see field doc on ArcGenesisInfo). - if let Some(time) = self.zero_7_time { - hardforks.push((ArcHardfork::Zero7, ForkCondition::Timestamp(time))); - } - hardforks - } -} - -impl TryFrom<&OtherFields> for ArcGenesisInfo { - type Error = serde_json::Error; - - fn try_from(others: &OtherFields) -> Result { - others.deserialize_as() - } -} - -/// Feature-flag style hardfork information for EVM level. -/// -/// Each hardfork can be independently enabled without implying other hardforks. -/// This allows different networks to have different hardfork activation orders. -#[derive(Debug, Clone, Copy, Default, PartialEq, Eq)] -pub struct ArcHardforkFlags { - zero3: bool, - zero4: bool, - zero5: bool, - zero6: bool, - zero7: bool, -} - -impl ArcHardforkFlags { - /// All Arc hardfork variants. Update this when adding new hardforks. - const ALL_HARDFORKS: &'static [ArcHardfork] = &[ - ArcHardfork::Zero3, - ArcHardfork::Zero4, - ArcHardfork::Zero5, - ArcHardfork::Zero6, - ArcHardfork::Zero7, - ]; - - /// Check if a specific hardfork is active. - pub fn is_active(&self, hardfork: ArcHardfork) -> bool { - match hardfork { - ArcHardfork::Zero3 => self.zero3, - ArcHardfork::Zero4 => self.zero4, - ArcHardfork::Zero5 => self.zero5, - ArcHardfork::Zero6 => self.zero6, - ArcHardfork::Zero7 => self.zero7, - } - } - - /// Set a specific hardfork flag. - fn set(&mut self, hardfork: ArcHardfork, value: bool) { - match hardfork { - ArcHardfork::Zero3 => self.zero3 = value, - ArcHardfork::Zero4 => self.zero4 = value, - ArcHardfork::Zero5 => self.zero5 = value, - ArcHardfork::Zero6 => self.zero6 = value, - ArcHardfork::Zero7 => self.zero7 = value, - } - } - - /// Create flags from chain hardforks at a given (block, timestamp). - /// - /// Arc hardfork activation is network-specific: some forks activate by block and - /// others by timestamp. Evaluate both dimensions so timestamp-based testnet - /// Zero5/Zero6 and block-based devnet Zero5/Zero6 are both handled correctly. - pub fn from_chain_hardforks(hardforks: &ChainHardforks, block: u64, timestamp: u64) -> Self { - let mut flags = Self::default(); - for &hf in Self::ALL_HARDFORKS { - if hardforks.is_fork_active_at_block(hf, block) - || hardforks.is_fork_active_at_timestamp(hf, timestamp) - { - flags.set(hf, true); - } - } - flags - } - - /// Create flags with specific hardforks enabled. - #[cfg(any(feature = "test-utils", test))] - pub fn with(hardforks: &[ArcHardfork]) -> Self { - let mut flags = Self::default(); - for &hf in hardforks { - flags.set(hf, true); - } - flags - } - - /// Returns an iterator over all possible hardfork flag combinations (2^n for n hardforks). - /// - /// This is useful for exhaustive testing to ensure code works correctly - /// regardless of which hardforks are active, including non-sequential - /// activation (e.g., Zero4 without Zero3). - #[cfg(any(feature = "test-utils", test))] - pub fn all_combinations() -> impl Iterator { - let n = Self::ALL_HARDFORKS.len(); - // generate 2^n combinations - (0..(1 << n)).map(move |bits| { - let mut flags = Self::default(); - for (i, &hf) in Self::ALL_HARDFORKS.iter().enumerate() { - if bits & (1 << i) != 0 { - flags.set(hf, true); - } - } - flags - }) - } -} - -/// Checks whether an Arc hardfork is active at the given `(block_number, block_timestamp)`, -/// covering both block-based and timestamp-based activation. -/// -/// **Use this for any runtime gating of Arc hardforks.** Zero7+ activates by timestamp on -/// every network, and Zero5/Zero6 activate by timestamp on testnet. A bare -/// `is_fork_active_at_block(hf, n)` silently returns `false` for timestamp-activated forks, -/// so the corresponding EVM/validation behaviour would never trigger. -/// -/// The OR is safe by construction: `ForkCondition::active_at_block` and `active_at_timestamp` -/// both pattern-match the variant before comparing values, so a `Timestamp(_)` fork never -/// matches the block branch and vice versa — the two checks are mutually exclusive per fork. -/// -/// Mirrors the dual-check pattern in [`ArcHardforkFlags::from_chain_hardforks`]. -pub fn is_arc_fork_active( - chain_spec: &CS, - fork: ArcHardfork, - block_number: u64, - block_timestamp: u64, -) -> bool { - chain_spec.is_fork_active_at_block(fork, block_number) - || chain_spec.is_fork_active_at_timestamp(fork, block_timestamp) -} - -// Reference Ethereum forks -// - https://github.com/ethereum/execution-specs/blob/forks/osaka/README.md -// - https://github.com/paradigmxyz/reth/blob/91defb2f9c9522007436ba6f41098d73e41cc34c/crates/ethereum/hardforks/src/hardforks/dev.rs#L14 -pub(crate) static BASE_FORKS: LazyLock = LazyLock::new(|| { - ChainHardforks::new(vec![ - (EthereumHardfork::Frontier.boxed(), ForkCondition::Block(0)), - (EthereumHardfork::Homestead.boxed(), ForkCondition::Block(0)), - (EthereumHardfork::Tangerine.boxed(), ForkCondition::Block(0)), - ( - EthereumHardfork::SpuriousDragon.boxed(), - ForkCondition::Block(0), - ), - (EthereumHardfork::Byzantium.boxed(), ForkCondition::Block(0)), - ( - EthereumHardfork::Constantinople.boxed(), - ForkCondition::Block(0), - ), - ( - EthereumHardfork::Petersburg.boxed(), - ForkCondition::Block(0), - ), - (EthereumHardfork::Istanbul.boxed(), ForkCondition::Block(0)), - ( - EthereumHardfork::MuirGlacier.boxed(), - ForkCondition::Block(0), - ), - (EthereumHardfork::Berlin.boxed(), ForkCondition::Block(0)), - (EthereumHardfork::London.boxed(), ForkCondition::Block(0)), - ( - EthereumHardfork::ArrowGlacier.boxed(), - ForkCondition::Block(0), - ), - ( - EthereumHardfork::GrayGlacier.boxed(), - ForkCondition::Block(0), - ), - ( - EthereumHardfork::Paris.boxed(), - ForkCondition::TTD { - activation_block_number: 0, - fork_block: None, - total_difficulty: U256::ZERO, - }, - ), - ( - EthereumHardfork::Shanghai.boxed(), - ForkCondition::Timestamp(0), - ), - ( - EthereumHardfork::Cancun.boxed(), - ForkCondition::Timestamp(0), - ), - ( - EthereumHardfork::Prague.boxed(), - ForkCondition::Timestamp(0), - ), - ]) -}); - -/// Arc Local Dev network (1337) hardforks. -pub static ARC_LOCALDEV_HARDFORKS: LazyLock = LazyLock::new(|| { - let mut forks = BASE_FORKS.clone(); - forks.insert(ArcHardfork::Zero3.boxed(), ForkCondition::Block(0)); - forks.insert(ArcHardfork::Zero4.boxed(), ForkCondition::Block(0)); - // Zero5 : Osaka — paired per convention above - forks.insert(EthereumHardfork::Osaka.boxed(), ForkCondition::Timestamp(0)); - forks.insert(ArcHardfork::Zero5.boxed(), ForkCondition::Block(0)); - forks.insert(ArcHardfork::Zero6.boxed(), ForkCondition::Block(0)); - // Zero7+ activate by timestamp to keep the EIP-2124 fork-id stable across - // mixed-version peers (block-based forks declared after a timestamp fork break - // ForkFilter's BTreeMap ordering). - forks.insert(ArcHardfork::Zero7.boxed(), ForkCondition::Timestamp(0)); - forks -}); - -/// Arc Mainnet network (5042) hardforks. -pub static ARC_MAINNET_HARDFORKS: LazyLock = LazyLock::new(|| { - let mut forks = BASE_FORKS.clone(); - forks.insert(ArcHardfork::Zero3.boxed(), ForkCondition::Block(0)); - forks.insert(ArcHardfork::Zero4.boxed(), ForkCondition::Block(0)); - // Zero5 : Osaka — paired per convention above - forks.insert(EthereumHardfork::Osaka.boxed(), ForkCondition::Timestamp(0)); - forks.insert(ArcHardfork::Zero5.boxed(), ForkCondition::Block(0)); - forks.insert(ArcHardfork::Zero6.boxed(), ForkCondition::Block(0)); - forks -}); - -/// Arc Devnet network (5042001) hardforks. -pub static ARC_DEVNET_HARDFORKS: LazyLock = LazyLock::new(|| { - let mut forks = BASE_FORKS.clone(); - forks.insert( - ArcHardfork::Zero3.boxed(), - ForkCondition::Block(ARC_ZERO3_HARDFORK_BLOCK_ACTIVATION_DEVNET), - ); - forks.insert( - ArcHardfork::Zero4.boxed(), - ForkCondition::Block(ARC_ZERO4_HARDFORK_BLOCK_ACTIVATION_DEVNET), - ); - forks.insert( - EthereumHardfork::Osaka.boxed(), - ForkCondition::Timestamp(ARC_OSAKA_HARDFORK_TIMESTAMP_ACTIVATION_DEVNET), - ); - forks.insert( - ArcHardfork::Zero5.boxed(), - ForkCondition::Block(ARC_ZERO5_HARDFORK_BLOCK_ACTIVATION_DEVNET), - ); - forks.insert( - ArcHardfork::Zero6.boxed(), - ForkCondition::Block(ARC_ZERO6_HARDFORK_BLOCK_ACTIVATION_DEVNET), - ); - forks.insert( - ArcHardfork::Zero7.boxed(), - ForkCondition::Timestamp(ARC_ZERO7_HARDFORK_TIMESTAMP_ACTIVATION_DEVNET), - ); - forks -}); - -/// Arc Testnet network (5042002) hardforks. -pub static ARC_TESTNET_HARDFORKS: LazyLock = LazyLock::new(|| { - let mut forks = BASE_FORKS.clone(); - forks.insert( - ArcHardfork::Zero3.boxed(), - ForkCondition::Block(ARC_ZERO3_HARDFORK_BLOCK_ACTIVATION_TESTNET), - ); - forks.insert( - ArcHardfork::Zero4.boxed(), - ForkCondition::Block(ARC_ZERO4_HARDFORK_BLOCK_ACTIVATION_TESTNET), - ); - forks.insert( - EthereumHardfork::Osaka.boxed(), - ForkCondition::Timestamp(ARC_OSAKA_HARDFORK_TIMESTAMP_ACTIVATION_TESTNET), - ); - // Zero5/Zero6 must activate by timestamp - // (not block) — declaring them as Block-after-Osaka would trigger the EIP-2124 - // BTreeMap-ordering bug for any peer that doesn't have them declared yet. - forks.insert( - ArcHardfork::Zero5.boxed(), - ForkCondition::Timestamp(ARC_ZERO5_HARDFORK_TIMESTAMP_ACTIVATION_TESTNET), - ); - forks.insert( - ArcHardfork::Zero6.boxed(), - ForkCondition::Timestamp(ARC_ZERO6_HARDFORK_TIMESTAMP_ACTIVATION_TESTNET), - ); - forks.insert( - ArcHardfork::Zero7.boxed(), - ForkCondition::Timestamp(ARC_ZERO7_HARDFORK_TIMESTAMP_ACTIVATION_TESTNET), - ); - - forks -}); - -/// Constants -/// Zero3 -pub const ARC_ZERO3_HARDFORK_BLOCK_ACTIVATION_DEVNET: u64 = 7437594; -pub const ARC_ZERO3_HARDFORK_BLOCK_ACTIVATION_TESTNET: u64 = 11172019; -/// Zero4 -pub const ARC_ZERO4_HARDFORK_BLOCK_ACTIVATION_DEVNET: u64 = 19491165; -pub const ARC_ZERO4_HARDFORK_BLOCK_ACTIVATION_TESTNET: u64 = 26148086; -/// Zero5 -pub const ARC_ZERO5_HARDFORK_BLOCK_ACTIVATION_DEVNET: u64 = 32371192; -pub const ARC_ZERO5_HARDFORK_TIMESTAMP_ACTIVATION_TESTNET: u64 = 1779894517; -/// Zero6 -pub const ARC_ZERO6_HARDFORK_BLOCK_ACTIVATION_DEVNET: u64 = 40033853; -pub const ARC_ZERO6_HARDFORK_TIMESTAMP_ACTIVATION_TESTNET: u64 = 1779894517; -/// Osaka (paired with Zero5) -pub const ARC_OSAKA_HARDFORK_TIMESTAMP_ACTIVATION_DEVNET: u64 = 1775483400; -pub const ARC_OSAKA_HARDFORK_TIMESTAMP_ACTIVATION_TESTNET: u64 = 1779890400; -/// Zero7 -pub const ARC_ZERO7_HARDFORK_TIMESTAMP_ACTIVATION_DEVNET: u64 = 1780495200; -pub const ARC_ZERO7_HARDFORK_TIMESTAMP_ACTIVATION_TESTNET: u64 = 1781791200; - -#[cfg(test)] -mod tests { - use super::*; - use alloy_genesis::Genesis; - - #[test] - fn test_arc_hardfork_names() { - assert_eq!(ArcHardfork::Zero3.name(), "Zero3"); - assert_eq!(ArcHardfork::Zero4.name(), "Zero4"); - assert_eq!(ArcHardfork::Zero5.name(), "Zero5"); - assert_eq!(ArcHardfork::Zero6.name(), "Zero6"); - assert_eq!(ArcHardfork::Zero7.name(), "Zero7"); - } - - #[test] - fn test_arc_hardfork_flags() { - // Test default (no hardforks active) - let flags = ArcHardforkFlags::default(); - assert!(!flags.is_active(ArcHardfork::Zero3)); - assert!(!flags.is_active(ArcHardfork::Zero4)); - assert!(!flags.is_active(ArcHardfork::Zero5)); - assert!(!flags.is_active(ArcHardfork::Zero6)); - assert!(!flags.is_active(ArcHardfork::Zero7)); - - // Test from chain hardforks - localdev has all Arc hardforks active at genesis - let flags = ArcHardforkFlags::from_chain_hardforks(&ARC_LOCALDEV_HARDFORKS, 0, 0); - assert!(flags.is_active(ArcHardfork::Zero3)); - assert!(flags.is_active(ArcHardfork::Zero4)); - assert!(flags.is_active(ArcHardfork::Zero5)); - assert!(flags.is_active(ArcHardfork::Zero6)); - assert!(flags.is_active(ArcHardfork::Zero7)); - - // Test from chain hardforks - devnet has Zero3 and Zero4 active after their activation blocks - let flags = ArcHardforkFlags::from_chain_hardforks( - &ARC_DEVNET_HARDFORKS, - ARC_ZERO4_HARDFORK_BLOCK_ACTIVATION_DEVNET, - 0, - ); - assert!(flags.is_active(ArcHardfork::Zero3)); - assert!(flags.is_active(ArcHardfork::Zero4)); - assert!(!flags.is_active(ArcHardfork::Zero5)); - assert!(!flags.is_active(ArcHardfork::Zero6)); - assert!(!flags.is_active(ArcHardfork::Zero7)); - - // Test from chain hardforks - devnet before Zero4 activation - let flags = ArcHardforkFlags::from_chain_hardforks( - &ARC_DEVNET_HARDFORKS, - ARC_ZERO4_HARDFORK_BLOCK_ACTIVATION_DEVNET - 1, - 0, - ); - assert!(flags.is_active(ArcHardfork::Zero3)); - assert!(!flags.is_active(ArcHardfork::Zero4)); - - // Test from chain hardforks - devnet before Zero3 activation - let flags = ArcHardforkFlags::from_chain_hardforks( - &ARC_DEVNET_HARDFORKS, - ARC_ZERO3_HARDFORK_BLOCK_ACTIVATION_DEVNET - 1, - 0, - ); - assert!(!flags.is_active(ArcHardfork::Zero3)); - assert!(!flags.is_active(ArcHardfork::Zero4)); - - // Test with() helper - let flags = ArcHardforkFlags::with(&[ArcHardfork::Zero3]); - assert!(flags.is_active(ArcHardfork::Zero3)); - assert!(!flags.is_active(ArcHardfork::Zero4)); - - let flags = ArcHardforkFlags::with(&[ArcHardfork::Zero4]); - assert!(!flags.is_active(ArcHardfork::Zero3)); - assert!(flags.is_active(ArcHardfork::Zero4)); - - let flags = ArcHardforkFlags::with(&[ArcHardfork::Zero3, ArcHardfork::Zero4]); - assert!(flags.is_active(ArcHardfork::Zero3)); - assert!(flags.is_active(ArcHardfork::Zero4)); - - let flags = ArcHardforkFlags::with(&[]); - assert!(!flags.is_active(ArcHardfork::Zero3)); - assert!(!flags.is_active(ArcHardfork::Zero4)); - - // Test all_combinations() helper - should yield 32 combinations (2^5) - let combinations: Vec<_> = ArcHardforkFlags::all_combinations().collect(); - assert_eq!(combinations.len(), 32); - - // Verify some key combinations are present - assert!(combinations.contains(&ArcHardforkFlags::with(&[]))); - assert!(combinations.contains(&ArcHardforkFlags::with(&[ArcHardfork::Zero3]))); - assert!(combinations.contains(&ArcHardforkFlags::with(&[ArcHardfork::Zero4]))); - assert!(combinations.contains(&ArcHardforkFlags::with(&[ - ArcHardfork::Zero3, - ArcHardfork::Zero4 - ]))); - assert!(combinations.contains(&ArcHardforkFlags::with(&[ - ArcHardfork::Zero3, - ArcHardfork::Zero4, - ArcHardfork::Zero5, - ArcHardfork::Zero6, - ArcHardfork::Zero7, - ]))); - } - - #[test] - fn test_parse_arc_hardfork_from_genesis() { - let s = r#"{ "config": { "zero3Block": 123123, "zero4Block": 223881, "zero5Block": 323496, "zero6Block": 423000, "zero7Time": 1800000000 } }"#; - - let genesis = serde_json::from_str::(s).expect("Failed to parse genesis"); - let info = ArcGenesisInfo::extract_from(&genesis.config.extra_fields) - .expect("Failed to extract genesis info"); - assert_eq!(info.zero_3_block, Some(123123)); - assert_eq!(info.zero_4_block, Some(223881)); - assert_eq!(info.zero_5_block, Some(323496)); - assert_eq!(info.zero_6_block, Some(423000)); - assert_eq!(info.zero_7_time, Some(1800000000)); - } - - // Verify ethereum hardforks are supported for all networks. - fn assert_base_hardforks(forks: &ChainHardforks) { - assert!(!forks.is_empty()); - - // Ethereum hardforks not supported - assert_eq!(forks.get(EthereumHardfork::Dao), None); - - // Ethereum hardforks supported - assert!(forks.is_fork_active_at_block(EthereumHardfork::Frontier, 0)); - assert!(forks.is_fork_active_at_block(EthereumHardfork::Homestead, 0)); - assert!(forks.is_fork_active_at_block(EthereumHardfork::Tangerine, 0)); - assert!(forks.is_fork_active_at_block(EthereumHardfork::SpuriousDragon, 0)); - assert!(forks.is_fork_active_at_block(EthereumHardfork::Byzantium, 0)); - assert!(forks.is_fork_active_at_block(EthereumHardfork::Constantinople, 0)); - assert!(forks.is_fork_active_at_block(EthereumHardfork::Petersburg, 0)); - assert!(forks.is_fork_active_at_block(EthereumHardfork::Istanbul, 0)); - assert!(forks.is_fork_active_at_block(EthereumHardfork::Berlin, 0)); - assert!(forks.is_fork_active_at_block(EthereumHardfork::London, 0)); - assert!(forks.is_fork_active_at_block(EthereumHardfork::Paris, 0)); - // hardforks for delay the difficulty bomb - assert!(forks.is_fork_active_at_block(EthereumHardfork::MuirGlacier, 0)); - assert!(forks.is_fork_active_at_block(EthereumHardfork::ArrowGlacier, 0)); - assert!(forks.is_fork_active_at_block(EthereumHardfork::GrayGlacier, 0)); - // Time based hardforks - assert!(forks.is_fork_active_at_timestamp(EthereumHardfork::Shanghai, 0)); - assert!(forks.is_fork_active_at_timestamp(EthereumHardfork::Cancun, 0)); - assert!(forks.is_fork_active_at_timestamp(EthereumHardfork::Prague, 0)); - } - - #[test] - fn test_arc_base_hardforks() { - let forks = BASE_FORKS.clone(); - assert_base_hardforks(&forks); - assert_eq!(forks.len(), 17); - } - - #[test] - fn test_arc_localdev_forks() { - let forks = ARC_LOCALDEV_HARDFORKS.clone(); - assert_base_hardforks(&forks); - assert_eq!(forks.len(), 23); - - // verify hardfork zero3 block - assert!(!forks.is_fork_active_at_timestamp(ArcHardfork::Zero3, 0)); - assert!(forks.is_fork_active_at_block(ArcHardfork::Zero3, 0)); - - // verify hardfork zero4 block - assert!(!forks.is_fork_active_at_timestamp(ArcHardfork::Zero4, 0)); - assert!(forks.is_fork_active_at_block(ArcHardfork::Zero4, 0)); - - // verify hardfork zero5 block - assert!(!forks.is_fork_active_at_timestamp(ArcHardfork::Zero5, 0)); - assert!(forks.is_fork_active_at_block(ArcHardfork::Zero5, 0)); - - // verify hardfork zero6 block - assert!(!forks.is_fork_active_at_timestamp(ArcHardfork::Zero6, 0)); - assert!(forks.is_fork_active_at_block(ArcHardfork::Zero6, 0)); - - // Zero7 activates by timestamp (Arc convention from Zero7 onward). - assert!(forks.is_fork_active_at_timestamp(ArcHardfork::Zero7, 0)); - assert!(!forks.is_fork_active_at_block(ArcHardfork::Zero7, 0)); - } - - #[test] - fn test_arc_devnet_forks() { - let forks = ARC_DEVNET_HARDFORKS.clone(); - assert_base_hardforks(&forks); - assert_eq!(forks.len(), 23); - - // verify hardfork zero3 block - assert_eq!( - forks.get(ArcHardfork::Zero3), - Some(ForkCondition::Block( - ARC_ZERO3_HARDFORK_BLOCK_ACTIVATION_DEVNET - )) - ); - assert!(!forks.is_fork_active_at_block( - ArcHardfork::Zero3, - ARC_ZERO3_HARDFORK_BLOCK_ACTIVATION_DEVNET - 1 - )); - assert!(forks.is_fork_active_at_block( - ArcHardfork::Zero3, - ARC_ZERO3_HARDFORK_BLOCK_ACTIVATION_DEVNET - )); - - // verify hardfork zero4 block - assert_eq!( - forks.get(ArcHardfork::Zero4), - Some(ForkCondition::Block( - ARC_ZERO4_HARDFORK_BLOCK_ACTIVATION_DEVNET - )) - ); - assert!(!forks.is_fork_active_at_block( - ArcHardfork::Zero4, - ARC_ZERO4_HARDFORK_BLOCK_ACTIVATION_DEVNET - 1 - )); - assert!(forks.is_fork_active_at_block( - ArcHardfork::Zero4, - ARC_ZERO4_HARDFORK_BLOCK_ACTIVATION_DEVNET - )); - - // verify hardfork zero5 block - assert_eq!( - forks.get(ArcHardfork::Zero5), - Some(ForkCondition::Block( - ARC_ZERO5_HARDFORK_BLOCK_ACTIVATION_DEVNET - )) - ); - assert!(!forks.is_fork_active_at_block( - ArcHardfork::Zero5, - ARC_ZERO5_HARDFORK_BLOCK_ACTIVATION_DEVNET - 1 - )); - assert!(forks.is_fork_active_at_block( - ArcHardfork::Zero5, - ARC_ZERO5_HARDFORK_BLOCK_ACTIVATION_DEVNET - )); - - // verify osaka timestamp - assert_eq!( - forks.get(EthereumHardfork::Osaka), - Some(ForkCondition::Timestamp( - ARC_OSAKA_HARDFORK_TIMESTAMP_ACTIVATION_DEVNET - )) - ); - assert!(!forks.is_fork_active_at_timestamp( - EthereumHardfork::Osaka, - ARC_OSAKA_HARDFORK_TIMESTAMP_ACTIVATION_DEVNET - 1 - )); - assert!(forks.is_fork_active_at_timestamp( - EthereumHardfork::Osaka, - ARC_OSAKA_HARDFORK_TIMESTAMP_ACTIVATION_DEVNET - )); - - // verify hardfork zero6 block - assert_eq!( - forks.get(ArcHardfork::Zero6), - Some(ForkCondition::Block( - ARC_ZERO6_HARDFORK_BLOCK_ACTIVATION_DEVNET - )) - ); - assert!(!forks.is_fork_active_at_block( - ArcHardfork::Zero6, - ARC_ZERO6_HARDFORK_BLOCK_ACTIVATION_DEVNET - 1 - )); - assert!(forks.is_fork_active_at_block( - ArcHardfork::Zero6, - ARC_ZERO6_HARDFORK_BLOCK_ACTIVATION_DEVNET - )); - - // verify hardfork zero7 timestamp - assert_eq!( - forks.get(ArcHardfork::Zero7), - Some(ForkCondition::Timestamp( - ARC_ZERO7_HARDFORK_TIMESTAMP_ACTIVATION_DEVNET - )) - ); - assert!(!forks.is_fork_active_at_timestamp( - ArcHardfork::Zero7, - ARC_ZERO7_HARDFORK_TIMESTAMP_ACTIVATION_DEVNET - 1 - )); - assert!(forks.is_fork_active_at_timestamp( - ArcHardfork::Zero7, - ARC_ZERO7_HARDFORK_TIMESTAMP_ACTIVATION_DEVNET - )); - } - - #[test] - fn test_arc_testnet_forks() { - let forks = ARC_TESTNET_HARDFORKS.clone(); - assert_base_hardforks(&forks); - assert_eq!(forks.len(), 23); - - // verify hardfork zero3 block - assert_eq!( - forks.get(ArcHardfork::Zero3), - Some(ForkCondition::Block( - ARC_ZERO3_HARDFORK_BLOCK_ACTIVATION_TESTNET - )) - ); - assert!(!forks.is_fork_active_at_block( - ArcHardfork::Zero3, - ARC_ZERO3_HARDFORK_BLOCK_ACTIVATION_TESTNET - 1 - )); - assert!(forks.is_fork_active_at_block( - ArcHardfork::Zero3, - ARC_ZERO3_HARDFORK_BLOCK_ACTIVATION_TESTNET - )); - - // verify hardfork zero4 block - assert_eq!( - forks.get(ArcHardfork::Zero4), - Some(ForkCondition::Block( - ARC_ZERO4_HARDFORK_BLOCK_ACTIVATION_TESTNET - )) - ); - assert!(!forks.is_fork_active_at_block( - ArcHardfork::Zero4, - ARC_ZERO4_HARDFORK_BLOCK_ACTIVATION_TESTNET - 1 - )); - assert!(forks.is_fork_active_at_block( - ArcHardfork::Zero4, - ARC_ZERO4_HARDFORK_BLOCK_ACTIVATION_TESTNET - )); - - // verify osaka timestamp - assert_eq!( - forks.get(EthereumHardfork::Osaka), - Some(ForkCondition::Timestamp( - ARC_OSAKA_HARDFORK_TIMESTAMP_ACTIVATION_TESTNET - )) - ); - assert!(!forks.is_fork_active_at_timestamp( - EthereumHardfork::Osaka, - ARC_OSAKA_HARDFORK_TIMESTAMP_ACTIVATION_TESTNET - 1 - )); - assert!(forks.is_fork_active_at_timestamp( - EthereumHardfork::Osaka, - ARC_OSAKA_HARDFORK_TIMESTAMP_ACTIVATION_TESTNET - )); - - // verify hardfork zero5 timestamp (testnet activates Zero5 by timestamp) - assert_eq!( - forks.get(ArcHardfork::Zero5), - Some(ForkCondition::Timestamp( - ARC_ZERO5_HARDFORK_TIMESTAMP_ACTIVATION_TESTNET - )) - ); - assert!(!forks.is_fork_active_at_timestamp( - ArcHardfork::Zero5, - ARC_ZERO5_HARDFORK_TIMESTAMP_ACTIVATION_TESTNET - 1 - )); - assert!(forks.is_fork_active_at_timestamp( - ArcHardfork::Zero5, - ARC_ZERO5_HARDFORK_TIMESTAMP_ACTIVATION_TESTNET - )); - - // verify hardfork zero6 timestamp (testnet activates Zero6 by timestamp) - assert_eq!( - forks.get(ArcHardfork::Zero6), - Some(ForkCondition::Timestamp( - ARC_ZERO6_HARDFORK_TIMESTAMP_ACTIVATION_TESTNET - )) - ); - assert!(!forks.is_fork_active_at_timestamp( - ArcHardfork::Zero6, - ARC_ZERO6_HARDFORK_TIMESTAMP_ACTIVATION_TESTNET - 1 - )); - assert!(forks.is_fork_active_at_timestamp( - ArcHardfork::Zero6, - ARC_ZERO6_HARDFORK_TIMESTAMP_ACTIVATION_TESTNET - )); - - // verify hardfork zero7 timestamp - assert_eq!( - forks.get(ArcHardfork::Zero7), - Some(ForkCondition::Timestamp( - ARC_ZERO7_HARDFORK_TIMESTAMP_ACTIVATION_TESTNET - )) - ); - assert!(!forks.is_fork_active_at_timestamp( - ArcHardfork::Zero7, - ARC_ZERO7_HARDFORK_TIMESTAMP_ACTIVATION_TESTNET - 1 - )); - assert!(forks.is_fork_active_at_timestamp( - ArcHardfork::Zero7, - ARC_ZERO7_HARDFORK_TIMESTAMP_ACTIVATION_TESTNET - )); - } - - /// Per-network policy: from a given cutoff hardfork (inclusive) onward, all - /// declarations MUST activate by timestamp. - /// - /// Background — once any timestamp-based fork is in a network's `ChainHardforks`, - /// adding a future `ForkCondition::Block(n>0)` to the same table breaks EIP-2124 - /// peering with binaries that don't have the new fork declared (the BTreeMap - /// ordering in alloy-eip2124 folds the new Block key into the existing Time - /// entry's cumulative hash). The cutoff is per-network: - /// - /// - mainnet: Zero7+ (Zero3-Zero6 are at Block(0), filtered out by alloy - /// before the cumulative hash is built — they don't trigger the bug). - /// - devnet: Zero7+. Zero5/Zero6 were declared as Block before this invariant - /// was understood; the v0.6.1↔v0.7.1 mismatch was the operational cost. - /// Going forward, Zero7+ MUST be Timestamp to prevent recurrence. - /// - testnet: Zero5+. Testnet has external validators we cannot coordinate - /// a chainspec change with, so the cutoff is stricter — every fork after - /// Osaka must be Timestamp. - /// - /// Localdev is intentionally omitted: it activates everything at genesis, and - /// `Block(0)` / `Timestamp(0)` keys are filtered out by alloy-eip2124, so no - /// future-block-fork shape can arise. - #[test] - fn test_no_future_block_forks_per_network() { - // `ArcHardfork::VARIANTS` is the canonical ordering (Zero3, Zero4, ..., Zero7, - // and any future variants appended to the enum). The cutoff names where the - // timestamp-only invariant starts taking effect. - let policy: &[(&str, &ChainHardforks, ArcHardfork)] = &[ - ("devnet", &*ARC_DEVNET_HARDFORKS, ArcHardfork::Zero7), - ("testnet", &*ARC_TESTNET_HARDFORKS, ArcHardfork::Zero5), - ("mainnet", &*ARC_MAINNET_HARDFORKS, ArcHardfork::Zero7), - ]; - - for &(name, forks, cutoff) in policy { - let cutoff_idx = ArcHardfork::VARIANTS - .iter() - .position(|hf| *hf == cutoff) - .expect("cutoff hardfork must be in ArcHardfork::VARIANTS"); - - for hf in ArcHardfork::VARIANTS[cutoff_idx..].iter().copied() { - let Some(cond) = forks.get(hf) else { continue }; - match cond { - ForkCondition::Timestamp(_) => {} - ForkCondition::Block(0) => {} // genesis-equivalent, filtered by alloy-eip2124 - other => panic!( - "{name}: hardfork {hf:?} must activate by timestamp \ - (or be Block(0)), got {other:?}. Cutoff for this network \ - is {cutoff:?}+. See test_arc_hardfork_ids for the EIP-2124 \ - invariant.", - ), - } - } - } - } - - /// `is_arc_fork_active` must dispatch on the fork's condition variant — a block-based - /// fork only cares about `block_number`, a timestamp-based fork only cares about - /// `block_timestamp`. The OR cannot cross-talk between dimensions. - #[test] - fn test_is_arc_fork_active_dispatches_by_variant() { - // Build a minimal chainspec with Zero3 as Block(100) and Zero5 as Timestamp(2_000). - let mut forks = BASE_FORKS.clone(); - forks.insert(ArcHardfork::Zero3.boxed(), ForkCondition::Block(100)); - forks.insert(ArcHardfork::Zero5.boxed(), ForkCondition::Timestamp(2_000)); - let spec = crate::chainspec::ArcChainSpec::new(reth_chainspec::ChainSpec { - hardforks: forks, - ..reth_chainspec::ChainSpec::from_genesis(alloy_genesis::Genesis::default()) - }); - - // Zero3 is Block(100). Only block_number matters. - assert!(!is_arc_fork_active(&spec, ArcHardfork::Zero3, 99, 0)); - assert!(is_arc_fork_active(&spec, ArcHardfork::Zero3, 100, 0)); - // A block_timestamp huge enough to exceed Zero5's Timestamp(2_000) must NOT - // accidentally activate Zero3 — Zero3's variant is Block, so the timestamp branch - // returns false for it regardless of value. - assert!(!is_arc_fork_active(&spec, ArcHardfork::Zero3, 99, u64::MAX)); - - // Zero5 is Timestamp(2_000). Only block_timestamp matters. - assert!(!is_arc_fork_active(&spec, ArcHardfork::Zero5, 0, 1_999)); - assert!(is_arc_fork_active(&spec, ArcHardfork::Zero5, 0, 2_000)); - // Likewise a block_number that coincidentally exceeds Zero5's u64 value (2_000) - // must NOT activate Zero5 — Zero5's variant is Timestamp, the block branch is false. - assert!(!is_arc_fork_active( - &spec, - ArcHardfork::Zero5, - u64::MAX, - 1_999 - )); - - // Undeclared fork: both branches false. - assert!(!is_arc_fork_active( - &spec, - ArcHardfork::Zero7, - u64::MAX, - u64::MAX - )); - } -} diff --git a/crates/execution-config/src/lib.rs b/crates/execution-config/src/lib.rs deleted file mode 100644 index 97ce670..0000000 --- a/crates/execution-config/src/lib.rs +++ /dev/null @@ -1,30 +0,0 @@ -// Copyright 2026 Circle Internet Group, Inc. All rights reserved. -// -// SPDX-License-Identifier: Apache-2.0 -// -// Licensed under the Apache License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. -// You may obtain a copy of the License at -// -// http://www.apache.org/licenses/LICENSE-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// See the License for the specific language governing permissions and -// limitations under the License. - -//! Arc Node Configuration -//! -//! This crate contains types and functions for the configuration of the Arc node. - -pub use arc_shared::chain_ids; -pub mod addresses_denylist; -pub mod call_from; -pub mod chainspec; -pub mod defaults; -pub mod follow; -pub mod gas_fee; -pub mod hardforks; -pub mod native_coin_control; -pub mod protocol_config; diff --git a/crates/execution-config/src/native_coin_control.rs b/crates/execution-config/src/native_coin_control.rs deleted file mode 100644 index dd9f891..0000000 --- a/crates/execution-config/src/native_coin_control.rs +++ /dev/null @@ -1,82 +0,0 @@ -// Copyright 2026 Circle Internet Group, Inc. All rights reserved. -// -// SPDX-License-Identifier: Apache-2.0 -// -// Licensed under the Apache License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. -// You may obtain a copy of the License at -// -// http://www.apache.org/licenses/LICENSE-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// See the License for the specific language governing permissions and -// limitations under the License. - -//! Native coin control storage helpers. - -use alloy_primitives::{b256, keccak256, Address, B256}; -use revm_primitives::U256; - -/// Solidity mapping slot index for NativeCoinControl `isBlocklisted`. -pub const BLOCKLIST_MAPPING_SLOT: B256 = - b256!("0000000000000000000000000000000000000000000000000000000000000002"); - -/// Computes the storage slot for a mapping key of type address. -/// -/// Implements Solidity's mapping storage slot calculation: -/// Formula: `keccak256(h(k) . p)`, where: -/// - `k` is the mapping key (address) -/// - `p` is the mapping slot position ([`BLOCKLIST_MAPPING_SLOT`]) -/// - `h` left-pads the key to 32 bytes -/// - `.` is concatenation -#[inline] -pub fn compute_is_blocklisted_storage_slot(key: Address) -> B256 { - // Left-pad address to 32 bytes (addresses are 20 bytes, so 12 zero bytes prefix). - let mut key_bytes = [0u8; 32]; - key_bytes[12..].copy_from_slice(key.as_ref()); - - // Concatenate key and slot, then hash. - let mut data = [0u8; 64]; - data[..32].copy_from_slice(&key_bytes); - data[32..].copy_from_slice(BLOCKLIST_MAPPING_SLOT.as_ref()); - - keccak256(data) -} - -/// Returns true if a blocklist storage word means "blocked". -#[inline] -pub fn is_blocklisted_status(status: U256) -> bool { - status != U256::ZERO -} - -#[cfg(test)] -mod tests { - use super::{compute_is_blocklisted_storage_slot, is_blocklisted_status}; - use alloy_primitives::{address, b256}; - use revm_primitives::U256; - - #[test] - fn is_blocklisted_status_returns_false_for_zero() { - assert!(!is_blocklisted_status(U256::ZERO)); - } - - #[test] - fn is_blocklisted_status_returns_true_for_non_zero() { - assert!(is_blocklisted_status(U256::from(1u64))); - assert!(is_blocklisted_status(U256::MAX)); - } - - #[test] - fn compute_is_blocklisted_storage_slot_matches() { - // Example account taken from `assets/localdev/genesis.json` alloc. - // Expected slot computed via: - // cast index address 0xD308a07F97db36C338e8FE2AfB09267781d00811 2 - let account = address!("0xD308a07F97db36C338e8FE2AfB09267781d00811"); - let expected_slot = - b256!("c0814ebfa96e99aee5c17f259ae3205e7b664343916807a4a968c9f94e32f89b"); - let actual = compute_is_blocklisted_storage_slot(account); - assert_eq!(actual, expected_slot); - } -} diff --git a/crates/execution-config/src/protocol_config.rs b/crates/execution-config/src/protocol_config.rs deleted file mode 100644 index feb61fe..0000000 --- a/crates/execution-config/src/protocol_config.rs +++ /dev/null @@ -1,284 +0,0 @@ -// Copyright 2025 Circle Internet Group, Inc. All rights reserved. -// -// SPDX-License-Identifier: Apache-2.0 -// -// Licensed under the Apache License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. -// You may obtain a copy of the License at -// -// http://www.apache.org/licenses/LICENSE-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// See the License for the specific language governing permissions and -// limitations under the License. - -//! ProtocolConfig -//! -//! This module contains functions and types for reading data from the ProtocolConfig contract. - -use alloy_primitives::address; -use alloy_primitives::Bytes; -use alloy_sol_types::sol; -use alloy_sol_types::SolCall; -use reth_evm::Evm; -use revm::handler::SYSTEM_ADDRESS; -use revm::DatabaseCommit; -use revm_primitives::Address; - -/// Error types for ProtocolConfig contract interactions -#[derive(Debug, thiserror::Error)] -pub enum ProtocolConfigError { - #[error("System call execution failed: {0:?}")] - SystemCallFailed(revm::context::result::ExecutionResult), - #[error("ProtocolConfig contract returned empty output")] - EmptyOutput, - #[error("Failed to decode contract response: {0}")] - DecodingError(#[from] alloy_sol_types::Error), - #[error("EVM execution error: {0}")] - EvmError(String), -} - -// Constants - -// ProtocolConfig contract address -pub const PROTOCOL_CONFIG_ADDRESS: Address = address!("0x3600000000000000000000000000000000000001"); - -sol! { - /// ProtocolConfig interface for gas and consensus parameters - interface IProtocolConfig { - /// FeeParams struct matching the contract definition - struct FeeParams { - uint64 alpha; - uint64 kRate; - uint64 inverseElasticityMultiplier; - uint256 minBaseFee; - uint256 maxBaseFee; - uint256 blockGasLimit; - } - - /// Returns the current fee parameters - function feeParams() external view returns (FeeParams params); - } -} - -/// Returns the gas limit from `fee_params` if it is representable as `u64` and -/// within the configured bounds, otherwise returns the default. -/// -/// Used by both the proposer (payload building) and the receiver (pre-execution -/// validation) to derive the expected block gas limit from ProtocolConfig. -pub fn expected_gas_limit( - fee_params: Option<&IProtocolConfig::FeeParams>, - config: &crate::chainspec::BlockGasLimitConfig, -) -> u64 { - fee_params - .and_then(|fp| fp.blockGasLimit.try_into().ok()) - .filter(|&gl: &u64| gl >= config.min() && gl <= config.max()) - .unwrap_or(config.default()) -} - -/// Clamps the base fee based on configurations -pub fn determine_bounded_base_fee(fee_params: &IProtocolConfig::FeeParams, base_fee: u64) -> u64 { - let configured_max = fee_params.maxBaseFee.try_into().unwrap_or(u64::MAX); - let configured_min = fee_params.minBaseFee.try_into().unwrap_or(u64::MAX); - - // Nonsensical range - if configured_max == 0 || configured_max < configured_min { - base_fee - } else { - base_fee.clamp(configured_min, configured_max) - } -} - -/// Query the ProtocolConfig system contract for the configured fee parameters -/// -/// Returns the fee parameters if successfully queried, -/// or `Err(ProtocolConfigError)` if there was an error during execution, -/// contract deployment issues, or empty output. -pub fn retrieve_fee_params( - evm: &mut E, -) -> Result> -where - E: Evm, - E::DB: DatabaseCommit, -{ - let call_data = IProtocolConfig::feeParamsCall {}.abi_encode(); - - let result_and_state = evm - .transact_system_call( - SYSTEM_ADDRESS, - PROTOCOL_CONFIG_ADDRESS, - Bytes::from(call_data), - ) - .map_err(|e| ProtocolConfigError::EvmError(format!("{e:?}")))?; - - if !result_and_state.result.is_success() { - return Err(ProtocolConfigError::SystemCallFailed( - result_and_state.result, - )); - } - - let output = result_and_state - .result - .output() - .ok_or(ProtocolConfigError::EmptyOutput)?; - - let fee_params = IProtocolConfig::feeParamsCall::abi_decode_returns(output) - .map_err(ProtocolConfigError::DecodingError)?; - - Ok(fee_params) -} - -#[cfg(test)] -mod tests { - use super::*; - use crate::chainspec::BlockGasLimitConfig; - use reth_ethereum::evm::revm::primitives::U256; - - fn fee_params_with(block_gas_limit: U256) -> IProtocolConfig::FeeParams { - IProtocolConfig::FeeParams { - alpha: 0, - kRate: 0, - inverseElasticityMultiplier: 0, - minBaseFee: U256::from(0), - maxBaseFee: U256::from(0), - blockGasLimit: block_gas_limit, - } - } - - fn fee_params_with_min_max(min: U256, max: U256) -> IProtocolConfig::FeeParams { - IProtocolConfig::FeeParams { - alpha: 0, - kRate: 0, - inverseElasticityMultiplier: 0, - minBaseFee: min, - maxBaseFee: max, - blockGasLimit: U256::from(30_000_000u64), - } - } - - #[test] - fn determine_bounded_base_fee_table() { - struct Case { - name: &'static str, - min: U256, - max: U256, - base: u64, - expect: u64, - } - - let cases = vec![ - // No max configured (max==0) -> passthrough - Case { - name: "no_max_configured_passthrough", - min: U256::from(0), - max: U256::from(0), - base: 1_000_000, - expect: 1_000_000, - }, - // Invalid range (max < min) -> passthrough - Case { - name: "invalid_range_passthrough", - min: U256::from(2_000_000u64), - max: U256::from(1_000_000u64), - base: 1_500_000, - expect: 1_500_000, - }, - // Clamp up to min - Case { - name: "below_min_clamped_up", - min: U256::from(1_000_000u64), - max: U256::from(10_000_000u64), - base: 999_999, - expect: 1_000_000, - }, - // Clamp down to max - Case { - name: "above_max_clamped_down", - min: U256::from(1_000_000u64), - max: U256::from(10_000_000u64), - base: 10_000_001, - expect: 10_000_000, - }, - // Within range unchanged - Case { - name: "within_range_unchanged", - min: U256::from(1_000_000u64), - max: U256::from(10_000_000u64), - base: 4_000_000, - expect: 4_000_000, - }, - // Below min, narrow range - Case { - name: "below_min_narrow_Range", - min: U256::from(1_000_000u64), - max: U256::from(1_000_000u64), - base: 900_000, - expect: 1_000_000u64, - }, - // Above max, narrow range - Case { - name: "above_max_narrow_Range", - min: U256::from(1_000_000u64), - max: U256::from(1_000_000u64), - base: 2_000_000, - expect: 1_000_000u64, - }, - ]; - - for c in cases { - let fee = fee_params_with_min_max(c.min, c.max); - let got = determine_bounded_base_fee(&fee, c.base); - assert_eq!(got, c.expect, "case: {}", c.name); - } - } - - #[test] - fn expected_gas_limit_returns_value_when_in_bounds() { - let fee = fee_params_with(U256::from(30_000_000u64)); - let config = BlockGasLimitConfig::new(1_000_000, 1_000_000_000, 5_000_000); - assert_eq!(expected_gas_limit(Some(&fee), &config), 30_000_000); - } - - #[test] - fn expected_gas_limit_returns_default_when_below_min() { - let fee = fee_params_with(U256::from(500_000u64)); - let config = BlockGasLimitConfig::new(1_000_000, 1_000_000_000, 30_000_000); - assert_eq!(expected_gas_limit(Some(&fee), &config), 30_000_000); - } - - #[test] - fn expected_gas_limit_returns_default_when_above_max() { - let fee = fee_params_with(U256::from(2_000_000_000u64)); - let config = BlockGasLimitConfig::new(1_000_000, 1_000_000_000, 30_000_000); - assert_eq!(expected_gas_limit(Some(&fee), &config), 30_000_000); - } - - #[test] - fn expected_gas_limit_returns_default_when_overflows_u64() { - let fee = fee_params_with(U256::from(u64::MAX) + U256::from(1)); - let config = BlockGasLimitConfig::new(1_000_000, 1_000_000_000, 30_000_000); - assert_eq!(expected_gas_limit(Some(&fee), &config), 30_000_000); - } - - #[test] - fn expected_gas_limit_returns_default_when_none() { - let config = BlockGasLimitConfig::new(1_000_000, 1_000_000_000, 30_000_000); - assert_eq!(expected_gas_limit(None, &config), 30_000_000); - } - - #[test] - fn expected_gas_limit_accepts_boundary_values() { - let config = BlockGasLimitConfig::new(1_000_000, 1_000_000_000, 5_000_000); - - let fee_at_min = fee_params_with(U256::from(1_000_000u64)); - assert_eq!(expected_gas_limit(Some(&fee_at_min), &config), 1_000_000); - - let fee_at_max = fee_params_with(U256::from(1_000_000_000u64)); - assert_eq!( - expected_gas_limit(Some(&fee_at_max), &config), - 1_000_000_000 - ); - } -} diff --git a/crates/execution-e2e/Cargo.toml b/crates/execution-e2e/Cargo.toml deleted file mode 100644 index 18f7c03..0000000 --- a/crates/execution-e2e/Cargo.toml +++ /dev/null @@ -1,65 +0,0 @@ -[package] -name = "arc-execution-e2e" -version.workspace = true -edition.workspace = true -repository.workspace = true -license.workspace = true -rust-version.workspace = true -publish.workspace = true - -[features] -integration = ["arc-evm-node/integration"] - -[dependencies] -alloy-consensus.workspace = true -alloy-eips.workspace = true -alloy-network.workspace = true - -# Alloy -alloy-primitives.workspace = true -alloy-rpc-types-engine.workspace = true -alloy-rpc-types-eth.workspace = true -alloy-rpc-types-trace.workspace = true -alloy-sol-types.workspace = true -arc-evm-node = { path = "../evm-node" } -# Arc crates -arc-execution-config = { path = "../execution-config", features = ["test-utils"] } -arc-execution-txpool = { path = "../execution-txpool" } -arc-precompiles = { path = "../precompiles" } - -# Error handling -eyre.workspace = true -futures-util.workspace = true -jsonrpsee = { workspace = true, features = ["http-client"] } -reth-chainspec.workspace = true - -# Reth test utilities -reth-e2e-test-utils.workspace = true -reth-ethereum.workspace = true -reth-ethereum-engine-primitives.workspace = true -reth-ethereum-primitives.workspace = true -reth-node-api.workspace = true -reth-node-builder = { workspace = true, features = ["test-utils"] } -reth-node-core.workspace = true - -# Reth primitives -reth-primitives-traits.workspace = true -reth-provider.workspace = true -reth-rpc-api.workspace = true -reth-rpc-server-types.workspace = true -reth-tasks.workspace = true -reth-transaction-pool.workspace = true -serde_json.workspace = true - -# Async -tokio = { workspace = true, features = ["full"] } -tracing.workspace = true - -[dev-dependencies] -arc-precompiles = { path = "../precompiles", features = ["test-utils"] } -reth-tracing.workspace = true -revm-bytecode.workspace = true -rstest.workspace = true - -[lints] -workspace = true diff --git a/crates/execution-e2e/src/action.rs b/crates/execution-e2e/src/action.rs deleted file mode 100644 index 00a4153..0000000 --- a/crates/execution-e2e/src/action.rs +++ /dev/null @@ -1,43 +0,0 @@ -// Copyright 2026 Circle Internet Group, Inc. All rights reserved. -// -// SPDX-License-Identifier: Apache-2.0 -// -// Licensed under the Apache License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. -// You may obtain a copy of the License at -// -// http://www.apache.org/licenses/LICENSE-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// See the License for the specific language governing permissions and -// limitations under the License. - -//! Action trait and utilities for Arc e2e tests. - -use crate::ArcEnvironment; -use futures_util::future::BoxFuture; - -/// An action that can be executed on the Arc test environment. -/// -/// Actions are the building blocks of test scenarios. -pub trait Action: Send + 'static { - /// Executes the action on the given environment. - fn execute<'a>(&'a mut self, env: &'a mut ArcEnvironment) -> BoxFuture<'a, eyre::Result<()>>; -} - -/// Type-erased wrapper for actions, allowing storage in heterogeneous collections. -pub struct ActionBox(Box); - -impl ActionBox { - /// Creates a new boxed action. - pub fn new(action: A) -> Self { - Self(Box::new(action)) - } - - /// Executes the wrapped action. - pub async fn execute(&mut self, env: &mut ArcEnvironment) -> eyre::Result<()> { - self.0.execute(env).await - } -} diff --git a/crates/execution-e2e/src/actions/assert_named.rs b/crates/execution-e2e/src/actions/assert_named.rs deleted file mode 100644 index 3ba3f47..0000000 --- a/crates/execution-e2e/src/actions/assert_named.rs +++ /dev/null @@ -1,248 +0,0 @@ -// Copyright 2026 Circle Internet Group, Inc. All rights reserved. -// -// SPDX-License-Identifier: Apache-2.0 -// -// Licensed under the Apache License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. -// You may obtain a copy of the License at -// -// http://www.apache.org/licenses/LICENSE-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// See the License for the specific language governing permissions and -// limitations under the License. - -//! Environment-aware assertion actions that resolve named addresses at execution time. -//! -//! These complement the static `AssertTxLogs` and `AssertBalance` actions for cases -//! where the target address is only known after a prior action (e.g. `StoreDeployedAddress`) -//! has run. - -use crate::{action::Action, ArcEnvironment}; -use alloy_primitives::{Address, U256}; -use alloy_rpc_types_eth::BlockNumberOrTag; -use futures_util::future::BoxFuture; -use reth_provider::ReceiptProvider; -use reth_rpc_api::EthApiClient; -use tracing::info; - -use super::assert_tx_logs::{validate_event_log, TRANSFER_EVENT_SIGNATURE}; - -/// An address reference that can be either a concrete address or a named address -/// resolved from the environment at execution time. -#[derive(Clone)] -pub enum AddressRef { - /// A concrete address known at builder time. - Literal(Address), - /// A named address resolved from the environment at execution time. - Named(String), -} - -impl AddressRef { - fn resolve(&self, env: &ArcEnvironment) -> eyre::Result
{ - match self { - Self::Literal(addr) => Ok(*addr), - Self::Named(name) => env - .get_address(name) - .copied() - .ok_or_else(|| eyre::eyre!("Named address '{}' not found in environment", name)), - } - } -} - -impl From
for AddressRef { - fn from(addr: Address) -> Self { - Self::Literal(addr) - } -} - -/// Asserts an EIP-7708 Transfer event at a specific log index in a named transaction's receipt. -/// -/// Resolves `from` and `to` from the environment at execution time, supporting -/// both literal addresses and named deployed-contract addresses. -pub struct AssertTransferEvent { - tx_name: String, - log_index: usize, - from: AddressRef, - to: AddressRef, - value: U256, -} - -impl AssertTransferEvent { - /// Creates a new transfer event assertion. - /// - /// `from` and `to` accept either `Address` or `AddressRef::Named("name")`. - pub fn new( - tx_name: impl Into, - log_index: usize, - from: impl Into, - to: impl Into, - value: U256, - ) -> Self { - Self { - tx_name: tx_name.into(), - log_index, - from: from.into(), - to: to.into(), - value, - } - } - - /// Helper: named address reference for use with `from` / `to` parameters. - pub fn named(name: impl Into) -> AddressRef { - AddressRef::Named(name.into()) - } -} - -impl Action for AssertTransferEvent { - fn execute<'a>(&'a mut self, env: &'a mut ArcEnvironment) -> BoxFuture<'a, eyre::Result<()>> { - Box::pin(async move { - let from = self.from.resolve(env)?; - let to = self.to.resolve(env)?; - - let tx_hash = *env.get_tx_hash(&self.tx_name).ok_or_else(|| { - eyre::eyre!("Transaction '{}' not found in environment", self.tx_name) - })?; - - let receipt = env - .node() - .inner - .provider() - .receipt_by_hash(tx_hash)? - .ok_or_else(|| { - eyre::eyre!("Receipt not found for tx '{}' ({})", self.tx_name, tx_hash) - })?; - - let log = receipt.logs.get(self.log_index).ok_or_else(|| { - eyre::eyre!( - "Tx '{}': no log at index {} (total: {})", - self.tx_name, - self.log_index, - receipt.logs.len() - ) - })?; - - validate_event_log( - &self.tx_name, - self.log_index, - log, - TRANSFER_EVENT_SIGNATURE, - from, - to, - self.value, - )?; - - info!( - tx = %self.tx_name, - index = self.log_index, - from = %from, - to = %to, - value = %self.value, - "Transfer event assertion passed" - ); - Ok(()) - }) - } -} - -/// Asserts account balance for a named address from the environment. -pub struct AssertNamedBalance { - address_name: String, - expected: U256, -} - -impl AssertNamedBalance { - /// Assert the balance of a named address equals the expected value. - pub fn of(address_name: impl Into) -> AssertNamedBalanceBuilder { - AssertNamedBalanceBuilder { - address_name: address_name.into(), - } - } -} - -/// Builder for `AssertNamedBalance`. -pub struct AssertNamedBalanceBuilder { - address_name: String, -} - -impl AssertNamedBalanceBuilder { - /// Assert exact balance match. - pub fn equals(self, expected: U256) -> AssertNamedBalance { - AssertNamedBalance { - address_name: self.address_name, - expected, - } - } -} - -impl Action for AssertNamedBalance { - fn execute<'a>(&'a mut self, env: &'a mut ArcEnvironment) -> BoxFuture<'a, eyre::Result<()>> { - Box::pin(async move { - let address = *env.get_address(&self.address_name).ok_or_else(|| { - eyre::eyre!( - "Named address '{}' not found in environment", - self.address_name - ) - })?; - - let block_number = env.block_number(); - - info!( - name = %self.address_name, - address = %address, - expected = %self.expected, - block_number, - "Asserting named account balance" - ); - - let client = env - .node() - .rpc_client() - .ok_or_else(|| eyre::eyre!("RPC client not available"))?; - - let balance: U256 = >::balance( - &client, - address, - Some(BlockNumberOrTag::Number(block_number).into()), - ) - .await - .map_err(|e| { - eyre::eyre!( - "eth_getBalance failed for '{}' ({}) at block {}: {}", - self.address_name, - address, - block_number, - e - ) - })?; - - if balance != self.expected { - return Err(eyre::eyre!( - "Balance mismatch for '{}' ({}): expected {}, got {} (block {})", - self.address_name, - address, - self.expected, - balance, - block_number - )); - } - - info!( - name = %self.address_name, - address = %address, - balance = %balance, - "Named balance assertion passed" - ); - Ok(()) - }) - } -} diff --git a/crates/execution-e2e/src/actions/assert_tx_logs.rs b/crates/execution-e2e/src/actions/assert_tx_logs.rs deleted file mode 100644 index f1f3eb5..0000000 --- a/crates/execution-e2e/src/actions/assert_tx_logs.rs +++ /dev/null @@ -1,337 +0,0 @@ -// Copyright 2026 Circle Internet Group, Inc. All rights reserved. -// -// SPDX-License-Identifier: Apache-2.0 -// -// Licensed under the Apache License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. -// You may obtain a copy of the License at -// -// http://www.apache.org/licenses/LICENSE-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// See the License for the specific language governing permissions and -// limitations under the License. - -//! Receipt log assertion action for EIP-7708 e2e tests. - -use crate::{action::Action, ArcEnvironment}; -use alloy_primitives::{Address, Bytes, B256, U256}; -use alloy_sol_types::{sol, SolEvent}; -use futures_util::future::BoxFuture; -use reth_provider::ReceiptProvider; -use tracing::info; - -/// Expected log entry at a specific index. -enum ExpectedLog { - /// Exact match on address, topics, and data. - Exact { - address: Address, - topics: Vec, - data: Bytes, - }, - /// ERC-20 Transfer(address,address,uint256) decode helper. - TransferEvent { - from: Address, - to: Address, - value: U256, - }, - /// NativeCoinTransferred(address,address,uint256) decode helper (pre-Zero5). - NativeCoinTransferredEvent { - from: Address, - to: Address, - amount: U256, - }, - /// Verify only the emitter address at a given index. - EmitterOnly { address: Address }, -} - -sol! { - event Transfer(address indexed from, address indexed to, uint256 value); - event NativeCoinTransferred(address indexed from, address indexed to, uint256 amount); -} - -/// keccak256("Transfer(address,address,uint256)") — derived from sol! macro. -pub const TRANSFER_EVENT_SIGNATURE: B256 = Transfer::SIGNATURE_HASH; - -/// keccak256("NativeCoinTransferred(address,address,uint256)") — derived from sol! macro. -pub const NATIVE_COIN_TRANSFERRED_SIGNATURE: B256 = NativeCoinTransferred::SIGNATURE_HASH; - -/// Validates a log entry against expected 3-topic event fields (signature, from, to, data). -/// -/// Shared by `AssertTxLogs` and `AssertTransferEvent` to avoid duplicated validation logic. -pub(crate) fn validate_event_log( - tx_name: &str, - index: usize, - log: &reth_ethereum::primitives::Log, - signature: B256, - from: Address, - to: Address, - value: U256, -) -> eyre::Result<()> { - let topics = log.topics(); - if topics.len() != 3 { - return Err(eyre::eyre!( - "Tx '{}' log[{}]: expected 3 topics, got {}", - tx_name, - index, - topics.len() - )); - } - if topics[0] != signature { - return Err(eyre::eyre!( - "Tx '{}' log[{}]: topic[0] signature mismatch: expected {}, got {}", - tx_name, - index, - signature, - topics[0] - )); - } - let expected_from = B256::left_padding_from(from.as_slice()); - if topics[1] != expected_from { - return Err(eyre::eyre!( - "Tx '{}' log[{}]: topic[1] (from) mismatch: expected {}, got {}", - tx_name, - index, - expected_from, - topics[1] - )); - } - let expected_to = B256::left_padding_from(to.as_slice()); - if topics[2] != expected_to { - return Err(eyre::eyre!( - "Tx '{}' log[{}]: topic[2] (to) mismatch: expected {}, got {}", - tx_name, - index, - expected_to, - topics[2] - )); - } - let expected_data = value.to_be_bytes::<32>(); - if log.data.data.as_ref() != expected_data.as_slice() { - return Err(eyre::eyre!( - "Tx '{}' log[{}]: data mismatch: expected {}, got {}", - tx_name, - index, - value, - log.data.data - )); - } - Ok(()) -} - -/// Asserts on receipt logs for a named transaction. -/// -/// Retrieves receipt via the provider, then validates log count -/// and individual log entries against expectations. -pub struct AssertTxLogs { - tx_name: String, - expected_log_count: Option, - expected_logs: Vec<(usize, ExpectedLog)>, -} - -impl AssertTxLogs { - /// Creates a new log assertion for the named transaction. - pub fn new(tx_name: impl Into) -> Self { - Self { - tx_name: tx_name.into(), - expected_log_count: None, - expected_logs: Vec::new(), - } - } - - /// Assert exact total number of logs. - pub fn expect_log_count(mut self, count: usize) -> Self { - self.expected_log_count = Some(count); - self - } - - /// Shorthand for `expect_log_count(0)`. - pub fn expect_no_logs(self) -> Self { - self.expect_log_count(0) - } - - /// Exact match on a single log entry. - pub fn expect_log_at( - mut self, - index: usize, - address: Address, - topics: Vec, - data: Bytes, - ) -> Self { - self.expected_logs.push(( - index, - ExpectedLog::Exact { - address, - topics, - data, - }, - )); - self - } - - /// Decode helper for ERC-20 Transfer(address,address,uint256). - pub fn expect_transfer_event( - mut self, - index: usize, - from: Address, - to: Address, - value: U256, - ) -> Self { - self.expected_logs - .push((index, ExpectedLog::TransferEvent { from, to, value })); - self - } - - /// Decode helper for pre-Zero5 NativeCoinTransferred(address,address,uint256). - pub fn expect_native_coin_transferred_event( - mut self, - index: usize, - from: Address, - to: Address, - amount: U256, - ) -> Self { - self.expected_logs.push(( - index, - ExpectedLog::NativeCoinTransferredEvent { from, to, amount }, - )); - self - } - - /// Verify only the emitter address at a given index. - pub fn expect_emitter_at(mut self, index: usize, address: Address) -> Self { - self.expected_logs - .push((index, ExpectedLog::EmitterOnly { address })); - self - } -} - -impl Action for AssertTxLogs { - fn execute<'a>(&'a mut self, env: &'a mut ArcEnvironment) -> BoxFuture<'a, eyre::Result<()>> { - Box::pin(async move { - let tx_hash = *env.get_tx_hash(&self.tx_name).ok_or_else(|| { - eyre::eyre!("Transaction '{}' not found in environment", self.tx_name) - })?; - - info!( - name = %self.tx_name, - tx_hash = %tx_hash, - "Asserting transaction receipt logs" - ); - - let receipt = env - .node() - .inner - .provider() - .receipt_by_hash(tx_hash)? - .ok_or_else(|| { - eyre::eyre!("Receipt not found for tx '{}' ({})", self.tx_name, tx_hash) - })?; - - let logs = &receipt.logs; - - // Assert log count - if let Some(expected_count) = self.expected_log_count { - if logs.len() != expected_count { - return Err(eyre::eyre!( - "Tx '{}': expected {} logs, got {}. Logs: {:?}", - self.tx_name, - expected_count, - logs.len(), - logs - )); - } - } - - // Assert individual logs - for (index, expected) in &self.expected_logs { - let log = logs.get(*index).ok_or_else(|| { - eyre::eyre!( - "Tx '{}': no log at index {} (total: {})", - self.tx_name, - index, - logs.len() - ) - })?; - - match expected { - ExpectedLog::Exact { - address, - topics, - data, - } => { - if log.address != *address { - return Err(eyre::eyre!( - "Tx '{}' log[{}]: emitter mismatch: expected {}, got {}", - self.tx_name, - index, - address, - log.address - )); - } - let log_topics: Vec = log.topics().to_vec(); - if log_topics != *topics { - return Err(eyre::eyre!( - "Tx '{}' log[{}]: topics mismatch: expected {:?}, got {:?}", - self.tx_name, - index, - topics, - log_topics - )); - } - if log.data.data != *data { - return Err(eyre::eyre!( - "Tx '{}' log[{}]: data mismatch: expected {}, got {}", - self.tx_name, - index, - data, - log.data.data - )); - } - } - ExpectedLog::TransferEvent { from, to, value } => { - validate_event_log( - &self.tx_name, - *index, - log, - TRANSFER_EVENT_SIGNATURE, - *from, - *to, - *value, - )?; - } - ExpectedLog::NativeCoinTransferredEvent { from, to, amount } => { - validate_event_log( - &self.tx_name, - *index, - log, - NATIVE_COIN_TRANSFERRED_SIGNATURE, - *from, - *to, - *amount, - )?; - } - ExpectedLog::EmitterOnly { address } => { - if log.address != *address { - return Err(eyre::eyre!( - "Tx '{}' log[{}]: emitter mismatch: expected {}, got {}", - self.tx_name, - index, - address, - log.address - )); - } - } - } - } - - info!( - name = %self.tx_name, - log_count = logs.len(), - "Receipt log assertions passed" - ); - Ok(()) - }) - } -} diff --git a/crates/execution-e2e/src/actions/assert_tx_trace.rs b/crates/execution-e2e/src/actions/assert_tx_trace.rs deleted file mode 100644 index c14365b..0000000 --- a/crates/execution-e2e/src/actions/assert_tx_trace.rs +++ /dev/null @@ -1,201 +0,0 @@ -// Copyright 2026 Circle Internet Group, Inc. All rights reserved. -// -// SPDX-License-Identifier: Apache-2.0 -// -// Licensed under the Apache License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. -// You may obtain a copy of the License at -// -// http://www.apache.org/licenses/LICENSE-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// See the License for the specific language governing permissions and -// limitations under the License. - -//! Debug trace assertion action for EIP-7708 e2e tests. - -use crate::{action::Action, ArcEnvironment}; -use alloy_rpc_types_trace::geth::{ - GethDebugBuiltInTracerType, GethDebugTracerConfig, GethDebugTracerType, - GethDebugTracingOptions, GethDefaultTracingOptions, GethTrace, -}; -use futures_util::future::BoxFuture; -use reth_rpc_api::DebugApiClient; -use tracing::info; - -/// Calls `debug_traceTransaction` for a named tx and asserts the call succeeds. -/// -/// At minimum, every test instantiates this to verify the tracer does not panic -/// on EIP-7708 transactions. Content assertions (log count, topics, data) are -/// provided as builder methods but should be commented out until the tracing bug is fixed. -pub struct AssertTxTrace { - tx_name: String, -} - -impl AssertTxTrace { - /// Creates a new trace assertion for the named transaction. - /// - /// The trace call uses `callTracer` with `{ withLog: true, onlyTopCall: false }`. - pub fn new(tx_name: impl Into) -> Self { - Self { - tx_name: tx_name.into(), - } - } -} - -impl Action for AssertTxTrace { - fn execute<'a>(&'a mut self, env: &'a mut ArcEnvironment) -> BoxFuture<'a, eyre::Result<()>> { - Box::pin(async move { - let tx_hash = *env.get_tx_hash(&self.tx_name).ok_or_else(|| { - eyre::eyre!("Transaction '{}' not found in environment", self.tx_name) - })?; - - info!( - name = %self.tx_name, - tx_hash = %tx_hash, - "Calling debug_traceTransaction with callTracer" - ); - - let client = env - .node() - .rpc_client() - .ok_or_else(|| eyre::eyre!("RPC client not available"))?; - - let opts = GethDebugTracingOptions { - tracer: Some(GethDebugTracerType::BuiltInTracer( - GethDebugBuiltInTracerType::CallTracer, - )), - tracer_config: GethDebugTracerConfig( - serde_json::json!({ "withLog": true, "onlyTopCall": false }), - ), - ..Default::default() - }; - - let trace = >::debug_trace_transaction(&client, tx_hash, Some(opts)) - .await - .map_err(|e| { - eyre::eyre!( - "debug_traceTransaction failed for tx '{}' ({}): {}", - self.tx_name, - tx_hash, - e - ) - })?; - - info!( - name = %self.tx_name, - tx_hash = %tx_hash, - trace_variant = ?std::mem::discriminant(&trace), - "debug_traceTransaction succeeded" - ); - - Ok(()) - }) - } -} - -/// Calls `debug_traceTransaction` with the default struct logger and asserts the -/// gas cost of the last occurrence of an opcode. -pub struct AssertLastOpcodeGasCost { - tx_name: String, - opcode: String, - expected_gas_cost: u64, -} - -impl AssertLastOpcodeGasCost { - /// Creates a new opcode gas-cost assertion for the named transaction. - pub fn new(tx_name: impl Into, opcode: impl Into, gas_cost: u64) -> Self { - Self { - tx_name: tx_name.into(), - opcode: opcode.into(), - expected_gas_cost: gas_cost, - } - } -} - -impl Action for AssertLastOpcodeGasCost { - fn execute<'a>(&'a mut self, env: &'a mut ArcEnvironment) -> BoxFuture<'a, eyre::Result<()>> { - Box::pin(async move { - let tx_hash = *env.get_tx_hash(&self.tx_name).ok_or_else(|| { - eyre::eyre!("Transaction '{}' not found in environment", self.tx_name) - })?; - - info!( - name = %self.tx_name, - tx_hash = %tx_hash, - opcode = %self.opcode, - expected_gas_cost = self.expected_gas_cost, - "Calling debug_traceTransaction with struct logger" - ); - - let client = env - .node() - .rpc_client() - .ok_or_else(|| eyre::eyre!("RPC client not available"))?; - - let opts = GethDebugTracingOptions { - config: GethDefaultTracingOptions::default() - .with_enable_memory(false) - .disable_stack() - .disable_storage(), - ..Default::default() - }; - - let trace = >::debug_trace_transaction(&client, tx_hash, Some(opts)) - .await - .map_err(|e| { - eyre::eyre!( - "debug_traceTransaction failed for tx '{}' ({}): {}", - self.tx_name, - tx_hash, - e - ) - })?; - - let GethTrace::Default(frame) = trace else { - return Err(eyre::eyre!( - "Expected default struct-log trace for tx '{}'", - self.tx_name - )); - }; - - let Some(log) = frame - .struct_logs - .iter() - .rev() - .find(|log| log.opcode() == self.opcode) - else { - return Err(eyre::eyre!( - "Tx '{}': opcode '{}' not found in trace", - self.tx_name, - self.opcode - )); - }; - - if log.gas_cost != self.expected_gas_cost { - return Err(eyre::eyre!( - "Tx '{}': last '{}' gas cost mismatch. Expected {}, got {}", - self.tx_name, - self.opcode, - self.expected_gas_cost, - log.gas_cost - )); - } - - info!( - name = %self.tx_name, - opcode = %self.opcode, - gas_cost = log.gas_cost, - "Opcode gas-cost assertion passed" - ); - - Ok(()) - }) - } -} diff --git a/crates/execution-e2e/src/actions/assertions.rs b/crates/execution-e2e/src/actions/assertions.rs deleted file mode 100644 index d3dd344..0000000 --- a/crates/execution-e2e/src/actions/assertions.rs +++ /dev/null @@ -1,590 +0,0 @@ -// Copyright 2026 Circle Internet Group, Inc. All rights reserved. -// -// SPDX-License-Identifier: Apache-2.0 -// -// Licensed under the Apache License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. -// You may obtain a copy of the License at -// -// http://www.apache.org/licenses/LICENSE-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// See the License for the specific language governing permissions and -// limitations under the License. - -//! Assertion actions for Arc e2e tests. - -use crate::{action::Action, ArcEnvironment}; -use alloy_consensus::TxReceipt; -use alloy_eips::Encodable2718; -use alloy_primitives::{Address, U256}; -use alloy_rpc_types_eth::BlockNumberOrTag; -use arc_execution_config::hardforks::{is_arc_fork_active, ArcHardfork}; -use futures_util::future::BoxFuture; -use reth_chainspec::{ChainSpecProvider, EthereumHardfork, EthereumHardforks}; -use reth_node_api::Block; -use reth_provider::{BlockReaderIdExt, ReceiptProvider}; -use reth_rpc_api::EthApiClient; -use tracing::info; - -/// Asserts that the current block number matches the expected value. -#[derive(Debug)] -pub struct AssertBlockNumber { - /// Expected block number. - expected: u64, -} - -impl AssertBlockNumber { - /// Creates a new AssertBlockNumber action. - pub fn new(expected: u64) -> Self { - Self { expected } - } -} - -impl Action for AssertBlockNumber { - fn execute<'a>(&'a mut self, env: &'a mut ArcEnvironment) -> BoxFuture<'a, eyre::Result<()>> { - Box::pin(async move { - let current = env.block_number(); - info!( - current_block = current, - expected_block = self.expected, - "Asserting block number" - ); - - if current != self.expected { - return Err(eyre::eyre!( - "Block number mismatch: expected {}, got {}", - self.expected, - current - )); - } - - info!(block_number = current, "Block number assertion passed"); - Ok(()) - }) - } -} - -/// Asserts that a specific hardfork is active (or not active) at the current block. -#[derive(Debug)] -pub struct AssertHardfork { - /// The hardfork to check. - hardfork: ArcHardfork, - /// Whether the hardfork should be active. - expected_active: bool, -} - -impl AssertHardfork { - /// Creates a new AssertHardfork action. - /// - /// # Arguments - /// * `hardfork` - The hardfork to check - /// * `expected_active` - Whether the hardfork should be active at current block - pub fn new(hardfork: ArcHardfork, expected_active: bool) -> Self { - Self { - hardfork, - expected_active, - } - } - - /// Asserts that the hardfork IS active at current block. - pub fn is_active(hardfork: ArcHardfork) -> Self { - Self::new(hardfork, true) - } - - /// Asserts that the hardfork is NOT active at current block. - pub fn is_not_active(hardfork: ArcHardfork) -> Self { - Self::new(hardfork, false) - } -} - -impl Action for AssertHardfork { - fn execute<'a>(&'a mut self, env: &'a mut ArcEnvironment) -> BoxFuture<'a, eyre::Result<()>> { - Box::pin(async move { - let block = env.current_block(); - let block_number = block.number; - let block_timestamp = block.timestamp; - let chain_spec = env.node().inner.provider().chain_spec(); - // An Arc hardfork can be configured as either Block or Timestamp, - // so check both — the underlying `is_fork_active_at_*` returns false - // for the wrong condition type, and we OR them together. - let is_active = is_arc_fork_active( - chain_spec.as_ref(), - self.hardfork, - block_number, - block_timestamp, - ); - - info!( - hardfork = ?self.hardfork, - block_number, - block_timestamp, - is_active, - expected_active = self.expected_active, - "Asserting hardfork status" - ); - - if is_active != self.expected_active { - return Err(eyre::eyre!( - "Hardfork {:?} at block {} (ts {}): expected active={}, got active={}", - self.hardfork, - block_number, - block_timestamp, - self.expected_active, - is_active - )); - } - - info!( - hardfork = ?self.hardfork, - block_number, - "Hardfork assertion passed" - ); - Ok(()) - }) - } -} - -/// Asserts that a timestamp-based Ethereum hardfork is active (or not active) at the current block. -/// -/// Unlike `AssertHardfork` which checks block-based Arc hardforks, this checks -/// timestamp-based Ethereum hardforks like Osaka. -#[derive(Debug)] -pub struct AssertEthereumHardfork { - /// The Ethereum hardfork to check. - hardfork: EthereumHardfork, - /// Whether the hardfork should be active. - expected_active: bool, -} - -impl AssertEthereumHardfork { - /// Asserts that the Ethereum hardfork IS active at the current block's timestamp. - pub fn is_active(hardfork: EthereumHardfork) -> Self { - Self { - hardfork, - expected_active: true, - } - } - - /// Asserts that the Ethereum hardfork is NOT active at the current block's timestamp. - pub fn is_not_active(hardfork: EthereumHardfork) -> Self { - Self { - hardfork, - expected_active: false, - } - } -} - -impl Action for AssertEthereumHardfork { - fn execute<'a>(&'a mut self, env: &'a mut ArcEnvironment) -> BoxFuture<'a, eyre::Result<()>> { - Box::pin(async move { - let timestamp = env.current_block().timestamp; - let chain_spec = env.node().inner.provider().chain_spec(); - let is_active = - chain_spec.is_ethereum_fork_active_at_timestamp(self.hardfork, timestamp); - - info!( - hardfork = ?self.hardfork, - timestamp, - is_active, - expected_active = self.expected_active, - "Asserting Ethereum hardfork status" - ); - - if is_active != self.expected_active { - return Err(eyre::eyre!( - "Ethereum hardfork {:?} at timestamp {}: expected active={}, got active={}", - self.hardfork, - timestamp, - self.expected_active, - is_active - )); - } - - info!( - hardfork = ?self.hardfork, - timestamp, - "Ethereum hardfork assertion passed" - ); - Ok(()) - }) - } -} - -/// Expected transaction execution status. -#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)] -pub enum TxStatus { - /// Transaction should succeed (status = 1). - #[default] - Success, - /// Transaction should revert (status = 0). - Reverted, -} - -/// Asserts that a transaction was included in a block and checks its execution status. -/// -/// The transaction name must match one previously sent via `SendTransaction::new("name")`. -/// Optionally specify a block number with `in_block()` to check a specific block instead -/// of the current block. -#[derive(Debug)] -pub struct AssertTxIncluded { - /// Name of the transaction to look up in `env.tx_hashes`. - name: String, - /// Expected execution status (success or reverted). - expected_status: TxStatus, - /// Expected gas used. - expected_gas_used: Option, - /// Specific block number to check. If None, uses current block. - block_number: Option, -} - -impl AssertTxIncluded { - /// Creates a new assertion for a transaction. - /// - /// The name must match a transaction previously sent via `SendTransaction::new("name")`. - pub fn new(name: impl Into) -> Self { - Self { - name: name.into(), - expected_status: TxStatus::default(), - expected_gas_used: None, - block_number: None, - } - } - - /// Sets the expected transaction execution status. - pub fn expect(mut self, expected_status: TxStatus) -> Self { - self.expected_status = expected_status; - self - } - - /// Sets the expected transaction gas used. - pub fn expect_gas_used(mut self, expected_gas_used: u64) -> Self { - self.expected_gas_used = Some(expected_gas_used); - self - } - - /// Sets a specific block number to check instead of using the current block. - pub fn in_block(mut self, block_number: u64) -> Self { - self.block_number = Some(block_number); - self - } -} - -impl Action for AssertTxIncluded { - fn execute<'a>(&'a mut self, env: &'a mut ArcEnvironment) -> BoxFuture<'a, eyre::Result<()>> { - Box::pin(async move { - // Look up transaction hash by name - let tx_hash = *env.get_tx_hash(&self.name).ok_or_else(|| { - eyre::eyre!("Transaction '{}' not found in environment", self.name) - })?; - - // Use specified block number or current block - let block_number = self.block_number.unwrap_or_else(|| env.block_number()); - - info!( - name = %self.name, - tx_hash = %tx_hash, - block_number, - expected_status = ?self.expected_status, - "Asserting transaction inclusion and status" - ); - - // Get the block from the provider - let node = env.node(); - let block = node - .inner - .provider() - .block_by_number_or_tag(BlockNumberOrTag::Number(block_number))? - .ok_or_else(|| eyre::eyre!("Block {} not found", block_number))?; - - // Check if the transaction is in the block - let tx_hashes: Vec<_> = block - .body() - .transactions() - .map(|tx| tx.trie_hash()) - .collect(); - - let tx_index = tx_hashes - .iter() - .position(|h| *h == tx_hash) - .ok_or_else(|| { - eyre::eyre!( - "Transaction '{}' ({}) not found in block {}, Block contains {} transactions: {:?}", - self.name, - tx_hash, - block_number, - tx_hashes.len(), - tx_hashes - ) - })?; - - // Get the receipt to check execution status - let receipt = node - .inner - .provider() - .receipt_by_hash(tx_hash)? - .ok_or_else(|| { - eyre::eyre!( - "Receipt not found for transaction '{}' ({})", - self.name, - tx_hash - ) - })?; - - let actual_status = if receipt.status() { - TxStatus::Success - } else { - TxStatus::Reverted - }; - - if actual_status != self.expected_status { - return Err(eyre::eyre!( - "Transaction '{}' ({}) status mismatch: expected {:?}, got {:?}", - self.name, - tx_hash, - self.expected_status, - actual_status - )); - } - if let Some(expected_gas_used) = self.expected_gas_used { - // Consensus receipts only store cumulative gas in the block. Per-tx gas (what - // JSON-RPC reports as `gasUsed`) is the delta from the previous tx in the same - // block, not `cumulative_gas_used` on the receipt alone. - let prev_cumulative_gas_used = if tx_index == 0 { - 0 - } else { - let prev_tx_hash = tx_hashes[tx_index - 1]; - node.inner - .provider() - .receipt_by_hash(prev_tx_hash)? - .ok_or_else(|| { - eyre::eyre!( - "Previous receipt not found for transaction '{}' ({})", - self.name, - prev_tx_hash - ) - })? - .cumulative_gas_used() - }; - let cumulative_gas_used = receipt.cumulative_gas_used(); - let actual_gas_used = cumulative_gas_used - .checked_sub(prev_cumulative_gas_used) - .ok_or_else(|| { - eyre::eyre!( - "Cumulative gas regression for tx '{}' ({}): \ - prev_cumulative={}, current_cumulative={}", - self.name, - tx_hash, - prev_cumulative_gas_used, - cumulative_gas_used, - ) - })?; - if actual_gas_used != expected_gas_used { - return Err(eyre::eyre!( - "Transaction '{}' ({}) gas used mismatch: expected {}, got {}", - self.name, - tx_hash, - expected_gas_used, - actual_gas_used - )); - } - } - - info!( - name = %self.name, - tx_hash = %tx_hash, - block_number, - block_hash = %block.header().hash_slow(), - status = ?actual_status, - "Transaction inclusion and status assertion passed" - ); - - Ok(()) - }) - } -} - -/// Asserts that a transaction was NOT included in a block. -/// -/// Useful for verifying that a transaction was rejected during block building -/// (e.g., due to insufficient gas for Arc-specific intrinsic gas costs). -/// -/// The transaction name must match one previously sent via `SendTransaction::new("name")`. -/// Optionally specify a block number with `in_block()` to check a specific block instead -/// of the current block. -#[derive(Debug)] -pub struct AssertTxNotIncluded { - /// Name of the transaction to look up in `env.tx_hashes`. - name: String, - /// Specific block number to check. If None, uses current block. - block_number: Option, -} - -impl AssertTxNotIncluded { - /// Creates a new assertion that a transaction should NOT be in a block. - /// - /// The name must match a transaction previously sent via `SendTransaction::new("name")`. - pub fn new(name: impl Into) -> Self { - Self { - name: name.into(), - block_number: None, - } - } - - /// Sets a specific block number to check instead of using the current block. - pub fn in_block(mut self, block_number: u64) -> Self { - self.block_number = Some(block_number); - self - } -} - -impl Action for AssertTxNotIncluded { - fn execute<'a>(&'a mut self, env: &'a mut ArcEnvironment) -> BoxFuture<'a, eyre::Result<()>> { - Box::pin(async move { - let tx_hash = *env.get_tx_hash(&self.name).ok_or_else(|| { - eyre::eyre!("Transaction '{}' not found in environment", self.name) - })?; - - let block_number = self.block_number.unwrap_or_else(|| env.block_number()); - - info!( - name = %self.name, - tx_hash = %tx_hash, - block_number, - "Asserting transaction NOT included in block" - ); - - let node = env.node(); - let block = node - .inner - .provider() - .block_by_number_or_tag(BlockNumberOrTag::Number(block_number))? - .ok_or_else(|| eyre::eyre!("Block {} not found", block_number))?; - - let tx_hashes: Vec<_> = block - .body() - .transactions() - .map(|tx| tx.trie_hash()) - .collect(); - - if tx_hashes.contains(&tx_hash) { - return Err(eyre::eyre!( - "Transaction '{}' ({}) was found in block {} but should NOT have been included", - self.name, - tx_hash, - block_number, - )); - } - - info!( - name = %self.name, - tx_hash = %tx_hash, - block_number, - "Transaction exclusion assertion passed" - ); - - Ok(()) - }) - } -} - -/// Asserts that an address has a specific balance via `eth_getBalance` RPC. -/// -/// Supports exact match, minimum bound, and range (minimum + maximum) checks. -#[derive(Debug)] -pub struct AssertBalance { - address: Address, - expected: U256, - /// If set, asserts `balance >= expected` instead of exact equality. - at_least: bool, - /// Upper bound for range checks: `balance <= max`. - max: Option, -} - -impl AssertBalance { - /// Creates a new exact balance assertion. - pub fn new(address: Address, expected: U256) -> Self { - Self { - address, - expected, - at_least: false, - max: None, - } - } - - /// Asserts that the balance is at least `expected`. - pub fn at_least(mut self) -> Self { - self.at_least = true; - self - } - - /// Sets an upper bound and enables range mode: asserts `expected <= balance <= max`. - pub fn at_most(mut self, max: U256) -> Self { - self.at_least = true; - self.max = Some(max); - self - } -} - -impl Action for AssertBalance { - fn execute<'a>(&'a mut self, env: &'a mut ArcEnvironment) -> BoxFuture<'a, eyre::Result<()>> { - Box::pin(async move { - let client = env - .node() - .rpc_client() - .ok_or_else(|| eyre::eyre!("RPC client not available"))?; - - let balance = >::balance(&client, self.address, None) - .await?; - - info!( - address = %self.address, - balance = %balance, - expected = %self.expected, - at_least = self.at_least, - "Asserting balance" - ); - - if self.at_least { - if balance < self.expected { - return Err(eyre::eyre!( - "Balance of {} too low: expected >= {}, got {}", - self.address, - self.expected, - balance - )); - } - } else if balance != self.expected { - return Err(eyre::eyre!( - "Balance mismatch for {}: expected {}, got {}", - self.address, - self.expected, - balance - )); - } - - if let Some(max) = self.max { - if balance > max { - return Err(eyre::eyre!( - "Balance of {} too high: expected <= {}, got {}", - self.address, - max, - balance - )); - } - } - - info!(address = %self.address, balance = %balance, "Balance assertion passed"); - Ok(()) - }) - } -} diff --git a/crates/execution-e2e/src/actions/call_contract.rs b/crates/execution-e2e/src/actions/call_contract.rs deleted file mode 100644 index 3d548e1..0000000 --- a/crates/execution-e2e/src/actions/call_contract.rs +++ /dev/null @@ -1,222 +0,0 @@ -// Copyright 2026 Circle Internet Group, Inc. All rights reserved. -// -// SPDX-License-Identifier: Apache-2.0 -// -// Licensed under the Apache License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. -// You may obtain a copy of the License at -// -// http://www.apache.org/licenses/LICENSE-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// See the License for the specific language governing permissions and -// limitations under the License. - -//! Contract call actions for Arc e2e tests. -//! -//! Provides actions to execute eth_call and verify return values. -//! This is useful for testing precompiles and read-only contract calls -//! without submitting transactions. - -use crate::{action::Action, ArcEnvironment}; -use alloy_primitives::{Address, Bytes, TxKind}; -use alloy_rpc_types_eth::{TransactionInput, TransactionRequest}; -use futures_util::future::BoxFuture; -use reth_rpc_api::EthApiClient; -use tracing::info; - -/// Executes an eth_call to a contract or precompile and optionally verifies the result. -/// -/// This action: -/// 1. Executes an eth_call (read-only, no transaction submitted) -/// 2. Optionally verifies the return value matches expected -/// 3. Can be configured to expect the call to revert -/// 4. Can be configured to expect a non-zero result -/// -/// # Example -/// -/// ```ignore -/// use alloy_primitives::{address, bytes}; -/// -/// ArcTestBuilder::new() -/// .with_setup(ArcSetup::new()) -/// .with_action(ProduceBlocks::new(1)) -/// .with_action( -/// CallContract::new("my_call") -/// .to(address!("0000000000000000000000000000000000000100")) -/// .with_data(bytes!("...")) -/// .expect_result(bytes!("0000000000000000000000000000000000000000000000000000000000000001")) -/// ) -/// .run() -/// .await -/// ``` -#[derive(Debug)] -pub struct CallContract { - /// Name to reference this call in logs and errors. - name: String, - /// Target address (contract or precompile). - to: Address, - /// Call data to send. - data: Bytes, - /// Expected return value. If Some, will assert equality. - expected_result: Option, - /// If true, expect the call to fail/revert. - expect_revert: bool, - /// If true, expect the result to be non-zero (non-empty and not all zeros). - expect_non_zero: bool, -} - -impl CallContract { - /// Creates a new CallContract action with the given name. - /// - /// The name is used for logging and error messages. - pub fn new(name: impl Into) -> Self { - Self { - name: name.into(), - to: Address::ZERO, - data: Bytes::new(), - expected_result: None, - expect_revert: false, - expect_non_zero: false, - } - } - - /// Sets the target address for the call. - pub fn to(mut self, address: Address) -> Self { - self.to = address; - self - } - - /// Sets the call data. - pub fn with_data(mut self, data: Bytes) -> Self { - self.data = data; - self - } - - /// Sets the expected return value. - /// - /// The action will fail if the actual result doesn't match. - pub fn expect_result(mut self, result: Bytes) -> Self { - self.expected_result = Some(result); - self - } - - /// Configures the action to expect the call to revert. - /// - /// The action will fail if the call succeeds. - pub fn expect_revert(mut self) -> Self { - self.expect_revert = true; - self - } - - /// Configures the action to expect a non-zero result. - /// - /// The action will fail if the result is empty or all zeros. - pub fn expect_non_zero_result(mut self) -> Self { - self.expect_non_zero = true; - self - } -} - -impl Action for CallContract { - fn execute<'a>(&'a mut self, env: &'a mut ArcEnvironment) -> BoxFuture<'a, eyre::Result<()>> { - Box::pin(async move { - info!( - name = %self.name, - to = %self.to, - data_len = self.data.len(), - expect_revert = self.expect_revert, - "Executing eth_call" - ); - - // Get RPC client from node - let client = env - .node() - .rpc_client() - .ok_or_else(|| eyre::eyre!("RPC client not available"))?; - - // Build transaction request - let request = TransactionRequest { - to: Some(TxKind::Call(self.to)), - input: TransactionInput::new(self.data.clone()), - ..Default::default() - }; - - // Execute eth_call using the EthApiClient trait - // HttpClient implements EthApiClient with these type parameters - let result = >::call(&client, request, None, None, None) - .await; - - match result { - Ok(output) => { - if self.expect_revert { - return Err(eyre::eyre!( - "Call '{}' succeeded but was expected to revert. Output: {}", - self.name, - output - )); - } - - info!( - name = %self.name, - output_len = output.len(), - output = %output, - "eth_call succeeded" - ); - - // Verify expected result if provided - if let Some(expected) = &self.expected_result { - if output != *expected { - return Err(eyre::eyre!( - "Call '{}' result mismatch.\nExpected: {}\nActual: {}", - self.name, - expected, - output - )); - } - info!(name = %self.name, "Result matches expected value"); - } - - // Verify non-zero result if configured - if self.expect_non_zero { - if output.is_empty() || output.iter().all(|&b| b == 0) { - return Err(eyre::eyre!( - "Call '{}' returned zero/empty result but expected non-zero.\nActual: {}", - self.name, - output - )); - } - info!(name = %self.name, "Result is non-zero as expected"); - } - - Ok(()) - } - Err(err) => { - if self.expect_revert { - info!( - name = %self.name, - error = %err, - "eth_call reverted as expected" - ); - Ok(()) - } else { - Err(eyre::eyre!( - "Call '{}' failed unexpectedly: {}", - self.name, - err - )) - } - } - } - }) - } -} diff --git a/crates/execution-e2e/src/actions/mod.rs b/crates/execution-e2e/src/actions/mod.rs deleted file mode 100644 index 364c8b9..0000000 --- a/crates/execution-e2e/src/actions/mod.rs +++ /dev/null @@ -1,49 +0,0 @@ -// Copyright 2026 Circle Internet Group, Inc. All rights reserved. -// -// SPDX-License-Identifier: Apache-2.0 -// -// Licensed under the Apache License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. -// You may obtain a copy of the License at -// -// http://www.apache.org/licenses/LICENSE-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// See the License for the specific language governing permissions and -// limitations under the License. - -//! Actions for Arc e2e tests. -//! -//! Actions are composable building blocks for test scenarios. - -mod assert_named; -mod assert_tx_logs; -mod assert_tx_trace; -mod assertions; -mod call_contract; -mod payload_utils; -mod produce_blocks; -mod produce_invalid_block; -mod send_transaction; -mod store_deployed_address; - -pub use assert_named::{AssertNamedBalance, AssertTransferEvent}; -pub use assert_tx_logs::{ - AssertTxLogs, NATIVE_COIN_TRANSFERRED_SIGNATURE, TRANSFER_EVENT_SIGNATURE, -}; -pub use assert_tx_trace::{AssertLastOpcodeGasCost, AssertTxTrace}; -pub use assertions::{ - AssertBalance, AssertBlockNumber, AssertEthereumHardfork, AssertHardfork, AssertTxIncluded, - AssertTxNotIncluded, TxStatus, -}; -pub use call_contract::CallContract; -pub use payload_utils::{ - assert_valid_or_syncing, build_payload_for_next_block, set_payload_override_and_rehash, - submit_payload, -}; -pub use produce_blocks::ProduceBlocks; -pub use produce_invalid_block::ProduceInvalidBlock; -pub use send_transaction::SendTransaction; -pub use store_deployed_address::StoreDeployedAddress; diff --git a/crates/execution-e2e/src/actions/payload_utils.rs b/crates/execution-e2e/src/actions/payload_utils.rs deleted file mode 100644 index 8e0a0ff..0000000 --- a/crates/execution-e2e/src/actions/payload_utils.rs +++ /dev/null @@ -1,189 +0,0 @@ -// Copyright 2026 Circle Internet Group, Inc. All rights reserved. -// -// SPDX-License-Identifier: Apache-2.0 -// -// Licensed under the Apache License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. -// You may obtain a copy of the License at -// -// http://www.apache.org/licenses/LICENSE-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// See the License for the specific language governing permissions and -// limitations under the License. - -//! Shared payload utilities for e2e tests and actions. - -use crate::ArcEnvironment; -use alloy_eips::eip7685::{Requests, RequestsOrHash}; -use alloy_primitives::{address, B256}; -use alloy_rpc_types_engine::{ - CancunPayloadFields, ExecutionData, ExecutionPayload, ExecutionPayloadSidecar, - ExecutionPayloadV1, ExecutionPayloadV3, ForkchoiceState, PayloadAttributes, PayloadStatusEnum, - PraguePayloadFields, -}; -use reth_ethereum::node::EthEngineTypes; -use reth_rpc_api::clients::EngineApiClient; - -/// JSON-RPC error code for "Unsupported Fork" per the Engine API spec. -const UNSUPPORTED_FORK_CODE: i32 = -38005; - -/// Returns `true` if the error is a JSON-RPC "Unsupported Fork" (-38005) response. -pub(crate) fn is_unsupported_fork_err(err: &jsonrpsee::core::client::Error) -> bool { - matches!(err, jsonrpsee::core::client::Error::Call(obj) if obj.code() == UNSUPPORTED_FORK_CODE) -} - -/// Builds a payload for the next block and returns: -/// `(execution_payload, execution_requests, parent_beacon_block_root)`. -pub async fn build_payload_for_next_block( - env: &ArcEnvironment, -) -> eyre::Result<(ExecutionPayloadV3, Requests, B256)> { - build_payload_for_next_block_with_client( - &env.node().inner.auth_server_handle().http_client(), - env.current_block(), - ) - .await -} - -/// Builds a payload for the next block using the given engine client and parent block info. -/// Used when the node is created outside ArcEnvironment (e.g. NodeBuilder with custom config). -pub(crate) async fn build_payload_for_next_block_with_client>( - engine_client: &C, - current_block: &crate::environment::BlockInfo, -) -> eyre::Result<(ExecutionPayloadV3, Requests, B256)> { - let parent_hash = current_block.hash; - let parent_beacon_block_root = B256::ZERO; - - let fork_choice_state = ForkchoiceState { - head_block_hash: parent_hash, - safe_block_hash: parent_hash, - finalized_block_hash: parent_hash, - }; - - let payload_attributes = PayloadAttributes { - timestamp: current_block.timestamp + 1, - prev_randao: B256::random(), - suggested_fee_recipient: address!("0x65E0a200006D4FF91bD59F9694220dafc49dbBC1"), - withdrawals: Some(vec![]), - parent_beacon_block_root: Some(parent_beacon_block_root), - }; - - let fcu_result = EngineApiClient::::fork_choice_updated_v3( - engine_client, - fork_choice_state, - Some(payload_attributes), - ) - .await?; - assert_valid_or_syncing( - &fcu_result.payload_status.status, - "forkChoiceUpdated while building payload", - )?; - - let payload_id = fcu_result - .payload_id - .ok_or_else(|| eyre::eyre!("No payload ID returned from forkChoiceUpdated"))?; - - // Use getPayloadV5 (Osaka) if supported, otherwise fall back to V4 (Prague). - // V5 is required when Osaka is active; V4 is rejected post-Osaka. - let (execution_payload, execution_requests) = - match EngineApiClient::::get_payload_v5(engine_client, payload_id).await { - Ok(envelope) => ( - envelope.execution_payload.clone(), - envelope.execution_requests.clone(), - ), - Err(e) => { - if !is_unsupported_fork_err(&e) { - return Err(eyre::eyre!("getPayloadV5 failed: {e}")); - } - let envelope = - EngineApiClient::::get_payload_v4(engine_client, payload_id) - .await?; - ( - envelope.execution_payload.clone(), - envelope.execution_requests.clone(), - ) - } - }; - - Ok(( - execution_payload, - execution_requests, - parent_beacon_block_root, - )) -} - -/// Mutates payload and recomputes block hash with full sidecar context. -pub fn set_payload_override_and_rehash( - payload: &mut ExecutionPayloadV3, - execution_requests: &Requests, - parent_beacon_block_root: B256, - payload_override: ExecutionPayloadV1, -) -> eyre::Result<()> { - payload.payload_inner.payload_inner = payload_override; - - let sidecar = ExecutionPayloadSidecar::v4( - CancunPayloadFields::new(parent_beacon_block_root, vec![]), - PraguePayloadFields::new(execution_requests.clone()), - ); - payload.payload_inner.payload_inner.block_hash = - ExecutionData::new(ExecutionPayload::V3(payload.clone()), sidecar) - .into_block_raw()? - .hash_slow(); - - Ok(()) -} - -/// Submits a payload via `engine_newPayloadV4` and returns the status. -pub async fn submit_payload( - env: &ArcEnvironment, - payload: ExecutionPayloadV3, - execution_requests: Requests, - parent_beacon_block_root: B256, -) -> eyre::Result { - submit_payload_with_client( - &env.node().inner.auth_server_handle().http_client(), - payload, - execution_requests, - parent_beacon_block_root, - ) - .await -} - -/// Submits a payload via `engine_newPayloadV4` using the given engine client. -/// Used when the node is created outside ArcEnvironment (e.g. NodeBuilder with custom config). -pub(crate) async fn submit_payload_with_client>( - engine_client: &C, - payload: ExecutionPayloadV3, - execution_requests: Requests, - parent_beacon_block_root: B256, -) -> eyre::Result { - let result = EngineApiClient::::new_payload_v4( - engine_client, - payload, - vec![], - parent_beacon_block_root, - RequestsOrHash::Requests(execution_requests), - ) - .await; - - match result { - Ok(response) => Ok(response.status), - Err(err) => Err(err.into()), - } -} - -/// Validates that payload status is either VALID or SYNCING. -pub fn assert_valid_or_syncing(status: &PayloadStatusEnum, context: &str) -> eyre::Result<()> { - match status { - PayloadStatusEnum::Valid | PayloadStatusEnum::Syncing => Ok(()), - PayloadStatusEnum::Invalid { validation_error } => Err(eyre::eyre!( - "{context} returned INVALID: {validation_error}" - )), - status => Err(eyre::eyre!( - "{context} returned unexpected status: {:?}", - status - )), - } -} diff --git a/crates/execution-e2e/src/actions/produce_blocks.rs b/crates/execution-e2e/src/actions/produce_blocks.rs deleted file mode 100644 index ec8c236..0000000 --- a/crates/execution-e2e/src/actions/produce_blocks.rs +++ /dev/null @@ -1,140 +0,0 @@ -// Copyright 2026 Circle Internet Group, Inc. All rights reserved. -// -// SPDX-License-Identifier: Apache-2.0 -// -// Licensed under the Apache License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. -// You may obtain a copy of the License at -// -// http://www.apache.org/licenses/LICENSE-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// See the License for the specific language governing permissions and -// limitations under the License. - -//! Block production actions for Arc e2e tests. -//! -//! Uses Engine API directly to produce blocks, allowing for empty blocks -//! which is common in testing scenarios. - -use super::{assert_valid_or_syncing, build_payload_for_next_block, submit_payload}; -use crate::{action::Action, environment::BlockInfo, ArcEnvironment}; -use alloy_rpc_types_engine::ForkchoiceState; -use futures_util::future::BoxFuture; -use reth_ethereum::node::EthEngineTypes; -use reth_rpc_api::clients::EngineApiClient; -use tracing::{debug, info}; - -/// Produces a specified number of blocks using Engine API. -/// -/// For each block: -/// 1. Sends forkchoiceUpdated with payload attributes to start building -/// 2. Retrieves payload via getPayload -/// 3. Submits payload via newPayload -/// 4. Updates forkchoice to finalize the block -/// 5. Updates the environment's current block info -#[derive(Debug)] -pub struct ProduceBlocks { - /// Number of blocks to produce. - num_blocks: u64, -} - -impl ProduceBlocks { - /// Creates a new ProduceBlocks action. - pub fn new(num_blocks: u64) -> Self { - Self { num_blocks } - } -} - -impl Action for ProduceBlocks { - fn execute<'a>(&'a mut self, env: &'a mut ArcEnvironment) -> BoxFuture<'a, eyre::Result<()>> { - Box::pin(async move { - let starting_block = env.block_number(); - info!( - starting_block, - num_blocks = self.num_blocks, - "Producing blocks via Engine API" - ); - - for i in 0..self.num_blocks { - let current_block = env.current_block().clone(); - let parent_hash = current_block.hash; - - debug!( - parent_hash = %parent_hash, - parent_number = current_block.number, - "Building block on parent" - ); - - // Step 1-2: Build payload via Engine API (FCU + getPayload) - let (execution_payload, execution_requests, parent_beacon_block_root) = - build_payload_for_next_block(env).await?; - let block_hash = execution_payload.payload_inner.payload_inner.block_hash; - let block_number = execution_payload.payload_inner.payload_inner.block_number; - let block_timestamp = execution_payload.payload_inner.payload_inner.timestamp; - - debug!( - block_hash = %block_hash, - block_number, - "Got built payload" - ); - - // Step 3: Submit the payload - let new_payload_status = submit_payload( - env, - execution_payload, - execution_requests, - parent_beacon_block_root, - ) - .await?; - - debug!("newPayload status: {:?}", new_payload_status); - assert_valid_or_syncing(&new_payload_status, "newPayload")?; - - // Get the auth server handle from the node - let node = env.node(); - let auth_server = node.inner.auth_server_handle(); - let engine_client = auth_server.http_client(); - - // Step 4: Update forkchoice to make the new block canonical and finalized - let new_fork_choice = ForkchoiceState { - head_block_hash: block_hash, - safe_block_hash: block_hash, - finalized_block_hash: block_hash, - }; - - let finalize_result = EngineApiClient::::fork_choice_updated_v3( - &engine_client, - new_fork_choice, - None, - ) - .await?; - - debug!("Finalize FCU result: {:?}", finalize_result); - assert_valid_or_syncing(&finalize_result.payload_status.status, "Finalize FCU")?; - - // Step 5: Update environment's current block - env.set_current_block(BlockInfo::new(block_hash, block_number, block_timestamp)); - - info!( - block_number, - block_hash = %block_hash, - iteration = i + 1, - "Produced block" - ); - } - - let final_block = env.block_number(); - info!( - starting_block, - final_block, - blocks_produced = self.num_blocks, - "Finished producing blocks" - ); - - Ok(()) - }) - } -} diff --git a/crates/execution-e2e/src/actions/produce_invalid_block.rs b/crates/execution-e2e/src/actions/produce_invalid_block.rs deleted file mode 100644 index ad37aee..0000000 --- a/crates/execution-e2e/src/actions/produce_invalid_block.rs +++ /dev/null @@ -1,188 +0,0 @@ -// Copyright 2026 Circle Internet Group, Inc. All rights reserved. -// -// SPDX-License-Identifier: Apache-2.0 -// -// Licensed under the Apache License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. -// You may obtain a copy of the License at -// -// http://www.apache.org/licenses/LICENSE-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// See the License for the specific language governing permissions and -// limitations under the License. - -//! Block production with invalid payloads for bad validator simulation. -//! -//! This module provides actions to produce blocks with corrupted fields -//! to test how the execution layer handles invalid blocks. - -use crate::{action::Action, actions::payload_utils::is_unsupported_fork_err, ArcEnvironment}; -use alloy_eips::eip7685::RequestsOrHash; -use alloy_primitives::B256; -use alloy_rpc_types_engine::{ForkchoiceState, PayloadAttributes, PayloadStatusEnum}; -use futures_util::future::BoxFuture; -use reth_ethereum::node::EthEngineTypes; -use reth_rpc_api::clients::EngineApiClient; -use tracing::{debug, info}; - -/// Produces a block with a corrupted state root. -/// -/// This action simulates bad validator behavior by: -/// 1. Building a normal payload via forkchoiceUpdated + getPayload -/// 2. Corrupting the state root to a random value -/// 3. Submitting the corrupted payload via newPayload -/// 4. Expecting the payload to be rejected as INVALID -#[derive(Debug, Default)] -pub struct ProduceInvalidBlock; - -impl ProduceInvalidBlock { - /// Create a new action that produces a block with a corrupted state root. - pub fn new() -> Self { - Self - } -} - -impl Action for ProduceInvalidBlock { - fn execute<'a>(&'a mut self, env: &'a mut ArcEnvironment) -> BoxFuture<'a, eyre::Result<()>> { - Box::pin(async move { - let current_block = env.current_block().clone(); - let parent_hash = current_block.hash; - let parent_timestamp = current_block.timestamp; - - info!( - parent_hash = %parent_hash, - parent_number = current_block.number, - "Producing invalid block with corrupted state root" - ); - - // Get the auth server handle from the node - let node = env.node(); - let auth_server = node.inner.auth_server_handle(); - let engine_client = auth_server.http_client(); - - // Create forkchoice state pointing to current head - let fork_choice_state = ForkchoiceState { - head_block_hash: parent_hash, - safe_block_hash: parent_hash, - finalized_block_hash: parent_hash, - }; - - // Create payload attributes for the next block - let next_timestamp = parent_timestamp + 1; - let payload_attributes = PayloadAttributes { - timestamp: next_timestamp, - prev_randao: B256::random(), - suggested_fee_recipient: alloy_primitives::address!( - "0x65E0a200006D4FF91bD59F9694220dafc49dbBC1" - ), - withdrawals: Some(vec![]), - parent_beacon_block_root: Some(B256::ZERO), - }; - - // Step 1: Send FCU with payload attributes to start building - let fcu_result = EngineApiClient::::fork_choice_updated_v3( - &engine_client, - fork_choice_state, - Some(payload_attributes), - ) - .await?; - - debug!("FCU result: {:?}", fcu_result); - - match &fcu_result.payload_status.status { - PayloadStatusEnum::Valid | PayloadStatusEnum::Syncing => {} - PayloadStatusEnum::Invalid { validation_error } => { - return Err(eyre::eyre!( - "FCU returned Invalid status: {:?}", - validation_error - )); - } - status => { - return Err(eyre::eyre!("Unexpected FCU status: {:?}", status)); - } - } - - let payload_id = fcu_result - .payload_id - .ok_or_else(|| eyre::eyre!("No payload ID returned from FCU"))?; - - debug!("Got payload ID: {:?}", payload_id); - - // Step 2: Get the built payload - // Use getPayloadV5 (Osaka) if supported, otherwise fall back to V4 (Prague). - // V5 is required when Osaka is active; V4 is rejected post-Osaka. - let (execution_payload, execution_requests) = - match EngineApiClient::::get_payload_v5(&engine_client, payload_id) - .await - { - Ok(envelope) => ( - envelope.execution_payload.clone(), - envelope.execution_requests.clone(), - ), - Err(e) => { - if !is_unsupported_fork_err(&e) { - return Err(eyre::eyre!("getPayloadV5 failed: {e}")); - } - let envelope = EngineApiClient::::get_payload_v4( - &engine_client, - payload_id, - ) - .await?; - ( - envelope.execution_payload.clone(), - envelope.execution_requests.clone(), - ) - } - }; - - let mut corrupted_payload = execution_payload; - let block_hash = corrupted_payload.payload_inner.payload_inner.block_hash; - let block_number = corrupted_payload.payload_inner.payload_inner.block_number; - - debug!( - block_hash = %block_hash, - block_number, - "Got built payload, applying corruption" - ); - - // Step 3: Corrupt the state root - let original = corrupted_payload.payload_inner.payload_inner.state_root; - corrupted_payload.payload_inner.payload_inner.state_root = B256::random(); - info!( - original_state_root = %original, - corrupted_state_root = %corrupted_payload.payload_inner.payload_inner.state_root, - "Corrupted state root" - ); - - // Step 4: Submit the corrupted payload - let new_payload_result = EngineApiClient::::new_payload_v4( - &engine_client, - corrupted_payload, - vec![], - B256::ZERO, - RequestsOrHash::Requests(execution_requests), - ) - .await?; - - debug!("newPayload result: {:?}", new_payload_result); - - // Step 5: Expect the payload to be rejected as invalid - match &new_payload_result.status { - PayloadStatusEnum::Invalid { validation_error } => { - info!( - ?validation_error, - block_number, "Block correctly rejected as invalid" - ); - Ok(()) - } - status => Err(eyre::eyre!( - "Expected newPayload to return INVALID, but got {:?}", - status - )), - } - }) - } -} diff --git a/crates/execution-e2e/src/actions/send_transaction.rs b/crates/execution-e2e/src/actions/send_transaction.rs deleted file mode 100644 index f6baf85..0000000 --- a/crates/execution-e2e/src/actions/send_transaction.rs +++ /dev/null @@ -1,278 +0,0 @@ -// Copyright 2026 Circle Internet Group, Inc. All rights reserved. -// -// SPDX-License-Identifier: Apache-2.0 -// -// Licensed under the Apache License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. -// You may obtain a copy of the License at -// -// http://www.apache.org/licenses/LICENSE-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// See the License for the specific language governing permissions and -// limitations under the License. - -//! Transaction sending actions for Arc e2e tests. -//! -//! Provides actions to send EIP-1559 transactions to the node's transaction pool -//! via direct pool injection. - -use crate::{action::Action, ArcEnvironment}; -use alloy_network::eip2718::{Decodable2718, Encodable2718}; -use alloy_primitives::{Address, Bytes, TxHash, TxKind, U256}; -use alloy_rpc_types_eth::{TransactionInput, TransactionRequest}; -use futures_util::future::BoxFuture; -use reth_e2e_test_utils::transaction::TransactionTestContext; -use reth_ethereum_primitives::TransactionSigned; -use reth_primitives_traits::SignerRecoverable; -use reth_transaction_pool::{TransactionOrigin, TransactionPool}; -use tracing::{debug, info}; - -/// Closure that resolves calldata from the environment at execution time. -type DataResolver = Box eyre::Result + Send + Sync>; - -/// Sends an EIP-1559 transaction to the node's transaction pool. -/// -/// This action: -/// 1. Creates an EIP-1559 transaction from a wallet -/// 2. Signs and submits it directly to the transaction pool -/// 3. Stores the transaction hash under the given name for later assertions -/// -/// Supports both CALL (with `to` address) and CREATE (without `to`, using `with_create()`) -/// transactions. For CREATE transactions, the deployed contract address is extracted -/// from the receipt and stored under `"{name}_address"` in the environment. -pub struct SendTransaction { - /// Name to reference this transaction in assertions. - name: String, - /// The value to transfer (in wei). - value: U256, - /// The recipient address. If None and not create, sends to a random address. - to: Option
, - /// Named address to look up at execution time (overrides `to` if set). - to_named: Option, - /// If true, this is a CREATE transaction (no `to` address). - create: bool, - /// Optional input data for the transaction. - data: Option, - /// Deferred calldata resolver — called at execution time with access to the environment. - data_resolver: Option, - /// Gas limit for the transaction. - gas_limit: u64, - /// Wallet index to sign from (default: 0 = first wallet). - wallet_index: usize, -} - -impl std::fmt::Debug for SendTransaction { - fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { - f.debug_struct("SendTransaction") - .field("name", &self.name) - .field("value", &self.value) - .field("to", &self.to) - .field("to_named", &self.to_named) - .field("create", &self.create) - .field("data", &self.data) - .field("data_resolver", &self.data_resolver.as_ref().map(|_| "..")) - .field("gas_limit", &self.gas_limit) - .field("wallet_index", &self.wallet_index) - .finish() - } -} - -impl SendTransaction { - /// Creates a new named SendTransaction action with default values. - /// - /// The name is used to reference this transaction in assertions via - /// `AssertTxIncluded::new("name")`. - /// - /// Default is a simple 1 wei transfer to a random address. - pub fn new(name: impl Into) -> Self { - Self { - name: name.into(), - value: U256::from(1), - to: None, - to_named: None, - create: false, - data: None, - data_resolver: None, - gas_limit: 21000, - wallet_index: 0, - } - } - - /// Sets the value to transfer. - pub fn with_value(mut self, value: U256) -> Self { - self.value = value; - self - } - - /// Sets the recipient address. - pub fn with_to(mut self, to: Address) -> Self { - self.to = Some(to); - self - } - - /// Sets input data for the transaction (e.g., contract call). - pub fn with_data(mut self, data: Bytes) -> Self { - self.data = Some(data); - self - } - - /// Marks this as a CREATE transaction (no `to` address). - /// The `data` field should contain the deployment bytecode. - pub fn with_create(mut self) -> Self { - self.create = true; - self - } - - /// Sets the recipient to a named address stored in the environment. - /// - /// At execution time, looks up the address by name (e.g. `"deploy_address"` - /// for a contract deployed by a CREATE tx named `"deploy"`). - pub fn with_to_named(mut self, address_name: impl Into) -> Self { - self.to_named = Some(address_name.into()); - self - } - - /// Deferred calldata — the closure is called at execution time with access - /// to the environment, so it can resolve named addresses or other runtime state. - pub fn with_data_fn( - mut self, - f: impl Fn(&ArcEnvironment) -> eyre::Result + Send + Sync + 'static, - ) -> Self { - self.data_resolver = Some(Box::new(f)); - self - } - - /// Sets the gas limit for the transaction. - pub fn with_gas_limit(mut self, gas_limit: u64) -> Self { - self.gas_limit = gas_limit; - self - } - - /// Signs from the wallet at the given index (default: 0). - /// - /// Useful for tests that need to send from different roles, - /// e.g. index 7 is the operator in localdev genesis. - pub fn with_wallet_index(mut self, index: usize) -> Self { - self.wallet_index = index; - self - } - - /// Executes the action and returns the transaction hash. - pub async fn execute_and_return( - &self, - env: &mut ArcEnvironment, - ) -> eyre::Result<(TxHash, reth_primitives_traits::Recovered)> { - let (signer, chain_id) = { - let wallet = env.wallet_mut()?; - let wallets = wallet.wallet_gen(); - let signer = wallets - .get(self.wallet_index) - .ok_or_else(|| { - eyre::eyre!( - "Wallet index {} not available (only {} wallets)", - self.wallet_index, - wallets.len() - ) - })? - .clone(); - (signer, wallet.chain_id) - }; - - let nonce = env.next_nonce_for_wallet(self.wallet_index)?; - - let tx_kind = if self.create { - info!( - name = %self.name, - nonce, - value = %self.value, - "Sending CREATE transaction" - ); - TxKind::Create - } else { - // Resolve recipient: named address > explicit address > random - let to_address = if let Some(ref addr_name) = self.to_named { - *env.get_address(addr_name).ok_or_else(|| { - eyre::eyre!( - "Named address '{}' not found in environment for tx '{}'", - addr_name, - self.name - ) - })? - } else { - self.to.unwrap_or_else(Address::random) - }; - info!( - name = %self.name, - nonce, - value = %self.value, - to = %to_address, - "Sending transaction" - ); - TxKind::Call(to_address) - }; - - // Resolve calldata: deferred resolver > explicit data - let resolved_data = if let Some(ref resolver) = self.data_resolver { - Some(resolver(env)?) - } else { - self.data.clone() - }; - - // Build EIP-1559 transaction request - let tx = TransactionRequest { - nonce: Some(nonce), - value: Some(self.value), - to: Some(tx_kind), - gas: Some(self.gas_limit), - max_fee_per_gas: Some(1000e9 as u128), - max_priority_fee_per_gas: Some(1e9 as u128), - chain_id: Some(chain_id), - input: TransactionInput { - input: None, - data: resolved_data, - }, - ..Default::default() - }; - - // Sign transaction using reth's TransactionTestContext - let signed_tx = TransactionTestContext::sign_tx(signer, tx).await; - let tx_hash = *signed_tx.tx_hash(); - - debug!(tx_hash = %tx_hash, "Transaction signed"); - - // Convert TxEnvelope to TransactionSigned for pool submission - let raw_tx: Bytes = signed_tx.encoded_2718().into(); - let tx_signed = TransactionSigned::decode_2718(&mut raw_tx.as_ref()) - .map_err(|e| eyre::eyre!("Failed to decode transaction: {:?}", e))?; - - // Recover the signer - let recovered_tx = tx_signed - .try_into_recovered() - .map_err(|e| eyre::eyre!("Failed to recover transaction signer: {:?}", e))?; - - // Get pool from node and add transaction - env.node() - .inner - .pool - .add_consensus_transaction(recovered_tx.clone(), TransactionOrigin::Local) - .await - .map_err(|e| eyre::eyre!("Failed to submit transaction to pool: {:?}", e))?; - - info!(name = %self.name, tx_hash = %tx_hash, "Transaction submitted to pool"); - - Ok((tx_hash, recovered_tx)) - } -} - -impl Action for SendTransaction { - fn execute<'a>(&'a mut self, env: &'a mut ArcEnvironment) -> BoxFuture<'a, eyre::Result<()>> { - Box::pin(async move { - let (tx_hash, _) = self.execute_and_return(env).await?; - env.insert_tx_hash(self.name.clone(), tx_hash)?; - Ok(()) - }) - } -} diff --git a/crates/execution-e2e/src/actions/store_deployed_address.rs b/crates/execution-e2e/src/actions/store_deployed_address.rs deleted file mode 100644 index bbc742a..0000000 --- a/crates/execution-e2e/src/actions/store_deployed_address.rs +++ /dev/null @@ -1,81 +0,0 @@ -// Copyright 2026 Circle Internet Group, Inc. All rights reserved. -// -// SPDX-License-Identifier: Apache-2.0 -// -// Licensed under the Apache License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. -// You may obtain a copy of the License at -// -// http://www.apache.org/licenses/LICENSE-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// See the License for the specific language governing permissions and -// limitations under the License. - -//! Extracts deployed contract address from a CREATE transaction. - -use crate::{action::Action, ArcEnvironment}; -use futures_util::future::BoxFuture; -use reth_provider::TransactionsProvider; -use tracing::info; - -/// Extracts the deployed contract address from a CREATE transaction -/// and stores it in the environment under `"{tx_name}_address"`. -/// -/// Computes the address from the sender and nonce of the transaction. -pub struct StoreDeployedAddress { - tx_name: String, -} - -impl StoreDeployedAddress { - /// Creates a new action for the named CREATE transaction. - pub fn new(tx_name: impl Into) -> Self { - Self { - tx_name: tx_name.into(), - } - } -} - -impl Action for StoreDeployedAddress { - fn execute<'a>(&'a mut self, env: &'a mut ArcEnvironment) -> BoxFuture<'a, eyre::Result<()>> { - Box::pin(async move { - let tx_hash = *env.get_tx_hash(&self.tx_name).ok_or_else(|| { - eyre::eyre!("Transaction '{}' not found in environment", self.tx_name) - })?; - - let (tx, _meta) = env - .node() - .inner - .provider() - .transaction_by_hash_with_meta(tx_hash)? - .ok_or_else(|| { - eyre::eyre!("Transaction not found for '{}' ({})", self.tx_name, tx_hash) - })?; - - use reth_primitives_traits::SignerRecoverable; - let sender = tx.recover_signer().map_err(|e| { - eyre::eyre!("Failed to recover signer for '{}': {:?}", self.tx_name, e) - })?; - - use alloy_consensus::Transaction; - let nonce = tx.nonce(); - - let contract_address = sender.create(nonce); - let address_name = format!("{}_address", self.tx_name); - - info!( - tx_name = %self.tx_name, - sender = %sender, - nonce, - contract_address = %contract_address, - stored_as = %address_name, - "Stored deployed contract address" - ); - - env.insert_address(address_name, contract_address)?; - Ok(()) - }) - } -} diff --git a/crates/execution-e2e/src/chainspec.rs b/crates/execution-e2e/src/chainspec.rs deleted file mode 100644 index 9d212f8..0000000 --- a/crates/execution-e2e/src/chainspec.rs +++ /dev/null @@ -1,92 +0,0 @@ -// Copyright 2026 Circle Internet Group, Inc. All rights reserved. -// -// SPDX-License-Identifier: Apache-2.0 -// -// Licensed under the Apache License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. -// You may obtain a copy of the License at -// -// http://www.apache.org/licenses/LICENSE-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// See the License for the specific language governing permissions and -// limitations under the License. - -//! Helper module for creating custom chain specs for e2e tests. -//! -//! Re-exports the test utility from arc_execution_config for convenience. - -pub use arc_execution_config::chainspec::{ - localdev_with_block_gas_limit, localdev_with_denylisted_addresses, localdev_with_hardforks, - localdev_with_protocol_config_reverts, localdev_with_storage_override, BlockGasLimitProvider, - LOCAL_DEV, -}; - -#[cfg(test)] -mod tests { - use super::*; - use arc_execution_config::hardforks::ArcHardfork; - use reth_chainspec::{ForkCondition, Hardforks}; - - #[test] - fn test_localdev_with_hardforks_creates_valid_spec() { - let spec = localdev_with_hardforks(&[ - (ArcHardfork::Zero3, ForkCondition::Block(0)), - (ArcHardfork::Zero4, ForkCondition::Block(5)), - ]); - - // Zero3 should be active at block 0 - assert!(spec.is_fork_active_at_block(ArcHardfork::Zero3, 0)); - assert!(spec.is_fork_active_at_block(ArcHardfork::Zero3, 4)); - - // Zero4 should not be active before block 5 - assert!(!spec.is_fork_active_at_block(ArcHardfork::Zero4, 4)); - // Zero4 should be active at and after block 5 - assert!(spec.is_fork_active_at_block(ArcHardfork::Zero4, 5)); - assert!(spec.is_fork_active_at_block(ArcHardfork::Zero4, 10)); - } - - #[test] - fn test_zero4_at_block_3() { - let spec = localdev_with_hardforks(&[ - (ArcHardfork::Zero3, ForkCondition::Block(0)), - (ArcHardfork::Zero4, ForkCondition::Block(3)), - ]); - - assert!(spec.is_fork_active_at_block(ArcHardfork::Zero3, 0)); - assert!(!spec.is_fork_active_at_block(ArcHardfork::Zero4, 2)); - assert!(spec.is_fork_active_at_block(ArcHardfork::Zero4, 3)); - } - - #[test] - fn test_zero5_at_block_5() { - let spec = localdev_with_hardforks(&[ - (ArcHardfork::Zero3, ForkCondition::Block(0)), - (ArcHardfork::Zero4, ForkCondition::Block(0)), - (ArcHardfork::Zero5, ForkCondition::Block(5)), - ]); - - assert!(spec.is_fork_active_at_block(ArcHardfork::Zero3, 0)); - assert!(spec.is_fork_active_at_block(ArcHardfork::Zero4, 0)); - assert!(!spec.is_fork_active_at_block(ArcHardfork::Zero5, 4)); - assert!(spec.is_fork_active_at_block(ArcHardfork::Zero5, 5)); - } - - #[test] - fn test_zero6_at_block_5() { - let spec = localdev_with_hardforks(&[ - (ArcHardfork::Zero3, ForkCondition::Block(0)), - (ArcHardfork::Zero4, ForkCondition::Block(0)), - (ArcHardfork::Zero5, ForkCondition::Block(0)), - (ArcHardfork::Zero6, ForkCondition::Block(5)), - ]); - - assert!(spec.is_fork_active_at_block(ArcHardfork::Zero3, 0)); - assert!(spec.is_fork_active_at_block(ArcHardfork::Zero4, 0)); - assert!(spec.is_fork_active_at_block(ArcHardfork::Zero5, 0)); - assert!(!spec.is_fork_active_at_block(ArcHardfork::Zero6, 4)); - assert!(spec.is_fork_active_at_block(ArcHardfork::Zero6, 5)); - } -} diff --git a/crates/execution-e2e/src/environment.rs b/crates/execution-e2e/src/environment.rs deleted file mode 100644 index 8844d35..0000000 --- a/crates/execution-e2e/src/environment.rs +++ /dev/null @@ -1,185 +0,0 @@ -// Copyright 2026 Circle Internet Group, Inc. All rights reserved. -// -// SPDX-License-Identifier: Apache-2.0 -// -// Licensed under the Apache License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. -// You may obtain a copy of the License at -// -// http://www.apache.org/licenses/LICENSE-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// See the License for the specific language governing permissions and -// limitations under the License. - -//! Arc test environment for e2e tests. - -use alloy_primitives::{Address, BlockHash, TxHash}; -use arc_evm_node::node::ArcNode; -use reth_e2e_test_utils::{wallet::Wallet, NodeHelperType}; -use reth_node_builder::NodeTypesWithDBAdapter; -use reth_provider::providers::BlockchainProvider; -use std::collections::HashMap; - -/// Type alias for the Arc node test context. -pub type ArcNodeTestContext = NodeHelperType< - ArcNode, - BlockchainProvider>, ->; - -/// Information about a block. -#[derive(Debug, Clone)] -pub struct BlockInfo { - /// Block hash. - pub hash: BlockHash, - /// Block number. - pub number: u64, - /// Block timestamp. - pub timestamp: u64, -} - -impl BlockInfo { - /// Creates a new BlockInfo. - pub fn new(hash: BlockHash, number: u64, timestamp: u64) -> Self { - Self { - hash, - number, - timestamp, - } - } -} - -/// Arc test environment containing the node context and state. -pub struct ArcEnvironment { - /// The single node test context. - node: Option, - /// Current block information. - current_block: Option, - /// Wallet for signing transactions in tests. - wallet: Option, - /// Named transaction hashes for test assertions. - tx_hashes: HashMap, - /// Named addresses (e.g., deployed contract addresses) for test reference. - addresses: HashMap, - /// Per-wallet-index nonce counter. Index 0 is seeded from `wallet.inner_nonce` - /// during setup; other indices start at 0. - wallet_nonces: HashMap, -} - -impl Default for ArcEnvironment { - fn default() -> Self { - Self::new() - } -} - -impl ArcEnvironment { - /// Creates a new empty environment. - pub fn new() -> Self { - Self { - node: None, - current_block: None, - wallet: None, - tx_hashes: HashMap::new(), - addresses: HashMap::new(), - wallet_nonces: HashMap::new(), - } - } - - /// Sets the node context. Called by `ArcSetup::apply()`. - pub(crate) fn set_node(&mut self, node: ArcNodeTestContext) { - self.node = Some(node); - } - - /// Sets the wallet. Called by `ArcSetup::apply()`. - /// - /// Seeds the nonce counter for index 0 from the wallet's starting nonce. - pub(crate) fn set_wallet(&mut self, wallet: Wallet) { - self.wallet_nonces.insert(0, wallet.inner_nonce); - self.wallet = Some(wallet); - } - - /// Updates the current block info. - pub fn set_current_block(&mut self, block: BlockInfo) { - self.current_block = Some(block); - } - - /// Returns a reference to the node - pub fn node(&self) -> &ArcNodeTestContext { - self.node.as_ref().expect("Node not initialized.") - } - - /// Returns a mutable reference to the node - pub fn node_mut(&mut self) -> &mut ArcNodeTestContext { - self.node.as_mut().expect("Node not initialized") - } - - /// Returns the current block info - pub fn current_block(&self) -> &BlockInfo { - self.current_block - .as_ref() - .expect("No current block available.") - } - - /// Returns the current block number. - pub fn block_number(&self) -> u64 { - self.current_block().number - } - - /// Returns a mutable reference to the wallet - pub fn wallet_mut(&mut self) -> eyre::Result<&mut Wallet> { - self.wallet - .as_mut() - .ok_or_else(|| eyre::eyre!("No wallet available in environment")) - } - - /// Stores a transaction hash by name for later assertions. - pub fn insert_tx_hash(&mut self, name: String, tx_hash: TxHash) -> eyre::Result<()> { - if let Some(existing) = self.tx_hashes.get(&name) { - return Err(eyre::eyre!( - "Transaction name '{}' is already in use (existing tx_hash: {}). \ - Each transaction must have a unique name.", - name, - existing - )); - } - self.tx_hashes.insert(name, tx_hash); - Ok(()) - } - - /// Gets a transaction hash by name. - pub fn get_tx_hash(&self, name: &str) -> Option<&TxHash> { - self.tx_hashes.get(name) - } - - /// Stores a named address (e.g., deployed contract address). - pub fn insert_address(&mut self, name: String, address: Address) -> eyre::Result<()> { - if let Some(existing) = self.addresses.get(&name) { - return Err(eyre::eyre!( - "Address '{}' is already in use (existing address: {}). \ - Each address must have a unique name.", - name, - existing - )); - } - self.addresses.insert(name, address); - Ok(()) - } - - /// Gets a named address. - pub fn get_address(&self, name: &str) -> Option<&Address> { - self.addresses.get(name) - } - - /// Gets and increments the nonce for a wallet at the given index. - /// - /// All indices use the same `wallet_nonces` map. Index 0 is seeded from - /// `wallet.inner_nonce` during `set_wallet`; other indices default to 0. - pub fn next_nonce_for_wallet(&mut self, wallet_index: usize) -> eyre::Result { - let nonce = self.wallet_nonces.entry(wallet_index).or_insert(0); - let current = *nonce; - *nonce += 1; - Ok(current) - } -} diff --git a/crates/execution-e2e/src/lib.rs b/crates/execution-e2e/src/lib.rs deleted file mode 100644 index ae1c735..0000000 --- a/crates/execution-e2e/src/lib.rs +++ /dev/null @@ -1,108 +0,0 @@ -// Copyright 2026 Circle Internet Group, Inc. All rights reserved. -// -// SPDX-License-Identifier: Apache-2.0 -// -// Licensed under the Apache License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. -// You may obtain a copy of the License at -// -// http://www.apache.org/licenses/LICENSE-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// See the License for the specific language governing permissions and -// limitations under the License. - -#![allow( - clippy::arithmetic_side_effects, - clippy::cast_possible_truncation, - clippy::unwrap_used -)] - -//! Arc E2E Test Framework -//! -//! An e2e testing framework for Arc execution, inspired by reth's testsuite architecture. -//! Uses the Action pattern for composable test scenarios. - -mod action; -pub mod actions; -pub mod chainspec; -mod environment; -mod setup; - -pub use action::{Action, ActionBox}; -pub use environment::{ArcEnvironment, BlockInfo}; -pub use setup::ArcSetup; - -/// Builder for creating and running Arc test scenarios. -/// -/// Follows the builder pattern to compose tests from setup and actions. -pub struct ArcTestBuilder { - setup: Option, - actions: Vec, -} - -impl Default for ArcTestBuilder { - fn default() -> Self { - Self::new() - } -} - -impl ArcTestBuilder { - /// Creates a new test builder. - pub fn new() -> Self { - Self { - setup: None, - actions: Vec::new(), - } - } - - /// Sets the test setup configuration. - pub fn with_setup(mut self, setup: ArcSetup) -> Self { - self.setup = Some(setup); - self - } - - /// Adds an action to be executed. - pub fn with_action(mut self, action: A) -> Self { - self.actions.push(ActionBox::new(action)); - self - } - - /// Runs the test scenario. - /// - /// 1. Applies the setup to create the node - /// 2. Executes all actions in sequence - pub async fn run(self) -> eyre::Result<()> { - let mut env = ArcEnvironment::new(); - - // Apply setup - if let Some(setup) = self.setup { - setup.apply(&mut env).await?; - } else { - return Err(eyre::eyre!("No setup configured")); - } - - // Execute all actions - for mut action in self.actions { - action.execute(&mut env).await?; - } - - Ok(()) - } -} - -#[cfg(test)] -mod tests { - use super::*; - - #[tokio::test] - async fn test_run_without_setup_returns_error() { - let result = ArcTestBuilder::new().run().await; - - assert!(result.is_err()); - let err = result.unwrap_err(); - assert!(err.to_string().contains("No setup configured")); - } -} diff --git a/crates/execution-e2e/src/setup.rs b/crates/execution-e2e/src/setup.rs deleted file mode 100644 index d3c9bd1..0000000 --- a/crates/execution-e2e/src/setup.rs +++ /dev/null @@ -1,196 +0,0 @@ -// Copyright 2026 Circle Internet Group, Inc. All rights reserved. -// -// SPDX-License-Identifier: Apache-2.0 -// -// Licensed under the Apache License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. -// You may obtain a copy of the License at -// -// http://www.apache.org/licenses/LICENSE-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// See the License for the specific language governing permissions and -// limitations under the License. - -//! Setup configuration for Arc e2e tests. - -use crate::environment::{ArcEnvironment, BlockInfo}; -use arc_evm_node::node::ArcNode; -use arc_execution_config::addresses_denylist::AddressesDenylistConfig; -use arc_execution_config::chainspec::{ArcChainSpec, LOCAL_DEV}; -use arc_execution_txpool::InvalidTxListConfig; -use reth_chainspec::EthChainSpec; -use reth_e2e_test_utils::NodeHelperType; -use reth_ethereum_engine_primitives::EthPayloadBuilderAttributes; -use reth_node_builder::{EngineNodeLauncher, Node, NodeBuilder, NodeConfig, NodeHandle}; -use reth_node_core::args::{DiscoveryArgs, NetworkArgs, RpcServerArgs}; -use reth_provider::providers::BlockchainProvider; -use reth_provider::HeaderProvider; -use reth_rpc_server_types::RpcModuleSelection; -use reth_tasks::Runtime; -use std::sync::Arc; - -/// Setup configuration for Arc e2e tests. -/// -/// Creates a single-node environment using the Arc chain spec and node configuration. -pub struct ArcSetup { - chain_spec: Arc, - addresses_denylist_config: Option, - invalid_tx_list_config: Option, - rpc_gas_cap: Option, -} - -impl Default for ArcSetup { - fn default() -> Self { - Self::new() - } -} - -impl ArcSetup { - /// Creates a new setup with default configuration. - /// - /// Uses the LOCAL_DEV chain spec which has all Arc hardforks active at block 0. - pub fn new() -> Self { - Self { - chain_spec: LOCAL_DEV.clone(), - addresses_denylist_config: None, - invalid_tx_list_config: None, - rpc_gas_cap: None, - } - } - - /// Sets a custom chain spec. - pub fn with_chain_spec(mut self, chain_spec: Arc) -> Self { - self.chain_spec = chain_spec; - self - } - - /// Sets a custom addresses denylist config for Engine API / newPayload validation tests. - pub fn with_addresses_denylist_config( - mut self, - addresses_denylist_config: AddressesDenylistConfig, - ) -> Self { - self.addresses_denylist_config = Some(addresses_denylist_config); - self - } - - /// Sets a custom invalid tx list config for testing invalid transaction caching. - pub fn with_invalid_tx_list_config( - mut self, - invalid_tx_list_config: InvalidTxListConfig, - ) -> Self { - self.invalid_tx_list_config = Some(invalid_tx_list_config); - self - } - - /// Overrides Reth's `--rpc.gascap` (per-request gas budget on `eth_call`, - /// `eth_estimateGas`, and the trace/debug call variants). When unset the - /// node uses Reth's stock default. - pub fn with_rpc_gas_cap(mut self, rpc_gas_cap: u64) -> Self { - self.rpc_gas_cap = Some(rpc_gas_cap); - self - } - - /// Applies the setup to create the test environment. - /// - /// This creates a single Arc node and initializes the environment with - /// the genesis block information. - pub async fn apply(self, env: &mut ArcEnvironment) -> eyre::Result<()> { - let mut arc_node = ArcNode::default(); - if let Some(cfg) = self.addresses_denylist_config { - arc_node.addresses_denylist_config = cfg; - } - if let Some(cfg) = self.invalid_tx_list_config { - arc_node.invalid_tx_list_cfg = cfg; - } - - let (node, wallet, genesis_block) = - Self::launch_node(self.chain_spec, arc_node, self.rpc_gas_cap).await?; - - env.set_node(node); - env.set_wallet(wallet); - env.set_current_block(genesis_block); - Ok(()) - } - - /// Launches a single Arc test node with the given chain spec and node configuration. - async fn launch_node( - chain_spec: Arc, - arc_node: ArcNode, - rpc_gas_cap: Option, - ) -> eyre::Result<( - NodeHelperType, - reth_e2e_test_utils::wallet::Wallet, - BlockInfo, - )> { - let attributes_generator = |_timestamp: u64| -> EthPayloadBuilderAttributes { - EthPayloadBuilderAttributes::default() - }; - - let runtime = Runtime::with_existing_handle(tokio::runtime::Handle::current())?; - let network_config = NetworkArgs { - discovery: DiscoveryArgs { - disable_discovery: true, - ..DiscoveryArgs::default() - }, - ..NetworkArgs::default() - }; - let tree_config = - reth_node_api::TreeConfig::default().with_cross_block_cache_size(1024 * 1024); - let mut rpc_args = RpcServerArgs::default() - .with_unused_ports() - .with_http() - .with_http_api(RpcModuleSelection::All); - if let Some(cap) = rpc_gas_cap { - rpc_args.rpc_gas_cap = cap; - } - let node_config = NodeConfig::new(chain_spec.clone()) - .with_network(network_config) - .with_unused_ports() - .with_rpc(rpc_args); - - let NodeHandle { - node, - node_exit_future, - } = NodeBuilder::new(node_config) - .testing_node(runtime) - .with_types_and_provider::>() - .with_components(arc_node.components_builder()) - .with_add_ons(arc_node.add_ons()) - .launch_with_fn(|builder| { - let launcher = EngineNodeLauncher::new( - builder.task_executor().clone(), - builder.config().datadir(), - tree_config, - ); - builder.launch_with(launcher) - }) - .await?; - - tokio::spawn(async move { - node_exit_future.await.expect("node exited unexpectedly"); - }); - - let node: NodeHelperType = - reth_e2e_test_utils::node::NodeTestContext::new(node, attributes_generator).await?; - let genesis_number = chain_spec.genesis_header().number; - let genesis = node.block_hash(genesis_number); - node.update_forkchoice(genesis, genesis).await?; - - let genesis_header = node - .inner - .provider() - .header_by_number(genesis_number)? - .ok_or_else(|| eyre::eyre!("Genesis header not found"))?; - let genesis_block = BlockInfo::new(genesis, 0, genesis_header.timestamp); - - // Generate 10 wallets to match localdev genesis allocations. - // Index 7 is the operator (minter role on NativeFiatToken). - let wallet = - reth_e2e_test_utils::wallet::Wallet::new(10).with_chain_id(chain_spec.chain().id()); - - Ok((node, wallet, genesis_block)) - } -} diff --git a/crates/execution-e2e/tests/base_fee.rs b/crates/execution-e2e/tests/base_fee.rs deleted file mode 100644 index aee49b5..0000000 --- a/crates/execution-e2e/tests/base_fee.rs +++ /dev/null @@ -1,213 +0,0 @@ -// Copyright 2026 Circle Internet Group, Inc. All rights reserved. -// -// SPDX-License-Identifier: Apache-2.0 -// -// Licensed under the Apache License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. -// You may obtain a copy of the License at -// -// http://www.apache.org/licenses/LICENSE-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// See the License for the specific language governing permissions and -// limitations under the License. - -#![allow(clippy::arithmetic_side_effects, clippy::cast_possible_truncation)] - -use alloy_primitives::{Bytes, U256}; -use alloy_rpc_types_engine::PayloadStatusEnum; -use arc_execution_config::{gas_fee::decode_base_fee_from_bytes, hardforks::ArcHardfork}; -use arc_execution_e2e::{ - actions::{ - build_payload_for_next_block, set_payload_override_and_rehash, submit_payload, - ProduceBlocks, - }, - chainspec::localdev_with_hardforks, - Action, ArcEnvironment, ArcSetup, -}; -use eyre::Result; -use reth_chainspec::ForkCondition; - -// ADR-0004 encodes the next block's required base fee in parent's `extra_data` (8 bytes). -// Two independent checks enforce this on every new block: -// -// Check A — consensus layer (consensus.rs: arc_validate_against_parent_base_fee), fires first: -// parent.extra_data (decoded) == child.base_fee_per_gas -// "the header's base_fee_per_gas must match what the parent promised" -// -// Check B — execution layer (executor.rs: validate_extra_data_base_fee), fires during execution: -// child.extra_data (decoded) == freshly_computed_nextBaseFee -// "the extra_data you encoded for your child must match what I compute" -// -// The tests below isolate each check by corrupting a different field: -// test_parent_child_base_fee_continuity_rejected → corrupts base_fee_per_gas → trips Check A -// test_incorrect_extra_data_base_fee_rejected_as_invalid_payload → corrupts extra_data → trips Check B - -/// Check A: arc_validate_against_parent_base_fee (consensus layer). -/// -/// Corrupts `base_fee_per_gas` on block 2 so it no longer matches the `nextBaseFee` -/// stored in block 1's `extra_data`. Rejected before execution with "block base fee mismatch". -/// -/// Unlike the absolute bounds check (which is Zero5-gated), this check fires from Zero4 -/// onwards whenever the parent's extra_data decodes as a valid 8-byte base fee. -/// It skips only when the parent is genesis (block 0), so block 1 must be produced first. -#[tokio::test] -async fn test_parent_child_base_fee_continuity_rejected() -> Result<()> { - reth_tracing::init_test_tracing(); - - // Wrong base_fee_per_gas must be rejected with the continuity error. - let status = submit_with_wrong_parent_base_fee(ArcSetup::new()).await?; - assert!( - matches!( - &status, - PayloadStatusEnum::Invalid { validation_error } - if validation_error.contains("block base fee mismatch") - ), - "Expected INVALID with 'block base fee mismatch', got {status:?}" - ); - - Ok(()) -} - -/// Check B: validate_extra_data_base_fee (execution layer). -/// -/// Corrupts `extra_data` on block 2 so it encodes a wrong `nextBaseFee` (for block 3). -/// `base_fee_per_gas` is left correct, so Check A passes. Rejected during execution -/// with "extra_data base fee mismatch". -#[tokio::test] -async fn test_incorrect_extra_data_base_fee_rejected_as_invalid_payload() -> Result<()> { - reth_tracing::init_test_tracing(); - - let mut env = ArcEnvironment::new(); - ArcSetup::new().apply(&mut env).await?; - - // Produce block 1 so block 2 has a valid parent. - let mut produce = ProduceBlocks::new(1); - produce.execute(&mut env).await?; - - // Build block 2 payload and then corrupt extra_data with a wrong 8-byte base fee value. - let (mut payload, execution_requests, parent_beacon_block_root) = - build_payload_for_next_block(&env).await?; - - let correct_extra_data = &payload.payload_inner.payload_inner.extra_data; - let correct_base_fee = decode_base_fee_from_bytes(correct_extra_data) - .ok_or_else(|| eyre::eyre!("block 2 extra_data does not contain a valid base fee"))?; - - let wrong_base_fee = correct_base_fee.wrapping_add(1); - let wrong_extra_data: Bytes = wrong_base_fee.to_be_bytes().to_vec().into(); - - let mut payload_override = payload.payload_inner.payload_inner.clone(); - payload_override.extra_data = wrong_extra_data; - set_payload_override_and_rehash( - &mut payload, - &execution_requests, - parent_beacon_block_root, - payload_override, - )?; - - let status = - submit_payload(&env, payload, execution_requests, parent_beacon_block_root).await?; - - assert!( - matches!( - &status, - PayloadStatusEnum::Invalid { validation_error } - if validation_error.contains("extra_data base fee mismatch") - ), - "Expected INVALID with 'extra_data base fee mismatch' error, got {status:?}" - ); - - Ok(()) -} - -/// arc_validate_header_base_fee enforces absolute bounds on base_fee_per_gas under Zero5. -/// -/// - Zero5 active (LOCAL_DEV default): base_fee_per_gas = 0 is below absolute_min = 1 -/// → INVALID with "block base fee mismatch" (ConsensusError::BaseFeeDiff). -/// - Zero5 not yet active: same override passes the bounds check (the block may still -/// fail for other reasons such as state root mismatch, but NOT with the bounds error). -#[tokio::test] -async fn test_base_fee_absolute_bounds_enforced_only_after_zero5() -> Result<()> { - reth_tracing::init_test_tracing(); - - // Zero5 active: base_fee_per_gas=0 must be rejected with the bounds error. - let status = submit_with_base_fee(ArcSetup::new(), U256::ZERO).await?; - assert!( - matches!( - &status, - PayloadStatusEnum::Invalid { validation_error } - if validation_error.contains("block base fee mismatch") - ), - "Zero5 active: expected INVALID with 'block base fee mismatch', got {status:?}" - ); - - // Zero5 not yet active: the bounds check is skipped — "block base fee mismatch" must not appear. - let pre_zero5_spec = localdev_with_hardforks(&[ - (ArcHardfork::Zero3, ForkCondition::Block(0)), - (ArcHardfork::Zero4, ForkCondition::Block(0)), - (ArcHardfork::Zero5, ForkCondition::Block(10)), - ]); - let status = - submit_with_base_fee(ArcSetup::new().with_chain_spec(pre_zero5_spec), U256::ZERO).await?; - assert!( - !matches!( - &status, - PayloadStatusEnum::Invalid { validation_error } - if validation_error.contains("block base fee mismatch") - ), - "Zero5 inactive: expected bounds check to be skipped, got {status:?}" - ); - - Ok(()) -} - -/// Produces block 1, then builds block 2 with a base_fee_per_gas that does not match -/// the nextBaseFee encoded in block 1's extra_data, and submits it. -/// -/// arc_validate_against_parent_base_fee skips genesis parents (block 0), so block 1 -/// must exist before the continuity check can fire. -async fn submit_with_wrong_parent_base_fee(setup: ArcSetup) -> Result { - let mut env = ArcEnvironment::new(); - setup.apply(&mut env).await?; - - // Produce block 1 — this gives block 2 a non-genesis parent with valid extra_data. - ProduceBlocks::new(1).execute(&mut env).await?; - - let (mut payload, execution_requests, parent_beacon_block_root) = - build_payload_for_next_block(&env).await?; - - // The builder sets base_fee_per_gas to match parent's nextBaseFee. Adding 1 breaks continuity. - let correct = payload.payload_inner.payload_inner.base_fee_per_gas; - let mut payload_override = payload.payload_inner.payload_inner.clone(); - payload_override.base_fee_per_gas = correct + U256::from(1u64); - set_payload_override_and_rehash( - &mut payload, - &execution_requests, - parent_beacon_block_root, - payload_override, - )?; - - submit_payload(&env, payload, execution_requests, parent_beacon_block_root).await -} - -/// Builds a block with `base_fee_per_gas` overridden to the given value and submits it. -async fn submit_with_base_fee(setup: ArcSetup, base_fee: U256) -> Result { - let mut env = ArcEnvironment::new(); - setup.apply(&mut env).await?; - - let (mut payload, execution_requests, parent_beacon_block_root) = - build_payload_for_next_block(&env).await?; - - let mut payload_override = payload.payload_inner.payload_inner.clone(); - payload_override.base_fee_per_gas = base_fee; - set_payload_override_and_rehash( - &mut payload, - &execution_requests, - parent_beacon_block_root, - payload_override, - )?; - - submit_payload(&env, payload, execution_requests, parent_beacon_block_root).await -} diff --git a/crates/execution-e2e/tests/beneficiary_blocklist.rs b/crates/execution-e2e/tests/beneficiary_blocklist.rs deleted file mode 100644 index b9c2c09..0000000 --- a/crates/execution-e2e/tests/beneficiary_blocklist.rs +++ /dev/null @@ -1,71 +0,0 @@ -// Copyright 2026 Circle Internet Group, Inc. All rights reserved. -// -// SPDX-License-Identifier: Apache-2.0 -// -// Licensed under the Apache License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. -// You may obtain a copy of the License at -// -// http://www.apache.org/licenses/LICENSE-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// See the License for the specific language governing permissions and -// limitations under the License. - -//! E2E test covering beneficiary blocklist enforcement during payload validation. - -use alloy_primitives::address; -use alloy_rpc_types_engine::PayloadStatusEnum; -use arc_execution_e2e::{ - actions::{build_payload_for_next_block, set_payload_override_and_rehash, submit_payload}, - chainspec::localdev_with_storage_override, - ArcEnvironment, ArcSetup, -}; -use eyre::Result; - -/// Ensure proposer-selected beneficiaries are rejected when blocklisted. -/// -/// - Header beneficiary is pre-blocklisted in NativeCoinControl -/// - Payload must be INVALID with blocked-address validation error -#[tokio::test] -async fn test_proposer_selected_blocklisted_beneficiary_is_invalid() -> Result<()> { - reth_tracing::init_test_tracing(); - - let blocklisted_beneficiary = address!("0xbad0000000000000000000000000000000000001"); - let chain_spec = localdev_with_storage_override(Some(blocklisted_beneficiary)); - - let mut env = ArcEnvironment::new(); - ArcSetup::new() - .with_chain_spec(chain_spec) - .apply(&mut env) - .await?; - - let (mut payload, execution_requests, parent_beacon_block_root) = - build_payload_for_next_block(&env).await?; - let mut payload_override = payload.payload_inner.payload_inner.clone(); - payload_override.fee_recipient = blocklisted_beneficiary; - set_payload_override_and_rehash( - &mut payload, - &execution_requests, - parent_beacon_block_root, - payload_override, - )?; - - let status = submit_payload(&env, payload, execution_requests, parent_beacon_block_root) - .await - .expect("submit_payload should return Ok for blocklisted proposer-selected beneficiary"); - - assert!( - matches!( - &status, - PayloadStatusEnum::Invalid { validation_error } - if validation_error.to_ascii_lowercase().contains("blocked address") - ), - "Expected INVALID with blocked-address validation error, got {:?}", - status - ); - - Ok(()) -} diff --git a/crates/execution-e2e/tests/block_hash_history.rs b/crates/execution-e2e/tests/block_hash_history.rs deleted file mode 100644 index 2025b18..0000000 --- a/crates/execution-e2e/tests/block_hash_history.rs +++ /dev/null @@ -1,134 +0,0 @@ -// Copyright 2026 Circle Internet Group, Inc. All rights reserved. -// -// SPDX-License-Identifier: Apache-2.0 -// -// Licensed under the Apache License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. -// You may obtain a copy of the License at -// -// http://www.apache.org/licenses/LICENSE-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// See the License for the specific language governing permissions and -// limitations under the License. - -//! EIP-2935 BlockHashHistory e2e tests for Arc Chain. -//! -//! Tests that the EIP-2935 system call persists parent block hashes in the -//! history storage contract at `0x0000F90827F1C53a10cb7A02335B175320002935`. -//! -//! The system call runs at the start of each block (Zero5+), storing -//! `parent_hash` in a ring buffer of size 8191. - -use alloy_primitives::{address, Address, Bytes}; -use arc_execution_config::hardforks::ArcHardfork; -use arc_execution_e2e::{ - actions::{AssertBlockNumber, CallContract, ProduceBlocks}, - chainspec::localdev_with_hardforks, - ArcSetup, ArcTestBuilder, -}; -use eyre::Result; -use reth_chainspec::ForkCondition; - -/// EIP-2935 History Storage Contract address. -const HISTORY_STORAGE_ADDRESS: Address = address!("0000F90827F1C53a10cb7A02335B175320002935"); - -/// Helper: encode a block number as 32-byte big-endian calldata for the -/// history storage contract's `get(uint256)` interface. -fn block_number_calldata(block_number: u64) -> Bytes { - let mut buf = [0u8; 32]; - buf[24..32].copy_from_slice(&block_number.to_be_bytes()); - Bytes::copy_from_slice(&buf) -} - -/// After producing blocks, querying the history storage contract for a recent -/// block number should return a non-zero hash (the parent hash written by the -/// EIP-2935 system call). -#[tokio::test] -async fn test_block_hash_history_returns_non_zero_for_recent_block() -> Result<()> { - reth_tracing::init_test_tracing(); - - ArcTestBuilder::new() - .with_setup(ArcSetup::new()) - // Produce 3 blocks so block 1 and 2 have parent hashes stored. - .with_action(ProduceBlocks::new(3)) - // Query block 1: the system call at block 1 stores the genesis hash. - .with_action( - CallContract::new("block_hash_history_block_1") - .to(HISTORY_STORAGE_ADDRESS) - .with_data(block_number_calldata(1)) - .expect_non_zero_result(), - ) - // Query block 2: the system call at block 2 stores block 1's hash. - .with_action( - CallContract::new("block_hash_history_block_2") - .to(HISTORY_STORAGE_ADDRESS) - .with_data(block_number_calldata(2)) - .expect_non_zero_result(), - ) - .run() - .await -} - -/// Querying the history storage contract for a far-future block number should -/// return zero (no hash stored). -#[tokio::test] -async fn test_block_hash_history_returns_zero_for_future_block() -> Result<()> { - reth_tracing::init_test_tracing(); - - ArcTestBuilder::new() - .with_setup(ArcSetup::new()) - .with_action(ProduceBlocks::new(1)) - // Query a block number far in the future — should return zero. - .with_action( - CallContract::new("block_hash_history_future_block") - .to(HISTORY_STORAGE_ADDRESS) - .with_data(block_number_calldata(99999)) - .expect_revert(), - ) - .run() - .await -} - -/// Crossing the Zero5 activation boundary: hashes must start being written at the -/// activation block and not before. -/// -/// Chain spec: Zero5 activates at block 3. -/// - Produce blocks 1-2: pre-Zero5, no entries written. -/// - Produce blocks 3-4: Zero5 active, system call runs. -/// - Assert: slot for block 1 is zero (pre-activation, no system call wrote there). -/// - Assert: contract has an entry for block 3 (first Zero5 block). -#[tokio::test] -async fn test_block_hash_history_starts_at_zero5_activation() -> Result<()> { - reth_tracing::init_test_tracing(); - - let chain_spec = localdev_with_hardforks(&[ - (ArcHardfork::Zero3, ForkCondition::Block(0)), - (ArcHardfork::Zero4, ForkCondition::Block(0)), - (ArcHardfork::Zero5, ForkCondition::Block(3)), - (ArcHardfork::Zero6, ForkCondition::Block(3)), - ]); - - ArcTestBuilder::new() - .with_setup(ArcSetup::new().with_chain_spec(chain_spec)) - .with_action(ProduceBlocks::new(4)) - .with_action(AssertBlockNumber::new(4)) - // Block 1 is pre-Zero5 — no system call wrote to slot 1, expect zero. - .with_action( - CallContract::new("block_hash_history_pre_activation") - .to(HISTORY_STORAGE_ADDRESS) - .with_data(block_number_calldata(1)) - .expect_result(Bytes::from([0u8; 32])), - ) - // Block 3 is the first Zero5 block — system call ran, expect non-zero. - .with_action( - CallContract::new("block_hash_history_at_activation") - .to(HISTORY_STORAGE_ADDRESS) - .with_data(block_number_calldata(3)) - .expect_non_zero_result(), - ) - .run() - .await -} diff --git a/crates/execution-e2e/tests/block_production.rs b/crates/execution-e2e/tests/block_production.rs deleted file mode 100644 index 17f2847..0000000 --- a/crates/execution-e2e/tests/block_production.rs +++ /dev/null @@ -1,68 +0,0 @@ -// Copyright 2026 Circle Internet Group, Inc. All rights reserved. -// -// SPDX-License-Identifier: Apache-2.0 -// -// Licensed under the Apache License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. -// You may obtain a copy of the License at -// -// http://www.apache.org/licenses/LICENSE-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// See the License for the specific language governing permissions and -// limitations under the License. - -//! Basic block production e2e tests for Arc Chain. - -use arc_execution_e2e::{ - actions::{AssertBlockNumber, ProduceBlocks, ProduceInvalidBlock}, - ArcSetup, ArcTestBuilder, -}; -use eyre::Result; - -/// Test produce a single block. -#[tokio::test] -async fn test_produce_single_block() -> Result<()> { - reth_tracing::init_test_tracing(); - - ArcTestBuilder::new() - .with_setup(ArcSetup::new()) - .with_action(ProduceBlocks::new(1)) - .with_action(AssertBlockNumber::new(1)) - .run() - .await -} - -/// Test produce multiple blocks. -#[tokio::test] -async fn test_incremental_block_production() -> Result<()> { - reth_tracing::init_test_tracing(); - - ArcTestBuilder::new() - .with_setup(ArcSetup::new()) - .with_action(ProduceBlocks::new(3)) - .with_action(AssertBlockNumber::new(3)) - .with_action(ProduceBlocks::new(2)) - .with_action(AssertBlockNumber::new(5)) - .run() - .await -} - -/// Test that blocks with corrupted state root are rejected. -#[tokio::test] -async fn test_block_with_corrupted_state_root_rejected() -> Result<()> { - reth_tracing::init_test_tracing(); - - ArcTestBuilder::new() - .with_setup(ArcSetup::new()) - // Produce a valid block first - .with_action(ProduceBlocks::new(1)) - .with_action(AssertBlockNumber::new(1)) - // Try to produce a block with corrupted state root - should be rejected - .with_action(ProduceInvalidBlock::new()) - .with_action(AssertBlockNumber::new(1)) - .run() - .await -} diff --git a/crates/execution-e2e/tests/denylist.rs b/crates/execution-e2e/tests/denylist.rs deleted file mode 100644 index 56b8dc0..0000000 --- a/crates/execution-e2e/tests/denylist.rs +++ /dev/null @@ -1,183 +0,0 @@ -// Copyright 2026 Circle Internet Group, Inc. All rights reserved. -// -// SPDX-License-Identifier: Apache-2.0 -// -// Licensed under the Apache License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. -// You may obtain a copy of the License at -// -// http://www.apache.org/licenses/LICENSE-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// See the License for the specific language governing permissions and -// limitations under the License. - -#![allow(clippy::arithmetic_side_effects, clippy::cast_possible_truncation)] - -//! E2E tests for the addresses denylist. -//! -//! Covers: -//! - to: denylisted → rejected -//! - from: denylisted → rejected -//! - denylist disabled: from denylisted → accepted -//! - addresses-exclusions: from denylisted but excluded → accepted - -use alloy_network::eip2718::{Decodable2718, Encodable2718}; -use alloy_primitives::{address, Address, TxKind, U256}; -use alloy_rpc_types_eth::{TransactionInput, TransactionRequest}; -use arc_execution_config::addresses_denylist::{ - AddressesDenylistConfig, DEFAULT_DENYLIST_ADDRESS, DEFAULT_DENYLIST_ERC7201_BASE_SLOT, -}; -use arc_execution_config::chainspec::ArcChainSpec; -use arc_execution_e2e::{chainspec::localdev_with_denylisted_addresses, ArcEnvironment, ArcSetup}; -use arc_execution_txpool::ArcTransactionValidatorError; -use eyre::Result; -use reth_chainspec::EthChainSpec; -use reth_e2e_test_utils::transaction::TransactionTestContext; -use reth_ethereum_primitives::TransactionSigned; -use reth_primitives_traits::SignerRecoverable; -use reth_transaction_pool::error::{PoolError, PoolErrorKind}; -use reth_transaction_pool::{TransactionOrigin, TransactionPool}; -use std::sync::Arc; - -/// First account from test mnemonic (0xf39Fd...), funded in localdev genesis. -const WALLET_FIRST_ADDRESS: Address = address!("0xf39Fd6e51aad88F6F4ce6aB8827279cffFb92266"); - -fn assert_denylisted_address_error(err: &PoolError, expected_addr: Address) { - let invalid = match &err.kind { - PoolErrorKind::InvalidTransaction(e) => e, - other => panic!("Expected InvalidTransaction (denylist), got: {:?}", other), - }; - let arc_err = invalid - .downcast_other_ref::() - .expect("Expected ArcTransactionValidatorError"); - match arc_err { - ArcTransactionValidatorError::DenylistedAddressError(addr) => { - assert_eq!(addr, &expected_addr); - } - other => panic!("Expected DenylistedAddressError, got: {:?}", other), - } -} - -fn denylist_config_enabled(exclusions: Vec
) -> Result { - Ok(AddressesDenylistConfig::try_new( - true, - Some(DEFAULT_DENYLIST_ADDRESS), - Some(DEFAULT_DENYLIST_ERC7201_BASE_SLOT), - exclusions, - )?) -} - -fn denylist_config_disabled() -> Result { - Ok(AddressesDenylistConfig::try_new( - false, - None, - None, - Vec::new(), - )?) -} - -/// Builds a signed tx from the first test wallet (WALLET_FIRST_ADDRESS) to `to`, returns raw encoded bytes. -async fn build_signed_tx_raw(chain_spec: &ArcChainSpec, to: Address) -> alloy_primitives::Bytes { - let wallet = - reth_e2e_test_utils::wallet::Wallet::default().with_chain_id(chain_spec.chain().id()); - let signer = wallet - .wallet_gen() - .first() - .cloned() - .expect("First wallet from test mnemonic"); - let tx = TransactionRequest { - nonce: Some(0), - value: Some(U256::from(1)), - to: Some(TxKind::Call(to)), - gas: Some(26000), - max_fee_per_gas: Some(1000e9 as u128), - max_priority_fee_per_gas: Some(1e9 as u128), - chain_id: Some(wallet.chain_id), - input: TransactionInput::default(), - ..Default::default() - }; - let signed_tx = TransactionTestContext::sign_tx(signer, tx).await; - signed_tx.encoded_2718().into() -} - -/// Launches a node with the given chain spec and denylist config, then signs and submits -/// a tx from the first wallet to `to`. Returns pool result (Err is PoolError when pool rejects). -async fn sign_and_submit_tx( - chain_spec: Arc, - addresses_denylist_config: AddressesDenylistConfig, - to: Address, -) -> Result<(), eyre::Report> { - let mut env = ArcEnvironment::new(); - ArcSetup::new() - .with_chain_spec(chain_spec.clone()) - .with_addresses_denylist_config(addresses_denylist_config) - .apply(&mut env) - .await?; - - let raw_tx = build_signed_tx_raw(&chain_spec, to).await; - let tx_signed = TransactionSigned::decode_2718(&mut raw_tx.as_ref()).expect("Decode tx"); - let recovered_tx = tx_signed.try_into_recovered().expect("Recover signer"); - env.node() - .inner - .pool - .add_consensus_transaction(recovered_tx, TransactionOrigin::Local) - .await - .map_err(Into::into) - .map(|_| ()) -} - -/// Transaction to a denylisted address is rejected. -#[tokio::test] -async fn test_denylisted_to_rejected() -> Result<()> { - reth_tracing::init_test_tracing(); - let denylisted_to = address!("0xdead000000000000000000000000000000000001"); - let chain_spec = localdev_with_denylisted_addresses(vec![denylisted_to]); - let addresses_denylist_config = denylist_config_enabled(Vec::new())?; - let err = sign_and_submit_tx(chain_spec, addresses_denylist_config, denylisted_to) - .await - .expect_err("Expected pool to reject tx to denylisted address"); - let pool_err = err.downcast_ref::().expect("Expected PoolError"); - assert_denylisted_address_error(pool_err, denylisted_to); - Ok(()) -} - -/// Transaction from a denylisted address is rejected. -#[tokio::test] -async fn test_denylisted_from_rejected() -> Result<()> { - reth_tracing::init_test_tracing(); - let chain_spec = localdev_with_denylisted_addresses(vec![WALLET_FIRST_ADDRESS]); - let addresses_denylist_config = denylist_config_enabled(Vec::new())?; - let err = sign_and_submit_tx(chain_spec, addresses_denylist_config, Address::random()) - .await - .expect_err("Expected pool to reject tx from denylisted address"); - let pool_err = err.downcast_ref::().expect("Expected PoolError"); - assert_denylisted_address_error(pool_err, WALLET_FIRST_ADDRESS); - Ok(()) -} - -/// When denylist is disabled (--arc.denylist.enabled=false), tx from denylisted address is accepted. -#[tokio::test] -async fn test_denylist_disabled_accepts_from_denylisted() -> Result<()> { - reth_tracing::init_test_tracing(); - let chain_spec = localdev_with_denylisted_addresses(vec![WALLET_FIRST_ADDRESS]); - let addresses_denylist_config = denylist_config_disabled()?; - sign_and_submit_tx(chain_spec, addresses_denylist_config, Address::random()) - .await - .expect("Expected pool to accept tx when denylist disabled"); - Ok(()) -} - -/// When denylist is enabled --arc.denylist.enabled=true, but address is in --arc.denylist.addresses-exclusions, tx from that denylisted address is accepted. -#[tokio::test] -async fn test_denylist_exclusion_accepts_from_denylisted() -> Result<()> { - reth_tracing::init_test_tracing(); - let chain_spec = localdev_with_denylisted_addresses(vec![WALLET_FIRST_ADDRESS]); - let addresses_denylist_config = denylist_config_enabled(vec![WALLET_FIRST_ADDRESS])?; - sign_and_submit_tx(chain_spec, addresses_denylist_config, Address::random()) - .await - .expect("Expected pool to accept tx when sender in addresses-exclusions"); - Ok(()) -} diff --git a/crates/execution-e2e/tests/eip7708_denylist.rs b/crates/execution-e2e/tests/eip7708_denylist.rs deleted file mode 100644 index 40e8be3..0000000 --- a/crates/execution-e2e/tests/eip7708_denylist.rs +++ /dev/null @@ -1,224 +0,0 @@ -// Copyright 2026 Circle Internet Group, Inc. All rights reserved. -// -// SPDX-License-Identifier: Apache-2.0 -// -// Licensed under the Apache License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. -// You may obtain a copy of the License at -// -// http://www.apache.org/licenses/LICENSE-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// See the License for the specific language governing permissions and -// limitations under the License. - -//! EIP-7708 denylist interaction e2e tests. -//! -//! Verifies that the addresses denylist correctly blocks transactions with value -//! transfers to/from denylisted addresses, and that exclusion lists allow -//! transfers with proper EIP-7708 log emission. - -mod helpers; - -use alloy_network::eip2718::{Decodable2718, Encodable2718}; -use alloy_primitives::{address, Address, TxKind, U256}; -use alloy_rpc_types_eth::{TransactionInput, TransactionRequest}; -use arc_execution_config::addresses_denylist::{ - AddressesDenylistConfig, DEFAULT_DENYLIST_ADDRESS, DEFAULT_DENYLIST_ERC7201_BASE_SLOT, -}; -use arc_execution_config::chainspec::ArcChainSpec; -use arc_execution_e2e::{ - actions::{ - AssertTxIncluded, AssertTxLogs, AssertTxTrace, ProduceBlocks, SendTransaction, TxStatus, - }, - chainspec::localdev_with_denylisted_addresses, - ArcEnvironment, ArcSetup, ArcTestBuilder, -}; -use arc_execution_txpool::ArcTransactionValidatorError; -use helpers::constants::{SYSTEM_ADDRESS, WALLET_FIRST_ADDRESS}; -use reth_chainspec::EthChainSpec; -use reth_e2e_test_utils::transaction::TransactionTestContext; -use reth_ethereum_primitives::TransactionSigned; -use reth_primitives_traits::SignerRecoverable; -use reth_transaction_pool::error::{PoolError, PoolErrorKind}; -use reth_transaction_pool::{TransactionOrigin, TransactionPool}; -use std::sync::Arc; - -fn denylist_config_enabled(exclusions: Vec
) -> eyre::Result { - Ok(AddressesDenylistConfig::try_new( - true, - Some(DEFAULT_DENYLIST_ADDRESS), - Some(DEFAULT_DENYLIST_ERC7201_BASE_SLOT), - exclusions, - )?) -} - -/// Builds a signed tx from a wallet to `to` with a given value. -/// -/// Uses `wallet.inner` directly instead of regenerating signers via `wallet_gen()`. -async fn build_signed_tx_raw( - wallet: &reth_e2e_test_utils::wallet::Wallet, - to: Address, - value: U256, -) -> alloy_primitives::Bytes { - let tx = TransactionRequest { - nonce: Some(0), - value: Some(value), - to: Some(TxKind::Call(to)), - gas: Some(26000), - max_fee_per_gas: Some(1_000_000_000_000), - max_priority_fee_per_gas: Some(1_000_000_000), - chain_id: Some(wallet.chain_id), - input: TransactionInput::default(), - ..Default::default() - }; - let signed_tx = TransactionTestContext::sign_tx(wallet.inner.clone(), tx).await; - signed_tx.encoded_2718().into() -} - -/// Launches a node with denylist config, signs and submits a value transfer tx. -async fn sign_and_submit_value_tx( - chain_spec: Arc, - addresses_denylist_config: AddressesDenylistConfig, - to: Address, - value: U256, -) -> Result<(), eyre::Report> { - let mut env = ArcEnvironment::new(); - ArcSetup::new() - .with_chain_spec(chain_spec.clone()) - .with_addresses_denylist_config(addresses_denylist_config) - .apply(&mut env) - .await?; - - let wallet = - reth_e2e_test_utils::wallet::Wallet::default().with_chain_id(chain_spec.chain().id()); - - let raw_tx = build_signed_tx_raw(&wallet, to, value).await; - let tx_signed = TransactionSigned::decode_2718(&mut raw_tx.as_ref()).expect("Decode tx"); - let recovered_tx = tx_signed.try_into_recovered().expect("Recover signer"); - env.node() - .inner - .pool - .add_consensus_transaction(recovered_tx, TransactionOrigin::Local) - .await - .map_err(Into::into) - .map(|_| ()) -} - -/// Test #27: Value transfer to a denylisted address is rejected by the txpool. -#[tokio::test] -async fn test_value_transfer_to_denylisted_rejected() -> eyre::Result<()> { - reth_tracing::init_test_tracing(); - - let denylisted_to = address!("0xdead000000000000000000000000000000000001"); - let chain_spec = localdev_with_denylisted_addresses(vec![denylisted_to]); - let addresses_denylist_config = denylist_config_enabled(Vec::new())?; - - let err = sign_and_submit_value_tx( - chain_spec, - addresses_denylist_config, - denylisted_to, - U256::from(1_000_000), - ) - .await - .expect_err("Expected pool to reject tx to denylisted address"); - - let pool_err = err.downcast_ref::().expect("Expected PoolError"); - let invalid = match &pool_err.kind { - PoolErrorKind::InvalidTransaction(e) => e, - other => panic!("Expected InvalidTransaction (denylist), got: {:?}", other), - }; - let arc_err = invalid - .downcast_other_ref::() - .expect("Expected ArcTransactionValidatorError"); - assert!( - matches!( - arc_err, - ArcTransactionValidatorError::DenylistedAddressError(_) - ), - "Expected DenylistedAddressError, got: {:?}", - arc_err - ); - - Ok(()) -} - -/// Test #28: Value transfer from a denylisted sender is rejected by the txpool. -#[tokio::test] -async fn test_value_transfer_from_denylisted_rejected() -> eyre::Result<()> { - reth_tracing::init_test_tracing(); - - let chain_spec = localdev_with_denylisted_addresses(vec![WALLET_FIRST_ADDRESS]); - let addresses_denylist_config = denylist_config_enabled(Vec::new())?; - - let err = sign_and_submit_value_tx( - chain_spec, - addresses_denylist_config, - Address::random(), - U256::from(1_000_000), - ) - .await - .expect_err("Expected pool to reject tx from denylisted address"); - - let pool_err = err.downcast_ref::().expect("Expected PoolError"); - let invalid = match &pool_err.kind { - PoolErrorKind::InvalidTransaction(e) => e, - other => panic!("Expected InvalidTransaction (denylist), got: {:?}", other), - }; - let arc_err = invalid - .downcast_other_ref::() - .expect("Expected ArcTransactionValidatorError"); - assert!( - matches!( - arc_err, - ArcTransactionValidatorError::DenylistedAddressError(_) - ), - "Expected DenylistedAddressError, got: {:?}", - arc_err - ); - - Ok(()) -} - -/// Test #29: Excluded address can send value transfer and EIP-7708 log is emitted. -/// -/// When denylist is enabled but the sender is in the exclusion list, -/// the transfer proceeds and the standard EIP-7708 Transfer log is emitted. -#[tokio::test] -async fn test_denylist_exclusion_allows_transfer_with_log() { - reth_tracing::init_test_tracing(); - - let recipient = address!("0x000000000000000000000000000000000000CAFE"); - let value = U256::from(1_000_000); - - // The sender (WALLET_FIRST_ADDRESS) is denylisted but also in the exclusion list - let chain_spec = localdev_with_denylisted_addresses(vec![WALLET_FIRST_ADDRESS]); - let addresses_denylist_config = - denylist_config_enabled(vec![WALLET_FIRST_ADDRESS]).expect("denylist config"); - - ArcTestBuilder::new() - .with_setup( - ArcSetup::new() - .with_chain_spec(chain_spec) - .with_addresses_denylist_config(addresses_denylist_config), - ) - .with_action( - SendTransaction::new("transfer") - .with_to(recipient) - .with_value(value), - ) - .with_action(ProduceBlocks::new(1)) - .with_action(AssertTxIncluded::new("transfer").expect(TxStatus::Success)) - .with_action( - AssertTxLogs::new("transfer") - .expect_log_count(1) - .expect_emitter_at(0, SYSTEM_ADDRESS) - .expect_transfer_event(0, WALLET_FIRST_ADDRESS, recipient, value), - ) - .with_action(AssertTxTrace::new("transfer")) - .run() - .await - .expect("test_denylist_exclusion_allows_transfer_with_log failed"); -} diff --git a/crates/execution-e2e/tests/eip7708_edge_cases.rs b/crates/execution-e2e/tests/eip7708_edge_cases.rs deleted file mode 100644 index f0d6da5..0000000 --- a/crates/execution-e2e/tests/eip7708_edge_cases.rs +++ /dev/null @@ -1,356 +0,0 @@ -// Copyright 2026 Circle Internet Group, Inc. All rights reserved. -// -// SPDX-License-Identifier: Apache-2.0 -// -// Licensed under the Apache License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. -// You may obtain a copy of the License at -// -// http://www.apache.org/licenses/LICENSE-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// See the License for the specific language governing permissions and -// limitations under the License. - -//! EIP-7708 edge case e2e tests. -//! -//! Tests revert rollback, multi-log composition, inner-call semantics, -//! and unusual transfer patterns. - -mod helpers; - -use alloy_primitives::{address, U256}; -use arc_execution_e2e::{ - actions::{ - AssertTransferEvent, AssertTxIncluded, AssertTxLogs, AssertTxTrace, ProduceBlocks, - SendTransaction, StoreDeployedAddress, TxStatus, - }, - ArcSetup, ArcTestBuilder, -}; -use helpers::{ - constants::{NATIVE_COIN_AUTHORITY_ADDRESS, SYSTEM_ADDRESS, WALLET_FIRST_ADDRESS}, - contracts::right_pad_address, -}; -use rstest::rstest; - -/// Test #48: Send value to a reverting contract — tx reverts, no EIP-7708 log. -/// -/// When the entire CALL frame reverts, the EIP-7708 log is rolled back. -/// Deploys an actual reverting contract rather than using an existing address. -#[tokio::test] -async fn test_reverted_call_no_log() { - reth_tracing::init_test_tracing(); - - let deploy_code = helpers::contracts::reverting_contract_deploy_code(); - let value = U256::from(1_000_000); - - ArcTestBuilder::new() - .with_setup(ArcSetup::new()) - .with_action( - SendTransaction::new("deploy") - .with_create() - .with_data(deploy_code) - .with_value(U256::ZERO) - .with_gas_limit(100_000), - ) - .with_action(ProduceBlocks::new(1)) - .with_action(AssertTxIncluded::new("deploy").expect(TxStatus::Success)) - .with_action(StoreDeployedAddress::new("deploy")) - // Send value to the reverting contract - .with_action( - SendTransaction::new("revert_call") - .with_to_named("deploy_address") - .with_value(value) - .with_gas_limit(100_000), - ) - .with_action(ProduceBlocks::new(1)) - .with_action(AssertTxIncluded::new("revert_call").expect(TxStatus::Reverted)) - .with_action(AssertTxLogs::new("revert_call").expect_no_logs()) - .with_action(AssertTxTrace::new("revert_call")) - .run() - .await - .expect("test_reverted_call_no_log failed"); -} - -/// Tests #49/#50: Inner CALL reverts but outer succeeds — outer log emitted, inner log rolled back. -/// -/// Deploys a reverting contract and an outer contract that forwards value to it. -/// The outer contract accepts value (emitting sender→outer log), then makes an -/// inner CALL with value to the reverting contract. The inner frame reverts, so -/// the inner value transfer log (outer→reverting) is rolled back. Only the -/// outer log remains. Parameterized over transfer amount to verify consistency. -#[rstest] -#[case::standard_value(U256::from(1_000_000))] -#[case::smaller_value(U256::from(500_000))] -#[tokio::test] -async fn test_inner_call_reverts_outer_succeeds(#[case] value: U256) { - reth_tracing::init_test_tracing(); - - let reverting_deploy = helpers::contracts::reverting_contract_deploy_code(); - let outer_deploy = helpers::contracts::call_target_with_value_contract_deploy_code(); - - ArcTestBuilder::new() - .with_setup(ArcSetup::new()) - // Deploy the reverting contract (inner target) - .with_action( - SendTransaction::new("deploy_reverting") - .with_create() - .with_data(reverting_deploy) - .with_value(U256::ZERO) - .with_gas_limit(200_000), - ) - .with_action(ProduceBlocks::new(1)) - .with_action(AssertTxIncluded::new("deploy_reverting").expect(TxStatus::Success)) - .with_action(StoreDeployedAddress::new("deploy_reverting")) - // Deploy the outer contract (calls target from calldata with value) - .with_action( - SendTransaction::new("deploy_outer") - .with_create() - .with_data(outer_deploy) - .with_value(U256::ZERO) - .with_gas_limit(200_000), - ) - .with_action(ProduceBlocks::new(1)) - .with_action(AssertTxIncluded::new("deploy_outer").expect(TxStatus::Success)) - .with_action(StoreDeployedAddress::new("deploy_outer")) - // Call the outer contract with value, passing the reverting contract's address - // as calldata. Uses stored address instead of nonce-derived guess. - .with_action( - SendTransaction::new("call") - .with_to_named("deploy_outer_address") - .with_value(value) - .with_data_fn(|env| { - let addr = env.get_address("deploy_reverting_address").ok_or_else(|| { - eyre::eyre!("Named address 'deploy_reverting_address' not found") - })?; - Ok(right_pad_address(*addr)) - }) - .with_gas_limit(200_000), - ) - .with_action(ProduceBlocks::new(1)) - .with_action(AssertTxIncluded::new("call").expect(TxStatus::Success)) - // Outer frame succeeds → sender→outer_contract log preserved. - // Inner call reverts → inner log rolled back. - .with_action(AssertTxLogs::new("call").expect_log_count(1)) - .with_action(AssertTransferEvent::new( - "call", - 0, - WALLET_FIRST_ADDRESS, - AssertTransferEvent::named("deploy_outer_address"), - value, - )) - .with_action(AssertTxTrace::new("call")) - .run() - .await - .expect("test_inner_call_reverts_outer_succeeds failed"); -} - -/// Test #51: Multiple sequential value transfers in separate blocks. -/// -/// Each block contains a value transfer, verifying logs are emitted consistently -/// across blocks and don't leak between transactions. -#[tokio::test] -async fn test_sequential_blocks_each_emit_log() { - reth_tracing::init_test_tracing(); - - let recipient_1 = address!("0x000000000000000000000000000000000000AAA1"); - let recipient_2 = address!("0x000000000000000000000000000000000000AAA2"); - let value_1 = U256::from(100_000); - let value_2 = U256::from(200_000); - - ArcTestBuilder::new() - .with_setup(ArcSetup::new()) - // Block 1 - .with_action( - SendTransaction::new("tx1") - .with_to(recipient_1) - .with_value(value_1), - ) - .with_action(ProduceBlocks::new(1)) - .with_action(AssertTxIncluded::new("tx1").expect(TxStatus::Success)) - .with_action( - AssertTxLogs::new("tx1") - .expect_log_count(1) - .expect_emitter_at(0, SYSTEM_ADDRESS) - .expect_transfer_event(0, WALLET_FIRST_ADDRESS, recipient_1, value_1), - ) - // Block 2 - .with_action( - SendTransaction::new("tx2") - .with_to(recipient_2) - .with_value(value_2), - ) - .with_action(ProduceBlocks::new(1)) - .with_action(AssertTxIncluded::new("tx2").expect(TxStatus::Success)) - .with_action( - AssertTxLogs::new("tx2") - .expect_log_count(1) - .expect_emitter_at(0, SYSTEM_ADDRESS) - .expect_transfer_event(0, WALLET_FIRST_ADDRESS, recipient_2, value_2), - ) - .with_action(AssertTxTrace::new("tx1")) - .with_action(AssertTxTrace::new("tx2")) - .run() - .await - .expect("test_sequential_blocks_each_emit_log failed"); -} - -/// Test #52: Contract calls NativeCoinAuthority precompile with value. -/// -/// Deploys a contract that forwards a CALL with value to the NativeCoinAuthority -/// precompile address. The precompile will revert (unauthorized caller), but -/// the outer frame succeeds. The outer value transfer log (sender→contract) -/// is preserved; the inner log (contract→precompile) is rolled back because -/// the precompile rejects the call. -#[tokio::test] -async fn test_contract_calls_precompile_with_value() { - reth_tracing::init_test_tracing(); - - let deploy_code = helpers::contracts::call_target_with_value_contract_deploy_code(); - let value = U256::from(500_000); - - // Encode NativeCoinAuthority address as calldata target. - // The bytecode reads target via CALLDATALOAD(0) + SHR(96), extracting - // the top 20 bytes. So address must be right-padded (address at left). - let calldata = helpers::contracts::right_pad_address(NATIVE_COIN_AUTHORITY_ADDRESS); - - ArcTestBuilder::new() - .with_setup(ArcSetup::new()) - .with_action( - SendTransaction::new("deploy") - .with_create() - .with_data(deploy_code) - .with_value(U256::ZERO) - .with_gas_limit(200_000), - ) - .with_action(ProduceBlocks::new(1)) - .with_action(AssertTxIncluded::new("deploy").expect(TxStatus::Success)) - .with_action(StoreDeployedAddress::new("deploy")) - // Call the contract with value + precompile target in calldata - .with_action( - SendTransaction::new("call") - .with_to_named("deploy_address") - .with_value(value) - .with_data(calldata) - .with_gas_limit(200_000), - ) - .with_action(ProduceBlocks::new(1)) - .with_action(AssertTxIncluded::new("call").expect(TxStatus::Success)) - // Outer transfer succeeds (sender→contract), inner reverts (contract→precompile) - // Only the outer log with exact from/to/value - .with_action(AssertTxLogs::new("call").expect_log_count(1)) - .with_action(AssertTransferEvent::new( - "call", - 0, - WALLET_FIRST_ADDRESS, - AssertTransferEvent::named("deploy_address"), - value, - )) - .with_action(AssertTxTrace::new("call")) - .run() - .await - .expect("test_contract_calls_precompile_with_value failed"); -} - -/// Test #53: Value transfer after producing multiple empty blocks. -/// -/// Verifies that EIP-7708 log emission works correctly even when -/// there are empty blocks between genesis and the transfer. -#[tokio::test] -async fn test_log_after_empty_blocks() { - reth_tracing::init_test_tracing(); - - let recipient = address!("0x0000000000000000000000000000000000CC0001"); - let value = U256::from(500_000); - - ArcTestBuilder::new() - .with_setup(ArcSetup::new()) - // Produce several empty blocks first - .with_action(ProduceBlocks::new(5)) - // Now send a value transfer - .with_action( - SendTransaction::new("transfer") - .with_to(recipient) - .with_value(value), - ) - .with_action(ProduceBlocks::new(1)) - .with_action(AssertTxIncluded::new("transfer").expect(TxStatus::Success)) - .with_action( - AssertTxLogs::new("transfer") - .expect_log_count(1) - .expect_emitter_at(0, SYSTEM_ADDRESS) - .expect_transfer_event(0, WALLET_FIRST_ADDRESS, recipient, value), - ) - .with_action(AssertTxTrace::new("transfer")) - .run() - .await - .expect("test_log_after_empty_blocks failed"); -} - -/// Test: Transfer to a contract that exists but has no code (EOA-like). -#[tokio::test] -async fn test_transfer_to_codeless_address() { - reth_tracing::init_test_tracing(); - - let target = address!("0x000000000000000000000000000000000000DEAD"); - let value = U256::from(1_000); - - ArcTestBuilder::new() - .with_setup(ArcSetup::new()) - .with_action( - SendTransaction::new("transfer") - .with_to(target) - .with_value(value), - ) - .with_action(ProduceBlocks::new(1)) - .with_action(AssertTxIncluded::new("transfer").expect(TxStatus::Success)) - .with_action( - AssertTxLogs::new("transfer") - .expect_log_count(1) - .expect_emitter_at(0, SYSTEM_ADDRESS) - .expect_transfer_event(0, WALLET_FIRST_ADDRESS, target, value), - ) - .with_action(AssertTxTrace::new("transfer")) - .run() - .await - .expect("test_transfer_to_codeless_address failed"); -} - -/// Test: Value transfer and zero-value transfer in same block. -/// -/// Only the value transfer should emit a log; the zero-value transfer should not. -#[tokio::test] -async fn test_mixed_value_and_zero_value_in_block() { - reth_tracing::init_test_tracing(); - - let recipient = address!("0x000000000000000000000000000000000000F00D"); - let value = U256::from(1_000_000); - - ArcTestBuilder::new() - .with_setup(ArcSetup::new()) - .with_action( - SendTransaction::new("with_value") - .with_to(recipient) - .with_value(value), - ) - .with_action( - SendTransaction::new("zero_value") - .with_to(recipient) - .with_value(U256::ZERO), - ) - .with_action(ProduceBlocks::new(1)) - .with_action(AssertTxIncluded::new("with_value").expect(TxStatus::Success)) - .with_action(AssertTxIncluded::new("zero_value").expect(TxStatus::Success)) - .with_action( - AssertTxLogs::new("with_value") - .expect_log_count(1) - .expect_emitter_at(0, SYSTEM_ADDRESS) - .expect_transfer_event(0, WALLET_FIRST_ADDRESS, recipient, value), - ) - .with_action(AssertTxLogs::new("zero_value").expect_no_logs()) - .run() - .await - .expect("test_mixed_value_and_zero_value_in_block failed"); -} diff --git a/crates/execution-e2e/tests/eip7708_hardfork_transition.rs b/crates/execution-e2e/tests/eip7708_hardfork_transition.rs deleted file mode 100644 index 2fc4fc2..0000000 --- a/crates/execution-e2e/tests/eip7708_hardfork_transition.rs +++ /dev/null @@ -1,184 +0,0 @@ -// Copyright 2026 Circle Internet Group, Inc. All rights reserved. -// -// SPDX-License-Identifier: Apache-2.0 -// -// Licensed under the Apache License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. -// You may obtain a copy of the License at -// -// http://www.apache.org/licenses/LICENSE-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// See the License for the specific language governing permissions and -// limitations under the License. - -//! EIP-7708 hardfork transition e2e tests. -//! -//! Verifies that EIP-7708 Transfer logs activate correctly at the Zero5 boundary -//! and that pre-Zero5 blocks emit NativeCoinTransferred logs from -//! NATIVE_COIN_AUTHORITY_ADDRESS instead. - -mod helpers; - -use alloy_primitives::{address, U256}; -use arc_execution_config::hardforks::ArcHardfork; -use arc_execution_e2e::{ - actions::{ - AssertBlockNumber, AssertHardfork, AssertTxIncluded, AssertTxLogs, AssertTxTrace, - ProduceBlocks, SendTransaction, TxStatus, - }, - chainspec::localdev_with_hardforks, - ArcSetup, ArcTestBuilder, -}; -use helpers::constants::{NATIVE_COIN_AUTHORITY_ADDRESS, SYSTEM_ADDRESS, WALLET_FIRST_ADDRESS}; -use reth_chainspec::ForkCondition; - -/// Test #20: Pre-Zero5 value transfer emits NativeCoinTransferred from NativeCoinAuthority. -/// -/// Verifies exact event format: topic[0] = NativeCoinTransferred signature, -/// topic[1] = from, topic[2] = to, data = amount. -#[tokio::test] -async fn test_pre_zero5_emits_native_coin_transferred() { - reth_tracing::init_test_tracing(); - - let chain_spec = localdev_with_hardforks(&[ - (ArcHardfork::Zero3, ForkCondition::Block(0)), - (ArcHardfork::Zero4, ForkCondition::Block(0)), - (ArcHardfork::Zero5, ForkCondition::Block(100)), // far in the future - (ArcHardfork::Zero6, ForkCondition::Block(100)), - ]); - - let recipient = address!("0x000000000000000000000000000000000000bEEF"); - let value = U256::from(1_000_000); - - ArcTestBuilder::new() - .with_setup(ArcSetup::new().with_chain_spec(chain_spec)) - .with_action(AssertHardfork::is_not_active(ArcHardfork::Zero5)) - .with_action( - SendTransaction::new("transfer") - .with_to(recipient) - .with_value(value), - ) - .with_action(ProduceBlocks::new(1)) - .with_action(AssertTxIncluded::new("transfer").expect(TxStatus::Success)) - .with_action( - AssertTxLogs::new("transfer") - .expect_log_count(1) - .expect_emitter_at(0, NATIVE_COIN_AUTHORITY_ADDRESS) - // Verify exact NativeCoinTransferred event topics and data - .expect_native_coin_transferred_event(0, WALLET_FIRST_ADDRESS, recipient, value), - ) - .with_action(AssertTxTrace::new("transfer")) - .run() - .await - .expect("test_pre_zero5_emits_native_coin_transferred failed"); -} - -/// Test #21: Zero5 activation boundary — tx before activation uses old log format, -/// tx after activation uses EIP-7708 format. -/// -/// Pre-Zero5 tx emits NativeCoinTransferred with exact topics/data; -/// post-Zero5 tx emits ERC-20 Transfer with exact topics/data. -#[tokio::test] -async fn test_zero5_activation_boundary() { - reth_tracing::init_test_tracing(); - - // Zero5 activates at block 3 - let chain_spec = localdev_with_hardforks(&[ - (ArcHardfork::Zero3, ForkCondition::Block(0)), - (ArcHardfork::Zero4, ForkCondition::Block(0)), - (ArcHardfork::Zero5, ForkCondition::Block(3)), - (ArcHardfork::Zero6, ForkCondition::Block(100)), - ]); - - let recipient = address!("0x000000000000000000000000000000000000bEEF"); - let value = U256::from(1_000_000); - - ArcTestBuilder::new() - .with_setup(ArcSetup::new().with_chain_spec(chain_spec)) - .with_action(AssertHardfork::is_not_active(ArcHardfork::Zero5)) - // Send tx before Zero5 (block 1) - .with_action( - SendTransaction::new("pre_zero5") - .with_to(recipient) - .with_value(value), - ) - .with_action(ProduceBlocks::new(1)) - .with_action(AssertBlockNumber::new(1)) - .with_action(AssertTxIncluded::new("pre_zero5").expect(TxStatus::Success)) - .with_action( - AssertTxLogs::new("pre_zero5") - .expect_log_count(1) - .expect_emitter_at(0, NATIVE_COIN_AUTHORITY_ADDRESS) - // Exact pre-Zero5 event format - .expect_native_coin_transferred_event(0, WALLET_FIRST_ADDRESS, recipient, value), - ) - // Produce blocks 2-3 to reach Zero5 activation - .with_action(ProduceBlocks::new(2)) - .with_action(AssertBlockNumber::new(3)) - .with_action(AssertHardfork::is_active(ArcHardfork::Zero5)) - // Send tx after Zero5 (block 4) - .with_action( - SendTransaction::new("post_zero5") - .with_to(recipient) - .with_value(value), - ) - .with_action(ProduceBlocks::new(1)) - .with_action(AssertTxIncluded::new("post_zero5").expect(TxStatus::Success)) - .with_action( - AssertTxLogs::new("post_zero5") - .expect_log_count(1) - .expect_emitter_at(0, SYSTEM_ADDRESS) - .expect_transfer_event(0, WALLET_FIRST_ADDRESS, recipient, value), - ) - .with_action(AssertTxTrace::new("pre_zero5")) - .with_action(AssertTxTrace::new("post_zero5")) - .run() - .await - .expect("test_zero5_activation_boundary failed"); -} - -/// Test #22: Post-Zero5 value transfer emits Transfer from SYSTEM_ADDRESS. -#[tokio::test] -async fn test_post_zero5_emits_eip7708_transfer() { - reth_tracing::init_test_tracing(); - - let recipient = address!("0x000000000000000000000000000000000000bEEF"); - let value = U256::from(1_000_000); - - ArcTestBuilder::new() - .with_setup(ArcSetup::new()) - .with_action(AssertHardfork::is_active(ArcHardfork::Zero5)) - .with_action( - SendTransaction::new("transfer") - .with_to(recipient) - .with_value(value), - ) - .with_action(ProduceBlocks::new(1)) - .with_action(AssertTxIncluded::new("transfer").expect(TxStatus::Success)) - .with_action( - AssertTxLogs::new("transfer") - .expect_log_count(1) - .expect_emitter_at(0, SYSTEM_ADDRESS) - .expect_transfer_event(0, WALLET_FIRST_ADDRESS, recipient, value), - ) - .with_action(AssertTxTrace::new("transfer")) - .run() - .await - .expect("test_post_zero5_emits_eip7708_transfer failed"); -} - -/// Test #23: Verify Zero5 hardfork is active at genesis on default localdev. -#[tokio::test] -async fn test_zero5_active_at_genesis() { - reth_tracing::init_test_tracing(); - - ArcTestBuilder::new() - .with_setup(ArcSetup::new()) - .with_action(AssertHardfork::is_active(ArcHardfork::Zero5)) - .run() - .await - .expect("test_zero5_active_at_genesis failed"); -} diff --git a/crates/execution-e2e/tests/eip7708_log_format.rs b/crates/execution-e2e/tests/eip7708_log_format.rs deleted file mode 100644 index 5bc08f5..0000000 --- a/crates/execution-e2e/tests/eip7708_log_format.rs +++ /dev/null @@ -1,176 +0,0 @@ -// Copyright 2026 Circle Internet Group, Inc. All rights reserved. -// -// SPDX-License-Identifier: Apache-2.0 -// -// Licensed under the Apache License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. -// You may obtain a copy of the License at -// -// http://www.apache.org/licenses/LICENSE-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// See the License for the specific language governing permissions and -// limitations under the License. - -//! EIP-7708 log format compliance e2e tests. -//! -//! Verifies the byte-level ERC-20 Transfer log format: emitter address, -//! topic[0] (event signature), topic[1] (from), topic[2] (to), data (value). - -mod helpers; - -use alloy_primitives::{address, Bytes, B256, U256}; -use arc_execution_e2e::{ - actions::{ - AssertTxIncluded, AssertTxLogs, AssertTxTrace, ProduceBlocks, SendTransaction, TxStatus, - }, - ArcSetup, ArcTestBuilder, -}; -use helpers::constants::{SYSTEM_ADDRESS, TRANSFER_EVENT_SIGNATURE, WALLET_FIRST_ADDRESS}; - -/// Test #37: topic[0] matches ERC-20 Transfer(address,address,uint256) signature. -#[tokio::test] -async fn test_transfer_log_topic0_matches_erc20_signature() { - reth_tracing::init_test_tracing(); - - let recipient = address!("0x0000000000000000000000000000000000001111"); - let value = U256::from(1_000_000); - - // Build expected topics and data - let expected_topics = vec![ - TRANSFER_EVENT_SIGNATURE, - B256::left_padding_from(WALLET_FIRST_ADDRESS.as_slice()), - B256::left_padding_from(recipient.as_slice()), - ]; - let expected_data = Bytes::from(value.to_be_bytes::<32>().to_vec()); - - ArcTestBuilder::new() - .with_setup(ArcSetup::new()) - .with_action( - SendTransaction::new("transfer") - .with_to(recipient) - .with_value(value), - ) - .with_action(ProduceBlocks::new(1)) - .with_action(AssertTxIncluded::new("transfer").expect(TxStatus::Success)) - .with_action( - AssertTxLogs::new("transfer") - .expect_log_count(1) - .expect_log_at(0, SYSTEM_ADDRESS, expected_topics, expected_data), - ) - .with_action(AssertTxTrace::new("transfer")) - .run() - .await - .expect("test_transfer_log_topic0_matches_erc20_signature failed"); -} - -/// Test #38: topic[1] encodes sender address as left-padded bytes32. -#[tokio::test] -async fn test_transfer_log_topic1_encodes_sender() { - reth_tracing::init_test_tracing(); - - let recipient = address!("0x0000000000000000000000000000000000002222"); - let value = U256::from(42); - - ArcTestBuilder::new() - .with_setup(ArcSetup::new()) - .with_action( - SendTransaction::new("transfer") - .with_to(recipient) - .with_value(value), - ) - .with_action(ProduceBlocks::new(1)) - .with_action(AssertTxIncluded::new("transfer").expect(TxStatus::Success)) - .with_action( - AssertTxLogs::new("transfer") - .expect_log_count(1) - .expect_transfer_event(0, WALLET_FIRST_ADDRESS, recipient, value), - ) - .run() - .await - .expect("test_transfer_log_topic1_encodes_sender failed"); -} - -/// Test #39: topic[2] encodes recipient address as left-padded bytes32. -#[tokio::test] -async fn test_transfer_log_topic2_encodes_recipient() { - reth_tracing::init_test_tracing(); - - let recipient = address!("0x0000000000000000000000000000000000003333"); - let value = U256::from(999); - - ArcTestBuilder::new() - .with_setup(ArcSetup::new()) - .with_action( - SendTransaction::new("transfer") - .with_to(recipient) - .with_value(value), - ) - .with_action(ProduceBlocks::new(1)) - .with_action(AssertTxIncluded::new("transfer").expect(TxStatus::Success)) - .with_action( - AssertTxLogs::new("transfer") - .expect_log_count(1) - .expect_transfer_event(0, WALLET_FIRST_ADDRESS, recipient, value), - ) - .run() - .await - .expect("test_transfer_log_topic2_encodes_recipient failed"); -} - -/// Test #40: data encodes value as big-endian uint256. -#[tokio::test] -async fn test_transfer_log_data_encodes_value() { - reth_tracing::init_test_tracing(); - - let recipient = address!("0x0000000000000000000000000000000000004444"); - // Use a distinctive value to verify encoding - let value = U256::from(0xDEADBEEFu64); - - ArcTestBuilder::new() - .with_setup(ArcSetup::new()) - .with_action( - SendTransaction::new("transfer") - .with_to(recipient) - .with_value(value), - ) - .with_action(ProduceBlocks::new(1)) - .with_action(AssertTxIncluded::new("transfer").expect(TxStatus::Success)) - .with_action( - AssertTxLogs::new("transfer") - .expect_log_count(1) - .expect_transfer_event(0, WALLET_FIRST_ADDRESS, recipient, value), - ) - .run() - .await - .expect("test_transfer_log_data_encodes_value failed"); -} - -/// Test #41: emitter address is SYSTEM_ADDRESS, not the sender or NativeCoinAuthority. -#[tokio::test] -async fn test_transfer_log_emitter_is_system_address() { - reth_tracing::init_test_tracing(); - - let recipient = address!("0x0000000000000000000000000000000000005555"); - let value = U256::from(1); - - ArcTestBuilder::new() - .with_setup(ArcSetup::new()) - .with_action( - SendTransaction::new("transfer") - .with_to(recipient) - .with_value(value), - ) - .with_action(ProduceBlocks::new(1)) - .with_action(AssertTxIncluded::new("transfer").expect(TxStatus::Success)) - .with_action( - AssertTxLogs::new("transfer") - .expect_log_count(1) - .expect_emitter_at(0, SYSTEM_ADDRESS), - ) - .run() - .await - .expect("test_transfer_log_emitter_is_system_address failed"); -} diff --git a/crates/execution-e2e/tests/eip7708_native_transfer.rs b/crates/execution-e2e/tests/eip7708_native_transfer.rs deleted file mode 100644 index 57ddd17..0000000 --- a/crates/execution-e2e/tests/eip7708_native_transfer.rs +++ /dev/null @@ -1,855 +0,0 @@ -// Copyright 2026 Circle Internet Group, Inc. All rights reserved. -// -// SPDX-License-Identifier: Apache-2.0 -// -// Licensed under the Apache License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. -// You may obtain a copy of the License at -// -// http://www.apache.org/licenses/LICENSE-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// See the License for the specific language governing permissions and -// limitations under the License. - -//! EIP-7708 native transfer e2e tests. -//! -//! Tests that native value transfers emit ERC-20 Transfer logs from SYSTEM_ADDRESS -//! under the Zero5 hardfork via CALL to EOA, contract, and precompile recipients, -//! as well as CREATE, SELFDESTRUCT, and nested value transfer scenarios. - -mod helpers; - -use alloy_primitives::{address, U256}; -use arc_execution_e2e::{ - actions::{ - AssertBalance, AssertLastOpcodeGasCost, AssertNamedBalance, AssertTransferEvent, - AssertTxIncluded, AssertTxLogs, AssertTxTrace, ProduceBlocks, SendTransaction, - StoreDeployedAddress, TxStatus, - }, - ArcSetup, ArcTestBuilder, -}; -use helpers::{ - constants::{SYSTEM_ADDRESS, WALLET_FIRST_ADDRESS}, - contracts::right_pad_address, -}; - -// ===== CALL to EOA (#1-3) ===== - -/// Test #1: EOA sends nonzero USDC to another EOA — emits 1 EIP-7708 Transfer log. -#[tokio::test] -async fn test_call_eoa_with_value_emits_eip7708_log() { - reth_tracing::init_test_tracing(); - - let recipient = address!("0x000000000000000000000000000000000000bEEF"); - let value = U256::from(1_000_000); // 1 USDC (6 decimals) - - ArcTestBuilder::new() - .with_setup(ArcSetup::new()) - .with_action( - SendTransaction::new("transfer") - .with_to(recipient) - .with_value(value), - ) - .with_action(ProduceBlocks::new(1)) - .with_action( - AssertTxIncluded::new("transfer") - .expect(TxStatus::Success) - .expect_gas_used(21_000), - ) - .with_action( - AssertTxLogs::new("transfer") - .expect_log_count(1) - .expect_emitter_at(0, SYSTEM_ADDRESS) - .expect_transfer_event(0, WALLET_FIRST_ADDRESS, recipient, value), - ) - .with_action(AssertTxTrace::new("transfer")) - .run() - .await - .expect("test_call_eoa_with_value_emits_eip7708_log failed"); -} - -/// Test #2: EOA sends 0 value — no EIP-7708 log. -#[tokio::test] -async fn test_call_eoa_zero_value_no_log() { - reth_tracing::init_test_tracing(); - - let recipient = address!("0x000000000000000000000000000000000000bEEF"); - - ArcTestBuilder::new() - .with_setup(ArcSetup::new()) - .with_action( - SendTransaction::new("transfer") - .with_to(recipient) - .with_value(U256::ZERO), - ) - .with_action(ProduceBlocks::new(1)) - .with_action( - AssertTxIncluded::new("transfer") - .expect(TxStatus::Success) - .expect_gas_used(21_000), - ) - .with_action(AssertTxLogs::new("transfer").expect_no_logs()) - .with_action(AssertTxTrace::new("transfer")) - .run() - .await - .expect("test_call_eoa_zero_value_no_log failed"); -} - -/// Test #3: EOA sends value to self — no EIP-7708 log (self-transfer is suppressed). -#[tokio::test] -async fn test_call_eoa_self_transfer_no_log() { - reth_tracing::init_test_tracing(); - - let value = U256::from(1_000_000); - - ArcTestBuilder::new() - .with_setup(ArcSetup::new()) - .with_action( - SendTransaction::new("transfer") - .with_to(WALLET_FIRST_ADDRESS) - .with_value(value), - ) - .with_action(ProduceBlocks::new(1)) - .with_action( - AssertTxIncluded::new("transfer") - .expect(TxStatus::Success) - .expect_gas_used(21_000), - ) - .with_action(AssertTxLogs::new("transfer").expect_no_logs()) - .with_action(AssertTxTrace::new("transfer")) - .run() - .await - .expect("test_call_eoa_self_transfer_no_log failed"); -} - -// ===== CALL to Contract (#4-6) ===== - -/// Test #4: EOA sends value to a payable contract — emits exact EIP-7708 Transfer log. -/// -/// Deploys a payable contract via CREATE, then sends value to it. -/// Asserts exact from (sender), to (deployed contract), and value using stored addresses. -#[tokio::test] -async fn test_call_contract_with_value_emits_eip7708_log() { - reth_tracing::init_test_tracing(); - - let deploy_code = helpers::contracts::payable_contract_deploy_code(); - let transfer_value = U256::from(500_000); - - ArcTestBuilder::new() - .with_setup(ArcSetup::new()) - // Deploy payable contract - .with_action( - SendTransaction::new("deploy") - .with_create() - .with_data(deploy_code) - .with_value(U256::ZERO) - .with_gas_limit(100_000), - ) - .with_action(ProduceBlocks::new(1)) - .with_action(AssertTxIncluded::new("deploy").expect(TxStatus::Success)) - .with_action(StoreDeployedAddress::new("deploy")) - // Send value to the deployed contract - .with_action( - SendTransaction::new("value_call") - .with_to_named("deploy_address") - .with_value(transfer_value) - .with_gas_limit(100_000), - ) - .with_action(ProduceBlocks::new(1)) - .with_action(AssertTxIncluded::new("value_call").expect(TxStatus::Success)) - .with_action(AssertTxLogs::new("value_call").expect_log_count(1)) - .with_action(AssertTransferEvent::new( - "value_call", - 0, - WALLET_FIRST_ADDRESS, - AssertTransferEvent::named("deploy_address"), - transfer_value, - )) - .with_action(AssertNamedBalance::of("deploy_address").equals(transfer_value)) - .with_action(AssertTxTrace::new("value_call")) - .run() - .await - .expect("test_call_contract_with_value_emits_eip7708_log failed"); -} - -/// Test #5: EOA sends 0 value to a contract — no EIP-7708 log. -#[tokio::test] -async fn test_call_contract_zero_value_no_log() { - reth_tracing::init_test_tracing(); - - let deploy_code = helpers::contracts::payable_contract_deploy_code(); - - ArcTestBuilder::new() - .with_setup(ArcSetup::new()) - .with_action( - SendTransaction::new("deploy") - .with_create() - .with_data(deploy_code) - .with_value(U256::ZERO) - .with_gas_limit(100_000), - ) - .with_action(ProduceBlocks::new(1)) - .with_action(AssertTxIncluded::new("deploy").expect(TxStatus::Success)) - .with_action(StoreDeployedAddress::new("deploy")) - .with_action( - SendTransaction::new("zero_call") - .with_to_named("deploy_address") - .with_value(U256::ZERO) - .with_gas_limit(100_000), - ) - .with_action(ProduceBlocks::new(1)) - .with_action(AssertTxIncluded::new("zero_call").expect(TxStatus::Success)) - .with_action(AssertTxLogs::new("zero_call").expect_no_logs()) - .with_action(AssertTxTrace::new("zero_call")) - .run() - .await - .expect("test_call_contract_zero_value_no_log failed"); -} - -/// Test #6: EOA sends value to a reverting contract — tx reverts, no EIP-7708 log. -#[tokio::test] -async fn test_call_reverting_contract_with_value_no_log() { - reth_tracing::init_test_tracing(); - - let deploy_code = helpers::contracts::reverting_contract_deploy_code(); - let value = U256::from(500_000); - - ArcTestBuilder::new() - .with_setup(ArcSetup::new()) - .with_action( - SendTransaction::new("deploy") - .with_create() - .with_data(deploy_code) - .with_value(U256::ZERO) - .with_gas_limit(100_000), - ) - .with_action(ProduceBlocks::new(1)) - .with_action(AssertTxIncluded::new("deploy").expect(TxStatus::Success)) - .with_action(StoreDeployedAddress::new("deploy")) - .with_action( - SendTransaction::new("revert_call") - .with_to_named("deploy_address") - .with_value(value) - .with_gas_limit(100_000), - ) - .with_action(ProduceBlocks::new(1)) - .with_action(AssertTxIncluded::new("revert_call").expect(TxStatus::Reverted)) - .with_action(AssertTxLogs::new("revert_call").expect_no_logs()) - .with_action(AssertTxTrace::new("revert_call")) - .run() - .await - .expect("test_call_reverting_contract_with_value_no_log failed"); -} - -// ===== CALL to Precompile (#7-8) ===== - -/// Test #7: CALL to precompile with value — reverts (unauthorized), logs rolled back. -#[tokio::test] -async fn test_call_precompile_with_value() { - reth_tracing::init_test_tracing(); - - let precompile = address!("0x1800000000000000000000000000000000000000"); - let value = U256::from(1_000); - - ArcTestBuilder::new() - .with_setup(ArcSetup::new()) - .with_action( - SendTransaction::new("call_precompile") - .with_to(precompile) - .with_value(value) - .with_gas_limit(100_000), - ) - .with_action(ProduceBlocks::new(1)) - .with_action(AssertTxIncluded::new("call_precompile").expect(TxStatus::Reverted)) - .with_action(AssertTxLogs::new("call_precompile").expect_no_logs()) - .with_action(AssertTxTrace::new("call_precompile")) - .run() - .await - .expect("test_call_precompile_with_value failed"); -} - -/// Test #8: CALL to precompile with 0 value — no EIP-7708 log. -#[tokio::test] -async fn test_call_precompile_zero_value_no_log() { - reth_tracing::init_test_tracing(); - - let precompile = address!("0x1800000000000000000000000000000000000000"); - - ArcTestBuilder::new() - .with_setup(ArcSetup::new()) - .with_action( - SendTransaction::new("call_precompile") - .with_to(precompile) - .with_value(U256::ZERO) - .with_gas_limit(100_000), - ) - .with_action(ProduceBlocks::new(1)) - .with_action(AssertTxIncluded::new("call_precompile").expect(TxStatus::Reverted)) - .with_action(AssertTxLogs::new("call_precompile").expect_no_logs()) - .with_action(AssertTxTrace::new("call_precompile")) - .run() - .await - .expect("test_call_precompile_zero_value_no_log failed"); -} - -// ===== CREATE (#9-10) ===== - -/// Test #9: CREATE with nonzero value — emits exact EIP-7708 Transfer log. -/// -/// When deploying a contract with value (endowment), the value transfer from -/// the deployer to the new contract address emits an EIP-7708 Transfer log. -/// Asserts exact from (sender), to (deployed address from receipt), and value. -#[tokio::test] -async fn test_create_with_value_emits_eip7708_log() { - reth_tracing::init_test_tracing(); - - let deploy_code = helpers::contracts::payable_contract_deploy_code(); - let endowment = U256::from(1_000_000); - - ArcTestBuilder::new() - .with_setup(ArcSetup::new()) - .with_action( - SendTransaction::new("create") - .with_create() - .with_data(deploy_code) - .with_value(endowment) - .with_gas_limit(100_000), - ) - .with_action(ProduceBlocks::new(1)) - .with_action(AssertTxIncluded::new("create").expect(TxStatus::Success)) - .with_action(StoreDeployedAddress::new("create")) - .with_action(AssertTxLogs::new("create").expect_log_count(1)) - .with_action(AssertTransferEvent::new( - "create", - 0, - WALLET_FIRST_ADDRESS, - AssertTransferEvent::named("create_address"), - endowment, - )) - .with_action(AssertNamedBalance::of("create_address").equals(endowment)) - .with_action(AssertTxTrace::new("create")) - .run() - .await - .expect("test_create_with_value_emits_eip7708_log failed"); -} - -/// Test #10: CREATE with zero value — no EIP-7708 Transfer log. -#[tokio::test] -async fn test_create_zero_value_no_log() { - reth_tracing::init_test_tracing(); - - let deploy_code = helpers::contracts::payable_contract_deploy_code(); - - ArcTestBuilder::new() - .with_setup(ArcSetup::new()) - .with_action( - SendTransaction::new("create") - .with_create() - .with_data(deploy_code) - .with_value(U256::ZERO) - .with_gas_limit(100_000), - ) - .with_action(ProduceBlocks::new(1)) - .with_action(AssertTxIncluded::new("create").expect(TxStatus::Success)) - .with_action(AssertTxLogs::new("create").expect_no_logs()) - .with_action(AssertTxTrace::new("create")) - .run() - .await - .expect("test_create_zero_value_no_log failed"); -} - -/// Test: CREATE with nonzero value where constructor reverts — tx reverts, no log, no balance leak. -/// -/// Sends a CREATE tx with endowment but the constructor reverts. -/// The EIP-7708 Transfer log is rolled back with the frame. -/// The would-be contract address must not have any balance. -#[tokio::test] -async fn test_create_revert_with_endowment_no_log() { - reth_tracing::init_test_tracing(); - - let initcode = helpers::contracts::reverting_constructor_code(); - let endowment = U256::from(1_000_000); - - // Nonce-derived address is necessary here because the CREATE reverts — - // StoreDeployedAddress cannot recover the address from a failed CREATE. - // We compute it to verify no balance leaked to the would-be address. - let would_be_addr = WALLET_FIRST_ADDRESS.create(0); - - ArcTestBuilder::new() - .with_setup(ArcSetup::new()) - .with_action( - SendTransaction::new("create") - .with_create() - .with_data(initcode) - .with_value(endowment) - .with_gas_limit(100_000), - ) - .with_action(ProduceBlocks::new(1)) - // Constructor reverts → tx reverts - .with_action(AssertTxIncluded::new("create").expect(TxStatus::Reverted)) - // Transfer log rolled back - .with_action(AssertTxLogs::new("create").expect_no_logs()) - // No balance leakage to the would-be contract address - .with_action(AssertBalance::new(would_be_addr, U256::ZERO)) - .with_action(AssertTxTrace::new("create")) - .run() - .await - .expect("test_create_revert_with_endowment_no_log failed"); -} - -/// Test: successful CREATE2 with value leaves the created address warm. -/// -/// The probe contract funds a CREATE2 child with 1 wei and immediately executes -/// BALANCE on the precomputed created address passed in calldata. A successful -/// CREATE2 warms the child through revm's create handler, so that BALANCE costs -/// 100 gas. -#[tokio::test] -async fn test_create2_with_value_balance_probe_is_warm_after_successful_create() { - reth_tracing::init_test_tracing(); - - let deploy_code = helpers::contracts::create2_with_balance_probe(); - let endowment = U256::from(1); - - ArcTestBuilder::new() - .with_setup(ArcSetup::new()) - .with_action( - SendTransaction::new("deploy_probe") - .with_create() - .with_data(deploy_code) - .with_value(endowment) - .with_gas_limit(200_000), - ) - .with_action(ProduceBlocks::new(1)) - .with_action(AssertTxIncluded::new("deploy_probe").expect(TxStatus::Success)) - .with_action(StoreDeployedAddress::new("deploy_probe")) - .with_action( - SendTransaction::new("run_probe") - .with_to_named("deploy_probe_address") - .with_value(U256::ZERO) - .with_data_fn(|env| { - let probe = *env.get_address("deploy_probe_address").ok_or_else(|| { - eyre::eyre!("Named address 'deploy_probe_address' not found") - })?; - Ok(helpers::contracts::create2_balance_probe_calldata(probe)) - }) - .with_gas_limit(200_000), - ) - .with_action(ProduceBlocks::new(1)) - .with_action(AssertTxIncluded::new("run_probe").expect(TxStatus::Success)) - .with_action(AssertLastOpcodeGasCost::new("run_probe", "BALANCE", 100)) - .run() - .await - .expect("test_create2_with_value_balance_probe_is_warm_after_successful_create failed"); -} - -/// Test: out-of-funds CREATE2 with value does not warm the would-be created -/// address via Arc's selfdestruct-target probe. -/// -/// The probe has zero balance, so its CREATE2(value=1) fails before revm's -/// create handler warms the derived child address. The probe then checks BALANCE -/// for that precomputed address in the same frame. Under Zero7, Arc's -/// selfdestruct-target check is checkpointed, so BALANCE remains cold and costs -/// 2600 gas. -#[tokio::test] -async fn test_create2_out_of_funds_keeps_balance_probe_cold() { - reth_tracing::init_test_tracing(); - - let deploy_code = helpers::contracts::create2_with_balance_probe(); - - ArcTestBuilder::new() - .with_setup(ArcSetup::new()) - .with_action( - SendTransaction::new("deploy_probe") - .with_create() - .with_data(deploy_code) - .with_value(U256::ZERO) - .with_gas_limit(200_000), - ) - .with_action(ProduceBlocks::new(1)) - .with_action(AssertTxIncluded::new("deploy_probe").expect(TxStatus::Success)) - .with_action(StoreDeployedAddress::new("deploy_probe")) - .with_action( - SendTransaction::new("run_probe") - .with_to_named("deploy_probe_address") - .with_value(U256::ZERO) - .with_data_fn(|env| { - let probe = *env.get_address("deploy_probe_address").ok_or_else(|| { - eyre::eyre!("Named address 'deploy_probe_address' not found") - })?; - Ok(helpers::contracts::create2_balance_probe_calldata(probe)) - }) - .with_gas_limit(200_000), - ) - .with_action(ProduceBlocks::new(1)) - .with_action(AssertTxIncluded::new("run_probe").expect(TxStatus::Success)) - .with_action(AssertLastOpcodeGasCost::new("run_probe", "BALANCE", 2600)) - .run() - .await - .expect("test_create2_out_of_funds_keeps_balance_probe_cold failed"); -} - -// ===== SELFDESTRUCT (#11-18) ===== - -/// Test #11: SELFDESTRUCT sends balance to beneficiary — emits exact EIP-7708 Transfer log. -/// -/// Asserts: from = contract address (stored), to = beneficiary, value = endowment. -#[tokio::test] -async fn test_selfdestruct_with_balance_emits_log() { - reth_tracing::init_test_tracing(); - - let deploy_code = helpers::contracts::selfdestruct_contract_deploy_code(); - let endowment = U256::from(1_000_000); - let beneficiary = address!("0x000000000000000000000000000000000000BEEF"); - - let calldata = helpers::contracts::right_pad_address(beneficiary); - - ArcTestBuilder::new() - .with_setup(ArcSetup::new()) - // Deploy selfdestruct contract with endowment - .with_action( - SendTransaction::new("deploy") - .with_create() - .with_data(deploy_code) - .with_value(endowment) - .with_gas_limit(200_000), - ) - .with_action(ProduceBlocks::new(1)) - .with_action(AssertTxIncluded::new("deploy").expect(TxStatus::Success)) - .with_action(StoreDeployedAddress::new("deploy")) - // Trigger selfdestruct — sends balance to beneficiary - .with_action( - SendTransaction::new("selfdestruct") - .with_to_named("deploy_address") - .with_value(U256::ZERO) - .with_data(calldata) - .with_gas_limit(200_000), - ) - .with_action(ProduceBlocks::new(1)) - .with_action(AssertTxIncluded::new("selfdestruct").expect(TxStatus::Success)) - // Exact Transfer: from=stored contract addr, to=beneficiary, value=endowment - .with_action(AssertTxLogs::new("selfdestruct").expect_log_count(1)) - .with_action(AssertTransferEvent::new( - "selfdestruct", - 0, - AssertTransferEvent::named("deploy_address"), - beneficiary, - endowment, - )) - .with_action(AssertNamedBalance::of("deploy_address").equals(U256::ZERO)) - .with_action(AssertTxTrace::new("selfdestruct")) - .run() - .await - .expect("test_selfdestruct_with_balance_emits_log failed"); -} - -/// Test #12: SELFDESTRUCT with zero balance — no EIP-7708 Transfer log. -#[tokio::test] -async fn test_selfdestruct_zero_balance_no_log() { - reth_tracing::init_test_tracing(); - - let deploy_code = helpers::contracts::selfdestruct_contract_deploy_code(); - let beneficiary = address!("0x000000000000000000000000000000000000BEEF"); - - let calldata = helpers::contracts::right_pad_address(beneficiary); - - ArcTestBuilder::new() - .with_setup(ArcSetup::new()) - // Deploy with zero balance - .with_action( - SendTransaction::new("deploy") - .with_create() - .with_data(deploy_code) - .with_value(U256::ZERO) - .with_gas_limit(200_000), - ) - .with_action(ProduceBlocks::new(1)) - .with_action(AssertTxIncluded::new("deploy").expect(TxStatus::Success)) - .with_action(StoreDeployedAddress::new("deploy")) - // Trigger selfdestruct — zero balance to transfer - .with_action( - SendTransaction::new("selfdestruct") - .with_to_named("deploy_address") - .with_value(U256::ZERO) - .with_data(calldata) - .with_gas_limit(200_000), - ) - .with_action(ProduceBlocks::new(1)) - .with_action(AssertTxIncluded::new("selfdestruct").expect(TxStatus::Success)) - .with_action(AssertTxLogs::new("selfdestruct").expect_no_logs()) - .with_action(AssertTxTrace::new("selfdestruct")) - .run() - .await - .expect("test_selfdestruct_zero_balance_no_log failed"); -} - -/// Test #13: SELFDESTRUCT to self — beneficiary == contract address. -/// -/// The implementation explicitly rejects SELFDESTRUCT where source == target -/// with nonzero balance under Zero5 (see `check_selfdestruct_accounts` in opcode.rs). -/// The SELFDESTRUCT opcode halts with Revert, causing the tx to revert. -/// No log is emitted and the contract retains its balance. -#[tokio::test] -async fn test_selfdestruct_to_self_reverts() { - reth_tracing::init_test_tracing(); - - let deploy_code = helpers::contracts::selfdestruct_contract_deploy_code(); - let endowment = U256::from(1_000_000); - - ArcTestBuilder::new() - .with_setup(ArcSetup::new()) - .with_action( - SendTransaction::new("deploy") - .with_create() - .with_data(deploy_code) - .with_value(endowment) - .with_gas_limit(200_000), - ) - .with_action(ProduceBlocks::new(1)) - .with_action(AssertTxIncluded::new("deploy").expect(TxStatus::Success)) - .with_action(StoreDeployedAddress::new("deploy")) - // Trigger selfdestruct to self — implementation rejects this - .with_action( - SendTransaction::new("selfdestruct") - .with_to_named("deploy_address") - .with_value(U256::ZERO) - .with_data_fn(|env| { - let addr = env - .get_address("deploy_address") - .ok_or_else(|| eyre::eyre!("Named address 'deploy_address' not found"))?; - Ok(right_pad_address(*addr)) - }) - .with_gas_limit(200_000), - ) - .with_action(ProduceBlocks::new(1)) - // SELFDESTRUCT to self reverts the tx under Zero5 - .with_action(AssertTxIncluded::new("selfdestruct").expect(TxStatus::Reverted)) - .with_action(AssertTxLogs::new("selfdestruct").expect_no_logs()) - // Contract retains its balance — no state change - .with_action(AssertNamedBalance::of("deploy_address").equals(endowment)) - .with_action(AssertTxTrace::new("selfdestruct")) - .run() - .await - .expect("test_selfdestruct_to_self_reverts failed"); -} - -// ===== Nested/Forwarded Transfer (#19) ===== - -/// Test #19: Contract forwards received value to another address — both transfers emit exact logs. -/// -/// Deploys a forwarder contract, then sends value to it with a target address. -/// The forwarder CALLs the target with the received value (CALLVALUE). -/// Expected: 2 EIP-7708 Transfer logs in order: -/// log[0]: Transfer(sender, forwarder, value) -/// log[1]: Transfer(forwarder, final_recipient, value) -#[tokio::test] -async fn test_nested_value_transfer_emits_multiple_logs() { - reth_tracing::init_test_tracing(); - - let deploy_code = helpers::contracts::forwarder_contract_deploy_code(); - let final_recipient = address!("0x000000000000000000000000000000000000CAFE"); - let value = U256::from(500_000); - - let calldata = helpers::contracts::right_pad_address(final_recipient); - - ArcTestBuilder::new() - .with_setup(ArcSetup::new()) - // Deploy forwarder - .with_action( - SendTransaction::new("deploy") - .with_create() - .with_data(deploy_code) - .with_value(U256::ZERO) - .with_gas_limit(200_000), - ) - .with_action(ProduceBlocks::new(1)) - .with_action(AssertTxIncluded::new("deploy").expect(TxStatus::Success)) - .with_action(StoreDeployedAddress::new("deploy")) - // Send value to forwarder with target in calldata - .with_action( - SendTransaction::new("forward") - .with_to_named("deploy_address") - .with_value(value) - .with_data(calldata) - .with_gas_limit(200_000), - ) - .with_action(ProduceBlocks::new(1)) - .with_action(AssertTxIncluded::new("forward").expect(TxStatus::Success)) - // Exact 2 logs: sender→forwarder, forwarder→final_recipient - .with_action(AssertTxLogs::new("forward").expect_log_count(2)) - .with_action(AssertTransferEvent::new( - "forward", - 0, - WALLET_FIRST_ADDRESS, - AssertTransferEvent::named("deploy_address"), - value, - )) - .with_action(AssertTransferEvent::new( - "forward", - 1, - AssertTransferEvent::named("deploy_address"), - final_recipient, - value, - )) - .with_action(AssertNamedBalance::of("deploy_address").equals(U256::ZERO)) - .with_action(AssertTxTrace::new("forward")) - .run() - .await - .expect("test_nested_value_transfer_emits_multiple_logs failed"); -} - -// ===== Additional coverage ===== - -/// Test: large value transfer emits correct log. -#[tokio::test] -async fn test_large_value_transfer() { - reth_tracing::init_test_tracing(); - - let recipient = address!("0x0000000000000000000000000000000000001234"); - let value = U256::from(10_000_000); - - ArcTestBuilder::new() - .with_setup(ArcSetup::new()) - .with_action( - SendTransaction::new("transfer") - .with_to(recipient) - .with_value(value), - ) - .with_action(ProduceBlocks::new(1)) - .with_action(AssertTxIncluded::new("transfer").expect(TxStatus::Success)) - .with_action( - AssertTxLogs::new("transfer") - .expect_log_count(1) - .expect_emitter_at(0, SYSTEM_ADDRESS) - .expect_transfer_event(0, WALLET_FIRST_ADDRESS, recipient, value), - ) - .with_action(AssertTxTrace::new("transfer")) - .run() - .await - .expect("test_large_value_transfer failed"); -} - -/// Test: minimum value (1 wei) transfer emits correct log. -#[tokio::test] -async fn test_min_value_transfer() { - reth_tracing::init_test_tracing(); - - let recipient = address!("0x0000000000000000000000000000000000005678"); - let value = U256::from(1); - - ArcTestBuilder::new() - .with_setup(ArcSetup::new()) - .with_action( - SendTransaction::new("transfer") - .with_to(recipient) - .with_value(value), - ) - .with_action(ProduceBlocks::new(1)) - .with_action(AssertTxIncluded::new("transfer").expect(TxStatus::Success)) - .with_action( - AssertTxLogs::new("transfer") - .expect_log_count(1) - .expect_emitter_at(0, SYSTEM_ADDRESS) - .expect_transfer_event(0, WALLET_FIRST_ADDRESS, recipient, value), - ) - .with_action(AssertTxTrace::new("transfer")) - .run() - .await - .expect("test_min_value_transfer failed"); -} - -/// Test: multiple value transfers in one block each emit their own log. -#[tokio::test] -async fn test_multiple_transfers_in_block() { - reth_tracing::init_test_tracing(); - - let recipient_a = address!("0x000000000000000000000000000000000000aaaa"); - let recipient_b = address!("0x000000000000000000000000000000000000bbbb"); - let value_a = U256::from(100_000); - let value_b = U256::from(200_000); - - ArcTestBuilder::new() - .with_setup(ArcSetup::new()) - .with_action( - SendTransaction::new("tx_a") - .with_to(recipient_a) - .with_value(value_a), - ) - .with_action( - SendTransaction::new("tx_b") - .with_to(recipient_b) - .with_value(value_b), - ) - .with_action(ProduceBlocks::new(1)) - .with_action( - AssertTxIncluded::new("tx_a") - .expect(TxStatus::Success) - .expect_gas_used(21_000), - ) - .with_action( - AssertTxIncluded::new("tx_b") - .expect(TxStatus::Success) - .expect_gas_used(21_000), - ) - .with_action( - AssertTxLogs::new("tx_a") - .expect_log_count(1) - .expect_emitter_at(0, SYSTEM_ADDRESS) - .expect_transfer_event(0, WALLET_FIRST_ADDRESS, recipient_a, value_a), - ) - .with_action( - AssertTxLogs::new("tx_b") - .expect_log_count(1) - .expect_emitter_at(0, SYSTEM_ADDRESS) - .expect_transfer_event(0, WALLET_FIRST_ADDRESS, recipient_b, value_b), - ) - .with_action(AssertTxTrace::new("tx_a")) - .with_action(AssertTxTrace::new("tx_b")) - .run() - .await - .expect("test_multiple_transfers_in_block failed"); -} - -/// Test: reverted value transfer to reverting contract does not leak balance. -/// -/// Sends value to a reverting contract. The tx reverts, so no value is transferred. -/// Asserts the target contract's balance remains zero after the revert. -#[tokio::test] -async fn test_reverted_value_transfer_balance_unchanged() { - reth_tracing::init_test_tracing(); - - let deploy_code = helpers::contracts::reverting_contract_deploy_code(); - let value = U256::from(500_000); - - ArcTestBuilder::new() - .with_setup(ArcSetup::new()) - .with_action( - SendTransaction::new("deploy") - .with_create() - .with_data(deploy_code) - .with_value(U256::ZERO) - .with_gas_limit(100_000), - ) - .with_action(ProduceBlocks::new(1)) - .with_action(AssertTxIncluded::new("deploy").expect(TxStatus::Success)) - .with_action(StoreDeployedAddress::new("deploy")) - // Confirm contract starts with zero balance - .with_action(AssertNamedBalance::of("deploy_address").equals(U256::ZERO)) - // Attempt value transfer — will revert - .with_action( - SendTransaction::new("revert_call") - .with_to_named("deploy_address") - .with_value(value) - .with_gas_limit(100_000), - ) - .with_action(ProduceBlocks::new(1)) - .with_action(AssertTxIncluded::new("revert_call").expect(TxStatus::Reverted)) - .with_action(AssertTxLogs::new("revert_call").expect_no_logs()) - // Contract balance still zero — no value leaked through revert - .with_action(AssertNamedBalance::of("deploy_address").equals(U256::ZERO)) - .run() - .await - .expect("test_reverted_value_transfer_balance_unchanged failed"); -} diff --git a/crates/execution-e2e/tests/eip7708_payload_validation.rs b/crates/execution-e2e/tests/eip7708_payload_validation.rs deleted file mode 100644 index 3e7014a..0000000 --- a/crates/execution-e2e/tests/eip7708_payload_validation.rs +++ /dev/null @@ -1,110 +0,0 @@ -// Copyright 2026 Circle Internet Group, Inc. All rights reserved. -// -// SPDX-License-Identifier: Apache-2.0 -// -// Licensed under the Apache License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. -// You may obtain a copy of the License at -// -// http://www.apache.org/licenses/LICENSE-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// See the License for the specific language governing permissions and -// limitations under the License. - -//! EIP-7708 payload validation e2e tests. -//! -//! Verifies that Engine API accepts payloads containing EIP-7708 Transfer logs -//! and rejects payloads with corrupted state roots. - -use alloy_primitives::U256; -use alloy_rpc_types_engine::PayloadStatusEnum; -use arc_execution_e2e::{ - actions::{ - assert_valid_or_syncing, build_payload_for_next_block, set_payload_override_and_rehash, - submit_payload, AssertTxIncluded, ProduceBlocks, SendTransaction, TxStatus, - }, - Action, ArcEnvironment, ArcSetup, -}; -use eyre::Result; - -/// Test #42: Payload with EIP-7708 Transfer log is accepted as VALID. -#[tokio::test] -async fn test_payload_with_eip7708_log_accepted() -> Result<()> { - reth_tracing::init_test_tracing(); - - let mut env = ArcEnvironment::new(); - ArcSetup::new().apply(&mut env).await?; - - // Produce block 1 with a value transfer (triggers EIP-7708 log) - let mut send = SendTransaction::new("transfer") - .with_to(alloy_primitives::address!( - "0x000000000000000000000000000000000000bEEF" - )) - .with_value(U256::from(1_000_000)); - send.execute(&mut env).await?; - - let mut produce = ProduceBlocks::new(1); - produce.execute(&mut env).await?; - - // Verify the tx was included successfully - let mut assert_included = AssertTxIncluded::new("transfer").expect(TxStatus::Success); - assert_included.execute(&mut env).await?; - - // Now build the next payload and submit via Engine API - let (payload, execution_requests, parent_beacon_block_root) = - build_payload_for_next_block(&env).await?; - - let status = - submit_payload(&env, payload, execution_requests, parent_beacon_block_root).await?; - - assert_valid_or_syncing(&status, "EIP-7708 payload")?; - - Ok(()) -} - -/// Test #43: Payload with corrupted stateRoot after EIP-7708 tx is rejected as INVALID. -#[tokio::test] -async fn test_payload_with_corrupted_state_root_rejected() -> Result<()> { - reth_tracing::init_test_tracing(); - - let mut env = ArcEnvironment::new(); - ArcSetup::new().apply(&mut env).await?; - - // Produce block 1 with a value transfer - let mut send = SendTransaction::new("transfer") - .with_to(alloy_primitives::address!( - "0x000000000000000000000000000000000000bEEF" - )) - .with_value(U256::from(1_000_000)); - send.execute(&mut env).await?; - - let mut produce = ProduceBlocks::new(1); - produce.execute(&mut env).await?; - - // Build next payload - let (mut payload, execution_requests, parent_beacon_block_root) = - build_payload_for_next_block(&env).await?; - - // Corrupt the state root - let mut payload_override = payload.payload_inner.payload_inner.clone(); - payload_override.state_root = alloy_primitives::B256::repeat_byte(0xDE); - set_payload_override_and_rehash( - &mut payload, - &execution_requests, - parent_beacon_block_root, - payload_override, - )?; - - let status = - submit_payload(&env, payload, execution_requests, parent_beacon_block_root).await?; - - assert!( - matches!(status, PayloadStatusEnum::Invalid { .. }), - "Expected INVALID status for corrupted state root, got {status:?}" - ); - - Ok(()) -} diff --git a/crates/execution-e2e/tests/eip7708_precompile.rs b/crates/execution-e2e/tests/eip7708_precompile.rs deleted file mode 100644 index 264de98..0000000 --- a/crates/execution-e2e/tests/eip7708_precompile.rs +++ /dev/null @@ -1,471 +0,0 @@ -// Copyright 2026 Circle Internet Group, Inc. All rights reserved. -// -// SPDX-License-Identifier: Apache-2.0 -// -// Licensed under the Apache License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. -// You may obtain a copy of the License at -// -// http://www.apache.org/licenses/LICENSE-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// See the License for the specific language governing permissions and -// limitations under the License. - -//! EIP-7708 precompile interaction e2e tests. -//! -//! Tests cover both unauthorized (revert) and authorized (success) paths for -//! NativeCoinAuthority precompile operations (mint, burn, transfer). -//! -//! Under Zero5, the NativeCoinAuthority precompile only accepts calls from -//! `NATIVE_FIAT_TOKEN_ADDRESS` (0x3600..0000). Direct EOA calls are rejected. -//! Authorized calls go through the NativeFiatToken contract, which delegates -//! to the precompile. The operator wallet (index 7 in localdev genesis) has -//! the minter role. - -mod helpers; - -use alloy_primitives::{address, Address, Bytes, U256}; -use alloy_sol_types::{sol, SolCall}; -use arc_execution_e2e::{ - actions::{ - AssertBalance, AssertTxIncluded, AssertTxLogs, AssertTxTrace, ProduceBlocks, - SendTransaction, TxStatus, - }, - ArcSetup, ArcTestBuilder, -}; -use helpers::constants::NATIVE_COIN_AUTHORITY_ADDRESS; - -/// NativeFiatToken proxy contract address — the only caller authorized to invoke -/// NativeCoinAuthority under Zero5. -const NATIVE_FIAT_TOKEN_ADDRESS: Address = address!("0x3600000000000000000000000000000000000000"); - -/// NativeCoinControl precompile address. -const NATIVE_COIN_CONTROL_ADDRESS: Address = address!("0x1800000000000000000000000000000000000001"); - -/// Operator wallet index in localdev genesis (has minter role on NativeFiatToken). -const WALLET_OPERATOR_INDEX: usize = 7; - -/// NativeFiatToken uses 6 decimals; the precompile operates in 18-decimal native units. -/// NativeFiatToken converts by multiplying by 10^12 before calling the precompile. -/// So 1 USDC (1_000_000 in 6-dec) becomes 10^18 in the precompile's event and balance. -const USDC_TO_NATIVE: U256 = U256::from_limbs([1_000_000_000_000u64, 0, 0, 0]); // 10^12 - -sol! { - /// NativeFiatToken contract ABI (authorized path — operator calls these). - interface INativeFiatToken { - function mint(address to, uint256 amount) public; - function burn(uint256 amount) public; - function transfer(address to, uint256 amount) public returns (bool); - } - - /// NativeCoinAuthority precompile ABI (unauthorized path — direct calls). - interface INativeCoinAuthority { - function mint(address to, uint256 amount) external returns (bool); - function burn(address from, uint256 amount) external returns (bool); - function transfer(address from, address to, uint256 amount) external returns (bool); - function totalSupply() external view returns (uint256 supply); - } -} - -// ===== Unauthorized paths (#30-32): Direct EOA calls to precompile ===== - -/// Test #30: Direct unauthorized call to NativeCoinAuthority mint — reverts, no EIP-7708 log. -#[tokio::test] -async fn test_unauthorized_mint_call_reverts_no_log() { - reth_tracing::init_test_tracing(); - - let calldata = INativeCoinAuthority::mintCall { - to: address!("0x000000000000000000000000000000000000bEEF"), - amount: U256::from(1_000_000), - } - .abi_encode(); - - ArcTestBuilder::new() - .with_setup(ArcSetup::new()) - .with_action( - SendTransaction::new("mint_call") - .with_to(NATIVE_COIN_AUTHORITY_ADDRESS) - .with_value(U256::ZERO) - .with_data(Bytes::from(calldata)) - .with_gas_limit(100_000), - ) - .with_action(ProduceBlocks::new(1)) - .with_action(AssertTxIncluded::new("mint_call").expect(TxStatus::Reverted)) - .with_action(AssertTxLogs::new("mint_call").expect_no_logs()) - .with_action(AssertTxTrace::new("mint_call")) - .run() - .await - .expect("test_unauthorized_mint_call_reverts_no_log failed"); -} - -/// Test #31: Direct unauthorized call to NativeCoinAuthority burn — reverts, no EIP-7708 log. -#[tokio::test] -async fn test_unauthorized_burn_call_reverts_no_log() { - reth_tracing::init_test_tracing(); - - let calldata = INativeCoinAuthority::burnCall { - from: address!("0x000000000000000000000000000000000000bEEF"), - amount: U256::from(1_000), - } - .abi_encode(); - - ArcTestBuilder::new() - .with_setup(ArcSetup::new()) - .with_action( - SendTransaction::new("burn_call") - .with_to(NATIVE_COIN_AUTHORITY_ADDRESS) - .with_value(U256::ZERO) - .with_data(Bytes::from(calldata)) - .with_gas_limit(100_000), - ) - .with_action(ProduceBlocks::new(1)) - .with_action(AssertTxIncluded::new("burn_call").expect(TxStatus::Reverted)) - .with_action(AssertTxLogs::new("burn_call").expect_no_logs()) - .with_action(AssertTxTrace::new("burn_call")) - .run() - .await - .expect("test_unauthorized_burn_call_reverts_no_log failed"); -} - -/// Test #32: Direct unauthorized call to NativeCoinAuthority transfer — reverts, no EIP-7708 log. -#[tokio::test] -async fn test_unauthorized_transfer_call_reverts_no_log() { - reth_tracing::init_test_tracing(); - - let calldata = INativeCoinAuthority::transferCall { - from: address!("0x000000000000000000000000000000000000bEEF"), - to: address!("0x000000000000000000000000000000000000CAFE"), - amount: U256::from(1_000), - } - .abi_encode(); - - ArcTestBuilder::new() - .with_setup(ArcSetup::new()) - .with_action( - SendTransaction::new("transfer_call") - .with_to(NATIVE_COIN_AUTHORITY_ADDRESS) - .with_value(U256::ZERO) - .with_data(Bytes::from(calldata)) - .with_gas_limit(100_000), - ) - .with_action(ProduceBlocks::new(1)) - .with_action(AssertTxIncluded::new("transfer_call").expect(TxStatus::Reverted)) - .with_action(AssertTxLogs::new("transfer_call").expect_no_logs()) - .with_action(AssertTxTrace::new("transfer_call")) - .run() - .await - .expect("test_unauthorized_transfer_call_reverts_no_log failed"); -} - -// ===== Value to precompile addresses (#33-34) ===== - -/// Test #33: Value transfer to NativeCoinAuthority — reverts, no log. -#[tokio::test] -async fn test_value_to_native_coin_authority() { - reth_tracing::init_test_tracing(); - - ArcTestBuilder::new() - .with_setup(ArcSetup::new()) - .with_action( - SendTransaction::new("value_call") - .with_to(NATIVE_COIN_AUTHORITY_ADDRESS) - .with_value(U256::from(1_000)) - .with_gas_limit(100_000), - ) - .with_action(ProduceBlocks::new(1)) - .with_action(AssertTxIncluded::new("value_call").expect(TxStatus::Reverted)) - .with_action(AssertTxLogs::new("value_call").expect_no_logs()) - .with_action(AssertTxTrace::new("value_call")) - .run() - .await - .expect("test_value_to_native_coin_authority failed"); -} - -/// Test #34: Value transfer to NativeCoinControl — reverts, no log. -#[tokio::test] -async fn test_value_to_native_coin_control() { - reth_tracing::init_test_tracing(); - - ArcTestBuilder::new() - .with_setup(ArcSetup::new()) - .with_action( - SendTransaction::new("value_call") - .with_to(NATIVE_COIN_CONTROL_ADDRESS) - .with_value(U256::from(1_000)) - .with_gas_limit(100_000), - ) - .with_action(ProduceBlocks::new(1)) - .with_action(AssertTxIncluded::new("value_call").expect(TxStatus::Reverted)) - .with_action(AssertTxLogs::new("value_call").expect_no_logs()) - .with_action(AssertTxTrace::new("value_call")) - .run() - .await - .expect("test_value_to_native_coin_control failed"); -} - -/// Test #35: Zero-value call to NativeFiatToken — no EIP-7708 log. -#[tokio::test] -async fn test_zero_value_call_to_native_fiat_token() { - reth_tracing::init_test_tracing(); - - ArcTestBuilder::new() - .with_setup(ArcSetup::new()) - .with_action( - SendTransaction::new("call") - .with_to(NATIVE_FIAT_TOKEN_ADDRESS) - .with_value(U256::ZERO) - .with_gas_limit(100_000), - ) - .with_action(ProduceBlocks::new(1)) - .with_action(AssertTxIncluded::new("call").expect(TxStatus::Reverted)) - .with_action(AssertTxLogs::new("call").expect_no_logs()) - .with_action(AssertTxTrace::new("call")) - .run() - .await - .expect("test_zero_value_call_to_native_fiat_token failed"); -} - -/// Test #36: Direct totalSupply read — succeeds without log. -#[tokio::test] -async fn test_total_supply_read_no_log() { - reth_tracing::init_test_tracing(); - - let calldata = Bytes::from(INativeCoinAuthority::totalSupplyCall {}.abi_encode()); - - ArcTestBuilder::new() - .with_setup(ArcSetup::new()) - .with_action( - SendTransaction::new("total_supply") - .with_to(NATIVE_COIN_AUTHORITY_ADDRESS) - .with_value(U256::ZERO) - .with_data(calldata) - .with_gas_limit(100_000), - ) - .with_action(ProduceBlocks::new(1)) - .with_action(AssertTxIncluded::new("total_supply").expect(TxStatus::Success)) - .with_action(AssertTxLogs::new("total_supply").expect_no_logs()) - .with_action(AssertTxTrace::new("total_supply")) - .run() - .await - .expect("test_total_supply_read_no_log failed"); -} - -// ===== Authorized paths: NativeFiatToken mint/burn ===== - -/// Test: Authorized mint via NativeFiatToken — emits EIP-7708 Transfer log + Mint event. -/// -/// The operator (wallet index 7) calls NativeFiatToken.mint(to, amount). -/// NativeFiatToken delegates to NativeCoinAuthority precompile. -/// Under Zero5, the precompile emits an EIP-7708 Transfer log from SYSTEM_ADDRESS -/// for the minted amount, plus the Solidity-level Mint and Transfer events from -/// the NativeFiatToken contract. -#[tokio::test] -async fn test_authorized_mint_via_native_fiat_token() { - reth_tracing::init_test_tracing(); - - let mint_recipient = address!("0x000000000000000000000000000000000000CAFE"); - // NativeFiatToken uses 6 decimals. Mint 1 USDC = 1_000_000 (6 decimals). - // The precompile converts this to 18-decimal native units internally. - let mint_amount_usdc = U256::from(1_000_000u64); - - let calldata = INativeFiatToken::mintCall { - to: mint_recipient, - amount: mint_amount_usdc, - } - .abi_encode(); - - ArcTestBuilder::new() - .with_setup(ArcSetup::new()) - .with_action( - SendTransaction::new("mint") - .with_to(NATIVE_FIAT_TOKEN_ADDRESS) - .with_value(U256::ZERO) - .with_data(Bytes::from(calldata)) - .with_gas_limit(500_000) - .with_wallet_index(WALLET_OPERATOR_INDEX), - ) - .with_action(ProduceBlocks::new(1)) - .with_action(AssertTxIncluded::new("mint").expect(TxStatus::Success)) - // The precompile emits Transfer(0x0, to, amount) from SYSTEM_ADDRESS under Zero5. - // NativeFiatToken converts 6-dec USDC to 18-dec native before calling the precompile, - // so the event amount is in 18-decimal native units. - .with_action( - AssertTxLogs::new("mint").expect_transfer_event( - 0, - Address::ZERO, - mint_recipient, - mint_amount_usdc - .checked_mul(USDC_TO_NATIVE) - .expect("usdc to native overflow"), - ), - ) - .with_action(AssertTxTrace::new("mint")) - // Verify recipient balance in 18-decimal native units - .with_action(AssertBalance::new( - mint_recipient, - mint_amount_usdc - .checked_mul(USDC_TO_NATIVE) - .expect("usdc to native overflow"), - )) - .run() - .await - .expect("test_authorized_mint_via_native_fiat_token failed"); -} - -/// Test: Authorized burn via NativeFiatToken — emits EIP-7708 Transfer log + Burn event. -/// -/// Burns tokens from the operator's own balance. Requires the operator to have -/// balance, so we first mint to the operator, then burn. -#[tokio::test] -async fn test_authorized_burn_via_native_fiat_token() { - reth_tracing::init_test_tracing(); - - // Operator address (wallet index 7) - let operator = { - let wallet = reth_e2e_test_utils::wallet::Wallet::new(10).with_chain_id(1337); - wallet.wallet_gen()[WALLET_OPERATOR_INDEX].address() - }; - - let mint_amount = U256::from(2_000_000u64); // 2 USDC - let burn_amount = U256::from(1_000_000u64); // 1 USDC - - // Mint to operator first - let mint_calldata = INativeFiatToken::mintCall { - to: operator, - amount: mint_amount, - } - .abi_encode(); - - let burn_calldata = INativeFiatToken::burnCall { - amount: burn_amount, - } - .abi_encode(); - - ArcTestBuilder::new() - .with_setup(ArcSetup::new()) - // Step 1: Mint to operator - .with_action( - SendTransaction::new("mint") - .with_to(NATIVE_FIAT_TOKEN_ADDRESS) - .with_value(U256::ZERO) - .with_data(Bytes::from(mint_calldata)) - .with_gas_limit(500_000) - .with_wallet_index(WALLET_OPERATOR_INDEX), - ) - .with_action(ProduceBlocks::new(1)) - .with_action(AssertTxIncluded::new("mint").expect(TxStatus::Success)) - // Step 2: Burn from operator - .with_action( - SendTransaction::new("burn") - .with_to(NATIVE_FIAT_TOKEN_ADDRESS) - .with_value(U256::ZERO) - .with_data(Bytes::from(burn_calldata)) - .with_gas_limit(500_000) - .with_wallet_index(WALLET_OPERATOR_INDEX), - ) - .with_action(ProduceBlocks::new(1)) - .with_action(AssertTxIncluded::new("burn").expect(TxStatus::Success)) - // Burn emits Transfer(operator, 0x0, amount) from SYSTEM_ADDRESS under Zero5. - // Amount is in 18-decimal native units (NativeFiatToken converts before calling precompile). - .with_action( - AssertTxLogs::new("burn").expect_transfer_event( - 0, - operator, - Address::ZERO, - burn_amount - .checked_mul(USDC_TO_NATIVE) - .expect("usdc to native overflow"), - ), - ) - .with_action(AssertTxTrace::new("burn")) - // Note: operator balance assertion omitted because the operator is funded in genesis - // and pays gas in USDC, making the exact post-burn balance variable. - // The Transfer log assertion above verifies the burn semantics. - .run() - .await - .expect("test_authorized_burn_via_native_fiat_token failed"); -} - -/// Test: Authorized transfer via NativeFiatToken — emits exact EIP-7708 Transfer log. -/// -/// Mints to the operator, then the operator calls NativeFiatToken.transfer(to, amount). -/// NativeFiatToken delegates to NativeCoinAuthority.transfer(from, to, amount). -/// Under Zero5, the precompile emits Transfer(from, to, amount) from SYSTEM_ADDRESS. -/// Verifies exact log fields and balance side effects. -#[tokio::test] -async fn test_authorized_transfer_via_native_fiat_token() { - reth_tracing::init_test_tracing(); - - let operator = { - let wallet = reth_e2e_test_utils::wallet::Wallet::new(10).with_chain_id(1337); - wallet.wallet_gen()[WALLET_OPERATOR_INDEX].address() - }; - - let transfer_recipient = address!("0x000000000000000000000000000000000000D00D"); - let mint_amount = U256::from(2_000_000u64); // 2 USDC - let transfer_amount = U256::from(1_000_000u64); // 1 USDC - - let mint_calldata = INativeFiatToken::mintCall { - to: operator, - amount: mint_amount, - } - .abi_encode(); - - let transfer_calldata = INativeFiatToken::transferCall { - to: transfer_recipient, - amount: transfer_amount, - } - .abi_encode(); - - ArcTestBuilder::new() - .with_setup(ArcSetup::new()) - // Mint to operator first - .with_action( - SendTransaction::new("mint") - .with_to(NATIVE_FIAT_TOKEN_ADDRESS) - .with_value(U256::ZERO) - .with_data(Bytes::from(mint_calldata)) - .with_gas_limit(500_000) - .with_wallet_index(WALLET_OPERATOR_INDEX), - ) - .with_action(ProduceBlocks::new(1)) - .with_action(AssertTxIncluded::new("mint").expect(TxStatus::Success)) - // Transfer from operator to recipient - .with_action( - SendTransaction::new("transfer") - .with_to(NATIVE_FIAT_TOKEN_ADDRESS) - .with_value(U256::ZERO) - .with_data(Bytes::from(transfer_calldata)) - .with_gas_limit(500_000) - .with_wallet_index(WALLET_OPERATOR_INDEX), - ) - .with_action(ProduceBlocks::new(1)) - .with_action(AssertTxIncluded::new("transfer").expect(TxStatus::Success)) - // Transfer emits Transfer(operator, recipient, amount) from SYSTEM_ADDRESS. - // Amount is in 18-decimal native units. - .with_action( - AssertTxLogs::new("transfer").expect_transfer_event( - 0, - operator, - transfer_recipient, - transfer_amount - .checked_mul(USDC_TO_NATIVE) - .expect("usdc to native overflow"), - ), - ) - .with_action(AssertTxTrace::new("transfer")) - // Operator balance omitted (genesis-funded + gas costs make exact value variable). - // Recipient starts at zero and doesn't pay gas, so exact balance is deterministic. - .with_action(AssertBalance::new( - transfer_recipient, - transfer_amount - .checked_mul(USDC_TO_NATIVE) - .expect("usdc to native overflow"), - )) - .run() - .await - .expect("test_authorized_transfer_via_native_fiat_token failed"); -} diff --git a/crates/execution-e2e/tests/eip7708_zero_address.rs b/crates/execution-e2e/tests/eip7708_zero_address.rs deleted file mode 100644 index 54fb913..0000000 --- a/crates/execution-e2e/tests/eip7708_zero_address.rs +++ /dev/null @@ -1,109 +0,0 @@ -// Copyright 2026 Circle Internet Group, Inc. All rights reserved. -// -// SPDX-License-Identifier: Apache-2.0 -// -// Licensed under the Apache License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. -// You may obtain a copy of the License at -// -// http://www.apache.org/licenses/LICENSE-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// See the License for the specific language governing permissions and -// limitations under the License. - -//! EIP-7708 zero address e2e tests. -//! -//! Arc custom behavior: value transfers to Address::ZERO are rejected under Zero5. - -mod helpers; - -use alloy_primitives::{Address, U256}; -use arc_execution_config::hardforks::ArcHardfork; -use arc_execution_e2e::{ - actions::{ - AssertTxIncluded, AssertTxLogs, AssertTxTrace, ProduceBlocks, SendTransaction, TxStatus, - }, - chainspec::localdev_with_hardforks, - ArcSetup, ArcTestBuilder, -}; -use reth_chainspec::ForkCondition; - -/// Test #24: Send value to Address::ZERO under Zero5 — tx reverts. -#[tokio::test] -async fn test_zero_address_value_transfer_reverts() { - reth_tracing::init_test_tracing(); - - let value = U256::from(1_000_000); - - ArcTestBuilder::new() - .with_setup(ArcSetup::new()) - .with_action( - SendTransaction::new("zero_addr") - .with_to(Address::ZERO) - .with_value(value) - .with_gas_limit(100_000), - ) - .with_action(ProduceBlocks::new(1)) - .with_action(AssertTxIncluded::new("zero_addr").expect(TxStatus::Reverted)) - .with_action(AssertTxLogs::new("zero_addr").expect_no_logs()) - .with_action(AssertTxTrace::new("zero_addr")) - .run() - .await - .expect("test_zero_address_value_transfer_reverts failed"); -} - -/// Test #25: Send zero value to Address::ZERO — should succeed (no transfer, no log). -#[tokio::test] -async fn test_zero_address_zero_value_succeeds() { - reth_tracing::init_test_tracing(); - - ArcTestBuilder::new() - .with_setup(ArcSetup::new()) - .with_action( - SendTransaction::new("zero_addr") - .with_to(Address::ZERO) - .with_value(U256::ZERO) - .with_gas_limit(100_000), - ) - .with_action(ProduceBlocks::new(1)) - .with_action(AssertTxIncluded::new("zero_addr").expect(TxStatus::Success)) - .with_action(AssertTxLogs::new("zero_addr").expect_no_logs()) - .with_action(AssertTxTrace::new("zero_addr")) - .run() - .await - .expect("test_zero_address_zero_value_succeeds failed"); -} - -/// Test #26: Pre-Zero5 — value transfer to Address::ZERO is not rejected. -#[tokio::test] -async fn test_pre_zero5_zero_address_allowed() { - reth_tracing::init_test_tracing(); - - let chain_spec = localdev_with_hardforks(&[ - (ArcHardfork::Zero3, ForkCondition::Block(0)), - (ArcHardfork::Zero4, ForkCondition::Block(0)), - (ArcHardfork::Zero5, ForkCondition::Block(100)), - (ArcHardfork::Zero6, ForkCondition::Block(100)), - ]); - - let value = U256::from(1_000_000); - - ArcTestBuilder::new() - .with_setup(ArcSetup::new().with_chain_spec(chain_spec)) - .with_action( - SendTransaction::new("zero_addr") - .with_to(Address::ZERO) - .with_value(value) - .with_gas_limit(100_000), - ) - .with_action(ProduceBlocks::new(1)) - // Pre-Zero5: zero-address value transfer should succeed - .with_action(AssertTxIncluded::new("zero_addr").expect(TxStatus::Success)) - .with_action(AssertTxTrace::new("zero_addr")) - .run() - .await - .expect("test_pre_zero5_zero_address_allowed failed"); -} diff --git a/crates/execution-e2e/tests/gas_limit_validation.rs b/crates/execution-e2e/tests/gas_limit_validation.rs deleted file mode 100644 index cd67479..0000000 --- a/crates/execution-e2e/tests/gas_limit_validation.rs +++ /dev/null @@ -1,180 +0,0 @@ -// Copyright 2026 Circle Internet Group, Inc. All rights reserved. -// -// SPDX-License-Identifier: Apache-2.0 -// -// Licensed under the Apache License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. -// You may obtain a copy of the License at -// -// http://www.apache.org/licenses/LICENSE-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// See the License for the specific language governing permissions and -// limitations under the License. - -//! E2E tests for ADR-0003: Dynamic Block Gas Limit Configuration Validation. -//! -//! These tests exercise the gas limit validation pipeline end-to-end: -//! stateless bounds checking (consensus.rs) and stateful ProtocolConfig -//! conformance (executor.rs apply_pre_execution_changes). - -use alloy_rpc_types_engine::PayloadStatusEnum; -use arc_execution_e2e::{ - actions::{build_payload_for_next_block, set_payload_override_and_rehash, submit_payload}, - chainspec::{ - localdev_with_block_gas_limit, localdev_with_protocol_config_reverts, - BlockGasLimitProvider, LOCAL_DEV, - }, - ArcEnvironment, ArcSetup, -}; -use eyre::Result; - -/// Helper: build a block with the given gas_limit, then submit via Engine API. -async fn submit_with_gas_limit(setup: ArcSetup, gas_limit: u64) -> Result { - let mut env = ArcEnvironment::new(); - setup.apply(&mut env).await?; - - let (mut payload, execution_requests, parent_beacon_block_root) = - build_payload_for_next_block(&env).await?; - - let mut payload_override = payload.payload_inner.payload_inner.clone(); - payload_override.gas_limit = gas_limit; - set_payload_override_and_rehash( - &mut payload, - &execution_requests, - parent_beacon_block_root, - payload_override, - )?; - - let status = submit_payload(&env, payload, execution_requests, parent_beacon_block_root) - .await - .expect("submit_payload RPC call should succeed"); - - Ok(status) -} - -fn assert_valid(status: &PayloadStatusEnum) { - assert!( - matches!(status, PayloadStatusEnum::Valid), - "Expected VALID, got {:?}", - status - ); -} - -fn assert_invalid_gas_limit(status: &PayloadStatusEnum) { - assert!( - matches!( - status, - PayloadStatusEnum::Invalid { validation_error } - if validation_error.contains("block gas limit") - ), - "Expected INVALID with gas limit validation error, got {:?}", - status - ); -} - -/// Block built with the correct gas limit from ProtocolConfig is accepted. -#[tokio::test] -async fn test_correct_gas_limit_accepted() -> Result<()> { - reth_tracing::init_test_tracing(); - let config = LOCAL_DEV.block_gas_limit_config(0); - let status = submit_with_gas_limit(ArcSetup::new(), config.default()).await?; - assert_valid(&status); - Ok(()) -} - -/// Block with gas limit off-by-one from ProtocolConfig is rejected. -#[tokio::test] -async fn test_gas_limit_off_by_one_rejected() -> Result<()> { - reth_tracing::init_test_tracing(); - let config = LOCAL_DEV.block_gas_limit_config(0); - let status = submit_with_gas_limit(ArcSetup::new(), config.default() + 1).await?; - assert_invalid_gas_limit(&status); - Ok(()) -} - -/// When ProtocolConfig reverts, expected_gas_limit falls back to the chainspec -/// default. A block using that default is accepted. -#[tokio::test] -async fn test_protocol_config_reverts_default_gas_limit_accepted() -> Result<()> { - reth_tracing::init_test_tracing(); - let spec = localdev_with_protocol_config_reverts(); - let config = spec.block_gas_limit_config(0); - let status = - submit_with_gas_limit(ArcSetup::new().with_chain_spec(spec), config.default()).await?; - assert_valid(&status); - Ok(()) -} - -/// When ProtocolConfig reverts, a block whose gas limit differs from the -/// chainspec default is rejected. -#[tokio::test] -async fn test_protocol_config_reverts_wrong_gas_limit_rejected() -> Result<()> { - reth_tracing::init_test_tracing(); - let spec = localdev_with_protocol_config_reverts(); - let config = spec.block_gas_limit_config(0); - let status = - submit_with_gas_limit(ArcSetup::new().with_chain_spec(spec), config.default() + 1).await?; - assert_invalid_gas_limit(&status); - Ok(()) -} - -// --------------------------------------------------------------------------- -// ProtocolConfig returns a blockGasLimit below the chainspec minimum -// --------------------------------------------------------------------------- - -/// ProtocolConfig returns a value below the minimum. The value is out of bounds, -/// so expected_gas_limit falls back to the default. A block using the default -/// is accepted. -#[tokio::test] -async fn test_protocol_config_below_min_default_gas_limit_accepted() -> Result<()> { - reth_tracing::init_test_tracing(); - let config = LOCAL_DEV.block_gas_limit_config(0); - let spec = localdev_with_block_gas_limit(config.min() - 1); - let status = - submit_with_gas_limit(ArcSetup::new().with_chain_spec(spec), config.default()).await?; - assert_valid(&status); - Ok(()) -} - -/// ProtocolConfig returns a value below the minimum. A block whose gas limit is -/// off-by-one from the default is rejected. -#[tokio::test] -async fn test_protocol_config_below_min_wrong_gas_limit_rejected() -> Result<()> { - reth_tracing::init_test_tracing(); - let config = LOCAL_DEV.block_gas_limit_config(0); - let spec = localdev_with_block_gas_limit(config.min() - 1); - let status = - submit_with_gas_limit(ArcSetup::new().with_chain_spec(spec), config.default() + 1).await?; - assert_invalid_gas_limit(&status); - Ok(()) -} - -/// ProtocolConfig returns a value above the maximum. The value is out of -/// bounds, so expected_gas_limit falls back to the default. A block using the -/// default is accepted. -#[tokio::test] -async fn test_protocol_config_above_max_default_gas_limit_accepted() -> Result<()> { - reth_tracing::init_test_tracing(); - let config = LOCAL_DEV.block_gas_limit_config(0); - let spec = localdev_with_block_gas_limit(config.max() + 1); - let status = - submit_with_gas_limit(ArcSetup::new().with_chain_spec(spec), config.default()).await?; - assert_valid(&status); - Ok(()) -} - -/// ProtocolConfig returns a value above the maximum. A block whose gas limit -/// is off-by-one from the default is rejected. -#[tokio::test] -async fn test_protocol_config_above_max_wrong_gas_limit_rejected() -> Result<()> { - reth_tracing::init_test_tracing(); - let config = LOCAL_DEV.block_gas_limit_config(0); - let spec = localdev_with_block_gas_limit(config.max() + 1); - let status = - submit_with_gas_limit(ArcSetup::new().with_chain_spec(spec), config.default() - 1).await?; - assert_invalid_gas_limit(&status); - Ok(()) -} diff --git a/crates/execution-e2e/tests/hardfork_transition.rs b/crates/execution-e2e/tests/hardfork_transition.rs deleted file mode 100644 index 576dbd4..0000000 --- a/crates/execution-e2e/tests/hardfork_transition.rs +++ /dev/null @@ -1,110 +0,0 @@ -// Copyright 2026 Circle Internet Group, Inc. All rights reserved. -// -// SPDX-License-Identifier: Apache-2.0 -// -// Licensed under the Apache License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. -// You may obtain a copy of the License at -// -// http://www.apache.org/licenses/LICENSE-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// See the License for the specific language governing permissions and -// limitations under the License. - -//! Hardfork transition e2e tests for Arc Chain. -//! -//! These tests verify that block production works correctly across -//! hardfork boundaries for Zero4, Zero5, Zero6, and Zero7 hardforks. - -use arc_execution_config::hardforks::ArcHardfork; -use arc_execution_e2e::{ - actions::{AssertBlockNumber, AssertEthereumHardfork, AssertHardfork, ProduceBlocks}, - chainspec::localdev_with_hardforks, - ArcSetup, ArcTestBuilder, -}; -use eyre::Result; -use reth_chainspec::{EthereumHardfork, ForkCondition}; - -#[tokio::test] -async fn test_hardfork_active_at_genesis() -> Result<()> { - reth_tracing::init_test_tracing(); - - ArcTestBuilder::new() - .with_setup(ArcSetup::new()) - .with_action(AssertHardfork::is_active(ArcHardfork::Zero3)) - .with_action(AssertHardfork::is_active(ArcHardfork::Zero4)) - .with_action(AssertHardfork::is_active(ArcHardfork::Zero5)) - .with_action(AssertHardfork::is_active(ArcHardfork::Zero6)) - .with_action(AssertHardfork::is_active(ArcHardfork::Zero7)) - .run() - .await -} - -/// Test multiple hardfork transitions in sequence. -#[tokio::test] -async fn test_sequential_hardfork_transitions() -> Result<()> { - reth_tracing::init_test_tracing(); - - let chain_spec = localdev_with_hardforks(&[ - (ArcHardfork::Zero3, ForkCondition::Block(2)), - (ArcHardfork::Zero4, ForkCondition::Block(4)), - (ArcHardfork::Zero5, ForkCondition::Block(6)), - (ArcHardfork::Zero6, ForkCondition::Block(8)), - (ArcHardfork::Zero7, ForkCondition::Block(10)), - ]); - - ArcTestBuilder::new() - .with_setup(ArcSetup::new().with_chain_spec(chain_spec)) - // At genesis (block 0) - .with_action(AssertHardfork::is_not_active(ArcHardfork::Zero3)) - .with_action(AssertHardfork::is_not_active(ArcHardfork::Zero4)) - .with_action(AssertHardfork::is_not_active(ArcHardfork::Zero5)) - .with_action(AssertHardfork::is_not_active(ArcHardfork::Zero6)) - .with_action(AssertHardfork::is_not_active(ArcHardfork::Zero7)) - // Produce block 1-2 - Zero3 activates - .with_action(ProduceBlocks::new(2)) - .with_action(AssertBlockNumber::new(2)) - .with_action(AssertHardfork::is_active(ArcHardfork::Zero3)) - // Produce block 3-4 - Zero4 activates - .with_action(ProduceBlocks::new(2)) - .with_action(AssertBlockNumber::new(4)) - .with_action(AssertHardfork::is_active(ArcHardfork::Zero4)) - // Produce block 5-6 - Zero5 activates - .with_action(ProduceBlocks::new(2)) - .with_action(AssertBlockNumber::new(6)) - .with_action(AssertHardfork::is_active(ArcHardfork::Zero5)) - // Produce block 7-8 - Zero6 activates - .with_action(ProduceBlocks::new(2)) - .with_action(AssertBlockNumber::new(8)) - .with_action(AssertHardfork::is_active(ArcHardfork::Zero6)) - // Produce block 9-10 - Zero7 activates - .with_action(ProduceBlocks::new(2)) - .with_action(AssertBlockNumber::new(10)) - .with_action(AssertHardfork::is_active(ArcHardfork::Zero7)) - .run() - .await -} - -/// Test that Osaka (Fusaka) hardfork is active on localdev and blocks produce correctly. -/// -/// Osaka is a timestamp-based Ethereum hardfork that enables EIP-7212 (P256 precompile), -/// EIP-7934 (RLP block size limit), and other Fusaka EIPs. -#[tokio::test] -async fn test_osaka_active_on_localdev() -> Result<()> { - reth_tracing::init_test_tracing(); - - ArcTestBuilder::new() - .with_setup(ArcSetup::new()) - // Verify Osaka is active at genesis on localdev - .with_action(AssertEthereumHardfork::is_active(EthereumHardfork::Osaka)) - // Produce multiple blocks to confirm block production works with Osaka rules - .with_action(ProduceBlocks::new(3)) - .with_action(AssertBlockNumber::new(3)) - // Osaka should still be active after producing blocks - .with_action(AssertEthereumHardfork::is_active(EthereumHardfork::Osaka)) - .run() - .await -} diff --git a/crates/execution-e2e/tests/helpers/constants.rs b/crates/execution-e2e/tests/helpers/constants.rs deleted file mode 100644 index c44d602..0000000 --- a/crates/execution-e2e/tests/helpers/constants.rs +++ /dev/null @@ -1,37 +0,0 @@ -// Copyright 2026 Circle Internet Group, Inc. All rights reserved. -// -// SPDX-License-Identifier: Apache-2.0 -// -// Licensed under the Apache License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. -// You may obtain a copy of the License at -// -// http://www.apache.org/licenses/LICENSE-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// See the License for the specific language governing permissions and -// limitations under the License. - -//! Shared constants for EIP-7708 e2e tests. -//! -//! Each test binary only uses a subset of these; unused items are expected. -#![allow(dead_code)] - -use alloy_primitives::{address, Address}; - -// Re-export event signatures from the library to avoid duplication. -// Not all test files use both signatures, but they are shared constants. -#[allow(unused_imports)] -pub use arc_execution_e2e::actions::{NATIVE_COIN_TRANSFERRED_SIGNATURE, TRANSFER_EVENT_SIGNATURE}; - -/// EIP-7708 system address — emitter of Transfer logs under Zero5. -pub const SYSTEM_ADDRESS: Address = address!("0xfffffffffffffffffffffffffffffffffffffffe"); - -/// NativeCoinAuthority precompile — emitter of NativeCoinTransferred logs before Zero5. -pub const NATIVE_COIN_AUTHORITY_ADDRESS: Address = - address!("0x1800000000000000000000000000000000000000"); - -/// First account from test mnemonic (0xf39Fd...), funded in localdev genesis. -pub const WALLET_FIRST_ADDRESS: Address = address!("0xf39Fd6e51aad88F6F4ce6aB8827279cffFb92266"); diff --git a/crates/execution-e2e/tests/helpers/contracts.rs b/crates/execution-e2e/tests/helpers/contracts.rs deleted file mode 100644 index 19b8fd7..0000000 --- a/crates/execution-e2e/tests/helpers/contracts.rs +++ /dev/null @@ -1,282 +0,0 @@ -// Copyright 2026 Circle Internet Group, Inc. All rights reserved. -// -// SPDX-License-Identifier: Apache-2.0 -// -// Licensed under the Apache License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. -// You may obtain a copy of the License at -// -// http://www.apache.org/licenses/LICENSE-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// See the License for the specific language governing permissions and -// limitations under the License. - -//! Inline bytecode for minimal test contracts. -//! -//! Each test binary only uses a subset of these; unused items are expected. -#![allow(dead_code)] - -use alloy_primitives::{keccak256, Address, Bytes}; -use revm_bytecode::opcode::*; - -/// Contract with `receive() external payable {}` — accepts value, does nothing. -/// -/// ```eas -/// stop ;; [] — halt execution, accept any value sent -/// ``` -pub fn payable_contract_deploy_code() -> Bytes { - let runtime = [STOP]; - deploy_code(&runtime) -} - -/// Contract that always reverts with zero-length revert data. -/// -/// ```eas -/// push1 0x00 ;; [0] — revert data length -/// push1 0x00 ;; [0, 0] — revert data offset -/// revert ;; [] — revert(0, 0) -/// ``` -pub fn reverting_contract_deploy_code() -> Bytes { - #[rustfmt::skip] - let runtime = [ - PUSH1, 0x00, // revert(0, - PUSH1, 0x00, // 0) - REVERT, - ]; - deploy_code(&runtime) -} - -/// Constructor that reverts during deployment (no contract is created). -/// This is NOT wrapped in deploy_code — it IS the initcode that reverts. -/// -/// ```eas -/// ;; Initcode — reverts immediately, so CREATE/CREATE2 returns address(0). -/// push1 0x00 ;; [0] — revert data length -/// push1 0x00 ;; [0, 0] — revert data offset -/// revert ;; [] — revert(0, 0) -/// ``` -pub fn reverting_constructor_code() -> Bytes { - #[rustfmt::skip] - let initcode = vec![ - PUSH1, 0x00, // revert(0, - PUSH1, 0x00, // 0) - REVERT, - ]; - Bytes::from(initcode) -} - -/// Contract that calls SELFDESTRUCT with beneficiary from calldata. -/// -/// ```eas -/// push1 0x00 ;; [0] — calldata offset -/// calldataload ;; [cd[0..32]] — load 32-byte word -/// push1 0x60 ;; [96, cd] — shift amount (256 - 160) -/// shr ;; [addr] — isolate 20-byte address -/// selfdestruct ;; [] — send balance to addr and destroy -/// ``` -pub fn selfdestruct_contract_deploy_code() -> Bytes { - #[rustfmt::skip] - let runtime = [ - PUSH1, 0x00, CALLDATALOAD, // calldataload(0) → 32-byte word - PUSH1, 0x60, SHR, // shr(96) → target address - SELFDESTRUCT, // selfdestruct(addr) - ]; - deploy_code(&runtime) -} - -/// Contract that forwards received value to a target address via CALL. -/// -/// Pseudocode: `call(gas(), calldata[0:20], callvalue(), 0, 0, 0, 0)` -/// -/// ```eas -/// ;; Extract target address from calldata[0:20]. -/// push1 0x00 ;; [0] — calldata offset -/// calldataload ;; [cd[0..32]] — load 32-byte word -/// push1 0x60 ;; [96, cd[0..32]] — shift amount -/// shr ;; [addr] — isolate 20-byte address -/// -/// ;; Build CALL args (reverse order — EVM is stack-based). -/// push1 0x00 ;; [0, addr] — retLength -/// push1 0x00 ;; [0, 0, addr] — retOffset -/// push1 0x00 ;; [0, 0, 0, addr] — argsLength -/// push1 0x00 ;; [0, 0, 0, 0, addr] — argsOffset -/// callvalue ;; [value, 0, 0, 0, 0, addr] — msg.value -/// dup6 ;; [addr, value, 0, 0, 0, 0, addr] — copy target -/// gas ;; [gas, addr, value, 0, 0, 0, 0, addr] — remaining gas -/// call ;; [success, addr] — call(gas, addr, value, 0, 0, 0, 0) -/// stop ;; [] — halt -/// ``` -pub fn forwarder_contract_deploy_code() -> Bytes { - #[rustfmt::skip] - let runtime = [ - PUSH1, 0x00, CALLDATALOAD, // calldataload(0) → 32-byte word - PUSH1, 0x60, SHR, // shr(96) → target address - - PUSH1, 0x00, // call(gas(), - PUSH1, 0x00, // addr, - PUSH1, 0x00, // callvalue(), - PUSH1, 0x00, // 0, 0, 0, 0) - CALLVALUE, // - DUP6, // ↑ addr from stack position 6 - GAS, // - CALL, // → success - STOP, - ]; - deploy_code(&runtime) -} - -/// Like [`forwarder_contract_deploy_code`] but always succeeds — inner CALL -/// result is discarded with POP so the outer frame never reverts. -/// -/// Pseudocode: `pop(call(gas(), calldata[0:20], callvalue(), 0, 0, 0, 0))` -/// -/// ```eas -/// ;; Extract target address from calldata[0:20]. -/// push1 0x00 ;; [0] — calldata offset -/// calldataload ;; [cd[0..32]] — load 32-byte word -/// push1 0x60 ;; [96, cd[0..32]] — shift amount -/// shr ;; [addr] — isolate 20-byte address -/// -/// ;; Build CALL args (reverse order — EVM is stack-based). -/// push1 0x00 ;; [0, addr] — retLength -/// push1 0x00 ;; [0, 0, addr] — retOffset -/// push1 0x00 ;; [0, 0, 0, addr] — argsLength -/// push1 0x00 ;; [0, 0, 0, 0, addr] — argsOffset -/// callvalue ;; [value, 0, 0, 0, 0, addr] — msg.value -/// dup6 ;; [addr, value, 0, 0, 0, 0, addr] — copy target -/// gas ;; [gas, addr, value, 0, 0, 0, 0, addr] — remaining gas -/// call ;; [success, addr] — call(gas, addr, value, 0, 0, 0, 0) -/// pop ;; [addr] — discard success flag -/// stop ;; [] — always succeed -/// ``` -pub fn call_target_with_value_contract_deploy_code() -> Bytes { - #[rustfmt::skip] - let runtime = [ - PUSH1, 0x00, CALLDATALOAD, // calldataload(0) → 32-byte word - PUSH1, 0x60, SHR, // shr(96) → target address - - PUSH1, 0x00, // call(gas(), - PUSH1, 0x00, // addr, - PUSH1, 0x00, // callvalue(), - PUSH1, 0x00, // 0, 0, 0, 0) - CALLVALUE, // - DUP6, // ↑ addr from stack position 6 - GAS, // - CALL, // → success - POP, // discard success — always succeed - STOP, - ]; - deploy_code(&runtime) -} - -/// Contract that attempts CREATE2 with value=1, then checks the balance of an -/// expected CREATE2 address passed in calldata. -/// - stores the CREATE2 return value in slot 0. -/// - stores the balance + swap1 + pop gas cost in slot 1. -/// -/// The calldata address is expected as a right-padded 32-byte word. -/// -/// ```eas -/// push32 0x00..00 ;; [salt] — salt = 0 -/// push1 0x01 ;; [1, salt] — value = 1 -/// push1 0x00 ;; [0, 1, salt] — offset = 0 (mem[0] is 0x00 by default) -/// push1 0x01 ;; [1, 0, 1, salt] — initcode length = 1 -/// create2 ;; [addr] — create2(value=1, offset=0, len=1, salt=0) -/// push1 0x00 ;; [0, addr] -/// sstore ;; [] — sstore(0, addr) -/// push1 0x00 ;; [0] — calldata offset -/// calldataload ;; [cd[0..32]] — load expected address word -/// push1 0x60 ;; [96, cd[0..32]] — shift amount -/// shr ;; [expected_addr] — isolate expected address -/// gas ;; [gas before, expected_addr] -/// swap1 ;; [expected_addr, gas before] -/// balance ;; [balance, gas before] -/// pop ;; [gas before] -/// gas ;; [gas after, gas before] -/// swap1 ;; [gas before, gas after] -/// sub ;; [gas cost] -/// push1 0x01 ;; [0, gas cost] -/// sstore ;; [] - sstore(1, gas cost) -/// -/// ``` -pub fn create2_with_balance_probe() -> Bytes { - #[rustfmt::skip] - let runtime = [ - PUSH32, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - PUSH1, 0x01, - PUSH1, 0x00, - PUSH1, 0x01, - CREATE2, // create2(value=1, offset=0, len=1, salt=0) - PUSH1, 0x00, - SSTORE, // sstore(0, CREATE2 return value) - PUSH1, 0x00, - CALLDATALOAD, - PUSH1, 0x60, - SHR, - GAS, - SWAP1, - BALANCE, - POP, - GAS, - SWAP1, - SUB, - PUSH1, 0x01, - SSTORE, // sstore(1, gas cost) - ]; - deploy_code(&runtime) -} - -/// Creates calldata for the create2 balance probe contract. -pub fn create2_balance_probe_calldata(creator: Address) -> Bytes { - right_pad_address(creator.create2([0u8; 32], keccak256([0u8]))) -} - -/// Right-pads an address to 32 bytes for use as calldata. -/// -/// Contracts that read a target address via `CALLDATALOAD(0) + SHR(96)` expect -/// the address in the top 20 bytes of the 32-byte word (right-padded with zeros). -pub fn right_pad_address(addr: alloy_primitives::Address) -> Bytes { - let mut buf = [0u8; 32]; - buf[..20].copy_from_slice(addr.as_slice()); - Bytes::from(buf.to_vec()) -} - -/// Helper: wraps runtime bytecode in a minimal constructor that deploys it. -/// -/// ```eas -/// ;; Constructor (11 bytes) — copies runtime to memory and returns it. -/// push1 LL ;; [len] — runtime bytecode length -/// dup1 ;; [len, len] — duplicate for RETURN size -/// push1 0x0b ;; [11, len, len] — code offset (constructor is 11 bytes) -/// push1 0x00 ;; [0, 11, len, len] — memory destination offset -/// codecopy ;; [len] — mem[0..len] = code[11..11+len] -/// push1 0x00 ;; [0, len] — memory offset for RETURN -/// return ;; [] — return(0, len) → deployed runtime -/// ``` -fn deploy_code(runtime: &[u8]) -> Bytes { - let len = runtime.len(); - assert!(len < 256, "runtime bytecode too large for PUSH1"); - let constructor_len: u8 = 11; - let len_u8 = u8::try_from(len).expect("runtime len checked < 256"); - let mut code = Vec::with_capacity( - usize::from(constructor_len) - .checked_add(len) - .expect("total len overflow"), - ); - #[rustfmt::skip] - code.extend_from_slice(&[ - PUSH1, len_u8, // codecopy(0, - DUP1, // - PUSH1, constructor_len, // 11, - PUSH1, 0x00, // len) - CODECOPY, // - PUSH1, 0x00, // return(0, len) - RETURN, // - ]); - code.extend_from_slice(runtime); - Bytes::from(code) -} diff --git a/crates/execution-e2e/tests/helpers/mod.rs b/crates/execution-e2e/tests/helpers/mod.rs deleted file mode 100644 index e7fce07..0000000 --- a/crates/execution-e2e/tests/helpers/mod.rs +++ /dev/null @@ -1,18 +0,0 @@ -// Copyright 2026 Circle Internet Group, Inc. All rights reserved. -// -// SPDX-License-Identifier: Apache-2.0 -// -// Licensed under the Apache License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. -// You may obtain a copy of the License at -// -// http://www.apache.org/licenses/LICENSE-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// See the License for the specific language governing permissions and -// limitations under the License. - -pub mod constants; -pub mod contracts; diff --git a/crates/execution-e2e/tests/invalid_tx_list.rs b/crates/execution-e2e/tests/invalid_tx_list.rs deleted file mode 100644 index 4e87432..0000000 --- a/crates/execution-e2e/tests/invalid_tx_list.rs +++ /dev/null @@ -1,246 +0,0 @@ -// Copyright 2026 Circle Internet Group, Inc. All rights reserved. -// -// SPDX-License-Identifier: Apache-2.0 -// -// Licensed under the Apache License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. -// You may obtain a copy of the License at -// -// http://www.apache.org/licenses/LICENSE-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// See the License for the specific language governing permissions and -// limitations under the License. - -//! E2E tests for the invalid_tx_list functionality. -//! -//! The invalid_tx_list is an LRU cache that stores transaction hashes of transactions that -//! caused payload builder failures. This enables fast rejection during validation pre-check -//! to avoid repeatedly attempting to build blocks with problematic transactions. -//! -//! Key behavior: -//! - Unprocessable transactions (wrapped as `UnprocessableTransactionError`) are added to the cache -//! - When the payload builder panics, all pending transactions are added to the cache -//! - Cached transactions are rejected during validation pre-check with InvalidTxError -//! - LRU eviction removes oldest entries when capacity is exceeded -//! -//! Test coverage: -//! - Basic functionality: cache miss allows validation -//! - Disabled invalid_tx_list falls through to full validation -//! - Payload builder panic populates cache and resubmission is rejected - -use arc_execution_e2e::{ - actions::{AssertTxIncluded, ProduceBlocks, SendTransaction, TxStatus}, - ArcSetup, ArcTestBuilder, -}; -use arc_execution_txpool::InvalidTxListConfig; -use eyre::Result; -use rstest::rstest; - -/// Verifies that transactions not in the invalid_tx_list go through full validation -/// and are included in blocks across different configurations: -/// - Enabled with small/large capacity -/// - Disabled (falls through to full validation) -/// - Multiple independent transactions in a single block -#[rstest] -#[case::enabled(true, 1000, 1)] -#[case::disabled(false, 0, 1)] -#[case::large_capacity(true, 100_000, 1)] -#[case::multiple_txs(true, 1000, 3)] -#[tokio::test] -async fn test_normal_tx_processing( - #[case] enabled: bool, - #[case] capacity: u32, - #[case] num_txs: usize, -) -> Result<()> { - reth_tracing::init_test_tracing(); - - let mut builder = ArcTestBuilder::new().with_setup( - ArcSetup::new().with_invalid_tx_list_config(InvalidTxListConfig { enabled, capacity }), - ); - - let tx_names: Vec = (1..=num_txs).map(|i| format!("tx{i}")).collect(); - for name in &tx_names { - builder = builder.with_action(SendTransaction::new(name)); - } - - builder = builder.with_action(ProduceBlocks::new(1)); - - for name in &tx_names { - builder = builder.with_action(AssertTxIncluded::new(name).expect(TxStatus::Success)); - } - - builder.run().await -} - -/// Payload Builder Panic Populates Invalid TX List and Resubmission is Rejected -/// -/// Replicates the production flow when a single transaction causes a panic during execution: -/// 1. Submit two transactions: one valid transfer, one targeting a panicking precompile -/// 2. Trigger payload building — the per-transaction `catch_unwind` (payload.rs:589-622) -/// catches the panic and wraps it as `UnprocessableTransactionError`. The outer -/// `handle_build_res` calls `purge_unprocessable_tx`, which removes only the -/// offending transaction from the pool and adds it to the invalid_tx_list. -/// The valid transaction remains in the pool. -/// 3. Produce a subsequent block — succeeds (valid tx is included) -/// 4. Resubmit the panicking transaction — rejected with InvalidTxError -#[cfg(feature = "integration")] -#[tokio::test] -async fn test_payload_builder_panic_populates_invalid_tx_list() -> Result<()> { - use alloy_primitives::U256; - use arc_execution_e2e::ArcEnvironment; - use arc_execution_txpool::ArcTransactionValidatorError; - use arc_precompiles::precompile_provider::PANIC_PRECOMPILE_ADDRESS; - use reth_transaction_pool::error::{PoolError, PoolErrorKind}; - use reth_transaction_pool::{TransactionOrigin, TransactionPool}; - - reth_tracing::init_test_tracing(); - - let mut env = ArcEnvironment::new(); - ArcSetup::new() - .with_invalid_tx_list_config(InvalidTxListConfig { - enabled: true, - capacity: 1000, - }) - .apply(&mut env) - .await?; - - // Step 1: Submit two transactions, one valid, and one targeting the panicking precompile - let (good_tx_hash, _) = SendTransaction::new("good_tx") - .execute_and_return(&mut env) - .await?; - - let (panicking_tx_hash, panicking_tx) = SendTransaction::new("panic_tx") - .with_to(PANIC_PRECOMPILE_ADDRESS) - .with_value(U256::ZERO) - .with_gas_limit(100_000) - .execute_and_return(&mut env) - .await?; - - // Step 2: Attempt to produce a block. The payload builder executes both txs. - // The panicking precompile triggers a panic caught by the per-transaction - // catch_unwind, which wraps it as UnprocessableTransactionError. Only the - // offending tx is purged from the pool and added to invalid_tx_list. - // The build itself fails, but the side effect is what we're testing. - let mut produce = ProduceBlocks::new(1); - let result = arc_execution_e2e::Action::execute(&mut produce, &mut env).await; - assert!( - result.is_err(), - "Expected payload building to fail after panic" - ); - - // Assert the panicking tx was purged from the pool - let pool_size = env.node().inner.pool.len(); - assert_eq!( - pool_size, 1, - "Pool should have one transaction after panicking tx is purged" - ); - - assert!( - env.node().inner.pool.contains(&good_tx_hash), - "Good tx should be in the pool" - ); - - assert!( - !env.node().inner.pool.contains(&panicking_tx_hash), - "Panicking tx should not be in the pool" - ); - - // Step 3: Produce a block — succeeds now that the panicking tx has been purged - let mut produce_after = ProduceBlocks::new(1); - arc_execution_e2e::Action::execute(&mut produce_after, &mut env).await?; - - // Step 4: Resubmit the panicking transaction — should be rejected by invalid_tx_list - let result = env - .node() - .inner - .pool - .add_consensus_transaction(panicking_tx, TransactionOrigin::Local) - .await; - - match result { - Err(PoolError { - kind: PoolErrorKind::InvalidTransaction(ref e), - .. - }) => { - let arc_err = e - .downcast_other_ref::() - .expect("Expected ArcTransactionValidatorError"); - assert!( - matches!(arc_err, ArcTransactionValidatorError::InvalidTxError), - "Expected InvalidTxError, got: {arc_err:?}" - ); - Ok(()) - } - Ok(_) => Err(eyre::eyre!( - "Transaction {panicking_tx_hash} accepted on resubmission, expected rejection" - )), - Err(e) => Err(eyre::eyre!( - "Transaction {panicking_tx_hash} rejected with unexpected error: {e:?}" - )), - } -} - -/// Default-on regression: with no explicit `with_invalid_tx_list_config`, the -/// `InvalidTxListConfig::default()` must still quarantine a tx that triggers -/// `UnprocessableTransactionError` on resubmission. -#[cfg(feature = "integration")] -#[tokio::test] -async fn test_invalid_tx_list_default_on_quarantines_panicking_tx() -> Result<()> { - use alloy_primitives::U256; - use arc_execution_e2e::ArcEnvironment; - use arc_execution_txpool::ArcTransactionValidatorError; - use arc_precompiles::precompile_provider::PANIC_PRECOMPILE_ADDRESS; - use reth_transaction_pool::error::{PoolError, PoolErrorKind}; - use reth_transaction_pool::{TransactionOrigin, TransactionPool}; - - reth_tracing::init_test_tracing(); - - let mut env = ArcEnvironment::new(); - ArcSetup::new().apply(&mut env).await?; - - SendTransaction::new("good_tx") - .execute_and_return(&mut env) - .await?; - - let (panicking_tx_hash, panicking_tx) = SendTransaction::new("panic_tx") - .with_to(PANIC_PRECOMPILE_ADDRESS) - .with_value(U256::ZERO) - .with_gas_limit(100_000) - .execute_and_return(&mut env) - .await?; - - let mut produce = ProduceBlocks::new(1); - let _ = arc_execution_e2e::Action::execute(&mut produce, &mut env).await; - - let result = env - .node() - .inner - .pool - .add_consensus_transaction(panicking_tx, TransactionOrigin::Local) - .await; - - match result { - Err(PoolError { - kind: PoolErrorKind::InvalidTransaction(ref e), - .. - }) => { - let arc_err = e - .downcast_other_ref::() - .expect("Expected ArcTransactionValidatorError"); - assert!( - matches!(arc_err, ArcTransactionValidatorError::InvalidTxError), - "Expected InvalidTxError, got: {arc_err:?}" - ); - Ok(()) - } - Ok(_) => Err(eyre::eyre!( - "Transaction {panicking_tx_hash} accepted on resubmission, expected rejection under default config" - )), - Err(e) => Err(eyre::eyre!( - "Transaction {panicking_tx_hash} rejected with unexpected error: {e:?}" - )), - } -} diff --git a/crates/execution-e2e/tests/native_transfer_balance.rs b/crates/execution-e2e/tests/native_transfer_balance.rs deleted file mode 100644 index 2b4a9b4..0000000 --- a/crates/execution-e2e/tests/native_transfer_balance.rs +++ /dev/null @@ -1,140 +0,0 @@ -// Copyright 2026 Circle Internet Group, Inc. All rights reserved. -// -// SPDX-License-Identifier: Apache-2.0 -// -// Licensed under the Apache License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. -// You may obtain a copy of the License at -// -// http://www.apache.org/licenses/LICENSE-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// See the License for the specific language governing permissions and -// limitations under the License. - -#![allow(clippy::arithmetic_side_effects, clippy::cast_possible_truncation)] - -//! E2E tests verifying that native value transfers produce correct balance changes. - -use alloy_primitives::{address, Address, U256}; -use arc_execution_e2e::{ - actions::{AssertBalance, AssertTxIncluded, ProduceBlocks, SendTransaction, TxStatus}, - ArcSetup, ArcTestBuilder, -}; -use eyre::Result; - -/// Genesis-funded sender address (hardhat account #0). -const SENDER: Address = address!("0xf39Fd6e51aad88F6F4ce6aB8827279cffFb92266"); - -/// Recipient with zero balance in genesis. -const RECIPIENT: Address = address!("0x000000000000000000000000000000000000bEEF"); - -/// Sender's genesis balance: 1,000,000 USDC (1_000_000e18 wei). -/// 0xd3c21bcecceda1000000 = limbs [0x1bcecceda1000000, 0xd3c2, 0, 0] -const SENDER_GENESIS_BALANCE: U256 = U256::from_limbs([0x1bce_cced_a100_0000, 0xd3c2, 0, 0]); - -/// Transfer value used in tests: 100 USDC (100e18 wei). -fn transfer_value() -> U256 { - U256::from(100u64) * U256::from(10u64).pow(U256::from(18u64)) -} - -/// Max gas cost per tx: gas_limit(21_000) * max_fee_per_gas(1000e9). -fn max_gas_cost() -> U256 { - U256::from(21_000u64) * U256::from(1_000_000_000_000u64) -} - -/// Recipient balance goes from 0 to the transferred value. -#[tokio::test] -async fn test_value_transfer_credits_recipient() -> Result<()> { - reth_tracing::init_test_tracing(); - - let value = transfer_value(); - - ArcTestBuilder::new() - .with_setup(ArcSetup::new()) - .with_action( - SendTransaction::new("transfer") - .with_to(RECIPIENT) - .with_value(value), - ) - .with_action(ProduceBlocks::new(1)) - .with_action(AssertTxIncluded::new("transfer").expect(TxStatus::Success)) - .with_action(AssertBalance::new(RECIPIENT, value)) - .run() - .await -} - -/// Sender balance decreases by at least the transferred value (plus gas). -#[tokio::test] -async fn test_value_transfer_debits_sender() -> Result<()> { - reth_tracing::init_test_tracing(); - - let value = transfer_value(); - let min_remaining = SENDER_GENESIS_BALANCE - value - max_gas_cost(); - let max_remaining = SENDER_GENESIS_BALANCE - value; - - ArcTestBuilder::new() - .with_setup(ArcSetup::new()) - .with_action( - SendTransaction::new("transfer") - .with_to(RECIPIENT) - .with_value(value), - ) - .with_action(ProduceBlocks::new(1)) - .with_action(AssertTxIncluded::new("transfer").expect(TxStatus::Success)) - .with_action( - AssertBalance::new(SENDER, min_remaining) - .at_least() - .at_most(max_remaining), - ) - .run() - .await -} - -/// Zero-value transfer leaves recipient balance unchanged. -#[tokio::test] -async fn test_zero_value_transfer_no_balance_change() -> Result<()> { - reth_tracing::init_test_tracing(); - - ArcTestBuilder::new() - .with_setup(ArcSetup::new()) - .with_action( - SendTransaction::new("zero_transfer") - .with_to(RECIPIENT) - .with_value(U256::ZERO), - ) - .with_action(ProduceBlocks::new(1)) - .with_action(AssertTxIncluded::new("zero_transfer").expect(TxStatus::Success)) - .with_action(AssertBalance::new(RECIPIENT, U256::ZERO)) - .run() - .await -} - -/// Two transfers to the same recipient accumulate. -#[tokio::test] -async fn test_multiple_transfers_accumulate() -> Result<()> { - reth_tracing::init_test_tracing(); - - let value = transfer_value(); - - ArcTestBuilder::new() - .with_setup(ArcSetup::new()) - .with_action( - SendTransaction::new("tx1") - .with_to(RECIPIENT) - .with_value(value), - ) - .with_action( - SendTransaction::new("tx2") - .with_to(RECIPIENT) - .with_value(value), - ) - .with_action(ProduceBlocks::new(1)) - .with_action(AssertTxIncluded::new("tx1").expect(TxStatus::Success)) - .with_action(AssertTxIncluded::new("tx2").expect(TxStatus::Success)) - .with_action(AssertBalance::new(RECIPIENT, value + value)) - .run() - .await -} diff --git a/crates/execution-e2e/tests/p256_precompile.rs b/crates/execution-e2e/tests/p256_precompile.rs deleted file mode 100644 index e928536..0000000 --- a/crates/execution-e2e/tests/p256_precompile.rs +++ /dev/null @@ -1,95 +0,0 @@ -// Copyright 2026 Circle Internet Group, Inc. All rights reserved. -// -// SPDX-License-Identifier: Apache-2.0 -// -// Licensed under the Apache License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. -// You may obtain a copy of the License at -// -// http://www.apache.org/licenses/LICENSE-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// See the License for the specific language governing permissions and -// limitations under the License. - -//! P256 (secp256r1) precompile e2e tests for Arc Chain. -//! -//! Tests the EIP-7212 P256 signature verification precompile at address 0x100, -//! which is available starting from the Osaka hardfork. -//! -//! The precompile verifies ECDSA signatures on the secp256r1 (P-256) curve, -//! commonly used for passkey authentication. - -use alloy_primitives::{address, bytes, Address, Bytes}; -use arc_execution_e2e::{ - actions::{CallContract, ProduceBlocks}, - ArcSetup, ArcTestBuilder, -}; -use eyre::Result; -use rstest::rstest; - -/// P256 precompile address as defined in EIP-7212. -const P256_PRECOMPILE_ADDRESS: Address = address!("0000000000000000000000000000000000000100"); - -/// Expected output for valid signature: 32-byte big-endian 1. -const VALID_RESULT: Bytes = - bytes!("0000000000000000000000000000000000000000000000000000000000000001"); - -/// Test P256 signature verification via eth_call. -/// -/// Input format (160 bytes): hash (32) || r (32) || s (32) || x (32) || y (32) -/// -/// Test vectors sourced from: -/// - revm: -/// - p256-verifier: -#[rstest] -#[case::valid_signature( - "p256_valid_sig", - bytes!("4cee90eb86eaa050036147a12d49004b6b9c72bd725d39d4785011fe190f0b4da73bd4903f0ce3b639bbbf6e8e80d16931ff4bcf5993d58468e8fb19086e8cac36dbcd03009df8c59286b162af3bd7fcc0450c9aa81be5d10d312af6c66b1d604aebd3099c618202fcfe16ae7770b0c49ab5eadf74b754204a3bb6060e44eff37618b065f9832de4ca6ca971a7a1adc826d0f7c00181a5fb2ddf79ae00b4e10e"), - VALID_RESULT, -)] -#[case::valid_signature_2( - "p256_valid_sig_2", - bytes!("3fec5769b5cf4e310a7d150508e82fb8e3eda1c2c94c61492d3bd8aea99e06c9e22466e928fdccef0de49e3503d2657d00494a00e764fd437bdafa05f5922b1fbbb77c6817ccf50748419477e843d5bac67e6a70e97dde5a57e0c983b777e1ad31a80482dadf89de6302b1988c82c29544c9c07bb910596158f6062517eb089a2f54c9a0f348752950094d3228d3b940258c75fe2a413cb70baa21dc2e352fc5"), - VALID_RESULT, -)] -#[case::invalid_signature_wrong_hash( - "p256_invalid_sig_wrong_hash", - // First byte of hash changed from 4c to 3c - bytes!("3cee90eb86eaa050036147a12d49004b6b9c72bd725d39d4785011fe190f0b4da73bd4903f0ce3b639bbbf6e8e80d16931ff4bcf5993d58468e8fb19086e8cac36dbcd03009df8c59286b162af3bd7fcc0450c9aa81be5d10d312af6c66b1d604aebd3099c618202fcfe16ae7770b0c49ab5eadf74b754204a3bb6060e44eff37618b065f9832de4ca6ca971a7a1adc826d0f7c00181a5fb2ddf79ae00b4e10e"), - Bytes::new(), -)] -#[case::invalid_pubkey( - "p256_invalid_pubkey", - // Zeros for x and y coordinates - bytes!("4cee90eb86eaa050036147a12d49004b6b9c72bd725d39d4785011fe190f0b4da73bd4903f0ce3b639bbbf6e8e80d16931ff4bcf5993d58468e8fb19086e8cac36dbcd03009df8c59286b162af3bd7fcc0450c9aa81be5d10d312af6c66b1d6000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000"), - Bytes::new(), -)] -#[case::malformed_input( - "p256_malformed_input", - // Only 64 bytes instead of required 160 bytes - bytes!("4cee90eb86eaa050036147a12d49004b6b9c72bd725d39d4785011fe190f0b4da73bd4903f0ce3b639bbbf6e8e80d16931ff4bcf5993d58468e8fb19086e8cac"), - Bytes::new(), -)] -#[tokio::test] -async fn test_p256_verification( - #[case] label: &str, - #[case] input: Bytes, - #[case] expected: Bytes, -) -> Result<()> { - reth_tracing::init_test_tracing(); - - ArcTestBuilder::new() - .with_setup(ArcSetup::new()) - .with_action(ProduceBlocks::new(1)) - .with_action( - CallContract::new(label) - .to(P256_PRECOMPILE_ADDRESS) - .with_data(input) - .expect_result(expected), - ) - .run() - .await -} diff --git a/crates/execution-e2e/tests/pq_precompile.rs b/crates/execution-e2e/tests/pq_precompile.rs deleted file mode 100644 index a27121e..0000000 --- a/crates/execution-e2e/tests/pq_precompile.rs +++ /dev/null @@ -1,184 +0,0 @@ -// Copyright 2026 Circle Internet Group, Inc. All rights reserved. -// -// SPDX-License-Identifier: Apache-2.0 -// -// Licensed under the Apache License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. -// You may obtain a copy of the License at -// -// http://www.apache.org/licenses/LICENSE-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// See the License for the specific language governing permissions and -// limitations under the License. - -//! Post-quantum (SLH-DSA-SHA2-128s) precompile e2e tests for Arc Chain. -//! -//! Exercises the PQ verifier at [`PQ_ADDRESS`] via `eth_call`, matching the flow in -//! `tests/localdev/PQ.test.ts` but against the mock-engine `execution-e2e` harness. -//! -//! Test vectors are generated from deterministic seeds in -//! [`arc_precompiles::pq_test_vectors`] — the same seeds used by the -//! `generate_pq_test_vectors` binary. -//! -//! There is no case here for Zero6 disabled (PQ precompile unavailable): the harness uses -//! LOCAL_DEV, where Zero6 is active from genesis—same scope as `p256_precompile.rs`. - -use alloy_primitives::{bytes, Bytes}; -use alloy_sol_types::SolCall; -use arc_execution_e2e::{ - actions::{CallContract, ProduceBlocks}, - ArcSetup, ArcTestBuilder, -}; -use arc_precompiles::{ - pq::{IPQ, PQ_ADDRESS}, - pq_test_vectors::{self, MSG_EMPTY, MSG_GOODBYE_WORLD, MSG_HELLO_WORLD}, -}; -use eyre::Result; -use rstest::rstest; - -/// ABI-encoded `true` (single 32-byte word). -const RETURN_TRUE: Bytes = - bytes!("0000000000000000000000000000000000000000000000000000000000000001"); - -/// ABI-encoded `false` (single 32-byte word). -const RETURN_FALSE: Bytes = - bytes!("0000000000000000000000000000000000000000000000000000000000000000"); - -/// Wrong-length preimage used for malformed vk/sig cases (100 bytes). -const MALFORMED_100: [u8; 100] = [0u8; 100]; - -#[derive(Clone, Debug)] -enum PqExpected { - ReturnTrue, - ReturnFalse, - Revert, -} - -#[derive(Clone, Debug)] -struct PqVerifyVector { - call_label: &'static str, - message: Bytes, - vk: Bytes, - sig: Bytes, - expected: PqExpected, -} - -fn build_vectors() -> Vec<(&'static str, PqVerifyVector)> { - let tv = pq_test_vectors::cached_vectors(); - - let vk = Bytes::from(tv.verifying_key.clone()); - let wrong_vk = Bytes::from(tv.wrong_vk()); - let sig_hello = Bytes::from(tv.sig_hello_world.clone()); - let sig_empty = Bytes::from(tv.sig_empty_message.clone()); - - vec![ - ( - "valid_signature", - PqVerifyVector { - call_label: "pq_valid_sig", - message: Bytes::copy_from_slice(MSG_HELLO_WORLD), - vk: vk.clone(), - sig: sig_hello.clone(), - expected: PqExpected::ReturnTrue, - }, - ), - ( - "valid_empty_message", - PqVerifyVector { - call_label: "pq_valid_empty_msg", - message: Bytes::copy_from_slice(MSG_EMPTY), - vk: vk.clone(), - sig: sig_empty, - expected: PqExpected::ReturnTrue, - }, - ), - ( - "invalid_wrong_message", - PqVerifyVector { - call_label: "pq_invalid_sig", - message: Bytes::copy_from_slice(MSG_GOODBYE_WORLD), - vk: vk.clone(), - sig: sig_hello.clone(), - expected: PqExpected::ReturnFalse, - }, - ), - ( - "wrong_verifying_key_value", - PqVerifyVector { - call_label: "pq_wrong_vk_value", - message: Bytes::copy_from_slice(MSG_HELLO_WORLD), - vk: wrong_vk, - sig: sig_hello.clone(), - expected: PqExpected::ReturnFalse, - }, - ), - ( - "bad_verifying_key_len", - PqVerifyVector { - call_label: "pq_bad_vk_len", - message: Bytes::copy_from_slice(MSG_HELLO_WORLD), - vk: Bytes::copy_from_slice(&MALFORMED_100), - sig: sig_hello.clone(), - expected: PqExpected::Revert, - }, - ), - ( - "bad_signature_len", - PqVerifyVector { - call_label: "pq_bad_sig_len", - message: Bytes::copy_from_slice(MSG_HELLO_WORLD), - vk, - sig: Bytes::copy_from_slice(&MALFORMED_100), - expected: PqExpected::Revert, - }, - ), - ] -} - -/// SLH-DSA-SHA2-128s precompile via `eth_call`. Each case carries message, vk, sig, -/// and expected outcome. -#[rstest] -#[case::valid_signature(0)] -#[case::valid_empty_message(1)] -#[case::invalid_wrong_message(2)] -#[case::wrong_verifying_key_value(3)] -#[case::bad_verifying_key_len(4)] -#[case::bad_signature_len(5)] -#[tokio::test] -async fn test_pq_precompile(#[case] index: usize) -> Result<()> { - reth_tracing::init_test_tracing(); - - let vectors = build_vectors(); - let (_, vector) = &vectors[index]; - - let data: Bytes = IPQ::verifySlhDsaSha2128sCall { - vk: vector.vk.clone(), - message: vector.message.clone(), - sig: vector.sig.clone(), - } - .abi_encode() - .into(); - - let call = CallContract::new(vector.call_label) - .to(PQ_ADDRESS) - .with_data(data); - let call = match vector.expected { - PqExpected::ReturnTrue => call.expect_result(RETURN_TRUE), - PqExpected::ReturnFalse => call.expect_result(RETURN_FALSE), - PqExpected::Revert => call.expect_revert(), - }; - - ArcTestBuilder::new() - .with_setup(ArcSetup::new()) - // Advance one block after genesis — the usual e2e harness step so the mock node has a - // progressed head before `eth_call`. This is not waiting on a hardfork height: default - // `ArcSetup` uses LOCAL_DEV, where Arc forks (including Zero6 / PQ) are active at block 0 - // (`ARC_LOCALDEV_HARDFORKS` in `execution-config`). - .with_action(ProduceBlocks::new(1)) - .with_action(call) - .run() - .await -} diff --git a/crates/execution-e2e/tests/static_rpc_gas_cap.rs b/crates/execution-e2e/tests/static_rpc_gas_cap.rs deleted file mode 100644 index 7775937..0000000 --- a/crates/execution-e2e/tests/static_rpc_gas_cap.rs +++ /dev/null @@ -1,194 +0,0 @@ -// Copyright 2026 Circle Internet Group, Inc. All rights reserved. -// -// SPDX-License-Identifier: Apache-2.0 -// -// Licensed under the Apache License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. -// You may obtain a copy of the License at -// -// http://www.apache.org/licenses/LICENSE-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// See the License for the specific language governing permissions and -// limitations under the License. - -//! Launched-node coverage for the static `--rpc.gascap=30_000_000` default -//! installed by `crates/node/src/args.rs`. -//! -//! Differential test: -//! -//! - Chainspec block gas limit = 50M (above the static cap, so the cap is the -//! binding constraint). -//! - Node configured with `rpc_gas_cap = 30M` (the production Arc default). -//! - Deploys a counted-loop gas burner sized to consume ~35M gas. -//! - Issues `eth_call(gas = 40M)` against the burner. -//! -//! Direction-of-effect: -//! - With the cap at 30M, Reth clamps the request to 30M; the burner needs -//! ~35M and the call returns out-of-gas. Test passes. -//! - Without the cap (e.g. Reth's stock 50M default), the 40M request is not -//! clamped; the burner runs to completion and the call returns Ok. Test -//! fails — that is the registration-direction signal. - -use alloy_primitives::{Address, Bytes, TxKind}; -use alloy_rpc_types_eth::{TransactionInput, TransactionRequest}; -use arc_execution_config::chainspec::localdev_with_block_gas_limit; -use arc_execution_e2e::{ - actions::{ProduceBlocks, SendTransaction, StoreDeployedAddress}, - Action, ArcEnvironment, ArcSetup, ArcTestBuilder, -}; -use eyre::Result; -use futures_util::future::BoxFuture; -use reth_rpc_api::EthApiClient; - -/// Block gas limit for the test. Set above the static cap so the cap is the -/// binding constraint on `eth_call`. -const TEST_BLOCK_GAS_LIMIT: u64 = 50_000_000; -/// The production Arc default for `--rpc.gascap`. -const STATIC_GAS_CAP: u64 = 30_000_000; -/// Gas the differential check passes to `eth_call`. Above `STATIC_GAS_CAP` -/// (so the cap must clamp) and below `TEST_BLOCK_GAS_LIMIT` (so without the -/// cap the call would run to completion). -const REQUESTED_CALL_GAS: u64 = 40_000_000; - -/// Hand-crafted runtime that loops `KECCAK256(empty)` 448_000 times then -/// `STOP`s, consuming ~35M gas. Sized to OOG when clamped to 30M and to -/// complete when run with 40M. -/// -/// Init code (12 bytes): -/// ```text -/// 60 19 PUSH1 25 ; runtime size -/// 60 0c PUSH1 12 ; runtime offset within init code -/// 60 00 PUSH1 0 ; mem dest -/// 39 CODECOPY -/// 60 19 PUSH1 25 -/// 60 00 PUSH1 0 -/// f3 RETURN -/// ``` -/// -/// Runtime (25 bytes): -/// ```text -/// 62 06 d4 00 PUSH3 0x06d400 ; counter = 448_000 (pc=0) -/// 5b JUMPDEST ; loop start (pc=4) -/// 80 DUP1 ; copy counter -/// 15 ISZERO -/// 60 17 PUSH1 23 ; end pc -/// 57 JUMPI ; if counter == 0, jump to STOP -/// 60 00 PUSH1 0 ; size -/// 60 00 PUSH1 0 ; offset -/// 20 KECCAK256 ; hash empty mem (~30 gas) -/// 50 POP -/// 60 01 PUSH1 1 -/// 90 SWAP1 -/// 03 SUB ; counter -= 1 -/// 60 04 PUSH1 4 -/// 56 JUMP ; back to loop start -/// 5b JUMPDEST ; end (pc=23) -/// 00 STOP -/// ``` -const GAS_BURNER_BYTECODE_HEX: &str = - "6019600c60003960196000f36206d4005b8015601757600060002050600190036004565b00"; - -fn gas_burner_bytecode() -> Bytes { - alloy_primitives::hex::decode(GAS_BURNER_BYTECODE_HEX) - .expect("GAS_BURNER_BYTECODE_HEX is valid hex") - .into() -} - -/// Reads the named deployed address from the environment, issues `eth_call` -/// against it with explicit `gas`, and asserts the call fails out-of-gas. -/// With `--rpc.gascap=30M` Reth clamps the request to 30M and the burner OOGs. -/// Without the cap, the call retains its requested gas and the burner runs to -/// completion — that path makes this test fail, which is the -/// patch-direction signal. -struct AssertCallOutOfGas { - address_name: String, - gas: u64, -} - -impl Action for AssertCallOutOfGas { - fn execute<'a>(&'a mut self, env: &'a mut ArcEnvironment) -> BoxFuture<'a, Result<()>> { - Box::pin(async move { - let to: Address = *env - .get_address(&self.address_name) - .ok_or_else(|| eyre::eyre!("named address '{}' not stored", self.address_name))?; - let client = env - .node() - .rpc_client() - .ok_or_else(|| eyre::eyre!("RPC client not available"))?; - - let request = TransactionRequest { - to: Some(TxKind::Call(to)), - gas: Some(self.gas), - input: TransactionInput::default(), - ..Default::default() - }; - - let result = >::call(&client, request, None, None, None) - .await; - - match result { - Ok(output) => Err(eyre::eyre!( - "eth_call against gas burner with gas={} succeeded (output: {output}); \ - Reth did not clamp to --rpc.gascap. Is the Arc default in place?", - self.gas - )), - Err(err) => { - // Match revm's `OutOfGas(...)` family. Both the Debug - // (`OutOfGas`) and Display (`out of gas`) renderings are - // covered after `to_lowercase()`. Other "gas" substrings - // (e.g. "intrinsic gas too low", "max fee per gas") are - // intentionally not accepted: a non-OOG failure should - // surface, not silently pass. - let msg = err.to_string().to_lowercase(); - if msg.contains("out of gas") || msg.contains("outofgas") { - Ok(()) - } else { - Err(eyre::eyre!( - "eth_call failed with non-OOG error (expected out-of-gas): {err}" - )) - } - } - } - }) - } -} - -/// One launched-node test: with `--rpc.gascap=30M` and `block.gas_limit=50M`, -/// `eth_call(gas=40M)` against a ~35M gas burner returns out-of-gas. The -/// cap is the binding constraint; removing it (Reth's stock 50M default) -/// would let the call complete and fail this test. -#[tokio::test] -async fn static_rpc_gas_cap_clamps_eth_call() -> Result<()> { - reth_tracing::init_test_tracing(); - - ArcTestBuilder::new() - .with_setup( - ArcSetup::new() - .with_chain_spec(localdev_with_block_gas_limit(TEST_BLOCK_GAS_LIMIT)) - .with_rpc_gas_cap(STATIC_GAS_CAP), - ) - .with_action( - SendTransaction::new("deploy_burner") - .with_create() - .with_data(gas_burner_bytecode()) - .with_gas_limit(200_000), - ) - .with_action(ProduceBlocks::new(1)) - .with_action(StoreDeployedAddress::new("deploy_burner")) - .with_action(AssertCallOutOfGas { - address_name: "deploy_burner_address".to_string(), - gas: REQUESTED_CALL_GAS, - }) - .run() - .await -} diff --git a/crates/execution-e2e/tests/transaction.rs b/crates/execution-e2e/tests/transaction.rs deleted file mode 100644 index 155894f..0000000 --- a/crates/execution-e2e/tests/transaction.rs +++ /dev/null @@ -1,75 +0,0 @@ -// Copyright 2026 Circle Internet Group, Inc. All rights reserved. -// -// SPDX-License-Identifier: Apache-2.0 -// -// Licensed under the Apache License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. -// You may obtain a copy of the License at -// -// http://www.apache.org/licenses/LICENSE-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// See the License for the specific language governing permissions and -// limitations under the License. - -//! Transaction sending e2e tests for Arc Chain. - -use alloy_primitives::{address, bytes, U256}; -use arc_execution_e2e::{ - actions::{AssertTxIncluded, ProduceBlocks, SendTransaction, TxStatus}, - ArcSetup, ArcTestBuilder, -}; -use eyre::Result; - -/// Test sending multiple transactions in a single block. -#[tokio::test] -async fn test_multiple_transactions() -> Result<()> { - reth_tracing::init_test_tracing(); - - ArcTestBuilder::new() - .with_setup(ArcSetup::new()) - .with_action(SendTransaction::new("tx1")) - .with_action(SendTransaction::new("tx2")) - .with_action(ProduceBlocks::new(1)) - .with_action(SendTransaction::new("tx3")) - .with_action(ProduceBlocks::new(1)) - .with_action( - AssertTxIncluded::new("tx1") - .in_block(1) - .expect(TxStatus::Success), - ) - .with_action( - AssertTxIncluded::new("tx2") - .in_block(1) - .expect(TxStatus::Success), - ) - .with_action( - AssertTxIncluded::new("tx3") - .in_block(2) - .expect(TxStatus::Success), - ) - .run() - .await -} - -/// Test that a contract call that reverts is detected. -#[tokio::test] -async fn test_reverted_transaction() -> Result<()> { - reth_tracing::init_test_tracing(); - - ArcTestBuilder::new() - .with_setup(ArcSetup::new()) - .with_action( - SendTransaction::new("tx1") - .with_to(address!("0x3600000000000000000000000000000000000000")) - .with_value(U256::ZERO) // Value must be 0 — FiatTokenProxy (0x3600…0000) is pre-blocklisted in NativeCoinControl - .with_data(bytes!("0x1234abcd")) - .with_gas_limit(100_000), - ) - .with_action(ProduceBlocks::new(1)) - .with_action(AssertTxIncluded::new("tx1").expect(TxStatus::Reverted)) - .run() - .await -} diff --git a/crates/execution-payload/Cargo.toml b/crates/execution-payload/Cargo.toml deleted file mode 100644 index 4bb931e..0000000 --- a/crates/execution-payload/Cargo.toml +++ /dev/null @@ -1,53 +0,0 @@ -[package] -name = "arc-execution-payload" -version.workspace = true -edition.workspace = true -readme.workspace = true -license.workspace = true -exclude.workspace = true -rust-version.workspace = true -publish.workspace = true -repository.workspace = true - -[features] - -[dependencies] - -# alloy -alloy-consensus.workspace = true -alloy-primitives.workspace = true -alloy-rlp.workspace = true -arc-execution-txpool.workspace = true -eyre.workspace = true -metrics.workspace = true - -# reth -reth-basic-payload-builder.workspace = true -reth-chainspec.workspace = true -reth-consensus-common.workspace = true -reth-errors.workspace = true -reth-ethereum = { workspace = true, features = ["storage-api"] } -reth-ethereum-engine-primitives.workspace = true -reth-ethereum-payload-builder.workspace = true -reth-ethereum-primitives.workspace = true -reth-evm.workspace = true -reth-node-api.workspace = true -reth-node-builder.workspace = true -reth-payload-builder.workspace = true -reth-payload-primitives.workspace = true -reth-primitives-traits.workspace = true -reth-revm.workspace = true -reth-storage-api.workspace = true -reth-transaction-pool.workspace = true - -# revm -revm.workspace = true -tracing.workspace = true - -[dev-dependencies] -reth-transaction-pool = { workspace = true, features = ["test-utils"] } -tokio = { workspace = true, features = ["full"] } -tracing-test = "0.2" - -[lints] -workspace = true diff --git a/crates/execution-payload/src/builder.rs b/crates/execution-payload/src/builder.rs deleted file mode 100644 index 8436f6d..0000000 --- a/crates/execution-payload/src/builder.rs +++ /dev/null @@ -1,33 +0,0 @@ -// Copyright 2026 Circle Internet Group, Inc. All rights reserved. -// -// SPDX-License-Identifier: Apache-2.0 -// -// Licensed under the Apache License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. -// You may obtain a copy of the License at -// -// http://www.apache.org/licenses/LICENSE-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// See the License for the specific language governing permissions and -// limitations under the License. - -use std::fmt; -use std::fmt::Display; -use std::fmt::Formatter; - -/// Error type for transactions that cannot be processed for an unknown reason (e.g. panic during execution) -#[derive(Debug)] -pub struct UnprocessableTransactionError { - pub tx_hash: alloy_primitives::TxHash, -} - -impl Display for UnprocessableTransactionError { - fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result { - write!(f, "Transaction {} is unprocessable", self.tx_hash) - } -} - -impl std::error::Error for UnprocessableTransactionError {} diff --git a/crates/execution-payload/src/lib.rs b/crates/execution-payload/src/lib.rs deleted file mode 100644 index 0a5b79b..0000000 --- a/crates/execution-payload/src/lib.rs +++ /dev/null @@ -1,23 +0,0 @@ -// Copyright 2026 Circle Internet Group, Inc. All rights reserved. -// -// SPDX-License-Identifier: Apache-2.0 -// -// Licensed under the Apache License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. -// You may obtain a copy of the License at -// -// http://www.apache.org/licenses/LICENSE-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// See the License for the specific language governing permissions and -// limitations under the License. - -//! Arc Payload and Block Builder -//! -//! This crate implements the reth traits for building execution payloads and executing blocks. - -pub mod builder; -pub mod metrics; -pub mod payload; diff --git a/crates/execution-payload/src/metrics.rs b/crates/execution-payload/src/metrics.rs deleted file mode 100644 index 0661bad..0000000 --- a/crates/execution-payload/src/metrics.rs +++ /dev/null @@ -1,118 +0,0 @@ -// Copyright 2026 Circle Internet Group, Inc. All rights reserved. -// -// SPDX-License-Identifier: Apache-2.0 -// -// Licensed under the Apache License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. -// You may obtain a copy of the License at -// -// http://www.apache.org/licenses/LICENSE-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// See the License for the specific language governing permissions and -// limitations under the License. - -//! Prometheus metrics for payload building. -//! -//! Stage durations use a single histogram family with a `stage` label: -//! `arc_payload_stage_duration_seconds{stage="state_setup|pre_execution|tx_execution|post_execution|assembly_and_sealing"}` -//! -//! Total build duration: -//! `arc_payload_total_duration_seconds` -//! -//! Outcome counters use a single counter family with an `outcome` label: -//! `arc_payload_build_outcome_total{outcome="better|aborted|cancelled"}` -//! -//! Outcome semantics: -//! - **better**: built payload had higher total fees than current best; payload was sealed and returned. -//! - **aborted**: built candidate was not better than current best (by fees); candidate was discarded. -//! - **cancelled**: build was cancelled (e.g. job cancelled) before completion. - -use std::time::{Duration, Instant}; - -/// Metric name constants. -const STAGE_DURATION: &str = "arc_payload_stage_duration_seconds"; -const TOTAL_DURATION: &str = "arc_payload_total_duration_seconds"; -const BUILD_OUTCOME: &str = "arc_payload_build_outcome_total"; - -/// Label keys. -const STAGE_LABEL: &str = "stage"; -const OUTCOME_LABEL: &str = "outcome"; - -/// Stage label values. -const STATE_SETUP_STAGE: &str = "state_setup"; -const PRE_EXECUTION_STAGE: &str = "pre_execution"; -const TX_EXECUTION_STAGE: &str = "tx_execution"; -const POST_EXECUTION_STAGE: &str = "post_execution"; -const ASSEMBLY_AND_SEALING_STAGE: &str = "assembly_and_sealing"; - -/// Outcome label values. -const BETTER_OUTCOME: &str = "better"; -const ABORTED_OUTCOME: &str = "aborted"; -const CANCELLED_OUTCOME: &str = "cancelled"; - -/// Records payload build metrics to Prometheus. -/// -/// Stage durations use a single histogram family with a `stage` label: -/// `arc_payload_stage_duration_seconds{stage="..."}` -/// -/// Total build duration: -/// `arc_payload_total_duration_seconds` -/// -/// Outcome counters use a single counter family with an `outcome` label: -/// `arc_payload_build_outcome_total{outcome="..."}` -#[derive(Debug, Clone)] -pub struct PayloadBuildMetrics; - -impl PayloadBuildMetrics { - // ── Stage durations (histograms) ── - - pub fn record_stage_state_setup(duration: Duration) { - metrics::histogram!(STAGE_DURATION, STAGE_LABEL => STATE_SETUP_STAGE) - .record(duration.as_secs_f64()); - } - - pub fn record_stage_pre_execution(duration: Duration) { - metrics::histogram!(STAGE_DURATION, STAGE_LABEL => PRE_EXECUTION_STAGE) - .record(duration.as_secs_f64()); - } - - pub fn record_stage_tx_execution(duration: Duration) { - metrics::histogram!(STAGE_DURATION, STAGE_LABEL => TX_EXECUTION_STAGE) - .record(duration.as_secs_f64()); - } - - /// Records the post-execution stage, which covers `builder.finish()` — - /// primarily state root computation and block finalization. - pub fn record_stage_post_execution(duration: Duration) { - metrics::histogram!(STAGE_DURATION, STAGE_LABEL => POST_EXECUTION_STAGE) - .record(duration.as_secs_f64()); - } - - pub fn record_stage_assembly_and_sealing(duration: Duration) { - metrics::histogram!(STAGE_DURATION, STAGE_LABEL => ASSEMBLY_AND_SEALING_STAGE) - .record(duration.as_secs_f64()); - } - - // ── Total duration ── - - pub fn record_total_duration(payload_start: Instant) { - metrics::histogram!(TOTAL_DURATION).record(payload_start.elapsed().as_secs_f64()); - } - - // ── Outcome counters ── - - pub fn record_outcome_better() { - metrics::counter!(BUILD_OUTCOME, OUTCOME_LABEL => BETTER_OUTCOME).increment(1); - } - - pub fn record_outcome_aborted() { - metrics::counter!(BUILD_OUTCOME, OUTCOME_LABEL => ABORTED_OUTCOME).increment(1); - } - - pub fn record_outcome_cancelled() { - metrics::counter!(BUILD_OUTCOME, OUTCOME_LABEL => CANCELLED_OUTCOME).increment(1); - } -} diff --git a/crates/execution-payload/src/payload.rs b/crates/execution-payload/src/payload.rs deleted file mode 100644 index ebd6bb3..0000000 --- a/crates/execution-payload/src/payload.rs +++ /dev/null @@ -1,1231 +0,0 @@ -// Copyright 2025 Circle Internet Group, Inc. All rights reserved. -// -// SPDX-License-Identifier: Apache-2.0 -// -// Licensed under the Apache License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. -// You may obtain a copy of the License at -// -// http://www.apache.org/licenses/LICENSE-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// See the License for the specific language governing permissions and -// limitations under the License. - -//! Arc Node custom payload builder: InvalidTxFilteringPayloadBuilder. -//! Also, ArcNetworkPayloadBuilderBuilder is needed to inject it in reth_node_builder. -//! InvalidTxFilteringPayloadBuilder wraps ArcEthereumPayloadBuilder and -//! adds failed TXs to the invalid tx list when payload building fails or panics. -//! Panics during individual transaction execution are caught inline in -//! `arc_ethereum_payload` and converted to `UnprocessableTransactionError`. - -use alloy_primitives::U256; -use alloy_primitives::{hex, TxHash}; -use alloy_rlp::Encodable; -use eyre::Result; -use reth_basic_payload_builder::{ - is_better_payload, BuildArguments, BuildOutcome, HeaderForPayload, MissingPayloadBehaviour, - PayloadBuilder as RethPayloadBuilder, PayloadConfig, -}; -use reth_chainspec::{ChainSpecProvider, EthChainSpec, EthereumHardforks}; -use reth_consensus_common::validation::MAX_RLP_BLOCK_SIZE; -use reth_errors::{BlockExecutionError, BlockValidationError, ConsensusError}; -use reth_ethereum_payload_builder::EthereumBuilderConfig; -use reth_ethereum_primitives::{EthPrimitives, TransactionSigned}; -use reth_evm::{ - execute::{BlockBuilder, BlockBuilderOutcome}, - ConfigureEvm, Evm, NextBlockEnvAttributes, -}; -use reth_node_api::{NodeTypes, PrimitivesTy}; -use reth_node_builder::{ - components::PayloadBuilderBuilder, node::FullNodeTypes, BuilderContext, PayloadBuilderConfig, -}; -use reth_payload_builder::{BlobSidecars, EthBuiltPayload, EthPayloadBuilderAttributes}; -use reth_payload_primitives::{PayloadBuilderAttributes, PayloadBuilderError}; -use reth_primitives_traits::transaction::error::InvalidTransactionError; -use reth_revm::{database::StateProviderDatabase, db::State}; -use reth_storage_api::StateProviderFactory; -use reth_transaction_pool::{ - error::InvalidPoolTransactionError, BestTransactions, BestTransactionsAttributes, - PoolTransaction, TransactionPool, ValidPoolTransaction, -}; -use revm::context_interface::Block as _; -use std::{ - panic::{catch_unwind, AssertUnwindSafe}, - sync::Arc, - time::{Duration, Instant}, -}; -use tracing::{debug, error, info, trace, warn}; - -use crate::builder::UnprocessableTransactionError; -use crate::metrics::PayloadBuildMetrics; -use arc_execution_txpool::InvalidTxList; - -type BestTransactionsIter = Box< - dyn BestTransactions::Transaction>>>, ->; - -#[derive(Clone)] -pub struct ArcNetworkPayloadBuilderBuilder { - invalid_tx_list: Option, - /// Custom payload builder maximum execution time, in milliseconds. - /// When unset, Reth's `builder.deadline` is adopted. - payload_builder_deadline_ms: Option, - /// When true, `on_missing_payload` waits for the in-flight build instead of - /// racing an empty block. - wait_for_payload: bool, -} - -impl ArcNetworkPayloadBuilderBuilder { - pub fn new( - invalid_tx_list: Option, - payload_builder_deadline_ms: Option, - wait_for_payload: bool, - ) -> Self { - Self { - invalid_tx_list, - payload_builder_deadline_ms, - wait_for_payload, - } - } -} - -impl PayloadBuilderBuilder - for ArcNetworkPayloadBuilderBuilder -where - Node: FullNodeTypes, - Node::Types: NodeTypes, - Pool: TransactionPool>> - + Unpin - + 'static, - EvmCfg: ConfigureEvm< - Primitives = PrimitivesTy, - NextBlockEnvCtx = NextBlockEnvAttributes, - > + Clone - + Send - + 'static, - ::Payload: reth_node_api::PayloadTypes< - BuiltPayload = EthBuiltPayload, - PayloadAttributes = reth_ethereum_engine_primitives::EthPayloadAttributes, - PayloadBuilderAttributes = EthPayloadBuilderAttributes, - >, -{ - type PayloadBuilder = InvalidTxFilteringPayloadBuilder< - ArcEthereumPayloadBuilder, - Pool, - >; - - fn build_payload_builder( - self, - ctx: &BuilderContext, - pool: Pool, - evm_config: EvmCfg, - ) -> impl std::future::Future> + Send { - let invalid_tx_list = self.invalid_tx_list.clone(); - let payload_builder_deadline_ms = self.payload_builder_deadline_ms; - let provider = ctx.provider().clone(); - let conf = ctx.payload_builder_config(); - let chain = ctx.chain_spec().chain(); - let gas_limit = conf.gas_limit_for(chain); - let deadline = payload_builder_deadline_ms - .map(Duration::from_millis) - .unwrap_or(ctx.config().builder.deadline); - let loop_time_limit = Some(deadline); - let wait_for_payload = self.wait_for_payload; - async move { - let inner = ArcEthereumPayloadBuilder::new( - provider, - pool.clone(), - evm_config, - EthereumBuilderConfig::new() - .with_gas_limit(gas_limit) - .with_await_payload_on_missing(wait_for_payload), - loop_time_limit, - ); - Ok(InvalidTxFilteringPayloadBuilder { - inner, - pool, - invalid_tx_list, - }) - } - } -} - -#[derive(Clone)] -pub struct InvalidTxFilteringPayloadBuilder { - inner: B, - pool: P, - invalid_tx_list: Option, -} - -impl RethPayloadBuilder for InvalidTxFilteringPayloadBuilder -where - B: RethPayloadBuilder, - P: TransactionPool + Unpin, -{ - type Attributes = ::Attributes; - type BuiltPayload = ::BuiltPayload; - - fn try_build( - &self, - args: BuildArguments, - ) -> Result, PayloadBuilderError> { - let res = catch_unwind(AssertUnwindSafe(|| self.inner.try_build(args))); - handle_build_res(res, &self.pool, self.invalid_tx_list.as_ref()) - } - - fn on_missing_payload( - &self, - args: BuildArguments, - ) -> MissingPayloadBehaviour { - self.inner.on_missing_payload(args) - } - - fn build_empty_payload( - &self, - config: PayloadConfig>, - ) -> Result { - match catch_unwind(AssertUnwindSafe(|| self.inner.build_empty_payload(config))) { - Ok(Ok(payload)) => Ok(payload), - Ok(Err(e)) => { - purge_unprocessable_tx(&e, &self.pool, self.invalid_tx_list.as_ref()); - Err(e) - } - Err(panic) => { - purge_pending_and_resume_panic(panic, &self.pool, self.invalid_tx_list.as_ref()) - } - } - } -} - -/// Type alias for the result of `catch_unwind` wrapping a payload build operation. -type CatchUnwindBuildResult = - Result, PayloadBuilderError>, Box>; - -/// If the error wraps an `UnprocessableTransactionError`, purge that transaction -/// from the pool and add it to the invalid tx list. The error is always returned -/// unchanged so the caller can propagate it. -fn purge_unprocessable_tx( - e: &PayloadBuilderError, - pool: &P, - invalid_tx_list: Option<&InvalidTxList>, -) { - if let Some(tx_hash) = extract_unprocessable_tx_hash(e) { - if let Some(tx) = pool.get(&tx_hash) { - log_transaction_details(&tx, "unprocessable transaction details"); - } else { - error!(tx_hash = %tx_hash, "unprocessable transaction not found in pool"); - } - - if let Some(invalid_tx_list) = invalid_tx_list { - error!(tx_hash = %tx_hash, "adding unprocessable transaction to invalid tx list"); - add_pending_txs_to_invalid_list(pool, invalid_tx_list, vec![tx_hash]); - } else { - error!(tx_hash = %tx_hash, "invalid tx list is disabled, cannot add unprocessable transaction"); - } - } -} - -/// Purge all pending transactions from the pool into the invalid tx list, then -/// resume the panic. This function never returns. -fn purge_pending_and_resume_panic( - panic: Box, - pool: &P, - invalid_tx_list: Option<&InvalidTxList>, -) -> ! { - let pending_hashes: Vec = pool - .pending_transactions() - .iter() - .inspect(|tx| log_transaction_details(tx, "pending TX data on payload builder panic")) - .map(|tx| *tx.hash()) - .collect(); - - if let Some(invalid_tx_list) = invalid_tx_list { - error!("payload builder panicked, adding all PENDING TXs to invalid tx list"); - add_pending_txs_to_invalid_list(pool, invalid_tx_list, pending_hashes); - } else { - error!("payload builder panicked, but invalid tx list disabled"); - } - std::panic::resume_unwind(panic) -} - -/// Handles the result of a `catch_unwind` call around the inner payload builder's -/// `try_build`. -/// -/// This function processes three cases: -/// 1. Success: Returns the build outcome directly -/// 2. Builder error: Purges unprocessable transactions, then returns the error -/// 3. Panic: Purges all pending transactions, then resumes the panic -fn handle_build_res( - res: CatchUnwindBuildResult, - pool: &P, - invalid_tx_list: Option<&InvalidTxList>, -) -> Result, PayloadBuilderError> { - match res { - Ok(Ok(outcome)) => Ok(outcome), - Ok(Err(e)) => { - purge_unprocessable_tx(&e, pool, invalid_tx_list); - Err(e) - } - Err(panic) => purge_pending_and_resume_panic(panic, pool, invalid_tx_list), - } -} - -/// Logs detailed information about a transaction. -fn log_transaction_details(tx: &Arc>, context: &str) { - info!( - tx_hash = %tx.hash(), - tx_type = %tx.tx_type(), - sender = %tx.sender(), - to = ?tx.to(), - id = ?tx.id(), - encoded_length = %tx.encoded_length(), - nonce = %tx.nonce(), - gas_limit = %tx.gas_limit(), - cost = ?tx.cost(), - max_fee_per_gas = ?tx.max_fee_per_gas(), - priority_fee_or_price = ?tx.priority_fee_or_price(), - is_local = %tx.is_local(), - is_eip4844 = %tx.is_eip4844(), - authorization_count = ?tx.authorization_count(), - value = ?tx.transaction.value(), - input_len = %tx.transaction.input().len(), - input_dump = %dump_tx_data(tx.transaction.input()), - "{}", context - ); -} - -/// Extracts the transaction hash from an UnprocessableTransactionError if present. -/// -/// Error structure: -/// PayloadBuilderError::Other(UnprocessableTransactionError) -fn extract_unprocessable_tx_hash(err: &PayloadBuilderError) -> Option { - use reth_payload_primitives::PayloadBuilderError as PBE; - - match err { - PBE::Other(boxed_err) => boxed_err - .downcast_ref::() - .map(|e| e.tx_hash), - _ => None, - } -} - -/// Introduced to improve testability of `add_pending_txs_to_invalid_list` -trait PendingPool { - fn remove_transactions_and_descendants(&self, hashes: Vec) -> usize; - fn pending_len(&self) -> usize; -} - -impl PendingPool for T { - fn remove_transactions_and_descendants(&self, hashes: Vec) -> usize { - self.remove_transactions_and_descendants(hashes).len() - } - fn pending_len(&self) -> usize { - self.pending_transactions().len() - } -} - -fn add_pending_txs_to_invalid_list( - pool: &P, - invalid_tx_list: &InvalidTxList, - hashes: Vec, -) { - let before = pool.pending_len(); - - if hashes.is_empty() { - error!("add_pending_txs_to_invalid_list: no pending transactions to add"); - } - - invalid_tx_list.insert_many(hashes.iter().copied()); - let removed = pool.remove_transactions_and_descendants(hashes); - warn!( - removed, - pending_before = before, - pending_after = pool.pending_len(), - "added pending txs to invalid tx list" - ); -} - -/// Format TX data as a multi-line hexdump if too long. -fn dump_tx_data(bytes: &[u8]) -> String { - const INLINE_LIMIT_BYTES: usize = 512; - const BYTES_PER_LINE: usize = 128; - const MAX_LINES: usize = 64; // safety cap => 128 * 64 = 8192 bytes shown max - - if bytes.len() <= INLINE_LIMIT_BYTES { - return hex::encode(bytes); - } - - let mut out = String::new(); - let mut offset = 0usize; - let mut lines = 0usize; - while offset < bytes.len() && lines < MAX_LINES { - let end = (offset.saturating_add(BYTES_PER_LINE)).min(bytes.len()); - let slice = &bytes[offset..end]; - out.push_str(&format!("{:04x}: {}\n", offset, hex::encode(slice))); - offset = end; - lines = lines.saturating_add(1); - } - if offset < bytes.len() { - out.push_str(&format!( - "... truncated after {} bytes ({} total)", - offset, - bytes.len() - )); - } - out -} - -/// Arc's Custom payload builder based on upstream Reth: -/// https://github.com/paradigmxyz/reth/blob/74351d98e906b8af5f118694529fb2b71d316946/crates/ethereum/payload/src/lib.rs#L138 -/// Enforces a time budget to avoid overruns under heavy mempool load. -/// The rest is following the logic in EthereumPayloadBuilder. -#[derive(Debug, Clone, PartialEq, Eq)] -pub struct ArcEthereumPayloadBuilder { - /// Client providing access to node state. - client: Client, - /// Transaction pool. - pool: Pool, - /// The type responsible for creating the evm. - evm_config: EvmConfig, - /// Payload builder configuration. - builder_config: EthereumBuilderConfig, - /// Optional time limit for the main transaction selection loop. - loop_time_limit: Option, -} - -impl ArcEthereumPayloadBuilder { - /// `EthereumPayloadBuilder` constructor. - pub const fn new( - client: Client, - pool: Pool, - evm_config: EvmConfig, - builder_config: EthereumBuilderConfig, - loop_time_limit: Option, - ) -> Self { - Self { - client, - pool, - evm_config, - builder_config, - loop_time_limit, - } - } -} - -impl RethPayloadBuilder - for ArcEthereumPayloadBuilder -where - EvmConfig: ConfigureEvm, - Client: StateProviderFactory + ChainSpecProvider + Clone, - Pool: TransactionPool>, -{ - type Attributes = EthPayloadBuilderAttributes; - type BuiltPayload = EthBuiltPayload; - - fn try_build( - &self, - args: BuildArguments, - ) -> Result, PayloadBuilderError> { - arc_ethereum_payload( - self.evm_config.clone(), - self.client.clone(), - self.pool.clone(), - self.builder_config.clone(), - self.loop_time_limit, - args, - |attributes| self.pool.best_transactions_with_attributes(attributes), - ) - } - - /// Await the build in flight instead of racing a redundant second build via - /// `build_empty_payload`. - fn on_missing_payload( - &self, - _args: BuildArguments, - ) -> MissingPayloadBehaviour { - if self.builder_config.await_payload_on_missing { - MissingPayloadBehaviour::AwaitInProgress - } else { - MissingPayloadBehaviour::RaceEmptyPayload - } - } - - fn build_empty_payload( - &self, - config: PayloadConfig, - ) -> Result { - let args = BuildArguments::new(Default::default(), config, Default::default(), None); - - // This is what's done in upstream EthereumPayloadBuilder::build_empty_payload - arc_ethereum_payload( - self.evm_config.clone(), - self.client.clone(), - self.pool.clone(), - self.builder_config.clone(), - self.loop_time_limit, - args, - |attributes| self.pool.best_transactions_with_attributes(attributes), - )? - .into_payload() - .ok_or_else(|| PayloadBuilderError::MissingPayload) - } -} - -/// Proposer revenue contributed by a single transaction on Arc. -/// -/// On Arc, Proposer revenue equals `effective_gas_price * gas_used`, not -/// `effective_tip_per_gas * gas_used` (the upstream-reth formula, which -/// assumes base fees are burned). -fn proposer_revenue(tx: &T, gas_used: u64, base_fee: u64) -> U256 { - let effective_gas_price = tx.effective_gas_price(Some(base_fee)); - // u128 * u64 fits in U256 (max 192 bits); - // bounded by block_gas_limit * max_fee_per_gas. - #[allow(clippy::arithmetic_side_effects)] - { - U256::from(effective_gas_price) * U256::from(gas_used) - } -} - -/// Constructs an transaction payload using the best transactions from the pool. -/// It follows the upstream Ethereum payload building logic with a Arc-specific deadline for the main loop. -/// -/// -/// Given build arguments including an Ethereum client, transaction pool, -/// and configuration, this function creates a transaction payload. Returns -/// a result indicating success with the payload or an error in case of failure. -#[inline] -pub fn arc_ethereum_payload( - evm_config: EvmConfig, - client: Client, - _pool: Pool, - builder_config: EthereumBuilderConfig, - loop_time_limit: Option, - args: BuildArguments, - best_txs: F, -) -> Result, PayloadBuilderError> -where - EvmConfig: ConfigureEvm, - Client: StateProviderFactory + ChainSpecProvider, - Pool: TransactionPool>, - F: FnOnce(BestTransactionsAttributes) -> BestTransactionsIter, -{ - let BuildArguments { - mut cached_reads, - config, - cancel, - best_payload, - } = args; - let PayloadConfig { - parent_header, - attributes, - } = config; - - let total_start = Instant::now(); - - let stage_start = Instant::now(); - let state_provider = client.state_by_block_hash(parent_header.hash())?; - let state = StateProviderDatabase::new(state_provider.as_ref()); - let mut db = State::builder() - .with_database(cached_reads.as_db_mut(state)) - .with_bundle_update() - .build(); - PayloadBuildMetrics::record_stage_state_setup(stage_start.elapsed()); - - let mut builder = evm_config - .builder_for_next_block( - &mut db, - &parent_header, - NextBlockEnvAttributes { - timestamp: attributes.timestamp(), - suggested_fee_recipient: attributes.suggested_fee_recipient(), - prev_randao: attributes.prev_randao(), - gas_limit: builder_config.gas_limit(parent_header.gas_limit), - parent_beacon_block_root: attributes.parent_beacon_block_root(), - withdrawals: Some(attributes.withdrawals().clone()), - extra_data: builder_config.extra_data, - }, - ) - .map_err(PayloadBuilderError::other)?; - - let chain_spec = client.chain_spec(); - - info!(target: "payload_builder", id=%attributes.id, parent_header = ?parent_header.hash(), parent_number = parent_header.number, "(arc) building new payload"); - let mut cumulative_gas_used = 0u64; - let block_gas_limit: u64 = builder.evm_mut().block().gas_limit(); - let base_fee = builder.evm_mut().block().basefee(); - - let mut best_txs = best_txs(BestTransactionsAttributes::new( - base_fee, - None, // Explicitly disable blob transactions by not providing a blob gas price. - )); - let mut total_fees = U256::ZERO; - - let stage_start = Instant::now(); - builder.apply_pre_execution_changes().map_err(|err| { - warn!(target: "payload_builder", %err, "(arc) failed to apply pre-execution changes"); - PayloadBuilderError::Internal(err.into()) - })?; - PayloadBuildMetrics::record_stage_pre_execution(stage_start.elapsed()); - - let mut block_transactions_rlp_length = 0usize; - let is_osaka = chain_spec.is_osaka_active_at_timestamp(attributes.timestamp); - - let withdrawals_rlp_length = attributes.withdrawals().length(); - - let loop_started = Instant::now(); - - while let Some(pool_tx) = best_txs.next() { - // Break early if loop time budget exhausted - if let Some(limit) = loop_time_limit { - if loop_started.elapsed() >= limit { - #[allow(clippy::cast_possible_truncation)] - let elapsed_ms = loop_started.elapsed().as_millis() as u64; - warn!(elapsed_ms, "(arc) loop time budget reached; sealing early"); - break; - } - } - - // ensure we still have capacity for this transaction - if block_gas_limit - < cumulative_gas_used - .checked_add(pool_tx.gas_limit()) - .expect("total gas shouldn't overflow") - { - // we can't fit this transaction into the block, so we need to mark it as invalid - // which also removes all dependent transaction from the iterator before we can - // continue - best_txs.mark_invalid( - &pool_tx, - &InvalidPoolTransactionError::ExceedsGasLimit(pool_tx.gas_limit(), block_gas_limit), - ); - continue; - } - - // check if the job was cancelled, if so we can exit early - if cancel.is_cancelled() { - PayloadBuildMetrics::record_stage_tx_execution(loop_started.elapsed()); - PayloadBuildMetrics::record_outcome_cancelled(); - PayloadBuildMetrics::record_total_duration(total_start); - return Ok(BuildOutcome::Cancelled); - } - - // convert tx to a signed transaction - let tx = pool_tx.to_consensus(); - - let tx_rlp_len = tx.inner().length(); - - let estimated_block_size_with_tx = block_transactions_rlp_length - .saturating_add(tx_rlp_len) - .saturating_add(withdrawals_rlp_length) - .saturating_add(1024); // 1Kb of overhead for the block header - - if is_osaka && estimated_block_size_with_tx > MAX_RLP_BLOCK_SIZE { - best_txs.mark_invalid( - &pool_tx, - &InvalidPoolTransactionError::OversizedData { - size: estimated_block_size_with_tx, - limit: MAX_RLP_BLOCK_SIZE, - }, - ); - continue; - } - - let gas_used = match catch_unwind(AssertUnwindSafe(|| { - builder.execute_transaction(tx.clone()) - })) { - Ok(Ok(gas_used)) => gas_used, - Ok(Err(BlockExecutionError::Validation(BlockValidationError::InvalidTx { - error, - .. - }))) => { - if error.is_nonce_too_low() { - // if the nonce is too low, we can skip this transaction - trace!(target: "payload_builder", %error, ?tx, "(arc) skipping nonce too low transaction"); - } else { - // if the transaction is invalid, we can skip it and all of its - // descendants - trace!(target: "payload_builder", %error, ?tx, "(arc) skipping invalid transaction and its descendants"); - best_txs.mark_invalid( - &pool_tx, - &InvalidPoolTransactionError::Consensus( - InvalidTransactionError::TxTypeNotSupported, - ), - ); - } - continue; - } - // this is an error that we should treat as fatal for this attempt - Ok(Err(err)) => return Err(PayloadBuilderError::evm(err)), - // a single transaction caused a panic — wrap it so handle_build_res - // can identify the offending tx and purge it from the mempool - Err(_panic_payload) => { - let tx_hash = *pool_tx.hash(); - return Err(PayloadBuilderError::other(UnprocessableTransactionError { - tx_hash, - })); - } - }; - - block_transactions_rlp_length = block_transactions_rlp_length.saturating_add(tx_rlp_len); - - #[allow(clippy::arithmetic_side_effects)] - { - total_fees += proposer_revenue(tx.inner(), gas_used, base_fee); - } - cumulative_gas_used = cumulative_gas_used - .checked_add(gas_used) - .expect("total gas shouldn't overflow"); - } - - PayloadBuildMetrics::record_stage_tx_execution(loop_started.elapsed()); - - // check if we have a better block - if !is_better_payload(best_payload.as_ref(), total_fees) { - // Release db - drop(builder); - PayloadBuildMetrics::record_outcome_aborted(); - PayloadBuildMetrics::record_total_duration(total_start); - // can skip building the block - return Ok(BuildOutcome::Aborted { - fees: total_fees, - cached_reads, - }); - } - - let builder_finish = Instant::now(); - let BlockBuilderOutcome { - execution_result, - block, - .. - } = builder.finish(state_provider.as_ref())?; - PayloadBuildMetrics::record_stage_post_execution(builder_finish.elapsed()); - - let stage_start = Instant::now(); - let requests = chain_spec - .is_prague_active_at_timestamp(attributes.timestamp) - .then_some(execution_result.requests); - - let sealed_block = Arc::new(block.sealed_block().clone()); - debug!(target: "payload_builder", id=%attributes.id, sealed_block_header = ?sealed_block.sealed_header(), "(arc) sealed built block"); - - if is_osaka && sealed_block.rlp_length() > MAX_RLP_BLOCK_SIZE { - PayloadBuildMetrics::record_stage_assembly_and_sealing(stage_start.elapsed()); - PayloadBuildMetrics::record_total_duration(total_start); - return Err(PayloadBuilderError::other(ConsensusError::BlockTooLarge { - rlp_length: sealed_block.rlp_length(), - max_rlp_length: MAX_RLP_BLOCK_SIZE, - })); - } - - let payload = EthBuiltPayload::new(attributes.id, sealed_block, total_fees, requests) - // add blob sidecars from the executed txs; empty for now - .with_sidecars(BlobSidecars::Empty); - PayloadBuildMetrics::record_stage_assembly_and_sealing(stage_start.elapsed()); - - PayloadBuildMetrics::record_outcome_better(); - PayloadBuildMetrics::record_total_duration(total_start); - - Ok(BuildOutcome::Better { - payload, - cached_reads, - }) -} - -#[cfg(test)] -mod tests { - use super::*; - use alloy_primitives::TxHash; - use reth_transaction_pool::test_utils::testing_pool; - use std::panic::AssertUnwindSafe; - - #[derive(Clone, Debug)] - struct MockPendingPool { - hashes: Vec, - removed: std::cell::RefCell>, - } - impl MockPendingPool { - fn new(hashes: Vec) -> Self { - Self { - hashes, - removed: std::cell::RefCell::new(Vec::new()), - } - } - } - impl PendingPool for MockPendingPool { - fn remove_transactions_and_descendants(&self, hashes: Vec) -> usize { - let len = hashes.len(); - self.removed.borrow_mut().extend(hashes); - len - } - fn pending_len(&self) -> usize { - self.hashes.len() - } - } - - #[test] - fn add_pending_txs_to_invalid_list_inserts_all() { - let hashes: Vec = (0..3).map(TxHash::repeat_byte).collect(); - let pool = MockPendingPool::new(hashes.clone()); - let invalid_tx_list = InvalidTxList::new(16); - add_pending_txs_to_invalid_list(&pool, &invalid_tx_list, hashes.clone()); - assert_eq!(hashes.len(), invalid_tx_list.len()); - for h in hashes { - assert!(invalid_tx_list.contains(&h)); - } - } - - #[test] - fn add_pending_txs_to_invalid_list_empty_no_insert() { - let pool = MockPendingPool::new(vec![]); - let invalid_tx_list = InvalidTxList::new(16); - add_pending_txs_to_invalid_list(&pool, &invalid_tx_list, vec![]); - assert_eq!(0, invalid_tx_list.len()); - } - - #[test] - fn add_pending_txs_to_invalid_list_removes_from_pool() { - let hashes: Vec = (0..5).map(TxHash::repeat_byte).collect(); - let pool = MockPendingPool::new(hashes.clone()); - let invalid_tx_list = InvalidTxList::new(64); - add_pending_txs_to_invalid_list(&pool, &invalid_tx_list, hashes.clone()); - assert_eq!(hashes.len(), invalid_tx_list.len()); - for h in &hashes { - assert!(invalid_tx_list.contains(h)); - } - - let removed = pool.removed.borrow().clone(); - assert_eq!(hashes.len(), removed.len()); - for h in &hashes { - assert!(removed.contains(h)); - } - } - - #[test] - fn extract_unprocessable_tx_hash_extracts_correctly() { - let test_hash = TxHash::repeat_byte(0xCD); - let unproc_err = UnprocessableTransactionError { tx_hash: test_hash }; - let payload_err = PayloadBuilderError::other(unproc_err); - - let extracted = extract_unprocessable_tx_hash(&payload_err); - assert_eq!( - extracted, - Some(test_hash), - "Should extract the transaction hash" - ); - } - - #[test] - fn extract_unprocessable_tx_hash_returns_none_for_other_errors() { - // Test with a different PayloadBuilderError variant - let payload_err = PayloadBuilderError::MissingPayload; - - let extracted = extract_unprocessable_tx_hash(&payload_err); - assert_eq!( - extracted, None, - "Should return None for non-EvmExecutionError" - ); - } - - #[test] - fn extract_unprocessable_tx_hash_returns_none_for_non_unprocessable_other_error() { - let other_err = std::io::Error::other("dummy"); - let payload_err = PayloadBuilderError::other(other_err); - - let extracted = extract_unprocessable_tx_hash(&payload_err); - assert_eq!( - extracted, None, - "Should return None for Other errors that are not UnprocessableTransactionError" - ); - } - - #[tracing_test::traced_test] - #[test] - fn log_transaction_details_logs_expected_fields() { - use reth_transaction_pool::test_utils::{MockTransaction, MockTransactionFactory}; - - let mut factory = MockTransactionFactory::default(); - let tx = MockTransaction::eip1559(); - let valid_tx = factory.validated_arc(tx); - - log_transaction_details(&valid_tx, "test context"); - - assert!(logs_contain("tx_hash")); - assert!(logs_contain("sender")); - assert!(logs_contain("to")); - assert!(logs_contain("input_dump")); - assert!(logs_contain("nonce")); - assert!(logs_contain("gas_limit")); - assert!(logs_contain("test context")); - } - - #[test] - fn dump_tx_data_small_input_returns_hex() { - let data = vec![0xab, 0xcd, 0xef]; - let result = dump_tx_data(&data); - assert_eq!(result, "abcdef"); - } - - #[test] - fn dump_tx_data_at_inline_limit_returns_hex() { - let data = vec![0x42; 512]; - let result = dump_tx_data(&data); - // Should be simple hex, no line formatting - assert_eq!(result, "42".repeat(512)); - assert!(!result.contains(':')); - } - - #[test] - fn dump_tx_data_over_inline_limit_formats_with_offsets() { - let data = vec![0xaa; 513]; - let result = dump_tx_data(&data); - // Should have offset formatting - assert!(result.starts_with("0000: ")); - assert!(result.contains('\n')); - } - - #[test] - fn dump_tx_data_large_input_truncates() { - // 64 lines * 128 bytes = 8192, so use more than that - let data = vec![0xff; 10000]; - let result = dump_tx_data(&data); - assert!(result.contains("truncated")); - assert!(result.contains("10000 total")); - } - - #[test] - fn handle_build_res_returns_outcome_on_success() { - let pool = testing_pool(); - let outcome: BuildOutcome<()> = BuildOutcome::Cancelled; - let res: CatchUnwindBuildResult<()> = Ok(Ok(outcome)); - - let result = handle_build_res(res, &pool, None); - assert!(result.is_ok()); - assert!(matches!(result.unwrap(), BuildOutcome::Cancelled)); - } - - #[test] - fn handle_build_res_unprocessable_tx_without_invalid_tx_list() { - let pool = testing_pool(); - let test_hash = TxHash::repeat_byte(0xAB); - let unproc_err = UnprocessableTransactionError { tx_hash: test_hash }; - let payload_err = PayloadBuilderError::other(unproc_err); - let res: CatchUnwindBuildResult<()> = Ok(Err(payload_err)); - - let result = handle_build_res(res, &pool, None); - - assert!(result.is_err()); - } - - #[test] - fn handle_build_res_unprocessable_tx_with_invalid_tx_list() { - let pool = testing_pool(); - let invalid_tx_list = InvalidTxList::new(16); - let test_hash = TxHash::repeat_byte(0xCD); - let unproc_err = UnprocessableTransactionError { tx_hash: test_hash }; - let payload_err = PayloadBuilderError::other(unproc_err); - let res: CatchUnwindBuildResult<()> = Ok(Err(payload_err)); - - let result = handle_build_res(res, &pool, Some(&invalid_tx_list)); - - assert!(result.is_err()); - assert!(invalid_tx_list.contains(&test_hash)); - } - - #[test] - fn handle_build_res_other_error_without_invalid_tx_list() { - let pool = testing_pool(); - let res: CatchUnwindBuildResult<()> = Ok(Err(PayloadBuilderError::MissingPayload)); - - let result = handle_build_res(res, &pool, None); - - assert!(result.is_err()); - assert!(matches!( - result.unwrap_err(), - PayloadBuilderError::MissingPayload - )); - } - - #[test] - fn handle_build_res_other_error_with_invalid_tx_list() { - let pool = testing_pool(); - let invalid_tx_list = InvalidTxList::new(16); - let res: CatchUnwindBuildResult<()> = Ok(Err(PayloadBuilderError::MissingPayload)); - - let result = handle_build_res(res, &pool, Some(&invalid_tx_list)); - - assert!(result.is_err()); - assert!(matches!( - result.unwrap_err(), - PayloadBuilderError::MissingPayload - )); - // Ensure nothing was added to the invalid tx list - assert_eq!(invalid_tx_list.len(), 0); - } - - #[test] - #[should_panic(expected = "test panic")] - fn handle_build_res_panic_without_invalid_tx_list() { - let pool = testing_pool(); - let panic_res = catch_unwind(AssertUnwindSafe(|| -> Result, _> { - panic!("test panic") - })); - - let _ = handle_build_res(panic_res, &pool, None); - } - - #[tokio::test] - async fn handle_build_res_panic_with_invalid_tx_list() { - use reth_transaction_pool::test_utils::MockTransaction; - - let pool = testing_pool(); - let tx = MockTransaction::eip1559(); - let tx_hash = *tx.hash(); - pool.add_transaction(reth_transaction_pool::TransactionOrigin::Local, tx) - .await - .expect("failed to add transaction"); - - let invalid_tx_list = InvalidTxList::new(16); - let panic_res = catch_unwind(AssertUnwindSafe(|| -> Result, _> { - panic!("test panic") - })); - - // Catch the resumed panic so we can check the invalid tx list afterwards - let panic_result = catch_unwind(AssertUnwindSafe(|| { - handle_build_res(panic_res, &pool, Some(&invalid_tx_list)) - })); - - assert!(panic_result.is_err()); - assert!(invalid_tx_list.contains(&tx_hash)); - } - - // --- Tests for InvalidTxFilteringPayloadBuilder::build_empty_payload --- - - /// Configurable mock for the inner PayloadBuilder. - #[derive(Clone)] - struct MockInnerBuilder { - behavior: MockBuildBehavior, - } - - #[derive(Clone)] - enum MockBuildBehavior { - Succeed, - FailUnprocessable(TxHash), - Panic(&'static str), - } - - impl MockInnerBuilder { - fn build_payload() -> EthBuiltPayload { - use reth_payload_builder::{BlobSidecars, PayloadId}; - - let block = reth_ethereum::Block { - header: alloy_consensus::Header::default(), - body: Default::default(), - }; - let sealed = reth_ethereum::primitives::SealedBlock::from(block); - EthBuiltPayload::new(PayloadId::new([0u8; 8]), Arc::new(sealed), U256::ZERO, None) - .with_sidecars(BlobSidecars::Empty) - } - } - - impl RethPayloadBuilder for MockInnerBuilder { - type Attributes = EthPayloadBuilderAttributes; - type BuiltPayload = EthBuiltPayload; - - fn try_build( - &self, - _args: BuildArguments, - ) -> Result, PayloadBuilderError> { - unimplemented!("not used in build_empty_payload tests") - } - - fn build_empty_payload( - &self, - _config: PayloadConfig>, - ) -> Result { - match &self.behavior { - MockBuildBehavior::Succeed => Ok(Self::build_payload()), - MockBuildBehavior::FailUnprocessable(hash) => { - Err(PayloadBuilderError::other(UnprocessableTransactionError { - tx_hash: *hash, - })) - } - MockBuildBehavior::Panic(msg) => panic!("{msg}"), - } - } - } - - fn empty_payload_config() -> PayloadConfig { - let attributes = EthPayloadBuilderAttributes::new( - Default::default(), - reth_ethereum_engine_primitives::EthPayloadAttributes { - timestamp: 1, - prev_randao: Default::default(), - suggested_fee_recipient: Default::default(), - withdrawals: Some(vec![]), - parent_beacon_block_root: Some(Default::default()), - }, - ); - PayloadConfig { - parent_header: Arc::new(reth_ethereum::primitives::SealedHeader::default()), - attributes, - } - } - - fn filtering_builder( - behavior: MockBuildBehavior, - invalid_tx_list: Option, - ) -> InvalidTxFilteringPayloadBuilder< - MockInnerBuilder, - reth_transaction_pool::test_utils::TestPool, - > { - InvalidTxFilteringPayloadBuilder { - inner: MockInnerBuilder { behavior }, - pool: testing_pool(), - invalid_tx_list, - } - } - - #[test] - fn build_empty_payload_success() { - let builder = filtering_builder(MockBuildBehavior::Succeed, None); - let result = builder.build_empty_payload(empty_payload_config()); - assert!(result.is_ok()); - } - - #[test] - fn build_empty_payload_error_purges_unprocessable_tx() { - let invalid_tx_list = InvalidTxList::new(16); - let test_hash = TxHash::repeat_byte(0xBB); - let builder = filtering_builder( - MockBuildBehavior::FailUnprocessable(test_hash), - Some(invalid_tx_list.clone()), - ); - - let result = builder.build_empty_payload(empty_payload_config()); - assert!(result.is_err()); - assert!(invalid_tx_list.contains(&test_hash)); - } - - #[test] - fn build_empty_payload_error_without_invalid_list() { - let test_hash = TxHash::repeat_byte(0xBB); - let builder = filtering_builder(MockBuildBehavior::FailUnprocessable(test_hash), None); - - let result = builder.build_empty_payload(empty_payload_config()); - assert!(result.is_err()); - } - - #[test] - #[should_panic(expected = "empty payload panic")] - fn build_empty_payload_panic_resumes() { - let builder = filtering_builder( - MockBuildBehavior::Panic("empty payload panic"), - Some(InvalidTxList::new(16)), - ); - let _ = builder.build_empty_payload(empty_payload_config()); - } - - // --- Regression tests for `proposer_revenue` --- - // - // Arc redirects base fees to the beneficiary instead of burning them (see - // `ArcEvmHandler::reward_beneficiary`), so proposer revenue must be - // computed from `effective_gas_price`, not `effective_tip_per_gas`. These - // tests pin that formula against a reth bump accidentally reintroducing - // the upstream tip-only pattern. - #[test] - fn proposer_revenue_dynamic_fee_tx_includes_base_fee_plus_tip() { - use alloy_consensus::TxEip1559; - use alloy_primitives::{Address, TxKind}; - - let base_fee: u64 = 100; - let priority_fee: u128 = 50; - let max_fee: u128 = 200; - let gas_used: u64 = 21_000; - - let tx = TxEip1559 { - chain_id: 1, - nonce: 0, - gas_limit: 100_000, - max_fee_per_gas: max_fee, - max_priority_fee_per_gas: priority_fee, - to: TxKind::Call(Address::ZERO), - value: U256::ZERO, - access_list: Default::default(), - input: Default::default(), - }; - - // effective_gas_price = min(max_fee, base_fee + priority_fee) = 150 - let expected = U256::from(150u128) * U256::from(gas_used); - assert_eq!(proposer_revenue(&tx, gas_used, base_fee), expected); - - // Regression guard: full fee is strictly more than tip-only. If a reth - // bump reintroduces `effective_tip_per_gas`, this assert fails. - let tip_only = U256::from(priority_fee) * U256::from(gas_used); - assert!(proposer_revenue(&tx, gas_used, base_fee) > tip_only); - - // Zero-tip trip-wire: proposer revenue is `base_fee * gas_used`, not 0. - // `effective_tip_per_gas` would return 0 here. - let zero_tip_tx = TxEip1559 { - max_priority_fee_per_gas: 0, - ..tx - }; - assert_eq!( - proposer_revenue(&zero_tip_tx, gas_used, base_fee), - U256::from(base_fee) * U256::from(gas_used), - ); - } - - #[test] - fn proposer_revenue_dynamic_fee_tx_capped_at_max_fee() { - use alloy_consensus::TxEip1559; - use alloy_primitives::{Address, TxKind}; - - let base_fee: u64 = 100; - let priority_fee: u128 = 200; // base + tip would exceed max - let max_fee: u128 = 250; - let gas_used: u64 = 21_000; - - let tx = TxEip1559 { - chain_id: 1, - nonce: 0, - gas_limit: 100_000, - max_fee_per_gas: max_fee, - max_priority_fee_per_gas: priority_fee, - to: TxKind::Call(Address::ZERO), - value: U256::ZERO, - access_list: Default::default(), - input: Default::default(), - }; - - // effective_gas_price = min(250, 100 + 200) = 250 - let expected = U256::from(max_fee) * U256::from(gas_used); - assert_eq!(proposer_revenue(&tx, gas_used, base_fee), expected); - } - - #[test] - fn proposer_revenue_legacy_equals_gas_price_times_gas() { - use alloy_consensus::TxLegacy; - use alloy_primitives::{Address, TxKind}; - - let gas_price: u128 = 75; - let gas_used: u64 = 21_000; - // Base fee is irrelevant for legacy txs. - let base_fee: u64 = 100; - - let tx = TxLegacy { - chain_id: Some(1), - nonce: 0, - gas_price, - gas_limit: 100_000, - to: TxKind::Call(Address::ZERO), - value: U256::ZERO, - input: Default::default(), - }; - - let expected = U256::from(gas_price) * U256::from(gas_used); - assert_eq!(proposer_revenue(&tx, gas_used, base_fee), expected); - } -} diff --git a/crates/execution-txpool/Cargo.toml b/crates/execution-txpool/Cargo.toml deleted file mode 100644 index 5f4dfb3..0000000 --- a/crates/execution-txpool/Cargo.toml +++ /dev/null @@ -1,49 +0,0 @@ -[package] -name = "arc-execution-txpool" -version.workspace = true -edition.workspace = true -readme.workspace = true -license.workspace = true -exclude.workspace = true -rust-version.workspace = true -publish.workspace = true -repository.workspace = true - -[features] - -[dependencies] -alloy-primitives.workspace = true -arc-execution-config.workspace = true -arc-execution-validation.workspace = true -arc-precompiles.workspace = true -arc-shared.workspace = true -eyre.workspace = true -metrics.workspace = true -parking_lot.workspace = true -reth-chainspec.workspace = true -reth-ethereum = { workspace = true, features = ["pool"] } -reth-ethereum-primitives.workspace = true -reth-evm.workspace = true -reth-node-api.workspace = true -reth-node-builder.workspace = true -reth-primitives-traits.workspace = true -reth-provider = { workspace = true, features = ["test-utils"] } -reth-storage-api.workspace = true -reth-tracing.workspace = true -reth-transaction-pool = { workspace = true, features = ["test-utils"] } -schnellru.workspace = true -thiserror.workspace = true -tracing.workspace = true - -[dev-dependencies] -alloy-consensus.workspace = true -alloy-eips.workspace = true -k256 = { workspace = true, features = ["ecdsa"] } -reth-evm-ethereum = { workspace = true, default-features = false, features = ["std"] } -reth-provider = { workspace = true, features = ["test-utils"] } -reth-transaction-pool = { workspace = true, features = ["test-utils"] } -serial_test.workspace = true -tokio.workspace = true - -[lints] -workspace = true diff --git a/crates/execution-txpool/src/error.rs b/crates/execution-txpool/src/error.rs deleted file mode 100644 index 0158117..0000000 --- a/crates/execution-txpool/src/error.rs +++ /dev/null @@ -1,77 +0,0 @@ -// Copyright 2025 Circle Internet Group, Inc. All rights reserved. -// -// SPDX-License-Identifier: Apache-2.0 -// -// Licensed under the Apache License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. -// You may obtain a copy of the License at -// -// http://www.apache.org/licenses/LICENSE-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// See the License for the specific language governing permissions and -// limitations under the License. - -use alloy_primitives::Address; -use reth_transaction_pool::error::PoolTransactionError; -use std::any::Any; -use thiserror::Error; - -#[derive(Debug, Error)] -pub enum ArcTransactionValidatorError { - #[error("Blocked address")] - BlocklistedError, - #[error("Transaction in invalid tx list")] - InvalidTxError, - #[error("Address {0} is denylisted")] - DenylistedAddressError(Address), -} - -impl PoolTransactionError for ArcTransactionValidatorError { - fn is_bad_transaction(&self) -> bool { - match self { - Self::BlocklistedError => true, - Self::InvalidTxError => true, - // Node-local policy — peers can't know our denylist config - Self::DenylistedAddressError(_) => false, - } - } - - fn as_any(&self) -> &dyn Any { - self - } -} - -#[cfg(test)] -mod tests { - use super::*; - - #[test] - fn invalid_tx_variant_is_bad_transaction() { - let err = ArcTransactionValidatorError::InvalidTxError; - assert!( - err.is_bad_transaction(), - "InvalidTxError must be classified as bad transaction" - ); - } - - #[test] - fn blocklisted_variant_is_bad_transaction() { - let err = ArcTransactionValidatorError::BlocklistedError; - assert!( - err.is_bad_transaction(), - "BlocklistedError must be classified as bad transaction" - ); - } - - #[test] - fn denylisted_address_variant_is_not_bad_transaction() { - let err = ArcTransactionValidatorError::DenylistedAddressError(Address::ZERO); - assert!( - !err.is_bad_transaction(), - "DenylistedAddressError must not be classified as bad transaction" - ); - } -} diff --git a/crates/execution-txpool/src/lib.rs b/crates/execution-txpool/src/lib.rs deleted file mode 100644 index 0e93516..0000000 --- a/crates/execution-txpool/src/lib.rs +++ /dev/null @@ -1,41 +0,0 @@ -// Copyright 2025 Circle Internet Group, Inc. All rights reserved. -// -// SPDX-License-Identifier: Apache-2.0 -// -// Licensed under the Apache License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. -// You may obtain a copy of the License at -// -// http://www.apache.org/licenses/LICENSE-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// See the License for the specific language governing permissions and -// limitations under the License. - -//! Custom transaction pool components for ARC with blocklist support -//! -//! This module provides transaction pool components including a custom validator -//! that wraps the standard Ethereum validator and adds blocklist checking functionality. - -use reth_transaction_pool::{ - CoinbaseTipOrdering, EthPooledTransaction, TransactionValidationTaskExecutor, -}; - -mod error; -mod pool; -mod validator; - -pub use error::ArcTransactionValidatorError; -pub use pool::ArcPoolBuilder; -pub use validator::{ - ArcTransactionValidator, InvalidTxList, InvalidTxListConfig, ARC_INVALID_TX_LIST_DEFAULT_CAP, -}; - -/// Type alias for Arc transaction pool with custom validator -pub type ArcTransactionPool = reth_transaction_pool::Pool< - TransactionValidationTaskExecutor>, - CoinbaseTipOrdering, - BlobStore, ->; diff --git a/crates/execution-txpool/src/pool.rs b/crates/execution-txpool/src/pool.rs deleted file mode 100644 index 15df698..0000000 --- a/crates/execution-txpool/src/pool.rs +++ /dev/null @@ -1,246 +0,0 @@ -// Copyright 2025 Circle Internet Group, Inc. All rights reserved. -// -// SPDX-License-Identifier: Apache-2.0 -// -// Licensed under the Apache License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. -// You may obtain a copy of the License at -// -// http://www.apache.org/licenses/LICENSE-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// See the License for the specific language governing permissions and -// limitations under the License. - -//! Custom transaction pool implementation for ARC - -use crate::validator::InvalidTxList; -use crate::{ArcTransactionPool, ArcTransactionValidator}; -use arc_execution_config::addresses_denylist::AddressesDenylistConfig; -use arc_execution_config::chainspec::ArcChainSpec; -use reth_ethereum_primitives::TransactionSigned; -use reth_evm::ConfigureEvm; -use reth_node_api::{FullNodeTypes, NodePrimitives, NodeTypes}; -use reth_node_builder::{ - components::{PoolBuilder, TxPoolBuilder}, - BuilderContext, -}; -use reth_tracing::tracing::{debug, info}; -use reth_transaction_pool::{blobstore::DiskFileBlobStore, TransactionValidationTaskExecutor}; - -/// A basic Arc transaction pool builder. -/// -/// Fork from https://github.com/paradigmxyz/reth/blob/v1.7.0/crates/ethereum/node/src/node.rs#L435-L509 -/// with customization to use ArcTransactionValidator. -/// -/// This contains various settings that can be configured and take precedence over the node's -/// config. -#[derive(Debug, Clone)] -#[non_exhaustive] -pub struct ArcPoolBuilder { - // TODO add options for txpool args - invalid_tx_list: Option, - /// Config for addresses denylist (from/to). When None, no address denylist check in validator. - addresses_denylist_config: AddressesDenylistConfig, -} - -impl ArcPoolBuilder { - pub fn new( - invalid_tx_list: Option, - addresses_denylist_config: AddressesDenylistConfig, - ) -> Self { - Self { - invalid_tx_list, - addresses_denylist_config, - } - } -} - -impl PoolBuilder for ArcPoolBuilder -where - Node: FullNodeTypes< - Types: NodeTypes< - ChainSpec = ArcChainSpec, - Primitives: NodePrimitives, - >, - >, - Evm: ConfigureEvm::Primitives> + 'static, -{ - type Pool = ArcTransactionPool; - - async fn build_pool( - self, - ctx: &BuilderContext, - evm_config: Evm, - ) -> eyre::Result { - let pool_config = ctx.pool_config(); - - // Blobs are disabled; use 0 cache size - let blob_store = reth_node_builder::components::create_blob_store_with_cache(ctx, Some(0))?; - - let validator = - TransactionValidationTaskExecutor::eth_builder(ctx.provider().clone(), evm_config) - .with_max_tx_input_bytes(ctx.config().txpool.max_tx_input_bytes) - .with_local_transactions_config(pool_config.local_transactions_config.clone()) - .set_tx_fee_cap(ctx.config().rpc.rpc_tx_fee_cap) - .no_eip4844() - .with_max_tx_gas_limit(ctx.config().txpool.max_tx_gas_limit) - .with_minimum_priority_fee(ctx.config().txpool.minimum_priority_fee) - .with_additional_tasks(ctx.config().txpool.additional_validation_tasks) - .build_with_tasks(ctx.task_executor().clone(), blob_store.clone()) - .map(|inner| { - ArcTransactionValidator::new( - inner, - self.invalid_tx_list.clone(), - self.addresses_denylist_config.clone(), - ) - }); - - let transaction_pool = TxPoolBuilder::new(ctx) - .with_validator(validator) - .build_and_spawn_maintenance_task(blob_store, pool_config)?; - - info!(target: "reth::cli", "Transaction pool initialized"); - debug!(target: "reth::cli", "Spawned txpool maintenance task"); - - Ok(transaction_pool) - } -} - -#[cfg(test)] -mod tests { - use crate::ArcTransactionValidator; - use alloy_primitives::{B256, U256}; - use arc_execution_config::addresses_denylist::AddressesDenylistConfig; - use reth_evm_ethereum::EthEvmConfig; - use reth_provider::test_utils::{ExtendedAccount, MockEthProvider}; - use reth_transaction_pool::{ - blobstore::InMemoryBlobStore, test_utils::MockTransaction, - validate::EthTransactionValidatorBuilder, CoinbaseTipOrdering, Pool, PoolTransaction, - TransactionOrigin, TransactionPool, TransactionValidator, - }; - - /// Helper function to create a test transaction - fn create_test_transaction() -> MockTransaction { - MockTransaction::legacy() - .with_gas_limit(21_000) - .with_gas_price(1_000_000_000) // 1 gwei - .with_value(U256::from(1000)) - } - - /// Helper function to create an ArcTransactionValidator for testing - fn create_arc_validator_for_test( - provider: MockEthProvider, - enable_eip4844: bool, - ) -> ( - ArcTransactionValidator, - InMemoryBlobStore, - ) { - // MockEthProvider needs at least one header for best_block_number() to succeed - provider.add_block(B256::ZERO, reth_ethereum_primitives::Block::default()); - let blob_store = InMemoryBlobStore::default(); - - let mut builder = EthTransactionValidatorBuilder::new(provider, EthEvmConfig::mainnet()); - if !enable_eip4844 { - builder = builder.no_eip4844(); - } - - let eth_validator = builder.build(blob_store.clone()); - let arc_validator = - ArcTransactionValidator::new(eth_validator, None, AddressesDenylistConfig::Disabled); - - (arc_validator, blob_store) - } - - #[tokio::test] - async fn test_arc_validator_delegates_to_inner() { - let transaction = create_test_transaction(); - let sender = transaction.sender(); - - let provider = MockEthProvider::default(); - provider.add_account(sender, ExtendedAccount::new(0, U256::MAX)); - - let (arc_validator, blob_store) = create_arc_validator_for_test(provider, true); - - let outcome = arc_validator - .validate_one(TransactionOrigin::External, transaction.clone()) - .await; - - assert!( - outcome.is_valid(), - "Expected valid transaction, but got {:?}", - outcome - ); - - // Test full pool integration - let pool = Pool::new( - arc_validator, - CoinbaseTipOrdering::default(), - blob_store.clone(), - Default::default(), - ); - - let res = pool.add_external_transaction(transaction.clone()).await; - assert!(res.is_ok(), "Failed to add transaction to pool: {:?}", res); - - let tx = pool.get(transaction.hash()); - assert!(tx.is_some(), "Transaction should be retrievable from pool"); - } - - #[tokio::test] - async fn test_arc_validator_trait_implementation() { - let transaction = create_test_transaction(); - let sender = transaction.sender(); - - let provider = MockEthProvider::default(); - provider.add_account(sender, ExtendedAccount::new(0, U256::MAX)); - - let (arc_validator, _blob_store) = create_arc_validator_for_test(provider, true); - - let outcome = arc_validator - .validate_transaction(TransactionOrigin::External, transaction.clone()) - .await; - - assert!( - outcome.is_valid(), - "Expected valid transaction, but got {:?}", - outcome - ); - - let transactions = vec![ - (TransactionOrigin::External, transaction.clone()), - (TransactionOrigin::Local, transaction.clone()), - ]; - - let outcomes = arc_validator.validate_transactions(transactions).await; - assert_eq!(outcomes.len(), 2); - - let transactions = vec![transaction.clone(), transaction]; - let outcomes = arc_validator - .validate_transactions_with_origin(TransactionOrigin::External, transactions) - .await; - assert_eq!(outcomes.len(), 2); - } - - #[tokio::test] - async fn test_arc_validator_rejects_eip4844_transactions() { - let provider = MockEthProvider::default(); - - // EIP-4844 disabled (matches production config) - let (arc_validator, _blob_store) = create_arc_validator_for_test(provider, false); - - let blob_transaction = MockTransaction::eip4844(); - - let outcome = arc_validator - .validate_one(TransactionOrigin::External, blob_transaction.clone()) - .await; - - assert!( - outcome.is_invalid(), - "EIP-4844 transaction should be invalid when EIP-4844 is disabled, got: {:?}", - outcome - ); - } -} diff --git a/crates/execution-txpool/src/validator.rs b/crates/execution-txpool/src/validator.rs deleted file mode 100644 index 5d9fb4d..0000000 --- a/crates/execution-txpool/src/validator.rs +++ /dev/null @@ -1,1413 +0,0 @@ -// Copyright 2025 Circle Internet Group, Inc. All rights reserved. -// -// SPDX-License-Identifier: Apache-2.0 -// -// Licensed under the Apache License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. -// You may obtain a copy of the License at -// -// http://www.apache.org/licenses/LICENSE-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// See the License for the specific language governing permissions and -// limitations under the License. - -//! Arc transaction validator implementation with blocklist and addresses denylist support - -use crate::error::ArcTransactionValidatorError; -use alloy_primitives::{Address, TxHash}; -use arc_execution_config::addresses_denylist::AddressesDenylistConfig; -use arc_execution_validation::{is_denylisted, DenylistError}; -use arc_precompiles::{ - native_coin_control::{compute_is_blocklisted_storage_slot, UNBLOCKLISTED_STATUS}, - NATIVE_COIN_CONTROL_ADDRESS, -}; -use arc_shared::metrics::denylist::record_denylist_rejection; -use parking_lot::RwLock; -use reth_chainspec::{ChainSpecProvider, EthereumHardforks}; -use reth_ethereum::pool::EthTransactionValidator; -use reth_evm::ConfigureEvm; -use reth_primitives_traits::{BlockTy, SealedBlock}; -use reth_provider::StateProviderFactory; -use reth_storage_api::errors::provider::ProviderResult; -use reth_storage_api::AccountInfoReader; -use reth_storage_api::BlockNumReader; -use reth_storage_api::StateProvider; -use reth_transaction_pool::{ - error::InvalidPoolTransactionError, EthPoolTransaction, TransactionOrigin, - TransactionValidationOutcome, TransactionValidator, -}; -use schnellru::{ByLength, LruMap}; -use std::{marker::PhantomData, sync::Arc}; -use tracing::{info, warn}; - -/// Default capacity for the invalid transaction list when no override is provided. -pub const ARC_INVALID_TX_LIST_DEFAULT_CAP: u32 = 100_000; // 32 bytes * 100_000 = ~3.2 MB (+ LRU overhead) - -/// Configuration for the invalid transaction list. -#[derive(Debug, Clone)] -pub struct InvalidTxListConfig { - /// Whether the invalid tx list is enabled. - pub enabled: bool, - /// Maximum capacity of the invalid tx list LRU cache. - pub capacity: u32, -} - -impl Default for InvalidTxListConfig { - fn default() -> Self { - Self { - enabled: true, - capacity: ARC_INVALID_TX_LIST_DEFAULT_CAP, - } - } -} - -impl InvalidTxListConfig { - /// Creates a new InvalidTxListConfig. - pub fn new(enabled: bool, capacity: u32) -> Self { - Self { enabled, capacity } - } -} - -static METRICS_LABELS: &[(&str, &str)] = &[("component", "arc_reth_node")]; - -pub type InvalidTxList = InvalidTxListInner; - -pub trait InvalidTxListMetricsSink { - fn hit(); - fn size(len: usize); - fn inserts(n: usize); - fn insert_batches(); -} - -#[derive(Debug, Clone)] -pub struct DefaultInvalidTxListMetrics; - -impl InvalidTxListMetricsSink for DefaultInvalidTxListMetrics { - fn hit() { - metrics::counter!("arc_invalid_tx_list_hits_total", METRICS_LABELS).increment(1); - } - fn size(len: usize) { - metrics::gauge!("arc_invalid_tx_list_size", METRICS_LABELS).set(len as f64); - } - fn inserts(n: usize) { - metrics::counter!("arc_invalid_tx_list_inserts_total", METRICS_LABELS).increment(n as u64); - } - fn insert_batches() { - metrics::counter!("arc_invalid_tx_list_batch_inserts_total", METRICS_LABELS).increment(1); - } -} - -#[derive(Debug, Clone)] -pub struct InvalidTxListInner( - Arc>>, - PhantomData, -); - -impl InvalidTxListInner -where - Sink: InvalidTxListMetricsSink, -{ - pub fn new(cap: u32) -> Self { - let lru = Arc::new(RwLock::new(LruMap::new(ByLength::new(cap)))); - info!(capacity = cap, "Created InvalidTxList"); - Sink::size(0); - Self(lru, PhantomData) - } - - pub fn contains(&self, hash: &TxHash) -> bool { - let hit = { self.0.read().peek(hash).is_some() }; - if hit { - Sink::hit(); - } - hit - } - - pub fn insert_many(&self, hashes: impl IntoIterator) { - let mut hashes_count = 0usize; - let mut success = true; - let len = { - let mut map = self.0.write(); - for hash in hashes { - if !map.insert(hash, ()) { - success = false; - } - // Bounded by the iterator length - #[allow(clippy::arithmetic_side_effects)] - { - hashes_count += 1; - } - } - map.len() - }; - if !success { - info!("The invalid tx list has capacity 0, no TX was inserted") - } - Sink::size(len); - Sink::inserts(hashes_count); - Sink::insert_batches(); - } - - pub fn len(&self) -> usize { - self.0.read().len() - } -} - -/// Custom mempool transaction validator that includes blocklist and addresses denylist checks -#[derive(Debug, Clone)] -pub struct ArcTransactionValidator { - /// The underlying Ethereum validator that handles standard validation logic - inner: Arc>, - invalid_tx_list: Option, - addresses_denylist_config: AddressesDenylistConfig, -} - -impl ArcTransactionValidator { - /// Create a new [ArcTransactionValidator] wrapping the Ethereum validator - pub fn new( - inner: EthTransactionValidator, - invalid_tx_list: Option, - addresses_denylist_config: AddressesDenylistConfig, - ) -> Self { - Self { - inner: Arc::new(inner), - invalid_tx_list, - addresses_denylist_config, - } - } -} - -impl ArcTransactionValidator -where - Client: ChainSpecProvider + StateProviderFactory + BlockNumReader, - Tx: EthPoolTransaction, - Evm: ConfigureEvm, -{ - /// Validates a single transaction. - /// - /// This behaves the same as [`ArcTransactionValidator::validate_one_with_state`], but creates - /// a new state provider internally. - pub async fn validate_one( - &self, - origin: TransactionOrigin, - transaction: Tx, - ) -> TransactionValidationOutcome { - self.validate_one_with_state(origin, transaction, &mut None) - .await - } - - /// Validates a single transaction with a provided state provider. - /// This behaves the same as [`EthTransactionValidator::validate_one_with_state`], but in - /// addition applies blocklist and addresses denylist checks: - pub async fn validate_one_with_state( - &self, - origin: TransactionOrigin, - transaction: Tx, - state: &mut Option>, - ) -> TransactionValidationOutcome { - // ✅ invalid tx list pre-check: refuse tx by hash immediately - if let Some(invalid_tx_list) = &self.invalid_tx_list { - if invalid_tx_list.contains(transaction.hash()) { - warn!( - origin = ?origin, - hash = %transaction.hash(), - sender = %transaction.sender(), - reason = "invalid_tx", - "transaction rejected" - ); - return TransactionValidationOutcome::Invalid( - transaction, - InvalidPoolTransactionError::other( - ArcTransactionValidatorError::InvalidTxError, - ), - ); - } - } - - match self.inner.client().latest() { - Ok(state_provider) => { - match self.check_for_blocklisted_addresses(&transaction, &state_provider) { - Ok(Some(address)) => { - info!( - "Rejecting {:?} transaction {} - blocklisted address: {}", - origin, - transaction.hash(), - address - ); - return TransactionValidationOutcome::Invalid( - transaction, - InvalidPoolTransactionError::other( - ArcTransactionValidatorError::BlocklistedError, - ), - ); - } - Ok(None) => {} - Err(err) => { - warn!( - %err, - "blocklist storage read failed — rejecting transaction" - ); - return TransactionValidationOutcome::Error( - *transaction.hash(), - Box::new(err), - ); - } - } - - // check if transaction's to/from addresses are denylisted - match self.check_for_denylisted_addresses(&transaction, &state_provider) { - Ok(Some(address)) => { - record_denylist_rejection(); - warn!( - origin = ?origin, - address = %address, - reason = "denylisted", - location = "mempool", - "transaction rejected due to denylisted address" - ); - return TransactionValidationOutcome::Invalid( - transaction, - InvalidPoolTransactionError::other( - ArcTransactionValidatorError::DenylistedAddressError(address), - ), - ); - } - Ok(None) => {} - Err(err) => { - return TransactionValidationOutcome::Error( - *transaction.hash(), - Box::new(err), - ); - } - }; - - // Store the provider for the inner validator for reuse - *state = Some(Box::new(state_provider)); - } - Err(err) => { - return TransactionValidationOutcome::Error(*transaction.hash(), Box::new(err)); - } - } - - // If blocklist and addresses denylist validation pass, delegate to the inner validator - self.inner - .validate_one_with_state(origin, transaction, state) - } - - /// If the transaction has a denylisted address, returns Ok(Some(address)); otherwise Ok(None). - /// If an error occurs, returns Err(boxed TransactionValidationOutcome). - fn check_for_denylisted_addresses( - &self, - transaction: &Tx, - state_provider: &dyn StateProvider, - ) -> ProviderResult> { - if !self.addresses_denylist_config.is_enabled() { - return Ok(None); - } - - let auth_authorities: Vec
= transaction - .authorization_list() - .into_iter() - .flatten() - .filter_map(|auth| auth.recover_authority().ok()) - .collect(); - - let addresses = std::iter::once(transaction.sender()) - .chain(transaction.to()) - .chain(auth_authorities); - - for address in addresses { - match is_denylisted(state_provider, &self.addresses_denylist_config, address) { - Ok(true) => return Ok(Some(address)), - Ok(false) => {} - Err(DenylistError::StorageReadFailed(e)) => return Err(e), - } - } - - Ok(None) - } - - /// Returns Ok(Some(address)) if any checked address is blocklisted, Ok(None) if all clear. - /// Checks the sender unconditionally and the recipient only for value-bearing transactions. - fn check_for_blocklisted_addresses( - &self, - transaction: &Tx, - state_provider: &dyn StateProvider, - ) -> ProviderResult> { - let has_value = !transaction.value().is_zero(); - - let addresses = - std::iter::once(transaction.sender()).chain(transaction.to().filter(|_| has_value)); - - for address in addresses { - if self.is_address_blocklisted(address, state_provider)? { - return Ok(Some(address)); - } - } - - Ok(None) - } - - fn is_address_blocklisted( - &self, - address: Address, - state: &dyn StateProvider, - ) -> ProviderResult { - is_address_blocklisted(address, |addr, slot| state.storage(addr, slot)) - } -} - -/// Core blocklist check: reads the blocklist storage slot and fails closed on errors. -/// Extracted as a free function taking a storage reader closure for testability. -fn is_address_blocklisted( - address: Address, - read_storage: impl Fn( - Address, - alloy_primitives::StorageKey, - ) -> ProviderResult>, -) -> ProviderResult { - let storage_slot = compute_is_blocklisted_storage_slot(address); - let value = read_storage(NATIVE_COIN_CONTROL_ADDRESS, storage_slot)?; - Ok(value.is_some_and(|v| v != UNBLOCKLISTED_STATUS)) -} - -impl TransactionValidator for ArcTransactionValidator -where - Client: ChainSpecProvider + StateProviderFactory + BlockNumReader, - Tx: EthPoolTransaction, - Evm: ConfigureEvm, -{ - type Transaction = Tx; - type Block = BlockTy; - - async fn validate_transaction( - &self, - origin: TransactionOrigin, - transaction: Self::Transaction, - ) -> TransactionValidationOutcome { - self.validate_one(origin, transaction).await - } - - fn on_new_head_block(&self, new_tip_block: &SealedBlock) { - self.inner.on_new_head_block(new_tip_block); - } -} - -#[cfg(test)] -mod tests { - use super::*; - use alloy_consensus::Transaction; - use alloy_primitives::{Address, B256, U256}; - use arc_execution_config::addresses_denylist::compute_denylist_storage_slot; - use arc_execution_config::addresses_denylist::{ - AddressesDenylistConfig, DEFAULT_DENYLIST_ERC7201_BASE_SLOT, - }; - use arc_precompiles::native_coin_control::BLOCKLISTED_STATUS; - use reth_evm_ethereum::EthEvmConfig; - use reth_provider::test_utils::{ExtendedAccount, MockEthProvider}; - use reth_storage_api::errors::provider::ProviderError; - use reth_transaction_pool::blobstore::InMemoryBlobStore; - use reth_transaction_pool::{ - test_utils::MockTransaction, validate::EthTransactionValidatorBuilder, PoolTransaction, - }; - use serial_test::serial; - use std::sync::atomic::{AtomicU64, Ordering}; - - /// Helper function - fn create_arc_validator_for_test( - provider: MockEthProvider, - ) -> ArcTransactionValidator { - // MockEthProvider needs at least one header for best_block_number() to succeed - provider.add_block(B256::ZERO, reth_ethereum_primitives::Block::default()); - let blob_store = InMemoryBlobStore::default(); - let eth_validator = EthTransactionValidatorBuilder::new(provider, EthEvmConfig::mainnet()) - .no_eip4844() - .build(blob_store); - ArcTransactionValidator::new( - eth_validator, - Some(InvalidTxList::new(ARC_INVALID_TX_LIST_DEFAULT_CAP)), - AddressesDenylistConfig::Disabled, - ) - } - - #[derive(Debug)] - enum ExpectedOutcome { - Valid, - Invalid, - } - - struct BlocklistTestCase { - name: &'static str, - tx: MockTransaction, - sender_blocklisted: bool, - to_blocklisted: bool, - expected_outcome: ExpectedOutcome, - } - - #[tokio::test] - async fn test_validate_one_with_state() { - let test_cases = [ - BlocklistTestCase { - name: "valid_sender_and_recipient", - tx: MockTransaction::legacy() - .with_gas_limit(21_000) - .with_gas_price(1_000_000_000) - .with_value(U256::from(1000)), - sender_blocklisted: false, - to_blocklisted: false, - expected_outcome: ExpectedOutcome::Valid, - }, - BlocklistTestCase { - name: "blocklisted_sender", - tx: MockTransaction::legacy() - .with_gas_limit(21_000) - .with_gas_price(1_000_000_000) - .with_value(U256::from(1000)), - sender_blocklisted: true, - to_blocklisted: false, - expected_outcome: ExpectedOutcome::Invalid, - }, - BlocklistTestCase { - name: "blocklisted_to_with_value", - tx: MockTransaction::legacy() - .with_gas_limit(21_000) - .with_gas_price(1_000_000_000) - .with_value(U256::from(1000)), - sender_blocklisted: false, - to_blocklisted: true, - expected_outcome: ExpectedOutcome::Invalid, - }, - BlocklistTestCase { - name: "blocklisted_to_with_zero_value", - tx: MockTransaction::legacy() - .with_gas_limit(21_000) - .with_gas_price(1_000_000_000) - .with_value(U256::ZERO), - sender_blocklisted: false, - to_blocklisted: true, - expected_outcome: ExpectedOutcome::Valid, - }, - ]; - - for test_case in test_cases { - let transaction = test_case.tx; - - let provider = MockEthProvider::default(); - provider.add_block(B256::ZERO, reth_ethereum_primitives::Block::default()); - provider.add_account(transaction.sender(), ExtendedAccount::new(0, U256::MAX)); - if let Some(to_address) = transaction.to() { - provider.add_account(to_address, ExtendedAccount::new(0, U256::MAX)); - } - - // Set up blocklist storage for the precompile using ExtendedAccount - if test_case.sender_blocklisted || test_case.to_blocklisted { - let mut storage = std::collections::HashMap::new(); - - if test_case.sender_blocklisted { - let sender_slot = compute_is_blocklisted_storage_slot(transaction.sender()); - storage.insert(sender_slot, U256::from(1)); - } - if test_case.to_blocklisted { - if let Some(to_address) = transaction.to() { - let recipient_slot = compute_is_blocklisted_storage_slot(to_address); - storage.insert(recipient_slot, U256::from(1)); - } - } - - let precompile_account = - ExtendedAccount::new(0, U256::ZERO).extend_storage(storage); - provider.add_account(NATIVE_COIN_CONTROL_ADDRESS, precompile_account); - } - - let arc_validator = create_arc_validator_for_test(provider); - let outcome = arc_validator - .validate_one_with_state(TransactionOrigin::External, transaction, &mut None) - .await; - - let matches_expectation = matches!( - (&outcome, &test_case.expected_outcome), - ( - TransactionValidationOutcome::Valid { .. }, - ExpectedOutcome::Valid - ) | ( - TransactionValidationOutcome::Invalid(_, _), - ExpectedOutcome::Invalid - ) - ); - assert!(matches_expectation, "Test '{}' failed", test_case.name); - } - } - - #[tokio::test] - async fn invalid_tx_list_disabled_allows_tx() { - // Build a validator with invalid_tx_list None and ensure tx is not rejected for invalid_tx reason. - let tx = MockTransaction::legacy() - .with_gas_limit(21_000) - .with_gas_price(1_000_000_000) - .with_value(U256::from(777)); - let provider = MockEthProvider::default(); - provider.add_block(B256::ZERO, reth_ethereum_primitives::Block::default()); - provider.add_account(tx.sender(), ExtendedAccount::new(0, U256::MAX)); - if let Some(to) = tx.to() { - provider.add_account(to, ExtendedAccount::new(0, U256::MAX)); - } - - let blob_store = InMemoryBlobStore::default(); - let eth_validator = EthTransactionValidatorBuilder::new(provider, EthEvmConfig::mainnet()) - .no_eip4844() - .build(blob_store); - let arc_validator = - ArcTransactionValidator::new(eth_validator, None, AddressesDenylistConfig::Disabled); // invalid tx list disabled - - let outcome = arc_validator - .validate_one_with_state(TransactionOrigin::External, tx, &mut None) - .await; - - // Expect a Valid outcome (no invalid tx list pre-check, and no blocklist entries set up). - assert!( - matches!(outcome, TransactionValidationOutcome::Valid { .. }), - "tx should validate when invalid tx list disabled" - ); - } - - #[tokio::test] - async fn invalid_tx_hash_is_rejected() { - let tx = MockTransaction::legacy() - .with_gas_limit(21_000) - .with_gas_price(1_000_000_000) - .with_value(U256::from(1234)); - - let provider = MockEthProvider::default(); - provider.add_account(tx.sender(), ExtendedAccount::new(0, U256::MAX)); - let to = tx - .to() - .expect("legacy mock transaction must have a recipient address"); - provider.add_account(to, ExtendedAccount::new(0, U256::MAX)); - - let arc_validator = create_arc_validator_for_test(provider); - - // Pre-insert hash into invalid tx list. - if let Some(invalid_tx_list) = &arc_validator.invalid_tx_list { - invalid_tx_list.insert_many([*tx.hash()]); - assert!( - invalid_tx_list.contains(tx.hash()), - "hash should be in invalid tx list" - ); - } else { - panic!("invalid tx list unexpectedly disabled in test"); - } - - let outcome = arc_validator - .validate_one_with_state(TransactionOrigin::External, tx, &mut None) - .await; - - let TransactionValidationOutcome::Invalid(_, err) = &outcome else { - panic!("expected Invalid outcome with invalid tx error, got {outcome:?}"); - }; - let inner: &ArcTransactionValidatorError = err - .downcast_other_ref::() - .unwrap(); - assert!(matches!( - inner, - ArcTransactionValidatorError::InvalidTxError - )); - } - - #[tokio::test] - async fn addresses_denylist_config_none_accepts_tx() { - // When addresses_denylist_config is None, no address denylist check; tx is accepted. - let tx = MockTransaction::legacy() - .with_gas_limit(21_000) - .with_gas_price(1_000_000_000) - .with_value(U256::from(100)); - let provider = MockEthProvider::default(); - provider.add_block(B256::ZERO, reth_ethereum_primitives::Block::default()); - provider.add_account(tx.sender(), ExtendedAccount::new(0, U256::MAX)); - if let Some(to) = tx.to() { - provider.add_account(to, ExtendedAccount::new(0, U256::MAX)); - } - let blob_store = InMemoryBlobStore::default(); - let eth_validator = EthTransactionValidatorBuilder::new(provider, EthEvmConfig::mainnet()) - .no_eip4844() - .build(blob_store); - let arc_validator = - ArcTransactionValidator::new(eth_validator, None, AddressesDenylistConfig::Disabled); - let outcome = arc_validator - .validate_one_with_state(TransactionOrigin::External, tx, &mut None) - .await; - assert!( - matches!(outcome, TransactionValidationOutcome::Valid { .. }), - "tx should be accepted when addresses_denylist_config is None" - ); - } - - #[tokio::test] - async fn addresses_denylist_enabled_sender_excluded_accepts_tx() { - // When denylist is enabled but sender is in address exclusions, tx is accepted. - let tx = MockTransaction::legacy() - .with_gas_limit(21_000) - .with_gas_price(1_000_000_000) - .with_value(U256::from(200)); - let sender = tx.sender(); - let contract = Address::from([0x36u8; 20]); - let config = AddressesDenylistConfig::try_new( - true, - Some(contract), - Some(DEFAULT_DENYLIST_ERC7201_BASE_SLOT), - vec![sender], - ) - .unwrap(); - let provider = MockEthProvider::default(); - provider.add_block(B256::ZERO, reth_ethereum_primitives::Block::default()); - provider.add_account(sender, ExtendedAccount::new(0, U256::MAX)); - if let Some(to) = tx.to() { - provider.add_account(to, ExtendedAccount::new(0, U256::MAX)); - } - let blob_store = InMemoryBlobStore::default(); - let eth_validator = EthTransactionValidatorBuilder::new(provider, EthEvmConfig::mainnet()) - .no_eip4844() - .build(blob_store); - let arc_validator = ArcTransactionValidator::new(eth_validator, None, config); - let outcome = arc_validator - .validate_one_with_state(TransactionOrigin::External, tx, &mut None) - .await; - assert!( - matches!(outcome, TransactionValidationOutcome::Valid { .. }), - "tx should be accepted when sender is in address exclusions" - ); - } - - #[tokio::test] - async fn addresses_denylist_denylisted_address_rejected() { - // When denylist is enabled and sender or recipient is denylisted in contract storage, tx is rejected. - let tx = MockTransaction::legacy() - .with_gas_limit(21_000) - .with_gas_price(1_000_000_000) - .with_value(U256::from(300)); - let contract = Address::from([0x36u8; 20]); - let config = AddressesDenylistConfig::try_new( - true, - Some(contract), - Some(DEFAULT_DENYLIST_ERC7201_BASE_SLOT), - Vec::new(), - ) - .unwrap(); - - for (name, denylisted_address) in [ - ("denylisted_sender", tx.sender()), - ( - "denylisted_recipient", - tx.to().expect("MockTransaction::legacy has a to address"), - ), - ] { - let provider = MockEthProvider::default(); - provider.add_block(B256::ZERO, reth_ethereum_primitives::Block::default()); - provider.add_account(tx.sender(), ExtendedAccount::new(0, U256::MAX)); - if let Some(to) = tx.to() { - provider.add_account(to, ExtendedAccount::new(0, U256::MAX)); - } - let slot = compute_denylist_storage_slot( - denylisted_address, - DEFAULT_DENYLIST_ERC7201_BASE_SLOT, - ); - let mut denylist_storage = std::collections::HashMap::new(); - denylist_storage.insert(slot, U256::from(1)); - let denylist_account = - ExtendedAccount::new(0, U256::ZERO).extend_storage(denylist_storage); - provider.add_account(contract, denylist_account); - let blob_store = InMemoryBlobStore::default(); - let eth_validator = - EthTransactionValidatorBuilder::new(provider, EthEvmConfig::mainnet()) - .no_eip4844() - .build(blob_store); - let arc_validator = ArcTransactionValidator::new(eth_validator, None, config.clone()); - let outcome = arc_validator - .validate_one_with_state(TransactionOrigin::External, tx.clone(), &mut None) - .await; - let TransactionValidationOutcome::Invalid(_, err) = &outcome else { - panic!("expected Invalid outcome for {name}, got {outcome:?}"); - }; - let inner: &ArcTransactionValidatorError = err - .downcast_other_ref::() - .unwrap(); - assert!( - matches!(inner, ArcTransactionValidatorError::DenylistedAddressError(addr) if *addr == denylisted_address), - "expected DenylistedAddressError({denylisted_address}) for {name}, got {inner:?}" - ); - } - } - - /// Creates a signed EIP-7702 authorization with a fresh key. - /// Returns `(authority_address, SignedAuthorization)`. - fn create_signed_authorization( - chain_id: u64, - delegate_address: Address, - nonce: u64, - ) -> (Address, alloy_eips::eip7702::SignedAuthorization) { - use alloy_eips::eip7702::Authorization; - use alloy_primitives::Signature; - use k256::ecdsa::SigningKey; - - let key_byte = delegate_address.0[0] | 1; // deterministic, non-zero - let key_bytes: [u8; 32] = [key_byte; 32]; - let signing_key = SigningKey::from_bytes((&key_bytes).into()).expect("fixed key is valid"); - let auth = Authorization { - chain_id: U256::from(chain_id), - address: delegate_address, - nonce, - }; - let sig_hash = auth.signature_hash(); - let (sig, recid) = signing_key - .sign_prehash_recoverable(sig_hash.as_ref()) - .expect("signing must succeed"); - let alloy_sig = Signature::new( - U256::from_be_slice(&sig.r().to_bytes()), - U256::from_be_slice(&sig.s().to_bytes()), - recid.is_y_odd(), - ); - let signed_auth = auth.into_signed(alloy_sig); - let authority = signed_auth - .recover_authority() - .expect("authority recovery must succeed"); - (authority, signed_auth) - } - - const DENYLIST_CONTRACT: Address = Address::new([0x36u8; 20]); - - fn eip7702_tx_with_auths( - auth_list: Vec, - ) -> MockTransaction { - // 100_000 gas covers EIP-7702 intrinsic (21_000 + 12_500 per auth) - let mut tx = MockTransaction::eip7702() - .with_gas_limit(100_000) - .with_gas_price(1_000_000_000); - tx.set_authorization_list(auth_list); - tx - } - - fn provider_with_funded_accounts(tx: &MockTransaction) -> MockEthProvider { - let provider = MockEthProvider::default(); - // Post-Prague timestamp so the inner validator accepts EIP-7702 transactions - let mut block = reth_ethereum_primitives::Block::default(); - block.header.timestamp = 2_000_000_000; - provider.add_block(B256::ZERO, block); - provider.add_account(tx.sender(), ExtendedAccount::new(0, U256::MAX)); - provider.add_account(tx.to().unwrap(), ExtendedAccount::new(0, U256::MAX)); - provider - } - - fn add_denylisted_address(provider: &MockEthProvider, address: Address) { - let slot = compute_denylist_storage_slot(address, DEFAULT_DENYLIST_ERC7201_BASE_SLOT); - let mut storage = std::collections::HashMap::new(); - storage.insert(slot, U256::from(1)); - provider.add_account( - DENYLIST_CONTRACT, - ExtendedAccount::new(0, U256::ZERO).extend_storage(storage), - ); - } - - fn denylist_config(exclusions: Vec
) -> AddressesDenylistConfig { - AddressesDenylistConfig::try_new( - true, - Some(DENYLIST_CONTRACT), - Some(DEFAULT_DENYLIST_ERC7201_BASE_SLOT), - exclusions, - ) - .unwrap() - } - - async fn validate_eip7702( - tx: MockTransaction, - provider: MockEthProvider, - config: AddressesDenylistConfig, - ) -> TransactionValidationOutcome { - let blob_store = InMemoryBlobStore::default(); - let eth_validator = EthTransactionValidatorBuilder::new(provider, EthEvmConfig::mainnet()) - .no_eip4844() - .build(blob_store); - let arc_validator = ArcTransactionValidator::new(eth_validator, None, config); - arc_validator - .validate_one_with_state(TransactionOrigin::External, tx, &mut None) - .await - } - - fn assert_denylist_rejected( - outcome: &TransactionValidationOutcome, - expected_addr: Address, - ) { - let TransactionValidationOutcome::Invalid(_, err) = outcome else { - panic!("expected Invalid outcome, got {outcome:?}"); - }; - let inner: &ArcTransactionValidatorError = err - .downcast_other_ref::() - .unwrap(); - assert!( - matches!(inner, ArcTransactionValidatorError::DenylistedAddressError(addr) if *addr == expected_addr), - "expected DenylistedAddressError({expected_addr}), got {inner:?}" - ); - } - - fn assert_valid(outcome: &TransactionValidationOutcome) { - assert!( - matches!(outcome, TransactionValidationOutcome::Valid { .. }), - "expected Valid outcome, got {outcome:?}" - ); - } - - #[tokio::test] - async fn eip7702_denylisted_authority_rejected() { - let (authority, signed_auth) = create_signed_authorization(1, Address::from([0xAA; 20]), 0); - let tx = eip7702_tx_with_auths(vec![signed_auth]); - let provider = provider_with_funded_accounts(&tx); - add_denylisted_address(&provider, authority); - - let outcome = validate_eip7702(tx, provider, denylist_config(Vec::new())).await; - assert_denylist_rejected(&outcome, authority); - } - - #[tokio::test] - async fn eip7702_multiple_auths_second_denylisted_rejected() { - let (_clean, clean_auth) = create_signed_authorization(1, Address::from([0xAA; 20]), 0); - let (denylisted, denylisted_auth) = - create_signed_authorization(1, Address::from([0xBB; 20]), 0); - let tx = eip7702_tx_with_auths(vec![clean_auth, denylisted_auth]); - let provider = provider_with_funded_accounts(&tx); - add_denylisted_address(&provider, denylisted); - - let outcome = validate_eip7702(tx, provider, denylist_config(Vec::new())).await; - assert_denylist_rejected(&outcome, denylisted); - } - - #[tokio::test] - async fn eip7702_denylisted_authority_excluded_passes() { - let (authority, signed_auth) = create_signed_authorization(1, Address::from([0xEE; 20]), 0); - let tx = eip7702_tx_with_auths(vec![signed_auth]); - let provider = provider_with_funded_accounts(&tx); - add_denylisted_address(&provider, authority); - - let outcome = validate_eip7702(tx, provider, denylist_config(vec![authority])).await; - assert_valid(&outcome); - } - - #[tokio::test] - async fn eip7702_non_denylisted_authority_passes_denylist_check() { - let (_authority, signed_auth) = - create_signed_authorization(1, Address::from([0xBB; 20]), 0); - let tx = eip7702_tx_with_auths(vec![signed_auth]); - let provider = provider_with_funded_accounts(&tx); - provider.add_account(DENYLIST_CONTRACT, ExtendedAccount::new(0, U256::ZERO)); - - let outcome = validate_eip7702(tx, provider, denylist_config(Vec::new())).await; - assert_valid(&outcome); - } - - #[tokio::test] - async fn eip7702_invalid_auth_signature_skipped() { - // y_parity=2 forces SignatureError::InvalidParity, making recover_authority() return Err - let invalid_auth = alloy_eips::eip7702::SignedAuthorization::new_unchecked( - alloy_eips::eip7702::Authorization { - chain_id: U256::from(1), - address: Address::from([0xCC; 20]), - nonce: 0, - }, - 2, - U256::from(1), - U256::from(1), - ); - let tx = eip7702_tx_with_auths(vec![invalid_auth]); - let provider = provider_with_funded_accounts(&tx); - provider.add_account(DENYLIST_CONTRACT, ExtendedAccount::new(0, U256::ZERO)); - - let outcome = validate_eip7702(tx, provider, denylist_config(Vec::new())).await; - assert_valid(&outcome); - } - - static HITS: AtomicU64 = AtomicU64::new(0); - static SIZE_LAST: AtomicU64 = AtomicU64::new(0); - static INSERTS: AtomicU64 = AtomicU64::new(0); - static BATCHES: AtomicU64 = AtomicU64::new(0); - - fn load(a: &AtomicU64) -> u64 { - a.load(Ordering::Relaxed) - } - fn store(a: &AtomicU64, v: u64) { - a.store(v, Ordering::Relaxed); - } - fn inc(a: &AtomicU64, v: u64) { - a.fetch_add(v, Ordering::Relaxed); - } - - fn reset_counters() { - store(&HITS, 0); - store(&SIZE_LAST, 0); - store(&INSERTS, 0); - store(&BATCHES, 0); - } - - struct TestInvalidTxListMetrics; - - impl InvalidTxListMetricsSink for TestInvalidTxListMetrics { - fn hit() { - inc(&HITS, 1); - } - fn size(len: usize) { - store(&SIZE_LAST, len as u64); - } - fn inserts(n: usize) { - inc(&INSERTS, n as u64); - } - fn insert_batches() { - inc(&BATCHES, 1); - } - } - - type TestInvalidTxList = InvalidTxListInner; - - #[test] - #[serial] - fn reports_size_on_new_is_zero() { - reset_counters(); - - let _deny = TestInvalidTxList::new(8); - - assert_eq!(load(&SIZE_LAST), 0, "constructor should report size = 0"); - assert_eq!(load(&INSERTS), 0, "no inserts yet"); - assert_eq!(load(&BATCHES), 0, "no batches yet"); - assert_eq!(load(&HITS), 0, "no contains() calls yet"); - } - - #[test] - #[serial] - fn size_and_counters_after_insert_many() { - reset_counters(); - - let deny = TestInvalidTxList::new(8); - - // h1 duplicated once - let h1 = TxHash::repeat_byte(1); - let h2 = TxHash::repeat_byte(2); - deny.insert_many([h1, h2, h1]); - - assert_eq!(load(&SIZE_LAST), 2, "unique size in LRU"); - assert_eq!(load(&INSERTS), 3, "attempted inserts (incl. dups)"); - assert_eq!(load(&BATCHES), 1, "one batch recorded"); - assert_eq!(load(&HITS), 0, "no contains() hits"); - } - - #[test] - #[serial] - fn contains_increments_hits_on_true_hit_only() { - reset_counters(); - - let deny = TestInvalidTxList::new(8); - - // Insert one hash so it will be found later. - let h1 = TxHash::repeat_byte(42); - deny.insert_many([h1]); - - // Hit: should increment `hits`. - assert!(deny.contains(&h1)); - - // Miss: should NOT increment `hits`. - let h2 = TxHash::repeat_byte(99); - assert!(!deny.contains(&h2)); - - assert_eq!(load(&HITS), 1, "only the true hit increments the counter"); - assert_eq!( - load(&SIZE_LAST), - 1, - "LRU size should stay 1 after single insert" - ); - assert_eq!(load(&INSERTS), 1, "one insert attempted"); - assert_eq!(load(&BATCHES), 1, "one batch recorded"); - } - - #[test] - #[serial] - fn capacity_is_enforced_and_eviction_occurs() { - reset_counters(); - - let cap = 3u32; - let deny = TestInvalidTxList::new(cap); - - // Insert more unique hashes than capacity so eviction must happen. - let keys: Vec<_> = (0u8..10).map(TxHash::repeat_byte).collect(); - deny.insert_many(keys.clone()); - - assert_eq!( - load(&SIZE_LAST), - cap as u64, - "size must be capped at capacity" - ); - assert_eq!(load(&INSERTS), keys.len() as u64, "all attempts counted"); - assert_eq!(load(&BATCHES), 1, "one batch recorded so far"); - - assert!(deny.contains(&TxHash::repeat_byte(9))); // hit - assert!(deny.contains(&TxHash::repeat_byte(8))); // hit - assert!(!deny.contains(&TxHash::repeat_byte(0))); // miss (evicted long ago) - - assert_eq!(load(&HITS), 2, "only true contains() calls count as hits"); - } - - #[test] - #[serial] - fn cap_one_eviction_behavior() { - reset_counters(); - - let deny = TestInvalidTxList::new(1); - - let h1 = TxHash::repeat_byte(1); - let h2 = TxHash::repeat_byte(2); - deny.insert_many([h1]); - - // Hit: should increment `hits`. - assert!(deny.contains(&h1)); - - // Should evict a, keep b - deny.insert_many([h2]); - assert!(!deny.contains(&h1)); - assert!(deny.contains(&h2)); - - assert_eq!(load(&SIZE_LAST), 1, "LRU size should remain 1"); - assert_eq!(load(&INSERTS), 2, "two attempted inserts counted"); - assert_eq!(load(&BATCHES), 2, "two batches recorded"); - assert_eq!(load(&HITS), 2, "two successful contains() hits"); - } - - #[test] - #[serial] - fn duplicates_beyond_capacity_accounting() { - reset_counters(); - - let deny = TestInvalidTxList::new(3); - - let keys = [ - TxHash::repeat_byte(1), - TxHash::repeat_byte(2), - TxHash::repeat_byte(3), - TxHash::repeat_byte(2), - TxHash::repeat_byte(4), // triggers eviction - TxHash::repeat_byte(3), // may be evicted, duplicate attempt - ]; - deny.insert_many(keys); - - assert_eq!( - load(&INSERTS), - keys.len() as u64, - "all attempts counted including duplicates" - ); - assert_eq!(load(&SIZE_LAST), 3, "size capped at capacity"); - assert_eq!(load(&BATCHES), 1, "single batch recorded"); - assert_eq!(load(&HITS), 0, "no contains() calls made"); - } - - #[test] - #[serial] - fn cycling_eviction_hits_only_present() { - reset_counters(); - - let cap = 5u32; - let deny = TestInvalidTxList::new(cap); - // Cycle through 0..15 twice - for _ in 0..2 { - for i in 0u8..15u8 { - deny.insert_many([TxHash::repeat_byte(i)]); - } - } - // Only last cap keys should remain (10..14) - for i in 0u8..10u8 { - assert!( - !deny.contains(&TxHash::repeat_byte(i)), - "evicted key {} incorrectly present", - i - ); - } - for i in 10u8..15u8 { - assert!( - deny.contains(&TxHash::repeat_byte(i)), - "expected key {} to be present", - i - ); - } - assert_eq!(load(&SIZE_LAST), cap as u64, "size capped at capacity"); - assert_eq!(load(&INSERTS), 30, "30 total attempted inserts (2 * 15)"); - assert_eq!( - load(&BATCHES), - 30, - "each single-item insert counted as a batch" - ); - assert_eq!(load(&HITS), 5, "hits only for final 5 present keys"); - } - - #[test] - #[serial] - fn empty_batch_still_counts_as_batch() { - reset_counters(); - - let deny = TestInvalidTxList::new(8); - - // Insert an empty batch. - deny.insert_many(std::iter::empty()); - - // Metrics: batch counted, no inserts, size unchanged, no hits. - assert_eq!( - load(&BATCHES), - 1, - "even empty insert_many counts as a batch" - ); - assert_eq!(load(&INSERTS), 0, "no attempted inserts"); - assert_eq!(load(&SIZE_LAST), 0, "LRU size remains zero"); - assert_eq!(load(&HITS), 0, "no contains() hits"); - } - #[test] - #[serial] - fn inserting_same_key_twice_does_not_increase_size() { - reset_counters(); - - let deny = TestInvalidTxList::new(8); - let h = TxHash::repeat_byte(55); - - deny.insert_many([h]); - let first_size = load(&SIZE_LAST); - - // Insert the same key again. - deny.insert_many([h]); - let second_size = load(&SIZE_LAST); - - assert_eq!( - first_size, second_size, - "reinserting same key shouldn't grow size" - ); - assert_eq!(load(&INSERTS), 2, "two insert attempts counted"); - assert_eq!(load(&BATCHES), 2, "two batches recorded"); - assert_eq!(load(&HITS), 0, "no contains() hits"); - } - - #[test] - #[serial] - fn contains_does_not_promote_mru() { - reset_counters(); - - let deny = TestInvalidTxList::new(2); - - let a = TxHash::repeat_byte(1); - let b = TxHash::repeat_byte(2); - let c = TxHash::repeat_byte(3); - - // LRU order after these inserts: newest=b, oldest=a - deny.insert_many([a, b]); - assert_eq!(load(&SIZE_LAST), 2); - - // contains(a) MUST NOT promote since we use peek() - assert!(deny.contains(&a)); // hit 1 - - // Insert c -> should evict the oldest (which must still be 'a') - deny.insert_many([c]); - assert_eq!(load(&SIZE_LAST), 2); - - assert!(!deny.contains(&a)); // miss - assert!(deny.contains(&b)); // hit 2 - assert!(deny.contains(&c)); // hit 3 - - assert_eq!(load(&HITS), 3, "two post-insert hits + one pre-insert hit"); - assert_eq!(load(&INSERTS), 3, "three insert attempts counted"); - assert_eq!(load(&BATCHES), 2, "two batches recorded"); - } - - #[test] - #[serial] - fn exactly_at_capacity_no_eviction() { - reset_counters(); - - let cap = 3u32; - let deny = TestInvalidTxList::new(cap); - - let a = TxHash::repeat_byte(1); - let b = TxHash::repeat_byte(2); - let c = TxHash::repeat_byte(3); - let d = TxHash::repeat_byte(4); - deny.insert_many([a, b, c]); - - assert_eq!(load(&SIZE_LAST), cap as u64, "size should equal capacity"); - assert_eq!(load(&INSERTS), 3, "three attempted inserts"); - assert_eq!(load(&BATCHES), 1, "one batch"); - - assert!(deny.contains(&a)); - assert!(deny.contains(&b)); - assert!(deny.contains(&c)); - assert_eq!(load(&HITS), 3, "three successful contains() calls"); - assert!(!deny.contains(&d)); - assert_eq!(load(&HITS), 3, "still three successful contains() calls"); - } - - #[test] - #[serial] - fn duplicate_inserts_beyond_capacity_counts_attempts_and_capped_size() { - reset_counters(); - - let cap = 3; - let deny = TestInvalidTxList::new(cap); - - let a = TxHash::repeat_byte(1); - let b = TxHash::repeat_byte(2); - let c = TxHash::repeat_byte(3); - let d = TxHash::repeat_byte(4); - - // 6 attempts, with duplicates for a and c. - deny.insert_many([a, a, b, c, c, d]); - - assert_eq!(load(&INSERTS), 6, "all attempts counted"); - assert_eq!(load(&BATCHES), 1, "one batch recorded"); - assert_eq!(load(&SIZE_LAST), cap as u64, "size capped at capacity"); - - assert!(!deny.contains(&a)); - assert!(deny.contains(&b)); - assert!(deny.contains(&c)); - assert!(deny.contains(&d)); - assert_eq!(load(&HITS), 3, "only true hits are counted"); - } - - #[test] - #[serial] - fn zero_capacity_discards_everything() { - reset_counters(); - - let deny = TestInvalidTxList::new(0); - - let a = TxHash::repeat_byte(10); - let b = TxHash::repeat_byte(11); - let c = TxHash::repeat_byte(12); - deny.insert_many([a, b, c]); - - assert_eq!(load(&SIZE_LAST), 0, "size must remain zero"); - assert_eq!(load(&INSERTS), 3, "attempts still counted"); - assert_eq!(load(&BATCHES), 1, "one batch recorded"); - - // Nothing is present; contains() should miss and not increment hits. - assert!(!deny.contains(&a)); - assert!(!deny.contains(&b)); - assert!(!deny.contains(&c)); - assert_eq!(load(&HITS), 0, "misses do not increment hits"); - } - - #[test] - #[serial] - fn multiple_batches_accumulate_properly() { - reset_counters(); - - let cap = 4u32; - let deny = TestInvalidTxList::new(cap); - - let a = TxHash::repeat_byte(1); - let b = TxHash::repeat_byte(2); - let c = TxHash::repeat_byte(3); - let d = TxHash::repeat_byte(4); - let e = TxHash::repeat_byte(5); - - // Batch 1: 2 items - deny.insert_many([a, b]); - // Batch 2: 3 more (total uniques 5; size should end at min(5, cap)=4) - deny.insert_many([c, d, e]); - - assert_eq!(load(&BATCHES), 2, "two batches recorded"); - assert_eq!(load(&INSERTS), 5, "all attempts counted"); - assert_eq!(load(&SIZE_LAST), cap as u64, "size capped at capacity"); - - assert!(!deny.contains(&a)); - assert!(deny.contains(&b)); - assert!(deny.contains(&c)); - assert!(deny.contains(&d)); - assert!(deny.contains(&e)); - assert_eq!(load(&HITS), 4, "only hits increment"); - } - - #[test] - #[serial] - fn contains_on_missing_emits_no_metrics() { - reset_counters(); - - let deny = TestInvalidTxList::new(8); - - let x = TxHash::repeat_byte(200); - assert!(!deny.contains(&x)); - - assert_eq!(load(&HITS), 0, "miss should not count as a hit"); - assert_eq!(load(&SIZE_LAST), 0, "size still zero"); - assert_eq!(load(&INSERTS), 0, "no inserts attempted"); - assert_eq!(load(&BATCHES), 0, "no batches recorded"); - } - - #[test] - #[serial] - fn large_batch_eviction_correct() { - reset_counters(); - - let cap = 5u32; - let deny = TestInvalidTxList::new(cap); - - let keys: Vec<_> = (0u8..100).map(TxHash::repeat_byte).collect(); - deny.insert_many(keys.clone()); - - assert_eq!(load(&SIZE_LAST), cap as u64, "size capped at capacity"); - assert_eq!(load(&INSERTS), 100, "all attempts counted"); - assert_eq!(load(&BATCHES), 1, "one batch recorded"); - - // Old ones should miss... - assert!(!deny.contains(&TxHash::repeat_byte(0))); - assert!(!deny.contains(&TxHash::repeat_byte(1))); - - // ...last 5 should hit. - for b in 95u8..100u8 { - assert!(deny.contains(&TxHash::repeat_byte(b))); - } - - assert_eq!(load(&HITS), 5, "only the last five are present"); - } - - #[test] - fn blocklist_storage_error_propagates() { - let result = is_address_blocklisted(Address::from([0xABu8; 20]), |_, _| { - Err(ProviderError::BlockHashNotFound(B256::ZERO)) - }); - assert!(result.is_err(), "storage error must propagate"); - } - - #[test] - fn blocklist_returns_false_when_storage_returns_none() { - let result = is_address_blocklisted(Address::from([0xABu8; 20]), |_, _| Ok(None)); - assert!(!result.unwrap(), "Ok(None) means not blocklisted"); - } - - #[test] - fn blocklist_returns_false_for_unblocklisted_status() { - let result = is_address_blocklisted(Address::from([0xABu8; 20]), |_, _| { - Ok(Some(UNBLOCKLISTED_STATUS)) - }); - assert!( - !result.unwrap(), - "UNBLOCKLISTED_STATUS means not blocklisted" - ); - } - - #[test] - fn blocklist_returns_true_for_blocklisted_status() { - let result = is_address_blocklisted(Address::from([0xABu8; 20]), |_, _| { - Ok(Some(BLOCKLISTED_STATUS)) - }); - assert!(result.unwrap(), "BLOCKLISTED_STATUS means blocklisted"); - } - - #[test] - fn blocklist_returns_true_for_other_nonzero_values() { - for status in [U256::from(2), U256::from(42), U256::MAX] { - let result = - is_address_blocklisted(Address::from([0xABu8; 20]), |_, _| Ok(Some(status))); - assert!( - result.unwrap(), - "non-zero value {status} should be treated as blocklisted" - ); - } - } -} diff --git a/crates/execution-validation/Cargo.toml b/crates/execution-validation/Cargo.toml deleted file mode 100644 index 9c1284a..0000000 --- a/crates/execution-validation/Cargo.toml +++ /dev/null @@ -1,41 +0,0 @@ -[package] -name = "arc-execution-validation" -version.workspace = true -edition.workspace = true -readme.workspace = true -license.workspace = true -exclude.workspace = true -rust-version.workspace = true -publish.workspace = true -repository.workspace = true - -[features] - -[dependencies] -# alloy -alloy-eips.workspace = true -alloy-primitives.workspace = true - -# internal -arc-execution-config.workspace = true - -# reth -reth-chainspec.workspace = true -reth-consensus.workspace = true -reth-consensus-common.workspace = true -reth-ethereum = { workspace = true, features = ["consensus", "provider"] } -reth-ethereum-primitives.workspace = true -reth-primitives-traits.workspace = true -reth-storage-api.workspace = true - -# misc -thiserror.workspace = true -tracing.workspace = true - -[dev-dependencies] -alloy-consensus = { workspace = true } -arc-execution-config = { path = "../execution-config", features = ["test-utils"] } -mockall.workspace = true - -[lints] -workspace = true diff --git a/crates/execution-validation/src/consensus.rs b/crates/execution-validation/src/consensus.rs deleted file mode 100644 index 9bafc95..0000000 --- a/crates/execution-validation/src/consensus.rs +++ /dev/null @@ -1,1098 +0,0 @@ -// Copyright 2025 Circle Internet Group, Inc. All rights reserved. -// -// SPDX-License-Identifier: Apache-2.0 -// -// Licensed under the Apache License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. -// You may obtain a copy of the License at -// -// http://www.apache.org/licenses/LICENSE-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// See the License for the specific language governing permissions and -// limitations under the License. - -use alloy_primitives::{Bloom, B256}; -use reth_chainspec::{EthChainSpec, EthereumHardforks, Hardforks}; -use reth_consensus::{Consensus, ConsensusError, FullConsensus, HeaderValidator}; -use reth_consensus_common::validation::{ - validate_against_parent_4844, validate_against_parent_hash_number, - validate_block_pre_execution, validate_body_against_header, validate_header_base_fee, - validate_header_gas, -}; -use reth_ethereum::primitives::{NodePrimitives, RecoveredBlock, SealedHeader}; -use reth_ethereum::provider::BlockExecutionResult; -use reth_primitives_traits::{Block, BlockHeader, GotExpected, SealedBlock}; -use std::sync::Arc; - -use arc_execution_config::chainspec::{BaseFeeConfigProvider, BlockGasLimitProvider}; -use arc_execution_config::gas_fee::decode_base_fee_from_bytes; -use arc_execution_config::hardforks::{is_arc_fork_active, ArcHardfork}; - -/// Arc Network custom consensus implementation -#[derive(Debug, Clone)] -pub struct ArcConsensus { - chain_spec: Arc, -} - -impl ArcConsensus { - pub const fn new(chain_spec: Arc) -> Self { - Self { chain_spec } - } -} - -// Implement HeaderValidator trait -impl HeaderValidator for ArcConsensus -where - ChainSpec: EthChainSpec - + EthereumHardforks - + Hardforks - + BlockGasLimitProvider - + BaseFeeConfigProvider, - H: BlockHeader, -{ - fn validate_header(&self, header: &SealedHeader) -> Result<(), ConsensusError> { - // Perform standard header validation (gas limits, etc.) - validate_header_gas(header.header())?; - - // Validate header timestamp (independent of parent) - arc_validate_header_timestamp(header.header())?; - - // ADR-0003: Validate gas limit is within chainspec bounds (Zero5+) - arc_validate_gas_limit_bounds(header.header(), &self.chain_spec)?; - - // ADR-0004: extra_data must be exactly 8 bytes (Zero5+). - arc_validate_extra_data_format(header.header(), &self.chain_spec)?; - - // ADR-0004: base_fee_per_gas must be present (EIP-1559) and within absolute bounds (Zero5+). - arc_validate_header_base_fee(header.header(), &self.chain_spec)?; - - // Reject blocks with a zero beneficiary (Zero6+). - arc_validate_beneficiary_nonzero(header.header(), &self.chain_spec)?; - - Ok(()) - } - - // Perform all standard validations - // reference implementation: https://github.com/paradigmxyz/reth/blob/v1.9.1/crates/ethereum/consensus/src/lib.rs#L170 - fn validate_header_against_parent( - &self, - header: &SealedHeader, - parent: &SealedHeader, - ) -> Result<(), ConsensusError> { - // 1. Validate hash and number consistency - validate_against_parent_hash_number(header.header(), parent)?; - - // 2. Validate timestamp progression - arc_validate_against_parent_timestamp(header.header(), parent.header())?; - - // 3. Validate base fee using Arc's algorithm - arc_validate_against_parent_base_fee(header.header(), parent.header(), &self.chain_spec)?; - - // 4. Validate blob gas fields if applicable (EIP-4844) - if let Some(blob_params) = self.chain_spec.blob_params_at_timestamp(header.timestamp()) { - validate_against_parent_4844(header.header(), parent.header(), blob_params)?; - } - - Ok(()) - } -} - -/// Validates the timestamp against the parent. -/// -/// NOTE(arc): Because Arc is expected to produce blocks at subsecond interval, -/// and because the resolution of the timestamp is in seconds, we must allow -/// the header's timestamp to be equal to its parent's timestamp. -#[inline] -pub fn arc_validate_against_parent_timestamp( - header: &H, - parent: &H, -) -> Result<(), ConsensusError> { - if header.timestamp() < parent.timestamp() { - return Err(ConsensusError::TimestampIsInPast { - parent_timestamp: parent.timestamp(), - timestamp: header.timestamp(), - }); - } - - Ok(()) -} - -/// Validates the base fee against the parent using Arc's custom EMA calculation. -/// -/// Decodes `nextBaseFee` from the parent's `extra_data` (written by the executor) and -/// requires the child's `base_fee_per_gas` to match exactly. If `extra_data` is not -/// exactly 8 bytes (e.g. pre-Zero4 blocks), validation is silently skipped. -/// -/// Validation is always skipped when the parent is genesis (block 0). -#[inline] -pub fn arc_validate_against_parent_base_fee( - header: &H, - parent: &H, - chain_spec: &ChainSpec, -) -> Result<(), ConsensusError> -where - ChainSpec: EthChainSpec + EthereumHardforks + Hardforks, - H: BlockHeader, -{ - // Skip validation if parent is genesis block - if parent.number() == 0 { - return Ok(()); - } - - // Decode the expected base fee from parent's extra_data - let Some(expected_base_fee) = decode_base_fee_from_bytes(parent.extra_data()) else { - // Post-Zero5 this branch should be unreachable: `arc_validate_extra_data_format` - // enforces that extra_data is exactly 8 bytes. - if is_arc_fork_active( - chain_spec, - ArcHardfork::Zero5, - parent.number(), - parent.timestamp(), - ) { - tracing::error!( - parent_number = parent.number(), - extra_data_len = parent.extra_data().len(), - "Unexpectedly skipped base fee validation" - ); - } - return Ok(()); - }; - - // Get the actual base fee from the current header - let actual_base_fee = header - .base_fee_per_gas() - .ok_or(ConsensusError::BaseFeeMissing)?; - - // Verify they match - if expected_base_fee != actual_base_fee { - return Err(ConsensusError::BaseFeeDiff(GotExpected { - got: actual_base_fee, - expected: expected_base_fee, - })); - } - - Ok(()) -} - -/// Validates that the header's `extra_data` is exactly 8 bytes -/// -/// Post-Zero5, the executor always writes `nextBaseFee` as an 8-byte big-endian u64. -/// Any other length is malformed and should be rejected. -#[inline] -fn arc_validate_extra_data_format( - header: &H, - chain_spec: &CS, -) -> Result<(), ConsensusError> { - if !is_arc_fork_active( - chain_spec, - ArcHardfork::Zero5, - header.number(), - header.timestamp(), - ) { - return Ok(()); - } - - let len = header.extra_data().len(); - if len != 8 { - return Err(ConsensusError::Other(format!( - "invalid extra_data length {len}: must be 8 bytes" - ))); - } - - Ok(()) -} - -/// Validates the header's `base_fee_per_gas`. -/// -/// Performs the standard EIP-1559 presence check (via Reth's `validate_header_base_fee`) and, -/// post-Zero5, additionally enforces that the value lies within the chainspec's absolute bounds -/// `[absolute_min_base_fee, absolute_max_base_fee]`. -#[inline] -fn arc_validate_header_base_fee< - H: BlockHeader, - CS: EthChainSpec + EthereumHardforks + Hardforks + BaseFeeConfigProvider, ->( - header: &H, - chain_spec: &CS, -) -> Result<(), ConsensusError> { - // Standard EIP-1559 base_fee_per_gas presence check. - validate_header_base_fee(header, chain_spec)?; - - // Post-Zero5: enforce absolute bounds. - if !is_arc_fork_active( - chain_spec, - ArcHardfork::Zero5, - header.number(), - header.timestamp(), - ) { - return Ok(()); - } - - let base_fee = match header.base_fee_per_gas() { - Some(fee) => fee, - None => return Ok(()), // already caught by validate_header_base_fee above - }; - - let config = chain_spec.base_fee_config(header.number()); - let clamped = base_fee.clamp(config.absolute_min_base_fee, config.absolute_max_base_fee); - if base_fee != clamped { - return Err(ConsensusError::BaseFeeDiff(GotExpected { - got: base_fee, - expected: clamped, - })); - } - - Ok(()) -} - -/// Validates that the header's gas limit is within the chainspec bounds. -/// -/// This validation is only active when the Zero5 hardfork is enabled. -#[inline] -fn arc_validate_gas_limit_bounds( - header: &H, - chain_spec: &CS, -) -> Result<(), ConsensusError> { - if !is_arc_fork_active( - chain_spec, - ArcHardfork::Zero5, - header.number(), - header.timestamp(), - ) { - return Ok(()); - } - - let gas_limit = header.gas_limit(); - let config = chain_spec.block_gas_limit_config(header.number()); - - if gas_limit < config.min() || gas_limit > config.max() { - return Err(ConsensusError::Other(format!( - "block gas limit {gas_limit} outside allowed bounds [{}, {}]", - config.min(), - config.max() - ))); - } - - Ok(()) -} - -/// Rejects blocks whose beneficiary (coinbase) is the zero address. -/// -/// Post-Zero6, every block must have an explicit non-zero fee recipient set by the CL -/// via `--suggested-fee-recipient`. A zero beneficiary would burn all transaction fees -/// irrecoverably. -#[inline] -fn arc_validate_beneficiary_nonzero( - header: &H, - chain_spec: &CS, -) -> Result<(), ConsensusError> { - if !is_arc_fork_active( - chain_spec, - ArcHardfork::Zero6, - header.number(), - header.timestamp(), - ) { - return Ok(()); - } - - if header.beneficiary().is_zero() { - return Err(ConsensusError::Other( - "block beneficiary must not be the zero address".into(), - )); - } - - Ok(()) -} - -/// The maximum allowed clock skew for Arc proposers, in seconds. -const ARC_PROPOSER_CLOCK_SKEW_THRESHOLD: u64 = 30; // 30 seconds - -/// Validates that the header's timestamp is not too far in the future -/// compared to the local system time. -#[inline] -fn arc_validate_header_timestamp(header: &H) -> Result<(), ConsensusError> { - // Get the current local time in seconds since UNIX EPOCH - let local_time = std::time::SystemTime::now() - .duration_since(std::time::UNIX_EPOCH) - .map_err(|_| ConsensusError::Other("System time is before UNIX EPOCH".to_string()))? - .as_secs(); - - arc_validate_header_timestamp_with_time(header, local_time) -} - -/// Validates that the header's timestamp is not too far in the future -/// compared to the local system time. -#[inline] -fn arc_validate_header_timestamp_with_time( - header: &H, - local_time: u64, -) -> Result<(), ConsensusError> { - // Validate that the header's timestamp is not too far in the future - if header.timestamp() > local_time.saturating_add(ARC_PROPOSER_CLOCK_SKEW_THRESHOLD) { - return Err(ConsensusError::TimestampIsInFuture { - timestamp: header.timestamp(), - present_timestamp: local_time, - }); - } - - Ok(()) -} - -// Implement Consensus trait -impl Consensus for ArcConsensus -where - ChainSpec: EthChainSpec - + EthereumHardforks - + Hardforks - + BlockGasLimitProvider - + BaseFeeConfigProvider, - B: Block, -{ - fn validate_body_against_header( - &self, - body: &B::Body, - header: &SealedHeader, - ) -> Result<(), ConsensusError> { - // Perform standard body validation (transaction root, etc.) - validate_body_against_header(body, header.header()) - } - - // Use the standard pre-execution validation from reth - fn validate_block_pre_execution(&self, block: &SealedBlock) -> Result<(), ConsensusError> { - validate_block_pre_execution(block, self.chain_spec.as_ref()) - } -} - -impl FullConsensus for ArcConsensus -where - ChainSpec: EthChainSpec - + EthereumHardforks - + Hardforks - + BlockGasLimitProvider - + BaseFeeConfigProvider, - N: NodePrimitives, -{ - fn validate_block_post_execution( - &self, - block: &RecoveredBlock, - receipts: &BlockExecutionResult, - receipt_root_bloom: Option<(B256, Bloom)>, - ) -> Result<(), ConsensusError> { - reth_ethereum::consensus::validate_block_post_execution( - block, - self.chain_spec.as_ref(), - &receipts.receipts, - &receipts.requests, - receipt_root_bloom, - ) - } -} - -#[cfg(test)] -mod tests { - use super::*; - use alloy_eips::eip7685::Requests; - use alloy_primitives::{Bloom, B256}; - use arc_execution_config::chainspec::{ - localdev_with_hardforks, ArcChainSpec, BlockGasLimitProvider, LOCAL_DEV, - }; - use arc_execution_config::gas_fee::encode_base_fee_to_bytes; - use reth_chainspec::{ChainSpecBuilder, ForkCondition}; - use reth_ethereum::primitives::Header; - use reth_ethereum_primitives::{EthPrimitives, Receipt}; - - #[test] - fn test_circle_consensus_creation() { - let chain_spec = Arc::new(ArcChainSpec::new(ChainSpecBuilder::mainnet().build())); - let _consensus = ArcConsensus::new(chain_spec); - } - - #[test] - fn test_consensus_validates_base_fee_with_ema() { - let chain_spec = LOCAL_DEV.clone(); - let consensus = ArcConsensus::new(chain_spec); - - let expected_base_fee = 160_000_000_000u64; - let parent_header = reth_ethereum::primitives::Header { - number: 10, // Use a non-genesis block number where Zero4 is active - timestamp: 1000, - base_fee_per_gas: Some(150_000_000_000), - extra_data: encode_base_fee_to_bytes(expected_base_fee), - blob_gas_used: Some(0), - excess_blob_gas: Some(0), - ..Default::default() - }; - - let sealed_parent = SealedHeader::new(parent_header.clone(), parent_header.hash_slow()); - - let child_header = reth_ethereum::primitives::Header { - number: 11, - timestamp: 1001, - parent_hash: sealed_parent.hash(), - base_fee_per_gas: Some(expected_base_fee), - blob_gas_used: Some(0), - excess_blob_gas: Some(0), - ..Default::default() - }; - - let sealed_child = SealedHeader::new(child_header.clone(), child_header.hash_slow()); - - let result = consensus.validate_header_against_parent(&sealed_child, &sealed_parent); - - assert!( - result.is_ok(), - "Should validate base fee using EMA from parent extra_data, but failed: {result:?}" - ); - } - - #[test] - fn test_equal_timestamp_is_valid() { - let chain_spec = Arc::new(ArcChainSpec::new(ChainSpecBuilder::mainnet().build())); - let consensus = ArcConsensus::new(chain_spec); - - let parent_header = reth_ethereum::primitives::Header { - timestamp: 1000, - number: 1, - ..Default::default() - }; - - let sealed_parent = SealedHeader::new(parent_header.clone(), B256::ZERO); - - let mut child_header = parent_header; - child_header.number += 1; - child_header.parent_hash = sealed_parent.hash(); - - // Set same timestamp - child_header.timestamp = 1000; - let sealed_child = SealedHeader::new(child_header, B256::ZERO); - - let result = consensus.validate_header_against_parent(&sealed_child, &sealed_parent); - - assert!( - result.is_ok(), - "Blocks with equal timestamps should be valid, but got: {result:?}" - ); - } - - #[test] - fn test_past_timestamp_is_invalid() { - let chain_spec = Arc::new(ArcChainSpec::new(ChainSpecBuilder::mainnet().build())); - let consensus = ArcConsensus::new(chain_spec); - - let parent_header = reth_ethereum::primitives::Header { - timestamp: 1000, - number: 1, - ..Default::default() - }; - - let sealed_parent = SealedHeader::new(parent_header.clone(), B256::ZERO); - - let mut child_header = parent_header; - child_header.number += 1; - child_header.parent_hash = sealed_parent.hash(); - - // Set past timestamp - child_header.timestamp = 999; - let sealed_child = SealedHeader::new(child_header, B256::ZERO); - - let result = consensus.validate_header_against_parent(&sealed_child, &sealed_parent); - - assert!(matches!( - result, - Err(ConsensusError::TimestampIsInPast { .. }) - )); - } - - #[test] - fn test_header_timestamp_just_within_skew_is_valid() { - let local_time = 1_730_887_500; // Some time in 2024 - let header = Header { - timestamp: local_time + ARC_PROPOSER_CLOCK_SKEW_THRESHOLD, - ..Header::default() - }; - - let result = arc_validate_header_timestamp_with_time(&header, local_time); - assert!( - result.is_ok(), - "Timestamp at the exact skew threshold should be valid" - ); - } - - #[test] - fn test_header_timestamp_in_past_is_valid() { - let local_time = 1_730_887_500; - let header = Header { - timestamp: local_time - 100, - ..Header::default() - }; - - let result = arc_validate_header_timestamp_with_time(&header, local_time); - assert!( - result.is_ok(), - "Timestamp in the past should be valid, as this check is only for future timestamps" - ); - } - - #[test] - fn test_header_timestamp_too_far_in_future_is_invalid() { - let local_time = 1_730_887_500; - let header = Header { - timestamp: local_time + ARC_PROPOSER_CLOCK_SKEW_THRESHOLD + 10, - ..Header::default() - }; - - let result = arc_validate_header_timestamp_with_time(&header, local_time); - assert!( - matches!( - result, - Err(ConsensusError::TimestampIsInFuture { - timestamp, - present_timestamp, - }) if timestamp == header.timestamp && present_timestamp == local_time - ), - "Timestamp beyond the skew threshold should be invalid" - ); - } - - #[test] - fn test_header_timestamp_at_current_time_is_valid() { - let local_time = 1_730_887_500; - let header = Header { - timestamp: local_time, - ..Header::default() - }; - - let result = arc_validate_header_timestamp_with_time(&header, local_time); - assert!( - result.is_ok(), - "Timestamp equal to local time should be valid" - ); - } - - #[test] - fn test_arc_base_fee_validation_with_matching_fees() { - let chain_spec = LOCAL_DEV.clone(); - let expected_base_fee = 160_000_000_000u64; - - let parent_header = reth_ethereum::primitives::Header { - number: 10, - timestamp: 1000, - base_fee_per_gas: Some(150_000_000_000), - extra_data: encode_base_fee_to_bytes(expected_base_fee), - ..Default::default() - }; - - let child_header = reth_ethereum::primitives::Header { - number: 11, - timestamp: 1001, - base_fee_per_gas: Some(expected_base_fee), - ..Default::default() - }; - - let result = - arc_validate_against_parent_base_fee(&child_header, &parent_header, &chain_spec); - assert!( - result.is_ok(), - "Base fee validation should succeed when fees match: {result:?}" - ); - } - - #[test] - fn test_arc_base_fee_validation_with_mismatched_fees() { - let chain_spec = LOCAL_DEV.clone(); - let expected_base_fee = 160_000_000_000u64; - - let parent_header = reth_ethereum::primitives::Header { - number: 10, - timestamp: 1000, - base_fee_per_gas: Some(150_000_000_000), - extra_data: encode_base_fee_to_bytes(expected_base_fee), - ..Default::default() - }; - - let child_header = reth_ethereum::primitives::Header { - number: 11, - timestamp: 1001, - base_fee_per_gas: Some(170_000_000_000), // Wrong - doesn't match encoded value - ..Default::default() - }; - - let result = - arc_validate_against_parent_base_fee(&child_header, &parent_header, &chain_spec); - assert!( - matches!(result, Err(ConsensusError::BaseFeeDiff(_))), - "Base fee validation should fail when fees mismatch: {result:?}" - ); - } - - #[test] - fn test_arc_base_fee_validation_skips_malformed_extra_data() { - use alloy_primitives::Bytes; - - let chain_spec = LOCAL_DEV.clone(); - - let parent_header = reth_ethereum::primitives::Header { - number: 10, - timestamp: 1000, - base_fee_per_gas: Some(150_000_000_000), - extra_data: Bytes::from_static(&[0xff, 0xff]), // Invalid: not 8 bytes - ..Default::default() - }; - - let child_header = reth_ethereum::primitives::Header { - number: 11, - timestamp: 1001, - base_fee_per_gas: Some(160_000_000_000), - ..Default::default() - }; - - let result = - arc_validate_against_parent_base_fee(&child_header, &parent_header, &chain_spec); - assert!( - result.is_ok(), - "Base fee validation should skip when extra_data is malformed: {result:?}" - ); - } - - #[test] - fn test_arc_base_fee_validation_missing_base_fee() { - let chain_spec = LOCAL_DEV.clone(); - - let parent_header = reth_ethereum::primitives::Header { - number: 10, - timestamp: 1000, - base_fee_per_gas: Some(150_000_000_000), - extra_data: encode_base_fee_to_bytes(160_000_000_000), - ..Default::default() - }; - - let child_header = reth_ethereum::primitives::Header { - number: 11, - timestamp: 1001, - base_fee_per_gas: None, // Missing base fee - ..Default::default() - }; - - let result = - arc_validate_against_parent_base_fee(&child_header, &parent_header, &chain_spec); - assert!( - matches!(result, Err(ConsensusError::BaseFeeMissing)), - "Base fee validation should fail when base_fee_per_gas is None: {result:?}" - ); - } - - #[test] - fn test_arc_base_fee_validation_skips_genesis_parent() { - let chain_spec = LOCAL_DEV.clone(); - - // Parent is genesis (block 0) - let parent_header = reth_ethereum::primitives::Header { - number: 0, - timestamp: 1000, - base_fee_per_gas: Some(1000000000), - extra_data: Default::default(), // Genesis has no encoded base fee - ..Default::default() - }; - - // Child is block 1 - let child_header = reth_ethereum::primitives::Header { - number: 1, - timestamp: 1001, - base_fee_per_gas: Some(1000000000), - ..Default::default() - }; - - // Should pass because parent is genesis block - let result = - arc_validate_against_parent_base_fee(&child_header, &parent_header, &chain_spec); - assert!( - result.is_ok(), - "Validation should skip when parent is genesis block: {result:?}" - ); - } - - /// Helper to call validate_block_post_execution and satisfy type constraints. - fn run_validate_block_post_execution( - consensus: &ArcConsensus, - block: &reth_primitives_traits::RecoveredBlock, - result: &BlockExecutionResult, - ) -> Result<(), ConsensusError> { - use reth_consensus::FullConsensus; - FullConsensus::::validate_block_post_execution( - consensus, block, result, None, - ) - } - - /// Builds a block and receipts for post-execution validation testing. - /// Pass `None` for override values to use the computed value from receipts. - fn build_post_execution_block( - receipt_gas: u64, - logs: Vec, - override_header_gas: Option, - override_receipts_root: Option, - override_logs_bloom: Option, - ) -> ( - reth_primitives_traits::RecoveredBlock, - Vec, - ) { - use alloy_consensus::{proofs::calculate_receipt_root, BlockBody, TxReceipt}; - use alloy_eips::eip7685::EMPTY_REQUESTS_HASH; - use reth_ethereum_primitives::{Block, Receipt, TransactionSigned}; - use reth_primitives_traits::RecoveredBlock; - - let receipts = vec![Receipt { - tx_type: alloy_consensus::TxType::Legacy, - success: true, - cumulative_gas_used: receipt_gas, - logs, - }]; - - let receipts_with_bloom: Vec<_> = receipts.iter().map(|r| r.with_bloom_ref()).collect(); - let computed_root = calculate_receipt_root(&receipts_with_bloom); - let computed_bloom = receipts_with_bloom - .iter() - .fold(Bloom::ZERO, |bloom, r| bloom | r.bloom_ref()); - - let header = reth_ethereum::primitives::Header { - number: 100, - timestamp: 1700000000, - gas_used: override_header_gas.unwrap_or(receipt_gas), - receipts_root: override_receipts_root.unwrap_or(computed_root), - logs_bloom: override_logs_bloom.unwrap_or(computed_bloom), - requests_hash: Some(EMPTY_REQUESTS_HASH), - ..Default::default() - }; - - let block = RecoveredBlock::new_unhashed( - Block::new( - header, - BlockBody:: { - transactions: vec![], - ommers: vec![], - withdrawals: None, - }, - ), - vec![], - ); - - (block, receipts) - } - - #[test] - fn test_post_execution_validation_success() { - use alloy_primitives::{Address, Log}; - - let log = Log::new_unchecked( - Address::repeat_byte(0x42), - vec![B256::repeat_byte(0x01)], - alloy_primitives::Bytes::from_static(b"test data"), - ); - let (block, receipts) = build_post_execution_block(50000, vec![log], None, None, None); - let consensus = ArcConsensus::new(LOCAL_DEV.clone()); - let execution_result = BlockExecutionResult { - receipts, - requests: Requests::default(), - gas_used: block.header().gas_used, - blob_gas_used: 0, - }; - let result = run_validate_block_post_execution(&consensus, &block, &execution_result); - assert!(result.is_ok(), "Expected success: {result:?}"); - } - - #[test] - fn test_post_execution_gas_used_mismatch() { - let actual_gas_used = 42000; - let header_gas_used = 26000; - - let (block, receipts) = - build_post_execution_block(actual_gas_used, vec![], Some(header_gas_used), None, None); - let consensus = ArcConsensus::new(LOCAL_DEV.clone()); - let execution_result = BlockExecutionResult { - receipts, - requests: Requests::default(), - gas_used: actual_gas_used, - blob_gas_used: 0, - }; - let result = run_validate_block_post_execution(&consensus, &block, &execution_result); - assert!( - matches!(result, Err(ConsensusError::BlockGasUsed { .. })), - "Expected BlockGasUsed error: {result:?}" - ); - } - - #[test] - fn test_post_execution_receipts_root_mismatch() { - // Override receipts root to an arbitrary value - let (block, receipts) = - build_post_execution_block(26000, vec![], None, Some(B256::repeat_byte(0xff)), None); - let consensus = ArcConsensus::new(LOCAL_DEV.clone()); - let execution_result = BlockExecutionResult { - receipts, - requests: Requests::default(), - gas_used: block.header().gas_used, - blob_gas_used: 0, - }; - let result = run_validate_block_post_execution(&consensus, &block, &execution_result); - assert!( - matches!(result, Err(ConsensusError::BodyReceiptRootDiff(_))), - "Expected BodyReceiptRootDiff error: {result:?}" - ); - } - - #[test] - fn test_post_execution_logs_bloom_mismatch() { - // Override logs bloom to an arbitrary value - let (block, receipts) = - build_post_execution_block(26000, vec![], None, None, Some(Bloom::repeat_byte(0xff))); - let consensus = ArcConsensus::new(LOCAL_DEV.clone()); - let execution_result = BlockExecutionResult { - receipts, - requests: Requests::default(), - gas_used: block.header().gas_used, - blob_gas_used: 0, - }; - let result = run_validate_block_post_execution(&consensus, &block, &execution_result); - assert!( - matches!(result, Err(ConsensusError::BodyBloomLogDiff(_))), - "Expected BodyBloomLogDiff error: {result:?}" - ); - } - - #[test] - fn test_gas_limit_within_bounds_is_valid() { - // LOCAL_DEV has Zero5 active at block 0 - let spec = LOCAL_DEV.clone(); - let config = spec.block_gas_limit_config(1); - - let header = Header { - number: 1, - gas_limit: config.default(), - timestamp: 0, - ..Default::default() - }; - let result = arc_validate_gas_limit_bounds(&header, spec.as_ref()); - assert!( - result.is_ok(), - "Gas limit within bounds should be valid: {result:?}" - ); - } - - #[test] - fn test_gas_limit_below_min_is_invalid() { - let spec = LOCAL_DEV.clone(); - let config = spec.block_gas_limit_config(1); - - let header = Header { - number: 1, - gas_limit: config.min() - 1, - timestamp: 0, - ..Default::default() - }; - let result = arc_validate_gas_limit_bounds(&header, spec.as_ref()); - assert!( - matches!(result, Err(ConsensusError::Other(_))), - "Gas limit below min should be invalid: {result:?}" - ); - } - - #[test] - fn test_gas_limit_above_max_is_invalid() { - let spec = LOCAL_DEV.clone(); - let config = spec.block_gas_limit_config(1); - - let header = Header { - number: 1, - gas_limit: config.max() + 1, - timestamp: 0, - ..Default::default() - }; - let result = arc_validate_gas_limit_bounds(&header, spec.as_ref()); - assert!( - matches!(result, Err(ConsensusError::Other(_))), - "Gas limit above max should be invalid: {result:?}" - ); - } - - #[test] - fn test_arc_validate_header_base_fee() { - // LOCAL_DEV: Zero5 active - let spec = LOCAL_DEV.clone(); - let config = spec.base_fee_config(1); - - // Within bounds - for fee in [ - config.absolute_min_base_fee, - 1_000_000_000u64, - config.absolute_max_base_fee, - ] { - let header = Header { - number: 1, - base_fee_per_gas: Some(fee), - ..Default::default() - }; - assert!( - arc_validate_header_base_fee(&header, spec.as_ref()).is_ok(), - "fee={fee}" - ); - } - - // Out of bounds - for fee in [ - config.absolute_min_base_fee - 1, - config.absolute_max_base_fee + 1, - ] { - let header = Header { - number: 1, - base_fee_per_gas: Some(fee), - ..Default::default() - }; - assert!(matches!( - arc_validate_header_base_fee(&header, spec.as_ref()), - Err(ConsensusError::BaseFeeDiff(_)) - )); - } - - // Pre-Zero5: bounds check is skipped entirely. - let pre_zero5 = localdev_with_hardforks(&[(ArcHardfork::Zero4, ForkCondition::Block(0))]); - let header = Header { - number: 1, - base_fee_per_gas: Some(config.absolute_min_base_fee - 1), - ..Default::default() - }; - assert!(arc_validate_header_base_fee(&header, pre_zero5.as_ref()).is_ok()); - } - - #[test] - fn test_arc_validate_extra_data_format() { - use alloy_primitives::Bytes; - - // Zero5 - let spec = LOCAL_DEV.clone(); - - let valid = Header { - number: 1, - extra_data: Bytes::from([0u8; 8].as_slice()), - ..Default::default() - }; - assert!(arc_validate_extra_data_format(&valid, spec.as_ref()).is_ok()); - - let too_short = Header { - number: 1, - extra_data: Bytes::from([0u8; 7].as_slice()), - ..Default::default() - }; - assert!(matches!( - arc_validate_extra_data_format(&too_short, spec.as_ref()), - Err(ConsensusError::Other(_)) - )); - - let too_long = Header { - number: 1, - extra_data: Bytes::from([0u8; 9].as_slice()), - ..Default::default() - }; - assert!(matches!( - arc_validate_extra_data_format(&too_long, spec.as_ref()), - Err(ConsensusError::Other(_)) - )); - - // Pre-Zero5: length check is skipped entirely. - let pre_zero5 = localdev_with_hardforks(&[(ArcHardfork::Zero4, ForkCondition::Block(0))]); - let header = Header { - number: 1, - extra_data: Bytes::from([0u8; 7].as_slice()), - ..Default::default() - }; - assert!(arc_validate_extra_data_format(&header, pre_zero5.as_ref()).is_ok()); - } - - #[test] - fn test_gas_limit_validation_skipped_before_zero5() { - // Create a chain spec where Zero5 activates at block 100 - let mut inner = ChainSpecBuilder::mainnet().build(); - inner - .hardforks - .insert(ArcHardfork::Zero5, ForkCondition::Block(100)); - let spec = Arc::new(ArcChainSpec::new(inner)); - - // Before Zero5 — any gas limit should pass - let header = Header { - number: 99, - gas_limit: 0, // would be invalid after Zero5 - timestamp: 0, - ..Default::default() - }; - let result = arc_validate_gas_limit_bounds(&header, spec.as_ref()); - assert!( - result.is_ok(), - "Before Zero5, gas limit validation should be skipped: {result:?}" - ); - - // At Zero5 — now enforced - let header = Header { - number: 100, - gas_limit: 0, - timestamp: 0, - ..Default::default() - }; - let result = arc_validate_gas_limit_bounds(&header, spec.as_ref()); - assert!( - matches!(result, Err(ConsensusError::Other(_))), - "At Zero5, gas limit 0 should be invalid: {result:?}" - ); - } - - #[test] - fn test_beneficiary_nonzero_rejected_at_zero6() { - use alloy_primitives::{address, Address}; - - let spec = LOCAL_DEV.clone(); - - let zero_beneficiary = Header { - number: 1, - beneficiary: Address::ZERO, - ..Default::default() - }; - let result = arc_validate_beneficiary_nonzero(&zero_beneficiary, spec.as_ref()); - assert!( - matches!(result, Err(ConsensusError::Other(_))), - "Zero beneficiary should be rejected post-Zero6: {result:?}" - ); - - let nonzero_beneficiary = Header { - number: 1, - beneficiary: address!("0x65E0a200006D4FF91bD59F9694220dafc49dbBC1"), - ..Default::default() - }; - let result = arc_validate_beneficiary_nonzero(&nonzero_beneficiary, spec.as_ref()); - assert!( - result.is_ok(), - "Non-zero beneficiary should pass: {result:?}" - ); - } - - #[test] - fn test_beneficiary_nonzero_skipped_before_zero6() { - use alloy_primitives::Address; - - let mut inner = ChainSpecBuilder::mainnet().build(); - inner - .hardforks - .insert(ArcHardfork::Zero6, ForkCondition::Block(100)); - let spec = Arc::new(ArcChainSpec::new(inner)); - - let header = Header { - number: 99, - beneficiary: Address::ZERO, - ..Default::default() - }; - let result = arc_validate_beneficiary_nonzero(&header, spec.as_ref()); - assert!( - result.is_ok(), - "Before Zero6, zero beneficiary should be allowed: {result:?}" - ); - } -} diff --git a/crates/execution-validation/src/denylist.rs b/crates/execution-validation/src/denylist.rs deleted file mode 100644 index 42d16f5..0000000 --- a/crates/execution-validation/src/denylist.rs +++ /dev/null @@ -1,192 +0,0 @@ -// Copyright 2026 Circle Internet Group, Inc. All rights reserved. -// -// SPDX-License-Identifier: Apache-2.0 -// -// Licensed under the Apache License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. -// You may obtain a copy of the License at -// -// http://www.apache.org/licenses/LICENSE-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// See the License for the specific language governing permissions and -// limitations under the License. - -//! Addresses denylist checker: answers "is this address denylisted?" by reading the Denylist -//! contract storage. Call from both mempool and execution/Revm. -//! Uses ERC-7201 storage layout matching the Denylist contract. Provider/SLOAD -//! errors are propagated; callers may treat `Err` as fail-open (do not block) if desired. -//! -//! Config lives in [`arc_execution_config::addresses_denylist`]; this module only implements the check. - -use alloy_primitives::{Address, B256}; -use arc_execution_config::addresses_denylist::{ - compute_denylist_storage_slot, AddressesDenylistConfig, -}; -use reth_storage_api::errors::provider::ProviderError; -use reth_storage_api::StateProvider; -use tracing::trace; - -/// Error types for denylist storage lookups -#[derive(Debug, thiserror::Error)] -pub enum DenylistError { - #[error("Storage read failed: {0}")] - StorageReadFailed(#[from] ProviderError), -} - -/// Abstraction for reading a single storage slot. Implemented for [`StateProvider`]. -#[cfg_attr(test, mockall::automock)] -pub trait DenylistStorageReader { - /// Read storage at (address, slot). Returns Ok(Some(value)) if non-empty, Ok(None) if empty, - /// Err on provider/account failure. - fn read_storage( - &self, - address: Address, - slot: alloy_primitives::StorageKey, - ) -> Result, DenylistError>; -} - -impl DenylistStorageReader for T { - fn read_storage( - &self, - address: Address, - slot: alloy_primitives::StorageKey, - ) -> Result, DenylistError> { - self.storage(address, slot) - .map(|opt| opt.map(|v| B256::from(v.to_be_bytes::<32>()))) - .map_err(DenylistError::StorageReadFailed) - } -} - -/// Returns whether `address` is denylisted at the given state. -/// -/// Call from mempool validation with a state provider for the relevant block. -/// -/// - Returns `Ok(false)` if config has denylist disabled, or `address` is in -/// `addresses_exclusions`. -/// - Otherwise performs one SLOAD at `(contract_address, slot)` and returns -/// `Ok(true)` iff the value is non-zero (denylisted). -/// - On provider/SLOAD error returns `Err(DenylistError)`; callers may treat as fail-open (do not block). -#[inline] -#[must_use = "ignoring denylist check result could allow denylisted addresses to transact"] -pub fn is_denylisted( - provider: &P, - config: &AddressesDenylistConfig, - address: Address, -) -> Result { - let AddressesDenylistConfig::Enabled { - contract_address, - storage_slot, - .. - } = config - else { - return Ok(false); - }; - - if config.is_address_excluded(&address) { - trace!(%address, "address is explicitly excluded from denylist"); - return Ok(false); - } - - let storage_key = - alloy_primitives::StorageKey::from(compute_denylist_storage_slot(address, *storage_slot)); - let value = provider.read_storage(*contract_address, storage_key)?; - - // Non-zero means denylisted (bool true in Solidity). - // None (uninitialized storage) equals zero (not denylisted per Solidity semantics). - Ok(!value.unwrap_or_default().is_zero()) -} - -#[cfg(test)] -mod tests { - use super::*; - use arc_execution_config::addresses_denylist::DEFAULT_DENYLIST_ERC7201_BASE_SLOT; - - fn denylisted_slot_value() -> B256 { - let mut one = [0u8; 32]; - one[31] = 1; - B256::new(one) - } - - #[test] - fn is_denylisted_returns_false_when_disabled() { - let config = AddressesDenylistConfig::try_new( - false, - Some(Address::ZERO), - Some(DEFAULT_DENYLIST_ERC7201_BASE_SLOT), - Vec::new(), - ) - .unwrap(); - let mock = MockDenylistStorageReader::new(); - assert!(!is_denylisted(&mock, &config, Address::from([1u8; 20])).unwrap()); - } - - #[test] - fn is_denylisted_returns_false_when_address_excluded() { - let addr = Address::from([1u8; 20]); - let config = AddressesDenylistConfig::try_new( - true, - Some(Address::from([0x36u8; 20])), - Some(DEFAULT_DENYLIST_ERC7201_BASE_SLOT), - vec![addr], - ) - .unwrap(); - let mock = MockDenylistStorageReader::new(); - assert!(!is_denylisted(&mock, &config, addr).unwrap()); - } - - #[test] - fn is_denylisted_returns_true_when_storage_non_zero() { - let addr = Address::from([1u8; 20]); - let contract = Address::from([0x36u8; 20]); - let config = AddressesDenylistConfig::try_new( - true, - Some(contract), - Some(DEFAULT_DENYLIST_ERC7201_BASE_SLOT), - Vec::new(), - ) - .unwrap(); - let mut mock = MockDenylistStorageReader::new(); - mock.expect_read_storage() - .returning(|_addr, _slot| Ok(Some(denylisted_slot_value()))); - assert!(is_denylisted(&mock, &config, addr).unwrap()); - } - - #[test] - fn is_denylisted_returns_false_when_storage_zero() { - let addr = Address::from([1u8; 20]); - let contract = Address::from([0x36u8; 20]); - let config = AddressesDenylistConfig::try_new( - true, - Some(contract), - Some(DEFAULT_DENYLIST_ERC7201_BASE_SLOT), - Vec::new(), - ) - .unwrap(); - let mut mock = MockDenylistStorageReader::new(); - mock.expect_read_storage() - .returning(|_addr, _slot| Ok(None)); - assert!(!is_denylisted(&mock, &config, addr).unwrap()); - } - - #[test] - fn is_denylisted_returns_err_on_provider_error() { - let config = AddressesDenylistConfig::try_new( - true, - Some(Address::from([0x36u8; 20])), - Some(DEFAULT_DENYLIST_ERC7201_BASE_SLOT), - Vec::new(), - ) - .unwrap(); - let mut mock = MockDenylistStorageReader::new(); - mock.expect_read_storage().returning(|_addr, _slot| { - Err(DenylistError::StorageReadFailed( - reth_storage_api::errors::provider::ProviderError::BlockHashNotFound(B256::ZERO), - )) - }); - let res = is_denylisted(&mock, &config, Address::from([1u8; 20])); - assert!(res.is_err(), "provider error must be propagated"); - } -} diff --git a/crates/execution-validation/src/lib.rs b/crates/execution-validation/src/lib.rs deleted file mode 100644 index 1b4cc0c..0000000 --- a/crates/execution-validation/src/lib.rs +++ /dev/null @@ -1,21 +0,0 @@ -// Copyright 2026 Circle Internet Group, Inc. All rights reserved. -// -// SPDX-License-Identifier: Apache-2.0 -// -// Licensed under the Apache License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. -// You may obtain a copy of the License at -// -// http://www.apache.org/licenses/LICENSE-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// See the License for the specific language governing permissions and -// limitations under the License. - -mod consensus; -pub mod denylist; - -pub use consensus::ArcConsensus; -pub use denylist::{is_denylisted, DenylistError, DenylistStorageReader}; diff --git a/crates/malachite-app/Cargo.toml b/crates/malachite-app/Cargo.toml deleted file mode 100644 index a827a4c..0000000 --- a/crates/malachite-app/Cargo.toml +++ /dev/null @@ -1,95 +0,0 @@ -[package] -name = "arc-node-consensus" -version.workspace = true -edition.workspace = true -repository.workspace = true -license.workspace = true -rust-version.workspace = true -publish.workspace = true - -[features] -pprof = ["dep:pprof_hyper_server"] - -# allocator -[target.'cfg(not(target_env = "msvc"))'.dependencies] -tikv-jemalloc-ctl = { version = "0.6", features = ["stats"] } -tikv-jemallocator = "0.6" - -[target.'cfg(target_os = "linux")'.dependencies] -procfs = "0.17.0" - -[dependencies] -alloy-consensus = { workspace = true } -alloy-primitives = { workspace = true } -alloy-provider = { workspace = true, features = ["ws"] } -alloy-rlp = { workspace = true } -alloy-rpc-types-engine = { workspace = true, features = ["ssz"] } -alloy-rpc-types-eth = { workspace = true } -alloy-transport-ws.workspace = true -arc-consensus-db.workspace = true -arc-consensus-types.workspace = true -arc-eth-engine.workspace = true -arc-node-consensus-cli.workspace = true -arc-shared.workspace = true -arc-signer.workspace = true -arc-version.workspace = true -atomic-time.workspace = true -axum = { workspace = true, features = ["macros"] } -backon.workspace = true -base64.workspace = true -bon.workspace = true -bytes.workspace = true -bytesize.workspace = true -chrono = { workspace = true, features = ["serde"] } -ethereum_ssz = { workspace = true } -eyre.workspace = true -futures.workspace = true -hex.workspace = true -humantime.workspace = true -itertools.workspace = true -malachitebft-app-channel.workspace = true -malachitebft-core-state-machine.workspace = true -malachitebft-core-types.workspace = true -malachitebft-network.workspace = true -malachitebft-peer.workspace = true -malachitebft-sync.workspace = true -multihash.workspace = true -oneline-eyre.workspace = true -pprof_hyper_server = { version = "0.2", features = ["pprof"], optional = true } -ractor.workspace = true -rand.workspace = true -redb.workspace = true -reqwest = { workspace = true, features = ["json"] } -schnellru.workspace = true -serde.workspace = true -serde_json.workspace = true -serde_with = { workspace = true, features = ["macros", "base64"] } -sha3.workspace = true -thiserror.workspace = true -tokio.workspace = true -tokio-tungstenite.workspace = true -tokio-util.workspace = true -tracing.workspace = true -url.workspace = true - -[dev-dependencies] -alloy-primitives = { workspace = true } -alloy-rpc-types-engine = { workspace = true, features = ["arbitrary"] } -arbitrary = { workspace = true } -arc-consensus-db = { workspace = true, features = ["mock"] } -arc-consensus-types = { workspace = true, features = ["arbitrary"] } -arc-eth-engine = { workspace = true, features = ["mocks"] } -assert_cmd = "2.0" -malachitebft-network = { workspace = true } -malachitebft-peer = { workspace = true, features = ["rand"] } -mockall = { workspace = true } -predicates = "3.0" -proptest = { workspace = true } -reqwest = { workspace = true, features = ["json"] } -serial_test = { workspace = true } -tempfile = { workspace = true } -tokio = { workspace = true, features = ["test-util"] } -tower = { workspace = true } - -[lints] -workspace = true diff --git a/crates/malachite-app/METRICS.md b/crates/malachite-app/METRICS.md deleted file mode 100644 index b73e0d4..0000000 --- a/crates/malachite-app/METRICS.md +++ /dev/null @@ -1,196 +0,0 @@ -# Malachite App Metrics - -This application exposes Prometheus metrics on the `/metrics` endpoint. The following is a list of key metrics and their descriptions. - -## Metrics - -### Application Metrics - -- **`arc_malachite_app_block_time`** (Histogram) - - **Description:** Interval between two blocks, in seconds. - - **Buckets:** Provides a histogram of block intervals with exponential buckets from 0.01 to 2.0 seconds. - -- **`arc_malachite_app_block_finalize_time`** (Histogram) - - **Description:** Time taken to finalize a block, in seconds. - - **Buckets:** Provides a histogram of block finalization times with exponential buckets from 0.01 to 2.0 seconds. - -- **`arc_malachite_app_block_build_time`** (Histogram) - - **Description:** Time taken by the proposer to build a block, in seconds. - - **Buckets:** Provides a histogram of block build times with exponential buckets from 0.01 to 2.0 seconds. - -- **`arc_malachite_app_block_transactions_count`** (Histogram) - - **Description:** Number of transactions in each finalized block. - - **Buckets:** Exponential buckets: 1, 2, 4, .. , 8192, 16384 (16K) transactions. - -- **`arc_malachite_app_block_size_bytes`** (Histogram) - - **Description:** Size of each finalized block in bytes. - - **Buckets:** Exponential buckets: 1KB, 2KB, 4KB, .. , 512MB, 1GB. - -- **`arc_malachite_app_block_gas_used`** (Histogram) - - **Description:** Gas used in each finalized block. - - **Buckets:** Exponential buckets: 1K, 2K, 4K, .. , 16M, 32M gas. - -- **`arc_malachite_app_total_transactions_count`** (Counter) - - **Description:** Total number of transactions finalized since the application started. - -- **`arc_malachite_app_total_chain_bytes`** (Counter) - - **Description:** Total size of all finalized block payloads in bytes since the application started. - -- **`arc_malachite_app_validators_count`** (Gauge) - - **Description:** Number of validators in the current validator set. - -- **`arc_malachite_app_validators_total_voting_power`** (Gauge) - - **Description:** Total voting power of the current validator set. - -- **`arc_malachite_app_validator_voting_power`** (Gauge) - - **Description:** Voting power of each validator. - - **Labels:** - - `address`: The validator's address. - -- **`arc_malachite_app_consensus_params`** (Gauge) - - **Description:** Current consensus parameters. - - **Labels:** - - `timeout_propose`: Timeout for proposing a block (seconds) - - `timeout_propose_delta`: By which amount the timeout for proposing a block can be increased (seconds) - - `timeout_prevote`: Timeout for voting on a block (seconds) - - `timeout_prevote_delta`: By which amount the timeout for voting on a block can be increased (seconds) - - `timeout_precommit`: Timeout for committing a block (seconds) - - `timeout_precommit_delta`: By which amount the timeout for committing a block can be increased (seconds) - - `timeout_rebroadcast`: Timeout for rebroadcasting a block (seconds) - - `target_block_time`: The target block time, or `0` if no target block time is set (seconds) - -- **`arc_malachite_app_msg_process_time`** (Histogram) - - **Description:** Time taken to process a message, in seconds. - - **Labels:** - - `msg`: The type of message being processed. - - **Buckets:** Provides a histogram of message processing times with exponential buckets from 0.01 to 2.0 seconds. - -- **`arc_malachite_app_engine_api_time`** (Histogram) - - **Description:** Time taken for each Engine API call, in seconds. - - **Labels:** - - `api`: The Engine API method name. - - **Buckets:** Provides a histogram of API call times with exponential buckets from 0.001 to 2.0 seconds. - -- **`arc_malachite_app_height_restart_count`** (Counter) - - **Description:** Number of times the consensus height has been restarted due to errors or recovery scenarios. - -- **`arc_malachite_app_sync_fell_behind_count`** (Counter) - - **Description:** Number of times the node fell behind and transitioned from InSync to CatchingUp. - -- **`arc_malachite_app_pending_proposal_parts_count`** (Gauge) - - **Description:** Number of pending proposal parts waiting to be processed at a future height or round. - -- **`arc_malachite_app_version_info`** (Info) - - **Description:** Version information for the consensus layer. - - **Labels:** - - `version`: Version string in format "v0.2.0-rc1 (3ecc9383)" combining git tag and short commit hash. - - `git_commit`: Full git commit hash (40 characters). - -### Database Metrics - -- **`arc_malachite_app_db_size`** (Gauge) - - **Description:** Size of the database in bytes. - -- **`arc_malachite_app_db_write_bytes`** (Counter) - - **Description:** Amount of data written to the database in bytes. - -- **`arc_malachite_app_db_read_bytes`** (Counter) - - **Description:** Amount of data read from the database in bytes. - -- **`arc_malachite_app_db_key_read_bytes`** (Counter) - - **Description:** Amount of key data read from the database in bytes. - -- **`arc_malachite_app_db_read_count`** (Counter) - - **Description:** Total number of reads from the database. - -- **`arc_malachite_app_db_write_count`** (Counter) - - **Description:** Total number of writes to the database. - -- **`arc_malachite_app_db_delete_count`** (Counter) - - **Description:** Total number of deletions from the database. - -- **`arc_malachite_app_db_read_time`** (Histogram) - - **Description:** Time taken to read bytes from the database in seconds. - - **Buckets:** Provides a histogram of read times with exponential buckets from 0.001 to 2.0 seconds. - -- **`arc_malachite_app_db_write_time`** (Histogram) - - **Description:** Time taken to write bytes to the database in seconds. - - **Buckets:** Provides a histogram of write times with exponential buckets from 0.001 to 2.0 seconds. - -- **`arc_malachite_app_db_delete_time`** (Histogram) - - **Description:** Time taken to delete bytes from the database in seconds. - - **Buckets:** Provides a histogram of delete times with exponential buckets from 0.001 to 2.0 seconds. - -### Remote Signer Metrics - -- **`arc_remote_signer_sign_requests_count`** (Counter) - - **Description:** Total number of sign requests received. -- **`arc_remote_signer_sign_request_errors`** (Counter) - - **Description:** Total number of sign request errors. -- **`arc_remote_signer_sign_request_retries`** (Counter) - - **Description:** Total number of sign request retries. -- **`arc_remote_signer_sign_request_latency_total`** (Histogram) - - **Description:** Latency of sign requests in seconds (including retries). - - **Buckets:** Provides a histogram of latency distributions with exponential buckets from 1ms to 10s. -- **`arc_remote_signer_sign_request_latency_single`** (Histogram) - - **Description:** Latency of sign requests in seconds (excluding retries). - - **Buckets:** Provides a histogram of latency distributions with exponential buckets from 1ms to 10s. - - -### Jemalloc Memory Metrics (Unix systems) - -- **`arc_malachite_app_jemalloc_active`** (Gauge) - - **Description:** Total number of bytes in active pages allocated by the application. - -- **`arc_malachite_app_jemalloc_allocated`** (Gauge) - - **Description:** Total number of bytes allocated by the application. - -- **`arc_malachite_app_jemalloc_mapped`** (Gauge) - - **Description:** Total number of bytes in active extents mapped by the allocator. - -- **`arc_malachite_app_jemalloc_metadata`** (Gauge) - - **Description:** Total number of bytes dedicated to jemalloc metadata. - -- **`arc_malachite_app_jemalloc_resident`** (Gauge) - - **Description:** Total number of bytes in physically resident data pages mapped by the allocator. - -- **`arc_malachite_app_jemalloc_retained`** (Gauge) - - **Description:** Total number of bytes in virtual memory mappings that were retained rather than being returned to the operating system. - -### IO And Stats Metrics (Linux only) - -- **`arc_malachite_app_io_rchar`** (Gauge) - - **Description:** Characters read. - -- **`arc_malachite_app_io_wchar`** (Gauge) - - **Description:** Characters written. - -- **`arc_malachite_app_io_syscr`** (Gauge) - - **Description:** Read syscalls. - -- **`arc_malachite_app_io_syscw`** (Gauge) - - **Description:** Write syscalls. - -- **`arc_malachite_app_io_read_bytes`** (Gauge) - - **Description:** Bytes read. - -- **`arc_malachite_app_io_write_bytes`** (Gauge) - - **Description:** Bytes written. - -- **`arc_malachite_app_io_cancelled_write_bytes`** (Gauge) - - **Description:** Cancelled write bytes. - -- **`arc_malachite_app_process_cpu_seconds_total`** (Gauge) - - **Description:** Total user and system CPU time spent in seconds. - -- **`arc_malachite_app_process_open_fds`** (Gauge) - - **Description:** Number of open file descriptors. - -- **`arc_malachite_app_process_threads`** (Gauge) - - **Description:** Number of OS threads in the process. - -## Feature Requirements - -### Platform Support -- **Jemalloc Metrics**: Available on Unix systems -- **IO And Stats Metrics**: Available on Linux only diff --git a/crates/malachite-app/README.md b/crates/malachite-app/README.md deleted file mode 100644 index 5198969..0000000 --- a/crates/malachite-app/README.md +++ /dev/null @@ -1,374 +0,0 @@ -# arc-node-consensus - -This is a [Malachite][malachite] application that uses a channels-based API, developed for the Arc network. - -It serves as a shim layer (proxy) between the execution client (EL), such as [reth][reth], and the consensus client (CL), Malachite. Communication with the EL is handled via the [Engine API][engine-api]. - -## Table of Contents - -- [Usage](#usage) - - [Init](#init) - - [Start](#start) - - [Validator](#a-validator) - - [Full node](#b-full-node) - - [Follow mode (RPC sync)](#c-follow-mode-rpc-sync) - - [Optional Flags](#optional-flags) - - [Remote Signing](#remote-signing) - - [Download](#download) - - [Key](#key) - - [DB](#db) - - [Migrate (Upgrade)](#migrate-upgrade) - - [Compact](#compact) -- [Environment Variables](#environment-variables) -- [REST API](#rest-api) - - [API Versioning](#api-versioning) - - [Available Endpoints](#available-endpoints) - - [Example API Usage](#example-api-usage) - - [Deprecation Policy](#deprecation-policy) -- [Metrics](#metrics) - -## Usage - -### Init - -```bash -arc-node-consensus init --home=~/.arc/consensus -``` - -Generates the private validator key file in `~/.arc/consensus/config/priv_validator_key.json`. - -Use `--overwrite` to regenerate the key if it already exists: - -```bash -arc-node-consensus init --home=~/.arc/consensus --overwrite -``` - -> The private validator key file contains the private key for the libp2p network identity. -> -> Moreover, when `--signing.remote` is disabled, this private key is also used for signing consensus messages and therefore constitutes the consensus identity from which the validator's address is derived. - -### Start - -#### a) Validator - -A validator is a node that participates in consensus, proposes and votes on blocks, and is responsible for finalizing the blockchain. - - -**Minimal example**: - -```bash -arc-node-consensus start \ - --home=~/.arc/consensus \ - --moniker=validator-1 \ - --validator \ - --suggested-fee-recipient=0xYourAddressHere \ - --eth-socket=/tmp/reth.ipc \ - --execution-socket=/tmp/auth.ipc \ - --minimal -``` - -**Full example with IPC** (recommended for colocated reth and malachite): - -```bash -arc-node-consensus start \ - --home=~/.arc/consensus \ - --moniker=validator-1 \ - --validator \ - --suggested-fee-recipient=0xYourAddressHere \ - --p2p.addr=/ip4/172.19.0.5/tcp/27000 \ - --p2p.persistent-peers=/ip4/172.19.0.6/tcp/27000,/ip4/172.19.0.7/tcp/27000 \ - --metrics=172.19.0.5:29000 \ - --rpc.addr=0.0.0.0:31000 \ - --eth-socket=/tmp/reth.ipc \ - --execution-socket=/tmp/auth.ipc \ - --minimal -``` - -**Full example with RPC** (for remote deployments): - -```bash -arc-node-consensus start \ - --home=~/.arc/consensus \ - --moniker=validator-1 \ - --validator \ - --suggested-fee-recipient=0xYourAddressHere \ - --p2p.addr=/ip4/172.19.0.5/tcp/27000 \ - --p2p.persistent-peers=/ip4/172.19.0.6/tcp/27000,/ip4/172.19.0.7/tcp/27000 \ - --metrics=0.0.0.0:29000 \ - --rpc.addr=0.0.0.0:31000 \ - --eth-rpc-endpoint=http://localhost:8545 \ - --execution-endpoint=http://localhost:8551 \ - --execution-jwt=jwtsecret \ - --minimal -``` - -Note: to generate a JWT (JSON web token), use the following command: - -```bash -openssl rand -hex 32 | tr -d "\n" > "jwtsecret" -``` - -#### b) Full node - -A full node participates in block and transaction propagation, verifies consensus rules, but does **not** propose or vote on blocks. It keeps up with consensus and validates all blocks/txs, ensuring a full copy of network state, but doesn't require a validator private key. - -```bash -arc-node-consensus start \ - --home=~/.arc/consensus \ - --moniker=full-1 \ - --eth-socket=/tmp/reth.ipc \ - --execution-socket=/tmp/auth.ipc \ - --full -``` - -To run as a sync-only node that does not subscribe to consensus gossip topics, pass `--no-consensus`. - -#### c) Follow mode (RPC sync) - -Follow mode syncs blocks from trusted RPC endpoints instead of participating in P2P consensus. The node fetches blocks via HTTP, verifies commit certificates, and applies them locally. This is useful for read-only nodes that sync from validators without joining the P2P network. - -Follow mode implies `--no-consensus` automatically. - -```bash -arc-node-consensus start \ - --home=~/.arc/consensus \ - --moniker=follower-1 \ - --eth-socket=/tmp/reth.ipc \ - --execution-socket=/tmp/auth.ipc \ - --follow \ - --follow.endpoint http://validator1:26658 \ - --follow.endpoint http://validator2:26658 \ - --full -``` - -Multiple `--follow.endpoint` flags can be provided for redundancy. The endpoint format supports an optional WebSocket override for streaming: - -``` -http://validator1:26658,ws=8546 -https://example.com,wss=ws.example.com:1212 -``` - -#### Optional Flags - -- `--moniker` - Human-readable name for this node (if not provided, a random moniker like "brave-validator-742" will be generated) -- `--p2p.addr` - P2P listen multiaddr (default: `/ip4/0.0.0.0/tcp/27000`). Example: `/ip4/172.19.0.5/tcp/27000` or `/ip4/127.0.0.1/udp/27000/quic-v1` - -- `--p2p.persistent-peers` - Comma-separated list of persistent peer multiaddrs -- `--p2p.persistent-peers-only` - Only allow connections to/from persistent peers (default: false). Useful for sentry node setups where a validator should only communicate with known trusted peers. -- `--validator` - Run as a validator: load the consensus signing key, sign the validator proof, and advertise a validator identity. Without this flag the node runs as a full node (no signing, ephemeral consensus key). Mutually exclusive with `--no-consensus` and `--follow`. Requires `--suggested-fee-recipient`. -- `--no-consensus` - Run as a sync-only node that does not subscribe to consensus gossip topics. Mutually exclusive with `--validator`. -- `--discovery` - Enable peer discovery (default: false) -- `--discovery.num-outbound-peers` - Number of outbound peers (default: 20) -- `--discovery.num-inbound-peers` - Number of inbound peers (default: 20) -- `--value-sync` - Enable value sync (default: true) -- `--metrics` - Enable metrics and set listen address (e.g., "0.0.0.0:29000") -- `--rpc.addr` - Enable RPC and set listen address (e.g., "0.0.0.0:31000") -- `--full` - Arc full-node pruning preset; sets `--prune.certificates.distance 237600`; mutually exclusive with `--minimal` and the individual `--prune.certificates.*` flags -- `--minimal` - Arc minimal-storage pruning preset; sets `--prune.certificates.distance 237600`; mutually exclusive with `--full` and the individual `--prune.certificates.*` flags -- `--prune.certificates.distance` - Keep certificates for the last N heights (default: 0, disabled/archive node); mutually exclusive with `--prune.certificates.before` and `--full/--minimal` presets -- `--prune.certificates.before` - Prune all certificates below this height (default: 0, disabled); mutually exclusive with `--prune.certificates.distance` and `--full/--minimal` presets -- `--log-level` - Log level: "trace", "debug", "info", "warn", "error" (default: "debug") -- `--log-format` - Log format: "plaintext" or "json" (default: "plaintext") -- `--pprof.addr` - Profiling server bind address (default: "0.0.0.0:6060") -- `--suggested-fee-recipient
` - 20-byte address to receive tips and rewards. Required when `--validator` is set. -- `--follow` - Enable RPC sync mode. The node fetches blocks from trusted RPC endpoints instead of participating in consensus (requires `--follow.endpoint`) -- `--follow.endpoint ` - RPC endpoint to fetch blocks from in sync mode. Can be repeated. Format: `http://host:port[,ws=port]` (requires `--follow`) -- `--runtime.flavor` - Tokio runtime flavor: "single-threaded" or "multi-threaded" (default: "multi-threaded") -- `--runtime.worker-threads ` - Number of worker threads for the multi-threaded runtime (default: number of CPU cores; ignored with single-threaded) -- `--private-key ` - Path to private validator key file. Used for P2P identity and (when not using `--signing.remote`) consensus signing. Default: `{home}/config/priv_validator_key.json` -- `--db.skip-upgrade` - Skip database schema upgrade on startup -- `--signing.remote` - Use remote signing with specified endpoint URL (if not provided, uses local signing). Requires `--validator`. -- `--signing.tls-cert-path` - Path to TLS certificate file for remote signing; auto-enables TLS (requires `--signing.remote`) - -#### Remote Signing - -For validator nodes that use a remote signing service instead of local private keys: - -```bash -arc-node-consensus start \ - --home=~/.arc/consensus \ - --moniker=validator-1 \ - --validator \ - --suggested-fee-recipient=0xYourAddressHere \ - --eth-socket=/tmp/reth.ipc \ - --execution-socket=/tmp/auth.ipc \ - --minimal \ - --signing.remote=http://validator-signer-proxy:10340 \ - --signing.tls-cert-path=/path/to/ca_cert.pem -``` - -Note: The remote signer timeout is hardcoded to 30 seconds. - -### Download - -Download a consensus layer snapshot and extract it into the home directory. - -The snapshot archive uses bare paths — files are extracted directly into `--home` without any prefix stripping. For example, a `store.db` entry in the archive lands at `~/.arc/consensus/store.db`. - -```bash -arc-node-consensus download \ - --home=~/.arc/consensus \ - --url -``` - -If `--url` is omitted, the latest pruned snapshot for the selected `--chain` is fetched automatically from the snapshot API. - -```bash -# Devnet — latest snapshot (recommended) -arc-node-consensus download \ - --home=~/.arc/consensus \ - --chain arc-devnet -``` - -> For a full node restore (EL + CL), use the `arc-snapshots` tool instead — it downloads both archives in one command. See [`crates/snapshots/README.md`](../snapshots/README.md). - -### Key - -Display the public key and address derived from the private validator key: - -```bash -arc-node-consensus key --home=~/.arc/consensus -``` - -Optionally pass a key file path directly: - -```bash -arc-node-consensus key /path/to/priv_validator_key.json -``` - -### DB - -The `db` command provides database maintenance operations for the consensus layer database. - -#### Migrate (Upgrade) - -Migrate the database schema to the latest version (also available as `db upgrade`). This is useful when upgrading to a new version of the software that includes database schema changes. - -Normally, database migrations are applied automatically each time the node starts. Running this command manually is only necessary if the automatic migration fails during startup. - -```bash -arc-node-consensus db migrate --home=~/.arc/consensus -``` - -Use `--dry-run` to check what migrations would be applied without executing them: - -```bash -arc-node-consensus db migrate --home=~/.arc/consensus --dry-run -``` - -#### Compact - -Compact the database to reclaim disk space. This operation rewrites the database file to remove fragmentation and reclaim space from deleted records. - -**Important:** The node must be stopped before running the compact command. - -```bash -arc-node-consensus db compact --home=~/.arc/consensus -``` - -## Environment Variables - -The following environment variables can be used to modify behavior: - -- `ARC_HALT_AT_BLOCK_HEIGHT` - If set to a non-zero value, the node will gracefully shut down after reaching this block height. Used for automated testing. - -## REST API - -The consensus layer exposes a REST API for monitoring and querying consensus state when `--rpc.addr` is set (e.g., `--rpc.addr=0.0.0.0:26658`). - -### API Versioning - -The REST API uses **header-based versioning** with custom Accept headers: - -```bash -Accept: application/vnd.arc.v{N}+json -``` - -**Current Version:** `v1` - -#### Making Versioned Requests - -**Explicit Version (Recommended):** -```bash -curl -H "Accept: application/vnd.arc.v1+json" http://localhost:26658/status -``` - -**Backwards Compatible (defaults to v1):** -```bash -curl http://localhost:26658/status -# or -curl -H "Accept: application/json" http://localhost:26658/status -``` - -**Unsupported Version:** -```bash -curl -H "Accept: application/vnd.arc.v99+json" http://localhost:26658/status -# Returns: 406 Not Acceptable with error details -``` - -#### Response Headers - -All responses include a `Content-Type` header indicating the API version used: - -``` -Content-Type: application/vnd.arc.v1+json -``` - -#### Version Negotiation Rules - -1. **Explicit versioned Accept header** → Uses that version if supported, otherwise returns `406 Not Acceptable` -2. **`Accept: application/json`** → Defaults to current version (v1) -3. **Missing Accept header** → Defaults to current version (v1) -4. **Unrecognized format** → Defaults to current version (v1) for backwards compatibility - -#### Available Endpoints - -All endpoints support versioning: - -- `GET /` - API documentation and versioning info -- `GET /status` - Application status -- `GET /health` - Health check -- `GET /version` - Version information (git, cargo) -- `GET /consensus-state` - Current consensus state -- `GET /commit?height=N` - Commit certificate for specific height -- `GET /network-state` - Network peer information - -#### Example API Usage - -**Get Status:** -```bash -curl -H "Accept: application/vnd.arc.v1+json" http://localhost:26658/status | jq -``` - -**Get Commit Certificate:** -```bash -curl -H "Accept: application/vnd.arc.v1+json" \ - "http://localhost:26658/commit?height=100" | jq -``` - -**Get Health:** -```bash -curl http://localhost:26658/health -``` - -**Get API Documentation:** -```bash -curl http://localhost:26658/ -``` - -### Deprecation Policy - -When breaking changes are introduced: - -1. A new API version (e.g., v2) will be released -2. The previous version (v1) will remain available with a deprecation notice -3. After a deprecation period, the old version may be removed in a major release -4. Clients will be notified via response headers and documentation - -## Metrics - -See [METRICS.md](./METRICS.md). - -[malachite]: https://github.com/circlefin/malachite/ -[reth]: https://reth.rs/ -[engine-api]: https://github.com/ethereum/execution-apis/blob/main/src/engine/README.md diff --git a/crates/malachite-app/src/app.rs b/crates/malachite-app/src/app.rs deleted file mode 100644 index a8193af..0000000 --- a/crates/malachite-app/src/app.rs +++ /dev/null @@ -1,490 +0,0 @@ -// Copyright 2025 Circle Internet Group, Inc. All rights reserved. -// -// SPDX-License-Identifier: Apache-2.0 -// -// Licensed under the Apache License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. -// You may obtain a copy of the License at -// -// http://www.apache.org/licenses/LICENSE-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// See the License for the specific language governing permissions and -// limitations under the License. - -use eyre::{eyre, Context as _}; -use tokio::sync::mpsc::Receiver; -use tokio_util::sync::CancellationToken; -use tracing::{error, info, warn}; - -use malachitebft_app_channel::{AppMsg, Channels}; -use malachitebft_core_types::utils::height::DisplayRange; -use malachitebft_core_types::Context; - -use arc_consensus_types::{ArcContext, StoredCommitCertificate}; -use arc_eth_engine::engine::Engine; - -use crate::handlers::*; -use crate::request::{AppRequest, CommitCertificateInfo}; -use crate::state::State; - -pub async fn run( - mut state: State, - channels: Channels, - engine: Engine, - rx_app_req: Receiver, - cancel_token: CancellationToken, -) -> eyre::Result<()> { - if let Some(halt_height) = state.env_config().halt_height { - warn!("Consensus configured to halt at block height: {halt_height}"); - } - - let result = cancel_token - .run_until_cancelled_owned(go(&mut state, channels, &engine, rx_app_req)) - .await; - - let result = match result { - Some(Err(e)) => { - error!("🔴 Error in application: {e:#}"); - error!("🔴 Shutting down"); - Err(e) - } - None => { - info!("🟢🟢 Application is shutting down gracefully"); - Ok(()) - } - }; - - // Create a savepoint in the database helps avoiding its repair on next startup. - state.savepoint(); - - result -} - -/// A type that cannot be instantiated. -/// -/// Used to indicate that the function never returns normally. -enum Never {} - -/// The main event loop of the application. -/// -/// It listens for messages from consensus and application requests, -/// and dispatches them to the appropriate handlers. -/// -/// # Errors -/// Returns an error if handling a message fails or one of the channels is closed unexpectedly. -/// This will cause the application to shut down. -/// Otherwise, it runs indefinitely until cancelled. -async fn go( - state: &mut State, - mut channels: Channels, - engine: &Engine, - mut rx_app_req: Receiver, -) -> eyre::Result { - loop { - tokio::select! { - biased; - - msg = channels.consensus.recv() => match msg { - Some(msg) => { - // Abort on error to shut down the application. - handle_consensus(msg, state, &mut channels, engine).await - .wrap_err("Error handling consensus message")?; - }, - None => { - return Err(eyre!("Consensus channel closed unexpectedly")); - } - }, - - req = rx_app_req.recv() => match req { - Some(req) => { - if let Err(e) = handle_app_request(req, state, engine).await { - error!("🔴 Error handling application request: {e:#}"); - - // We continue processing other requests even if one fails. - continue; - } - }, - None => { - return Err(eyre!("Application request channel closed unexpectedly")); - } - } - } - } -} - -async fn handle_consensus( - msg: AppMsg, - state: &mut State, - channels: &mut Channels, - engine: &Engine, -) -> eyre::Result<()> { - match msg { - // Consensus is ready. - // The application replies with a message to instruct - // consensus to start at a given height. - AppMsg::ConsensusReady { reply } => { - let _guard = state.metrics.start_msg_process_timer("ConsensusReady"); - - info!("🚦 Consensus is ready"); - - consensus_ready::handle(state, engine, reply).await?; - } - - // Consensus has started a new round. - // The application replies to this message with the previously undecided proposals for the round. - AppMsg::StartedRound { - height, - round, - proposer, - role, - reply_value, - } => { - let _guard = state.metrics.start_msg_process_timer("StartedRound"); - - started_round::handle(state, engine, height, round, proposer, role, reply_value).await; - } - - // Request to build a local value to propose. - // The application replies to this message with the requested value within the timeout. - AppMsg::GetValue { - height, - round, - timeout, - reply, - } => { - let _guard = state.metrics.start_msg_process_timer("GetValue"); - - info!(%height, %round, "Consensus is requesting a value to propose"); - - get_value::handle( - state, - channels.network.clone(), - engine, - height, - round, - timeout, - reply, - ) - .await?; - } - - // Notification for a new proposal part. - // If a full proposal can be assembled, the application responds - // with the complete proposed value. Otherwise, it responds with `None`. - AppMsg::ReceivedProposalPart { from, part, reply } => { - let _guard = state - .metrics - .start_msg_process_timer("ReceivedProposalPart"); - - received_proposal_part::handle(state, engine, from, part, reply).await; - } - - // Notification that consensus has decided a value. - // - // The reply acknowledges that the certificate has been durably stored so the sync - // actor can advertise the new tip height. Without it, peers never learn we advanced - // beyond startup and late joiners cannot value-sync from us. `decided::handle` - // consumes the channel at the durability point. - AppMsg::Decided { - certificate, - extensions: _, - reply, - } => { - let _guard = state.metrics.start_msg_process_timer("Decided"); - - let height = certificate.height; - let round = certificate.round; - let value_id = certificate.value_id; - let signatures = certificate.commit_signatures.len(); - - info!(%height, %round, %value_id, %signatures, "🎉 Consensus has decided on value"); - - decided::handle(state, engine, certificate, reply).await?; - } - - // Notification that a height has been finalized. - AppMsg::Finalized { - certificate, - extensions: _, - evidence, - reply, - } => { - let _guard = state.metrics.start_msg_process_timer("Finalized"); - - let height = certificate.height; - let round = certificate.round; - let value_id = certificate.value_id; - let signatures = certificate.commit_signatures.len(); - - info!( - %height, %round, %value_id, %signatures, - "📜 Consensus has finalized the height" - ); - - finalized::handle(state, certificate, evidence, reply).await?; - } - - // A value has been synced from the network. - // This may happen when the node is catching up with the network. - AppMsg::ProcessSyncedValue { - height, - round, - proposer, - value_bytes, - reply, - } => { - let _guard = state.metrics.start_msg_process_timer("ProcessSyncedValue"); - - info!(%height, %round, "Processing synced value"); - - process_synced_value::handle( - state, - engine, - height, - round, - proposer, - value_bytes, - reply, - ) - .await?; - } - - // Request for previously decided blocks from the application's storage. - AppMsg::GetDecidedValues { range, reply } => { - info!(range = %DisplayRange(&range), "Received sync request"); - - get_decided_values::handle(state, engine, range, reply).await?; - } - - // Request for the earliest height available in the block store. - AppMsg::GetHistoryMinHeight { reply } => { - let _guard = state.metrics.start_msg_process_timer("GetHistoryMinHeight"); - - get_history_min_height::handle(state, engine, reply).await?; - } - - // Request to re-stream a proposal that was previously seen at valid_round or round (if valid_round is Nil). - AppMsg::RestreamProposal { - height, - round, - valid_round, - address: _, - value_id, - } => { - let _guard = state.metrics.start_msg_process_timer("RestreamProposal"); - - info!(%height, %round, %valid_round, %value_id, "Restreaming proposal"); - - restream_proposal::handle(state, channels, height, round, valid_round, value_id) - .await?; - } - - // Currently not supported - // Request to extend a precommit - AppMsg::ExtendVote { reply, .. } => { - if let Err(e) = reply.send(None) { - error!("🔴 Failed to send ExtendVote reply: {e:?}"); - } - } - - // Currently not supported - // Request to verify a vote extension - AppMsg::VerifyVoteExtension { reply, .. } => { - if let Err(e) = reply.send(Ok(())) { - error!("🔴 Failed to send VerifyVoteExtension reply: {e:?}"); - } - } - } - - Ok(()) -} - -#[allow(clippy::unit_arg)] -async fn handle_app_request(req: AppRequest, state: &State, engine: &Engine) -> eyre::Result<()> { - match req { - AppRequest::GetCertificate(height, reply) => { - let result = state - .store() - .get_certificate(height) - .await - .wrap_err_with(|| { - format!("GetCertificate: Failed to get certificate for height {height:?}") - })?; - - let info = match result { - Some(certificate) => get_certificate_info(&state.ctx, engine, certificate).await, - None => None, - }; - - if let Err(e) = reply.send(info) { - error!("GetCertificate: Failed to reply: {e:?}"); - } - } - - AppRequest::GetMisbehaviorEvidence(height, reply) => { - let evidence = state.store().get_misbehavior_evidence(height).await.wrap_err_with(|| { - format!( - "GetMisbehaviorEvidence: Failed to get misbehavior evidence for height {height:?}", - ) - })?; - if let Err(e) = reply.send(evidence) { - error!("GetMisbehaviorEvidence: Failed to reply: {e:?}"); - } - } - - AppRequest::GetProposalMonitorData(height, reply) => { - let data = state - .store() - .get_proposal_monitor_data(height) - .await - .wrap_err_with(|| { - format!( - "Failed to get proposal monitor data for height {:?} in response to a GetProposalMonitorData request", - height, - ) - })?; - if let Err(e) = reply.send(data) { - error!("Failed to reply to GetProposalMonitorData: {e:?}"); - } - } - - AppRequest::GetInvalidPayloads(height, reply) => { - let payloads = state.store().get_invalid_payloads(height).await.wrap_err_with(|| { - format!( - "Failed to get invalid payloads for height {:?} in response to a GetInvalidPayloads request", height, - ) - })?; - if let Err(e) = reply.send(payloads) { - error!("Failed to reply to GetInvalidPayloads: {e:?}"); - } - } - - AppRequest::GetStatus(reply) => { - let status = state - .get_status() - .await - .wrap_err("GetStatus: Failed to get the current status")?; - - if let Err(e) = reply.send(status) { - error!("GetStatus: Failed to reply: {e:?}"); - } - } - - AppRequest::GetHealth(reply) => { - if let Err(e) = reply.send(state.get_health()) { - error!("GetHealth: Failed to reply: {e:?}"); - } - } - - AppRequest::GetSyncState(reply) => { - if let Err(e) = reply.send(state.sync_state) { - error!("GetSyncState: Failed to reply: {e:?}"); - } - } - } - - Ok(()) -} - -async fn get_certificate_info( - ctx: &ArcContext, - engine: &Engine, - stored: StoredCommitCertificate, -) -> Option { - // The validator set that signed the certificate is the one *before* executing that block, - // since the block itself could contain validator set changes. - let prev_height = stored.certificate.height.as_u64().saturating_sub(1); - let validator_set = engine - .eth - .get_active_validator_set(prev_height) - .await - .ok()?; - - let proposer = stored.proposer.unwrap_or_else(|| { - ctx.select_proposer( - &validator_set, - stored.certificate.height, - stored.certificate.round, - ) - .address - }); - - Some(CommitCertificateInfo { - certificate: stored.certificate, - certificate_type: stored.certificate_type, - proposer, - }) -} - -#[cfg(test)] -mod tests { - use super::*; - - use arc_consensus_types::{ - Address, BlockHash, CommitCertificate, CommitCertificateType, Height, Round, ValueId, - }; - use arc_eth_engine::engine::{MockEngineAPI, MockEthereumAPI}; - use mockall::predicate::eq; - - fn stored_cert(height: u64) -> StoredCommitCertificate { - StoredCommitCertificate { - certificate: CommitCertificate::new( - Height::new(height), - Round::new(0), - ValueId::new(BlockHash::new([0xAA; 32])), - vec![], - ), - certificate_type: CommitCertificateType::Minimal, - // Set so get_certificate_info skips select_proposer (keeps the test focused - // on the validator set lookup). - proposer: Some(Address::new([0x42; 20])), - } - } - - /// get_certificate_info must fetch the validator set at `certificate.height - 1` — the - /// set that signed the certificate, i.e. the state *before* executing the certified block. - #[tokio::test] - async fn get_certificate_info_queries_validator_set_at_prev_height() { - let cert_height = 42u64; - - let mut mock_eth = MockEthereumAPI::new(); - mock_eth - .expect_get_active_validator_set() - .with(eq(cert_height - 1)) - .once() - .returning(|_| Ok(Default::default())); - - let engine = Engine::new(Box::new(MockEngineAPI::new()), Box::new(mock_eth)); - let ctx = ArcContext::default(); - - let info = get_certificate_info(&ctx, &engine, stored_cert(cert_height)) - .await - .expect("should return Some"); - - assert_eq!(info.certificate.height, Height::new(cert_height)); - } - - /// At genesis (height 0), the saturating subtraction must keep the query at 0 rather - /// than underflowing. - #[tokio::test] - async fn get_certificate_info_handles_genesis_height() { - let mut mock_eth = MockEthereumAPI::new(); - mock_eth - .expect_get_active_validator_set() - .with(eq(0u64)) - .once() - .returning(|_| Ok(Default::default())); - - let engine = Engine::new(Box::new(MockEngineAPI::new()), Box::new(mock_eth)); - let ctx = ArcContext::default(); - - let info = get_certificate_info(&ctx, &engine, stored_cert(0)) - .await - .expect("should return Some"); - - assert_eq!(info.certificate.height, Height::new(0)); - } -} diff --git a/crates/malachite-app/src/block.rs b/crates/malachite-app/src/block.rs deleted file mode 100644 index 131f7be..0000000 --- a/crates/malachite-app/src/block.rs +++ /dev/null @@ -1,17 +0,0 @@ -// Copyright 2025 Circle Internet Group, Inc. All rights reserved. -// -// SPDX-License-Identifier: Apache-2.0 -// -// Licensed under the Apache License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. -// You may obtain a copy of the License at -// -// http://www.apache.org/licenses/LICENSE-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// See the License for the specific language governing permissions and -// limitations under the License. - -pub use arc_consensus_types::block::*; diff --git a/crates/malachite-app/src/config.rs b/crates/malachite-app/src/config.rs deleted file mode 100644 index b59ab33..0000000 --- a/crates/malachite-app/src/config.rs +++ /dev/null @@ -1,408 +0,0 @@ -// Copyright 2025 Circle Internet Group, Inc. All rights reserved. -// -// SPDX-License-Identifier: Apache-2.0 -// -// Licensed under the Apache License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. -// You may obtain a copy of the License at -// -// http://www.apache.org/licenses/LICENSE-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// See the License for the specific language governing permissions and -// limitations under the License. - -//! The Application (or Node) definition. The Node trait implements the Consensus context and the -//! cryptographic library used for signing. - -use std::net::SocketAddr; - -use arc_consensus_types::rpc_sync::SyncEndpointUrl; -use backon::{BackoffBuilder, Retryable}; -use eyre::eyre; -use tracing::{info, warn}; -use url::Url; - -use malachitebft_app_channel::app::consensus::Multiaddr; - -use arc_consensus_db::DbUpgrade; -use arc_consensus_types::Address; -use arc_shared::chain_ids::{LOCALDEV_CHAIN_ID, TESTNET_CHAIN_ID}; - -use crate::hardcoded_config::GossipSubOverrides; -use arc_eth_engine::{engine::Engine, INITIAL_RETRY_DELAY}; - -pub enum EngineConfig<'a> { - Ipc(EthIpcConfig<'a>), - Rpc(EthRpcConfig<'a>), -} - -impl<'a> EngineConfig<'a> { - pub async fn connect(self) -> eyre::Result { - // Retry indefinitely with a constant delay of `INITIAL_RETRY_DELAY` - // seconds - let retry_policy = backon::ConstantBuilder::new() - .with_delay(INITIAL_RETRY_DELAY) - .without_max_times() - .build(); - match self { - EngineConfig::Ipc(EthIpcConfig { - eth_socket, - execution_socket, - }) => (|| Engine::new_ipc(execution_socket, eth_socket)) - .retry(retry_policy) - .notify(|e, dur| { - warn!("Failed to connect to Ethereum node via IPC: {e}, retrying in {dur:?}...") - }) - .await, - - EngineConfig::Rpc(EthRpcConfig { - eth_rpc_endpoint, - execution_endpoint, - execution_ws_endpoint, - execution_jwt, - }) => { - let ws_endpoint = execution_ws_endpoint; - (move || { - Engine::new_rpc( - execution_endpoint.clone(), - eth_rpc_endpoint.clone(), - ws_endpoint.clone(), - execution_jwt, - ) - }) - .retry(retry_policy) - .notify(|e, dur| { - warn!("Failed to connect to Ethereum node via RPC: {e}, retrying in {dur:?}...") - }) - .await - } - } - } -} - -pub struct EthIpcConfig<'a> { - pub eth_socket: &'a str, - pub execution_socket: &'a str, -} - -pub struct EthRpcConfig<'a> { - pub eth_rpc_endpoint: &'a Url, - pub execution_endpoint: &'a Url, - pub execution_ws_endpoint: Option, - pub execution_jwt: &'a str, -} - -/// Configuration parameters for the start. -#[derive(Clone, Default)] -pub struct StartConfig { - /// The persistent peers to connect to on startup - pub persistent_peers: Vec, - - /// Only allow connections to/from persistent peers - pub persistent_peers_only: bool, - - /// GossipSub overrides from CLI flags - pub gossipsub_overrides: GossipSubOverrides, - - /// The Ethereum IPC socket - pub eth_socket: Option, - /// The execution socket - pub execution_socket: Option, - - /// The Ethereum RPC endpoint - pub eth_rpc_endpoint: Option, - /// The execution endpoint - pub execution_endpoint: Option, - /// The execution WebSocket endpoint - pub execution_ws_endpoint: Option, - /// The execution JWT - pub execution_jwt: Option, - /// The bind address for the pprof server - pub pprof_bind_address: Option, - /// Whether to activate jemalloc heap profiling - pub pprof_heap_prof: bool, - /// The address to receive the fees and rewards from the execution layer - pub suggested_fee_recipient: Option
, - /// Skip database schema upgrade on startup - pub skip_db_upgrade: bool, - - /// Run as a validator (load consensus key, sign validator proof) - pub validator: bool, - - /// Enable RPC sync mode, a.k.a. follow (fetch blocks via HTTP RPC instead of P2P) - pub rpc_sync_enabled: bool, - /// RPC endpoints to fetch blocks from (only used in RPC sync mode) - pub rpc_sync_endpoints: Vec, -} - -impl StartConfig { - /// Check if RPC sync mode is enabled - pub fn is_rpc_sync_mode(&self) -> bool { - self.rpc_sync_enabled - } - - /// Populate `rpc_sync_endpoints` with chain-specific defaults when the user - /// enabled `--follow` without explicit `--follow.endpoint` arguments. - pub fn resolve_default_rpc_sync_endpoints(&mut self, chain_id: u64) -> eyre::Result<()> { - if !self.rpc_sync_enabled || !self.rpc_sync_endpoints.is_empty() { - return Ok(()); - } - - let url = default_rpc_sync_endpoint(chain_id)?; - self.rpc_sync_endpoints.push(url); - Ok(()) - } - - pub fn engine_config(&'_ self) -> Option> { - if let (Some(eth_socket), Some(execution_socket)) = - (self.eth_socket.as_ref(), self.execution_socket.as_ref()) - { - Some(EngineConfig::Ipc(EthIpcConfig { - eth_socket, - execution_socket, - })) - } else if let (Some(eth_rpc_endpoint), Some(execution_endpoint), Some(execution_jwt)) = ( - self.eth_rpc_endpoint.as_ref(), - self.execution_endpoint.as_ref(), - self.execution_jwt.as_ref(), - ) { - let ws_endpoint = self - .execution_ws_endpoint - .clone() - .or_else(|| derive_ws_url(eth_rpc_endpoint)); - - Some(EngineConfig::Rpc(EthRpcConfig { - eth_rpc_endpoint, - execution_endpoint, - execution_ws_endpoint: ws_endpoint, - execution_jwt, - })) - } else { - None - } - } - - pub fn db_upgrade(&self) -> DbUpgrade { - if self.skip_db_upgrade { - DbUpgrade::Skip - } else { - DbUpgrade::Perform - } - } -} - -/// Returns the default RPC sync endpoint for the given chain ID. -fn default_rpc_sync_endpoint(chain_id: u64) -> eyre::Result { - let url = match chain_id { - TESTNET_CHAIN_ID => "https://rpc.quicknode.testnet.arc.network/", - LOCALDEV_CHAIN_ID => "http://localhost:8545", - _ => { - return Err(eyre!( - "No default follow endpoint for chain ID {chain_id}. \ - Use --follow.endpoint to specify one explicitly." - )) - } - }; - - info!("Using default follow endpoint for chain {chain_id}: {url}"); - url.parse() - .map_err(|e| eyre!("Failed to parse default follow endpoint: {e}")) -} - -/// Derive a WebSocket URL from an HTTP RPC URL using the reth convention: -/// `http(s)://host:port` → `ws(s)://host:(port+1)`. -/// -/// Same convention used by `--follow.endpoint` (see [`SyncEndpointUrl::websocket`]). -/// Returns `None` if the URL has no explicit port or an unsupported scheme. -fn derive_ws_url(http_url: &Url) -> Option { - let mut ws_url = http_url.clone(); - match http_url.scheme() { - "http" => ws_url.set_scheme("ws").ok()?, - "https" => ws_url.set_scheme("wss").ok()?, - _ => return None, - } - let port = http_url.port()?.checked_add(1)?; - ws_url.set_port(Some(port)).ok()?; - Some(ws_url) -} - -#[cfg(test)] -mod tests { - use super::*; - - #[test] - fn derive_ws_url_increments_port() { - let http = Url::parse("http://localhost:8545").unwrap(); - let ws = derive_ws_url(&http).unwrap(); - assert_eq!(ws.as_str(), "ws://localhost:8546/"); - } - - #[test] - fn derive_ws_url_https_to_wss() { - let https = Url::parse("https://localhost:8545").unwrap(); - let wss = derive_ws_url(&https).unwrap(); - assert_eq!(wss.as_str(), "wss://localhost:8546/"); - } - - #[test] - fn derive_ws_url_returns_none_on_port_overflow() { - let http = Url::parse("http://localhost:65535").unwrap(); - assert!(derive_ws_url(&http).is_none()); - } - - #[test] - fn derive_ws_url_returns_none_without_port() { - let http = Url::parse("http://localhost").unwrap(); - assert!(derive_ws_url(&http).is_none()); - } - - #[test] - fn derive_ws_url_returns_none_for_unsupported_scheme() { - let ftp = Url::parse("ftp://localhost:8545").unwrap(); - assert!(derive_ws_url(&ftp).is_none()); - } - - #[test] - fn default_rpc_sync_endpoint_testnet() { - let endpoint = default_rpc_sync_endpoint(TESTNET_CHAIN_ID).unwrap(); - assert_eq!( - endpoint.http().as_str(), - "https://rpc.quicknode.testnet.arc.network/" - ); - } - - #[test] - fn default_rpc_sync_endpoint_localdev() { - let endpoint = default_rpc_sync_endpoint(LOCALDEV_CHAIN_ID).unwrap(); - assert_eq!(endpoint.http().as_str(), "http://localhost:8545/"); - } - - #[test] - fn default_rpc_sync_endpoint_unsupported_chain() { - let result = default_rpc_sync_endpoint(999); - assert!(result.is_err()); - assert!(result - .unwrap_err() - .to_string() - .contains("No default follow endpoint")); - } - - #[test] - fn resolve_defaults_populates_empty_endpoints() { - let mut config = StartConfig { - persistent_peers: Vec::new(), - persistent_peers_only: false, - gossipsub_overrides: Default::default(), - eth_socket: None, - execution_socket: None, - eth_rpc_endpoint: None, - execution_endpoint: None, - execution_ws_endpoint: None, - execution_jwt: None, - pprof_bind_address: None, - pprof_heap_prof: false, - suggested_fee_recipient: None, - skip_db_upgrade: false, - validator: false, - rpc_sync_enabled: true, - rpc_sync_endpoints: Vec::new(), - }; - - config - .resolve_default_rpc_sync_endpoints(TESTNET_CHAIN_ID) - .unwrap(); - assert_eq!(config.rpc_sync_endpoints.len(), 1); - assert_eq!( - config.rpc_sync_endpoints[0].http().as_str(), - "https://rpc.quicknode.testnet.arc.network/" - ); - } - - #[test] - fn resolve_defaults_preserves_explicit_endpoints() { - let explicit: SyncEndpointUrl = "http://my-validator:8545".parse().unwrap(); - let mut config = StartConfig { - persistent_peers: Vec::new(), - persistent_peers_only: false, - gossipsub_overrides: Default::default(), - eth_socket: None, - execution_socket: None, - eth_rpc_endpoint: None, - execution_endpoint: None, - execution_ws_endpoint: None, - execution_jwt: None, - pprof_bind_address: None, - pprof_heap_prof: false, - suggested_fee_recipient: None, - skip_db_upgrade: false, - validator: false, - rpc_sync_enabled: true, - rpc_sync_endpoints: vec![explicit.clone()], - }; - - config - .resolve_default_rpc_sync_endpoints(TESTNET_CHAIN_ID) - .unwrap(); - assert_eq!(config.rpc_sync_endpoints.len(), 1); - assert_eq!(config.rpc_sync_endpoints[0], explicit); - } - - #[test] - fn resolve_defaults_noop_when_disabled() { - let mut config = StartConfig { - persistent_peers: Vec::new(), - persistent_peers_only: false, - gossipsub_overrides: Default::default(), - eth_socket: None, - execution_socket: None, - eth_rpc_endpoint: None, - execution_endpoint: None, - execution_ws_endpoint: None, - execution_jwt: None, - pprof_bind_address: None, - pprof_heap_prof: false, - suggested_fee_recipient: None, - skip_db_upgrade: false, - validator: false, - rpc_sync_enabled: false, - rpc_sync_endpoints: Vec::new(), - }; - - config - .resolve_default_rpc_sync_endpoints(TESTNET_CHAIN_ID) - .unwrap(); - assert!(config.rpc_sync_endpoints.is_empty()); - } - - #[test] - fn resolve_defaults_errors_on_unsupported_chain() { - let mut config = StartConfig { - persistent_peers: Vec::new(), - persistent_peers_only: false, - gossipsub_overrides: Default::default(), - eth_socket: None, - execution_socket: None, - eth_rpc_endpoint: None, - execution_endpoint: None, - execution_ws_endpoint: None, - execution_jwt: None, - pprof_bind_address: None, - pprof_heap_prof: false, - suggested_fee_recipient: None, - skip_db_upgrade: false, - validator: false, - rpc_sync_enabled: true, - rpc_sync_endpoints: Vec::new(), - }; - - let result = config.resolve_default_rpc_sync_endpoints(999); - assert!(result.is_err()); - assert!(result - .unwrap_err() - .to_string() - .contains("No default follow endpoint")); - } -} diff --git a/crates/malachite-app/src/env_config.rs b/crates/malachite-app/src/env_config.rs deleted file mode 100644 index c59d93f..0000000 --- a/crates/malachite-app/src/env_config.rs +++ /dev/null @@ -1,331 +0,0 @@ -// Copyright 2026 Circle Internet Group, Inc. All rights reserved. -// -// SPDX-License-Identifier: Apache-2.0 -// -// Licensed under the Apache License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. -// You may obtain a copy of the License at -// -// http://www.apache.org/licenses/LICENSE-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// See the License for the specific language governing permissions and -// limitations under the License. - -use std::time::Duration; - -use arc_consensus_types::Height; -use bytesize::ByteSize; - -const ARC_HALT_AT_BLOCK_HEIGHT: &str = "ARC_HALT_AT_BLOCK_HEIGHT"; -const ARC_CONSENSUS_DB_CACHE_SIZE_BYTES: &str = "ARC_CONSENSUS_DB_CACHE_SIZE_BYTES"; -const ARC_SYNC_STATUS_UPDATE_INTERVAL: &str = "ARC_SYNC_STATUS_UPDATE_INTERVAL"; -const ARC_SYNC_CATCH_UP_THRESHOLD: &str = "ARC_SYNC_CATCH_UP_THRESHOLD"; -const ARC_GENESIS_FILE_PATH: &str = "ARC_GENESIS_FILE_PATH"; - -/// Default cache size for the database (1 GiB). -const DEFAULT_DB_CACHE_SIZE: ByteSize = ByteSize::gib(1); - -/// Default sync catch up threshold (1.5 seconds). -/// -/// Block timestamps are truncated to seconds, adding up to 1s variance to elapsed time. -/// Combined with ~500ms consensus+wait, elapsed ranges are in [500ms ..1.5s) even when perfectly in sync. -const DEFAULT_SYNC_CATCH_UP_THRESHOLD: Duration = Duration::from_millis(1500); - -/// Environment-based configuration read once at startup. -pub struct EnvConfig { - /// If set, the node will halt when reaching this block height. - pub halt_height: Option, - /// Cache size in bytes for the consensus database. - pub db_cache_size: ByteSize, - /// If set, overrides the hardcoded sync status update interval. - /// A value of `0s` means "update on every block". - pub status_update_interval: Option, - /// Catch up threshold for determining whether the node is syncing or not - pub sync_catch_up_threshold: Duration, - /// Path to the EL genesis.json file (for reading hardfork activation conditions). - pub genesis_file_path: Option, -} - -impl EnvConfig { - /// Read configuration from environment variables. - /// - /// - `ARC_HALT_AT_BLOCK_HEIGHT`: parsed as `u64`; 0 and missing both mean *no halt*. - /// - `ARC_CONSENSUS_DB_CACHE_SIZE_BYTES`: parsed as `usize`; missing means 1 GiB. - /// - `ARC_SYNC_STATUS_UPDATE_INTERVAL`: parsed via `humantime` (e.g. `"5s"`, `"500ms"`, `"0s"`); - /// missing or unparseable means use the hardcoded default. - pub fn from_env() -> Self { - let halt_height = std::env::var(ARC_HALT_AT_BLOCK_HEIGHT) - .ok() - .and_then(|s| s.parse::().ok()) - .filter(|&n| n != 0) - .map(Height::new); - - let db_cache_size = std::env::var(ARC_CONSENSUS_DB_CACHE_SIZE_BYTES) - .ok() - .and_then(|s| s.parse().ok()) - .unwrap_or(DEFAULT_DB_CACHE_SIZE); - - let status_update_interval = std::env::var(ARC_SYNC_STATUS_UPDATE_INTERVAL) - .ok() - .and_then(|s| humantime::parse_duration(&s).ok()); - let sync_catch_up_threshold = std::env::var(ARC_SYNC_CATCH_UP_THRESHOLD) - .ok() - .and_then(|s| humantime::parse_duration(&s).ok()) - .unwrap_or(DEFAULT_SYNC_CATCH_UP_THRESHOLD); - - let genesis_file_path = std::env::var(ARC_GENESIS_FILE_PATH) - .ok() - .filter(|s| !s.is_empty()); - - Self { - halt_height, - db_cache_size, - status_update_interval, - sync_catch_up_threshold, - genesis_file_path, - } - } -} - -impl Default for EnvConfig { - fn default() -> Self { - Self { - halt_height: None, - db_cache_size: DEFAULT_DB_CACHE_SIZE, - status_update_interval: None, - sync_catch_up_threshold: DEFAULT_SYNC_CATCH_UP_THRESHOLD, - genesis_file_path: None, - } - } -} - -#[cfg(test)] -mod tests { - use serial_test::serial; - - use super::*; - - struct EnvGuard { - old_halt: Option, - old_cache: Option, - old_status_interval: Option, - old_genesis: Option, - } - - impl EnvGuard { - fn new() -> Self { - let old_halt = std::env::var(ARC_HALT_AT_BLOCK_HEIGHT).ok(); - let old_cache = std::env::var(ARC_CONSENSUS_DB_CACHE_SIZE_BYTES).ok(); - let old_status_interval = std::env::var(ARC_SYNC_STATUS_UPDATE_INTERVAL).ok(); - let old_genesis = std::env::var(ARC_GENESIS_FILE_PATH).ok(); - Self { - old_halt, - old_cache, - old_status_interval, - old_genesis, - } - } - } - - impl Drop for EnvGuard { - fn drop(&mut self) { - if let Some(ref val) = self.old_halt { - unsafe { std::env::set_var(ARC_HALT_AT_BLOCK_HEIGHT, val) }; - } else { - unsafe { std::env::remove_var(ARC_HALT_AT_BLOCK_HEIGHT) }; - } - if let Some(ref val) = self.old_cache { - unsafe { std::env::set_var(ARC_CONSENSUS_DB_CACHE_SIZE_BYTES, val) }; - } else { - unsafe { std::env::remove_var(ARC_CONSENSUS_DB_CACHE_SIZE_BYTES) }; - } - if let Some(ref val) = self.old_status_interval { - unsafe { std::env::set_var(ARC_SYNC_STATUS_UPDATE_INTERVAL, val) }; - } else { - unsafe { std::env::remove_var(ARC_SYNC_STATUS_UPDATE_INTERVAL) }; - } - if let Some(ref val) = self.old_genesis { - unsafe { std::env::set_var(ARC_GENESIS_FILE_PATH, val) }; - } else { - unsafe { std::env::remove_var(ARC_GENESIS_FILE_PATH) }; - } - } - } - - // halt_height tests - - #[test] - #[serial] - fn test_env_halt_height_not_set() { - let _guard = EnvGuard::new(); - unsafe { std::env::remove_var(ARC_HALT_AT_BLOCK_HEIGHT) }; - unsafe { std::env::remove_var(ARC_CONSENSUS_DB_CACHE_SIZE_BYTES) }; - let cfg = EnvConfig::from_env(); - assert_eq!(cfg.halt_height, None); - } - - #[test] - #[serial] - fn test_env_halt_height_invalid_value() { - let _guard = EnvGuard::new(); - unsafe { std::env::set_var(ARC_HALT_AT_BLOCK_HEIGHT, "not_a_number") }; - unsafe { std::env::remove_var(ARC_CONSENSUS_DB_CACHE_SIZE_BYTES) }; - let cfg = EnvConfig::from_env(); - assert_eq!(cfg.halt_height, None); - } - - #[test] - #[serial] - fn test_env_halt_height_zero() { - let _guard = EnvGuard::new(); - unsafe { std::env::set_var(ARC_HALT_AT_BLOCK_HEIGHT, "0") }; - unsafe { std::env::remove_var(ARC_CONSENSUS_DB_CACHE_SIZE_BYTES) }; - let cfg = EnvConfig::from_env(); - assert_eq!(cfg.halt_height, None); - } - - #[test] - #[serial] - fn test_env_halt_height_valid_value() { - let _guard = EnvGuard::new(); - unsafe { std::env::set_var(ARC_HALT_AT_BLOCK_HEIGHT, "12345") }; - unsafe { std::env::remove_var(ARC_CONSENSUS_DB_CACHE_SIZE_BYTES) }; - let cfg = EnvConfig::from_env(); - assert_eq!(cfg.halt_height, Some(Height::new(12345))); - } - - // db_cache_size tests - - #[test] - #[serial] - fn test_env_db_cache_size_not_set() { - let _guard = EnvGuard::new(); - unsafe { std::env::remove_var(ARC_HALT_AT_BLOCK_HEIGHT) }; - unsafe { std::env::remove_var(ARC_CONSENSUS_DB_CACHE_SIZE_BYTES) }; - let cfg = EnvConfig::from_env(); - assert_eq!(cfg.db_cache_size, DEFAULT_DB_CACHE_SIZE); - } - - #[test] - #[serial] - fn test_env_db_cache_size_valid_value() { - let _guard = EnvGuard::new(); - unsafe { std::env::remove_var(ARC_HALT_AT_BLOCK_HEIGHT) }; - unsafe { std::env::set_var(ARC_CONSENSUS_DB_CACHE_SIZE_BYTES, "2048") }; - let cfg = EnvConfig::from_env(); - assert_eq!(cfg.db_cache_size, ByteSize::b(2048)); - } - - #[test] - #[serial] - fn test_env_db_cache_size_invalid_value() { - let _guard = EnvGuard::new(); - unsafe { std::env::remove_var(ARC_HALT_AT_BLOCK_HEIGHT) }; - unsafe { std::env::set_var(ARC_CONSENSUS_DB_CACHE_SIZE_BYTES, "not_a_number") }; - let cfg = EnvConfig::from_env(); - assert_eq!(cfg.db_cache_size, DEFAULT_DB_CACHE_SIZE); - } - - // status_update_interval tests - - #[test] - #[serial] - fn test_env_status_update_interval_not_set() { - let _guard = EnvGuard::new(); - unsafe { std::env::remove_var(ARC_HALT_AT_BLOCK_HEIGHT) }; - unsafe { std::env::remove_var(ARC_CONSENSUS_DB_CACHE_SIZE_BYTES) }; - unsafe { std::env::remove_var(ARC_SYNC_STATUS_UPDATE_INTERVAL) }; - let cfg = EnvConfig::from_env(); - assert_eq!(cfg.status_update_interval, None); - } - - #[test] - #[serial] - fn test_env_status_update_interval_seconds() { - let _guard = EnvGuard::new(); - unsafe { std::env::remove_var(ARC_HALT_AT_BLOCK_HEIGHT) }; - unsafe { std::env::remove_var(ARC_CONSENSUS_DB_CACHE_SIZE_BYTES) }; - unsafe { std::env::set_var(ARC_SYNC_STATUS_UPDATE_INTERVAL, "5s") }; - let cfg = EnvConfig::from_env(); - assert_eq!(cfg.status_update_interval, Some(Duration::from_secs(5))); - } - - #[test] - #[serial] - fn test_env_status_update_interval_millis() { - let _guard = EnvGuard::new(); - unsafe { std::env::remove_var(ARC_HALT_AT_BLOCK_HEIGHT) }; - unsafe { std::env::remove_var(ARC_CONSENSUS_DB_CACHE_SIZE_BYTES) }; - unsafe { std::env::set_var(ARC_SYNC_STATUS_UPDATE_INTERVAL, "500ms") }; - let cfg = EnvConfig::from_env(); - assert_eq!(cfg.status_update_interval, Some(Duration::from_millis(500))); - } - - #[test] - #[serial] - fn test_env_status_update_interval_zero() { - let _guard = EnvGuard::new(); - unsafe { std::env::remove_var(ARC_HALT_AT_BLOCK_HEIGHT) }; - unsafe { std::env::remove_var(ARC_CONSENSUS_DB_CACHE_SIZE_BYTES) }; - unsafe { std::env::set_var(ARC_SYNC_STATUS_UPDATE_INTERVAL, "0s") }; - let cfg = EnvConfig::from_env(); - assert_eq!(cfg.status_update_interval, Some(Duration::ZERO)); - } - - #[test] - #[serial] - fn test_env_status_update_interval_invalid() { - let _guard = EnvGuard::new(); - unsafe { std::env::remove_var(ARC_HALT_AT_BLOCK_HEIGHT) }; - unsafe { std::env::remove_var(ARC_CONSENSUS_DB_CACHE_SIZE_BYTES) }; - unsafe { std::env::set_var(ARC_SYNC_STATUS_UPDATE_INTERVAL, "not_a_duration") }; - let cfg = EnvConfig::from_env(); - assert_eq!(cfg.status_update_interval, None); - } - - // genesis_file_path tests - - #[test] - #[serial] - fn test_env_genesis_file_path_not_set() { - let _guard = EnvGuard::new(); - unsafe { std::env::remove_var(ARC_GENESIS_FILE_PATH) }; - let cfg = EnvConfig::from_env(); - assert_eq!(cfg.genesis_file_path, None); - } - - #[test] - #[serial] - fn test_env_genesis_file_path_valid_value() { - let _guard = EnvGuard::new(); - unsafe { std::env::set_var(ARC_GENESIS_FILE_PATH, "/app/assets/genesis.json") }; - let cfg = EnvConfig::from_env(); - assert_eq!( - cfg.genesis_file_path, - Some("/app/assets/genesis.json".to_string()) - ); - } - - #[test] - #[serial] - fn test_env_genesis_file_path_empty_string() { - let _guard = EnvGuard::new(); - unsafe { std::env::set_var(ARC_GENESIS_FILE_PATH, "") }; - let cfg = EnvConfig::from_env(); - assert_eq!(cfg.genesis_file_path, None); - } - - #[test] - #[serial] - fn test_env_default() { - let _guard = EnvGuard::new(); - let cfg = EnvConfig::default(); - assert_eq!(cfg.halt_height, None); - assert_eq!(cfg.db_cache_size, DEFAULT_DB_CACHE_SIZE); - assert_eq!(cfg.status_update_interval, None); - assert_eq!(cfg.genesis_file_path, None); - } -} diff --git a/crates/malachite-app/src/finalize.rs b/crates/malachite-app/src/finalize.rs deleted file mode 100644 index 3bdd6df..0000000 --- a/crates/malachite-app/src/finalize.rs +++ /dev/null @@ -1,226 +0,0 @@ -// Copyright 2025 Circle Internet Group, Inc. All rights reserved. -// -// SPDX-License-Identifier: Apache-2.0 -// -// Licensed under the Apache License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. -// You may obtain a copy of the License at -// -// http://www.apache.org/licenses/LICENSE-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// See the License for the specific language governing permissions and -// limitations under the License. - -use ssz::Encode; -use tracing::{debug, info}; - -use alloy_rpc_types_engine::ExecutionPayloadV3; -use arc_consensus_types::{BlockHash, Height}; -use arc_eth_engine::engine::Engine; -use arc_eth_engine::json_structures::ExecutionBlock; - -use crate::metrics::AppMetrics; -use crate::stats::Stats; - -/// Abstracts the finalization of decided blocks by the execution engine. -/// -/// This trait allows for testing block finalization logic without requiring -/// a full execution engine by enabling mock implementations. -#[cfg_attr(test, mockall::automock)] -pub trait BlockFinalizer { - async fn finalize_decided_block( - &self, - height: Height, - execution_payload: &ExecutionPayloadV3, - ) -> Result<(ExecutionBlock, BlockHash), eyre::Report>; -} - -impl BlockFinalizer for &T -where - T: BlockFinalizer + ?Sized, -{ - async fn finalize_decided_block( - &self, - height: Height, - execution_payload: &ExecutionPayloadV3, - ) -> Result<(ExecutionBlock, BlockHash), eyre::Report> { - (**self) - .finalize_decided_block(height, execution_payload) - .await - } -} - -pub struct EngineBlockFinalizer<'a> { - engine: &'a Engine, - stats: &'a Stats, - metrics: &'a AppMetrics, -} - -impl<'a> EngineBlockFinalizer<'a> { - pub fn new(engine: &'a Engine, stats: &'a Stats, metrics: &'a AppMetrics) -> Self { - Self { - engine, - stats, - metrics, - } - } -} - -impl BlockFinalizer for EngineBlockFinalizer<'_> { - async fn finalize_decided_block( - &self, - height: Height, - execution_payload: &ExecutionPayloadV3, - ) -> Result<(ExecutionBlock, BlockHash), eyre::Report> { - finalize_decided_block( - self.engine, - self.stats, - self.metrics, - height, - execution_payload, - ) - .await - } -} - -/// Finalizes a decided block by decoding, logging stats, and updating forkchoice -async fn finalize_decided_block( - engine: &Engine, - stats: &Stats, - metrics: &AppMetrics, - height: Height, - execution_payload: &ExecutionPayloadV3, -) -> Result<(ExecutionBlock, BlockHash), eyre::Report> { - let payload_inner = &execution_payload.payload_inner.payload_inner; - - // Decode bytes into execution payload (a block) - let new_block_hash = payload_inner.block_hash; - let new_block_number = payload_inner.block_number; - let new_block_timestamp = execution_payload.timestamp(); - - // Log stats - let tx_count = payload_inner.transactions.len() as u64; - let block_size = execution_payload.ssz_bytes_len() as u64; - let gas_used = payload_inner.gas_used; - - stats.add_txs_count(tx_count); - stats.add_chain_bytes(block_size); - - info!("👉 Stats at height {height}: {stats}"); - - // Make the decided block canonical using forkchoice_updated() - // The block should already be in the validated payloads pool from proposer/receiver validation - let latest_valid_hash = { - let _guard = metrics.start_engine_api_timer("set_latest_forkchoice_state"); - - engine.set_latest_forkchoice_state(new_block_hash).await? - }; - - debug!( - "🚀 Forkchoice updated to height {} for block hash={} and latest_valid_hash={}", - height, new_block_hash, latest_valid_hash - ); - - // Update Prometheus metrics after successful finalization - metrics.observe_block_transactions_count(tx_count); - metrics.observe_block_size_bytes(block_size); - metrics.observe_block_gas_used(gas_used); - metrics.inc_total_transactions_count(tx_count); - metrics.inc_total_chain_bytes(block_size); - - let new_latest_block = ExecutionBlock { - block_hash: new_block_hash, - block_number: new_block_number, - parent_hash: payload_inner.parent_hash, - timestamp: new_block_timestamp, - }; - - Ok((new_latest_block, latest_valid_hash)) -} - -#[cfg(test)] -mod tests { - use super::*; - - use alloy_primitives::{Address as AlloyAddress, Bloom, Bytes as AlloyBytes, U256}; - use alloy_rpc_types_engine::{ - ExecutionPayloadV1, ExecutionPayloadV2, ForkchoiceUpdated, PayloadStatus, PayloadStatusEnum, - }; - - use arc_consensus_types::B256; - use arc_eth_engine::engine::{MockEngineAPI, MockEthereumAPI}; - - use crate::metrics::app::AppMetrics; - use crate::stats::Stats; - - fn make_payload(parent_hash: B256, block_hash: B256) -> ExecutionPayloadV3 { - ExecutionPayloadV3 { - payload_inner: ExecutionPayloadV2 { - payload_inner: ExecutionPayloadV1 { - parent_hash, - fee_recipient: AlloyAddress::ZERO, - state_root: B256::ZERO, - receipts_root: B256::ZERO, - logs_bloom: Bloom::default(), - prev_randao: B256::ZERO, - block_number: 1, - gas_limit: 0, - gas_used: 0, - timestamp: 1000, - extra_data: AlloyBytes::default(), - base_fee_per_gas: U256::from(1u64), - block_hash, - transactions: vec![], - }, - withdrawals: vec![], - }, - blob_gas_used: 0, - excess_blob_gas: 0, - } - } - - /// finalize_decided_block must set parent_hash from the payload, - /// not from latest_valid_hash (which equals the block's own hash per Engine API spec). - #[tokio::test] - async fn finalize_decided_block_sets_correct_parent_hash() { - let parent_hash = B256::with_last_byte(0x11); - let block_hash = B256::with_last_byte(0x22); - - let mut mock_engine = MockEngineAPI::new(); - // Engine API spec: forkchoice_updated returns latest_valid_hash == head block hash - mock_engine - .expect_forkchoice_updated() - .returning(move |_, _| { - Ok(ForkchoiceUpdated { - payload_status: PayloadStatus { - status: PayloadStatusEnum::Valid, - latest_valid_hash: Some(block_hash), - }, - payload_id: None, - }) - }); - - let engine = Engine::new(Box::new(mock_engine), Box::new(MockEthereumAPI::new())); - let stats = Stats::default(); - let metrics = AppMetrics::default(); - let payload = make_payload(parent_hash, block_hash); - - let (execution_block, _) = - finalize_decided_block(&engine, &stats, &metrics, Height::new(1), &payload) - .await - .expect("finalization should succeed"); - - assert_eq!(execution_block.block_hash, block_hash); - assert_eq!( - execution_block.parent_hash, parent_hash, - "parent_hash should come from the payload, not from latest_valid_hash" - ); - assert_ne!( - execution_block.parent_hash, execution_block.block_hash, - "parent_hash must differ from block_hash" - ); - } -} diff --git a/crates/malachite-app/src/handlers/consensus_ready.rs b/crates/malachite-app/src/handlers/consensus_ready.rs deleted file mode 100644 index 5c3655b..0000000 --- a/crates/malachite-app/src/handlers/consensus_ready.rs +++ /dev/null @@ -1,1920 +0,0 @@ -// Copyright 2025 Circle Internet Group, Inc. All rights reserved. -// -// SPDX-License-Identifier: Apache-2.0 -// -// Licensed under the Apache License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. -// You may obtain a copy of the License at -// -// http://www.apache.org/licenses/LICENSE-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// See the License for the specific language governing permissions and -// limitations under the License. - -use eyre::{eyre, Context}; -use std::time::Duration; -use tracing::{debug, error, info, warn}; - -/// Timeout when blocked waiting for EL persistence to catch up. -const REPLAY_PERSISTENCE_WAIT_TIMEOUT: Duration = Duration::from_secs(30); - -use arc_consensus_types::{ArcContext, ConsensusParams, Height, ValidatorSet, ValueSyncConfig}; -use arc_eth_engine::capabilities::check_capabilities; -use arc_eth_engine::engine::{Engine, EngineAPI, EthereumAPI}; -use arc_eth_engine::json_structures::ExecutionBlock; -use arc_eth_engine::persistence_meter::{self, PersistenceMeter}; -use malachitebft_app_channel::Reply; -use malachitebft_core_types::HeightParams; - -use crate::finalize::{BlockFinalizer, EngineBlockFinalizer}; -use crate::metrics::AppMetrics; -use crate::payload::{EnginePayloadValidator, PayloadValidationResult, PayloadValidator}; -use crate::state::State; -use crate::store::repositories::{ - CertificatesRepository, PayloadsRepository, PendingProposalsRepository, -}; - -/// Handles the `ConsensusReady` message from the consensus engine. -/// -/// This is called when the consensus engine is ready to start. The application performs a handshake -/// and replay with the execution client to ensure a consistent state, and then provides the -/// consensus engine with the starting height and the active validator set. -pub async fn handle( - state: &mut State, - engine: &Engine, - reply: Reply<(Height, HeightParams)>, -) -> eyre::Result<()> { - // Create and attach the persistence meter before borrowing state fields, - // since set_persistence_meter requires &mut self. - { - let execution_config = &state.config().execution; - let meter = persistence_meter::create_with_fallback( - execution_config.persistence_backpressure, - engine.subscription_endpoint(), - execution_config.persistence_backpressure_threshold, - ) - .await; - - persistence_meter::seed_from_latest_block(meter.as_ref(), engine.eth.as_ref()).await; - - state.set_persistence_meter(meter); - } - - let (store, stats, metrics) = (state.store(), state.stats(), state.metrics()); - let max_pending_proposals = max_pending_proposals(&state.config().value_sync); - - let payload_validator = EnginePayloadValidator::new(engine, metrics); - let block_finalizer = EngineBlockFinalizer::new(engine, stats, metrics); - - let (next_height, next_validator_set, next_consensus_params, previous_block) = - on_consensus_ready( - metrics, - store, - store, - store, - payload_validator, - block_finalizer, - engine.api.as_ref(), - engine.eth.as_ref(), - state.persistence_meter(), - max_pending_proposals, - ) - .await?; - - let timeouts = next_consensus_params.timeouts(); - - // Update state with the previous block, current height and validator set - state.previous_block = Some(previous_block); - state.current_height = next_height; - state.set_validator_set(next_validator_set.clone()); - state.set_consensus_params(next_consensus_params); - - let next_height_params = HeightParams::new(next_validator_set, timeouts, None); - - if let Err(e) = reply.send((next_height, next_height_params)) { - error!("🔴 ConsensusReady: Failed to send reply: {e:?}"); - } - - Ok(()) -} - -#[allow(clippy::too_many_arguments)] -async fn on_consensus_ready( - metrics: &AppMetrics, - pending_proposals_repository: impl PendingProposalsRepository, - payloads_repository: impl PayloadsRepository, - certificates_repository: impl CertificatesRepository, - payload_validator: impl PayloadValidator, - block_finalizer: impl BlockFinalizer, - engine_api: impl EngineAPI, - ethereum_api: impl EthereumAPI, - persistence_meter: impl PersistenceMeter, - max_pending_proposals: usize, -) -> eyre::Result<(Height, ValidatorSet, ConsensusParams, ExecutionBlock)> { - // Perform handshake and replay any missing blocks - let HandshakeResult { - previous_block, - latest_height, - next_height, - } = handshake_and_replay( - &payload_validator, - &block_finalizer, - &engine_api, - ðereum_api, - &certificates_repository, - &payloads_repository, - persistence_meter, - metrics, - ) - .await?; - - // Prune pending proposals that are either no longer relevant or exceed storage limits - enforce_pending_proposals_limit( - pending_proposals_repository, - metrics, - max_pending_proposals, - next_height, - ) - .await?; - - // The validator set for the next height is the one at the latest committed height - let validator_set = ethereum_api - .get_active_validator_set(latest_height.as_u64()) - .await - .wrap_err("Failed to get the validator set at ConsensusReady")?; - - // The consensus params for the next height are the same as the latest committed height - let consensus_params = ethereum_api - .get_consensus_params(latest_height.as_u64()) - .await - .inspect_err(|e| { - error!(%latest_height, "Failed to get the consensus params at ConsensusReady: {e}"); - error!(%latest_height, "Using default consensus params as a fallback"); - }) - .unwrap_or_default(); - - Ok((next_height, validator_set, consensus_params, previous_block)) -} - -enum ReplayBlockError { - /// The payload for this height was not found in the repository during replay. - PayloadMissing(Height), - /// Any other error during replay. - Other(eyre::Report), -} - -impl From for ReplayBlockError { - fn from(err: eyre::Report) -> Self { - Self::Other(err) - } -} - -/// Replays a single previously-decided block during startup. -/// -/// Fetches the stored execution payload for the given `height`, -/// validates it via the engine, and finalises it with a -/// fork-choice update. This brings the execution client back -/// in sync with the consensus layer when the EL is behind. -/// -/// Returns the resulting [`ExecutionBlock`] so the caller can -/// chain consecutive replays. -/// -/// An invalid payload at this stage is treated as a fatal error -/// because the block was already decided by consensus -- there -/// is no recovery path and no need to record an -/// [`InvalidPayload`][crate::invalid_payloads::InvalidPayload]. -async fn replay_block( - height: Height, - payloads_repository: impl PayloadsRepository, - payload_validator: impl PayloadValidator, - block_finalizer: impl BlockFinalizer, -) -> Result { - info!("🔄 Replay: replaying block at height {height} from Consensus to Execution Client"); - - let payload = payloads_repository - .get(height) - .await - .wrap_err_with(|| format!("Replay: failed to fetch payload for height {height}"))? - .ok_or(ReplayBlockError::PayloadMissing(height))?; - - let payload_hash = payload.payload_inner.payload_inner.block_hash; - - // EngineAPI: New payload - let result = payload_validator - .validate_payload(&payload) - .await - .wrap_err_with(|| { - format!("Payload validation failed while replaying block at height={height}, payload_hash={}", payload_hash) - })?; - - if let PayloadValidationResult::Invalid { reason } = result { - return Err(eyre!( - "Replay: Execution payload validation failed for block {payload_hash}: {reason}" - ) - .into()); - } - - // EngineAPI: ForkchoiceUpdated - let (new_latest_block, latest_valid_hash) = block_finalizer - .finalize_decided_block(height, &payload) - .await - .wrap_err_with(|| { - format!( - "Failed to finalize block while replaying height={height}, payload_hash={payload_hash}" - ) - })?; - - info!( - "🔍 Replay: Updated canonical latest block; timestamp: {:?}, hash {:?}", - new_latest_block.timestamp, latest_valid_hash - ); - - Ok(new_latest_block) -} - -/// Trigger EL peer-to-peer sync via `forkchoice_updated`, then poll until complete. -/// If `timeout` is `Some`, the function will return an error after the specified duration. -/// If `timeout` is `None`, a default timeout of 1 hour is used. -async fn checkpoint_sync( - target_block_hash: arc_consensus_types::BlockHash, - target_block_height: Height, - engine_api: impl EngineAPI, - ethereum_api: impl EthereumAPI, - timeout: Option, -) -> eyre::Result { - use alloy_rpc_types_engine::PayloadStatusEnum; - - const DEFAULT_TIMEOUT: Duration = Duration::from_secs(3600); - let timeout = timeout.unwrap_or(DEFAULT_TIMEOUT); - - info!(block_hash = %target_block_hash, height = %target_block_height, timeout = ?timeout, "🔄 Checkpoint sync: targeting block hash"); - - let fcu_result = engine_api - .forkchoice_updated(target_block_hash, None) - .await - .wrap_err("Checkpoint sync: forkchoice_updated failed")?; - - match fcu_result.payload_status.status { - PayloadStatusEnum::Syncing => { - info!("🔄 Checkpoint sync: EL acknowledged, polling until complete"); - } - PayloadStatusEnum::Valid => { - info!(block_hash = %target_block_hash, height = %target_block_height, "🔄 Checkpoint sync: EL already has the target block"); - let height_str = format!("0x{:x}", target_block_height.as_u64()); - return ethereum_api - .get_block_by_number(&height_str) - .await? - .ok_or_else(|| { - eyre!( - "Checkpoint sync: no block at height {target_block_height} after Valid FCU" - ) - }); - } - status => { - return Err(eyre!("Checkpoint sync: unexpected FCU status: {status}")); - } - } - - const POLL_INTERVAL: Duration = Duration::from_secs(2); - let start = tokio::time::Instant::now(); - let height_str = format!("0x{:x}", target_block_height.as_u64()); - - loop { - tokio::time::sleep(POLL_INTERVAL).await; - - if start.elapsed() > timeout { - return Err(eyre!("Checkpoint sync: timed out after {timeout:?}")); - } - - let block = ethereum_api.get_block_by_number(&height_str).await?; - if let Some(b) = block { - if b.block_hash == target_block_hash { - info!( - height = b.block_number, - block_hash = %b.block_hash, - "🔄 Checkpoint sync: complete" - ); - return Ok(b); - } - return Err(eyre!( - "Checkpoint sync: block hash mismatch at height {target_block_height}: \ - expected {target_block_hash}, got {}", - b.block_hash - )); - } - - info!(target_height = %target_block_height, "🔄 Checkpoint sync: still syncing"); - } -} - -#[derive(Debug)] -struct HandshakeResult { - previous_block: ExecutionBlock, - latest_height: Height, - next_height: Height, -} - -#[allow(clippy::too_many_arguments)] -async fn handshake_and_replay( - payload_validator: impl PayloadValidator, - block_finalizer: impl BlockFinalizer, - engine_api: impl EngineAPI, - ethereum_api: impl EthereumAPI, - certificates_repository: impl CertificatesRepository, - payloads_repository: impl PayloadsRepository, - persistence_meter: impl PersistenceMeter, - metrics: &AppMetrics, -) -> eyre::Result { - // Node start-up: https://hackmd.io/@danielrachi/engine_api#Node-startup - // Check compatibility with execution client - { - let _guard = metrics.start_engine_api_timer("check_capabilities"); - check_capabilities(&engine_api) - .await - .wrap_err("Call to check_capabilities failed in handshake_and_replay")? - } - - // N.B. We only support the following cases: - // - // latest_height(EL) == latest_height(Consensus) --> Nothing to replay - // latest_height(EL) < latest_height(Consensus) --> Replay all the missing blocks - // - // The following case is an error condition (unrecoverable): - // latest_height(Consensus) < latest_height(EL) - - // Get the latest block from the execution engine - let mut latest_block = { - let _guard = metrics.start_engine_api_timer("get_block_by_number"); - - ethereum_api - .get_block_by_number("latest") - .await? - .ok_or_else(|| { - eyre::eyre!("Handshake: Could not get latest block from execution client") - })? - }; - - debug!("👉 Handshake: EL's latest_block: {:?}", latest_block); - - let latest_height_el = Height::new(latest_block.block_number); - let latest_height_cons = certificates_repository - .max_height() - .await - .wrap_err("Handshake: failed to get latest consensus height")? - .unwrap_or_default(); - - if latest_height_el > latest_height_cons { - if latest_height_cons == Height::default() { - return Err(eyre!( - "Handshake: EL has blocks (height {latest_height_el}) but CL has no committed \ - state (height 0). The CL snapshot is missing. \ - Download one with: `arc-node-consensus download`" - )); - } - return Err(eyre!( - "Handshake: inconsistent state: EL latest height ({latest_height_el}) \ - is greater than CL latest committed height ({latest_height_cons}). \ - This may indicate CL database corruption or a partial snapshot restore. \ - Try re-downloading both snapshots with: `arc-snapshots download`" - )); - } - - info!( - "🤝 Handshake: EL latest height: {}, CL latest committed height: {}", - latest_height_el, latest_height_cons - ); - - // Replay missing blocks from CL payloads. If a payload is missing at any height, - // fall back to checkpoint sync using the block hash from the CL's latest certificate. - let mut replay_height = latest_height_el.increment(); - while replay_height <= latest_height_cons { - match replay_block( - replay_height, - &payloads_repository, - &payload_validator, - &block_finalizer, - ) - .await - { - Ok(block) => { - latest_block = block; - if let Err(e) = persistence_meter - .wait_for_persisted_block( - latest_block.block_number, - REPLAY_PERSISTENCE_WAIT_TIMEOUT, - ) - .await - { - error!( - block_number = latest_block.block_number, - %e, - "🔄 Replay: persistence backpressure timed out, proceeding" - ); - } - } - Err(ReplayBlockError::PayloadMissing(h)) => { - warn!(height = %h, latest_height_cons = %latest_height_cons, "🔄 Handshake: payload missing, triggering checkpoint sync"); - - // This can happen when the EL does not shut down cleanly and does not flush a large - // number of blocks to disk, "lower" than what the CL has pruned to. In that case, no - // payload will be found to replay, since neither the CL's DB (due to pruning) or the - // EL (due to the bad shutdown) will have it. - // - // In this case, we take the decided blockhash from the last stored certificate, and - // trigger a checkpoint sync on the EL. - let certificate = certificates_repository - .get(latest_height_cons) - .await - .wrap_err("Handshake: failed to get certificate")? - .ok_or_else(|| { - eyre!("Handshake: no certificate at height {latest_height_cons}") - })?; - - let target_hash = certificate.certificate.value_id.block_hash(); - latest_block = checkpoint_sync( - target_hash, - latest_height_cons, - &engine_api, - ðereum_api, - None, - ) - .await?; - break; - } - Err(ReplayBlockError::Other(e)) => return Err(e), - } - - replay_height = replay_height.increment(); - } - - let replayed_blocks = latest_height_cons - .as_u64() - .saturating_sub(latest_height_el.as_u64()); - metrics.set_handshake_replay_blocks(replayed_blocks); - - let latest_height_el = Height::new(latest_block.block_number); - let next_height = latest_height_el.increment(); - - info!("🤝 Handshake complete: Next height will be {next_height}"); - - Ok(HandshakeResult { - previous_block: latest_block, - latest_height: latest_height_cons, - next_height, - }) -} - -/// Maximum number of pending proposals allowed -/// Defined to be equal to the size of the consensus input buffer, -/// which is itself sized to handle all in-flight sync responses. -fn max_pending_proposals(config: &ValueSyncConfig) -> usize { - let limit = config - .parallel_requests - .checked_mul(config.batch_size) - .expect("max_pending_proposals overflow"); - assert!(limit > 0, "max_pending_proposals must be greater than 0"); - limit -} - -/// Enforce pending proposals limit on startup. -/// Cleans up any excess proposals from previous runs. -async fn enforce_pending_proposals_limit( - pending_proposals_repository: impl PendingProposalsRepository, - metrics: &AppMetrics, - max_pending_proposals: usize, - current_height: Height, -) -> eyre::Result<()> { - pending_proposals_repository - .enforce_limit(max_pending_proposals, current_height) - .await - .wrap_err("failed to enforce pending proposals limit on startup")?; - - // Update metrics - let pending_count = pending_proposals_repository - .count() - .await - .wrap_err("failed to get pending proposals count after enforcing limit")?; - - metrics.observe_pending_proposal_parts_count(pending_count); - - Ok(()) -} - -#[cfg(test)] -mod tests { - use std::sync::Arc; - - use super::*; - use crate::finalize::MockBlockFinalizer; - use crate::metrics::AppMetrics; - use crate::payload::{MockPayloadValidator, PayloadValidationResult}; - use crate::store::repositories::mocks::{ - MockCertificatesRepository, MockPayloadsRepository, MockPendingProposalsRepository, - }; - - use alloy_primitives::{Address as AlloyAddress, Bloom, Bytes as AlloyBytes, U256}; - use alloy_rpc_types_engine::{ExecutionPayloadV1, ExecutionPayloadV2, ExecutionPayloadV3}; - use arc_consensus_types::{ValidatorSet, B256}; - use arc_eth_engine::capabilities::EngineCapabilities; - use arc_eth_engine::mocks::{MockEngineAPI, MockEthereumAPI, MockPersistenceMeter}; - use arc_eth_engine::persistence_meter::NoopPersistenceMeter; - use eyre::eyre; - use mockall::predicate::*; - - // Helper functions for creating test fixtures - fn test_execution_block(height: u64, timestamp: u64) -> ExecutionBlock { - ExecutionBlock { - block_hash: B256::repeat_byte((height % 256) as u8), - block_number: height, - parent_hash: if height > 0 { - B256::repeat_byte(((height - 1) % 256) as u8) - } else { - B256::ZERO - }, - timestamp, - } - } - - fn test_payload(height: u64, timestamp: u64) -> ExecutionPayloadV3 { - ExecutionPayloadV3 { - payload_inner: ExecutionPayloadV2 { - payload_inner: ExecutionPayloadV1 { - parent_hash: if height > 0 { - B256::repeat_byte(((height - 1) % 256) as u8) - } else { - B256::ZERO - }, - fee_recipient: AlloyAddress::ZERO, - state_root: B256::ZERO, - receipts_root: B256::ZERO, - logs_bloom: Bloom::default(), - prev_randao: B256::ZERO, - block_number: height, - gas_limit: 30000000, - gas_used: 0, - timestamp, - extra_data: AlloyBytes::default(), - base_fee_per_gas: U256::from(1u64), - block_hash: B256::repeat_byte((height % 256) as u8), - transactions: vec![], - }, - withdrawals: vec![], - }, - blob_gas_used: 0, - excess_blob_gas: 0, - } - } - - fn test_validator_set() -> ValidatorSet { - ValidatorSet { - validators: Arc::new(vec![]), - } - } - - fn test_consensus_params() -> ConsensusParams { - ConsensusParams::default() - } - - fn test_metrics() -> AppMetrics { - AppMetrics::default() - } - - fn setup_mock_engine_api_success() -> MockEngineAPI { - let mut engine_api = MockEngineAPI::new(); - engine_api - .expect_exchange_capabilities() - .return_once(|| Ok(EngineCapabilities::all())); - engine_api - } - - fn setup_mock_ethereum_api_with_block( - block: ExecutionBlock, - validator_set_height: u64, - ) -> MockEthereumAPI { - let mut ethereum_api = MockEthereumAPI::new(); - ethereum_api - .expect_get_block_by_number() - .with(eq("latest")) - .returning(move |_| Ok(Some(block))); - ethereum_api - .expect_get_active_validator_set() - .with(eq(validator_set_height)) - .returning(|_| Ok(test_validator_set())); - ethereum_api - .expect_get_consensus_params() - .with(eq(validator_set_height)) - .returning(|_| Ok(test_consensus_params())); - ethereum_api - } - - fn setup_mock_ethereum_api_no_block() -> MockEthereumAPI { - let mut ethereum_api = MockEthereumAPI::new(); - ethereum_api - .expect_get_block_by_number() - .with(eq("latest")) - .returning(|_| Ok(None)); - ethereum_api - } - - // Test 1: Exact sync (EL == CL) - #[tokio::test] - async fn test_exact_sync_no_replay_needed() { - let el_height = 5u64; - let cl_height = Height::new(el_height); - let latest_block = test_execution_block(el_height, 1000); - let expected_replayed = 0u64; - - let mut payload_validator = MockPayloadValidator::new(); - payload_validator.expect_validate_payload().never(); - - let mut block_finalizer = MockBlockFinalizer::new(); - block_finalizer.expect_finalize_decided_block().never(); - - let engine_api = setup_mock_engine_api_success(); - let ethereum_api = setup_mock_ethereum_api_with_block(latest_block, el_height); - - let mut certificates_repo = MockCertificatesRepository::new(); - certificates_repo - .expect_max_height() - .return_once(move || Ok(Some(cl_height))); - - let mut payloads_repo = MockPayloadsRepository::new(); - payloads_repo.expect_get().never(); - - let metrics = test_metrics(); - - let result = handshake_and_replay( - &payload_validator, - &block_finalizer, - &engine_api, - ðereum_api, - &certificates_repo, - &payloads_repo, - NoopPersistenceMeter, - &metrics, - ) - .await; - - let HandshakeResult { - previous_block, - latest_height, - next_height, - } = result.unwrap(); - - assert_eq!(previous_block.block_number, el_height); - assert_eq!(latest_height, cl_height); - assert_eq!(next_height, cl_height.increment()); - assert_eq!(metrics.get_handshake_replay_blocks(), expected_replayed); - } - - // Test 2: Single block replay (EL + 1 == CL) - #[tokio::test] - async fn test_single_block_replay() { - let el_height = 4u64; - let cl_height = Height::new(el_height + 1); - let latest_block_el = test_execution_block(el_height, 1000); - let replayed_block = test_execution_block(cl_height.as_u64(), 1100); - let payload_to_replay = test_payload(cl_height.as_u64(), 1100); - let expected_replayed = 1u64; - - let mut payload_validator = MockPayloadValidator::new(); - payload_validator - .expect_validate_payload() - .return_once(move |p| { - assert_eq!( - p.payload_inner.payload_inner.block_number, - cl_height.as_u64() - ); - - Ok(PayloadValidationResult::Valid) - }); - - let mut block_finalizer = MockBlockFinalizer::new(); - block_finalizer - .expect_finalize_decided_block() - .return_once(move |h, p| { - assert_eq!(h, cl_height); - assert_eq!( - p.payload_inner.payload_inner.block_number, - cl_height.as_u64() - ); - - Ok((replayed_block, replayed_block.block_hash)) - }); - - let engine_api = setup_mock_engine_api_success(); - let ethereum_api = setup_mock_ethereum_api_with_block(latest_block_el, cl_height.as_u64()); - - let mut certificates_repo = MockCertificatesRepository::new(); - certificates_repo - .expect_max_height() - .return_once(move || Ok(Some(cl_height))); - - let mut payloads_repo = MockPayloadsRepository::new(); - payloads_repo.expect_get().return_once(move |h| { - assert_eq!(h, cl_height); - Ok(Some(payload_to_replay)) - }); - - let metrics = test_metrics(); - - let result = handshake_and_replay( - &payload_validator, - &block_finalizer, - &engine_api, - ðereum_api, - &certificates_repo, - &payloads_repo, - NoopPersistenceMeter, - &metrics, - ) - .await; - - let HandshakeResult { - previous_block, - latest_height, - next_height, - } = result.unwrap(); - - assert_eq!(previous_block.block_number, cl_height.as_u64()); - assert_eq!(latest_height, cl_height); - assert_eq!(next_height, cl_height.increment()); - assert_eq!(metrics.get_handshake_replay_blocks(), expected_replayed); - } - - #[tokio::test] - async fn test_replay_checks_persistence_after_each_replayed_block_for_small_gap() { - let el_height = 3u64; - let cl_height = Height::new(5); - let latest_block_el = test_execution_block(el_height, 1000); - - let mut payload_validator = MockPayloadValidator::new(); - payload_validator - .expect_validate_payload() - .times(2) - .returning(|_| Ok(PayloadValidationResult::Valid)); - - let mut block_finalizer = MockBlockFinalizer::new(); - block_finalizer - .expect_finalize_decided_block() - .times(2) - .returning(|height, _| { - let h = height.as_u64(); - let block = test_execution_block(h, 1000 + h); - Ok((block, block.block_hash)) - }); - - let engine_api = setup_mock_engine_api_success(); - let ethereum_api = setup_mock_ethereum_api_with_block(latest_block_el, cl_height.as_u64()); - - let mut certificates_repo = MockCertificatesRepository::new(); - certificates_repo - .expect_max_height() - .return_once(move || Ok(Some(cl_height))); - - let mut payloads_repo = MockPayloadsRepository::new(); - payloads_repo - .expect_get() - .times(2) - .returning(|height| Ok(Some(test_payload(height.as_u64(), 1000 + height.as_u64())))); - - let mut persistence_meter = MockPersistenceMeter::new(); - let mut sequence = mockall::Sequence::new(); - persistence_meter - .expect_wait_for_persisted_block() - .withf(|&block, _| block == 4) - .times(1) - .in_sequence(&mut sequence) - .return_once(|_, _| Ok(())); - persistence_meter - .expect_wait_for_persisted_block() - .withf(|&block, _| block == 5) - .times(1) - .in_sequence(&mut sequence) - .return_once(|_, _| Ok(())); - - let metrics = test_metrics(); - - let result = handshake_and_replay( - &payload_validator, - &block_finalizer, - &engine_api, - ðereum_api, - &certificates_repo, - &payloads_repo, - &persistence_meter, - &metrics, - ) - .await; - - let HandshakeResult { previous_block, .. } = result.unwrap(); - assert_eq!(previous_block.block_number, cl_height.as_u64()); - } - - #[tokio::test] - async fn test_replay_checks_persistence_after_each_replayed_block() { - let el_height = 3u64; - let cl_height = Height::new(13); - let latest_block_el = test_execution_block(el_height, 1000); - - let mut payload_validator = MockPayloadValidator::new(); - payload_validator - .expect_validate_payload() - .times(10) - .returning(|_| Ok(PayloadValidationResult::Valid)); - - let mut block_finalizer = MockBlockFinalizer::new(); - block_finalizer - .expect_finalize_decided_block() - .times(10) - .returning(|height, _| { - let h = height.as_u64(); - let block = test_execution_block(h, 1000 + h); - Ok((block, block.block_hash)) - }); - - let engine_api = setup_mock_engine_api_success(); - let ethereum_api = setup_mock_ethereum_api_with_block(latest_block_el, cl_height.as_u64()); - - let mut certificates_repo = MockCertificatesRepository::new(); - certificates_repo - .expect_max_height() - .return_once(move || Ok(Some(cl_height))); - - let mut payloads_repo = MockPayloadsRepository::new(); - payloads_repo - .expect_get() - .times(10) - .returning(|height| Ok(Some(test_payload(height.as_u64(), 1000 + height.as_u64())))); - - let mut persistence_meter = MockPersistenceMeter::new(); - let mut sequence = mockall::Sequence::new(); - for height in 4u64..=13 { - persistence_meter - .expect_wait_for_persisted_block() - .withf(move |&block, _| block == height) - .times(1) - .in_sequence(&mut sequence) - .return_once(|_, _| Ok(())); - } - - let metrics = test_metrics(); - - let result = handshake_and_replay( - &payload_validator, - &block_finalizer, - &engine_api, - ðereum_api, - &certificates_repo, - &payloads_repo, - &persistence_meter, - &metrics, - ) - .await; - - let HandshakeResult { previous_block, .. } = result.unwrap(); - assert_eq!(previous_block.block_number, cl_height.as_u64()); - } - - #[tokio::test] - async fn test_replay_proceeds_when_persistence_meter_fails() { - let el_height = 4u64; - let cl_height = Height::new(5); - let latest_block_el = test_execution_block(el_height, 1000); - let payload_to_replay = test_payload(5, 1100); - - let mut payload_validator = MockPayloadValidator::new(); - payload_validator - .expect_validate_payload() - .return_once(|_| Ok(PayloadValidationResult::Valid)); - - let mut block_finalizer = MockBlockFinalizer::new(); - block_finalizer - .expect_finalize_decided_block() - .return_once(|height, _| { - let h = height.as_u64(); - let block = test_execution_block(h, 1000 + h); - Ok((block, block.block_hash)) - }); - - let engine_api = setup_mock_engine_api_success(); - let ethereum_api = setup_mock_ethereum_api_with_block(latest_block_el, cl_height.as_u64()); - - let mut certificates_repo = MockCertificatesRepository::new(); - certificates_repo - .expect_max_height() - .return_once(move || Ok(Some(cl_height))); - - let mut payloads_repo = MockPayloadsRepository::new(); - payloads_repo - .expect_get() - .return_once(move |_| Ok(Some(payload_to_replay))); - - let mut persistence_meter = MockPersistenceMeter::new(); - persistence_meter - .expect_wait_for_persisted_block() - .withf(|&block, _| block == 5) - .times(1) - .return_once(|_, _| Err(eyre!("persistence meter failed"))); - - let metrics = test_metrics(); - - let result = handshake_and_replay( - &payload_validator, - &block_finalizer, - &engine_api, - ðereum_api, - &certificates_repo, - &payloads_repo, - &persistence_meter, - &metrics, - ) - .await; - - // Meter error is logged but replay proceeds - let HandshakeResult { previous_block, .. } = result.unwrap(); - assert_eq!(previous_block.block_number, cl_height.as_u64()); - } - - async fn do_multiple_block_replay(num_blocks: usize) { - let el_height = 3u64; - let cl_height = Height::new(el_height + num_blocks as u64); - let latest_block_el = test_execution_block(el_height, 1000); - let expected_replayed = num_blocks as u64; - - let mut payload_validator = MockPayloadValidator::new(); - payload_validator - .expect_validate_payload() - .times(num_blocks) - .returning(|_| Ok(PayloadValidationResult::Valid)); - - let mut block_finalizer = MockBlockFinalizer::new(); - block_finalizer - .expect_finalize_decided_block() - .times(num_blocks) - .returning(|height, _| { - let h = height.as_u64(); - let block = test_execution_block(h, 1000 + h); - Ok((block, block.block_hash)) - }); - - let engine_api = setup_mock_engine_api_success(); - let ethereum_api = setup_mock_ethereum_api_with_block(latest_block_el, cl_height.as_u64()); - - let mut certificates_repo = MockCertificatesRepository::new(); - certificates_repo - .expect_max_height() - .return_once(move || Ok(Some(cl_height))); - - let mut payloads_repo = MockPayloadsRepository::new(); - payloads_repo - .expect_get() - .times(num_blocks) - .returning(|height| Ok(Some(test_payload(height.as_u64(), 1000 + height.as_u64())))); - - let metrics = test_metrics(); - - let result = handshake_and_replay( - &payload_validator, - &block_finalizer, - &engine_api, - ðereum_api, - &certificates_repo, - &payloads_repo, - NoopPersistenceMeter, - &metrics, - ) - .await; - - let HandshakeResult { - previous_block, - latest_height, - next_height, - } = result.unwrap(); - - assert_eq!(previous_block.block_number, cl_height.as_u64()); - assert_eq!(latest_height, cl_height); - assert_eq!(next_height, cl_height.increment()); - assert_eq!(metrics.get_handshake_replay_blocks(), expected_replayed); - } - - // Test 3: Multiple block replay (EL + i == CL) - #[tokio::test] - async fn test_multiple_block_replay() { - for i in 1..=10 { - println!("multiple_block_replay: Running test with {i} blocks"); - do_multiple_block_replay(i).await; - } - } - - // Test 4: Fresh start (both at genesis) - #[tokio::test] - async fn test_fresh_start_at_genesis() { - let el_height = 0u64; - let cl_height = Height::new(0); - let genesis_block = test_execution_block(el_height, 0); - - let mut payload_validator = MockPayloadValidator::new(); - payload_validator.expect_validate_payload().never(); - - let mut block_finalizer = MockBlockFinalizer::new(); - block_finalizer.expect_finalize_decided_block().never(); - - let engine_api = setup_mock_engine_api_success(); - let ethereum_api = setup_mock_ethereum_api_with_block(genesis_block, el_height); - - let mut certificates_repo = MockCertificatesRepository::new(); - certificates_repo - .expect_max_height() - .return_once(move || Ok(Some(cl_height))); - - let mut payloads_repo = MockPayloadsRepository::new(); - payloads_repo.expect_get().never(); - - let metrics = test_metrics(); - - let result = handshake_and_replay( - &payload_validator, - &block_finalizer, - &engine_api, - ðereum_api, - &certificates_repo, - &payloads_repo, - NoopPersistenceMeter, - &metrics, - ) - .await; - - let HandshakeResult { - previous_block, - latest_height, - next_height, - } = result.unwrap(); - - assert_eq!(previous_block.block_number, 0); - assert_eq!(latest_height, Height::new(0)); - assert_eq!(next_height, Height::new(1)); - } - - // Test 5: EL ahead of CL (generic case — both have data but EL is further) - #[tokio::test] - async fn test_el_ahead_of_cl_error() { - let el_height = 10u64; - let cl_height = Height::new(5); - let latest_block_el = test_execution_block(el_height, 1000); - - let payload_validator = MockPayloadValidator::new(); - let block_finalizer = MockBlockFinalizer::new(); - let engine_api = setup_mock_engine_api_success(); - let ethereum_api = setup_mock_ethereum_api_with_block(latest_block_el, cl_height.as_u64()); - - let mut certificates_repo = MockCertificatesRepository::new(); - certificates_repo - .expect_max_height() - .return_once(move || Ok(Some(cl_height))); - - let payloads_repo = MockPayloadsRepository::new(); - let metrics = test_metrics(); - - let result = handshake_and_replay( - &payload_validator, - &block_finalizer, - &engine_api, - ðereum_api, - &certificates_repo, - &payloads_repo, - NoopPersistenceMeter, - &metrics, - ) - .await; - - let err_msg = result.unwrap_err().to_string(); - assert!(err_msg.contains("inconsistent state")); - assert!( - err_msg.contains("arc-snapshots download"), - "should suggest re-downloading snapshots" - ); - } - - // Test 5b: EL has data but CL is at height 0 (missing CL snapshot) - #[tokio::test] - async fn test_el_ahead_of_cl_missing_snapshot() { - let el_height = 10u64; - let latest_block_el = test_execution_block(el_height, 1000); - - let payload_validator = MockPayloadValidator::new(); - let block_finalizer = MockBlockFinalizer::new(); - let engine_api = setup_mock_engine_api_success(); - let ethereum_api = - setup_mock_ethereum_api_with_block(latest_block_el, Height::default().as_u64()); - - // CL has no data — max_height returns None → defaults to Height(0) - let mut certificates_repo = MockCertificatesRepository::new(); - certificates_repo - .expect_max_height() - .return_once(move || Ok(None)); - - let payloads_repo = MockPayloadsRepository::new(); - let metrics = test_metrics(); - - let result = handshake_and_replay( - &payload_validator, - &block_finalizer, - &engine_api, - ðereum_api, - &certificates_repo, - &payloads_repo, - NoopPersistenceMeter, - &metrics, - ) - .await; - - let err_msg = result.unwrap_err().to_string(); - assert!( - err_msg.contains("CL snapshot is missing"), - "should identify missing CL snapshot, got: {err_msg}" - ); - assert!( - err_msg.contains("arc-node-consensus download"), - "should suggest downloading CL snapshot, got: {err_msg}" - ); - } - - // Test 6: Missing latest block from EL - #[tokio::test] - async fn test_missing_latest_block_from_el() { - let payload_validator = MockPayloadValidator::new(); - let block_finalizer = MockBlockFinalizer::new(); - let engine_api = setup_mock_engine_api_success(); - let ethereum_api = setup_mock_ethereum_api_no_block(); - - let mut certificates_repo = MockCertificatesRepository::new(); - certificates_repo.expect_max_height().never(); - - let payloads_repo = MockPayloadsRepository::new(); - let metrics = test_metrics(); - - let result = handshake_and_replay( - &payload_validator, - &block_finalizer, - &engine_api, - ðereum_api, - &certificates_repo, - &payloads_repo, - NoopPersistenceMeter, - &metrics, - ) - .await; - - let err_msg = result.unwrap_err().to_string(); - assert!(err_msg.contains("Could not get latest block from execution client")); - } - - // Test 7: Missing payload during replay triggers checkpoint sync, which fails - // if the CL's latest certificate is also missing (corrupted CL DB) - #[tokio::test] - async fn test_missing_payload_and_certificate_during_replay() { - let el_height = 4u64; - let cl_height = Height::new(5); - let latest_block_el = test_execution_block(el_height, 1000); - - let payload_validator = MockPayloadValidator::new(); - let block_finalizer = MockBlockFinalizer::new(); - let engine_api = setup_mock_engine_api_success(); - let ethereum_api = setup_mock_ethereum_api_with_block(latest_block_el, cl_height.as_u64()); - - let mut certificates_repo = MockCertificatesRepository::new(); - certificates_repo - .expect_max_height() - .return_once(move || Ok(Some(cl_height))); - - // No payload found for replay height - let mut payloads_repo = MockPayloadsRepository::new(); - payloads_repo.expect_get().times(1).returning(|_| Ok(None)); - - // Will fallback to checkpoint sync, but no certificate found either - certificates_repo.expect_get().return_once(move |height| { - assert_eq!(height, cl_height); - Ok(None) - }); - - let metrics = test_metrics(); - - let result = handshake_and_replay( - &payload_validator, - &block_finalizer, - &engine_api, - ðereum_api, - &certificates_repo, - &payloads_repo, - NoopPersistenceMeter, - &metrics, - ) - .await; - - let err_msg = result.unwrap_err().to_string(); - assert!(err_msg.contains("no certificate at height")); - } - - // Test 8: Invalid payload during replay - #[tokio::test] - async fn test_invalid_payload_during_replay() { - let el_height = 4u64; - let cl_height = Height::new(5); - let latest_block_el = test_execution_block(el_height, 1000); - let payload_to_replay = test_payload(5, 1100); - - let mut payload_validator = MockPayloadValidator::new(); - payload_validator - .expect_validate_payload() - .return_once(|_| { - Ok(PayloadValidationResult::Invalid { - reason: "test".into(), - }) - }); - - let block_finalizer = MockBlockFinalizer::new(); - let engine_api = setup_mock_engine_api_success(); - let ethereum_api = setup_mock_ethereum_api_with_block(latest_block_el, cl_height.as_u64()); - - let mut certificates_repo = MockCertificatesRepository::new(); - certificates_repo - .expect_max_height() - .return_once(move || Ok(Some(cl_height))); - - let mut payloads_repo = MockPayloadsRepository::new(); - payloads_repo - .expect_get() - .return_once(move |_| Ok(Some(payload_to_replay.clone()))); - - let metrics = test_metrics(); - - let result = handshake_and_replay( - &payload_validator, - &block_finalizer, - &engine_api, - ðereum_api, - &certificates_repo, - &payloads_repo, - NoopPersistenceMeter, - &metrics, - ) - .await; - - let err_msg = result.unwrap_err().to_string(); - assert!(err_msg.contains("Execution payload validation failed")); - } - - // Test 9: Finalization failure during replay - #[tokio::test] - async fn test_finalization_failure_during_replay() { - let el_height = 4u64; - let cl_height = Height::new(5); - let latest_block_el = test_execution_block(el_height, 1000); - let payload_to_replay = test_payload(5, 1100); - - let mut payload_validator = MockPayloadValidator::new(); - payload_validator - .expect_validate_payload() - .return_once(|_| Ok(PayloadValidationResult::Valid)); - - let mut block_finalizer = MockBlockFinalizer::new(); - block_finalizer - .expect_finalize_decided_block() - .return_once(|_, _| Err(eyre!("Finalization failed"))); - - let engine_api = setup_mock_engine_api_success(); - let ethereum_api = setup_mock_ethereum_api_with_block(latest_block_el, cl_height.as_u64()); - - let mut certificates_repo = MockCertificatesRepository::new(); - certificates_repo - .expect_max_height() - .return_once(move || Ok(Some(cl_height))); - - let mut payloads_repo = MockPayloadsRepository::new(); - payloads_repo - .expect_get() - .return_once(move |_| Ok(Some(payload_to_replay))); - - let metrics = test_metrics(); - - let result = handshake_and_replay( - &payload_validator, - &block_finalizer, - &engine_api, - ðereum_api, - &certificates_repo, - &payloads_repo, - NoopPersistenceMeter, - &metrics, - ) - .await; - - let err_msg = result.unwrap_err().to_string(); - assert!(err_msg.contains("Failed to finalize block")); - } - - // Test 10: Capability check failure - #[tokio::test] - async fn test_capability_check_failure() { - let payload_validator = MockPayloadValidator::new(); - let block_finalizer = MockBlockFinalizer::new(); - let mut engine_api = MockEngineAPI::new(); - engine_api - .expect_exchange_capabilities() - .return_once(|| Err(eyre!("Capability check failed"))); - - let ethereum_api = setup_mock_ethereum_api_with_block(test_execution_block(5, 1000), 5); - - let certificates_repo = MockCertificatesRepository::new(); - let payloads_repo = MockPayloadsRepository::new(); - let metrics = test_metrics(); - - let result = handshake_and_replay( - &payload_validator, - &block_finalizer, - &engine_api, - ðereum_api, - &certificates_repo, - &payloads_repo, - NoopPersistenceMeter, - &metrics, - ) - .await; - - let err_msg = result.unwrap_err().to_string(); - assert!(err_msg.contains("check_capabilities")); - } - - // Test 11: Empty consensus DB - #[tokio::test] - async fn test_empty_consensus_db() { - let el_height = 0u64; - let genesis_block = test_execution_block(el_height, 0); - - let mut payload_validator = MockPayloadValidator::new(); - payload_validator.expect_validate_payload().never(); - - let mut block_finalizer = MockBlockFinalizer::new(); - block_finalizer.expect_finalize_decided_block().never(); - - let engine_api = setup_mock_engine_api_success(); - let ethereum_api = setup_mock_ethereum_api_with_block(genesis_block, el_height); - - let mut certificates_repo = MockCertificatesRepository::new(); - certificates_repo - .expect_max_height() - .return_once(|| Ok(None)); // Empty DB returns None - - let mut payloads_repo = MockPayloadsRepository::new(); - payloads_repo.expect_get().never(); - - let metrics = test_metrics(); - - let result = handshake_and_replay( - &payload_validator, - &block_finalizer, - &engine_api, - ðereum_api, - &certificates_repo, - &payloads_repo, - NoopPersistenceMeter, - &metrics, - ) - .await; - - let HandshakeResult { - previous_block, - latest_height, - next_height, - } = result.unwrap(); - - assert_eq!(previous_block.block_number, 0); - assert_eq!(latest_height, Height::new(0)); // Defaults to 0 - assert_eq!(next_height, Height::new(1)); - } - - // Test 12: Large gap replay - #[tokio::test] - async fn test_large_gap_replay() { - let el_height = 0u64; - let cl_height = Height::new(20); - let latest_block_el = test_execution_block(el_height, 0); - - let mut payload_validator = MockPayloadValidator::new(); - payload_validator - .expect_validate_payload() - .times(20) // Heights 1 to 20 - .returning(|_| Ok(PayloadValidationResult::Valid)); - - let mut block_finalizer = MockBlockFinalizer::new(); - block_finalizer - .expect_finalize_decided_block() - .times(20) - .returning(|height, _| { - let h = height.as_u64(); - let block = test_execution_block(h, h * 100); - Ok((block, block.block_hash)) - }); - - let engine_api = setup_mock_engine_api_success(); - let ethereum_api = setup_mock_ethereum_api_with_block(latest_block_el, cl_height.as_u64()); - - let mut certificates_repo = MockCertificatesRepository::new(); - certificates_repo - .expect_max_height() - .return_once(move || Ok(Some(cl_height))); - - let mut payloads_repo = MockPayloadsRepository::new(); - payloads_repo - .expect_get() - .times(20) - .returning(|height| Ok(Some(test_payload(height.as_u64(), height.as_u64() * 100)))); - - let metrics = test_metrics(); - - let result = handshake_and_replay( - &payload_validator, - &block_finalizer, - &engine_api, - ðereum_api, - &certificates_repo, - &payloads_repo, - NoopPersistenceMeter, - &metrics, - ) - .await; - - let HandshakeResult { - previous_block, - latest_height, - next_height, - } = result.unwrap(); - - assert_eq!(previous_block.block_number, 20); - assert_eq!(latest_height, cl_height); - assert_eq!(next_height, Height::new(21)); - } - - // Test 17: Repository errors propagate correctly - #[tokio::test] - async fn test_certificates_repo_error_propagates() { - let payload_validator = MockPayloadValidator::new(); - let block_finalizer = MockBlockFinalizer::new(); - let engine_api = setup_mock_engine_api_success(); - let ethereum_api = setup_mock_ethereum_api_with_block(test_execution_block(5, 1000), 5); - - let mut certificates_repo = MockCertificatesRepository::new(); - certificates_repo - .expect_max_height() - .return_once(|| Err(std::io::Error::other("Decided blocks fetch error"))); - - let payloads_repo = MockPayloadsRepository::new(); - let metrics = test_metrics(); - - let result = handshake_and_replay( - &payload_validator, - &block_finalizer, - &engine_api, - ðereum_api, - &certificates_repo, - &payloads_repo, - NoopPersistenceMeter, - &metrics, - ) - .await; - - let err_msg = result.unwrap_err().to_string(); - assert!(err_msg.contains("failed to get latest consensus height")); - } - - #[tokio::test] - async fn test_payloads_repo_error_propagates() { - let el_height = 4u64; - let cl_height = Height::new(5); - let latest_block_el = test_execution_block(el_height, 1000); - - let payload_validator = MockPayloadValidator::new(); - let block_finalizer = MockBlockFinalizer::new(); - let engine_api = setup_mock_engine_api_success(); - let ethereum_api = setup_mock_ethereum_api_with_block(latest_block_el, cl_height.as_u64()); - - let mut certificates_repo = MockCertificatesRepository::new(); - certificates_repo - .expect_max_height() - .return_once(move || Ok(Some(cl_height))); - - let mut payloads_repo = MockPayloadsRepository::new(); - payloads_repo - .expect_get() - .return_once(|_| Err(std::io::Error::other("Payload fetch error"))); - - let metrics = test_metrics(); - - let result = handshake_and_replay( - &payload_validator, - &block_finalizer, - &engine_api, - ðereum_api, - &certificates_repo, - &payloads_repo, - NoopPersistenceMeter, - &metrics, - ) - .await; - - let err_msg = result.unwrap_err().to_string(); - assert!( - err_msg.contains("Replay: failed to fetch payload"), - "Expected payload fetch error, got: {err_msg}" - ); - } - - #[tokio::test] - async fn test_on_consensus_ready_fallback_to_default_params() { - let metrics = test_metrics(); - - let mut pending_proposals_repo = MockPendingProposalsRepository::new(); - pending_proposals_repo - .expect_enforce_limit() - .returning(|_, _| Ok(Vec::new())); - pending_proposals_repo.expect_count().returning(|| Ok(0)); - - let payloads_repo = MockPayloadsRepository::new(); - let mut certificates_repo = MockCertificatesRepository::new(); - let payload_validator = MockPayloadValidator::new(); - let block_finalizer = MockBlockFinalizer::new(); - let engine_api = setup_mock_engine_api_success(); - - let latest_height = Height::new(5); - let latest_block = test_execution_block(latest_height.as_u64(), 1000); - - certificates_repo - .expect_max_height() - .return_once(move || Ok(Some(latest_height))); - - let mut ethereum_api = MockEthereumAPI::new(); - ethereum_api - .expect_get_block_by_number() - .with(eq("latest")) - .returning(move |_| Ok(Some(latest_block))); - ethereum_api - .expect_get_active_validator_set() - .with(eq(latest_height.as_u64())) - .returning(|_| Ok(test_validator_set())); - ethereum_api - .expect_get_consensus_params() - .with(eq(latest_height.as_u64())) - .returning(|_| Err(eyre!("mock get_consensus_params failed"))); - - let (next_height, _, consensus_params, _) = on_consensus_ready( - &metrics, - pending_proposals_repo, - payloads_repo, - certificates_repo, - payload_validator, - block_finalizer, - &engine_api, - ðereum_api, - NoopPersistenceMeter, - 10, - ) - .await - .unwrap(); - - assert_eq!(next_height, latest_height.increment()); - assert_eq!(consensus_params, ConsensusParams::default()); - } - - // Test: Partial replay then checkpoint sync - // Replays heights 5..7 successfully, payload missing at height 8, - // triggers checkpoint sync from that point. - // This also covers the case where height 5 (or initial height) does not - // have a payload in the repository. - #[tokio::test] - async fn test_partial_replay_then_checkpoint_sync() { - use alloy_rpc_types_engine::{ForkchoiceUpdated, PayloadStatus, PayloadStatusEnum}; - use arc_consensus_types::{CommitCertificateType, Round, StoredCommitCertificate, ValueId}; - use malachitebft_core_types::CommitCertificate; - - // Replay covers heights [5, 10] - let el_height = 4u64; - let cl_height = Height::new(10); - - // Check point block hash - let check_point_block_hash = B256::repeat_byte(0xAA); - let check_point_block = ExecutionBlock { - block_hash: check_point_block_hash, - block_number: cl_height.as_u64(), - parent_hash: B256::repeat_byte(0x09), - timestamp: 2000, - }; - let latest_block_el = test_execution_block(el_height, 1000); - - // Heights 5, 6, 7 replay successfully; height 8 is missing - let mut payload_validator = MockPayloadValidator::new(); - payload_validator - .expect_validate_payload() - .times(3) - .returning(|_| Ok(PayloadValidationResult::Valid)); - - let mut block_finalizer = MockBlockFinalizer::new(); - block_finalizer - .expect_finalize_decided_block() - .times(3) - .returning(|height, _| { - assert!(height >= Height::new(5) && height <= Height::new(7)); - let h = height.as_u64(); - let block = test_execution_block(h, 1000 + h); - Ok((block, block.block_hash)) - }); - - let mut engine_api = setup_mock_engine_api_success(); - engine_api - .expect_forkchoice_updated() - .return_once(move |_, _| { - Ok(ForkchoiceUpdated::new(PayloadStatus::new( - PayloadStatusEnum::Syncing, - None, - ))) - }); - - let mut ethereum_api = MockEthereumAPI::new(); - // Handshake fetches "latest" for the initial EL height - ethereum_api - .expect_get_block_by_number() - .with(eq("latest")) - .returning(move |_| Ok(Some(latest_block_el))); - // Checkpoint sync polls by target height - ethereum_api - .expect_get_block_by_number() - .with(eq("0xa")) - .returning(move |_| Ok(Some(check_point_block))); - ethereum_api - .expect_get_active_validator_set() - .with(eq(cl_height.as_u64())) - .returning(|_| Ok(test_validator_set())); - ethereum_api - .expect_get_consensus_params() - .with(eq(cl_height.as_u64())) - .returning(|_| Ok(test_consensus_params())); - - // Simulate having a cert at the latest height - let mut certificates_repo = MockCertificatesRepository::new(); - certificates_repo - .expect_max_height() - .return_once(move || Ok(Some(cl_height))); - certificates_repo.expect_get().return_once(move |_| { - Ok(Some(StoredCommitCertificate { - certificate: CommitCertificate::new( - cl_height, - Round::new(0), - ValueId::new(check_point_block_hash), - vec![], - ), - certificate_type: CommitCertificateType::Minimal, - proposer: None, - })) - }); - - let mut payloads_repo = MockPayloadsRepository::new(); - payloads_repo - .expect_get() - .times(4) // 3 successful + 1 missing at height 8 - .returning(|height| { - let h = height.as_u64(); - if h <= 7 { - Ok(Some(test_payload(h, 1000 + h))) - } else { - Ok(None) - } - }); - - let metrics = test_metrics(); - - let result = handshake_and_replay( - &payload_validator, - &block_finalizer, - &engine_api, - ðereum_api, - &certificates_repo, - &payloads_repo, - NoopPersistenceMeter, - &metrics, - ) - .await; - - let HandshakeResult { - previous_block, - latest_height, - next_height, - } = result.unwrap(); - - assert_eq!(previous_block.block_hash, check_point_block_hash); - assert_eq!(previous_block.block_number, cl_height.as_u64()); - assert_eq!(latest_height, cl_height); - assert_eq!(next_height, cl_height.increment()); - } - - // Test: checkpoint_sync returns once target block matches the target hash - #[tokio::test] - async fn test_checkpoint_sync_polls_until_match() { - use alloy_rpc_types_engine::{ForkchoiceUpdated, PayloadStatus, PayloadStatusEnum}; - - let target_height = Height::new(50); - let check_point_block_hash = B256::repeat_byte(0xBB); - let check_point_block = ExecutionBlock { - block_hash: check_point_block_hash, - block_number: 50, - parent_hash: B256::repeat_byte(0x31), - timestamp: 5000, - }; - - let mut engine_api = MockEngineAPI::new(); - engine_api - .expect_forkchoice_updated() - .return_once(move |_, _| { - Ok(ForkchoiceUpdated::new(PayloadStatus::new( - PayloadStatusEnum::Syncing, - None, - ))) - }); - - let mut ethereum_api = MockEthereumAPI::new(); - let mut poll_count = 0u32; - ethereum_api - .expect_get_block_by_number() - .with(eq("0x32")) - .returning(move |_| { - poll_count += 1; - if poll_count < 3 { - // First two polls: block not yet available at target height - Ok(None) - } else { - // Third poll: matches target - Ok(Some(check_point_block)) - } - }); - - let result = checkpoint_sync( - check_point_block_hash, - target_height, - &engine_api, - ðereum_api, - None, - ) - .await; - - let block = result.unwrap(); - assert_eq!(block.block_hash, check_point_block_hash); - assert_eq!(block.block_number, 50); - } - - // Test: checkpoint_sync times out if the EL never reaches the target block - #[tokio::test(start_paused = true)] - async fn test_checkpoint_sync_times_out() { - use alloy_rpc_types_engine::{ForkchoiceUpdated, PayloadStatus, PayloadStatusEnum}; - - let target_hash = B256::repeat_byte(0xCC); - let mut engine_api = MockEngineAPI::new(); - engine_api.expect_forkchoice_updated().return_once(|_, _| { - Ok(ForkchoiceUpdated::new(PayloadStatus::new( - PayloadStatusEnum::Syncing, - None, - ))) - }); - - let mut ethereum_api = MockEthereumAPI::new(); - // Block at target height is never available, forcing a timeout - ethereum_api - .expect_get_block_by_number() - .with(eq("0x32")) - .returning(|_| Ok(None)); - - let timeout = Some(Duration::from_secs(10)); - let result = checkpoint_sync( - target_hash, - Height::new(50), - &engine_api, - ðereum_api, - timeout, - ) - .await; - - assert!(result.is_err()); - let err_msg = result.unwrap_err().to_string(); - assert!(err_msg.contains("timed out")); - } - - // Test: checkpoint_sync fails on unexpected FCU status - #[tokio::test] - async fn test_checkpoint_sync_unexpected_fcu_status() { - use alloy_rpc_types_engine::{ForkchoiceUpdated, PayloadStatus, PayloadStatusEnum}; - - let target_hash = B256::repeat_byte(0xDD); - let mut engine_api = MockEngineAPI::new(); - engine_api.expect_forkchoice_updated().return_once(|_, _| { - Ok(ForkchoiceUpdated::new(PayloadStatus::new( - PayloadStatusEnum::Invalid { - validation_error: "block not found".to_string(), - }, - None, - ))) - }); - - let ethereum_api = MockEthereumAPI::new(); - - let result = checkpoint_sync( - target_hash, - Height::new(100), - &engine_api, - ðereum_api, - None, - ) - .await; - - assert!(result.is_err()); - let err_msg = result.unwrap_err().to_string(); - assert!(err_msg.contains("unexpected FCU status")); - } - - // Test: checkpoint_sync fails when block at target height has a different hash - #[tokio::test] - async fn test_checkpoint_sync_block_hash_mismatch() { - use alloy_rpc_types_engine::{ForkchoiceUpdated, PayloadStatus, PayloadStatusEnum}; - - let target_hash = B256::repeat_byte(0xAA); - let wrong_hash = B256::repeat_byte(0xFF); - let target_height = Height::new(50); - - let mut engine_api = MockEngineAPI::new(); - engine_api.expect_forkchoice_updated().return_once(|_, _| { - Ok(ForkchoiceUpdated::new(PayloadStatus::new( - PayloadStatusEnum::Syncing, - None, - ))) - }); - - let mut ethereum_api = MockEthereumAPI::new(); - let height_str = format!("0x{:x}", target_height.as_u64()); - ethereum_api - .expect_get_block_by_number() - .with(eq(height_str)) - .returning(move |_| { - Ok(Some(ExecutionBlock { - block_hash: wrong_hash, - block_number: target_height.as_u64(), - parent_hash: B256::ZERO, - timestamp: 1000, - })) - }); - - let result = - checkpoint_sync(target_hash, target_height, &engine_api, ðereum_api, None).await; - - assert!(result.is_err()); - let err_msg = result.unwrap_err().to_string(); - assert!(err_msg.contains("block hash mismatch")); - } - - // Test: checkpoint_sync uses hex quantity when EL already has the target block. - #[tokio::test] - async fn test_checkpoint_sync_valid_status_uses_hex_block_number() { - use alloy_rpc_types_engine::{ForkchoiceUpdated, PayloadStatus, PayloadStatusEnum}; - - let target_height = Height::new(50); - let target_hash = B256::repeat_byte(0xAB); - let target_block = ExecutionBlock { - block_hash: target_hash, - block_number: target_height.as_u64(), - parent_hash: B256::repeat_byte(0x31), - timestamp: 5000, - }; - - let mut engine_api = MockEngineAPI::new(); - engine_api - .expect_forkchoice_updated() - .return_once(move |_, _| { - Ok(ForkchoiceUpdated::new(PayloadStatus::new( - PayloadStatusEnum::Valid, - None, - ))) - }); - - let mut ethereum_api = MockEthereumAPI::new(); - ethereum_api - .expect_get_block_by_number() - .with(eq("0x32")) - .return_once(move |_| Ok(Some(target_block))); - - let result = - checkpoint_sync(target_hash, target_height, &engine_api, ðereum_api, None).await; - - let block = result.unwrap(); - assert_eq!(block.block_hash, target_hash); - assert_eq!(block.block_number, target_height.as_u64()); - } -} diff --git a/crates/malachite-app/src/handlers/decided.rs b/crates/malachite-app/src/handlers/decided.rs deleted file mode 100644 index e1903b4..0000000 --- a/crates/malachite-app/src/handlers/decided.rs +++ /dev/null @@ -1,883 +0,0 @@ -// Copyright 2026 Circle Internet Group, Inc. All rights reserved. -// -// SPDX-License-Identifier: Apache-2.0 -// -// Licensed under the Apache License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. -// You may obtain a copy of the License at -// -// http://www.apache.org/licenses/LICENSE-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// See the License for the specific language governing permissions and -// limitations under the License. - -use eyre::{eyre, Context}; -use tracing::{debug, error, info, warn}; - -use malachitebft_app_channel::Reply; -use malachitebft_core_types::CommitCertificate; - -use arc_consensus_types::{ArcContext, Height}; -use arc_eth_engine::engine::Engine; -use arc_eth_engine::json_structures::ExecutionBlock; - -use crate::block::ConsensusBlock; -use crate::finalize::{BlockFinalizer, EngineBlockFinalizer}; -use crate::metrics::AppMetrics; -use crate::state::{Decision, NextHeightInfo, State}; -use crate::stats::Stats; -use crate::store::repositories::{DecidedBlocksRepository, UndecidedBlocksRepository}; -use crate::store::services::{ProdPruningService, PruningService}; -use crate::utils::sync_state::{sync_state, SyncState}; - -/// Handles the `Decided` message from the consensus engine. -/// -/// This is called when the consensus engine has decided on a value for a given height and round. -/// The application processes the decided value, executes the decided block and, based on the -/// output of those steps, stores the decision result as either `Success` or `Failure`. -/// -/// The `Finalized` message that will follow this message sends the appropriate `Next` message to -/// consensus to start the next height, or in case of failure, restart the current height. -/// -/// The `commit_ack` channel is consumed once the certificate is durably stored, so the sync actor -/// can advertise the new tip height. If the commit fails before the store completes (block lookup -/// or storage error), the channel is dropped and no acknowledgement is sent. -#[tracing::instrument( - name = "decided", - skip_all, - fields( - height = %certificate.height, - round = %certificate.round, - ) -)] -pub async fn handle( - state: &mut State, - engine: &Engine, - certificate: CommitCertificate, - commit_ack: Reply<()>, -) -> eyre::Result<()> { - let decided_height = certificate.height; - let decided_value_id = certificate.value_id; - - store_proposal_monitor_on_decision(state, decided_height, &decided_value_id).await; - - let (store, metrics, stats) = (state.store(), state.metrics(), state.stats()); - - let block_finalizer = EngineBlockFinalizer::new(engine, stats, metrics); - let pruning_service = ProdPruningService::new(store, &state.config().prune); - - let block = decide( - block_finalizer, - store, // undecided blocks repository - store, // decided blocks repository - pruning_service, - certificate, - stats, - metrics, - commit_ack, - ) - .await; - - match block { - Ok(block) => { - info!("🟢 Successfully committed the decided value"); - - let catch_up_threshold = state.env_config().sync_catch_up_threshold; - let new_sync_state = sync_state(block.timestamp, catch_up_threshold); - - if SyncState::fell_behind(state.sync_state, new_sync_state) { - debug!("Node fell behind: transitioned from InSync to CatchingUp"); - state.metrics().inc_sync_fell_behind_count(); - } - - state.sync_state = new_sync_state; - - let next_height_info = - prepare_next_height(decided_height, block, new_sync_state, engine).await?; - - state.decision = Some(Decision::Success(Box::new(next_height_info))); - } - Err(e) => { - error!("🔴 Failed to process decided value: {e:#}"); - - state.decision = Some(Decision::Failure(e)); - } - } - - Ok(()) -} - -/// Update proposal monitor data upon decision. -async fn store_proposal_monitor_on_decision( - state: &mut State, - decided_height: Height, - decided_value_id: &arc_consensus_types::ValueId, -) { - let Some(mut monitor) = state.proposal_monitor.take() else { - warn!(%decided_height, "No proposal monitor found for decided height"); - return; - }; - assert!(monitor.height == decided_height); - - monitor.mark_decided(decided_value_id); - - if let Err(e) = state.store().store_proposal_monitor_data(monitor).await { - error!( - %decided_height, - "Failed to store proposal monitor data: {e}" - ); - } -} - -/// Commits a value with the given certificate, finalizes the block, -/// updates internal state and moves to the next height. -/// -/// The `commit_ack` channel is consumed inside `commit` once the certificate has been durably -/// stored. If we error out before reaching the store (block lookup), the channel is dropped here -/// without firing. -#[allow(clippy::too_many_arguments)] -async fn decide( - block_finalizer: impl BlockFinalizer, - undecided_blocks: impl UndecidedBlocksRepository, - decided_blocks: impl DecidedBlocksRepository, - pruning_service: impl PruningService, - certificate: CommitCertificate, - stats: &Stats, - metrics: &AppMetrics, - commit_ack: Reply<()>, -) -> eyre::Result { - let height = certificate.height; - let round = certificate.round; - let value_id = certificate.value_id; - - // NOTE: here the node searches for the block with maching value_id from any round - // It needs to read the complete undecided blocks table, but the expectation is it should be small. - let block = match undecided_blocks - .get_by_hash(height, value_id.block_hash()) - .await - { - Ok(Some(block)) => block, - Ok(None) => { - return Err(eyre!( - "Cannot find undecided block for certificate with height={height}, round={round}, value_id={value_id}" - )); - } - Err(e) => { - return Err(eyre!( - "Failed to retrieve undecided block for certificate with height={height}, round={round}, value_id={value_id}: {e}" - )); - } - }; - - debug!( - "🎁 Block size: {:?}, payload size: {:?}", - block.size_bytes(), - block.payload_size() - ); - - // Commit the decision to the store before finalizing the block. - // This way we ensure that latest decided height >= latest finalized block. - let new_latest_block = commit( - block_finalizer, - decided_blocks, - pruning_service, - certificate, - &block, - commit_ack, - ) - .await - .wrap_err_with(|| { - format!("Failed to commit block at height={height}, round={round}, value_id={value_id}") - })?; - - // Update the latest block - info!( - "🔍 Updating latest block with timestamp: {:?}", - new_latest_block.timestamp - ); - - // Update block finalize time metric - metrics.observe_block_finalize_time(stats.height_started().elapsed().as_secs_f64()); - - Ok(new_latest_block) -} - -/// Commits a value with the given certificate, cleanup stale consensus data and prune historical data. -/// -/// The `commit_ack` channel is consumed immediately after the certificate is durably stored — -/// before any post-store work (cleanup, finalize, pruning) — so the sync actor learns the new tip -/// even if a later step fails. If the store itself fails, the channel is dropped without firing. -async fn commit( - block_finalizer: impl BlockFinalizer, - decided_blocks: impl DecidedBlocksRepository, - pruning_service: impl PruningService, - certificate: CommitCertificate, - block: &ConsensusBlock, - commit_ack: Reply<()>, -) -> eyre::Result { - let certificate_height = certificate.height; - let certificate_round = certificate.round; - let value_id = certificate.value_id; - - decided_blocks - .store(certificate, block.execution_payload.clone(), block.proposer) - .await - .wrap_err_with(|| { - format!("Failed to store decided block at height={certificate_height}, round={certificate_round}, value_id={value_id}") - })?; - - if commit_ack.send(()).is_err() { - error!( - %certificate_height, - "Decided: Failed to send commit acknowledgement (sync actor may not be notified)" - ); - } - - // Clean up stale consensus data (undecided blocks and pending proposals up to the certificate height) - if let Err(e) = pruning_service - .clean_stale_consensus_data(certificate_height) - .await - { - error!("Failed to clean stale consensus data: {e}"); - } - - // Finalize the decided payload - let (new_latest_block, _latest_valid_hash) = - block_finalizer.finalize_decided_block(certificate_height, &block.execution_payload) - .await - .wrap_err_with(|| { - format!("Failed to finalize block at height={certificate_height}, round={certificate_round}, value_id={value_id}") - })?; - - // Prune historical decided certificates if pruning is enabled - if let Err(e) = pruning_service - .prune_historical_certs(certificate_height) - .await - { - error!("Failed to prune historical data: {e}"); - } - - // Prune decided blocks - // NOTE: Always performed, even if pruning is disabled, as CL does not store - // historical blocks anymore, besides the few needed to recover from EL amnesia. - if let Err(e) = pruning_service.prune_decided_blocks().await { - error!("Failed to prune decided blocks: {e}"); - } - - Ok(new_latest_block) -} - -/// Prepares the state for the next height by incrementing the height, -/// fetching the new validator set and consensus params, -/// and determining the target block time based on the sync state. -/// -/// ## Arguments -/// * `decided_height`: The height that was just decided. -/// * `decided_block`: The block that was just decided. -/// * `engine`: The Ethereum engine to fetch validator sets and consensus params. -async fn prepare_next_height( - decided_height: Height, - decided_block: ExecutionBlock, - sync_state: SyncState, - engine: &Engine, -) -> eyre::Result { - let next_height = decided_height.increment(); - - // Fetch the validator set for the next height - // NOTE: Validator set is fetched at the decided height for the next height - let validator_set = engine - .eth - .get_active_validator_set(decided_height.as_u64()) - .await - .wrap_err_with(|| { - format!("Failed to fetch validator set at height {decided_height} for next height {next_height}") - })?; - - // Fetch the consensus params for the next height - // NOTE: Consensus params are fetched at the decided height for the next height - let consensus_params = engine - .eth - .get_consensus_params(decided_height.as_u64()) - .await - .inspect_err(|e| { - let next_height = decided_height.increment(); - error!(%decided_height, %next_height, "Failed to fetch consensus params for next height: {e}"); - error!(%decided_height, %next_height, "Using default consensus params as a fallback"); - }) - .unwrap_or_default(); - - // If we are catching up, we skip the stable block times logic and start the next height right away. - let target_time = match sync_state { - SyncState::InSync => consensus_params.target_block_time(), - SyncState::CatchingUp => { - debug!("Node is catching up: no target duration for the next height"); - None - } - }; - - Ok(NextHeightInfo { - next_height, - validator_set, - consensus_params, - decided_block, - target_time, - }) -} - -#[cfg(test)] -mod tests { - - use eyre::eyre; - use mockall::predicate::*; - - use alloy_primitives::{Address as AlloyAddress, Bloom, Bytes as AlloyBytes, U256}; - use alloy_rpc_types_engine::{ExecutionPayloadV1, ExecutionPayloadV2, ExecutionPayloadV3}; - use arc_consensus_types::signing::Signature; - use arc_consensus_types::{Address, Height, Round, ValueId, B256}; - use malachitebft_app_channel::app::types::core::Validity; - use malachitebft_core_types::{CommitCertificate, CommitSignature}; - - use crate::finalize::MockBlockFinalizer; - use crate::metrics::AppMetrics; - use crate::stats::Stats; - use crate::store::repositories::mocks::{ - MockDecidedBlocksRepository, MockUndecidedBlocksRepository, - }; - use crate::store::services::mocks::MockPruningService; - - use super::*; - - // Helper functions for creating test fixtures - fn test_execution_block(height: u64, timestamp: u64) -> ExecutionBlock { - ExecutionBlock { - block_hash: B256::repeat_byte((height % 256) as u8), - block_number: height, - parent_hash: if height > 0 { - B256::repeat_byte(((height - 1) % 256) as u8) - } else { - B256::ZERO - }, - timestamp, - } - } - - fn test_execution_payload(height: u64, timestamp: u64) -> ExecutionPayloadV3 { - ExecutionPayloadV3 { - payload_inner: ExecutionPayloadV2 { - payload_inner: ExecutionPayloadV1 { - parent_hash: if height > 0 { - B256::repeat_byte(((height - 1) % 256) as u8) - } else { - B256::ZERO - }, - fee_recipient: AlloyAddress::ZERO, - state_root: B256::ZERO, - receipts_root: B256::ZERO, - logs_bloom: Bloom::default(), - prev_randao: B256::ZERO, - block_number: height, - gas_limit: 30000000, - gas_used: 0, - timestamp, - extra_data: AlloyBytes::default(), - base_fee_per_gas: U256::from(1u64), - block_hash: B256::repeat_byte((height % 256) as u8), - transactions: vec![], - }, - withdrawals: vec![], - }, - blob_gas_used: 0, - excess_blob_gas: 0, - } - } - - fn test_consensus_block(height: u64, round: u32, timestamp: u64) -> ConsensusBlock { - ConsensusBlock { - height: Height::new(height), - round: Round::new(round), - valid_round: Round::new(0), - proposer: Address::default(), - validity: Validity::Valid, - execution_payload: test_execution_payload(height, timestamp), - signature: Some(Signature::test()), - } - } - - fn test_commit_certificate( - height: u64, - round: u32, - block_hash: B256, - ) -> CommitCertificate { - CommitCertificate { - height: Height::new(height), - round: Round::new(round), - value_id: ValueId::new(block_hash), - commit_signatures: vec![CommitSignature::new(Address::default(), Signature::test())], - } - } - - fn test_metrics() -> AppMetrics { - AppMetrics::default() - } - - fn test_stats() -> Stats { - Stats::default() - } - - /// A dummy commit-ack channel for tests that don't assert ack delivery. - /// The receiver is dropped, so a successful `commit_ack.send(())` will - /// return `Err`, which `commit` logs and ignores. - fn dummy_commit_ack() -> Reply<()> { - let (tx, _rx) = tokio::sync::oneshot::channel(); - tx - } - - // Tests for decide() function - - // Successful decision with valid block found - #[tokio::test] - async fn test_decide_success() { - let height = 5u64; - let round = 2u32; - let timestamp = 1000u64; - let block_hash = B256::repeat_byte((height % 256) as u8); - let certificate = test_commit_certificate(height, round, block_hash); - let consensus_block = test_consensus_block(height, round, timestamp); - let expected_execution_block = test_execution_block(height, timestamp); - - let mut undecided_blocks = MockUndecidedBlocksRepository::new(); - undecided_blocks - .expect_get_by_hash() - .with(eq(Height::new(height)), eq(block_hash)) - .return_once(move |_, _| Ok(Some(consensus_block.clone()))); - - let mut decided_blocks = MockDecidedBlocksRepository::new(); - decided_blocks - .expect_store() - .return_once(move |cert, payload, proposer| { - assert_eq!(cert.height, Height::new(height)); - assert_eq!(cert.round, Round::new(round)); - assert_eq!(payload.payload_inner.payload_inner.block_number, height); - assert_eq!(proposer, Address::default()); - Ok(()) - }); - - let mut block_finalizer = MockBlockFinalizer::new(); - block_finalizer - .expect_finalize_decided_block() - .return_once(move |h, _| { - assert_eq!(h, Height::new(height)); - Ok((expected_execution_block, block_hash)) - }); - - let mut pruning_service = MockPruningService::new(); - pruning_service - .expect_clean_stale_consensus_data() - .return_once(|_| Ok(())); - pruning_service - .expect_prune_historical_certs() - .return_once(|_| Ok(vec![])); - pruning_service - .expect_prune_decided_blocks() - .return_once(|| Ok(vec![])); - - let metrics = test_metrics(); - let stats = test_stats(); - - let result = decide( - block_finalizer, - undecided_blocks, - decided_blocks, - pruning_service, - certificate, - &stats, - &metrics, - dummy_commit_ack(), - ) - .await; - - let block = result.unwrap(); - assert_eq!(block.block_number, height); - assert_eq!(block.timestamp, timestamp); - } - - // Block not found in undecided blocks - #[tokio::test] - async fn test_decide_block_not_found() { - let height = 5u64; - let round = 2u32; - let block_hash = B256::repeat_byte((height % 256) as u8); - let certificate = test_commit_certificate(height, round, block_hash); - - let mut undecided_blocks = MockUndecidedBlocksRepository::new(); - undecided_blocks - .expect_get_by_hash() - .with(eq(Height::new(height)), eq(block_hash)) - .return_once(|_, _| Ok(None)); - - let decided_blocks = MockDecidedBlocksRepository::new(); - let block_finalizer = MockBlockFinalizer::new(); - let pruning_service = MockPruningService::new(); - let metrics = test_metrics(); - let stats = test_stats(); - - let result = decide( - block_finalizer, - undecided_blocks, - decided_blocks, - pruning_service, - certificate, - &stats, - &metrics, - dummy_commit_ack(), - ) - .await; - - let err_msg = result.unwrap_err().to_string(); - assert!(err_msg.contains("Cannot find undecided block")); - } - - // Repository error when fetching undecided block - #[tokio::test] - async fn test_decide_undecided_blocks_fetch_error() { - let height = 5u64; - let round = 2u32; - let block_hash = B256::repeat_byte((height % 256) as u8); - let certificate = test_commit_certificate(height, round, block_hash); - - let mut undecided_blocks = MockUndecidedBlocksRepository::new(); - undecided_blocks - .expect_get_by_hash() - .with(eq(Height::new(height)), eq(block_hash)) - .return_once(|_, _| Err(std::io::Error::other("Database error"))); - - let decided_blocks = MockDecidedBlocksRepository::new(); - let block_finalizer = MockBlockFinalizer::new(); - let pruning_service = MockPruningService::new(); - let metrics = test_metrics(); - let stats = test_stats(); - - let result = decide( - block_finalizer, - undecided_blocks, - decided_blocks, - pruning_service, - certificate, - &stats, - &metrics, - dummy_commit_ack(), - ) - .await; - - let err_msg = result.unwrap_err().to_string(); - assert!(err_msg.contains("Failed to retrieve undecided block")); - } - - // Commit failure propagates error - #[tokio::test] - async fn test_decide_commit_failure() { - let height = 5u64; - let round = 2u32; - let timestamp = 1000u64; - let block_hash = B256::repeat_byte((height % 256) as u8); - let certificate = test_commit_certificate(height, round, block_hash); - let consensus_block = test_consensus_block(height, round, timestamp); - - let mut undecided_blocks = MockUndecidedBlocksRepository::new(); - undecided_blocks - .expect_get_by_hash() - .with(eq(Height::new(height)), eq(block_hash)) - .return_once(move |_, _| Ok(Some(consensus_block.clone()))); - - let mut decided_blocks = MockDecidedBlocksRepository::new(); - decided_blocks - .expect_store() - .return_once(|_, _, _| Err(std::io::Error::other("Store failed"))); - - let block_finalizer = MockBlockFinalizer::new(); - let pruning_service = MockPruningService::new(); - let metrics = test_metrics(); - let stats = test_stats(); - - let result = decide( - block_finalizer, - undecided_blocks, - decided_blocks, - pruning_service, - certificate, - &stats, - &metrics, - dummy_commit_ack(), - ) - .await; - - let err_msg = result.unwrap_err().to_string(); - assert!(err_msg.contains("Failed to commit block")); - } - - // Tests for commit() function - - // Successful commit flow - #[tokio::test] - async fn test_commit_success() { - let height = 5u64; - let round = 2u32; - let timestamp = 1000u64; - let block_hash = B256::repeat_byte((height % 256) as u8); - let certificate = test_commit_certificate(height, round, block_hash); - let consensus_block = test_consensus_block(height, round, timestamp); - let expected_execution_block = test_execution_block(height, timestamp); - - let mut decided_blocks = MockDecidedBlocksRepository::new(); - decided_blocks - .expect_store() - .return_once(move |cert, payload, proposer| { - assert_eq!(cert.height, Height::new(height)); - assert_eq!(cert.round, Round::new(round)); - assert_eq!(payload.payload_inner.payload_inner.block_number, height); - assert_eq!(proposer, Address::default()); - Ok(()) - }); - - let mut block_finalizer = MockBlockFinalizer::new(); - block_finalizer - .expect_finalize_decided_block() - .return_once(move |h, _| { - assert_eq!(h, Height::new(height)); - Ok((expected_execution_block, block_hash)) - }); - - let mut pruning_service = MockPruningService::new(); - pruning_service - .expect_clean_stale_consensus_data() - .with(eq(Height::new(height))) - .return_once(|_| Ok(())); - pruning_service - .expect_prune_historical_certs() - .with(eq(Height::new(height))) - .return_once(|_| Ok(vec![Height::new(1), Height::new(2)])); - pruning_service - .expect_prune_decided_blocks() - .return_once(|| Ok(vec![Height::new(0)])); - - let result = commit( - block_finalizer, - decided_blocks, - pruning_service, - certificate, - &consensus_block, - dummy_commit_ack(), - ) - .await; - - let block = result.unwrap(); - assert_eq!(block.block_number, height); - assert_eq!(block.timestamp, timestamp); - } - - // DecidedBlocksRepository store failure - #[tokio::test] - async fn test_commit_store_failure() { - let height = 5u64; - let round = 2u32; - let timestamp = 1000u64; - let block_hash = B256::repeat_byte((height % 256) as u8); - let certificate = test_commit_certificate(height, round, block_hash); - let consensus_block = test_consensus_block(height, round, timestamp); - - let mut decided_blocks = MockDecidedBlocksRepository::new(); - decided_blocks - .expect_store() - .return_once(|_, _, _| Err(std::io::Error::other("Store failed"))); - - let block_finalizer = MockBlockFinalizer::new(); - let pruning_service = MockPruningService::new(); - - let result = commit( - block_finalizer, - decided_blocks, - pruning_service, - certificate, - &consensus_block, - dummy_commit_ack(), - ) - .await; - - let err_msg = result.unwrap_err().to_string(); - assert!(err_msg.contains("Failed to store decided block")); - } - - // BlockFinalizer finalization failure - #[tokio::test] - async fn test_commit_finalization_failure() { - let height = 5u64; - let round = 2u32; - let timestamp = 1000u64; - let block_hash = B256::repeat_byte((height % 256) as u8); - let certificate = test_commit_certificate(height, round, block_hash); - let consensus_block = test_consensus_block(height, round, timestamp); - - let mut decided_blocks = MockDecidedBlocksRepository::new(); - decided_blocks.expect_store().return_once(|_, _, _| Ok(())); - - let mut block_finalizer = MockBlockFinalizer::new(); - block_finalizer - .expect_finalize_decided_block() - .return_once(|_, _| Err(eyre!("Finalization failed"))); - - let mut pruning_service = MockPruningService::new(); - pruning_service - .expect_clean_stale_consensus_data() - .return_once(|_| Ok(())); - - let result = commit( - block_finalizer, - decided_blocks, - pruning_service, - certificate, - &consensus_block, - dummy_commit_ack(), - ) - .await; - - let err_msg = result.unwrap_err().to_string(); - assert!(err_msg.contains("Failed to finalize block")); - } - - // Pruning errors are logged but don't fail the operation - #[tokio::test] - async fn test_commit_pruning_errors_logged_not_fatal() { - let height = 5u64; - let round = 2u32; - let timestamp = 1000u64; - let block_hash = B256::repeat_byte((height % 256) as u8); - let certificate = test_commit_certificate(height, round, block_hash); - let consensus_block = test_consensus_block(height, round, timestamp); - let expected_execution_block = test_execution_block(height, timestamp); - - let mut decided_blocks = MockDecidedBlocksRepository::new(); - decided_blocks.expect_store().return_once(|_, _, _| Ok(())); - - let mut block_finalizer = MockBlockFinalizer::new(); - block_finalizer - .expect_finalize_decided_block() - .return_once(move |_, _| Ok((expected_execution_block, block_hash))); - - let mut pruning_service = MockPruningService::new(); - - // Stale data cleanup fails - but is logged, not fatal - pruning_service - .expect_clean_stale_consensus_data() - .return_once(|_| Err(std::io::Error::other("Cleanup failed"))); - - // Historical cert pruning fails - but is logged, not fatal - pruning_service - .expect_prune_historical_certs() - .return_once(|_| Err(std::io::Error::other("Historical prune failed"))); - - // Decided blocks pruning must succeed - pruning_service - .expect_prune_decided_blocks() - .return_once(|| Ok(vec![])); - - let result = commit( - block_finalizer, - decided_blocks, - pruning_service, - certificate, - &consensus_block, - dummy_commit_ack(), - ) - .await; - - // Despite pruning errors, the commit should succeed - let block = result.unwrap(); - assert_eq!(block.block_number, height); - } - - /// commit_ack must fire after the certificate is durably stored, even if a later - /// post-store step (finalize) fails — sync should still learn the new tip. - #[tokio::test] - async fn test_commit_acks_when_store_succeeds_but_finalize_fails() { - let height = 5u64; - let round = 2u32; - let timestamp = 1000u64; - let block_hash = B256::repeat_byte((height % 256) as u8); - let certificate = test_commit_certificate(height, round, block_hash); - let consensus_block = test_consensus_block(height, round, timestamp); - - let mut decided_blocks = MockDecidedBlocksRepository::new(); - decided_blocks.expect_store().return_once(|_, _, _| Ok(())); - - let mut block_finalizer = MockBlockFinalizer::new(); - block_finalizer - .expect_finalize_decided_block() - .return_once(|_, _| Err(eyre!("Finalization failed"))); - - let mut pruning_service = MockPruningService::new(); - pruning_service - .expect_clean_stale_consensus_data() - .return_once(|_| Ok(())); - - let (ack_tx, ack_rx) = tokio::sync::oneshot::channel(); - - let result = commit( - block_finalizer, - decided_blocks, - pruning_service, - certificate, - &consensus_block, - ack_tx, - ) - .await; - - assert!(result.is_err(), "expected finalize failure"); - assert_eq!( - ack_rx.await, - Ok(()), - "ack must be sent when store succeeds, even if finalize later fails" - ); - } - - /// commit_ack must NOT fire if the durable store fails — otherwise sync - /// would advertise a height we cannot serve. - #[tokio::test] - async fn test_commit_does_not_ack_when_store_fails() { - let height = 5u64; - let round = 2u32; - let timestamp = 1000u64; - let block_hash = B256::repeat_byte((height % 256) as u8); - let certificate = test_commit_certificate(height, round, block_hash); - let consensus_block = test_consensus_block(height, round, timestamp); - - let mut decided_blocks = MockDecidedBlocksRepository::new(); - decided_blocks - .expect_store() - .return_once(|_, _, _| Err(std::io::Error::other("Store failed"))); - - let block_finalizer = MockBlockFinalizer::new(); - let pruning_service = MockPruningService::new(); - - let (ack_tx, ack_rx) = tokio::sync::oneshot::channel(); - - let result = commit( - block_finalizer, - decided_blocks, - pruning_service, - certificate, - &consensus_block, - ack_tx, - ) - .await; - - assert!(result.is_err(), "expected store failure"); - // Sender dropped without sending → recv resolves to Err(RecvError) - assert!( - ack_rx.await.is_err(), - "ack channel must be dropped (not sent) when store fails" - ); - } -} diff --git a/crates/malachite-app/src/handlers/finalized.rs b/crates/malachite-app/src/handlers/finalized.rs deleted file mode 100644 index 61dd2ba..0000000 --- a/crates/malachite-app/src/handlers/finalized.rs +++ /dev/null @@ -1,150 +0,0 @@ -// Copyright 2025 Circle Internet Group, Inc. All rights reserved. -// -// SPDX-License-Identifier: Apache-2.0 -// -// Licensed under the Apache License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. -// You may obtain a copy of the License at -// -// http://www.apache.org/licenses/LICENSE-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// See the License for the specific language governing permissions and -// limitations under the License. - -use eyre::{bail, eyre}; -use tracing::{error, info, warn}; - -use malachitebft_app_channel::app::engine::host::Next; -use malachitebft_app_channel::app::types::MisbehaviorEvidence; -use malachitebft_app_channel::Reply; -use malachitebft_core_types::{CommitCertificate, HeightParams}; - -use arc_consensus_types::evidence::StoredMisbehaviorEvidence; -use arc_consensus_types::{ArcContext, Height}; - -use crate::state::{Decision, NextHeightInfo, State}; -use crate::utils::check_halt_height; - -/// Handles the `Finalized` message from the consensus engine. -/// -/// This message always follows a corresponding a `Decided` message, whose processing defines -/// the `Next` message to be sent to consensus, in order to start a new or restart a height. -/// -/// This method sends the appropriate `Next` message to consensus to start the next height. -#[tracing::instrument( - name = "finalized", - skip_all, - fields( - height = %certificate.height, - round = %certificate.round, - ))] -pub async fn handle( - state: &mut State, - certificate: CommitCertificate, - evidence: MisbehaviorEvidence, - reply: Reply>, -) -> eyre::Result<()> { - let Some(decision) = state.decision.take() else { - bail!("Finalized: Received message without a corresponding decision in state"); - }; - - // Store misbehavior evidence if any was observed during this height - if !evidence.is_empty() { - let stored_evidence = - StoredMisbehaviorEvidence::from_misbehavior_evidence(certificate.height, &evidence); - - let validators_count = stored_evidence.validators.len(); - let validator_addresses = stored_evidence - .validators - .iter() - .map(|v| v.address.to_string()) - .collect::>() - .join(", "); - - if let Err(e) = state - .store() - .store_misbehavior_evidence(stored_evidence) - .await - { - error!( - %validators_count, - %validator_addresses, - "Failed to store misbehavior evidence: {e:#}" - ); - } else { - warn!( - %validators_count, - %validator_addresses, - "Stored misbehavior evidence" - ); - } - } - - let height = certificate.height; - let signatures_count = certificate.commit_signatures.len(); - - if let Err(e) = state.store().extend_certificate(certificate).await { - error!( - %signatures_count, - "Failed to store extended certificate: {e}" - ); - - // Continue anyway - shouldn't block consensus progress - } - - let next = match decision { - Decision::Success(next_height_info) => start_next_height(state, *next_height_info).await?, - Decision::Failure(report) => { - error!(error = ?report, "🔴 Decision failure, restarting height"); - - restart_height(state, height).await? - } - }; - - info!( - duration = ?state.stats().height_started().elapsed(), - "Height duration from started until finalized" - ); - - reply - .send(next) - .map_err(|e| eyre!("Finalized: Failed to send Next reply for height {height}: {e:?}"))?; - - Ok(()) -} - -/// Prepare the start of the next height after a successful decision. -async fn start_next_height( - state: &mut State, - info: NextHeightInfo, -) -> eyre::Result> { - let next_height = info.next_height; - let next_params = info.height_params(); - - state.move_to_next_height(info); - - // Check if the next height matches the configured halt height (if any). - // If so, the node will halt instead of starting the next height. - let halt_height = state.env_config().halt_height; - check_halt_height(state.store(), next_height, halt_height).await?; - - Ok(Next::Start(next_height, next_params)) -} - -/// Prepare the restart of the current height after a failed decision. -async fn restart_height(state: &mut State, height: Height) -> eyre::Result> { - let validator_set = state.validator_set().clone(); - let consensus_params = state.consensus_params().clone(); - let timeouts = consensus_params.timeouts(); - - state - .restart_height(height, validator_set.clone(), consensus_params) - .await?; - - // Note that no `target_time` is set for a height that is restarted - let height_params = HeightParams::new(validator_set, timeouts, None); - Ok(Next::Restart(height, height_params)) -} diff --git a/crates/malachite-app/src/handlers/get_decided_values.rs b/crates/malachite-app/src/handlers/get_decided_values.rs deleted file mode 100644 index 8ae4a94..0000000 --- a/crates/malachite-app/src/handlers/get_decided_values.rs +++ /dev/null @@ -1,543 +0,0 @@ -// Copyright 2025 Circle Internet Group, Inc. All rights reserved. -// -// SPDX-License-Identifier: Apache-2.0 -// -// Licensed under the Apache License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. -// You may obtain a copy of the License at -// -// http://www.apache.org/licenses/LICENSE-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// See the License for the specific language governing permissions and -// limitations under the License. - -use std::ops::RangeInclusive; - -use alloy_rpc_types_engine::ExecutionPayloadV3; -use arc_eth_engine::engine::Engine; -use bytesize::ByteSize; -use eyre::{eyre, WrapErr}; -use ssz::Encode; -use tracing::{debug, error, info, warn}; - -use malachitebft_app_channel::app::types::codec::HasEncodedLen; -use malachitebft_app_channel::app::types::sync::RawDecidedValue; -use malachitebft_app_channel::Reply; -use malachitebft_core_types::utils::height::{DisplayRange, HeightRangeExt}; -use malachitebft_core_types::Height as _; - -use arc_consensus_types::codec::proto::ProtobufCodec; -use arc_consensus_types::sync::{Response, ValueResponse}; -use arc_consensus_types::{ArcContext, Height}; - -use crate::block::DecidedBlock; -use crate::metrics::AppMetrics; -use crate::state::State; -use crate::store::Store; - -pub async fn handle( - state: &mut State, - engine: &Engine, - range: RangeInclusive, - reply: Reply>>, -) -> Result<(), eyre::Error> { - let config = state.config().value_sync; - - if !config.enabled { - warn!("GetDecidedValues: Sync is disabled in the configuration"); - let _ = reply.send(Vec::new()); - return Ok(()); - } - - let latest_height = state - .store() - .max_height() - .await - .wrap_err("GetDecidedValues: Failed to fetch the latest height from the state")? - .unwrap_or_default(); - - let earliest_height = state - .store() - .min_height() - .await - .wrap_err("GetDecidedValues: Failed to fetch the earliest height from the state")? - .unwrap_or_default(); - - let store = state.store().clone(); - let metrics = state.metrics().clone(); - let engine = engine.clone(); - - // Spawn retrieval of decided values in a separate task to avoid blocking the main application loop. - tokio::spawn(async move { - let values = get_decided_values( - range, - earliest_height..=latest_height, - config.batch_size, - config.max_response_size, - store, - engine, - metrics, - ) - .await - .inspect_err(|e| { - error!("🔴 GetDecidedValues: Error while getting decided values: {e:?}"); - }) - .unwrap_or_default(); - - if let Err(e) = reply.send(values) { - error!("🔴 GetDecidedValues: Failed to send reply: {e:?}"); - } - }); - - Ok(()) -} - -async fn get_decided_values( - requested_range: RangeInclusive, - available_range: RangeInclusive, - batch_size: usize, - max_response_size: ByteSize, - store: Store, - engine: Engine, - metrics: AppMetrics, -) -> Result>, eyre::Error> { - let _guard = metrics.start_msg_process_timer("GetDecidedValues"); - - let earliest_height = *available_range.start(); - let latest_height = *available_range.end(); - - let Some(range) = - get_clamped_request_range(requested_range, earliest_height, latest_height, batch_size) - else { - return Ok(Vec::new()); // Warn logged inside get_clamped_request_range - }; - - // Batch fetch all execution payloads in one RPC call - let heights = range.clone().iter_heights().collect::>(); - let block_numbers = heights - .iter() - .map(|height| format!("0x{:x}", height.as_u64())) - .collect::>(); - - let execution_payloads = engine.eth.get_execution_payloads(&block_numbers).await?; - - let mut values = Vec::with_capacity(range.len()); - let mut total_bytes = ByteSize::b(0); - - for (height, execution_payload) in heights.into_iter().zip(execution_payloads.into_iter()) { - let Some(execution_payload) = execution_payload else { - debug!(%height, "No execution payload found at this height from EL, skipping"); - continue; - }; - - let (raw_value, raw_bytes_len) = - match get_raw_decided_value(&store, execution_payload, height).await { - Ok(result) => result, - Err(e) => { - warn!(%height, "Failed to get decided value at height: {e}"); - continue; - } - }; - - // NOTE: This size estimate slightly over-approximates the true wire size. - // These estimates assume each value is sent in its own SyncResponse message, - // whereas in practice all values are batched into a single message. - // - // For 10 SyncedValues each with 10 signatures, batching all values into - // one SyncResponse (~10X + 9,990 bytes) is about 90 bytes smaller than sending - // 10 separate SyncResponses (~10X + 10,080 bytes). In other words, splitting - // adds ~9 bytes of framing overhead per message (<1% overhead for typical payloads). - // - // This over-approximation is acceptable for our purpose of ensuring we are not going - // over the max response size limit. - // - // Moreover, Malachite will perform a very similar over-approximation when checking - // the response to GetDecidedValues, so this keeps our behavior consistent. - #[allow(clippy::arithmetic_side_effects)] - // Equivalent to `total_bytes + raw_bytes_len > max_response_size`, - // but rearranged so the subtraction cannot overflow (raw_bytes_len <= max_response_size - // is checked first, and max_response_size.0 - raw_bytes_len.0 is then non-negative). - if raw_bytes_len > max_response_size - || total_bytes.as_u64() > max_response_size.as_u64() - raw_bytes_len.as_u64() - { - warn!( - %height, %max_response_size, %raw_bytes_len, - "GetDecidedValues: Reached max total bytes limit for response, stopping here", - ); - - break; - } - - #[allow(clippy::arithmetic_side_effects)] // Guarded by the comparison above - { - total_bytes += raw_bytes_len; - } - - values.push(raw_value); - } - - info!( - values = %values.len(), - %total_bytes, - %max_response_size, - "GetDecidedValues: Returning decided values" - ); - - Ok(values) -} - -async fn get_raw_decided_value( - store: &Store, - execution_payload: ExecutionPayloadV3, - height: Height, -) -> eyre::Result<(RawDecidedValue, ByteSize)> { - let stored = store - .get_certificate(Some(height)) - .await? - .ok_or_else(|| eyre!("No certificate found at height {height}"))?; - - let decided_block = DecidedBlock::new(execution_payload, stored.certificate); - - let raw_value = RawDecidedValue { - certificate: decided_block.certificate, - value_bytes: decided_block.execution_payload.as_ssz_bytes().into(), - }; - - let response = Response::ValueResponse(ValueResponse::new(height, vec![raw_value.clone()])); - - let Ok(raw_bytes_len) = ProtobufCodec.encoded_len(&response) else { - return Err(eyre!( - "Failed to determine encoded length of value at height {height}" - )); - }; - - // encoded_len returns usize; on 64-bit targets this fits in u64 - #[allow(clippy::cast_possible_truncation)] - Ok((raw_value, ByteSize::b(raw_bytes_len as u64))) -} - -fn get_clamped_request_range( - range: RangeInclusive, - earliest_height: Height, - latest_height: Height, - batch_size: usize, -) -> Option> { - assert!( - earliest_height <= latest_height, - "Earliest height must always be less than or equal to latest height" - ); - let mut start = *range.start(); - let mut end = *range.end(); - - if end < start { - warn!(requested_start = %start, requested_end = %end, "GetDecidedValues: Invalid inverted request range"); - return None; - } - - if end < earliest_height || start > latest_height { - warn!( - requested_start = %start, - requested_end = %end, - %earliest_height, - %latest_height, - "GetDecidedValues: Requested range lies wholly outside available bounds", - ); - return None; - } - - if start < earliest_height { - warn!( - %earliest_height, - requested_start = %start, - "GetDecidedValues: Requested start is before earliest height; clamping", - ); - start = earliest_height; - } - if end > latest_height { - warn!( - %latest_height, - requested_end = %end, - "GetDecidedValues: Requested end is beyond latest height; clamping", - ); - end = latest_height; - } - - debug_assert!(start <= end, "Post-clamp range must satisfy start <= end"); - - if batch_size == 0 { - warn!( - requested = %DisplayRange(&(start..=end)), - "GetDecidedValues: Batch size is zero; returning None", - ); - return None; - } - - // start <= end guaranteed by clamp logic above; +1 cannot overflow after clamping - // to real block heights, but we handle it gracefully regardless. - #[allow(clippy::arithmetic_side_effects)] - let Some(requested_count) = (end.as_u64() - start.as_u64()).checked_add(1) else { - warn!("GetDecidedValues: height range count overflow"); - return None; - }; - // batch_size > 0 checked above; fits in u64 on 64-bit targets - #[allow(clippy::cast_possible_truncation)] - if requested_count > batch_size as u64 { - end = start.increment_by(batch_size.saturating_sub(1) as u64); - warn!( - requested = %requested_count, - max = %batch_size, - clamped = %DisplayRange(&(start..=end)), - "GetDecidedValues: Clamping request range to max batch size", - ); - } - - Some(start..=end) -} - -#[cfg(test)] -mod tests { - use super::*; - - // Helper to easily create ranges - fn h(start: u64, end: u64) -> RangeInclusive { - Height::new(start)..=Height::new(end) - } - - #[test] - fn test_returns_range_unchanged_when_within_limits() { - let range = h(10, 14); - let batch_size = 10; - let earliest = Height::new(5); - let latest = Height::new(20); - - let result = - get_clamped_request_range(range.clone(), earliest, latest, batch_size).unwrap(); - assert_eq!(result, range); - } - - #[test] - fn test_clamps_when_range_exceeds_batch_size() { - let range = h(10, 25); // 16 items total - let batch_size = 10; - let earliest = Height::new(5); - let latest = Height::new(50); - - let result = get_clamped_request_range(range, earliest, latest, batch_size).unwrap(); - let expected = h(10, 19); // 10 total values: 10..=19 - - assert_eq!(result, expected); - } - - #[test] - fn test_clamps_when_end_exceeds_latest_height() { - let range = h(10, 25); - let batch_size = 20; - let earliest = Height::new(5); - let latest = Height::new(15); - - let result = get_clamped_request_range(range, earliest, latest, batch_size).unwrap(); - let expected = h(10, 15); - - assert_eq!(result, expected); - } - - #[test] - fn test_clamps_for_both_batch_and_latest_height() { - let range = h(10, 50); - let batch_size = 10; - let earliest = Height::new(5); - let latest = Height::new(15); - - let result = get_clamped_request_range(range, earliest, latest, batch_size).unwrap(); - // First clamp: 10..=19 (batch) - // Then clamp again: 10..=15 (latest) - let expected = h(10, 15); - - assert_eq!(result, expected); - } - - #[test] - fn test_handles_range_equal_to_latest_height() { - let range = h(10, 20); - let batch_size = 15; - let earliest = Height::new(5); - let latest = Height::new(20); - - let result = - get_clamped_request_range(range.clone(), earliest, latest, batch_size).unwrap(); - assert_eq!(result, range); - } - - #[test] - fn test_handles_singleton_range() { - // When start == end, still valid range - let range = h(5, 5); - let batch_size = 10; - let earliest = Height::new(1); - let latest = Height::new(100); - - let result = - get_clamped_request_range(range.clone(), earliest, latest, batch_size).unwrap(); - assert_eq!(result, range); - } - - #[test] - fn test_handles_zero_batch_size_is_none() { - let range = h(10, 20); - let batch_size = 0; - let earliest = Height::new(5); - let latest = Height::new(50); - - let res = get_clamped_request_range(range, earliest, latest, batch_size); - assert!(res.is_none()); - } - - #[test] - fn test_clamps_request_range_within_bounds() { - let range = h(1, 10); - let earliest = Height::new(1); - let latest = Height::new(20); - let batch_size = 100; - let result = - get_clamped_request_range(range.clone(), earliest, latest, batch_size).unwrap(); - assert_eq!(result, range); - } - - #[test] - fn test_clamp_request_range_end_beyond_tip() { - let range = h(5, 15); - let earliest = Height::new(1); - let latest = Height::new(10); - let batch_size = 100; - let result = - get_clamped_request_range(range.clone(), earliest, latest, batch_size).unwrap(); - assert_eq!(result, h(5, 10)); - } - - #[test] - fn test_clamp_request_range_start_and_end_at_tip() { - let range = h(10, 10); - let earliest = Height::new(1); - let latest = Height::new(10); - let batch_size = 100; - let result = - get_clamped_request_range(range.clone(), earliest, latest, batch_size).unwrap(); - assert_eq!(result, range); - } - - #[test] - fn test_clamp_request_range_start_above_tip() { - let range = h(15, 20); - let earliest = Height::new(1); - let latest = Height::new(10); - let batch_size = 10; - let res = get_clamped_request_range(range.clone(), earliest, latest, batch_size); - assert!(res.is_none()); - } - - #[test] - fn test_inverted_above_bounds_is_none() { - let range = h(20, 10); - let earliest = Height::new(3); - let latest = Height::new(8); - let batch_size = 20; - let res = get_clamped_request_range(range, earliest, latest, batch_size); - assert!(res.is_none()); - } - - #[test] - fn test_inverted_below_bounds_is_none() { - let range = h(20, 19); - let earliest = Height::new(40); - let latest = Height::new(80); - let batch_size = 10; - let res = get_clamped_request_range(range, earliest, latest, batch_size); - assert!(res.is_none()); - } - - #[test] - fn test_inverted_within_bounds_is_none() { - let range = h(20, 19); - let earliest = Height::new(5); - let latest = Height::new(50); - let batch_size = 10; - let res = get_clamped_request_range(range, earliest, latest, batch_size); - assert!(res.is_none()); - } - - #[test] - fn test_entirely_below_bounds_is_none() { - let range = h(1, 2); - let earliest = Height::new(5); - let latest = Height::new(50); - let batch_size = 10; - let res = get_clamped_request_range(range, earliest, latest, batch_size); - assert!(res.is_none()); - } - - #[test] - fn test_entirely_above_bounds_is_none() { - let range = h(60, 65); - let earliest = Height::new(5); - let latest = Height::new(50); - let batch_size = 10; - let res = get_clamped_request_range(range, earliest, latest, batch_size); - assert!(res.is_none()); - } - - #[test] - fn test_batch_cap_near_latest() { - let range = h(95, 120); - let earliest = Height::new(50); - let latest = Height::new(100); - let batch_size = 10; - let result = get_clamped_request_range(range, earliest, latest, batch_size).unwrap(); - assert_eq!(result, h(95, 100)); - } - - #[test] - fn test_bounds_first_then_batch() { - let range = h(1, 100); - let earliest = Height::new(20); - let latest = Height::new(200); - let batch_size = 5; - let result = get_clamped_request_range(range, earliest, latest, batch_size).unwrap(); - assert_eq!(result, h(20, 24)); - } - - #[test] - fn test_exact_batch_size_no_change() { - let range = h(10, 19); - let earliest = Height::new(1); - let latest = Height::new(100); - let batch_size = 10; - let result = - get_clamped_request_range(range.clone(), earliest, latest, batch_size).unwrap(); - assert_eq!(result, range); - } - - #[test] - fn test_exact_limits() { - let range = h(20, 29); - let earliest = Height::new(20); - let latest = Height::new(29); - let batch_size = 10; - let result = - get_clamped_request_range(range.clone(), earliest, latest, batch_size).unwrap(); - assert_eq!(result, range); - } - - #[test] - fn test_off_by_1() { - let range = h(19, 29); - let earliest = Height::new(20); - let latest = Height::new(28); - let batch_size = 8; - let result = - get_clamped_request_range(range.clone(), earliest, latest, batch_size).unwrap(); - assert_eq!(result, h(20, 27)); - } -} diff --git a/crates/malachite-app/src/handlers/get_history_min_height.rs b/crates/malachite-app/src/handlers/get_history_min_height.rs deleted file mode 100644 index 28e6e28..0000000 --- a/crates/malachite-app/src/handlers/get_history_min_height.rs +++ /dev/null @@ -1,173 +0,0 @@ -// Copyright 2025 Circle Internet Group, Inc. All rights reserved. -// -// SPDX-License-Identifier: Apache-2.0 -// -// Licensed under the Apache License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. -// You may obtain a copy of the License at -// -// http://www.apache.org/licenses/LICENSE-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// See the License for the specific language governing permissions and -// limitations under the License. - -use eyre::Context; -use tracing::{debug, error, warn}; - -use arc_consensus_types::Height; -use arc_eth_engine::engine::Engine; -use malachitebft_app_channel::Reply; - -use crate::state::State; - -/// Handles the `GetHistoryMinHeight` message from the consensus engine. -/// -/// This is called when the consensus engine requests the minimum height of the application's -/// history. The application retrieves the earliest height from the consensus store and the -/// earliest available block from the execution layer, taking the maximum of the two as the -/// floor. If a target halt height is configured and is less than the latest height, it further -/// caps the minimum height to the target halt height. This ensures that the consensus engine -/// does not request history at heights where either the CL store or EL no longer has data, -/// and does not go below the configured halt height which typically corresponds to a hard fork -/// at the consensus level. -pub async fn handle(state: &State, engine: &Engine, reply: Reply) -> eyre::Result<()> { - let cl_earliest_height = state - .store() - .min_height() - .await - .wrap_err("Failed to get earliest height from the store")? - .unwrap_or_default(); - - let cl_latest_height = state - .store() - .max_height() - .await - .wrap_err("Failed to get latest height from the store")? - .unwrap_or_default(); - - let el_earliest_height = match engine.eth.get_block_by_number("earliest").await { - Ok(Some(block)) => Height::new(block.block_number), - Ok(None) => { - warn!("EL returned no block for 'earliest', falling back to CL-only"); - cl_earliest_height - } - Err(e) => { - warn!("Failed to get EL earliest block, falling back to CL-only: {e:#}"); - cl_earliest_height - } - }; - - let halt_height = state.env_config().halt_height; - - debug!( - %cl_earliest_height, - %el_earliest_height, - %cl_latest_height, - halt_height = halt_height.map(|h| h.to_string()).unwrap_or_else(|| "None".to_string()), - "History bounds" - ); - - let min_height = get_history_min_height( - cl_earliest_height, - el_earliest_height, - cl_latest_height, - halt_height, - ); - - if let Err(e) = reply.send(min_height) { - error!("🔴 Failed to send reply: {e:?}"); - } - - Ok(()) -} - -pub fn get_history_min_height( - cl_earliest_height: Height, - el_earliest_height: Height, - cl_latest_height: Height, - target_halt_height: Option, -) -> Height { - let floor = cl_earliest_height.max(el_earliest_height); - - if let Some(halt_height) = target_halt_height - && halt_height < cl_latest_height - { - floor.max(halt_height) - } else { - floor - } -} - -#[cfg(test)] -mod tests { - use super::*; - - const fn h(n: u64) -> Height { - Height::new(n) - } - - #[test] - fn test_get_history_min_height() { - // No halt height configured, CL and EL agree - assert_eq!(get_history_min_height(h(10), h(10), h(100), None), h(10)); - - // Halt height greater than max decided block height - assert_eq!( - get_history_min_height(h(10), h(10), h(100), Some(h(150))), - h(10) - ); - - // Halt height less than max decided block height, but greater than earliest height - assert_eq!( - get_history_min_height(h(10), h(10), h(100), Some(h(50))), - h(50) - ); - - // Halt height less than both max decided block height and earliest height - assert_eq!( - get_history_min_height(h(60), h(60), h(100), Some(h(50))), - h(60) - ); - } - - #[test] - fn test_el_earliest_higher_than_cl() { - // EL has pruned blocks, so its earliest is higher than CL's - assert_eq!(get_history_min_height(h(10), h(50), h(100), None), h(50)); - } - - #[test] - fn test_cl_earliest_higher_than_el() { - // CL earliest is higher than EL earliest - assert_eq!(get_history_min_height(h(50), h(10), h(100), None), h(50)); - } - - #[test] - fn test_el_earliest_with_halt_height() { - // EL earliest (50) > halt height (30), halt height < latest (100) - // floor = max(10, 50) = 50, then max(50, 30) = 50 - assert_eq!( - get_history_min_height(h(10), h(50), h(100), Some(h(30))), - h(50) - ); - - // EL earliest (20) < halt height (50), halt height < latest (100) - // floor = max(10, 20) = 20, then max(20, 50) = 50 - assert_eq!( - get_history_min_height(h(10), h(20), h(100), Some(h(50))), - h(50) - ); - } - - #[test] - fn test_halt_height_equals_latest_height() { - // halt_height == cl_latest_height: halt has not been passed yet, so it is ignored - assert_eq!( - get_history_min_height(h(10), h(10), h(100), Some(h(100))), - h(10) - ); - } -} diff --git a/crates/malachite-app/src/handlers/get_value.rs b/crates/malachite-app/src/handlers/get_value.rs deleted file mode 100644 index 517c88c..0000000 --- a/crates/malachite-app/src/handlers/get_value.rs +++ /dev/null @@ -1,310 +0,0 @@ -// Copyright 2025 Circle Internet Group, Inc. All rights reserved. -// -// SPDX-License-Identifier: Apache-2.0 -// -// Licensed under the Apache License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. -// You may obtain a copy of the License at -// -// http://www.apache.org/licenses/LICENSE-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// See the License for the specific language governing permissions and -// limitations under the License. - -use std::time::{Duration, Instant}; - -use eyre::{eyre, Context}; -use tokio::sync::mpsc; -use tracing::{debug, error, info, warn}; - -use malachitebft_app_channel::app::streaming::StreamId; -use malachitebft_app_channel::app::types::core::Round; -use malachitebft_app_channel::app::types::LocallyProposedValue; -use malachitebft_app_channel::{NetworkMsg, Reply}; -use malachitebft_core_types::Validity; - -use arc_consensus_types::{Address, ArcContext, Height}; -use arc_eth_engine::engine::Engine; -use arc_eth_engine::json_structures::ExecutionBlock; -use arc_signer::ArcSigningProvider; - -use crate::block::ConsensusBlock; -use crate::metrics::AppMetrics; -use crate::payload::{ - generate_payload_with_retry, validate_consensus_block, EnginePayloadGenerator, - EnginePayloadValidator, -}; -use crate::proposal_parts::{prepare_stream, stream_proposal}; -use crate::state::State; -use crate::store::repositories::UndecidedBlocksRepository; -use crate::store::Store; -use crate::utils::pretty::PrettyPayload; - -type NetworkHandle = mpsc::Sender>; - -/// Handles the `GetValue` message from the consensus engine. -/// -/// This is called when the consensus engine requests a value to propose for a specific height and round. -/// -/// - The application first checks if there are any previously built blocks for the given height and round. -/// - If such blocks exist, it selects the first one to propose. -/// - If no previously built blocks are found, the application builds a new block using the execution engine, -/// validates it, and prepares it for proposal. -/// - Finally, it sends the proposed value back to the consensus engine and streams the proposal parts over the network. -pub async fn handle( - state: &mut State, - network: NetworkHandle, - engine: &Engine, - height: Height, - round: Round, - timeout: Duration, - reply: Reply>, -) -> eyre::Result<()> { - let metrics = state.metrics().clone(); - let store = state.store().clone(); - - let address = state.address(); - let fee_recipient = state.fee_recipient(); - let stream_id = state.next_stream_id(height, round); - let previous_block = state.previous_block.as_ref(); - let signing_provider = state.signing_provider(); - - let proposed_value = on_get_value( - network, - engine, - metrics, - store, - height, - round, - address, - previous_block, - fee_recipient, - signing_provider, - stream_id, - timeout, - ) - .await?; - - if let Some(proposed_value) = proposed_value { - if round.as_i64() == 0 { - if let Some(monitor) = &mut state.proposal_monitor { - debug_assert_eq!(monitor.height, height, "proposal monitor height mismatch"); - monitor.record_proposal(proposed_value.value.id()); - } else { - warn!(%height, %round, "No proposal monitor present"); - } - } - - if let Err(e) = reply.send(proposed_value) { - error!("🔴 GetValue: Failed to send reply: {e:?}"); - } - } - - Ok(()) -} - -#[allow(clippy::too_many_arguments)] -async fn on_get_value( - network: NetworkHandle, - engine: &Engine, - metrics: AppMetrics, - store: Store, - height: Height, - round: Round, - address: Address, - previous_block: Option<&ExecutionBlock>, - fee_recipient: Address, - signing_provider: &ArcSigningProvider, - stream_id: StreamId, - timeout: Duration, -) -> eyre::Result>> { - let block = get_previously_built_block(&store, address, height, round) - .await - .wrap_err_with(|| { - format!( - "Proposer failed to get previously built blocks (if any) for height {} and round {}", - height, round, - ) - })?; - - let mut block = match block { - Some(block) => { - info!(block_hash = %block.block_hash(), "✅ Using previously built block"); - block - } - None => { - info!(%height, %round, "🌈 Building new block"); - - let previous_block = previous_block.ok_or_else(|| { - eyre!("No previous block available to build new block at height={height} and round={round}") - })?; - - let task = build_and_validate_block( - engine, - &metrics, - &store, - height, - round, - address, - previous_block, - &fee_recipient, - ); - - let result = match tokio::time::timeout(timeout, task).await { - Ok(result) => result, - Err(_) => { - error!(%height, %round, "⏰ Proposer timed out while building block after {timeout:?}"); - return Ok(None); - } - }; - - result.wrap_err_with(|| { - format!("Proposer failed to build new block at height={height} and round={round}") - })? - } - }; - - let proposed_value = LocallyProposedValue::from(&block); - - debug!( - %height, %round, - block_size = %block.size_bytes(), - payload_size = %block.payload_size(), - "🎁 Sending proposal: {proposed_value:?}", - ); - - let block_hash = block.block_hash(); - - let (stream_messages, signature) = prepare_stream(stream_id, signing_provider, &block) - .await - .wrap_err_with(|| { - format!( - "Proposer failed to prepare stream for block {block_hash} \ - it wants to propose at height={height}, round={round}", - ) - })?; - - // Store the block with its signature - block.signature = Some(signature); - store - .store_undecided_block(block) - .await - .wrap_err_with(|| format!("Proposer failed to store block {block_hash}"))?; - - tokio::spawn(async move { - if let Err(e) = stream_proposal(network, height, round, stream_messages).await { - error!(%height, %round, "🔴 Failed to stream proposal parts: {e:#}"); - } - }); - - debug!(%height, %round, "✅ Proposal sent"); - - Ok(Some(proposed_value)) -} - -/// Builds a new execution payload and validates it via the Engine API. -/// -/// If the engine rejects the payload, an [`InvalidPayload`] record is -/// persisted (handled by [`validate_consensus_block`]) and the function -/// returns an error since a self-built block should never be invalid. -#[allow(clippy::too_many_arguments)] -async fn build_and_validate_block( - engine: &Engine, - metrics: &AppMetrics, - store: &Store, - height: Height, - round: Round, - proposer: Address, - previous_block: &ExecutionBlock, - fee_recipient: &Address, -) -> eyre::Result { - let start = Instant::now(); - - let block = build_block( - engine, - metrics, - height, - round, - proposer, - previous_block, - fee_recipient, - ) - .await?; - - let validator = EnginePayloadValidator::new(engine, metrics); - let validity = validate_consensus_block(&validator, &block, store, metrics) - .await - .wrap_err_with(|| { - format!( - "Payload validation failed on self-built block at height={height}, round={round}: {}", - block.block_hash() - ) - })?; - - if !validity.is_valid() { - return Err(eyre!("Self-built block {} is invalid", block.block_hash())); - } - - debug!( - "✅ Proposer validated self-built block {}", - block.block_hash() - ); - - metrics.observe_block_build_time(start.elapsed().as_secs_f64()); - - Ok(block) -} - -/// Build a new block, validate it, and store it alongside its corresponding proposal. -/// -/// Includes timing delay enforcement to ensure proper block intervals -pub async fn build_block( - engine: &Engine, - metrics: &AppMetrics, - height: Height, - round: Round, - proposer: Address, - previous_block: &ExecutionBlock, - fee_recipient: &Address, -) -> eyre::Result { - let generator = EnginePayloadGenerator { engine }; // TODO: make this configurable - - let execution_payload = - generate_payload_with_retry(previous_block, fee_recipient, &generator, metrics).await?; - - debug!( - "🌈 Got execution payload: {:?}", - PrettyPayload(&execution_payload) - ); - - Ok(ConsensusBlock { - height, - round, - valid_round: Round::Nil, - proposer, - validity: Validity::Valid, - execution_payload, - signature: None, - }) -} - -/// Retrieves the previously built block for the given height and round. -/// Called by the consensus engine to re-use a previously built block. -/// Returns the first block found for the given height and round with the matching proposer. -/// -/// There should be at most one block for a given height and round when the proposer is not byzantine. -/// We assume this implementation is not byzantine and we are the proposer for the given height and round. -/// Therefore there must be a single block for the rounds where we are the proposer, with the proposer address matching our own. -async fn get_previously_built_block( - undecided_blocks: impl UndecidedBlocksRepository, - proposer: Address, - height: Height, - round: Round, -) -> eyre::Result> { - let blocks = undecided_blocks.get_by_round(height, round).await?; - let block = blocks.into_iter().find(|p| p.proposer == proposer); - Ok(block) -} diff --git a/crates/malachite-app/src/handlers/mod.rs b/crates/malachite-app/src/handlers/mod.rs deleted file mode 100644 index 2b2215a..0000000 --- a/crates/malachite-app/src/handlers/mod.rs +++ /dev/null @@ -1,34 +0,0 @@ -// Copyright 2025 Circle Internet Group, Inc. All rights reserved. -// -// SPDX-License-Identifier: Apache-2.0 -// -// Licensed under the Apache License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. -// You may obtain a copy of the License at -// -// http://www.apache.org/licenses/LICENSE-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// See the License for the specific language governing permissions and -// limitations under the License. - -//! This module contains the handlers for messages received from the consensus engine. -//! -//! Each submodule corresponds to a variant of the `AppMsg` enum and -//! exposes a `handle` function to process the message. - -pub mod consensus_ready; -pub mod decided; -pub mod finalized; -pub mod get_decided_values; -pub mod get_history_min_height; -pub mod get_value; -pub mod process_synced_value; -pub mod received_proposal_part; -pub mod restream_proposal; -pub mod started_round; - -#[cfg(test)] -mod test_utils; diff --git a/crates/malachite-app/src/handlers/process_synced_value.rs b/crates/malachite-app/src/handlers/process_synced_value.rs deleted file mode 100644 index f56125d..0000000 --- a/crates/malachite-app/src/handlers/process_synced_value.rs +++ /dev/null @@ -1,716 +0,0 @@ -// Copyright 2025 Circle Internet Group, Inc. All rights reserved. -// -// SPDX-License-Identifier: Apache-2.0 -// -// Licensed under the Apache License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. -// You may obtain a copy of the License at -// -// http://www.apache.org/licenses/LICENSE-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// See the License for the specific language governing permissions and -// limitations under the License. - -use std::time::Duration; - -use bytes::Bytes; -use eyre::Context; -use ssz::Decode; -use tracing::{error, warn}; - -use malachitebft_app_channel::app::types::core::Round; -use malachitebft_app_channel::app::types::ProposedValue; -use malachitebft_app_channel::Reply; - -use alloy_rpc_types_engine::ExecutionPayloadV3; -use arc_consensus_types::{Address, ArcContext, Height}; -use arc_eth_engine::engine::Engine; -use arc_eth_engine::persistence_meter::PersistenceMeter; - -use malachitebft_app_channel::app::types::core::Validity; - -use crate::block::ConsensusBlock; -use crate::metrics::{AppMetrics, InvalidPayloadSource}; -use crate::payload::{validate_consensus_block, EnginePayloadValidator, PayloadValidator}; -use crate::state::State; -use crate::store::repositories::{InvalidPayloadsRepository, UndecidedBlocksRepository}; -use arc_consensus_db::invalid_payloads::InvalidPayload; - -/// Timeout when blocked waiting for EL persistence to catch up during sync. -const SYNC_PERSISTENCE_WAIT_TIMEOUT: Duration = Duration::from_secs(30); - -/// Handles the `ProcessSyncedValue` message from the consensus engine. -/// -/// This is called when the consensus engine has received a value via sync for a given height and round. -/// The application processes the synced value, validates it, and stores it for future use. -/// If the value is valid, it is returned as a `ProposedValue` to the consensus engine. -/// If the value is invalid, `None` is returned. -/// In both cases, the block is stored in the undecided blocks store for use once consensus reaches -/// that height. -pub async fn handle( - state: &mut State, - engine: &Engine, - height: Height, - round: Round, - proposer: Address, - value_bytes: Bytes, - reply: Reply>>, -) -> Result<(), eyre::Error> { - let proposal = match on_process_synced_value( - EnginePayloadValidator::new(engine, state.metrics()), - state.store(), - state.store(), - state.persistence_meter(), - state.metrics(), - height, - round, - proposer, - value_bytes, - ) - .await - { - Ok(proposal) => proposal, - Err(e) => { - error!(%height, %round, %proposer, "ProcessSyncedValue failed: {e:#}"); - if let Err(send_err) = reply.send(None) { - error!("🔴 ProcessSyncedValue: Failed to send error reply: {send_err:?}"); - } - return Err(e); - } - }; - - // Mark this height as synced for proposal monitoring - if let Some(p) = &proposal - && p.validity.is_valid() - { - state.mark_height_synced(height); - } - - if let Err(e) = reply.send(proposal) { - error!("🔴 ProcessSyncedValue: Failed to send reply: {e:?}"); - } - - Ok(()) -} - -/// Processes a synced value received from a peer. -/// -/// Decodes the raw bytes into an [`ExecutionPayloadV3`], validates it via -/// [`validate_consensus_block`], and stores the resulting [`ConsensusBlock`] as an -/// undecided block. If the engine rejects the payload, an -/// [`InvalidPayload`](crate::invalid_payloads::InvalidPayload) record is persisted -/// through `store` and the block is kept with [`Validity::Invalid`] so that -/// consensus can proceed with the correct validity information. -/// -/// Returns `Ok(None)` when the raw bytes cannot be SSZ-decoded (the error is logged -/// but not propagated). -#[allow(clippy::too_many_arguments)] -async fn on_process_synced_value( - engine: impl PayloadValidator, - undecided_blocks_repo: impl UndecidedBlocksRepository, - invalid_payloads_repo: impl InvalidPayloadsRepository, - persistence_meter: impl PersistenceMeter, - metrics: &AppMetrics, - height: Height, - round: Round, - proposer: Address, - value_bytes: Bytes, -) -> eyre::Result>> { - let payload = match ExecutionPayloadV3::from_ssz_bytes(&value_bytes) { - Ok(payload) => payload, - Err(e) => { - warn!( - %height, %round, %proposer, - "Failed to decode synced value into an execution payload: {e:?}", - ); - metrics.inc_invalid_payloads_count(InvalidPayloadSource::SyncDecode); - - let invalid = - InvalidPayload::new_without_payload(height, round, proposer, &format!("{e:?}")); - - invalid_payloads_repo.append(invalid).await.wrap_err_with(|| { - format!( - "Failed to store invalid payload after receiving synced value (height={height}, round={round}, proposer={proposer})", - ) - })?; - - return Ok(None); - } - }; - - // Build the block before validation so that - // `validate_consensus_block` can record an `InvalidPayload` - // with the full block context if the engine rejects it. - let mut block = ConsensusBlock { - height, - round, - valid_round: Round::Nil, - proposer, - execution_payload: payload, - validity: Validity::Valid, - signature: None, - }; - - let validity = validate_consensus_block( - &engine, &block, &invalid_payloads_repo, metrics, - ) - .await - .wrap_err_with(|| { - format!( - "Payload validation failed on block built from synced value at height={}, round={} received from {}", - height, round, proposer, - ) - })?; - - block.validity = validity; - - let block_hash = block.block_hash(); - - if !validity.is_valid() { - error!(%height, %round, %proposer, %block_hash, "❌ Received invalid payload via sync"); - } - - // If a undecided block for the sync value height round and hash exists then skip `wait_for_persisted_block` - // so consensus path is not blocked on EL persistence. - if let Some(existing) = undecided_blocks_repo - .get_by_round_and_hash(height, round, block_hash) - .await - .wrap_err_with(|| { - format!( - "Failed to query undecided blocks repo for dedup at \ - height={height}, round={round}, block_hash={block_hash}" - ) - })? - { - debug_assert_eq!( - existing.validity, validity, - "dedup hit at height={height}, round={round}, block_hash={block_hash}: \ - existing.validity ({:?}) != freshly-computed validity ({validity:?})", - existing.validity, - ); - return Ok(Some(ProposedValue::from(&existing))); - } - - let proposal = ProposedValue::from(&block); - - undecided_blocks_repo.store_undecided_block(block).await.wrap_err_with(|| { - format!( - "Failed to store undecided block {} synced from the network for height={}, round={}, proposer={}", - block_hash, height, round, proposer, - ) - })?; - - if validity.is_valid() { - if let Err(e) = persistence_meter - .wait_for_persisted_block(height.as_u64(), SYNC_PERSISTENCE_WAIT_TIMEOUT) - .await - { - error!( - block_number = height.as_u64(), - %e, - "ProcessSyncedValue: persistence backpressure timed out, proceeding" - ); - } - } - - Ok(Some(proposal)) -} - -#[cfg(test)] -mod tests { - use super::*; - - use crate::payload::{MockPayloadValidator, PayloadValidationResult}; - use crate::store::repositories::mocks::{ - MockInvalidPayloadsRepository, MockUndecidedBlocksRepository, - }; - - use arbitrary::{Arbitrary, Unstructured}; - use arc_consensus_types::Value; - use arc_eth_engine::mocks::MockPersistenceMeter; - use arc_eth_engine::persistence_meter::NoopPersistenceMeter; - use bytes::Bytes; - use malachitebft_core_types::Validity; - use mockall::predicate::*; - use ssz::Encode; - use std::io; - - /// Sets up the dedup query (`get_by_round_and_hash`) on an - /// `UndecidedBlocksRepository` mock to return `None` so the main path - /// flows through to `store_undecided_block`. Use in tests that are not - /// specifically exercising the dedup race. - fn expect_no_undecided_dedup_hit(mock: &mut MockUndecidedBlocksRepository) { - mock.expect_get_by_round_and_hash() - .returning(|_, _, _| Ok(None)); - } - - async fn test_on_process_synced_value_validity( - result: PayloadValidationResult, - expected: Validity, - ) { - let mut u = Unstructured::new(&[0u8; 512]); - - let height = Height::new(1); - let round = Round::new(0); - let proposer = Address::new([0u8; 20]); - let payload = ExecutionPayloadV3::arbitrary(&mut u).unwrap(); - let value_bytes = Bytes::from(payload.as_ssz_bytes()); - - let mut engine = MockPayloadValidator::new(); - engine - .expect_validate_payload() - .with(always()) - .returning(move |_| Ok(result.clone())); - - let mut undecided = MockUndecidedBlocksRepository::new(); - expect_no_undecided_dedup_hit(&mut undecided); - undecided - .expect_store_undecided_block() - .withf(move |block| { - block.height == height && block.round == round && block.proposer == proposer - }) - .times(1) - .returning(|_| Ok(())); - - let is_invalid = !expected.is_valid(); - let mut invalid = MockInvalidPayloadsRepository::new(); - invalid - .expect_append() - .times(if is_invalid { 1 } else { 0 }) - .returning(|_| Ok(())); - - let metrics = AppMetrics::default(); - let Some(proposal) = on_process_synced_value( - engine, - undecided, - invalid, - NoopPersistenceMeter, - &metrics, - height, - round, - proposer, - value_bytes, - ) - .await - .expect("Failed to process synced value") else { - panic!("Expected proposal to be Some even for invalid payload"); - }; - - assert_eq!(proposal.validity, expected); - let expected_count = if expected.is_valid() { 0 } else { 1 }; - assert_eq!(metrics.get_invalid_payloads_count(), expected_count); - } - - #[tokio::test] - async fn on_process_synced_value_invalid_payload() { - test_on_process_synced_value_validity( - PayloadValidationResult::Invalid { - reason: "test rejection".into(), - }, - Validity::Invalid, - ) - .await; - } - - #[tokio::test] - async fn on_process_synced_value_valid_payload() { - test_on_process_synced_value_validity(PayloadValidationResult::Valid, Validity::Valid) - .await; - } - - #[tokio::test] - async fn test_on_process_synced_value_invalid_bytes() { - let mut engine = MockPayloadValidator::new(); - let mut undecided = MockUndecidedBlocksRepository::new(); - let mut invalid = MockInvalidPayloadsRepository::new(); - - // Expectation: If bytes are invalid, we should NOT validate the payload - // and definitely NOT store the block. - engine.expect_validate_payload().times(0); - undecided.expect_store_undecided_block().times(0); - invalid.expect_append().times(1).returning(|_| Ok(())); - - let height = Height::new(1); - let round = Round::new(0); - let proposer = Address::new([0u8; 20]); - let value_bytes = Bytes::from(vec![0u8; 10]); - - let metrics = AppMetrics::default(); - let proposal = on_process_synced_value( - engine, - undecided, - invalid, - NoopPersistenceMeter, - &metrics, - height, - round, - proposer, - value_bytes, - ) - .await - .expect("Failed to process synced value"); - - assert!(proposal.is_none()); - assert_eq!(metrics.get_invalid_payloads_count(), 1); - } - - // These two tests cover error paths in `on_process_synced_value` that were - // previously untested. They do NOT directly test the error-reply-before-propagation - // fix in `handle()`, which requires `State` and `Engine` -- concrete types - // with no test builder. - #[tokio::test] - async fn on_process_synced_value_engine_validation_error() { - let mut u = Unstructured::new(&[0u8; 512]); - - let height = Height::new(1); - let round = Round::new(0); - let proposer = Address::new([0u8; 20]); - let payload = ExecutionPayloadV3::arbitrary(&mut u).unwrap(); - let value_bytes = Bytes::from(payload.as_ssz_bytes()); - - let mut engine = MockPayloadValidator::new(); - engine - .expect_validate_payload() - .returning(|_| Err(io::Error::other("Simulated engine error").into())); - - let mut undecided = MockUndecidedBlocksRepository::new(); - undecided.expect_store_undecided_block().times(0); - - let mut invalid = MockInvalidPayloadsRepository::new(); - invalid.expect_append().times(0); - - let metrics = AppMetrics::default(); - let result = on_process_synced_value( - engine, - undecided, - invalid, - NoopPersistenceMeter, - &metrics, - height, - round, - proposer, - value_bytes, - ) - .await; - - assert!(result.is_err()); - assert_eq!(metrics.get_invalid_payloads_count(), 0); - } - - #[tokio::test] - async fn on_process_synced_value_invalid_payload_store_fails() { - let height = Height::new(1); - let round = Round::new(0); - let proposer = Address::new([0u8; 20]); - let value_bytes = Bytes::from(vec![0u8; 10]); // garbage bytes trigger SSZ decode failure - - let mut engine = MockPayloadValidator::new(); - engine.expect_validate_payload().times(0); - - let mut undecided = MockUndecidedBlocksRepository::new(); - undecided.expect_store_undecided_block().times(0); - - let mut invalid = MockInvalidPayloadsRepository::new(); - invalid - .expect_append() - .times(1) - .returning(|_| Err(io::Error::other("Simulated invalid payload store error"))); - - let metrics = AppMetrics::default(); - let result = on_process_synced_value( - engine, - undecided, - invalid, - NoopPersistenceMeter, - &metrics, - height, - round, - proposer, - value_bytes, - ) - .await; - - assert!(result.is_err()); - assert_eq!(metrics.get_invalid_payloads_count(), 1); - } - - #[tokio::test] - async fn test_on_process_synced_value_store_error() { - let mut u = Unstructured::new(&[0u8; 512]); - - let height = Height::new(1); - let round = Round::new(0); - let proposer = Address::new([0u8; 20]); - let payload = ExecutionPayloadV3::arbitrary(&mut u).unwrap(); - let value_bytes = Bytes::from(payload.as_ssz_bytes()); - - let mut engine = MockPayloadValidator::new(); - engine - .expect_validate_payload() - .returning(|_| Ok(PayloadValidationResult::Valid)); - - let mut undecided = MockUndecidedBlocksRepository::new(); - expect_no_undecided_dedup_hit(&mut undecided); - undecided - .expect_store_undecided_block() - .times(1) - .returning(|_| Err(io::Error::other("Simulated store error"))); - - let mut invalid = MockInvalidPayloadsRepository::new(); - invalid.expect_append().times(0); - - let metrics = AppMetrics::default(); - let result = on_process_synced_value( - engine, - undecided, - invalid, - NoopPersistenceMeter, - &metrics, - height, - round, - proposer, - value_bytes, - ) - .await; - - assert!(result.is_err()); - assert!(result.unwrap_err().downcast_ref::().is_some()); - assert_eq!(metrics.get_invalid_payloads_count(), 0); - } - - #[tokio::test] - async fn on_process_synced_value_calls_persistence_meter_for_valid_payload() { - let mut u = Unstructured::new(&[0u8; 512]); - - let height = Height::new(42); - let round = Round::new(0); - let proposer = Address::new([0u8; 20]); - let payload = ExecutionPayloadV3::arbitrary(&mut u).unwrap(); - let value_bytes = Bytes::from(payload.as_ssz_bytes()); - - let mut engine = MockPayloadValidator::new(); - engine - .expect_validate_payload() - .returning(|_| Ok(PayloadValidationResult::Valid)); - - let mut undecided = MockUndecidedBlocksRepository::new(); - expect_no_undecided_dedup_hit(&mut undecided); - undecided - .expect_store_undecided_block() - .times(1) - .returning(|_| Ok(())); - - let mut invalid = MockInvalidPayloadsRepository::new(); - invalid.expect_append().times(0); - - let mut persistence_meter = MockPersistenceMeter::new(); - persistence_meter - .expect_wait_for_persisted_block() - .withf(|&block, _| block == 42) - .times(1) - .return_once(|_, _| Ok(())); - - let metrics = AppMetrics::default(); - let proposal = on_process_synced_value( - engine, - undecided, - invalid, - persistence_meter, - &metrics, - height, - round, - proposer, - value_bytes, - ) - .await - .expect("should succeed"); - - assert!(proposal.is_some()); - assert_eq!(proposal.unwrap().validity, Validity::Valid); - assert_eq!(metrics.get_invalid_payloads_count(), 0); - } - - #[tokio::test] - async fn on_process_synced_value_skips_persistence_meter_for_invalid_payload() { - let mut u = Unstructured::new(&[0u8; 512]); - - let height = Height::new(42); - let round = Round::new(0); - let proposer = Address::new([0u8; 20]); - let payload = ExecutionPayloadV3::arbitrary(&mut u).unwrap(); - let value_bytes = Bytes::from(payload.as_ssz_bytes()); - - let mut engine = MockPayloadValidator::new(); - engine.expect_validate_payload().returning(|_| { - Ok(PayloadValidationResult::Invalid { - reason: "bad".into(), - }) - }); - - let mut undecided = MockUndecidedBlocksRepository::new(); - expect_no_undecided_dedup_hit(&mut undecided); - undecided - .expect_store_undecided_block() - .times(1) - .returning(|_| Ok(())); - - let mut invalid = MockInvalidPayloadsRepository::new(); - invalid.expect_append().times(1).returning(|_| Ok(())); - - let mut persistence_meter = MockPersistenceMeter::new(); - persistence_meter.expect_wait_for_persisted_block().times(0); - - let metrics = AppMetrics::default(); - let proposal = on_process_synced_value( - engine, - undecided, - invalid, - persistence_meter, - &metrics, - height, - round, - proposer, - value_bytes, - ) - .await - .expect("should succeed"); - - assert!(proposal.is_some()); - assert_eq!(proposal.unwrap().validity, Validity::Invalid); - assert_eq!(metrics.get_invalid_payloads_count(), 1); - } - - #[tokio::test] - async fn on_process_synced_value_proceeds_when_persistence_meter_fails() { - let mut u = Unstructured::new(&[0u8; 512]); - - let height = Height::new(7); - let round = Round::new(0); - let proposer = Address::new([0u8; 20]); - let payload = ExecutionPayloadV3::arbitrary(&mut u).unwrap(); - let value_bytes = Bytes::from(payload.as_ssz_bytes()); - - let mut engine = MockPayloadValidator::new(); - engine - .expect_validate_payload() - .returning(|_| Ok(PayloadValidationResult::Valid)); - - let mut undecided = MockUndecidedBlocksRepository::new(); - expect_no_undecided_dedup_hit(&mut undecided); - undecided - .expect_store_undecided_block() - .times(1) - .returning(|_| Ok(())); - - let mut invalid = MockInvalidPayloadsRepository::new(); - invalid.expect_append().times(0); - - let mut persistence_meter = MockPersistenceMeter::new(); - persistence_meter - .expect_wait_for_persisted_block() - .withf(|&block, _| block == 7) - .times(1) - .return_once(|_, _| Err(eyre::eyre!("persistence meter timeout"))); - - let metrics = AppMetrics::default(); - let proposal = on_process_synced_value( - engine, - undecided, - invalid, - persistence_meter, - &metrics, - height, - round, - proposer, - value_bytes, - ) - .await - .expect("should succeed even when meter fails"); - - assert!(proposal.is_some()); - assert_eq!(proposal.unwrap().validity, Validity::Valid); - assert_eq!(metrics.get_invalid_payloads_count(), 0); - } - - /// Race: the proposer's gossiped proposal arrived first and was already - /// stored as `UndecidedBlock(height, round, block_hash)` (and the EL has - /// it persisted) by the time the in-flight ProcessSyncedValue with - /// identical bytes runs. We must: - /// - still run engine validation (defense in depth: the synced bytes - /// are from a peer we don't trust implicitly), but - /// - skip the redundant `store_undecided_block` upsert and the - /// `wait_for_persisted_block` call, - /// and return a `ProposedValue` carrying the existing block's validity - /// (which equals the freshly-validated one, since engine validation is - /// deterministic). - #[tokio::test] - async fn on_process_synced_value_dedups_against_existing_undecided_block() { - let mut u = Unstructured::new(&[7u8; 512]); - - let height = Height::new(42); - let round = Round::new(0); - let proposer = Address::new([1u8; 20]); - let payload = ExecutionPayloadV3::arbitrary(&mut u).unwrap(); - let block_hash = payload.payload_inner.payload_inner.block_hash; - let value_bytes = Bytes::from(payload.as_ssz_bytes()); - - let existing_block = ConsensusBlock { - height, - round, - valid_round: Round::Nil, - proposer, - execution_payload: payload, - validity: Validity::Valid, - signature: None, - }; - - // Engine validation still runs once (defense in depth on the synced - // bytes), and accepts. - let mut engine = MockPayloadValidator::new(); - engine - .expect_validate_payload() - .times(1) - .returning(|_| Ok(PayloadValidationResult::Valid)); - - let mut undecided = MockUndecidedBlocksRepository::new(); - undecided - .expect_get_by_round_and_hash() - .with(eq(height), eq(round), eq(block_hash)) - .times(1) - .return_once(move |_, _, _| Ok(Some(existing_block))); - // No redundant upsert. - undecided.expect_store_undecided_block().times(0); - - let mut invalid = MockInvalidPayloadsRepository::new(); - invalid.expect_append().times(0); - - // No persistence wait — the proposer's path already satisfied it. - let mut persistence_meter = MockPersistenceMeter::new(); - persistence_meter.expect_wait_for_persisted_block().times(0); - - let metrics = AppMetrics::default(); - let proposal = on_process_synced_value( - engine, - undecided, - invalid, - persistence_meter, - &metrics, - height, - round, - proposer, - value_bytes, - ) - .await - .expect("should succeed via dedup short-circuit"); - - let proposal = proposal.expect("expected Some(proposal) on dedup hit"); - assert_eq!(proposal.height, height); - assert_eq!(proposal.round, round); - assert_eq!(proposal.proposer, proposer); - assert_eq!(proposal.validity, Validity::Valid); - assert_eq!(proposal.value, Value::new(block_hash)); - } -} diff --git a/crates/malachite-app/src/handlers/received_proposal_part.rs b/crates/malachite-app/src/handlers/received_proposal_part.rs deleted file mode 100644 index d68cb9b..0000000 --- a/crates/malachite-app/src/handlers/received_proposal_part.rs +++ /dev/null @@ -1,466 +0,0 @@ -// Copyright 2025 Circle Internet Group, Inc. All rights reserved. -// -// SPDX-License-Identifier: Apache-2.0 -// -// Licensed under the Apache License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. -// You may obtain a copy of the License at -// -// http://www.apache.org/licenses/LICENSE-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// See the License for the specific language governing permissions and -// limitations under the License. - -use eyre::Context as _; -use tracing::{debug, error, info, warn}; - -use malachitebft_app_channel::app::streaming::{StreamContent, StreamMessage}; -use malachitebft_app_channel::app::types::core::Validity; -use malachitebft_app_channel::app::types::{PeerId, ProposedValue}; -use malachitebft_app_channel::Reply; -use malachitebft_core_types::Height as _; - -use arc_consensus_types::proposer::ProposerSelector; -use arc_consensus_types::{ArcContext, Height, ProposalPart, ProposalParts, Round, ValidatorSet}; -use arc_eth_engine::engine::Engine; -use arc_signer::ArcSigningProvider; - -use crate::block::ConsensusBlock; -use crate::metrics::{AppMetrics, InvalidPayloadSource}; -use crate::payload::{validate_consensus_block, EnginePayloadValidator}; -use crate::proposal_parts::{ - assemble_block_from_parts, resolve_expected_proposer, validate_proposal_parts, -}; -use crate::state::State; -use crate::store::Store; -use crate::streaming::{InsertResult, PartStreamsMap}; -use arc_consensus_db::invalid_payloads::InvalidPayload; - -/// Handles the `ReceivedProposalPart` message from the consensus engine. -/// -/// This is called when a proposal part is received from a peer. -/// The application processes the received part, and if the full proposal -/// has been reconstructed, it validates the payload. -/// If the block is valid, it returns the `ProposedValue` to the consensus engine. -/// If the block is invalid, it logs an error. -/// In both cases, the complete block is stored for future use once consensus -/// reaches that height. -pub async fn handle( - state: &mut State, - engine: &Engine, - from: PeerId, - part: StreamMessage, - reply: Reply>>, -) { - let max_pending_proposals = state.max_pending_proposals(); - let current_height = state.current_height; - let current_round = state.current_round; - let current_validator_set = state.validator_set().clone(); - let proposer_selector = state.ctx.proposer_selector; - - let context = HandlerContext { - engine, - store: state.store().clone(), - metrics: state.metrics().clone(), - signing_provider: state.signing_provider().clone(), - streams_map: state.streams_map_mut(), - current_height, - current_round, - current_validator_set, - proposer_selector: &proposer_selector, - max_pending_proposals, - }; - - let response = on_received_proposal_part(context, from, part) - .await - .inspect_err(|e| { - error!(%from, "🔴 Error processing proposal part: {e:#}"); - }) - .unwrap_or(None); - - if let Some(proposed_value) = &response { - record_proposal_in_monitor(state, proposed_value); - } - - if let Err(e) = reply.send(response) { - error!("🔴 ReceivedProposalPart: Failed to send reply: {e:?}"); - } -} - -/// Records the proposal receipt in the proposal monitor. -fn record_proposal_in_monitor(state: &mut State, proposed_value: &ProposedValue) { - let current_round = state.current_round; - if current_round.as_i64() != 0 { - // We only monitor round 0 - return; - } - - let Some(monitor) = &mut state.proposal_monitor else { - warn!( - %proposed_value.height, - %proposed_value.round, - %proposed_value.proposer, - "No proposal monitor present", - ); - return; - }; - - // Sanity checks - should always hold - if monitor.height != proposed_value.height || monitor.proposer != proposed_value.proposer { - warn!( - monitor.height = %monitor.height, - monitor.proposer = %monitor.proposer, - %proposed_value.height, - %proposed_value.proposer, - "Proposal monitor mismatch, skipping recording", - ); - return; - } - - if proposed_value.round.as_i64() != 0 { - warn!( - proposed_value.round = %proposed_value.round, - "Received proposed value not in round 0", - ); - return; - } - - monitor.record_proposal(proposed_value.value.id()); -} - -struct HandlerContext<'a, 'b> { - engine: &'a Engine, - store: Store, - metrics: AppMetrics, - signing_provider: ArcSigningProvider, - streams_map: &'b mut PartStreamsMap, - current_height: Height, - current_round: Round, - current_validator_set: ValidatorSet, - proposer_selector: &'a dyn ProposerSelector, - max_pending_proposals: usize, -} - -async fn on_received_proposal_part( - context: HandlerContext<'_, '_>, - from: PeerId, - part: StreamMessage, -) -> eyre::Result>> { - let (part_type, part_size) = match &part.content { - StreamContent::Data(part) => (part.get_type(), part.size_bytes()), - StreamContent::Fin => ("end of stream", 0), - }; - - info!( - %from, %part.sequence, part.type = %part_type, part.size = %part_size, stream_id = %part.stream_id, - "Received proposal part" - ); - - // Check if we have a full proposal - let parts = match context.streams_map.insert(from, part) { - InsertResult::Complete(parts) => parts, - InsertResult::Pending => return Ok(None), - // Benign: an old proposal part re-circulating on the network. Dropped - // without warning — not peer misbehaviour. - InsertResult::Stale => return Ok(None), - InsertResult::Invalid(e) => { - warn!(%from, error = %e, "Rejecting stream message"); - return Ok(None); - } - }; - - // Process complete proposal parts, validate and assemble them into a block. - let block = process_proposal_parts((&context).into(), parts).await?; - - let Some(mut block) = block else { - return Ok(None); - }; - - // Validate the block - validate_block( - context.engine, - &context.metrics, - &context.store, - &mut block, - from, - ) - .await?; - - let proposed_value = ProposedValue::from(&block); - - debug!( - block_size = %block.size_bytes(), - payload_size = %block.payload_size(), - "🎁 Received complete proposal: {proposed_value:?}", - ); - - // Store the full undecided block in the store - let block_hash = block.block_hash(); - - context.store.store_undecided_block(block).await.wrap_err_with(|| - format!( - "Failed to store undecided block {} built from parts received from {} for height={}, round={}, proposer={}", - block_hash, from, proposed_value.height, proposed_value.round, proposed_value.proposer, - ) - )?; - - Ok(Some(proposed_value)) -} - -/// Validates a block received from a peer via the Engine API and records -/// the result. If the engine rejects the payload, an [`InvalidPayload`] -/// record is persisted by [`validate_consensus_block`] and the block's -/// validity is set to [`Validity::Invalid`]. The block is kept either way -/// so that consensus can proceed with the correct validity information. -async fn validate_block( - engine: &Engine, - metrics: &AppMetrics, - store: &Store, - block: &mut ConsensusBlock, - from: PeerId, -) -> eyre::Result<()> { - let validator = EnginePayloadValidator::new(engine, metrics); - let validity = validate_consensus_block(&validator, block, store, metrics) - .await - .wrap_err_with(|| { - format!( - "Payload validation failed on block built after \ - receiving proposal part at height={}, round={} from {}", - block.height, block.round, from, - ) - })?; - - match validity { - Validity::Invalid => { - error!("❌ Received invalid block: {}", block.block_hash()); - } - Validity::Valid => { - debug!("✅ Received valid block: {}", block.block_hash()); - } - } - - // Update the block validity - block.validity = validity; - - Ok(()) -} - -struct ProcessingContext<'a> { - store: &'a Store, - metrics: &'a AppMetrics, - signing_provider: &'a ArcSigningProvider, - current_height: Height, - current_round: Round, - current_validator_set: &'a ValidatorSet, - proposer_selector: &'a dyn ProposerSelector, - max_pending_proposals: usize, -} - -impl<'a> From<&'a HandlerContext<'_, '_>> for ProcessingContext<'a> { - fn from(handler_ctx: &'a HandlerContext<'_, '_>) -> Self { - Self { - store: &handler_ctx.store, - metrics: &handler_ctx.metrics, - signing_provider: &handler_ctx.signing_provider, - current_height: handler_ctx.current_height, - current_round: handler_ctx.current_round, - current_validator_set: &handler_ctx.current_validator_set, - proposer_selector: handler_ctx.proposer_selector, - max_pending_proposals: handler_ctx.max_pending_proposals, - } - } -} - -/// Process complete proposal parts, validating and assembling them into a block. -/// -/// - If the parts are for a past height, they are ignored. -/// - If the parts are for a future height, they are stored in pending without validation. -/// - If the parts are for the current height, they are validated and assembled into a block. -/// -/// See [`validate_proposal_parts`] for details on validation. -async fn process_proposal_parts( - ctx: ProcessingContext<'_>, - parts: ProposalParts, -) -> eyre::Result> { - let parts_height = parts.height(); - let parts_round = parts.round(); - let parts_proposer = parts.proposer(); - - // Ignore the proposal if from past height - if parts_height < ctx.current_height { - debug!( - height = %ctx.current_height, - round = %ctx.current_round, - parts.height = %parts_height, - parts.round = %parts_round, - parts.proposer = %parts_proposer, - "Received proposal from a previous height, ignoring" - ); - - return Ok(None); - } - - // Store future proposals parts in pending without validation - if parts_height > ctx.current_height || parts_round > ctx.current_round { - maybe_store_pending_proposal( - ctx.store, - ctx.metrics, - ctx.current_height, - ctx.current_round, - ctx.max_pending_proposals, - parts, - ) - .await?; - - return Ok(None); - } - - debug_assert_eq!(parts_height, ctx.current_height); - - // Proposal is for the current height, validate its proposer and signature. - let expected_proposer = - resolve_expected_proposer(ctx.proposer_selector, ctx.current_validator_set, &parts); - - if !validate_proposal_parts(&parts, expected_proposer, ctx.signing_provider).await { - return Ok(None); - } - - // Assemble the block - let block = match assemble_block_from_parts(&parts) { - Ok(block) => block, - Err(e) => { - warn!( - height = %parts_height, - round = %parts_round, - proposer = %parts_proposer, - "Failed to assemble block from parts: {e}", - ); - ctx.metrics - .inc_invalid_payloads_count(InvalidPayloadSource::AssemblyFailure); - let invalid = InvalidPayload::new_from_parts(&parts, &e.to_string()); - ctx.store.append_invalid_payload(invalid).await.wrap_err_with(|| { - format!( - "Failed to store invalid payload after assembling block from parts (height={parts_height}, round={parts_round}, proposer={parts_proposer})", - ) - })?; - return Err(e.wrap_err(format!( - "Failed to assemble block from parts (height={parts_height}, round={parts_round}, proposer={parts_proposer})", - ))); - } - }; - - debug!("Block hash: {}", block.block_hash()); - - Ok(Some(block)) -} - -/// Store a pending proposal if it's not too far in the future -async fn maybe_store_pending_proposal( - store: &Store, - metrics: &AppMetrics, - current_height: Height, - current_round: Round, - max_pending_proposals: usize, - parts: ProposalParts, -) -> eyre::Result<()> { - // max_pending_proposals > 0 (asserted at construction); fits in u64 on 64-bit targets - #[allow(clippy::cast_possible_truncation, clippy::arithmetic_side_effects)] - let max_future_height = current_height.increment_by(max_pending_proposals as u64 - 1); - - // Check that proposal is not for a height too far in the future - if parts.height() > max_future_height { - debug!( - height = %current_height, - round = %current_round, - parts.height = %parts.height(), - parts.round = %parts.round(), - parts.proposer = %parts.proposer(), - max_height = %max_future_height, - "Received proposal for a height too far in the future, ignoring" - ); - return Ok(()); - } - - debug!( - height = %current_height, - round = %current_round, - parts.height = %parts.height(), - parts.round = %parts.round(), - parts.proposer = %parts.proposer(), - "Storing pending proposal for a future height/round" - ); - - // Store the parts for future processing - store - .store_pending_proposal_parts(parts, max_pending_proposals, current_height) - .await - .wrap_err("Failed to store pending proposal parts")?; - - // Update metrics - let pending_count = store - .get_pending_proposal_parts_count() - .await - .wrap_err("failed to get pending proposals count after storing new pending proposal")?; - - metrics.observe_pending_proposal_parts_count(pending_count); - - Ok(()) -} - -#[cfg(test)] -mod tests { - use super::*; - - use arc_consensus_db::{DbMetrics, DbUpgrade}; - use arc_consensus_types::proposer::RoundRobin; - use arc_consensus_types::Validator; - use arc_signer::local::{LocalSigningProvider, PrivateKey}; - use bytesize::ByteSize; - use tempfile::tempdir; - - use crate::handlers::test_utils::signed_parts_without_data; - - #[tokio::test] - async fn process_proposal_parts_increments_on_assembly_failure() { - let dir = tempdir().unwrap(); - let store = Store::open( - dir.path().join("db"), - DbMetrics::default(), - DbUpgrade::Skip, - ByteSize::mib(64), - ) - .await - .unwrap(); - - let signing_key = PrivateKey::generate(rand::rngs::OsRng); - let validator = Validator::new(signing_key.public_key(), 1); - let validator_set = ValidatorSet::new(vec![validator]); - - let height = Height::new(1); - let round = Round::new(0); - let parts = signed_parts_without_data(height, round, &signing_key).await; - - let selector = RoundRobin; - let provider = ArcSigningProvider::Local(LocalSigningProvider::new(signing_key)); - let metrics = AppMetrics::default(); - - let ctx = ProcessingContext { - store: &store, - metrics: &metrics, - signing_provider: &provider, - current_height: height, - current_round: round, - current_validator_set: &validator_set, - proposer_selector: &selector, - max_pending_proposals: 10, - }; - - let result = process_proposal_parts(ctx, parts).await; - - assert!(result.is_err()); - assert_eq!(metrics.get_invalid_payloads_count(), 1); - } -} diff --git a/crates/malachite-app/src/handlers/restream_proposal.rs b/crates/malachite-app/src/handlers/restream_proposal.rs deleted file mode 100644 index 267be36..0000000 --- a/crates/malachite-app/src/handlers/restream_proposal.rs +++ /dev/null @@ -1,299 +0,0 @@ -// Copyright 2025 Circle Internet Group, Inc. All rights reserved. -// -// SPDX-License-Identifier: Apache-2.0 -// -// Licensed under the Apache License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. -// You may obtain a copy of the License at -// -// http://www.apache.org/licenses/LICENSE-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// See the License for the specific language governing permissions and -// limitations under the License. - -use eyre::{eyre, Context}; -use tracing::{error, info}; - -use malachitebft_app_channel::app::streaming::StreamId; -use malachitebft_app_channel::app::types::core::Round; -use malachitebft_app_channel::Channels; - -use arc_consensus_types::signing::SigningProvider; -use arc_consensus_types::{ArcContext, BlockHash, Height, ValueId}; - -use crate::block::ConsensusBlock; -use crate::proposal_parts::{prepare_stream, stream_proposal, PublishProposalPart}; -use crate::state::State; -use crate::store::repositories::UndecidedBlocksRepository; - -/// Handles the `RestreamProposal` message from the consensus engine. -/// -/// This is called when the consensus engine requests to restream a proposal for a specific height and round. -/// The block is looked up by height and block hash (ignoring round), so it will be found -/// regardless of which round it was originally stored under. The stored block is not modified -/// (round and valid_round are left as when the block was first stored). -/// -/// ## Errors -/// - If no block is found for the specified height and value id -/// - If there are issues fetching the block from the repository -/// - If there are issues preparing or streaming the proposal parts -pub async fn handle( - state: &mut State, - channels: &mut Channels, - height: Height, - round: Round, - valid_round: Round, - value_id: ValueId, -) -> eyre::Result<()> { - let block_to_restream = - get_block_to_restream(state.store(), height, value_id.block_hash()).await?; - - if let Some(block) = block_to_restream { - let stream_id = state.next_stream_id(block.height, block.round); - let signing_provider = state.signing_provider(); - - restream_proposal(&channels.network, stream_id, signing_provider, &block).await - } else { - error!(%height, %round, %valid_round, "No block found to restream"); - - Err(eyre!("No block found to restream (height={height}, round={round}, valid_round={valid_round})")) - } -} - -pub async fn restream_proposal( - publish: impl PublishProposalPart, - stream_id: StreamId, - signing_provider: &impl SigningProvider, - block: &ConsensusBlock, -) -> eyre::Result<()> { - let (height, round) = (block.height, block.round); - - info!( - %height, %round, valid_round = %block.valid_round, - "Restreaming proposal, block size: {:?}, payload size: {:?}", - block.size_bytes(), block.payload_size() - ); - - let (stream_messages, _signature) = prepare_stream(stream_id, signing_provider, block) - .await - .wrap_err_with(|| { - format!( - "Failed to prepare proposal parts for restreaming (height={height}, round={round})" - ) - })?; - - stream_proposal(publish, height, round, stream_messages) - .await - .wrap_err_with(|| { - format!("Failed to restream proposal (height={height}, round={round})") - })?; - - Ok(()) -} - -async fn get_block_to_restream( - undecided_blocks: impl UndecidedBlocksRepository, - height: Height, - block_hash: BlockHash, -) -> eyre::Result> { - undecided_blocks - .get_by_hash(height, block_hash) - .await - .wrap_err_with(|| { - format!( - "Failed to fetch block for restreaming \ - (height={height}, block_hash={block_hash})" - ) - }) -} - -#[cfg(test)] -mod tests { - use crate::proposal_parts::{ - make_proposal_parts, resolve_expected_proposer, validate_proposal_parts, - MockPublishProposalPart, - }; - use crate::store::repositories::mocks::MockUndecidedBlocksRepository; - - use super::*; - - use alloy_rpc_types_engine::ExecutionPayloadV3; - use arbitrary::Arbitrary; - use arc_consensus_types::proposer::{ProposerSelector, RoundRobin}; - use arc_consensus_types::{Address, BlockHash, Height, ProposalParts, Validator, ValidatorSet}; - use arc_signer::local::{LocalSigningProvider, PrivateKey}; - use bytes::Bytes; - use malachitebft_app_channel::app::types::core::Round; - use malachitebft_core_types::Validity; - use mockall::predicate::eq; - - fn create_dummy_block(height: Height, round: Round, valid_round: Round) -> ConsensusBlock { - let mut u = arbitrary::Unstructured::new(&[0u8; 1024]); - - ConsensusBlock { - height, - round, - valid_round, - proposer: Address::arbitrary(&mut u).unwrap(), - validity: Validity::Valid, - execution_payload: ExecutionPayloadV3::arbitrary(&mut u).unwrap(), - signature: None, - } - } - - fn make_validator_set(n: usize) -> (Vec, ValidatorSet) { - let mut rng = rand::thread_rng(); - let keys: Vec = (0..n).map(|_| PrivateKey::generate(&mut rng)).collect(); - let validators: Vec = keys - .iter() - .map(|k| Validator::new(k.public_key(), 1)) - .collect(); - (keys, ValidatorSet::new(validators)) - } - - #[tokio::test] - async fn get_block_found() { - let mut mock_repo = MockUndecidedBlocksRepository::new(); - - let height = Height::new(10); - let block_hash = BlockHash::default(); - - let original_block = create_dummy_block(height, Round::new(0), Round::Nil); - let from_repo = original_block.clone(); - - mock_repo - .expect_get_by_hash() - .with(eq(height), eq(block_hash)) - .times(1) - .returning(move |_, _| Ok(Some(from_repo.clone()))); - - let result = get_block_to_restream(&mock_repo, height, block_hash) - .await - .unwrap() - .expect("block should be found"); - assert_eq!(result, original_block); - } - - #[tokio::test] - async fn get_block_not_found() { - let mut mock_repo = MockUndecidedBlocksRepository::new(); - - let height = Height::new(10); - let block_hash = BlockHash::default(); - - mock_repo - .expect_get_by_hash() - .with(eq(height), eq(block_hash)) - .times(1) - .returning(|_, _| Ok(None)); - - let result = get_block_to_restream(&mock_repo, height, block_hash).await; - - assert!(result.unwrap().is_none()); - } - - #[tokio::test] - async fn get_block_repo_error_propagation() { - let mut mock_repo = MockUndecidedBlocksRepository::new(); - let height = Height::new(10); - - mock_repo - .expect_get_by_hash() - .returning(|_, _| Err(std::io::Error::other("DB connection failed"))); - - let result = get_block_to_restream(&mock_repo, height, BlockHash::default()).await; - - assert!(result.is_err()); - assert!(result - .unwrap_err() - .to_string() - .contains("Failed to fetch block")); - } - - #[tokio::test] - async fn restream_proposal_happy_path() { - let mut rng = rand::thread_rng(); - - let mut mock = MockPublishProposalPart::new(); - let stream_id = StreamId::new(Bytes::from_static(&[42; 20])); - let signing_provider = LocalSigningProvider::new(PrivateKey::generate(&mut rng)); - let block = create_dummy_block(Height::new(10), Round::new(2), Round::Nil); - - mock.expect_publish_proposal_part().returning(|_| Ok(())); - - let result = restream_proposal(mock, stream_id, &signing_provider, &block).await; - - assert!(result.is_ok()); - } - - /// Retrieve a stored block via `get_block_to_restream` and verify that proposal - /// parts regenerated from it validate against the cached signature — i.e., the - /// read path preserves the block's original signing inputs end-to-end. - #[tokio::test] - async fn get_block_to_restream_make_proposal_parts_and_verify() { - use arbitrary::Unstructured; - - let mut u = Unstructured::new(&[0u8; 512]); - let payload = ExecutionPayloadV3::arbitrary(&mut u).unwrap(); - - let selector = RoundRobin; - let (keys, validator_set) = make_validator_set(3); - let height = Height::new(7); - let round = Round::new(0); - - let round0_proposer = selector.select_proposer(&validator_set, height, round); - let signing_key = keys - .iter() - .find(|k| Address::from_public_key(&k.public_key()) == round0_proposer.address) - .unwrap(); - let provider = LocalSigningProvider::new(signing_key.clone()); - - let mut block = ConsensusBlock { - height, - round, - valid_round: Round::Nil, - proposer: round0_proposer.address, - validity: Validity::Valid, - execution_payload: payload, - signature: None, - }; - - let (raw_first, first_sig) = make_proposal_parts(&provider, &block).await.unwrap(); - let first_parts = ProposalParts::new(raw_first).unwrap(); - let expected_first = resolve_expected_proposer(&selector, &validator_set, &first_parts); - assert!(validate_proposal_parts(&first_parts, expected_first, &provider).await); - - block.signature = Some(first_sig); - let block_hash = block.block_hash(); - let stored_block = block.clone(); - - let mut mock_repo = MockUndecidedBlocksRepository::new(); - mock_repo - .expect_get_by_hash() - .with(eq(height), eq(block_hash)) - .times(1) - .returning(move |_, _| Ok(Some(stored_block.clone()))); - - let block_to_restream = get_block_to_restream(&mock_repo, height, block_hash) - .await - .unwrap() - .expect("get_block_to_restream should return the block"); - - assert_eq!(block_to_restream.signature, Some(first_sig)); - - let (raw_restream, _) = make_proposal_parts(&provider, &block_to_restream) - .await - .unwrap(); - let restream_parts = ProposalParts::new(raw_restream).unwrap(); - - let expected_restream = - resolve_expected_proposer(&selector, &validator_set, &restream_parts); - assert!( - validate_proposal_parts(&restream_parts, expected_restream, &provider).await, - "restreamed proposal parts should verify" - ); - } -} diff --git a/crates/malachite-app/src/handlers/started_round.rs b/crates/malachite-app/src/handlers/started_round.rs deleted file mode 100644 index 76e7730..0000000 --- a/crates/malachite-app/src/handlers/started_round.rs +++ /dev/null @@ -1,661 +0,0 @@ -// Copyright 2025 Circle Internet Group, Inc. All rights reserved. -// -// SPDX-License-Identifier: Apache-2.0 -// -// Licensed under the Apache License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. -// You may obtain a copy of the License at -// -// http://www.apache.org/licenses/LICENSE-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// See the License for the specific language governing permissions and -// limitations under the License. - -use eyre::Context; -use tracing::{error, info, warn}; - -use malachitebft_app_channel::app::consensus::Role; -use malachitebft_app_channel::app::types::core::Validity; -use malachitebft_app_channel::app::types::ProposedValue; -use malachitebft_app_channel::Reply; - -use arc_consensus_types::proposer::ProposerSelector; -use arc_consensus_types::{Address, ArcContext, Height, ProposalParts, Round, ValidatorSet}; -use arc_eth_engine::engine::Engine; -use arc_signer::ArcSigningProvider; - -use crate::block::ConsensusBlock; -use crate::metrics::{AppMetrics, InvalidPayloadSource}; -use crate::payload::{validate_consensus_block, EnginePayloadValidator, PayloadValidator}; -use crate::proposal_parts::{ - assemble_block_from_parts, resolve_expected_proposer, validate_proposal_parts, -}; -use crate::state::State; -use crate::store::repositories::{InvalidPayloadsRepository, UndecidedBlocksRepository}; -use crate::store::Store; -use arc_consensus_db::invalid_payloads::InvalidPayload; - -/// Handles the `StartedRound` message from the consensus engine. -/// -/// This is called when the consensus engine starts a new round for a given height. -/// The application performs the following steps: -/// 1. If it's the first round of a new height, it resets the height timer -/// 2. Updates the current round and proposer in the state -/// 3. Retrieves any pending proposal parts for the current height and round -/// 4. Processes the pending proposal parts to reconstruct any complete proposals, -/// adding them to the undecided blocks table -/// 5. Validates all undecided blocks for the current height and round by sending them -/// to the execution client, and updating their validity status -/// 6. Returns the valid proposed values to the consensus engine -pub async fn handle( - state: &mut State, - engine: &Engine, - height: Height, - round: Round, - proposer: Address, - role: Role, - reply: Reply>>, -) { - let proposals = match on_started_round(state, engine, height, round, proposer, role).await { - Ok(proposals) => { - info!(%height, %round, "StartedRound: sending {} undecided proposals to consensus", proposals.len()); - proposals - } - Err(e) => { - error!(%height, %round, "StartedRound: failed to process pending proposal parts: {e}"); - - // In case of error, we send an empty list of proposals to consensus - Vec::new() - } - }; - - if let Err(e) = reply.send(proposals) { - error!("🔴 StartedRound: Failed to send reply: {e:?}"); - } -} - -async fn on_started_round( - state: &mut State, - engine: &Engine, - height: Height, - round: Round, - proposer: Address, - role: Role, -) -> eyre::Result>> { - // If we are starting a new height, reset the height timer - if round.as_i64() == 0 { - let network_id = state.started_height(height, round, proposer); - info!(%height, %network_id, "🦋 Started height"); - } - - info!(%height, %round, ?role, %proposer, "🔮 Started round"); - - assert_eq!(state.current_height, height, "Consensus height mismatch"); - assert!(round != Round::Nil, "Round cannot be Nil"); - assert!(round >= state.current_round, "Round cannot go backwards"); - - state.current_round = round; - state.current_proposer = Some(proposer); - - fetch_and_process_pending_proposals( - height, - round, - state.validator_set(), - &state.ctx.proposer_selector, - state.store(), - engine, - state.signing_provider(), - state.metrics(), - ) - .await -} - -#[allow(clippy::too_many_arguments)] -async fn fetch_and_process_pending_proposals( - height: Height, - round: Round, - validator_set: &ValidatorSet, - proposer_selector: &dyn ProposerSelector, - store: &Store, - engine: &Engine, - signing_provider: &ArcSigningProvider, - metrics: &AppMetrics, -) -> eyre::Result>> { - let pending_parts = store - .get_pending_proposal_parts(height, round) - .await - .wrap_err("failed to fetch pending proposal parts")?; - - info!(%height, %round, "StartedRound: Found {} pending proposal parts", pending_parts.len()); - - // Convert the pending proposal parts for the current round, - // into blocks and add them to undecided blocks table. - process_pending_proposal_parts( - store, - pending_parts, - height, - round, - validator_set, - proposer_selector, - signing_provider, - metrics, - ) - .await - .wrap_err("Failed to validate pending proposal parts")?; - - let blocks = validate_undecided_blocks( - height, - round, - store, - &EnginePayloadValidator::new(engine, metrics), - store, - metrics, - ) - .await - .wrap_err("failed to validate undecided blocks")?; - - Ok(blocks.iter().map(ProposedValue::from).collect()) -} - -/// Process the pending proposal parts for the current height, assembling them -/// into blocks and moving the blocks to the undecided table. -/// -/// ## Important -/// This function assumes that the pending parts are for the current height and round. -#[allow(clippy::too_many_arguments)] -async fn process_pending_proposal_parts( - store: &Store, - pending_parts: Vec, - current_height: Height, - current_round: Round, - validator_set: &ValidatorSet, - proposer_selector: &dyn ProposerSelector, - signing_provider: &ArcSigningProvider, - metrics: &AppMetrics, -) -> eyre::Result<()> { - for parts in pending_parts { - let (height, round, proposer) = (parts.height(), parts.round(), parts.proposer()); - - debug_assert_eq!(height, current_height, "Pending parts height mismatch"); - debug_assert_eq!(round, current_round, "Pending parts round mismatch"); - - let expected_proposer = resolve_expected_proposer(proposer_selector, validator_set, &parts); - - if !validate_proposal_parts(&parts, expected_proposer, signing_provider).await { - continue; - } - - // NOTE: The block is initially assigned a default validity status - // (i.e., `Validity::Valid`), even though it has not yet been validated - // by the execution client. - // By inserting this block into the undecided blocks table, we are - // temporarily violating the assumption that all blocks in that table - // have been validated at least once by the execution client. - // This temporary inconsistency is acceptable here because all blocks - // in the undecided table are immediately validated by the subsequent - // `validate_undecided_blocks` in `AppMsg::StartedRound` handler. - match assemble_block_from_parts(&parts) { - Ok(block) => { - info!(%height, %round, %proposer, "Added pending block to undecided"); - - // Atomically remove from pending and store as undecided - // This ensures that if the process fails, the parts are not lost - remove_pending_parts_and_store_undecided_block(store, parts, block).await?; - } - Err(e) => { - warn!(%height, %round, %proposer, "Failed to assemble block from pending parts: {e}"); - metrics.inc_invalid_payloads_count(InvalidPayloadSource::AssemblyFailure); - let invalid_payload = InvalidPayload::new_from_parts(&parts, &e.to_string()); - store.append_invalid_payload(invalid_payload).await.wrap_err_with(|| { - format!( - "Failed to store invalid payload after assembling block from pending parts (height={height}, round={round}, proposer={proposer})", - ) - })?; - } - } - } - - Ok(()) -} - -/// Sends all undecided blocks for the given height and round to the execution -/// client, ensuring the client has the corresponding payloads locally. -/// This is important in two scenarios: -/// 1. when validating newly created undecided blocks reconstructed from proposal -/// parts. -/// 2. when re-validating undecided blocks after a crash or restart. -/// -/// The second case addresses the EL "amnesia" issue, where the execution client may -/// have forgotten previously validated payloads that were only stored in memory and -/// lost after a restart. -/// -/// After each block is validated, the engine's verdict is persisted back to the -/// undecided blocks table. -async fn validate_undecided_blocks( - height: Height, - round: Round, - undecided_blocks: &impl UndecidedBlocksRepository, - payload_validator: &impl PayloadValidator, - invalid_payloads: &impl InvalidPayloadsRepository, - metrics: &AppMetrics, -) -> eyre::Result> { - let blocks = undecided_blocks - .get_by_round(height, round) - .await - .wrap_err_with(|| { - format!( - "Failed to fetch undecided blocks for height {height} and round {round} \ - from the state before sending them to execution client for validation" - ) - })?; - - // Holds all blocks that were validated (either valid or invalid) - let mut validated_blocks = Vec::with_capacity(blocks.len()); - - for mut block in blocks { - let block_hash = block.block_hash(); - - info!(%height, %round, %block_hash, "Validating undecided block"); - - let validity = match validate_consensus_block( - payload_validator, - &block, - invalid_payloads, - metrics, - ) - .await - { - Ok(validity) => validity, - Err(e) => { - error!(%height, %round, %block_hash, "Failed to validate undecided block, marking Invalid: {e}"); - Validity::Invalid - } - }; - - block.validity = validity; - - // Persist the engine's verdict before returning the block to consensus. - undecided_blocks - .store_undecided_block(block.clone()) - .await - .wrap_err_with(|| { - format!( - "Failed to persist validated undecided block {block_hash} \ - at height={height} round={round}" - ) - })?; - - validated_blocks.push(block); - - if !validity.is_valid() { - // It is possible that we had multiple blocks before restart, - // and one or more of them are invalid. We continue to the next block. - warn!(%height, %round, %block_hash, "Undecided block is invalid"); - } - } - - Ok(validated_blocks) -} - -/// Atomically removes pending proposal parts and stores the undecided block. -/// This ensures that if the process fails, the parts are not lost. -async fn remove_pending_parts_and_store_undecided_block( - store: &Store, - parts: ProposalParts, - block: ConsensusBlock, -) -> eyre::Result<()> { - let height = block.height; - let round = block.round; - let block_hash = block.block_hash(); - - store - .remove_pending_parts_and_store_undecided_block(parts, block) - .await - .wrap_err_with(|| { - format!( - "Failed to atomically remove pending parts and store undecided block at height={}, round={}, block_hash={}", - height, round, block_hash - ) - }) -} - -#[cfg(test)] -mod tests { - use super::*; - - use crate::payload::{MockPayloadValidator, PayloadValidationResult}; - use crate::store::repositories::mocks::{ - MockInvalidPayloadsRepository, MockUndecidedBlocksRepository, - }; - - use std::sync::{Arc, Mutex}; - - use alloy_rpc_types_engine::ExecutionPayloadV3; - use arbitrary::{Arbitrary, Unstructured}; - use arc_consensus_db::{DbMetrics, DbUpgrade}; - use arc_consensus_types::proposer::RoundRobin; - use arc_consensus_types::Validator; - use arc_signer::local::{LocalSigningProvider, PrivateKey}; - use bytesize::ByteSize; - use malachitebft_core_types::Validity; - use tempfile::tempdir; - - use crate::handlers::test_utils::signed_parts_without_data; - - fn create_dummy_block(height: Height, round: Round, seed: u8) -> ConsensusBlock { - let bytes = [seed; 1024]; - let mut u = Unstructured::new(&bytes); - - ConsensusBlock { - height, - round, - valid_round: Round::Nil, - proposer: Address::arbitrary(&mut u).unwrap(), - validity: Validity::Valid, - execution_payload: ExecutionPayloadV3::arbitrary(&mut u).unwrap(), - signature: None, - } - } - - async fn test_store() -> (Store, tempfile::TempDir) { - let dir = tempdir().unwrap(); - let store = Store::open( - dir.path().join("db"), - DbMetrics::default(), - DbUpgrade::Skip, - ByteSize::mib(64), - ) - .await - .unwrap(); - (store, dir) - } - - #[tokio::test] - async fn validate_undecided_blocks_all_valid() { - let height = Height::new(1); - let round = Round::new(0); - - let block1 = create_dummy_block(height, round, 0x11); - let block2 = create_dummy_block(height, round, 0x22); - let blocks = vec![block1, block2]; - - let mut undecided = MockUndecidedBlocksRepository::new(); - undecided - .expect_get_by_round() - .returning(move |_, _| Ok(blocks.clone())); - undecided - .expect_store_undecided_block() - .times(2) - .withf(|b| b.validity == Validity::Valid) - .returning(|_| Ok(())); - - let mut validator = MockPayloadValidator::new(); - validator - .expect_validate_payload() - .times(2) - .returning(|_| Ok(PayloadValidationResult::Valid)); - - let mut invalid = MockInvalidPayloadsRepository::new(); - invalid.expect_append().times(0); - - let metrics = AppMetrics::default(); - let result = - validate_undecided_blocks(height, round, &undecided, &validator, &invalid, &metrics) - .await - .expect("should succeed"); - - assert_eq!(result.len(), 2); - assert!(result.iter().all(|b| b.validity == Validity::Valid)); - assert_eq!(metrics.get_invalid_payloads_count(), 0); - } - - #[tokio::test] - async fn validate_undecided_blocks_mixed_validity() { - let height = Height::new(1); - let round = Round::new(0); - - let block1 = create_dummy_block(height, round, 0x11); - let block2 = create_dummy_block(height, round, 0x22); - let blocks = vec![block1, block2]; - - let mut undecided = MockUndecidedBlocksRepository::new(); - undecided - .expect_get_by_round() - .returning(move |_, _| Ok(blocks.clone())); - - // Record the validity of every block persisted, in call order. - let persisted = Arc::new(Mutex::new(Vec::::new())); - let persisted_clone = Arc::clone(&persisted); - undecided - .expect_store_undecided_block() - .times(2) - .returning(move |b| { - persisted_clone.lock().unwrap().push(b.validity); - Ok(()) - }); - - let mut call_count = 0usize; - let mut validator = MockPayloadValidator::new(); - validator - .expect_validate_payload() - .times(2) - .returning(move |_| { - call_count += 1; - if call_count == 1 { - Ok(PayloadValidationResult::Valid) - } else { - Ok(PayloadValidationResult::Invalid { - reason: "bad block".into(), - }) - } - }); - - let mut invalid = MockInvalidPayloadsRepository::new(); - invalid.expect_append().times(1).returning(|_| Ok(())); - - let metrics = AppMetrics::default(); - let result = - validate_undecided_blocks(height, round, &undecided, &validator, &invalid, &metrics) - .await - .expect("should succeed"); - - assert_eq!(result.len(), 2); - assert_eq!(result[0].validity, Validity::Valid); - assert_eq!(result[1].validity, Validity::Invalid); - assert_eq!( - *persisted.lock().unwrap(), - vec![Validity::Valid, Validity::Invalid], - "persisted validity should match engine verdict, not the placeholder" - ); - assert_eq!(metrics.get_invalid_payloads_count(), 1); - } - - #[tokio::test] - async fn validate_undecided_blocks_empty() { - let height = Height::new(1); - let round = Round::new(0); - - let mut undecided = MockUndecidedBlocksRepository::new(); - undecided.expect_get_by_round().returning(|_, _| Ok(vec![])); - - let validator = MockPayloadValidator::new(); - let invalid = MockInvalidPayloadsRepository::new(); - - let metrics = AppMetrics::default(); - let result = - validate_undecided_blocks(height, round, &undecided, &validator, &invalid, &metrics) - .await - .expect("should succeed"); - - assert!(result.is_empty()); - assert_eq!(metrics.get_invalid_payloads_count(), 0); - } - - #[tokio::test] - async fn validate_undecided_blocks_repository_error() { - let height = Height::new(1); - let round = Round::new(0); - - let mut undecided = MockUndecidedBlocksRepository::new(); - undecided - .expect_get_by_round() - .returning(|_, _| Err(std::io::Error::other("DB connection failed"))); - - let validator = MockPayloadValidator::new(); - let invalid = MockInvalidPayloadsRepository::new(); - - let metrics = AppMetrics::default(); - let err = - validate_undecided_blocks(height, round, &undecided, &validator, &invalid, &metrics) - .await - .expect_err("should propagate repository error"); - - assert!( - err.to_string().contains("Failed to fetch undecided blocks"), - "error should describe the failure, got: {err}", - ); - assert_eq!(metrics.get_invalid_payloads_count(), 0); - } - - #[tokio::test] - async fn validate_undecided_blocks_validation_error_marks_invalid() { - // When the engine call fails (transport error, SYNCING/ACCEPTED, etc.) - // we treat the block as `Invalid` for the current round so the placeholder - // `Valid` written by `process_pending_proposal_parts` does not leak. - // Malachite's `FullProposalKeeper::handle_validity_change` rejects any - // subsequent `Valid -> Invalid` flip on the same WAL entry. - let height = Height::new(1); - let round = Round::new(0); - - let block1 = create_dummy_block(height, round, 0x11); - let block2 = create_dummy_block(height, round, 0x22); - let blocks = vec![block1, block2]; - - let mut undecided = MockUndecidedBlocksRepository::new(); - undecided - .expect_get_by_round() - .returning(move |_, _| Ok(blocks.clone())); - - // Both blocks must be persisted: the errored block with `Invalid`, - // the successful one with `Valid`. Order matches the input order. - let persisted = Arc::new(Mutex::new(Vec::::new())); - let persisted_clone = Arc::clone(&persisted); - undecided - .expect_store_undecided_block() - .times(2) - .returning(move |b| { - persisted_clone.lock().unwrap().push(b.validity); - Ok(()) - }); - - let mut call_count = 0usize; - let mut validator = MockPayloadValidator::new(); - validator - .expect_validate_payload() - .times(2) - .returning(move |_| { - call_count += 1; - if call_count == 1 { - Err(eyre::eyre!("engine down")) - } else { - Ok(PayloadValidationResult::Valid) - } - }); - - // Engine `Err` is not a verdict, so no forensic record is written. - let mut invalid = MockInvalidPayloadsRepository::new(); - invalid.expect_append().times(0); - - let metrics = AppMetrics::default(); - let result = - validate_undecided_blocks(height, round, &undecided, &validator, &invalid, &metrics) - .await - .expect("should succeed despite one block erroring"); - - assert_eq!(result.len(), 2, "both blocks should be returned"); - assert_eq!(result[0].validity, Validity::Invalid); - assert_eq!(result[1].validity, Validity::Valid); - assert_eq!( - *persisted.lock().unwrap(), - vec![Validity::Invalid, Validity::Valid], - "errored block must be persisted as Invalid, not left with placeholder Valid", - ); - // Engine failure is not counted as an engine rejection; the metric - // tracks `EngineReject` and `AssemblyFailure`, not transport errors. - assert_eq!(metrics.get_invalid_payloads_count(), 0); - } - - #[tokio::test] - async fn process_pending_proposal_parts_increments_on_assembly_failure() { - let (store, _dir) = test_store().await; - - let signing_key = PrivateKey::generate(rand::rngs::OsRng); - let validator = Validator::new(signing_key.public_key(), 1); - let validator_set = ValidatorSet::new(vec![validator]); - - let height = Height::new(1); - let round = Round::new(0); - let parts = signed_parts_without_data(height, round, &signing_key).await; - - let selector = RoundRobin; - let provider = ArcSigningProvider::Local(LocalSigningProvider::new(signing_key)); - let metrics = AppMetrics::default(); - - process_pending_proposal_parts( - &store, - vec![parts], - height, - round, - &validator_set, - &selector, - &provider, - &metrics, - ) - .await - .expect("should handle assembly failure gracefully"); - - assert_eq!(metrics.get_invalid_payloads_count(), 1); - } - - #[tokio::test] - async fn validate_undecided_blocks_propagates_persist_error() { - let height = Height::new(1); - let round = Round::new(0); - - let block = create_dummy_block(height, round, 0x33); - let blocks = vec![block]; - - let mut undecided = MockUndecidedBlocksRepository::new(); - undecided - .expect_get_by_round() - .returning(move |_, _| Ok(blocks.clone())); - undecided - .expect_store_undecided_block() - .times(1) - .returning(|_| Err(std::io::Error::other("disk full"))); - - let mut validator = MockPayloadValidator::new(); - validator - .expect_validate_payload() - .times(1) - .returning(|_| Ok(PayloadValidationResult::Valid)); - - let invalid = MockInvalidPayloadsRepository::new(); - - let metrics = AppMetrics::default(); - let err = - validate_undecided_blocks(height, round, &undecided, &validator, &invalid, &metrics) - .await - .expect_err("persist error should propagate"); - - assert!( - err.to_string() - .contains("Failed to persist validated undecided block"), - "error should describe the persist failure, got: {err}", - ); - assert_eq!(metrics.get_invalid_payloads_count(), 0); - } -} diff --git a/crates/malachite-app/src/handlers/test_utils.rs b/crates/malachite-app/src/handlers/test_utils.rs deleted file mode 100644 index 3f296e6..0000000 --- a/crates/malachite-app/src/handlers/test_utils.rs +++ /dev/null @@ -1,52 +0,0 @@ -// Copyright 2025 Circle Internet Group, Inc. All rights reserved. -// -// SPDX-License-Identifier: Apache-2.0 -// -// Licensed under the Apache License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. -// You may obtain a copy of the License at -// -// http://www.apache.org/licenses/LICENSE-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// See the License for the specific language governing permissions and -// limitations under the License. - -//! Shared helpers for handler unit tests. - -use sha3::Digest; - -use arc_consensus_types::{ - Address, Height, ProposalFin, ProposalInit, ProposalPart, ProposalParts, Round, -}; -use arc_signer::local::{LocalSigningProvider, PrivateKey}; -use arc_signer::SigningProvider; - -/// Builds signed `ProposalParts` with only `Init` and `Fin` (no data chunks). -/// Useful for exercising assembly-failure paths: the empty data makes -/// `assemble_block_from_parts` fail on SSZ decode of zero bytes, but the -/// signature over `height + round` still verifies against the signer. -pub(super) async fn signed_parts_without_data( - height: Height, - round: Round, - signing_key: &PrivateKey, -) -> ProposalParts { - let proposer = Address::from_public_key(&signing_key.public_key()); - let init = ProposalInit::new(height, round, Round::Nil, proposer); - - let mut hasher = sha3::Keccak256::new(); - hasher.update(height.as_u64().to_be_bytes()); - hasher.update(round.as_i64().to_be_bytes()); - let hash = hasher.finalize().to_vec(); - - let provider = LocalSigningProvider::new(signing_key.clone()); - let signature = provider.sign_bytes(&hash).await.unwrap(); - - ProposalParts::new(vec![ - ProposalPart::Init(init), - ProposalPart::Fin(ProposalFin::new(signature)), - ]) - .unwrap() -} diff --git a/crates/malachite-app/src/hardcoded_config.rs b/crates/malachite-app/src/hardcoded_config.rs deleted file mode 100644 index 3ed389e..0000000 --- a/crates/malachite-app/src/hardcoded_config.rs +++ /dev/null @@ -1,609 +0,0 @@ -// Copyright 2025 Circle Internet Group, Inc. All rights reserved. -// -// SPDX-License-Identifier: Apache-2.0 -// -// Licensed under the Apache License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. -// You may obtain a copy of the License at -// -// http://www.apache.org/licenses/LICENSE-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// See the License for the specific language governing permissions and -// limitations under the License. - -//! Hardcoded configuration values that must be consistent across all nodes. - -use std::time::Duration; - -use arc_consensus_types::{ - ChainId, ConsensusConfig, RemoteSigningConfig, RetryConfig, ValueSyncConfig, -}; -use arc_node_consensus_cli::config::ScoringStrategy; -use malachitebft_app_channel::app::config::{ - BootstrapProtocol, DiscoveryConfig, GossipSubConfig, P2pConfig, ProtocolNames, PubSubProtocol, - Selector, ValuePayload, -}; -use malachitebft_app_channel::app::net::Multiaddr; - -/// Hardcoded consensus parameters that must be consistent across all nodes. -pub mod consensus { - use super::*; - - /// Message types required by consensus to deliver the value being proposed. - /// - /// Default: `ValuePayload::PartsOnly` - pub const VALUE_PAYLOAD: ValuePayload = ValuePayload::ProposalAndParts; - - /// Capacity of the consensus message queue, in heights. - /// - /// Default: `10` - pub const QUEUE_CAPACITY: usize = 16; - - /// Maximum number of buffered inputs per height in the gossip input queue. - /// - /// Default: `500` - pub const QUEUE_PER_HEIGHT_CAPACITY: usize = 500; - - /// Duration to wait before replaying the WAL on recovery. - /// - /// Default: `5s` - pub const WAL_REPLAY_DELAY: Duration = Duration::from_secs(5); - - /// Chain-specific libp2p protocol names. - pub mod p2p { - use super::*; - - pub(crate) fn protocol_names_malachite_defaults() -> ProtocolNames { - ProtocolNames { - consensus: "/malachitebft-core-consensus/v1beta1".to_string(), - discovery_kad: "/malachitebft-discovery/kad/v1beta1".to_string(), - discovery_regres: "/malachitebft-discovery/reqres/v1beta1".to_string(), - sync: "/malachitebft-sync/v1beta1".to_string(), - validator_proof: "/malachitebft-validator-proof/v1".to_string(), - } - } - - fn protocol_names_arc_v1() -> ProtocolNames { - ProtocolNames { - consensus: "/arc/consensus/v1".to_string(), - discovery_kad: "/arc/discovery/kad/v1".to_string(), - discovery_regres: "/arc/discovery/req-res/v1".to_string(), - sync: "/arc/sync/v1".to_string(), - validator_proof: "/arc/validator-proof/v1".to_string(), - } - } - - /// Chain-specific libp2p protocol names. - /// - /// Mainnet uses Arc-branded names; Testnet, Devnet, and Localdev - /// keep the Malachite defaults to avoid breaking the wire protocol - /// on already-running networks (including CI upgrade scenarios). - pub fn protocol_names(chain_id: ChainId) -> ProtocolNames { - match chain_id { - ChainId::Mainnet => protocol_names_arc_v1(), - ChainId::Localdev => protocol_names_malachite_defaults(), - ChainId::Testnet => protocol_names_malachite_defaults(), - ChainId::Devnet => protocol_names_malachite_defaults(), - } - } - } -} - -/// Hardcoded discovery parameters. -pub mod discovery { - use super::*; - - /// Bootstrap protocol for discovery. - /// "full" bootstrap is faster for small (< 1000) networks. - /// Kademlia bootstrap is slower but can scale to large (> 1000) nodes network. - /// - /// Default: `BootstrapProtocol::Kademlia` - pub const BOOTSTRAP_PROTOCOL: BootstrapProtocol = BootstrapProtocol::Full; - - /// Peer selector for discovery. - /// For Arc, we use random selector with full bootstrap. - /// - /// Default: `Selector::Kademlia` - pub const SELECTOR: Selector = Selector::Random; - - /// Default: `Duration::from_secs(60)` - pub const EPHEMERAL_CONNECTION_TIMEOUT: Duration = Duration::from_secs(5); -} - -/// Hardcoded remote signing parameters. -pub mod remote_signing { - use super::*; - - /// Timeout for remote signing requests. - pub const TIMEOUT: Duration = Duration::from_secs(30); -} - -/// Hardcoded sync parameters. -pub mod value_sync { - use bytesize::ByteSize; - - use super::*; - - /// Interval at which to update other peers of our status - /// Default: `Duration::from_secs(10)` - /// - /// Note: Setting this to 0 means "update status on every block". - pub const STATUS_UPDATE_INTERVAL: Duration = Duration::from_secs(0); - - /// Timeout duration for sync requests - /// Default: `Duration::from_secs(10)` - pub const REQUEST_TIMEOUT: Duration = Duration::from_secs(1); - - /// Maximum size of a request - /// Default: `1 MiB` - pub const MAX_REQUEST_SIZE: ByteSize = ByteSize::mib(1); - - /// Maximum size of a response - /// Default: `10 MiB` - pub const MAX_RESPONSE_SIZE: ByteSize = ByteSize::mib(10); - - /// Maximum number of parallel requests to send - /// Default: `5` - pub const PARALLEL_REQUESTS: usize = 5; - - /// Threshold for considering a peer inactive - /// Default: `Duration::from_secs(60)` (1 minute) - pub const INACTIVE_THRESHOLD: Duration = Duration::from_secs(60); - - /// Maximum number of decided values to request in a single batch - /// Default: `5` - pub const BATCH_SIZE: usize = 10; -} - -/// Gossipsub network load profile. -#[derive(Clone, Debug, Default, PartialEq)] -pub enum GossipLoad { - /// Fewer mesh peers, lower bandwidth (mesh_n=3) - Low, - /// Balanced for typical deployments (mesh_n=6) - #[default] - Average, - /// More mesh peers, higher bandwidth (mesh_n=10) - High, -} - -impl GossipLoad { - /// Parse from an optional CLI string. Unrecognized or missing values default to `Average`. - pub fn from_str_opt(s: Option<&str>) -> Self { - match s { - Some("low") => Self::Low, - Some("high") => Self::High, - _ => Self::Average, - } - } - - /// Canonical mesh parameters for this load profile. - pub fn mesh_params(&self) -> GossipMeshParams { - match self { - Self::Low => GossipMeshParams { - mesh_n: 3, - mesh_n_high: 4, - mesh_n_low: 1, - mesh_outbound_min: 1, - }, - Self::Average => GossipMeshParams { - mesh_n: 6, - mesh_n_high: 12, - mesh_n_low: 4, - mesh_outbound_min: 2, - }, - Self::High => GossipMeshParams { - mesh_n: 10, - mesh_n_high: 15, - mesh_n_low: 5, - mesh_outbound_min: 3, - }, - } - } -} - -/// Named gossipsub mesh sizing parameters. -#[derive(Clone, Debug)] -pub struct GossipMeshParams { - pub mesh_n: usize, - pub mesh_n_high: usize, - pub mesh_n_low: usize, - pub mesh_outbound_min: usize, -} - -/// CLI provided gossipsub overrides. -#[derive(Clone, Debug, Default)] -pub struct GossipSubOverrides { - pub explicit_peering: bool, - pub mesh_prioritization: bool, - pub load: GossipLoad, -} - -/// Build a consensus configuration with hardcoded parameters. -#[allow(clippy::too_many_arguments)] -pub fn build_consensus_config( - listen_addr: Multiaddr, - persistent_peers: Vec, - persistent_peers_only: bool, - discovery_enabled: bool, - num_outbound_peers: usize, - num_inbound_peers: usize, - consensus_enabled: bool, - gossipsub_overrides: GossipSubOverrides, -) -> ConsensusConfig { - ConsensusConfig { - enabled: consensus_enabled, - value_payload: consensus::VALUE_PAYLOAD, - queue_capacity: consensus::QUEUE_CAPACITY, - queue_per_height_capacity: consensus::QUEUE_PER_HEIGHT_CAPACITY, - wal_replay_delay: consensus::WAL_REPLAY_DELAY, - p2p: P2pConfig { - listen_addr, - persistent_peers, - persistent_peers_only, - protocol: PubSubProtocol::GossipSub(generate_p2p_gossip_config(gossipsub_overrides)), - discovery: DiscoveryConfig { - enabled: discovery_enabled, - bootstrap_protocol: discovery::BOOTSTRAP_PROTOCOL, - selector: discovery::SELECTOR, - num_outbound_peers, - num_inbound_peers, - ephemeral_connection_timeout: discovery::EPHEMERAL_CONNECTION_TIMEOUT, - ..Default::default() - }, - ..Default::default() - }, - } -} - -fn generate_p2p_gossip_config(overrides: GossipSubOverrides) -> GossipSubConfig { - let p = overrides.load.mesh_params(); - - GossipSubConfig::new( - p.mesh_n, - p.mesh_n_high, - p.mesh_n_low, - p.mesh_outbound_min, - overrides.mesh_prioritization, - overrides.explicit_peering, - true, // flood_publish - ) -} - -/// Build a value sync configuration with hardcoded parameters. -pub fn build_value_sync_config(enabled: bool) -> ValueSyncConfig { - ValueSyncConfig { - enabled, - status_update_interval: value_sync::STATUS_UPDATE_INTERVAL, - request_timeout: value_sync::REQUEST_TIMEOUT, - max_request_size: value_sync::MAX_REQUEST_SIZE, - max_response_size: value_sync::MAX_RESPONSE_SIZE, - parallel_requests: value_sync::PARALLEL_REQUESTS, - scoring_strategy: ScoringStrategy::Ema, - inactive_threshold: value_sync::INACTIVE_THRESHOLD, - batch_size: value_sync::BATCH_SIZE, - } -} - -/// Build a remote signing configuration with hardcoded parameters. -pub fn build_remote_signing_config( - endpoint: String, - tls_cert_path: Option, -) -> RemoteSigningConfig { - RemoteSigningConfig { - endpoint, - timeout: remote_signing::TIMEOUT, - retry: RetryConfig::default(), - enable_tls: tls_cert_path.is_some(), // auto-enable if cert path provided - tls_cert_path, - } -} - -#[cfg(test)] -mod tests { - use super::*; - - #[test] - fn generate_gossipsub_config_uses_defaults() { - let config = GossipSubConfig::default(); - - assert_eq!(config.mesh_n(), 6); - assert_eq!(config.mesh_n_low(), 4); - assert_eq!(config.mesh_n_high(), 12); - assert_eq!(config.mesh_outbound_min(), 2); - } - - fn default_overrides() -> GossipSubOverrides { - GossipSubOverrides::default() - } - - #[test] - fn build_consensus_config_sets_hardcoded_values() { - let listen_addr: Multiaddr = "/ip4/127.0.0.1/tcp/27000".parse().unwrap(); - let peer1: Multiaddr = "/ip4/127.0.0.1/tcp/27001".parse().unwrap(); - let peer2: Multiaddr = "/ip4/127.0.0.1/tcp/27002".parse().unwrap(); - let persistent_peers = vec![peer1.clone(), peer2.clone()]; - - let config = build_consensus_config( - listen_addr.clone(), - persistent_peers.clone(), - false, // persistent_peers_only - false, - 20, - 20, - true, // consensus enabled - default_overrides(), - ); - - assert!(config.enabled); - assert_eq!(config.value_payload, consensus::VALUE_PAYLOAD); - assert_eq!(config.queue_capacity, consensus::QUEUE_CAPACITY); - assert_eq!( - config.queue_per_height_capacity, - consensus::QUEUE_PER_HEIGHT_CAPACITY - ); - assert_eq!(config.wal_replay_delay, consensus::WAL_REPLAY_DELAY); - assert_eq!(config.p2p.listen_addr, listen_addr); - assert_eq!(config.p2p.persistent_peers, persistent_peers); - } - - #[test] - fn build_consensus_config_with_discovery_enabled() { - let listen_addr: Multiaddr = "/ip4/0.0.0.0/tcp/27000".parse().unwrap(); - - let config = build_consensus_config( - listen_addr, - vec![], - false, // persistent_peers_only - true, // discovery enabled - 30, - 40, - true, // consensus enabled - default_overrides(), - ); - - assert!(config.p2p.discovery.enabled); - assert_eq!(config.p2p.discovery.num_outbound_peers, 30); - assert_eq!(config.p2p.discovery.num_inbound_peers, 40); - assert_eq!( - config.p2p.discovery.bootstrap_protocol, - discovery::BOOTSTRAP_PROTOCOL - ); - assert_eq!(config.p2p.discovery.selector, discovery::SELECTOR); - assert_eq!( - config.p2p.discovery.ephemeral_connection_timeout, - discovery::EPHEMERAL_CONNECTION_TIMEOUT - ); - } - - #[test] - fn build_consensus_config_with_discovery_disabled() { - let listen_addr: Multiaddr = "/ip4/0.0.0.0/tcp/27000".parse().unwrap(); - - let config = build_consensus_config( - listen_addr, - vec![], - false, // persistent_peers_only - false, // discovery disabled - 20, - 20, - true, // consensus enabled - default_overrides(), - ); - - assert!(!config.p2p.discovery.enabled); - } - - #[test] - fn build_consensus_config_supports_tcp_multiaddr() { - let listen_addr: Multiaddr = "/ip4/172.19.0.5/tcp/27000".parse().unwrap(); - - let config = build_consensus_config( - listen_addr.clone(), - vec![], - false, - false, - 20, - 20, - true, - default_overrides(), - ); - - assert_eq!(config.p2p.listen_addr, listen_addr); - } - - #[test] - fn build_consensus_config_supports_quic_multiaddr() { - let listen_addr: Multiaddr = "/ip4/127.0.0.1/udp/27000/quic-v1".parse().unwrap(); - - let config = build_consensus_config( - listen_addr.clone(), - vec![], - false, - false, - 20, - 20, - true, - default_overrides(), - ); - - assert_eq!(config.p2p.listen_addr, listen_addr); - } - - #[test] - fn build_consensus_config_uses_gossipsub_protocol() { - let listen_addr: Multiaddr = "/ip4/0.0.0.0/tcp/27000".parse().unwrap(); - - let config = build_consensus_config( - listen_addr, - vec![], - false, - false, - 20, - 20, - true, - default_overrides(), - ); - - // Verify it's using GossipSub protocol - matches!(config.p2p.protocol, PubSubProtocol::GossipSub(_)); - } - - #[test] - fn build_consensus_config_disabled() { - let listen_addr: Multiaddr = "/ip4/0.0.0.0/tcp/27000".parse().unwrap(); - - let config = build_consensus_config( - listen_addr, - vec![], - false, - false, - 20, - 20, - false, - default_overrides(), - ); - - assert!(!config.enabled); - } - - #[test] - fn build_consensus_config_with_gossipsub_overrides() { - let listen_addr: Multiaddr = "/ip4/0.0.0.0/tcp/27000".parse().unwrap(); - - let overrides = GossipSubOverrides { - explicit_peering: true, - mesh_prioritization: true, - load: GossipLoad::High, - }; - - let config = - build_consensus_config(listen_addr, vec![], false, false, 20, 20, true, overrides); - - if let PubSubProtocol::GossipSub(gs) = config.p2p.protocol { - assert!(gs.enable_explicit_peering()); - assert!(gs.enable_peer_scoring()); - assert_eq!(gs.mesh_n(), 10); - assert_eq!(gs.mesh_n_high(), 15); - assert_eq!(gs.mesh_n_low(), 5); - assert_eq!(gs.mesh_outbound_min(), 3); - } else { - panic!("Expected GossipSub protocol"); - } - } - - #[test] - fn build_value_sync_config_enabled() { - let config = build_value_sync_config(true); - - assert!(config.enabled); - } - - #[test] - fn build_value_sync_config_disabled() { - let config = build_value_sync_config(false); - - assert!(!config.enabled); - } - - #[test] - fn build_remote_signing_config_without_tls() { - let endpoint = "http://signer:10340".to_string(); - let config = build_remote_signing_config(endpoint.clone(), None); - - assert_eq!(config.endpoint, endpoint); - assert_eq!(config.timeout, remote_signing::TIMEOUT); - assert!(!config.enable_tls); - assert_eq!(config.tls_cert_path, None); - } - - #[test] - fn build_remote_signing_config_with_tls_cert() { - let endpoint = "https://signer:10340".to_string(); - let cert_path = "/path/to/cert.pem".to_string(); - let config = build_remote_signing_config(endpoint.clone(), Some(cert_path.clone())); - - assert_eq!(config.endpoint, endpoint); - assert_eq!(config.timeout, remote_signing::TIMEOUT); - assert!(config.enable_tls); // Auto-enabled when cert path provided - assert_eq!(config.tls_cert_path, Some(cert_path)); - } - - #[test] - fn build_remote_signing_config_uses_hardcoded_timeout() { - let config = build_remote_signing_config("http://signer:10340".to_string(), None); - - assert_eq!(config.timeout, Duration::from_secs(30)); - assert_eq!(config.timeout, remote_signing::TIMEOUT); - } - - #[test] - fn build_remote_signing_config_uses_default_retry() { - let config = build_remote_signing_config("http://signer:10340".to_string(), None); - - assert_eq!(config.retry, RetryConfig::default()); - } - - #[test] - fn constants_have_expected_values() { - // Verify consensus constants - assert_eq!(consensus::VALUE_PAYLOAD, ValuePayload::ProposalAndParts); - assert_eq!(consensus::QUEUE_CAPACITY, 16); - assert_eq!(consensus::QUEUE_PER_HEIGHT_CAPACITY, 500); - assert_eq!(consensus::WAL_REPLAY_DELAY, Duration::from_secs(5)); - - // Verify discovery constants - assert_eq!(discovery::BOOTSTRAP_PROTOCOL, BootstrapProtocol::Full); - assert_eq!(discovery::SELECTOR, Selector::Random); - assert_eq!( - discovery::EPHEMERAL_CONNECTION_TIMEOUT, - Duration::from_secs(5) - ); - - // Verify remote signing constants - assert_eq!(remote_signing::TIMEOUT, Duration::from_secs(30)); - } - - fn expected_arc_v1_names() -> ProtocolNames { - ProtocolNames { - consensus: "/arc/consensus/v1".to_string(), - discovery_kad: "/arc/discovery/kad/v1".to_string(), - discovery_regres: "/arc/discovery/req-res/v1".to_string(), - sync: "/arc/sync/v1".to_string(), - validator_proof: "/arc/validator-proof/v1".to_string(), - } - } - - #[test] - fn protocol_names_mainnet_uses_arc_brand() { - assert_eq!( - consensus::p2p::protocol_names(ChainId::Mainnet), - expected_arc_v1_names(), - ); - } - - #[test] - fn protocol_names_non_mainnet_uses_malachite_defaults() { - for chain_id in [ChainId::Testnet, ChainId::Devnet, ChainId::Localdev] { - assert_eq!( - consensus::p2p::protocol_names(chain_id), - ProtocolNames::default(), - "expected Malachite defaults for {chain_id:?}", - ); - } - } - - // Tripwire: our inlined Malachite defaults must match upstream. If Malachite - // bumps its defaults, this fails and forces a conscious decision about whether - // non-mainnet chains follow the bump or stay pinned to the inlined strings. - #[test] - fn inlined_malachite_defaults_match_upstream() { - assert_eq!( - consensus::p2p::protocol_names_malachite_defaults(), - ProtocolNames::default(), - ); - } -} diff --git a/crates/malachite-app/src/lib.rs b/crates/malachite-app/src/lib.rs deleted file mode 100644 index 6cb900e..0000000 --- a/crates/malachite-app/src/lib.rs +++ /dev/null @@ -1,42 +0,0 @@ -// Copyright 2026 Circle Internet Group, Inc. All rights reserved. -// -// SPDX-License-Identifier: Apache-2.0 -// -// Licensed under the Apache License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. -// You may obtain a copy of the License at -// -// http://www.apache.org/licenses/LICENSE-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// See the License for the specific language governing permissions and -// limitations under the License. - -#![cfg_attr( - test, - allow(clippy::arithmetic_side_effects, clippy::cast_possible_truncation) -)] - -mod app; -mod block; -mod config; -mod env_config; -mod finalize; -mod handlers; -mod metrics; -mod payload; -mod proposal_parts; -mod state; -mod stats; -mod streaming; -pub mod utils; -mod validator_proof; - -pub mod hardcoded_config; -pub mod node; -pub mod request; -pub mod rpc; -pub mod rpc_sync; -pub use arc_consensus_db as store; diff --git a/crates/malachite-app/src/main.rs b/crates/malachite-app/src/main.rs deleted file mode 100644 index d69e8b2..0000000 --- a/crates/malachite-app/src/main.rs +++ /dev/null @@ -1,778 +0,0 @@ -// Copyright 2025 Circle Internet Group, Inc. All rights reserved. -// -// SPDX-License-Identifier: Apache-2.0 -// -// Licensed under the Apache License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. -// You may obtain a copy of the License at -// -// http://www.apache.org/licenses/LICENSE-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// See the License for the specific language governing permissions and -// limitations under the License. - -//! Arc Malachite application. - -/// Number of certificate heights retained by the pruning presets. -/// Approximately 33 hours at 0.5 s/block (237600 × 0.5 s ≈ 33 h). -const PRESETS_PRUNE_CERTIFICATES_DISTANCE: u64 = 237_600; - -use bytesize::ByteSize; -use eyre::{eyre, Result}; -use tracing::{info, trace}; - -use arc_consensus_types::{ - Config, ExecutionConfig, Height, MetricsConfig, PruningConfig, RpcConfig, RuntimeConfig, - SigningConfig, -}; -use arc_node_consensus::hardcoded_config; -use arc_node_consensus::node::{App, StartConfig}; -use arc_node_consensus::store::migrations::MigrationCoordinator; -use arc_node_consensus::store::{rollback_to_height, CERTIFICATES_TABLE, ROLLBACK_BATCH_SIZE}; -use arc_node_consensus_cli::{ - args::{Args, Commands}, - cmd::{ - db::DbCommands, - db::MigrateCmd, - db::RollbackCmd, - download::DownloadCmd, - init::InitCmd, - key::KeyCmd, - start::{StartCmd, RUNTIME_MULTI_THREADED, RUNTIME_SINGLE_THREADED}, - }, - config, logging, runtime, -}; -use redb::ReadableTable; - -#[cfg(not(target_env = "msvc"))] -#[global_allocator] -static ALLOC: tikv_jemallocator::Jemalloc = tikv_jemallocator::Jemalloc; - -/// Profiling configuration for jemalloc. -#[cfg(feature = "pprof")] -#[allow(non_upper_case_globals)] -#[unsafe(export_name = "malloc_conf")] -pub static malloc_conf: &[u8] = b"prof:true,prof_active:false,lg_prof_sample:19\0"; - -/// Main entry point for the application -/// -/// This function: -/// - Parses command-line arguments -/// - Builds configuration from CLI arguments -/// - Initializes logging system -/// - Sets up error handling -/// - Creates and runs the application node -fn main() -> Result<()> { - oneline_eyre::install()?; - - // Load command-line arguments and possible configuration file. - let args = Args::new(); - - // StartCmd log_level/log_format take precedence over Args-level (for backwards compat). - let (start_cmd_log_level, start_cmd_log_format) = match &args.command { - Commands::Start(cmd) => (cmd.log_level, cmd.log_format), - _ => (None, None), - }; - let logging = config::LoggingConfig { - log_level: start_cmd_log_level.unwrap_or(args.log_level), - log_format: start_cmd_log_format.unwrap_or(args.log_format), - }; - - // This is a drop guard responsible for flushing any remaining logs when the program terminates. - // It must be assigned to a binding that is not _, as _ will result in the guard being dropped immediately. - let _guard = logging::init(logging.log_level, logging.log_format); - - info!( - version = arc_version::GIT_VERSION, - commit = arc_version::GIT_COMMIT_HASH, - "Arc Consensus CL starting" - ); - - trace!("Command-line parameters: {args:?}"); - - // Parse the input command. - match &args.command { - Commands::Start(cmd) => start(&args, cmd, logging), - Commands::Init(cmd) => init(&args, cmd, logging), - Commands::Key(cmd) => key(&args, cmd), - Commands::Db(db_cmd) => match db_cmd { - DbCommands::Migrate(cmd) => db_migrate(&args, cmd), - DbCommands::Compact => compact(&args), - DbCommands::Rollback(cmd) => rollback(&args, cmd), - }, - Commands::Download(cmd) => download(&args, cmd), - } -} - -/// Build signing configuration from CLI arguments -fn build_signing_config(cmd: &StartCmd) -> Result { - if let Some(endpoint) = &cmd.signing_remote { - Ok(SigningConfig::Remote( - hardcoded_config::build_remote_signing_config( - endpoint.clone(), - cmd.signing_tls_cert_path.clone(), - ), - )) - } else { - // Default to local signing if neither flag is specified - Ok(SigningConfig::Local) - } -} - -/// Build configuration from CLI arguments -fn build_config_from_cli(cmd: &StartCmd, logging: config::LoggingConfig) -> Result { - let p2p_listen_addr = cmd.p2p_listen_addr()?; - let persistent_peers = cmd.persistent_peers(); - - // Consensus is disabled when --no-consensus is set or follow mode is enabled - let consensus_enabled = !cmd.no_consensus && !cmd.follow; - - let gossipsub_overrides = hardcoded_config::GossipSubOverrides { - explicit_peering: cmd.gossipsub_explicit_peering, - mesh_prioritization: cmd.gossipsub_mesh_prioritization, - load: hardcoded_config::GossipLoad::from_str_opt(cmd.gossipsub_load.as_deref()), - }; - - let consensus = hardcoded_config::build_consensus_config( - p2p_listen_addr, - persistent_peers.clone(), - cmd.p2p_persistent_peers_only, - cmd.discovery, - cmd.discovery_num_outbound_peers, - cmd.discovery_num_inbound_peers, - consensus_enabled, - gossipsub_overrides, - ); - - let value_sync = hardcoded_config::build_value_sync_config(cmd.value_sync); - - let metrics = MetricsConfig { - enabled: cmd.metrics.is_some(), - listen_addr: cmd - .metrics - .unwrap_or_else(|| "0.0.0.0:29000".parse().expect("valid socket address")), - }; - - let runtime = match cmd.runtime_flavor.as_str() { - RUNTIME_SINGLE_THREADED => RuntimeConfig::single_threaded(), - RUNTIME_MULTI_THREADED => RuntimeConfig::multi_threaded(cmd.worker_threads.unwrap_or(0)), - _ => { - return Err(eyre!( - "Invalid runtime flavor: {}. Must be '{}' or '{}'", - cmd.runtime_flavor, - RUNTIME_SINGLE_THREADED, - RUNTIME_MULTI_THREADED, - )); - } - }; - - let rpc = RpcConfig { - enabled: cmd.rpc_addr.is_some(), - listen_addr: cmd - .rpc_addr - .unwrap_or_else(|| "0.0.0.0:31000".parse().expect("valid socket address")), - }; - - let certificates_distance = if cmd.full || cmd.minimal { - PRESETS_PRUNE_CERTIFICATES_DISTANCE - } else { - cmd.prune_certificates_distance - }; - let prune = PruningConfig { - certificates_distance, - certificates_before: Height::new(cmd.prune_certificates_before), - }; - - let execution = ExecutionConfig { - persistence_backpressure: cmd.execution_persistence_backpressure, - persistence_backpressure_threshold: cmd.execution_persistence_backpressure_threshold, - }; - - Ok(Config { - moniker: cmd.get_moniker(), - logging, - consensus, - value_sync, - metrics, - runtime, - prune, - rpc, - execution, - signing: build_signing_config(cmd)?, - }) -} - -fn start(args: &Args, cmd: &StartCmd, logging: config::LoggingConfig) -> Result<()> { - // Validate command options before proceeding - cmd.validate() - .map_err(|error| eyre!("Invalid command options: {error}"))?; - - // Build configuration from CLI arguments - let config = build_config_from_cli(cmd, logging)?; - - let rt = runtime::build_runtime(config.runtime)?; - - info!( - moniker = %config.moniker, - p2p_addr = %config.consensus.p2p.listen_addr, - "Built configuration from CLI arguments", - ); - - trace!(?config, "Configuration"); - - let private_key_file = { - let default = args.get_default_priv_validator_key_file_path()?; - cmd.private_key_file(default)? - }; - - let start_config = StartConfig { - persistent_peers: cmd.persistent_peers(), - persistent_peers_only: cmd.p2p_persistent_peers_only, - gossipsub_overrides: hardcoded_config::GossipSubOverrides { - explicit_peering: cmd.gossipsub_explicit_peering, - mesh_prioritization: cmd.gossipsub_mesh_prioritization, - load: hardcoded_config::GossipLoad::from_str_opt(cmd.gossipsub_load.as_deref()), - }, - eth_socket: cmd.eth_socket.clone(), - execution_socket: cmd.execution_socket.clone(), - eth_rpc_endpoint: cmd.eth_rpc_endpoint.clone(), - execution_endpoint: cmd.execution_endpoint.clone(), - execution_ws_endpoint: cmd.execution_ws_endpoint.clone(), - execution_jwt: cmd.execution_jwt.clone(), - pprof_bind_address: Some(cmd.pprof_addr.parse()?), - pprof_heap_prof: cmd.pprof_heap_prof, - suggested_fee_recipient: cmd.suggested_fee_recipient, - skip_db_upgrade: cmd.skip_db_upgrade, - validator: cmd.validator, - rpc_sync_enabled: cmd.follow, - rpc_sync_endpoints: cmd.follow_endpoints.clone(), - }; - - // Setup the application - let app = App::new(config, args.get_home_dir()?, private_key_file, start_config); - - // Start the node - rt.block_on(app.run()) -} - -fn init(args: &Args, cmd: &InitCmd, _logging: config::LoggingConfig) -> Result<()> { - cmd.run(&args.get_default_priv_validator_key_file_path()?) - .map_err(|error| eyre!("Failed to run init command {error:?}")) -} - -fn key(args: &Args, cmd: &KeyCmd) -> Result<()> { - cmd.run(&args.get_default_priv_validator_key_file_path()?) - .map_err(|error| eyre!("Failed to run key command {error:?}")) -} - -fn db_migrate(args: &Args, cmd: &MigrateCmd) -> Result<()> { - info!("Starting database migration"); - - let db_path = args.get_db_path()?; - - if !db_path.exists() { - return Err(eyre!( - "Database file does not exist at path: {}", - db_path.display() - )); - } - - info!(path = %db_path.display(), "Opening database"); - - let db = redb::Database::builder() - .open(&db_path) - .map_err(|e| eyre!("Failed to open database: {e}"))?; - - let coordinator = MigrationCoordinator::new(db); - - // Check if migration is needed - let needs_migration = coordinator - .needs_migration(db_path.exists()) - .map_err(|e| eyre!("Failed to check migration status: {e}"))?; - - if !needs_migration { - info!("Database is already up to date"); - return Ok(()); - } - - if cmd.dry_run { - let stats = coordinator - .preview_migrate() - .map_err(|e| eyre!("Dry-run migration scan failed: {e}"))?; - - info!( - tables = stats.tables_migrated, - scanned = stats.records_scanned, - would_upgrade = stats.records_upgraded, - skipped = stats.records_skipped, - duration = ?stats.duration, - "Dry-run mode: migration scan complete (no changes committed)" - ); - return Ok(()); - } - - info!("Performing database migration"); - - // Perform migration - let stats = coordinator - .migrate() - .map_err(|e| eyre!("Migration failed: {e}"))?; - - info!( - tables = stats.tables_migrated, - scanned = stats.records_scanned, - upgraded = stats.records_upgraded, - skipped = stats.records_skipped, - duration = ?stats.duration, - "Database migration completed successfully" - ); - - Ok(()) -} - -fn download(args: &Args, cmd: &DownloadCmd) -> Result<()> { - let home_dir = args.get_home_dir()?; - let rt = runtime::build_runtime(arc_consensus_types::RuntimeConfig::multi_threaded(0))?; - rt.block_on(cmd.run(&home_dir)) -} - -fn compact(args: &Args) -> Result<()> { - info!("Starting database compaction"); - - let db_path = args.get_db_path()?; - if !db_path.exists() { - return Err(eyre!("Database file not found at {}", db_path.display())); - } - - info!(path = %db_path.display(), "Opening database"); - - let mut db = redb::Database::builder() - .open(&db_path) - .map_err(|e| eyre!("Failed to open database: {e}"))?; - - let before_size = std::fs::metadata(&db_path) - .map_err(|e| eyre!("Failed to get database file metadata: {e}"))? - .len(); - - info!(size.before = %ByteSize::b(before_size), "Compacting database"); - - db.compact() - .map_err(|e| eyre!("Database compaction failed: {e}"))?; - - let after_size = std::fs::metadata(&db_path) - .map_err(|e| eyre!("Failed to get database file metadata: {e}"))? - .len(); - - info!( - size.before = %ByteSize::b(before_size), - size.after = %ByteSize::b(after_size), - reclaimed = %ByteSize::b(before_size.saturating_sub(after_size)), - "Database compaction completed successfully" - ); - - Ok(()) -} - -fn rollback(args: &Args, cmd: &RollbackCmd) -> Result<()> { - info!("Starting database rollback"); - - let db_path = args.get_db_path()?; - if !db_path.exists() { - return Err(eyre!("Database file not found at {}", db_path.display())); - } - - info!(path = %db_path.display(), "Opening database"); - - let db = redb::Database::builder() - .open(&db_path) - .map_err(|e| eyre!("Failed to open database: {e}"))?; - - let current_height = { - let tx = db - .begin_read() - .map_err(|e| eyre!("Failed to start read transaction: {e}"))?; - let table = tx - .open_table(CERTIFICATES_TABLE) - .map_err(|e| eyre!("Failed to open certificates table: {e}"))?; - table - .last() - .map_err(|e| eyre!("Failed to read certificates tip: {e}"))? - .map(|(k, _)| k.value()) - .ok_or_else(|| eyre!("Consensus database is empty; nothing to roll back"))? - }; - - let target_height = match (cmd.num_heights, cmd.to_height) { - (Some(n), None) => { - if n == 0 { - return Err(eyre!("--num-heights must be greater than 0")); - } - current_height.saturating_sub(n) - } - (None, Some(h)) => Height::new(h), - _ => { - return Err(eyre!( - "Specify exactly one of --num-heights or --to-height " - )); - } - }; - - if target_height >= current_height { - return Err(eyre!( - "Target height {} is not below current height {}; nothing to roll back", - target_height, - current_height, - )); - } - - if target_height < Height::new(1) { - return Err(eyre!( - "Rolling back to height {} would erase genesis (height 1). \ - Minimum target height is 1.", - target_height, - )); - } - - let heights_to_remove = current_height - .as_u64() - .checked_sub(target_height.as_u64()) - .expect("target_height < current_height guarded above"); - - info!( - current_height = %current_height, - target_height = %target_height, - heights_to_remove = heights_to_remove, - execute = cmd.execute, - "Rolling back consensus database" - ); - - let dry_run = !cmd.execute; - let report = rollback_to_height(&db, target_height, ROLLBACK_BATCH_SIZE, dry_run) - .map_err(|e| eyre!("Rollback failed: {e}"))?; - - info!( - prior_height = %current_height, - target_height = %target_height, - certificates = report.certificates, - decided_blocks = report.decided_blocks, - invalid_payloads = report.invalid_payloads, - misbehavior_evidence = report.misbehavior_evidence, - proposal_monitor_data = report.proposal_monitor_data, - undecided_blocks = report.undecided_blocks, - pending_proposal_parts = report.pending_proposal_parts, - "Rollback report" - ); - - if dry_run { - info!("Dry run complete. To execute this rollback, re-run with --execute"); - } else { - info!("Database rollback completed successfully"); - } - - Ok(()) -} - -#[cfg(test)] -mod tests { - use super::*; - use arc_consensus_types::{LogFormat, LogLevel}; - use arc_node_consensus_cli::cmd::start::StartCmd; - - fn test_logging_config() -> config::LoggingConfig { - config::LoggingConfig { - log_level: LogLevel::Info, - log_format: LogFormat::Plaintext, - } - } - - fn minimal_start_cmd() -> StartCmd { - StartCmd { - moniker: Some("test-node".to_string()), - p2p_addr: "/ip4/127.0.0.1/tcp/27000".parse().unwrap(), - ..Default::default() - } - } - - #[test] - fn build_signing_config_defaults_to_local() { - let cmd = minimal_start_cmd(); - let config = build_signing_config(&cmd).unwrap(); - assert!(matches!(config, SigningConfig::Local)); - } - - #[test] - fn build_signing_config_with_remote_endpoint_only() { - let mut cmd = minimal_start_cmd(); - cmd.signing_remote = Some("http://signer:10340".to_string()); - - let config = build_signing_config(&cmd).unwrap(); - - match config { - SigningConfig::Remote(remote) => { - assert_eq!(remote.endpoint, "http://signer:10340"); - assert_eq!(remote.timeout, std::time::Duration::from_secs(30)); - assert!(!remote.enable_tls); - assert_eq!(remote.tls_cert_path, None); - } - _ => panic!("Expected remote signing config"), - } - } - - #[test] - fn build_signing_config_with_remote_and_tls_cert() { - let mut cmd = minimal_start_cmd(); - cmd.signing_remote = Some("http://signer:10340".to_string()); - cmd.signing_tls_cert_path = Some("/path/to/cert.pem".to_string()); - - let config = build_signing_config(&cmd).unwrap(); - - match config { - SigningConfig::Remote(remote) => { - assert_eq!(remote.endpoint, "http://signer:10340"); - assert_eq!(remote.timeout, std::time::Duration::from_secs(30)); - assert!(remote.enable_tls); // Auto-enabled when cert path provided - assert_eq!(remote.tls_cert_path, Some("/path/to/cert.pem".to_string())); - } - _ => panic!("Expected remote signing config"), - } - } - - #[test] - fn build_config_from_cli_with_minimal_flags() { - let cmd = minimal_start_cmd(); - let logging = test_logging_config(); - - let config = build_config_from_cli(&cmd, logging).unwrap(); - - assert_eq!(config.moniker, "test-node"); - assert!(config.consensus.enabled); - assert_eq!( - config.consensus.p2p.listen_addr.to_string(), - "/ip4/127.0.0.1/tcp/27000" - ); - assert!(config.consensus.p2p.persistent_peers.is_empty()); - assert!(!config.consensus.p2p.discovery.enabled); - assert!(config.value_sync.enabled); - assert!(!config.metrics.enabled); - assert!(!config.rpc.enabled); - assert_eq!(config.prune.certificates_distance, 0); - assert!(matches!(config.signing, SigningConfig::Local)); - } - - #[test] - fn build_config_from_cli_with_all_optional_flags() { - let mut cmd = minimal_start_cmd(); - cmd.moniker = Some("validator-1".to_string()); - cmd.p2p_addr = "/ip4/172.19.0.5/tcp/27000".parse().unwrap(); - cmd.p2p_persistent_peers = vec![ - "/ip4/172.19.0.6/tcp/27000".parse().unwrap(), - "/ip4/172.19.0.7/tcp/27000".parse().unwrap(), - ]; - cmd.discovery = true; - cmd.discovery_num_outbound_peers = 30; - cmd.discovery_num_inbound_peers = 40; - cmd.value_sync = false; - cmd.metrics = Some("0.0.0.0:29000".parse().unwrap()); - cmd.rpc_addr = Some("0.0.0.0:31000".parse().unwrap()); - cmd.prune_certificates_distance = 1000; - cmd.prune_certificates_before = 100; - cmd.signing_remote = Some("http://signer:10340".to_string()); - - let logging = test_logging_config(); - let config = build_config_from_cli(&cmd, logging).unwrap(); - - assert_eq!(config.moniker, "validator-1"); - assert_eq!(config.consensus.p2p.persistent_peers.len(), 2); - assert!(config.consensus.p2p.discovery.enabled); - assert_eq!(config.consensus.p2p.discovery.num_outbound_peers, 30); - assert_eq!(config.consensus.p2p.discovery.num_inbound_peers, 40); - assert!(!config.value_sync.enabled); - assert!(config.metrics.enabled); - assert_eq!(config.metrics.listen_addr.to_string(), "0.0.0.0:29000"); - assert!(config.rpc.enabled); - assert_eq!(config.rpc.listen_addr.to_string(), "0.0.0.0:31000"); - assert_eq!(config.prune.certificates_distance, 1000); - assert_eq!(config.prune.certificates_before.as_u64(), 100); - assert!(matches!(config.signing, SigningConfig::Remote(_))); - } - - #[test] - fn build_config_from_cli_persistent_peers_only() { - let mut cmd = minimal_start_cmd(); - cmd.p2p_persistent_peers_only = true; - - let logging = test_logging_config(); - let config = build_config_from_cli(&cmd, logging).unwrap(); - - assert!(config.consensus.p2p.persistent_peers_only); - } - - #[test] - fn build_config_from_cli_persistent_peers_only_default_false() { - let cmd = minimal_start_cmd(); - let logging = test_logging_config(); - - let config = build_config_from_cli(&cmd, logging).unwrap(); - - assert!(!config.consensus.p2p.persistent_peers_only); - } - - #[test] - fn build_config_from_cli_calculates_num_nodes_from_peers() { - let mut cmd = minimal_start_cmd(); - cmd.p2p_persistent_peers = vec![ - "/ip4/172.19.0.6/tcp/27000".parse().unwrap(), - "/ip4/172.19.0.7/tcp/27000".parse().unwrap(), - "/ip4/172.19.0.8/tcp/27000".parse().unwrap(), - ]; - - let logging = test_logging_config(); - let config = build_config_from_cli(&cmd, logging).unwrap(); - - // num_nodes = persistent_peers.len() + 1 = 3 + 1 = 4 - // This affects gossipsub config generation - assert_eq!(config.consensus.p2p.persistent_peers.len(), 3); - } - - #[test] - fn build_config_from_cli_metrics_enabled_when_flag_present() { - let mut cmd = minimal_start_cmd(); - cmd.metrics = Some("127.0.0.1:29000".parse().unwrap()); - - let logging = test_logging_config(); - let config = build_config_from_cli(&cmd, logging).unwrap(); - - assert!(config.metrics.enabled); - assert_eq!(config.metrics.listen_addr.to_string(), "127.0.0.1:29000"); - } - - #[test] - fn build_config_from_cli_metrics_disabled_when_flag_absent() { - let cmd = minimal_start_cmd(); - - let logging = test_logging_config(); - let config = build_config_from_cli(&cmd, logging).unwrap(); - - assert!(!config.metrics.enabled); - // Default address is still set even when disabled - assert_eq!(config.metrics.listen_addr.to_string(), "0.0.0.0:29000"); - } - - #[test] - fn build_config_from_cli_rpc_enabled_when_flag_present() { - let mut cmd = minimal_start_cmd(); - cmd.rpc_addr = Some("127.0.0.1:31000".parse().unwrap()); - - let logging = test_logging_config(); - let config = build_config_from_cli(&cmd, logging).unwrap(); - - assert!(config.rpc.enabled); - assert_eq!(config.rpc.listen_addr.to_string(), "127.0.0.1:31000"); - } - - #[test] - fn build_config_from_cli_rpc_disabled_when_flag_absent() { - let cmd = minimal_start_cmd(); - - let logging = test_logging_config(); - let config = build_config_from_cli(&cmd, logging).unwrap(); - - assert!(!config.rpc.enabled); - // Default address is still set even when disabled - assert_eq!(config.rpc.listen_addr.to_string(), "0.0.0.0:31000"); - } - - #[test] - fn build_config_from_cli_pruning_configuration() { - let mut cmd = minimal_start_cmd(); - cmd.prune_certificates_distance = 500; - cmd.prune_certificates_before = 50; - - let logging = test_logging_config(); - let config = build_config_from_cli(&cmd, logging).unwrap(); - - assert_eq!(config.prune.certificates_distance, 500); - assert_eq!(config.prune.certificates_before.as_u64(), 50); - } - - #[test] - fn build_config_from_cli_full_preset() { - let mut cmd = minimal_start_cmd(); - cmd.full = true; - - let logging = test_logging_config(); - let config = build_config_from_cli(&cmd, logging).unwrap(); - - assert_eq!( - config.prune.certificates_distance, - PRESETS_PRUNE_CERTIFICATES_DISTANCE - ); - assert_eq!(config.prune.certificates_before.as_u64(), 0); - } - - #[test] - fn build_config_from_cli_minimal_preset() { - let mut cmd = minimal_start_cmd(); - cmd.minimal = true; - - let logging = test_logging_config(); - let config = build_config_from_cli(&cmd, logging).unwrap(); - - assert_eq!( - config.prune.certificates_distance, - PRESETS_PRUNE_CERTIFICATES_DISTANCE - ); - assert_eq!(config.prune.certificates_before.as_u64(), 0); - } - - #[test] - fn build_config_from_cli_uses_hardcoded_runtime() { - let cmd = minimal_start_cmd(); - let logging = test_logging_config(); - - let config = build_config_from_cli(&cmd, logging).unwrap(); - - // Should use the multi-threaded runtime - assert_eq!(config.runtime, RuntimeConfig::multi_threaded(0)); - } - - #[test] - fn build_config_from_cli_multi_threaded_runtime_with_threads() { - let mut cmd = minimal_start_cmd(); - cmd.runtime_flavor = RUNTIME_MULTI_THREADED.to_string(); - cmd.worker_threads = Some(8); - - let logging = test_logging_config(); - let config = build_config_from_cli(&cmd, logging).unwrap(); - - // Should use the multi-threaded runtime with 8 threads - assert_eq!(config.runtime, RuntimeConfig::multi_threaded(8)); - } - - #[test] - fn build_config_from_cli_single_threaded_runtime() { - let mut cmd = minimal_start_cmd(); - cmd.runtime_flavor = RUNTIME_SINGLE_THREADED.to_string(); - - let logging = test_logging_config(); - let config = build_config_from_cli(&cmd, logging).unwrap(); - - // Should use the single-threaded runtime - assert_eq!(config.runtime, RuntimeConfig::single_threaded()); - } - - #[test] - fn build_config_from_cli_invalid_runtime_flavor() { - let mut cmd = minimal_start_cmd(); - cmd.runtime_flavor = "invalid-flavor".to_string(); - - let logging = test_logging_config(); - let result = build_config_from_cli(&cmd, logging); - - assert!(result.is_err()); - let error_message = format!("{}", result.unwrap_err()); - assert!(error_message.contains("Invalid runtime flavor")); - } -} diff --git a/crates/malachite-app/src/metrics/app.rs b/crates/malachite-app/src/metrics/app.rs deleted file mode 100644 index c8eca57..0000000 --- a/crates/malachite-app/src/metrics/app.rs +++ /dev/null @@ -1,644 +0,0 @@ -// Copyright 2025 Circle Internet Group, Inc. All rights reserved. -// -// SPDX-License-Identifier: Apache-2.0 -// -// Licensed under the Apache License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. -// You may obtain a copy of the License at -// -// http://www.apache.org/licenses/LICENSE-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// See the License for the specific language governing permissions and -// limitations under the License. - -use std::collections::HashSet; -use std::fmt::Write; -use std::ops::Deref; -use std::sync::atomic::AtomicU64; -use std::sync::Arc; -use std::sync::RwLock; -use std::time::{Duration, Instant}; - -use arc_consensus_types::ConsensusParams; -use arc_consensus_types::{Address, ValidatorSet}; -use malachitebft_app_channel::app::metrics::prometheus::encoding::{ - EncodeLabelSet, EncodeLabelValue, LabelValueEncoder, -}; -use malachitebft_app_channel::app::metrics::prometheus::metrics::counter::Counter; -use malachitebft_app_channel::app::metrics::prometheus::metrics::family::Family; -use malachitebft_app_channel::app::metrics::prometheus::metrics::gauge::Gauge; -use malachitebft_app_channel::app::metrics::prometheus::metrics::histogram::{ - exponential_buckets, exponential_buckets_range, Histogram, -}; -use malachitebft_app_channel::app::metrics::prometheus::metrics::info::Info; -use malachitebft_app_channel::app::metrics::SharedRegistry; - -/// Metrics for the database. -/// Metrics for the application. -#[derive(Clone, Debug)] -pub struct AppMetrics(Arc); - -impl Deref for AppMetrics { - type Target = Inner; - - fn deref(&self) -> &Self::Target { - &self.0 - } -} - -/// Inner struct for database metrics. -/// Inner struct for application metrics. -#[derive(Debug)] -pub struct Inner { - /// Time between two blocks - block_time: Histogram, - - /// Time taken to finalize a block - block_finalize_time: Histogram, - - /// Time taken to build a block - block_build_time: Histogram, - - /// Number of transactions in each finalized block - block_transactions_count: Histogram, - - /// Size of each finalized block in bytes - block_size_bytes: Histogram, - - /// Gas used in each finalized block - block_gas_used: Histogram, - - /// Total number of transactions finalized since start - total_transactions_count: Counter, - - /// Total size of all finalized blocks in bytes since start - total_chain_bytes: Counter, - - /// Size of the validator set - validators_count: Gauge, - - /// Total voting power of the validator set - validators_total_voting_power: Gauge, - - /// Voting power of each validator - validator_voting_power: Family, - - /// Time taken to process a message - msg_process_time: Family, - - /// Time taken for each Engine API call - engine_api_time: Family, - - /// The number of times the consensus height has been restarted - height_restart_count: Counter, - - /// Number of times the node fell behind and transitioned from InSync to CatchingUp - sync_fell_behind_count: Counter, - - /// Number of invalid payloads observed and persisted for forensics, - /// labelled by source (engine reject, assembly failure, sync decode). - invalid_payloads_count: Family, - - /// Number of pending proposal parts waiting to be processed at a future height or round - pending_proposal_parts_count: Gauge, - - /// Consensus parameters - consensus_params: Family>, - - /// Number of blocks replayed from CL to EL during startup handshake - handshake_replay_blocks: Gauge, - - /// Internal state recording the previous validator set. - /// Useful field to manage validators' metrics. - /// This field is only accessible internally, and is not a metrics itself. - previous_validator_set: RwLock>, -} - -impl Inner { - /// Create a new `Inner` struct. - pub fn new() -> Self { - Self { - block_time: Histogram::new(exponential_buckets_range(0.01, 2.0, 10)), - block_finalize_time: Histogram::new(exponential_buckets_range(0.01, 2.0, 10)), - block_build_time: Histogram::new(exponential_buckets_range(0.01, 2.0, 10)), - block_transactions_count: Histogram::new(exponential_buckets(1.0, 2.0, 15)), // 1, 2, 4, .. , 16384 txs - block_size_bytes: Histogram::new(exponential_buckets(1000.0, 2.0, 21)), // 1KB, 2KB, 4KB, .. , 1GB - block_gas_used: Histogram::new(exponential_buckets(1000.0, 2.0, 16)), // 1K, 2K, 4K, .. , 32M gas (fixed: was 15, now 16 buckets) - total_transactions_count: Counter::default(), - total_chain_bytes: Counter::default(), - validators_count: Gauge::default(), - validators_total_voting_power: Gauge::default(), - validator_voting_power: Family::default(), - previous_validator_set: RwLock::new(HashSet::new()), - msg_process_time: Family::new_with_constructor(|| { - Histogram::new(exponential_buckets_range(0.01, 2.0, 10)) - }), - engine_api_time: Family::new_with_constructor(|| { - Histogram::new(exponential_buckets_range(0.001, 2.0, 10)) - }), - height_restart_count: Counter::default(), - sync_fell_behind_count: Counter::default(), - invalid_payloads_count: Family::default(), - pending_proposal_parts_count: Gauge::default(), - consensus_params: Family::default(), - handshake_replay_blocks: Gauge::default(), - } - } -} - -impl Default for Inner { - fn default() -> Self { - Self::new() - } -} - -impl AppMetrics { - /// Create a new `AppMetrics` struct. - pub fn new() -> Self { - Self(Arc::new(Inner::new())) - } - - /// Register the metrics with the given registry. - pub fn register(registry: &SharedRegistry) -> Self { - let metrics = Self::new(); - - registry.with_prefix("arc_malachite_app", |registry| { - registry.register( - "block_time", - "Interval between two blocks, in seconds", - metrics.block_time.clone(), - ); - - registry.register( - "block_finalize_time", - "Time taken to finalize a block, in seconds", - metrics.block_finalize_time.clone(), - ); - - registry.register( - "block_build_time", - "Time taken to build a block, in seconds", - metrics.block_build_time.clone(), - ); - - registry.register( - "block_transactions_count", - "Number of transactions in each finalized block", - metrics.block_transactions_count.clone(), - ); - - registry.register( - "block_size_bytes", - "Size of each finalized block in bytes", - metrics.block_size_bytes.clone(), - ); - - registry.register( - "block_gas_used", - "Gas used in each finalized block", - metrics.block_gas_used.clone(), - ); - - registry.register( - "total_transactions_count", - "Total number of transactions finalized since start", - metrics.total_transactions_count.clone(), - ); - - registry.register( - "total_chain_bytes", - "Total size of all finalized blocks in bytes since start", - metrics.total_chain_bytes.clone(), - ); - - registry.register( - "validators_count", - "Number of validators in the current validator set", - metrics.validators_count.clone(), - ); - - registry.register( - "validators_total_voting_power", - "Total voting power of the current validator set", - metrics.validators_total_voting_power.clone(), - ); - - registry.register( - "validator_voting_power", - "Voting power of each validator", - metrics.validator_voting_power.clone(), - ); - - registry.register( - "consensus_params", - "Consensus parameters", - metrics.consensus_params.clone(), - ); - - registry.register( - "msg_process_time", - "Time taken to process a message, in seconds", - metrics.msg_process_time.clone(), - ); - - registry.register( - "engine_api_time", - "Time taken for each Engine API call, in seconds", - metrics.engine_api_time.clone(), - ); - - registry.register( - "height_restart_count", - "The number of times the consensus height has been restarted", - metrics.height_restart_count.clone(), - ); - - registry.register( - "sync_fell_behind_count", - "Number of times the node fell behind and transitioned from InSync to CatchingUp", - metrics.sync_fell_behind_count.clone(), - ); - - registry.register( - "invalid_payloads_count", - "Number of invalid payloads observed and persisted for forensics", - metrics.invalid_payloads_count.clone(), - ); - - registry.register( - "pending_proposal_parts_count", - "Number of pending proposal parts waiting to be processed at a future height or round", - metrics.pending_proposal_parts_count.clone(), - ); - - registry.register( - "handshake_replay_blocks", - "Number of blocks replayed from CL to EL during startup handshake", - metrics.handshake_replay_blocks.clone(), - ); - - // Register version info as a separate Info metric - let version_info = Info::new(VersionInfoLabel { - version: arc_version::SHORT_VERSION, - git_commit: arc_version::GIT_COMMIT_HASH, - }); - registry.register( - "version", // NOTE: The Prometheus exporter will add an `_info` suffix automatically - "Version information for the consensus layer", - version_info, - ); - - }); - - metrics - } - - /// Observe the time between two blocks, in seconds. - pub fn observe_block_time(&self, seconds: f64) { - self.block_time.observe(seconds); - } - - /// Observe the time taken to finalize a block, in seconds. - pub fn observe_block_finalize_time(&self, seconds: f64) { - self.block_finalize_time.observe(seconds); - } - - /// Observe the time taken to build a block, in seconds. - pub fn observe_block_build_time(&self, seconds: f64) { - self.block_build_time.observe(seconds); - } - - /// Observe the number of transactions in a finalized block. - pub fn observe_block_transactions_count(&self, count: u64) { - self.block_transactions_count.observe(count as f64); - } - - /// Observe the size of a finalized block in bytes. - pub fn observe_block_size_bytes(&self, size: u64) { - self.block_size_bytes.observe(size as f64); - } - - /// Observe the gas used in a finalized block. - pub fn observe_block_gas_used(&self, gas: u64) { - self.block_gas_used.observe(gas as f64); - } - - /// Increment the total number of transactions finalized. - pub fn inc_total_transactions_count(&self, count: u64) { - self.total_transactions_count.inc_by(count); - } - - /// Increment the total size of all finalized blocks in bytes. - pub fn inc_total_chain_bytes(&self, size: u64) { - self.total_chain_bytes.inc_by(size); - } - - /// Update metrics related to the active validator set. - pub fn update_validator_set(&self, validator_set: &ValidatorSet) { - self.validators_count.set(validator_set.len() as i64); - - self.validators_total_voting_power - .set(validator_set.total_voting_power() as i64); - - // Update the voting power of the current validator set - let mut current_validators = HashSet::new(); - for validator in validator_set.iter() { - let label = AddressLabel::new(validator.address); - - current_validators.insert(label); - - self.validator_voting_power - .get_or_create(&label) - .set(validator.voting_power as i64); - } - - // Set the validator power of validators that are no longer in the validator - // set to 0. - let mut prev_validators = self - .previous_validator_set - .write() - .expect("lock poisoning is unrecoverable"); - for removed_validator in prev_validators.difference(¤t_validators) { - self.validator_voting_power - .get_or_create(removed_validator) - .set(0); - } - - *prev_validators = current_validators; - } - - /// Update consensus parameters - pub fn update_consensus_params(&self, p: &ConsensusParams) { - let data = [ - ("timeout_propose", p.timeouts().propose), - ("timeout_propose_delta", p.timeouts().propose_delta), - ("timeout_prevote", p.timeouts().prevote), - ("timeout_prevote_delta", p.timeouts().prevote_delta), - ("timeout_precommit", p.timeouts().precommit), - ("timeout_precommit_delta", p.timeouts().precommit_delta), - ("timeout_rebroadcast", p.timeouts().rebroadcast), - ( - "target_block_time", - p.target_block_time().unwrap_or_default(), - ), - ]; - - for (param, duration) in data.iter() { - self.consensus_params - .get_or_create(&ConsensusParamsLabel { param }) - .set(duration.as_secs_f64()); - } - } - - /// Start a timer for processing a message. - /// - /// The returned guard will record the time taken to process the message when dropped. - #[must_use] - pub fn start_msg_process_timer(&self, msg: &'static str) -> MetricsGuard { - MetricsGuard::new(self.clone(), msg, |metrics, msg, elapsed| { - metrics - .msg_process_time - .get_or_create(&ProcessMsgLabel::new(msg)) - .observe(elapsed.as_secs_f64()); - }) - } - - /// Start a timer for an Engine API call. - /// - /// The returned guard will record the time taken for the API call when dropped. - #[must_use] - pub fn start_engine_api_timer(&self, api: &'static str) -> MetricsGuard { - MetricsGuard::new(self.clone(), api, |metrics, api, elapsed| { - metrics - .engine_api_time - .get_or_create(&EngineApiLabel::new(api)) - .observe(elapsed.as_secs_f64()); - }) - } - - /// Increment the number of times the consensus height has been restarted - pub fn inc_height_restart_count(&self) { - self.height_restart_count.inc(); - } - - /// Increment the number of times the node fell behind (transitioned from InSync to CatchingUp) - pub fn inc_sync_fell_behind_count(&self) { - self.sync_fell_behind_count.inc(); - } - - /// Increment the invalid-payload counter for the given source. - pub fn inc_invalid_payloads_count(&self, source: InvalidPayloadSource) { - self.invalid_payloads_count - .get_or_create(&InvalidPayloadSourceLabel::new(source)) - .inc(); - } - - /// Total number of invalid payloads across all sources. - #[cfg(test)] - pub fn get_invalid_payloads_count(&self) -> u64 { - InvalidPayloadSource::ALL - .iter() - .map(|source| self.get_invalid_payloads_count_by_source(*source)) - .sum() - } - - /// Number of invalid payloads recorded for a specific source. - #[cfg(test)] - pub fn get_invalid_payloads_count_by_source(&self, source: InvalidPayloadSource) -> u64 { - self.invalid_payloads_count - .get_or_create(&InvalidPayloadSourceLabel::new(source)) - .get() - } - - /// Observe the number of pending proposal parts - pub fn observe_pending_proposal_parts_count(&self, count: usize) { - self.pending_proposal_parts_count.set(count as i64); - } - - /// Set the number of blocks replayed during the startup handshake. - pub fn set_handshake_replay_blocks(&self, count: u64) { - self.handshake_replay_blocks.set(count); - } - - /// Get the number of blocks replayed during the startup handshake. - #[cfg(test)] - pub fn get_handshake_replay_blocks(&self) -> u64 { - self.handshake_replay_blocks.get() - } -} - -impl Default for AppMetrics { - fn default() -> Self { - Self::new() - } -} - -/// This wrapper allows us to derive `AsLabelValue` for any type without running into Rust orphan rules. -#[derive(Clone, Copy, Debug, Hash, PartialEq, Eq)] -struct AsLabelValue(T); - -impl EncodeLabelValue for AsLabelValue
{ - fn encode(&self, encoder: &mut LabelValueEncoder) -> Result<(), std::fmt::Error> { - // Preserve legacy uppercase-no-prefix format for Prometheus label continuity - for byte in self.0.into_inner() { - encoder.write_fmt(format_args!("{byte:02X}"))?; - } - Ok(()) - } -} - -use malachitebft_app_channel::app::metrics::prometheus as prometheus_client; - -#[derive(Clone, Copy, Debug, Hash, PartialEq, Eq, EncodeLabelSet)] -struct AddressLabel { - address: AsLabelValue
, -} - -impl AddressLabel { - fn new(address: Address) -> Self { - Self { - address: AsLabelValue(address), - } - } -} - -#[derive(Clone, Debug, Hash, PartialEq, Eq, EncodeLabelSet)] -struct ProcessMsgLabel { - msg: &'static str, -} - -impl ProcessMsgLabel { - fn new(msg: &'static str) -> Self { - Self { msg } - } -} - -#[derive(Clone, Debug, Hash, PartialEq, Eq, EncodeLabelSet)] -struct EngineApiLabel { - api: &'static str, -} - -impl EngineApiLabel { - fn new(api: &'static str) -> Self { - Self { api } - } -} - -/// Source of an invalid-payload record, used to label the counter. -#[derive(Clone, Copy, Debug, Hash, PartialEq, Eq)] -pub enum InvalidPayloadSource { - /// Execution engine rejected the payload. - EngineReject, - /// Assembling a block from proposal parts failed (malformed parts or SSZ - /// decode error on the concatenated data). - AssemblyFailure, - /// SSZ decode of a value received via sync failed. - SyncDecode, -} - -impl InvalidPayloadSource { - #[cfg(test)] - pub(super) const ALL: [InvalidPayloadSource; 3] = - [Self::EngineReject, Self::AssemblyFailure, Self::SyncDecode]; - - fn as_str(&self) -> &'static str { - match self { - Self::EngineReject => "engine_reject", - Self::AssemblyFailure => "assembly_failure", - Self::SyncDecode => "sync_decode", - } - } -} - -#[derive(Clone, Debug, Hash, PartialEq, Eq, EncodeLabelSet)] -struct InvalidPayloadSourceLabel { - source: &'static str, -} - -impl InvalidPayloadSourceLabel { - fn new(source: InvalidPayloadSource) -> Self { - Self { - source: source.as_str(), - } - } -} - -#[derive(Clone, Debug, Hash, PartialEq, Eq, EncodeLabelSet)] -pub struct VersionInfoLabel { - pub version: &'static str, - pub git_commit: &'static str, -} - -#[derive(Clone, Debug, Hash, PartialEq, Eq, EncodeLabelSet)] -pub struct ConsensusParamsLabel { - pub param: &'static str, -} - -pub struct MetricsGuard { - inner: AppMetrics, - label: &'static str, - callback: fn(&AppMetrics, &'static str, Duration), - start_time: Instant, -} - -impl MetricsGuard { - fn new( - inner: AppMetrics, - label: &'static str, - callback: fn(&AppMetrics, &'static str, Duration), - ) -> Self { - Self { - inner, - label, - callback, - start_time: Instant::now(), - } - } -} - -impl Drop for MetricsGuard { - fn drop(&mut self) { - let elapsed = self.start_time.elapsed(); - - (self.callback)(&self.inner, self.label, elapsed); - } -} - -#[cfg(test)] -mod tests { - use super::*; - use prometheus_client::encoding::text::encode; - use prometheus_client::metrics::gauge::Gauge; - use prometheus_client::registry::Registry; - - #[test] - fn test_address_label_preserves_legacy_format() { - let address = Address::new([ - 0x12, 0x34, 0x56, 0x78, 0x9A, 0xBC, 0xDE, 0xF0, 0x11, 0x22, 0x33, 0x44, 0x55, 0x66, - 0x77, 0x88, 0x99, 0xAA, 0xBB, 0xCC, - ]); - - let mut registry = Registry::default(); - let family = prometheus_client::metrics::family::Family::::default(); - registry.register("test_metric", "test", family.clone()); - - family.get_or_create(&AddressLabel::new(address)).set(1); - - let mut buf = String::new(); - encode(&mut buf, ®istry).unwrap(); - - // Prometheus labels must use legacy uppercase-no-prefix format, quoted - assert!( - buf.contains("\"123456789ABCDEF0112233445566778899AABBCC\""), - "Expected uppercase-no-prefix address in metrics, got: {buf}" - ); - // No 0x prefix anywhere in the output - assert!( - !buf.contains("0x"), - "Metrics should not contain 0x prefix: {buf}" - ); - } -} diff --git a/crates/malachite-app/src/metrics/db.rs b/crates/malachite-app/src/metrics/db.rs deleted file mode 100644 index cf03343..0000000 --- a/crates/malachite-app/src/metrics/db.rs +++ /dev/null @@ -1,17 +0,0 @@ -// Copyright 2026 Circle Internet Group, Inc. All rights reserved. -// -// SPDX-License-Identifier: Apache-2.0 -// -// Licensed under the Apache License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. -// You may obtain a copy of the License at -// -// http://www.apache.org/licenses/LICENSE-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// See the License for the specific language governing permissions and -// limitations under the License. - -pub use arc_consensus_db::DbMetrics; diff --git a/crates/malachite-app/src/metrics/mod.rs b/crates/malachite-app/src/metrics/mod.rs deleted file mode 100644 index 90628bd..0000000 --- a/crates/malachite-app/src/metrics/mod.rs +++ /dev/null @@ -1,25 +0,0 @@ -// Copyright 2025 Circle Internet Group, Inc. All rights reserved. -// -// SPDX-License-Identifier: Apache-2.0 -// -// Licensed under the Apache License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. -// You may obtain a copy of the License at -// -// http://www.apache.org/licenses/LICENSE-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// See the License for the specific language governing permissions and -// limitations under the License. - -pub mod app; -pub mod db; -pub mod process; -pub mod validator_set; - -pub use app::{AppMetrics, InvalidPayloadSource}; -pub use db::DbMetrics; -pub use process::ProcessMetrics; -pub use validator_set::ValidatorSetMetrics; diff --git a/crates/malachite-app/src/metrics/process.rs b/crates/malachite-app/src/metrics/process.rs deleted file mode 100644 index 112b553..0000000 --- a/crates/malachite-app/src/metrics/process.rs +++ /dev/null @@ -1,414 +0,0 @@ -// Copyright 2025 Circle Internet Group, Inc. All rights reserved. -// -// SPDX-License-Identifier: Apache-2.0 -// -// Licensed under the Apache License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. -// You may obtain a copy of the License at -// -// http://www.apache.org/licenses/LICENSE-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// See the License for the specific language governing permissions and -// limitations under the License. - -#[cfg(not(target_env = "msvc"))] -use malachitebft_app_channel::app::metrics::prometheus::metrics::gauge::Gauge; -use malachitebft_app_channel::app::metrics::SharedRegistry; -use std::sync::Arc; - -/// Process metrics for monitoring system resources -#[derive(Clone, Debug)] -pub struct ProcessMetrics(Arc); - -impl std::ops::Deref for ProcessMetrics { - type Target = Inner; - - fn deref(&self) -> &Self::Target { - &self.0 - } -} - -/// Jemalloc metrics -#[derive(Debug)] -#[cfg(not(target_env = "msvc"))] -struct JemallocMetricsInner { - /// Jemalloc active memory in bytes - active: Gauge, - - /// Jemalloc allocated memory in bytes - allocated: Gauge, - - /// Jemalloc mapped memory in bytes - mapped: Gauge, - - /// Jemalloc metadata memory in bytes - metadata: Gauge, - - /// Jemalloc resident memory in bytes - resident: Gauge, - - /// Jemalloc retained memory in bytes - retained: Gauge, -} - -/// IO and stats metrics -#[derive(Debug)] -#[cfg(target_os = "linux")] -struct IoMetricsInner { - /// IO read characters - rchar: Gauge, - - /// IO written characters - wchar: Gauge, - - /// IO read syscalls - syscr: Gauge, - - /// IO write syscalls - syscw: Gauge, - - /// IO read bytes - read_bytes: Gauge, - - /// IO written bytes - write_bytes: Gauge, - - /// IO cancelled write bytes - cancelled_write_bytes: Gauge, - - /// Process CPU time in seconds (user + system) - process_cpu_seconds_total: Gauge, - - /// Number of open file descriptors - process_open_fds: Gauge, - - /// Number of OS threads in the process - process_threads: Gauge, -} - -/// Main inner struct for process metrics -#[derive(Debug)] -pub struct Inner { - #[cfg(not(target_env = "msvc"))] - jemalloc: JemallocMetricsInner, - - #[cfg(target_os = "linux")] - io: IoMetricsInner, -} - -#[cfg(not(target_env = "msvc"))] -impl JemallocMetricsInner { - /// Create a new `JemallocMetricsInner` struct - pub fn new() -> Self { - Self { - active: Gauge::default(), - allocated: Gauge::default(), - mapped: Gauge::default(), - metadata: Gauge::default(), - resident: Gauge::default(), - retained: Gauge::default(), - } - } - - /// Register the metrics with the given registry - pub fn register(&self, registry: &SharedRegistry) { - registry.with_prefix("arc_malachite_app", |registry| { - registry.register( - "jemalloc_active", - "Total number of bytes in active pages allocated by the application", - self.active.clone(), - ); - - registry.register( - "jemalloc_allocated", - "Total number of bytes allocated by the application", - self.allocated.clone(), - ); - - registry.register( - "jemalloc_mapped", - "Total number of bytes in active extents mapped by the allocator", - self.mapped.clone(), - ); - - registry.register( - "jemalloc_metadata", - "Total number of bytes dedicated to jemalloc metadata", - self.metadata.clone(), - ); - - registry.register( - "jemalloc_resident", - "Total number of bytes in physically resident data pages mapped by the allocator", - self.resident.clone(), - ); - - registry.register( - "jemalloc_retained", - "Total number of bytes in virtual memory mappings that were retained rather than being returned to the operating system", - self.retained.clone(), - ); - }); - } - - /// Update metrics - pub fn update(&self) { - use tracing::error; - - if let Err(error) = tikv_jemalloc_ctl::epoch::advance() { - error!(%error, "Failed to advance jemalloc epoch"); - return; - } - - if let Ok(value) = tikv_jemalloc_ctl::stats::active::read() { - self.active.set(value as i64); - } - - if let Ok(value) = tikv_jemalloc_ctl::stats::allocated::read() { - self.allocated.set(value as i64); - } - - if let Ok(value) = tikv_jemalloc_ctl::stats::mapped::read() { - self.mapped.set(value as i64); - } - - if let Ok(value) = tikv_jemalloc_ctl::stats::metadata::read() { - self.metadata.set(value as i64); - } - - if let Ok(value) = tikv_jemalloc_ctl::stats::resident::read() { - self.resident.set(value as i64); - } - - if let Ok(value) = tikv_jemalloc_ctl::stats::retained::read() { - self.retained.set(value as i64); - } - } -} - -#[cfg(not(target_env = "msvc"))] -impl Default for JemallocMetricsInner { - fn default() -> Self { - Self::new() - } -} - -#[cfg(target_os = "linux")] -impl IoMetricsInner { - /// Create a new `IoMetricsInner` struct - pub fn new() -> Self { - Self { - rchar: Gauge::default(), - wchar: Gauge::default(), - syscr: Gauge::default(), - syscw: Gauge::default(), - read_bytes: Gauge::default(), - write_bytes: Gauge::default(), - cancelled_write_bytes: Gauge::default(), - process_cpu_seconds_total: Gauge::default(), - process_open_fds: Gauge::default(), - process_threads: Gauge::default(), - } - } - - /// Register the metrics with the given registry - pub fn register(&self, registry: &SharedRegistry) { - registry.with_prefix("arc_malachite_app", |registry| { - registry.register("io_rchar", "Characters read", self.rchar.clone()); - - registry.register("io_wchar", "Characters written", self.wchar.clone()); - - registry.register("io_syscr", "Read syscalls", self.syscr.clone()); - - registry.register("io_syscw", "Write syscalls", self.syscw.clone()); - - registry.register("io_read_bytes", "Bytes read", self.read_bytes.clone()); - - registry.register("io_write_bytes", "Bytes written", self.write_bytes.clone()); - - registry.register( - "io_cancelled_write_bytes", - "Cancelled write bytes", - self.cancelled_write_bytes.clone(), - ); - - registry.register( - "process_cpu_seconds_total", - "Total user and system CPU time spent in seconds", - self.process_cpu_seconds_total.clone(), - ); - - registry.register( - "process_open_fds", - "Number of open file descriptors", - self.process_open_fds.clone(), - ); - - registry.register( - "process_threads", - "Number of OS threads in the process", - self.process_threads.clone(), - ); - }); - } - - /// Update metrics - pub fn update(&self) { - use tracing::error; - - let Ok(process) = procfs::process::Process::myself() else { - error!("Failed to get currently running process"); - return; - }; - - let Ok(io) = process.io() else { - error!("Failed to get IO stats for the currently running process"); - return; - }; - - // Update IO metrics - self.rchar.set(io.rchar as i64); - self.wchar.set(io.wchar as i64); - self.syscr.set(io.syscr as i64); - self.syscw.set(io.syscw as i64); - self.read_bytes.set(io.read_bytes as i64); - self.write_bytes.set(io.write_bytes as i64); - self.cancelled_write_bytes - .set(io.cancelled_write_bytes as i64); - - // Update stats metrics - if let Ok(stat) = process.stat() { - // CPU time in seconds (user + system). - // Kernel tick counters fit in u64; sum of two won't overflow for any real process. - // Seconds as i64 covers ~292 billion years of CPU time. - #[allow(clippy::arithmetic_side_effects, clippy::cast_possible_truncation)] - let cpu_time = (stat.utime + stat.stime) as f64 / procfs::ticks_per_second() as f64; - #[allow(clippy::cast_possible_truncation)] - self.process_cpu_seconds_total.set(cpu_time as i64); - - // Number of threads - self.process_threads.set(stat.num_threads); - } - - // Get open file descriptors count - if let Ok(fds) = process.fd() { - let fd_count = fds.count(); - self.process_open_fds.set(fd_count as i64); - } - } -} - -#[cfg(target_os = "linux")] -impl Default for IoMetricsInner { - fn default() -> Self { - Self::new() - } -} - -impl Inner { - /// Create a new `Inner` struct - pub fn new() -> Self { - Self { - #[cfg(not(target_env = "msvc"))] - jemalloc: JemallocMetricsInner::new(), - #[cfg(target_os = "linux")] - io: IoMetricsInner::new(), - } - } -} - -impl Default for Inner { - fn default() -> Self { - Self::new() - } -} - -impl ProcessMetrics { - /// Create a new `ProcessMetrics` struct - pub fn new() -> Self { - Self(Arc::new(Inner::new())) - } - - /// Register the metrics with the given registry - #[allow(unused_variables)] - pub fn register(registry: &SharedRegistry) -> Self { - let metrics = Self::new(); - - #[cfg(not(target_env = "msvc"))] - metrics.jemalloc.register(registry); - - #[cfg(target_os = "linux")] - metrics.io.register(registry); - - metrics - } - - /// Update all metrics - pub fn update_all_metrics(&self) { - #[cfg(not(target_env = "msvc"))] - self.jemalloc.update(); - #[cfg(target_os = "linux")] - self.io.update(); - } -} - -impl Default for ProcessMetrics { - fn default() -> Self { - Self::new() - } -} - -#[cfg(test)] -mod tests { - use super::*; - use malachitebft_app_channel::app::metrics::SharedRegistry; - - /// Test that ProcessMetrics can be created using the new() method - #[test] - fn test_process_metrics_new() { - let metrics = ProcessMetrics::new(); - // Test that we can call methods on it without panicking - metrics.update_all_metrics(); - } - - /// Test that ProcessMetrics can be created using the default() method - #[test] - fn test_process_metrics_default() { - let metrics = ProcessMetrics::default(); - // Test that we can call methods on it without panicking - metrics.update_all_metrics(); - } - - /// Test that ProcessMetrics can be cloned - #[test] - fn test_process_metrics_clone() { - let metrics1 = ProcessMetrics::new(); - let metrics2 = metrics1.clone(); - - // Both should be usable - metrics1.update_all_metrics(); - metrics2.update_all_metrics(); - } - - /// Test that ProcessMetrics implements Debug - #[test] - fn test_process_metrics_debug() { - let metrics = ProcessMetrics::new(); - let debug_str = format!("{:?}", metrics); - assert!(debug_str.contains("ProcessMetrics")); - } - - /// Test that ProcessMetrics can be registered with a registry - #[test] - fn test_process_metrics_register() { - // Use the global registry for testing - let registry = SharedRegistry::global().with_moniker("test"); - let metrics = ProcessMetrics::register(®istry); - - // Should not panic and return a valid ProcessMetrics instance - metrics.update_all_metrics(); - } -} diff --git a/crates/malachite-app/src/metrics/validator_set.rs b/crates/malachite-app/src/metrics/validator_set.rs deleted file mode 100644 index 5d6e764..0000000 --- a/crates/malachite-app/src/metrics/validator_set.rs +++ /dev/null @@ -1,73 +0,0 @@ -// Copyright 2026 Circle Internet Group, Inc. All rights reserved. -// -// SPDX-License-Identifier: Apache-2.0 -// -// Licensed under the Apache License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. -// You may obtain a copy of the License at -// -// http://www.apache.org/licenses/LICENSE-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// See the License for the specific language governing permissions and -// limitations under the License. - -//! Validator-set decoding metrics. -//! -//! Registered against the same `SharedRegistry` as `AppMetrics`/`DbMetrics`/`ProcessMetrics` -//! so the counter is exported by the consensus `/metrics` endpoint with the standard -//! `moniker` label. - -use malachitebft_app_channel::app::metrics::prometheus::metrics::counter::Counter; -use malachitebft_app_channel::app::metrics::SharedRegistry; - -/// Metrics for the `abi_decode_validator_set` decoder living in `arc-eth-engine`. -#[derive(Clone, Debug, Default)] -pub struct ValidatorSetMetrics { - /// Active validators skipped because their on-chain public key was malformed. - skipped: Counter, -} - -impl ValidatorSetMetrics { - /// Register the counter under prefix `arc_validator_set`. The exported metric name is - /// `arc_validator_set_skipped_total` (the `_total` suffix is added by `prometheus-client`). - pub fn register(registry: &SharedRegistry) -> Self { - let metrics = Self::default(); - - registry.with_prefix("arc_validator_set", |registry| { - registry.register( - "skipped", - "Active validators skipped during validator-set decoding due to malformed public keys", - metrics.skipped.clone(), - ); - }); - - metrics - } - - /// Install this counter as the global recorder used by `arc_shared::metrics::validator_set`. - /// Idempotent: subsequent calls are dropped. - pub fn install_global(&self) { - let counter = self.skipped.clone(); - arc_shared::metrics::validator_set::set_recorder(Box::new(move || { - counter.inc(); - })); - } -} - -#[cfg(test)] -mod tests { - use super::*; - - #[test] - fn register_and_install_global_does_not_panic() { - let registry = SharedRegistry::global().with_moniker("test"); - let metrics = ValidatorSetMetrics::register(®istry); - metrics.install_global(); - - // Recording through the shared API must not panic once a recorder is installed. - arc_shared::metrics::validator_set::record_skipped_validator(); - } -} diff --git a/crates/malachite-app/src/node.rs b/crates/malachite-app/src/node.rs deleted file mode 100644 index ae51a75..0000000 --- a/crates/malachite-app/src/node.rs +++ /dev/null @@ -1,1011 +0,0 @@ -// Copyright 2025 Circle Internet Group, Inc. All rights reserved. -// -// SPDX-License-Identifier: Apache-2.0 -// -// Licensed under the Apache License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. -// You may obtain a copy of the License at -// -// http://www.apache.org/licenses/LICENSE-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// See the License for the specific language governing permissions and -// limitations under the License. - -//! Node lifecycle management and identity configuration. -//! -//! This module defines the [`App`] struct which orchestrates the startup and shutdown -//! of a consensus node, including: -//! -//! - Loading and configuring node identity (P2P keys and consensus signing) -//! - Opening the persistent store -//! - Starting the consensus engine -//! - Connecting to the execution layer -//! - Spawning the RPC and metrics servers -//! - Handling graceful shutdown on SIGTERM -//! -//! The module also defines identity types ([`P2pIdentity`], [`ConsensusIdentity`], -//! [`NodeIdentity`]) that encapsulate the cryptographic keys and addresses used -//! for network communication and block signing. - -use std::path::PathBuf; -use std::time::Duration; - -use bytesize::ByteSize; -use eyre::Context; -use rand::rngs::OsRng; -use tokio::signal::unix::SignalKind; -use tokio::sync::{mpsc, oneshot}; -use tokio::task::JoinHandle; -use tokio_util::sync::CancellationToken; -use tracing::{error, info, warn}; - -use malachitebft_app_channel::app::events::TxEvent; -use malachitebft_app_channel::app::metrics::SharedRegistry; -use malachitebft_app_channel::app::types::Keypair; -use malachitebft_app_channel::{ - Channels, ConsensusContext, EngineHandle, NetworkContext, NetworkIdentity, RequestContext, - SyncContext, WalContext, -}; - -use malachitebft_app_channel::app::config::{GossipSubConfig, PubSubProtocol}; - -use arc_consensus_types::codec::{network::NetCodec, wal::WalCodec}; -use arc_consensus_types::signing::PublicKey; -use arc_consensus_types::{Address, ArcContext, ChainId, Config, ConsensusSpec, SigningConfig}; -use arc_eth_engine::engine::Engine; -use arc_eth_engine::json_structures::ExecutionBlock; -use arc_node_consensus_cli::metrics; -use arc_signer::local::{LocalSigningProvider, PrivateKey}; -use arc_signer::ArcSigningProvider; - -use crate::env_config::EnvConfig; -use crate::hardcoded_config::{GossipLoad, GossipMeshParams}; -use crate::metrics::{AppMetrics, DbMetrics, ProcessMetrics, ValidatorSetMetrics}; -use crate::request::AppRequest; -use crate::state::State; -use crate::store::Store; -use crate::utils::HaltAndWait; - -pub use crate::config::StartConfig; -use crate::utils::pretty::Pretty; - -const APP_REQUEST_CHANNEL_SIZE: usize = 64; -/// At most 5 different clients sending requests concurrently -/// Note that ConsensusRequest::dump_state blocks until the response is sent back -const CONSENSUS_REQUEST_CHANNEL_SIZE: usize = 5; - -/// Main application struct implementing the consensus node functionality -pub struct App { - /// The configuration for the node - config: Config, - /// The home directory for the node - home_dir: PathBuf, - /// The path to the private key file - private_key_file: PathBuf, - /// The configuration for the start - start_config: StartConfig, - /// Metrics registry - registry: SharedRegistry, -} - -/// Handle for the application. -pub struct Handle { - pub app: JoinHandle>, - pub rpc: Option>, - pub engine: EngineHandle, - pub store: Store, - pub store_monitor: JoinHandle<()>, - pub tx_event: TxEvent, - pub cancel_token: CancellationToken, - /// Fires when the EL IPC watchdog triggered shutdown (as opposed to SIGTERM or normal halt). - el_watchdog_triggered: oneshot::Receiver<()>, - /// Kept alive to prevent the app request channel from closing when RPC is disabled. - _tx_app_req: mpsc::Sender, -} - -#[derive(Clone)] -pub struct P2pIdentity { - pub keypair: Keypair, -} - -impl P2pIdentity { - pub fn new(private_key: &PrivateKey) -> Self { - let keypair = Keypair::ed25519_from_bytes(private_key.inner().to_bytes()) - .expect("valid ed25519 key bytes"); - Self { keypair } - } -} - -#[derive(Clone)] -pub struct ConsensusIdentity { - address: Address, - public_key: PublicKey, - signing_provider: ArcSigningProvider, -} - -impl ConsensusIdentity { - pub fn new( - address: Address, - public_key: PublicKey, - signing_provider: ArcSigningProvider, - ) -> Self { - Self { - address, - public_key, - signing_provider, - } - } - - pub fn address(&self) -> Address { - self.address - } - - pub fn public_key(&self) -> &PublicKey { - &self.public_key - } - - pub fn signing_provider(&self) -> &ArcSigningProvider { - &self.signing_provider - } -} - -impl From for ConsensusContext { - fn from(identity: ConsensusIdentity) -> Self { - ConsensusContext::new_validator( - identity.address, - Box::new(identity.signing_provider.clone()), - Box::new(identity.signing_provider), - ) - } -} - -#[derive(Clone)] -pub struct NodeIdentity { - pub moniker: String, - pub p2p: P2pIdentity, - pub consensus: ConsensusIdentity, -} - -impl NodeIdentity { - pub fn new(moniker: String, p2p: P2pIdentity, consensus: ConsensusIdentity) -> Self { - Self { - moniker, - p2p, - consensus, - } - } -} - -impl App { - pub fn new( - mut config: Config, - home_dir: PathBuf, - private_key_file: PathBuf, - start_config: StartConfig, - ) -> Self { - Self::prepare_config(&mut config, &start_config); - - let registry = SharedRegistry::global().with_moniker(&config.moniker); - - Self { - config, - home_dir, - private_key_file, - start_config, - registry, - } - } - - /// Apply CLI gossipsub overrides on top of the existing config. - /// - /// For the CLI path this re-applies overrides that `build_consensus_config` already set, - /// which is harmless (idempotent). The method must exist because the config-file path - /// loads a `Config` from disk and still needs CLI flags merged in. - fn prepare_config(config: &mut Config, start_config: &StartConfig) { - if !start_config.persistent_peers.is_empty() { - config.consensus.p2p.persistent_peers = start_config.persistent_peers.clone(); - } - if start_config.persistent_peers_only { - config.consensus.p2p.persistent_peers_only = true; - } - - let overrides = &start_config.gossipsub_overrides; - if let PubSubProtocol::GossipSub(ref gs) = config.consensus.p2p.protocol { - let needs_override = overrides.explicit_peering - || overrides.mesh_prioritization - || overrides.load != GossipLoad::Average; - - if needs_override { - // Average preserves existing mesh sizes so only peering/prioritization flags - // take effect; Low/High replace mesh sizes with canonical values. - let p = if overrides.load != GossipLoad::Average { - overrides.load.mesh_params() - } else { - GossipMeshParams { - mesh_n: gs.mesh_n(), - mesh_n_high: gs.mesh_n_high(), - mesh_n_low: gs.mesh_n_low(), - mesh_outbound_min: gs.mesh_outbound_min(), - } - }; - - config.consensus.p2p.protocol = PubSubProtocol::GossipSub(GossipSubConfig::new( - p.mesh_n, - p.mesh_n_high, - p.mesh_n_low, - p.mesh_outbound_min, - overrides.mesh_prioritization || gs.enable_peer_scoring(), - overrides.explicit_peering || gs.enable_explicit_peering(), - gs.enable_flood_publish(), - )); - } - } - } - - fn load_private_key(&self) -> eyre::Result { - let private_key = std::fs::read_to_string(&self.private_key_file)?; - serde_json::from_str(&private_key).map_err(|e| e.into()) - } - - async fn setup_node_identity(&self) -> eyre::Result { - // In RPC sync mode, use ephemeral keys since we don't sign anything - // and don't participate in P2P networking - if self.start_config.is_rpc_sync_mode() { - return Ok(self.setup_ephemeral_identity()); - } - - let p2p_identity = self.p2p_identity()?; - - let consensus_identity = if self.start_config.validator { - self.consensus_identity().await? - } else { - self.ephemeral_consensus_identity() - }; - - Ok(NodeIdentity::new( - self.config.moniker.clone(), - p2p_identity, - consensus_identity, - )) - } - - /// Generate ephemeral identity for RPC sync mode - /// - /// In RPC sync mode, we don't need real keys because: - /// - No P2P: We fetch blocks via HTTP RPC, not libp2p - /// - No signing: We're not a validator, so we never sign proposals/votes - /// - Verification only: The signing provider is only used to verify - /// signatures from validators (using their public keys, not ours) - fn setup_ephemeral_identity(&self) -> NodeIdentity { - // Generate random private key for P2P identity - // (not actually used since we don't connect to P2P network) - let p2p_key = PrivateKey::generate(OsRng); - let p2p_identity = P2pIdentity::new(&p2p_key); - - // Generate random private key for consensus identity - // (signing methods will never be called since we're not a validator) - let consensus_key = PrivateKey::generate(OsRng); - let local_provider = LocalSigningProvider::new(consensus_key); - let public_key = local_provider.public_key(); - let address = Address::from_public_key(&public_key); - - info!( - %address, - "Using ephemeral identity for RPC sync mode (no signing will occur)" - ); - - let consensus_identity = ConsensusIdentity::new( - address, - public_key, - ArcSigningProvider::Local(local_provider), - ); - - NodeIdentity::new( - self.config.moniker.clone(), - p2p_identity, - consensus_identity, - ) - } - - /// Generate an ephemeral consensus identity for full nodes. - /// - /// Full nodes participate in gossip but do not sign votes or proposals, - /// so they do not need a persistent consensus key. - fn ephemeral_consensus_identity(&self) -> ConsensusIdentity { - let consensus_key = PrivateKey::generate(OsRng); - let local_provider = LocalSigningProvider::new(consensus_key); - let public_key = local_provider.public_key(); - let address = Address::from_public_key(&public_key); - - info!( - %address, - "Using ephemeral consensus identity for full node (no signing will occur)" - ); - - ConsensusIdentity::new( - address, - public_key, - ArcSigningProvider::Local(local_provider), - ) - } - - fn p2p_identity(&self) -> eyre::Result { - let private_key = self.load_private_key()?; - Ok(P2pIdentity::new(&private_key)) - } - - async fn consensus_identity(&self) -> eyre::Result { - use arc_signer::local::LocalSigningProvider; - use arc_signer::remote::{RemoteSigningConfig, RemoteSigningProvider}; - - match &self.config.signing { - SigningConfig::Local => { - info!("Using local signing provider"); - - let private_key = self.load_private_key()?; - let local_provider = LocalSigningProvider::new(private_key); - let public_key = local_provider.public_key(); - let address = Address::from_public_key(&public_key); - - info!(%address, public_key = %Pretty(&public_key), "Loaded local signer identity"); - - Ok(ConsensusIdentity::new( - address, - public_key, - ArcSigningProvider::Local(local_provider), - )) - } - SigningConfig::Remote(cfg) => { - info!(endpoint = %cfg.endpoint, "Using remote signing provider"); - - let config = RemoteSigningConfig::from(cfg.clone()); - let remote_provider = RemoteSigningProvider::new(config) - .await - .wrap_err("Failed to create remote signing provider")?; - - self.registry.with_prefix("arc_remote_signer", |registry| { - remote_provider.metrics().register(registry); - }); - - let public_key = remote_provider - .public_key() - .await - .wrap_err("Failed to get public key from remote signer")?; - - let address = Address::from_public_key(&public_key); - - info!(%address, public_key = %Pretty(&public_key), "Loaded remote signer identity"); - - Ok(ConsensusIdentity::new( - address, - public_key, - ArcSigningProvider::Remote(remote_provider), - )) - } - } - } - - fn setup_metrics(&self) -> (AppMetrics, DbMetrics, ProcessMetrics) { - let app_metrics = AppMetrics::register(&self.registry); - let db_metrics = DbMetrics::register(&self.registry); - let process_metrics = ProcessMetrics::register(&self.registry); - - // Wire the global recorder for `arc_validator_set_skipped_total` so the counter - // emitted from `abi_decode_validator_set` lands on the consensus `/metrics` endpoint. - ValidatorSetMetrics::register(&self.registry).install_global(); - - (app_metrics, db_metrics, process_metrics) - } - - fn spawn_metrics_server(&self, process_metrics: ProcessMetrics) { - if self.config.metrics.enabled { - tokio::spawn(metrics::serve(self.config.metrics.listen_addr)); - - tokio::spawn(async move { - loop { - process_metrics.update_all_metrics(); - tokio::time::sleep(Duration::from_secs(1)).await; - } - }); - } - } - - async fn open_store( - &self, - db_metrics: DbMetrics, - cache_size: ByteSize, - ) -> eyre::Result<(Store, JoinHandle<()>)> { - let db_path = self.home_dir.join("store.db"); - - info!(path = %db_path.display(), "Opening database"); - - let store = Store::open( - &db_path, - db_metrics, - self.start_config.db_upgrade(), - cache_size, - ) - .await - .wrap_err(format!("Failed to open database at {}", db_path.display()))?; - - info!("Database opened"); - - let store_monitor = store.spawn_monitor(Duration::from_secs(30)); - - Ok((store, store_monitor)) - } - - fn override_config_from_env(&mut self, env_config: &EnvConfig) { - if let Some(interval) = env_config.status_update_interval { - self.config.value_sync.status_update_interval = interval; - } - } - - /// Must run after `resolve_chain_identity` and before the engine is started, - /// so the overrides are observed when `self.config` is cloned into the engine. - fn apply_chain_specific_config(&mut self, chain_id: ChainId) { - let protocol_names = crate::hardcoded_config::consensus::p2p::protocol_names(chain_id); - info!( - ?chain_id, - consensus = %protocol_names.consensus, - sync = %protocol_names.sync, - "Applying chain-specific libp2p protocol names", - ); - self.config.consensus.p2p.protocol_names = protocol_names; - } - - fn apply_state_overrides(&self, state: &mut State) { - if let Some(suggested_fee_recipient) = self.start_config.suggested_fee_recipient { - state.set_suggested_fee_recipient(suggested_fee_recipient); - } - } - - async fn start_consensus_engine( - &self, - ctx: ArcContext, - identity: NodeIdentity, - ) -> eyre::Result<(Channels, EngineHandle)> { - let wal_path = self.home_dir.join("wal").join("consensus.wal"); - - if self.start_config.is_rpc_sync_mode() { - // Use EngineBuilder with custom Network and Sync actors for RPC sync mode - // Streaming will start when Sync actor receives first StartedHeight from Consensus - self.start_rpc_sync_engine(ctx, identity, wal_path).await - } else { - let network_identity = if self.start_config.validator { - self.create_network_identity_with_proof(&identity) - .await - .wrap_err("Failed to create network identity with validator proof")? - } else { - NetworkIdentity::new(identity.moniker.clone(), identity.p2p.keypair.clone(), None) - }; - - let (channels, engine_handle) = malachitebft_app_channel::start_engine( - ctx, - self.config.clone(), - WalContext::new(wal_path, WalCodec), - NetworkContext::new(network_identity, NetCodec), - ConsensusContext::from(identity.consensus), - SyncContext::new(NetCodec), - RequestContext::new(CONSENSUS_REQUEST_CHANNEL_SIZE), - ) - .await - .wrap_err("Failed to start consensus engine")?; - - Ok((channels, engine_handle)) - } - } - - /// Create a NetworkIdentity with a signed validator proof. - /// - /// The validator proof binds the consensus public key to the libp2p peer ID, - /// proving that the validator controls both keys. - async fn create_network_identity_with_proof( - &self, - identity: &NodeIdentity, - ) -> eyre::Result { - let public_key_bytes = identity.consensus.public_key().as_bytes().to_vec(); - let peer_id_bytes = identity.p2p.keypair.public().to_peer_id().to_bytes(); - let address = identity.consensus.address().to_string(); - - let proof_bytes = crate::validator_proof::create_validator_proof( - identity.consensus.signing_provider(), - public_key_bytes, - peer_id_bytes, - &address, - ) - .await?; - - Ok(NetworkIdentity::new_validator( - identity.moniker.clone(), - identity.p2p.keypair.clone(), - address, - proof_bytes, - )) - } - - /// Start the consensus engine with RPC sync mode - /// - /// Uses custom Network and default Sync actors: - /// - Network actor manages WebSocket subscriptions and handles fetch requests - /// - Sync actor coordinates sync state - async fn start_rpc_sync_engine( - &self, - ctx: ArcContext, - identity: NodeIdentity, - wal_path: std::path::PathBuf, - ) -> eyre::Result<(Channels, EngineHandle)> { - use malachitebft_app_channel::EngineBuilder; - - info!("Starting consensus engine with RPC sync mode"); - - // Spawn Network actor - manages WebSocket subscriptions and RPC fetching - let (network_ref, network_tx) = - crate::rpc_sync::spawn_rpc_network_actor(self.start_config.rpc_sync_endpoints.clone()) - .await?; - - // Build engine with custom network actor and default sync actor - // The network actor handles WebSocket subscriptions and sends `NetworkEvent::Status` - // It fetches blocks and certificates via RPC, sends `NetworkEvent::SyncResponse` - let (channels, engine_handle) = EngineBuilder::new(ctx, self.config.clone()) - .with_default_wal(WalContext::new(wal_path, WalCodec)) - .with_custom_network(network_ref, network_tx) - .with_default_sync(SyncContext::new(NetCodec)) - .with_default_consensus(ConsensusContext::from(identity.consensus)) - .with_default_request(RequestContext::new(CONSENSUS_REQUEST_CHANNEL_SIZE)) - .build() - .await - .wrap_err("Failed to start consensus engine with RPC sync")?; - - Ok((channels, engine_handle)) - } - - fn start_rpc_server( - &self, - channels: &Channels, - ) -> ( - mpsc::Sender, - mpsc::Receiver, - Option>, - ) { - let (tx_rpc_req, rx_app_req) = mpsc::channel(APP_REQUEST_CHANNEL_SIZE); - - let rpc_handle = if self.config.rpc.enabled { - let join_handle = tokio::spawn({ - let listen_addr = self.config.rpc.listen_addr; - let request_handle = channels.requests.clone(); - let net_request_handle = channels.net_requests.clone(); - crate::rpc::serve( - listen_addr, - request_handle, - tx_rpc_req.clone(), - net_request_handle, - ) - }); - Some(join_handle) - } else { - None - }; - - (tx_rpc_req, rx_app_req, rpc_handle) - } - - async fn connect_to_execution_engine(&self) -> eyre::Result { - match self.start_config.engine_config() { - Some(engine_config) => engine_config - .connect() - .await - .wrap_err("Failed to connect to execution engine"), - None => Err(eyre::eyre!( - "No engine configuration provided. Please specify either IPC sockets or RPC endpoints." - )), - } - } - - /// Query the execution engine to retrieve the chain ID and genesis block. - /// - /// These are used during node startup to compute the initial network ID - /// and to configure the consensus state with the genesis block's hash and timestamp. - async fn resolve_chain_identity( - &self, - engine: &Engine, - ) -> eyre::Result<(ChainId, ExecutionBlock)> { - let eth_chain_id = engine - .eth - .get_chain_id() - .await - .wrap_err("Failed to get chain ID from execution engine")?; - - let chain_id: ChainId = eth_chain_id - .parse() - .wrap_err("Invalid chain ID from execution engine")?; - - let genesis_block = engine - .eth - .get_genesis_block() - .await - .wrap_err("Failed to get genesis block from execution engine")?; - - Ok((chain_id, genesis_block)) - } - - #[tracing::instrument(name = "node", skip_all, fields(moniker = %self.config.moniker))] - pub async fn start(&mut self) -> eyre::Result { - let ctx = ArcContext::new(); - - // Read environment-based configuration once at startup - let env_config = EnvConfig::from_env(); - - // Apply config overrides from environment variables - self.override_config_from_env(&env_config); - - // Setup node identity, uses ephemeral keys in follow mode (RPC sync mode)) - let identity = self.setup_node_identity().await?; - - // Setup metrics - let (app_metrics, db_metrics, process_metrics) = self.setup_metrics(); - - // Open the store - let (store, store_monitor) = self - .open_store(db_metrics, env_config.db_cache_size) - .await?; - - // Connect to the execution engine early so we can resolve consensus spec and genesis hash - let engine = self.connect_to_execution_engine().await?; - - let (chain_id, genesis_block) = self - .resolve_chain_identity(&engine) - .await - .wrap_err("Failed to resolve chain identity from execution engine")?; - - self.apply_chain_specific_config(chain_id); - - let consensus_spec = ConsensusSpec::from(chain_id); - - // Resolve default follow endpoints when --follow is used without explicit endpoints - self.start_config - .resolve_default_rpc_sync_endpoints(chain_id.as_u64()) - .wrap_err("Failed to resolve default follow endpoints")?; - - // Configure Engine API version selection (V4 vs V5) based on the chainspec. - // When ARC_GENESIS_FILE_PATH is set, parse the same genesis.json the EL uses - // so that patched hardfork timestamps (e.g. nightly-upgrade) are picked up - // correctly. Otherwise fall back to the static chainspec for the chain ID. - if let Some(ref genesis_path) = env_config.genesis_file_path { - engine - .set_osaka_from_genesis_file(genesis_path) - .wrap_err("Failed to configure Osaka activation from genesis file")?; - } else { - engine.set_osaka_from_chain_id(chain_id.as_u64()); - } - - info!( - %chain_id, - genesis_hash = %genesis_block.block_hash, - genesis_timestamp = genesis_block.timestamp, - "Resolved chain identity from execution engine" - ); - - // Initialize the application state with the resolved spec and genesis block - let mut state = State::builder(ctx) - .identity(identity.consensus.clone()) - .store(store.clone()) - .config(self.config.clone()) - .env_config(env_config) - .spec(consensus_spec) - .genesis_block(genesis_block) - .metrics(app_metrics) - .build(); - - // Apply any state overrides from the start configuration (e.g. suggested fee recipient) - self.apply_state_overrides(&mut state); - - // Spawn the metrics server - self.spawn_metrics_server(process_metrics); - - // Start the consensus engine - let (channels, engine_handle) = self.start_consensus_engine(ctx, identity).await?; - - // Start the application RPC server - let (tx_app_req, rx_app_req, rpc_handle) = self.start_rpc_server(&channels); - - let tx_event = channels.events.clone(); - let cancel_token = CancellationToken::new(); - - // Watchdog: cancel the app task if the EL IPC connection closes unexpectedly. - // run() will detect the signal and return an error, letting the tokio runtime - // unwind naturally (running all Drop implementations) instead of process::exit. - let engine_for_watchdog = engine.clone(); - let (el_watchdog_tx, el_watchdog_rx) = oneshot::channel::<()>(); - tokio::spawn({ - let cancel_token = cancel_token.clone(); - async move { - tokio::select! { - _ = engine_for_watchdog.wait_for_disconnect() => { - tracing::error!("EL IPC connection closed; shutting down"); - // Send before cancel so the oneshot is filled before the app task - // can observe cancellation and exit, eliminating a try_recv race. - el_watchdog_tx.send(()).ok(); - cancel_token.cancel(); - } - _ = cancel_token.cancelled() => {} - } - } - }); - - // Start the application task - let app_handle = tokio::spawn({ - let cancel_token = cancel_token.clone(); - crate::app::run(state, channels, engine, rx_app_req, cancel_token) - }); - - // Start the pprof server if enabled - if let Some(pprof_bind_address) = self.start_config.pprof_bind_address { - spawn_pprof_server(pprof_bind_address, self.start_config.pprof_heap_prof); - } - - Ok(Handle { - app: app_handle, - rpc: rpc_handle, - engine: engine_handle, - store_monitor, - tx_event, - store, - cancel_token, - el_watchdog_triggered: el_watchdog_rx, - _tx_app_req: tx_app_req, - }) - } - - pub async fn run(mut self) -> eyre::Result<()> { - if self.start_config.is_rpc_sync_mode() { - info!("Running in RPC sync mode"); - } - - // Start the application - let mut handles = match self.start().await { - Ok(handles) => handles, - Err(e) => { - let startup_error = e.wrap_err("Node failed to start"); - error!("{startup_error:?}"); - error!("Manual intervention required! Waiting for termination signal (SIGTERM)..."); - - // Wait for SIGTERM to allow graceful shutdown - wait_for_termination().await; - - return Err(startup_error); - } - }; - - // Install SIGTERM handler for graceful shutdown - install_sigterm_handler(&handles); - - // Wait for the application to finish - let result = handles.app.await?; - - // If the EL IPC watchdog triggered the shutdown, propagate an error so the - // caller (main) exits with a non-zero code. The tokio runtime unwinds naturally - // after run() returns, running all Drop implementations — no process::exit needed. - if handles.el_watchdog_triggered.try_recv().is_ok() { - return Err(eyre::eyre!("EL IPC connection closed unexpectedly")); - } - - if let Err(e) = &result { - // If the application halted due to reaching a configured height, - // we stop the consensus engine and wait indefinitely for a termination signal. - if e.downcast_ref::().is_some() { - warn!("Node halted, stopping consensus..."); - - // Stop the consensus engine - handles - .engine - .actor - .stop_and_wait(Some("Node halted at configured height".to_string()), None) - .await?; - - // Create a database savepoint ensuring no repair is needed on restart - handles.store.savepoint(); - - info!("Node stopped, waiting for termination signal..."); - tokio::time::sleep(Duration::MAX).await; - } - } - - result - } -} - -/// Install a SIGTERM handler to gracefully shutdown the node -/// -/// ## Note -/// This is only available on Unix systems. -#[cfg(unix)] -fn install_sigterm_handler(handle: &Handle) { - use tokio::signal::unix::signal; - use tokio::time::sleep; - - let node = handle.engine.actor.clone(); - let store = handle.store.clone(); - let cancel_token = handle.cancel_token.clone(); - - let mut sigterm = signal(SignalKind::terminate()).expect("inside Tokio runtime"); - - tokio::spawn(async move { - // Wait for the SIGTERM signal - sigterm.recv().await; - - warn!("Received SIGTERM, shutting down..."); - - // Trigger cancellation of the application - cancel_token.cancel(); - - // Give some time to the application to process the cancellation - sleep(Duration::from_millis(500)).await; - - // Stop the consensus engine - if let Err(e) = node - .stop_and_wait(Some("Received SIGTERM signal".to_string()), None) - .await - { - warn!(%e, "Failed to stop the node gracefully"); - } - - // Create a database savepoint ensuring no repair is needed on restart - store.savepoint(); - - info!("Waiting for all tasks to finish..."); - sleep(Duration::from_millis(500)).await; - info!("Shutdown complete, exiting"); - - // In Kubernetes signals, exit code 143 means that a container - // was terminated by receiving a SIGTERM signal - std::process::exit(143); - }); -} - -#[cfg(not(unix))] -fn install_sigterm_handler(_handle: &Handle) {} - -/// Wait for a termination signal (SIGTERM on Unix) -async fn wait_for_termination() { - #[cfg(unix)] - { - use tokio::signal::unix::{signal, SignalKind}; - let mut sigterm = signal(SignalKind::terminate()).expect("inside Tokio runtime"); - sigterm.recv().await; - } - - #[cfg(not(unix))] - { - // On non-Unix systems, we simply wait indefinitely - futures::future::pending::<()>().await; - } -} - -#[cfg(feature = "pprof")] -fn spawn_pprof_server(bind_address: std::net::SocketAddr, heap_prof: bool) { - if heap_prof { - // SAFETY: writing a bool to a well-known jemalloc mallctl key. - if let Err(e) = unsafe { tikv_jemalloc_ctl::raw::write(b"prof.active\0", true) } { - tracing::error!(error = %e, "failed to activate jemalloc heap profiling; /debug/pprof/allocs will return empty profiles"); - } else { - tracing::info!("jemalloc heap profiling activated"); - } - } - - tokio::spawn(async move { - if let Err(e) = - pprof_hyper_server::serve(bind_address, pprof_hyper_server::Config::default()).await - { - tracing::error!(error = %e, "pprof server failed to start"); - } - }); -} - -#[cfg(not(feature = "pprof"))] -fn spawn_pprof_server(_bind_address: std::net::SocketAddr, _heap_prof: bool) {} - -#[cfg(test)] -mod tests { - use super::*; - use tempfile::tempdir; - - #[tokio::test] - async fn start_requires_execution_engine() { - let tmp = tempfile::tempdir().unwrap(); - let start_config = StartConfig { - rpc_sync_enabled: true, - rpc_sync_endpoints: vec!["http://unused:8545".parse().unwrap()], - ..Default::default() - }; - let mut app = App::new( - Config::default(), - tmp.path().to_path_buf(), - tmp.path().join("unused.key"), - start_config, - ); - match app.start().await { - Err(err) => assert!( - format!("{err:?}").contains("engine"), - "unexpected error: {err:?}" - ), - Ok(_) => panic!("should fail without execution engine"), - } - } - - fn write_key_file(dir: &std::path::Path) -> (PathBuf, PrivateKey) { - let key = PrivateKey::generate(OsRng); - let path = dir.join("priv_validator_key.json"); - let json = serde_json::to_string(&key).expect("serialize key"); - std::fs::write(&path, json).expect("write key file"); - (path, key) - } - - fn test_app(private_key_file: PathBuf, validator: bool) -> App { - let config = Config { - moniker: "test-node".to_string(), - ..Default::default() - }; - let start_config = StartConfig { - validator, - ..Default::default() - }; - App::new( - config, - PathBuf::from("/tmp"), - private_key_file, - start_config, - ) - } - - #[tokio::test] - async fn full_node_uses_ephemeral_consensus_identity() { - let dir = tempdir().unwrap(); - let (key_path, original_key) = write_key_file(dir.path()); - - let app = test_app(key_path, false); - let identity = app.setup_node_identity().await.unwrap(); - - // P2P identity uses the key from file - let expected_keypair = - Keypair::ed25519_from_bytes(original_key.inner().to_bytes()).unwrap(); - assert_eq!( - identity.p2p.keypair.public().to_peer_id(), - expected_keypair.public().to_peer_id(), - ); - - // Consensus identity is ephemeral (address differs from the file key) - let file_provider = LocalSigningProvider::new(original_key); - let file_address = Address::from_public_key(&file_provider.public_key()); - assert_ne!(identity.consensus.address(), file_address); - } - - #[tokio::test] - async fn validator_loads_consensus_identity_from_key_file() { - let dir = tempdir().unwrap(); - let (key_path, original_key) = write_key_file(dir.path()); - let app = test_app(key_path, true); - let identity = app.setup_node_identity().await.unwrap(); - - // Both P2P and consensus derive from the same key file - let file_provider = LocalSigningProvider::new(original_key); - let file_address = Address::from_public_key(&file_provider.public_key()); - assert_eq!(identity.consensus.address(), file_address); - } - - #[test] - fn ephemeral_consensus_identity_generates_valid_identity() { - let dir = tempdir().unwrap(); - let (key_path, _) = write_key_file(dir.path()); - - let app = test_app(key_path, false); - let id1 = app.ephemeral_consensus_identity(); - let id2 = app.ephemeral_consensus_identity(); - - // Each call produces a different address - assert_ne!(id1.address(), id2.address()); - } -} diff --git a/crates/malachite-app/src/payload.rs b/crates/malachite-app/src/payload.rs deleted file mode 100644 index 2a4df9a..0000000 --- a/crates/malachite-app/src/payload.rs +++ /dev/null @@ -1,800 +0,0 @@ -// Copyright 2025 Circle Internet Group, Inc. All rights reserved. -// -// SPDX-License-Identifier: Apache-2.0 -// -// Licensed under the Apache License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. -// You may obtain a copy of the License at -// -// http://www.apache.org/licenses/LICENSE-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// See the License for the specific language governing permissions and -// limitations under the License. - -use std::time::Duration; - -use backon::{BackoffBuilder, ConstantBuilder, Retryable}; -use tracing::{error, warn}; - -use malachitebft_app_channel::app::types::core::Validity; - -use alloy_rpc_types_engine::{ExecutionPayloadV3, PayloadStatusEnum}; - -use arc_consensus_types::Address; -use arc_eth_engine::engine::Engine; -use arc_eth_engine::json_structures::ExecutionBlock; -use arc_eth_engine::rpc::EngineApiRpcError; - -use crate::block::ConsensusBlock; -use crate::metrics::app::{AppMetrics, InvalidPayloadSource}; -use crate::store::repositories::InvalidPayloadsRepository; -use arc_consensus_db::invalid_payloads::InvalidPayload; - -pub async fn generate_payload_with_retry( - previous_block: &ExecutionBlock, - fee_recipient: &Address, - generator: &impl PayloadGenerator, - metrics: &AppMetrics, -) -> eyre::Result { - const MAX_RETRIES: usize = 5; - const RETRY_POLICY: ConstantBuilder = ConstantBuilder::new() - .with_delay(Duration::from_millis(100)) - .with_max_times(MAX_RETRIES); - - let call_once = || async { - // Ensure timestamp is non-decreasing by setting it to max(previous_block.timestamp, now()) - // This allows us to continue making progress, proposing blocks that have - // the same block timestamp as the "jumped" block until enough time has elapsed such - // that we can continue making progress with advancing timestamps. - let now = Engine::timestamp_now(); - let timestamp = std::cmp::max(previous_block.timestamp, now); - - if previous_block.timestamp > now { - // timestamp >= now (since max chose previous_block.timestamp > now) - let skew = timestamp.saturating_sub(now); - warn!( - timestamp = timestamp, - skew = skew, - "Clock skew detected: using parent timestamp", - ); - } - - let _guard = metrics.start_engine_api_timer("generate_block"); - - generator - .generate_block(previous_block, timestamp, fee_recipient) - .await - }; - - let mut attempt_num = 0usize; - - call_once - .retry(RETRY_POLICY.build()) - .sleep(tokio::time::sleep) // give reth time to breathe - .notify(|_e, dur| { - // Bounded by MAX_RETRIES (5) - #[allow(clippy::arithmetic_side_effects)] - { - attempt_num += 1; - } - let attempts_left = MAX_RETRIES.saturating_sub(attempt_num); - error!( - attempt = attempt_num, - attempts_left, - delay_ms = dur.as_millis(), - "reth forgot its payload id; retrying (forking off the same previous block)" - ); - }) - .when(|e| { - EngineApiRpcError::try_from(e) - .map(|err| err.is_unknown_payload()) - .unwrap_or(false) - }) - .await -} - -/// Introduced to improve testability of `generate_payload_with_retry` -#[cfg_attr(test, mockall::automock)] -pub trait PayloadGenerator: Send + Sync { - async fn generate_block( - &self, - parent: &ExecutionBlock, - timestamp: u64, - fee_recipient: &Address, - ) -> eyre::Result; -} - -pub struct EnginePayloadGenerator<'a> { - pub engine: &'a Engine, -} - -impl<'a> PayloadGenerator for EnginePayloadGenerator<'a> { - async fn generate_block( - &self, - parent: &ExecutionBlock, - timestamp: u64, - fee_recipient: &Address, - ) -> eyre::Result { - self.engine - .generate_block(parent, timestamp, fee_recipient) - .await - } -} - -/// Abstraction over execution payload validation. -/// -/// This trait exists so that handler code can validate payloads -/// without depending on the concrete [`Engine`] type, making it -/// possible to substitute a mock in unit tests. -#[cfg_attr(test, mockall::automock)] -pub trait PayloadValidator { - /// Validates an execution payload via the engine. - async fn validate_payload( - &self, - payload: &ExecutionPayloadV3, - ) -> eyre::Result; -} - -impl PayloadValidator for &T -where - T: PayloadValidator + ?Sized, -{ - async fn validate_payload( - &self, - payload: &ExecutionPayloadV3, - ) -> eyre::Result { - (*self).validate_payload(payload).await - } -} - -/// [`PayloadValidator`] backed by a real [`Engine`] instance. -/// -/// Delegates to the module-private [`validate_payload`] function, -/// which sends the payload to the execution client via -/// `engine.newPayload` and interprets the response. -pub struct EnginePayloadValidator<'a> { - engine: &'a Engine, - metrics: &'a AppMetrics, -} - -impl<'a> EnginePayloadValidator<'a> { - pub fn new(engine: &'a Engine, metrics: &'a AppMetrics) -> Self { - Self { engine, metrics } - } -} - -impl PayloadValidator for EnginePayloadValidator<'_> { - async fn validate_payload( - &self, - payload: &ExecutionPayloadV3, - ) -> eyre::Result { - validate_payload(self.engine, payload, self.metrics).await - } -} - -/// Result of validating an execution payload via the engine. -/// -/// Carries the engine's verdict so that callers can act on it (e.g. store the -/// rejection reason) without losing the detail across the call boundary. -#[derive(Debug, Clone, PartialEq, Eq)] -pub enum PayloadValidationResult { - /// The engine accepted the payload. - Valid, - /// The engine rejected the payload for the given reason. - Invalid { reason: String }, -} - -impl std::fmt::Display for PayloadValidationResult { - fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { - match self { - Self::Valid => write!(f, "Valid"), - Self::Invalid { reason } => { - write!(f, "Invalid: {reason}") - } - } - } -} - -/// Validates an execution payload by sending it to the engine via `newPayload`. -/// -/// # Return values -/// -/// - `Ok(Valid)`: the engine accepted the payload, or returned an unexpected status -/// such as `SYNCING` or `ACCEPTED` (logged as a warning). -/// - `Ok(Invalid { reason })`: the engine explicitly rejected the payload, either -/// via its status response (`INVALID`) or via a JSON-RPC error -/// (`EngineApiRpcError`). -/// - `Err(..)`: the engine replied with status `SYNCING` or `ACCEPTED`, or an -/// unrelated internal error occurred in the call stack. -async fn validate_payload( - engine: &Engine, - execution_payload: &ExecutionPayloadV3, - metrics: &AppMetrics, -) -> eyre::Result { - let block_hash = execution_payload.payload_inner.payload_inner.block_hash; - - // EIP-4844 blobs are not supported and not needed for our use case. - // - // Rationale: - // - Blobs are not required for private or public testnet deployments. - // Integration teams don't use them; blobs are typically used by L2s. - // - Proper blob support requires propagating the actual blob data via - // consensus layer gossip mechanisms, which our current malachite-app - // implementation does not handle. - // - Managing blob hashes alone without blob propagation is insufficient - // and would be an incomplete implementation. - // - If blob support becomes necessary in the future, it will require - // a complete design including blob propagation mechanisms. - let versioned_hashes = Vec::new(); - let _guard = metrics.start_engine_api_timer("notify_new_block"); - - match engine - .notify_new_block(execution_payload, versioned_hashes) - .await - { - Ok(status) => match status.status { - PayloadStatusEnum::Valid => Ok(PayloadValidationResult::Valid), - PayloadStatusEnum::Invalid { validation_error } => { - Ok(PayloadValidationResult::Invalid { - reason: validation_error, - }) - } - // The remaining cases are SYNCING and ACCEPTED: - // - SYNCING: we don't expect this in ARC because the CL and EL are kept - // in sync. As a result, the EL should always have the information it - // needs to validate a payload. - // - ACCEPTED: we don't expect to have side chains in ARC, so this status - // should never be returned. - _ => { - let height = execution_payload.payload_inner.payload_inner.block_number; - warn!( - %block_hash, - %height, - "Unexpected payload status: {status:?}", - ); - Err(eyre::eyre!( - "unexpected {status:?} status from engine for block {block_hash} at height {height}" - )) - } - }, - Err(e) => { - if let Ok(engine_api_error) = EngineApiRpcError::try_from(&e) { - // JSON-RPC error here means that the call to - // `engine.newPayload` failed the preliminary structural - // validation of the payload. - // Instead of returning an error and possibly crashing the app, - // we mark the payload as invalid. - error!( - %block_hash, - "Invalid payload: {engine_api_error}", - ); - return Ok(PayloadValidationResult::Invalid { - reason: engine_api_error.to_string(), - }); - } - - // Unrelated internal error in the call stack. - let msg = format!( - "call to EngineAPI::new_payload failed when validating block: {block_hash}", - ); - Err(e.wrap_err(msg)) - } - } -} - -/// Validates a consensus block's payload and stores it in the database -/// if the engine rejects it. -/// -/// This is the higher-level entry point for callers that have a -/// [`ConsensusBlock`] and an [`InvalidPayloadsRepository`]. It delegates -/// to [`PayloadValidator::validate_payload`] for the actual engine call -/// and then persists an [`InvalidPayload`] record when the verdict is -/// `Invalid`. -/// -/// # Return contract -/// -/// - `Ok(Validity::Valid)`: the engine accepted the payload. -/// - `Ok(Validity::Invalid)`: the engine rejected the payload. Persisting -/// the forensic [`InvalidPayload`] record is **best-effort**: a failure -/// to append is logged at `error` but does not change the verdict -/// returned to the caller. The engine's verdict is authoritative and -/// must reach the consensus layer so the corresponding undecided block -/// is marked `Invalid` rather than left with a placeholder `Valid`. -/// - `Err(_)`: no verdict was obtained (engine transport error, -/// `SYNCING`/`ACCEPTED` status, etc.). -pub async fn validate_consensus_block( - payload_validator: &impl PayloadValidator, - block: &ConsensusBlock, - store: &impl InvalidPayloadsRepository, - metrics: &AppMetrics, -) -> eyre::Result { - let result = payload_validator - .validate_payload(&block.execution_payload) - .await?; - - match result { - PayloadValidationResult::Valid => Ok(Validity::Valid), - PayloadValidationResult::Invalid { reason } => { - warn!( - height = %block.height, - round = %block.round, - block_hash = %block.block_hash(), - proposer = %block.proposer, - reason = %reason, - "Engine rejected payload, storing for forensics", - ); - metrics.inc_invalid_payloads_count(InvalidPayloadSource::EngineReject); - let invalid = InvalidPayload::new_from_block(block, &reason); - if let Err(e) = store.append(invalid).await { - error!( - height = %block.height, - round = %block.round, - block_hash = %block.block_hash(), - proposer = %block.proposer, - "Failed to persist invalid-payload forensic record: {e}", - ); - } - Ok(Validity::Invalid) - } - } -} - -#[cfg(test)] -mod tests { - use super::*; - use std::sync::atomic::{AtomicUsize, Ordering}; - - use eyre::eyre; - - use malachitebft_app_channel::app::types::core::Validity; - - use alloy_primitives::{Address as AlloyAddress, Bloom, Bytes as AlloyBytes, U256}; - use alloy_rpc_types_engine::{ - ExecutionPayloadV1, ExecutionPayloadV2, ExecutionPayloadV3, PayloadStatus, - }; - - use arc_consensus_types::{Address, Height, Round, B256}; - use arc_eth_engine::engine::{MockEngineAPI, MockEthereumAPI}; - use arc_eth_engine::json_structures::ExecutionBlock; - - use crate::block::ConsensusBlock; - use crate::metrics::app::AppMetrics; - use crate::store::repositories::mocks::MockInvalidPayloadsRepository; - use arc_consensus_db::invalid_payloads::InvalidPayload; - - fn test_payload(timestamp: u64) -> ExecutionPayloadV3 { - ExecutionPayloadV3 { - payload_inner: ExecutionPayloadV2 { - payload_inner: ExecutionPayloadV1 { - parent_hash: B256::ZERO, - fee_recipient: AlloyAddress::ZERO, - state_root: B256::ZERO, - receipts_root: B256::ZERO, - logs_bloom: Bloom::default(), - prev_randao: B256::ZERO, - block_number: 0, - gas_limit: 0, - gas_used: 0, - timestamp, - extra_data: AlloyBytes::default(), - base_fee_per_gas: U256::from(1u64), - block_hash: B256::ZERO, - transactions: vec![], - }, - withdrawals: vec![], - }, - blob_gas_used: 0, - excess_blob_gas: 0, - } - } - - #[tokio::test] - async fn validate_payload_returns_valid_on_ok_status() { - let mut mock = MockEngineAPI::new(); - mock.expect_new_payload().returning(|_, _, _| { - Ok(PayloadStatus { - status: PayloadStatusEnum::Valid, - latest_valid_hash: None, - }) - }); - - let engine = Engine::new(Box::new(mock), Box::new(MockEthereumAPI::new())); - let payload = test_payload(0); - let metrics = AppMetrics::default(); - - let result = validate_payload(&engine, &payload, &metrics) - .await - .expect("payload validation should succeed"); - - assert_eq!(result, PayloadValidationResult::Valid); - } - - #[tokio::test] - async fn validate_payload_returns_invalid_on_invalid_status() { - let mut mock = MockEngineAPI::new(); - mock.expect_new_payload().returning(|_, _, _| { - Ok(PayloadStatus { - status: PayloadStatusEnum::Invalid { - validation_error: "validation error".to_string(), - }, - latest_valid_hash: None, - }) - }); - - let engine = Engine::new(Box::new(mock), Box::new(MockEthereumAPI::new())); - let payload = test_payload(0); - let metrics = AppMetrics::default(); - - let result = validate_payload(&engine, &payload, &metrics) - .await - .expect("payload validation should succeed"); - - assert_eq!( - result, - PayloadValidationResult::Invalid { - reason: "validation error".to_string(), - }, - ); - } - - #[tokio::test] - async fn validate_payload_returns_invalid_on_rpc_error() { - let mut mock = MockEngineAPI::new(); - mock.expect_new_payload().returning(|_, _, _| { - let rpc_error = EngineApiRpcError::new(42, "engine API error", None); - Err(eyre::Report::new(rpc_error)) - }); - - let engine = Engine::new(Box::new(mock), Box::new(MockEthereumAPI::new())); - let payload = test_payload(0); - let metrics = AppMetrics::default(); - - let result = validate_payload(&engine, &payload, &metrics) - .await - .expect("should succeed without error"); - - match &result { - PayloadValidationResult::Invalid { reason } => { - assert!( - reason.contains("engine API error"), - "reason should contain the RPC error message, got: {reason}", - ); - } - other => { - panic!("expected Invalid, got {other:?}") - } - } - } - - #[tokio::test] - async fn validate_payload_propagates_other_errors() { - let mut mock = MockEngineAPI::new(); - mock.expect_new_payload() - .returning(|_, _, _| Err(eyre::eyre!("some error"))); - - let engine = Engine::new(Box::new(mock), Box::new(MockEthereumAPI::new())); - let payload = test_payload(0); - let metrics = AppMetrics::default(); - - let err = validate_payload(&engine, &payload, &metrics) - .await - .expect_err("payload validation should return an error"); - - let msg = err.to_string(); - assert!( - msg.contains("call to EngineAPI::new_payload failed"), - "error message should describe the failure, got: {msg}", - ); - } - - #[tokio::test] - async fn validate_payload_returns_err_on_unexpected_status() { - let test_cases = [PayloadStatusEnum::Syncing, PayloadStatusEnum::Accepted]; - - for status in test_cases { - let mut mock = MockEngineAPI::new(); - let status_for_mock = status.clone(); - mock.expect_new_payload().returning(move |_, _, _| { - Ok(PayloadStatus { - status: status_for_mock.clone(), - latest_valid_hash: None, - }) - }); - - let engine = Engine::new(Box::new(mock), Box::new(MockEthereumAPI::new())); - let payload = test_payload(0); - let metrics = AppMetrics::default(); - - let result = validate_payload(&engine, &payload, &metrics) - .await - .expect_err("payload validation should return an error"); - - let got_msg = result.to_string(); - let want_status = PayloadStatus { - status, - latest_valid_hash: None, - }; - let want_err_msg = format!( - "unexpected {want_status:?} status from engine for block {} \ - at height {}", - payload.payload_inner.payload_inner.block_hash, - payload.payload_inner.payload_inner.block_number, - ); - assert_eq!(got_msg, want_err_msg); - } - } - - fn test_block() -> ConsensusBlock { - ConsensusBlock { - height: Height::new(1), - round: Round::new(0), - valid_round: Round::Nil, - proposer: Address::new([0u8; 20]), - execution_payload: test_payload(0), - validity: Validity::Valid, - signature: None, - } - } - - #[tokio::test] - async fn validate_consensus_block_returns_valid() { - let mut validator = MockPayloadValidator::new(); - validator - .expect_validate_payload() - .returning(|_| Ok(PayloadValidationResult::Valid)); - - let mut store = MockInvalidPayloadsRepository::new(); - store.expect_append().times(0); - - let metrics = AppMetrics::default(); - let block = test_block(); - let result = validate_consensus_block(&validator, &block, &store, &metrics) - .await - .expect("should succeed"); - - assert_eq!(result, Validity::Valid); - assert_eq!(metrics.get_invalid_payloads_count(), 0); - } - - #[tokio::test] - async fn validate_consensus_block_returns_invalid_and_stores() { - let mut validator = MockPayloadValidator::new(); - validator.expect_validate_payload().returning(|_| { - Ok(PayloadValidationResult::Invalid { - reason: "bad block".into(), - }) - }); - - let mut store = MockInvalidPayloadsRepository::new(); - store - .expect_append() - .times(1) - .withf(|ip: &InvalidPayload| { - ip.height == Height::new(1) - && ip.round == Round::new(0) - && ip.proposer_address == Address::new([0u8; 20]) - && ip.reason == "bad block" - && ip.payload.is_some() - }) - .returning(|_| Ok(())); - - let metrics = AppMetrics::default(); - let block = test_block(); - let result = validate_consensus_block(&validator, &block, &store, &metrics) - .await - .expect("should succeed"); - - assert_eq!(result, Validity::Invalid); - assert_eq!(metrics.get_invalid_payloads_count(), 1); - } - - #[tokio::test] - async fn validate_consensus_block_propagates_validation_error() { - let mut validator = MockPayloadValidator::new(); - validator - .expect_validate_payload() - .returning(|_| Err(eyre!("engine down"))); - - let mut store = MockInvalidPayloadsRepository::new(); - store.expect_append().times(0); - - let metrics = AppMetrics::default(); - let block = test_block(); - let err = validate_consensus_block(&validator, &block, &store, &metrics) - .await - .expect_err("should propagate error"); - - assert!( - err.to_string().contains("engine down"), - "error should contain the original message, \ - got: {err}", - ); - assert_eq!(metrics.get_invalid_payloads_count(), 0); - } - - #[tokio::test] - async fn validate_consensus_block_returns_invalid_when_forensics_persist_fails() { - // When the engine returns Invalid but persisting the forensic record - // fails (e.g. transient DB issue), the engine's verdict is still the - // authoritative answer and must be returned. Otherwise the caller - // (validate_undecided_blocks) treats it as "no verdict obtained" and - // leaves the placeholder `Valid` in undecided_blocks, masking a - // rejected block. - let mut validator = MockPayloadValidator::new(); - validator.expect_validate_payload().returning(|_| { - Ok(PayloadValidationResult::Invalid { - reason: "bad".into(), - }) - }); - - let mut store = MockInvalidPayloadsRepository::new(); - store - .expect_append() - .times(1) - .returning(|_| Err(std::io::Error::other("disk full"))); - - let metrics = AppMetrics::default(); - let block = test_block(); - let validity = validate_consensus_block(&validator, &block, &store, &metrics) - .await - .expect("verdict should be returned even when forensics persist fails"); - - assert_eq!(validity, Validity::Invalid); - assert_eq!(metrics.get_invalid_payloads_count(), 1); - } - - #[derive(Clone, Debug)] - enum Scenario { - Success, - UnknownPayloadUntil { succeed_on: usize }, - OtherError, - } - - struct TestPayloadGenerator { - scenario: Scenario, - attempts: AtomicUsize, - } - - impl TestPayloadGenerator { - fn new(scenario: Scenario) -> Self { - Self { - scenario, - attempts: AtomicUsize::new(0), - } - } - - fn dummy_payload(timestamp: u64) -> ExecutionPayloadV3 { - test_payload(timestamp) - } - } - - impl PayloadGenerator for TestPayloadGenerator { - async fn generate_block( - &self, - _parent: &ExecutionBlock, - timestamp: u64, - _fee_recipient: &Address, - ) -> eyre::Result { - let attempt = self.attempts.fetch_add(1, Ordering::SeqCst) + 1; - match self.scenario { - Scenario::Success => Ok(Self::dummy_payload(timestamp)), - Scenario::UnknownPayloadUntil { succeed_on } => { - if attempt < succeed_on { - Err(EngineApiRpcError::new(-38001, "Unknown payload", None).into()) - } else { - Ok(Self::dummy_payload(timestamp)) - } - } - Scenario::OtherError => Err(eyre!("a different error")), - } - } - } - - fn parent_block(timestamp: u64) -> ExecutionBlock { - ExecutionBlock { - block_hash: B256::ZERO, - block_number: 0, - parent_hash: B256::ZERO, - timestamp, - } - } - - fn fee_recipient() -> Address { - AlloyAddress::ZERO.into() - } - - fn metrics() -> AppMetrics { - AppMetrics::new() - } - - #[tokio::test] - async fn retry_success_first_attempt() { - let generator = TestPayloadGenerator::new(Scenario::Success); - let payload = - generate_payload_with_retry(&parent_block(0), &fee_recipient(), &generator, &metrics()) - .await - .expect("payload generation should succeed on first try"); - - assert_eq!( - generator.attempts.load(Ordering::SeqCst), - 1, - "should only attempt once" - ); - assert!(payload.timestamp() >= parent_block(0).timestamp); - } - - #[tokio::test] - async fn retry_unknown_until_success() { - let succeed_on = 6; // 5 failures + 1 success; limit of max retries - let generator = TestPayloadGenerator::new(Scenario::UnknownPayloadUntil { succeed_on }); - let payload = generate_payload_with_retry( - &parent_block(10), - &fee_recipient(), - &generator, - &metrics(), - ) - .await - .expect("payload should eventually succeed"); - - assert_eq!( - generator.attempts.load(Ordering::SeqCst), - succeed_on, - "attempt count should equal succeed_on" - ); - assert!(payload.timestamp() >= parent_block(10).timestamp); - } - - #[tokio::test] - async fn retry_unknown_too_late() { - let succeed_on = 7; // exceeds max retries - let generator = TestPayloadGenerator::new(Scenario::UnknownPayloadUntil { succeed_on }); - let err = generate_payload_with_retry( - &parent_block(100), - &fee_recipient(), - &generator, - &metrics(), - ) - .await - .expect_err("should fail after exhausting retries"); - - let engine_err = - EngineApiRpcError::try_from(err).expect("error should be EngineApiRpcError"); - assert!( - engine_err.is_unknown_payload(), - "error should be UnknownPayload kind" - ); - assert_eq!( - generator.attempts.load(Ordering::SeqCst), - 6, - "total attempts should be 6 (1 initial + 5 retries)" - ); - } - - #[tokio::test] - async fn retry_immediate_other_error() { - let generator = TestPayloadGenerator::new(Scenario::OtherError); - let err = generate_payload_with_retry( - &parent_block(1000), - &fee_recipient(), - &generator, - &metrics(), - ) - .await - .expect_err("should fail immediately without retry"); - - if let Ok(engine_err) = EngineApiRpcError::try_from(err) { - assert!( - !engine_err.is_unknown_payload(), - "should not classify as UnknownPayload" - ); - } - assert_eq!( - generator.attempts.load(Ordering::SeqCst), - 1, - "should only attempt once" - ); - } -} diff --git a/crates/malachite-app/src/proposal_parts.rs b/crates/malachite-app/src/proposal_parts.rs deleted file mode 100644 index 66849d4..0000000 --- a/crates/malachite-app/src/proposal_parts.rs +++ /dev/null @@ -1,537 +0,0 @@ -// Copyright 2025 Circle Internet Group, Inc. All rights reserved. -// -// SPDX-License-Identifier: Apache-2.0 -// -// Licensed under the Apache License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. -// You may obtain a copy of the License at -// -// http://www.apache.org/licenses/LICENSE-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// See the License for the specific language governing permissions and -// limitations under the License. - -use bytes::Bytes; -use eyre::{eyre, Context as _}; -use sha3::Digest; -use ssz::{Decode, Encode}; -use tokio::sync::mpsc; -use tracing::{info, warn}; - -use crate::streaming::CHUNK_SIZE; - -use malachitebft_app_channel::app::streaming::{StreamContent, StreamId, StreamMessage}; -use malachitebft_app_channel::app::types::core::{Round, Validity}; -use malachitebft_app_channel::NetworkMsg; - -use alloy_rpc_types_engine::ExecutionPayloadV3; -use arc_consensus_types::proposer::ProposerSelector; -use arc_consensus_types::signing::{Signature, SigningError, SigningProvider, VerificationResult}; -use arc_consensus_types::{ - ArcContext, Height, ProposalData, ProposalFin, ProposalInit, ProposalPart, ProposalParts, - Validator, ValidatorSet, -}; - -use crate::block::ConsensusBlock; - -#[cfg_attr(test, mockall::automock(type Error = std::io::Error;))] -pub trait PublishProposalPart { - type Error: std::error::Error + Send + Sync + 'static; - - async fn publish_proposal_part( - &self, - msg: StreamMessage, - ) -> Result<(), Self::Error>; -} - -impl PublishProposalPart for &'_ T -where - T: PublishProposalPart, -{ - type Error = T::Error; - - async fn publish_proposal_part( - &self, - msg: StreamMessage, - ) -> Result<(), Self::Error> { - (*self).publish_proposal_part(msg).await - } -} - -impl PublishProposalPart for mpsc::Sender> { - type Error = mpsc::error::SendError>; - - async fn publish_proposal_part( - &self, - msg: StreamMessage, - ) -> Result<(), Self::Error> { - self.send(NetworkMsg::PublishProposalPart(msg)).await - } -} - -/// Streams the given proposal parts over the network. -pub async fn stream_proposal( - publish: impl PublishProposalPart, - height: Height, - round: Round, - stream_messages: Vec>, -) -> Result<(), eyre::Error> { - for msg in stream_messages { - info!( - %height, %round, stream_id = %msg.stream_id, sequence = %msg.sequence, - "Streaming proposal part: {:?}", msg.content - ); - - publish - .publish_proposal_part(msg) - .await - .wrap_err("Failed to send proposal part to network")?; - } - - Ok(()) -} - -/// Splits the given consensus block into proposal parts and prepares stream messages -/// for each part, along with the signature of the entire proposal. -pub async fn prepare_stream( - stream_id: StreamId, - signing_provider: &impl SigningProvider, - consensus_block: &ConsensusBlock, -) -> eyre::Result<(Vec>, Signature)> { - let (parts, signature) = make_proposal_parts(signing_provider, consensus_block) - .await - .wrap_err("Failed to construct proposal parts")?; - - // +1 for the Fin message; Vec length <= isize::MAX, so +1 cannot overflow usize - #[allow(clippy::arithmetic_side_effects)] - let mut msgs = Vec::with_capacity(parts.len() + 1); - let mut sequence = 0u64; - - for part in parts { - let msg = StreamMessage::new(stream_id.clone(), sequence, StreamContent::Data(part)); - // Bounded by parts.len() which is bounded by MAX_MESSAGES_PER_STREAM - #[allow(clippy::arithmetic_side_effects)] - { - sequence += 1; - } - msgs.push(msg); - } - - msgs.push(StreamMessage::new(stream_id, sequence, StreamContent::Fin)); - - Ok((msgs, signature)) -} - -/// Splits the given consensus block into proposal parts and computes the signature -/// for the entire proposal. -pub async fn make_proposal_parts( - signing_provider: &impl SigningProvider, - block: &ConsensusBlock, -) -> Result<(Vec, Signature), SigningError> { - let mut hasher = sha3::Keccak256::new(); - let mut parts = Vec::new(); - - let data = block.execution_payload.as_ssz_bytes(); - - // Init - { - parts.push(ProposalPart::Init(ProposalInit::new( - block.height, - block.round, - block.valid_round, - block.proposer, - ))); - - hasher.update(block.height.as_u64().to_be_bytes().as_slice()); - hasher.update(block.round.as_i64().to_be_bytes().as_slice()); - } - - // Data - { - for chunk in data.chunks(CHUNK_SIZE) { - let chunk_data = ProposalData::new(Bytes::copy_from_slice(chunk)); - parts.push(ProposalPart::Data(chunk_data)); - hasher.update(chunk); - } - } - - // Fin - let signature = match &block.signature { - Some(signature) => { - // Use the existing signature if it exists (restreaming) - *signature - } - None => { - // We are streaming a new proposal, so we need to sign it - let hash = hasher.finalize().to_vec(); - signing_provider.sign_bytes(&hash).await? - } - }; - - parts.push(ProposalPart::Fin(ProposalFin::new(signature))); - - Ok((parts, signature)) -} - -/// Validates the proposal parts by checking the proposer and signature. -/// -/// ## Important -/// This function assumes that the parts are for the current height -pub async fn validate_proposal_parts( - parts: &ProposalParts, - expected_proposer: &Validator, - signing_provider: &impl SigningProvider, -) -> bool { - // Check that the parts are from the expected proposer - if expected_proposer.address != parts.proposer() { - warn!( - parts.height = %parts.height(), - parts.round = %parts.round(), - parts.proposer = %parts.proposer(), - expected_proposer = %expected_proposer.address, - "Received proposal part from non-proposer, ignoring" - ); - - return false; - } - - let fin = parts.fin(); - let hash = parts.hash(); - - assert_eq!( - expected_proposer.address, - parts.proposer(), - "Proposer address must match expected proposer" - ); - - // Check proposal parts signature - // NOTE: `expected_proposer` is guaranteed to be the proposer of these parts - let result = signing_provider - .verify_signed_bytes(&hash, &fin.signature, &expected_proposer.public_key) - .await; - - match result { - Ok(VerificationResult::Valid) => true, - - Ok(VerificationResult::Invalid) => { - warn!( - parts.height = %parts.height(), - parts.round = %parts.round(), - parts.proposer = %parts.proposer(), - parts.hash = %hex::encode(hash), - parts.signature = %hex::encode(fin.signature.to_bytes()), - "Received proposal parts with invalid signature, ignoring" - ); - - false - } - - Err(error) => { - warn!( - parts.height = %parts.height(), - parts.round = %parts.round(), - parts.proposer = %parts.proposer(), - parts.hash = %hex::encode(hash), - parts.signature = %hex::encode(fin.signature.to_bytes()), - %error, - "Error verifying proposal parts signature, ignoring" - ); - - false - } - } -} - -/// Resolves the expected proposer for a set of proposal parts. -/// -/// When `pol_round` (proof-of-lock round) is set, the proposal is a re-stream -/// of a locked block. The proposer embedded in the parts is the original proposer -/// from `pol_round`, not the proposer for `parts.round()` (the restream round). -pub fn resolve_expected_proposer<'a>( - proposer_selector: &dyn ProposerSelector, - validator_set: &'a ValidatorSet, - parts: &ProposalParts, -) -> &'a Validator { - let pol_round = parts.init().pol_round; - let proposer_round = if pol_round != Round::Nil { - pol_round - } else { - parts.round() - }; - proposer_selector.select_proposer(validator_set, parts.height(), proposer_round) -} - -/// Re-assemble a [`ConsensusBlock`] from its [`ProposalParts`]. -pub fn assemble_block_from_parts(parts: &ProposalParts) -> eyre::Result { - // Calculate total size and allocate buffer - let total_size = parts.data_size(); - let mut block_bytes = Vec::with_capacity(total_size); - - // Concatenate all chunks - for part in parts.data() { - block_bytes.extend_from_slice(&part.bytes); - } - - // Convert the concatenated data vector into an execution payload - let execution_payload = ExecutionPayloadV3::from_ssz_bytes(&block_bytes) - .map_err(|e| eyre!("Failed to decode execution payload: {e:?}"))?; - - let consensus_block = ConsensusBlock { - height: parts.height(), - round: parts.round(), - valid_round: parts.init().pol_round, - proposer: parts.proposer(), - validity: Validity::Valid, - execution_payload, - signature: Some(parts.fin().signature), - }; - - Ok(consensus_block) -} - -#[cfg(test)] -mod tests { - use super::*; - - use arc_consensus_types::proposer::RoundRobin; - use arc_consensus_types::signing::SigningProvider; - use arc_consensus_types::{Address, ProposalFin, ProposalInit, ValidatorSet}; - use arc_signer::local::{LocalSigningProvider, PrivateKey, PublicKey}; - - fn make_validator_set(n: usize) -> (Vec, ValidatorSet) { - let mut rng = rand::thread_rng(); - let keys: Vec = (0..n).map(|_| PrivateKey::generate(&mut rng)).collect(); - let validators: Vec = keys - .iter() - .map(|k| Validator::new(k.public_key(), 1)) - .collect(); - (keys, ValidatorSet::new(validators)) - } - - /// Build minimal ProposalParts with the given init fields and sign with the given key. - async fn make_signed_parts( - height: Height, - round: Round, - pol_round: Round, - proposer_pub: PublicKey, - signing_key: &PrivateKey, - ) -> ProposalParts { - use sha3::Digest; - - let proposer = Address::from_public_key(&proposer_pub); - let init = ProposalInit::new(height, round, pol_round, proposer); - - let mut hasher = sha3::Keccak256::new(); - hasher.update(height.as_u64().to_be_bytes()); - hasher.update(round.as_i64().to_be_bytes()); - let hash = hasher.finalize().to_vec(); - - let provider = LocalSigningProvider::new(signing_key.clone()); - let signature = provider.sign_bytes(&hash).await.unwrap(); - - ProposalParts::new(vec![ - ProposalPart::Init(init), - ProposalPart::Fin(ProposalFin::new(signature)), - ]) - .unwrap() - } - - #[test] - fn resolve_proposer_without_pol_round_uses_parts_round() { - let selector = RoundRobin; - let (_keys, validator_set) = make_validator_set(3); - - let height = Height::new(1); - let round = Round::new(2); - - // Build minimal parts with pol_round = Nil - let init = ProposalInit::new( - height, - round, - Round::Nil, - validator_set.get_by_index(0).unwrap().address, - ); - let fin = ProposalFin::new(arc_consensus_types::signing::Signature::test()); - let parts = - ProposalParts::new(vec![ProposalPart::Init(init), ProposalPart::Fin(fin)]).unwrap(); - - let expected = resolve_expected_proposer(&selector, &validator_set, &parts); - let round_proposer = selector.select_proposer(&validator_set, height, round); - - assert_eq!(expected.address, round_proposer.address); - } - - #[test] - fn resolve_proposer_with_pol_round_uses_original_round() { - let selector = RoundRobin; - let (_keys, validator_set) = make_validator_set(3); - - let height = Height::new(1); - let restream_round = Round::new(2); - let pol_round = Round::new(0); - - let original_proposer = selector.select_proposer(&validator_set, height, pol_round); - let restream_proposer = selector.select_proposer(&validator_set, height, restream_round); - - // Ensure they differ so the test is meaningful - assert_ne!( - original_proposer.address, restream_proposer.address, - "Test requires different proposers for pol_round and restream_round" - ); - - // Build parts as if restreamed: round=2, pol_round=0, proposer=original - let init = ProposalInit::new(height, restream_round, pol_round, original_proposer.address); - let fin = ProposalFin::new(arc_consensus_types::signing::Signature::test()); - let parts = - ProposalParts::new(vec![ProposalPart::Init(init), ProposalPart::Fin(fin)]).unwrap(); - - let expected = resolve_expected_proposer(&selector, &validator_set, &parts); - - // Should resolve to the pol_round proposer, not the restream round proposer - assert_eq!(expected.address, original_proposer.address); - assert_ne!(expected.address, restream_proposer.address); - } - - /// End-to-end: restreamed proposal parts signed by the original proposer - /// pass validation when expected_proposer is resolved via pol_round. - #[tokio::test] - async fn restreamed_parts_pass_validation_with_pol_round_proposer() { - let selector = RoundRobin; - let (keys, validator_set) = make_validator_set(3); - - let height = Height::new(1); - let pol_round = Round::new(0); - let restream_round = Round::new(2); - - let original_proposer = selector.select_proposer(&validator_set, height, pol_round); - - // Find the signing key for the original proposer - let signing_key = keys - .iter() - .find(|k| Address::from_public_key(&k.public_key()) == original_proposer.address) - .unwrap(); - - let parts = make_signed_parts( - height, - restream_round, - pol_round, - signing_key.public_key(), - signing_key, - ) - .await; - - // Resolve via pol_round (the fix) — should match and verify - let expected = resolve_expected_proposer(&selector, &validator_set, &parts); - let provider = LocalSigningProvider::new(signing_key.clone()); - assert!(validate_proposal_parts(&parts, expected, &provider).await); - } - - /// Restreamed parts would fail validation if we used parts_round instead - /// of pol_round to resolve the expected proposer (the old buggy behavior). - #[tokio::test] - async fn restreamed_parts_fail_validation_with_wrong_round_proposer() { - let selector = RoundRobin; - let (keys, validator_set) = make_validator_set(3); - - let height = Height::new(1); - let pol_round = Round::new(0); - let restream_round = Round::new(2); - - let original_proposer = selector.select_proposer(&validator_set, height, pol_round); - let wrong_proposer = selector.select_proposer(&validator_set, height, restream_round); - - assert_ne!(original_proposer.address, wrong_proposer.address); - - let signing_key = keys - .iter() - .find(|k| Address::from_public_key(&k.public_key()) == original_proposer.address) - .unwrap(); - - let parts = make_signed_parts( - height, - restream_round, - pol_round, - signing_key.public_key(), - signing_key, - ) - .await; - - // Using parts_round (the old bug) resolves to the wrong proposer → validation fails - let provider = LocalSigningProvider::new(signing_key.clone()); - assert!(!validate_proposal_parts(&parts, wrong_proposer, &provider).await); - } - - /// assemble_block_from_parts must preserve pol_round as valid_round. - #[tokio::test] - async fn assemble_block_preserves_valid_round_from_pol_round() { - use alloy_rpc_types_engine::ExecutionPayloadV3; - use arbitrary::{Arbitrary, Unstructured}; - - let mut u = Unstructured::new(&[0u8; 512]); - let payload = ExecutionPayloadV3::arbitrary(&mut u).unwrap(); - - let (keys, _) = make_validator_set(1); - let signing_key = &keys[0]; - let proposer = Address::from_public_key(&signing_key.public_key()); - - let pol_round = Round::new(1); - - let block = ConsensusBlock { - height: Height::new(10), - round: Round::new(3), - valid_round: pol_round, - proposer, - validity: Validity::Valid, - execution_payload: payload, - signature: None, - }; - - let provider = LocalSigningProvider::new(signing_key.clone()); - let (raw_parts, _sig) = make_proposal_parts(&provider, &block).await.unwrap(); - let parts = ProposalParts::new(raw_parts).unwrap(); - - // Sanity: Init carries the pol_round we set - assert_eq!(parts.init().pol_round, pol_round); - - let assembled = assemble_block_from_parts(&parts).unwrap(); - assert_eq!( - assembled.valid_round, pol_round, - "assemble_block_from_parts must propagate pol_round as valid_round" - ); - } - - #[tokio::test] - async fn assemble_block_preserves_nil_valid_round() { - use alloy_rpc_types_engine::ExecutionPayloadV3; - use arbitrary::{Arbitrary, Unstructured}; - - let mut u = Unstructured::new(&[0u8; 512]); - let payload = ExecutionPayloadV3::arbitrary(&mut u).unwrap(); - - let (keys, _) = make_validator_set(1); - let signing_key = &keys[0]; - let proposer = Address::from_public_key(&signing_key.public_key()); - - let block = ConsensusBlock { - height: Height::new(5), - round: Round::new(0), - valid_round: Round::Nil, - proposer, - validity: Validity::Valid, - execution_payload: payload, - signature: None, - }; - - let provider = LocalSigningProvider::new(signing_key.clone()); - let (raw_parts, _sig) = make_proposal_parts(&provider, &block).await.unwrap(); - let parts = ProposalParts::new(raw_parts).unwrap(); - - assert_eq!(parts.init().pol_round, Round::Nil); - - let assembled = assemble_block_from_parts(&parts).unwrap(); - assert_eq!(assembled.valid_round, Round::Nil); - } -} diff --git a/crates/malachite-app/src/request.rs b/crates/malachite-app/src/request.rs deleted file mode 100644 index 304eab8..0000000 --- a/crates/malachite-app/src/request.rs +++ /dev/null @@ -1,260 +0,0 @@ -// Copyright 2026 Circle Internet Group, Inc. All rights reserved. -// -// SPDX-License-Identifier: Apache-2.0 -// -// Licensed under the Apache License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. -// You may obtain a copy of the License at -// -// http://www.apache.org/licenses/LICENSE-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// See the License for the specific language governing permissions and -// limitations under the License. - -use std::time::SystemTime; - -use tokio::sync::{mpsc, oneshot}; -use tracing::error; - -use arc_consensus_db::invalid_payloads::StoredInvalidPayloads; -use arc_consensus_types::evidence::StoredMisbehaviorEvidence; -use arc_consensus_types::{ - signing::PublicKey, Address, ArcContext, BlockHash, CommitCertificateType, Height, Round, - ValidatorSet, -}; -use malachitebft_core_types::CommitCertificate; - -use arc_consensus_types::proposal_monitor::ProposalMonitor; - -use crate::utils::sync_state::SyncState; - -#[derive(Copy, Clone, Debug, PartialEq, Eq)] -pub enum AppRequestError { - Closed, - Full, - Recv, -} - -impl From> for AppRequestError { - fn from(err: mpsc::error::TrySendError) -> Self { - match err { - mpsc::error::TrySendError::Closed(_) => AppRequestError::Closed, - mpsc::error::TrySendError::Full(_) => AppRequestError::Full, - } - } -} - -impl From for AppRequestError { - fn from(_: oneshot::error::RecvError) -> Self { - AppRequestError::Recv - } -} - -#[derive(Debug)] -pub struct Status { - pub height: Height, - pub round: Round, - pub address: Address, - pub public_key: PublicKey, - pub proposer: Option
, - pub height_start_time: SystemTime, - pub prev_payload_hash: Option, - pub db_latest_height: Height, - pub db_earliest_height: Height, - pub undecided_blocks_count: usize, - pub pending_proposal_parts: Vec<(Height, usize)>, - pub validator_set: ValidatorSet, - pub sync_state: SyncState, -} - -#[derive(Debug)] -pub struct CommitCertificateInfo { - pub certificate: CommitCertificate, - pub certificate_type: CommitCertificateType, - pub proposer: Address, -} - -#[allow(clippy::enum_variant_names)] -pub enum AppRequest { - /// Retrieves a commit certificate at the given height - GetCertificate( - /// The height to get the certificate for. If None, get the latest certificate. - Option, - /// The channel to send the certificate back on. - oneshot::Sender>, - ), - /// Retrieves misbehavior evidence at the given height - GetMisbehaviorEvidence( - /// The height to get the evidence for. If None, use the latest height. - Option, - /// The channel to send the evidence back on. - oneshot::Sender>, - ), - /// Retrieves proposal monitor data at the given height - GetProposalMonitorData( - /// The height to get the data for. If None, get the latest. - Option, - /// The channel to send the data back on. - oneshot::Sender>, - ), - /// Retrieves invalid payloads at the given height - GetInvalidPayloads( - /// The height to get the payloads for. If None, get the latest. - Option, - /// The channel to send the payloads back on. - oneshot::Sender>, - ), - /// Get the application status - GetStatus(oneshot::Sender), - /// Check if the application is healthy - GetHealth(oneshot::Sender<()>), - /// Get the current sync state (lightweight, no DB queries) - GetSyncState(oneshot::Sender), -} - -pub type TxAppReq = mpsc::Sender; - -impl AppRequest { - /// Request the certificate for a height. - /// - /// If the request fails, return an error. - pub async fn get_certificate( - height: Option, - tx_app_req: &mpsc::Sender, - ) -> Result, AppRequestError> { - let (tx, rx) = oneshot::channel(); - tx_app_req - .try_send(Self::GetCertificate(height, tx)) - .inspect_err(|e| error!("Failed to send GetCertificate request to consensus: {e}"))?; - - let cert = rx.await.inspect_err(|e| { - error!("Failed to receive GetCertificate response from consensus: {e}") - })?; - - Ok(cert) - } - - /// Request the misbehavior evidence for a height. - /// - /// Returns: - /// - `Some(evidence)` with actual or empty evidence for finalized heights - /// - `None` if the requested height was not yet finalized - pub async fn get_misbehavior_evidence( - height: Option, - tx_app_req: &mpsc::Sender, - ) -> Result, AppRequestError> { - let (tx, rx) = oneshot::channel(); - tx_app_req - .try_send(Self::GetMisbehaviorEvidence(height, tx)) - .inspect_err(|e| { - error!("Failed to send GetMisbehaviorEvidence request to consensus: {e}") - })?; - - let evidence = rx.await.inspect_err(|e| { - error!("Failed to receive GetMisbehaviorEvidence response from consensus: {e}") - })?; - - Ok(evidence) - } - - /// Request the proposal monitor data for a height. - /// - /// If the request fails, return `None`. - pub async fn get_proposal_monitor_data( - height: Option, - tx_app_req: &mpsc::Sender, - ) -> Result, AppRequestError> { - let (tx, rx) = oneshot::channel(); - tx_app_req - .try_send(Self::GetProposalMonitorData(height, tx)) - .inspect_err(|e| { - error!("Failed to send GetProposalMonitorData request to consensus: {e}") - })?; - - let data = rx.await.inspect_err(|e| { - error!("Failed to receive GetProposalMonitorData response from consensus: {e}") - })?; - - Ok(data) - } - - /// Request the invalid payloads for a height. - /// - /// Returns: - /// - `Some(payloads)` with actual or empty payloads for finalized heights - /// - `None` if the requested height was not yet finalized - pub async fn get_invalid_payloads( - height: Option, - tx_app_req: &mpsc::Sender, - ) -> Result, AppRequestError> { - let (tx, rx) = oneshot::channel(); - tx_app_req - .try_send(Self::GetInvalidPayloads(height, tx)) - .inspect_err(|e| { - error!("Failed to send GetInvalidPayloads request to consensus: {e}") - })?; - - let payloads = rx.await.inspect_err(|e| { - error!("Failed to receive GetInvalidPayloads response from consensus: {e}") - })?; - - Ok(payloads) - } - - /// Get `malachite-app`'s status. - /// - /// If the request fails, return an error. - pub async fn get_status( - tx_app_req: &mpsc::Sender, - ) -> Result { - let (tx, rx) = oneshot::channel(); - - tx_app_req - .try_send(Self::GetStatus(tx)) - .inspect_err(|e| error!("Failed to send GetStatus request to consensus: {e}"))?; - - let status = rx - .await - .inspect_err(|e| error!("Failed to receive GetStatus response from consensus: {e}"))?; - - Ok(status) - } - - /// Get the current sync state. Lightweight — reads an in-memory field, no DB queries. - pub async fn get_sync_state( - tx_app_req: &mpsc::Sender, - ) -> Result { - let (tx, rx) = oneshot::channel(); - - tx_app_req - .try_send(Self::GetSyncState(tx)) - .inspect_err(|e| error!("Failed to send GetSyncState request to consensus: {e}"))?; - - let sync_state = rx.await.inspect_err(|e| { - error!("Failed to receive GetSyncState response from consensus: {e}") - })?; - - Ok(sync_state) - } - - /// Get node's health. Returns unit type. Used to check whether the node is responsive. - /// - /// If the request fails, return an error. - pub async fn get_health(tx_app_req: &mpsc::Sender) -> Result<(), AppRequestError> { - let (tx, rx) = oneshot::channel(); - - tx_app_req - .try_send(Self::GetHealth(tx)) - .inspect_err(|e| error!("Failed to send GetHealth request to consensus: {e}"))?; - - #[allow(clippy::let_unit_value)] - let status = rx - .await - .inspect_err(|e| error!("Failed to receive GetHealth response from consensus: {e}"))?; - - Ok(status) - } -} diff --git a/crates/malachite-app/src/rpc/handlers.rs b/crates/malachite-app/src/rpc/handlers.rs deleted file mode 100644 index 81b45a5..0000000 --- a/crates/malachite-app/src/rpc/handlers.rs +++ /dev/null @@ -1,255 +0,0 @@ -// Copyright 2026 Circle Internet Group, Inc. All rights reserved. -// -// SPDX-License-Identifier: Apache-2.0 -// -// Licensed under the Apache License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. -// You may obtain a copy of the License at -// -// http://www.apache.org/licenses/LICENSE-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// See the License for the specific language governing permissions and -// limitations under the License. - -use axum::extract::Extension; -use axum::extract::Query; -use axum::extract::State; -use axum::http::StatusCode; -use axum::response::IntoResponse; -use axum::Json; -use serde_json::json; - -use malachitebft_app_channel::ConsensusRequest; -use malachitebft_app_channel::NetworkRequest; - -use crate::request::AppRequest; -use crate::request::TxAppReq; -use crate::rpc::types::persistent_peer_error_to_response; -use crate::rpc::types::request_error_to_response; -use crate::rpc::types::RpcVersion; -use crate::rpc::version::ApiVersion; -use crate::utils::sync_state::SyncState; - -use super::types::{ - AddOrRemovePersistentPeerBody, GetCertificateParams, RpcAppStatus, RpcCommitCertificate, - RpcConsensusStateDump, RpcInvalidPayloads, RpcMisbehaviorEvidence, RpcNetworkStateDump, -}; -use super::types::{TxConsensusReq, TxNetworkReq}; - -pub(crate) async fn get_consensus_state( - tx_consensus_req: State, - Extension(version): Extension, -) -> impl IntoResponse { - tracing::debug!(?version, "get_consensus_state called"); - - match ConsensusRequest::dump_state(&tx_consensus_req).await { - Ok(Some(state)) => Json(RpcConsensusStateDump::from(&state)).into_response(), - Ok(None) => { - let body = Json(json!({ "error": "Consensus state not available" })); - (StatusCode::SERVICE_UNAVAILABLE, body).into_response() - } - Err(e) => request_error_to_response(e).into_response(), - } -} - -pub(crate) async fn get_network_state( - tx_network_req: State, - Extension(version): Extension, -) -> impl IntoResponse { - tracing::debug!(?version, "get_network_state called"); - - match NetworkRequest::dump_state(&tx_network_req).await { - Ok(Some(state)) => Json(RpcNetworkStateDump::from(state)).into_response(), - Ok(None) => { - let body = Json(json!({ "error": "Network state not available" })); - (StatusCode::SERVICE_UNAVAILABLE, body).into_response() - } - Err(e) => request_error_to_response(e).into_response(), - } -} - -pub(crate) async fn get_commit( - tx_app_req: State, - query: Query, - Extension(version): Extension, -) -> impl IntoResponse { - tracing::debug!(?version, "get_commit called"); - - AppRequest::get_certificate(query.height, &tx_app_req) - .await - .map_err(request_error_to_response)? - .map(|cert| Json(RpcCommitCertificate::from(cert))) - .ok_or_else(|| { - ( - StatusCode::NOT_FOUND, - Json(json!({"error": "Certificate not found"})), - ) - }) -} - -pub(crate) async fn get_misbehavior_evidence( - tx_app_req: State, - query: Query, - Extension(version): Extension, -) -> impl IntoResponse { - tracing::debug!(?version, "get_misbehavior_evidence called"); - - AppRequest::get_misbehavior_evidence(query.height, &tx_app_req) - .await - .map_err(request_error_to_response)? - .map(|evidence| Json(RpcMisbehaviorEvidence::from(evidence))) - .ok_or_else(|| { - ( - StatusCode::NOT_FOUND, - Json(json!({"error": "Misbehavior evidence not found"})), - ) - }) -} - -pub(crate) async fn get_proposal_monitor( - tx_app_req: State, - query: Query, - Extension(version): Extension, -) -> impl IntoResponse { - tracing::debug!(?version, "get_proposal_monitor called"); - - match AppRequest::get_proposal_monitor_data(query.height, &tx_app_req).await { - Ok(Some(data)) => { - Ok(Json(super::types::RpcProposalMonitorData::from(data)).into_response()) - } - Ok(None) => Err(( - StatusCode::NOT_FOUND, - Json(json!({"error": "Proposal monitor data not found"})), - ) - .into_response()), - Err(e) => Err(request_error_to_response(e).into_response()), - } -} - -pub(crate) async fn get_invalid_payloads( - tx_app_req: State, - query: Query, - Extension(version): Extension, -) -> impl IntoResponse { - tracing::debug!(?version, "get_invalid_payloads called"); - - AppRequest::get_invalid_payloads(query.height, &tx_app_req) - .await - .map_err(request_error_to_response)? - .map(|payloads| Json(RpcInvalidPayloads::from(payloads))) - .ok_or_else(|| { - ( - StatusCode::NOT_FOUND, - Json(json!({"error": "Invalid payloads not found"})), - ) - }) -} - -pub(crate) async fn get_status( - tx_app_req: State, - Extension(version): Extension, -) -> impl IntoResponse { - tracing::debug!(?version, "get_status called"); - - AppRequest::get_status(&tx_app_req) - .await - .map(|cert| Json(RpcAppStatus::from(cert))) - .map_err(request_error_to_response) -} - -pub(crate) async fn get_health( - tx_app_req: State, - Extension(version): Extension, -) -> impl IntoResponse { - tracing::debug!(?version, "get_health called"); - - AppRequest::get_health(&tx_app_req) - .await - .map(|()| Json(json!({ "status": "ok" }))) - .map_err(request_error_to_response) -} - -pub(crate) async fn get_ready( - tx_app_req: State, - Extension(version): Extension, -) -> impl IntoResponse { - tracing::debug!(?version, "get_ready called"); - - match AppRequest::get_sync_state(&tx_app_req).await { - Ok(sync_state) => { - let status_code = match sync_state { - SyncState::InSync => StatusCode::OK, - SyncState::CatchingUp => StatusCode::SERVICE_UNAVAILABLE, - }; - (status_code, Json(json!({ "sync_state": sync_state }))).into_response() - } - Err(e) => request_error_to_response(e).into_response(), - } -} - -pub(crate) async fn get_version(Extension(version): Extension) -> impl IntoResponse { - tracing::debug!(?version, "get_version called"); - - Json(RpcVersion { - git_version: arc_version::GIT_VERSION, - git_commit: arc_version::GIT_COMMIT_HASH, - git_short_hash: arc_version::GIT_SHORT_HASH, - cargo_version: arc_version::SHORT_VERSION, - }) -} - -pub(crate) async fn add_persistent_peer( - tx_network_req: State, - Extension(version): Extension, - Json(body): Json, -) -> impl IntoResponse { - let addr: malachitebft_app_channel::app::net::Multiaddr = match body.addr.parse() { - Ok(a) => a, - Err(_) => { - let body = Json(json!({"error": "Invalid multiaddr"})); - return (StatusCode::BAD_REQUEST, body).into_response(); - } - }; - - tracing::debug!(?version, ?addr, "add_persistent_peer called"); - - // For future ref: https://github.com/circlefin/malachite/pull/1485 - // let has_p2p = addr.iter().any(|p| matches!(p, multiaddr::Protocol::P2p(_))); - // if !has_p2p { - // let body = Json(json!({ - // "error": "Multiaddr must include /p2p/, e.g. /ip4/127.0.0.1/tcp/26656/p2p/12D3KooW..." - // })); - // return (StatusCode::BAD_REQUEST, body).into_response(); - // } - - match NetworkRequest::add_persistent_peer(&tx_network_req, addr).await { - Ok(Ok(())) => (StatusCode::OK, Json(json!({ "status": "ok" }))).into_response(), - Ok(Err(e)) => persistent_peer_error_to_response(e).into_response(), - Err(e) => request_error_to_response(e).into_response(), - } -} - -pub(crate) async fn remove_persistent_peer( - tx_network_req: State, - Extension(version): Extension, - Json(body): Json, -) -> impl IntoResponse { - let addr: malachitebft_app_channel::app::net::Multiaddr = match body.addr.parse() { - Ok(a) => a, - Err(_) => { - let body = Json(json!({"error": "Invalid multiaddr"})); - return (StatusCode::BAD_REQUEST, body).into_response(); - } - }; - - tracing::debug!(?version, ?addr, "remove_persistent_peer called"); - - match NetworkRequest::remove_persistent_peer(&tx_network_req, addr).await { - Ok(Ok(())) => (StatusCode::OK, Json(json!({ "status": "ok" }))).into_response(), - Ok(Err(e)) => persistent_peer_error_to_response(e).into_response(), - Err(e) => request_error_to_response(e).into_response(), - } -} diff --git a/crates/malachite-app/src/rpc/middleware.rs b/crates/malachite-app/src/rpc/middleware.rs deleted file mode 100644 index d7b62c1..0000000 --- a/crates/malachite-app/src/rpc/middleware.rs +++ /dev/null @@ -1,117 +0,0 @@ -// Copyright 2025 Circle Internet Group, Inc. All rights reserved. -// -// SPDX-License-Identifier: Apache-2.0 -// -// Licensed under the Apache License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. -// You may obtain a copy of the License at -// -// http://www.apache.org/licenses/LICENSE-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// See the License for the specific language governing permissions and -// limitations under the License. - -//! Middleware for API version extraction and negotiation - -use axum::extract::Request; -use axum::http::{header, StatusCode}; -use axum::middleware::Next; -use axum::response::{IntoResponse, Response}; -use serde_json::json; -use tracing::debug; - -use super::version::ApiVersion; - -/// Axum middleware that extracts the API version from the Accept header -/// and stores it in the request extensions. -/// -/// If the Accept header is missing or contains `application/json`, -/// defaults to the current API version (V1). -/// -/// If the Accept header specifies an unsupported version, -/// returns a 406 Not Acceptable response. -pub async fn extract_version(mut req: Request, next: Next) -> Response { - // Extract Accept header - let accept_header = req - .headers() - .get(header::ACCEPT) - .and_then(|h| match h.to_str() { - Ok(s) => Some(s), - Err(err) => { - debug!(?err, "Accept header is not a valid string"); - None - } - }) - .unwrap_or(""); - - // Parse version from header - match ApiVersion::from_accept_header(accept_header) { - Some(version) => { - debug!(?version, accept_header, "API version extracted"); - - // Store version in request extensions - req.extensions_mut().insert(version); - - // Continue to handler - let mut response = next.run(req).await; - - // Add Content-Type header to response - response.headers_mut().insert( - header::CONTENT_TYPE, - version - .media_type() - .parse() - .expect("valid content-type header"), // okay since we know it's valid (see ApiVersion::media_type()) - ); - - response - } - None => { - // Unsupported version - debug!( - accept_header, - "Unsupported API version requested, returning 406" - ); - - let body = json!({ - "error": "Unsupported API version", - "supported_versions": [ApiVersion::V1.to_string()], - "message": format!( - "The requested API version is not supported. Please use Accept: {} for {}.", - ApiVersion::V1.media_type(), - ApiVersion::V1.to_string() - ) - }); - - (StatusCode::NOT_ACCEPTABLE, axum::Json(body)).into_response() - } - } -} - -#[cfg(test)] -mod tests { - use super::*; - - // Note: Integration tests for version negotiation are in the parent module's tests - // These unit tests verify the middleware logic in isolation - - #[test] - fn test_version_parsing() { - assert_eq!( - ApiVersion::from_accept_header("application/vnd.arc.v1+json"), - Some(ApiVersion::V1) - ); - assert_eq!( - ApiVersion::from_accept_header("application/json"), - Some(ApiVersion::V1) - ); - assert_eq!(ApiVersion::from_accept_header(""), Some(ApiVersion::V1)); - assert_eq!( - ApiVersion::from_accept_header("application/vnd.arc.v99+json"), - None - ); - } -} diff --git a/crates/malachite-app/src/rpc/mod.rs b/crates/malachite-app/src/rpc/mod.rs deleted file mode 100644 index 54d8e4c..0000000 --- a/crates/malachite-app/src/rpc/mod.rs +++ /dev/null @@ -1,29 +0,0 @@ -// Copyright 2025 Circle Internet Group, Inc. All rights reserved. -// -// SPDX-License-Identifier: Apache-2.0 -// -// Licensed under the Apache License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. -// You may obtain a copy of the License at -// -// http://www.apache.org/licenses/LICENSE-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// See the License for the specific language governing permissions and -// limitations under the License. - -//! RPC server for the consensus layer -//! -//! Provides a REST API with header-based versioning using custom Accept headers. - -mod handlers; -mod middleware; -#[macro_use] -mod types; -mod routes; -mod version; - -pub use routes::build_router; -pub use routes::serve; diff --git a/crates/malachite-app/src/rpc/routes.rs b/crates/malachite-app/src/rpc/routes.rs deleted file mode 100644 index 16a4f8c..0000000 --- a/crates/malachite-app/src/rpc/routes.rs +++ /dev/null @@ -1,936 +0,0 @@ -// Copyright 2026 Circle Internet Group, Inc. All rights reserved. -// -// SPDX-License-Identifier: Apache-2.0 -// -// Licensed under the Apache License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. -// You may obtain a copy of the License at -// -// http://www.apache.org/licenses/LICENSE-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// See the License for the specific language governing permissions and -// limitations under the License. - -use std::collections::BTreeMap; -use std::sync::Arc; - -use axum::response::IntoResponse; -use axum::routing::get; -use axum::{Json, Router}; -use eyre::Result; -use serde_json::json; -use tokio::net::{TcpListener, ToSocketAddrs}; -use tracing::{error, info}; - -use super::middleware::extract_version; -use super::types::{EndpointInfo, RpcState, TxConsensusReq, TxNetworkReq}; -use super::version::ApiVersion; -use crate::request::TxAppReq; - -// List of RPC routes. -routes![ - route!( - get, - "/consensus-state", - crate::rpc::handlers::get_consensus_state, - "Get the current consensus state." - ), - route!( - get, - "/commit", - crate::rpc::handlers::get_commit, - "Get the commit certificate for a specific height", - params = { - "height (optional)" => "The height of the commit certificate to retrieve. No height returns the latest certificate." - } - ), - route!( - get, - "/misbehavior-evidence", - crate::rpc::handlers::get_misbehavior_evidence, - "Get misbehavior evidence (double votes or proposals) for a specific height", - params = { - "height (optional)" => "The height of the misbehavior evidence to retrieve. No height returns the latest." - } - ), - route!( - get, - "/proposal-monitor", - crate::rpc::handlers::get_proposal_monitor, - "Get round-0 proposal monitoring data (timing and success) for a specific height", - params = { - "height (optional)" => "The height to get monitoring data for. No height returns the latest." - } - ), - route!( - get, - "/invalid-payloads", - crate::rpc::handlers::get_invalid_payloads, - "Get invalid payloads for a specific height", - params = { - "height (optional)" => "The height of the invalid payloads to retrieve. No height returns the latest." - } - ), - route!( - get, - "/status", - crate::rpc::handlers::get_status, - "Get the application status" - ), - route!( - get, - "/health", - crate::rpc::handlers::get_health, - "Returns empty value. Used to check the node's health" - ), - route!( - get, - "/ready", - crate::rpc::handlers::get_ready, - "Readiness probe. Returns 200 when in sync, 503 when catching up" - ), - route!( - get, - "/version", - crate::rpc::handlers::get_version, - "Get the consensus layer version information" - ), - route!( - get, - "/network-state", - crate::rpc::handlers::get_network_state, - "Get the current network state (peers, topics, scores)" - ), - route!( - post, - "/persistent-peers", - crate::rpc::handlers::add_persistent_peer, - "Add a persistent peer at runtime.", - params = { - "body" => "JSON object with \"addr\" (string): multiaddr of the peer, e.g. \"/ip4/127.0.0.1/tcp/26656/p2p/12D3KooW...\"." - } - ), - route!( - delete, - "/persistent-peers", - crate::rpc::handlers::remove_persistent_peer, - "Remove a persistent peer at runtime.", - params = { - "body" => "JSON object with \"addr\" (string): multiaddr of the peer to remove, e.g. \"/ip4/127.0.0.1/tcp/26656/p2p/12D3KooW...\"." - } - ), -]; - -#[tracing::instrument(name = "rpc", skip_all)] -pub async fn serve( - listen_addr: impl ToSocketAddrs, - tx_consensus_req: TxConsensusReq, - tx_app_req: TxAppReq, - tx_network_req: TxNetworkReq, -) { - if let Err(e) = inner(listen_addr, tx_consensus_req, tx_app_req, tx_network_req).await { - error!("RPC server failed: {e}"); - } -} - -/// Build the RPC router with all routes and middleware -/// -/// This is exposed publicly for testing purposes, allowing integration tests -/// to create a server with the actual production router. -pub fn build_router( - tx_consensus_req: TxConsensusReq, - tx_app_req: TxAppReq, - tx_network_req: TxNetworkReq, -) -> Router { - let rpc_state = RpcState { - tx_consensus_req, - tx_app_req, - tx_network_req, - }; - - let routes = build_routes(); - - let mut router = Router::new(); - for route in &routes { - router = router.route(route.path, (route.handler)()); - } - - let docs = routes - .into_iter() - .map(|r| (format!("{} {}", r.method, r.path), r.doc)) - .collect::>(); - - router = { - let docs = Arc::new(docs); - router.route("/", get(move || get_index(Arc::clone(&docs)))) - }; - - // Apply version extraction middleware - router - .layer(axum::middleware::from_fn(extract_version)) - .with_state(rpc_state) -} - -async fn inner( - listen_addr: impl ToSocketAddrs, - tx_consensus_req: TxConsensusReq, - tx_app_req: TxAppReq, - tx_network_req: TxNetworkReq, -) -> Result<()> { - let app = build_router(tx_consensus_req, tx_app_req, tx_network_req); - - let listener = TcpListener::bind(listen_addr).await?; - let address = listener.local_addr()?; - - info!(%address, "RPC server listening"); - axum::serve(listener, app).await?; - - Ok(()) -} - -async fn get_index(endpoints: Arc>) -> impl IntoResponse { - Json(json!({ - "endpoints": endpoints, - "rpc_versioning": { - "method": "header-based", - "header": "Accept", - "format": "application/vnd.arc.v{N}+json", - "supported_versions": [ApiVersion::V1.to_string()], - "default_version": "v1", - "example": "curl -H \"Accept: application/vnd.arc.v1+json\" http://localhost:26658/status" - } - })) -} - -#[cfg(test)] -mod tests { - use arc_consensus_types::CommitCertificateType; - use axum::body::Body; - use axum::http::{Request, Response, StatusCode}; - use core::panic; - use std::collections::{BTreeMap, HashMap, HashSet}; - use std::time::{Duration, SystemTime}; - use tokio::sync::mpsc; - use tower::ServiceExt; - - use arc_consensus_types::{ - signing::PrivateKey, Address, ArcContext, BlockHash, Height, Round, Validator, - ValidatorSet, ValueId, - }; - use malachitebft_app_channel::app::{ - engine::{ - consensus::state_dump::{ - types as dump_types, ProposalKeeperState, StateDump, VoteKeeperState, - }, - network::NetworkStateDump, - }, - net::{Multiaddr, PeerId}, - }; - use malachitebft_app_channel::{ConsensusRequest, NetworkRequest}; - use malachitebft_core_state_machine::state::State as MState; - use malachitebft_core_types::CommitCertificate; - use malachitebft_network::{LocalNodeInfo, PersistentPeerError, ValidatorInfo}; - - use super::*; - use crate::request::{AppRequest, CommitCertificateInfo, Status}; - use crate::rpc::types::{ - RpcAppStatus, RpcCommitCertificate, RpcConsensusStateDump, RpcNetworkStateDump, - }; - use crate::utils::sync_state::SyncState; - - enum MockValue { - Present, - Absent, - } - - enum MockConfig { - AppGetHealth, - AppGetStatus, - AppGetSyncState(SyncState), - AppGetCertificate(MockValue), - ConsensusDumpState(MockValue), - NetworkDumpState(MockValue), - AddPersistentPeer(Result<(), PersistentPeerError>), - RemovePersistentPeer(Result<(), PersistentPeerError>), - } - - struct MockBackend { - rx_consensus: mpsc::Receiver>, - rx_app: mpsc::Receiver, - rx_network: mpsc::Receiver, - config: MockConfig, - } - - impl MockBackend { - fn spawn_new( - config: MockConfig, - ) -> ( - mpsc::Sender>, - mpsc::Sender, - mpsc::Sender, - ) { - let (tx_consensus, rx_consensus) = mpsc::channel::>(1); - let (tx_app, rx_app) = mpsc::channel::(1); - let (tx_network, rx_network) = mpsc::channel::(1); - - let backend = Self { - rx_consensus, - rx_app, - rx_network, - config, - }; - tokio::spawn(backend.run()); - - (tx_consensus, tx_app, tx_network) - } - - async fn run(self) { - let MockBackend { - mut rx_consensus, - mut rx_app, - mut rx_network, - config, - } = self; - - tokio::select! { - msg = rx_consensus.recv() => { - Self::handle_consensus_msg(msg, config); - }, - msg = rx_app.recv() => { - Self::handle_app_msg(msg, config); - }, - msg = rx_network.recv() => { - Self::handle_network_msg(msg, config); - }, - _ = tokio::time::sleep(Duration::from_secs(2)) => { - panic!("Mock backend did not receive any request within 2s"); - }, - } - } - - fn handle_consensus_msg(msg: Option>, config: MockConfig) { - match config { - MockConfig::ConsensusDumpState(ret) => { - let Some(ConsensusRequest::DumpState(reply_port)) = msg else { - panic!("Unexpected msg"); - }; - let _ = reply_port.send(match ret { - MockValue::Present => Some(Self::a_consensus_dump()), - MockValue::Absent => None, - }); - } - _ => panic!("Unexpected config"), - } - } - - fn handle_app_msg(msg: Option, config: MockConfig) { - match config { - MockConfig::AppGetHealth => { - let Some(AppRequest::GetHealth(reply)) = msg else { - panic!("Unexpected msg"); - }; - let _ = reply.send(()); - } - MockConfig::AppGetStatus => { - let Some(AppRequest::GetStatus(reply_port)) = msg else { - panic!("Unexpected msg"); - }; - let _ = reply_port.send(Self::a_status()); - } - MockConfig::AppGetSyncState(state) => { - let Some(AppRequest::GetSyncState(reply)) = msg else { - panic!("Unexpected msg"); - }; - let _ = reply.send(state); - } - MockConfig::AppGetCertificate(ret) => { - let Some(AppRequest::GetCertificate(None, reply_port)) = msg else { - panic!("Unexpected msg"); - }; - let _ = reply_port.send(match ret { - MockValue::Present => Some(Self::a_commit_cert_info()), - MockValue::Absent => None, - }); - } - _ => panic!("Unexpected config"), - } - } - - fn handle_network_msg(msg: Option, config: MockConfig) { - match config { - MockConfig::NetworkDumpState(ret) => { - let Some(NetworkRequest::DumpState(reply_port)) = msg else { - panic!("Unexpected msg"); - }; - let _ = reply_port.send(match ret { - MockValue::Present => Some(Self::a_network_dump()), - MockValue::Absent => None, - }); - } - MockConfig::AddPersistentPeer(result) => { - let Some(NetworkRequest::UpdatePersistentPeers(_, reply_port)) = msg else { - panic!("Unexpected msg"); - }; - let _ = reply_port.send(result); - } - MockConfig::RemovePersistentPeer(result) => { - let Some(NetworkRequest::UpdatePersistentPeers(_, reply_port)) = msg else { - panic!("Unexpected msg"); - }; - let _ = reply_port.send(result); - } - _ => panic!("Unexpected config"), - } - } - - fn a_consensus_dump() -> StateDump { - let consensus = MState::::default(); - let address = Address::new([0xEE; 20]); - let proposer = Some(Address::new([0xBC; 20])); - - let sk = PrivateKey::from([0x77; 32]); - let v = Validator::new(sk.public_key(), 541); - let validator_set = ValidatorSet::new(vec![v]); - - let vote_keeper = VoteKeeperState { - votes: BTreeMap::new(), - evidence: dump_types::VoteEvidenceMap::new(), - }; - let proposal_keeper = ProposalKeeperState { - proposals: BTreeMap::new(), - evidence: dump_types::ProposalEvidenceMap::new(), - }; - - let params = dump_types::ConsensusParams { - address, - threshold_params: dump_types::ThresholdParams::default(), - value_payload: dump_types::ValuePayload::ProposalAndParts, - enabled: true, - }; - - StateDump { - consensus, - address, - proposer, - params, - validator_set, - vote_keeper, - proposal_keeper, - full_proposal_keeper: Default::default(), - last_signed_prevote: None, - last_signed_precommit: None, - round_certificate: None, - input_queue: dump_types::BoundedQueue::new(0, 0), - } - } - - fn a_network_dump() -> malachitebft_app_channel::app::engine::network::NetworkStateDump { - let mut peer_id_bytes = vec![0x00, 0x20]; // identity multihash code + length - peer_id_bytes.extend_from_slice(&[5u8; 32]); // 32 bytes of data - let peer_id = PeerId::from_bytes(&peer_id_bytes).unwrap(); - - let listen_addr: Multiaddr = "/ip4/127.0.0.1/tcp/34567".parse().unwrap(); - let local = LocalNodeInfo { - moniker: "a-node".to_string(), - peer_id, - listen_addr: listen_addr.clone(), - consensus_address: Some("ADDR1".to_string()), - is_validator: true, - persistent_peers_only: false, - subscribed_topics: HashSet::from(["/consensus".to_string()]), - proof_bytes: None, - }; - - NetworkStateDump { - local_node: local, - peers: HashMap::new(), - validator_set: vec![ValidatorInfo { - address: "ADDR1".to_string(), - public_key: vec![0x01; 32], - voting_power: 313, - }], - persistent_peer_ids: vec![peer_id], - persistent_peer_addrs: vec![listen_addr.clone()], - } - } - - fn a_status() -> Status { - let height = Height::new(42); - let round = Round::new(10); - let address = Address::new([0x11; 20]); - let proposer = Some(Address::new([0x22; 20])); - let height_start_time = SystemTime::UNIX_EPOCH; - let prev_payload_hash = Some(BlockHash::new([0xCC; 32])); - let db_latest_height = Height::new(100); - let db_earliest_height = Height::new(2); - let undecided_blocks_count = 3; - let pending_proposal_parts = vec![]; - - let sk = PrivateKey::from([0x33; 32]); - let public_key = sk.public_key(); - let v = Validator::new(public_key, 1234); - let validator_set = ValidatorSet::new(vec![v]); - - Status { - height, - round, - address, - public_key, - proposer, - height_start_time, - prev_payload_hash, - db_latest_height, - db_earliest_height, - undecided_blocks_count, - pending_proposal_parts, - validator_set, - sync_state: SyncState::InSync, - } - } - - fn a_commit_cert() -> CommitCertificate { - let height = Height::new(7); - let round = Round::new(3); - let value_id = ValueId::new(BlockHash::new([0xAA; 32])); - let votes = vec![]; - CommitCertificate::new(height, round, value_id, votes) - } - - fn a_commit_cert_info() -> CommitCertificateInfo { - CommitCertificateInfo { - certificate: Self::a_commit_cert(), - certificate_type: CommitCertificateType::Minimal, - proposer: Address::new([0x55; 20]), - } - } - } - - async fn response_to_json(resp: Response) -> serde_json::Value { - let bytes = axum::body::to_bytes(resp.into_body(), usize::MAX) - .await - .unwrap(); - serde_json::from_slice(&bytes).unwrap() - } - - async fn build_no_backend_router_and_request(uri: &str) -> (StatusCode, serde_json::Value) { - let (tx_dummy_cons_req, _dummy_rx_c) = mpsc::channel::>(1); - let (tx_dummy_app_req, _dummy_rx_a) = mpsc::channel::(1); - let (tx_dummy_nw_req, _dummy_rx_n) = mpsc::channel::(1); - build_router_and_request(tx_dummy_cons_req, tx_dummy_app_req, tx_dummy_nw_req, uri).await - } - - async fn build_router_and_request( - tx_consensus_req: mpsc::Sender>, - tx_app_req: mpsc::Sender, - tx_network_req: mpsc::Sender, - uri: &str, - ) -> (StatusCode, serde_json::Value) { - let app = build_router(tx_consensus_req, tx_app_req, tx_network_req); - let req = Request::builder() - .method("GET") - .uri(uri) - .body(Body::empty()) - .unwrap(); - let resp = app.oneshot(req).await.unwrap(); - let status = resp.status(); - let val = response_to_json(resp).await; - (status, val) - } - - async fn build_router_and_request_with_body( - method: &str, - tx_consensus_req: mpsc::Sender>, - tx_app_req: mpsc::Sender, - tx_network_req: mpsc::Sender, - uri: &str, - body: serde_json::Value, - ) -> (StatusCode, serde_json::Value) { - let app = build_router(tx_consensus_req, tx_app_req, tx_network_req); - let req = Request::builder() - .method(method) - .uri(uri) - .header("content-type", "application/json") - .body(Body::from(serde_json::to_vec(&body).unwrap())) - .unwrap(); - let resp = app.oneshot(req).await.unwrap(); - let status = resp.status(); - let val = response_to_json(resp).await; - (status, val) - } - - #[test] - fn test_build_routes_contains_expected_paths() { - let mut paths: Vec<_> = build_routes().iter().map(|r| r.path).collect(); - paths.sort(); - assert_eq!( - paths, - vec![ - "/commit", - "/consensus-state", - "/health", - "/invalid-payloads", - "/misbehavior-evidence", - "/network-state", - "/persistent-peers", - "/persistent-peers", - "/proposal-monitor", - "/ready", - "/status", - "/version", - ] - ); - } - - #[test] - fn test_commit_route_has_params_docs() { - let routes = build_routes(); - let commit = routes - .iter() - .find(|r| r.method == "GET" && r.path == "/commit") - .unwrap(); - let params = commit.doc.params.as_ref().unwrap(); - assert_eq!( - *params.get("height (optional)").unwrap(), - "The height of the commit certificate to retrieve. No height returns the latest certificate." - ); - } - - #[tokio::test] - async fn test_get_index_json() { - let mut docs = BTreeMap::new(); - docs.insert( - "GET /dummy".to_string(), - EndpointInfo { - desc: "Dummy endpoint", - params: None, - }, - ); - - let resp = get_index(Arc::new(docs)).await.into_response(); - let val = response_to_json(resp).await; - - assert!(val.get("endpoints").is_some()); - assert!(val["endpoints"].get("GET /dummy").is_some()); - assert!(val.get("rpc_versioning").is_some()); - assert!(val["rpc_versioning"].get("default_version").is_some()); - let supported = val["rpc_versioning"]["supported_versions"] - .as_array() - .unwrap(); - assert!(supported.contains(&json!("v1"))); - } - - #[tokio::test] - async fn test_index_documents_both_persistent_peers_methods() { - let routes = build_routes(); - let docs = routes - .into_iter() - .map(|r| (format!("{} {}", r.method, r.path), r.doc)) - .collect::>(); - let resp = get_index(Arc::new(docs)).await.into_response(); - let val = response_to_json(resp).await; - let endpoints = &val["endpoints"]; - assert!( - endpoints.get("POST /persistent-peers").is_some(), - "index must document POST /persistent-peers" - ); - assert!( - endpoints.get("DELETE /persistent-peers").is_some(), - "index must document DELETE /persistent-peers" - ); - assert_eq!( - endpoints["POST /persistent-peers"]["desc"], - "Add a persistent peer at runtime." - ); - assert_eq!( - endpoints["DELETE /persistent-peers"]["desc"], - "Remove a persistent peer at runtime." - ); - } - - #[tokio::test] - async fn test_version_success() { - // '/version' endpoint does not use the backend - let (status, val) = build_no_backend_router_and_request("/version").await; - assert_eq!(status, StatusCode::OK); - assert!(val.get("git_version").is_some()); - assert!(val.get("git_commit").is_some()); - assert!(val.get("git_short_hash").is_some()); - assert!(val.get("cargo_version").is_some()); - } - - #[tokio::test] - async fn test_commit_latest_404() { - let (tx_cons_req, tx_app_req, tx_nw_req) = - MockBackend::spawn_new(MockConfig::AppGetCertificate(MockValue::Absent)); - let (status, val) = - build_router_and_request(tx_cons_req, tx_app_req, tx_nw_req, "/commit").await; - assert_eq!(status, StatusCode::NOT_FOUND); - assert_eq!(val, json!({"error": "Certificate not found"})); - } - - #[tokio::test] - async fn test_consensus_state_503() { - let (tx_cons_req, tx_app_req, tx_nw_req) = - MockBackend::spawn_new(MockConfig::ConsensusDumpState(MockValue::Absent)); - let (status, val) = - build_router_and_request(tx_cons_req, tx_app_req, tx_nw_req, "/consensus-state").await; - assert_eq!(status, StatusCode::SERVICE_UNAVAILABLE); - assert_eq!(val, json!({"error": "Consensus state not available"})); - } - - #[tokio::test] - async fn test_network_state_503() { - let (tx_cons_req, tx_app_req, tx_nw_req) = - MockBackend::spawn_new(MockConfig::NetworkDumpState(MockValue::Absent)); - let (status, val) = - build_router_and_request(tx_cons_req, tx_app_req, tx_nw_req, "/network-state").await; - assert_eq!(status, StatusCode::SERVICE_UNAVAILABLE); - assert_eq!(val, json!({"error": "Network state not available"})); - } - - #[tokio::test] - async fn test_health_success() { - let (tx_cons_req, tx_app_req, tx_nw_req) = MockBackend::spawn_new(MockConfig::AppGetHealth); - let (status, val) = - build_router_and_request(tx_cons_req, tx_app_req, tx_nw_req, "/health").await; - assert_eq!(status, StatusCode::OK); - assert_eq!(val, json!({"status": "ok"})); - } - - #[tokio::test] - async fn test_ready_in_sync() { - let (tx_cons_req, tx_app_req, tx_nw_req) = - MockBackend::spawn_new(MockConfig::AppGetSyncState(SyncState::InSync)); - let (status, val) = - build_router_and_request(tx_cons_req, tx_app_req, tx_nw_req, "/ready").await; - assert_eq!(status, StatusCode::OK); - assert_eq!(val, json!({"sync_state": "InSync"})); - } - - #[tokio::test] - async fn test_ready_catching_up() { - let (tx_cons_req, tx_app_req, tx_nw_req) = - MockBackend::spawn_new(MockConfig::AppGetSyncState(SyncState::CatchingUp)); - let (status, val) = - build_router_and_request(tx_cons_req, tx_app_req, tx_nw_req, "/ready").await; - assert_eq!(status, StatusCode::SERVICE_UNAVAILABLE); - assert_eq!(val, json!({"sync_state": "CatchingUp"})); - } - - #[tokio::test] - async fn test_status_success() { - let (tx_cons_req, tx_app_req, tx_nw_req) = MockBackend::spawn_new(MockConfig::AppGetStatus); - let (status, val) = - build_router_and_request(tx_cons_req, tx_app_req, tx_nw_req, "/status").await; - assert_eq!(status, StatusCode::OK); - let expected = serde_json::to_value(RpcAppStatus::from(MockBackend::a_status())).unwrap(); - assert_eq!(val, expected); - } - - #[tokio::test] - async fn test_commit_latest_success() { - let (tx_cons_req, tx_app_req, tx_nw_req) = - MockBackend::spawn_new(MockConfig::AppGetCertificate(MockValue::Present)); - let (status, val) = - build_router_and_request(tx_cons_req, tx_app_req, tx_nw_req, "/commit").await; - assert_eq!(status, StatusCode::OK); - let expected = - serde_json::to_value(RpcCommitCertificate::from(MockBackend::a_commit_cert_info())) - .unwrap(); - assert_eq!(val, expected); - } - - #[tokio::test] - async fn test_consensus_state_success() { - let (tx_cons_req, tx_app_req, tx_nw_req) = - MockBackend::spawn_new(MockConfig::ConsensusDumpState(MockValue::Present)); - let (status, val) = - build_router_and_request(tx_cons_req, tx_app_req, tx_nw_req, "/consensus-state").await; - assert_eq!(status, StatusCode::OK); - let expected = - serde_json::to_value(RpcConsensusStateDump::from(&MockBackend::a_consensus_dump())) - .unwrap(); - assert_eq!(val, expected); - } - - #[tokio::test] - async fn test_network_state_success() { - let (tx_cons_req, tx_app_req, tx_nw_req) = - MockBackend::spawn_new(MockConfig::NetworkDumpState(MockValue::Present)); - let (status, val) = - build_router_and_request(tx_cons_req, tx_app_req, tx_nw_req, "/network-state").await; - assert_eq!(status, StatusCode::OK); - let expected = - serde_json::to_value(RpcNetworkStateDump::from(MockBackend::a_network_dump())).unwrap(); - assert_eq!(val, expected); - } - - fn valid_add_persistent_peer_addr() -> serde_json::Value { - json!({ "addr": "/ip4/127.0.0.1/tcp/26656/p2p/QmNnooDu7bfjPFoTZYxMNLWUQJyrVwtbZg5gBMjTezGAJN" }) - } - - #[tokio::test] - async fn test_add_persistent_peer_success() { - let (tx_cons_req, tx_app_req, tx_nw_req) = - MockBackend::spawn_new(MockConfig::AddPersistentPeer(Ok(()))); - let (status, val) = build_router_and_request_with_body( - "POST", - tx_cons_req, - tx_app_req, - tx_nw_req, - "/persistent-peers", - valid_add_persistent_peer_addr(), - ) - .await; - assert_eq!(status, StatusCode::OK); - assert_eq!(val, json!({ "status": "ok" })); - } - - #[tokio::test] - async fn test_add_persistent_peer_already_exists() { - let (tx_cons_req, tx_app_req, tx_nw_req) = MockBackend::spawn_new( - MockConfig::AddPersistentPeer(Err(PersistentPeerError::AlreadyExists)), - ); - let (status, val) = build_router_and_request_with_body( - "POST", - tx_cons_req, - tx_app_req, - tx_nw_req, - "/persistent-peers", - valid_add_persistent_peer_addr(), - ) - .await; - assert_eq!(status, StatusCode::CONFLICT); - assert_eq!(val, json!({"error": "Persistent peer already exists"})); - } - - // For future ref: https://github.com/circlefin/malachite/pull/1485 - // #[tokio::test] - // async fn test_add_persistent_peer_missing_p2p() { - // let (tx_cons_req, tx_app_req, tx_nw_req) = - // MockBackend::spawn_new(MockConfig::NetworkDumpState(MockValue::Absent)); - // let (status, val) = build_router_and_request_with_body("POST", - // tx_cons_req, - // tx_app_req, - // tx_nw_req, - // "/persistent-peers", - // json!({ "addr": "/ip4/127.0.0.1/tcp/26656" }), - // ) - // .await; - // assert_eq!(status, StatusCode::BAD_REQUEST); - // assert!(val - // .get("error") - // .and_then(|e| e.as_str()) - // .unwrap_or("") - // .contains("/p2p/")); - // } - - #[tokio::test] - async fn test_add_persistent_peer_network_stopped() { - let (tx_cons_req, tx_app_req, tx_nw_req) = MockBackend::spawn_new( - MockConfig::AddPersistentPeer(Err(PersistentPeerError::NetworkStopped)), - ); - let (status, val) = build_router_and_request_with_body( - "POST", - tx_cons_req, - tx_app_req, - tx_nw_req, - "/persistent-peers", - valid_add_persistent_peer_addr(), - ) - .await; - assert_eq!(status, StatusCode::SERVICE_UNAVAILABLE); - assert_eq!(val, json!({"error": "Network not started"})); - } - - #[tokio::test] - async fn test_add_persistent_peer_internal_error() { - let (tx_cons_req, tx_app_req, tx_nw_req) = - MockBackend::spawn_new(MockConfig::AddPersistentPeer(Err( - PersistentPeerError::InternalError("detail".to_string()), - ))); - let (status, val) = build_router_and_request_with_body( - "POST", - tx_cons_req, - tx_app_req, - tx_nw_req, - "/persistent-peers", - valid_add_persistent_peer_addr(), - ) - .await; - assert_eq!(status, StatusCode::INTERNAL_SERVER_ERROR); - assert_eq!(val, json!({"error": "Internal error"})); - } - - #[tokio::test] - async fn test_add_persistent_peer_invalid_multiaddr() { - let (tx_cons_req, tx_app_req, tx_nw_req) = - MockBackend::spawn_new(MockConfig::NetworkDumpState(MockValue::Absent)); - let (status, val) = build_router_and_request_with_body( - "POST", - tx_cons_req, - tx_app_req, - tx_nw_req, - "/persistent-peers", - json!({ "addr": "not-a-valid-multiaddr" }), - ) - .await; - assert_eq!(status, StatusCode::BAD_REQUEST); - assert_eq!(val, json!({"error": "Invalid multiaddr"})); - } - - #[tokio::test] - async fn test_remove_persistent_peer_success() { - let (tx_cons_req, tx_app_req, tx_nw_req) = - MockBackend::spawn_new(MockConfig::RemovePersistentPeer(Ok(()))); - let (status, val) = build_router_and_request_with_body( - "DELETE", - tx_cons_req, - tx_app_req, - tx_nw_req, - "/persistent-peers", - valid_add_persistent_peer_addr(), - ) - .await; - assert_eq!(status, StatusCode::OK); - assert_eq!(val, json!({ "status": "ok" })); - } - - #[tokio::test] - async fn test_remove_persistent_peer_not_found() { - let (tx_cons_req, tx_app_req, tx_nw_req) = MockBackend::spawn_new( - MockConfig::RemovePersistentPeer(Err(PersistentPeerError::NotFound)), - ); - let (status, val) = build_router_and_request_with_body( - "DELETE", - tx_cons_req, - tx_app_req, - tx_nw_req, - "/persistent-peers", - valid_add_persistent_peer_addr(), - ) - .await; - assert_eq!(status, StatusCode::NOT_FOUND); - assert_eq!(val, json!({"error": "Persistent peer not found"})); - } - - #[tokio::test] - async fn test_remove_persistent_peer_invalid_multiaddr() { - let (tx_cons_req, tx_app_req, tx_nw_req) = - MockBackend::spawn_new(MockConfig::NetworkDumpState(MockValue::Absent)); - let (status, val) = build_router_and_request_with_body( - "DELETE", - tx_cons_req, - tx_app_req, - tx_nw_req, - "/persistent-peers", - json!({ "addr": "not-a-valid-multiaddr" }), - ) - .await; - assert_eq!(status, StatusCode::BAD_REQUEST); - assert_eq!(val, json!({"error": "Invalid multiaddr"})); - } -} diff --git a/crates/malachite-app/src/rpc/types.rs b/crates/malachite-app/src/rpc/types.rs deleted file mode 100644 index be05ab2..0000000 --- a/crates/malachite-app/src/rpc/types.rs +++ /dev/null @@ -1,1106 +0,0 @@ -// Copyright 2026 Circle Internet Group, Inc. All rights reserved. -// -// SPDX-License-Identifier: Apache-2.0 -// -// Licensed under the Apache License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. -// You may obtain a copy of the License at -// -// http://www.apache.org/licenses/LICENSE-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// See the License for the specific language governing permissions and -// limitations under the License. - -use axum::extract::FromRef; -use axum::http::StatusCode; -use axum::Json; -use chrono::{DateTime, Utc}; -use itertools::Itertools; -use serde::Serialize; -use serde_json::json; -use serde_with::{base64::Base64, serde_as}; -use std::collections::BTreeMap; -use std::time::UNIX_EPOCH; -use tokio::sync::mpsc::Sender; -use tracing::error; - -use arc_consensus_types::{ - commit_http::HttpCommitSignature, - evidence::{DoubleProposal, DoubleVote, StoredMisbehaviorEvidence, ValidatorEvidence}, - signing::PublicKey, - Address, ArcContext, BlockHash, Height, Validator, ValidatorSet, Value, ValueId, -}; -use malachitebft_app_channel::app::engine::{ - consensus::state_dump::{types::VotePerRound, StateDump, VoteKeeperState}, - network::NetworkStateDump, -}; -use malachitebft_app_channel::app::net::{Multiaddr, PeerId}; -use malachitebft_app_channel::ConsensusRequest; -use malachitebft_app_channel::ConsensusRequestError; -use malachitebft_app_channel::NetworkRequest; -use malachitebft_core_state_machine::state::{RoundValue, State as MState}; -use malachitebft_core_types::{NilOrVal, VoteType}; -use malachitebft_network::PersistentPeerError; - -use crate::request::{AppRequestError, CommitCertificateInfo, Status, TxAppReq}; -use crate::utils::sync_state::SyncState; -use alloy_rpc_types_engine::ExecutionPayloadV3; - -use arc_consensus_db::invalid_payloads::{InvalidPayload, StoredInvalidPayloads}; -use arc_consensus_types::proposal_monitor::ProposalMonitor; - -pub(crate) type TxConsensusReq = Sender>; -pub(crate) type TxNetworkReq = Sender; - -#[derive(Clone, FromRef)] -pub(crate) struct RpcState { - pub tx_consensus_req: TxConsensusReq, - pub tx_app_req: TxAppReq, - pub tx_network_req: TxNetworkReq, -} - -pub(crate) struct RouteDef { - pub method: &'static str, - pub path: &'static str, - pub handler: fn() -> axum::routing::MethodRouter, - pub doc: EndpointInfo, -} - -macro_rules! method_str { - (get) => { - "GET" - }; - (post) => { - "POST" - }; - (delete) => { - "DELETE" - }; -} - -macro_rules! routes { - ( $( $route_def:expr ),* $(,)? ) => { - fn build_routes() -> Vec { - vec![ $( $route_def ),* ] - } - }; -} - -macro_rules! route { - ($method:ident, $path:expr, $handler_fn:path, $desc:expr) => { - route!(@build $method, $path, $handler_fn, $desc, None) - }; - ($method:ident, $path:expr, $handler_fn:path, $desc:expr, params = { $( $pkey:expr => $pval:expr ),* $(,)? }) => { - route!(@build $method, $path, $handler_fn, $desc, Some(::std::collections::BTreeMap::from([ $( ($pkey, $pval) ),* ]))) - }; - (@build $method:ident, $path:expr, $handler_fn:path, $desc:expr, $params:expr) => { - crate::rpc::types::RouteDef { - method: method_str!($method), - path: $path, - handler: || axum::routing::$method($handler_fn), - doc: crate::rpc::types::EndpointInfo { - desc: $desc, - params: $params, - }, - } - }; -} - -#[derive(serde::Serialize)] -pub(crate) struct EndpointInfo { - pub desc: &'static str, - pub params: Option>, -} - -#[derive(Serialize)] -pub(crate) struct RpcVersion { - pub git_version: &'static str, - pub git_commit: &'static str, - pub git_short_hash: &'static str, - pub cargo_version: &'static str, -} - -#[derive(Serialize)] -pub(crate) struct RpcNetworkStateDump { - local_node: RpcLocalNodeInfo, - peers: Vec, - persistent_peer_ids: Vec, - persistent_peer_addrs: Vec, - validator_set: RpcNwValidatorSet, -} - -#[derive(Serialize)] -struct RpcLocalNodeInfo { - moniker: String, - peer_id: PeerId, - listen_addr: Multiaddr, - consensus_address: Option, - is_validator: bool, - persistent_peers_only: bool, - subscribed_topics: Vec, -} - -#[derive(Serialize)] -struct RpcPeerInfo { - peer_id: PeerId, - p2p_address: Multiaddr, - consensus_address: Option, - moniker: String, - peer_type: String, - connection_direction: Option, - score: f64, - topics: Vec, -} - -impl From for RpcNetworkStateDump { - fn from(s: NetworkStateDump) -> Self { - let local_node = RpcLocalNodeInfo { - moniker: s.local_node.moniker, - peer_id: s.local_node.peer_id, - listen_addr: s.local_node.listen_addr, - consensus_address: s.local_node.consensus_address, - is_validator: s.local_node.is_validator, - persistent_peers_only: s.local_node.persistent_peers_only, - subscribed_topics: { - let mut v = s - .local_node - .subscribed_topics - .into_iter() - .collect::>(); - v.sort(); - v - }, - }; - - let peers = s - .peers - .into_iter() - .map(|(peer_id, info)| RpcPeerInfo { - peer_id, - p2p_address: info.address, - consensus_address: info.consensus_address, - moniker: info.moniker, - peer_type: info.peer_type.primary_type_str().to_string(), - connection_direction: info.connection_direction.map(|d| d.as_str().to_string()), - score: info.score, - topics: { - let mut t = info.topics.iter().cloned().collect::>(); - t.sort(); - t - }, - }) - .sorted_by_key(|p| (p.peer_type.clone(), p.moniker.clone())) - .collect::>(); - - let validator_pairs = s - .validator_set - .into_iter() - .map(|v| (v.address, v.voting_power)) - .collect::>(); - let validator_set = build_network_validator_set(&validator_pairs); - - RpcNetworkStateDump { - local_node, - peers, - persistent_peer_ids: s.persistent_peer_ids, - persistent_peer_addrs: s.persistent_peer_addrs, - validator_set, - } - } -} - -#[derive(Serialize)] -pub(crate) struct RpcNwValidatorSet { - total_voting_power: u64, - count: usize, - validators: Vec, -} - -#[derive(Serialize)] -pub(crate) struct RpcNwValidatorInfo { - address: String, - voting_power: u64, -} - -pub(crate) fn build_network_validator_set(vs: &[(String, u64)]) -> RpcNwValidatorSet { - // Voting powers are small protocol values; sum fits in u64 - #[allow(clippy::arithmetic_side_effects)] - let total_voting_power: u64 = vs.iter().map(|(_, vp)| *vp).sum(); - let validators = vs - .iter() - .map(|(addr, vp)| RpcNwValidatorInfo { - address: addr.clone(), - voting_power: *vp, - }) - .collect(); - RpcNwValidatorSet { - total_voting_power, - count: vs.len(), - validators, - } -} - -#[derive(serde::Deserialize)] -pub(crate) struct GetCertificateParams { - pub height: Option, -} - -#[derive(serde::Deserialize)] -pub(crate) struct GetProposalMonitorParams { - pub height: Option, -} - -#[derive(serde::Deserialize)] -pub(crate) struct AddOrRemovePersistentPeerBody { - pub addr: String, -} - -#[derive(Serialize)] -pub(crate) struct RpcProposalMonitorData { - pub height: u64, - pub proposer: Address, - pub start_time: DateTime, - pub proposal_receive_time: Option>, - pub value_id: Option, - pub successful: Option, - pub synced: bool, - pub proposal_delay_ms: Option, -} - -impl From for RpcProposalMonitorData { - fn from(data: ProposalMonitor) -> Self { - let start_time: DateTime = data.start_time.into(); - let proposal_receive_time: Option> = - data.proposal_receive_time.map(Into::into); - - // It can be negative - #[allow(clippy::cast_possible_truncation, clippy::arithmetic_side_effects)] - let proposal_delay_ms = data.proposal_receive_time.map(|receive| { - let start_ms = data - .start_time - .duration_since(UNIX_EPOCH) - .unwrap_or_default() - .as_millis() as i64; - let receive_ms = receive - .duration_since(UNIX_EPOCH) - .unwrap_or_default() - .as_millis() as i64; - receive_ms - start_ms - }); - - RpcProposalMonitorData { - height: data.height.as_u64(), - proposer: data.proposer, - start_time, - proposal_receive_time, - value_id: data.value_id, - successful: data.successful.to_bool(), - synced: data.synced, - proposal_delay_ms, - } - } -} - -#[derive(Serialize)] -pub(crate) struct RpcAppStatus { - height: u64, - round: i64, - address: Address, - public_key: String, - proposer: Address, - height_start_time: DateTime, - prev_payload_hash: Option, - db_latest_height: u64, - db_earliest_height: u64, - undecided_blocks_count: usize, - pending_proposal_parts: Vec, - validator_set: RpcValidatorSet, - sync_state: SyncState, -} - -impl From for RpcAppStatus { - fn from(status: Status) -> Self { - RpcAppStatus { - height: status.height.as_u64(), - round: status.round.as_i64(), - address: status.address, - public_key: format!("0x{}", hex::encode(status.public_key.as_bytes())), - proposer: status.proposer.unwrap_or_else(|| Address::repeat_byte(0)), - height_start_time: status.height_start_time.into(), - prev_payload_hash: status.prev_payload_hash, - db_latest_height: status.db_latest_height.as_u64(), - db_earliest_height: status.db_earliest_height.as_u64(), - validator_set: RpcValidatorSet::from(&status.validator_set), - undecided_blocks_count: status.undecided_blocks_count, - pending_proposal_parts: status - .pending_proposal_parts - .into_iter() - .map(|(height, count)| RpcPendingProposalParts { - height: height.as_u64(), - count, - }) - .collect(), - sync_state: status.sync_state, - } - } -} - -#[derive(Serialize)] -struct RpcPendingProposalParts { - height: u64, - count: usize, -} - -#[derive(Serialize)] -pub(crate) struct RpcCommitCertificate { - height: u64, - round: i64, - block_hash: ValueId, - signatures: Vec, - proposer: Address, - extended: bool, -} - -impl From for RpcCommitCertificate { - fn from(info: CommitCertificateInfo) -> Self { - RpcCommitCertificate { - height: info.certificate.height.as_u64(), - round: info.certificate.round.as_i64(), - block_hash: info.certificate.value_id, - signatures: info - .certificate - .commit_signatures - .into_iter() - .map(Into::into) - .collect(), - proposer: info.proposer, - extended: info.certificate_type.is_extended(), - } - } -} - -#[derive(Serialize)] -pub(crate) struct RpcConsensusStateDump { - address: Address, - proposer: Option
, - validator_set: RpcValidatorSet, - consensus: RpcConsensus, - vote_keeper: RpcVoteKeeperState, -} - -#[derive(Serialize)] -struct RpcValidatorSet { - total_voting_power: u64, - count: usize, - validators: Vec, -} - -#[derive(Serialize)] -struct RpcValidator { - address: Address, - voting_power: u64, - public_key: PublicKey, - public_key_hex: String, -} - -#[derive(Serialize)] -struct RpcConsensus { - height: u64, - round: i64, - step: String, - locked: Option, - valid: Option, - decision: Option, -} - -#[derive(Serialize)] -struct RpcRoundVotes { - round: i64, - prevotes: VoteData, - precommits: VoteData, -} - -#[derive(Serialize)] -struct RpcVoteKeeperState { - total_validators: usize, - rounds: Vec, -} - -impl RpcVoteKeeperState { - fn new(vk: &VoteKeeperState, val_set: &ValidatorSet) -> Self { - let rounds = vk - .votes - .iter() - .map(|(round, per_round)| { - let prevotes = VoteData::new(val_set, per_round, VoteType::Prevote); - let precommits = VoteData::new(val_set, per_round, VoteType::Precommit); - - RpcRoundVotes { - round: round.as_i64(), - prevotes, - precommits, - } - }) - .collect(); - - Self { - total_validators: val_set.len(), - rounds, - } - } -} - -#[derive(Serialize)] -struct VoteData { - votes: Vec, - #[serde(flatten)] - details: VotesDetails, -} - -#[derive(Serialize)] -struct RpcVote { - address: Address, - value: Option, -} - -#[derive(Serialize)] -struct VotesDetails { - total_validators_count: usize, - total_voting_power: u64, - validators_count: usize, - voting_power: u64, - fraction: f64, - bit_array: String, -} - -impl VotesDetails { - #[allow(clippy::arithmetic_side_effects, clippy::cast_possible_truncation)] - fn new(val_set: &ValidatorSet, voted: &[RpcVote]) -> Self { - const X_GROUP: usize = 5; - - let total_validators_count = val_set.len(); - let total_voting_power = val_set.total_voting_power(); - - let mut voting_power = 0u64; - let mut bit_array = - String::with_capacity(total_validators_count + total_validators_count / X_GROUP); - - for (i, v) in val_set.iter().enumerate() { - if i > 0 && i % X_GROUP == 0 { - bit_array.push(' '); - } - if voted.iter().any(|d| d.address == v.address) { - bit_array.push('X'); - voting_power += v.voting_power; - } else { - bit_array.push('_'); - } - } - - let fraction = if total_voting_power > 0 { - voting_power as f64 / total_voting_power as f64 - } else { - 0.0 - }; - - Self { - total_validators_count, - total_voting_power, - validators_count: voted.len(), - voting_power, - fraction, - bit_array, - } - } -} - -impl VoteData { - fn new( - val_set: &ValidatorSet, - per_round: &VotePerRound, - vote_type: VoteType, - ) -> Self { - let to_option = |v: NilOrVal| match v { - NilOrVal::Nil => None, - NilOrVal::Val(val) => Some(val), - }; - - let votes = per_round - .received_votes() - .iter() - .filter(|sv| sv.typ == vote_type) - .map(|sv| RpcVote { - address: sv.validator_address, - value: to_option(sv.value), - }) - .collect::>(); - - let details = VotesDetails::new(val_set, &votes); - - Self { votes, details } - } -} - -#[derive(Serialize)] -struct RpcRoundValue { - value: Value, - round: i64, -} - -impl From<&StateDump> for RpcConsensusStateDump { - fn from(s: &StateDump) -> Self { - RpcConsensusStateDump { - address: s.address, - proposer: s.proposer, - validator_set: RpcValidatorSet::from(&s.validator_set), - consensus: RpcConsensus::from(&s.consensus), - vote_keeper: RpcVoteKeeperState::new(&s.vote_keeper, &s.validator_set), - } - } -} - -impl From<&ValidatorSet> for RpcValidatorSet { - fn from(vs: &ValidatorSet) -> Self { - RpcValidatorSet { - total_voting_power: vs.total_voting_power(), - count: vs.len(), - validators: vs.iter().map(RpcValidator::from).collect(), - } - } -} - -impl From<&Validator> for RpcValidator { - fn from(v: &Validator) -> Self { - RpcValidator { - address: v.address, - voting_power: v.voting_power, - public_key_hex: format!("0x{}", hex::encode(v.public_key.as_bytes())), - public_key: v.public_key, - } - } -} - -impl From<&MState> for RpcConsensus { - fn from(c: &MState) -> Self { - RpcConsensus { - height: c.height.as_u64(), - round: c.round.as_i64(), - step: format!("{:?}", c.step), - locked: c.locked.as_ref().map(RpcRoundValue::from), - valid: c.valid.as_ref().map(RpcRoundValue::from), - decision: c.decision.as_ref().map(RpcRoundValue::from), - } - } -} - -impl From<&RoundValue> for RpcRoundValue { - fn from(rv: &RoundValue) -> Self { - RpcRoundValue { - value: rv.value.clone(), - round: rv.round.as_i64(), - } - } -} - -#[derive(Copy, Clone, Debug)] -pub(crate) enum RequestError { - Full, - Closed, - Recv, -} - -impl From for RequestError { - fn from(e: AppRequestError) -> Self { - match e { - AppRequestError::Full => RequestError::Full, - AppRequestError::Closed => RequestError::Closed, - AppRequestError::Recv => RequestError::Recv, - } - } -} - -impl From for RequestError { - fn from(e: ConsensusRequestError) -> Self { - match e { - ConsensusRequestError::Full => RequestError::Full, - ConsensusRequestError::Closed => RequestError::Closed, - ConsensusRequestError::Recv => RequestError::Recv, - } - } -} - -pub(crate) fn request_error_to_response( - err: impl Into, -) -> (StatusCode, Json) { - match err.into() { - RequestError::Full => ( - StatusCode::TOO_MANY_REQUESTS, - Json(json!({"error": "Too many requests. Please retry later."})), - ), - RequestError::Closed => ( - StatusCode::INTERNAL_SERVER_ERROR, - Json(json!({"error": "Request channel is closed"})), - ), - RequestError::Recv => ( - StatusCode::INTERNAL_SERVER_ERROR, - Json(json!({"error": "Failed to receive response"})), - ), - } -} - -pub(crate) fn persistent_peer_error_to_response( - err: PersistentPeerError, -) -> (StatusCode, Json) { - match err { - PersistentPeerError::AlreadyExists => ( - StatusCode::CONFLICT, - Json(json!({"error": "Persistent peer already exists"})), - ), - PersistentPeerError::NotFound => ( - StatusCode::NOT_FOUND, - Json(json!({"error": "Persistent peer not found"})), - ), - PersistentPeerError::NetworkStopped => ( - StatusCode::SERVICE_UNAVAILABLE, - Json(json!({"error": "Network not started"})), - ), - PersistentPeerError::InternalError(msg) => { - error!(%msg, "Persistent peer operation failed"); - ( - StatusCode::INTERNAL_SERVER_ERROR, - Json(json!({"error": "Internal error"})), - ) - } - } -} - -#[derive(Serialize)] -pub(crate) struct RpcMisbehaviorEvidence { - height: u64, - validators: Vec, -} - -#[derive(Serialize)] -struct RpcValidatorEvidence { - address: Address, - double_votes: Vec, - double_proposals: Vec, -} - -#[serde_as] -#[derive(Serialize)] -struct RpcDoubleVote { - height: u64, - round: i64, - vote_type: String, - #[serde_as(as = "Base64")] - first_signature: Vec, - #[serde_as(as = "Base64")] - second_signature: Vec, - first_value_id: Option, - second_value_id: Option, -} - -#[serde_as] -#[derive(Serialize)] -struct RpcDoubleProposal { - height: u64, - round: i64, - #[serde_as(as = "Base64")] - first_signature: Vec, - #[serde_as(as = "Base64")] - second_signature: Vec, - first_value_id: ValueId, - second_value_id: ValueId, -} - -impl From for RpcMisbehaviorEvidence { - fn from(e: StoredMisbehaviorEvidence) -> Self { - RpcMisbehaviorEvidence { - height: e.height.as_u64(), - validators: e.validators.into_iter().map(Into::into).collect(), - } - } -} - -impl From for RpcValidatorEvidence { - fn from(e: ValidatorEvidence) -> Self { - RpcValidatorEvidence { - address: e.address, - double_votes: e.double_votes.into_iter().map(Into::into).collect(), - double_proposals: e.double_proposals.into_iter().map(Into::into).collect(), - } - } -} - -impl From for RpcDoubleVote { - fn from(v: DoubleVote) -> Self { - RpcDoubleVote { - height: v.first.message.height.as_u64(), - round: v.first.message.round.as_i64(), - vote_type: format!("{:?}", v.first.message.typ), - first_signature: v.first.signature.to_bytes().to_vec(), - second_signature: v.second.signature.to_bytes().to_vec(), - first_value_id: match v.first.message.value { - NilOrVal::Nil => None, - NilOrVal::Val(id) => Some(id), - }, - second_value_id: match v.second.message.value { - NilOrVal::Nil => None, - NilOrVal::Val(id) => Some(id), - }, - } - } -} - -impl From for RpcDoubleProposal { - fn from(p: DoubleProposal) -> Self { - RpcDoubleProposal { - height: p.first.message.height.as_u64(), - round: p.first.message.round.as_i64(), - first_signature: p.first.signature.to_bytes().to_vec(), - second_signature: p.second.signature.to_bytes().to_vec(), - first_value_id: p.first.message.value.id(), - second_value_id: p.second.message.value.id(), - } - } -} - -#[derive(Serialize)] -pub(crate) struct RpcInvalidPayloads { - height: u64, - payloads: Vec, -} - -#[derive(Serialize)] -struct RpcInvalidPayload { - height: u64, - round: i64, - proposer_address: Address, - payload: Option, - reason: String, -} - -impl From for RpcInvalidPayloads { - fn from(s: StoredInvalidPayloads) -> Self { - RpcInvalidPayloads { - height: s.height.as_u64(), - payloads: s.payloads.into_iter().map(Into::into).collect(), - } - } -} - -impl From for RpcInvalidPayload { - fn from(p: InvalidPayload) -> Self { - RpcInvalidPayload { - height: p.height.as_u64(), - round: p.round.as_i64(), - proposer_address: p.proposer_address, - payload: p.payload, - reason: p.reason, - } - } -} - -#[cfg(test)] -mod tests_network { - use super::*; - - #[test] - fn test_network_validator_set_empty() { - let vs: Vec<(String, u64)> = vec![]; - let rpc = build_network_validator_set(&vs); - assert_eq!(rpc.total_voting_power, 0); - assert_eq!(rpc.count, 0); - assert!(rpc.validators.is_empty()); - } - - #[test] - fn test_network_validator_set_basic() { - let vs = vec![ - ("addr1".to_string(), 10), - ("addr2".to_string(), 20), - ("addr3".to_string(), 30), - ]; - - let rpc = build_network_validator_set(&vs); - - assert_eq!(rpc.total_voting_power, 60); - assert_eq!(rpc.count, 3); - assert_eq!(rpc.validators.len(), 3); - - assert_eq!(rpc.validators[0].address, "addr1"); - assert_eq!(rpc.validators[0].voting_power, 10); - assert_eq!(rpc.validators[1].address, "addr2"); - assert_eq!(rpc.validators[1].voting_power, 20); - assert_eq!(rpc.validators[2].address, "addr3"); - assert_eq!(rpc.validators[2].voting_power, 30); - } -} - -#[cfg(test)] -mod tests_errors { - use super::*; - - #[test] - fn test_request_error_full_maps_to_429() { - let (status, body) = request_error_to_response(RequestError::Full); - assert_eq!(status, StatusCode::TOO_MANY_REQUESTS); - assert_eq!( - body.0, - json!({"error": "Too many requests. Please retry later."}) - ); - } - - #[test] - fn test_request_error_closed_maps_to_500() { - let (status, body) = request_error_to_response(RequestError::Closed); - assert_eq!(status, StatusCode::INTERNAL_SERVER_ERROR); - assert_eq!(body.0, json!({"error": "Request channel is closed"})); - } - - #[test] - fn test_request_error_recv_maps_to_500() { - let (status, body) = request_error_to_response(RequestError::Recv); - assert_eq!(status, StatusCode::INTERNAL_SERVER_ERROR); - assert_eq!(body.0, json!({"error": "Failed to receive response"})); - } -} - -#[cfg(test)] -mod tests_misbehavior_evidence { - use super::*; - use alloy_primitives::Address as AlloyAddress; - use arc_consensus_types::signing::Signature; - use arc_consensus_types::{Address, Height, Round, Value, Vote}; - use malachitebft_core_types::{NilOrVal, SignedMessage, SignedProposal}; - - fn create_test_vote( - height: Height, - round: Round, - value: NilOrVal, - address: Address, - sig_byte: u8, - ) -> SignedMessage { - let vote = Vote::new_precommit(height, round, value, address); - let signature = Signature::from_bytes([sig_byte; 64]); - SignedMessage::new(vote, signature) - } - - fn create_test_proposal( - height: Height, - round: Round, - value: Value, - address: Address, - sig_byte: u8, - ) -> SignedProposal { - let proposal = - arc_consensus_types::Proposal::new(height, round, value, Round::Nil, address); - let signature = Signature::from_bytes([sig_byte; 64]); - SignedProposal::new(proposal, signature) - } - - #[test] - fn test_empty_evidence_conversion() { - let evidence = StoredMisbehaviorEvidence { - height: Height::new(100), - validators: vec![], - }; - - let rpc: RpcMisbehaviorEvidence = evidence.into(); - - assert_eq!(rpc.height, 100); - assert!(rpc.validators.is_empty()); - } - - #[test] - fn test_evidence_with_double_votes() { - let height = Height::new(42); - let round = Round::new(1); - let address = Address::from(AlloyAddress::from([0xaa; 20])); - - let value_id_1 = ValueId::new(BlockHash::repeat_byte(0x11)); - let value_id_2 = ValueId::new(BlockHash::repeat_byte(0x22)); - - let vote1 = create_test_vote(height, round, NilOrVal::Val(value_id_1), address, 0x01); - let vote2 = create_test_vote(height, round, NilOrVal::Val(value_id_2), address, 0x02); - - let double_vote = DoubleVote { - first: vote1, - second: vote2, - }; - - let validator_evidence = ValidatorEvidence { - address, - double_votes: vec![double_vote], - double_proposals: vec![], - }; - - let evidence = StoredMisbehaviorEvidence { - height, - validators: vec![validator_evidence], - }; - - let rpc: RpcMisbehaviorEvidence = evidence.into(); - - assert_eq!(rpc.height, 42); - assert_eq!(rpc.validators.len(), 1); - assert_eq!(rpc.validators[0].address, address); - assert_eq!(rpc.validators[0].double_votes.len(), 1); - assert!(rpc.validators[0].double_proposals.is_empty()); - - let dv = &rpc.validators[0].double_votes[0]; - assert_eq!(dv.round, 1); - assert_eq!(dv.vote_type, "Precommit"); - assert_eq!(dv.first_signature, vec![0x01; 64]); - assert_eq!(dv.second_signature, vec![0x02; 64]); - assert_eq!(dv.first_value_id, Some(value_id_1)); - assert_eq!(dv.second_value_id, Some(value_id_2)); - } - - #[test] - fn test_evidence_with_nil_votes() { - let height = Height::new(50); - let round = Round::new(2); - let address = Address::from(AlloyAddress::from([0xbb; 20])); - - let value_id = ValueId::new(BlockHash::repeat_byte(0x33)); - - // One vote for a value, one nil vote - let vote1 = create_test_vote(height, round, NilOrVal::Val(value_id), address, 0x03); - let vote2 = create_test_vote(height, round, NilOrVal::Nil, address, 0x04); - - let double_vote = DoubleVote { - first: vote1, - second: vote2, - }; - - let validator_evidence = ValidatorEvidence { - address, - double_votes: vec![double_vote], - double_proposals: vec![], - }; - - let evidence = StoredMisbehaviorEvidence { - height, - validators: vec![validator_evidence], - }; - - let rpc: RpcMisbehaviorEvidence = evidence.into(); - - let dv = &rpc.validators[0].double_votes[0]; - assert_eq!(dv.first_value_id, Some(value_id)); - assert_eq!(dv.second_value_id, None); - } - - #[test] - fn test_evidence_with_double_proposals() { - let height = Height::new(99); - let round = Round::new(0); - let address = Address::from(AlloyAddress::from([0xcc; 20])); - - let value1 = Value::new(BlockHash::repeat_byte(0x44)); - let value2 = Value::new(BlockHash::repeat_byte(0x55)); - - let proposal1 = create_test_proposal(height, round, value1.clone(), address, 0x05); - let proposal2 = create_test_proposal(height, round, value2.clone(), address, 0x06); - - let double_proposal = DoubleProposal { - first: proposal1, - second: proposal2, - }; - - let validator_evidence = ValidatorEvidence { - address, - double_votes: vec![], - double_proposals: vec![double_proposal], - }; - - let evidence = StoredMisbehaviorEvidence { - height, - validators: vec![validator_evidence], - }; - - let rpc: RpcMisbehaviorEvidence = evidence.into(); - - assert_eq!(rpc.height, 99); - assert_eq!(rpc.validators.len(), 1); - assert!(rpc.validators[0].double_votes.is_empty()); - assert_eq!(rpc.validators[0].double_proposals.len(), 1); - - let dp = &rpc.validators[0].double_proposals[0]; - assert_eq!(dp.round, 0); - assert_eq!(dp.first_signature, vec![0x05; 64]); - assert_eq!(dp.second_signature, vec![0x06; 64]); - assert_eq!(dp.first_value_id, value1.id()); - assert_eq!(dp.second_value_id, value2.id()); - } - - #[test] - fn test_evidence_with_multiple_validators() { - let height = Height::new(200); - let round = Round::new(3); - - let addr1 = Address::from(AlloyAddress::from([0xdd; 20])); - let addr2 = Address::from(AlloyAddress::from([0xee; 20])); - - let value_id_1 = ValueId::new(BlockHash::repeat_byte(0x66)); - let value_id_2 = ValueId::new(BlockHash::repeat_byte(0x77)); - - // First validator with vote equivocation - let vote1 = create_test_vote(height, round, NilOrVal::Val(value_id_1), addr1, 0x07); - let vote2 = create_test_vote(height, round, NilOrVal::Val(value_id_2), addr1, 0x08); - - let double_vote = DoubleVote { - first: vote1, - second: vote2, - }; - - let validator1 = ValidatorEvidence { - address: addr1, - double_votes: vec![double_vote], - double_proposals: vec![], - }; - - // Second validator with proposal equivocation - let value1 = Value::new(BlockHash::repeat_byte(0x88)); - let value2 = Value::new(BlockHash::repeat_byte(0x99)); - - let proposal1 = create_test_proposal(height, round, value1, addr2, 0x09); - let proposal2 = create_test_proposal(height, round, value2, addr2, 0x0a); - - let double_proposal = DoubleProposal { - first: proposal1, - second: proposal2, - }; - - let validator2 = ValidatorEvidence { - address: addr2, - double_votes: vec![], - double_proposals: vec![double_proposal], - }; - - let evidence = StoredMisbehaviorEvidence { - height, - validators: vec![validator1, validator2], - }; - - let rpc: RpcMisbehaviorEvidence = evidence.into(); - - assert_eq!(rpc.height, 200); - assert_eq!(rpc.validators.len(), 2); - - // First validator - assert_eq!(rpc.validators[0].address, addr1); - assert_eq!(rpc.validators[0].double_votes.len(), 1); - assert!(rpc.validators[0].double_proposals.is_empty()); - - // Second validator - assert_eq!(rpc.validators[1].address, addr2); - assert!(rpc.validators[1].double_votes.is_empty()); - assert_eq!(rpc.validators[1].double_proposals.len(), 1); - } -} diff --git a/crates/malachite-app/src/rpc/version.rs b/crates/malachite-app/src/rpc/version.rs deleted file mode 100644 index d00d916..0000000 --- a/crates/malachite-app/src/rpc/version.rs +++ /dev/null @@ -1,187 +0,0 @@ -// Copyright 2025 Circle Internet Group, Inc. All rights reserved. -// -// SPDX-License-Identifier: Apache-2.0 -// -// Licensed under the Apache License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. -// You may obtain a copy of the License at -// -// http://www.apache.org/licenses/LICENSE-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// See the License for the specific language governing permissions and -// limitations under the License. - -//! API versioning types and utilities - -use std::fmt; -use std::str::FromStr; - -/// Vendor-specific media type prefix for Arc Network consensus API -pub const MEDIA_TYPE_PREFIX: &str = "application/vnd.arc.v"; - -/// Fallback media type (generic JSON) -pub const MEDIA_TYPE_JSON: &str = "application/json"; - -/// Any media type -pub const MEDIA_TYPE_ANY: &str = "*/*"; - -/// API version for the consensus layer REST endpoints -#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)] -pub enum ApiVersion { - /// Version 1 (initial version) - #[default] - V1, -} - -impl ApiVersion { - /// Returns the version number as a u32 - pub fn as_number(&self) -> u32 { - match self { - Self::V1 => 1, - } - } - - /// Returns the full media type for this version - pub fn media_type(&self) -> String { - format!("{}{}+json", MEDIA_TYPE_PREFIX, self.as_number()) - } - - /// Parses an Accept header value to extract the API version - /// - /// Supported formats: - /// - `application/vnd.arc.v1+json` -> V1 - /// - `application/json` -> default (V1) - /// - Missing/empty -> default (V1) - /// - /// Returns `None` if the header specifies an unsupported version or the - /// format is unrecognized/malformed (e.g. "text/html") - pub fn from_accept_header(value: &str) -> Option { - let trimmed = value.trim(); - - // Empty or generic JSON defaults to V1 - if trimmed.is_empty() || trimmed == MEDIA_TYPE_JSON || trimmed == MEDIA_TYPE_ANY { - return Some(Self::default()); - } - - // Parse versioned media type - if let Some(version_part) = trimmed.strip_prefix(MEDIA_TYPE_PREFIX) { - if let Some(version_str) = version_part.strip_suffix("+json") { - return ApiVersion::from_str(version_str).ok(); - } - } - - // Unrecognized/malformed format defaults to None - None - } -} - -impl fmt::Display for ApiVersion { - fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { - write!(f, "v{}", self.as_number()) - } -} - -impl FromStr for ApiVersion { - type Err = ParseVersionError; - - fn from_str(s: &str) -> Result { - match s { - "v1" | "1" => Ok(Self::V1), - _ => Err(ParseVersionError), - } - } -} - -/// Error returned when parsing an API version fails -#[derive(Debug, Clone, Copy)] -pub struct ParseVersionError; - -impl fmt::Display for ParseVersionError { - fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { - write!(f, "invalid API version") - } -} - -impl std::error::Error for ParseVersionError {} - -#[cfg(test)] -mod tests { - use super::*; - - #[test] - fn test_api_version_default() { - assert_eq!(ApiVersion::default(), ApiVersion::V1); - assert_eq!(ApiVersion::V1.as_number(), 1); - } - - #[test] - fn test_api_version_display() { - assert_eq!(format!("{}", ApiVersion::V1), "v1"); - } - - #[test] - fn test_api_version_media_type() { - assert_eq!(ApiVersion::V1.media_type(), "application/vnd.arc.v1+json"); - } - - #[test] - fn test_from_accept_header_v1() { - assert_eq!( - ApiVersion::from_accept_header("application/vnd.arc.v1+json"), - Some(ApiVersion::V1) - ); - } - - #[test] - fn test_from_accept_header_generic_json() { - assert_eq!( - ApiVersion::from_accept_header("application/json"), - Some(ApiVersion::V1) - ); - } - - #[test] - fn test_from_accept_header_any() { - assert_eq!(ApiVersion::from_accept_header("*/*"), Some(ApiVersion::V1)); - } - - #[test] - fn test_from_accept_header_empty() { - assert_eq!(ApiVersion::from_accept_header(""), Some(ApiVersion::V1)); - assert_eq!(ApiVersion::from_accept_header(" "), Some(ApiVersion::V1)); - } - - #[test] - fn test_from_accept_header_unsupported() { - assert_eq!( - ApiVersion::from_accept_header("application/vnd.arc.v99+json"), - None - ); - assert_eq!( - ApiVersion::from_accept_header("application/vnd.arc.v2+json"), - None - ); - } - - #[test] - fn test_from_accept_header_malformed() { - // Malformed formats default to None - assert_eq!(ApiVersion::from_accept_header("text/html"), None); - assert_eq!( - ApiVersion::from_accept_header("application/vnd.arc.v1"), - None - ); - assert_eq!(ApiVersion::from_accept_header("something/random"), None); - } - - #[test] - fn test_from_str() { - assert_eq!("v1".parse::().unwrap(), ApiVersion::V1); - assert_eq!("1".parse::().unwrap(), ApiVersion::V1); - assert!("v2".parse::().is_err()); - assert!("invalid".parse::().is_err()); - } -} diff --git a/crates/malachite-app/src/rpc_sync/client.rs b/crates/malachite-app/src/rpc_sync/client.rs deleted file mode 100644 index d97e663..0000000 --- a/crates/malachite-app/src/rpc_sync/client.rs +++ /dev/null @@ -1,304 +0,0 @@ -// Copyright 2026 Circle Internet Group, Inc. All rights reserved. -// -// SPDX-License-Identifier: Apache-2.0 -// -// Licensed under the Apache License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. -// You may obtain a copy of the License at -// -// http://www.apache.org/licenses/LICENSE-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// See the License for the specific language governing permissions and -// limitations under the License. - -//! RPC Client for fetching blocks from remote endpoints -//! -//! This client fetches commit certificates and execution payloads -//! from trusted RPC endpoints. It is used by the network actor to -//! fulfill block requests from malachite's sync actor. -//! -//! Note: Validator sets are not fetched here, consensus uses its own -//! validator set state to verify signatures. - -use std::ops::RangeInclusive; -use std::time::{Duration, Instant}; - -use alloy_rpc_types_engine::ExecutionPayloadV3; -use alloy_rpc_types_eth::Block; -use bytes::Bytes; -use eyre::Context; -use reqwest::Client; -use serde::Deserialize; -use serde_json::{json, Value}; -use tracing::debug; -use url::Url; - -use malachitebft_core_types::utils::height::HeightRangeExt; -use malachitebft_core_types::CommitCertificate; - -use arc_consensus_types::commit_http::HttpCommitCertificate; -use arc_consensus_types::{ArcContext, Height}; - -use crate::utils::pretty::Pretty; - -/// A block with its associated data (certificate + payload) -#[derive(Debug, Clone)] -pub struct SyncedBlock { - pub height: Height, - pub certificate: CommitCertificate, - pub payload: ExecutionPayloadV3, - pub value_bytes: Bytes, -} - -/// RPC client for fetching blocks -#[derive(Clone)] -pub struct RpcSyncClient { - client: Client, -} - -impl Default for RpcSyncClient { - fn default() -> Self { - Self::new() - } -} - -impl RpcSyncClient { - /// Create a new RPC sync client with connection pooling - pub fn new() -> Self { - let client = Client::builder() - .pool_max_idle_per_host(10) - .pool_idle_timeout(Duration::from_secs(30)) - .tcp_keepalive(Duration::from_secs(60)) - .timeout(Duration::from_secs(30)) - .build() - .expect("Failed to build HTTP client"); - - Self { client } - } - - /// Fetch a batch of blocks from a specific endpoint. - /// The caller, malachite sync actor, handles peer selection and retry logic. - pub async fn fetch_blocks_batch( - &self, - endpoint: &Url, - range: &RangeInclusive, - ) -> eyre::Result> { - self.fetch_blocks_batch_impl(endpoint, range).await - } - - async fn fetch_blocks_batch_impl( - &self, - endpoint: &Url, - range: &RangeInclusive, - ) -> eyre::Result> { - let mut blocks = Vec::with_capacity(range.len()); - - // Build batch requests for certificates and payloads only - // Note: Validator sets are NOT needed - consensus has its own validator set state - let mut cert_requests = Vec::with_capacity(range.len()); - let mut block_requests = Vec::with_capacity(range.len()); - - for height in range.clone().iter_heights() { - let height_hex = format!("0x{:x}", height.as_u64()); - - cert_requests.push(json!({ - "jsonrpc": "2.0", - "method": "arc_getCertificate", - "params": [height.as_u64()], - "id": height.as_u64() - })); - - block_requests.push(json!({ - "jsonrpc": "2.0", - "method": "eth_getBlockByNumber", - "params": [height_hex, true], - "id": height.as_u64() - })); - } - - // Send both batch requests in parallel - let start = Instant::now(); - - let (cert_result, payload_result) = tokio::join!( - self.send_batch_request::(endpoint, &cert_requests), - self.send_batch_request::(endpoint, &block_requests), - ); - - let elapsed = start.elapsed(); - - let cert_responses = cert_result.wrap_err("Failed to fetch certificates")?; - let block_responses = payload_result.wrap_err("Failed to fetch blocks")?; - - debug!( - %endpoint, - range = %Pretty(range), - ?elapsed, - certs = cert_responses.len(), - blocks = block_responses.len(), - "Fetched batch", - ); - - // Verify we got exactly what we asked for (zip silently drops extras) - let expected_count = range.len(); - let (cert_count, block_count) = (cert_responses.len(), block_responses.len()); - if cert_count != expected_count || block_count != expected_count { - return Err(eyre::eyre!( - "Response count mismatch: expected {expected_count}, got {cert_count} certificates and {block_count} blocks", - )); - } - - // Parse responses and build blocks, verifying heights match exactly what we requested - for ((height, rpc_cert), block) in range - .clone() - .iter_heights() - .zip(cert_responses) - .zip(block_responses) - { - let cert_height = Height::new(rpc_cert.height); - let block_height = Height::new(block.number()); - - // Verify both certificate and block match the expected height - if cert_height != height { - return Err(eyre::eyre!( - "Certificate height mismatch: expected {}, got {}", - height, - cert_height - )); - } - - if block_height != height { - return Err(eyre::eyre!( - "Block height mismatch: expected {}, got {}", - height, - block_height - )); - } - - let certificate = rpc_cert - .try_into_commit_certificate() - .wrap_err_with(|| format!("Failed to convert certificate at height {height}"))?; - - // Convert Block to ExecutionPayloadV3, verifying hash consistency - let payload = block_to_execution_payload(block) - .wrap_err_with(|| format!("Invalid block at height {height}"))?; - - // Encode payload to SSZ bytes for consensus - let value_bytes = Bytes::from(ssz::Encode::as_ssz_bytes(&payload)); - - blocks.push(SyncedBlock { - height, - certificate, - payload, - value_bytes, - }); - } - - Ok(blocks) - } - - async fn send_batch_request( - &self, - endpoint: &Url, - requests: &[Value], - ) -> eyre::Result> - where - V: for<'de> Deserialize<'de>, - { - let response = self - .client - .post(endpoint.clone()) - .json(requests) - .send() - .await - .wrap_err("Failed to send batch request")? - .error_for_status() - .wrap_err("RPC endpoint returned an error status")?; - - let mut responses: Vec> = response - .json() - .await - .wrap_err("Failed to parse batch response")?; - - // Sort by id to restore original request order, since the - // JSON-RPC spec does not guarantee batch response ordering. - responses.sort_by_key(|r| r.id); - - responses - .into_iter() - .map(|r| match (r.result, r.error) { - (Some(result), _) => Ok(result), - (_, Some(error)) => Err(eyre::eyre!("JSON-RPC error: {error}")), - (None, None) => Err(eyre::eyre!( - "JSON-RPC response (id={}) has neither result nor error", - r.id - )), - }) - .collect() - } -} - -mod json_rpc { - use serde::Deserialize; - use serde_json::Value; - - /// A JSON-RPC error object returned when the server reports a failure. - #[derive(Debug, Deserialize)] - pub struct Error { - pub code: i64, - pub message: String, - #[serde(default)] - pub data: Option, - } - - impl std::fmt::Display for Error { - fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { - write!(f, "code {}: {}", self.code, self.message)?; - if let Some(data) = &self.data { - write!(f, " (data: {data})")?; - } - Ok(()) - } - } - - /// Raw JSON-RPC response with the `id` field preserved so that - /// batch responses can be re-ordered to match the original request - /// order (the JSON-RPC spec does not guarantee ordering). - #[derive(Deserialize)] - #[serde(bound(deserialize = "V: Deserialize<'de>"))] - pub struct Response { - pub id: u64, - pub result: Option, - pub error: Option, - } -} - -/// Convert an Ethereum block (from eth_getBlockByNumber) to ExecutionPayloadV3 -/// -/// Verifies the block hash by recomputing it from header fields. This ensures the RPC -/// response is internally consistent (claimed hash matches actual header content). -/// Any header field tampering will be detected and rejected early. -/// -/// CL (malachite) will verify the block hash matches the certificate's value_id (signed by validators). -/// EL (reth) will additionally validate body-header consistency (transactions_root, withdrawals_root) -/// and execution correctness (state_root, receipts_root, gas_used) when processing the block. -fn block_to_execution_payload(block: Block) -> eyre::Result { - use alloy_consensus::TxEnvelope; - - let claimed_hash = block.header.hash; - let computed_hash = block.header.inner.hash_slow(); - - if claimed_hash != computed_hash { - return Err(eyre::eyre!( - "Block hash mismatch: RPC claimed {claimed_hash}, computed {computed_hash}" - )); - } - - let consensus_block = block.into_consensus().convert_transactions::(); - Ok(ExecutionPayloadV3::from_block_unchecked( - claimed_hash, - &consensus_block, - )) -} diff --git a/crates/malachite-app/src/rpc_sync/mod.rs b/crates/malachite-app/src/rpc_sync/mod.rs deleted file mode 100644 index 906e528..0000000 --- a/crates/malachite-app/src/rpc_sync/mod.rs +++ /dev/null @@ -1,57 +0,0 @@ -// Copyright 2026 Circle Internet Group, Inc. All rights reserved. -// -// SPDX-License-Identifier: Apache-2.0 -// -// Licensed under the Apache License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. -// You may obtain a copy of the License at -// -// http://www.apache.org/licenses/LICENSE-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// See the License for the specific language governing permissions and -// limitations under the License. - -//! RPC Sync Mode -//! -//! This module provides an alternative to P2P-based block synchronization. -//! Instead of participating in the gossip network, nodes in RPC sync mode -//! fetch blocks directly from trusted RPC endpoints. -//! -//! ## Architecture -//! -//! - **RPC Network Actor**: Manages WebSocket subscriptions to RPC endpoints, -//! sends `NetworkEvent::Status` when peer heights update, and handles -//! `OutgoingRequest` from malachite's sync to fetch blocks via RPC. -//! -//! - **Malachite's Default Sync**: Receives Status events, tracks peers, -//! and makes parallel batch requests based on configuration. -//! -//! - **RPC Peers**: Maps RPC endpoints to malachite PeerIds. -//! -//! ## Flow -//! -//! 1. Engine starts, consensus and sync subscribe to Network -//! 2. WebSocket subscriptions connect and receive block notifications -//! 3. Network sends `NetworkEvent::Status` to subscribers -//! 4. Malachite's sync sees peers ahead and sends `OutgoingRequest` -//! 5. Network fetches blocks via RPC, sends `NetworkEvent::SyncResponse` -//! 6. Consensus validates and decides -//! 7. Repeat -//! -//! ## Usage -//! -//! Enable with `--follow` and provide `--follow.endpoint`. - -pub mod client; -pub mod network; -pub mod peers; -pub mod ws_subscription; - -pub use arc_consensus_types::rpc_sync::SyncEndpointUrl; -pub use client::{RpcSyncClient, SyncedBlock}; -pub use network::{spawn_rpc_network_actor, RpcNetworkActor}; -pub use peers::{RpcPeer, RpcPeers}; -pub use ws_subscription::{PeerHeightUpdate, WsSubscriptionManager}; diff --git a/crates/malachite-app/src/rpc_sync/network.rs b/crates/malachite-app/src/rpc_sync/network.rs deleted file mode 100644 index 598650d..0000000 --- a/crates/malachite-app/src/rpc_sync/network.rs +++ /dev/null @@ -1,408 +0,0 @@ -// Copyright 2026 Circle Internet Group, Inc. All rights reserved. -// -// SPDX-License-Identifier: Apache-2.0 -// -// Licensed under the Apache License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. -// You may obtain a copy of the License at -// -// http://www.apache.org/licenses/LICENSE-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// See the License for the specific language governing permissions and -// limitations under the License. - -//! RPC Network Actor for RPC Sync Mode -//! -//! This module provides a Network actor that integrates with malachite's default -//! sync actor. It: -//! 1. Manages WebSocket subscriptions to RPC endpoints for real-time block notifications -//! 2. Sends `NetworkEvent::Status` to malachite when peer heights update -//! 3. Handles `OutgoingRequest` from malachite's sync and fetches blocks via RPC -//! 4. Returns blocks via `NetworkEvent::SyncResponse` -//! -//! ## Architecture -//! -//! ```text -//! WebSocket (newHeads) -//! │ -//! ▼ -//! ┌─────────────────┐ NetworkEvent::Status ┌─────────────────┐ -//! │ Network Actor │ ────────────────────────▶ │ Sync Actor │ -//! │ │ ◀──────────────────────── │ │ -//! └────────┬────────┘ OutgoingRequest └────────▲────────┘ -//! │ │ -//! │ (fetch blocks via RPC) │ -//! │ │ -//! └──────── NetworkEvent::SyncResponse ─────────┤ -//! │ -//! ▼ -//! ┌─────────────────┐ -//! │ Consensus │ -//! └─────────────────┘ -//! ``` - -use std::ops::RangeInclusive; -use std::sync::Arc; - -use arc_consensus_types::rpc_sync::SyncEndpointUrl; -use eyre::{eyre, Context}; -use ractor::{Actor, ActorProcessingErr, ActorRef}; -use tokio::sync::mpsc; -use tokio_util::sync::CancellationToken; -use tracing::{debug, error, info, trace, warn}; - -use malachitebft_app_channel::app::engine::network::{ - Msg as NetworkActorMsg, NetworkEvent, NetworkRef, Status as NetworkStatus, Subscriber, -}; -use malachitebft_app_channel::app::engine::util::output_port::OutputPort; -use malachitebft_app_channel::NetworkMsg; -use malachitebft_peer::PeerId; -use malachitebft_sync::{OutboundRequestId, RawDecidedValue, Response, ValueResponse}; - -use arc_consensus_types::{ArcContext, Height}; - -use crate::rpc_sync::client::RpcSyncClient; -use crate::rpc_sync::peers::RpcPeers; -use crate::rpc_sync::ws_subscription::WsSubscriptionManager; - -/// State for the RPC Network actor -pub struct RpcNetworkState { - /// RPC peers for peer-to-URL lookups and WebSocket subscriptions - peers: RpcPeers, - /// Cancellation token for background tasks - cancel_token: CancellationToken, - /// Request ID counter - next_request_id: u64, - /// Whether WebSocket forwarding has been started - ws_started: bool, -} - -/// Arguments for spawning the RPC Network actor -pub struct RpcNetworkArgs { - /// RPC endpoint URLs - pub endpoints: Vec, -} - -/// RPC Network Actor -/// -/// Manages WebSocket subscriptions for peer status and handles fetch requests. -pub struct RpcNetworkActor { - /// Shared output port for sending events to Consensus - output_port: Arc>>, - /// RPC client for fetching blocks - rpc_client: RpcSyncClient, -} - -impl RpcNetworkActor { - pub fn new( - output_port: Arc>>, - rpc_client: RpcSyncClient, - ) -> Self { - Self { - output_port, - rpc_client, - } - } -} - -/// Spawn the RPC Network actor -/// -/// Returns: -/// - `NetworkRef`: Reference to the actor -/// - `mpsc::Sender>`: Channel for the app to send network messages -pub async fn spawn_rpc_network_actor( - endpoints: Vec, -) -> eyre::Result<(NetworkRef, mpsc::Sender>)> { - // Create RPC client and shared output port - let rpc_client = RpcSyncClient::new(); - let output_port = Arc::new(OutputPort::with_capacity(256)); - - let actor = RpcNetworkActor::new(output_port.clone(), rpc_client); - let args = RpcNetworkArgs { endpoints }; - - let (actor_ref, _handle) = Actor::spawn(Some("rpc-network".to_string()), actor, args) - .await - .map_err(|e| eyre::eyre!("Failed to spawn RPC network actor: {e}"))?; - - // Create the network message channel (app → network) - // In this mode, the node does not participate in consensus, - // and therefore never needs to send a NetworkMsg to the network actor. - // However, we still need to return a sender for the NetworkRef, - // so we create a dummy channel that just logs a warning if any messages are sent to it. - let (network_tx, mut network_rx) = mpsc::channel(16); - - tokio::spawn(async move { - // Show a warning if any messages are sent to the network actor, - // since this should never happen in RPC sync mode - while let Some(msg) = network_rx.recv().await { - warn!(?msg, "Received unexpected NetworkMsg in RPC sync mode"); - } - }); - - Ok((actor_ref, network_tx)) -} - -/// Fetch a range of blocks from an RPC endpoint -async fn fetch_range( - rpc_client: &RpcSyncClient, - endpoint: &url::Url, - range: &RangeInclusive, - cancel_token: &CancellationToken, -) -> eyre::Result>> { - // Heights are validated in fetch_blocks_batch - let blocks = cancel_token - .run_until_cancelled(rpc_client.fetch_blocks_batch(endpoint, range)) - .await - .ok_or_else(|| eyre!("Fetch cancelled"))? - .wrap_err("Fetch failed")? - .into_iter() - .map(|block| RawDecidedValue { - value_bytes: block.value_bytes, - certificate: block.certificate, - }) - .collect(); - - Ok(blocks) -} - -#[ractor::async_trait] -impl Actor for RpcNetworkActor { - type Msg = NetworkActorMsg; - type State = RpcNetworkState; - type Arguments = RpcNetworkArgs; - - async fn pre_start( - &self, - _myself: ActorRef, - args: Self::Arguments, - ) -> Result { - info!(endpoints = ?args.endpoints, "RPC Network actor starting"); - - // Create RPC peers from endpoints (for WebSocket subscriptions) - // WebSocket subscriptions will be started lazily when first subscriber registers - let peers = RpcPeers::new(args.endpoints); - - // Create cancellation token - let cancel_token = CancellationToken::new(); - - Ok(RpcNetworkState { - peers, - cancel_token, - next_request_id: 1, - ws_started: false, - }) - } - - async fn handle( - &self, - _myself: ActorRef, - message: Self::Msg, - state: &mut Self::State, - ) -> Result<(), ActorProcessingErr> { - match message { - NetworkActorMsg::Subscribe(subscriber) => { - on_subscribe(state, subscriber, &self.output_port); - } - - NetworkActorMsg::OutgoingRequest(peer_id, request, reply) => { - on_outgoing_request( - state, - peer_id, - request, - reply, - &self.output_port, - &self.rpc_client, - ); - } - - // These messages are not used in RPC sync mode - NetworkActorMsg::PublishConsensusMsg(_) => { - trace!("RPC sync mode: ignoring PublishConsensusMsg"); - } - NetworkActorMsg::PublishLivenessMsg(_) => { - trace!("RPC sync mode: ignoring PublishLivenessMsg"); - } - NetworkActorMsg::PublishProposalPart(_) => { - trace!("RPC sync mode: ignoring PublishProposalPart"); - } - NetworkActorMsg::BroadcastStatus(_) => { - trace!("RPC sync mode: ignoring BroadcastStatus"); - } - NetworkActorMsg::OutgoingResponse(_, _) => { - trace!("RPC sync mode: ignoring OutgoingResponse"); - } - NetworkActorMsg::UpdateValidatorSet(_) => { - trace!("RPC sync mode: ignoring UpdateValidatorSet"); - } - NetworkActorMsg::DumpState(reply) => { - let _ = reply.send(Default::default()); - } - _ => { - trace!("RPC sync mode: ignoring unhandled network message"); - } - } - - Ok(()) - } - - async fn post_stop( - &self, - _myself: ActorRef, - state: &mut Self::State, - ) -> Result<(), ActorProcessingErr> { - info!("RPC Network actor stopping"); - state.cancel_token.cancel(); - Ok(()) - } -} - -// ---------------------------------------------------------------------------- -// Message handlers -// ---------------------------------------------------------------------------- - -/// Handle a Subscribe message from consensus or sync -fn on_subscribe( - state: &mut RpcNetworkState, - subscriber: Box>>, - output_port: &Arc>>, -) { - info!("Subscriber registered to RPC network events"); - subscriber.subscribe_to_port(output_port); - - // Start WebSocket subscriptions on first subscribe - // This ensures sync actor is subscribed before we send Status events - if state.ws_started { - return; - } - - state.ws_started = true; - - info!("Starting WebSocket subscriptions (first subscriber registered)"); - - // Create and start WebSocket subscription manager - let (mut ws_manager, mut ws_update_rx) = WsSubscriptionManager::new(); - - // Override the default cancellation token - ws_manager.set_cancel_token(state.cancel_token.clone()); - - // Clone peers for the async task (we keep the original for URL lookups) - let peers = state.peers.clone(); - - // Start subscriptions in a separate task to not block - let port = output_port.clone(); - let cancel_token = state.cancel_token.clone(); - - tokio::spawn(async move { - // Start WebSocket subscriptions - ws_manager.start_subscriptions(&peers).await; - - // Forward updates to consensus via NetworkEvent::Status - loop { - tokio::select! { - Some(update) = ws_update_rx.recv() => { - // Send Status event to subscribers (sync and consensus) - let status = NetworkStatus::new(update.height, Height::new(0)); - let event = NetworkEvent::Status(update.peer_id, status); - port.send(event); - } - _ = cancel_token.cancelled() => { - break; - } - } - } - }); - - // Emit NetworkEvent::Listening to trigger Consensus to send ConsensusReady - let fake_address = "/ip4/127.0.0.1/tcp/0".parse().expect("valid multiaddr"); - info!("Emitting NetworkEvent::Listening to trigger ConsensusReady"); - output_port.send(NetworkEvent::Listening(fake_address)); -} - -/// Handle an OutgoingRequest (sync request) from malachite's sync actor -fn on_outgoing_request( - state: &mut RpcNetworkState, - peer_id: PeerId, - request: malachitebft_sync::Request, - reply: ractor::RpcReplyPort, - output_port: &Arc>>, - rpc_client: &RpcSyncClient, -) { - debug!( - ?peer_id, - ?request, - "Received sync request from malachite sync actor" - ); - - let request_id = OutboundRequestId::new(state.next_request_id); - // Request IDs are sequential and won't reach u64::MAX in practice - #[allow(clippy::arithmetic_side_effects)] - { - state.next_request_id += 1; - } - - // Send request ID back immediately - if let Err(e) = reply.send(request_id.clone()) { - error!("Failed to send request ID reply: {e:?}"); - return; - } - - // Look up the endpoint URL for this peer - // Malachite's sync actor selects which peer to request from based on peer scores - let endpoint = match state.peers.url_for_peer(&peer_id) { - Some(url) => url.clone(), - None => { - warn!(%peer_id, "Unknown peer_id in sync request, no endpoint found"); - let event = NetworkEvent::SyncResponse(request_id, peer_id, None); - output_port.send(event); - return; - } - }; - - // Handle the value request - let malachitebft_sync::Request::ValueRequest(value_req) = request; - let range = value_req.range.clone(); - let rpc_client = rpc_client.clone(); - let output_port = output_port.clone(); - let cancel_token = state.cancel_token.clone(); - - tokio::spawn(async move { - let result = fetch_range(&rpc_client, &endpoint, &range, &cancel_token).await; - - match result { - Ok(values) if !values.is_empty() => { - let start_height = values.first().expect("non-empty").certificate.height; - info!( - %request_id, - %start_height, - count = values.len(), - "✅ Fetch completed, sending blocks to consensus" - ); - - let response = ValueResponse { - start_height, - values, - }; - - let event = NetworkEvent::SyncResponse( - request_id, - peer_id, - Some(Response::ValueResponse(response)), - ); - output_port.send(event); - } - Ok(_) => { - warn!(%request_id, "Fetch returned no values"); - let event = NetworkEvent::SyncResponse(request_id, peer_id, None); - output_port.send(event); - } - Err(e) => { - warn!(%request_id, error = format!("{e:#}"), "Fetch failed"); - let event = NetworkEvent::SyncResponse(request_id, peer_id, None); - output_port.send(event); - } - } - }); -} diff --git a/crates/malachite-app/src/rpc_sync/peers.rs b/crates/malachite-app/src/rpc_sync/peers.rs deleted file mode 100644 index 8113e11..0000000 --- a/crates/malachite-app/src/rpc_sync/peers.rs +++ /dev/null @@ -1,252 +0,0 @@ -// Copyright 2026 Circle Internet Group, Inc. All rights reserved. -// -// SPDX-License-Identifier: Apache-2.0 -// -// Licensed under the Apache License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. -// You may obtain a copy of the License at -// -// http://www.apache.org/licenses/LICENSE-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// See the License for the specific language governing permissions and -// limitations under the License. - -//! RPC Peers -//! -//! Maps RPC endpoints to malachite PeerIds. Each RPC endpoint is treated -//! as a "peer" that malachite's sync actor can request blocks from. -//! Peer heights are updated via WebSocket subscriptions (newHeads). - -use std::collections::HashMap; - -use multihash::Multihash; -use sha3::{Digest, Keccak256}; -use tracing::{debug, info}; -use url::Url; - -use arc_consensus_types::rpc_sync::SyncEndpointUrl; -use arc_consensus_types::Height; -use malachitebft_peer::PeerId; - -/// RPC peer information -#[derive(Debug, Clone)] -pub struct RpcPeer { - /// HTTP RPC endpoint URL - pub http_url: Url, - /// WebSocket RPC endpoint URL (derived from HTTP URL) - pub ws_url: Option, - /// Derived PeerId for this endpoint - pub peer_id: PeerId, - /// Current tip height (latest known block) - pub tip_height: Height, - /// Minimum height in history (RPC endpoints typically have full history) - pub history_min_height: Height, - /// Whether this peer is currently connected/reachable - pub connected: bool, -} - -impl RpcPeer { - /// Create a new RPC peer from an endpoint URL - pub fn new(url: SyncEndpointUrl) -> Self { - let peer_id = url_to_peer_id(url.http()); - - Self { - http_url: url.http().clone(), - ws_url: Some(url.websocket()), - peer_id, - tip_height: Height::new(0), - history_min_height: Height::new(0), // Assumes RPC endpoint is an archive node with full history - connected: false, - } - } - - /// Update the tip height for this peer - pub fn update_tip_height(&mut self, height: Height) { - if height > self.tip_height { - debug!( - peer_id = %self.peer_id, - old_height = %self.tip_height, - new_height = %height, - "RPC peer tip height updated" - ); - self.tip_height = height; - } - } - - /// Convert to malachite Status - pub fn to_status(&self) -> malachitebft_sync::Status { - malachitebft_sync::Status { - peer_id: self.peer_id, - tip_height: self.tip_height, - history_min_height: self.history_min_height, - } - } -} - -/// Collection of RPC peers -#[derive(Debug, Clone)] -pub struct RpcPeers { - /// Map from PeerId to RpcPeer - peers: HashMap, - /// List of endpoints in order (for iteration) - endpoints: Vec, -} - -impl RpcPeers { - /// Create a new RpcPeers collection from a list of endpoint URLs - pub fn new(endpoints: Vec) -> Self { - let mut peers = HashMap::new(); - - for url in &endpoints { - let peer = RpcPeer::new(url.clone()); - info!( - peer_id = %peer.peer_id, - http_url = %peer.http_url, - ws_url = ?peer.ws_url, - "Added RPC peer" - ); - peers.insert(peer.peer_id, peer); - } - - Self { peers, endpoints } - } - - /// Get a peer by PeerId - pub fn get(&self, peer_id: &PeerId) -> Option<&RpcPeer> { - self.peers.get(peer_id) - } - - /// Get a mutable peer by PeerId - pub fn get_mut(&mut self, peer_id: &PeerId) -> Option<&mut RpcPeer> { - self.peers.get_mut(peer_id) - } - - /// Get a peer by HTTP URL - pub fn get_by_url(&self, url: &Url) -> Option<&RpcPeer> { - let peer_id = url_to_peer_id(url); - self.peers.get(&peer_id) - } - - /// Update tip height for a peer identified by URL - pub fn update_tip_height_by_url(&mut self, url: &Url, height: Height) { - let peer_id = url_to_peer_id(url); - if let Some(peer) = self.peers.get_mut(&peer_id) { - peer.update_tip_height(height); - } - } - - /// Update tip height for a peer - pub fn update_tip_height(&mut self, peer_id: &PeerId, height: Height) { - if let Some(peer) = self.peers.get_mut(peer_id) { - peer.update_tip_height(height); - } - } - - /// Mark a peer as connected - pub fn set_connected(&mut self, peer_id: &PeerId, connected: bool) { - if let Some(peer) = self.peers.get_mut(peer_id) { - peer.connected = connected; - } - } - - /// Get all peers - pub fn all(&self) -> impl Iterator { - self.peers.values() - } - - /// Get all connected peers - pub fn connected(&self) -> impl Iterator { - self.peers.values().filter(|p| p.connected) - } - - /// Get endpoint URLs in order - pub fn endpoints(&self) -> &[SyncEndpointUrl] { - &self.endpoints - } - - /// Get the PeerId for an endpoint URL - pub fn peer_id_for_url(&self, url: &Url) -> PeerId { - url_to_peer_id(url) - } - - /// Get the HTTP URL for a PeerId - pub fn url_for_peer(&self, peer_id: &PeerId) -> Option<&Url> { - self.peers.get(peer_id).map(|p| &p.http_url) - } - - /// Get the highest tip height among all connected peers - pub fn max_tip_height(&self) -> Height { - self.peers - .values() - .filter(|p| p.connected) - .map(|p| p.tip_height) - .max() - .unwrap_or(Height::new(0)) - } - - /// Convert to malachite peer statuses for the sync state machine - pub fn to_statuses(&self) -> Vec> { - self.peers - .values() - .filter(|p| p.connected) - .map(|p| p.to_status()) - .collect() - } -} - -/// Derive a PeerId from an endpoint URL -/// -/// Uses Keccak256 hash of the URL to create a deterministic PeerId. -/// This ensures the same URL always maps to the same PeerId. -fn url_to_peer_id(url: &Url) -> PeerId { - // Hash the URL to get 32 bytes - let mut hasher = Keccak256::new(); - hasher.update(url.as_str().as_bytes()); - let hash = hasher.finalize(); - - // Create PeerId using identity multihash format: - // [0x00, 0x20] = identity hash code + 32 byte length - // followed by 32 bytes of hash - let multihash = Multihash::wrap(0x00, &hash[..32]).expect("Valid multihash from URL hash"); - PeerId::from_multihash(multihash).expect("Valid peer ID from multihash") -} - -#[cfg(test)] -mod tests { - use super::*; - - #[test] - fn test_url_to_peer_id_deterministic() { - let url = Url::parse("http://localhost:8545").unwrap(); - let peer_id1 = url_to_peer_id(&url); - let peer_id2 = url_to_peer_id(&url); - assert_eq!(peer_id1, peer_id2); - } - - #[test] - fn test_url_to_peer_id_different_urls() { - let peer_id1 = url_to_peer_id(&"http://localhost:8545".parse().unwrap()); - let peer_id2 = url_to_peer_id(&"http://localhost:8546".parse().unwrap()); - assert_ne!(peer_id1, peer_id2); - } - - #[test] - fn test_rpc_peers_creation() { - let endpoints = vec![ - "http://localhost:8545".parse().unwrap(), - "http://localhost:8546".parse().unwrap(), - ]; - let peers = RpcPeers::new(endpoints.clone()); - - assert_eq!(peers.endpoints().len(), 2); - assert!(peers - .get_by_url(&"http://localhost:8545".parse().unwrap()) - .is_some()); - assert!(peers - .get_by_url(&"http://localhost:8546".parse().unwrap()) - .is_some()); - } -} diff --git a/crates/malachite-app/src/rpc_sync/ws_subscription.rs b/crates/malachite-app/src/rpc_sync/ws_subscription.rs deleted file mode 100644 index 9cb2085..0000000 --- a/crates/malachite-app/src/rpc_sync/ws_subscription.rs +++ /dev/null @@ -1,250 +0,0 @@ -// Copyright 2026 Circle Internet Group, Inc. All rights reserved. -// -// SPDX-License-Identifier: Apache-2.0 -// -// Licensed under the Apache License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. -// You may obtain a copy of the License at -// -// http://www.apache.org/licenses/LICENSE-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// See the License for the specific language governing permissions and -// limitations under the License. - -//! WebSocket Subscriptions for RPC Sync -//! -//! Subscribes to `eth_subscribe("newHeads")` on each RPC endpoint to receive -//! real-time block header notifications. The network actor converts these to -//! `NetworkEvent::Status` messages to inform malachite's sync actor about -//! peer heights, triggering block fetch requests when peers are ahead. - -use std::time::Duration; - -use alloy_provider::{Provider, ProviderBuilder}; -use alloy_transport_ws::WsConnect; -use malachitebft_peer::PeerId; -use tokio::sync::mpsc; -use tokio_util::sync::CancellationToken; -use tracing::{debug, error, info, warn}; - -use arc_consensus_types::Height; -use url::Url; - -/// Message sent when a peer's tip height is updated -#[derive(Debug, Clone)] -pub struct PeerHeightUpdate { - pub peer_id: PeerId, - pub http_url: Url, - pub height: Height, -} - -/// Spawn a WebSocket subscription task for a single endpoint -/// -/// This task: -/// 1. Connects to the WebSocket endpoint -/// 2. Subscribes to newHeads -/// 3. Sends height updates through the channel -/// 4. Reconnects on disconnection -pub async fn spawn_ws_subscription( - http_url: Url, - ws_url: Url, - peer_id: PeerId, - update_tx: mpsc::Sender, - cancel_token: CancellationToken, -) { - tokio::spawn(run_ws_subscription( - http_url, - ws_url, - peer_id, - update_tx, - cancel_token, - )); -} - -/// Backoff strategy for WebSocket reconnections -/// -/// Uses a Fibonacci backoff with jitter, starting at 5 seconds and maxing out at 60 seconds, -/// no maximum number of attempts (will keep trying indefinitely until cancelled). -const WS_BACKOFF: backon::FibonacciBuilder = backon::FibonacciBuilder::new() - .with_min_delay(Duration::from_secs(5)) - .with_max_delay(Duration::from_secs(60)) - .without_max_times() - .with_jitter(); - -/// Run the WebSocket subscription loop with reconnection -async fn run_ws_subscription( - http_url: Url, - ws_url: Url, - peer_id: PeerId, - update_tx: mpsc::Sender, - cancel_token: CancellationToken, -) { - use backon::RetryableWithContext; - - struct Context { - ws_url: Url, - http_url: Url, - peer_id: PeerId, - update_tx: mpsc::Sender, - } - - async fn task(ctx: Context) -> (Context, eyre::Result<()>) { - info!(ws_url = %ctx.ws_url, "Connecting to WebSocket endpoint"); - let result = - connect_and_subscribe(&ctx.ws_url, &ctx.http_url, ctx.peer_id, &ctx.update_tx).await; - (ctx, result) - } - - let mut retry_attempts: usize = 0; - - let retryable_task = task - .retry(WS_BACKOFF) - .context(Context { - ws_url: ws_url.clone(), - http_url, - peer_id, - update_tx, - }) - .notify(|error, delay| { - // Unbounded retries, but at ~60s backoff, overflow takes ~10^10 years - #[allow(clippy::arithmetic_side_effects)] - { - retry_attempts += 1; - } - - warn!( - %ws_url, error = format!("{error:#}"), attempt = retry_attempts, - "WebSocket connection failed, retrying in {delay:?}" - ); - }); - - match cancel_token.run_until_cancelled(retryable_task).await { - Some((_, Ok(()))) => { - // Normal termination - info!(%ws_url, "WebSocket subscription ended normally"); - } - Some((_, Err(error))) => { - // The task failed with an error that was not retried (should not happen since we have no max attempts) - error!(%ws_url, error = format!("{error:#}"), "WebSocket subscription failed with unrecoverable error"); - } - None => { - // The WebSocket subscription task was cancelled - info!(%ws_url, "WebSocket subscription cancelled"); - } - } -} - -/// Connect to a WebSocket endpoint, subscribe to newHeads, -/// and forward height updates until the connection is lost or the channel closes. -async fn connect_and_subscribe( - ws_url: &Url, - http_url: &Url, - peer_id: PeerId, - update_tx: &mpsc::Sender, -) -> eyre::Result<()> { - // Connect to WebSocket - let ws = WsConnect::new(ws_url.to_string()); - let provider = ProviderBuilder::new().connect_ws(ws).await?; - - info!(%ws_url, "Connected to WebSocket"); - - // Subscribe to new block headers - let mut subscription = provider.subscribe_blocks().await?; - - info!(%ws_url, "Subscribed to newHeads"); - - let make_update = |height| PeerHeightUpdate { - peer_id, - http_url: http_url.clone(), - height, - }; - - match provider.get_block_number().await { - Ok(block_number) => { - let height = Height::new(block_number); - debug!(%ws_url, %height, "Initial block height"); - let _ = update_tx.send(make_update(height)).await; - } - Err(error) => { - warn!(%ws_url, %error, "Failed to get initial block number"); - } - } - - // Stream incoming block headers until the subscription breaks - loop { - let header = subscription - .recv() - .await - .map_err(|e| eyre::eyre!("WebSocket subscription error: {e}"))?; - - let height = Height::new(header.number); - - debug!(%ws_url, %height, hash = %header.hash, "Received new block header via WebSocket"); - - if update_tx.send(make_update(height)).await.is_err() { - // Channel closed, exit - return Ok(()); - } - } -} - -/// Manager for all WebSocket subscriptions -pub struct WsSubscriptionManager { - /// Sender for spawning new subscriptions - update_tx: mpsc::Sender, - /// Cancellation token for all subscriptions - cancel_token: CancellationToken, -} - -impl WsSubscriptionManager { - /// Create a new subscription manager - pub fn new() -> (Self, mpsc::Receiver) { - let (update_tx, update_rx) = mpsc::channel(256); - - let this = Self { - update_tx, - cancel_token: CancellationToken::new(), - }; - - (this, update_rx) - } - - /// Set a custom cancellation token - pub fn set_cancel_token(&mut self, token: CancellationToken) { - self.cancel_token = token; - } - - /// Start WebSocket subscriptions for all peers - pub async fn start_subscriptions(&self, peers: &crate::rpc_sync::peers::RpcPeers) { - for peer in peers.all() { - if let Some(ws_url) = &peer.ws_url { - spawn_ws_subscription( - peer.http_url.clone(), - ws_url.clone(), - peer.peer_id, - self.update_tx.clone(), - self.cancel_token.clone(), - ) - .await; - } else { - warn!( - http_url = %peer.http_url, - "No WebSocket URL for peer, skipping subscription" - ); - } - } - } - - /// Stop all subscriptions - pub fn stop(&self) { - self.cancel_token.cancel(); - } - - /// Get the cancellation token - pub fn cancel_token(&self) -> CancellationToken { - self.cancel_token.clone() - } -} diff --git a/crates/malachite-app/src/state.rs b/crates/malachite-app/src/state.rs deleted file mode 100644 index ea3d239..0000000 --- a/crates/malachite-app/src/state.rs +++ /dev/null @@ -1,539 +0,0 @@ -// Copyright 2025 Circle Internet Group, Inc. All rights reserved. -// -// SPDX-License-Identifier: Apache-2.0 -// -// Licensed under the Apache License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. -// You may obtain a copy of the License at -// -// http://www.apache.org/licenses/LICENSE-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// See the License for the specific language governing permissions and -// limitations under the License. - -//! Internal state of the application. This is a simplified abstract to keep it simple. -//! A regular application would have mempool implemented, a proper database and input methods like RPC. - -use std::collections::HashMap; -use std::time::{Duration, Instant, SystemTime}; - -use eyre::Context as _; - -use malachitebft_app_channel::app::streaming::StreamId; -use malachitebft_app_channel::app::types::core::Round; - -use crate::streaming; - -use arc_consensus_types::{ - Address, AlloyAddress, ArcContext, BlockHash, ChainId, Config, ConsensusParams, ConsensusSpec, - Height, NetworkId, ValidatorSet, -}; -use arc_eth_engine::json_structures::ExecutionBlock; -use arc_eth_engine::persistence_meter::{NoopPersistenceMeter, PersistenceMeter}; -use arc_signer::ArcSigningProvider; -use malachitebft_core_types::HeightParams; - -use crate::block::ConsensusBlock; -use crate::env_config::EnvConfig; -use crate::metrics::app::AppMetrics; -use crate::node::ConsensusIdentity; -use crate::request::Status; -use crate::stats::Stats; -use crate::store::repositories::UndecidedBlocksRepository; -use crate::store::Store; -use crate::streaming::PartStreamsMap; -use crate::utils::sync_state::SyncState; -use arc_consensus_types::proposal_monitor::ProposalMonitor; - -/// Information needed to start the next height after a decision is reached. -#[derive(Debug)] -pub struct NextHeightInfo { - /// The next height to move to after the current height is finalized. - pub next_height: Height, - /// The validator set for the next height. - pub validator_set: ValidatorSet, - /// The consensus parameters for the next height. - pub consensus_params: ConsensusParams, - /// The block that was decided at the current height. - pub decided_block: ExecutionBlock, - /// The target time for the next block to be proposed. - pub target_time: Option, -} - -impl NextHeightInfo { - /// Get the height parameters for the next height, which are used to start the next height in consensus. - pub fn height_params(&self) -> HeightParams { - HeightParams::new( - self.validator_set.clone(), - self.consensus_params.timeouts(), - self.target_time, - ) - } -} - -/// Represents whether or not a decision was successfully committed for the current height. -/// This is used to determine the appropriate next steps when a height is finalized, -/// such as whether to start the next height or restart the current height. -#[derive(Debug)] -pub enum Decision { - /// Decision was sucessfully committed, and we have the information needed to start the next height. - Success(Box), - - /// Processing the decided value failed for the given height and round. - Failure(eyre::Report), -} - -/// Represents the internal state of the application node -/// Contains information about current height, round, proposals and blocks -pub struct State { - pub ctx: ArcContext, - - identity: ConsensusIdentity, - validator_set: ValidatorSet, - store: Store, - stream_nonce: u32, - streams_map: PartStreamsMap, - config: Config, - env_config: EnvConfig, - stats: Stats, - - /// The genesis block of the execution layer, fetched at startup. - genesis_block: ExecutionBlock, - - /// Computed network identifier: `keccak256(rlp(chain_id, genesis_hash, cl_fork_version))`. - /// Recomputed at each height start because the fork version can change at fork boundaries. - network_id: NetworkId, - - /// Address set by a validator to receive tips (transactions' priority fee) and - /// rewards. The execution layer deposits fees and rewards to this address - /// whenever the validator successfully proposes a new block. Not setting it - /// to a valid address will result in losing the tips/rewards. - suggested_fee_recipient: Address, - - /// Information about the current height, round, and proposer. - pub current_height: Height, - pub current_round: Round, - pub current_proposer: Option
, - - /// Whether the commit for the current height and round was successful or not, - /// along with relevant information for next steps. - pub decision: Option, - - /// The current synchronization state of the node. - pub sync_state: SyncState, - - /// The block that was decided at the previous height. - pub previous_block: Option, - - /// Consensus parameters - pub consensus_params: ConsensusParams, - - /// Monitor for tracking round-0 proposal timing and success - pub proposal_monitor: Option, - - /// Timestamps of heights that received a synced value via ProcessSyncedValue. - synced_heights: HashMap, - - /// Meters EL block persistence to apply backpressure during sync catch-up. - persistence_meter: Box, - - /// Consensus-layer chain spec (fork activation by height). - #[allow(dead_code)] - pub spec: ConsensusSpec, - - /// Metrics for the application. - pub metrics: AppMetrics, -} - -#[bon::bon] -impl State { - /// Creates a new State instance with the given validator address and starting height. - /// - /// # Example - /// ```rust,ignore - /// State::builder(ctx) - /// .identity(identity.consensus.clone()) - /// .store(store.clone()) - /// .config(self.config.clone()) - /// .env_config(env_config) - /// .spec(consensus_spec) - /// .genesis_block(genesis_block) - /// .metrics(app_metrics) - /// .build(); - /// ``` - #[builder(finish_fn = build)] - pub fn new( - #[builder(start_fn)] ctx: ArcContext, - identity: ConsensusIdentity, - store: Store, - config: Config, - env_config: EnvConfig, - spec: ConsensusSpec, - genesis_block: ExecutionBlock, - metrics: AppMetrics, - ) -> Self { - let initial_height = Height::new(0); - let network_id = NetworkId::new( - spec.chain_id, - genesis_block.block_hash, - spec.fork_version_at(initial_height), - ); - - Self { - ctx, - identity, - current_height: initial_height, // will be updated from reth - current_round: Round::Nil, - current_proposer: None, - validator_set: ValidatorSet::default(), // initially empty, will be updated from reth - store, - stream_nonce: 0, - streams_map: PartStreamsMap::new(initial_height, 0), - config, - env_config, - stats: Stats::default(), - genesis_block, - network_id, - suggested_fee_recipient: AlloyAddress::ZERO.into(), - decision: None, - sync_state: SyncState::CatchingUp, // assume node is catching up at startup until we know more - previous_block: None, - consensus_params: ConsensusParams::default(), - proposal_monitor: None, - synced_heights: HashMap::new(), - persistence_meter: Box::new(NoopPersistenceMeter), - spec, - metrics, - } - } - - pub fn config(&self) -> &Config { - &self.config - } - - pub fn env_config(&self) -> &EnvConfig { - &self.env_config - } - - pub fn store(&self) -> &Store { - &self.store - } - - pub fn stats(&self) -> &Stats { - &self.stats - } - - pub fn metrics(&self) -> &AppMetrics { - &self.metrics - } - - /// Get the chain ID. - pub fn chain_id(&self) -> ChainId { - self.spec.chain_id - } - - /// Get the computed network ID. - #[allow(dead_code)] - pub fn network_id(&self) -> NetworkId { - self.network_id - } - - /// Get the genesis block hash of the execution layer. - #[allow(dead_code)] - pub fn genesis_hash(&self) -> BlockHash { - self.genesis_block.block_hash - } - - /// Get the validator's address - pub fn address(&self) -> Address { - self.identity.address() - } - - /// Get the signing provider - pub fn signing_provider(&self) -> &ArcSigningProvider { - self.identity.signing_provider() - } - - // Get the current validator set - pub fn validator_set(&self) -> &ValidatorSet { - &self.validator_set - } - - /// Get the current consensus parameters - pub fn consensus_params(&self) -> &ConsensusParams { - &self.consensus_params - } - - /// Sets the current validator set and updates metrics - pub fn set_validator_set(&mut self, val_set: ValidatorSet) { - self.metrics.update_validator_set(&val_set); - self.streams_map.set_num_validators(val_set.len()); - self.validator_set = val_set; - } - - /// Sets the consensus parameters - pub fn set_consensus_params(&mut self, consensus_params: ConsensusParams) { - self.metrics.update_consensus_params(&consensus_params); - self.consensus_params = consensus_params; - } - - pub fn persistence_meter(&self) -> &dyn PersistenceMeter { - self.persistence_meter.as_ref() - } - - pub fn set_persistence_meter(&mut self, meter: Box) { - self.persistence_meter = meter; - } - - /// Get mutable reference to the streams map - pub fn streams_map_mut(&mut self) -> &mut PartStreamsMap { - &mut self.streams_map - } - - /// Get the fee recipient address - pub fn fee_recipient(&self) -> Address { - self.suggested_fee_recipient - } - - /// Set the fee recipient address - pub fn set_suggested_fee_recipient(&mut self, fee_recipient: Address) { - self.suggested_fee_recipient = fee_recipient; - } - - /// Update metrics when starting a new height - #[must_use] - pub fn started_height(&mut self, height: Height, round: Round, proposer: Address) -> NetworkId { - let elapsed = self.stats.height_started().elapsed(); - self.metrics.observe_block_time(elapsed.as_secs_f64()); - self.stats.set_height_started(Instant::now()); - - self.streams_map.set_current_height(height); - - let network_id = self.recompute_network_id(); - - self.init_proposal_monitor(round, proposer); - - network_id - } - - /// Recompute the network ID from the current chain ID, genesis hash, and fork version. - #[must_use] - fn recompute_network_id(&mut self) -> NetworkId { - let fork_version = self.spec.fork_version_at(self.current_height); - - self.network_id = - NetworkId::new(self.chain_id(), self.genesis_block.block_hash, fork_version); - self.network_id - } - - /// Initialize the proposal monitor for round 0. - fn init_proposal_monitor(&mut self, round: Round, proposer: Address) { - assert_eq!(round.as_i64(), 0); - let height = self.current_height; - - let start_time = SystemTime::now(); - let mut monitor = ProposalMonitor::new(height, proposer, start_time); - - // If an early `ProcessSyncedValue` event was processed for this height, - // use the associated recorded timestamp as proposal receive time. - if let Some(synced_time) = self.synced_heights.remove(&height) { - monitor.proposal_receive_time = Some(synced_time); - monitor.mark_synced(); - } - - self.proposal_monitor = Some(monitor); - } - - /// Mark a height as having received a synced value, storing the receive time. - pub fn mark_height_synced(&mut self, height: Height) { - let now = SystemTime::now(); - - let Some(monitor) = &mut self.proposal_monitor else { - self.synced_heights.insert(height, now); - return; - }; - if monitor.height != height { - self.synced_heights.insert(height, now); - return; - } - if monitor.proposal_receive_time.is_some() { - // Normal proposals take precedence over synced values - return; - } - monitor.proposal_receive_time = Some(now); - monitor.mark_synced(); - } - - /// Clean up synced height tracking for past heights. - pub fn cleanup_synced_heights(&mut self, current_height: Height) { - self.synced_heights.retain(|h, _| *h >= current_height); - } - - /// Maximum number of pending proposals allowed - /// Defined to be equal to the size of the consensus input buffer, - /// which is itself sized to handle all in-flight sync responses. - pub fn max_pending_proposals(&self) -> usize { - let limit = self - .config - .value_sync - .parallel_requests - .checked_mul(self.config.value_sync.batch_size) - .expect("max_pending_proposals overflow"); - assert!(limit > 0, "max_pending_proposals must be greater than 0"); - limit - } - - /// Return important current information. - pub async fn get_status(&self) -> eyre::Result { - let undecided_blocks_count = self - .get_undecided_blocks(self.current_height, self.current_round) - .await - .wrap_err_with(|| { - format!( - "Failed to get undecided blocks for height {} and round {} from the state", - self.current_height, self.current_round, - ) - })? - .len(); - - let pending_proposal_parts = self - .store - .get_pending_proposal_parts_counts() - .await - .wrap_err("Failed to get pending proposal parts counts from the state")?; - - Ok(Status { - height: self.current_height, - round: self.current_round, - address: self.address(), - public_key: *self.identity.public_key(), - proposer: self.current_proposer, - // elapsed() is always <= time since epoch, so this won't underflow - #[allow(clippy::arithmetic_side_effects)] - height_start_time: SystemTime::now() - self.stats.height_started().elapsed(), - prev_payload_hash: self.previous_block.map(|b| b.block_hash), - db_latest_height: self - .store() - .max_height() - .await - .wrap_err("Failed to get the latest height from the state")? - .unwrap_or_default(), - db_earliest_height: self - .store() - .min_height() - .await - .wrap_err("Failed to get earliest height from the state")? - .unwrap_or_default(), - undecided_blocks_count, - pending_proposal_parts, - validator_set: self.validator_set().to_owned(), - sync_state: self.sync_state, - }) - } - - /// Return unit type. Used to check the app is active. - pub fn get_health(&self) {} - - /// Retrieves all undecided blocks at the given height and round. - pub async fn get_undecided_blocks( - &self, - height: Height, - round: Round, - ) -> eyre::Result> { - self.store - .get_by_round(height, round) - .await - .wrap_err_with(|| { - format!("Failed to get undecided blocks for height {height} and round {round} from the database") - }) - } - - /// Move to the next height, updating the previous block, validator set, and consensus params. - /// - /// # Arguments - /// * `info` - The information needed to move to the next height - pub fn move_to_next_height(&mut self, info: NextHeightInfo) { - // Move to next height - self.current_height = info.next_height; - self.current_round = Round::Nil; - - // Update the previous block to the block that was decided - self.previous_block = Some(info.decided_block); - - // Update the validator set for the next height - self.set_validator_set(info.validator_set); - - // Update the consensus params for the next height - self.set_consensus_params(info.consensus_params); - - // Clean up synced heights tracking for past heights - self.cleanup_synced_heights(info.next_height); - } - - /// Build the next stream ID for a proposal at the given `height` and `round`. - /// - /// The height and round come from the proposal being streamed, so the - /// stream_id agrees with the proposal's `Init` part by construction. - pub fn next_stream_id(&mut self, height: Height, round: Round) -> StreamId { - let nonce = self.stream_nonce; - // Stream nonce increases monotonically; collision unreachable within a session - #[allow(clippy::arithmetic_side_effects)] - { - self.stream_nonce += 1; - } - streaming::new_stream_id(height, round, nonce) - } - - pub async fn restart_height( - &mut self, - height: Height, - validator_set: ValidatorSet, - consensus_params: ConsensusParams, - ) -> eyre::Result<()> { - // Reset the state to that of the height prior to the given height being restarted - self.current_height = height; - self.current_round = Round::Nil; - self.current_proposer = None; - self.set_validator_set(validator_set); - self.set_consensus_params(consensus_params); - - let previous_block_height = height.saturating_sub(1); - - self.previous_block = self - .store - .get_decided_block(previous_block_height) - .await - .wrap_err_with(|| format!( - "Failed to retrieve previous block at height {previous_block_height} for restart at height {height}" - ))? - .map(|b| b.execution_payload.payload_inner.payload_inner) - .map(|p| ExecutionBlock { - block_hash: p.block_hash, - block_number: p.block_number, - parent_hash: p.parent_hash, - timestamp: p.timestamp, - }); - - // Clean up any consensus data for the height that we are about to restart - self.store - .clean_stale_consensus_data(height) - .await - .wrap_err_with(|| { - format!("Failed to clean stale consensus data for restart at height {height}") - })?; - - // Update metrics - self.metrics.inc_height_restart_count(); - - Ok(()) - } - - /// Create a savepoint in the database to ensure the allocator state table is up to date. - /// Doing this before shutting down the database can help avoid repair on next startup. - pub fn savepoint(&self) { - self.store.savepoint(); - } -} diff --git a/crates/malachite-app/src/stats.rs b/crates/malachite-app/src/stats.rs deleted file mode 100644 index be49f13..0000000 --- a/crates/malachite-app/src/stats.rs +++ /dev/null @@ -1,104 +0,0 @@ -// Copyright 2026 Circle Internet Group, Inc. All rights reserved. -// -// SPDX-License-Identifier: Apache-2.0 -// -// Licensed under the Apache License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. -// You may obtain a copy of the License at -// -// http://www.apache.org/licenses/LICENSE-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// See the License for the specific language governing permissions and -// limitations under the License. - -use core::fmt; -use std::ops::Deref; -use std::sync::atomic::{AtomicU64, Ordering}; -use std::sync::Arc; -use std::time::Instant; - -use atomic_time::AtomicInstant; -use bytesize::ByteSize; - -#[derive(Clone, Default)] -pub struct Stats(Arc); - -impl Deref for Stats { - type Target = StatsInner; - - fn deref(&self) -> &Self::Target { - &self.0 - } -} - -pub struct StatsInner { - /// Total number of transactions processed - pub txs_count: AtomicU64, - /// Total number of bytes processed in the chain - pub chain_bytes: AtomicU64, - /// Time when the current height started - pub height_started: AtomicInstant, - /// Start time of the node - pub start_time: Instant, -} - -impl Default for StatsInner { - fn default() -> Self { - Self { - txs_count: AtomicU64::new(0), - chain_bytes: AtomicU64::new(0), - height_started: AtomicInstant::now(), - start_time: Instant::now(), - } - } -} - -impl fmt::Display for Stats { - fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { - let uptime = self.start_time.elapsed(); - let height_duration = self.height_started().elapsed(); - let txs_rate = self.txs_count() as f64 / uptime.as_secs_f64(); - let bytes_rate = self.chain_bytes().as_u64() as f64 / uptime.as_secs_f64(); - - write!( - f, - "#txs={}, txs/s={:.2}, chain_bytes={}, bytes/s={:.2}, uptime={:?}, height_duration={:?}", - self.txs_count(), - txs_rate, - self.chain_bytes(), - bytes_rate, - uptime, - height_duration - ) - } -} - -// NOTE: The methods use Relaxed ordering as we don't need strong consistency for these stats. -impl StatsInner { - pub fn txs_count(&self) -> u64 { - self.txs_count.load(Ordering::Relaxed) - } - - pub fn chain_bytes(&self) -> ByteSize { - ByteSize::b(self.chain_bytes.load(Ordering::Relaxed)) - } - - pub fn add_txs_count(&self, count: u64) { - self.txs_count.fetch_add(count, Ordering::Relaxed); - } - - pub fn add_chain_bytes(&self, bytes: u64) { - self.chain_bytes.fetch_add(bytes, Ordering::Relaxed); - } - - pub fn set_height_started(&self, instant: Instant) { - self.height_started.store(instant, Ordering::Relaxed); - } - - pub fn height_started(&self) -> Instant { - self.height_started.load(Ordering::Relaxed) - } -} diff --git a/crates/malachite-app/src/streaming.rs b/crates/malachite-app/src/streaming.rs deleted file mode 100644 index 56f1bf5..0000000 --- a/crates/malachite-app/src/streaming.rs +++ /dev/null @@ -1,2538 +0,0 @@ -// Copyright 2025 Circle Internet Group, Inc. All rights reserved. -// -// SPDX-License-Identifier: Apache-2.0 -// -// Licensed under the Apache License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. -// You may obtain a copy of the License at -// -// http://www.apache.org/licenses/LICENSE-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// See the License for the specific language governing permissions and -// limitations under the License. - -use std::cmp::Ordering; -use std::collections::{BTreeMap, BinaryHeap, HashSet}; -use std::mem::size_of; -use std::time::{Duration, Instant}; - -use bytes::Bytes; -use schnellru::{ByLength, LruMap}; -use tracing::{debug, warn}; - -use arc_consensus_types::{Height, ProposalPart, ProposalParts, ProposalPartsError, Round}; -use malachitebft_app_channel::app::streaming::{Sequence, StreamId, StreamMessage}; -use malachitebft_app_channel::app::types::PeerId; - -/// Maximum number of messages allowed per stream -/// -/// Maximum block size -/// = MAX_MESSAGES_PER_STREAM * CHUNK_SIZE -/// = 128 * 128 KiB = 16 MiB -const MAX_MESSAGES_PER_STREAM: usize = 128; - -/// Maximum number of concurrent streams allowed per peer. -/// -/// A proposer needs ~2 streams per round (one for the proposal, one potential -/// retry). A value of 4 gives headroom for a couple of in-flight rounds while -/// keeping the per-peer memory footprint bounded: -/// -/// = MAX_STREAMS_PER_PEER * MAX_MESSAGES_PER_STREAM * CHUNK_SIZE -/// = 4 * 128 * 128 KiB = 64 MiB -const MAX_STREAMS_PER_PEER: usize = 4; - -/// Size of chunks in which proposal data is split for streaming -pub(crate) const CHUNK_SIZE: usize = 128 * 1024; - -/// Maximum age for a stream before it's evicted -const MAX_STREAM_AGE: Duration = Duration::from_secs(60); - -/// Maximum number of evicted streams tracked in the LRU cache. -const MAX_EVICTED_STREAMS: usize = 10_000; - -/// Stream IDs are exactly 16 bytes: u64 height + u32 round + u32 nonce. -pub(crate) const STREAM_ID_LEN: usize = size_of::() + size_of::() + size_of::(); - -/// Compute the global stream cap for a given validator set size. -/// -/// Sized as `MAX_STREAMS_PER_PEER * num_validators` so every validator can fill -/// its per-peer quota without triggering global eviction. Floored at -/// [`MAX_STREAMS_PER_PEER`] so the cap is non-zero before the validator set is -/// configured at startup (when `num_validators` is still 0). -fn max_total_streams(num_validators: usize) -> usize { - MAX_STREAMS_PER_PEER - .saturating_mul(num_validators) - .max(MAX_STREAMS_PER_PEER) -} - -/// Outcome of [`PartStreamsMap::insert`]. -#[derive(Debug)] -pub enum InsertResult { - /// The stream is complete; contains the assembled proposal parts. - Complete(ProposalParts), - /// The message was accepted (or silently dropped) but the stream is not yet complete. - Pending, - /// The message belongs to a height below the current one and was dropped. - /// - /// Distinct from [`Pending`](InsertResult::Pending) so callers can treat - /// stale gossip (old proposal parts re-circulating on the network) as - /// benign, expected noise rather than an in-progress stream, and never as - /// peer misbehaviour. - Stale, - /// The message was rejected due to peer misbehaviour. - Invalid(InsertError), -} - -/// Reason a stream message was rejected by [`PartStreamsMap::insert`]. -#[derive(Debug)] -pub enum InsertError { - /// The stream_id is not exactly [`STREAM_ID_LEN`] bytes. - InvalidStreamIdLength { actual: usize, expected: usize }, - /// The stream_id height disagrees with the proposal Init payload height. - StreamIdHeightMismatch { - stream_height: Height, - init_height: Height, - }, - /// A completed stream failed final assembly into [`ProposalParts`]. - AssemblyFailed(ProposalPartsError), -} - -impl std::fmt::Display for InsertError { - fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { - match self { - InsertError::InvalidStreamIdLength { actual, expected } => { - write!( - f, - "invalid stream_id length: {actual} bytes (expected {expected})" - ) - } - InsertError::StreamIdHeightMismatch { - stream_height, - init_height, - } => { - write!( - f, - "stream_id height {stream_height} does not match Init height {init_height}" - ) - } - InsertError::AssemblyFailed(err) => { - write!(f, "failed to assemble proposal parts: {err}") - } - } - } -} - -/// Build a new stream ID for the given height, round, and nonce. -pub(crate) fn new_stream_id(height: Height, round: Round, nonce: u32) -> StreamId { - let mut bytes = [0u8; STREAM_ID_LEN]; - bytes[..8].copy_from_slice(&height.as_u64().to_be_bytes()); - bytes[8..12].copy_from_slice( - &round - .as_u32() - .expect("expected non-Nil round") - .to_be_bytes(), - ); - bytes[12..16].copy_from_slice(&nonce.to_be_bytes()); - StreamId::new(Bytes::copy_from_slice(&bytes)) -} - -/// Extension trait for reading the structured fields encoded in a proposal-part -/// [`StreamId`] by [`new_stream_id`]. -trait StreamIdExt { - /// The height encoded in the leading 8 bytes, or `None` if the id is shorter. - fn height(&self) -> Option; -} - -impl StreamIdExt for StreamId { - fn height(&self) -> Option { - let bytes = self.to_bytes(); - let raw: [u8; size_of::()] = bytes.get(..size_of::())?.try_into().ok()?; - Some(Height::new(u64::from_be_bytes(raw))) - } -} - -struct MinSeq(StreamMessage); - -impl PartialEq for MinSeq { - fn eq(&self, other: &Self) -> bool { - self.0.sequence == other.0.sequence - } -} - -impl Eq for MinSeq {} - -impl Ord for MinSeq { - fn cmp(&self, other: &Self) -> Ordering { - other.0.sequence.cmp(&self.0.sequence) - } -} - -impl PartialOrd for MinSeq { - fn partial_cmp(&self, other: &Self) -> Option { - Some(self.cmp(other)) - } -} - -struct MinHeap(BinaryHeap>); - -impl Default for MinHeap { - fn default() -> Self { - Self(BinaryHeap::new()) - } -} - -impl MinHeap { - fn push(&mut self, msg: StreamMessage) { - self.0.push(MinSeq(msg)); - } - - fn len(&self) -> usize { - self.0.len() - } - - fn drain(&mut self) -> Vec { - let mut vec = Vec::with_capacity(self.0.len()); - while let Some(MinSeq(msg)) = self.0.pop() { - if let Some(data) = msg.content.into_data() { - vec.push(data); - } - } - vec - } -} - -struct StreamState { - buffer: MinHeap, - seen_sequences: HashSet, - expected_messages: usize, - message_count: usize, - height: Option, - fin_received: bool, - is_complete: bool, - created_at: Instant, -} - -impl Default for StreamState { - fn default() -> Self { - Self::new() - } -} - -enum StreamInsertResult { - Duplicate, - Incomplete(Option), - ExceededMaxMessages, - ExceededMaxChunkSize(usize), - Complete(Vec), -} - -impl StreamState { - fn new() -> Self { - Self { - buffer: MinHeap::default(), - seen_sequences: HashSet::default(), - expected_messages: 0, - message_count: 0, - height: None, - fin_received: false, - is_complete: false, - created_at: Instant::now(), - } - } - - fn insert(&mut self, msg: StreamMessage) -> StreamInsertResult { - // Reject oversized Data chunks before recording the sequence as seen - if let Some(ProposalPart::Data(data)) = msg.content.as_data() { - if data.bytes.len() > CHUNK_SIZE { - return StreamInsertResult::ExceededMaxChunkSize(data.bytes.len()); - } - } - - if !self.seen_sequences.insert(msg.sequence) { - // We have already seen a message with this sequence number, ignore it. - return StreamInsertResult::Duplicate; - } - - // Check if we've exceeded the maximum number of messages per stream - if self.message_count >= MAX_MESSAGES_PER_STREAM { - return StreamInsertResult::ExceededMaxMessages; - } - - // Increment message count - // Bounded by MAX_MESSAGES_PER_STREAM check above - #[allow(clippy::arithmetic_side_effects)] - { - self.message_count += 1; - } - - // This is the `Init` message. - if let Some(init) = msg.content.as_data().and_then(|part| part.as_init()) { - self.height = Some(init.height); - } - - // This is the `Fin` message. - if msg.is_fin() { - self.fin_received = true; - - // If we have received the fin message, we can determine when we will be done. - // We are done if we have already received all messages from 0 to fin.sequence, - // included. That is to say, if we have received `fin.sequence + 1` messages. - // Sequence is a u64 protocol field; on 64-bit targets usize == u64. - // The +1 cannot overflow because MAX_MESSAGES_PER_STREAM << u64::MAX. - #[allow(clippy::cast_possible_truncation, clippy::arithmetic_side_effects)] - { - self.expected_messages = msg.sequence as usize + 1; - } - } - - // Add the message to the buffer. - self.buffer.push(msg); - - // Check if we are done, ie. we have received Init, Fin, and all messages in between. - self.is_complete = self.height.is_some() - && self.fin_received - && self.buffer.len() == self.expected_messages; - - // Otherwise, abort early. - if !self.is_complete { - return StreamInsertResult::Incomplete(self.height); - } - - // We are complete, drain the buffer and assemble the proposal parts. - let parts = self.buffer.drain(); - - // NOTE: The order of the parts is guaranteed by the MinHeap - StreamInsertResult::Complete(parts) - } -} - -/// Map to track active proposal part streams from peers. -/// -/// Enforces the following limits: -/// - [`MAX_STREAMS_PER_PEER`] streams per peer -/// - [`MAX_MESSAGES_PER_STREAM`] messages per stream -/// - [`CHUNK_SIZE`] per data chunk -/// - `max_total_streams` total concurrent streams (= `MAX_STREAMS_PER_PEER * num_validators`) -/// - Evict streams older than [`MAX_STREAM_AGE`] -/// - Immediately evict streams that exceed message or size limits -/// - Immediately evict streams from previous heights -/// -/// Worst-case memory at full saturation: -/// = max_total_streams * MAX_MESSAGES_PER_STREAM * CHUNK_SIZE -/// = (MAX_STREAMS_PER_PEER * num_validators) * 128 * 128 KiB -/// = 64 MiB * num_validators -pub struct PartStreamsMap { - current_height: Height, - /// `MAX_STREAMS_PER_PEER * num_validators`, floored at - /// [`MAX_STREAMS_PER_PEER`] during the pre-validator-set startup window. - max_total_streams: usize, - streams: BTreeMap<(PeerId, StreamId), StreamState>, - evicted: LruMap<(PeerId, StreamId), ()>, - last_eviction: Instant, -} - -impl PartStreamsMap { - /// Create a new empty PartStreamsMap. - /// - /// `num_validators` sets the global stream cap to - /// `MAX_STREAMS_PER_PEER * num_validators`. - pub fn new(current_height: Height, num_validators: usize) -> Self { - Self { - streams: BTreeMap::new(), - last_eviction: Instant::now(), - // MAX_EVICTED_STREAMS (10_000) fits in u32 - #[allow(clippy::cast_possible_truncation)] - evicted: LruMap::new(ByLength::new(MAX_EVICTED_STREAMS as u32)), - current_height, - max_total_streams: max_total_streams(num_validators), - } - } - - /// Update the current height, purging any tracked streams that are now - /// stale so they stop consuming per-peer budget before the age sweep would - /// reach them. - /// - /// Height comes from the stream_id (see [`new_stream_id`]); lone non-Init - /// streams carry no payload height. Purged streams are not added to - /// `evicted`: the up-front height check in [`insert`](Self::insert) is - /// idempotent and prevents re-creation. - pub fn set_current_height(&mut self, height: Height) { - self.current_height = height; - self.remove_stale_streams(height); - } - - /// Remove streams whose height is below the given height. - /// Called by `set_current_height` to purge stale streams after a height increase. - fn remove_stale_streams(&mut self, height: Height) { - self.streams - .retain(|(_, stream_id), _| stream_id.height().is_some_and(|h| h >= height)); - } - - /// Update the global stream cap after a validator set change. - /// - /// If the new cap is below the current stream count, evict from the busiest - /// peer until the invariant `streams.len() <= max_total_streams` holds - /// again. - pub fn set_num_validators(&mut self, num_validators: usize) { - self.max_total_streams = max_total_streams(num_validators); - while self.streams.len() > self.max_total_streams { - self.evict_oldest_stream(); - } - } - - /// Insert a new proposal part message into the map - /// - /// ## Parameters - /// - `peer_id`: The ID of the peer sending the message - /// - `msg`: The stream message containing the proposal part - /// - /// ## Returns - /// - [`InsertResult::Complete`] if the stream is complete after insertion - /// - [`InsertResult::Pending`] if the message was accepted but the stream is not - /// yet complete, or was silently rejected (duplicate, evicted, limit exceeded) - /// - [`InsertResult::Invalid`] if the message was rejected due to misbehaviour - pub fn insert(&mut self, peer_id: PeerId, msg: StreamMessage) -> InsertResult { - let stream_id = msg.stream_id.clone(); - let key = (peer_id, stream_id.clone()); - let actual = stream_id.to_bytes().len(); - if actual != STREAM_ID_LEN { - self.streams.remove(&key); - return InsertResult::Invalid(InsertError::InvalidStreamIdLength { - actual, - expected: STREAM_ID_LEN, - }); - } - - // Reject parts for already-decided heights up front, before any per-peer - // accounting. The stream_id encodes the height (see `new_stream_id`), so - // this catches stale parts re-circulating on the network even when only - // non-Init parts arrive — those never carry the height in their payload, - // so without this they would linger and consume the proposer's per-peer - // stream budget until the age sweep. - let Some(stream_height) = stream_id.height() else { - self.streams.remove(&key); - return InsertResult::Invalid(InsertError::InvalidStreamIdLength { - actual, - expected: STREAM_ID_LEN, - }); - }; - - if stream_height < self.current_height { - debug!( - %peer_id, - %stream_id, - %stream_height, - current_height = %self.current_height, - "Dropping stale proposal-part stream" - ); - - // A stream created while it was still current can become stale - // as the node advances; drop any tracked entry now so it stops - // consuming the peer's budget instead of lingering until the age - // sweep. The height check above is idempotent and prevents - // re-creation, so there is no need to mark it in `evicted`. - self.streams.remove(&key); - - return InsertResult::Stale; - } - - // First, evict any streams that have exceeded MAX_STREAM_AGE - self.evict_old_streams(); - - if self.evicted.peek(&key).is_some() { - return InsertResult::Pending; - } - - // Check if this is a new stream - let is_new_stream = !self.streams.contains_key(&key); - - // If it's a new stream, check if we've exceeded the per-peer limit - if is_new_stream { - let stream_count = self.peer_streams_count(peer_id); - if stream_count >= MAX_STREAMS_PER_PEER { - warn!( - %peer_id, - %stream_count, - max = MAX_STREAMS_PER_PEER, - "Peer exceeded maximum number of concurrent streams, rejecting new stream" - ); - - return InsertResult::Pending; - } - - // Check if we've exceeded the total streams limit - if self.streams.len() >= self.max_total_streams { - self.evict_oldest_stream(); - } - } - - let state = self.streams.entry(key.clone()).or_default(); - - // Insert the message into the stream state. - let result = state.insert(msg); - - let parts = match result { - StreamInsertResult::Duplicate => return InsertResult::Pending, - - StreamInsertResult::Incomplete(None) => return InsertResult::Pending, - - StreamInsertResult::Incomplete(Some(init_height)) => { - if init_height != stream_height { - self.evict(&key); - return InsertResult::Invalid(InsertError::StreamIdHeightMismatch { - stream_height, - init_height, - }); - } - return InsertResult::Pending; - } - - StreamInsertResult::ExceededMaxMessages => { - warn!( - %peer_id, - %stream_id, - message_count = state.message_count, - max = MAX_MESSAGES_PER_STREAM, - "Stream exceeded maximum message count, message rejected" - ); - - self.evict(&key); - return InsertResult::Pending; - } - - StreamInsertResult::ExceededMaxChunkSize(actual) => { - warn!( - %peer_id, - %stream_id, - actual, - max = CHUNK_SIZE, - "Stream sent oversized data chunk, evicting" - ); - - self.evict(&key); - return InsertResult::Pending; - } - - StreamInsertResult::Complete(parts) => { - self.streams.remove(&key); - parts - } - }; - - // `StreamState` guarantees that an `Init` and a stream-end `Fin` are - // observed before the stream is marked complete, but it deduplicates - // only by message `sequence` — not by part *type*. A misbehaving peer - // can send multiple `ProposalPart::Init` (or `ProposalPart::Fin`) - // envelopes at distinct sequence numbers, complete the stream, and - // reach this branch with `ProposalPartsError::DuplicatePart(..)`. - // Surface the failure as `Invalid` so the caller can act on the - // misbehaviour rather than silently dropping the stream. - match ProposalParts::new(parts) { - Ok(proposal_parts) => { - let init_height = proposal_parts.height(); - if init_height != stream_height { - self.evict(&key); - return InsertResult::Invalid(InsertError::StreamIdHeightMismatch { - stream_height, - init_height, - }); - } - InsertResult::Complete(proposal_parts) - } - Err(e) => InsertResult::Invalid(InsertError::AssemblyFailed(e)), - } - } - - /// Evict a stream from the map and mark it as evicted. - /// The evicted LRU map is bounded by [`MAX_EVICTED_STREAMS`]; the oldest - /// entry is automatically dropped when capacity is exceeded. - fn evict(&mut self, key: &(PeerId, StreamId)) { - self.streams.remove(key); - self.evicted.insert((key.0, key.1.clone()), ()); - } - - /// Evict streams that have exceeded MAX_STREAM_AGE - fn evict_old_streams(&mut self) { - let now = Instant::now(); - - // Only perform eviction check periodically, - // to avoid excessive overhead on every insert. - if now.duration_since(self.last_eviction) < MAX_STREAM_AGE { - return; - } - - // Update last eviction time - self.last_eviction = now; - - // Identify streams to evict, ie. those older than MAX_STREAM_AGE - let keys_to_remove: Vec<_> = self - .streams - .iter() - .filter(|(_, state)| now.duration_since(state.created_at) > MAX_STREAM_AGE) - .map(|(key, _)| key.clone()) - .collect(); - - // Evict the identified streams - for key @ (peer_id, stream_id) in &keys_to_remove { - warn!(%peer_id, %stream_id, "Evicting stream due to age timeout"); - self.evict(key); - } - } - - /// Evict the oldest stream from the peer with the most active streams. - /// - /// Targets the busiest peer so no single peer can push others out via the - /// global cap. - fn evict_oldest_stream(&mut self) { - let peer_id = self.busiest_peer(); - let Some(peer_id) = peer_id else { return }; - - let key = self.oldest_stream_of(peer_id); - let Some(ref key @ (ref peer_id, ref stream_id)) = key else { - return; - }; - - warn!(%peer_id, %stream_id, "Evicting oldest stream from peer with most streams"); - self.evict(key); - } - - /// Return the peer with the most active streams, if any. - /// - /// Uses a [`BTreeMap`] so ties are broken deterministically by [`PeerId`] - /// ordering rather than by hash-map iteration order. - fn busiest_peer(&self) -> Option { - let mut counts: BTreeMap = BTreeMap::new(); - for (pid, _) in self.streams.keys() { - #[allow(clippy::arithmetic_side_effects)] - { - *counts.entry(*pid).or_default() += 1; - } - } - counts - .into_iter() - .max_by_key(|&(_, c)| c) - .map(|(pid, _)| pid) - } - - /// Count active streams for a given peer - fn peer_streams_count(&self, peer_id: PeerId) -> usize { - self.streams - .keys() - .filter(|(pid, _)| *pid == peer_id) - .count() - } - - /// Return the key of the oldest stream belonging to `peer_id`, if any. - fn oldest_stream_of(&self, peer_id: PeerId) -> Option<(PeerId, StreamId)> { - self.streams - .iter() - .filter(|((pid, _), _)| *pid == peer_id) - .min_by_key(|(_, state)| state.created_at) - .map(|(key, _)| key.clone()) - } -} - -#[cfg(test)] -mod tests { - use arc_consensus_types::signing::Signature; - use arc_consensus_types::{Address, Height, ProposalData, ProposalFin, ProposalInit, Round}; - use malachitebft_app_channel::app::streaming::StreamContent; - use proptest::prelude::*; - - use super::*; - - /// Default validator count for tests. Large enough that the global cap - /// (`MAX_STREAMS_PER_PEER * NUM_VALIDATORS`) does not interfere with - /// per-peer or per-stream limit tests. - const NUM_VALIDATORS: usize = 100; - - /// Height encoded into helper-built stream IDs. Matches the `current_height` - /// the helper-based tests construct their maps with, so those streams are - /// not dropped as stale by the height check in `PartStreamsMap::insert`. - const HELPER_STREAM_HEIGHT: u64 = 1; - - impl PartStreamsMap { - /// Test-only wrapper that panics on [`InsertResult::Invalid`]. - /// Returns `Some(parts)` on [`InsertResult::Complete`], `None` on [`InsertResult::Pending`]. - fn must_insert( - &mut self, - peer_id: PeerId, - msg: StreamMessage, - ) -> Option { - match self.insert(peer_id, msg) { - InsertResult::Complete(parts) => Some(parts), - InsertResult::Pending | InsertResult::Stale => None, - InsertResult::Invalid(e) => panic!("unexpected InsertError: {e}"), - } - } - } - - // Helper functions to easily create test messages - fn make_message( - stream_id: &StreamId, - sequence: Sequence, - part: ProposalPart, - ) -> StreamMessage { - StreamMessage { - stream_id: stream_id.clone(), - sequence, - content: StreamContent::Data(part), - } - } - - fn make_fin_message(stream_id: &StreamId, sequence: Sequence) -> StreamMessage { - StreamMessage { - stream_id: stream_id.clone(), - sequence, - content: StreamContent::Fin, - } - } - - fn make_init_part() -> ProposalPart { - ProposalPart::Init(ProposalInit { - height: Height::new(1), - round: Round::new(0), - pol_round: Round::new(0), - proposer: Address::new([0xa; 20]), - }) - } - - fn make_data_part(data: u8) -> ProposalPart { - ProposalPart::Data(ProposalData { - bytes: vec![data].into(), - }) - } - - fn make_data_part_with_size(len: usize) -> ProposalPart { - ProposalPart::Data(ProposalData { - bytes: vec![0xAB; len].into(), - }) - } - - fn make_stream_id(id: u8) -> StreamId { - let mut bytes = vec![0u8; STREAM_ID_LEN]; - bytes[..8].copy_from_slice(&HELPER_STREAM_HEIGHT.to_be_bytes()); - bytes[STREAM_ID_LEN - 1] = id; - StreamId::new(bytes.into()) - } - - fn make_stream_id_u16(id: u16) -> StreamId { - let mut bytes = vec![0u8; STREAM_ID_LEN]; - bytes[..8].copy_from_slice(&HELPER_STREAM_HEIGHT.to_be_bytes()); - bytes[STREAM_ID_LEN - 2..].copy_from_slice(&id.to_be_bytes()); - StreamId::new(bytes.into()) - } - - fn make_fin_part() -> ProposalPart { - ProposalPart::Fin(ProposalFin { - signature: Signature::test(), - }) - } - - // --- Unit Tests --- - - #[test] - fn test_insert_single_message_stream_not_complete() { - let peer_1 = PeerId::random(); - let stream_1 = make_stream_id(101); - - let mut map = PartStreamsMap::new(Height::new(1), NUM_VALIDATORS); - let msg = make_message(&stream_1, 0, make_init_part()); - - let result = map.must_insert(peer_1, msg); - - assert!( - result.is_none(), - "Stream should not be complete after one message" - ); - assert_eq!(map.streams.len(), 1, "Map should contain one active stream"); - } - - #[test] - fn test_insert_in_order_completes_and_removes_stream() { - let peer_1 = PeerId::random(); - let stream_1 = make_stream_id(101); - - let mut map = PartStreamsMap::new(Height::new(1), NUM_VALIDATORS); - let init_msg = make_message(&stream_1, 0, make_init_part()); - let data_msg = make_message(&stream_1, 1, make_data_part(42)); - let data_fin_msg = make_message(&stream_1, 2, make_fin_part()); - let fin_msg = make_fin_message(&stream_1, 3); - - // Insert Init and Data parts - assert!(map.must_insert(peer_1, init_msg).is_none()); - assert_eq!(map.streams.len(), 1); - assert!(map.must_insert(peer_1, data_msg).is_none()); - assert_eq!(map.streams.len(), 1); - assert!(map.must_insert(peer_1, data_fin_msg).is_none()); - assert_eq!(map.streams.len(), 1); - - // Insert final part - let result = map.must_insert(peer_1, fin_msg); - - assert!( - result.is_some(), - "Stream should be complete and return ProposalParts" - ); - assert!( - map.streams.is_empty(), - "Map should be empty after stream is complete" - ); - } - - #[test] - fn test_insert_out_of_order_completes_and_removes_stream() { - let peer_1 = PeerId::random(); - let stream_1 = make_stream_id(101); - - let init_msg = make_message(&stream_1, 0, make_init_part()); - let data_msg = make_message(&stream_1, 1, make_data_part(42)); - let data_fin_msg = make_message(&stream_1, 2, make_fin_part()); - let fin_msg = make_fin_message(&stream_1, 3); - - let parts = [ - init_msg.clone(), - data_msg.clone(), - data_fin_msg.clone(), - fin_msg.clone(), - ]; - - use itertools::Itertools; - - // Test all permutations of message order - for perm in parts.iter().permutations(parts.len()) { - let mut map = PartStreamsMap::new(Height::new(1), NUM_VALIDATORS); - - // Insert all but the last message - for msg in &perm[..3] { - assert!(map.must_insert(peer_1, (*msg).clone()).is_none()); - assert_eq!(map.streams.len(), 1); - } - - // Insert the last message, which should complete the stream - let result = map.must_insert(peer_1, perm[3].clone()); - - assert!( - result.is_some(), - "Stream should be complete and return ProposalParts" - ); - assert!( - map.streams.is_empty(), - "Map should be empty after stream is complete" - ); - } - } - - #[test] - fn test_insert_returns_invalid_with_duplicate_init() { - let peer = PeerId::random(); - let stream = make_stream_id(101); - - let mut map = PartStreamsMap::new(Height::new(1), NUM_VALIDATORS); - - // First Init - assert!(map - .must_insert(peer, make_message(&stream, 0, make_init_part())) - .is_none()); - - // Second Init - assert!(map - .must_insert(peer, make_message(&stream, 1, make_init_part())) - .is_none()); - - // Complete the stream - let result = map.insert(peer, make_fin_message(&stream, 2)); - - // Insert fails with DuplicatePart("Init") - assert!(matches!( - result, - InsertResult::Invalid(InsertError::AssemblyFailed( - ProposalPartsError::DuplicatePart("Init") - )) - )); - assert!( - map.streams.is_empty(), - "completed stream must be removed even when assembly fails" - ); - } - - #[test] - fn test_insert_returns_invalid_with_duplicate_fin() { - let peer = PeerId::random(); - let stream = make_stream_id(101); - - let mut map = PartStreamsMap::new(Height::new(1), NUM_VALIDATORS); - - // Init - assert!(map - .must_insert(peer, make_message(&stream, 0, make_init_part())) - .is_none()); - - // First Fin - assert!(map - .must_insert(peer, make_message(&stream, 1, make_fin_part())) - .is_none()); - - // Second Fin - assert!(map - .must_insert(peer, make_message(&stream, 2, make_fin_part())) - .is_none()); - - // Complete the stream - let result = map.insert(peer, make_fin_message(&stream, 3)); - - // Insert fails with DuplicatePart("Fin") - assert!(matches!( - result, - InsertResult::Invalid(InsertError::AssemblyFailed( - ProposalPartsError::DuplicatePart("Fin") - )) - )); - assert!( - map.streams.is_empty(), - "completed stream must be removed even when assembly fails" - ); - } - - #[test] - fn test_insert_duplicate_sequence_is_ignored() { - let peer_1 = PeerId::random(); - let stream_1 = make_stream_id(101); - - let mut map = PartStreamsMap::new(Height::new(1), NUM_VALIDATORS); - let init_msg = make_message(&stream_1, 0, make_init_part()); - let data_msg = make_message(&stream_1, 1, make_data_part(42)); - let data_msg_duplicate = make_message(&stream_1, 1, make_data_part(99)); // Same seq - let data_fin_msg = make_message(&stream_1, 2, make_fin_part()); - let fin_msg = make_fin_message(&stream_1, 3); - - map.must_insert(peer_1, init_msg); - map.must_insert(peer_1, data_msg); - map.must_insert(peer_1, data_fin_msg); - - // Insert duplicate message - let result_duplicate = map.must_insert(peer_1, data_msg_duplicate); - assert!( - result_duplicate.is_none(), - "Duplicate message should be ignored and return None" - ); - - // The stream state should not be corrupted and should complete normally - let result_final = map.must_insert(peer_1, fin_msg); - assert!( - result_final.is_some(), - "Stream should complete successfully after ignoring a duplicate" - ); - assert!(map.streams.is_empty(), "Completed stream should be removed"); - } - - #[test] - fn test_stream_with_missing_part_is_not_completed() { - let peer_1 = PeerId::random(); - let stream_1 = make_stream_id(101); - - let mut map = PartStreamsMap::new(Height::new(1), NUM_VALIDATORS); - let init_msg = make_message(&stream_1, 0, make_init_part()); - // Sequence 1 is missing - let fin_msg = make_message(&stream_1, 2, make_fin_part()); - - map.must_insert(peer_1, init_msg); - let result = map.must_insert(peer_1, fin_msg); - - assert!( - result.is_none(), - "Stream should not complete if a part is missing" - ); - assert_eq!( - map.streams.len(), - 1, - "Incomplete stream should remain in the map" - ); - } - - #[test] - fn test_multiple_interleaved_streams() { - let peer_1 = PeerId::random(); - let stream_1 = make_stream_id(101); - let stream_2 = make_stream_id(202); - - let mut map = PartStreamsMap::new(Height::new(1), NUM_VALIDATORS); - - // Messages for two different streams - let s1_init = make_message(&stream_1, 0, make_init_part()); - let s1_data_fin = make_message(&stream_1, 1, make_fin_part()); - let s1_fin = make_fin_message(&stream_1, 2); - let s2_init = make_message(&stream_2, 0, make_init_part()); - let s2_data = make_message(&stream_2, 1, make_data_part(10)); - let s2_data_fin = make_message(&stream_2, 2, make_fin_part()); - let s2_fin = make_fin_message(&stream_2, 3); - - // Interleave inserts - map.must_insert(peer_1, s1_init); - assert_eq!(map.streams.len(), 1); - map.must_insert(peer_1, s2_init); - assert_eq!( - map.streams.len(), - 2, - "Map should track two separate streams" - ); - - map.must_insert(peer_1, s1_data_fin); - assert_eq!(map.streams.len(), 2); - map.must_insert(peer_1, s2_data_fin); - assert_eq!(map.streams.len(), 2); - - // Complete stream 1 - let s1_result = map.must_insert(peer_1, s1_fin); - assert!(s1_result.is_some(), "Stream 1 should complete"); - assert_eq!( - map.streams.len(), - 1, - "Map should have one stream left after S1 completes" - ); - - // Continue and complete stream 2 - map.must_insert(peer_1, s2_data); - let s2_result = map.must_insert(peer_1, s2_fin); - assert!(s2_result.is_some(), "Stream 2 should complete"); - assert!( - map.streams.is_empty(), - "Map should be empty after all streams are complete" - ); - } - - #[test] - fn test_per_peer_stream_limit() { - let peer_1 = PeerId::random(); - let mut map = PartStreamsMap::new(Height::new(1), NUM_VALIDATORS); - - // Create MAX_STREAMS_PER_PEER streams - for i in 0..MAX_STREAMS_PER_PEER { - let stream = make_stream_id(i as u8); - let msg = make_message(&stream, 0, make_init_part()); - assert!( - map.must_insert(peer_1, msg).is_none(), - "Should accept stream {i}" - ); - } - - assert_eq!( - map.streams.len(), - MAX_STREAMS_PER_PEER, - "Should have exactly MAX_STREAMS_PER_PEER streams" - ); - - // Try to create one more stream - should be rejected - let overflow_stream = make_stream_id(255); - let overflow_msg = make_message(&overflow_stream, 0, make_init_part()); - let result = map.must_insert(peer_1, overflow_msg); - - assert!( - result.is_none(), - "Should reject stream exceeding per-peer limit" - ); - assert_eq!( - map.streams.len(), - MAX_STREAMS_PER_PEER, - "Stream count should remain unchanged after rejection" - ); - - // Complete one stream to free up a slot - let stream_0 = make_stream_id(0); - let fin_msg = make_message(&stream_0, 1, make_fin_part()); - map.must_insert(peer_1, fin_msg); - let fin = make_fin_message(&stream_0, 2); - map.must_insert(peer_1, fin); - - assert_eq!( - map.streams.len(), - MAX_STREAMS_PER_PEER - 1, - "Should have one less stream after completion" - ); - - // Now we should be able to add a new stream - let new_stream = make_stream_id(100); - let new_msg = make_message(&new_stream, 0, make_init_part()); - assert!( - map.must_insert(peer_1, new_msg).is_none(), - "Should accept new stream after one completes" - ); - assert_eq!(map.streams.len(), MAX_STREAMS_PER_PEER); - } - - #[test] - fn test_per_stream_message_limit() { - let peer_1 = PeerId::random(); - let stream_1 = make_stream_id(101); - let mut map = PartStreamsMap::new(Height::new(1), NUM_VALIDATORS); - - // Send Init - let init_msg = make_message(&stream_1, 0, make_init_part()); - assert!(map.must_insert(peer_1, init_msg).is_none()); - - // Send MAX_MESSAGES_PER_STREAM - 1 data messages (accounting for init already sent) - for i in 1..MAX_MESSAGES_PER_STREAM { - let msg = make_message(&stream_1, i as u64, make_data_part(i as u8)); - let result = map.must_insert(peer_1, msg); - assert!( - result.is_none(), - "Should accept message {i} of {MAX_MESSAGES_PER_STREAM}" - ); - } - - assert_eq!(map.streams.len(), 1, "Stream should still be active"); - - // Try to send one more message - should be rejected - let overflow_msg = make_message( - &stream_1, - MAX_MESSAGES_PER_STREAM as u64, - make_data_part(MAX_MESSAGES_PER_STREAM as u8), - ); - let result = map.must_insert(peer_1, overflow_msg); - - assert!( - result.is_none(), - "Should reject message exceeding per-stream limit" - ); - - assert_eq!(map.streams.len(), 0, "Stream has been evicted"); - } - - #[test] - fn test_per_peer_limit_independent_across_peers() { - let peer_1 = PeerId::random(); - let peer_2 = PeerId::random(); - let mut map = PartStreamsMap::new(Height::new(1), NUM_VALIDATORS); - - // Peer 1 creates MAX_STREAMS_PER_PEER streams - for i in 0..MAX_STREAMS_PER_PEER { - let stream = make_stream_id(i as u8); - let msg = make_message(&stream, 0, make_init_part()); - map.must_insert(peer_1, msg); - } - - // Peer 2 should also be able to create MAX_STREAMS_PER_PEER streams - for i in 0..MAX_STREAMS_PER_PEER { - let stream = make_stream_id(i as u8); - let msg = make_message(&stream, 0, make_init_part()); - let result = map.must_insert(peer_2, msg); - assert!( - result.is_none(), - "Peer 2 should be able to create stream {i}" - ); - } - - if MAX_STREAMS_PER_PEER * 2 <= max_total_streams(NUM_VALIDATORS) { - // Both peers should have their streams accepted - assert_eq!( - map.streams.len(), - MAX_STREAMS_PER_PEER * 2, - "Should have streams from both peers" - ); - } else { - // Total streams limit should have been enforced - assert_eq!( - map.streams.len(), - max_total_streams(NUM_VALIDATORS), - "Should have total streams limited to max_total_streams(NUM_VALIDATORS)" - ); - } - - // Both peers should now be at their limit - let overflow_stream = make_stream_id(255); - let overflow_msg_p1 = make_message(&overflow_stream, 0, make_init_part()); - assert!( - map.must_insert(peer_1, overflow_msg_p1).is_none(), - "Peer 1 should be rejected" - ); - - let overflow_msg_p2 = make_message(&overflow_stream, 0, make_init_part()); - assert!( - map.must_insert(peer_2, overflow_msg_p2).is_none(), - "Peer 2 should be rejected" - ); - } - - #[test] - fn test_stream_age_eviction() { - let peer_1 = PeerId::random(); - let stream_1 = make_stream_id(101); - let stream_2 = make_stream_id(102); - let stream_3 = make_stream_id(103); - - let mut map = PartStreamsMap::new(Height::new(1), NUM_VALIDATORS); - - // Create first stream - let msg1 = make_message(&stream_1, 0, make_init_part()); - map.must_insert(peer_1, msg1); - assert_eq!(map.streams.len(), 1); - - // Manually set the created_at time to be older than MAX_STREAM_AGE - if let Some(state) = map.streams.get_mut(&(peer_1, stream_1.clone())) { - state.created_at = Instant::now() - MAX_STREAM_AGE - Duration::from_secs(1); - } - - // Create a second stream (this will not trigger eviction of the old one yet) - let msg2 = make_message(&stream_2, 0, make_init_part()); - map.must_insert(peer_1, msg2); - - // The old stream should not have been evicted - assert!( - map.streams.contains_key(&(peer_1, stream_1.clone())), - "Old stream should not have been evicted yet" - ); - assert!( - map.streams.contains_key(&(peer_1, stream_2.clone())), - "New stream should be present" - ); - - // Set last_eviction far enough in the past to force eviction check - map.last_eviction = Instant::now() - MAX_STREAM_AGE - Duration::from_secs(1); - - // Create a third stream to trigger eviction of the old one - let msg3 = make_message(&stream_3, 0, make_init_part()); - map.must_insert(peer_1, msg3); - - // The old stream should have been evicted - assert!( - !map.streams.contains_key(&(peer_1, stream_1)), - "Old stream should have been evicted" - ); - assert!( - map.streams.contains_key(&(peer_1, stream_2)), - "Second stream should be present" - ); - assert!( - map.streams.contains_key(&(peer_1, stream_3)), - "New stream should be present" - ); - } - - #[test] - fn test_total_streams_limit_eviction() { - // Use a small validator count so we can fill the global cap easily. - let num_validators = 4; - let cap = max_total_streams(num_validators); - let mut map = PartStreamsMap::new(Height::new(1), num_validators); - - // One peer opens 2 streams (more than any other peer). - let busy_peer = PeerId::random(); - for i in 0..2u8 { - let stream = make_stream_id(i); - let msg = make_message(&stream, 0, make_init_part()); - map.must_insert(busy_peer, msg); - } - - // Make its first stream the oldest. - let oldest_stream = make_stream_id(0); - let oldest_key = (busy_peer, oldest_stream.clone()); - map.streams.get_mut(&oldest_key).unwrap().created_at = - Instant::now() - Duration::from_secs(100); - - // Fill the remaining capacity with 1-stream peers. - #[allow(clippy::arithmetic_side_effects)] - for _ in 0..cap - 2 { - let peer = PeerId::random(); - let stream = make_stream_id(0); - let msg = make_message(&stream, 0, make_init_part()); - map.must_insert(peer, msg); - } - assert_eq!(map.streams.len(), cap); - - // One more stream from a new peer triggers eviction. - let new_peer = PeerId::random(); - let new_stream = make_stream_id(0); - let new_msg = make_message(&new_stream, 0, make_init_part()); - map.must_insert(new_peer, new_msg); - - // Cap preserved: evicted one, added one. - assert_eq!(map.streams.len(), cap); - - // The busiest peer's oldest stream should have been evicted. - assert!( - !map.streams.contains_key(&oldest_key), - "Oldest stream from the busiest peer should have been evicted" - ); - - // The new stream should be present. - assert!( - map.streams.contains_key(&(new_peer, new_stream)), - "New stream should be present" - ); - } - - #[test] - fn test_eviction_targets_busiest_peer_not_globally_oldest() { - // 3 validators, cap = 3 * 4 = 12 streams. - let num_validators = 3; - let cap = max_total_streams(num_validators); - let mut map = PartStreamsMap::new(Height::new(1), num_validators); - - // Peer A opens 1 stream and we make it the globally oldest. - let peer_a = PeerId::random(); - let stream_a = make_stream_id(0); - let msg = make_message(&stream_a, 0, make_init_part()); - map.must_insert(peer_a, msg); - map.streams - .get_mut(&(peer_a, stream_a.clone())) - .unwrap() - .created_at = Instant::now() - Duration::from_secs(200); - - // Peer B opens 4 streams — the per-peer maximum. - let peer_b = PeerId::random(); - for i in 0..MAX_STREAMS_PER_PEER as u8 { - let stream = make_stream_id(i); - let msg = make_message(&stream, 0, make_init_part()); - map.must_insert(peer_b, msg); - } - - // Make peer B's first stream older than all of its other streams - // but still newer than peer A's stream. - let peer_b_oldest = make_stream_id(0); - map.streams - .get_mut(&(peer_b, peer_b_oldest.clone())) - .unwrap() - .created_at = Instant::now() - Duration::from_secs(100); - - // Fill the rest of the cap with single-stream peers. - #[allow(clippy::arithmetic_side_effects)] - let remaining = cap - 1 - MAX_STREAMS_PER_PEER; - for i in 0..remaining { - let peer = PeerId::random(); - let stream = make_stream_id(i as u8); - let msg = make_message(&stream, 0, make_init_part()); - map.must_insert(peer, msg); - } - assert_eq!(map.streams.len(), cap); - - // Trigger eviction by inserting one more stream. - let new_peer = PeerId::random(); - let new_stream = make_stream_id(0); - map.must_insert(new_peer, make_message(&new_stream, 0, make_init_part())); - - assert_eq!(map.streams.len(), cap); - - // Peer A's stream is the globally oldest, but peer B is the busiest. - // The eviction policy targets peer B's oldest stream, not peer A's. - assert!( - map.streams.contains_key(&(peer_a, stream_a)), - "Peer A's stream should be preserved despite being the globally oldest" - ); - assert!( - !map.streams.contains_key(&(peer_b, peer_b_oldest)), - "Peer B's oldest stream should have been evicted (busiest peer)" - ); - } - - #[test] - fn test_zero_validators_uses_per_peer_floor() { - // With zero validators the cap must floor at MAX_STREAMS_PER_PEER so the - // map is usable during the pre-validator-set startup window. - let map = PartStreamsMap::new(Height::new(1), 0); - assert_eq!(map.max_total_streams, MAX_STREAMS_PER_PEER); - assert_eq!(max_total_streams(0), MAX_STREAMS_PER_PEER); - } - - #[test] - fn test_set_num_validators_grows_cap() { - let mut map = PartStreamsMap::new(Height::new(1), 0); - assert_eq!(map.max_total_streams, MAX_STREAMS_PER_PEER); - - map.set_num_validators(25); - assert_eq!(map.max_total_streams, MAX_STREAMS_PER_PEER * 25); - } - - #[test] - fn test_set_num_validators_shrinks_cap_and_trims_over_cap_streams() { - // Start with a generous cap and populate it with streams from many peers. - let num_validators = 10; - let cap = max_total_streams(num_validators); - let mut map = PartStreamsMap::new(Height::new(1), num_validators); - - for i in 0..cap { - let peer = PeerId::random(); - let stream = make_stream_id_u16(i as u16); - let msg = make_message(&stream, 0, make_init_part()); - map.must_insert(peer, msg); - } - assert_eq!(map.streams.len(), cap); - - // Shrinking the validator set reduces the cap; existing streams above - // the new cap must be trimmed in place. - let new_num_validators = 3; - let new_cap = max_total_streams(new_num_validators); - map.set_num_validators(new_num_validators); - - assert_eq!(map.max_total_streams, new_cap); - assert_eq!( - map.streams.len(), - new_cap, - "streams.len() must be clamped to the new cap after shrinkage" - ); - } - - #[test] - fn test_busiest_peer_tie_breaking_is_deterministic() { - // Two peers with equal stream counts. The BTreeMap-based implementation - // must pick the larger PeerId (max_by_key returns the last tied element - // in sorted order) regardless of insertion order. - let peer_a = PeerId::random(); - let peer_b = PeerId::random(); - let expected = peer_a.max(peer_b); - - for insertion_order in [[peer_a, peer_b], [peer_b, peer_a]] { - let mut map = PartStreamsMap::new(Height::new(1), 10); - for peer in insertion_order { - for i in 0..2u8 { - let stream = make_stream_id(i); - map.must_insert(peer, make_message(&stream, 0, make_init_part())); - } - } - assert_eq!( - map.busiest_peer(), - Some(expected), - "busiest_peer must return the larger PeerId when counts tie" - ); - } - } - - #[test] - fn test_completed_streams_dont_count_toward_limits() { - let peer_1 = PeerId::random(); - let mut map = PartStreamsMap::new(Height::new(1), NUM_VALIDATORS); - - // Create and complete MAX_STREAMS_PER_PEER streams - for i in 0..MAX_STREAMS_PER_PEER { - let stream = make_stream_id(i as u8); - - // Send complete stream - let init = make_message(&stream, 0, make_init_part()); - let fin_part = make_message(&stream, 1, make_fin_part()); - let fin = make_fin_message(&stream, 2); - - map.must_insert(peer_1, init); - map.must_insert(peer_1, fin_part); - map.must_insert(peer_1, fin); - } - - // All streams should be completed and removed - assert_eq!( - map.streams.len(), - 0, - "All completed streams should be removed" - ); - - // Should be able to create MAX_STREAMS_PER_PEER new streams - for i in 0..MAX_STREAMS_PER_PEER { - let stream = make_stream_id((i + 100) as u8); - let msg = make_message(&stream, 0, make_init_part()); - assert!( - map.must_insert(peer_1, msg).is_none(), - "Should accept new stream {i} after previous ones completed" - ); - } - - assert_eq!( - map.streams.len(), - MAX_STREAMS_PER_PEER, - "Should have MAX_STREAMS_PER_PEER new streams" - ); - } - - #[test] - fn test_evict_old_streams_removes_all_expired() { - let mut map = PartStreamsMap::new(Height::new(1), NUM_VALIDATORS); - let peer = PeerId::random(); - - // Create 3 streams, 2 old and 1 new - for i in 0..3 { - let stream = make_stream_id(i); - let msg = make_message(&stream, 0, make_init_part()); - map.must_insert(peer, msg); - } - - // Age first two streams - for i in 0..2 { - let stream = make_stream_id(i); - if let Some(state) = map.streams.get_mut(&(peer, stream)) { - state.created_at = Instant::now() - MAX_STREAM_AGE - Duration::from_secs(1); - } - } - - // No eviction yet because `last_eviction` is recent - map.last_eviction = Instant::now(); - - let stream_2 = make_stream_id(2); - let msg = make_message(&stream_2, 1, make_data_part(1)); - map.must_insert(peer, msg); - - // Should still have all 3 streams - assert_eq!(map.streams.len(), 3); - - // Now trigger eviction by setting `last_eviction` far in the past - map.last_eviction = Instant::now() - MAX_STREAM_AGE - Duration::from_secs(1); - - // Trigger eviction by inserting new message into remaining stream - let msg = make_message(&stream_2, 1, make_data_part(2)); - map.must_insert(peer, msg); - - // Should only have 1 stream left - assert_eq!(map.streams.len(), 1); - assert!(map.streams.contains_key(&(peer, stream_2))); - } - - #[test] - fn test_message_limit_independent_across_streams() { - let peer = PeerId::random(); - let mut map = PartStreamsMap::new(Height::new(1), NUM_VALIDATORS); - - // Fill first stream to capacity - let stream_1 = make_stream_id(1); - for i in 0..MAX_MESSAGES_PER_STREAM { - let msg = make_message(&stream_1, i as u64, make_data_part(i as u8)); - map.must_insert(peer, msg); - } - - // Second stream should still accept messages - let stream_2 = make_stream_id(2); - let msg = make_message(&stream_2, 0, make_init_part()); - assert!( - map.must_insert(peer, msg).is_none(), - "Second stream should accept messages despite first being at limit" - ); - } - - #[test] - fn test_evicted_stream_rejects_new_messages() { - let peer_1 = PeerId::random(); - let stream_1 = make_stream_id(101); - let mut map = PartStreamsMap::new(Height::new(1), NUM_VALIDATORS); - - // Send Init - let init_msg = make_message(&stream_1, 0, make_init_part()); - map.must_insert(peer_1, init_msg); - - // Exceed message limit to trigger eviction - for i in 1..=MAX_MESSAGES_PER_STREAM { - let msg = make_message(&stream_1, i as u64, make_data_part(i as u8)); - map.must_insert(peer_1, msg); - } - - // Verify stream was evicted - assert_eq!(map.streams.len(), 0, "Stream should be evicted"); - - // Try to send another message to the same stream - let new_msg = make_message( - &stream_1, - (MAX_MESSAGES_PER_STREAM + 1) as u64, - make_data_part(99), - ); - let result = map.must_insert(peer_1, new_msg); - - assert!( - result.is_none(), - "Message to evicted stream should be rejected" - ); - assert_eq!( - map.streams.len(), - 0, - "No new stream should be created for evicted stream" - ); - } - - #[test] - fn test_stream_id_init_height_mismatch_rejected() { - let peer_1 = PeerId::random(); - let stream_1 = new_stream_id(Height::new(5), Round::new(0), 101); - let mut map = PartStreamsMap::new(Height::new(5), NUM_VALIDATORS); - - // Send Init message whose payload height disagrees with stream_id. - let mut init_part = make_init_part(); - if let ProposalPart::Init(ref mut init) = init_part { - init.height = Height::new(3); - } - let init_msg = make_message(&stream_1, 0, init_part); - let result = map.insert(peer_1, init_msg); - - assert!(matches!( - result, - InsertResult::Invalid(InsertError::StreamIdHeightMismatch { - stream_height, - init_height, - }) if stream_height == Height::new(5) && init_height == Height::new(3) - )); - assert_eq!(map.streams.len(), 0, "Mismatched stream should be evicted"); - assert!( - map.evicted.peek(&(peer_1, stream_1)).is_some(), - "Mismatched stream should be marked as evicted" - ); - } - - #[test] - fn test_stale_lone_data_parts_do_not_starve_current_proposal() { - // Regression guard: stale lone non-Init parts (e.g. old proposal parts - // re-circulating on gossipsub) must not consume a peer's per-peer stream - // budget, so a proposer's current-height proposal is always admitted. - // `insert` rejects parts whose stream_id-encoded height is below - // `current_height` up front, before they can occupy a slot. - let peer = PeerId::random(); - let mut map = PartStreamsMap::new(Height::new(100), NUM_VALIDATORS); - - // Each stale stream carries only a lone data part (no Init at sequence 0); - // the stream_id still encodes the old height, so it must be rejected. - for nonce in 0..MAX_STREAMS_PER_PEER as u32 { - let stream_id = new_stream_id(Height::new(90 + nonce as u64), Round::new(0), nonce); - let data_msg = make_message(&stream_id, 1, make_data_part(nonce as u8)); - assert!(matches!(map.insert(peer, data_msg), InsertResult::Stale)); - } - - assert_eq!( - map.streams.len(), - 0, - "Stale lone-data streams must be rejected up front, not lingering" - ); - assert_eq!(map.peer_streams_count(peer), 0); - - // The legitimate current-height proposal (carrying a real Init) must be - // admitted because no stale stream is occupying the per-peer budget. - let mut init_part = make_init_part(); - if let ProposalPart::Init(ref mut init) = init_part { - init.height = Height::new(100); - } - let current_id = - new_stream_id(Height::new(100), Round::new(0), MAX_STREAMS_PER_PEER as u32); - let init_msg = make_message(¤t_id, 0, init_part); - - assert!( - map.must_insert(peer, init_msg).is_none(), - "Current proposal Init is incomplete on its own" - ); - assert_eq!( - map.streams.len(), - 1, - "Current-height Init stream must be admitted" - ); - assert_eq!(map.peer_streams_count(peer), 1); - } - - #[test] - fn test_height_advance_purges_tracked_stale_streams() { - // A stream created while its height was current becomes stale once the - // node advances. Such streams must be purged on the height advance so - // they stop consuming the proposer's per-peer budget, instead of - // lingering (and potentially starving the current proposal) until the - // age sweep. - let peer = PeerId::random(); - let mut map = PartStreamsMap::new(Height::new(10), NUM_VALIDATORS); - - // Fill the peer's budget with incomplete lone-data streams at the - // current height — each lingers because it carries no Init yet. - for nonce in 0..MAX_STREAMS_PER_PEER as u32 { - let stream_id = new_stream_id(Height::new(10), Round::new(0), nonce); - let data = make_message(&stream_id, 1, make_data_part(nonce as u8)); - assert!(map.must_insert(peer, data).is_none()); - } - assert_eq!(map.peer_streams_count(peer), MAX_STREAMS_PER_PEER); - - // Advancing past those heights must purge them. - map.set_current_height(Height::new(11)); - assert_eq!( - map.streams.len(), - 0, - "stale tracked streams must be purged on height advance" - ); - - // The peer's current-height proposal is now admitted. - let mut init_part = make_init_part(); - if let ProposalPart::Init(ref mut init) = init_part { - init.height = Height::new(11); - } - let current = new_stream_id(Height::new(11), Round::new(0), 0); - let init_msg = make_message(¤t, 0, init_part); - assert!(map.must_insert(peer, init_msg).is_none()); - assert_eq!( - map.streams.len(), - 1, - "current-height proposal must be admitted" - ); - assert_eq!(map.peer_streams_count(peer), 1); - } - - #[test] - fn test_complete_on_stream_id_init_height_mismatch_returns_invalid() { - // A stream can complete on the Init insert if Init arrives last. Even - // then, a stream_id/Init height disagreement is peer misbehaviour. - let peer = PeerId::random(); - let mut map = PartStreamsMap::new(Height::new(10), NUM_VALIDATORS); - let stream = new_stream_id(Height::new(10), Round::new(0), 0); - - // Fin parts arrive first; height stays unknown so the stream is kept. - assert!(matches!( - map.insert(peer, make_message(&stream, 1, make_fin_part())), - InsertResult::Pending - )); - assert!(matches!( - map.insert(peer, make_fin_message(&stream, 2)), - InsertResult::Pending - )); - - // Init arrives last with a mismatched payload height, completing the stream. - let mut init_part = make_init_part(); - if let ProposalPart::Init(ref mut init) = init_part { - init.height = Height::new(3); - } - let result = map.insert(peer, make_message(&stream, 0, init_part)); - assert!(matches!( - result, - InsertResult::Invalid(InsertError::StreamIdHeightMismatch { - stream_height, - init_height, - }) if stream_height == Height::new(10) && init_height == Height::new(3) - )); - assert_eq!(map.streams.len(), 0, "completed stream must be removed"); - assert!( - map.evicted.peek(&(peer, stream)).is_some(), - "Mismatched stream should be marked as evicted" - ); - } - - #[test] - fn test_evicted_set_retained_across_eviction_cycles() { - let mut map = PartStreamsMap::new(Height::new(1), NUM_VALIDATORS); - let peer = PeerId::random(); - - // Create and evict a stream by exceeding message limit - let stream = make_stream_id(1); - for i in 0..=MAX_MESSAGES_PER_STREAM { - let msg = make_message(&stream, i as u64, make_data_part(i as u8)); - map.must_insert(peer, msg); - } - - assert!( - !map.evicted.is_empty(), - "Evicted set should contain entries" - ); - - // Simulate time passing beyond MAX_STREAM_AGE and trigger eviction cycle - map.last_eviction = Instant::now() - MAX_STREAM_AGE - Duration::from_secs(1); - map.evict_old_streams(); - - // Evicted entries should be retained — the LRU is self-bounding - assert!( - !map.evicted.is_empty(), - "Evicted set should be retained across eviction cycles" - ); - } - - #[test] - fn test_oversized_chunk_rejected() { - let peer = PeerId::random(); - let stream = make_stream_id(1); - let mut map = PartStreamsMap::new(Height::new(1), NUM_VALIDATORS); - - let init_msg = make_message(&stream, 0, make_init_part()); - map.insert(peer, init_msg); - - // Send a data chunk exceeding CHUNK_SIZE - let oversized = make_data_part_with_size(CHUNK_SIZE + 1); - let msg = make_message(&stream, 1, oversized); - let result = map.insert(peer, msg); - - assert!( - matches!(result, InsertResult::Pending), - "Oversized chunk should be rejected" - ); - assert!( - map.streams.is_empty(), - "Stream should be evicted after oversized chunk" - ); - assert!( - map.evicted.peek(&(peer, stream)).is_some(), - "Stream should be marked as evicted" - ); - } - - #[test] - fn test_normal_chunk_accepted() { - let peer = PeerId::random(); - let stream = make_stream_id(1); - let mut map = PartStreamsMap::new(Height::new(1), NUM_VALIDATORS); - - let init_msg = make_message(&stream, 0, make_init_part()); - map.insert(peer, init_msg); - - // CHUNK_SIZE - 1 should be accepted - let under_limit = make_data_part_with_size(CHUNK_SIZE - 1); - let msg = make_message(&stream, 1, under_limit); - map.insert(peer, msg); - assert_eq!(map.streams.len(), 1, "Under-limit chunk should be accepted"); - - // Data chunk exactly at CHUNK_SIZE should be accepted - let at_limit = make_data_part_with_size(CHUNK_SIZE); - let msg = make_message(&stream, 2, at_limit); - let result = map.insert(peer, msg); - - assert!( - matches!(result, InsertResult::Pending), - "Stream should not be complete yet" - ); - assert_eq!(map.streams.len(), 1, "Stream should still be active"); - } - - #[test] - fn test_non_data_parts_not_subject_to_size_check() { - let peer = PeerId::random(); - let stream = make_stream_id(1); - let mut map = PartStreamsMap::new(Height::new(1), NUM_VALIDATORS); - - // Init and Fin are not Data variants, so they bypass the byte limit - let init_msg = make_message(&stream, 0, make_init_part()); - map.insert(peer, init_msg); - assert_eq!(map.streams.len(), 1, "Init should be accepted"); - - let fin_part_msg = make_message(&stream, 1, make_fin_part()); - map.insert(peer, fin_part_msg); - assert_eq!(map.streams.len(), 1, "Fin part should be accepted"); - - let fin_msg = make_fin_message(&stream, 2); - let result = map.insert(peer, fin_msg); - - assert!( - matches!(result, InsertResult::Complete(_)), - "Stream should complete — Init/Fin are not subject to chunk size limit" - ); - } - - #[test] - fn test_invalid_stream_id_length_rejected() { - let peer = PeerId::random(); - let mut map = PartStreamsMap::new(Height::new(1), NUM_VALIDATORS); - - // Too short - let short = StreamId::new(vec![0x01; 8].into()); - let msg = make_message(&short, 0, make_init_part()); - assert!(matches!( - map.insert(peer, msg), - InsertResult::Invalid(InsertError::InvalidStreamIdLength { - actual: 8, - expected: 16 - }) - )); - assert!(map.streams.is_empty(), "short stream_id should be rejected"); - - // Too long - let long = StreamId::new(vec![0x01; 1024].into()); - let msg = make_message(&long, 0, make_init_part()); - assert!(matches!( - map.insert(peer, msg), - InsertResult::Invalid(InsertError::InvalidStreamIdLength { - actual: 1024, - expected: 16 - }) - )); - assert!(map.streams.is_empty(), "long stream_id should be rejected"); - - // Empty - let empty = StreamId::new(vec![].into()); - let msg = make_message(&empty, 0, make_init_part()); - assert!(matches!( - map.insert(peer, msg), - InsertResult::Invalid(InsertError::InvalidStreamIdLength { - actual: 0, - expected: 16 - }) - )); - assert!(map.streams.is_empty(), "empty stream_id should be rejected"); - - // Exactly 16 bytes — accepted - let valid = new_stream_id(Height::new(1), Round::new(0), 0x01); - let msg = make_message(&valid, 0, make_init_part()); - assert!(map.must_insert(peer, msg).is_none()); // not complete, but accepted - assert_eq!(map.streams.len(), 1, "valid stream_id should be accepted"); - } - - #[test] - fn test_evicted_set_capped() { - let peer = PeerId::random(); - let mut map = PartStreamsMap::new(Height::new(100), NUM_VALIDATORS); - - // Send many Init messages whose payload height disagrees with the - // current-height stream ID. Each mismatch is rejected, evicted, and - // recorded in the evicted set. - let count = MAX_EVICTED_STREAMS + 500; - for i in 0..count { - let mut init = make_init_part(); - if let ProposalPart::Init(ref mut part) = init { - part.height = Height::new(1); - } - - let stream = new_stream_id(Height::new(100), Round::new(0), i as u32); - let msg = make_message(&stream, 0, init); - assert!(matches!( - map.insert(peer, msg), - InsertResult::Invalid(InsertError::StreamIdHeightMismatch { .. }) - )); - - assert!( - map.evicted.len() <= MAX_EVICTED_STREAMS, - "Evicted set should never exceed MAX_EVICTED_STREAMS, got {}", - map.evicted.len() - ); - } - - // After the loop, evicted should have been cleared at least once - assert!( - map.evicted.len() <= MAX_EVICTED_STREAMS, - "Evicted set should be bounded, got {}", - map.evicted.len() - ); - - // Map should still function correctly after clearing. - // Use a new peer and a current-height stream so it isn't stale-evicted. - let peer2 = PeerId::random(); - let stream = new_stream_id(Height::new(100), Round::new(0), 0xFF); - let mut init_part = make_init_part(); - if let ProposalPart::Init(ref mut part) = init_part { - part.height = Height::new(100); - } - let init = make_message(&stream, 0, init_part); - let data = make_message(&stream, 1, make_fin_part()); - let fin = make_fin_message(&stream, 2); - - assert!(map.must_insert(peer2, init).is_none()); - assert!(map.must_insert(peer2, data).is_none()); - assert!( - map.must_insert(peer2, fin).is_some(), - "Map should still complete streams after evicted set clearing" - ); - } - - #[test] - fn test_global_eviction_spares_low_volume_peer_when_others_saturate_cap() { - // Small validator set so the global cap is easy to saturate and we - // can exercise the global-eviction path. - let num_validators = 3; - let cap = max_total_streams(num_validators); - let saturating_peers_num = 2; - let mut map = PartStreamsMap::new(Height::new(1), num_validators); - let mut stream_id: u16 = 0; - - // Low-volume peer starts a single stream. - let low_volume_peer = PeerId::random(); - let low_volume_stream = make_stream_id_u16(stream_id); - let msg_init = make_message(&low_volume_stream, 0, make_init_part()); - let msg_part = make_message(&low_volume_stream, 1, make_data_part(0x42)); - stream_id += 1; - assert!( - map.must_insert(low_volume_peer, msg_init).is_none(), - "Init message on the low-volume stream should be accepted" - ); - - // Make it the globally oldest stream so a naive FIFO policy would - // target it first. - let low_volume_key = (low_volume_peer, low_volume_stream.clone()); - map.streams.get_mut(&low_volume_key).unwrap().created_at = - Instant::now() - Duration::from_secs(100); - - // Saturating peers each try to open MAX_STREAMS_PER_PEER + 5 streams; - // the per-peer limit caps each at MAX_STREAMS_PER_PEER. - let mut saturating_peers = Vec::with_capacity(saturating_peers_num); - for _ in 0..saturating_peers_num { - let peer_id = PeerId::random(); - saturating_peers.push(peer_id); - #[allow(clippy::arithmetic_side_effects)] - for _ in 0..MAX_STREAMS_PER_PEER + 5 { - let stream = make_stream_id_u16(stream_id); - stream_id += 1; - let msg = make_message(&stream, 0, make_init_part()); - map.must_insert(peer_id, msg); - } - - assert_eq!(map.peer_streams_count(peer_id), MAX_STREAMS_PER_PEER); - } - - // Fill the remaining capacity with 1-stream peers so the pool is at - // the global cap and the next insert triggers global eviction. - #[allow(clippy::arithmetic_side_effects)] - let filler_needed = cap - map.streams.len(); - for _ in 0..filler_needed { - let filler_peer = PeerId::random(); - let stream = make_stream_id_u16(stream_id); - stream_id += 1; - let msg = make_message(&stream, 0, make_init_part()); - map.must_insert(filler_peer, msg); - } - assert_eq!(map.streams.len(), cap, "Pool should be at the global cap"); - - // A new peer's stream now triggers global eviction. The busiest-peer - // policy must evict from a saturating peer, never the single-stream - // low-volume peer (even though it owns the globally oldest stream). - let new_peer = PeerId::random(); - let new_stream = make_stream_id_u16(stream_id); - map.must_insert(new_peer, make_message(&new_stream, 0, make_init_part())); - - assert_eq!(map.streams.len(), cap, "Pool should still be at the cap"); - assert!( - map.streams.contains_key(&low_volume_key), - "The low-volume stream should be preserved despite being the globally oldest" - ); - for peer_id in &saturating_peers { - assert!( - map.peer_streams_count(*peer_id) <= MAX_STREAMS_PER_PEER, - "Saturating peers should remain bounded by MAX_STREAMS_PER_PEER" - ); - } - - // Follow-up messages on the low-volume stream should still be accepted. - assert!( - map.must_insert(low_volume_peer, msg_part).is_none(), - "Follow up message on the low-volume stream should be accepted" - ); - } - - // --- Property-Based Tests --- - - proptest! { - #[test] - fn prop_per_peer_stream_limit_never_exceeded( - stream_attempts in prop::collection::vec(any::(), 1..50) - ) { - let peer = PeerId::random(); - let mut map = PartStreamsMap::new(Height::new(1), NUM_VALIDATORS); - - // Try to create streams using different IDs - for stream_id_byte in stream_attempts { - let stream = make_stream_id(stream_id_byte); - let msg = make_message(&stream, 0, make_init_part()); - map.must_insert(peer, msg); - - // Count how many streams this peer has - let peer_stream_count = map.peer_streams_count(peer); - - // Should never exceed the limit - prop_assert!( - peer_stream_count <= MAX_STREAMS_PER_PEER, - "Peer stream count {} exceeded limit {}", - peer_stream_count, - MAX_STREAMS_PER_PEER - ); - } - } - - #[test] - fn prop_per_stream_message_limit_never_exceeded( - message_count in 1..500usize - ) { - let peer = PeerId::random(); - let stream = make_stream_id(1); - let mut map = PartStreamsMap::new(Height::new(1), NUM_VALIDATORS); - - // Try to send many messages to the same stream - for i in 0..message_count { - let msg = make_message(&stream, i as u64, make_data_part((i % 256) as u8)); - map.must_insert(peer, msg); - - // Check the stream state if it still exists - if let Some(state) = map.streams.get(&(peer, stream.clone())) { - prop_assert!( - state.message_count <= MAX_MESSAGES_PER_STREAM, - "Stream message count {} exceeded limit {}", - state.message_count, - MAX_MESSAGES_PER_STREAM - ); - } - } - } - - #[test] - fn prop_total_streams_limit_never_exceeded( - peer_count in 1..50usize, - streams_per_peer in 1..=MAX_STREAMS_PER_PEER - ) { - // Use a small validator count so that `peer_count * streams_per_peer` - // can exceed the global cap and actually exercise global eviction. - let num_validators = 10; - let cap = max_total_streams(num_validators); - let mut map = PartStreamsMap::new(Height::new(1), num_validators); - let mut peers = Vec::new(); - - // Generate unique peers - for _ in 0..peer_count { - peers.push(PeerId::random()); - } - - // Try to create multiple streams for each peer - for peer in &peers { - for stream_idx in 0..streams_per_peer { - let stream = make_stream_id_u16(stream_idx as u16); - let msg = make_message(&stream, 0, make_init_part()); - map.must_insert(*peer, msg); - - // Total streams should never exceed the limit - prop_assert!( - map.streams.len() <= cap, - "Total stream count {} exceeded limit {}", - map.streams.len(), - cap - ); - } - } - } - - #[test] - fn prop_stream_age_eviction_works( - stream_count in 1..=MAX_STREAMS_PER_PEER - ) { - let peer = PeerId::random(); - let mut map = PartStreamsMap::new(Height::new(1), NUM_VALIDATORS); - - // Create streams - for i in 0..stream_count { - let stream = make_stream_id(i as u8); - let msg = make_message(&stream, 0, make_init_part()); - map.must_insert(peer, msg); - } - - let initial_count = map.streams.len(); - - // Age all streams beyond MAX_STREAM_AGE - for state in map.streams.values_mut() { - state.created_at = Instant::now() - MAX_STREAM_AGE - Duration::from_secs(1); - } - - // Set last eviction time far in the past to force eviction on next insert - map.last_eviction = Instant::now() - MAX_STREAM_AGE - Duration::from_secs(1); - - // Trigger eviction by inserting a new stream - let new_stream = make_stream_id(255); - let msg = make_message(&new_stream, 0, make_init_part()); - map.must_insert(peer, msg); - - // All old streams should be evicted, only the new one should remain - prop_assert!( - map.streams.len() <= 1, - "Expected at most 1 stream after aging {}, but found {}", - initial_count, - map.streams.len() - ); - } - - #[test] - fn prop_completed_streams_are_removed( - completion_count in 1..20usize - ) { - let peer = PeerId::random(); - let mut map = PartStreamsMap::new(Height::new(1), NUM_VALIDATORS); - - // Complete multiple streams - for i in 0..completion_count { - let stream = make_stream_id(i as u8); - - // Send complete stream: init, data, fin_part, fin - let init = make_message(&stream, 0, make_init_part()); - let data = make_message(&stream, 1, make_data_part(42)); - let fin_part = make_message(&stream, 2, make_fin_part()); - let fin = make_fin_message(&stream, 3); - - map.must_insert(peer, init); - map.must_insert(peer, data); - map.must_insert(peer, fin_part); - map.must_insert(peer, fin); - } - - // All completed streams should be removed - prop_assert_eq!( - map.streams.len(), - 0, - "Expected all completed streams to be removed, but {} remain", - map.streams.len() - ); - } - - #[test] - fn prop_limits_independent_across_peers( - peer_count in 2..10usize - ) { - let mut map = PartStreamsMap::new(Height::new(1), NUM_VALIDATORS); - let mut peers = Vec::new(); - - // Generate unique peers - for _ in 0..peer_count { - peers.push(PeerId::random()); - } - - // Each peer creates streams up to their limit - for peer in &peers { - for i in 0..MAX_STREAMS_PER_PEER { - let stream = make_stream_id(i as u8); - let msg = make_message(&stream, 0, make_init_part()); - map.must_insert(*peer, msg); - } - } - - // Verify each peer's stream count independently - for peer in &peers { - let stream_count = map.peer_streams_count(*peer); - - prop_assert!( - stream_count <= MAX_STREAMS_PER_PEER, - "Peer stream count {} exceeded limit {} for peer {:?}", - stream_count, - MAX_STREAMS_PER_PEER, - peer - ); - } - - // Also verify total doesn't exceed global limit - prop_assert!( - map.streams.len() <= max_total_streams(NUM_VALIDATORS), - "Total stream count {} exceeded limit {}", - map.streams.len(), - max_total_streams(NUM_VALIDATORS) - ); - } - - #[test] - fn prop_incomplete_streams_remain_buffered( - message_count in 1..10usize - ) { - let peer = PeerId::random(); - let stream = make_stream_id(1); - let mut map = PartStreamsMap::new(Height::new(1), NUM_VALIDATORS); - - // Send incomplete stream (no Fin message) - for i in 0..message_count { - let msg = make_message(&stream, i as u64, make_data_part(i as u8)); - let result = map.must_insert(peer, msg); - - // Should never complete without Fin - prop_assert!( - result.is_none(), - "Stream should not complete without Fin message" - ); - } - - // Stream should still be in the map - prop_assert!( - map.streams.contains_key(&(peer, stream)), - "Incomplete stream should remain buffered" - ); - } - - #[test] - fn prop_out_of_order_messages_complete_correctly( - // Generate a shuffled sequence of indices - seed in any::() - ) { - use rand::{SeedableRng, seq::SliceRandom}; - use rand::rngs::StdRng; - - let peer = PeerId::random(); - let stream = make_stream_id(1); - let mut map = PartStreamsMap::new(Height::new(1), NUM_VALIDATORS); - - // Create messages in order - let init = make_message(&stream, 0, make_init_part()); - let data = make_message(&stream, 1, make_data_part(42)); - let fin_part = make_message(&stream, 2, make_fin_part()); - let fin = make_fin_message(&stream, 3); - - let mut messages = [init, data, fin_part, fin]; - - // Shuffle messages - let mut rng = StdRng::seed_from_u64(seed); - messages.shuffle(&mut rng); - - // Insert all but the last message - for msg in &messages[..3] { - let result = map.must_insert(peer, msg.clone()); - prop_assert!( - result.is_none(), - "Stream should not complete until all messages received" - ); - } - - // Insert the last message, should complete - let result = map.must_insert(peer, messages[3].clone()); - prop_assert!( - result.is_some(), - "Stream should complete when all messages received, regardless of order" - ); - - // Stream should be removed after completion - prop_assert!( - !map.streams.contains_key(&(peer, stream)), - "Completed stream should be removed from map" - ); - } - - #[test] - fn prop_duplicate_sequences_ignored( - message_count in 1..20usize, - duplicate_indices in prop::collection::vec(0..20usize, 1..10) - ) { - let peer = PeerId::random(); - let stream = make_stream_id(1); - let mut map = PartStreamsMap::new(Height::new(1), NUM_VALIDATORS); - - // Send initial messages - for i in 0..message_count { - let msg = make_message(&stream, i as u64, make_data_part(i as u8)); - map.must_insert(peer, msg); - } - - let state_before = map.streams.get(&(peer, stream.clone())) - .map(|s| s.message_count); - - // Send duplicate messages - for &idx in &duplicate_indices { - if idx < message_count { - let duplicate = make_message(&stream, idx as u64, make_data_part(99)); - map.must_insert(peer, duplicate); - } - } - - // Message count should not increase from duplicates - if let Some(state) = map.streams.get(&(peer, stream)) { - prop_assert_eq!( - state.message_count, - state_before.unwrap_or(0), - "Duplicate messages should not increase message count" - ); - } - } - - #[test] - fn prop_missing_parts_prevent_completion( - total_parts in 5..15usize, // Need at least 5 parts: init, data1, data2, fin_part, fin - ) { - let peer = PeerId::random(); - let stream = make_stream_id(1); - let mut map = PartStreamsMap::new(Height::new(1), NUM_VALIDATORS); - - // Choose a missing index in the middle of data parts (not init, not fin_part, not fin) - // For total_parts=5: seq 0=init, 1=data, 2=data, 3=fin_part, 4=fin - // We can skip seq 1 or 2 - let missing_index = 1 + (total_parts % 2); // Will be 1 or 2 - - // Send init - map.must_insert(peer, make_message(&stream, 0, make_init_part())); - - // Send data parts, skipping the missing one - // Data parts go from seq 1 to seq (total_parts - 3) - for i in 1..total_parts - 2 { - if i != missing_index { - let msg = make_message(&stream, i as u64, make_data_part(i as u8)); - map.must_insert(peer, msg); - } - } - - // Send fin_part and fin - let fin_part = make_message(&stream, (total_parts - 2) as u64, make_fin_part()); - let fin = make_fin_message(&stream, (total_parts - 1) as u64); - - map.must_insert(peer, fin_part); - let result = map.must_insert(peer, fin); - - // Should not complete with missing part - prop_assert!( - result.is_none(), - "Stream should not complete with missing part at index {}", - missing_index - ); - - // Stream should remain in map - prop_assert!( - map.streams.contains_key(&(peer, stream)), - "Incomplete stream should remain in map" - ); - } - - #[test] - fn prop_multiple_interleaved_streams_independent( - stream_count in 2..=MAX_STREAMS_PER_PEER, - messages_per_stream in 2..10usize, - seed in any::() - ) { - use rand::{SeedableRng, seq::SliceRandom}; - use rand::rngs::StdRng; - - let peer = PeerId::random(); - let mut map = PartStreamsMap::new(Height::new(1), NUM_VALIDATORS); - let mut rng = StdRng::seed_from_u64(seed); - - // Create all messages for all streams - let mut all_messages = Vec::new(); - - for stream_idx in 0..stream_count { - let stream = make_stream_id(stream_idx as u8); - - // Init - all_messages.push((stream_idx, make_message(&stream, 0, make_init_part()))); - - // Data parts - for msg_idx in 1..messages_per_stream { - all_messages.push(( - stream_idx, - make_message(&stream, msg_idx as u64, make_data_part(msg_idx as u8)) - )); - } - - // Fin part - all_messages.push(( - stream_idx, - make_message(&stream, messages_per_stream as u64, make_fin_part()) - )); - - // Fin - all_messages.push(( - stream_idx, - make_fin_message(&stream, (messages_per_stream + 1) as u64) - )); - } - - // Shuffle to interleave messages from different streams - all_messages.shuffle(&mut rng); - - let mut completed = vec![false; stream_count]; - - // Insert all messages - for (stream_idx, msg) in all_messages { - let result = map.must_insert(peer, msg); - - // Mark stream as completed if it returns a result - if result.is_some() { - completed[stream_idx] = true; - } - } - - // All streams should have completed - prop_assert!( - completed.iter().all(|&c| c), - "All streams should complete independently: {:?}", - completed - ); - - // Map should be empty after all streams complete - prop_assert_eq!( - map.streams.len(), - 0, - "Map should be empty after all streams complete" - ); - } - - #[test] - fn prop_stream_completion_requires_init_and_fin( - has_init in any::(), - has_fin in any::(), - data_count in 1..10usize - ) { - let peer = PeerId::random(); - let stream = make_stream_id(1); - let mut map = PartStreamsMap::new(Height::new(1), NUM_VALIDATORS); - - let mut seq = 0u64; - - // Conditionally send init - if has_init { - map.must_insert(peer, make_message(&stream, seq, make_init_part())); - seq += 1; - } - - // Send data parts - for i in 0..data_count { - map.must_insert(peer, make_message(&stream, seq, make_data_part(i as u8))); - seq += 1; - } - - // Conditionally send fin_part and fin - let result = if has_fin { - map.must_insert(peer, make_message(&stream, seq, make_fin_part())); - seq += 1; - map.must_insert(peer, make_fin_message(&stream, seq)) - } else { - None - }; - - // Should only complete if both init and fin are present - if has_init && has_fin { - prop_assert!( - result.is_some(), - "Stream with init and fin should complete" - ); - } else { - prop_assert!( - result.is_none(), - "Stream without init={} or fin={} should not complete", - has_init, - has_fin - ); - } - } - - #[test] - fn prop_message_limit_independent_across_streams( - stream1_message_count in 1..MAX_MESSAGES_PER_STREAM, - stream2_message_count in 1..(MAX_MESSAGES_PER_STREAM / 2), - ) { - let peer = PeerId::random(); - let mut map = PartStreamsMap::new(Height::new(1), NUM_VALIDATORS); - - // Fill first stream up to its message count - let stream_1 = make_stream_id(1); - for i in 0..stream1_message_count { - let msg = make_message(&stream_1, i as u64, make_data_part(i as u8)); - map.must_insert(peer, msg); - } - - // Verify first stream exists and has the expected message count - let stream1_state = map.streams.get(&(peer, stream_1.clone())); - prop_assert!( - stream1_state.is_some(), - "Stream 1 should exist after inserting messages" - ); - prop_assert_eq!( - stream1_state.unwrap().message_count, - stream1_message_count, - "Stream 1 should have expected message count" - ); - - // Second stream should still accept messages independently - let stream_2 = make_stream_id(2); - for i in 0..stream2_message_count { - let msg = make_message(&stream_2, i as u64, make_data_part(i as u8)); - let result = map.must_insert(peer, msg); - - prop_assert!( - result.is_none(), - "Stream 2 message {} should be accepted despite stream 1 having {} messages", - i, - stream1_message_count - ); - } - - // Verify second stream exists and has its own independent message count - let stream2_state = map.streams.get(&(peer, stream_2)); - prop_assert!( - stream2_state.is_some(), - "Stream 2 should exist after inserting messages" - ); - prop_assert_eq!( - stream2_state.unwrap().message_count, - stream2_message_count, - "Stream 2 should have expected message count independent of stream 1" - ); - - // Verify first stream's message count hasn't changed - let stream1_state_after = map.streams.get(&(peer, stream_1)); - prop_assert_eq!( - stream1_state_after.unwrap().message_count, - stream1_message_count, - "Stream 1 message count should remain unchanged after stream 2 operations" - ); - } - } -} diff --git a/crates/malachite-app/src/utils/coord_upgrade.rs b/crates/malachite-app/src/utils/coord_upgrade.rs deleted file mode 100644 index f53a4dd..0000000 --- a/crates/malachite-app/src/utils/coord_upgrade.rs +++ /dev/null @@ -1,53 +0,0 @@ -// Copyright 2025 Circle Internet Group, Inc. All rights reserved. -// -// SPDX-License-Identifier: Apache-2.0 -// -// Licensed under the Apache License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. -// You may obtain a copy of the License at -// -// http://www.apache.org/licenses/LICENSE-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// See the License for the specific language governing permissions and -// limitations under the License. - -use std::time::Duration; - -use arc_consensus_types::Height; -use tokio::time::sleep; -use tracing::{info, warn}; - -use crate::store::Store; - -#[derive(Debug, thiserror::Error)] -#[error("Halt and wait for external termination signal")] -pub struct HaltAndWait; - -/// Check if the next height matches the configured halt height. -pub async fn check_halt_height( - store: &Store, - next_height: Height, - halt_height: Option, -) -> eyre::Result<()> { - const SLEEP_BEFORE_HALT: Duration = Duration::from_secs(10); - - if let Some(height) = halt_height { - if height == next_height { - warn!("Next height matches configured halt height {height}"); - - // Create a savepoint in the database to ensure that - // no repair of the database is needed on restart. - store.savepoint(); - - info!("Sleeping {SLEEP_BEFORE_HALT:?} before halting..."); - sleep(Duration::from_secs(10)).await; - - return Err(HaltAndWait.into()); - } - } - - Ok(()) -} diff --git a/crates/malachite-app/src/utils/mod.rs b/crates/malachite-app/src/utils/mod.rs deleted file mode 100644 index 5bc7bde..0000000 --- a/crates/malachite-app/src/utils/mod.rs +++ /dev/null @@ -1,22 +0,0 @@ -// Copyright 2026 Circle Internet Group, Inc. All rights reserved. -// -// SPDX-License-Identifier: Apache-2.0 -// -// Licensed under the Apache License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. -// You may obtain a copy of the License at -// -// http://www.apache.org/licenses/LICENSE-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// See the License for the specific language governing permissions and -// limitations under the License. - -pub mod pretty; - -pub mod sync_state; - -mod coord_upgrade; -pub use coord_upgrade::{check_halt_height, HaltAndWait}; diff --git a/crates/malachite-app/src/utils/pretty.rs b/crates/malachite-app/src/utils/pretty.rs deleted file mode 100644 index a7c4815..0000000 --- a/crates/malachite-app/src/utils/pretty.rs +++ /dev/null @@ -1,77 +0,0 @@ -// Copyright 2026 Circle Internet Group, Inc. All rights reserved. -// -// SPDX-License-Identifier: Apache-2.0 -// -// Licensed under the Apache License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. -// You may obtain a copy of the License at -// -// http://www.apache.org/licenses/LICENSE-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// See the License for the specific language governing permissions and -// limitations under the License. - -use std::fmt; -use std::ops::RangeInclusive; - -use alloy_rpc_types_engine::ExecutionPayloadV3; -use arc_signer::PublicKey; - -pub struct Pretty<'a, T>(pub &'a T); - -impl fmt::Display for Pretty<'_, PublicKey> { - fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { - write!(f, "0x{}", hex::encode(self.0.as_bytes())) - } -} - -pub struct PrettyPayload<'a>(pub &'a ExecutionPayloadV3); - -impl fmt::Debug for PrettyPayload<'_> { - fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { - f.debug_struct("ExecutionPayloadV3") - .field( - "block_number", - &self.0.payload_inner.payload_inner.block_number, - ) - .field("block_hash", &self.0.payload_inner.payload_inner.block_hash) - .field( - "parent_hash", - &self.0.payload_inner.payload_inner.parent_hash, - ) - .field("timestamp", &self.0.payload_inner.payload_inner.timestamp) - .field( - "transactions_len", - &self.0.payload_inner.payload_inner.transactions.len(), - ) - .field("state_root", &self.0.payload_inner.payload_inner.state_root) - .field( - "fee_recipient", - &self.0.payload_inner.payload_inner.fee_recipient, - ) - .field( - "receipts_root", - &self.0.payload_inner.payload_inner.receipts_root, - ) - .field("gas_limit", &self.0.payload_inner.payload_inner.gas_limit) - .field("gas_used", &self.0.payload_inner.payload_inner.gas_used) - .field( - "base_fee_per_gas", - &self.0.payload_inner.payload_inner.base_fee_per_gas, - ) - .field("extra_data", &self.0.payload_inner.payload_inner.extra_data) - .finish() - } -} - -impl fmt::Display for Pretty<'_, RangeInclusive> -where - T: fmt::Display, -{ - fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { - write!(f, "{}..={}", self.0.start(), self.0.end()) - } -} diff --git a/crates/malachite-app/src/utils/sync_state.rs b/crates/malachite-app/src/utils/sync_state.rs deleted file mode 100644 index b081305..0000000 --- a/crates/malachite-app/src/utils/sync_state.rs +++ /dev/null @@ -1,80 +0,0 @@ -// Copyright 2026 Circle Internet Group, Inc. All rights reserved. -// -// SPDX-License-Identifier: Apache-2.0 -// -// Licensed under the Apache License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. -// You may obtain a copy of the License at -// -// http://www.apache.org/licenses/LICENSE-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// See the License for the specific language governing permissions and -// limitations under the License. - -use std::time::Duration; - -use arc_consensus_types::BlockTimestamp; - -/// Represents the synchronization state of the node with the network. -#[derive(Debug, Clone, Copy, PartialEq, Eq, serde::Serialize)] -pub enum SyncState { - /// The node is still catching up with the network and has not yet reached the latest blocks. - CatchingUp, - /// The node is in sync with the network and has reached the latest blocks. - InSync, -} - -impl SyncState { - /// Returns `true` if the node fell behind, ie. transitioned from `InSync` to `CatchingUp`. - pub fn fell_behind(previous: SyncState, current: SyncState) -> bool { - previous == SyncState::InSync && current == SyncState::CatchingUp - } -} - -/// Represents the synchronization state of the node with the network. -/// -/// Determined by comparing the timestamp of the latest block with the current time. -/// If the difference is more than or equal to the catch-up threshold, we consider ourselves to be catching up. -pub fn sync_state( - latest_block_timestamp: BlockTimestamp, - catch_up_threshold: Duration, -) -> SyncState { - if is_catching_up(latest_block_timestamp, catch_up_threshold) { - SyncState::CatchingUp - } else { - SyncState::InSync - } -} - -/// Check if we are still catching up with the network -/// by comparing the timestamp of the latest block with the current time. -/// If the difference is more than or equal to CATCH_UP_THRESHOLD, we consider ourselves to be catching up. -fn is_catching_up(latest_block_timestamp: BlockTimestamp, catch_up_threshold: Duration) -> bool { - // Time elapsed since the new latest block's timestamp - let elapsed = timestamp_now().saturating_sub(Duration::from_secs(latest_block_timestamp)); - - // Check if we are still catching up with the network - // by comparing the timestamp of the latest block with the current time. - // If the difference is more than a few seconds, we consider ourselves to be catching up. - let is_catching_up = elapsed >= catch_up_threshold; - - tracing::debug!( - ?elapsed, - is_catching_up, - "Checking if node is catching up with the network" - ); - - is_catching_up -} - -/// Returns the duration since the unix epoch. -fn timestamp_now() -> Duration { - use std::time::{SystemTime, UNIX_EPOCH}; - - SystemTime::now() - .duration_since(UNIX_EPOCH) - .expect("Clock is before UNIX epoch!") -} diff --git a/crates/malachite-app/src/validator_proof.rs b/crates/malachite-app/src/validator_proof.rs deleted file mode 100644 index 6fc755f..0000000 --- a/crates/malachite-app/src/validator_proof.rs +++ /dev/null @@ -1,200 +0,0 @@ -// Copyright 2026 Circle Internet Group, Inc. All rights reserved. -// -// SPDX-License-Identifier: Apache-2.0 -// -// Licensed under the Apache License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. -// You may obtain a copy of the License at -// -// http://www.apache.org/licenses/LICENSE-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// See the License for the specific language governing permissions and -// limitations under the License. - -//! Validator proof creation and verification. -//! -//! A validator proof binds a consensus public key to a libp2p peer ID, -//! proving that the validator controls both keys. This prevents identity -//! spoofing attacks where an attacker could claim to be a validator on -//! the P2P network without possessing their consensus key. - -use arc_consensus_types::codec::{network::NetCodec, Codec}; -use arc_consensus_types::signing::Signer; -use arc_signer::ArcSigningProvider; -use bytes::Bytes; -use eyre::Result; -use tracing::info; - -/// Create a signed validator proof binding the consensus public key to the P2P peer ID. -/// -/// The proof is signed using the consensus signing provider and encoded for network -/// transmission using the network codec. -/// -/// # Arguments -/// * `signing_provider` - The signing provider to sign the proof with -/// * `public_key_bytes` - The consensus public key bytes -/// * `peer_id_bytes` - The libp2p peer ID bytes -/// * `address` - The consensus address (for logging) -/// -/// # Returns -/// The encoded validator proof bytes suitable for network transmission. -pub async fn create_validator_proof( - signing_provider: &ArcSigningProvider, - public_key_bytes: Vec, - peer_id_bytes: Vec, - address: &str, -) -> Result { - let proof = signing_provider - .sign_validator_proof(public_key_bytes, peer_id_bytes) - .await - .map_err(|e| eyre::eyre!("Failed to sign validator proof: {e}"))?; - - let proof_bytes = NetCodec - .encode(&proof) - .map_err(|e| eyre::eyre!("Failed to encode validator proof: {e}"))?; - - info!(address = %address, "Created validator proof for network identity"); - - Ok(proof_bytes) -} - -#[cfg(test)] -mod tests { - use super::*; - use arc_consensus_types::signing::Verifier; - use arc_consensus_types::ArcContext; - use arc_signer::local::{LocalSigningProvider, PrivateKey}; - use malachitebft_app_channel::app::types::Keypair; - use malachitebft_core_types::ValidatorProof; - use rand::rngs::OsRng; - - #[tokio::test] - async fn test_validator_proof_signing_and_encoding() { - let private_key = PrivateKey::generate(OsRng); - let local_signer = LocalSigningProvider::new(private_key.clone()); - let signing_provider = ArcSigningProvider::Local(local_signer); - - let p2p_keypair = Keypair::ed25519_from_bytes(private_key.inner().to_bytes()).unwrap(); - let peer_id = p2p_keypair.public().to_peer_id(); - - let public_key_bytes = private_key.public_key().as_bytes().to_vec(); - let peer_id_bytes = peer_id.to_bytes(); - - let proof: ValidatorProof = signing_provider - .sign_validator_proof(public_key_bytes.clone(), peer_id_bytes.clone()) - .await - .expect("signing validator proof should succeed"); - - assert_eq!( - proof.public_key, public_key_bytes, - "proof public_key should match input" - ); - assert_eq!( - proof.peer_id, peer_id_bytes, - "proof peer_id should match input" - ); - - let encoded = NetCodec - .encode(&proof) - .expect("encoding proof should succeed"); - - let decoded: ValidatorProof = NetCodec - .decode(encoded) - .expect("decoding proof should succeed"); - - assert_eq!( - decoded.public_key, public_key_bytes, - "decoded public_key should match original" - ); - assert_eq!( - decoded.peer_id, peer_id_bytes, - "decoded peer_id should match original" - ); - assert_eq!( - decoded.signature.to_bytes(), - proof.signature.to_bytes(), - "decoded signature should match original" - ); - - let verification_result = signing_provider - .verify_validator_proof(&decoded) - .await - .expect("verification should not error"); - assert!( - verification_result.is_valid(), - "validator proof signature should be valid" - ); - } - - #[tokio::test] - async fn test_validator_proof_tampered_peer_id_fails_verification() { - let private_key = PrivateKey::generate(OsRng); - let local_signer = LocalSigningProvider::new(private_key.clone()); - let signing_provider = ArcSigningProvider::Local(local_signer); - - let p2p_keypair = Keypair::ed25519_from_bytes(private_key.inner().to_bytes()).unwrap(); - let peer_id = p2p_keypair.public().to_peer_id(); - - let public_key_bytes = private_key.public_key().as_bytes().to_vec(); - let peer_id_bytes = peer_id.to_bytes(); - - let proof: ValidatorProof = signing_provider - .sign_validator_proof(public_key_bytes.clone(), peer_id_bytes.clone()) - .await - .expect("signing should succeed"); - - let mut tampered_peer_id = peer_id_bytes.clone(); - tampered_peer_id[0] ^= 0xFF; - - let tampered_proof = - ValidatorProof::::new(proof.public_key, tampered_peer_id, proof.signature); - - let result = signing_provider - .verify_validator_proof(&tampered_proof) - .await - .expect("verification should not error"); - assert!( - result.is_invalid(), - "tampered peer_id should fail verification" - ); - } - - #[tokio::test] - async fn test_validator_proof_tampered_public_key_fails_verification() { - let private_key = PrivateKey::generate(OsRng); - let local_signer = LocalSigningProvider::new(private_key.clone()); - let signing_provider = ArcSigningProvider::Local(local_signer); - - let p2p_keypair = Keypair::ed25519_from_bytes(private_key.inner().to_bytes()).unwrap(); - let peer_id = p2p_keypair.public().to_peer_id(); - - let public_key_bytes = private_key.public_key().as_bytes().to_vec(); - let peer_id_bytes = peer_id.to_bytes(); - - let proof: ValidatorProof = signing_provider - .sign_validator_proof(public_key_bytes.clone(), peer_id_bytes.clone()) - .await - .expect("signing should succeed"); - - let different_private_key = PrivateKey::generate(OsRng); - let different_public_key_bytes = different_private_key.public_key().as_bytes().to_vec(); - - let tampered_proof = ValidatorProof::::new( - different_public_key_bytes, - proof.peer_id, - proof.signature, - ); - - let result = signing_provider - .verify_validator_proof(&tampered_proof) - .await - .expect("verification should not error"); - assert!( - result.is_invalid(), - "proof with different public_key should fail verification" - ); - } -} diff --git a/crates/malachite-app/tests/cli_db_migrate.rs b/crates/malachite-app/tests/cli_db_migrate.rs deleted file mode 100644 index d40df89..0000000 --- a/crates/malachite-app/tests/cli_db_migrate.rs +++ /dev/null @@ -1,220 +0,0 @@ -// Copyright 2025 Circle Internet Group, Inc. All rights reserved. -// -// SPDX-License-Identifier: Apache-2.0 -// -// Licensed under the Apache License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. -// You may obtain a copy of the License at -// -// http://www.apache.org/licenses/LICENSE-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// See the License for the specific language governing permissions and -// limitations under the License. - -//! CLI integration tests for the database migrate command -//! -//! These tests verify the end-to-end behavior of the `arc-node-consensus db migrate` -//! command, including argument parsing, database operations, and output messages. - -use std::fs; -use std::path::PathBuf; -use std::process::Command; - -use assert_cmd::assert::OutputAssertExt; -use predicates::prelude::*; -use tempfile::tempdir; - -use arc_node_consensus::store::migrations::METADATA_TABLE; -use arc_node_consensus::store::versions::SchemaVersion; -use arc_node_consensus::store::{CERTIFICATES_TABLE, DECIDED_BLOCKS_TABLE}; - -/// Helper function to create a v0 test database (without metadata table) -fn create_v0_test_database(path: PathBuf) { - // Create a basic redb database with the old schema (no metadata table) - let db = redb::Database::builder() - .create(&path) - .expect("Failed to create test database"); - - // Create the old tables without metadata table - // Using u64 as key type since Height wraps u64 - let tx = db.begin_write().expect("Failed to begin write"); - { - // Create old schema tables - let _certificates = tx - .open_table(CERTIFICATES_TABLE) - .expect("Failed to create certificates table"); - - let _decided_blocks = tx - .open_table(DECIDED_BLOCKS_TABLE) - .expect("Failed to create decided_blocks table"); - } - tx.commit().expect("Failed to commit transaction"); -} - -/// Helper function to create a current version test database -fn create_current_test_database(path: PathBuf) { - // Create a database with metadata table and current version - // We'll use redb directly to avoid needing private types - let db = redb::Database::builder() - .create(&path) - .expect("Failed to create test database"); - - let tx = db.begin_write().expect("Failed to begin write"); - { - // Create metadata table - let mut metadata = tx - .open_table(METADATA_TABLE) - .expect("Failed to create metadata table"); - - // Set to current schema version (v1) - metadata - .insert("schema_version", SchemaVersion::V1) - .expect("Failed to set schema version"); - - // Create the standard tables - let _ = tx - .open_table(CERTIFICATES_TABLE) - .expect("Failed to create certificates table"); - - let _ = tx - .open_table(DECIDED_BLOCKS_TABLE) - .expect("Failed to create decided_blocks table"); - } - tx.commit().expect("Failed to commit transaction"); -} - -#[test] -fn test_migrate_command_with_nonexistent_database() { - let dir = tempdir().unwrap(); - let home_dir = dir.path(); - - let mut cmd = Command::new(assert_cmd::cargo::cargo_bin!("arc-node-consensus")); - cmd.args(["db", "migrate", "--home", home_dir.to_str().unwrap()]) - .assert() - .failure() - .stderr(predicate::str::contains( - "Database file does not exist at path", - )); -} - -#[test] -fn test_migrate_command_with_empty_database() { - let dir = tempdir().unwrap(); - let home_dir = dir.path(); - fs::create_dir_all(home_dir).unwrap(); - - // Create an empty v0 database (without metadata table, without data) - create_v0_test_database(home_dir.join("store.db")); - - let mut cmd = Command::new(assert_cmd::cargo::cargo_bin!("arc-node-consensus")); - let output = cmd - .args(["db", "migrate", "--home", home_dir.to_str().unwrap()]) - .output() - .expect("Failed to execute command"); - - // The migrate should complete successfully for an empty database - let stdout = String::from_utf8_lossy(&output.stdout); - assert!( - output.status.success() || stdout.contains("Database"), - "Should handle empty database migrate" - ); -} - -#[test] -fn test_migrate_command_with_up_to_date_database() { - let dir = tempdir().unwrap(); - let home_dir = dir.path(); - fs::create_dir_all(home_dir).unwrap(); - - // Create a current version database - create_current_test_database(home_dir.join("store.db")); - - let mut cmd = Command::new(assert_cmd::cargo::cargo_bin!("arc-node-consensus")); - cmd.args(["db", "migrate", "--home", home_dir.to_str().unwrap()]) - .assert() - .success() - .stdout(predicate::str::contains("Database is already up to date")); -} - -#[test] -fn test_migrate_command_dry_run() { - let dir = tempdir().unwrap(); - let home_dir = dir.path(); - fs::create_dir_all(home_dir).unwrap(); - - create_v0_test_database(home_dir.join("store.db")); - - let mut cmd = Command::new(assert_cmd::cargo::cargo_bin!("arc-node-consensus")); - cmd.args([ - "db", - "migrate", - "--home", - home_dir.to_str().unwrap(), - "--dry-run", - ]) - .assert() - .success() - .stdout(predicate::str::contains("Dry-run mode")) - .stdout(predicate::str::contains("migration scan complete")); -} - -#[test] -fn test_migrate_command_shows_log_messages() { - let dir = tempdir().unwrap(); - let home_dir = dir.path(); - fs::create_dir_all(home_dir).unwrap(); - - create_v0_test_database(home_dir.join("store.db")); - - let mut cmd = Command::new(assert_cmd::cargo::cargo_bin!("arc-node-consensus")); - let output = cmd - .args(["db", "migrate", "--home", home_dir.to_str().unwrap()]) - .output() - .expect("Failed to execute command"); - - // The command should complete (success or failure) - // Just verify it shows the expected log messages - let stdout = String::from_utf8_lossy(&output.stdout); - assert!( - stdout.contains("Starting database migration") - || stdout.contains("Opening database") - || stdout.contains("Database"), - "Should show database-related messages" - ); -} - -#[test] -fn test_migrate_command_without_home_flag() { - // Test that command uses default home directory when --home is not provided - // This should fail because the default location likely doesn't have a database - let mut cmd = Command::new(assert_cmd::cargo::cargo_bin!("arc-node-consensus")); - cmd.args(["db", "migrate"]).assert().failure(); -} - -#[test] -fn test_migrate_command_with_invalid_home_path() { - let mut cmd = Command::new(assert_cmd::cargo::cargo_bin!("arc-node-consensus")); - cmd.args(["db", "migrate", "--home", "/nonexistent/path/to/nowhere"]) - .assert() - .failure() - .stderr(predicate::str::contains( - "Database file does not exist at path", - )); -} - -#[test] -fn test_migrate_command_help() { - let mut cmd = Command::new(assert_cmd::cargo::cargo_bin!("arc-node-consensus")); - cmd.args(["db", "migrate", "--help"]) - .assert() - .success() - .stdout(predicate::str::contains("Migrate the database schema")) - .stdout(predicate::str::contains("--dry-run")); -} - -// Note: Data preservation test is skipped because it requires matching the exact -// internal HeightKey type used by the store, which is not publicly exposed. -// The migration logic itself is tested in the unit tests in migrations.rs. diff --git a/crates/malachite-app/tests/cli_db_rollback.rs b/crates/malachite-app/tests/cli_db_rollback.rs deleted file mode 100644 index 3646b70..0000000 --- a/crates/malachite-app/tests/cli_db_rollback.rs +++ /dev/null @@ -1,727 +0,0 @@ -// Copyright 2026 Circle Internet Group, Inc. All rights reserved. -// -// SPDX-License-Identifier: Apache-2.0 - -use std::fs; -use std::path::PathBuf; -use std::process::Command; - -use arc_consensus_types::{Height, B256}; -use arc_node_consensus::store::{ - rollback_to_height, CERTIFICATES_TABLE, DECIDED_BLOCKS_TABLE, INVALID_PAYLOADS_TABLE, - MISBEHAVIOR_EVIDENCE_TABLE, PENDING_PROPOSAL_PARTS_TABLE, PROPOSAL_MONITOR_DATA_TABLE, - UNDECIDED_BLOCKS_TABLE, -}; -use assert_cmd::assert::OutputAssertExt; -use malachitebft_app_channel::app::types::core::Round; -use predicates::prelude::*; -use redb::ReadableTable; -use tempfile::tempdir; - -fn create_test_database(path: PathBuf) { - let db = redb::Database::builder() - .create(&path) - .expect("Failed to create test database"); - - let tx = db.begin_write().expect("Failed to begin write"); - { - let mut certificates = tx - .open_table(CERTIFICATES_TABLE) - .expect("Failed to open certificates table"); - let mut decided = tx - .open_table(DECIDED_BLOCKS_TABLE) - .expect("Failed to open decided blocks table"); - let mut invalid = tx - .open_table(INVALID_PAYLOADS_TABLE) - .expect("Failed to open invalid payloads table"); - let mut misbehavior = tx - .open_table(MISBEHAVIOR_EVIDENCE_TABLE) - .expect("Failed to open misbehavior evidence table"); - let mut proposal_monitor = tx - .open_table(PROPOSAL_MONITOR_DATA_TABLE) - .expect("Failed to open proposal monitor table"); - let mut undecided = tx - .open_table(UNDECIDED_BLOCKS_TABLE) - .expect("Failed to open undecided blocks table"); - let mut pending = tx - .open_table(PENDING_PROPOSAL_PARTS_TABLE) - .expect("Failed to open pending proposal parts table"); - - for h in 1u64..=3 { - let h_u8 = u8::try_from(h).expect("loop index fits in u8"); - let height = Height::new(h); - let payload = vec![h_u8]; - certificates - .insert(height, payload.clone()) - .expect("insert certificate"); - decided - .insert(height, payload.clone()) - .expect("insert decided block"); - invalid - .insert(height, payload.clone()) - .expect("insert invalid payload"); - misbehavior - .insert(height, payload.clone()) - .expect("insert misbehavior evidence"); - proposal_monitor - .insert(height, payload.clone()) - .expect("insert proposal monitor data"); - - let block_hash = B256::repeat_byte(h_u8); - undecided - .insert((height, Round::new(0), block_hash), payload.clone()) - .expect("insert undecided block"); - pending - .insert((height, Round::new(0), block_hash), payload) - .expect("insert pending proposal parts"); - } - } - tx.commit().expect("Failed to commit transaction"); -} - -#[test] -fn test_rollback_command_removes_recent_heights() { - let dir = tempdir().unwrap(); - let home_dir = dir.path(); - fs::create_dir_all(home_dir).unwrap(); - let db_path = home_dir.join("store.db"); - create_test_database(db_path.clone()); - - let mut cmd = Command::new(assert_cmd::cargo::cargo_bin!("arc-node-consensus")); - cmd.args([ - "db", - "rollback", - "--home", - home_dir.to_str().unwrap(), - "--num-heights", - "1", - "--execute", - ]) - .assert() - .success() - .stdout(predicate::str::contains( - "Database rollback completed successfully", - )); - - let db = redb::Database::builder() - .open(&db_path) - .expect("Failed to reopen test database"); - let tx = db.begin_read().expect("Failed to start read transaction"); - - // Height 3 must be gone from all tables - let certificates = tx.open_table(CERTIFICATES_TABLE).unwrap(); - assert_eq!( - certificates.last().unwrap().map(|(k, _)| k.value()), - Some(Height::new(2)) - ); - assert!(certificates.get(Height::new(3)).unwrap().is_none()); - - let decided = tx.open_table(DECIDED_BLOCKS_TABLE).unwrap(); - assert!(decided.get(Height::new(3)).unwrap().is_none()); - - let invalid = tx.open_table(INVALID_PAYLOADS_TABLE).unwrap(); - assert!(invalid.get(Height::new(3)).unwrap().is_none()); - - let misbehavior = tx.open_table(MISBEHAVIOR_EVIDENCE_TABLE).unwrap(); - assert!(misbehavior.get(Height::new(3)).unwrap().is_none()); - - let proposal_monitor = tx.open_table(PROPOSAL_MONITOR_DATA_TABLE).unwrap(); - assert!(proposal_monitor.get(Height::new(3)).unwrap().is_none()); - - let undecided = tx.open_table(UNDECIDED_BLOCKS_TABLE).unwrap(); - assert!(undecided - .get((Height::new(3), Round::new(0), B256::repeat_byte(3))) - .unwrap() - .is_none()); - - let pending = tx.open_table(PENDING_PROPOSAL_PARTS_TABLE).unwrap(); - assert!(pending - .get((Height::new(3), Round::new(0), B256::repeat_byte(3))) - .unwrap() - .is_none()); - - // Heights 1 and 2 must be intact in all tables - for h in [1u64, 2] { - let h_u8 = u8::try_from(h).expect("loop index fits in u8"); - let height = Height::new(h); - assert!( - certificates.get(height).unwrap().is_some(), - "certificates missing height {h}" - ); - assert!( - decided.get(height).unwrap().is_some(), - "decided_blocks missing height {h}" - ); - assert!( - invalid.get(height).unwrap().is_some(), - "invalid_payloads missing height {h}" - ); - assert!( - misbehavior.get(height).unwrap().is_some(), - "misbehavior_evidence missing height {h}" - ); - assert!( - proposal_monitor.get(height).unwrap().is_some(), - "proposal_monitor_data missing height {h}" - ); - assert!( - undecided - .get((height, Round::new(0), B256::repeat_byte(h_u8))) - .unwrap() - .is_some(), - "undecided_blocks missing height {h}" - ); - assert!( - pending - .get((height, Round::new(0), B256::repeat_byte(h_u8))) - .unwrap() - .is_some(), - "pending_proposal_parts missing height {h}" - ); - } -} - -#[test] -fn test_rollback_default_is_dry_run() { - let dir = tempdir().unwrap(); - let home_dir = dir.path(); - fs::create_dir_all(home_dir).unwrap(); - let db_path = home_dir.join("store.db"); - create_test_database(db_path.clone()); - - let mut cmd = Command::new(assert_cmd::cargo::cargo_bin!("arc-node-consensus")); - cmd.args([ - "db", - "rollback", - "--home", - home_dir.to_str().unwrap(), - "--num-heights", - "2", - ]) - .assert() - .success() - .stdout(predicate::str::contains("re-run with --execute")); - - // Database must be untouched - let db = redb::Database::builder() - .open(&db_path) - .expect("Failed to reopen test database"); - let tx = db.begin_read().expect("Failed to start read transaction"); - let certificates = tx.open_table(CERTIFICATES_TABLE).unwrap(); - - assert_eq!( - certificates.last().unwrap().map(|(k, _)| k.value()), - Some(Height::new(3)) - ); - assert!(certificates.get(Height::new(3)).unwrap().is_some()); -} - -#[test] -fn test_rollback_past_genesis_errors() { - let dir = tempdir().unwrap(); - let home_dir = dir.path(); - fs::create_dir_all(home_dir).unwrap(); - let db_path = home_dir.join("store.db"); - create_test_database(db_path.clone()); - - let mut cmd = Command::new(assert_cmd::cargo::cargo_bin!("arc-node-consensus")); - cmd.args([ - "db", - "rollback", - "--home", - home_dir.to_str().unwrap(), - "--num-heights", - "10", - "--execute", - ]) - .assert() - .failure() - .stderr(predicate::str::contains("erase genesis")); - - // Database must be untouched - let db = redb::Database::builder() - .open(&db_path) - .expect("Failed to reopen test database"); - let tx = db.begin_read().expect("Failed to start read transaction"); - let certificates = tx.open_table(CERTIFICATES_TABLE).unwrap(); - assert_eq!( - certificates.last().unwrap().map(|(k, _)| k.value()), - Some(Height::new(3)) - ); -} - -#[test] -fn test_rollback_removes_all_composite_key_variations() { - let dir = tempdir().unwrap(); - let home_dir = dir.path(); - fs::create_dir_all(home_dir).unwrap(); - let db_path = home_dir.join("store.db"); - - let h2 = Height::new(2); - let h3 = Height::new(3); - let hash_a = B256::repeat_byte(0xAA); - let hash_b = B256::repeat_byte(0xBB); - - // Seed: height 2 and 3 each get multiple (round, hash) combinations, - // plus a certificate so the CLI can read current_height. - { - let db = redb::Database::builder() - .create(&db_path) - .expect("create db"); - let tx = db.begin_write().expect("begin write"); - { - let mut certs = tx.open_table(CERTIFICATES_TABLE).expect("open certs"); - certs.insert(h2, vec![2u8]).expect("insert cert h2"); - certs.insert(h3, vec![3u8]).expect("insert cert h3"); - - let mut undecided = tx - .open_table(UNDECIDED_BLOCKS_TABLE) - .expect("open undecided"); - let mut pending = tx - .open_table(PENDING_PROPOSAL_PARTS_TABLE) - .expect("open pending"); - - // Height 2: two rounds, two hashes - for round in [Round::Nil, Round::new(0), Round::new(1)] { - for hash in [hash_a, hash_b] { - undecided - .insert((h2, round, hash), vec![2u8]) - .expect("insert undecided h2"); - pending - .insert((h2, round, hash), vec![2u8]) - .expect("insert pending h2"); - } - } - - // Height 3: same spread - for round in [Round::Nil, Round::new(0), Round::new(1)] { - for hash in [hash_a, hash_b] { - undecided - .insert((h3, round, hash), vec![3u8]) - .expect("insert undecided h3"); - pending - .insert((h3, round, hash), vec![3u8]) - .expect("insert pending h3"); - } - } - } - tx.commit().expect("commit"); - } - - // Roll back 1 height (remove height 3, keep height 2) - let mut cmd = Command::new(assert_cmd::cargo::cargo_bin!("arc-node-consensus")); - cmd.args([ - "db", - "rollback", - "--home", - home_dir.to_str().unwrap(), - "--num-heights", - "1", - "--execute", - ]) - .assert() - .success() - .stdout(predicate::str::contains( - "Database rollback completed successfully", - )); - - let db = redb::Database::builder().open(&db_path).expect("reopen db"); - let tx = db.begin_read().expect("begin read"); - let undecided = tx.open_table(UNDECIDED_BLOCKS_TABLE).unwrap(); - let pending = tx.open_table(PENDING_PROPOSAL_PARTS_TABLE).unwrap(); - - // Every (round, hash) combination at height 3 must be gone - for round in [Round::Nil, Round::new(0), Round::new(1)] { - for hash in [hash_a, hash_b] { - assert!( - undecided.get((h3, round, hash)).unwrap().is_none(), - "undecided entry at h3/round={round:?}/hash={hash} should be deleted" - ); - assert!( - pending.get((h3, round, hash)).unwrap().is_none(), - "pending entry at h3/round={round:?}/hash={hash} should be deleted" - ); - } - } - - // Every (round, hash) combination at height 2 must survive - for round in [Round::Nil, Round::new(0), Round::new(1)] { - for hash in [hash_a, hash_b] { - assert!( - undecided.get((h2, round, hash)).unwrap().is_some(), - "undecided entry at h2/round={round:?}/hash={hash} should be retained" - ); - assert!( - pending.get((h2, round, hash)).unwrap().is_some(), - "pending entry at h2/round={round:?}/hash={hash} should be retained" - ); - } - } -} - -#[test] -fn test_rollback_with_batch_size_one() { - let dir = tempdir().unwrap(); - let db_path = dir.path().join("store.db"); - - let db = redb::Database::builder() - .create(&db_path) - .expect("create db"); - - // Seed heights 1..=5 in all 7 tables - { - let tx = db.begin_write().expect("begin write"); - { - let mut certs = tx.open_table(CERTIFICATES_TABLE).unwrap(); - let mut decided = tx.open_table(DECIDED_BLOCKS_TABLE).unwrap(); - let mut invalid = tx.open_table(INVALID_PAYLOADS_TABLE).unwrap(); - let mut misbehavior = tx.open_table(MISBEHAVIOR_EVIDENCE_TABLE).unwrap(); - let mut proposal_monitor = tx.open_table(PROPOSAL_MONITOR_DATA_TABLE).unwrap(); - let mut undecided = tx.open_table(UNDECIDED_BLOCKS_TABLE).unwrap(); - let mut pending = tx.open_table(PENDING_PROPOSAL_PARTS_TABLE).unwrap(); - - for h in 1u64..=5 { - let h_u8 = u8::try_from(h).expect("loop index fits in u8"); - let height = Height::new(h); - let payload = vec![h_u8]; - certs.insert(height, payload.clone()).unwrap(); - decided.insert(height, payload.clone()).unwrap(); - invalid.insert(height, payload.clone()).unwrap(); - misbehavior.insert(height, payload.clone()).unwrap(); - proposal_monitor.insert(height, payload.clone()).unwrap(); - - let hash = B256::repeat_byte(h_u8); - undecided - .insert((height, Round::new(0), hash), payload.clone()) - .unwrap(); - pending - .insert((height, Round::new(0), hash), payload) - .unwrap(); - } - } - tx.commit().unwrap(); - } - - // Roll back to height 2, batch_size=1 forces 3 separate transactions (heights 3, 4, 5) - let report = - rollback_to_height(&db, Height::new(2), 1, false).expect("rollback should succeed"); - - assert_eq!(report.certificates, 3); - assert_eq!(report.decided_blocks, 3); - assert_eq!(report.invalid_payloads, 3); - assert_eq!(report.misbehavior_evidence, 3); - assert_eq!(report.proposal_monitor_data, 3); - assert_eq!(report.undecided_blocks, 3); - assert_eq!(report.pending_proposal_parts, 3); - - let tx = db.begin_read().unwrap(); - - let certs = tx.open_table(CERTIFICATES_TABLE).unwrap(); - assert_eq!( - certs.last().unwrap().map(|(k, _)| k.value()), - Some(Height::new(2)) - ); - for h in 3u64..=5 { - assert!(certs.get(Height::new(h)).unwrap().is_none()); - } - for h in 1u64..=2 { - assert!(certs.get(Height::new(h)).unwrap().is_some()); - } - - let undecided = tx.open_table(UNDECIDED_BLOCKS_TABLE).unwrap(); - let pending = tx.open_table(PENDING_PROPOSAL_PARTS_TABLE).unwrap(); - for h in 3u64..=5 { - let h_u8 = u8::try_from(h).expect("loop index fits in u8"); - let key = (Height::new(h), Round::new(0), B256::repeat_byte(h_u8)); - assert!(undecided.get(key).unwrap().is_none()); - assert!(pending.get(key).unwrap().is_none()); - } - for h in 1u64..=2 { - let h_u8 = u8::try_from(h).expect("loop index fits in u8"); - let key = (Height::new(h), Round::new(0), B256::repeat_byte(h_u8)); - assert!(undecided.get(key).unwrap().is_some()); - assert!(pending.get(key).unwrap().is_some()); - } -} - -/// Regression test: batch_size that doesn't divide evenly into the deletion range. -/// Heights 1–7, target=2, batch_size=2 → 5 heights to delete (3,4,5,6,7). -/// Batches (high-to-low): [6,7], [4,5], [3]. The last partial batch must clamp -/// at the lower bound and not delete heights 1 or 2. -#[test] -fn test_rollback_uneven_batch_respects_lower_bound() { - let dir = tempdir().unwrap(); - let db_path = dir.path().join("store.db"); - - let db = redb::Database::builder() - .create(&db_path) - .expect("create db"); - - { - let tx = db.begin_write().expect("begin write"); - { - let mut certs = tx.open_table(CERTIFICATES_TABLE).unwrap(); - let mut decided = tx.open_table(DECIDED_BLOCKS_TABLE).unwrap(); - let mut invalid = tx.open_table(INVALID_PAYLOADS_TABLE).unwrap(); - let mut misbehavior = tx.open_table(MISBEHAVIOR_EVIDENCE_TABLE).unwrap(); - let mut proposal_monitor = tx.open_table(PROPOSAL_MONITOR_DATA_TABLE).unwrap(); - let mut undecided = tx.open_table(UNDECIDED_BLOCKS_TABLE).unwrap(); - let mut pending = tx.open_table(PENDING_PROPOSAL_PARTS_TABLE).unwrap(); - - for h in 1u64..=7 { - let h_u8 = u8::try_from(h).expect("loop index fits in u8"); - let height = Height::new(h); - let payload = vec![h_u8]; - certs.insert(height, payload.clone()).unwrap(); - decided.insert(height, payload.clone()).unwrap(); - invalid.insert(height, payload.clone()).unwrap(); - misbehavior.insert(height, payload.clone()).unwrap(); - proposal_monitor.insert(height, payload.clone()).unwrap(); - - let hash = B256::repeat_byte(h_u8); - undecided - .insert((height, Round::new(0), hash), payload.clone()) - .unwrap(); - pending - .insert((height, Round::new(0), hash), payload) - .unwrap(); - } - } - tx.commit().unwrap(); - } - - let report = - rollback_to_height(&db, Height::new(2), 2, false).expect("rollback should succeed"); - - assert_eq!(report.certificates, 5); - assert_eq!(report.decided_blocks, 5); - assert_eq!(report.invalid_payloads, 5); - assert_eq!(report.misbehavior_evidence, 5); - assert_eq!(report.proposal_monitor_data, 5); - assert_eq!(report.undecided_blocks, 5); - assert_eq!(report.pending_proposal_parts, 5); - - let tx = db.begin_read().unwrap(); - - // Heights 3–7 must be deleted - let certs = tx.open_table(CERTIFICATES_TABLE).unwrap(); - for h in 3u64..=7 { - assert!( - certs.get(Height::new(h)).unwrap().is_none(), - "height {h} should be deleted" - ); - } - - // Heights 1–2 must be intact across all tables - let decided = tx.open_table(DECIDED_BLOCKS_TABLE).unwrap(); - let invalid = tx.open_table(INVALID_PAYLOADS_TABLE).unwrap(); - let misbehavior = tx.open_table(MISBEHAVIOR_EVIDENCE_TABLE).unwrap(); - let proposal_monitor = tx.open_table(PROPOSAL_MONITOR_DATA_TABLE).unwrap(); - let undecided = tx.open_table(UNDECIDED_BLOCKS_TABLE).unwrap(); - let pending = tx.open_table(PENDING_PROPOSAL_PARTS_TABLE).unwrap(); - - for h in 1u64..=2 { - let h_u8 = u8::try_from(h).expect("loop index fits in u8"); - let height = Height::new(h); - assert!( - certs.get(height).unwrap().is_some(), - "certs missing height {h}" - ); - assert!( - decided.get(height).unwrap().is_some(), - "decided missing height {h}" - ); - assert!( - invalid.get(height).unwrap().is_some(), - "invalid missing height {h}" - ); - assert!( - misbehavior.get(height).unwrap().is_some(), - "misbehavior missing height {h}" - ); - assert!( - proposal_monitor.get(height).unwrap().is_some(), - "proposal_monitor missing height {h}" - ); - let key = (height, Round::new(0), B256::repeat_byte(h_u8)); - assert!( - undecided.get(key).unwrap().is_some(), - "undecided missing height {h}" - ); - assert!( - pending.get(key).unwrap().is_some(), - "pending missing height {h}" - ); - } - - assert_eq!( - certs.last().unwrap().map(|(k, _)| k.value()), - Some(Height::new(2)) - ); -} - -#[test] -fn test_rollback_to_height_flag() { - let dir = tempdir().unwrap(); - let home_dir = dir.path(); - fs::create_dir_all(home_dir).unwrap(); - let db_path = home_dir.join("store.db"); - create_test_database(db_path.clone()); - - let mut cmd = Command::new(assert_cmd::cargo::cargo_bin!("arc-node-consensus")); - cmd.args([ - "db", - "rollback", - "--home", - home_dir.to_str().unwrap(), - "--to-height", - "1", - "--execute", - ]) - .assert() - .success() - .stdout(predicate::str::contains( - "Database rollback completed successfully", - )); - - let db = redb::Database::builder() - .open(&db_path) - .expect("Failed to reopen test database"); - let tx = db.begin_read().expect("Failed to start read transaction"); - let certificates = tx.open_table(CERTIFICATES_TABLE).unwrap(); - - // Only height 1 should remain - assert_eq!( - certificates.last().unwrap().map(|(k, _)| k.value()), - Some(Height::new(1)) - ); - assert!(certificates.get(Height::new(2)).unwrap().is_none()); - assert!(certificates.get(Height::new(3)).unwrap().is_none()); -} - -#[test] -fn test_rollback_to_height_dry_run() { - let dir = tempdir().unwrap(); - let home_dir = dir.path(); - fs::create_dir_all(home_dir).unwrap(); - let db_path = home_dir.join("store.db"); - create_test_database(db_path.clone()); - - let mut cmd = Command::new(assert_cmd::cargo::cargo_bin!("arc-node-consensus")); - cmd.args([ - "db", - "rollback", - "--home", - home_dir.to_str().unwrap(), - "--to-height", - "1", - ]) - .assert() - .success() - .stdout(predicate::str::contains("re-run with --execute")); - - // Database must be untouched - let db = redb::Database::builder() - .open(&db_path) - .expect("Failed to reopen test database"); - let tx = db.begin_read().expect("Failed to start read transaction"); - let certificates = tx.open_table(CERTIFICATES_TABLE).unwrap(); - assert_eq!( - certificates.last().unwrap().map(|(k, _)| k.value()), - Some(Height::new(3)) - ); -} - -#[test] -fn test_rollback_conflicting_flags_errors() { - let dir = tempdir().unwrap(); - let home_dir = dir.path(); - fs::create_dir_all(home_dir).unwrap(); - let db_path = home_dir.join("store.db"); - create_test_database(db_path); - - let mut cmd = Command::new(assert_cmd::cargo::cargo_bin!("arc-node-consensus")); - cmd.args([ - "db", - "rollback", - "--home", - home_dir.to_str().unwrap(), - "--num-heights", - "1", - "--to-height", - "2", - "--execute", - ]) - .assert() - .failure() - .stderr(predicate::str::contains("cannot be used with")); -} - -#[test] -fn test_rollback_neither_flag_errors() { - let dir = tempdir().unwrap(); - let home_dir = dir.path(); - fs::create_dir_all(home_dir).unwrap(); - let db_path = home_dir.join("store.db"); - create_test_database(db_path); - - let mut cmd = Command::new(assert_cmd::cargo::cargo_bin!("arc-node-consensus")); - cmd.args([ - "db", - "rollback", - "--home", - home_dir.to_str().unwrap(), - "--execute", - ]) - .assert() - .failure() - .stderr(predicate::str::contains( - "Specify exactly one of --num-heights", - )); -} - -#[test] -fn test_rollback_to_height_above_current_errors() { - let dir = tempdir().unwrap(); - let home_dir = dir.path(); - fs::create_dir_all(home_dir).unwrap(); - let db_path = home_dir.join("store.db"); - create_test_database(db_path); - - let mut cmd = Command::new(assert_cmd::cargo::cargo_bin!("arc-node-consensus")); - cmd.args([ - "db", - "rollback", - "--home", - home_dir.to_str().unwrap(), - "--to-height", - "100", - "--execute", - ]) - .assert() - .failure() - .stderr(predicate::str::contains("nothing to roll back")); -} - -#[test] -fn test_rollback_to_height_zero_errors() { - let dir = tempdir().unwrap(); - let home_dir = dir.path(); - fs::create_dir_all(home_dir).unwrap(); - let db_path = home_dir.join("store.db"); - create_test_database(db_path); - - let mut cmd = Command::new(assert_cmd::cargo::cargo_bin!("arc-node-consensus")); - cmd.args([ - "db", - "rollback", - "--home", - home_dir.to_str().unwrap(), - "--to-height", - "0", - "--execute", - ]) - .assert() - .failure() - .stderr(predicate::str::contains("erase genesis")); -} diff --git a/crates/malachite-app/tests/common/mod.rs b/crates/malachite-app/tests/common/mod.rs deleted file mode 100644 index 29c500e..0000000 --- a/crates/malachite-app/tests/common/mod.rs +++ /dev/null @@ -1,122 +0,0 @@ -// Copyright 2025 Circle Internet Group, Inc. All rights reserved. -// -// SPDX-License-Identifier: Apache-2.0 -// -// Licensed under the Apache License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. -// You may obtain a copy of the License at -// -// http://www.apache.org/licenses/LICENSE-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// See the License for the specific language governing permissions and -// limitations under the License. - -//! Common test utilities for RPC integration tests - -use std::net::SocketAddr; -use std::sync::Arc; - -use arc_consensus_types::ArcContext; -use arc_node_consensus::request::AppRequest; -use malachitebft_app_channel::{ConsensusRequest, NetworkRequest}; -use tokio::net::TcpListener; -use tokio::sync::{mpsc, Mutex}; -use tokio::task::JoinHandle; - -/// Test server wrapper that manages the RPC server lifecycle -pub struct TestServer { - addr: SocketAddr, - _server_handle: JoinHandle<()>, - app_rx: Arc>>, - consensus_rx: Arc>>>, - network_rx: Arc>>, -} - -impl TestServer { - /// Start a new test server on a random available port - pub async fn start() -> Self { - Self::start_with_capacity(100).await - } - - /// Start a new test server with specified channel capacity - pub async fn start_with_capacity(capacity: usize) -> Self { - // Create channels for communication - let (app_tx, app_rx) = mpsc::channel(capacity); - let (consensus_tx, consensus_rx) = mpsc::channel(capacity); - let (network_tx, network_rx) = mpsc::channel(capacity); - - // Bind to a random available port - let listener = TcpListener::bind("127.0.0.1:0") - .await - .expect("Failed to bind to random port"); - let addr = listener.local_addr().expect("Failed to get local address"); - - // Build the actual production router - let router = arc_node_consensus::rpc::build_router(consensus_tx, app_tx, network_tx); - - // Spawn the server - let server_handle = tokio::spawn(async move { - axum::serve(listener, router) - .await - .expect("Server failed to start"); - }); - - Self { - addr, - _server_handle: server_handle, - app_rx: Arc::new(Mutex::new(app_rx)), - consensus_rx: Arc::new(Mutex::new(consensus_rx)), - network_rx: Arc::new(Mutex::new(network_rx)), - } - } - - /// Get the base URL for the test server - pub fn url(&self) -> String { - format!("http://{}", self.addr) - } - - /// Expect and handle an app request - pub fn expect_app_request(&self, handler: F) - where - F: FnOnce(AppRequest) + Send + 'static, - { - let rx = Arc::clone(&self.app_rx); - tokio::spawn(async move { - let mut rx = rx.lock().await; - if let Some(req) = rx.recv().await { - handler(req); - } - }); - } - - /// Expect and handle a consensus request - pub fn expect_consensus_request(&self, handler: F) - where - F: FnOnce(ConsensusRequest) + Send + 'static, - { - let rx = Arc::clone(&self.consensus_rx); - tokio::spawn(async move { - let mut rx = rx.lock().await; - if let Some(req) = rx.recv().await { - handler(req); - } - }); - } - - /// Expect and handle a network request - pub fn expect_network_request(&self, handler: F) - where - F: FnOnce(NetworkRequest) + Send + 'static, - { - let rx = Arc::clone(&self.network_rx); - tokio::spawn(async move { - let mut rx = rx.lock().await; - if let Some(req) = rx.recv().await { - handler(req); - } - }); - } -} diff --git a/crates/malachite-app/tests/rpc_integration.rs b/crates/malachite-app/tests/rpc_integration.rs deleted file mode 100644 index 498ad5e..0000000 --- a/crates/malachite-app/tests/rpc_integration.rs +++ /dev/null @@ -1,502 +0,0 @@ -// Copyright 2025 Circle Internet Group, Inc. All rights reserved. -// -// SPDX-License-Identifier: Apache-2.0 -// -// Licensed under the Apache License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. -// You may obtain a copy of the License at -// -// http://www.apache.org/licenses/LICENSE-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// See the License for the specific language governing permissions and -// limitations under the License. - -//! End-to-end integration tests for the RPC server -//! -//! These tests start a real HTTP server and make actual HTTP requests to it, -//! verifying the complete request/response cycle including middleware, routing, -//! serialization, and API versioning. - -use std::time::Duration; - -use arc_consensus_types::{signing::PrivateKey, Address, Height, Round, ValidatorSet}; -use arc_node_consensus::request::{AppRequest, Status}; -use arc_node_consensus::utils::sync_state::SyncState; -use malachitebft_app_channel::{ConsensusRequest, NetworkRequest}; - -mod common; -use common::TestServer; - -/// Test that the root endpoint returns API documentation -#[tokio::test] -async fn test_root_endpoint_returns_docs() { - let server = TestServer::start().await; - let client = reqwest::Client::new(); - - let response = client - .get(format!("{}/", server.url())) - .send() - .await - .expect("Failed to send request"); - - assert_eq!(response.status(), 200); - - let body: serde_json::Value = response.json().await.expect("Failed to parse JSON"); - - // Verify endpoints documentation is present - assert!(body.get("endpoints").is_some()); - assert!(body.get("rpc_versioning").is_some()); - - // Verify versioning info - let versioning = body.get("rpc_versioning").unwrap(); - assert_eq!(versioning.get("method").unwrap(), "header-based"); - assert_eq!(versioning.get("header").unwrap(), "Accept"); -} - -/// Test the /health endpoint with default API version -#[tokio::test] -async fn test_health_endpoint_ok() { - let server = TestServer::start().await; - - // Respond to health check request - server.expect_app_request(|req| match req { - AppRequest::GetHealth(reply) => { - reply.send(()).ok(); - } - _ => panic!("Unexpected request type"), - }); - - let client = reqwest::Client::new(); - let response = client - .get(format!("{}/health", server.url())) - .send() - .await - .expect("Failed to send request"); - - assert_eq!(response.status(), 200); - - let body: serde_json::Value = response.json().await.expect("Failed to parse JSON"); - assert_eq!(body.get("status").unwrap(), "ok"); -} - -/// Test the /version endpoint returns version information -#[tokio::test] -async fn test_version_endpoint() { - let server = TestServer::start().await; - let client = reqwest::Client::new(); - - let response = client - .get(format!("{}/version", server.url())) - .send() - .await - .expect("Failed to send request"); - - assert_eq!(response.status(), 200); - - let body: serde_json::Value = response.json().await.expect("Failed to parse JSON"); - - // Verify version fields are present - assert!(body.get("git_version").is_some()); - assert!(body.get("git_commit").is_some()); - assert!(body.get("git_short_hash").is_some()); - assert!(body.get("cargo_version").is_some()); -} - -/// Test the /status endpoint returns application status -#[tokio::test] -async fn test_status_endpoint() { - let server = TestServer::start().await; - - // Respond to status request - server.expect_app_request(|req| match req { - AppRequest::GetStatus(reply) => { - let public_key = PrivateKey::from([0x11; 32]).public_key(); - let status = Status { - height: Height::new(100), - round: Round::new(1), - address: Address::repeat_byte(1), - public_key, - proposer: Some(Address::repeat_byte(2)), - height_start_time: std::time::SystemTime::now(), - prev_payload_hash: None, - db_latest_height: Height::new(99), - db_earliest_height: Height::new(1), - validator_set: ValidatorSet::default(), - undecided_blocks_count: 0, - pending_proposal_parts: vec![], - sync_state: SyncState::InSync, - }; - reply.send(status).ok(); - } - _ => panic!("Unexpected request type"), - }); - - let client = reqwest::Client::new(); - let response = client - .get(format!("{}/status", server.url())) - .send() - .await - .expect("Failed to send request"); - - assert_eq!(response.status(), 200); - - let body: serde_json::Value = response.json().await.expect("Failed to parse JSON"); - - // Verify status fields - assert_eq!(body.get("height").unwrap(), 100); - assert_eq!(body.get("round").unwrap(), 1); - assert!(body.get("address").is_some()); - assert!(body.get("public_key").is_some()); - assert!(body.get("validator_set").is_some()); -} - -/// Test the /commit endpoint with a valid height parameter -#[tokio::test] -async fn test_commit_endpoint_with_height() { - let server = TestServer::start().await; - - // Respond to certificate request - server.expect_app_request(|req| match req { - AppRequest::GetCertificate(height, reply) => { - assert_eq!(height, Some(Height::new(42))); - reply.send(None).ok(); - } - _ => panic!("Unexpected request type"), - }); - - let client = reqwest::Client::new(); - let response = client - .get(format!("{}/commit?height=42", server.url())) - .send() - .await - .expect("Failed to send request"); - - assert_eq!(response.status(), 404); // Not found since we returned None -} - -/// Test the /commit endpoint without height parameter -#[tokio::test] -async fn test_commit_endpoint_without_height() { - let server = TestServer::start().await; - - // Respond to certificate request - server.expect_app_request(|req| match req { - AppRequest::GetCertificate(height, reply) => { - assert_eq!(height, None); - reply.send(None).ok(); - } - _ => panic!("Unexpected request type"), - }); - - let client = reqwest::Client::new(); - let response = client - .get(format!("{}/commit", server.url())) - .send() - .await - .expect("Failed to send request"); - - assert_eq!(response.status(), 404); // Not found since we returned None -} - -/// Test API versioning with explicit v1 Accept header -#[tokio::test] -async fn test_api_versioning_explicit_v1() { - let server = TestServer::start().await; - let client = reqwest::Client::new(); - - let response = client - .get(format!("{}/version", server.url())) - .header("Accept", "application/vnd.arc.v1+json") - .send() - .await - .expect("Failed to send request"); - - assert_eq!(response.status(), 200); - assert_eq!( - response.headers().get("content-type").unwrap(), - "application/vnd.arc.v1+json" - ); -} - -/// Test API versioning with generic JSON Accept header (defaults to v1) -#[tokio::test] -async fn test_api_versioning_generic_json() { - let server = TestServer::start().await; - let client = reqwest::Client::new(); - - let response = client - .get(format!("{}/version", server.url())) - .header("Accept", "application/json") - .send() - .await - .expect("Failed to send request"); - - assert_eq!(response.status(), 200); - assert_eq!( - response.headers().get("content-type").unwrap(), - "application/vnd.arc.v1+json" - ); -} - -/// Test API versioning with unsupported version returns 406 Not Acceptable -#[tokio::test] -async fn test_api_versioning_unsupported_version() { - let server = TestServer::start().await; - let client = reqwest::Client::new(); - - let response = client - .get(format!("{}/version", server.url())) - .header("Accept", "application/vnd.arc.v99+json") - .send() - .await - .expect("Failed to send request"); - - assert_eq!(response.status(), 406); // Not Acceptable -} - -/// Test the /consensus-state endpoint when state is available -#[tokio::test] -async fn test_consensus_state_available() { - let server = TestServer::start().await; - - // Respond to state dump request - server.expect_consensus_request(|req| { - let ConsensusRequest::DumpState(reply) = req; - reply.send(None).ok(); // Simulate state not available yet - }); - - let client = reqwest::Client::new(); - let response = client - .get(format!("{}/consensus-state", server.url())) - .send() - .await - .expect("Failed to send request"); - - assert_eq!(response.status(), 503); // Service unavailable -} - -/// Test the /network-state endpoint when state is available -#[tokio::test] -async fn test_network_state_available() { - let server = TestServer::start().await; - - // Respond to network state dump request - server.expect_network_request(|req| { - let NetworkRequest::DumpState(reply) = req else { - panic!("Unexpected request type"); - }; - reply.send(None).ok(); // Simulate state not available yet - }); - - let client = reqwest::Client::new(); - let response = client - .get(format!("{}/network-state", server.url())) - .send() - .await - .expect("Failed to send request"); - - assert_eq!(response.status(), 503); // Service unavailable -} - -/// Test that multiple concurrent requests are handled correctly -#[tokio::test] -async fn test_concurrent_requests() { - let server = TestServer::start().await; - let client = reqwest::Client::new(); - - // Spawn multiple concurrent requests - let mut handles = vec![]; - for _ in 0..10 { - let client = client.clone(); - let url = server.url().to_string(); - let handle = tokio::spawn(async move { - client - .get(format!("{}/version", url)) - .send() - .await - .expect("Failed to send request") - }); - handles.push(handle); - } - - // Wait for all requests to complete - for handle in handles { - let response = handle.await.expect("Task panicked"); - assert_eq!(response.status(), 200); - } -} - -/// Test that server handles graceful shutdown -#[tokio::test] -async fn test_server_shutdown() { - let server = TestServer::start().await; - let url = server.url().to_string(); - - // Make a successful request - let client = reqwest::Client::new(); - let response = client - .get(format!("{}/version", url)) - .send() - .await - .expect("Failed to send request"); - assert_eq!(response.status(), 200); - - // Drop the server to trigger shutdown - drop(server); - - // Give it a moment to shut down - tokio::time::sleep(Duration::from_millis(200)).await; - - // Subsequent requests should fail (connection refused or similar) - // Note: In a real scenario, the server would shut down properly - // For this test, we're just verifying the server was working before drop -} - -/// Test error handling when app request channel is full -#[tokio::test] -async fn test_app_request_channel_full() { - let server = TestServer::start().await; - - // Don't respond to requests - let them pile up - // The channel has limited capacity, so eventually we should get a 429 - - let client = reqwest::Client::new(); - - // Make many requests without processing them - for _ in 0..100 { - let _response = client - .get(format!("{}/health", server.url())) - .timeout(Duration::from_millis(100)) - .send() - .await; - } - - // At least some requests should have succeeded or timed out - // This test mainly verifies the server doesn't crash under load -} - -/// Test that the actual production status response format is correct -#[tokio::test] -async fn test_status_response_structure() { - let server = TestServer::start().await; - - // Respond to status request with real data - server.expect_app_request(|req| match req { - AppRequest::GetStatus(reply) => { - let public_key = PrivateKey::from([0xAB; 32]).public_key(); - let status = Status { - height: Height::new(12345), - round: Round::new(7), - address: Address::repeat_byte(0xAB), - public_key, - proposer: Some(Address::repeat_byte(0xCD)), - height_start_time: std::time::SystemTime::now(), - prev_payload_hash: None, - db_latest_height: Height::new(12344), - db_earliest_height: Height::new(1), - validator_set: ValidatorSet::default(), - undecided_blocks_count: 5, - pending_proposal_parts: vec![(Height::new(100), 3)], - sync_state: SyncState::InSync, - }; - reply.send(status).ok(); - } - _ => panic!("Unexpected request type"), - }); - - let client = reqwest::Client::new(); - let response = client - .get(format!("{}/status", server.url())) - .send() - .await - .expect("Failed to send request"); - - assert_eq!(response.status(), 200); - - let body: serde_json::Value = response.json().await.expect("Failed to parse JSON"); - - // Verify the actual production response structure - assert_eq!(body.get("height").unwrap(), 12345); - assert_eq!(body.get("round").unwrap(), 7); - assert!(body.get("address").is_some()); - assert!(body.get("public_key").is_some()); - assert!(body.get("proposer").is_some()); - assert!(body.get("height_start_time").is_some()); - assert!(body.get("db_latest_height").is_some()); - assert!(body.get("db_earliest_height").is_some()); - assert!(body.get("validator_set").is_some()); - assert!(body.get("undecided_blocks_count").is_some()); - assert!(body.get("pending_proposal_parts").is_some()); - - // Verify validator_set structure - let validator_set = body.get("validator_set").unwrap(); - assert!(validator_set.get("total_voting_power").is_some()); - assert!(validator_set.get("count").is_some()); - assert!(validator_set.get("validators").is_some()); -} - -/// Test that invalid query parameters are handled gracefully -#[tokio::test] -async fn test_invalid_query_parameters() { - let server = TestServer::start().await; - let client = reqwest::Client::new(); - - // Invalid height parameter - let response = client - .get(format!("{}/commit?height=not_a_number", server.url())) - .send() - .await - .expect("Failed to send request"); - - // Should return 400 Bad Request - assert_eq!(response.status(), 400); -} - -/// Test the /misbehavior-evidence endpoint with a valid height parameter -#[tokio::test] -async fn test_no_misbehavior_evidence_endpoint_with_height() { - let server = TestServer::start().await; - let client = reqwest::Client::new(); - - server.expect_app_request(|req| match req { - AppRequest::GetMisbehaviorEvidence(height, reply) => { - assert_eq!(height, Some(Height::new(100))); - reply.send(None).ok(); - } - _ => panic!("Unexpected request type"), - }); - - let response = client - .get(format!("{}/misbehavior-evidence?height=100", server.url())) - .send() - .await - .expect("Failed to send request"); - - assert_eq!(response.status(), 404); // Not found since we returned None -} - -/// Test the /misbehavior-evidence endpoint without height parameter -#[tokio::test] -async fn test_no_misbehavior_evidence_endpoint_without_height() { - let server = TestServer::start().await; - let client = reqwest::Client::new(); - - server.expect_app_request(|req| match req { - AppRequest::GetMisbehaviorEvidence(height, reply) => { - assert_eq!(height, None); - reply.send(None).ok(); - } - _ => panic!("Unexpected request type"), - }); - - let response = client - .get(format!("{}/misbehavior-evidence", server.url())) - .send() - .await - .expect("Failed to send request"); - - assert_eq!(response.status(), 404); // Not found since we returned None -} diff --git a/crates/malachite-cli/Cargo.toml b/crates/malachite-cli/Cargo.toml deleted file mode 100644 index aeb5f27..0000000 --- a/crates/malachite-cli/Cargo.toml +++ /dev/null @@ -1,47 +0,0 @@ -[package] -name = "arc-node-consensus-cli" -version.workspace = true -edition.workspace = true -repository.workspace = true -license.workspace = true -publish.workspace = true - -[dependencies] -arc-consensus-types.workspace = true - -# arc -arc-snapshots.workspace = true -arc-version.workspace = true - -axum = { workspace = true } -bip32 = { workspace = true } -bip39 = { workspace = true } -clap = { workspace = true, features = ["derive", "env"] } -color-eyre = { workspace = true } -directories = { workspace = true } -eyre = { workspace = true } -hex = { workspace = true } -itertools = { workspace = true } - -# malachite -malachitebft-app.workspace = true -malachitebft-config.workspace = true -rand = { workspace = true } -reqwest = { workspace = true, features = ["json"] } -serde = { workspace = true } -serde_json = { workspace = true } -thiserror = { workspace = true } -tokio = { workspace = true, features = ["full"] } -tracing = { workspace = true } -tracing-appender = { workspace = true } -tracing-subscriber = { workspace = true, features = ["env-filter", "fmt", "json"] } -url = { workspace = true } - -[dev-dependencies] -lz4.workspace = true -tar.workspace = true -tempfile = { workspace = true } -wiremock.workspace = true - -[lints] -workspace = true diff --git a/crates/malachite-cli/src/args.rs b/crates/malachite-cli/src/args.rs deleted file mode 100644 index 09eaa5f..0000000 --- a/crates/malachite-cli/src/args.rs +++ /dev/null @@ -1,144 +0,0 @@ -// Copyright 2025 Circle Internet Group, Inc. All rights reserved. -// -// SPDX-License-Identifier: Apache-2.0 -// -// Licensed under the Apache License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. -// You may obtain a copy of the License at -// -// http://www.apache.org/licenses/LICENSE-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// See the License for the specific language governing permissions and -// limitations under the License. - -//! Command-line interface arguments for a basic implementation. -//! -//! Read configuration from the configuration files found in the directory -//! provided with the `--home` global parameter. -//! -//! The command-line parameters are stored in the `Args` structure. -//! `clap` parses the command-line parameters into this structure. - -use std::path::PathBuf; - -use clap::{Parser, Subcommand}; -use directories::BaseDirs; - -use malachitebft_config::{LogFormat, LogLevel}; - -use crate::cmd::db::DbCommands; -use crate::cmd::download::DownloadCmd; -use crate::cmd::init::InitCmd; -use crate::cmd::key::KeyCmd; -use crate::cmd::start::StartCmd; -use crate::error::Error; - -const APP_FOLDER: &str = ".arc/consensus"; -const CONFIG_FILE: &str = "config.toml"; -const GENESIS_FILE: &str = "genesis.json"; -const PRIV_VALIDATOR_KEY_FILE: &str = "priv_validator_key.json"; - -#[derive(Parser, Clone, Debug, Default)] -#[command( - name = "arc-node-consensus", - version = arc_version::SHORT_VERSION, - long_version = arc_version::LONG_VERSION, - about = "Arc consensus layer" -)] -pub struct Args { - /// Home directory for the consensus layer (default: `~/.arc/consensus`) - #[arg(long, global = true, value_name = "HOME_DIR")] - pub home: Option, - - /// Log level - #[arg(long, global = true, value_name = "LOG_LEVEL", default_value = "info")] - pub log_level: LogLevel, - - /// Log format - #[arg( - long, - global = true, - value_name = "LOG_FORMAT", - default_value = "plaintext" - )] - pub log_format: LogFormat, - - #[command(subcommand)] - pub command: Commands, -} - -#[allow(clippy::large_enum_variant)] -#[derive(Subcommand, Clone, Debug)] -pub enum Commands { - /// Start node - Start(StartCmd), - - /// Initialize configuration - Init(InitCmd), - - /// Display public key and address - Key(KeyCmd), - - /// Database management commands - #[command(subcommand)] - Db(DbCommands), - - /// Download a consensus layer snapshot - Download(DownloadCmd), -} - -impl Default for Commands { - fn default() -> Self { - Commands::Start(StartCmd::default()) - } -} - -impl Args { - /// new returns a new instance of the arguments. - pub fn new() -> Args { - Args::parse() - } - - /// get_home_dir returns the application home folder. - /// Typically, `$HOME/.arc/consensus`, dependent on the operating system. - pub fn get_home_dir(&self) -> Result { - match self.home { - Some(ref path) => Ok(path.clone()), - None => Ok(BaseDirs::new() - .ok_or(Error::DirPath)? - .home_dir() - .join(APP_FOLDER)), - } - } - - /// get_config_dir returns the configuration folder based on the home folder. - pub fn get_config_dir(&self) -> Result { - Ok(self.get_home_dir()?.join("config")) - } - - /// get_config_file_path returns the configuration file path based on the command-line arguments - /// and the configuration folder. - pub fn get_config_file_path(&self) -> Result { - Ok(self.get_config_dir()?.join(CONFIG_FILE)) - } - - /// get_genesis_file_path returns the genesis file path based on the command-line arguments and - /// the configuration folder. - pub fn get_genesis_file_path(&self) -> Result { - Ok(self.get_config_dir()?.join(GENESIS_FILE)) - } - - /// get_db_path returns the database file path based on the home folder. - pub fn get_db_path(&self) -> Result { - Ok(self.get_home_dir()?.join("store.db")) - } - - /// get_priv_validator_key_file_path returns the private validator key file path based on the - /// configuration folder. - pub fn get_default_priv_validator_key_file_path(&self) -> Result { - Ok(self.get_config_dir()?.join(PRIV_VALIDATOR_KEY_FILE)) - } -} diff --git a/crates/malachite-cli/src/cmd/db.rs b/crates/malachite-cli/src/cmd/db.rs deleted file mode 100644 index 77795c0..0000000 --- a/crates/malachite-cli/src/cmd/db.rs +++ /dev/null @@ -1,57 +0,0 @@ -// Copyright 2025 Circle Internet Group, Inc. All rights reserved. -// -// SPDX-License-Identifier: Apache-2.0 -// -// Licensed under the Apache License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. -// You may obtain a copy of the License at -// -// http://www.apache.org/licenses/LICENSE-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// See the License for the specific language governing permissions and -// limitations under the License. - -//! Database management commands - -use clap::{Args, Subcommand}; - -#[derive(Subcommand, Clone, Debug)] -pub enum DbCommands { - /// Migrate the database schema to latest version - #[clap(alias = "upgrade")] - Migrate(MigrateCmd), - - /// Compact the database to reclaim space. The node must be stopped before running this command. - Compact, - - /// Roll back the database by removing heights. Dry-run by default; pass --execute to commit. - #[clap(alias = "unwind")] - Rollback(RollbackCmd), -} - -#[derive(Args, Clone, Debug, Default)] -pub struct MigrateCmd { - /// Perform a dry-run without actually upgrading - #[arg(long)] - pub dry_run: bool, -} - -#[derive(Args, Clone, Debug, Default)] -pub struct RollbackCmd { - /// Number of heights to remove from the tip of the consensus DB. - /// Mutually exclusive with --to-height. - #[arg(long, value_name = "COUNT", conflicts_with = "to_height")] - pub num_heights: Option, - - /// Absolute height to roll back to. All data above this height is removed. - /// Mutually exclusive with --num-heights. - #[arg(long, value_name = "HEIGHT", conflicts_with = "num_heights")] - pub to_height: Option, - - /// Actually execute the rollback. Without this flag the command is a dry run. - #[arg(long)] - pub execute: bool, -} diff --git a/crates/malachite-cli/src/cmd/download.rs b/crates/malachite-cli/src/cmd/download.rs deleted file mode 100644 index 28385f6..0000000 --- a/crates/malachite-cli/src/cmd/download.rs +++ /dev/null @@ -1,227 +0,0 @@ -// Copyright 2026 Circle Internet Group, Inc. All rights reserved. -// -// SPDX-License-Identifier: Apache-2.0 -// -// Licensed under the Apache License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. -// You may obtain a copy of the License at -// -// http://www.apache.org/licenses/LICENSE-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// See the License for the specific language governing permissions and -// limitations under the License. - -//! Download command for the consensus layer. -//! -//! Downloads a CL snapshot archive and extracts bare paths (e.g. `store.db`) directly -//! into the home directory. - -use std::path::Path; - -use arc_snapshots::download::{ - consensus_snapshot_exists, fetch_latest_snapshot_urls, should_download, stream_and_extract, - write_snapshot_version, Chain, -}; -use clap::Args; -use eyre::Result; -use tracing::info; - -#[derive(Args, Clone, Debug, Default)] -pub struct DownloadCmd { - /// URL of the CL snapshot to download. - /// - /// If omitted, the latest snapshot for --chain is fetched automatically. - #[arg(long, short)] - pub url: Option, - - /// Network to download a snapshot for. - /// - /// [possible values: arc-testnet, arc-devnet] - #[arg(long, default_value = "arc-testnet")] - pub chain: String, - - /// Force re-download even if snapshot data already exists. - #[arg(long = "force")] - pub force_redownload: bool, -} - -impl DownloadCmd { - pub async fn run(&self, home_dir: &Path) -> Result<()> { - let chain = parse_chain(&self.chain)?; - - let url = match &self.url { - Some(u) => u.clone(), - None => { - info!(chain = %self.chain, "Fetching latest CL snapshot URL"); - let (_el_url, cl_url) = fetch_latest_snapshot_urls(chain).await?; - cl_url - } - }; - - if !should_download( - "Consensus layer", - home_dir, - &url, - consensus_snapshot_exists(home_dir), - self.force_redownload, - ) { - return Ok(()); - } - - let tmp_dir = home_dir.join(".snapshot-tmp"); - - info!( - url = %url, - home_dir = %home_dir.display(), - "Starting CL snapshot download" - ); - - stream_and_extract(url.clone(), home_dir.to_path_buf(), tmp_dir).await?; - write_snapshot_version(home_dir, &url)?; - - info!("CL snapshot downloaded and extracted successfully"); - Ok(()) - } -} - -fn parse_chain(name: &str) -> Result { - match name { - "arc-testnet" => Ok(Chain::Testnet), - "arc-devnet" => Ok(Chain::Devnet), - other => Err(eyre::eyre!( - "Unknown chain '{}'. Valid values: arc-testnet, arc-devnet", - other - )), - } -} - -#[cfg(test)] -mod tests { - use super::*; - - #[test] - fn parse_chain_known_values() { - assert!(matches!( - parse_chain("arc-testnet").unwrap(), - Chain::Testnet - )); - assert!(matches!(parse_chain("arc-devnet").unwrap(), Chain::Devnet)); - } - - #[test] - fn parse_chain_unknown_is_error() { - assert!(parse_chain("unknown").is_err()); - } - - #[tokio::test] - async fn run_extracts_cl_snapshot_into_home_dir() -> eyre::Result<()> { - use wiremock::matchers::{method, path}; - use wiremock::{Mock, MockServer, ResponseTemplate}; - - // Build a minimal CL archive with bare paths - let buf = Vec::new(); - let encoder = lz4::EncoderBuilder::new().build(buf)?; - let mut builder = tar::Builder::new(encoder); - let content = b"consensus-store"; - let mut header = tar::Header::new_gnu(); - header.set_size(content.len() as u64); - header.set_mode(0o644); - header.set_cksum(); - builder.append_data(&mut header, "store.db", content.as_ref())?; - let (data, result) = builder.into_inner()?.finish(); - result?; - - let server = MockServer::start().await; - Mock::given(method("GET")) - .and(path("/cl.tar.lz4")) - .respond_with( - ResponseTemplate::new(200) - .set_body_bytes(data.clone()) - .append_header("Content-Length", data.len().to_string().as_str()), - ) - .mount(&server) - .await; - - let dir = tempfile::tempdir()?; - let cmd = DownloadCmd { - url: Some(format!("{}/cl.tar.lz4", server.uri())), - chain: "arc-devnet".into(), - force_redownload: false, - }; - - let url = format!("{}/cl.tar.lz4", server.uri()); - cmd.run(dir.path()).await?; - - assert!(dir.path().join("store.db").exists()); - // Version marker should be written - assert_eq!( - std::fs::read_to_string(dir.path().join(".snapshot-url"))?, - url - ); - Ok(()) - } - - #[tokio::test] - async fn run_skips_when_url_matches() -> eyre::Result<()> { - use wiremock::matchers::{method, path}; - use wiremock::{Mock, MockServer, ResponseTemplate}; - - let buf = Vec::new(); - let encoder = lz4::EncoderBuilder::new().build(buf)?; - let mut builder = tar::Builder::new(encoder); - let content = b"consensus-store"; - let mut header = tar::Header::new_gnu(); - header.set_size(content.len() as u64); - header.set_mode(0o644); - header.set_cksum(); - builder.append_data(&mut header, "store.db", content.as_ref())?; - let (data, result) = builder.into_inner()?.finish(); - result?; - - let server = MockServer::start().await; - let mock = Mock::given(method("GET")) - .and(path("/cl.tar.lz4")) - .respond_with( - ResponseTemplate::new(200) - .set_body_bytes(data.clone()) - .append_header("Content-Length", data.len().to_string().as_str()), - ) - .expect(0) - .mount_as_scoped(&server) - .await; - - let dir = tempfile::tempdir()?; - let url = format!("{}/cl.tar.lz4", server.uri()); - - // Pre-populate data and matching marker - std::fs::write(dir.path().join("store.db"), b"existing")?; - std::fs::write(dir.path().join(".snapshot-url"), &url)?; - - let cmd = DownloadCmd { - url: Some(url), - chain: "arc-devnet".into(), - force_redownload: false, - }; - cmd.run(dir.path()).await?; - - // Data should be untouched - assert_eq!(std::fs::read(dir.path().join("store.db"))?, b"existing"); - - drop(mock); - Ok(()) - } - - #[tokio::test] - async fn run_errors_on_unknown_chain() { - let dir = tempfile::tempdir().unwrap(); - let cmd = DownloadCmd { - url: Some("http://example.com/cl.tar.lz4".into()), - chain: "not-a-chain".into(), - force_redownload: false, - }; - assert!(cmd.run(dir.path()).await.is_err()); - } -} diff --git a/crates/malachite-cli/src/cmd/init.rs b/crates/malachite-cli/src/cmd/init.rs deleted file mode 100644 index ceecc27..0000000 --- a/crates/malachite-cli/src/cmd/init.rs +++ /dev/null @@ -1,217 +0,0 @@ -// Copyright 2025 Circle Internet Group, Inc. All rights reserved. -// -// SPDX-License-Identifier: Apache-2.0 -// -// Licensed under the Apache License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. -// You may obtain a copy of the License at -// -// http://www.apache.org/licenses/LICENSE-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// See the License for the specific language governing permissions and -// limitations under the License. - -//! Init command -//! -//! This command generates a private validator key file for the node. -//! Configuration is now provided via CLI flags instead of a config file. - -use std::path::Path; - -use clap::Parser; -use tracing::{info, warn}; - -use arc_consensus_types::Address; - -use crate::error::Error; -use crate::file::save_priv_validator_key; -use crate::new::generate_private_keys; - -#[derive(Parser, Debug, Clone, Default, PartialEq)] -pub struct InitCmd { - /// Overwrite existing private key file - #[clap(long)] - pub overwrite: bool, -} - -impl InitCmd { - /// Execute the init command - /// - /// This generates only the private validator key file. - /// Configuration is now provided via CLI flags when starting the node. - pub fn run(&self, priv_validator_key_file: &Path) -> Result<(), Error> { - init(priv_validator_key_file, self.overwrite) - } -} - -/// init command to generate the private validator key. -pub fn init(priv_validator_key_file: &Path, overwrite: bool) -> Result<(), Error> { - // Save default priv_validator_key - if priv_validator_key_file.exists() && !overwrite { - warn!( - file = %priv_validator_key_file.display(), - "Private key file already exists, skipping. Use --overwrite to replace it.", - ); - - return Ok(()); - } - - info!(file = %priv_validator_key_file.display(), "Generating private validator key"); - - let private_keys = generate_private_keys(1, false)?; - let priv_validator_key = private_keys[0].clone(); - save_priv_validator_key(priv_validator_key_file, &priv_validator_key)?; - - info!(file = %priv_validator_key_file.display(), "Private validator key generated successfully"); - - let public_key = priv_validator_key.public_key(); - - info!( - address = %Address::from_public_key(&public_key), - public_key = %format!("0x{}", hex::encode(public_key.as_bytes())), - "Key information", - ); - - Ok(()) -} - -#[cfg(test)] -mod tests { - use super::*; - use std::fs; - use tempfile::tempdir; - - /// Assert that a file has secure permissions (0600 - read/write for owner only) on Unix systems - #[cfg(unix)] - fn assert_file_permissions_secure(path: &std::path::Path) { - use std::os::unix::fs::PermissionsExt; - let metadata = fs::metadata(path).unwrap(); - let permissions = metadata.permissions(); - assert_eq!( - permissions.mode() & 0o777, - 0o600, - "File permissions should be 0600 (read/write for owner only)" - ); - } - - #[test] - fn init_generates_private_key_file() { - let dir = tempdir().unwrap(); - let key_file = dir.path().join("priv_validator_key.json"); - - let result = init(&key_file, false); - assert!(result.is_ok()); - assert!(key_file.exists()); - - // Verify the file contains valid JSON - let contents = fs::read_to_string(&key_file).unwrap(); - assert!(serde_json::from_str::(&contents).is_ok()); - - // Verify file permissions on Unix systems - #[cfg(unix)] - assert_file_permissions_secure(&key_file); - } - - #[test] - fn init_with_overwrite_replaces_existing_key() { - let dir = tempdir().unwrap(); - let key_file = dir.path().join("priv_validator_key.json"); - - // Create initial key - init(&key_file, false).unwrap(); - let original_contents = fs::read_to_string(&key_file).unwrap(); - - // Overwrite with new key - init(&key_file, true).unwrap(); - let new_contents = fs::read_to_string(&key_file).unwrap(); - - // Keys should be different (different random generation) - assert_ne!(original_contents, new_contents); - } - - #[test] - fn init_without_overwrite_skips_existing_key() { - let dir = tempdir().unwrap(); - let key_file = dir.path().join("priv_validator_key.json"); - - // Create initial key - init(&key_file, false).unwrap(); - let original_contents = fs::read_to_string(&key_file).unwrap(); - - // Try to init again without overwrite - init(&key_file, false).unwrap(); - let contents_after = fs::read_to_string(&key_file).unwrap(); - - // Contents should be unchanged - assert_eq!(original_contents, contents_after); - } - - #[test] - fn init_cmd_run_generates_key() { - let dir = tempdir().unwrap(); - let key_file = dir.path().join("priv_validator_key.json"); - - let cmd = InitCmd { overwrite: false }; - let result = cmd.run(&key_file); - - assert!(result.is_ok()); - assert!(key_file.exists()); - } - - #[test] - fn init_cmd_with_overwrite_flag() { - let dir = tempdir().unwrap(); - let key_file = dir.path().join("priv_validator_key.json"); - - // Create initial key - let cmd = InitCmd { overwrite: false }; - cmd.run(&key_file).unwrap(); - let original_contents = fs::read_to_string(&key_file).unwrap(); - - // Overwrite - let cmd = InitCmd { overwrite: true }; - cmd.run(&key_file).unwrap(); - let new_contents = fs::read_to_string(&key_file).unwrap(); - - assert_ne!(original_contents, new_contents); - } - - #[test] - fn init_creates_parent_directories() { - let dir = tempdir().unwrap(); - let key_file = dir.path().join("config").join("priv_validator_key.json"); - - // Parent directory doesn't exist yet - assert!(!key_file.parent().unwrap().exists()); - - let result = init(&key_file, false); - - // Should create parent directories - assert!(result.is_ok()); - assert!(key_file.exists()); - assert!(key_file.parent().unwrap().exists()); - } - - #[test] - fn init_cmd_default_has_overwrite_false() { - let cmd = InitCmd::default(); - assert!(!cmd.overwrite); - } - - #[test] - fn init_cmd_parses_overwrite_flag() { - let args = vec!["init", "--overwrite"]; - let cmd = InitCmd::try_parse_from(args).unwrap(); - assert!(cmd.overwrite); - } - - #[test] - fn init_cmd_without_overwrite_flag() { - let args = vec!["init"]; - let cmd = InitCmd::try_parse_from(args).unwrap(); - assert!(!cmd.overwrite); - } -} diff --git a/crates/malachite-cli/src/cmd/key.rs b/crates/malachite-cli/src/cmd/key.rs deleted file mode 100644 index c658b98..0000000 --- a/crates/malachite-cli/src/cmd/key.rs +++ /dev/null @@ -1,103 +0,0 @@ -// Copyright 2025 Circle Internet Group, Inc. All rights reserved. -// -// SPDX-License-Identifier: Apache-2.0 -// -// Licensed under the Apache License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. -// You may obtain a copy of the License at -// -// http://www.apache.org/licenses/LICENSE-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// See the License for the specific language governing permissions and -// limitations under the License. - -//! Key command -//! -//! Loads the private validator key from disk and displays the public key and address. - -use std::path::{Path, PathBuf}; - -use clap::Parser; -use tracing::info; - -use arc_consensus_types::signing::PrivateKey; -use arc_consensus_types::Address; - -use crate::error::Error; - -#[derive(Parser, Debug, Clone, Default, PartialEq)] -pub struct KeyCmd { - /// Path to the private validator key file. - /// If not specified, uses the default path under --home. - pub key_file: Option, -} - -impl KeyCmd { - pub fn run(&self, default_key_file: &Path) -> Result<(), Error> { - let priv_validator_key_file = self.key_file.as_deref().unwrap_or(default_key_file); - let contents = std::fs::read_to_string(priv_validator_key_file) - .map_err(|_| Error::LoadFile(priv_validator_key_file.to_path_buf()))?; - - let private_key: PrivateKey = - serde_json::from_str(&contents).map_err(|e| Error::FromJSON(e.to_string()))?; - - info!(file = %priv_validator_key_file.display(), "Loaded private key"); - - let public_key = private_key.public_key(); - let address = Address::from_public_key(&public_key); - - info!( - public_key = %format!("0x{}", hex::encode(public_key.as_bytes())), - address = %address, - "Key information", - ); - - Ok(()) - } -} - -#[cfg(test)] -mod tests { - use super::*; - use crate::file::save_priv_validator_key; - use crate::new::generate_private_keys; - use tempfile::tempdir; - - #[test] - fn key_cmd_displays_key_info() { - let dir = tempdir().unwrap(); - let key_file = dir.path().join("priv_validator_key.json"); - - let private_keys = generate_private_keys(1, false).unwrap(); - let priv_key = private_keys[0].clone(); - save_priv_validator_key(&key_file, &priv_key).unwrap(); - - let cmd = KeyCmd::default(); - let result = cmd.run(&key_file); - assert!(result.is_ok()); - } - - #[test] - fn key_cmd_fails_on_missing_file() { - let dir = tempdir().unwrap(); - let key_file = dir.path().join("nonexistent.json"); - - let cmd = KeyCmd::default(); - let result = cmd.run(&key_file); - assert!(result.is_err()); - } - - #[test] - fn key_cmd_fails_on_invalid_json() { - let dir = tempdir().unwrap(); - let key_file = dir.path().join("bad_key.json"); - std::fs::write(&key_file, "not valid json").unwrap(); - - let cmd = KeyCmd::default(); - let result = cmd.run(&key_file); - assert!(result.is_err()); - } -} diff --git a/crates/malachite-cli/src/cmd/mod.rs b/crates/malachite-cli/src/cmd/mod.rs deleted file mode 100644 index eb07d72..0000000 --- a/crates/malachite-cli/src/cmd/mod.rs +++ /dev/null @@ -1,21 +0,0 @@ -// Copyright 2025 Circle Internet Group, Inc. All rights reserved. -// -// SPDX-License-Identifier: Apache-2.0 -// -// Licensed under the Apache License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. -// You may obtain a copy of the License at -// -// http://www.apache.org/licenses/LICENSE-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// See the License for the specific language governing permissions and -// limitations under the License. - -pub mod db; -pub mod download; -pub mod init; -pub mod key; -pub mod start; diff --git a/crates/malachite-cli/src/cmd/start.rs b/crates/malachite-cli/src/cmd/start.rs deleted file mode 100644 index 8ac42a9..0000000 --- a/crates/malachite-cli/src/cmd/start.rs +++ /dev/null @@ -1,1727 +0,0 @@ -// Copyright 2025 Circle Internet Group, Inc. All rights reserved. -// -// SPDX-License-Identifier: Apache-2.0 -// -// Licensed under the Apache License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. -// You may obtain a copy of the License at -// -// http://www.apache.org/licenses/LICENSE-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// See the License for the specific language governing permissions and -// limitations under the License. - -use std::net::SocketAddr; -use std::path::PathBuf; - -use arc_consensus_types::rpc_sync::SyncEndpointUrl; -use clap::Parser; -use color_eyre::eyre; -use serde::{Deserialize, Serialize}; -use tracing::info; -use url::Url; - -use arc_consensus_types::Address; -use malachitebft_app::consensus::Multiaddr; - -use crate::file::save_priv_validator_key; -use crate::new::generate_private_keys; - -/// Tokio single-threaded runtime flavor. -pub const RUNTIME_SINGLE_THREADED: &str = "single-threaded"; - -/// Tokio multi-threaded runtime flavor. -pub const RUNTIME_MULTI_THREADED: &str = "multi-threaded"; - -/// Start command to run a node. -/// -/// Derives `clap::Parser` for CLI parsing and `serde::Serialize` / -/// `serde::Deserialize` for TOML-based deserialization by external -/// tooling. Deployment-specific fields are marked `#[serde(skip)]`, so -/// TOML cannot set them. They inherit their values from -/// `StartCmd::default()` via the container-level `#[serde(default)]`. -#[derive(Parser, Debug, Clone, PartialEq, Serialize, Deserialize)] -#[serde(default, deny_unknown_fields)] -pub struct StartCmd { - // ===== Node Identity ===== - /// A custom human-readable name for this node. - /// - /// If not provided, a random moniker will be generated. - #[clap(long, value_name = "NAME")] - #[serde(skip)] - pub moniker: Option, - - // ===== P2P Networking ===== - /// P2P listen multiaddr - /// - /// Example: /ip4/172.19.0.5/tcp/27000 - #[clap( - long = "p2p.addr", - value_name = "MULTIADDR", - default_value = "/ip4/0.0.0.0/tcp/27000" - )] - #[serde(skip)] - pub p2p_addr: Multiaddr, - - /// Comma-separated list of persistent peer multiaddrs to connect to - /// - /// Example: /ip4/172.19.0.21/tcp/27000,/ip4/172.19.0.22/tcp/27000 - #[clap(long = "p2p.persistent-peers", value_delimiter = ',', num_args = 0..)] - #[serde(skip)] - pub p2p_persistent_peers: Vec, - - /// Only allow connections to/from persistent peers. - /// - /// When set, the node will reject connections from peers that are not - /// in the persistent peers list. Useful for sentry node setups where - /// a validator should only communicate with known trusted peers. - #[clap(long = "p2p.persistent-peers-only")] - #[serde(skip)] - pub p2p_persistent_peers_only: bool, - - /// Enable gossipsub explicit peering for persistent peers. - /// - /// When enabled, persistent peers are added as explicit peers in GossipSub, - /// meaning a node always sends and forwards messages to its explicit peers, - /// regardless of mesh membership. - #[clap(long = "gossipsub.explicit-peering", help_heading = "GossipSub")] - #[serde(skip)] - pub gossipsub_explicit_peering: bool, - - /// Enable gossipsub mesh peer scoring / prioritization. - /// - /// When enabled, peers are scored and prioritized based on their type - /// during mesh formation. - #[clap(long = "gossipsub.mesh-prioritization", help_heading = "GossipSub")] - #[serde(skip)] - pub gossipsub_mesh_prioritization: bool, - - /// Gossipsub network load profile controlling mesh size and bandwidth. - /// - /// - low: fewer mesh peers, lower bandwidth (mesh_n=3) - /// - average: balanced for typical deployments (mesh_n=6) [default] - /// - high: more mesh peers, higher bandwidth (mesh_n=10) - #[clap( - long = "gossipsub.load", - value_name = "PROFILE", - help_heading = "GossipSub", - value_parser = ["low", "average", "high"] - )] - #[serde(skip)] - pub gossipsub_load: Option, - - // ===== Logging ===== - /// Log level - #[clap(long, value_name = "LOG_LEVEL")] - pub log_level: Option, - - /// Log format - #[clap(long, value_name = "LOG_FORMAT")] - pub log_format: Option, - - // ===== Discovery ===== - /// Enable peer discovery - #[clap(long)] - pub discovery: bool, - - /// Number of outbound peers for discovery - #[clap( - long = "discovery.num-outbound-peers", - value_name = "COUNT", - default_value = "20" - )] - pub discovery_num_outbound_peers: usize, - - /// Number of inbound peers for discovery - #[clap( - long = "discovery.num-inbound-peers", - value_name = "COUNT", - default_value = "20" - )] - pub discovery_num_inbound_peers: usize, - - // ===== Consensus ===== - /// Disable consensus protocol participation. - /// - /// When set, the node only runs the synchronization protocol - /// and does not subscribe to consensus-related gossip topics. - /// Use for sync-only full nodes. - #[clap(long, conflicts_with = "validator")] - pub no_consensus: bool, - - /// Run as a validator node. - /// - /// When set, the node loads its consensus signing key, - /// signs a validator proof, and advertises a - /// validator identity on the P2P network. Requires - /// `--suggested-fee-recipient` so tips and rewards go to - /// an explicit address rather than being silently burned. - /// - /// Without this flag the node runs as a full node: it - /// participates in gossip but does not sign votes or proposals. - #[clap( - long, - conflicts_with_all = ["no_consensus", "follow"], - requires = "suggested_fee_recipient", - )] - pub validator: bool, - - // ===== Value Sync ===== - /// Enable value sync - #[clap( - long, - default_value_t = true, - num_args = 0..=1, - default_missing_value = "true", - )] - pub value_sync: bool, - - // ===== Execution Layer Connection ===== - /// The path to the Ethereum IPC socket. For reth with default settings, - /// this will be /tmp/reth.ipc. To change the path in reth, you need to - /// provide the `--ipcpath` flag. - /// - /// This is recommended option if the consensus and execution layers are colocated on the same machine. - #[clap(long, value_name = "PATH")] - #[serde(skip)] - pub eth_socket: Option, - - /// The path to the execution engine socket. To enable this in reth, you - /// need to provide the `--auth-ipc` and `--auth-ipc.path` flags. - /// - /// This is recommended option if the consensus and execution layers are colocated on the same machine. - #[clap(long, value_name = "PATH")] - #[serde(skip)] - pub execution_socket: Option, - - /// The URL of the Ethereum JSON-RPC API. If the Ethereum full node is - /// running on the same computer with the default port, this will be - /// http://localhost:8545. Most of the execution clients provide this - /// functionality. - /// - /// Use this option if the consensus and executation layer are on different machines. - #[clap(long, value_name = "URL")] - #[serde(skip)] - pub eth_rpc_endpoint: Option, - - /// The URL of the execution engine API. If the execution engine is running - /// on the same computer with the default port, this will be - /// http://localhost:8551. - /// - /// Use this option if the consensus and executation layer are on different machines. - #[clap(long, value_name = "URL")] - #[serde(skip)] - pub execution_endpoint: Option, - - /// The WebSocket URL of the execution engine. Used for subscribing to - /// real-time execution layer events (e.g. persisted block notifications). - /// - /// If omitted, derived from --eth-rpc-endpoint using the convention - /// (scheme http→ws / https→wss, port + 1). - /// - /// Example: ws://localhost:8546 - #[clap(long, value_name = "URL")] - #[serde(skip)] - pub execution_ws_endpoint: Option, - - /// Enable persistence backpressure during startup replay. When enabled, - /// the consensus layer waits for the execution layer to persist blocks - /// before replaying further. - /// - /// Requires --execution-ws-endpoint (RPC mode) or IPC mode. - #[clap(long = "execution-persistence-backpressure")] - pub execution_persistence_backpressure: bool, - - /// Maximum canonical-minus-persisted gap the execution layer may have - /// before persistence backpressure is applied. - /// - /// Backpressure begins once the gap reaches this threshold. - /// - /// Only takes effect when --execution-persistence-backpressure is enabled. - /// Large values weaken backpressure and may allow the execution layer - /// to accumulate a significant unpersisted block buffer. - #[clap( - long = "execution-persistence-backpressure-threshold", - value_name = "BLOCKS", - default_value = "16" - )] - pub execution_persistence_backpressure_threshold: u64, - - /// The path to the JWT secret file shared by Malachite and the execution - /// engine. This is a mandatory form of authentication which ensures that - /// Malachite has the authority to control the execution engine. - /// - /// Use this option if the consensus and executation layer are on different machines. - #[clap(long, value_name = "PATH")] - #[serde(skip)] - pub execution_jwt: Option, - - // ===== Metrics ===== - /// Enable Prometheus metrics and set listen address. - /// - /// If omitted, metrics are disabled. - /// If provided, metrics are enabled on the given address. - /// - /// Example: 0.0.0.0:29000 - #[clap(long, value_name = "ADDR")] - #[serde(skip)] - pub metrics: Option, - - // ===== RPC ===== - /// Enable RPC server and set listen address. - /// - /// If omitted, RPC is disabled. - /// If provided, RPC is enabled on the given address. - /// - /// Example: 0.0.0.0:31000 - #[clap(long = "rpc.addr", value_name = "ADDR")] - #[serde(skip)] - pub rpc_addr: Option, - - // ===== Runtime ===== - /// Tokio runtime flavor to use. - #[clap( - long = "runtime.flavor", - value_name = "FLAVOR", - default_value = RUNTIME_MULTI_THREADED, - value_parser = [RUNTIME_SINGLE_THREADED, RUNTIME_MULTI_THREADED] - )] - pub runtime_flavor: String, - - /// Number of worker threads for the Tokio multi-threaded runtime. - /// - /// If not set, the runtime will default to the number of CPU cores. - /// This option is ignored if the single-threaded runtime is selected. - #[clap(long = "runtime.worker-threads", value_name = "COUNT")] - pub worker_threads: Option, - - // ===== Pruning presets ===== - /// Full-node pruning preset. Sets --prune.certificates.distance 237600. - /// Mutually exclusive with --minimal and the individual --prune.certificates.* flags. - /// Note: on the CL, both --full and --minimal retain the same certificate - /// history for now; the distinction applies at the EL layer. - #[clap( - long = "full", - default_value_t = false, - conflicts_with_all = &["minimal", "prune_certificates_distance", "prune_certificates_before"], - help_heading = "Arc pruning presets" - )] - #[serde(skip)] - pub full: bool, - - /// Minimal-storage pruning preset. Sets --prune.certificates.distance 237600. - /// Mutually exclusive with --full and the individual --prune.certificates.* flags. - /// Note: on the CL, both --full and --minimal retain the same certificate - /// history for now; the distinction applies at the EL layer. - #[clap( - long = "minimal", - default_value_t = false, - conflicts_with_all = &["full", "prune_certificates_distance", "prune_certificates_before"], - help_heading = "Arc pruning presets" - )] - #[serde(skip)] - pub minimal: bool, - - // ===== Pruning ===== - /// Keep certificates for the last N heights. Certificates for heights older than - /// current_height - N will be pruned. Mirrors reth's --prune.*.distance semantics. - /// Setting this to 0 disables distance-based pruning. - /// Mutually exclusive with --prune.certificates.before and the pruning presets. - #[clap( - long = "prune.certificates.distance", - alias = "pruning.block-interval", - value_name = "COUNT", - default_value = "0", - conflicts_with_all = &["prune_certificates_before", "full", "minimal"] - )] - pub prune_certificates_distance: u64, - - /// Prune all certificates at heights strictly below this value. - /// Setting this to 0 disables height-based pruning. - /// Mutually exclusive with --prune.certificates.distance and the pruning presets. - #[clap( - long = "prune.certificates.before", - alias = "pruning.min-height", - value_name = "HEIGHT", - default_value = "0", - conflicts_with_all = &["prune_certificates_distance", "full", "minimal"] - )] - pub prune_certificates_before: u64, - - // ===== Other ===== - /// The path to the validator private key file. - /// - /// This file contains the private key used for: - /// - P2P/libp2p network identity (always required) - /// - Consensus message signing (only when using local signing, not with --signing.remote) - /// - /// When using --signing.remote, if this file doesn't exist, it will be automatically - /// generated with a random key for P2P network identity purposes. - /// - /// Default: {home_dir}/config/priv_validator_key.json - /// where `home_dir` is the directory provided with the `--home` global option - #[clap(long, value_name = "PATH")] - #[serde(skip)] - pub private_key: Option, - - /// Profiling server bind address - #[clap( - long = "pprof.addr", - value_name = "ADDR", - default_value = "0.0.0.0:6060" - )] - #[serde(skip)] - pub pprof_addr: String, - - /// Activate jemalloc heap profiling at startup. - /// - /// When built with the `pprof` feature, heap profiling infrastructure is - /// always available but inactive by default. This flag activates it so - /// that the `/debug/pprof/allocs` endpoint returns meaningful data. - #[clap(long = "pprof.heap-prof", default_value_t = false)] - pub pprof_heap_prof: bool, - - /// 20-byte ethereum-style address to receive tips (transactions' priority fee) - /// and rewards. - /// - /// The execution layer deposits fees and rewards to this address whenever the - /// validator successfully proposes a new block. The zero address is rejected - /// because rewards credited to it bypass the native-coin blocklist and are - /// unrecoverable. - #[clap(long, value_name = "ADDRESS", value_parser = parse_non_zero_address)] - #[serde(skip)] - pub suggested_fee_recipient: Option
, - - /// Skip database schema upgrade on startup. - /// - /// WARNING: This flag should only be used when a database upgrade failed. - /// Not upgrading the database may lead to errors or data corruption. - #[clap(long = "db.skip-upgrade")] - #[serde(skip)] - pub skip_db_upgrade: bool, - - // ===== Signing ===== - /// Use remote signing with the specified endpoint URL - /// - /// If not provided, local signing will be used (default behavior). - /// - /// Only meaningful together with `--validator`; for full and sync-only nodes - /// no consensus signing occurs. - /// - /// Example: http://validator-signer-proxy:10340 - #[clap( - long = "signing.remote", - value_name = "ENDPOINT", - requires = "validator" - )] - #[serde(skip)] - pub signing_remote: Option, - - /// Path to TLS certificate for remote signing - /// - /// Only used when --signing.remote is specified. - /// If provided, TLS will be automatically enabled. - #[clap( - long = "signing.tls-cert-path", - value_name = "PATH", - requires = "signing_remote" - )] - #[serde(skip)] - pub signing_tls_cert_path: Option, - - /// Enable RPC sync mode (follow with verification). - /// - /// In RPC sync mode, the node fetches blocks from trusted RPC endpoints - /// instead of participating in consensus. This is useful for running - /// read-only nodes that sync from validators. - /// - /// When no --follow.endpoint is provided, a default endpoint is resolved - /// from the chain ID at startup. - #[clap(long = "follow")] - #[serde(skip)] - pub follow: bool, - - /// RPC endpoint to fetch blocks from in RPC sync mode. - /// This flag can be repeated. - /// Optional when --follow is set; defaults are resolved from the chain ID. - /// - /// Format: - /// [,=] - /// where is an http:// or https:// URL, - /// and is either ws or wss. - /// - /// The WebSocket override value can be: - /// - A port number (e.g., wss=8546) — same host, explicit port - /// - A hostname (e.g., wss=ws.example.com) — different host, default port - /// - A host:port pair (e.g., wss=ws.example.com:1212) — different host and port - /// - /// If not specified, the WebSocket URL is derived from the HTTP URL - /// (scheme http->ws / https->wss, port HTTP+1 if non-default). - /// - /// Examples: - /// http://validator1:8545,ws=8546 - /// https://validator1:8545,wss=8546 - /// https://example.com,wss=ws.example.com - /// https://example.com,wss=ws.example.com:1212 - #[clap(long = "follow.endpoint", value_name = "ENDPOINT", requires = "follow")] - #[serde(skip)] - pub follow_endpoints: Vec, -} - -fn parse_non_zero_address(s: &str) -> Result { - let addr: Address = s.parse().map_err(|e| format!("invalid address: {e}"))?; - if addr.to_alloy_address().is_zero() { - return Err( - "must not be the zero address; rewards credited to 0x0 bypass the \ - native-coin blocklist and are unrecoverable" - .to_string(), - ); - } - Ok(addr) -} - -impl Default for StartCmd { - fn default() -> Self { - Self { - moniker: None, - p2p_addr: "/ip4/0.0.0.0/tcp/27000".parse().expect("valid multiaddr"), - p2p_persistent_peers: Vec::new(), - p2p_persistent_peers_only: false, - gossipsub_explicit_peering: false, - gossipsub_mesh_prioritization: false, - gossipsub_load: None, - log_level: None, - log_format: None, - discovery: false, - discovery_num_outbound_peers: 20, - discovery_num_inbound_peers: 20, - no_consensus: false, - validator: false, - value_sync: true, - eth_socket: None, - execution_socket: None, - eth_rpc_endpoint: None, - execution_endpoint: None, - execution_ws_endpoint: None, - execution_persistence_backpressure: false, - execution_persistence_backpressure_threshold: 16, - execution_jwt: None, - metrics: None, - rpc_addr: None, - runtime_flavor: RUNTIME_MULTI_THREADED.to_string(), - worker_threads: None, - full: false, - minimal: false, - prune_certificates_distance: 0, - prune_certificates_before: 0, - private_key: None, - pprof_addr: "0.0.0.0:6060".to_string(), - pprof_heap_prof: false, - suggested_fee_recipient: None, - skip_db_upgrade: false, - signing_remote: None, - signing_tls_cert_path: None, - follow: false, - follow_endpoints: Vec::new(), - } - } -} - -impl StartCmd { - /// Generate CLI flag strings for all non-default, manifest-configurable fields. - /// - /// Each field maps 1:1 to its `#[clap(long = "...")]` name. Only fields - /// whose values differ from `StartCmd::default()` are emitted. Deployment- - /// specific fields (marked `#[serde(skip)]`) are included here because - /// callers populate these before invoking. - pub fn to_cli_flags(&self) -> Vec { - let defaults = Self::default(); - let mut flags = Vec::new(); - - macro_rules! push_each { - ($flag:literal, $items:expr) => { - for item in $items { - flags.push(format!(concat!("--", $flag, "={}"), item)); - } - }; - } - macro_rules! push_if_some { - ($flag:literal, $opt:expr) => { - if let Some(ref v) = $opt { - flags.push(format!(concat!("--", $flag, "={}"), v)); - } - }; - } - macro_rules! push_if { - ($flag:literal, $cond:expr) => { - if $cond { - flags.push(concat!("--", $flag).to_string()); - } - }; - } - macro_rules! push_if_non_default { - ($flag:literal, $field:ident) => { - if self.$field != defaults.$field { - flags.push(format!(concat!("--", $flag, "={}"), self.$field)); - } - }; - } - - // --- Deployment-specific fields (set by quake at runtime) --- - - push_if_some!("moniker", self.moniker); - push_if_non_default!("p2p.addr", p2p_addr); - if !self.p2p_persistent_peers.is_empty() { - let peers: Vec = self - .p2p_persistent_peers - .iter() - .map(|p| p.to_string()) - .collect(); - flags.push(format!("--p2p.persistent-peers={}", peers.join(","))); - } - push_if!("p2p.persistent-peers-only", self.p2p_persistent_peers_only); - push_if!( - "gossipsub.explicit-peering", - self.gossipsub_explicit_peering - ); - push_if!( - "gossipsub.mesh-prioritization", - self.gossipsub_mesh_prioritization - ); - push_if_some!("gossipsub.load", self.gossipsub_load); - - // --- Manifest-configurable fields --- - - push_if_some!("log-level", self.log_level); - push_if_some!("log-format", self.log_format); - push_if!("discovery", self.discovery); - push_if_non_default!("discovery.num-outbound-peers", discovery_num_outbound_peers); - push_if_non_default!("discovery.num-inbound-peers", discovery_num_inbound_peers); - push_if!("no-consensus", self.no_consensus); - push_if!("validator", self.validator); - push_if_non_default!("value-sync", value_sync); - push_if!( - "execution-persistence-backpressure", - self.execution_persistence_backpressure - ); - push_if_non_default!( - "execution-persistence-backpressure-threshold", - execution_persistence_backpressure_threshold - ); - push_if_non_default!("runtime.flavor", runtime_flavor); - push_if_some!("runtime.worker-threads", self.worker_threads); - push_if_non_default!("prune.certificates.distance", prune_certificates_distance); - push_if_non_default!("prune.certificates.before", prune_certificates_before); - - // --- Deployment-specific fields (set by quake, continued) --- - - push_if_some!("eth-socket", self.eth_socket); - push_if_some!("execution-socket", self.execution_socket); - push_if_some!("eth-rpc-endpoint", self.eth_rpc_endpoint); - push_if_some!("execution-endpoint", self.execution_endpoint); - push_if_some!("execution-ws-endpoint", self.execution_ws_endpoint); - push_if_some!("execution-jwt", self.execution_jwt); - push_if_some!("metrics", self.metrics); - push_if_some!("rpc.addr", self.rpc_addr); - push_if!("full", self.full); - push_if!("minimal", self.minimal); - if let Some(ref path) = self.private_key { - flags.push(format!("--private-key={}", path.display())); - } - push_if_non_default!("pprof.addr", pprof_addr); - push_if!("pprof.heap-prof", self.pprof_heap_prof); - push_if_some!("suggested-fee-recipient", self.suggested_fee_recipient); - push_if!("db.skip-upgrade", self.skip_db_upgrade); - push_if_some!("signing.remote", self.signing_remote); - push_if_some!("signing.tls-cert-path", self.signing_tls_cert_path); - push_if!("follow", self.follow); - push_each!("follow.endpoint", &self.follow_endpoints); - - flags - } - - /// Validates that conflicting options are not provided simultaneously. - /// - /// This method ensures that users don't specify both IPC and RPC options - /// at the same time, as they represent different communication methods. - pub fn validate(&self) -> eyre::Result<()> { - // Check if both IPC and RPC options are provided - let has_ipc_options = self.eth_socket.is_some() || self.execution_socket.is_some(); - let has_rpc_options = self.eth_rpc_endpoint.is_some() - || self.execution_endpoint.is_some() - || self.execution_jwt.is_some(); - - if has_ipc_options && has_rpc_options { - return Err(eyre::eyre!( - "Conflicting options detected: Cannot specify both IPC and RPC options simultaneously.\n\ - IPC options: --eth-socket, --execution-socket\n\ - RPC options: --eth-rpc-endpoint, --execution-endpoint, --execution-jwt\n\ - Please choose either IPC (for local communication) or RPC (for remote communication)." - )); - } - - // Validate persistent-peers-only configuration - if self.p2p_persistent_peers_only && self.p2p_persistent_peers.is_empty() { - return Err(eyre::eyre!( - "--p2p.persistent-peers-only requires at least one --p2p.persistent-peers entry.\n\ - Without persistent peers, the node would reject all connections." - )); - } - - if self.execution_persistence_backpressure_threshold == 0 { - return Err(eyre::eyre!( - "--execution-persistence-backpressure-threshold must be greater than 0.\n\ - A value of 0 would cause persistence backpressure to stall indefinitely once active." - )); - } - - Ok(()) - } - - pub fn private_key_file(&self, default: PathBuf) -> eyre::Result { - let priv_key_path = self.private_key.as_ref().unwrap_or(&default); - - if priv_key_path.exists() { - info!(path = %priv_key_path.display(), "Using existing private key file"); - return Ok(priv_key_path.clone()); - } - - // The private key file does not exist. - if self.signing_remote.is_some() { - // With remote signing, we can auto-generate a key for P2P identity. - info!(file = %priv_key_path.display(), "Generating private key for P2P network identity"); - let private_keys = generate_private_keys(1, false)?; - let priv_validator_key = private_keys[0].clone(); - save_priv_validator_key(priv_key_path, &priv_validator_key)?; - info!( - path = %priv_key_path.display(), - "✅ Private key generated successfully for P2P network identity" - ); - Ok(priv_key_path.clone()) - } else if self.private_key.is_some() { - // A specific key file was requested but not found. - Err(eyre::eyre!( - "The specified private key file does not exist: {}", - priv_key_path.display() - )) - } else { - // Using default path, but the key file is not found. - Err(eyre::eyre!( - "The default private key file does not exist: {}. \n\n\ - You can generate it by running 'arc-node-consensus init' or \ - provide a path to the existing file using the --private-key option.", - priv_key_path.display() - )) - } - } - - /// Get the moniker, generating a random one if not provided. - pub fn get_moniker(&self) -> String { - self.moniker.clone().unwrap_or_else(|| { - use rand::Rng; - let adjectives = [ - "happy", "brave", "swift", "wise", "quiet", "bright", "calm", "eager", "gentle", - "kind", "noble", "proud", "swift", "witty", "zesty", - ]; - let nouns = [ - "node", - "validator", - "sentinel", - "guardian", - "keeper", - "watcher", - "beacon", - "herald", - "oracle", - "pilot", - "ranger", - "scout", - ]; - let mut rng = rand::thread_rng(); - let adj = adjectives[rng.gen_range(0..adjectives.len())]; - let noun = nouns[rng.gen_range(0..nouns.len())]; - let num = rng.gen_range(100..999); - format!("{}-{}-{}", adj, noun, num) - }) - } - - /// Get the P2P listen multiaddr. - pub fn p2p_listen_addr(&self) -> eyre::Result { - Ok(self.p2p_addr.clone()) - } - - /// Get persistent peers multiaddrs - pub fn persistent_peers(&self) -> Vec { - self.p2p_persistent_peers.clone() - } -} - -#[cfg(test)] -mod tests { - use super::*; - use std::fs::File; - use tempfile::tempdir; - - const TEST_FEE_RECIPIENT: &str = "0xf97e180c050e5ab072211ad2c213eb5aee4df134"; - - fn new_start_cmd() -> StartCmd { - StartCmd { - moniker: Some("test-node".to_string()), - p2p_addr: "/ip4/127.0.0.1/tcp/27000".parse().unwrap(), - ..Default::default() - } - } - - fn dummy_url() -> Url { - Url::parse("http://localhost:8545").unwrap() - } - - /// Assert that a file has secure permissions (0600 - read/write for owner only) on Unix systems - #[cfg(unix)] - fn assert_file_permissions_secure(path: &std::path::Path) { - use std::os::unix::fs::PermissionsExt; - let metadata = std::fs::metadata(path).unwrap(); - let permissions = metadata.permissions(); - assert_eq!( - permissions.mode() & 0o777, - 0o600, - "File permissions should be 0600 (read/write for owner only)" - ); - } - - #[test] - fn validate_ok_with_no_conflicting_options() { - let cmd = new_start_cmd(); - assert!( - cmd.validate().is_ok(), - "Command with valid options should be valid" - ); - } - - #[test] - fn validate_ok_with_only_ipc_options() { - let mut cmd = new_start_cmd(); - cmd.eth_socket = Some("/tmp/reth.ipc".to_string()); - cmd.execution_socket = Some("/tmp/reth-auth.ipc".to_string()); - assert!( - cmd.validate().is_ok(), - "Should be valid with only IPC options" - ); - } - - #[test] - fn validate_ok_with_only_rpc_options() { - let mut cmd = new_start_cmd(); - cmd.eth_rpc_endpoint = Some(dummy_url()); - cmd.execution_endpoint = Some(dummy_url()); - cmd.execution_jwt = Some("/path/to/jwt.hex".to_string()); - assert!( - cmd.validate().is_ok(), - "Should be valid with only RPC options" - ); - } - - #[test] - fn validate_err_when_mixing_ipc_and_rpc() { - let mut cmd = new_start_cmd(); - cmd.eth_socket = Some("/tmp/reth.ipc".to_string()); - cmd.eth_rpc_endpoint = Some(dummy_url()); - - let result = cmd.validate(); - assert!(result.is_err(), "Should fail when mixing IPC and RPC"); - - let err_msg = result.unwrap_err().to_string(); - assert!(err_msg.contains("Conflicting options detected")); - } - - #[test] - fn validate_err_with_another_mix_of_options() { - let mut cmd = new_start_cmd(); - cmd.execution_socket = Some("/tmp/reth-auth.ipc".to_string()); - cmd.execution_jwt = Some("/path/to/jwt.hex".to_string()); - - let result = cmd.validate(); - assert!( - result.is_err(), - "Should fail with another mix of IPC and RPC" - ); - } - - #[test] - fn validate_err_with_zero_persistence_backpressure_threshold() { - let mut cmd = new_start_cmd(); - cmd.execution_persistence_backpressure_threshold = 0; - - let result = cmd.validate(); - assert!( - result.is_err(), - "Should fail with zero persistence backpressure threshold" - ); - - let err_msg = result.unwrap_err().to_string(); - assert!(err_msg.contains("execution-persistence-backpressure-threshold")); - } - - #[test] - fn private_key_file_uses_provided_path_if_it_exists() { - let dir = tempdir().unwrap(); - let key_path = dir.path().join("priv_validator_key.json"); - File::create(&key_path).unwrap(); - - let mut cmd = new_start_cmd(); - cmd.private_key = Some(key_path.clone()); - - let default_path = PathBuf::from("/non/existent/path"); - - let result = cmd.private_key_file(default_path); - assert!(result.is_ok()); - assert_eq!(result.unwrap(), key_path); - } - - #[test] - fn private_key_file_errs_if_provided_path_does_not_exist() { - let non_existent_path = PathBuf::from("/some/made/up/path/key.json"); - let mut cmd = new_start_cmd(); - cmd.private_key = Some(non_existent_path.clone()); - - let result = cmd.private_key_file(PathBuf::from("/another/path")); - assert!(result.is_err()); - let err_msg = result.unwrap_err().to_string(); - assert!(err_msg.contains("private key file does not exist")); - assert!(err_msg.contains(non_existent_path.to_str().unwrap())); - } - - #[test] - fn private_key_file_uses_default_path_if_it_exists_and_none_provided() { - let dir = tempdir().unwrap(); - let default_key_path = dir.path().join("default_key.json"); - File::create(&default_key_path).unwrap(); - - let cmd = new_start_cmd(); - - let result = cmd.private_key_file(default_key_path.clone()); - assert!(result.is_ok()); - assert_eq!(result.unwrap(), default_key_path); - } - - #[test] - fn private_key_file_errs_if_default_path_does_not_exist_and_none_provided() { - let non_existent_default = PathBuf::from("/another/made/up/path/default_key.json"); - let cmd = new_start_cmd(); - - let result = cmd.private_key_file(non_existent_default.clone()); - assert!(result.is_err()); - let err_msg = result.unwrap_err().to_string(); - assert!(err_msg.contains("default private key file does not exist")); - assert!(err_msg.contains(non_existent_default.to_str().unwrap())); - } - - #[test] - fn private_key_file_auto_generates_when_remote_signing_and_default_missing() { - let dir = tempdir().unwrap(); - let key_path = dir.path().join("priv_validator_key.json"); - - let mut cmd = new_start_cmd(); - cmd.signing_remote = Some("http://remote-signer:10340".to_string()); - - // Key doesn't exist yet - assert!(!key_path.exists()); - - // Should auto-generate the key - let result = cmd.private_key_file(key_path.clone()); - assert!(result.is_ok()); - assert_eq!(result.unwrap(), key_path); - - // Key should now exist - assert!(key_path.exists()); - - // Verify the file contains valid JSON - let contents = std::fs::read_to_string(&key_path).unwrap(); - assert!(serde_json::from_str::(&contents).is_ok()); - - // Verify file permissions on Unix systems - #[cfg(unix)] - assert_file_permissions_secure(&key_path); - } - - #[test] - fn private_key_file_auto_generates_when_remote_signing_and_custom_path_missing() { - let dir = tempdir().unwrap(); - let key_path = dir.path().join("custom_key.json"); - - let mut cmd = new_start_cmd(); - cmd.signing_remote = Some("http://remote-signer:10340".to_string()); - cmd.private_key = Some(key_path.clone()); - - // Key doesn't exist yet - assert!(!key_path.exists()); - - // Should auto-generate the key - let result = cmd.private_key_file(PathBuf::from("/unused/default")); - assert!(result.is_ok()); - assert_eq!(result.unwrap(), key_path); - - // Key should now exist - assert!(key_path.exists()); - - // Verify the file contains valid JSON - let contents = std::fs::read_to_string(&key_path).unwrap(); - assert!(serde_json::from_str::(&contents).is_ok()); - - // Verify file permissions on Unix systems - #[cfg(unix)] - assert_file_permissions_secure(&key_path); - } - - #[test] - fn private_key_file_does_not_overwrite_existing_key_with_remote_signing() { - let dir = tempdir().unwrap(); - let key_path = dir.path().join("priv_validator_key.json"); - - // Create a key file with known content - let original_content = r#"{"test": "original"}"#; - std::fs::write(&key_path, original_content).unwrap(); - - let mut cmd = new_start_cmd(); - cmd.signing_remote = Some("http://remote-signer:10340".to_string()); - - // Should use existing key - let result = cmd.private_key_file(key_path.clone()); - assert!(result.is_ok()); - - // Verify content hasn't changed - let contents = std::fs::read_to_string(&key_path).unwrap(); - assert_eq!(contents, original_content); - } - - #[test] - fn clap_parses_persistent_peers() { - let peer1 = "/ip4/172.19.0.21/tcp/27000"; - let peer2 = "/ip4/172.19.0.22/tcp/27000"; - let peers_str = format!("{},{}", peer1, peer2); - let args = vec![ - "arc-node-consensus", - "--moniker", - "test", - "--p2p.addr", - "/ip4/127.0.0.1/tcp/27000", - "--p2p.persistent-peers", - &peers_str, - ]; - - let cmd = StartCmd::try_parse_from(args).unwrap(); - assert_eq!(cmd.p2p_persistent_peers.len(), 2); - assert_eq!(cmd.p2p_persistent_peers[0], peer1.parse().unwrap()); - assert_eq!(cmd.p2p_persistent_peers[1], peer2.parse().unwrap()); - } - - #[test] - fn clap_uses_default_values() { - let args = vec![ - "arc-node-consensus", - "--moniker", - "test", - "--p2p.addr", - "/ip4/127.0.0.1/tcp/27000", - ]; - let cmd = StartCmd::try_parse_from(args).unwrap(); - assert_eq!(cmd.pprof_addr, "0.0.0.0:6060"); - assert!(!cmd.pprof_heap_prof); - assert_eq!(cmd.prune_certificates_distance, 0); - assert_eq!(cmd.prune_certificates_before, 0); - assert_eq!(cmd.discovery_num_outbound_peers, 20); - assert_eq!(cmd.discovery_num_inbound_peers, 20); - assert!(cmd.value_sync); - assert!(!cmd.discovery); - assert!(!cmd.validator); - } - - #[test] - fn pprof_heap_prof_when_set() { - let args = vec![ - "arc-node-consensus", - "--moniker", - "test", - "--p2p.addr", - "/ip4/127.0.0.1/tcp/27000", - "--pprof.heap-prof", - ]; - let cmd = StartCmd::try_parse_from(args).unwrap(); - assert!(cmd.pprof_heap_prof); - } - - #[test] - fn p2p_listen_addr_returns_multiaddr() { - let mut cmd = new_start_cmd(); - cmd.p2p_addr = "/ip4/172.19.0.5/tcp/27000".parse().unwrap(); - - let multiaddr = cmd.p2p_listen_addr().unwrap(); - assert_eq!(multiaddr.to_string(), "/ip4/172.19.0.5/tcp/27000"); - } - - #[test] - fn p2p_addr_has_default_value() { - let cmd = StartCmd::default(); - assert_eq!(cmd.p2p_addr.to_string(), "/ip4/0.0.0.0/tcp/27000"); - } - - #[test] - fn get_moniker_returns_provided_moniker() { - let mut cmd = new_start_cmd(); - cmd.moniker = Some("my-validator".to_string()); - - assert_eq!(cmd.get_moniker(), "my-validator"); - } - - #[test] - fn get_moniker_generates_random_when_not_provided() { - let mut cmd = new_start_cmd(); - cmd.moniker = None; - - let moniker = cmd.get_moniker(); - // Check format: {adjective}-{noun}-{number} - let parts: Vec<&str> = moniker.split('-').collect(); - assert_eq!(parts.len(), 3, "Generated moniker should have 3 parts"); - // Verify the last part is a number - assert!( - parts[2].parse::().is_ok(), - "Last part should be a number" - ); - } - - #[test] - fn p2p_addr_supports_different_protocols() { - let args = vec![ - "arc-node-consensus", - "--moniker", - "test", - "--p2p.addr", - "/ip4/127.0.0.1/udp/27000/quic-v1", - ]; - let cmd = StartCmd::try_parse_from(args).unwrap(); - assert_eq!(cmd.p2p_addr.to_string(), "/ip4/127.0.0.1/udp/27000/quic-v1"); - } - - #[test] - fn p2p_addr_uses_default_and_moniker_is_optional() { - let args = vec!["arc-node-consensus"]; - let cmd = StartCmd::try_parse_from(args).unwrap(); - assert_eq!(cmd.p2p_addr.to_string(), "/ip4/0.0.0.0/tcp/27000"); - assert!(cmd.moniker.is_none()); - } - - // Remote signing tests - #[test] - fn signing_remote_alone_sets_remote_signing() { - let args = vec![ - "arc-node-consensus", - "--moniker", - "test", - "--p2p.addr", - "/ip4/127.0.0.1/tcp/27000", - "--validator", - "--suggested-fee-recipient", - TEST_FEE_RECIPIENT, - "--signing.remote", - "http://signer:10340", - ]; - let cmd = StartCmd::try_parse_from(args).unwrap(); - assert_eq!(cmd.signing_remote, Some("http://signer:10340".to_string())); - assert_eq!(cmd.signing_tls_cert_path, None); - } - - #[test] - fn signing_remote_with_tls_cert_path() { - let args = vec![ - "arc-node-consensus", - "--moniker", - "test", - "--p2p.addr", - "/ip4/127.0.0.1/tcp/27000", - "--validator", - "--suggested-fee-recipient", - TEST_FEE_RECIPIENT, - "--signing.remote", - "http://signer:10340", - "--signing.tls-cert-path", - "/path/to/cert.pem", - ]; - let cmd = StartCmd::try_parse_from(args).unwrap(); - assert_eq!(cmd.signing_remote, Some("http://signer:10340".to_string())); - assert_eq!( - cmd.signing_tls_cert_path, - Some("/path/to/cert.pem".to_string()) - ); - } - - #[test] - fn signing_remote_without_validator_fails() { - let args = vec![ - "arc-node-consensus", - "--moniker", - "test", - "--p2p.addr", - "/ip4/127.0.0.1/tcp/27000", - "--signing.remote", - "http://signer:10340", - ]; - assert!(StartCmd::try_parse_from(args).is_err()); - } - - #[test] - fn signing_tls_cert_path_without_remote_fails() { - let args = vec![ - "arc-node-consensus", - "--moniker", - "test", - "--p2p.addr", - "/ip4/127.0.0.1/tcp/27000", - "--signing.tls-cert-path", - "/path/to/cert.pem", - ]; - let result = StartCmd::try_parse_from(args); - assert!(result.is_err()); - } - - #[test] - fn default_is_local_signing() { - let cmd = new_start_cmd(); - assert_eq!(cmd.signing_remote, None); - assert_eq!(cmd.signing_tls_cert_path, None); - } - - // Discovery tests - #[test] - fn discovery_flag_enables_discovery() { - let args = vec![ - "arc-node-consensus", - "--moniker", - "test", - "--p2p.addr", - "/ip4/127.0.0.1/tcp/27000", - "--discovery", - ]; - let cmd = StartCmd::try_parse_from(args).unwrap(); - assert!(cmd.discovery); - } - - #[test] - fn discovery_num_outbound_peers_parsing() { - let args = vec![ - "arc-node-consensus", - "--moniker", - "test", - "--p2p.addr", - "/ip4/127.0.0.1/tcp/27000", - "--discovery", - "--discovery.num-outbound-peers", - "30", - ]; - let cmd = StartCmd::try_parse_from(args).unwrap(); - assert_eq!(cmd.discovery_num_outbound_peers, 30); - } - - #[test] - fn discovery_num_inbound_peers_parsing() { - let args = vec![ - "arc-node-consensus", - "--moniker", - "test", - "--p2p.addr", - "/ip4/127.0.0.1/tcp/27000", - "--discovery", - "--discovery.num-inbound-peers", - "40", - ]; - let cmd = StartCmd::try_parse_from(args).unwrap(); - assert_eq!(cmd.discovery_num_inbound_peers, 40); - } - - #[test] - fn discovery_defaults_to_20_peers() { - let cmd = new_start_cmd(); - assert_eq!(cmd.discovery_num_outbound_peers, 20); - assert_eq!(cmd.discovery_num_inbound_peers, 20); - } - - // Metrics and RPC tests - #[test] - fn metrics_flag_with_valid_socket_address() { - let args = vec![ - "arc-node-consensus", - "--moniker", - "test", - "--p2p.addr", - "/ip4/127.0.0.1/tcp/27000", - "--metrics", - "0.0.0.0:29000", - ]; - let cmd = StartCmd::try_parse_from(args).unwrap(); - assert_eq!(cmd.metrics, Some("0.0.0.0:29000".parse().unwrap())); - } - - #[test] - fn rpc_addr_flag_with_valid_socket_address() { - let args = vec![ - "arc-node-consensus", - "--moniker", - "test", - "--p2p.addr", - "/ip4/127.0.0.1/tcp/27000", - "--rpc.addr", - "0.0.0.0:31000", - ]; - let cmd = StartCmd::try_parse_from(args).unwrap(); - assert_eq!(cmd.rpc_addr, Some("0.0.0.0:31000".parse().unwrap())); - } - - #[test] - fn metrics_and_rpc_are_optional() { - let cmd = new_start_cmd(); - assert_eq!(cmd.metrics, None); - assert_eq!(cmd.rpc_addr, None); - } - - // Pruning tests - #[test] - fn prune_certificates_distance_parsing() { - let args = vec![ - "arc-node-consensus", - "--moniker", - "test", - "--p2p.addr", - "/ip4/127.0.0.1/tcp/27000", - "--prune.certificates.distance", - "1000", - ]; - let cmd = StartCmd::try_parse_from(args).unwrap(); - assert_eq!(cmd.prune_certificates_distance, 1000); - } - - #[test] - fn prune_certificates_before_parsing() { - let args = vec![ - "arc-node-consensus", - "--moniker", - "test", - "--p2p.addr", - "/ip4/127.0.0.1/tcp/27000", - "--prune.certificates.before", - "500", - ]; - let cmd = StartCmd::try_parse_from(args).unwrap(); - assert_eq!(cmd.prune_certificates_before, 500); - } - - #[test] - fn pruning_defaults_to_zero() { - let cmd = new_start_cmd(); - assert!(!cmd.full); - assert!(!cmd.minimal); - assert_eq!(cmd.prune_certificates_distance, 0); - assert_eq!(cmd.prune_certificates_before, 0); - } - - #[test] - fn full_preset_parsing() { - let args = vec![ - "arc-node-consensus", - "--moniker", - "test", - "--p2p.addr", - "/ip4/127.0.0.1/tcp/27000", - "--full", - ]; - let cmd = StartCmd::try_parse_from(args).unwrap(); - assert!(cmd.full); - assert!(!cmd.minimal); - } - - #[test] - fn minimal_preset_parsing() { - let args = vec![ - "arc-node-consensus", - "--moniker", - "test", - "--p2p.addr", - "/ip4/127.0.0.1/tcp/27000", - "--minimal", - ]; - let cmd = StartCmd::try_parse_from(args).unwrap(); - assert!(cmd.minimal); - assert!(!cmd.full); - } - - #[test] - fn full_and_minimal_conflict() { - let args = vec![ - "arc-node-consensus", - "--moniker", - "test", - "--p2p.addr", - "/ip4/127.0.0.1/tcp/27000", - "--full", - "--minimal", - ]; - assert!(StartCmd::try_parse_from(args).is_err()); - } - - #[test] - fn full_conflicts_with_prune_certificates_distance() { - let args = vec![ - "arc-node-consensus", - "--moniker", - "test", - "--p2p.addr", - "/ip4/127.0.0.1/tcp/27000", - "--full", - "--prune.certificates.distance", - "1000", - ]; - assert!(StartCmd::try_parse_from(args).is_err()); - } - - // P2P tests - #[test] - fn validate_err_when_persistent_peers_only_without_peers() { - let mut cmd = new_start_cmd(); - cmd.p2p_persistent_peers_only = true; - // p2p_persistent_peers is empty by default - - let result = cmd.validate(); - assert!(result.is_err(), "Should fail without persistent peers"); - - let err_msg = result.unwrap_err().to_string(); - assert!(err_msg.contains("--p2p.persistent-peers-only requires")); - } - - #[test] - fn validate_ok_when_persistent_peers_only_with_peers() { - let mut cmd = new_start_cmd(); - cmd.p2p_persistent_peers_only = true; - cmd.p2p_persistent_peers = vec!["/ip4/172.19.0.21/tcp/27000".parse().unwrap()]; - - assert!(cmd.validate().is_ok()); - } - - #[test] - fn p2p_persistent_peers_only_flag_enables_mode() { - let args = vec![ - "arc-node-consensus", - "--moniker", - "test", - "--p2p.addr", - "/ip4/127.0.0.1/tcp/27000", - "--p2p.persistent-peers-only", - ]; - let cmd = StartCmd::try_parse_from(args).unwrap(); - assert!(cmd.p2p_persistent_peers_only); - } - - #[test] - fn p2p_persistent_peers_only_defaults_to_false() { - let cmd = StartCmd::default(); - assert!(!cmd.p2p_persistent_peers_only); - } - - #[test] - fn p2p_persistent_peers_with_empty_list() { - let args = vec![ - "arc-node-consensus", - "--moniker", - "test", - "--p2p.addr", - "/ip4/127.0.0.1/tcp/27000", - ]; - let cmd = StartCmd::try_parse_from(args).unwrap(); - assert!(cmd.p2p_persistent_peers.is_empty()); - } - - #[test] - fn p2p_persistent_peers_with_multiple_peers() { - let peer1 = "/ip4/172.19.0.21/tcp/27000"; - let peer2 = "/ip4/172.19.0.22/tcp/27000"; - let peer3 = "/ip4/172.19.0.23/tcp/27000"; - let peers_str = format!("{},{},{}", peer1, peer2, peer3); - let args = vec![ - "arc-node-consensus", - "--moniker", - "test", - "--p2p.addr", - "/ip4/127.0.0.1/tcp/27000", - "--p2p.persistent-peers", - &peers_str, - ]; - - let cmd = StartCmd::try_parse_from(args).unwrap(); - assert_eq!(cmd.p2p_persistent_peers.len(), 3); - assert_eq!(cmd.p2p_persistent_peers[0], peer1.parse().unwrap()); - assert_eq!(cmd.p2p_persistent_peers[1], peer2.parse().unwrap()); - assert_eq!(cmd.p2p_persistent_peers[2], peer3.parse().unwrap()); - } - - #[test] - fn value_sync_default_is_true() { - let cmd = new_start_cmd(); - assert!(cmd.value_sync); - } - - // GossipSub tests - #[test] - fn gossipsub_explicit_peering_defaults_to_false() { - let cmd = StartCmd::default(); - assert!(!cmd.gossipsub_explicit_peering); - } - - #[test] - fn gossipsub_mesh_prioritization_defaults_to_false() { - let cmd = StartCmd::default(); - assert!(!cmd.gossipsub_mesh_prioritization); - } - - #[test] - fn gossipsub_load_defaults_to_none() { - let cmd = StartCmd::default(); - assert!(cmd.gossipsub_load.is_none()); - } - - #[test] - fn gossipsub_explicit_peering_flag() { - let args = vec![ - "arc-node-consensus", - "--moniker", - "test", - "--p2p.addr", - "/ip4/127.0.0.1/tcp/27000", - "--gossipsub.explicit-peering", - ]; - let cmd = StartCmd::try_parse_from(args).unwrap(); - assert!(cmd.gossipsub_explicit_peering); - } - - #[test] - fn gossipsub_mesh_prioritization_flag() { - let args = vec![ - "arc-node-consensus", - "--moniker", - "test", - "--p2p.addr", - "/ip4/127.0.0.1/tcp/27000", - "--gossipsub.mesh-prioritization", - ]; - let cmd = StartCmd::try_parse_from(args).unwrap(); - assert!(cmd.gossipsub_mesh_prioritization); - } - - #[test] - fn gossipsub_load_profile_parsing() { - for profile in &["low", "average", "high"] { - let args = vec![ - "arc-node-consensus", - "--moniker", - "test", - "--p2p.addr", - "/ip4/127.0.0.1/tcp/27000", - "--gossipsub.load", - profile, - ]; - let cmd = StartCmd::try_parse_from(args).unwrap(); - assert_eq!(cmd.gossipsub_load.as_deref(), Some(*profile)); - } - } - - #[test] - fn gossipsub_load_rejects_invalid_profile() { - let args = vec![ - "arc-node-consensus", - "--moniker", - "test", - "--p2p.addr", - "/ip4/127.0.0.1/tcp/27000", - "--gossipsub.load", - "ultra", - ]; - assert!(StartCmd::try_parse_from(args).is_err()); - } - - #[test] - fn gossipsub_all_flags_combined() { - let args = vec![ - "arc-node-consensus", - "--moniker", - "test", - "--p2p.addr", - "/ip4/127.0.0.1/tcp/27000", - "--gossipsub.explicit-peering", - "--gossipsub.mesh-prioritization", - "--gossipsub.load", - "high", - ]; - let cmd = StartCmd::try_parse_from(args).unwrap(); - assert!(cmd.gossipsub_explicit_peering); - assert!(cmd.gossipsub_mesh_prioritization); - assert_eq!(cmd.gossipsub_load.as_deref(), Some("high")); - } - - // to_cli_flags tests - #[test] - fn to_cli_flags_empty_for_defaults() { - assert!(StartCmd::default().to_cli_flags().is_empty()); - } - - #[test] - fn to_cli_flags_round_trip_preserves_all_fields() { - let original = StartCmd { - moniker: Some("validator-01".to_string()), - p2p_addr: "/ip4/10.0.0.1/tcp/27001".parse().unwrap(), - p2p_persistent_peers: vec![ - "/ip4/10.0.0.2/tcp/27000".parse().unwrap(), - "/ip4/10.0.0.3/tcp/27000".parse().unwrap(), - ], - p2p_persistent_peers_only: true, - gossipsub_explicit_peering: true, - gossipsub_mesh_prioritization: true, - gossipsub_load: Some("high".to_string()), - log_level: None, - log_format: None, - discovery: true, - discovery_num_outbound_peers: 30, - discovery_num_inbound_peers: 40, - no_consensus: true, - validator: false, - value_sync: false, - eth_socket: Some("/tmp/reth.ipc".to_string()), - execution_socket: Some("/tmp/reth-auth.ipc".to_string()), - eth_rpc_endpoint: None, - execution_endpoint: None, - execution_ws_endpoint: None, - execution_persistence_backpressure: true, - execution_persistence_backpressure_threshold: 32, - execution_jwt: None, - metrics: Some("127.0.0.1:9000".parse().unwrap()), - rpc_addr: Some("127.0.0.1:31000".parse().unwrap()), - runtime_flavor: RUNTIME_SINGLE_THREADED.to_string(), - worker_threads: Some(8), - full: false, - minimal: false, - prune_certificates_distance: 1000, - prune_certificates_before: 0, - private_key: Some(PathBuf::from("/etc/arc/key.json")), - pprof_addr: "127.0.0.1:7070".to_string(), - pprof_heap_prof: true, - suggested_fee_recipient: Some( - "0x0000000000000000000000000000000000000042" - .parse() - .unwrap(), - ), - skip_db_upgrade: true, - signing_remote: Some("http://signer:10340".to_string()), - signing_tls_cert_path: Some("/etc/arc/signer.pem".to_string()), - follow: true, - follow_endpoints: vec!["http://rpc-1:8545,ws=8546".parse().unwrap()], - }; - - let args = std::iter::once("arc-node-consensus".to_string()) - .chain(original.to_cli_flags()) - .collect::>(); - let parsed = StartCmd::try_parse_from(args).expect("emitted flags should parse"); - - assert_eq!(parsed, original); - } - - // Validator flag tests - #[test] - fn validator_defaults_to_false() { - let cmd = StartCmd::default(); - assert!(!cmd.validator); - } - - #[test] - fn validator_flag_parsing() { - let args = vec![ - "arc-node-consensus", - "--moniker", - "test", - "--p2p.addr", - "/ip4/127.0.0.1/tcp/27000", - "--validator", - "--suggested-fee-recipient", - TEST_FEE_RECIPIENT, - ]; - let cmd = StartCmd::try_parse_from(args).unwrap(); - assert!(cmd.validator); - } - - #[test] - fn validator_requires_suggested_fee_recipient() { - let args = vec![ - "arc-node-consensus", - "--moniker", - "test", - "--p2p.addr", - "/ip4/127.0.0.1/tcp/27000", - "--validator", - ]; - let err = StartCmd::try_parse_from(args).unwrap_err().to_string(); - assert!( - err.contains("--suggested-fee-recipient"), - "expected error to mention --suggested-fee-recipient, got: {err}" - ); - } - - #[test] - fn suggested_fee_recipient_rejects_zero_address() { - let args = vec![ - "arc-node-consensus", - "--moniker", - "test", - "--p2p.addr", - "/ip4/127.0.0.1/tcp/27000", - "--validator", - "--suggested-fee-recipient", - "0x0000000000000000000000000000000000000000", - ]; - let err = StartCmd::try_parse_from(args).unwrap_err().to_string(); - assert!( - err.contains("zero address"), - "expected zero-address rejection, got: {err}" - ); - } - - #[test] - fn validator_conflicts_with_no_consensus() { - let args = vec![ - "arc-node-consensus", - "--moniker", - "test", - "--p2p.addr", - "/ip4/127.0.0.1/tcp/27000", - "--validator", - "--suggested-fee-recipient", - TEST_FEE_RECIPIENT, - "--no-consensus", - ]; - assert!(StartCmd::try_parse_from(args).is_err()); - } - - #[test] - fn validator_conflicts_with_follow() { - let args = vec![ - "arc-node-consensus", - "--moniker", - "test", - "--p2p.addr", - "/ip4/127.0.0.1/tcp/27000", - "--validator", - "--suggested-fee-recipient", - TEST_FEE_RECIPIENT, - "--follow", - "--follow.endpoint", - "http://localhost:8545", - ]; - assert!(StartCmd::try_parse_from(args).is_err()); - } -} diff --git a/crates/malachite-cli/src/error.rs b/crates/malachite-cli/src/error.rs deleted file mode 100644 index 48f9a58..0000000 --- a/crates/malachite-cli/src/error.rs +++ /dev/null @@ -1,63 +0,0 @@ -// Copyright 2025 Circle Internet Group, Inc. All rights reserved. -// -// SPDX-License-Identifier: Apache-2.0 -// -// Licensed under the Apache License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. -// You may obtain a copy of the License at -// -// http://www.apache.org/licenses/LICENSE-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// See the License for the specific language governing permissions and -// limitations under the License. - -//! Custom error messages for CLI helper functions. -//! This low level implementation allows the developer to choose their own error handling library. - -use std::path::PathBuf; - -/// Error messages for commands -#[derive(Debug, thiserror::Error)] -pub enum Error { - /// Error creating parent directory - #[error("Error creating parent directory: {}", .0.display())] - ParentDir(PathBuf), - - /// Error opening file - #[error("Error opening file: {}", .0.display())] - OpenFile(PathBuf), - - /// Error writing file - #[error("Error writing file: {}", .0.display())] - WriteFile(PathBuf), - - /// Error loading file - #[error("Error loading file: {}", .0.display())] - LoadFile(PathBuf), - - /// Error converting to JSON - #[error("Error converting to JSON: {0}")] - ToJSON(String), - - /// Error parsing JSON - #[error("Error parsing JSON: {0}")] - FromJSON(String), - - /// Error deriving keys from BIP-39/BIP-32 - #[error("Error deriving test keys (BIP-39/BIP-32): {0}")] - DeriveBip39(String), - - /// Error determining home directory path - #[error("Error determining home directory path")] - DirPath, - - /// Error joining threads - #[error("Error joining threads")] - Join, - - #[error("Error running spammer: {0}")] - Spammer(eyre::Error), -} diff --git a/crates/malachite-cli/src/file.rs b/crates/malachite-cli/src/file.rs deleted file mode 100644 index 5f49e3c..0000000 --- a/crates/malachite-cli/src/file.rs +++ /dev/null @@ -1,70 +0,0 @@ -// Copyright 2025 Circle Internet Group, Inc. All rights reserved. -// -// SPDX-License-Identifier: Apache-2.0 -// -// Licensed under the Apache License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. -// You may obtain a copy of the License at -// -// http://www.apache.org/licenses/LICENSE-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// See the License for the specific language governing permissions and -// limitations under the License. - -//! File save functions - -use std::fs; -use std::path::Path; - -use arc_consensus_types::signing::PrivateKey; - -use crate::error::Error; - -/// Save private_key validator key to file -pub fn save_priv_validator_key( - priv_validator_key_file: &Path, - priv_validator_key: &PrivateKey, -) -> Result<(), Error> { - save( - priv_validator_key_file, - &serde_json::to_string_pretty(priv_validator_key) - .map_err(|e| Error::ToJSON(e.to_string()))?, - ) -} - -fn save(path: &Path, data: &str) -> Result<(), Error> { - use std::io::Write; - - if let Some(parent_dir) = path.parent() { - fs::create_dir_all(parent_dir).map_err(|_| Error::ParentDir(parent_dir.to_path_buf()))?; - } - - // Create file with secure permissions (0600) on Unix systems - #[cfg(unix)] - let mut f = { - use std::os::unix::fs::OpenOptionsExt; - fs::OpenOptions::new() - .write(true) - .create(true) - .truncate(true) - .mode(0o600) // Set permissions at creation time - .open(path) - .map_err(|_| Error::OpenFile(path.to_path_buf()))? - }; - - #[cfg(not(unix))] - let mut f = fs::OpenOptions::new() - .write(true) - .create(true) - .truncate(true) - .open(path) - .map_err(|_| Error::OpenFile(path.to_path_buf()))?; - - f.write_all(data.as_bytes()) - .map_err(|_| Error::WriteFile(path.to_path_buf()))?; - - Ok(()) -} diff --git a/crates/malachite-cli/src/lib.rs b/crates/malachite-cli/src/lib.rs deleted file mode 100644 index e9c2cfd..0000000 --- a/crates/malachite-cli/src/lib.rs +++ /dev/null @@ -1,28 +0,0 @@ -// Copyright 2025 Circle Internet Group, Inc. All rights reserved. -// -// SPDX-License-Identifier: Apache-2.0 -// -// Licensed under the Apache License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. -// You may obtain a copy of the License at -// -// http://www.apache.org/licenses/LICENSE-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// See the License for the specific language governing permissions and -// limitations under the License. - -pub mod args; -pub mod cmd; -pub mod error; -pub mod file; -pub mod logging; -pub mod metrics; -pub mod new; -pub mod runtime; - -pub mod config { - pub use malachitebft_config::*; -} diff --git a/crates/malachite-cli/src/logging.rs b/crates/malachite-cli/src/logging.rs deleted file mode 100644 index 870dfd1..0000000 --- a/crates/malachite-cli/src/logging.rs +++ /dev/null @@ -1,90 +0,0 @@ -// Copyright 2025 Circle Internet Group, Inc. All rights reserved. -// -// SPDX-License-Identifier: Apache-2.0 -// -// Licensed under the Apache License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. -// You may obtain a copy of the License at -// -// http://www.apache.org/licenses/LICENSE-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// See the License for the specific language governing permissions and -// limitations under the License. - -use tracing::level_filters::LevelFilter; -use tracing_appender::non_blocking::WorkerGuard; -use tracing_subscriber::filter::EnvFilter; -use tracing_subscriber::util::SubscriberInitExt; -use tracing_subscriber::FmtSubscriber; - -use malachitebft_config::{LogFormat, LogLevel}; - -/// Initialize logging. -/// -/// Returns a drop guard responsible for flushing any remaining logs when the program terminates. -/// The guard must be assigned to a binding that is not _, as _ will result in the guard being dropped immediately. -pub fn init(log_level: LogLevel, log_format: LogFormat) -> WorkerGuard { - let filter = build_tracing_filter(log_level); - - let (non_blocking, guard) = tracing_appender::non_blocking(std::io::stdout()); - - // Construct a tracing subscriber with the supplied filter and enable reloading. - let builder = FmtSubscriber::builder() - .with_target(false) - .with_env_filter(filter) - .with_writer(non_blocking) - .with_ansi(enable_ansi()) - .with_thread_ids(false); - - // There must be a better way to use conditionals in the builder pattern. - match log_format { - LogFormat::Plaintext => { - let subscriber = builder.finish(); - subscriber.init(); - } - LogFormat::Json => { - let subscriber = builder.json().finish(); - subscriber.init(); - } - }; - - guard -} - -/// Check if both stdout and stderr are proper terminal (tty), -/// so that we know whether or not to enable colored output, -/// using ANSI escape codes. If either is not, eg. because -/// stdout is redirected to a file, we don't enable colored output. -pub fn enable_ansi() -> bool { - use std::io::IsTerminal; - std::io::stdout().is_terminal() && std::io::stderr().is_terminal() -} - -/// Common prefixes of the crates targeted by the default log level. -const TARGET_CRATES: &[&str] = &["arc", "circle"]; - -/// Build a tracing directive setting the log level for the -/// crates to the given `log_level`. -pub fn default_directive(log_level: LogLevel) -> String { - use itertools::Itertools; - - TARGET_CRATES - .iter() - .map(|&c| format!("{c}={log_level}")) - .join(",") -} - -/// Builds a tracing filter based on the input `log_level`. -/// If the `RUST_LOG` environment variable is set, it takes precedence. -/// Returns error if the filter failed to build. -fn build_tracing_filter(log_level: LogLevel) -> EnvFilter { - EnvFilter::try_from_default_env().unwrap_or_else(|_| { - EnvFilter::builder() - .with_default_directive(LevelFilter::INFO.into()) - .parse(default_directive(log_level)) - .expect("valid tracing filter directive") - }) -} diff --git a/crates/malachite-cli/src/metrics.rs b/crates/malachite-cli/src/metrics.rs deleted file mode 100644 index a772fe9..0000000 --- a/crates/malachite-cli/src/metrics.rs +++ /dev/null @@ -1,52 +0,0 @@ -// Copyright 2025 Circle Internet Group, Inc. All rights reserved. -// -// SPDX-License-Identifier: Apache-2.0 -// -// Licensed under the Apache License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. -// You may obtain a copy of the License at -// -// http://www.apache.org/licenses/LICENSE-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// See the License for the specific language governing permissions and -// limitations under the License. - -use std::io; - -use axum::response::IntoResponse; -use axum::routing::get; -use axum::Router; -use tokio::net::{TcpListener, ToSocketAddrs}; -use tracing::{error, info}; - -use malachitebft_app::metrics::export; - -const CONTENT_TYPE: &str = "application/openmetrics-text; version=1.0.0; charset=utf-8"; - -#[tracing::instrument(name = "metrics", skip_all)] -pub async fn serve(listen_addr: impl ToSocketAddrs) { - if let Err(e) = inner(listen_addr).await { - error!("Metrics server failed: {e}"); - } -} - -async fn inner(listen_addr: impl ToSocketAddrs) -> io::Result<()> { - let app = Router::new().route("/metrics", get(get_metrics)); - let listener = TcpListener::bind(listen_addr).await?; - let local_addr = listener.local_addr()?; - - info!(address = %local_addr, "Serving metrics"); - axum::serve(listener, app).await?; - - Ok(()) -} - -async fn get_metrics() -> impl IntoResponse { - let mut buf = String::new(); - export(&mut buf); - - ([("Content-Type", CONTENT_TYPE)], buf) -} diff --git a/crates/malachite-cli/src/new.rs b/crates/malachite-cli/src/new.rs deleted file mode 100644 index 68e88b6..0000000 --- a/crates/malachite-cli/src/new.rs +++ /dev/null @@ -1,117 +0,0 @@ -// Copyright 2025 Circle Internet Group, Inc. All rights reserved. -// -// SPDX-License-Identifier: Apache-2.0 -// -// Licensed under the Apache License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. -// You may obtain a copy of the License at -// -// http://www.apache.org/licenses/LICENSE-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// See the License for the specific language governing permissions and -// limitations under the License. - -//! key and configuration generation - -use std::str::FromStr; - -use rand::rngs::OsRng; -use tracing::debug; - -use bip32::{ChildNumber, DerivationPath, XPrv}; -use bip39::Mnemonic; - -use arc_consensus_types::signing::PrivateKey; - -use crate::error::Error; - -/// Fixed, non-secret test mnemonic for reproducible local testnets. -/// These are made to match reth test genesis generation in `assets/localdev/genesis.config.ts`. -/// DO NOT USE THESE SEEDS IN PRODUCTION! -const TEST_MNEMONIC: &str = "test test test test test test test test test test test junk"; -const TEST_PASSPHRASE: &str = ""; -const TEST_BIP39_DERIVATION_PATH_PREFIX: &str = "m/44'/60'/0'/1"; - -/// Derive `count` child private keys at m/44'/60'/0'/1/{start..}. -fn derive_bip39_child_sk_bytes(start_index: usize, count: usize) -> Result, Error> { - let m = Mnemonic::parse(TEST_MNEMONIC).map_err(|e| Error::DeriveBip39(e.to_string()))?; - let seed = Mnemonic::to_seed(&m, TEST_PASSPHRASE); - - let base_path = DerivationPath::from_str(TEST_BIP39_DERIVATION_PATH_PREFIX) - .map_err(|e| Error::DeriveBip39(e.to_string()))?; - let base_xprv = - XPrv::derive_from_path(seed, &base_path).map_err(|e| Error::DeriveBip39(e.to_string()))?; - - let mut out = Vec::with_capacity(count); - for i in 0..count { - // start_index and count are small test constants; cannot overflow - #[allow(clippy::arithmetic_side_effects)] - let idx = start_index + i; - // idx is bounded by start_index + count, both small; fits in u32 - #[allow(clippy::cast_possible_truncation)] - let cn = - ChildNumber::new(idx as u32, false).map_err(|e| Error::DeriveBip39(e.to_string()))?; - let child = base_xprv - .derive_child(cn) - .map_err(|e| Error::DeriveBip39(e.to_string()))?; - let ga = child.private_key().to_bytes(); - let mut sk = [0u8; 32]; - sk.copy_from_slice(&ga); - - let display_path = format!("{TEST_BIP39_DERIVATION_PATH_PREFIX}/{idx}"); - debug!(path = %display_path, seed = %format!("0x{}", hex::encode(sk)), "derived child seed"); - - out.push(sk); - } - - Ok(out) -} - -/// Generate private keys. Supports: -/// - random (default). -/// - deterministic via BIP-39/BIP-32 derived child private keys. -pub fn generate_private_keys(size: usize, deterministic: bool) -> Result, Error> { - if deterministic { - // `start_index` matches `assets/localdev/genesis.config.ts`. - let start_index = 2; - let seeds = derive_bip39_child_sk_bytes(start_index, size)?; - let keys = seeds.into_iter().map(Into::into).collect(); - Ok(keys) - } else { - Ok((0..size).map(|_| PrivateKey::generate(OsRng)).collect()) - } -} - -#[cfg(test)] -mod tests { - use super::*; - - #[test] - fn derives_child_keys_deterministic() { - // first three indices starting at 2 - let seeds = derive_bip39_child_sk_bytes(2, 3).unwrap(); - assert_eq!(seeds.len(), 3); - - // Re-run and ensure determinism - let seeds2 = derive_bip39_child_sk_bytes(2, 3).unwrap(); - assert_eq!(seeds, seeds2); - } - - #[test] - fn derives_child_keys_well_known() { - let expected_seeds = vec![ - "93ac6a66e7b27d3b21eb05c3edf07d3380019460b761ee117cdca9d3215e1b2d", - "1a864302982c12335b26a63fd7b841c6491e58530fc2f25c23a4191a7ea31c90", - "a8bf2e57dbee36fcc50f072cd71ee0f885e8b36cad256c927048fd5474b6ad56", - ]; - - let seeds = derive_bip39_child_sk_bytes(2, 3).unwrap(); - - for (i, (seed, expected)) in seeds.iter().zip(expected_seeds).enumerate() { - assert_eq!(hex::encode(seed), expected, "index {i}"); - } - } -} diff --git a/crates/malachite-cli/src/runtime.rs b/crates/malachite-cli/src/runtime.rs deleted file mode 100644 index 988a9be..0000000 --- a/crates/malachite-cli/src/runtime.rs +++ /dev/null @@ -1,36 +0,0 @@ -// Copyright 2025 Circle Internet Group, Inc. All rights reserved. -// -// SPDX-License-Identifier: Apache-2.0 -// -// Licensed under the Apache License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. -// You may obtain a copy of the License at -// -// http://www.apache.org/licenses/LICENSE-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// See the License for the specific language governing permissions and -// limitations under the License. - -//! Multithreaded runtime builder. - -use malachitebft_config::RuntimeConfig; -use std::io::Result; -use tokio::runtime::{Builder as RtBuilder, Runtime}; - -pub fn build_runtime(cfg: RuntimeConfig) -> Result { - let mut builder = match cfg { - RuntimeConfig::SingleThreaded => RtBuilder::new_current_thread(), - RuntimeConfig::MultiThreaded { worker_threads } => { - let mut builder = RtBuilder::new_multi_thread(); - if worker_threads > 0 { - builder.worker_threads(worker_threads); - } - builder - } - }; - - builder.enable_all().build() -} diff --git a/crates/mesh-analysis/Cargo.toml b/crates/mesh-analysis/Cargo.toml deleted file mode 100644 index d3660af..0000000 --- a/crates/mesh-analysis/Cargo.toml +++ /dev/null @@ -1,24 +0,0 @@ -[package] -name = "arc-mesh-analysis" -description = "Gossipsub mesh analysis for Malachite BFT networks" -version.workspace = true -edition.workspace = true -repository.workspace = true -license.workspace = true -publish.workspace = true - -[[bin]] -name = "arc-mesh-analysis" -path = "src/main.rs" - -[dependencies] -clap = { workspace = true, features = ["derive"] } -color-eyre = { workspace = true } -futures-util = { workspace = true } -prometheus-parse = { workspace = true } -reqwest = { workspace = true } -tokio = { workspace = true, features = ["full"] } -url = { workspace = true } - -[lints] -workspace = true diff --git a/crates/mesh-analysis/src/analyze.rs b/crates/mesh-analysis/src/analyze.rs deleted file mode 100644 index 39f6e41..0000000 --- a/crates/mesh-analysis/src/analyze.rs +++ /dev/null @@ -1,315 +0,0 @@ -// Copyright 2026 Circle Internet Group, Inc. All rights reserved. -// -// SPDX-License-Identifier: Apache-2.0 -// -// Licensed under the Apache License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. -// You may obtain a copy of the License at -// -// http://www.apache.org/licenses/LICENSE-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// See the License for the specific language governing permissions and -// limitations under the License. - -use std::collections::{BTreeSet, HashMap, HashSet, VecDeque}; - -use super::types::{ - MeshAnalysis, NodeMetricsData, NodeType, TopicAnalysis, ValidatorConnectivity, TOPICS, -}; - -fn find_partitions( - graph: &HashMap>, - all_nodes: &[String], -) -> Vec> { - let node_set: HashSet<&String> = all_nodes.iter().collect(); - let mut visited = HashSet::new(); - let mut partitions = Vec::new(); - - for node in all_nodes { - if visited.contains(node) { - continue; - } - let mut partition = BTreeSet::new(); - let mut queue = VecDeque::new(); - queue.push_back(node.clone()); - - while let Some(current) = queue.pop_front() { - if !visited.insert(current.clone()) { - continue; - } - partition.insert(current.clone()); - if let Some(neighbors) = graph.get(¤t) { - for neighbor in neighbors { - if !visited.contains(neighbor) && node_set.contains(neighbor) { - queue.push_back(neighbor.clone()); - } - } - } - } - partitions.push(partition); - } - partitions -} - -fn find_shortest_path(graph: &HashMap>, start: &str, end: &str) -> Vec { - if start == end { - return vec![start.to_string()]; - } - let mut visited = HashSet::new(); - let mut queue: VecDeque<(String, Vec)> = VecDeque::new(); - queue.push_back((start.to_string(), vec![start.to_string()])); - - while let Some((current, path)) = queue.pop_front() { - if !visited.insert(current.clone()) { - continue; - } - if let Some(neighbors) = graph.get(¤t) { - for neighbor in neighbors { - if neighbor == end { - let mut full = path.clone(); - full.push(neighbor.clone()); - return full; - } - if !visited.contains(neighbor) { - let mut new_path = path.clone(); - new_path.push(neighbor.clone()); - queue.push_back((neighbor.clone(), new_path)); - } - } - } - } - vec![] // no path -} - -pub fn analyze(nodes: &[NodeMetricsData]) -> MeshAnalysis { - let node_count = nodes.len(); - let validator_count = nodes - .iter() - .filter(|n| n.node_type == NodeType::Validator) - .count(); - let persistent_peer_count = nodes - .iter() - .filter(|n| n.node_type == NodeType::PersistentPeer) - .count(); - let full_node_count = nodes - .iter() - .filter(|n| n.node_type == NodeType::FullNode) - .count(); - - let mut topic_analyses = Vec::new(); - let mut validator_connectivity = Vec::new(); - - for &topic in &TOPICS { - // Topic partition analysis - let mut graph: HashMap> = HashMap::new(); - let mut meshed_nodes = Vec::new(); - let mut isolated_nodes = Vec::new(); - - for node in nodes { - let count = node.mesh_counts.get(topic).copied().unwrap_or(0); - if count > 0 { - let peers = node.mesh_peers.get(topic).cloned().unwrap_or_default(); - graph.insert(node.moniker.clone(), peers); - meshed_nodes.push(node.moniker.clone()); - } else { - isolated_nodes.push(node.moniker.clone()); - } - } - - let partitions = if meshed_nodes.is_empty() { - vec![] - } else { - find_partitions(&graph, &meshed_nodes) - }; - - topic_analyses.push(TopicAnalysis { - topic_name: topic.to_string(), - meshed_count: meshed_nodes.len(), - isolated_count: isolated_nodes.len(), - isolated_nodes, - partitions, - }); - - // Validator connectivity analysis - let all_validators: BTreeSet = nodes - .iter() - .filter(|n| n.node_type == NodeType::Validator) - .map(|n| n.moniker.clone()) - .collect(); - - if all_validators.is_empty() { - continue; - } - - // Build complete mesh graph (all nodes) and validator-only graph - let mut complete_mesh: HashMap> = HashMap::new(); - let mut validator_mesh: HashMap> = HashMap::new(); - - for node in nodes { - let peers = node.mesh_peers.get(topic).cloned().unwrap_or_default(); - complete_mesh.insert(node.moniker.clone(), peers.clone()); - - if all_validators.contains(&node.moniker) { - let val_peers: Vec = peers - .iter() - .filter(|p| all_validators.contains(*p)) - .cloned() - .collect(); - validator_mesh.insert(node.moniker.clone(), val_peers); - } - } - - // Find actual mesh partitions using complete graph - let all_meshed: Vec = complete_mesh.keys().cloned().collect(); - let all_mesh_partitions = find_partitions(&complete_mesh, &all_meshed); - - let actual_partitions: Vec> = all_mesh_partitions - .iter() - .filter_map(|partition| { - let vals: BTreeSet = partition - .iter() - .filter(|v| all_validators.contains(*v)) - .cloned() - .collect(); - if vals.is_empty() { - None - } else { - Some(vals) - } - }) - .collect(); - - // Direct val-to-val connections: divide by 2 because gossipsub meshes are - // bidirectional — if A lists B as a mesh peer, B also lists A. If metrics - // scraping is asymmetric (e.g. stale data on one node), the sum may be odd - // and integer division will slightly undercount. - let direct_val_connections: usize = - validator_mesh.values().map(|p| p.len()).sum::() / 2; - - // Diameter per partition - let mut partition_diameters = Vec::new(); - for partition in &actual_partitions { - if partition.len() <= 1 { - partition_diameters.push(None); - continue; - } - let mut max_hops = 0usize; - let vals: Vec<&String> = partition.iter().collect(); - for (i, v1) in vals.iter().enumerate() { - for v2 in &vals[i + 1..] { - let path = find_shortest_path(&complete_mesh, v1, v2); - if !path.is_empty() { - max_hops = max_hops.max(path.len() - 1); - } - } - } - partition_diameters.push(if max_hops > 0 { Some(max_hops) } else { None }); - } - - let max_diameter = partition_diameters - .iter() - .filter_map(|d| *d) - .max() - .unwrap_or(0); - - // Completely isolated validators (zero mesh peers) - let mut completely_isolated = Vec::new(); - let mut isolated_with_explicit = Vec::new(); - for v in all_validators.iter() { - let peer_count = complete_mesh.get(v).map(|p| p.len()).unwrap_or(0); - if peer_count == 0 { - let node = nodes.iter().find(|n| &n.moniker == v); - if let Some(node) = node { - if node.explicit_peers.is_empty() { - completely_isolated.push(v.clone()); - } else { - isolated_with_explicit.push((v.clone(), node.explicit_peers.clone())); - } - } else { - completely_isolated.push(v.clone()); - } - } - } - - // Validators with no direct validator mesh peers but with some mesh peers - let validators_without_val_peers: Vec = all_validators - .iter() - .filter(|v| { - validator_mesh.get(*v).map(|p| p.is_empty()).unwrap_or(true) - && complete_mesh - .get(*v) - .map(|p| !p.is_empty()) - .unwrap_or(false) - }) - .cloned() - .collect(); - - // Indirect paths between validators (through full nodes), computed - // within each partition so the data is available even when the mesh is - // partitioned. - let mut indirect_paths = Vec::new(); - for partition in &actual_partitions { - let vals: Vec<&String> = partition.iter().collect(); - for (i, v1) in vals.iter().enumerate() { - for v2 in &vals[i + 1..] { - if validator_mesh - .get(*v1) - .map(|p| p.contains(*v2)) - .unwrap_or(false) - { - continue; - } - let path = find_shortest_path(&complete_mesh, v1, v2); - if path.len() > 2 { - let intermediate: Vec = path[1..path.len() - 1].to_vec(); - let hops = path.len() - 1; - indirect_paths.push(((*v1).clone(), (*v2).clone(), intermediate, hops)); - } - } - } - } - indirect_paths.sort_by(|a, b| a.3.cmp(&b.3).then(a.0.cmp(&b.0)).then(a.1.cmp(&b.1))); - - validator_connectivity.push(ValidatorConnectivity { - topic_name: topic.to_string(), - all_validators: all_validators.clone(), - actual_partitions, - direct_val_connections, - max_diameter, - partition_diameters, - completely_isolated, - isolated_with_explicit, - validators_without_val_peers, - indirect_paths, - }); - } - - // Zero-mesh warnings - let zero_mesh_warnings: Vec<(String, i64, i64, i64)> = nodes - .iter() - .filter_map(|n| { - let c = n.mesh_counts.get("/consensus").copied().unwrap_or(0); - let p = n.mesh_counts.get("/proposal_parts").copied().unwrap_or(0); - let l = n.mesh_counts.get("/liveness").copied().unwrap_or(0); - if c == 0 || p == 0 || l == 0 { - Some((n.moniker.clone(), c, p, l)) - } else { - None - } - }) - .collect(); - - MeshAnalysis { - node_count, - validator_count, - persistent_peer_count, - full_node_count, - nodes: nodes.to_vec(), - topic_analyses, - validator_connectivity, - zero_mesh_warnings, - } -} diff --git a/crates/mesh-analysis/src/fetch.rs b/crates/mesh-analysis/src/fetch.rs deleted file mode 100644 index 3c4154a..0000000 --- a/crates/mesh-analysis/src/fetch.rs +++ /dev/null @@ -1,55 +0,0 @@ -// Copyright 2026 Circle Internet Group, Inc. All rights reserved. -// -// SPDX-License-Identifier: Apache-2.0 -// -// Licensed under the Apache License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. -// You may obtain a copy of the License at -// -// http://www.apache.org/licenses/LICENSE-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// See the License for the specific language governing permissions and -// limitations under the License. - -use std::time::Duration; - -use futures_util::stream::{self, StreamExt}; -use url::Url; - -/// Max concurrent metrics fetches to avoid saturating SSM tunnels on large -/// remote testnets (80+ nodes × ~170 KB each through a single SSH session). -const MAX_CONCURRENT_FETCHES: usize = 10; - -pub async fn fetch_all_metrics(metrics_urls: &[(String, Url)]) -> Vec<(String, String)> { - let mut sorted_urls: Vec<_> = metrics_urls.to_vec(); - sorted_urls.sort_by(|(a, _), (b, _)| a.cmp(b)); - - let client = reqwest::Client::builder() - .timeout(Duration::from_secs(15)) - .build() - .unwrap_or_default(); - - let futures: Vec<_> = sorted_urls - .iter() - .map(|(name, url)| { - let client = client.clone(); - let name = name.clone(); - let url = url.clone(); - async move { - let body = match client.get(url.as_str()).send().await { - Ok(resp) => resp.text().await.unwrap_or_default(), - Err(_) => String::new(), - }; - (name, body) - } - }) - .collect(); - - stream::iter(futures) - .buffer_unordered(MAX_CONCURRENT_FETCHES) - .collect() - .await -} diff --git a/crates/mesh-analysis/src/lib.rs b/crates/mesh-analysis/src/lib.rs deleted file mode 100644 index 7d2d83e..0000000 --- a/crates/mesh-analysis/src/lib.rs +++ /dev/null @@ -1,35 +0,0 @@ -// Copyright 2026 Circle Internet Group, Inc. All rights reserved. -// -// SPDX-License-Identifier: Apache-2.0 -// -// Licensed under the Apache License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. -// You may obtain a copy of the License at -// -// http://www.apache.org/licenses/LICENSE-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// See the License for the specific language governing permissions and -// limitations under the License. - -// Offline diagnostic CLI tool — not part of the node runtime. -#![allow(clippy::arithmetic_side_effects, clippy::cast_possible_truncation)] - -mod analyze; -mod fetch; -mod parse; -mod report; -mod tier; -mod types; - -pub use analyze::analyze; -pub use fetch::fetch_all_metrics; -pub use parse::parse_all_metrics; -pub use report::format_report; -pub use tier::{classify_all, MeshTier}; -pub use types::{ - DiscoveredPeer, MeshAnalysis, MeshDisplayOptions, MessageCounts, NodeMetricsData, NodeType, - TopicAnalysis, ValidatorConnectivity, -}; diff --git a/crates/mesh-analysis/src/main.rs b/crates/mesh-analysis/src/main.rs deleted file mode 100644 index 56ff30b..0000000 --- a/crates/mesh-analysis/src/main.rs +++ /dev/null @@ -1,164 +0,0 @@ -// Copyright 2026 Circle Internet Group, Inc. All rights reserved. -// -// SPDX-License-Identifier: Apache-2.0 -// -// Licensed under the Apache License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. -// You may obtain a copy of the License at -// -// http://www.apache.org/licenses/LICENSE-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// See the License for the specific language governing permissions and -// limitations under the License. - -// Offline diagnostic CLI tool — not part of the node runtime. -#![allow(clippy::arithmetic_side_effects, clippy::cast_possible_truncation)] - -use std::io::{self, BufRead}; - -use clap::Parser; -use color_eyre::eyre::{bail, Result}; -use url::Url; - -use arc_mesh_analysis::{ - analyze, classify_all, fetch_all_metrics, format_report, parse_all_metrics, MeshDisplayOptions, - MeshTier, -}; - -#[derive(Parser)] -#[command( - name = "arc-mesh-analysis", - about = "Gossipsub mesh analysis for Malachite BFT networks" -)] -struct Cli { - /// Metrics URLs (e.g. http://host:26660/metrics) - urls: Vec, - - /// Read URLs from a file (one per line, supports `name=url` format and `#` comments). - /// Use `-` for stdin. - #[arg(short = 'f', long = "file")] - file: Option, - - /// Show only mesh topology (skip status table) - #[arg(long)] - mesh_only: bool, - - /// Show per-node peer detail - #[arg(long)] - peers: bool, - - /// Include peer types and scores in peer detail - #[arg(long)] - peers_full: bool, - - /// Show duplicate message rates - #[arg(long)] - duplicates: bool, - - /// Exit non-zero if any node is classified at this tier. - /// Can be repeated: `--fail not-connected --fail multi-hop`. - /// Valid tiers: fully-connected, multi-hop, not-connected. - #[arg(long = "fail", value_name = "TIER")] - fail_tiers: Vec, -} - -fn parse_urls_from_lines(lines: impl Iterator) -> Result> { - let mut urls = Vec::new(); - for line in lines { - let line = line.trim().to_string(); - if line.is_empty() || line.starts_with('#') { - continue; - } - if let Some((name, raw_url)) = line.split_once('=') { - let url = Url::parse(raw_url.trim())?; - urls.push((name.trim().to_string(), url)); - } else { - let url = Url::parse(&line)?; - // Derive a name from the host - let name = url - .host_str() - .map(|h| h.to_string()) - .unwrap_or_else(|| format!("node-{}", urls.len())); - urls.push((name, url)); - } - } - Ok(urls) -} - -#[tokio::main] -async fn main() -> Result<()> { - color_eyre::install()?; - - let cli = Cli::parse(); - - let metrics_urls = if let Some(file_path) = &cli.file { - if file_path == "-" { - let stdin = io::stdin(); - let lines = stdin.lock().lines().map_while(Result::ok); - parse_urls_from_lines(lines)? - } else { - let content = std::fs::read_to_string(file_path)?; - parse_urls_from_lines(content.lines().map(String::from))? - } - } else if !cli.urls.is_empty() { - let mut urls = Vec::new(); - for raw in &cli.urls { - let url = Url::parse(raw)?; - let name = url - .host_str() - .map(|h| h.to_string()) - .unwrap_or_else(|| format!("node-{}", urls.len())); - urls.push((name, url)); - } - urls - } else { - bail!("No metrics URLs provided. Pass URLs as arguments or use -f ."); - }; - - if metrics_urls.is_empty() { - bail!("No metrics URLs provided."); - } - - let raw_metrics = fetch_all_metrics(&metrics_urls).await; - let nodes_data = parse_all_metrics(&raw_metrics); - let analysis = analyze(&nodes_data); - - let options = MeshDisplayOptions { - show_counts: !cli.mesh_only, - show_mesh: true, - show_peers: cli.peers || cli.peers_full, - show_peers_full: cli.peers_full, - show_duplicates: cli.duplicates, - }; - - print!("{}", format_report(&analysis, &options)); - - if !cli.fail_tiers.is_empty() { - let classifications = classify_all(&analysis); - - println!("\n--- Tier Classification ---"); - for (moniker, node_type, tier) in &classifications { - println!(" {moniker} ({node_type}): {tier}"); - } - - let failing: Vec<_> = classifications - .iter() - .filter(|(_, _, tier)| cli.fail_tiers.contains(tier)) - .collect(); - - if !failing.is_empty() { - println!(); - for (moniker, node_type, tier) in &failing { - println!("FAIL: {moniker} ({node_type}) is {tier}"); - } - bail!("{} node(s) in failing tier(s)", failing.len()); - } - - println!("\nAll nodes pass tier checks."); - } - - Ok(()) -} diff --git a/crates/mesh-analysis/src/parse.rs b/crates/mesh-analysis/src/parse.rs deleted file mode 100644 index 973521d..0000000 --- a/crates/mesh-analysis/src/parse.rs +++ /dev/null @@ -1,305 +0,0 @@ -// Copyright 2026 Circle Internet Group, Inc. All rights reserved. -// -// SPDX-License-Identifier: Apache-2.0 -// -// Licensed under the Apache License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. -// You may obtain a copy of the License at -// -// http://www.apache.org/licenses/LICENSE-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// See the License for the specific language governing permissions and -// limitations under the License. - -use std::collections::{BTreeMap, HashSet}; - -use prometheus_parse::{Sample, Scrape, Value}; - -use super::types::{DiscoveredPeer, MessageCounts, NodeMetricsData, NodeType, TOPICS}; - -/// Metric name prefixes we actually use. Lines not matching any of these -/// (and not starting with `#`) are dropped before parsing, which avoids -/// allocating `Sample` objects for the hundreds of metrics we don't need. -const METRIC_PREFIXES: &[&str] = &[ - "malachitebft_network_gossipsub_mesh_peer_counts", - "malachitebft_network_gossipsub_topic_msg_recv_counts", - "malachitebft_network_peer_mesh_membership", - "malachitebft_network_explicit_peers", - "malachitebft_network_discovered_peers", - "malachitebft_core_consensus_connected_peers", - "malachitebft_discovery_num_inbound_peers", - "malachitebft_discovery_num_outbound_peers", - "malachitebft_discovery_num_active_connections", - "malachitebft_discovery_num_inbound_connections", - "malachitebft_discovery_num_outbound_connections", -]; - -/// Parse raw Prometheus text, keeping only the metrics we care about. -fn parse_metrics(raw: &str) -> Vec { - let filtered: String = raw - .lines() - .filter(|line| { - // Keep comment lines (# TYPE, # HELP) — needed for type inference - if line.starts_with('#') { - return line - .split_whitespace() - .nth(2) - .is_some_and(|name| METRIC_PREFIXES.iter().any(|p| name.starts_with(p))); - } - METRIC_PREFIXES.iter().any(|p| line.starts_with(p)) - }) - .collect::>() - .join("\n"); - - let lines = filtered.lines().map(|l| Ok(l.to_owned())); - Scrape::parse(lines) - .map(|scrape| scrape.samples) - .unwrap_or_default() -} - -/// Extract the `moniker` label from the first sample that has one. -fn extract_moniker(samples: &[Sample]) -> String { - for s in samples { - if let Some(m) = s.labels.get("moniker") { - return m.to_string(); - } - } - "unknown".to_string() -} - -/// Extract f64 from a gauge/counter/untyped value. -fn value_as_f64(v: &Value) -> Option { - match v { - Value::Gauge(f) | Value::Counter(f) | Value::Untyped(f) => Some(*f), - _ => None, - } -} - -/// Find the first sample matching `metric` and return its value as i64. -fn extract_gauge_value(samples: &[Sample], metric: &str) -> i64 { - samples - .iter() - .find(|s| s.metric == metric) - .and_then(|s| value_as_f64(&s.value)) - .map(|f| f as i64) - .unwrap_or(0) -} - -/// Find the first sample matching `metric` with a specific label value, -/// and return its value as i64. -fn extract_gauge_value_with_label( - samples: &[Sample], - metric: &str, - label_key: &str, - label_value: &str, -) -> i64 { - samples - .iter() - .find(|s| s.metric == metric && s.labels.get(label_key).is_some_and(|v| v == label_value)) - .and_then(|s| value_as_f64(&s.value)) - .map(|f| f as i64) - .unwrap_or(0) -} - -/// Collect `peer_moniker` labels from `malachitebft_network_peer_mesh_membership` -/// samples where the `topic` label matches and the gauge value is 1.0. -fn extract_peer_monikers(samples: &[Sample], topic: &str) -> Vec { - let mut peers = HashSet::new(); - for s in samples { - if s.metric == "malachitebft_network_peer_mesh_membership" - && s.labels.get("topic").is_some_and(|t| t == topic) - && value_as_f64(&s.value) == Some(1.0) - { - if let Some(m) = s.labels.get("peer_moniker") { - peers.insert(m.to_string()); - } - } - } - peers.into_iter().collect() -} - -/// Collect sorted `peer_moniker` labels from `malachitebft_network_explicit_peers` -/// samples where the gauge value is 1.0. -fn extract_explicit_peers(samples: &[Sample]) -> Vec { - let mut peers = HashSet::new(); - for s in samples { - if s.metric == "malachitebft_network_explicit_peers" && value_as_f64(&s.value) == Some(1.0) - { - if let Some(m) = s.labels.get("peer_moniker") { - peers.insert(m.to_string()); - } - } - } - let mut v: Vec<_> = peers.into_iter().collect(); - v.sort(); - v -} - -/// Sum message counts across all topics for duplicate analysis. -fn extract_message_counts(samples: &[Sample]) -> MessageCounts { - let mut unfiltered = 0u64; - let mut filtered = 0u64; - - for sample in samples { - let value = value_as_f64(&sample.value).unwrap_or(0.0) as u64; - if sample.metric == "malachitebft_network_gossipsub_topic_msg_recv_counts_unfiltered_total" - { - unfiltered += value; - } else if sample.metric == "malachitebft_network_gossipsub_topic_msg_recv_counts_total" { - filtered += value; - } - } - - MessageCounts { - unfiltered, - filtered, - } -} - -/// Extract per-peer detail from `malachitebft_network_discovered_peers` for this node. -fn extract_discovered_peers(samples: &[Sample]) -> BTreeMap { - let mut peers = BTreeMap::new(); - for s in samples { - if s.metric != "malachitebft_network_discovered_peers" { - continue; - } - let score = value_as_f64(&s.value).unwrap_or(0.0); - if score < -1_000_000_000.0 { - continue; // stale entry - } - let Some(peer_moniker) = s.labels.get("peer_moniker") else { - continue; - }; - let peer_type = s - .labels - .get("peer_type") - .map(|s| s.to_string()) - .unwrap_or_default(); - peers.insert( - peer_moniker.to_string(), - DiscoveredPeer { - peer_moniker: peer_moniker.to_string(), - peer_type, - score, - }, - ); - } - peers -} - -/// Determine the node type by inspecting `malachitebft_network_discovered_peers` -/// across all other nodes' metrics. -fn determine_node_type( - target_moniker: &str, - all_parsed: &[(String, Vec)], // (moniker, samples) -) -> NodeType { - let mut found = NodeType::FullNode; - - for (moniker, samples) in all_parsed { - if moniker == target_moniker { - continue; - } - for s in samples { - if s.metric != "malachitebft_network_discovered_peers" { - continue; - } - if s.labels - .get("peer_moniker") - .is_none_or(|m| m != target_moniker) - { - continue; - } - let score = value_as_f64(&s.value).unwrap_or(0.0); - if score < -1_000_000_000.0 { - continue; // stale entry - } - if s.labels.get("peer_type").is_some_and(|t| t == "validator") { - return NodeType::Validator; - } - if s.labels - .get("peer_type") - .is_some_and(|t| t == "persistent_peer") - { - found = NodeType::PersistentPeer; - } - } - } - found -} - -pub fn parse_all_metrics(raw_metrics: &[(String, String)]) -> Vec { - // First pass: parse each node's raw text into (moniker, Vec) - let parsed: Vec<(String, Vec)> = raw_metrics - .iter() - .filter(|(_, m)| !m.is_empty()) - .map(|(_, m)| { - let samples = parse_metrics(m); - let moniker = extract_moniker(&samples); - (moniker, samples) - }) - .collect(); - - let running_monikers: HashSet = parsed.iter().map(|(m, _)| m.clone()).collect(); - - // Second pass: build full data with cross-node type resolution - parsed - .iter() - .map(|(moniker, samples)| { - let node_type = determine_node_type(moniker, &parsed); - - let mut mesh_counts = BTreeMap::new(); - let mut mesh_peers = BTreeMap::new(); - for &topic in &TOPICS { - let count = extract_gauge_value_with_label( - samples, - "malachitebft_network_gossipsub_mesh_peer_counts", - "hash", - topic, - ); - mesh_counts.insert(topic.to_string(), count); - - let peers: Vec = extract_peer_monikers(samples, topic) - .into_iter() - .filter(|p| running_monikers.contains(p)) - .collect(); - mesh_peers.insert(topic.to_string(), peers); - } - - let explicit_peers = extract_explicit_peers(samples); - let discovered_peers = extract_discovered_peers(samples); - let message_counts = extract_message_counts(samples); - - let connected_peers = - extract_gauge_value(samples, "malachitebft_core_consensus_connected_peers"); - let inbound_peers = - extract_gauge_value(samples, "malachitebft_discovery_num_inbound_peers"); - let outbound_peers = - extract_gauge_value(samples, "malachitebft_discovery_num_outbound_peers"); - let active_connections = - extract_gauge_value(samples, "malachitebft_discovery_num_active_connections"); - let inbound_connections = - extract_gauge_value(samples, "malachitebft_discovery_num_inbound_connections"); - let outbound_connections = - extract_gauge_value(samples, "malachitebft_discovery_num_outbound_connections"); - - NodeMetricsData { - moniker: moniker.clone(), - node_type, - mesh_counts, - mesh_peers, - explicit_peers, - discovered_peers, - message_counts, - connected_peers, - inbound_peers, - outbound_peers, - active_connections, - inbound_connections, - outbound_connections, - } - }) - .collect() -} diff --git a/crates/mesh-analysis/src/report.rs b/crates/mesh-analysis/src/report.rs deleted file mode 100644 index d0bbf85..0000000 --- a/crates/mesh-analysis/src/report.rs +++ /dev/null @@ -1,641 +0,0 @@ -// Copyright 2026 Circle Internet Group, Inc. All rights reserved. -// -// SPDX-License-Identifier: Apache-2.0 -// -// Licensed under the Apache License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. -// You may obtain a copy of the License at -// -// http://www.apache.org/licenses/LICENSE-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// See the License for the specific language governing permissions and -// limitations under the License. - -use std::fmt::Write; - -use super::types::{ - MeshAnalysis, MeshDisplayOptions, NodeMetricsData, NodeType, TopicAnalysis, - ValidatorConnectivity, TOPICS, -}; - -const COL_TYPE: usize = 12; -const COL_SMALL: usize = 5; -const COL_PEERS: usize = 6; -const COL_IN_PEERS: usize = 8; -const COL_OUT_PEERS: usize = 9; -const COL_CONNS: usize = 6; -const COL_IN_CONNS: usize = 8; -const COL_OUT_CONNS: usize = 8; - -pub fn format_report(analysis: &MeshAnalysis, options: &MeshDisplayOptions) -> String { - let mut out = String::new(); - - // Network summary - let mut type_parts = Vec::new(); - if analysis.validator_count > 0 { - type_parts.push(format!( - "{} validator{}", - analysis.validator_count, - if analysis.validator_count == 1 { - "" - } else { - "s" - } - )); - } - if analysis.persistent_peer_count > 0 { - type_parts.push(format!( - "{} persistent peer{}", - analysis.persistent_peer_count, - if analysis.persistent_peer_count == 1 { - "" - } else { - "s" - } - )); - } - if analysis.full_node_count > 0 { - type_parts.push(format!( - "{} full node{}", - analysis.full_node_count, - if analysis.full_node_count == 1 { - "" - } else { - "s" - } - )); - } - let network_line = format!( - "Network: {} ({} total)\n", - type_parts.join(", "), - analysis.node_count - ); - - // -- Counts table -------------------------------------------------------- - if options.show_counts { - let name_w = analysis - .nodes - .iter() - .map(|n| n.moniker.len()) - .max() - .unwrap_or(7) - .max(7); // at least "Moniker" - let col_widths = [ - name_w, - COL_TYPE, - COL_SMALL, - COL_SMALL, - COL_SMALL, - COL_SMALL, - COL_PEERS, - COL_IN_PEERS, - COL_OUT_PEERS, - COL_CONNS, - COL_IN_CONNS, - COL_OUT_CONNS, - ]; - let total_w = col_widths.iter().sum::() + col_widths.len() - 1; - - let _ = writeln!(out, "{}", "=".repeat(total_w)); - let _ = writeln!(out, "Status - mesh peers, connected peers, connections"); - let _ = write!(out, "{network_line}"); - let _ = write!(out, "{}\n\n", "=".repeat(total_w)); - let _ = writeln!( - out, - "{: = analysis.nodes.iter().collect(); - sorted_nodes.sort_by(|a, b| { - a.node_type - .cmp(&b.node_type) - .reverse() - .then(a.moniker.cmp(&b.moniker)) - }); - - let mut prev_type: Option = None; - for node in &sorted_nodes { - if let Some(pt) = prev_type { - if pt != node.node_type { - let _ = writeln!(out, "{}", "-".repeat(total_w)); - } - } - prev_type = Some(node.node_type); - - let c = node.mesh_counts.get("/consensus").copied().unwrap_or(0); - let p = node - .mesh_counts - .get("/proposal_parts") - .copied() - .unwrap_or(0); - let l = node.mesh_counts.get("/liveness").copied().unwrap_or(0); - let expl = node.explicit_peers.len(); - - let _ = writeln!( - out, - "{: 0 && ta.partitions.len() == 1 { - let _ = writeln!( - out, - " ⚠️ {}: {} of {total} nodes meshed ({} isolated)", - ta.topic_name, ta.meshed_count, ta.isolated_count - ); - } else if ta.partitions.len() > 1 { - let _ = writeln!( - out, - " ⚠️ {}: partitioned into {} groups ({} total nodes)", - ta.topic_name, - ta.partitions.len(), - ta.meshed_count - ); - } - - for (idx, partition) in ta.partitions.iter().enumerate() { - let nodes: Vec<&String> = partition.iter().collect(); - let _ = writeln!( - out, - " group {}: {} nodes - {}", - idx + 1, - nodes.len(), - nodes - .iter() - .map(|s| s.as_str()) - .collect::>() - .join(", ") - ); - } - - if ta.isolated_count > 0 { - let mut sorted = ta.isolated_nodes.clone(); - sorted.sort(); - let _ = writeln!(out, " isolated: {}", sorted.join(", ")); - } - } -} - -fn format_validator_connectivity(out: &mut String, vc: &ValidatorConnectivity) { - if vc.all_validators.is_empty() { - let _ = writeln!(out, " ℹ️ {}: no validators found in mesh", vc.topic_name); - return; - } - - let num_validators = vc.all_validators.len(); - let num_partitions = vc.actual_partitions.len(); - - if num_partitions > 1 { - let _ = writeln!( - out, - " ⚠️ {}: {num_validators} validators PARTITIONED into {num_partitions} mesh groups (must use IHAVE/IWANT)", - vc.topic_name - ); - let diameter_strs: Vec = vc - .partition_diameters - .iter() - .enumerate() - .map(|(i, d)| { - format!( - "P{}={}", - i + 1, - d.map(|v| format!("{v} hops")).unwrap_or("N/A".to_string()) - ) - }) - .collect(); - let _ = writeln!( - out, - " Network diameter per partition: {}", - diameter_strs.join(", ") - ); - let _ = writeln!( - out, - " Direct validator-to-validator connections: {}", - vc.direct_val_connections - ); - } else { - if vc.max_diameter > 1 { - let _ = writeln!( - out, - " ⚠️ {}: {num_validators} validators in single mesh (eager push works, NOT fully meshed)", - vc.topic_name - ); - } else { - let _ = writeln!( - out, - " ✅ {}: {num_validators} validators in single mesh (eager push works, fully meshed)", - vc.topic_name - ); - } - let _ = writeln!( - out, - " Network diameter: {} hops (max distance between any two validators)", - vc.max_diameter - ); - let _ = writeln!( - out, - " Direct validator-to-validator mesh connections: {}", - vc.direct_val_connections - ); - } - - // Completely isolated validators - if !vc.completely_isolated.is_empty() { - let _ = writeln!( - out, - " 🚨 CRITICAL: Validators with ZERO mesh peers (NOT receiving eager push):" - ); - for v in &vc.completely_isolated { - let _ = writeln!(out, " {v}"); - } - } - - // Isolated with explicit peers - if !vc.isolated_with_explicit.is_empty() { - let _ = writeln!( - out, - " ℹ️ Validators using explicit peering (bypassing mesh, direct delivery):" - ); - for (v, peers) in &vc.isolated_with_explicit { - let mut sorted = peers.clone(); - sorted.sort(); - let _ = writeln!(out, " {v} → explicit peers: {}", sorted.join(", ")); - } - } - - // Validators without direct validator mesh peers - if !vc.validators_without_val_peers.is_empty() { - let _ = writeln!( - out, - " Validators without direct validator mesh connections (meshed with full nodes only):" - ); - for v in &vc.validators_without_val_peers { - let _ = writeln!(out, " {v}"); - } - } - - // Indirect paths - if !vc.indirect_paths.is_empty() { - let _ = writeln!( - out, - " Indirect paths (persistent peers communicating via full nodes):" - ); - for (v1, v2, intermediate, hops) in &vc.indirect_paths { - let _ = writeln!( - out, - " {v1} → {v2}: via [{}] ({hops} hops)", - intermediate.join(", ") - ); - } - } -} - -fn format_duplicates(out: &mut String, nodes: &[NodeMetricsData]) { - let nodes_with_counts: Vec<&NodeMetricsData> = nodes - .iter() - .filter(|n| n.message_counts.unfiltered > 0) - .collect(); - - if nodes_with_counts.is_empty() { - let _ = writeln!(out, " No duplicate metrics available."); - return; - } - - let max_name = nodes_with_counts - .iter() - .map(|n| n.moniker.len()) - .max() - .unwrap_or(4) - .max(4); - - let _ = writeln!( - out, - "{:12} {:>10} {:>10} {:>8}", - "Node", - "Unfiltered", - "Filtered", - "Dups", - "Dup%", - mw = max_name, - ); - let _ = writeln!( - out, - "{:-12} {:->10} {:->10} {:->8}", - "", - "", - "", - "", - "", - mw = max_name, - ); - - for node in &nodes_with_counts { - let mc = &node.message_counts; - let _ = writeln!( - out, - "{:12} {:>10} {:>10} {:>7.1}%", - node.moniker, - mc.unfiltered, - mc.filtered, - mc.duplicates(), - mc.duplicate_pct(), - mw = max_name, - ); - } -} - -fn format_explicit_peering(out: &mut String, analysis: &MeshAnalysis) { - let validators: Vec<&NodeMetricsData> = analysis - .nodes - .iter() - .filter(|n| n.node_type == NodeType::Validator) - .collect(); - - let full_nodes: Vec<&NodeMetricsData> = analysis - .nodes - .iter() - .filter(|n| n.node_type == NodeType::FullNode) - .collect(); - - let validators_with_explicit: Vec<(&str, &[String])> = validators - .iter() - .filter(|n| !n.explicit_peers.is_empty()) - .map(|n| (n.moniker.as_str(), n.explicit_peers.as_slice())) - .collect(); - - let validators_without_explicit: Vec<&str> = validators - .iter() - .filter(|n| n.explicit_peers.is_empty()) - .map(|n| n.moniker.as_str()) - .collect(); - - let fullnodes_with_explicit: Vec<(&str, &[String])> = full_nodes - .iter() - .filter(|n| !n.explicit_peers.is_empty()) - .map(|n| (n.moniker.as_str(), n.explicit_peers.as_slice())) - .collect(); - - if !validators_with_explicit.is_empty() { - let _ = write!( - out, - " ✅ Explicit peering ENABLED - {} validators using direct delivery\n\n", - validators_with_explicit.len() - ); - let _ = writeln!( - out, - " Validators with explicit peers (bypassing mesh for direct delivery):" - ); - let mut sorted = validators_with_explicit; - sorted.sort_by_key(|(name, _)| *name); - for (name, peers) in &sorted { - let mut p: Vec<&str> = peers.iter().map(|s| s.as_str()).collect(); - p.sort(); - let _ = writeln!(out, " {name} → {}", p.join(", ")); - } - - if !fullnodes_with_explicit.is_empty() { - let _ = write!( - out, - "\n Full nodes with explicit peers ({} nodes):\n", - fullnodes_with_explicit.len() - ); - let mut sorted = fullnodes_with_explicit; - sorted.sort_by_key(|(name, _)| *name); - for (name, peers) in &sorted { - let mut p: Vec<&str> = peers.iter().map(|s| s.as_str()).collect(); - p.sort(); - let _ = writeln!(out, " {name} → {}", p.join(", ")); - } - } - - let _ = write!( - out, - "\n ℹ️ With explicit peering, mesh partitioning warnings above may not indicate\n" - ); - let _ = writeln!( - out, - " a problem - validators communicate directly outside the mesh." - ); - } else { - let _ = write!( - out, - " ℹ️ Explicit peering NOT enabled (or no explicit peers connected)\n\n" - ); - if !validators_without_explicit.is_empty() { - let mut sorted = validators_without_explicit; - sorted.sort(); - let _ = write!( - out, - " Validators relying on mesh only: {}\n\n", - sorted.join(", ") - ); - - // Only warn if there are actual partitioning issues above - let has_partition_warnings = - analysis.topic_analyses.iter().any(|ta| { - ta.partitions.len() > 1 || ta.isolated_count > 0 || ta.meshed_count == 0 - }) || analysis.validator_connectivity.iter().any(|vc| { - vc.actual_partitions.len() > 1 - || !vc.completely_isolated.is_empty() - || !vc.validators_without_val_peers.is_empty() - }); - - if has_partition_warnings { - let _ = writeln!( - out, - " ⚠️ Mesh partitioning warnings above ARE significant - validators need" - ); - let _ = writeln!( - out, - " mesh connectivity or IHAVE/IWANT gossip for message delivery." - ); - } else { - let _ = writeln!( - out, - " ✅ All validators fully meshed - no partitioning concerns." - ); - } - } - } -} diff --git a/crates/mesh-analysis/src/tier.rs b/crates/mesh-analysis/src/tier.rs deleted file mode 100644 index 9562cd7..0000000 --- a/crates/mesh-analysis/src/tier.rs +++ /dev/null @@ -1,236 +0,0 @@ -// Copyright 2026 Circle Internet Group, Inc. All rights reserved. -// -// SPDX-License-Identifier: Apache-2.0 -// -// Licensed under the Apache License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. -// You may obtain a copy of the License at -// -// http://www.apache.org/licenses/LICENSE-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// See the License for the specific language governing permissions and -// limitations under the License. - -use std::fmt; -use std::str::FromStr; - -use super::types::{MeshAnalysis, NodeType, ValidatorConnectivity}; - -/// Mesh health tier for a node. -/// -/// Tiers are ordered from healthiest to least healthy: -/// - `FullyConnected`: direct mesh peer links to all relevant peers -/// - `MultiHop`: reachable but only via intermediate relayers -/// - `NotConnected`: isolated or in a minority partition -#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)] -pub enum MeshTier { - FullyConnected, - MultiHop, - NotConnected, -} - -impl fmt::Display for MeshTier { - fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { - f.write_str(match self { - MeshTier::FullyConnected => "fully-connected", - MeshTier::MultiHop => "multi-hop", - MeshTier::NotConnected => "not-connected", - }) - } -} - -impl FromStr for MeshTier { - type Err = String; - - fn from_str(s: &str) -> Result { - match s { - "fully-connected" => Ok(MeshTier::FullyConnected), - "multi-hop" => Ok(MeshTier::MultiHop), - "not-connected" => Ok(MeshTier::NotConnected), - other => Err(format!( - "unknown tier '{other}'; expected fully-connected, multi-hop, or not-connected" - )), - } - } -} - -/// Classify a single node's mesh health on the `/consensus` topic. -fn classify_validator(moniker: &str, consensus_connectivity: &ValidatorConnectivity) -> MeshTier { - // Completely isolated → not connected - if consensus_connectivity - .completely_isolated - .iter() - .any(|v| v == moniker) - { - return MeshTier::NotConnected; - } - - // Isolated with only explicit peers (no mesh peers) → not connected - if consensus_connectivity - .isolated_with_explicit - .iter() - .any(|(v, _)| v == moniker) - { - return MeshTier::NotConnected; - } - - // Multiple partitions: node not in the largest partition → not connected - if consensus_connectivity.actual_partitions.len() > 1 { - let max_size = consensus_connectivity - .actual_partitions - .iter() - .map(|p| p.len()) - .max() - .unwrap_or(0); - - let in_largest = consensus_connectivity - .actual_partitions - .iter() - .filter(|p| p.len() == max_size) - .any(|p| p.contains(moniker)); - - if !in_largest { - return MeshTier::NotConnected; - } - } - - // No direct validator mesh peers but has some mesh peers → multi-hop - if consensus_connectivity - .validators_without_val_peers - .iter() - .any(|v| v == moniker) - { - return MeshTier::MultiHop; - } - - // Has indirect paths through non-validator nodes → multi-hop - if consensus_connectivity - .indirect_paths - .iter() - .any(|(v1, v2, _, _)| v1 == moniker || v2 == moniker) - { - return MeshTier::MultiHop; - } - - MeshTier::FullyConnected -} - -/// Classify a non-validator node on the `/consensus` topic. -fn classify_non_validator(moniker: &str, analysis: &MeshAnalysis) -> MeshTier { - let consensus_topic = analysis - .topic_analyses - .iter() - .find(|t| t.topic_name == "/consensus"); - - let Some(topic) = consensus_topic else { - return MeshTier::NotConnected; - }; - - // Isolated on /consensus → not connected - if topic.isolated_nodes.iter().any(|n| n == moniker) { - return MeshTier::NotConnected; - } - - // Single partition → fully connected - if topic.partitions.len() <= 1 { - return MeshTier::FullyConnected; - } - - // Multiple partitions: check if in any partition of maximum size - let max_size = topic.partitions.iter().map(|p| p.len()).max().unwrap_or(0); - - let in_largest = topic - .partitions - .iter() - .filter(|p| p.len() == max_size) - .any(|p| p.contains(moniker)); - - if in_largest { - return MeshTier::FullyConnected; - } - - MeshTier::MultiHop -} - -/// Classify every node in the analysis and return a vec of (moniker, node_type, tier). -pub fn classify_all(analysis: &MeshAnalysis) -> Vec<(String, NodeType, MeshTier)> { - let consensus_connectivity = analysis - .validator_connectivity - .iter() - .find(|vc| vc.topic_name == "/consensus"); - - analysis - .nodes - .iter() - .map(|node| { - let tier = if node.node_type == NodeType::Validator { - if let Some(vc) = consensus_connectivity { - classify_validator(&node.moniker, vc) - } else { - // No validator connectivity data → can't classify - MeshTier::NotConnected - } - } else { - classify_non_validator(&node.moniker, analysis) - }; - (node.moniker.clone(), node.node_type, tier) - }) - .collect() -} - -#[cfg(test)] -mod tests { - use super::*; - use crate::types::ValidatorConnectivity; - use std::collections::BTreeSet; - - #[test] - fn tier_round_trip() { - for tier in [ - MeshTier::FullyConnected, - MeshTier::MultiHop, - MeshTier::NotConnected, - ] { - let s = tier.to_string(); - let parsed: MeshTier = s.parse().unwrap(); - assert_eq!(tier, parsed); - } - } - - #[test] - fn tier_from_str_error() { - let result = "unknown".parse::(); - assert!(result.is_err()); - } - - #[test] - fn classify_validator_equal_partitions() { - // Two partitions of equal size — node in the first must not be - // misclassified (regression for max_by_key tie-breaking). - let vc = ValidatorConnectivity { - topic_name: "/consensus".to_string(), - all_validators: BTreeSet::from([ - "val1".into(), - "val2".into(), - "val3".into(), - "val4".into(), - ]), - actual_partitions: vec![ - BTreeSet::from(["val1".into(), "val2".into()]), - BTreeSet::from(["val3".into(), "val4".into()]), - ], - direct_val_connections: 0, - max_diameter: 0, - partition_diameters: vec![], - completely_isolated: vec![], - isolated_with_explicit: vec![], - validators_without_val_peers: vec![], - indirect_paths: vec![], - }; - assert_eq!(classify_validator("val1", &vc), MeshTier::FullyConnected); - assert_eq!(classify_validator("val3", &vc), MeshTier::FullyConnected); - } -} diff --git a/crates/mesh-analysis/src/types.rs b/crates/mesh-analysis/src/types.rs deleted file mode 100644 index a2504ee..0000000 --- a/crates/mesh-analysis/src/types.rs +++ /dev/null @@ -1,142 +0,0 @@ -// Copyright 2026 Circle Internet Group, Inc. All rights reserved. -// -// SPDX-License-Identifier: Apache-2.0 -// -// Licensed under the Apache License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. -// You may obtain a copy of the License at -// -// http://www.apache.org/licenses/LICENSE-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// See the License for the specific language governing permissions and -// limitations under the License. - -use std::collections::{BTreeMap, BTreeSet}; -use std::fmt; - -pub(super) const TOPICS: [&str; 3] = ["/consensus", "/proposal_parts", "/liveness"]; - -/// Aggregated message receive counts across all topics (total and after dedup). -#[derive(Debug, Clone, Default)] -pub struct MessageCounts { - /// Total messages received (including duplicates), summed across all topics. - pub unfiltered: u64, - /// Messages after deduplication, summed across all topics. - pub filtered: u64, -} - -impl MessageCounts { - pub fn duplicates(&self) -> u64 { - self.unfiltered.saturating_sub(self.filtered) - } - - /// Duplicate percentage (0.0–100.0). Returns 0.0 when no messages received. - pub fn duplicate_pct(&self) -> f64 { - if self.unfiltered == 0 { - return 0.0; - } - (self.duplicates() as f64 / self.unfiltered as f64) * 100.0 - } -} - -#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord)] -pub enum NodeType { - FullNode, - PersistentPeer, - Validator, -} - -impl fmt::Display for NodeType { - fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { - f.pad(match self { - NodeType::FullNode => "full_node", - NodeType::PersistentPeer => "persistent", - NodeType::Validator => "validator", - }) - } -} - -#[derive(Debug, Clone)] -pub struct NodeMetricsData { - pub moniker: String, - pub node_type: NodeType, - - /// mesh peer counts per topic hash (e.g. "/consensus" -> 3) - pub mesh_counts: BTreeMap, - - /// mesh peer monikers per topic name (e.g. "/consensus" -> ["val0", "val1"]) - pub mesh_peers: BTreeMap>, - - /// explicit gossipsub peers (monikers) - pub explicit_peers: Vec, - - /// Per-peer detail from `malachitebft_network_discovered_peers` - /// (moniker -> discovered peer info as seen by this node) - pub discovered_peers: BTreeMap, - - /// Gossipsub message counts (aggregate across all topics) for duplicate analysis. - pub message_counts: MessageCounts, - - // connection counts - pub connected_peers: i64, - pub inbound_peers: i64, - pub outbound_peers: i64, - pub active_connections: i64, - pub inbound_connections: i64, - pub outbound_connections: i64, -} - -/// Detail about a peer as seen by a particular node, extracted from -/// the `malachitebft_network_discovered_peers` metric. -#[derive(Debug, Clone)] -pub struct DiscoveredPeer { - pub peer_moniker: String, - pub peer_type: String, - pub score: f64, -} - -#[derive(Debug)] -pub struct TopicAnalysis { - pub topic_name: String, - pub meshed_count: usize, - pub isolated_count: usize, - pub isolated_nodes: Vec, - pub partitions: Vec>, -} - -#[derive(Debug)] -pub struct ValidatorConnectivity { - pub topic_name: String, - pub all_validators: BTreeSet, - pub actual_partitions: Vec>, - pub direct_val_connections: usize, - pub max_diameter: usize, - pub partition_diameters: Vec>, - pub completely_isolated: Vec, - pub isolated_with_explicit: Vec<(String, Vec)>, - pub validators_without_val_peers: Vec, - pub indirect_paths: Vec<(String, String, Vec, usize)>, -} - -#[derive(Debug)] -pub struct MeshAnalysis { - pub node_count: usize, - pub validator_count: usize, - pub persistent_peer_count: usize, - pub full_node_count: usize, - pub nodes: Vec, - pub topic_analyses: Vec, - pub validator_connectivity: Vec, - pub zero_mesh_warnings: Vec<(String, i64, i64, i64)>, -} - -pub struct MeshDisplayOptions { - pub show_counts: bool, - pub show_mesh: bool, - pub show_peers: bool, - pub show_peers_full: bool, - pub show_duplicates: bool, -} diff --git a/crates/node/Cargo.toml b/crates/node/Cargo.toml deleted file mode 100644 index 7ee8394..0000000 --- a/crates/node/Cargo.toml +++ /dev/null @@ -1,89 +0,0 @@ -[package] -name = "arc-node-execution" -version.workspace = true -edition.workspace = true -repository.workspace = true -license.workspace = true -rust-version.workspace = true -publish.workspace = true - -[[bin]] -name = "arc-node-execution" -path = "src/main.rs" - -[features] -default = [] -js-tracer = ["reth-node-builder/js-tracer", "reth-ethereum/js-tracer"] -pprof = ["dep:pprof_hyper_server", "dep:reth-node-metrics", "dep:tikv-jemalloc-ctl"] -arbitrary = [ - "alloy-primitives/arbitrary", - "arc-execution-config/arbitrary", - "arc-evm/arbitrary", - "arc-evm-node/arbitrary", - "revm-primitives/arbitrary", - "revm/arbitrary", - "reth-ethereum/arbitrary", -] -remote-signer = ["dep:protox", "dep:tonic-build"] -integration = ["remote-signer"] - -[dependencies] -alloy-evm.workspace = true -alloy-genesis.workspace = true -alloy-primitives.workspace = true -alloy-rlp.workspace = true -alloy-rpc-types-trace.workspace = true -alloy-sol-types.workspace = true -arc-evm.workspace = true -arc-evm-node.workspace = true -arc-execution-config.workspace = true -arc-execution-txpool.workspace = true -arc-execution-validation.workspace = true -arc-version.workspace = true -clap.workspace = true -directories.workspace = true -eyre.workspace = true - -metrics.workspace = true -pprof_hyper_server = { version = "0.2", features = ["pprof"], optional = true } -reth-chainspec.workspace = true -reth-db.workspace = true -reth-ethereum = { workspace = true, features = ["node-api", "node", "provider", "network", "evm", "pool", "cli", "js-tracer"] } -reth-evm.workspace = true -reth-node-builder = { workspace = true, features = ["js-tracer"] } -reth-node-core.workspace = true -reth-node-ethereum.workspace = true -reth-node-metrics = { workspace = true, optional = true, features = ["jemalloc"] } -reth-prune-types.workspace = true -reth-rpc-builder.workspace = true -reth-rpc-eth-types.workspace = true -reth-rpc-server-types.workspace = true - -# revm -revm.workspace = true -revm-inspectors.workspace = true -revm-primitives.workspace = true -serde_json = { workspace = true, default-features = false, features = ["alloc"] } -tikv-jemalloc-ctl = { version = "0.6", optional = true } - -tokio = { workspace = true, features = ["signal"] } -tracing.workspace = true - -# allocator -[target.'cfg(not(target_env = "msvc"))'.dependencies] -tikv-jemallocator = "0.6" - -[build-dependencies] -protox = { workspace = true, optional = true } -tonic-build = { workspace = true, optional = true } - -[dev-dependencies] -arc-execution-config = { workspace = true, features = ["test-utils"] } - -arc-precompiles.workspace = true -reth-e2e-test-utils.workspace = true -reth-rpc-eth-types.workspace = true -tracing-test = "0.2" - -[lints] -workspace = true diff --git a/crates/node/README.md b/crates/node/README.md deleted file mode 100644 index eef6369..0000000 --- a/crates/node/README.md +++ /dev/null @@ -1,258 +0,0 @@ -# arc-node-execution - -This is a [Reth][reth]-based execution layer (EL) implementation customized for the Arc Network. - -It serves as the execution client that processes transactions, executes smart contracts, and maintains the blockchain state. Communication with the consensus layer (CL) is handled via the [Engine API][engine-api]. - -## Table of Contents - -- [Usage](#usage) - - [a) Full Node](#a-full-node) - - [b) Unsafe RPC Node](#b-unsafe-rpc-node) - - [c) RPC Node (with consensus verification)](#c-rpc-node-with-consensus-verification) - - [CLI Flags](#cli-flags) - - [Custom flags](#custom-flags) - - [Init](#init) - - [Database Commands](#database-commands) -- [Invalid Transaction List](#invalid-transaction-list) -- [Pending Txs Filter](#pending-txs-filter) -- [Architecture](#architecture) -- [Metrics](#metrics) -- [Development](#development) - - [Running Without Consensus (Mock Mode)](#running-without-consensus-mock-mode) -- [Further Reading](#further-reading) - -## Usage - -### a) Full Node - -**Minimal example with IPC** (recommended for colocated execution and consensus): - -```bash -arc-node-execution node \ - --chain=assets/localdev/genesis.json \ - --http --http.port=8545 \ - --ipcpath=/tmp/reth.ipc \ - --full -``` - -**Full example with detailed configuration**: - -```bash -arc-node-execution node \ - --chain=assets/localdev/genesis.json \ - --datadir=/var/lib/arc-execution \ - --http --http.addr=0.0.0.0 --http.port=8545 \ - --http.api=eth,net,web3,txpool,trace,debug \ - --http.corsdomain="*" \ - --ws --ws.addr=0.0.0.0 --ws.port=8546 \ - --authrpc.addr=0.0.0.0 --authrpc.port=8551 --authrpc.jwtsecret=jwtsecret \ - --ipcpath=/tmp/reth.ipc \ - --metrics=0.0.0.0:9001 \ - --enable-arc-rpc \ - --full -``` - -Note: to generate a JWT (JSON web token), use the following command: - -```bash -openssl rand -hex 32 | tr -d "\n" > "jwtsecret" -``` - -### b) Unsafe RPC Node - -Use RPC nodes to interact with the Arc network via API. While they don't participate in consensus, they stay fully synced with the latest state. - -In this mode, consensus data is not verified. It is essential to only follow and synchronize with a trusted node. - -```bash -# Download snapshot (this will help you sync much faster) -arc-node-execution download - -arc-node-execution node \ - --unsafe-follow \ - --http --http.port=8545 \ - --http.api eth,net,web3,txpool,trace \ - --enable-arc-rpc \ - --minimal -``` - -> **Note:** When running a node in RPC mode (as shown above), you do **not** need to run or download the consensus binary. The execution node will follow the network without participating in consensus. - -### c) RPC Node (with consensus verification) - -For an RPC node with full consensus verification, run both the execution and consensus -layers together. The `--follow` and `--follow.endpoint` flags are configured on -`arc-node-consensus`, not this binary. See the [Consensus Layer README](../malachite-app/README.md) -for details on setting up a verified full node. - ---- - -#### CLI Flags - -For a complete list of available flags, see the [Reth CLI reference](https://reth.rs/cli/reth/node/) or run: - -```bash -arc-node-execution node --help -``` - -#### Custom flags - -In addition to standard Reth flags, `arc-node-execution` provides the following custom flags: - -| Flag | Default | Environment Variable | Description | -|------|---------|---------------------|-------------| -| `--enable-arc-rpc` | `false` | - | Enable custom ARC RPC namespace (certificates, etc.) | -| `--arc-rpc-upstream-url ` | - | `ARC_RPC_UPSTREAM_URL` | Upstream malachite-app base URL for ARC RPC (e.g., `http://127.0.0.1:31000`). Only read if `--enable-arc-rpc` is set | -| `--unsafe-follow [URL]` | - | `ARC_UNSAFE_FOLLOW_URL` | Run an RPC node (unsafe - no verification). Use without value for auto-config or specify WebSocket URL (e.g., `ws://trusted-node:8546`) | -| `--invalid-tx-list-enable[=]` | `true` | - | Enable the invalid transaction list feature. Opt out with `--invalid-tx-list-enable=false`. | -| `--invalid-tx-list-cap ` | `100000` | - | Maximum capacity of the invalid tx list LRU cache. Only read if `--invalid-tx-list-enable` is `true`. | -| `--arc.rpc.max-batch-entries ` | `100` | - | Maximum number of entries permitted in a JSON-RPC batch request. Oversized batches are rejected with JSON-RPC error `-32600` before any per-entry handler runs. Must be `>= 1`. | -| `--full` | - | - | Full-node pruning preset. Fully prunes sender recovery; keeps the last 237,600 blocks for all other segments. Also sets `--prune.block-interval=5000`. Mutually exclusive with `--minimal`. | -| `--minimal` | - | - | Minimal-storage pruning preset. Fully prunes sender recovery; keeps transaction lookup for 64 blocks, receipts for 64 blocks, account/storage history for 10,064 blocks, and block bodies for 237,600 blocks. Also sets `--prune.block-interval=5000`. Mutually exclusive with `--full`. | -| `--arc.expose-pending-txs` | `false` | - | Expose pending-tx RPCs. By default pending-tx subscriptions, filters, and pending-block queries are blocked — set this on trusted / internal nodes where exposing pending state is intentional. | -| `--public-api` | `false` | - | Convenience flag for externally-exposed RPC nodes. Forces pending-tx hiding and warns if `--http.api` / `--ws.api` expose namespaces outside `{eth, net, web3, rpc}`. Conflicts with `--arc.expose-pending-txs`. | - -**Examples:** - -Enable ARC RPC namespace: - -```bash -arc-node-execution node \ - --enable-arc-rpc \ - --arc-rpc-upstream-url http://localhost:31000 \ - --chain genesis.json -``` - -Override the invalid transaction list capacity (the feature is enabled by default): - -```bash -arc-node-execution node \ - --invalid-tx-list-cap 50000 \ - --chain genesis.json -``` - -Disable the invalid transaction list: - -```bash -arc-node-execution node \ - --invalid-tx-list-enable=false \ - --chain genesis.json -``` - -Tighten the JSON-RPC batch entry cap: - -```bash -arc-node-execution node \ - --arc.rpc.max-batch-entries 25 \ - --chain genesis.json -``` - -### Init - -Initialize the database from a genesis file: - -```bash -arc-node-execution init --chain=assets/localdev/genesis.json -``` - -This creates the genesis block and initializes the state database. - -### Database Commands - -The `db` command provides database maintenance and debugging utilities. - -For available database operations, run: -```bash -arc-node-execution db --help -``` - -## Invalid Transaction List - -The node includes an in-memory invalid transaction list (LRU) used to proactively reject known-bad transaction hashes and to add all currently pending transactions to the list in the event the payload builder panics. Enabled by default; opt out with `--invalid-tx-list-enable=false`. - -**Configuration:** - -Use the `--invalid-tx-list-enable` and `--invalid-tx-list-cap` flags (see Custom flags section above). - -**Behavior when enabled (default):** -- On payload builder panic, all pending transactions are added to the invalid tx list and removed from the mempool — resubmit them after investigating the panic -- O(1) hash membership check during transaction validation -- Metrics exposed: `arc_invalid_tx_list_size`, `arc_invalid_tx_list_hits_total`, `arc_invalid_tx_list_inserts_total`, `arc_invalid_tx_list_batch_inserts_total` - -**Behavior when disabled (`--invalid-tx-list-enable=false`):** -- No invalid tx list is created -- No metrics are exposed -- On payload builder panic, no action is taken - -**Example:** -```bash -arc-node-execution node \ - --chain genesis.json \ - --invalid-tx-list-cap 10000 -``` - -**Operational Notes:** -- Setting `--invalid-tx-list-cap 0` keeps the invalid tx list logically enabled (metrics + panic handling) but stores no hashes - -## Pending Txs Filter - -By default, the node hides pending-tx RPCs. Pass `--arc.expose-pending-txs` -to disable the filter on trusted / internal nodes. External / public-facing -nodes should instead use `--public-api`, which also narrows the advised -RPC surface. - -**When the filter is enabled (default, or enforced by `--public-api`):** - -| Method or call | Behavior | -|----------------|----------| -| `eth_subscribe("newPendingTransactions")` | Error -32001 | -| `eth_newPendingTransactionFilter` | Error -32001 | -| `eth_getBlockByNumber("pending")` | Returns `null` (success) | - -**When the filter is disabled (`--arc.expose-pending-txs`):** - -All three methods bypass the middleware. Pending-block queries additionally depend on `--rpc.pending-block` (Arc default: `none`); to actually receive pending-block data, set `--rpc.pending-block=full` alongside `--arc.expose-pending-txs`. - -## Architecture - -The execution layer is built on top of [Reth][reth], extending it with Arc-specific functionality: - -- **Custom Precompiles** - Native implementations for Arc-specific operations (native coin control, - post-quantum signatures, system accounting). -- **Custom EVM Configuration** - Specialized gas calculations and execution logic -- **Transaction Pool Enhancements** - Custom validation -- **Block Executor** - Optimized block execution with Arc-specific features - -For architectural details, see the [Architecture Guide](../../docs/ARCHITECTURE.md). - -## Metrics - -The execution layer exposes Prometheus metrics on the configured metrics endpoint (e.g., `http://localhost:9001/metrics`). - -Key metric prefixes: -- `reth_*` - Core Reth metrics (block processing, sync, etc.) -- `arc_*` - Arc-specific metrics (precompiles, invalid tx list, etc.) - -## Development - -### Running Without Consensus (Mock Mode) - -For execution-layer-only development and testing: - -```bash -./scripts/localdev.mjs start -``` - -This runs the execution layer with a mock consensus layer for rapid iteration. - -## Further Reading - -- [Main README](../../README.md) - Getting started and development workflow -- [Architecture Guide](../../docs/ARCHITECTURE.md) - System design and component interactions -- [Consensus Layer README](../malachite-app/README.md) - Consensus layer documentation -- [Reth Book][reth-book] - Upstream Reth documentation - -[reth]: https://github.com/paradigmxyz/reth -[reth-book]: https://paradigmxyz.github.io/reth/ -[engine-api]: https://github.com/ethereum/execution-apis/blob/main/src/engine/README.md diff --git a/crates/node/src/args.rs b/crates/node/src/args.rs deleted file mode 100644 index 9e7447e..0000000 --- a/crates/node/src/args.rs +++ /dev/null @@ -1,151 +0,0 @@ -// Copyright 2025 Circle Internet Group, Inc. All rights reserved. -// -// SPDX-License-Identifier: Apache-2.0 -// -// Licensed under the Apache License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. -// You may obtain a copy of the License at -// -// http://www.apache.org/licenses/LICENSE-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// See the License for the specific language governing permissions and -// limitations under the License. - -//! Default CLI args for the Reth `node` subcommand. -//! -//! Sets Arc's default values on the node subcommand's args. -//! User-passed flags override these defaults as usual. - -use std::collections::HashMap; - -use clap::Command; - -/// Built-in Arc default flags for the `node` subcommand (used to derive arg default overrides). -pub(crate) const ARC_DEFAULT_NODE_FLAGS: &[&str] = &[ - "--engine.memory-block-buffer-target=0", - "--engine.persistence-threshold=0", - "--rpc.pending-block=none", - "--rpc.txfeecap=1000", - // Caps the per-request gas budget on `eth_call`, `eth_estimateGas`, and - // the trace/debug call variants. 30M matches genesis `blockGasLimit` and - // stays well below every chainspec ceiling. Operators raise it via - // `--rpc.gascap N`. - "--rpc.gascap=30000000", -]; - -// Subcommand name for the `node` subcommand. -const NODE_SUBCOMMAND: &str = "node"; - -/// Patches the node subcommand's args so their default values are Arc's. -pub fn patch_node_command_defaults(root_cmd: Command) -> Command { - let overrides: HashMap<_, _> = arc_node_arg_default_overrides().collect(); - root_cmd.mut_subcommand(NODE_SUBCOMMAND, |subcmd| { - subcmd.mut_args(|arg| { - if let Some(long_name) = arg.get_long() { - if let Some(&arc_val) = overrides.get(long_name) { - return arg.default_value(arc_val); - } - } - arg - }) - }) -} - -/// Returns `(long_name, value)` for each flag in `ARC_DEFAULT_NODE_FLAGS`. -/// Flags with `=` use that value; flags without (e.g. `--http`, `--ws`) get `"true"`. -fn arc_node_arg_default_overrides() -> impl Iterator { - ARC_DEFAULT_NODE_FLAGS.iter().filter_map(|f| { - let s = f.strip_prefix("--")?; - let (name, val) = s.split_once('=').unwrap_or((s, "true")); - Some((name, val)) - }) -} - -#[cfg(test)] -mod tests { - use super::*; - use clap::{Arg, Command}; - use std::collections::HashMap; - - #[test] - fn test_arg_default_overrides_derived_from_const() { - let overrides: HashMap<_, _> = arc_node_arg_default_overrides().collect(); - let mut expected = HashMap::new(); - expected.insert("engine.memory-block-buffer-target", "0"); - expected.insert("engine.persistence-threshold", "0"); - expected.insert("rpc.pending-block", "none"); - expected.insert("rpc.txfeecap", "1000"); - expected.insert("rpc.gascap", "30000000"); - - assert_eq!( - overrides, expected, - "Overrides should match expected defaults" - ); - } - - #[test] - fn test_patch_node_command_defaults_applies_default() { - let cmd = Command::new("bin").subcommand( - Command::new(NODE_SUBCOMMAND).arg( - Arg::new("rpc.txfeecap") - .long("rpc.txfeecap") - .default_value("1.0"), - ), - ); - let patched = patch_node_command_defaults(cmd); - let args_matches = patched.get_matches_from(["bin", "node"]); - let node_matches = args_matches.subcommand_matches(NODE_SUBCOMMAND).unwrap(); - - assert_eq!( - node_matches.get_one::("rpc.txfeecap").cloned(), - Some("1000".to_string()), - "Arc default overrides Reth default" - ); - } - - /// `--rpc.gascap` default flips from Reth's 50M to Arc's 30M when the - /// operator does not pass the flag. - #[test] - fn test_rpc_gascap_default_is_thirty_million() { - let cmd = Command::new("bin").subcommand( - Command::new(NODE_SUBCOMMAND).arg( - Arg::new("rpc.gascap") - .long("rpc.gascap") - .default_value("50000000"), - ), - ); - let patched = patch_node_command_defaults(cmd); - let args_matches = patched.get_matches_from(["bin", "node"]); - let node_matches = args_matches.subcommand_matches(NODE_SUBCOMMAND).unwrap(); - - assert_eq!( - node_matches.get_one::("rpc.gascap").cloned(), - Some("30000000".to_string()), - "Arc default (30M) overrides Reth's stock 50M" - ); - } - - /// Explicit `--rpc.gascap` from the operator wins over Arc's default. - #[test] - fn test_explicit_rpc_gascap_overrides_arc_default() { - let cmd = Command::new("bin").subcommand( - Command::new(NODE_SUBCOMMAND).arg( - Arg::new("rpc.gascap") - .long("rpc.gascap") - .default_value("50000000"), - ), - ); - let patched = patch_node_command_defaults(cmd); - let args_matches = patched.get_matches_from(["bin", "node", "--rpc.gascap=12345"]); - let node_matches = args_matches.subcommand_matches(NODE_SUBCOMMAND).unwrap(); - - assert_eq!( - node_matches.get_one::("rpc.gascap").cloned(), - Some("12345".to_string()), - "Explicit operator value beats the Arc default" - ); - } -} diff --git a/crates/node/src/lib.rs b/crates/node/src/lib.rs deleted file mode 100644 index 57dcf43..0000000 --- a/crates/node/src/lib.rs +++ /dev/null @@ -1,31 +0,0 @@ -// Copyright 2026 Circle Internet Group, Inc. All rights reserved. -// -// SPDX-License-Identifier: Apache-2.0 -// -// Licensed under the Apache License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. -// You may obtain a copy of the License at -// -// http://www.apache.org/licenses/LICENSE-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// See the License for the specific language governing permissions and -// limitations under the License. - -//! Arc Network - A custom Reth node implementation -//! -//! This crate demonstrates how to build a custom blockchain node using Reth -//! with custom EVM configuration, precompiles, and transaction pool. - -mod args; - -pub use args::patch_node_command_defaults; - -pub mod metrics; - -// Re-export commonly used types -pub use arc_evm::{ArcEvmConfig, ArcEvmFactory}; -pub use arc_evm_node::ArcEngineValidator; -pub use arc_execution_validation::ArcConsensus; diff --git a/crates/node/src/main.rs b/crates/node/src/main.rs deleted file mode 100644 index 4d70032..0000000 --- a/crates/node/src/main.rs +++ /dev/null @@ -1,1609 +0,0 @@ -// Copyright 2026 Circle Internet Group, Inc. All rights reserved. -// -// SPDX-License-Identifier: Apache-2.0 -// -// Licensed under the Apache License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. -// You may obtain a copy of the License at -// -// http://www.apache.org/licenses/LICENSE-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// See the License for the specific language governing permissions and -// limitations under the License. - -//! Arc Network - A custom Reth node implementation -//! -//! This example demonstrates how to create a custom blockchain node using Reth -//! with custom EVM configuration, precompiles, and transaction pool. - -#[cfg(not(target_env = "msvc"))] -#[global_allocator] -static ALLOC: tikv_jemallocator::Jemalloc = tikv_jemallocator::Jemalloc; - -/// Profiling configuration for jemalloc. -#[cfg(feature = "pprof")] -#[allow(non_upper_case_globals)] -#[unsafe(export_name = "malloc_conf")] -pub static malloc_conf: &[u8] = b"prof:true,prof_active:false,lg_prof_sample:19\0"; - -use arc_evm_node::node::{ArcNode, ArcRpcConfig}; -use arc_evm_node::ARC_RPC_MAX_BATCH_ENTRIES_DEFAULT; -use arc_execution_config::addresses_denylist::{ - AddressesDenylistConfig, AddressesDenylistConfigError, DEFAULT_DENYLIST_ADDRESS, - DEFAULT_DENYLIST_ERC7201_BASE_SLOT, -}; -use arc_execution_config::chainspec::{ArcChainSpec, ArcChainSpecParser}; -use arc_execution_config::defaults; -use arc_execution_config::follow; -use arc_node_execution::patch_node_command_defaults; -use clap::{Args, CommandFactory, FromArgMatches, Parser}; -use directories::BaseDirs; -use reth_chainspec::EthChainSpec; -use reth_ethereum::cli::interface::{Cli as RethCli, Commands}; -use reth_node_core::version::default_extra_data; -use reth_rpc_builder::config::RethRpcServerConfig; -use reth_rpc_server_types::{RethRpcModule, RpcModuleSelection}; -use tracing::info; - -use std::collections::HashSet; -use std::sync::Arc; - -use reth_node_core::args::DefaultPruningValues; -use reth_prune_types::{PruneMode, PruneModes}; - -use arc_execution_txpool::{InvalidTxListConfig, ARC_INVALID_TX_LIST_DEFAULT_CAP}; -use arc_node_execution::ArcConsensus; -use arc_node_execution::ArcEvmConfig; -use arc_node_execution::ArcEvmFactory; -use reth_db::DatabaseEnv; -use reth_node_builder::NodeBuilder; -use reth_node_builder::WithLaunchContext; -use reth_node_ethereum::EthEvmConfig; - -/// Arc Network node CLI with custom version handling -#[derive(Debug, Parser)] -#[command( - name = "arc-node-execution", - version = arc_version::SHORT_VERSION, - long_version = arc_version::LONG_VERSION, - about = "Arc execution layer", - disable_help_subcommand = true -)] -struct ArcCli { - #[command(flatten)] - inner: RethCli, -} - -impl ArcCli { - /// Validate Arc-specific CLI constraints. - fn validate(&self) -> Result<(), &'static str> { - if let Commands::Node(ref node_cmd) = self.inner.command { - // Reject --builder.extradata if user explicitly set it. - // Arc uses the extra_data field to store the next block's base fee. - if node_cmd.builder.extra_data != default_extra_data() { - return Err("--builder.extradata is not supported"); - } - - // The middleware intercepts pending-block and pool-based queries in both - // single and batch paths. Enforce `--rpc.pending-block=none` so reth - // replaces pending data with finalized data for all other queries. - if compute_filter_pending_txs(&node_cmd.ext) - && node_cmd.rpc.rpc_pending_block - != reth_rpc_eth_types::builder::config::PendingBlockKind::None - { - return Err( - "--rpc.pending-block must be 'none' when the pending-tx filter is active; \ - pass --arc.expose-pending-txs to opt out of hiding or set --rpc.pending-block=none", - ); - } - } - Ok(()) - } -} - -fn arc_components(spec: Arc) -> (ArcEvmConfig, Arc>) { - let eth_evm = - EthEvmConfig::new_with_evm_factory(spec.clone(), ArcEvmFactory::new(spec.clone())); - let evm = ArcEvmConfig::new(eth_evm); - let consensus = Arc::new(ArcConsensus::new(spec.clone())); - - (evm, consensus) -} - -/// Configure the node builder to follow a trusted node for consensus. -fn follow_url_for_consensus( - builder: &mut WithLaunchContext>, - follow_url: &str, -) -> eyre::Result<()> { - let chain_id = builder.config().chain.chain().id(); - - let url = if follow_url.is_empty() || follow_url == "auto" { - follow::ws_url_for_chain_id(chain_id)? - } else { - follow_url.to_string() - }; - - info!("🔗 Following trusted node: {}", url); - - // Configure the builder to use the follow URL for consensus (get the latest block and subscribe for new blocks) - // - // "Runs a fake consensus client using blocks fetched from an RPC endpoint. - // Supports both HTTP and WebSocket endpoints - WebSocket endpoints will use - // subscriptions, while HTTP endpoints will poll for new blocks" - builder.config_mut().debug.rpc_consensus_url = Some(url); - - // Configure trusted peers (needed to backfill the missing blocks via devp2p) - if let Ok(trusted_peers) = follow::trusted_peers_for_chain_id(chain_id) { - if !trusted_peers.is_empty() { - info!( - "🤝 Configuring {} trusted peers for chain {}", - trusted_peers.len(), - chain_id - ); - builder.config_mut().network.trusted_peers = trusted_peers; - } - } - - Ok(()) -} - -#[derive(Debug, Args)] -struct ArcExtraCli { - /// Enable custom ARC RPC namespace (certificates etc.). - #[arg(long = "enable-arc-rpc", default_value_t = false)] - enable_arc_rpc: bool, - /// Upstream malachite-app base URL used by ARC RPC (e.g. http://127.0.0.1:31000). - #[arg( - long = "arc-rpc-upstream-url", - value_name = "URL", - env = "ARC_RPC_UPSTREAM_URL" - )] - arc_rpc_upstream_url: Option, - - /// Run an RPC node (unsafe - no verification). - /// - /// Use without a value (--unsafe-follow) to automatically use the preconfigured trusted node or - /// provide the WebSocket URL of the trusted node (e.g., ws://trusted-node:8546). - #[arg( - long = "unsafe-follow", - value_name = "URL", - env = "ARC_UNSAFE_FOLLOW_URL", - default_missing_value = "auto", - num_args = 0..=1 - )] - unsafe_follow_url: Option, - - /// Enable the invalid transaction list. - /// - /// When enabled, problematic transactions that cause builder panics or errors - /// are cached and rejected on subsequent submissions. A builder panic flushes - /// all currently-pending transactions into the list; resubmit them after - /// investigating the panic. - #[arg( - long = "invalid-tx-list-enable", - default_value_t = true, - // Flag is true by default; `Set` action lets `--invalid-tx-list-enable=false` opt out. - action = clap::ArgAction::Set, - help_heading = "Invalid tx list" - )] - invalid_tx_list_enable: bool, - - /// Maximum capacity of the invalid tx list LRU cache. - /// - /// Only relevant when --invalid-tx-list-enable is true. - /// A value of 0 disables storage (all inserts are ignored, but counted in metrics). - #[arg( - long = "invalid-tx-list-cap", - default_value_t = ARC_INVALID_TX_LIST_DEFAULT_CAP, - value_name = "CAPACITY", - help_heading = "Invalid tx list" - )] - invalid_tx_list_cap: u32, - - /// Maximum number of entries permitted in a JSON-RPC batch request. - /// - /// Batches with more entries are rejected before any per-entry handler runs. - /// Must be >= 1. - #[arg( - long = "arc.rpc.max-batch-entries", - default_value_t = ARC_RPC_MAX_BATCH_ENTRIES_DEFAULT, - value_parser = parse_max_batch_entries, - value_name = "COUNT", - help_heading = "Arc RPC limits" - )] - arc_rpc_max_batch_entries: usize, - - /// Maximum duration for the custom payload builder's transaction selection loop, in milliseconds. - /// - /// When unset, Reth's `builder.deadline` (seconds) is adopted as the maximum loop duration. - #[arg( - long = "arc.builder.deadline", - value_name = "MS", - env = "ARC_BUILDER_DEADLINE_MS", - help_heading = "Payload builder deadline" - )] - payload_builder_deadline_ms: Option, - - /// Wait for the in-flight payload build instead of racing an - /// empty block when `engine_getPayload` arrives early. - #[arg( - long = "arc.builder.wait-for-payload", - default_value_t = true, - // because the flag is true by default, we need `Set` action so that we can - // do `--arc.builder.wait-for-payload=false` in the CLI. - action = clap::ArgAction::Set, - env = "ARC_BUILDER_WAIT_FOR_PAYLOAD", - help_heading = "Payload builder" - )] - wait_for_payload: bool, - - /// Enable denylist checks. When false, no denylist lookups. - #[arg( - long = "arc.denylist.enabled", - default_value_t = false, - help_heading = "Arc denylist" - )] - arc_denylist_enabled: bool, - - /// Denylist address (0x-prefixed). Required when --arc.denylist.enabled is true. - #[arg( - long = "arc.denylist.address", - value_name = "ADDRESS", - help_heading = "Arc denylist" - )] - arc_denylist_address: Option, - - /// ERC-7201 base storage slot (0x-prefixed 32 bytes). Required when --arc.denylist.enabled is true. - #[arg( - long = "arc.denylist.storage-slot", - value_name = "SLOT", - help_heading = "Arc denylist" - )] - arc_denylist_storage_slot: Option, - - /// Comma-separated addresses to exclude from denylist checks (e.g. for ops recovery). - #[arg( - long = "arc.denylist.addresses-exclusions", - value_name = "ADDRESSES", - value_delimiter = ',', - help_heading = "Arc denylist" - )] - arc_denylist_addresses_exclusions: Vec, - - /// Expose pending-tx RPCs on externally-reachable sockets. - /// - /// Off by default: the middleware blocks `eth_subscribe("newPendingTransactions")`, - /// `eth_newPendingTransactionFilter`, and returns null for - /// `eth_getBlockByNumber("pending")` and `eth_getTransactionBySenderAndNonce`. - /// Set this flag on trusted / internal nodes where exposing pending-tx state - /// is desired (e.g. debugging). - #[arg( - long = "arc.expose-pending-txs", - default_value_t = false, - help_heading = "Arc RPC" - )] - arc_expose_pending_txs: bool, - - /// Convenience flag for externally-exposed RPC nodes. - /// - /// Forces hiding of pending-tx RPCs. Conflicts with - /// `--arc.expose-pending-txs`, and warns at startup if `--http.api` or - /// `--ws.api` exposes namespaces outside `{eth, net, web3, rpc}`. - #[arg( - long = "public-api", - default_value_t = false, - conflicts_with = "arc_expose_pending_txs", - help_heading = "Arc RPC" - )] - public_api: bool, - - /// Accept pre-EIP-155 (replay-unprotected) transactions over JSON-RPC. - /// - /// Defaults to false, matching Geth: raw transaction submission RPCs reject - /// transactions whose signature does not encode a chain ID, returning the - /// standard error "only replay-protected (EIP-155) transactions allowed over RPC". - /// Affects the RPC submission path only — transactions received from - /// peers or included in blocks by other validators are still accepted - /// by the txpool and execution layers. - /// - /// Enable on nodes that need to relay legacy deployer transactions - /// (Nick's-method singletons such as CreateX, ERC-2470, ERC-1820). - #[arg( - long = "arc.rpc.allow-unprotected-txs", - default_value_t = false, - help_heading = "Arc RPC" - )] - arc_rpc_allow_unprotected_txs: bool, - - /// Interval in seconds between transaction rebroadcast rounds. - /// - /// Pending transactions are periodically re-announced to all peers to recover - /// from missed gossip. Set to 0 to disable. - #[arg( - long = "txpool.rebroadcast-interval", - value_name = "SECONDS", - default_value_t = 60, - help_heading = "Transaction pool" - )] - txpool_rebroadcast_interval: u64, - - /// Profiling server bind address. - #[arg( - long = "pprof.addr", - value_name = "ADDR", - default_value = "0.0.0.0:6061", - help_heading = "Profiling" - )] - pprof_addr: String, - - /// Activate jemalloc heap profiling at startup. - /// - /// When built with the `pprof` feature, heap profiling infrastructure is - /// always available but inactive by default. This flag activates it so - /// that the `/debug/pprof/allocs` endpoint returns meaningful data. - #[arg( - long = "pprof.heap-prof", - default_value_t = false, - help_heading = "Profiling" - )] - pprof_heap_prof: bool, -} - -/// Build [`AddressesDenylistConfig`] from CLI flags. -/// When enabled, address and storage slot default to genesis constants if not provided. -fn build_addresses_denylist_config(ext: &ArcExtraCli) -> eyre::Result { - use alloy_primitives::{Address, B256}; - - let contract_address = ext - .arc_denylist_address - .as_deref() - .map(|s| s.parse::
()) - .transpose() - .map_err(|e| eyre::eyre!("invalid --arc.denylist.address: {}", e))? - .or(ext.arc_denylist_enabled.then_some(DEFAULT_DENYLIST_ADDRESS)); - - let storage_slot = ext - .arc_denylist_storage_slot - .as_deref() - .map(|s| s.parse::()) - .transpose() - .map_err(|e| eyre::eyre!("invalid --arc.denylist.storage-slot: {}", e))? - .or(ext - .arc_denylist_enabled - .then_some(DEFAULT_DENYLIST_ERC7201_BASE_SLOT)); - - let addresses_exclusions: Vec
= ext - .arc_denylist_addresses_exclusions - .iter() - .map(|s| s.trim().parse::
()) - .collect::, _>>() - .map_err(|e| eyre::eyre!("invalid --arc.denylist.addresses-exclusions: {}", e))?; - - let config = AddressesDenylistConfig::try_new( - ext.arc_denylist_enabled, - contract_address, - storage_slot, - addresses_exclusions, - ) - .map_err(|e| match e { - AddressesDenylistConfigError::MissingContractAddress => { - eyre::eyre!("--arc.denylist.enabled is set but --arc.denylist.address is missing") - } - AddressesDenylistConfigError::MissingStorageSlot => { - eyre::eyre!("--arc.denylist.enabled is set but --arc.denylist.storage-slot is missing") - } - })?; - Ok(config) -} - -/// Namespaces considered safe on a `--public-api` node. -/// -/// Excludes anything that exposes pending / mempool state, admin controls, -/// tracing, MEV endpoints, or implementation-specific internals. -const PUBLIC_API_SAFE_MODULES: [RethRpcModule; 4] = [ - RethRpcModule::Eth, - RethRpcModule::Net, - RethRpcModule::Web3, - RethRpcModule::Rpc, -]; - -/// Returns the modules in `selection` that are not in `PUBLIC_API_SAFE_MODULES`. -/// `RethRpcModule::Other(_)` is always considered unsafe. -pub(crate) fn unsafe_public_api_modules(selection: &RpcModuleSelection) -> Vec { - let safe: HashSet = PUBLIC_API_SAFE_MODULES.into_iter().collect(); - selection - .to_selection() - .into_iter() - .filter(|m| !safe.contains(m)) - .collect() -} - -/// Emits a `warn!` if `selection` contains modules outside the safe set. -/// `None` is safe: Reth's default is `Standard` = {eth, net, web3}, a subset of our safe set. -fn warn_if_public_api_unsafe(selection: Option<&RpcModuleSelection>, socket_flag: &str) { - let Some(sel) = selection else { return }; - let unsafe_modules = unsafe_public_api_modules(sel); - if !unsafe_modules.is_empty() { - let names: Vec = unsafe_modules.iter().map(|m| m.to_string()).collect(); - tracing::warn!( - "--public-api set but {socket_flag} exposes sensitive namespaces: {names:?}. \ - Consider dropping them or removing --public-api to acknowledge the risk." - ); - } -} - -/// Computes whether the pending-tx RPC filter should be active for this run. -/// `--public-api` wins; clap enforces it can't coexist with `--arc.expose-pending-txs`. -fn compute_filter_pending_txs(ext: &ArcExtraCli) -> bool { - ext.public_api || !ext.arc_expose_pending_txs -} - -/// Parses `--arc.rpc.max-batch-entries`, rejecting `0` so the cap is never silently disabled. -fn parse_max_batch_entries(s: &str) -> Result { - let n: usize = s.parse().map_err(|_| format!("invalid number: {s}"))?; - if n == 0 { - return Err("must be >= 1".to_string()); - } - Ok(n) -} - -/// Number of bodies, receipts, etc. to retain after pruning. -/// See init_arc_pruning for more details. -const PRESETS_PRUNE_DISTANCE: u64 = 237_600; -const FLAG_FULL: &str = "--full"; -const FLAG_MINIMAL: &str = "--minimal"; -const FLAG_BLOCK_INTERVAL: &str = "--prune.block-interval=5000"; -const FLAG_DATADIR: &str = "--datadir"; - -/// Registers Arc-specific `DefaultPruningValues` with Reth's global static, then injects -/// Arc defaults into argv: -/// - `--prune.block-interval=5000` whenever `--full` or `--minimal` is present -/// - `--datadir=~/.arc/execution` unless the user already supplied `--datadir` -fn init_arc_pruning(argv: I) -> Vec -where - I: IntoIterator, - S: Into, -{ - // Register Arc-specific pruning defaults. This must happen before clap parses --full / - // --minimal, so that DefaultPruningValues::get_global() returns our values. - let _ = DefaultPruningValues::default() - .with_full_prune_modes(PruneModes { - sender_recovery: Some(PruneMode::Full), - transaction_lookup: Some(PruneMode::Distance(PRESETS_PRUNE_DISTANCE)), - receipts: Some(PruneMode::Distance(PRESETS_PRUNE_DISTANCE)), - account_history: Some(PruneMode::Distance(PRESETS_PRUNE_DISTANCE)), - storage_history: Some(PruneMode::Distance(PRESETS_PRUNE_DISTANCE)), - bodies_history: Some(PruneMode::Distance(PRESETS_PRUNE_DISTANCE)), - receipts_log_filter: Default::default(), - }) - .with_full_bodies_history_use_pre_merge(false) - .with_minimal_prune_modes(PruneModes { - sender_recovery: Some(PruneMode::Full), - transaction_lookup: Some(PruneMode::Distance(64)), // Can be `Full`, but we use 64 here because our smoke tests rely on tx lookup - receipts: Some(PruneMode::Distance(64)), // Min enforced by Reth - account_history: Some(PruneMode::Distance(10064)), // Min enforced by Reth - storage_history: Some(PruneMode::Distance(10064)), // Min enforced by Reth - bodies_history: Some(PruneMode::Distance(PRESETS_PRUNE_DISTANCE)), - receipts_log_filter: Default::default(), - }) - .try_init(); - - // Collect argv so we can inspect it before rewriting. - let mut args: Vec = argv.into_iter().map(Into::into).collect(); - - // Inject --prune.block-interval=5000 when --full or --minimal is present, - // unless the user already supplied one. - let has_preset = args - .iter() - .any(|a| matches!(a.to_str(), Some(FLAG_FULL) | Some(FLAG_MINIMAL))); - let has_explicit_block_interval = args.iter().any(|a| { - a.to_str() - .is_some_and(|s| s.starts_with("--prune.block-interval")) - }); - if has_preset && !has_explicit_block_interval { - args.push(std::ffi::OsString::from(FLAG_BLOCK_INTERVAL)); - } - - // Inject --datadir=~/.arc/execution unless the user already supplied --datadir. - // Only inject for subcommands that accept --datadir; skip the ones that don't. - const SUBCOMMANDS_WITH_DATADIR: &[&str] = &[ - // Keep in sync with Reth subcommands that accept --datadir (as of Reth v1.11.3). - // When upgrading Reth, check for new subcommands and update this list. - "node", - "init", - "init-state", - "import", - "import-era", - "export-era", - "db", - "download", - "stage", - "prune", - "re-execute", - ]; - let has_datadir_subcommand = args.iter().any(|a| { - a.to_str() - .is_some_and(|s| SUBCOMMANDS_WITH_DATADIR.contains(&s)) - }); - let has_explicit_datadir = args.iter().any(|a| { - a.to_str() - .is_some_and(|s| s == FLAG_DATADIR || s.starts_with("--datadir=")) - }); - if has_datadir_subcommand && !has_explicit_datadir { - if let Some(home) = BaseDirs::new().map(|d| d.home_dir().to_path_buf()) { - let datadir = home.join(".arc").join("execution"); - args.push(std::ffi::OsString::from(format!( - "--datadir={}", - datadir.display() - ))); - } - } - - args -} - -fn main() { - // Initialize Arc Network defaults (download URLs, etc.) before parsing CLI - defaults::init_defaults(); - - let argv = init_arc_pruning(std::env::args_os()); - let patched_cmd = patch_node_command_defaults(ArcCli::command()); - let cli = - ArcCli::from_arg_matches(&patched_cmd.get_matches_from(argv)).unwrap_or_else(|e| e.exit()); - if let Err(err) = cli.validate() { - eprintln!("Error: {err}"); - std::process::exit(1); - } - - let addresses_denylist_config = match &cli.inner.command { - Commands::Node(cmd) => build_addresses_denylist_config(&cmd.ext).unwrap_or_else(|e| { - eprintln!("Error: {e}"); - std::process::exit(1); - }), - _ => AddressesDenylistConfig::default(), - }; - if let Err(err) = cli.inner.run_with_components::( - arc_components, - |mut builder: WithLaunchContext>, - ext: ArcExtraCli| async move { - let arc_rpc_cfg = - ArcRpcConfig::new(ext.enable_arc_rpc, ext.arc_rpc_upstream_url.clone()); - let invalid_tx_list_cfg = - InvalidTxListConfig::new(ext.invalid_tx_list_enable, ext.invalid_tx_list_cap); - let payload_builder_deadline_ms = ext.payload_builder_deadline_ms; - - if ext.public_api { - let rpc = &builder.config().rpc; - warn_if_public_api_unsafe(rpc.http_api.as_ref(), "--http.api"); - warn_if_public_api_unsafe(rpc.ws_api.as_ref(), "--ws.api"); - } - - // Run an RPC node if enabled (unsafe - no verification) - if let Some(ref unsafe_follow_url) = ext.unsafe_follow_url { - follow_url_for_consensus(&mut builder, unsafe_follow_url)?; - } - - // Log version information when node is actually starting - info!( - version = arc_version::GIT_VERSION, - commit = arc_version::GIT_COMMIT_HASH, - "Arc Execution EL starting" - ); - - // Register version information in metrics - arc_node_execution::metrics::register_version_info(); - - let wait_for_payload = ext.wait_for_payload; - let filter_pending_txs = compute_filter_pending_txs(&ext); - let allow_unprotected_txs = ext.arc_rpc_allow_unprotected_txs; - let max_response_body_size = builder.config().rpc.rpc_max_response_size_bytes(); - let max_batch_entries = ext.arc_rpc_max_batch_entries; - let rebroadcast_interval = - std::time::Duration::from_secs(ext.txpool_rebroadcast_interval); - let handle = builder - .node(ArcNode::new( - arc_rpc_cfg, - invalid_tx_list_cfg, - addresses_denylist_config, - payload_builder_deadline_ms, - wait_for_payload, - filter_pending_txs, - allow_unprotected_txs, - max_response_body_size, - max_batch_entries, - rebroadcast_interval, - )) - .launch_with_debug_capabilities() - .await?; - - spawn_pprof_server(ext.pprof_addr.parse()?, ext.pprof_heap_prof); - - #[cfg(unix)] - install_sigterm_handler(handle.node.add_ons_handle.engine_shutdown.clone()); - - handle.node_exit_future.await - }, - ) { - eprintln!("Error: {err:?}"); - std::process::exit(1); - } -} - -/// Install a SIGTERM handler to gracefully shutdown the engine. -/// -/// When SIGTERM is received, triggers engine shutdown so in-memory blocks are persisted -/// before the process exits. The main `node_exit_future` will complete when the engine -/// shuts down. -/// -/// # Note -/// This is only available on Unix systems. -#[cfg(unix)] -fn install_sigterm_handler(engine_shutdown: reth_node_builder::rpc::EngineShutdown) { - use tokio::signal::unix::{signal, SignalKind}; - use tokio::time::{timeout, Duration}; - - match signal(SignalKind::terminate()) { - Ok(mut sigterm) => { - tokio::spawn(async move { - if sigterm.recv().await.is_some() { - tracing::info!(target: "arc::node", "Received SIGTERM, shutting down engine..."); - - // A second SIGTERM during shutdown forces an immediate exit. - tokio::spawn(async move { - if sigterm.recv().await.is_some() { - tracing::warn!(target: "arc::node", "Received second SIGTERM, forcing exit"); - std::process::exit(143); - } - }); - - if let Some(done_rx) = engine_shutdown.shutdown() { - match timeout(Duration::from_secs(30), done_rx).await { - Ok(Ok(_)) => { - tracing::info!(target: "arc::node", "Engine shutdown complete"); - } - Ok(Err(err)) => { - tracing::error!(target: "arc::node", ?err, "Engine shutdown failed"); - } - Err(_) => { - tracing::error!( - target: "arc::node", - "Engine shutdown timed out after 30s" - ); - } - } - } else { - tracing::warn!(target: "arc::node", "Engine shutdown channel already closed"); - } - - // Exit with the conventional SIGTERM code (128 + 15). - std::process::exit(143); - } - }); - } - Err(err) => { - tracing::warn!( - target: "arc::node", - %err, - "Failed to register SIGTERM handler; graceful engine shutdown on SIGTERM will not be available" - ); - } - } -} - -#[cfg(not(unix))] -fn install_sigterm_handler(_engine_shutdown: reth_node_builder::rpc::EngineShutdown) {} - -#[cfg(feature = "pprof")] -fn spawn_pprof_server(bind_address: std::net::SocketAddr, heap_prof: bool) { - if heap_prof { - // SAFETY: writing a bool to a well-known jemalloc mallctl key. - if let Err(e) = unsafe { tikv_jemalloc_ctl::raw::write(b"prof.active\0", true) } { - tracing::error!(error = %e, "failed to activate jemalloc heap profiling; /debug/pprof/allocs will return empty profiles"); - } else { - tracing::info!("jemalloc heap profiling activated"); - } - } - - tokio::spawn(async move { - if let Err(e) = - pprof_hyper_server::serve(bind_address, pprof_hyper_server::Config::default()).await - { - tracing::error!( - error = %e, - "pprof server failed to start" - ); - } - }); -} - -#[cfg(not(feature = "pprof"))] -fn spawn_pprof_server(_bind_address: std::net::SocketAddr, _heap_prof: bool) {} - -#[cfg(test)] -mod tests { - use super::*; - use alloy_primitives::{address, b256}; - - /// Parse CLI args with `patch_node_command_defaults` applied (mirrors production). - fn parse_with_arc_defaults(argv: I) -> ArcCli - where - I: IntoIterator, - { - let patched = patch_node_command_defaults(ArcCli::command()); - ArcCli::from_arg_matches(&patched.get_matches_from(argv)).unwrap() - } - - #[test] - fn test_extradata_default_is_allowed() { - let cli = parse_with_arc_defaults(["arc-node-execution", "node"]); - assert!(cli.validate().is_ok()); - } - - #[test] - fn test_extradata_custom_is_rejected() { - let cli = parse_with_arc_defaults([ - "arc-node-execution", - "node", - "--builder.extradata", - "custom", - ]); - assert_eq!(cli.validate(), Err("--builder.extradata is not supported")); - } - - #[test] - fn test_validate_rejects_filter_with_pending_block_full() { - let cli = - parse_with_arc_defaults(["arc-node-execution", "node", "--rpc.pending-block=full"]); - assert!( - cli.validate() - .unwrap_err() - .contains("--rpc.pending-block must be 'none'"), - "default filter + --rpc.pending-block=full must be rejected" - ); - } - - #[test] - fn test_validate_allows_expose_with_pending_block_full() { - let cli = parse_with_arc_defaults([ - "arc-node-execution", - "node", - "--arc.expose-pending-txs", - "--rpc.pending-block=full", - ]); - assert!( - cli.validate().is_ok(), - "--arc.expose-pending-txs + --rpc.pending-block=full must be allowed" - ); - } - - #[test] - fn test_validate_rejects_public_api_with_pending_block_full() { - let cli = parse_with_arc_defaults([ - "arc-node-execution", - "node", - "--public-api", - "--rpc.pending-block=full", - ]); - assert!( - cli.validate() - .unwrap_err() - .contains("--rpc.pending-block must be 'none'"), - "--public-api + --rpc.pending-block=full must be rejected" - ); - } - - #[test] - fn test_pending_block_default_is_none() { - let patched = patch_node_command_defaults(ArcCli::command()); - let cli = - ArcCli::from_arg_matches(&patched.get_matches_from(["arc-node-execution", "node"])) - .unwrap(); - if let Commands::Node(node_cmd) = cli.inner.command { - assert_eq!( - node_cmd.rpc.rpc_pending_block, - reth_rpc_eth_types::builder::config::PendingBlockKind::None, - "Arc default for --rpc.pending-block should be none" - ); - } else { - panic!("Expected Node command"); - } - } - - #[test] - fn test_invalid_tx_list_flags_default_values() { - let cli = ArcCli::try_parse_from(["arc-node-execution", "node"]).unwrap(); - if let Commands::Node(node_cmd) = cli.inner.command { - assert!(node_cmd.ext.invalid_tx_list_enable); - assert_eq!(node_cmd.ext.invalid_tx_list_cap, 100_000); - } else { - panic!("Expected Node command"); - } - } - - #[test] - fn test_invalid_tx_list_flag_explicit_disable() { - let cli = ArcCli::try_parse_from([ - "arc-node-execution", - "node", - "--invalid-tx-list-enable=false", - ]) - .unwrap(); - if let Commands::Node(node_cmd) = cli.inner.command { - assert!(!node_cmd.ext.invalid_tx_list_enable); - } else { - panic!("Expected Node command"); - } - } - - #[test] - fn test_invalid_tx_list_flags_custom_values() { - let cli = ArcCli::try_parse_from([ - "arc-node-execution", - "node", - "--invalid-tx-list-enable=false", - "--invalid-tx-list-cap", - "50000", - ]) - .unwrap(); - if let Commands::Node(node_cmd) = cli.inner.command { - assert!(!node_cmd.ext.invalid_tx_list_enable); - assert_eq!(node_cmd.ext.invalid_tx_list_cap, 50000); - } else { - panic!("Expected Node command"); - } - } - - #[test] - fn test_invalid_tx_list_cap_invalid_value_rejected() { - let result = ArcCli::try_parse_from([ - "arc-node-execution", - "node", - "--invalid-tx-list-cap", - "notanumber", - ]); - assert!(result.is_err_and(|err| err.to_string().contains("invalid value"))); - } - - #[test] - fn test_invalid_tx_list_cap_overflow_rejected() { - let result = ArcCli::try_parse_from([ - "arc-node-execution", - "node", - "--invalid-tx-list-cap", - &u128::MAX.to_string(), - ]); - assert!(result.is_err_and(|err| err.to_string().contains("invalid value"))); - } - - #[test] - fn test_arc_rpc_max_batch_entries_default() { - let cli = ArcCli::try_parse_from(["arc-node-execution", "node"]).unwrap(); - if let Commands::Node(node_cmd) = cli.inner.command { - assert_eq!( - node_cmd.ext.arc_rpc_max_batch_entries, - ARC_RPC_MAX_BATCH_ENTRIES_DEFAULT - ); - } else { - panic!("Expected Node command"); - } - } - - #[test] - fn test_arc_rpc_max_batch_entries_custom() { - let cli = ArcCli::try_parse_from([ - "arc-node-execution", - "node", - "--arc.rpc.max-batch-entries", - "250", - ]) - .unwrap(); - if let Commands::Node(node_cmd) = cli.inner.command { - assert_eq!(node_cmd.ext.arc_rpc_max_batch_entries, 250); - } else { - panic!("Expected Node command"); - } - } - - #[test] - fn test_arc_rpc_max_batch_entries_zero_rejected() { - let result = ArcCli::try_parse_from([ - "arc-node-execution", - "node", - "--arc.rpc.max-batch-entries", - "0", - ]); - assert!( - result.is_err_and(|err| err.to_string().contains("must be >= 1")), - "0 should be rejected with the must-be->=1 message" - ); - } - - #[test] - fn test_arc_builder_deadline_default_unset() { - let cli = ArcCli::try_parse_from(["arc-node-execution", "node"]).unwrap(); - if let Commands::Node(node_cmd) = cli.inner.command { - assert!(node_cmd.ext.payload_builder_deadline_ms.is_none()); - } else { - panic!("Expected Node command"); - } - } - - #[test] - fn test_arc_builder_deadline_custom_value() { - let cli = ArcCli::try_parse_from([ - "arc-node-execution", - "node", - "--arc.builder.deadline", - "900", - ]) - .unwrap(); - if let Commands::Node(node_cmd) = cli.inner.command { - assert_eq!(node_cmd.ext.payload_builder_deadline_ms, Some(900)); - } else { - panic!("Expected Node command"); - } - } - - #[test] - fn test_wait_for_payload_default_is_true() { - let cli = ArcCli::try_parse_from(["arc-node-execution", "node"]).unwrap(); - if let Commands::Node(node_cmd) = cli.inner.command { - assert!(node_cmd.ext.wait_for_payload); - } else { - panic!("Expected Node command"); - } - } - - #[test] - fn test_wait_for_payload_disabled() { - let cli = ArcCli::try_parse_from([ - "arc-node-execution", - "node", - "--arc.builder.wait-for-payload=false", - ]) - .unwrap(); - if let Commands::Node(node_cmd) = cli.inner.command { - assert!(!node_cmd.ext.wait_for_payload); - } else { - panic!("Expected Node command"); - } - } - - #[test] - fn test_arc_rpc_allow_unprotected_txs_default_is_false() { - let cli = ArcCli::try_parse_from(["arc-node-execution", "node"]).unwrap(); - if let Commands::Node(node_cmd) = cli.inner.command { - assert!( - !node_cmd.ext.arc_rpc_allow_unprotected_txs, - "default must reject pre-EIP-155 txs over RPC" - ); - } else { - panic!("Expected Node command"); - } - } - - #[test] - fn test_arc_rpc_allow_unprotected_txs_explicit() { - let cli = ArcCli::try_parse_from([ - "arc-node-execution", - "node", - "--arc.rpc.allow-unprotected-txs", - ]) - .unwrap(); - if let Commands::Node(node_cmd) = cli.inner.command { - assert!(node_cmd.ext.arc_rpc_allow_unprotected_txs); - } else { - panic!("Expected Node command"); - } - } - - #[test] - fn test_arc_denylist_flags_default_values() { - let cli = ArcCli::try_parse_from(["arc-node-execution", "node"]).unwrap(); - if let Commands::Node(node_cmd) = cli.inner.command { - assert!(!node_cmd.ext.arc_denylist_enabled); - assert!(node_cmd.ext.arc_denylist_address.is_none()); - assert!(node_cmd.ext.arc_denylist_storage_slot.is_none()); - assert!(node_cmd.ext.arc_denylist_addresses_exclusions.is_empty()); - } else { - panic!("Expected Node command"); - } - } - - #[test] - fn test_arc_denylist_flags_custom_values() { - let cli = ArcCli::try_parse_from([ - "arc-node-execution", - "node", - "--arc.denylist.enabled", - "--arc.denylist.address", - "0x3600000000000000000000000000000000000001", - "--arc.denylist.storage-slot", - "0x0000000000000000000000000000000000000000000000000000000000000001", - "--arc.denylist.addresses-exclusions", - "0x1000000000000000000000000000000000000001,0x1000000000000000000000000000000000000002", - ]) - .unwrap(); - if let Commands::Node(node_cmd) = cli.inner.command { - assert!(node_cmd.ext.arc_denylist_enabled); - assert_eq!( - node_cmd.ext.arc_denylist_address.as_deref(), - Some("0x3600000000000000000000000000000000000001") - ); - assert_eq!( - node_cmd.ext.arc_denylist_storage_slot.as_deref(), - Some("0x0000000000000000000000000000000000000000000000000000000000000001") - ); - assert_eq!(node_cmd.ext.arc_denylist_addresses_exclusions.len(), 2); - } else { - panic!("Expected Node command"); - } - } - - #[test] - fn test_build_addresses_denylist_config_default() { - let cli = ArcCli::try_parse_from(["arc-node-execution", "node"]).unwrap(); - let ext = match &cli.inner.command { - Commands::Node(cmd) => &cmd.ext, - _ => panic!("Expected Node command"), - }; - let cfg = build_addresses_denylist_config(ext).unwrap(); - assert!(!cfg.is_enabled()); - } - - #[test] - fn test_build_addresses_denylist_config_enabled_uses_default_address_and_slot() { - let cli = ArcCli::try_parse_from(["arc-node-execution", "node", "--arc.denylist.enabled"]) - .unwrap(); - let ext = match &cli.inner.command { - Commands::Node(cmd) => &cmd.ext, - _ => panic!("Expected Node command"), - }; - let cfg = build_addresses_denylist_config(ext).unwrap(); - - if let AddressesDenylistConfig::Enabled { - contract_address, - storage_slot, - addresses_exclusions, - } = &cfg - { - assert_eq!(*contract_address, DEFAULT_DENYLIST_ADDRESS); - assert_eq!(*storage_slot, DEFAULT_DENYLIST_ERC7201_BASE_SLOT); - assert!(addresses_exclusions.is_empty()); - } else { - panic!("Expected Enabled variant"); - } - } - - #[test] - fn test_build_addresses_denylist_config_enabled_with_address_uses_default_slot() { - let cli = ArcCli::try_parse_from([ - "arc-node-execution", - "node", - "--arc.denylist.enabled", - "--arc.denylist.address", - "0x3600000000000000000000000000000000000001", - ]) - .unwrap(); - let ext = match &cli.inner.command { - Commands::Node(cmd) => &cmd.ext, - _ => panic!("Expected Node command"), - }; - let cfg = build_addresses_denylist_config(ext).unwrap(); - - if let AddressesDenylistConfig::Enabled { - contract_address, - storage_slot, - addresses_exclusions, - } = &cfg - { - assert_eq!( - *contract_address, - address!("0x3600000000000000000000000000000000000001") - ); - assert_eq!(*storage_slot, DEFAULT_DENYLIST_ERC7201_BASE_SLOT); - assert!(addresses_exclusions.is_empty()); - } else { - panic!("Expected Enabled variant"); - } - } - - #[test] - fn test_build_addresses_denylist_config_enabled_with_both_succeeds() { - let cli = ArcCli::try_parse_from([ - "arc-node-execution", - "node", - "--arc.denylist.enabled", - "--arc.denylist.address", - "0x3600000000000000000000000000000000000001", - "--arc.denylist.storage-slot", - "0x0000000000000000000000000000000000000000000000000000000000000001", - ]) - .unwrap(); - let ext = match &cli.inner.command { - Commands::Node(cmd) => &cmd.ext, - _ => panic!("Expected Node command"), - }; - let cfg = build_addresses_denylist_config(ext).unwrap(); - - if let AddressesDenylistConfig::Enabled { - contract_address, - storage_slot, - addresses_exclusions, - } = &cfg - { - assert_eq!( - *contract_address, - address!("0x3600000000000000000000000000000000000001") - ); - assert_eq!( - *storage_slot, - b256!("0x0000000000000000000000000000000000000000000000000000000000000001") - ); - assert!(addresses_exclusions.is_empty()); - } else { - panic!("Expected Enabled variant"); - } - } - - #[test] - fn test_build_addresses_denylist_config_invalid_address_rejected() { - let cli = ArcCli::try_parse_from([ - "arc-node-execution", - "node", - "--arc.denylist.address", - "not-an-address", - ]) - .unwrap(); - let ext = match &cli.inner.command { - Commands::Node(cmd) => &cmd.ext, - _ => panic!("Expected Node command"), - }; - let err = build_addresses_denylist_config(ext).unwrap_err(); - assert!(err.to_string().contains("invalid --arc.denylist.address")); - } - - #[test] - fn test_build_addresses_denylist_config_invalid_storage_slot_rejected() { - let cli = ArcCli::try_parse_from([ - "arc-node-execution", - "node", - "--arc.denylist.storage-slot", - "0x1234", // too short for 32 bytes - ]) - .unwrap(); - let ext = match &cli.inner.command { - Commands::Node(cmd) => &cmd.ext, - _ => panic!("Expected Node command"), - }; - let err = build_addresses_denylist_config(ext).unwrap_err(); - assert!(err - .to_string() - .contains("invalid --arc.denylist.storage-slot")); - } - - #[test] - fn test_build_addresses_denylist_config_enabled_with_exclusions_succeeds() { - let cli = ArcCli::try_parse_from([ - "arc-node-execution", - "node", - "--arc.denylist.enabled", - "--arc.denylist.addresses-exclusions", - "0x3600000000000000000000000000000000000001,0x3600000000000000000000000000000000000002", - ]) - .unwrap(); - - let ext = match &cli.inner.command { - Commands::Node(cmd) => &cmd.ext, - _ => panic!("Expected Node command"), - }; - let cfg = build_addresses_denylist_config(ext).unwrap(); - - if let AddressesDenylistConfig::Enabled { - contract_address, - storage_slot, - addresses_exclusions, - } = &cfg - { - assert_eq!(*contract_address, DEFAULT_DENYLIST_ADDRESS); - assert_eq!(*storage_slot, DEFAULT_DENYLIST_ERC7201_BASE_SLOT); - assert_eq!(addresses_exclusions.len(), 2); - assert_eq!( - addresses_exclusions[0], - address!("0x3600000000000000000000000000000000000001") - ); - assert_eq!( - addresses_exclusions[1], - address!("0x3600000000000000000000000000000000000002") - ); - } else { - panic!("Expected Enabled variant"); - } - } - - #[test] - fn test_arc_expose_pending_txs_default_is_false() { - let cli = ArcCli::try_parse_from(["arc-node-execution", "node"]).unwrap(); - if let Commands::Node(node_cmd) = cli.inner.command { - assert!( - !node_cmd.ext.arc_expose_pending_txs, - "Default: --arc.expose-pending-txs should be false (pending txs hidden by default)" - ); - } else { - panic!("Expected Node command"); - } - } - - #[test] - fn test_arc_expose_pending_txs_when_set() { - let cli = - ArcCli::try_parse_from(["arc-node-execution", "node", "--arc.expose-pending-txs"]) - .unwrap(); - if let Commands::Node(node_cmd) = cli.inner.command { - assert!( - node_cmd.ext.arc_expose_pending_txs, - "--arc.expose-pending-txs should flip the flag" - ); - } else { - panic!("Expected Node command"); - } - } - - #[test] - fn test_public_api_default_is_false() { - let cli = ArcCli::try_parse_from(["arc-node-execution", "node"]).unwrap(); - if let Commands::Node(node_cmd) = cli.inner.command { - assert!( - !node_cmd.ext.public_api, - "Default: --public-api should be false" - ); - } else { - panic!("Expected Node command"); - } - } - - #[test] - fn test_public_api_when_set() { - let cli = ArcCli::try_parse_from(["arc-node-execution", "node", "--public-api"]).unwrap(); - if let Commands::Node(node_cmd) = cli.inner.command { - assert!(node_cmd.ext.public_api, "--public-api should flip the flag"); - } else { - panic!("Expected Node command"); - } - } - - #[test] - fn test_public_api_conflicts_with_expose_pending_txs() { - use clap::error::ErrorKind; - - let err = ArcCli::try_parse_from([ - "arc-node-execution", - "node", - "--public-api", - "--arc.expose-pending-txs", - ]) - .unwrap_err(); - assert_eq!( - err.kind(), - ErrorKind::ArgumentConflict, - "clap should reject --public-api + --arc.expose-pending-txs as a conflict" - ); - } - - #[test] - fn test_public_api_enables_filter() { - let cli = ArcCli::try_parse_from(["arc-node-execution", "node", "--public-api"]).unwrap(); - if let Commands::Node(node_cmd) = cli.inner.command { - assert!( - compute_filter_pending_txs(&node_cmd.ext), - "--public-api alone must enable the filter" - ); - } else { - panic!("Expected Node command"); - } - } - - #[test] - fn test_compute_filter_pending_txs_default_hides() { - let cli = ArcCli::try_parse_from(["arc-node-execution", "node"]).unwrap(); - if let Commands::Node(node_cmd) = cli.inner.command { - assert!( - compute_filter_pending_txs(&node_cmd.ext), - "default config must keep the filter on" - ); - } else { - panic!("Expected Node command"); - } - } - - #[test] - fn test_compute_filter_pending_txs_expose_disables() { - let cli = - ArcCli::try_parse_from(["arc-node-execution", "node", "--arc.expose-pending-txs"]) - .unwrap(); - if let Commands::Node(node_cmd) = cli.inner.command { - assert!( - !compute_filter_pending_txs(&node_cmd.ext), - "--arc.expose-pending-txs must disable the filter" - ); - } else { - panic!("Expected Node command"); - } - } - - #[test] - fn test_pprof_heap_prof_default_is_false() { - let cli = ArcCli::try_parse_from(["arc-node-execution", "node"]).unwrap(); - if let Commands::Node(node_cmd) = cli.inner.command { - assert!(!node_cmd.ext.pprof_heap_prof); - } else { - panic!("Expected Node command"); - } - } - - #[test] - fn test_pprof_heap_prof_when_set() { - let cli = - ArcCli::try_parse_from(["arc-node-execution", "node", "--pprof.heap-prof"]).unwrap(); - if let Commands::Node(node_cmd) = cli.inner.command { - assert!(node_cmd.ext.pprof_heap_prof); - } else { - panic!("Expected Node command"); - } - } - - /// --full gets --prune.block-interval=5000 injected. - #[test] - fn test_full_preset_argv_translation() { - let argv = init_arc_pruning(["arc-node", "node", "--full"]); - let translated: Vec<_> = argv - .iter() - .map(|s| s.to_str().unwrap().to_owned()) - .collect(); - assert!( - translated.contains(&"--full".to_owned()), - "must retain --full" - ); - assert!( - translated.iter().any(|s| s == FLAG_BLOCK_INTERVAL), - "must inject --prune.block-interval" - ); - } - - /// --minimal gets --prune.block-interval=5000 injected. - #[test] - fn test_minimal_preset_argv_translation() { - let argv = init_arc_pruning(["arc-node", "node", "--minimal"]); - let translated: Vec<_> = argv - .iter() - .map(|s| s.to_str().unwrap().to_owned()) - .collect(); - assert!( - translated.contains(&"--minimal".to_owned()), - "must retain --minimal" - ); - assert!( - translated.iter().any(|s| s == FLAG_BLOCK_INTERVAL), - "must inject --prune.block-interval" - ); - } - - /// Explicit --prune.block-interval overrides the injected default. - #[test] - fn test_full_preset_explicit_block_interval_overrides() { - let argv = init_arc_pruning(["arc-node", "node", "--full", "--prune.block-interval=1000"]); - let translated: Vec<_> = argv - .iter() - .map(|s| s.to_str().unwrap().to_owned()) - .collect(); - assert!( - translated.contains(&"--full".to_owned()), - "must retain --full" - ); - assert!( - translated.contains(&"--prune.block-interval=1000".to_owned()), - "must keep user-supplied block interval" - ); - assert!( - !translated.contains(&FLAG_BLOCK_INTERVAL.to_owned()), - "must not inject default block interval when user supplied one" - ); - } - - /// Unrelated args are passed through and --datadir is injected. - #[test] - fn test_arc_pruning_init_injects_datadir() { - let argv = init_arc_pruning(["arc-node", "node", "--http"]); - let translated: Vec<_> = argv - .iter() - .map(|s| s.to_str().unwrap().to_owned()) - .collect(); - assert!(translated.contains(&"arc-node".to_owned())); - assert!(translated.contains(&"--http".to_owned())); - assert!( - translated.iter().any(|s| s.starts_with("--datadir=")), - "must inject --datadir" - ); - assert!( - translated.iter().any(|s| s.contains(".arc/execution")), - "--datadir must point to ~/.arc/execution" - ); - } - - /// Explicit --datadir is not overridden. - #[test] - fn test_arc_pruning_explicit_datadir_not_overridden() { - let argv = init_arc_pruning(["arc-node", "node", "--datadir=/custom/path"]); - let translated: Vec<_> = argv - .iter() - .map(|s| s.to_str().unwrap().to_owned()) - .collect(); - assert!(translated.contains(&"--datadir=/custom/path".to_owned())); - assert_eq!( - translated - .iter() - .filter(|s| s.starts_with("--datadir")) - .count(), - 1, - "must not inject a second --datadir" - ); - } - - /// Subcommands that don't accept --datadir must not receive the injected flag. - #[test] - fn test_arc_pruning_no_datadir_for_p2p() { - let argv = init_arc_pruning(["arc-node", "p2p"]); - let translated: Vec<_> = argv - .iter() - .map(|s| s.to_str().unwrap().to_owned()) - .collect(); - assert!( - !translated.iter().any(|s| s.starts_with("--datadir")), - "p2p must not receive --datadir" - ); - } - - #[test] - fn test_arc_pruning_no_datadir_for_config() { - let argv = init_arc_pruning(["arc-node", "config"]); - let translated: Vec<_> = argv - .iter() - .map(|s| s.to_str().unwrap().to_owned()) - .collect(); - assert!( - !translated.iter().any(|s| s.starts_with("--datadir")), - "config must not receive --datadir" - ); - } - - #[test] - fn test_arc_pruning_no_datadir_for_dump_genesis() { - let argv = init_arc_pruning(["arc-node", "dump-genesis"]); - let translated: Vec<_> = argv - .iter() - .map(|s| s.to_str().unwrap().to_owned()) - .collect(); - assert!( - !translated.iter().any(|s| s.starts_with("--datadir")), - "dump-genesis must not receive --datadir" - ); - } - - #[test] - fn test_txpool_rebroadcast_interval_default() { - let cli = ArcCli::try_parse_from(["arc-node-execution", "node"]).unwrap(); - if let Commands::Node(node_cmd) = cli.inner.command { - assert_eq!(node_cmd.ext.txpool_rebroadcast_interval, 60); - } else { - panic!("Expected Node command"); - } - } - - #[test] - fn test_txpool_rebroadcast_interval_custom() { - let cli = ArcCli::try_parse_from([ - "arc-node-execution", - "node", - "--txpool.rebroadcast-interval", - "120", - ]) - .unwrap(); - if let Commands::Node(node_cmd) = cli.inner.command { - assert_eq!(node_cmd.ext.txpool_rebroadcast_interval, 120); - } else { - panic!("Expected Node command"); - } - } - - #[test] - fn test_txpool_rebroadcast_interval_zero_disables() { - let cli = ArcCli::try_parse_from([ - "arc-node-execution", - "node", - "--txpool.rebroadcast-interval", - "0", - ]) - .unwrap(); - if let Commands::Node(node_cmd) = cli.inner.command { - assert_eq!(node_cmd.ext.txpool_rebroadcast_interval, 0); - } else { - panic!("Expected Node command"); - } - } -} - -#[cfg(test)] -mod public_api_tests { - use super::*; - - #[test] - fn unsafe_modules_empty_when_selection_is_subset_of_safe() { - let sel = RpcModuleSelection::try_from_selection(["eth", "net", "web3", "rpc"]).unwrap(); - let unsafe_ = unsafe_public_api_modules(&sel); - assert!(unsafe_.is_empty(), "eth/net/web3/rpc are all safe"); - } - - #[test] - fn unsafe_modules_lists_sensitive_namespaces() { - let sel = RpcModuleSelection::try_from_selection([ - "eth", "net", "web3", "txpool", "debug", "trace", "admin", - ]) - .unwrap(); - let unsafe_: HashSet<_> = unsafe_public_api_modules(&sel).into_iter().collect(); - assert_eq!( - unsafe_, - HashSet::from([ - RethRpcModule::Txpool, - RethRpcModule::Debug, - RethRpcModule::Trace, - RethRpcModule::Admin, - ]) - ); - } - - #[test] - fn unsafe_modules_treats_other_as_unsafe() { - let sel = RpcModuleSelection::try_from_selection(["eth", "custom"]).unwrap(); - let unsafe_ = unsafe_public_api_modules(&sel); - assert_eq!(unsafe_.len(), 1); - assert!(matches!(unsafe_[0], RethRpcModule::Other(_))); - } - - #[test] - fn unsafe_modules_handles_all_selection() { - let sel = RpcModuleSelection::All; - let unsafe_ = unsafe_public_api_modules(&sel); - assert!(!unsafe_.is_empty()); - assert!(!unsafe_.contains(&RethRpcModule::Eth)); - assert!(!unsafe_.contains(&RethRpcModule::Rpc)); - } - - #[tracing_test::traced_test] - #[test] - fn warn_if_public_api_unsafe_none_is_silent() { - warn_if_public_api_unsafe(None, "--http.api"); - assert!(!logs_contain("sensitive namespaces")); - } - - #[tracing_test::traced_test] - #[test] - fn warn_if_public_api_unsafe_safe_selection_is_silent() { - let sel = RpcModuleSelection::try_from_selection(["eth", "net", "web3"]).unwrap(); - warn_if_public_api_unsafe(Some(&sel), "--http.api"); - assert!(!logs_contain("sensitive namespaces")); - } - - #[tracing_test::traced_test] - #[test] - fn warn_if_public_api_unsafe_unsafe_selection_warns() { - let sel = RpcModuleSelection::try_from_selection(["eth", "txpool", "debug"]).unwrap(); - warn_if_public_api_unsafe(Some(&sel), "--ws.api"); - assert!(logs_contain("sensitive namespaces")); - assert!(logs_contain("--ws.api")); - assert!(logs_contain("txpool")); - assert!(logs_contain("debug")); - } -} diff --git a/crates/node/src/metrics.rs b/crates/node/src/metrics.rs deleted file mode 100644 index 9ac4e19..0000000 --- a/crates/node/src/metrics.rs +++ /dev/null @@ -1,34 +0,0 @@ -// Copyright 2026 Circle Internet Group, Inc. All rights reserved. -// -// SPDX-License-Identifier: Apache-2.0 -// -// Licensed under the Apache License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. -// You may obtain a copy of the License at -// -// http://www.apache.org/licenses/LICENSE-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// See the License for the specific language governing permissions and -// limitations under the License. - -//! Metrics for Arc execution layer - -use metrics::gauge; - -/// Register and set version information metrics -pub fn register_version_info() { - let version = arc_version::SHORT_VERSION; - let git_commit = arc_version::GIT_COMMIT_HASH; - - // Register the version info metric with labels - // This is a common pattern for exposing info-level data in Prometheus, - // using a gauge with a constant value of 1. - gauge!("arc_node_version_info", - "version" => version, - "git_commit" => git_commit - ) - .set(1.0); -} diff --git a/crates/node/tests/common.rs b/crates/node/tests/common.rs deleted file mode 100644 index 230fc46..0000000 --- a/crates/node/tests/common.rs +++ /dev/null @@ -1,181 +0,0 @@ -// Copyright 2025 Circle Internet Group, Inc. All rights reserved. -// -// SPDX-License-Identifier: Apache-2.0 -// -// Licensed under the Apache License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. -// You may obtain a copy of the License at -// -// http://www.apache.org/licenses/LICENSE-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// See the License for the specific language governing permissions and -// limitations under the License. - -#![allow( - dead_code, - clippy::arithmetic_side_effects, - clippy::cast_possible_truncation, - clippy::unwrap_used -)] - -use alloy_genesis::Genesis; -use alloy_primitives::KECCAK256_EMPTY; -use alloy_sol_types::sol; -use arc_node_execution::{ArcEvmConfig, ArcEvmFactory}; -use reth_chainspec::EthChainSpec; -use reth_e2e_test_utils::wallet::Wallet; -use reth_ethereum::evm::EthEvmConfig; -use reth_evm::{ConfigureEvm, EvmEnv, EvmFactory, InspectorFor}; -use revm::{ - context::{BlockEnv, CfgEnv}, - database::InMemoryDB, - inspector::NoOpInspector, - state::{AccountInfo, Bytecode}, -}; -use revm_primitives::{hardfork::SpecId, keccak256}; - -use arc_execution_config::chainspec::LOCAL_DEV; - -pub fn load_genesis() -> Genesis { - serde_json::from_str(include_str!("../../../assets/localdev/genesis.json")).unwrap() -} - -pub const WALLET_COUNT: usize = 10; -pub const WALLET_SENDER_INDEX: usize = 0; -pub const WALLET_RECEIVER_INDEX: usize = 1; -pub const WALLET_OPERATOR_INDEX: usize = 7; - -pub fn insert_alloc_into_db(db: &mut InMemoryDB, genesis: &Genesis) { - for addr in genesis.alloc.keys() { - let data = genesis.alloc.get(addr).unwrap().clone(); - match data.code.clone() { - Some(code) => db.insert_account_info( - *addr, - AccountInfo { - balance: data.balance, - nonce: data.nonce.unwrap_or_default(), - code_hash: keccak256(&code), - code: Some(Bytecode::new_raw(code)), - account_id: None, - }, - ), - None => db.insert_account_info( - *addr, - AccountInfo { - balance: data.balance, - nonce: data.nonce.unwrap_or_default(), - code_hash: KECCAK256_EMPTY, - code: None, - account_id: None, - }, - ), - } - for (k, v) in data.storage_slots() { - db.insert_account_storage(*addr, k.into(), v) - .expect("insert storage"); - } - } -} - -pub fn setup_evm_env() -> (ArcEvmConfig, InMemoryDB, EvmEnv, Wallet) { - let chain_spec = LOCAL_DEV.clone(); - setup_evm_env_with_chainspec(chain_spec) -} - -pub fn setup_evm_env_with_chainspec( - chain_spec: std::sync::Arc, -) -> (ArcEvmConfig, InMemoryDB, EvmEnv, Wallet) { - let genesis = chain_spec.genesis(); - - // Create the in-memory database with the states in genesis file. - let mut db = InMemoryDB::default(); - insert_alloc_into_db(&mut db, genesis); - - // Create testing wallets & config. - let wallet = Wallet::new(WALLET_COUNT).with_chain_id(chain_spec.chain_id()); - let mut cfg_env = CfgEnv::new() - .with_chain_id(chain_spec.chain_id()) - .with_spec_and_mainnet_gas_params(SpecId::PRAGUE); - - cfg_env.disable_base_fee = true; - let evm_config = ArcEvmConfig::new(EthEvmConfig::new_with_evm_factory( - chain_spec.clone(), - ArcEvmFactory::new(chain_spec.clone()), - )); - - let block_env = BlockEnv::default(); - let evm_env = EvmEnv { cfg_env, block_env }; - (evm_config, db, evm_env, wallet) -} - -pub fn setup_evm_with_chainspec( - chain_spec: std::sync::Arc, -) -> ( - ::Evm, - Wallet, -) { - let (evm_config, db, evm_env, wallet) = setup_evm_env_with_chainspec(chain_spec); - let evm = evm_config.evm_with_env(db, evm_env); - (evm, wallet) -} - -pub fn setup_evm_with_chainspec_and_spec( - chain_spec: std::sync::Arc, - spec: SpecId, -) -> ( - ::Evm, - Wallet, -) { - let (evm_config, db, mut evm_env, wallet) = setup_evm_env_with_chainspec(chain_spec); - evm_env.cfg_env.spec = spec; - let evm = evm_config.evm_with_env(db, evm_env); - (evm, wallet) -} - -pub fn setup_evm() -> ( - ::Evm, - Wallet, -) { - let (evm_config, db, evm_env, wallet) = setup_evm_env(); - let evm = evm_config.evm_with_env(db, evm_env); - (evm, wallet) -} - -pub fn setup_evm_with_inspector( - inspector: I, -) -> (::Evm, Wallet) -where - I: InspectorFor, -{ - let (evm_config, db, evm_env, wallet) = setup_evm_env(); - let evm = evm_config.evm_with_env_and_inspector(db, evm_env, inspector); - (evm, wallet) -} - -sol! { - contract NativeCoinAuthority { - #[derive(PartialEq, Eq, Debug)] - event NativeCoinMinted(address indexed recipient, uint256 amount); - #[derive(PartialEq, Eq, Debug)] - event NativeCoinBurned(address indexed from, uint256 amount); - #[derive(PartialEq, Eq, Debug)] - event NativeCoinTransferred(address indexed from, address indexed to, uint256 amount); - } - - contract NativeFiatTokenV2_2 { - function mint(address to, uint256 amount) public {} - function burn(uint256 amount) public {} - function blacklist(address account) public {} - function owner() public view returns (address) {} - - #[derive(PartialEq, Eq, Debug)] - event Mint(address indexed minter, address indexed recipient, uint256 value); - #[derive(PartialEq, Eq, Debug)] - event Burn(address indexed burner, uint256 value); - #[derive(PartialEq, Eq, Debug)] - event Transfer(address indexed from, address indexed to, uint256 value); - } -} diff --git a/crates/node/tests/fiat_token.rs b/crates/node/tests/fiat_token.rs deleted file mode 100644 index 829c411..0000000 --- a/crates/node/tests/fiat_token.rs +++ /dev/null @@ -1,229 +0,0 @@ -// Copyright 2025 Circle Internet Group, Inc. All rights reserved. -// -// SPDX-License-Identifier: Apache-2.0 -// -// Licensed under the Apache License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. -// You may obtain a copy of the License at -// -// http://www.apache.org/licenses/LICENSE-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// See the License for the specific language governing permissions and -// limitations under the License. - -mod common; -use common::{setup_evm, NativeFiatTokenV2_2, WALLET_OPERATOR_INDEX}; - -use alloy_sol_types::{SolCall, SolEvent}; -use arc_precompiles::helpers::NATIVE_FIAT_TOKEN_ADDRESS; -use reth_chainspec::{EthChainSpec, DEV}; -use reth_evm::Evm; -use revm::context::TxEnv; -use revm::handler::SYSTEM_ADDRESS; -use revm_primitives::{Address, TxKind, U256}; - -const EIP7708_LOG_ADDRESS: Address = SYSTEM_ADDRESS; - -#[test] -fn evm_usdc_mint() { - let (mut evm, wallet) = setup_evm(); - let signer = wallet.wallet_gen()[WALLET_OPERATOR_INDEX].clone(); - - let amount = U256::from(10_u128.pow(6) * 3); - let tx = TxEnv { - chain_id: Some(DEV.chain_id()), - caller: signer.address(), - kind: TxKind::Call(NATIVE_FIAT_TOKEN_ADDRESS), - gas_limit: 100_000, - gas_price: 0, - data: NativeFiatTokenV2_2::mintCall { - to: signer.address(), - amount, - } - .abi_encode() - .into(), - ..Default::default() - }; - - let exec = evm.transact_raw(tx).expect("Tx should be accepted"); - assert!(exec.result.is_success(), "execution failed, {exec:?}"); - // The precompile operates in 18-decimal native units (scaled from 6-decimal USDC by the - // NativeFiatToken Solidity contract), so the EIP-7708 Transfer value is in wei. - let native_amount = amount * U256::from(10_u128.pow(12)); - // Zero5 (active on localdev): EIP-7708 Transfer(0x0, to) replaces NativeCoinMinted. - // Logs: EIP-7708 Transfer + Mint + Solidity Transfer from the NativeFiatToken contract. - assert_eq!(exec.result.logs().len(), 3); - // Log 0: EIP-7708 Transfer (mint) from system address — 18-decimal native amount - assert_eq!(exec.result.logs()[0].address, EIP7708_LOG_ADDRESS); - assert_eq!( - NativeFiatTokenV2_2::Transfer::decode_log_data(&exec.result.logs()[0].data).unwrap(), - NativeFiatTokenV2_2::Transfer { - from: Address::ZERO, - to: signer.address(), - value: native_amount, - } - ); - // Log 1: Solidity Mint event from NativeFiatToken contract - assert_eq!(exec.result.logs()[1].address, NATIVE_FIAT_TOKEN_ADDRESS); - assert_eq!( - NativeFiatTokenV2_2::Mint::decode_log_data(&exec.result.logs()[1].data).unwrap(), - NativeFiatTokenV2_2::Mint { - minter: signer.address(), - recipient: signer.address(), - value: amount, - } - ); - // Log 2: Solidity Transfer event from NativeFiatToken contract - assert_eq!(exec.result.logs()[2].address, NATIVE_FIAT_TOKEN_ADDRESS); - assert_eq!( - NativeFiatTokenV2_2::Transfer::decode_log_data(&exec.result.logs()[2].data).unwrap(), - NativeFiatTokenV2_2::Transfer { - from: Address::ZERO, - to: signer.address(), - value: amount, - } - ); - - let balance_before = evm - .db_mut() - .load_account(signer.address()) - .map(|a| a.info.balance) - .unwrap_or(U256::ZERO); - - let balance_after = exec - .state - .get(&signer.address()) - .map(|a| a.info.balance) - .unwrap_or(U256::ZERO); - assert_eq!( - balance_after, - balance_before + amount * U256::from(10_u128.pow(12)) - ); -} - -#[test] -fn evm_usdc_burn() { - let (mut evm, wallet) = setup_evm(); - let signer = wallet.wallet_gen()[WALLET_OPERATOR_INDEX].clone(); - - let amount = U256::from(10_u128.pow(6) * 3); - let tx = TxEnv { - chain_id: Some(DEV.chain_id()), - caller: signer.address(), - kind: TxKind::Call(NATIVE_FIAT_TOKEN_ADDRESS), - value: U256::from(0), - gas_limit: 100_000, - gas_price: 0, - data: NativeFiatTokenV2_2::burnCall { amount }.abi_encode().into(), - ..Default::default() - }; - - let exec = evm.transact_raw(tx).expect("Tx should be accepted"); - assert!(exec.result.is_success(), "execution failed, {exec:?}",); - // The precompile operates in 18-decimal native units — see mint test for explanation. - let native_amount = amount * U256::from(10_u128.pow(12)); - // Zero5 (active on localdev): EIP-7708 Transfer(from, 0x0) replaces NativeCoinBurned. - // Logs: EIP-7708 Transfer + Burn + Solidity Transfer from the NativeFiatToken contract. - assert_eq!(exec.result.logs().len(), 3); - // Log 0: EIP-7708 Transfer (burn) from system address — 18-decimal native amount - assert_eq!(exec.result.logs()[0].address, EIP7708_LOG_ADDRESS); - assert_eq!( - NativeFiatTokenV2_2::Transfer::decode_log_data(&exec.result.logs()[0].data).unwrap(), - NativeFiatTokenV2_2::Transfer { - from: signer.address(), - to: Address::ZERO, - value: native_amount, - } - ); - // Log 1: Solidity Burn event from NativeFiatToken contract - assert_eq!(exec.result.logs()[1].address, NATIVE_FIAT_TOKEN_ADDRESS); - assert_eq!( - NativeFiatTokenV2_2::Burn::decode_log_data(&exec.result.logs()[1].data).unwrap(), - NativeFiatTokenV2_2::Burn { - burner: signer.address(), - value: amount, - } - ); - // Log 2: Solidity Transfer event from NativeFiatToken contract - assert_eq!(exec.result.logs()[2].address, NATIVE_FIAT_TOKEN_ADDRESS); - assert_eq!( - NativeFiatTokenV2_2::Transfer::decode_log_data(&exec.result.logs()[2].data).unwrap(), - NativeFiatTokenV2_2::Transfer { - from: signer.address(), - to: Address::ZERO, - value: amount, - } - ); - - let balance_before = evm - .db_mut() - .load_account(signer.address()) - .map(|a| a.info.balance) - .unwrap_or(U256::ZERO); - - let balance_after = exec - .state - .get(&signer.address()) - .map(|a| a.info.balance) - .unwrap_or(U256::ZERO); - assert_eq!( - balance_after + amount * U256::from(10_u128.pow(12)), - balance_before - ); -} - -/// Tests that the blacklister cannot blacklist the USDC owner. -/// This is a security guard to prevent a compromised blacklister from -/// disabling the owner's ability to rotate roles. -#[test] -fn evm_usdc_cannot_blacklist_owner() { - let (mut evm, wallet) = setup_evm(); - // Wallet index 7 is the operator/blacklister in localdev genesis - let blacklister = wallet.wallet_gen()[WALLET_OPERATOR_INDEX].clone(); - - // First, get the owner address by calling owner() - let owner_call_tx = TxEnv { - chain_id: Some(DEV.chain_id()), - caller: blacklister.address(), - kind: TxKind::Call(NATIVE_FIAT_TOKEN_ADDRESS), - gas_limit: 100_000, - gas_price: 0, - data: NativeFiatTokenV2_2::ownerCall {}.abi_encode().into(), - ..Default::default() - }; - - let owner_result = evm - .transact_raw(owner_call_tx) - .expect("Owner call should succeed"); - assert!(owner_result.result.is_success(), "owner() call failed"); - - let owner_address = Address::from_slice(&owner_result.result.output().unwrap()[12..32]); - - // Now attempt to blacklist the owner - this should fail - let blacklist_tx = TxEnv { - chain_id: Some(DEV.chain_id()), - caller: blacklister.address(), - kind: TxKind::Call(NATIVE_FIAT_TOKEN_ADDRESS), - gas_limit: 100_000, - gas_price: 0, - data: NativeFiatTokenV2_2::blacklistCall { - account: owner_address, - } - .abi_encode() - .into(), - ..Default::default() - }; - - let exec = evm - .transact_raw(blacklist_tx) - .expect("Tx should be accepted"); - - // The transaction should revert - assert!( - !exec.result.is_success(), - "Blacklisting owner should have reverted, but succeeded" - ); -} diff --git a/crates/node/tests/native_transfer.rs b/crates/node/tests/native_transfer.rs deleted file mode 100644 index 7eec4d5..0000000 --- a/crates/node/tests/native_transfer.rs +++ /dev/null @@ -1,961 +0,0 @@ -// Copyright 2025 Circle Internet Group, Inc. All rights reserved. -// -// SPDX-License-Identifier: Apache-2.0 -// -// Licensed under the Apache License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. -// You may obtain a copy of the License at -// -// http://www.apache.org/licenses/LICENSE-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// See the License for the specific language governing permissions and -// limitations under the License. - -#![allow(clippy::arithmetic_side_effects, clippy::cast_possible_truncation)] - -mod common; -use common::{setup_evm, setup_evm_with_inspector, WALLET_RECEIVER_INDEX, WALLET_SENDER_INDEX}; - -use alloy_evm::eth::EthEvmContext; -use alloy_primitives::{Address, Log}; -use alloy_rlp::Bytes; -use alloy_rpc_types_trace::geth::call::CallConfig; -use alloy_sol_types::SolEvent; -use arc_evm::ArcEvm; -use arc_precompiles::NATIVE_COIN_AUTHORITY_ADDRESS; -use common::NativeCoinAuthority; -use reth_chainspec::{EthChainSpec, ForkCondition, DEV}; -use reth_e2e_test_utils::wallet::Wallet; -use reth_ethereum::evm::revm::{inspector::Inspector, interpreter::interpreter::EthInterpreter}; -use reth_evm::Evm; -use revm::handler::SYSTEM_ADDRESS; -use revm::{ - context::result::{ExecResultAndState, ExecutionResult}, - handler::{instructions::EthInstructions, PrecompileProvider}, - interpreter::InterpreterResult, -}; -use revm::{context::TxEnv, database::InMemoryDB}; -use revm_inspectors::tracing::{TracingInspector, TracingInspectorConfig}; -use revm_primitives::{hardfork::SpecId, TxKind, U256}; - -use arc_precompiles::NATIVE_COIN_CONTROL_ADDRESS; - -const EIP7708_LOG_ADDRESS: Address = SYSTEM_ADDRESS; - -fn test_native_transfer( - evm: &mut ArcEvm< - EthEvmContext, - I, - EthInstructions>, - PRECOMPILE, - >, - wallet: &Wallet, -) -> (TxEnv, ExecResultAndState) -where - I: Inspector, EthInterpreter>, - PRECOMPILE: PrecompileProvider, Output = InterpreterResult>, -{ - let sender = wallet.wallet_gen()[WALLET_SENDER_INDEX].clone(); - let receiver = wallet.wallet_gen()[WALLET_RECEIVER_INDEX].clone(); - let amount = U256::from(42); - let tx = TxEnv { - chain_id: Some(DEV.chain_id()), - caller: sender.address(), - kind: TxKind::Call(receiver.address()), - value: amount, - gas_limit: 21_000, - gas_price: 0, - ..Default::default() - }; - - let exec: ExecResultAndState = - evm.transact_raw(tx.clone()).expect("Tx should be accepted"); - assert!(exec.result.is_success(), "execution failed, {exec:?}",); - // Localdev activates Zero5: CALL transfers emit an EIP-7708 Transfer log - // from the system address 0xfffe...fffe instead of NativeCoinTransferred. - let logs = exec.result.logs(); - assert_eq!(logs.len(), 1); - let log: &Log = &logs[0]; - assert_eq!(log.address, EIP7708_LOG_ADDRESS); - let decoded = common::NativeFiatTokenV2_2::Transfer::decode_log(log) - .expect("Failed to decode EIP-7708 Transfer log"); - assert_eq!(decoded.from, sender.address()); - assert_eq!(decoded.to, receiver.address()); - assert_eq!(decoded.value, amount); - - let balance_before = evm - .db_mut() - .load_account(sender.address()) - .map(|a| a.info.balance) - .unwrap_or(U256::ZERO); - - let balance_after = exec - .state - .get(&sender.address()) - .map(|a| a.info.balance) - .unwrap_or(U256::ZERO); - assert_eq!(balance_after + amount, balance_before); - - (tx, exec) -} - -#[test] -fn evm_native_transfer() { - let (mut evm, wallet) = setup_evm(); - test_native_transfer(&mut evm, &wallet); -} - -#[test] -fn inspect_evm_native_transfer() { - let call_config = CallConfig { - with_log: Some(true), - only_top_call: Some(false), - }; - let mut inspector = - TracingInspector::new(TracingInspectorConfig::from_geth_call_config(&call_config)); - let (mut evm, wallet) = setup_evm_with_inspector(&mut inspector); - - let (tx, exec) = test_native_transfer(&mut evm, &wallet); - - let frame = inspector - .with_transaction_gas_limit(21_000) - .into_geth_builder() - .geth_call_traces(call_config, exec.result.gas_used()); - - assert_eq!(frame.from, tx.caller); - assert_eq!(frame.to, tx.kind.to().cloned()); - assert_eq!(frame.value, Some(tx.value)); - assert_eq!(frame.input, Bytes::new()); - assert_eq!(frame.output, None); - assert_eq!(frame.gas, 21_000); - assert_eq!(frame.error, None); - assert_eq!(frame.revert_reason, None); - assert_eq!(frame.calls.len(), 0); - - assert_eq!(frame.logs.len(), 0); - - assert_eq!(frame.typ, "CALL"); -} - -/// Helper to create a chainspec with Zero5 active at block 0. -fn chainspec_with_zero5() -> std::sync::Arc { - use arc_execution_config::{chainspec::localdev_with_hardforks, hardforks::ArcHardfork}; - localdev_with_hardforks(&[ - (ArcHardfork::Zero3, ForkCondition::Block(0)), - (ArcHardfork::Zero4, ForkCondition::Block(0)), - (ArcHardfork::Zero5, ForkCondition::Block(0)), - ]) -} - -/// Zero5: plain CALL value transfer emits 1 EIP-7708 Transfer log. -#[test] -fn evm_native_transfer_zero5_eip7708_log() { - let chain_spec = chainspec_with_zero5(); - let (mut evm, wallet) = common::setup_evm_with_chainspec(chain_spec); - - let sender = wallet.wallet_gen()[WALLET_SENDER_INDEX].clone(); - let receiver = wallet.wallet_gen()[WALLET_RECEIVER_INDEX].clone(); - let amount = U256::from(42); - - let tx = TxEnv { - chain_id: Some(DEV.chain_id()), - caller: sender.address(), - kind: TxKind::Call(receiver.address()), - value: amount, - gas_limit: 21_000, - gas_price: 0, - ..Default::default() - }; - - let exec: ExecResultAndState = - evm.transact_raw(tx).expect("Tx should be accepted"); - assert!(exec.result.is_success(), "execution failed, {exec:?}"); - - let logs = exec.result.logs(); - assert_eq!(logs.len(), 1, "Zero5 should emit 1 EIP-7708 Transfer log"); - - assert_eq!( - logs[0].address, EIP7708_LOG_ADDRESS, - "Log should be from EIP-7708 system address" - ); - - // Decode as ERC-20 Transfer event — same topic signature as NativeCoinTransferred - // but emitted from the EIP-7708 system address instead of the native coin authority. - let decoded = common::NativeFiatTokenV2_2::Transfer::decode_log(&logs[0]) - .expect("Failed to decode EIP-7708 Transfer log"); - assert_eq!(decoded.from, sender.address()); - assert_eq!(decoded.to, receiver.address()); - assert_eq!(decoded.value, amount); -} - -/// Pre-Zero5: plain CALL value transfer emits 1 NativeCoinTransferred log. -#[test] -fn evm_native_transfer_pre_zero5_emits_native_coin_transferred() { - use arc_execution_config::{chainspec::localdev_with_hardforks, hardforks::ArcHardfork}; - // Zero4 active but NOT Zero5 - let chain_spec = localdev_with_hardforks(&[ - (ArcHardfork::Zero3, ForkCondition::Block(0)), - (ArcHardfork::Zero4, ForkCondition::Block(0)), - ]); - let (mut evm, wallet) = common::setup_evm_with_chainspec(chain_spec); - - let sender = wallet.wallet_gen()[WALLET_SENDER_INDEX].clone(); - let receiver = wallet.wallet_gen()[WALLET_RECEIVER_INDEX].clone(); - let amount = U256::from(42); - - let tx = TxEnv { - chain_id: Some(DEV.chain_id()), - caller: sender.address(), - kind: TxKind::Call(receiver.address()), - value: amount, - gas_limit: 21_000, - gas_price: 0, - ..Default::default() - }; - - let exec: ExecResultAndState = - evm.transact_raw(tx).expect("Tx should be accepted"); - assert!(exec.result.is_success(), "execution failed, {exec:?}"); - - // Pre-Zero5: should emit 1 NativeCoinTransferred log - let logs = exec.result.logs(); - assert_eq!( - logs.len(), - 1, - "Pre-Zero5 should emit 1 NativeCoinTransferred log" - ); - let log: &Log = &logs[0]; - assert_eq!(log.address, NATIVE_COIN_AUTHORITY_ADDRESS); - let decoded = NativeCoinAuthority::NativeCoinTransferred::decode_log(log) - .expect("Failed to decode NativeCoinTransferred log"); - assert_eq!(decoded.from, sender.address()); - assert_eq!(decoded.to, receiver.address()); - assert_eq!(decoded.amount, amount); -} - -/// Helper to create a chainspec with Zero4 active but NOT Zero5. -fn chainspec_pre_zero5() -> std::sync::Arc { - use arc_execution_config::{chainspec::localdev_with_hardforks, hardforks::ArcHardfork}; - localdev_with_hardforks(&[ - (ArcHardfork::Zero3, ForkCondition::Block(0)), - (ArcHardfork::Zero4, ForkCondition::Block(0)), - ]) -} - -/// B1: Zero5 self-transfer (sender == receiver) with non-zero value produces no logs. -/// EIP-7708 specifies Transfer logs only for "nonzero-value-transferring ... to a different account". -#[test] -fn evm_native_transfer_zero5_self_transfer_no_log() { - let chain_spec = chainspec_with_zero5(); - let (mut evm, wallet) = common::setup_evm_with_chainspec(chain_spec); - - let sender = wallet.wallet_gen()[WALLET_SENDER_INDEX].clone(); - let amount = U256::from(42); - - // Self-transfer: sender == receiver - let tx = TxEnv { - chain_id: Some(DEV.chain_id()), - caller: sender.address(), - kind: TxKind::Call(sender.address()), - value: amount, - gas_limit: 21_000, - gas_price: 0, - ..Default::default() - }; - - let exec: ExecResultAndState = - evm.transact_raw(tx).expect("Tx should be accepted"); - assert!(exec.result.is_success(), "execution failed, {exec:?}"); - - // EIP-7708: self-transfers do not emit a Transfer log - let logs = exec.result.logs(); - assert_eq!(logs.len(), 0, "Zero5: self-transfer should emit 0 logs"); - - // Balance should be unchanged (self-transfer) - let balance_before = evm - .db_mut() - .load_account(sender.address()) - .map(|a| a.info.balance) - .unwrap_or(U256::ZERO); - let balance_after = exec - .state - .get(&sender.address()) - .map(|a| a.info.balance) - .unwrap_or(U256::ZERO); - assert_eq!( - balance_after, balance_before, - "balance should be unchanged for self-transfer" - ); -} - -/// B2: Zero5 zero-value CALL produces no logs. -/// EIP-7708 specifies Transfer logs only for "nonzero-value-transferring" operations. -#[test] -fn evm_native_transfer_zero5_zero_value_no_log() { - let chain_spec = chainspec_with_zero5(); - let (mut evm, wallet) = common::setup_evm_with_chainspec(chain_spec); - - let sender = wallet.wallet_gen()[WALLET_SENDER_INDEX].clone(); - let receiver = wallet.wallet_gen()[WALLET_RECEIVER_INDEX].clone(); - - // Zero value transfer - let tx = TxEnv { - chain_id: Some(DEV.chain_id()), - caller: sender.address(), - kind: TxKind::Call(receiver.address()), - value: U256::ZERO, - gas_limit: 21_000, - gas_price: 0, - ..Default::default() - }; - - let exec: ExecResultAndState = - evm.transact_raw(tx).expect("Tx should be accepted"); - assert!(exec.result.is_success(), "execution failed, {exec:?}"); - - // Zero-value transfers do not emit any log - let logs = exec.result.logs(); - assert_eq!(logs.len(), 0, "Zero5: zero-value CALL should emit 0 logs"); -} - -/// B3: Zero5 CREATE with value emits an EIP-7708 Transfer log. -/// The log should be from the EIP-7708 system address (0xfffe...fffe) with from=caller, to=created. -#[test] -fn evm_native_transfer_zero5_create_with_value_emits_eip7708_log() { - let chain_spec = chainspec_with_zero5(); - let (mut evm, wallet) = common::setup_evm_with_chainspec(chain_spec); - - let sender = wallet.wallet_gen()[WALLET_SENDER_INDEX].clone(); - let amount = U256::from(42); - - // Minimal contract bytecode: PUSH1 0x00 PUSH1 0x00 RETURN (deploys empty contract) - let creation_code: alloy_primitives::Bytes = vec![0x60, 0x00, 0x60, 0x00, 0xF3].into(); - - let tx = TxEnv { - chain_id: Some(DEV.chain_id()), - caller: sender.address(), - kind: TxKind::Create, - value: amount, - gas_limit: 100_000, - gas_price: 0, - data: creation_code, - ..Default::default() - }; - - let exec: ExecResultAndState = - evm.transact_raw(tx).expect("Tx should be accepted"); - assert!(exec.result.is_success(), "execution failed, {exec:?}"); - - let logs = exec.result.logs(); - assert_eq!( - logs.len(), - 1, - "Zero5: CREATE with value should emit 1 EIP-7708 Transfer log" - ); - - assert_eq!(logs[0].address, EIP7708_LOG_ADDRESS); - - let decoded = common::NativeFiatTokenV2_2::Transfer::decode_log(&logs[0]) - .expect("Failed to decode EIP-7708 Transfer log"); - assert_eq!(decoded.from, sender.address()); - // `to` should be the newly created contract address - assert_ne!( - decoded.to, - alloy_primitives::Address::ZERO, - "created address should not be zero" - ); - assert_eq!(decoded.value, amount); -} - -/// B4: Zero5 CALL to Address::ZERO with non-zero value should revert. -/// Arc disallows value transfers to the zero address at the EVM level under Zero5. -#[test] -fn evm_native_transfer_zero5_to_zero_address_reverts() { - let chain_spec = chainspec_with_zero5(); - let (mut evm, wallet) = common::setup_evm_with_chainspec(chain_spec); - - let sender = wallet.wallet_gen()[WALLET_SENDER_INDEX].clone(); - let amount = U256::from(42); - - let tx = TxEnv { - chain_id: Some(DEV.chain_id()), - caller: sender.address(), - kind: TxKind::Call(alloy_primitives::Address::ZERO), - value: amount, - gas_limit: 21_000, - gas_price: 0, - ..Default::default() - }; - - let exec: ExecResultAndState = - evm.transact_raw(tx).expect("Tx should be accepted"); - - // Should revert due to zero address check - assert!( - !exec.result.is_success(), - "Zero5: CALL to Address::ZERO with value should revert" - ); - - // Balance should be unchanged (revert) - let balance_before = evm - .db_mut() - .load_account(sender.address()) - .map(|a| a.info.balance) - .unwrap_or(U256::ZERO); - let balance_after = exec - .state - .get(&sender.address()) - .map(|a| a.info.balance) - .unwrap_or(U256::ZERO); - assert_eq!( - balance_after, balance_before, - "balance should be unchanged after revert" - ); -} - -/// B6: Event ordering is preserved across the Zero5 hardfork boundary. -/// The Transfer log should appear at the same position (index 0) as the old -/// NativeCoinTransferred log for plain CALL value transfers. -#[test] -fn evm_native_transfer_event_ordering_preserved_across_zero5() { - let sender_idx = WALLET_SENDER_INDEX; - let receiver_idx = WALLET_RECEIVER_INDEX; - let amount = U256::from(42); - - // Pre-Zero5: NativeCoinTransferred - let chain_spec_pre = chainspec_pre_zero5(); - let (mut evm_pre, wallet) = common::setup_evm_with_chainspec(chain_spec_pre); - let sender = wallet.wallet_gen()[sender_idx].clone(); - let receiver = wallet.wallet_gen()[receiver_idx].clone(); - let tx_pre = TxEnv { - chain_id: Some(DEV.chain_id()), - caller: sender.address(), - kind: TxKind::Call(receiver.address()), - value: amount, - gas_limit: 21_000, - gas_price: 0, - ..Default::default() - }; - let exec_pre = evm_pre - .transact_raw(tx_pre) - .expect("Pre-Zero5 tx should be accepted"); - assert!(exec_pre.result.is_success()); - - // Zero5: EIP-7708 Transfer - let chain_spec_z5 = chainspec_with_zero5(); - let (mut evm_z5, wallet_z5) = common::setup_evm_with_chainspec(chain_spec_z5); - let sender_z5 = wallet_z5.wallet_gen()[sender_idx].clone(); - let receiver_z5 = wallet_z5.wallet_gen()[receiver_idx].clone(); - let tx_z5 = TxEnv { - chain_id: Some(DEV.chain_id()), - caller: sender_z5.address(), - kind: TxKind::Call(receiver_z5.address()), - value: amount, - gas_limit: 21_000, - gas_price: 0, - ..Default::default() - }; - let exec_z5 = evm_z5 - .transact_raw(tx_z5) - .expect("Zero5 tx should be accepted"); - assert!(exec_z5.result.is_success()); - - let logs_pre = exec_pre.result.logs(); - let logs_z5 = exec_z5.result.logs(); - - // Same number of logs - assert_eq!( - logs_pre.len(), - logs_z5.len(), - "log count should be identical" - ); - assert_eq!( - logs_pre.len(), - 1, - "plain CALL value transfer should emit exactly 1 log" - ); - - // Both logs are at index 0 — ordering is preserved - // Pre-Zero5: NativeCoinTransferred from authority address - assert_eq!(logs_pre[0].address, NATIVE_COIN_AUTHORITY_ADDRESS); - let decoded_pre = NativeCoinAuthority::NativeCoinTransferred::decode_log(&logs_pre[0]) - .expect("Failed to decode NativeCoinTransferred"); - assert_eq!(decoded_pre.from, sender.address()); - assert_eq!(decoded_pre.to, receiver.address()); - assert_eq!(decoded_pre.amount, amount); - - // Zero5: EIP-7708 Transfer from system address - assert_eq!(logs_z5[0].address, EIP7708_LOG_ADDRESS); - let decoded_z5 = common::NativeFiatTokenV2_2::Transfer::decode_log(&logs_z5[0]) - .expect("Failed to decode EIP-7708 Transfer"); - assert_eq!(decoded_z5.from, sender_z5.address()); - assert_eq!(decoded_z5.to, receiver_z5.address()); - assert_eq!(decoded_z5.value, amount); -} - -/// Zero5 + AMSTERDAM: Arc self-emits EIP-7708 Transfer log regardless of SpecId. -/// When REVM is eventually upgraded with native EIP-7708 journal support, we may see -/// duplicate logs (one from Arc, one from REVM). That migration is tracked separately. -#[test] -fn evm_native_transfer_zero5_amsterdam_eip7708_log() { - let chain_spec = chainspec_with_zero5(); - let (mut evm, wallet) = - common::setup_evm_with_chainspec_and_spec(chain_spec, SpecId::AMSTERDAM); - - let sender = wallet.wallet_gen()[WALLET_SENDER_INDEX].clone(); - let receiver = wallet.wallet_gen()[WALLET_RECEIVER_INDEX].clone(); - let amount = U256::from(42); - - let tx = TxEnv { - chain_id: Some(DEV.chain_id()), - caller: sender.address(), - kind: TxKind::Call(receiver.address()), - value: amount, - gas_limit: 21_000, - gas_price: 0, - ..Default::default() - }; - - let exec: ExecResultAndState = - evm.transact_raw(tx).expect("Tx should be accepted"); - assert!(exec.result.is_success(), "execution failed, {exec:?}"); - - let logs = exec.result.logs(); - assert_eq!( - logs.len(), - 1, - "Zero5 should emit 1 EIP-7708 Transfer log (self-implemented)" - ); - - assert_eq!(logs[0].address, EIP7708_LOG_ADDRESS); - - // Verify balance was still transferred correctly - let balance_before = evm - .db_mut() - .load_account(sender.address()) - .map(|a| a.info.balance) - .unwrap_or(U256::ZERO); - let balance_after = exec - .state - .get(&sender.address()) - .map(|a| a.info.balance) - .unwrap_or(U256::ZERO); - assert_eq!(balance_after + amount, balance_before); -} - -/// Hardfork boundary test: proves that behavior switches exactly at the Zero5 activation block. -/// Block 9 (pre-Zero5) emits NativeCoinTransferred from NATIVE_COIN_AUTHORITY_ADDRESS; -/// Block 10 (Zero5 active) emits EIP-7708 Transfer from the system address. -#[test] -fn evm_native_transfer_hardfork_boundary_zero5_activation() { - use arc_execution_config::{chainspec::localdev_with_hardforks, hardforks::ArcHardfork}; - use reth_evm::ConfigureEvm; - - let chain_spec = localdev_with_hardforks(&[ - (ArcHardfork::Zero3, ForkCondition::Block(0)), - (ArcHardfork::Zero4, ForkCondition::Block(0)), - (ArcHardfork::Zero5, ForkCondition::Block(10)), - ]); - - let amount = U256::from(42); - - // --- Block 9: pre-Zero5 --- - let (evm_config, db, mut evm_env, wallet) = - common::setup_evm_env_with_chainspec(chain_spec.clone()); - evm_env.block_env.number = U256::from(9); - let mut evm_pre = evm_config.evm_with_env(db, evm_env); - - let sender = wallet.wallet_gen()[WALLET_SENDER_INDEX].clone(); - let receiver = wallet.wallet_gen()[WALLET_RECEIVER_INDEX].clone(); - - let tx_pre = TxEnv { - chain_id: Some(chain_spec.chain_id()), - caller: sender.address(), - kind: TxKind::Call(receiver.address()), - value: amount, - gas_limit: 21_000, - gas_price: 0, - ..Default::default() - }; - - let exec_pre = evm_pre - .transact_raw(tx_pre) - .expect("Pre-Zero5 tx should be accepted"); - assert!( - exec_pre.result.is_success(), - "block 9 tx should succeed: {:?}", - exec_pre.result - ); - - let logs_pre = exec_pre.result.logs(); - assert_eq!( - logs_pre.len(), - 1, - "block 9: should emit exactly 1 NativeCoinTransferred log" - ); - assert_eq!( - logs_pre[0].address, NATIVE_COIN_AUTHORITY_ADDRESS, - "block 9: log should come from NativeCoinAuthority" - ); - let decoded_pre = NativeCoinAuthority::NativeCoinTransferred::decode_log(&logs_pre[0]) - .expect("Failed to decode NativeCoinTransferred log at block 9"); - assert_eq!(decoded_pre.from, sender.address()); - assert_eq!(decoded_pre.to, receiver.address()); - assert_eq!(decoded_pre.amount, amount); - - // --- Block 10: Zero5 active --- - let (evm_config, db, mut evm_env, wallet) = - common::setup_evm_env_with_chainspec(chain_spec.clone()); - evm_env.block_env.number = U256::from(10); - let mut evm_z5 = evm_config.evm_with_env(db, evm_env); - - let sender = wallet.wallet_gen()[WALLET_SENDER_INDEX].clone(); - let receiver = wallet.wallet_gen()[WALLET_RECEIVER_INDEX].clone(); - - let tx_z5 = TxEnv { - chain_id: Some(chain_spec.chain_id()), - caller: sender.address(), - kind: TxKind::Call(receiver.address()), - value: amount, - gas_limit: 21_000, - gas_price: 0, - ..Default::default() - }; - - let exec_z5 = evm_z5 - .transact_raw(tx_z5) - .expect("Zero5 tx should be accepted"); - assert!( - exec_z5.result.is_success(), - "block 10 tx should succeed: {:?}", - exec_z5.result - ); - - let logs_z5 = exec_z5.result.logs(); - assert_eq!( - logs_z5.len(), - 1, - "block 10: should emit exactly 1 EIP-7708 Transfer log" - ); - assert_eq!( - logs_z5[0].address, EIP7708_LOG_ADDRESS, - "block 10: log should come from EIP-7708 system address" - ); - let decoded_z5 = common::NativeFiatTokenV2_2::Transfer::decode_log(&logs_z5[0]) - .expect("Failed to decode EIP-7708 Transfer log at block 10"); - assert_eq!(decoded_z5.from, sender.address()); - assert_eq!(decoded_z5.to, receiver.address()); - assert_eq!(decoded_z5.value, amount); -} - -/// Blocklist enforcement must be identical on inspect and non-inspect paths. -/// -/// If `ArcEvm::inspect_frame_init` delegates to the inner EVM instead of going through -/// `ArcEvm::frame_init`, the blocklist check in `before_frame_init` is bypassed. The -/// top-level tx validation catches blocklisted addresses, but for *nested* calls the only -/// guard is `before_frame_init` (called from `frame_init`). -/// -/// This test deploys a contract that performs a nested CALL with value to a blocklisted -/// address and verifies both the non-inspect and inspect paths produce the same revert. -#[test] -fn blocklist_enforced_on_inspect_path() { - use arc_execution_config::native_coin_control::compute_is_blocklisted_storage_slot; - use reth_evm::ConfigureEvm; - use revm_primitives::keccak256; - - use revm::bytecode::opcode; - - let blocklisted_target = Address::repeat_byte(0xBB); - - // Runtime bytecode: CALL(gas, target, 1 wei, 0, 0, 0, 0); POP; STOP - #[rustfmt::skip] - let mut code: Vec = vec![ - opcode::PUSH1, 0x00, // retLength - opcode::PUSH1, 0x00, // retOffset - opcode::PUSH1, 0x00, // argsLength - opcode::PUSH1, 0x00, // argsOffset - opcode::PUSH1, 0x01, // value = 1 wei - opcode::PUSH20, // target address (20 bytes follow) - ]; - code.extend_from_slice(blocklisted_target.as_ref()); - #[rustfmt::skip] - code.extend_from_slice(&[ - opcode::GAS, // forward all remaining gas - opcode::CALL, // CALL(gas, target, value, argsOff, argsSz, retOff, retSz) - opcode::POP, // discard success flag - opcode::STOP, - ]); - - let factory_addr = Address::repeat_byte(0xFA); - - // --- Helper: set up DB with blocklisted target and factory contract --- - let setup_db = |db: &mut InMemoryDB| { - // Blocklist the target - let slot = compute_is_blocklisted_storage_slot(blocklisted_target); - db.insert_account_storage(NATIVE_COIN_CONTROL_ADDRESS, slot.into(), U256::from(1)) - .expect("insert blocklist storage"); - - // Insert factory contract with some balance - let bytecode = code.clone(); - db.insert_account_info( - factory_addr, - revm::state::AccountInfo { - balance: U256::from(10_000), - nonce: 1, - code_hash: keccak256(&bytecode), - code: Some(revm::state::Bytecode::new_raw(bytecode.into())), - account_id: None, - }, - ); - }; - - // --- Non-inspect path --- - let (evm_config, mut db, evm_env, wallet) = common::setup_evm_env(); - setup_db(&mut db); - let mut evm = evm_config.evm_with_env(db, evm_env); - - let sender = wallet.wallet_gen()[WALLET_SENDER_INDEX].clone(); - let tx = TxEnv { - chain_id: Some(reth_chainspec::DEV.chain_id()), - caller: sender.address(), - kind: TxKind::Call(factory_addr), - value: U256::ZERO, - gas_limit: 100_000, - gas_price: 0, - ..Default::default() - }; - - let exec_no_inspect = evm.transact_raw(tx.clone()).expect("tx should be accepted"); - // The top-level tx succeeds but the nested CALL to the blocklisted address reverts. - // The factory's CALL returns 0 (failure) on the stack, but since we STOP right after, - // the overall tx succeeds. The key check: the blocklisted target should NOT receive funds. - assert!( - exec_no_inspect.result.is_success(), - "non-inspect: top-level tx should succeed (nested call reverts internally)" - ); - let target_balance_no_inspect = exec_no_inspect - .state - .get(&blocklisted_target) - .map(|a| a.info.balance) - .unwrap_or(U256::ZERO); - assert_eq!( - target_balance_no_inspect, - U256::ZERO, - "non-inspect: blocklisted target should not receive funds via nested CALL" - ); - - // --- Inspect path --- - let (evm_config, mut db, evm_env, _wallet) = common::setup_evm_env(); - setup_db(&mut db); - let call_config = CallConfig { - with_log: Some(true), - only_top_call: Some(false), - }; - let inspector = - TracingInspector::new(TracingInspectorConfig::from_geth_call_config(&call_config)); - let mut evm_inspect = evm_config.evm_with_env_and_inspector(db, evm_env, inspector); - - let exec_inspect = evm_inspect.transact_raw(tx).expect("tx should be accepted"); - assert!( - exec_inspect.result.is_success(), - "inspect: top-level tx should succeed (nested call reverts internally)" - ); - let target_balance_inspect = exec_inspect - .state - .get(&blocklisted_target) - .map(|a| a.info.balance) - .unwrap_or(U256::ZERO); - assert_eq!( - target_balance_inspect, - U256::ZERO, - "inspect: blocklisted target should not receive funds via nested CALL" - ); - - // Both paths should consume the same gas - assert_eq!( - exec_no_inspect.result.gas_used(), - exec_inspect.result.gas_used(), - "inspect and non-inspect paths should consume identical gas" - ); -} - -/// CREATE2 with value under Zero5 emits an EIP-7708 Transfer log with the correct -/// deterministic CREATE2 address as `to`. -#[test] -fn evm_native_create2_with_value_zero5_emits_eip7708_log() { - use reth_evm::ConfigureEvm; - use revm_primitives::keccak256; - - let chain_spec = chainspec_with_zero5(); - let (evm_config, mut db, evm_env, wallet) = - common::setup_evm_env_with_chainspec(chain_spec.clone()); - - let sender = wallet.wallet_gen()[WALLET_SENDER_INDEX].clone(); - let factory = Address::repeat_byte(0xFA); - let endowment = U256::from(42); - let salt = U256::from(0xBEEF_u64); - - // Child init code: PUSH1 0x00 PUSH1 0x00 RETURN (deploys empty contract) - let child_initcode: Vec = vec![0x60, 0x00, 0x60, 0x00, 0xF3]; - let child_initcode_len = child_initcode.len(); - let child_initcode_hash = keccak256(&child_initcode); - - // Derive the expected CREATE2 address: - // address = keccak256(0xff ++ factory ++ salt ++ keccak256(initcode))[12..] - let expected_create2_addr = factory.create2(salt.to_be_bytes(), child_initcode_hash); - - // Factory runtime bytecode that: - // 1. Copies child initcode from code into memory - // 2. Executes CREATE2(value=CALLVALUE, offset=0, size=initcode_len, salt) - // 3. STOPs - // - // Layout (child initcode is appended after the opcodes): - // PUSH1 ; size - // PUSH1 ; offset in code (after these opcodes) - // PUSH1 0x00 ; destOffset in memory - // CODECOPY ; mem[0..len] = code[offset..offset+len] - // PUSH32 ; salt - // PUSH1 ; size - // PUSH1 0x00 ; offset in memory - // CALLVALUE ; endowment from msg.value - // CREATE2 ; CREATE2(value, offset, size, salt) - // STOP - // Factory runtime bytecode prefix (initcode offset at byte 3 is patched below). - // PUSH1 PUSH1 PUSH1 0x00 CODECOPY - // PUSH32 PUSH1 PUSH1 0x00 CALLVALUE CREATE2 STOP - let mut prefix: Vec = vec![ - 0x60, - child_initcode_len as u8, // PUSH1 - 0x60, - 0x00, // PUSH1 — patched below - 0x60, - 0x00, // PUSH1 0x00 (destOffset) - 0x39, // CODECOPY - 0x7F, // PUSH32 (salt follows) - ]; - prefix.extend_from_slice(&salt.to_be_bytes::<32>()); - prefix.extend_from_slice(&[ - 0x60, - child_initcode_len as u8, // PUSH1 - 0x60, - 0x00, // PUSH1 0x00 (offset) - 0x34, // CALLVALUE - 0xF5, // CREATE2 - 0x00, // STOP - ]); - - // Patch the initcode offset (byte index 3) — initcode is appended after prefix. - prefix[3] = prefix.len() as u8; - - let mut factory_code = prefix; - factory_code.extend_from_slice(&child_initcode); - - let factory_runtime = revm::state::Bytecode::new_raw(factory_code.clone().into()); - - // Insert factory account with balance to fund the CREATE2 - db.insert_account_info( - factory, - revm::state::AccountInfo { - balance: U256::from(10_000), - nonce: 1, - code_hash: keccak256(&factory_code), - code: Some(factory_runtime), - account_id: None, - }, - ); - - let trace_db = db.clone(); - let trace_evm_env = evm_env.clone(); - let mut evm = evm_config.evm_with_env(db, evm_env); - - let tx = TxEnv { - chain_id: Some(chain_spec.chain_id()), - caller: sender.address(), - kind: TxKind::Call(factory), - value: endowment, - gas_limit: 200_000, - gas_price: 0, - ..Default::default() - }; - - let exec = evm - .transact_raw(tx.clone()) - .expect("CREATE2 tx should be accepted"); - assert!( - exec.result.is_success(), - "CREATE2 with value should succeed: {:?}", - exec.result - ); - - // Two EIP-7708 Transfer logs expected: - // 1. sender → factory (top-level CALL value transfer) - // 2. factory → create2_addr (CREATE2 endowment) - let logs = exec.result.logs(); - assert_eq!( - logs.len(), - 2, - "Expected 2 EIP-7708 Transfer logs (CALL + CREATE2), got {}", - logs.len() - ); - - // First log: sender → factory (the top-level value transfer) - assert_eq!(logs[0].address, EIP7708_LOG_ADDRESS); - let decoded_call = common::NativeFiatTokenV2_2::Transfer::decode_log(&logs[0]) - .expect("Failed to decode CALL Transfer log"); - assert_eq!(decoded_call.from, sender.address()); - assert_eq!(decoded_call.to, factory); - assert_eq!(decoded_call.value, endowment); - - // Second log: factory → create2_addr (the CREATE2 endowment) - assert_eq!(logs[1].address, EIP7708_LOG_ADDRESS); - let decoded_create2 = common::NativeFiatTokenV2_2::Transfer::decode_log(&logs[1]) - .expect("Failed to decode CREATE2 Transfer log"); - assert_eq!( - decoded_create2.from, factory, - "CREATE2 Transfer from should be the factory" - ); - assert_eq!( - decoded_create2.to, expected_create2_addr, - "CREATE2 Transfer to should be the deterministic CREATE2 address" - ); - assert_eq!( - decoded_create2.value, endowment, - "CREATE2 Transfer amount should match the endowment" - ); - - let call_config = CallConfig { - with_log: Some(true), - only_top_call: Some(false), - }; - let mut inspector = - TracingInspector::new(TracingInspectorConfig::from_geth_call_config(&call_config)); - let mut trace_evm = - evm_config.evm_with_env_and_inspector(trace_db, trace_evm_env, &mut inspector); - let trace_exec = trace_evm - .transact_raw(tx.clone()) - .expect("inspected CREATE2 tx should be accepted"); - assert!( - trace_exec.result.is_success(), - "inspected CREATE2 with value should succeed: {:?}", - trace_exec.result - ); - drop(trace_evm); - - let frame = inspector - .with_transaction_gas_limit(tx.gas_limit) - .into_geth_builder() - .geth_call_traces(call_config, trace_exec.result.gas_used()); - assert_eq!(frame.typ, "CALL"); - assert_eq!(frame.to, Some(factory)); - assert_eq!( - frame.calls.len(), - 1, - "factory should have exactly one CREATE2 child frame" - ); - let create2_frame = &frame.calls[0]; - assert_eq!(create2_frame.typ, "CREATE2"); - assert_eq!(create2_frame.to, Some(expected_create2_addr)); -} diff --git a/crates/precompiles/Cargo.toml b/crates/precompiles/Cargo.toml deleted file mode 100644 index b88e32b..0000000 --- a/crates/precompiles/Cargo.toml +++ /dev/null @@ -1,58 +0,0 @@ -[package] -name = "arc-precompiles" -version.workspace = true -edition.workspace = true -readme.workspace = true -license.workspace = true -exclude.workspace = true -rust-version.workspace = true -publish.workspace = true -repository.workspace = true - -[[bin]] -name = "generate_pq_test_vectors" -path = "src/bin/generate_pq_test_vectors.rs" - -[features] -integration = [] -test-utils = [] - -[dependencies] - -# alloy -alloy-evm.workspace = true -alloy-primitives.workspace = true -alloy-sol-types.workspace = true -arc-execution-config.workspace = true - -# local crates -reth-ethereum = { workspace = true, features = ["evm"] } -reth-evm.workspace = true - -# revm -revm.workspace = true -revm-context-interface.workspace = true -revm-interpreter.workspace = true -revm-primitives.workspace = true -serde = { workspace = true, features = ["derive"] } -serde_json = { workspace = true, default-features = false, features = ["alloc"] } -slh-dsa.workspace = true -thiserror.workspace = true - -[dev-dependencies] -alloy-consensus.workspace = true -arc-evm.workspace = true -arc-execution-config = { workspace = true, features = ["test-utils"] } -criterion.workspace = true -reth-chainspec.workspace = true -reth-ethereum-forks.workspace = true -revm-context-interface.workspace = true -serde_with.workspace = true -sha2.workspace = true - -[[bench]] -name = "pq" -harness = false - -[lints] -workspace = true diff --git a/crates/precompiles/benches/pq.rs b/crates/precompiles/benches/pq.rs deleted file mode 100644 index 9e2559d..0000000 --- a/crates/precompiles/benches/pq.rs +++ /dev/null @@ -1,93 +0,0 @@ -// Copyright 2026 Circle Internet Group, Inc. All rights reserved. -// -// SPDX-License-Identifier: Apache-2.0 -// -// Licensed under the Apache License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. -// You may obtain a copy of the License at -// -// http://www.apache.org/licenses/LICENSE-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// See the License for the specific language governing permissions and -// limitations under the License. - -use alloy_primitives::keccak256; -use criterion::{criterion_group, criterion_main, Criterion}; -use sha2::{Digest, Sha256}; -use slh_dsa::{ - signature::{Keypair, Signer, Verifier}, - Sha2_128s, Signature, SigningKey, VerifyingKey as SlhDsaVerifyingKey, -}; -use std::hint::black_box; - -const HASH_INPUT_BYTES: usize = 64; - -// A single worst-case SLH-DSA-SHA2-128s verification performs ~4,000 SHA-256 -// compression-block-equivalent calls with `slh-dsa`'s pk_seed midstate caching. -// The benchmark below compares the real verifier against one 64-byte SHA-256 -// digest. On the short local PR calibration run, SLH-DSA measured ~87.1 us -// while ~4,000 * sha256_64_bytes measured ~901 us. The gas comparison is -// intentionally more conservative: 230,000 gas is greater than ~4,000 * 24 = -// ~96,000 pure SHA-256 work gas. - -struct PqBenchmarkVector { - verifying_key: SlhDsaVerifyingKey, - message: [u8; 32], - signature: Signature, -} - -fn pq_benchmark_vector() -> PqBenchmarkVector { - let sk_seed = [1u8; 16]; - let sk_prf = [2u8; 16]; - let pk_seed = [3u8; 16]; - let signing_key = SigningKey::::slh_keygen_internal(&sk_seed, &sk_prf, &pk_seed); - let message = [0xA5; 32]; - let signature = signing_key.sign(&message); - - PqBenchmarkVector { - verifying_key: signing_key.verifying_key().clone(), - message, - signature, - } -} - -fn benchmark_pq(c: &mut Criterion) { - let vector = pq_benchmark_vector(); - let hash_input = [0xA5; HASH_INPUT_BYTES]; - - let mut group = c.benchmark_group("pq"); - - group.bench_function("slh_dsa_sha2_128s_verify", |b| { - b.iter(|| { - let is_valid = black_box(&vector.verifying_key) - .verify( - black_box(vector.message.as_slice()), - black_box(&vector.signature), - ) - .is_ok(); - black_box(is_valid) - }); - }); - - group.bench_function("sha256_64_bytes", |b| { - b.iter(|| { - let digest = Sha256::digest(black_box(hash_input.as_slice())); - black_box(digest) - }); - }); - - group.bench_function("keccak256_64_bytes", |b| { - b.iter(|| { - let digest = keccak256(black_box(hash_input.as_slice())); - black_box(digest) - }); - }); - - group.finish(); -} - -criterion_group!(benches, benchmark_pq); -criterion_main!(benches); diff --git a/crates/precompiles/src/bin/generate_pq_test_vectors.rs b/crates/precompiles/src/bin/generate_pq_test_vectors.rs deleted file mode 100644 index e85287d..0000000 --- a/crates/precompiles/src/bin/generate_pq_test_vectors.rs +++ /dev/null @@ -1,96 +0,0 @@ -// Copyright 2026 Circle Internet Group, Inc. All rights reserved. -// -// SPDX-License-Identifier: Apache-2.0 - -//! Generate test vectors for PQ signature verification tests -//! -//! This binary generates valid test vectors for all supported PQ signature schemes -//! and outputs them in JSON format for use in TypeScript tests. - -use serde::Serialize; -use slh_dsa::{ - signature::{Keypair as SlhDsaKeypair, Signer as SlhDsaSigner}, - Sha2_128s, SigningKey as SlhDsaSigningKey, -}; - -#[derive(Serialize)] -struct TestVector { - scheme: String, - verifying_key: String, - message: String, - signature: String, - is_valid: bool, -} - -#[derive(Serialize)] -struct TestVectors { - slh_dsa_sha2_128s: Vec, -} - -fn bytes_to_hex(bytes: &[u8]) -> String { - format!( - "0x{}", - bytes - .iter() - .map(|b| format!("{:02x}", b)) - .collect::() - ) -} - -fn generate_slh_dsa_sha2_vectors() -> Vec { - let mut vectors = Vec::new(); - - // Generate a valid keypair using internal keygen with deterministic seeds - let sk_seed = [1u8; 16]; - let sk_prf = [2u8; 16]; - let pk_seed = [3u8; 16]; - let signing_key = - SlhDsaSigningKey::::slh_keygen_internal(&sk_seed, &sk_prf, &pk_seed); - let verifying_key = signing_key.verifying_key(); - let public_key = verifying_key.to_bytes(); - - // Test 1: Valid signature - let msg1 = b"Hello, World!"; - let sig1 = signing_key.sign(msg1); - vectors.push(TestVector { - scheme: "SLH-DSA-SHA2-128s".to_string(), - verifying_key: bytes_to_hex(&public_key), - message: bytes_to_hex(msg1), - signature: bytes_to_hex(&sig1.to_bytes()), - is_valid: true, - }); - - // Test 2: Valid signature with empty message - let msg2 = b""; - let sig2 = signing_key.sign(msg2); - vectors.push(TestVector { - scheme: "SLH-DSA-SHA2-128s".to_string(), - verifying_key: bytes_to_hex(&public_key), - message: bytes_to_hex(msg2), - signature: bytes_to_hex(&sig2.to_bytes()), - is_valid: true, - }); - - // Test 3: Invalid signature (wrong message) - let msg3 = b"Hello, World!"; - let msg3_wrong = b"Goodbye, World!"; - let sig3 = signing_key.sign(msg3); - vectors.push(TestVector { - scheme: "SLH-DSA-SHA2-128s".to_string(), - verifying_key: bytes_to_hex(&public_key), - message: bytes_to_hex(msg3_wrong), - signature: bytes_to_hex(&sig3.to_bytes()), - is_valid: false, - }); - - vectors -} - -fn main() { - let vectors = TestVectors { - slh_dsa_sha2_128s: generate_slh_dsa_sha2_vectors(), - }; - - let json = serde_json::to_string_pretty(&vectors).expect("Failed to serialize test vectors"); - println!("{}", json); -} diff --git a/crates/precompiles/src/call_from.rs b/crates/precompiles/src/call_from.rs deleted file mode 100644 index 328c876..0000000 --- a/crates/precompiles/src/call_from.rs +++ /dev/null @@ -1,285 +0,0 @@ -// Copyright 2026 Circle Internet Group, Inc. All rights reserved. -// -// SPDX-License-Identifier: Apache-2.0 -// -// Licensed under the Apache License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. -// You may obtain a copy of the License at -// -// http://www.apache.org/licenses/LICENSE-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// See the License for the specific language governing permissions and -// limitations under the License. - -//! CallFrom subcall precompile — sender-preserving sub-calls. -//! -//! Allows an allowlisted caller contract to invoke a target contract while specifying -//! a custom `msg.sender`. This is the building block for Memo and Multicall3From. -//! -//! ## Solidity Interface -//! -//! ```solidity -//! function callFrom(address sender, address target, bytes calldata data) -//! external returns (bool success, bytes memory returnData); -//! ``` -//! -//! ## Security -//! -//! Access is restricted via the subcall registry's allowlist — only allowlisted contracts -//! may invoke CallFrom. The `sender` parameter is trusted: the allowlisted caller is -//! responsible for ensuring it passes the correct sender (typically its own `msg.sender`). - -use crate::subcall::{ - SubcallCompletionResult, SubcallContinuationData, SubcallError, SubcallInitResult, - SubcallPrecompile, -}; -use alloy_primitives::{address, Address, U256}; -use alloy_sol_types::{sol, SolCall}; -use revm::handler::FrameResult; -use revm::interpreter::interpreter_action::{CallInput, CallInputs, CallScheme, CallValue}; -use revm_context_interface::cfg::gas; - -/// Address of the CallFrom precompile: `0x1800...0003` -pub const CALL_FROM_ADDRESS: Address = address!("1800000000000000000000000000000000000003"); - -/// Fixed base gas for init_subcall ABI decoding of `callFrom(address, address, bytes)`. -/// -/// Covers selector matching plus the fixed-size ABI head: 2 address words + 1 offset word + -/// 1 length word. The dynamic `bytes data` payload is charged separately at -/// [`COPY`] gas per 32-byte word (see [`abi_decode_gas`]). -pub const ABI_DECODE_BASE_GAS: u64 = 100; - -/// Computes total init_subcall gas: base overhead + ceil(data.len() / 32) * COPY. -pub fn abi_decode_gas(data_len: usize) -> u64 { - let words = (data_len as u64).div_ceil(32); - ABI_DECODE_BASE_GAS.saturating_add(words.saturating_mul(gas::COPY)) -} - -/// Fixed base gas for complete_subcall ABI encoding of `(bool success, bytes returnData)`. -/// Covers the fixed-size tuple header (bool + offset + length words). -pub const ABI_ENCODE_BASE_GAS: u64 = 100; - -/// Computes total complete_subcall gas: base overhead + ceil(data.len() / 32) * COPY. -pub fn abi_encode_gas(data_len: usize) -> u64 { - let words = (data_len as u64).div_ceil(32); - ABI_ENCODE_BASE_GAS.saturating_add(words.saturating_mul(gas::COPY)) -} - -sol! { - /// CallFrom precompile interface. - interface ICallFrom { - /// Execute a call to `target` with `data` as calldata, preserving `sender` as msg.sender. - function callFrom(address sender, address target, bytes calldata data) external returns (bool success, bytes memory returnData); - } -} - -/// Stateless CallFrom precompile implementing the two-phase subcall pattern. -/// -/// `init_subcall`: ABI-decode `(sender, target, data)`, construct child call with the given sender. -/// `complete_subcall`: ABI-encode `(bool success, bytes returnData)` from the child result. -#[derive(Debug)] -pub struct CallFromPrecompile; - -/// ABI-decode `callFrom(address, address, bytes)` from `inputs` and build the child -/// [`CallInputs`]. Returns the child inputs and the gas overhead consumed by decoding. -/// -/// Shared by [`CallFromPrecompile::init_subcall`] and -/// [`CallFromPrecompile::trace_child_call`]. -fn decode_child_call(inputs: &CallInputs) -> Result<(CallInputs, u64), SubcallError> { - let input_bytes = match &inputs.input { - CallInput::Bytes(b) => b, - // SharedBuffer should not occur for precompile calls dispatched via frame_init, - // but handle it defensively. - CallInput::SharedBuffer(_) => { - return Err(SubcallError::AbiDecodeError( - "unexpected shared buffer input".into(), - )); - } - }; - let decoded = ICallFrom::callFromCall::abi_decode_validate(input_bytes) - .map_err(|e| SubcallError::AbiDecodeError(format!("callFrom: {e}")))?; - - let sender = decoded.sender; - let target = decoded.target; - let calldata = decoded.data; - - // init_subcall overhead: fixed base + per-word charge for the dynamic `bytes data`. - let overhead = abi_decode_gas(calldata.len()); - - // EIP-150: deduct init_subcall overhead, then forward 63/64ths to child. - // Note: the EVM layer (`ArcEvm::init_subcall`) recalculates child_gas_limit to - // include EIP-2929 account access costs. This calculation serves as a fast-fail - // OOG check for the ABI decode overhead alone. - let available = inputs.gas_limit.checked_sub(overhead).ok_or_else(|| { - SubcallError::InsufficientGas("gas limit below ABI decode overhead".into()) - })?; - // EIP-150: forward 63/64ths of available gas to child. - // available / 64 <= available, so the subtraction cannot underflow. - #[allow(clippy::arithmetic_side_effects)] - let child_gas_limit = available - (available / 64); - - let child_inputs = CallInputs { - scheme: CallScheme::Call, - target_address: target, - bytecode_address: target, - known_bytecode: None, - value: CallValue::Transfer(U256::ZERO), - input: CallInput::Bytes(calldata), - gas_limit: child_gas_limit, - is_static: false, - caller: sender, - return_memory_offset: 0..0, - }; - - Ok((child_inputs, overhead)) -} - -impl SubcallPrecompile for CallFromPrecompile { - fn init_subcall(&self, inputs: &CallInputs) -> Result { - let (child_inputs, overhead) = decode_child_call(inputs)?; - - Ok(SubcallInitResult { - child_inputs: Box::new(child_inputs), - continuation_data: SubcallContinuationData { - // CallFrom is stateless — no continuation state needed - state: Box::new(()), - }, - gas_overhead: overhead, - }) - } - - fn trace_child_call(&self, inputs: &CallInputs) -> Option { - decode_child_call(inputs) - .ok() - .map(|(child_inputs, _)| child_inputs) - } - - fn complete_subcall( - &self, - _continuation_data: SubcallContinuationData, - child_result: &FrameResult, - ) -> Result { - let outcome = match child_result { - FrameResult::Call(outcome) => outcome, - _ => return Err(SubcallError::UnexpectedFrameResult), - }; - - let child_success = outcome.result.result.is_ok(); - let child_output = outcome.result.output.clone(); - let encode_gas = abi_encode_gas(child_output.len()); - - // ABI-encode (bool success, bytes returnData) matching the declared interface. - // The precompile always succeeds; the caller inspects the bool to determine - // whether the child call succeeded or reverted. - let encoded = ICallFrom::callFromCall::abi_encode_returns(&ICallFrom::callFromReturn { - success: child_success, - returnData: child_output, - }); - - Ok(SubcallCompletionResult { - output: encoded.into(), - success: true, - gas_overhead: encode_gas, - }) - } -} - -#[cfg(test)] -mod tests { - use super::*; - - /// Guard against silent upstream changes to the EVM COPY gas cost. - /// An unexpected change would alter `abi_decode_gas` and `abi_encode_gas` results, - /// effectively creating an unintentional hardfork. - #[test] - fn evm_copy_gas_cost_is_3() { - assert_eq!( - gas::COPY, - 3, - "revm COPY gas cost changed — review abi_decode_gas/abi_encode_gas impact" - ); - } - - #[test] - fn abi_encode_gas_base_plus_per_word() { - // 0 bytes → base only (no per-word charge since div_ceil(0, 32) = 0) - assert_eq!(abi_encode_gas(0), ABI_ENCODE_BASE_GAS); - // 1 byte → 1 word - assert_eq!(abi_encode_gas(1), ABI_ENCODE_BASE_GAS + gas::COPY); - // 32 bytes → 1 word - assert_eq!(abi_encode_gas(32), ABI_ENCODE_BASE_GAS + gas::COPY); - // 33 bytes → 2 words - assert_eq!(abi_encode_gas(33), ABI_ENCODE_BASE_GAS + 2 * gas::COPY); - // 64 bytes → 2 words - assert_eq!(abi_encode_gas(64), ABI_ENCODE_BASE_GAS + 2 * gas::COPY); - } - - /// Guard against `trace_child_call` and `init_subcall` diverging. - /// - /// Both methods share `decode_child_call`, but if someone bypasses it in one - /// path, the trace identity would silently drift from the executed identity. - /// This test asserts that caller, target, scheme, value, and calldata match. - /// Gas limit is intentionally excluded — `init_subcall` applies EIP-150 - /// 63/64ths forwarding while `trace_child_call` passes through the raw limit. - #[test] - fn trace_child_call_matches_init_subcall() { - use alloy_primitives::address; - use alloy_sol_types::SolCall; - use revm::interpreter::interpreter_action::{CallInput, CallScheme, CallValue}; - - let precompile = CallFromPrecompile; - let sender = address!("e000000000000000000000000000000000000001"); - let target = address!("c000000000000000000000000000000000000002"); - let child_data: Vec = vec![0x42, 0x43]; - let gas_limit: u64 = 100_000; - - let calldata = ICallFrom::callFromCall { - sender, - target, - data: child_data.clone().into(), - } - .abi_encode(); - - let inputs = CallInputs { - scheme: CallScheme::Call, - target_address: CALL_FROM_ADDRESS, - bytecode_address: CALL_FROM_ADDRESS, - known_bytecode: None, - value: CallValue::Transfer(U256::ZERO), - input: CallInput::Bytes(calldata.into()), - gas_limit, - is_static: false, - caller: address!("c000000000000000000000000000000000000001"), - return_memory_offset: 0..0, - }; - - let init_result = precompile - .init_subcall(&inputs) - .expect("init_subcall should succeed"); - let trace_result = precompile - .trace_child_call(&inputs) - .expect("trace_child_call should return Some for valid input"); - - let init_child = &*init_result.child_inputs; - - assert_eq!(trace_result.caller, init_child.caller, "caller mismatch"); - assert_eq!( - trace_result.target_address, init_child.target_address, - "target_address mismatch" - ); - assert_eq!( - trace_result.bytecode_address, init_child.bytecode_address, - "bytecode_address mismatch" - ); - assert_eq!(trace_result.scheme, init_child.scheme, "scheme mismatch"); - assert_eq!(trace_result.value, init_child.value, "value mismatch"); - assert_eq!(trace_result.input, init_child.input, "input mismatch"); - assert_eq!( - trace_result.is_static, init_child.is_static, - "is_static mismatch" - ); - } -} diff --git a/crates/precompiles/src/helpers.rs b/crates/precompiles/src/helpers.rs deleted file mode 100644 index 13e4493..0000000 --- a/crates/precompiles/src/helpers.rs +++ /dev/null @@ -1,1074 +0,0 @@ -// Copyright 2025 Circle Internet Group, Inc. All rights reserved. -// -// SPDX-License-Identifier: Apache-2.0 -// -// Licensed under the Apache License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. -// You may obtain a copy of the License at -// -// http://www.apache.org/licenses/LICENSE-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// See the License for the specific language governing permissions and -// limitations under the License. - -use alloy_evm::EvmInternals; -use alloy_primitives::{Address, Bytes, StorageKey, U256}; -use alloy_sol_types::{SolCall, SolEvent, SolValue}; -use reth_ethereum::evm::revm::precompile::{PrecompileError, PrecompileOutput}; -use reth_evm::precompiles::PrecompileInput; -use revm::context_interface::journaled_state::TransferError; -use revm::state::AccountInfo; -use revm_context_interface::cfg::gas::CALL_STIPEND; -use revm_context_interface::journaled_state::account::JournaledAccountTr; -use revm_interpreter::Gas; -use revm_primitives::address; -use revm_primitives::constants::KECCAK_EMPTY; - -use arc_execution_config::hardforks::{ArcHardfork, ArcHardforkFlags}; - -// system addresses in genesis -pub const NATIVE_FIAT_TOKEN_ADDRESS: Address = - address!("0x3600000000000000000000000000000000000000"); - -/// Selector for the Solidity Error(string) format used in revert messages. -pub const REVERT_SELECTOR: [u8; 4] = [0x08, 0xc3, 0x79, 0xa0]; - -/// Approximate gas costs for precompile read / writes -pub const PRECOMPILE_SSTORE_GAS_COST: u64 = 2900; -pub const PRECOMPILE_SLOAD_GAS_COST: u64 = 2100; -/// EIP-161 account creation surcharge when crediting an empty account. -pub const PRECOMPILE_EMPTY_ACCOUNT_GAS_COST: u64 = 25_000; - -/// Gas costs for emitting a log -pub const LOG_BASE_COST: u64 = 375; // Base cost for emitting a log -pub const LOG_TOPIC_COST: u64 = 375; // Cost per log topic -pub const LOG_DATA_COST: u64 = 8; // Cost per byte of log data - -/// Common precompile revert messages -pub const ERR_EXECUTION_REVERTED: &str = "Execution reverted"; -pub const ERR_INSUFFICIENT_FUNDS: &str = "Insufficient funds"; -pub const ERR_OVERFLOW: &str = "Arithmetic overflow"; -pub const ERR_INVALID_CALLER: &str = "Invalid caller"; -pub const ERR_CLEAR_EMPTY: &str = "Cannot clear balance of empty account"; -pub const ERR_DELEGATE_CALL_NOT_ALLOWED: &str = "Delegate call not allowed"; -pub const ERR_STATE_CHANGE_DURING_STATIC_CALL: &str = "State change during static call"; -pub const ERR_BLOCKED_ADDRESS: &str = "Blocked address"; -pub const ERR_ZERO_ADDRESS: &str = "Zero address not allowed"; -pub const ERR_SELFDESTRUCTED_BALANCE_INCREASED: &str = - "Cannot increase the balance of selfdestructed account"; - -/// Encodes a revert error string into ABI‑encoded bytes according to Solidity’s Error(string) format. -/// -/// The returned bytes consist of: -/// - 4 bytes selector: 0x08c379a0 -/// - ABI-encoded string value of the error message. -pub fn revert_message_to_bytes(msg: &str) -> Bytes { - let encoded = msg.abi_encode(); - let mut result = Vec::with_capacity(REVERT_SELECTOR.len().saturating_add(encoded.len())); - result.extend_from_slice(&REVERT_SELECTOR); - result.extend_from_slice(&encoded); - Bytes::from(result) -} - -/// Gas penalty added to early-path reverts so callers cannot probe precompiles -/// for free. -/// -/// Pre-Zero6: applied only to ABI decode failures (truncated input, unknown -/// selector) via `new_reverted_with_penalty`. -/// -/// Zero6+: also applied to authorization and validation reverts (unauthorized -/// caller, blocklist, zero address, zero amount, overflow) via -/// [`new_reverted_with_early_penalty`]. -pub(crate) const PRECOMPILE_EARLY_REVERT_GAS_PENALTY: u64 = 200; - -/// Enum to represent either a reverted precompile output or an error -pub(crate) enum PrecompileErrorOrRevert { - Revert(PrecompileOutput), - Error(PrecompileError), -} - -impl PrecompileErrorOrRevert { - pub(crate) fn new_reverted(gas_counter: Gas, msg: &str) -> Self { - Self::Revert(PrecompileOutput::new_reverted( - gas_counter.used(), - revert_message_to_bytes(msg), - )) - } - - pub(crate) fn new_reverted_with_penalty(gas_counter: Gas, gas_penalty: u64, msg: &str) -> Self { - let mut gas_with_penalty = gas_counter; - if !gas_with_penalty.record_cost(gas_penalty) { - return Self::Error(PrecompileError::OutOfGas); - } - Self::Revert(PrecompileOutput::new_reverted( - gas_with_penalty.used(), - revert_message_to_bytes(msg), - )) - } -} - -/// Gas cost to load an account balance for stateful precompiles. -/// -/// Under Zero6+, applies EIP-2929 warm/cold pricing. Before Zero6, a flat -/// cost is charged (matches pre-hardfork behavior for the `balance_incr`, -/// `balance_decr` and `transfer` helpers). -fn account_load_cost(is_cold: bool, hardfork_flags: ArcHardforkFlags) -> u64 { - if hardfork_flags.is_active(ArcHardfork::Zero6) { - if is_cold { - revm_interpreter::gas::COLD_ACCOUNT_ACCESS_COST - } else { - revm_interpreter::gas::WARM_STORAGE_READ_COST - } - } else { - PRECOMPILE_SLOAD_GAS_COST - } -} - -fn storage_io_error(op: &str, e: impl core::fmt::Debug) -> PrecompileErrorOrRevert { - PrecompileErrorOrRevert::Error(PrecompileError::Other( - format!("Storage {op} failed: {e:?}").into(), - )) -} - -fn record_zero6_empty_account_creation_cost( - gas_counter: &mut Gas, - account_info: &AccountInfo, - amount: U256, - hardfork_flags: ArcHardforkFlags, -) -> Result<(), PrecompileErrorOrRevert> { - if hardfork_flags.is_active(ArcHardfork::Zero6) && !amount.is_zero() && account_info.is_empty() - { - record_cost_or_out_of_gas(gas_counter, PRECOMPILE_EMPTY_ACCOUNT_GAS_COST)?; - } - Ok(()) -} - -pub(crate) fn record_cost_or_out_of_gas( - gas_counter: &mut Gas, - cost: u64, -) -> Result<(), PrecompileErrorOrRevert> { - if !gas_counter.record_cost(cost) { - return Err(PrecompileErrorOrRevert::Error(PrecompileError::OutOfGas)); - } - Ok(()) -} - -pub(crate) fn check_gas_remaining( - gas_counter: &Gas, - cost: u64, -) -> Result<(), PrecompileErrorOrRevert> { - if gas_counter.remaining() < cost { - return Err(PrecompileErrorOrRevert::Error(PrecompileError::OutOfGas)); - } - Ok(()) -} - -impl From for Result { - fn from(val: PrecompileErrorOrRevert) -> Self { - match val { - PrecompileErrorOrRevert::Revert(output) => Ok(output.reverted()), - PrecompileErrorOrRevert::Error(error) => Err(error), - } - } -} - -/// Build a revert that charges [`PRECOMPILE_EARLY_REVERT_GAS_PENALTY`] -/// when Zero6 is active, and zero gas otherwise. -/// -/// Use at early-path reverts (unauthorized caller, blocklist, zero address, -/// zero amount, overflow) to give uniform gas accounting under Zero6 and -/// prevent free probing of precompile revert paths. -pub(crate) fn new_reverted_with_early_penalty( - gas_counter: Gas, - msg: &str, - hardfork_flags: ArcHardforkFlags, -) -> PrecompileErrorOrRevert { - if hardfork_flags.is_active(ArcHardfork::Zero6) { - PrecompileErrorOrRevert::new_reverted_with_penalty( - gas_counter, - PRECOMPILE_EARLY_REVERT_GAS_PENALTY, - msg, - ) - } else { - PrecompileErrorOrRevert::new_reverted(gas_counter, msg) - } -} - -/// ABI-decodes raw precompile call arguments. -/// -/// Pre-Zero6, this preserves the legacy lenient Alloy decode behavior. Zero6 -/// switches to validated decoding, which rejects non-canonical ABI padding for -/// short static types such as `address`, `bool`, and `uint64`. -pub(crate) fn abi_decode_raw_with_zero6_validation( - input: &[u8], - hardfork_flags: ArcHardforkFlags, -) -> alloy_sol_types::Result { - if hardfork_flags.is_active(ArcHardfork::Zero6) { - C::abi_decode_raw_validate(input) - } else { - C::abi_decode_raw(input) - } -} - -/// Reads a value from storage for stateful precompiles. -/// -/// # Parameters -/// - `internals`: The execution context with journal access -/// - `address`: The address whose storage to read from -/// - `storage_key`: The storage slot to read -/// - `gas_counter`: Available gas for this operation -/// - `hardfork`: The current hardfork for gas calculation -/// -/// # Gas Cost -/// - Pre-Zero5: Fixed cost of 2,100 gas units -/// - Zero5+: EIP-2929 warm/cold aware (100 warm, 2100 cold) -/// -/// # Returns -/// - `Ok(Bytes)`: The stored value as big-endian bytes -/// - `Err(PrecompileErrorOrRevert)`: If out of gas or storage read fails -/// -/// # Example -/// ```rust,ignore -/// let output = read(internals, precompile_address, StorageKey::ZERO, gas_counter, &hardfork)?; -/// let value = U256::from_be_slice(&output); -/// ``` -pub(crate) fn read( - internals: &mut EvmInternals, - address: Address, - storage_key: StorageKey, - gas_counter: &mut Gas, - hardfork_flags: ArcHardforkFlags, -) -> Result { - if hardfork_flags.is_active(ArcHardfork::Zero5) { - let mut account = internals - .load_account_mut(address) - .map_err(|e| storage_io_error("read", e))? - .data; - - // Probe slot warmth without DB I/O (skip_cold_load=true). - // Warm → Ok with cached value. Cold → ColdLoadSkipped error, retry after charging. - match account.sload(storage_key.into(), true) { - Ok(slot_load) => { - record_cost_or_out_of_gas( - gas_counter, - revm_interpreter::gas::WARM_STORAGE_READ_COST, - )?; - Ok(slot_load.data.present_value().to_be_bytes_vec().into()) - } - Err(e) if e.is_cold_load_skipped() => { - record_cost_or_out_of_gas(gas_counter, revm_interpreter::gas::COLD_SLOAD_COST)?; - let slot_load = account - .sload(storage_key.into(), false) - .map_err(|e| storage_io_error("read", e))?; - Ok(slot_load.data.present_value().to_be_bytes_vec().into()) - } - Err(e) => Err(storage_io_error("read", e)), - } - } else { - record_cost_or_out_of_gas(gas_counter, PRECOMPILE_SLOAD_GAS_COST)?; - let state_load = internals - .sload(address, storage_key.into()) - .map_err(|e| storage_io_error("read", e))?; - Ok(state_load.data.to_be_bytes_vec().into()) - } -} - -/// Value-change component of SSTORE gas, excluding the cold-load penalty. -/// -/// Mirrors revm v29 `istanbul_sstore_cost`. -fn sstore_base_cost(original: U256, present: U256, new: U256) -> u64 { - if new == present { - revm_interpreter::gas::WARM_STORAGE_READ_COST - } else if original == present { - if original.is_zero() { - revm_interpreter::gas::SSTORE_SET - } else { - revm_interpreter::gas::WARM_SSTORE_RESET - } - } else { - revm_interpreter::gas::WARM_STORAGE_READ_COST - } -} - -/// Writes a value to storage for stateful precompiles. -/// -/// # Parameters -/// - `internals`: The execution context with journal access -/// - `address`: The address whose storage to write to -/// - `storage_key`: The storage slot to write -/// - `input`: The value to store (as big-endian bytes) -/// - `gas_counter`: Available gas for this operation -/// - `hardfork`: The current hardfork for gas calculation -/// -/// # Gas Cost -/// - Pre-Zero5: Fixed cost of 2,900 gas units -/// - Zero5+: EIP-2929/EIP-2200 aware (varies based on warm/cold and value changes) -/// -/// # EIP-2200 Sentry (Zero6+) -/// Mirrors revm's SSTORE opcode behavior: if the remaining gas is less than or -/// equal to [`CALL_STIPEND`] (2,300), the call frame fails with `OutOfGas` -/// before any storage mutation is journaled. -/// -/// # Returns -/// - `Ok(())`: Success -/// - `Err(PrecompileErrorOrRevert)`: If out of gas or storage write fails -/// -/// # Example -/// ```rust,ignore -/// let new_value = U256::from(42); -/// write( -/// internals, -/// precompile_address, -/// StorageKey::ZERO, -/// &new_value.to_be_bytes_vec(), -/// gas_counter, -/// &hardfork -/// )?; -/// ``` -pub(crate) fn write( - internals: &mut EvmInternals, - address: Address, - storage_key: StorageKey, - input: &[u8], - gas_counter: &mut Gas, - hardfork_flags: ArcHardforkFlags, -) -> Result<(), PrecompileErrorOrRevert> { - // EIP-2200 reentrancy sentry: refuse SSTORE when remaining gas does not - // exceed the call stipend. - if hardfork_flags.is_active(ArcHardfork::Zero6) && gas_counter.remaining() <= CALL_STIPEND { - return Err(PrecompileErrorOrRevert::Error(PrecompileError::OutOfGas)); - } - - let value = U256::from_be_slice(input); - - if hardfork_flags.is_active(ArcHardfork::Zero5) { - let mut account = internals - .load_account_mut(address) - .map_err(|e| storage_io_error("write", e))? - .data; - - // Probe slot warmth via sload to get current values for gas calculation. - // This lets us charge all gas before the actual sstore mutation. - let slot = match account.sload(storage_key.into(), true) { - Ok(slot_load) => slot_load.data, - Err(e) if e.is_cold_load_skipped() => { - record_cost_or_out_of_gas(gas_counter, revm_interpreter::gas::COLD_SLOAD_COST)?; - account - .sload(storage_key.into(), false) - .map_err(|e| storage_io_error("write", e))? - .data - } - Err(e) => return Err(storage_io_error("write", e)), - }; - - record_cost_or_out_of_gas( - gas_counter, - sstore_base_cost(slot.original_value, slot.present_value, value), - )?; - - // All gas charged — safe to mutate. Slot is warm from the sload. - account - .sstore(storage_key.into(), value, false) - .map_err(|e| storage_io_error("write", e))?; - Ok(()) - } else { - record_cost_or_out_of_gas(gas_counter, PRECOMPILE_SSTORE_GAS_COST)?; - internals - .sstore(address, storage_key.into(), value) - .map_err(|e| storage_io_error("write", e))?; - Ok(()) - } -} - -/// Helper to transfer funds between two accounts using the Journal -/// -/// Account gas is charged after the load because `load_account_mut_skip_cold_load` -/// panics on cold accounts in revm ≤36. Storage slot helpers (`read`/`write`) -/// use `skip_cold_load` to charge before I/O; accounts cannot until revm ≥37. -pub(crate) fn transfer( - internals: &mut EvmInternals, - from: Address, - to: Address, - amount: U256, - gas_counter: &mut Gas, - hardfork_flags: ArcHardforkFlags, -) -> Result<(), PrecompileErrorOrRevert> { - let loaded_from_account = internals.load_account(from).map_err(|_| { - PrecompileErrorOrRevert::Error(PrecompileError::Other(ERR_EXECUTION_REVERTED.into())) - })?; - record_cost_or_out_of_gas( - gas_counter, - account_load_cost(loaded_from_account.is_cold, hardfork_flags), - )?; - - // Check that the account can be decremented by the amount - check_can_decr_account(&loaded_from_account.info, amount, gas_counter)?; - - // Mirrors prior balance_decr + balance_incr; Zero6+ uses cold/warm via account_load_cost. - record_cost_or_out_of_gas(gas_counter, PRECOMPILE_SSTORE_GAS_COST)?; - - let to_load = internals.load_account(to).map_err(|_| { - PrecompileErrorOrRevert::Error(PrecompileError::Other(ERR_EXECUTION_REVERTED.into())) - })?; - record_cost_or_out_of_gas( - gas_counter, - account_load_cost(to_load.is_cold, hardfork_flags), - )?; - - record_cost_or_out_of_gas(gas_counter, PRECOMPILE_SSTORE_GAS_COST)?; - - if hardfork_flags.is_active(ArcHardfork::Zero5) && to_load.is_selfdestructed() { - return Err(PrecompileErrorOrRevert::new_reverted( - *gas_counter, - ERR_SELFDESTRUCTED_BALANCE_INCREASED, - )); - } - - record_zero6_empty_account_creation_cost(gas_counter, &to_load.info, amount, hardfork_flags)?; - - let transfer_result = internals.transfer(from, to, amount).map_err(|_e| { - PrecompileErrorOrRevert::new_reverted(*gas_counter, ERR_EXECUTION_REVERTED) - })?; - - match transfer_result { - None => Ok(()), - Some(error) => match error { - // This should never be hit, due to the check prior - TransferError::OutOfFunds => Err(PrecompileErrorOrRevert::new_reverted( - *gas_counter, - ERR_INSUFFICIENT_FUNDS, - )), - TransferError::OverflowPayment => Err(PrecompileErrorOrRevert::new_reverted( - *gas_counter, - ERR_OVERFLOW, - )), - TransferError::CreateCollision => Err(PrecompileErrorOrRevert::new_reverted( - *gas_counter, - ERR_EXECUTION_REVERTED, - )), - }, - } -} - -/// Helper to increment an account's balance by an amount using the Journal -pub(crate) fn balance_incr( - internals: &mut EvmInternals, - to: Address, - amount: U256, - gas_counter: &mut Gas, - hardfork_flags: ArcHardforkFlags, -) -> Result<(), PrecompileErrorOrRevert> { - // Balance check, but doesn't touch state - let account = internals.load_account(to).map_err(|_| { - PrecompileErrorOrRevert::Error(PrecompileError::Other(ERR_EXECUTION_REVERTED.into())) - })?; - record_cost_or_out_of_gas( - gas_counter, - account_load_cost(account.is_cold, hardfork_flags), - )?; - - if hardfork_flags.is_active(ArcHardfork::Zero5) && account.is_selfdestructed() { - return Err(PrecompileErrorOrRevert::new_reverted( - *gas_counter, - ERR_SELFDESTRUCTED_BALANCE_INCREASED, - )); - } - - let account_balance = account.info.balance; - account_balance - .checked_add(amount) - .ok_or(PrecompileErrorOrRevert::new_reverted( - *gas_counter, - ERR_OVERFLOW, - ))?; - - // Update state - record_cost_or_out_of_gas(gas_counter, PRECOMPILE_SSTORE_GAS_COST)?; - record_zero6_empty_account_creation_cost(gas_counter, &account.info, amount, hardfork_flags)?; - internals.balance_incr(to, amount).map_err(|_| { - PrecompileErrorOrRevert::Error(PrecompileError::Other(ERR_EXECUTION_REVERTED.into())) - })?; - - Ok(()) -} - -/// Helper to decrement an account's balance by an amount using the Journal -pub(crate) fn balance_decr( - internals: &mut EvmInternals, - from: Address, - amount: U256, - gas_counter: &mut Gas, - hardfork_flags: ArcHardforkFlags, -) -> Result<(), PrecompileErrorOrRevert> { - let loaded_from_account = internals.load_account(from).map_err(|_| { - PrecompileErrorOrRevert::Error(PrecompileError::Other(ERR_EXECUTION_REVERTED.into())) - })?; - record_cost_or_out_of_gas( - gas_counter, - account_load_cost(loaded_from_account.is_cold, hardfork_flags), - )?; - - // Check that the account can be decremented by the amount - check_can_decr_account(&loaded_from_account.info, amount, gas_counter)?; - - // Perform the decrement - record_cost_or_out_of_gas(gas_counter, PRECOMPILE_SSTORE_GAS_COST)?; - let mut account = internals.load_account_mut(from).map_err(|_| { - PrecompileErrorOrRevert::Error(PrecompileError::Other(ERR_EXECUTION_REVERTED.into())) - })?; - - // False is only returned if insufficient funds, which should theoretically anyways never be reached due to the prior check - if !account.decr_balance(amount) { - return Err(PrecompileErrorOrRevert::new_reverted( - *gas_counter, - ERR_INSUFFICIENT_FUNDS, - )); - } - - Ok(()) -} - -/// Helper to prevent state modifications during static calls -pub(crate) fn check_staticcall( - precompile_input: &PrecompileInput, - gas_counter: &mut Gas, -) -> Result<(), PrecompileErrorOrRevert> { - if precompile_input.is_static { - // Spend all remaining gas - gas_counter.spend_all(); - return Err(PrecompileErrorOrRevert::new_reverted( - *gas_counter, - ERR_STATE_CHANGE_DURING_STATIC_CALL, - )); - } - Ok(()) -} - -/// Helper to check delegatecall -pub(crate) fn check_delegatecall( - precompile_address: Address, - precompile_input: &PrecompileInput, - gas_counter: &Gas, -) -> Result<(), PrecompileErrorOrRevert> { - if precompile_input.target_address != precompile_address - || precompile_input.bytecode_address != precompile_address - { - return Err(PrecompileErrorOrRevert::new_reverted( - *gas_counter, - ERR_DELEGATE_CALL_NOT_ALLOWED, - )); - } - Ok(()) -} - -/// Helper to determine if an account can be decremented by an amount -/// Decrements gas counter if account would be emptied -pub(crate) fn check_can_decr_account( - loaded_account_info: &AccountInfo, - amount: U256, - gas_counter: &mut Gas, -) -> Result<(), PrecompileErrorOrRevert> { - // Check that the account has sufficient balance - let from_account_balance = loaded_account_info.balance.checked_sub(amount).ok_or( - PrecompileErrorOrRevert::new_reverted(*gas_counter, ERR_INSUFFICIENT_FUNDS), - )?; - - // Check that the account would not be emptied if this transfer goes through - let from_account_is_empty = from_account_balance.is_zero() - && loaded_account_info.nonce == 0 - && (loaded_account_info.code_hash() == KECCAK_EMPTY - || loaded_account_info.code_hash().is_zero()); - - if from_account_is_empty { - record_cost_or_out_of_gas(gas_counter, PRECOMPILE_SSTORE_GAS_COST)?; - return Err(PrecompileErrorOrRevert::new_reverted( - *gas_counter, - ERR_CLEAR_EMPTY, - )); - } - - Ok(()) -} - -/// Stores a log event in the journal -pub(crate) fn emit_event( - internals: &mut EvmInternals, - address: Address, - event: &Event, - gas_counter: &mut Gas, -) -> Result<(), PrecompileErrorOrRevert> { - let data = event.encode_log_data(); - - let topic_gas = LOG_TOPIC_COST.saturating_mul(data.topics().len() as u64); - let data_gas = LOG_DATA_COST.saturating_mul(data.data.len() as u64); - let log_gas = LOG_BASE_COST - .saturating_add(topic_gas) - .saturating_add(data_gas); - record_cost_or_out_of_gas(gas_counter, log_gas)?; - - let log = revm::primitives::Log { address, data }; - - internals.log(log); - Ok(()) -} - -#[cfg(test)] -pub(crate) mod test_utils { - use alloy_primitives::{Address, B256, U256}; - use revm::database_interface::{DBErrorMarker, Database, DatabaseRef}; - use revm::state::{AccountInfo, Bytecode}; - use std::cell::Cell; - - /// Database wrapper that counts `storage()` calls via a shared `Cell` - /// counter while returning empty state. Use to prove that an OOG path - /// does not hit the database. - #[derive(Debug, Clone)] - pub(crate) struct TrackingDB { - storage_reads: std::rc::Rc>, - } - - #[derive(Debug, Clone, PartialEq, Eq)] - pub(crate) struct TrackingDBError; - - impl core::fmt::Display for TrackingDBError { - fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result { - write!(f, "TrackingDBError") - } - } - - impl core::error::Error for TrackingDBError {} - impl DBErrorMarker for TrackingDBError {} - - type TrackingContext = revm::Context< - revm::context::BlockEnv, - revm::context::TxEnv, - revm::context::CfgEnv, - TrackingDB, - revm::context::Journal, - >; - - impl TrackingDB { - pub(crate) fn new() -> (Self, std::rc::Rc>) { - let counter = std::rc::Rc::new(Cell::new(0)); - ( - Self { - storage_reads: counter.clone(), - }, - counter, - ) - } - - pub(crate) fn context() -> (TrackingContext, std::rc::Rc>) { - let (db, counter) = Self::new(); - ( - revm::context::Context::new(db, revm_primitives::hardfork::SpecId::default()), - counter, - ) - } - } - - impl Database for TrackingDB { - type Error = TrackingDBError; - - fn basic(&mut self, _address: Address) -> Result, Self::Error> { - Ok(None) - } - - fn code_by_hash(&mut self, _code_hash: B256) -> Result { - Ok(Bytecode::default()) - } - - fn storage(&mut self, _address: Address, _index: U256) -> Result { - self.storage_reads - .set(self.storage_reads.get().saturating_add(1)); - Ok(U256::ZERO) - } - - fn block_hash(&mut self, number: u64) -> Result { - Ok(alloy_primitives::keccak256(number.to_string().as_bytes())) - } - } - - impl DatabaseRef for TrackingDB { - type Error = TrackingDBError; - - fn basic_ref(&self, _address: Address) -> Result, Self::Error> { - Ok(None) - } - - fn code_by_hash_ref(&self, _code_hash: B256) -> Result { - Ok(Bytecode::default()) - } - - fn storage_ref(&self, _address: Address, _index: U256) -> Result { - self.storage_reads - .set(self.storage_reads.get().saturating_add(1)); - Ok(U256::ZERO) - } - - fn block_hash_ref(&self, number: u64) -> Result { - Ok(alloy_primitives::keccak256(number.to_string().as_bytes())) - } - } -} - -#[cfg(test)] -mod tests { - use super::*; - use alloy_primitives::{address, U256}; - use alloy_sol_types::sol; - use revm_primitives::B256; - - /// Demonstrates that revm's `JournalTr::load_account_mut_skip_cold_load` - /// panics on cold accounts, making it unusable for the probe-then-charge - /// pattern we use for storage slots via `JournaledAccountTr::sload/sstore`. - /// - /// This is fixed in revm 37+ (bluealloy/revm#3477). When upgrading past - /// revm 36, this test should start failing (no longer panics). At that - /// point, switch `transfer`/`balance_incr`/`balance_decr` to the - /// skip-cold-then-retry pattern used by `read`/`write`, and remove this test. - #[test] - #[should_panic(expected = "Expected DBError")] - fn load_account_mut_skip_cold_load_panics_on_cold_account() { - use revm::context_interface::journaled_state::JournalTr; - use revm::{Journal, JournalEntry}; - - let db = revm::database_interface::EmptyDB::default(); - let mut journal = Journal::<_, JournalEntry>::new_with_inner(db, Default::default()); - - let cold_address = address!("dead000000000000000000000000000000000001"); - // Panics because the JournalTr impl maps ColdLoadSkipped through - // unwrap_db_error(), which expects a DBError variant. - let _ = journal.load_account_mut_skip_cold_load(cold_address, true); - } - - /// Asserts all branches of [`sstore_base_cost`] with explicit values to - /// catch silent upstream changes. - #[test] - fn sstore_base_cost_covers_all_branches() { - // new == present → WARM_STORAGE_READ_COST (100) - assert_eq!( - sstore_base_cost(U256::from(1), U256::from(2), U256::from(2)), - 100, - ); - - // original == present, original == 0 → SSTORE_SET (20000) - assert_eq!( - sstore_base_cost(U256::ZERO, U256::ZERO, U256::from(1)), - 20000, - ); - - // original == present, original != 0 → WARM_SSTORE_RESET (2900) - assert_eq!( - sstore_base_cost(U256::from(1), U256::from(1), U256::from(2)), - 2900, - ); - - // original != present, new != present → WARM_STORAGE_READ_COST (100) - assert_eq!( - sstore_base_cost(U256::from(1), U256::from(2), U256::from(3)), - 100, - ); - } - - sol! { - interface IAbiDecodeTest { - function takesAddress(address account) external; - function takesUint64(uint64 value) external; - } - } - - #[test] - fn abi_decode_raw_validation_is_zero6_gated_for_address_padding() { - let mut input = [0u8; 32]; - input[..12].fill(0x11); - input[12] = 0xaa; - - let pre_zero6 = abi_decode_raw_with_zero6_validation::( - &input, - ArcHardforkFlags::with(&[ArcHardfork::Zero5]), - ) - .expect("pre-Zero6 decode preserves legacy lenient padding"); - assert_eq!( - pre_zero6.account, - address!("aa00000000000000000000000000000000000000") - ); - - let zero6 = abi_decode_raw_with_zero6_validation::( - &input, - ArcHardforkFlags::with(&[ArcHardfork::Zero6]), - ); - assert!(zero6.is_err(), "Zero6 rejects non-zero address padding"); - } - - #[test] - fn abi_decode_raw_validation_is_zero6_gated_for_uint64_padding() { - let mut input = [0u8; 32]; - input[0] = 0x11; - input[31] = 42; - - let pre_zero6 = abi_decode_raw_with_zero6_validation::( - &input, - ArcHardforkFlags::with(&[ArcHardfork::Zero5]), - ) - .expect("pre-Zero6 decode preserves legacy lenient padding"); - assert_eq!(pre_zero6.value, 42); - - let zero6 = abi_decode_raw_with_zero6_validation::( - &input, - ArcHardforkFlags::with(&[ArcHardfork::Zero6]), - ); - assert!(zero6.is_err(), "Zero6 rejects non-zero uint64 padding"); - } - - #[test] - fn zero6_empty_account_creation_cost_charges_only_for_nonzero_empty_accounts() { - let zero6 = ArcHardforkFlags::with(&[ArcHardfork::Zero6]); - let pre_zero6 = ArcHardforkFlags::with(&[ArcHardfork::Zero5]); - - let mut gas_counter = Gas::new(100_000); - assert!(record_zero6_empty_account_creation_cost( - &mut gas_counter, - &AccountInfo::default(), - U256::from(1), - pre_zero6, - ) - .is_ok()); - assert_eq!(gas_counter.used(), 0); - - assert!(record_zero6_empty_account_creation_cost( - &mut gas_counter, - &AccountInfo::default(), - U256::ZERO, - zero6, - ) - .is_ok()); - assert_eq!(gas_counter.used(), 0); - - assert!(record_zero6_empty_account_creation_cost( - &mut gas_counter, - &AccountInfo::default(), - U256::from(1), - zero6, - ) - .is_ok()); - assert_eq!(gas_counter.used(), PRECOMPILE_EMPTY_ACCOUNT_GAS_COST); - - for non_empty_account in [ - AccountInfo { - balance: U256::from(1), - ..Default::default() - }, - AccountInfo { - nonce: 1, - ..Default::default() - }, - AccountInfo { - code_hash: B256::from([1u8; 32]), - ..Default::default() - }, - ] { - assert!(record_zero6_empty_account_creation_cost( - &mut gas_counter, - &non_empty_account, - U256::from(1), - zero6, - ) - .is_ok()); - assert_eq!(gas_counter.used(), PRECOMPILE_EMPTY_ACCOUNT_GAS_COST); - } - } - - #[test] - fn zero6_empty_account_creation_cost_errors_when_out_of_gas() { - let mut gas_counter = Gas::new(PRECOMPILE_EMPTY_ACCOUNT_GAS_COST.saturating_sub(1)); - assert!(matches!( - record_zero6_empty_account_creation_cost( - &mut gas_counter, - &AccountInfo::default(), - U256::from(1), - ArcHardforkFlags::with(&[ArcHardfork::Zero6]), - ), - Err(PrecompileErrorOrRevert::Error(PrecompileError::OutOfGas)) - )); - } - - // Generated 11/30/2025 with AI assistance - #[test] - fn test_check_can_decr_account() { - struct TestCase { - name: &'static str, - balance: U256, - nonce: u64, - code_hash: [u8; 32], - decr_amount: U256, - expect_revert: bool, - revert_message: &'static str, - expected_gas_used: u64, - } - - let testcases = vec![ - TestCase { - name: "insufficient_funds_reverts_for_non-empty_account", - balance: U256::from(100), - nonce: 1, - code_hash: *KECCAK_EMPTY, - decr_amount: U256::from(101), - expect_revert: true, - revert_message: ERR_INSUFFICIENT_FUNDS, - expected_gas_used: 0, - }, - TestCase { - name: "insufficient_funds_reverts_for_empty_account_with_KECCAK_EMPTY_code_hash", - balance: U256::from(100), - nonce: 0, - code_hash: *KECCAK_EMPTY, - decr_amount: U256::from(101), - expect_revert: true, - revert_message: ERR_INSUFFICIENT_FUNDS, - expected_gas_used: 0, - }, - TestCase { - name: "insufficient_funds_reverts_for_empty_account_with_zero_code_hash", - balance: U256::from(100), - nonce: 0, - code_hash: B256::ZERO.into(), - decr_amount: U256::from(101), - expect_revert: true, - revert_message: ERR_INSUFFICIENT_FUNDS, - expected_gas_used: 0, - }, - TestCase { - name: "custom_revert_if_account_will_be_empty_with_KECCAK_EMPTY_code_hash", - balance: U256::from(100), - nonce: 0, - code_hash: *KECCAK_EMPTY, - decr_amount: U256::from(100), - expect_revert: true, - revert_message: ERR_CLEAR_EMPTY, - expected_gas_used: PRECOMPILE_SSTORE_GAS_COST, - }, - TestCase { - name: "custom_revert_if_account_will_be_empty_with_zero_code_hash", - balance: U256::from(100), - nonce: 0, - code_hash: B256::ZERO.into(), - decr_amount: U256::from(100), - expect_revert: true, - revert_message: ERR_CLEAR_EMPTY, - expected_gas_used: PRECOMPILE_SSTORE_GAS_COST, - }, - TestCase { - name: "can_clear_account_with_non-zero_nonce", - balance: U256::from(100), - nonce: 1, - code_hash: *KECCAK_EMPTY, - decr_amount: U256::from(100), - expect_revert: false, - revert_message: "", - expected_gas_used: 0, - }, - TestCase { - name: "can_clear_account_with_non-empty_code_hash", - balance: U256::from(100), - nonce: 0, - code_hash: B256::from([1u8; 32]).into(), - decr_amount: U256::from(100), - expect_revert: false, - revert_message: "", - expected_gas_used: 0, - }, - TestCase { - name: "account_with_sufficient_funds_can_be_decremented", - balance: U256::from(100), - nonce: 0, - code_hash: *KECCAK_EMPTY, - decr_amount: U256::from(99), - expect_revert: false, - revert_message: "", - expected_gas_used: 0, - }, - ]; - - for tc in testcases { - let mut gas_counter = Gas::new(1_000_000); - let account_info = AccountInfo { - balance: tc.balance, - nonce: tc.nonce, - code_hash: tc.code_hash.into(), - ..Default::default() - }; - - let result = check_can_decr_account(&account_info, tc.decr_amount, &mut gas_counter); - if tc.expect_revert { - assert!( - result.is_err(), - "Test case {}: expected revert but got success", - tc.name - ); - let err = result.err().unwrap(); - match err { - PrecompileErrorOrRevert::Revert(output) => { - let revert_bytes = output.bytes; - let expected_revert_bytes = revert_message_to_bytes(tc.revert_message); - assert_eq!( - revert_bytes, expected_revert_bytes, - "Test case {}: revert message mismatch", - tc.name - ); - } - PrecompileErrorOrRevert::Error(_) => { - panic!("Test case {}: expected revert but got error", tc.name); - } - } - assert_eq!( - gas_counter.used(), - tc.expected_gas_used, - "Test case {}: gas used mismatch", - tc.name - ); - } else { - assert!( - result.is_ok(), - "Test case {}: expected success but got error", - tc.name - ); - assert_eq!( - gas_counter.used(), - tc.expected_gas_used, - "Test case {}: gas used mismatch", - tc.name - ); - } - } - } - - #[test] - fn from_precompile_error_or_revert_revert_sets_reverted_flag() { - let revert_bytes = revert_message_to_bytes("test revert"); - let err_or_revert = - PrecompileErrorOrRevert::Revert(PrecompileOutput::new(1_000, revert_bytes.clone())); - - let result: Result = err_or_revert.into(); - - let output = result.expect("Revert variant must convert to Ok(PrecompileOutput)"); - assert!( - output.reverted, - "canonical From impl must set reverted flag on Revert variant" - ); - assert_eq!(output.gas_used, 1_000); - assert_eq!(output.bytes, revert_bytes); - } - - #[test] - fn from_precompile_error_or_revert_error_maps_to_err() { - let err_or_revert = PrecompileErrorOrRevert::Error(PrecompileError::OutOfGas); - - let result: Result = err_or_revert.into(); - - assert!(matches!(result, Err(PrecompileError::OutOfGas))); - } -} diff --git a/crates/precompiles/src/lib.rs b/crates/precompiles/src/lib.rs deleted file mode 100644 index 9ffa5b1..0000000 --- a/crates/precompiles/src/lib.rs +++ /dev/null @@ -1,208 +0,0 @@ -// Copyright 2025 Circle Internet Group, Inc. All rights reserved. -// -// SPDX-License-Identifier: Apache-2.0 -// -// Licensed under the Apache License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. -// You may obtain a copy of the License at -// -// http://www.apache.org/licenses/LICENSE-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// See the License for the specific language governing permissions and -// limitations under the License. - -//! Custom precompiles for Arc Chain -//! -//! This module provides a framework for implementing custom precompiles in Arc Chain. -//! Precompiles are special contracts deployed at fixed addresses that provide optimized -//! implementations of commonly used functionality. -//! -//! ## Types of Precompiles -//! -//! ### Stateful Precompiles -//! These precompiles can read from and write to storage, making them suitable for: -//! - Managing on-chain state -//! - Implementing complex protocols -//! - Building upgradeable logic -//! -//! Example from our implementation: -//! ```rust,ignore -//! // Define storage keys -//! const COUNTER_STORAGE_KEY: StorageKey = StorageKey::ZERO; -//! -//! // Define the interface -//! sol! { -//! interface IStatefulPrecompile { -//! function increment() external returns (uint256 newValue); -//! function getCounter() external view returns (uint256 value); -//! } -//! } -//! -//! // Implement using the precompile! macro. Each arm receives the calldata bytes -//! // following the 4-byte selector (`input` below) and must evaluate to -//! // `Result`. -//! precompile!(run_stateful_precompile, precompile_input, hardfork_flags; { -//! IStatefulPrecompile::incrementCall => |_input| { -//! (|| -> Result { -//! let mut gas_counter = Gas::new(precompile_input.gas); -//! let mut precompile_input = precompile_input; -//! -//! let output = read( -//! &mut precompile_input.internals, -//! ADDRESS, -//! COUNTER_STORAGE_KEY, -//! &mut gas_counter, -//! hardfork_flags, -//! )?; -//! let new_value = U256::from_be_slice(&output) + U256::from(1); -//! -//! write( -//! &mut precompile_input.internals, -//! ADDRESS, -//! COUNTER_STORAGE_KEY, -//! &new_value.to_be_bytes_vec(), -//! &mut gas_counter, -//! hardfork_flags, -//! )?; -//! -//! Ok(PrecompileOutput::new(gas_counter.used(), new_value.abi_encode().into())) -//! })() -//! }, -//! }); -//! ``` -//! -//! ### Subcall Precompiles -//! Subcall precompiles need to run a child EVM call and then post-process the -//! child result. They are not registered through `ArcPrecompileProvider`. -//! Implement [`crate::subcall::SubcallPrecompile`] instead, then register the -//! address, implementation, and allowed callers in `ArcEvmFactory::build_subcall_registry`. -//! [`crate::call_from::CallFromPrecompile`] is the production example. -//! -//! ## Creating a New Precompile -//! -//! ### Step 1: Choose an Address -//! Select a unique address for your precompile. Convention is to use low addresses: -//! ```rust,ignore -//! const MY_PRECOMPILE_ADDRESS: Address = address!("0x0000000000000000000000000000000000000044"); -//! ``` -//! -//! ### Step 2: Define the Interface -//! Use the `sol!` macro to define your precompile's Solidity interface: -//! ```rust,ignore -//! sol! { -//! interface IMyPrecompile { -//! function myFunction(uint256 param) external returns (uint256); -//! } -//! } -//! ``` -//! -//! ### Step 3: Implement the Logic -//! -//! ```rust,ignore -//! precompile!(run_my_precompile, precompile_input, hardfork_flags; { -//! IMyPrecompile::myFunctionCall => |input| { -//! (|| -> Result { -//! let mut gas_counter = Gas::new(precompile_input.gas); -//! let mut precompile_input = precompile_input; -//! -//! let args = abi_decode_raw_with_zero6_validation::( -//! input, -//! hardfork_flags, -//! ) -//! .map_err(|_| PrecompileErrorOrRevert::new_reverted_with_penalty( -//! gas_counter, -//! PRECOMPILE_EARLY_REVERT_GAS_PENALTY, -//! ERR_EXECUTION_REVERTED, -//! ))?; -//! -//! let output = read( -//! &mut precompile_input.internals, -//! MY_PRECOMPILE_ADDRESS, -//! StorageKey::from(0), -//! &mut gas_counter, -//! hardfork_flags, -//! )?; -//! -//! Ok(PrecompileOutput::new(gas_counter.used(), output)) -//! })() -//! }, -//! }); -//! ``` -//! -//! ### Step 4: Register the Precompile -//! Provider-managed precompiles should be registered in the precompile map. -//! Add a match arm to `ArcPrecompileProvider::create_precompiles_map` in -//! `precompile_provider.rs`: -//! ```rust,ignore -//! MY_PRECOMPILE_ADDRESS => Some(DynPrecompile::new_stateful( -//! PrecompileId::Custom("MY_PRECOMPILE".into()), -//! move |input| run_my_precompile(input, hardfork_flags), -//! )), -//! ``` -//! -//! For a subcall precompile, skip `create_precompiles_map`. Register it in -//! `ArcEvmFactory::build_subcall_registry` with an `AllowedCallers` policy so -//! `ArcEvm::frame_init` can intercept the call and drive the two-phase -//! `init_subcall` / `complete_subcall` flow. -//! -//! ## Gas Accounting -//! -//! The `precompile!` macro does not track gas on its own — each arm constructs a `Gas` -//! counter from `precompile_input.gas` and threads `&mut gas_counter` through the helpers -//! (`read`, `write`, `emit_event`, `balance_incr`, …). Helpers mutate the counter in place -//! and return `PrecompileErrorOrRevert::Error(OutOfGas)` when the remaining gas is -//! insufficient. The macro adds `PRECOMPILE_EARLY_REVERT_GAS_PENALTY` when the -//! selector is unknown or the input is shorter than 4 bytes; arms should use the same -//! penalty when ABI decoding fails. -//! -//! ## Storage Operations -//! -//! `read` and `write` take a mutable borrow of `precompile_input.internals`, the gas -//! counter, and the active hardfork flags. `read` returns the stored value as -//! big-endian `Bytes`; `write` returns `()`: -//! -//! ```rust,ignore -//! let output = read( -//! &mut precompile_input.internals, -//! address, -//! key, -//! &mut gas_counter, -//! hardfork_flags, -//! )?; -//! let current = U256::from_be_slice(&output); -//! -//! write( -//! &mut precompile_input.internals, -//! address, -//! key, -//! &new_value.to_be_bytes_vec(), -//! &mut gas_counter, -//! hardfork_flags, -//! )?; -//! ``` -//! -//! Gas costs: -//! - `read`: 2,100 gas pre-Zero5; EIP-2929 warm/cold pricing from Zero5+ -//! (`WARM_STORAGE_READ_COST` / `COLD_SLOAD_COST`). -//! - `write`: 2,900 gas pre-Zero5; EIP-2929 / EIP-2200 pricing from Zero5+. - -pub mod helpers; -mod macros; -mod native_coin_authority; -pub mod native_coin_control; -pub mod pq; -pub mod precompile_provider; -pub mod system_accounting; -pub use native_coin_authority::INativeCoinAuthority; -pub use native_coin_authority::NATIVE_COIN_AUTHORITY_ADDRESS; -pub mod subcall; -pub use native_coin_control::INativeCoinControl; -pub use native_coin_control::NATIVE_COIN_CONTROL_ADDRESS; - -pub mod call_from; - -#[cfg(any(test, feature = "test-utils"))] -pub mod pq_test_vectors; diff --git a/crates/precompiles/src/macros.rs b/crates/precompiles/src/macros.rs deleted file mode 100644 index 1d5c29d..0000000 --- a/crates/precompiles/src/macros.rs +++ /dev/null @@ -1,134 +0,0 @@ -// Copyright 2025 Circle Internet Group, Inc. All rights reserved. -// -// SPDX-License-Identifier: Apache-2.0 -// -// Licensed under the Apache License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. -// You may obtain a copy of the License at -// -// http://www.apache.org/licenses/LICENSE-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// See the License for the specific language governing permissions and -// limitations under the License. - -/// Macro for creating stateful precompiles with function-selector dispatch and revert -/// conversion. Gas accounting and ABI decoding are the arm body's responsibility. -/// -/// # Syntax -/// ```rust,ignore -/// precompile!(fn_name, precompile_input, hardfork_flags; { -/// Interface::functionCall => |calldata_bytes| { -/// // Must evaluate to Result. -/// // `calldata_bytes` is the input after the 4-byte selector. -/// }, -/// // Additional functions... -/// }); -/// ``` -/// -/// The generated function takes `(PrecompileInput, ArcHardforkFlags)` and returns -/// `Result`. Arms that return -/// `PrecompileErrorOrRevert::Revert(...)` are converted into an `Ok(PrecompileOutput)` -/// carrying the revert payload; `PrecompileErrorOrRevert::Error(...)` becomes `Err`. -/// If the calldata is shorter than 4 bytes or the selector is unknown, the macro -/// charges `PRECOMPILE_EARLY_REVERT_GAS_PENALTY` and returns a revert. -/// -/// # Example -/// ```rust,ignore -/// precompile!(run_counter_precompile, precompile_input, hardfork_flags; { -/// ICounter::incrementCall => |_input| { -/// (|| -> Result { -/// let mut gas_counter = Gas::new(precompile_input.gas); -/// let mut precompile_input = precompile_input; -/// -/// let output = read( -/// &mut precompile_input.internals, -/// ADDRESS, -/// KEY, -/// &mut gas_counter, -/// hardfork_flags, -/// )?; -/// let new_value = U256::from_be_slice(&output) + U256::from(1); -/// -/// write( -/// &mut precompile_input.internals, -/// ADDRESS, -/// KEY, -/// &new_value.to_be_bytes_vec(), -/// &mut gas_counter, -/// hardfork_flags, -/// )?; -/// -/// Ok(PrecompileOutput::new(gas_counter.used(), new_value.abi_encode().into())) -/// })() -/// }, -/// ICounter::getCountCall => |_input| { -/// (|| -> Result { -/// let mut gas_counter = Gas::new(precompile_input.gas); -/// let mut precompile_input = precompile_input; -/// -/// let output = read( -/// &mut precompile_input.internals, -/// ADDRESS, -/// KEY, -/// &mut gas_counter, -/// hardfork_flags, -/// )?; -/// -/// Ok(PrecompileOutput::new(gas_counter.used(), output)) -/// })() -/// }, -/// }); -/// ``` -/// -/// The macro handles: -/// - Function selector matching -/// - Fallback revert (with gas penalty) for unknown selectors or truncated input -/// - Conversion of `PrecompileErrorOrRevert` into the final `Result` -/// -/// ABI decoding, gas accounting, and output encoding remain the arm body's job; use -/// `helpers::abi_decode_raw_with_zero6_validation` on the supplied calldata bytes when -/// you need the decoded arguments. -#[macro_export] -macro_rules! precompile { - ($fn_name:ident, $precompile_input:ident, $hardfork_flags:ident; { - $( - $fn_call:path => |$arg:ident| $body:expr - ),* $(,)? - }) => { - pub(crate) fn $fn_name( - $precompile_input: reth_evm::precompiles::PrecompileInput, - $hardfork_flags: arc_execution_config::hardforks::ArcHardforkFlags, - ) -> Result { - let input_bytes = $precompile_input.data; - let gas_counter = revm_interpreter::Gas::new($precompile_input.gas); - - if input_bytes.len() < 4 { - return $crate::helpers::PrecompileErrorOrRevert::new_reverted_with_penalty( - gas_counter, PRECOMPILE_EARLY_REVERT_GAS_PENALTY, "Input too short").into(); - } - - let selector: [u8; 4] = input_bytes[0..4].try_into().unwrap(); - - let result: Result = match selector { - $( - sel if sel == <$fn_call>::SELECTOR => { - let $arg = input_bytes.get(4..).unwrap_or_default(); - $body - } - ),* - _ => { - return $crate::helpers::PrecompileErrorOrRevert::new_reverted_with_penalty( - gas_counter, PRECOMPILE_EARLY_REVERT_GAS_PENALTY, "Invalid selector").into(); - }, - }; - - match result { - Ok(output) => Ok(output), - Err(err_or_revert) => err_or_revert.into(), - } - } - }; -} diff --git a/crates/precompiles/src/native_coin_authority.rs b/crates/precompiles/src/native_coin_authority.rs deleted file mode 100644 index d1749d6..0000000 --- a/crates/precompiles/src/native_coin_authority.rs +++ /dev/null @@ -1,2845 +0,0 @@ -// Copyright 2025 Circle Internet Group, Inc. All rights reserved. -// -// SPDX-License-Identifier: Apache-2.0 -// -// Licensed under the Apache License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. -// You may obtain a copy of the License at -// -// http://www.apache.org/licenses/LICENSE-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// See the License for the specific language governing permissions and -// limitations under the License. - -//! Native Coin Authority Precompile -//! -//! This precompile implements native coin operations including mint, burn, -//! transfer, and total supply management. - -use crate::helpers::{ - abi_decode_raw_with_zero6_validation, balance_decr, balance_incr, check_delegatecall, - check_gas_remaining, check_staticcall, emit_event, new_reverted_with_early_penalty, read, - transfer, write, PrecompileErrorOrRevert, ERR_BLOCKED_ADDRESS, ERR_EXECUTION_REVERTED, - LOG_BASE_COST, LOG_DATA_COST, LOG_TOPIC_COST, NATIVE_FIAT_TOKEN_ADDRESS, - PRECOMPILE_EARLY_REVERT_GAS_PENALTY, PRECOMPILE_SLOAD_GAS_COST, PRECOMPILE_SSTORE_GAS_COST, -}; -use crate::native_coin_control::{compute_is_blocklisted_storage_slot, UNBLOCKLISTED_STATUS}; -use crate::precompile; -use crate::NATIVE_COIN_CONTROL_ADDRESS; -use alloy_evm::EvmInternals; -use alloy_primitives::{address, Address, StorageKey, U256}; -use alloy_sol_types::{sol, SolCall, SolValue}; -use arc_execution_config::hardforks::{ArcHardfork, ArcHardforkFlags}; -use reth_ethereum::evm::revm::precompile::PrecompileOutput; -use revm_interpreter::Gas; - -// Native coin authority precompile address -pub const NATIVE_COIN_AUTHORITY_ADDRESS: Address = - address!("0x1800000000000000000000000000000000000000"); - -use revm::handler::SYSTEM_ADDRESS; - -// Allowed caller from NativeFiatToken -const ALLOWED_CALLER_ADDRESS: Address = NATIVE_FIAT_TOKEN_ADDRESS; - -// Storage key for allowed caller (deprecated since Zero5) -const ALLOWED_CALLER_STORAGE_KEY: StorageKey = StorageKey::new([ - 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, -]); - -// Storage key for total supply -const TOTAL_SUPPLY_STORAGE_KEY: StorageKey = StorageKey::new([ - 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 2, -]); - -const MINT_EVENT_GAS_COST: u64 = LOG_BASE_COST + 2 * LOG_TOPIC_COST + 32 * LOG_DATA_COST; // 2 topics + 32 bytes of data -const BURN_EVENT_GAS_COST: u64 = LOG_BASE_COST + 2 * LOG_TOPIC_COST + 32 * LOG_DATA_COST; // 2 topics + 32 bytes of data -const TRANSFER_EVENT_GAS_COST: u64 = LOG_BASE_COST + 3 * LOG_TOPIC_COST + 32 * LOG_DATA_COST; // 3 topics + 32 bytes of data - -// Total gas costs for each operation - -// Mint (pre-Zero5): NativeCoinMinted event (2 topics) -// - Reading allowed caller (2100 gas) -// - Reading blocked to (2100 gas) -// - Reading total supply (2100 gas) -// - Reading account balance (2100 gas) -// - Writing total supply (2900 gas) -// - Writing account balance (2900 gas) -// - Emitting NativeCoinMinted event (1381 gas) -// Total: 15,581 gas -const MINT_GAS_COST: u64 = - 4 * PRECOMPILE_SLOAD_GAS_COST + 2 * PRECOMPILE_SSTORE_GAS_COST + MINT_EVENT_GAS_COST; - -// Mint (Zero5+): upper-bound gas limit for tests. -// Zero5 uses warm/cold SLOAD pricing (100/2100) and removes the auth SLOAD, so -// the actual cost varies per call. This assumes all-cold as a safe upper bound. -// Only used as a test gas limit, not for production gas checks. -#[cfg(test)] -const MINT_GAS_COST_EIP7708: u64 = - 4 * PRECOMPILE_SLOAD_GAS_COST + 2 * PRECOMPILE_SSTORE_GAS_COST + TRANSFER_EVENT_GAS_COST; - -// Burn (pre-Zero5): NativeCoinBurned event (2 topics) -// - Reading allowed caller (2100 gas) -// - Reading blocked from (2100 gas) -// - Reading account balance (2100 gas) -// - Reading total supply (2100 gas) -// - Writing account balance (2900 gas) -// - Writing total supply (2900 gas) -// - Emitting NativeCoinBurned event (1381 gas) -// Total: 15,581 gas -const BURN_GAS_COST: u64 = - 4 * PRECOMPILE_SLOAD_GAS_COST + 2 * PRECOMPILE_SSTORE_GAS_COST + BURN_EVENT_GAS_COST; - -// Burn (Zero5+): upper-bound gas limit for tests. -// Same rationale as MINT_GAS_COST_EIP7708 above. -#[cfg(test)] -const BURN_GAS_COST_EIP7708: u64 = - 4 * PRECOMPILE_SLOAD_GAS_COST + 2 * PRECOMPILE_SSTORE_GAS_COST + TRANSFER_EVENT_GAS_COST; - -// - Reading allowed caller (2100 gas) (removed since Zero5) -// - Reading blocked from (2100 gas) -// - Reading blocked to (2100 gas) -// - Reading account balance x2 (4200 gas) -// - Writing account balance x2 (5800 gas) -// - Emitting event (1756 gas) -// Total: 18056 gas -const TRANSFER_GAS_COST: u64 = - 5 * PRECOMPILE_SLOAD_GAS_COST + 2 * PRECOMPILE_SSTORE_GAS_COST + TRANSFER_EVENT_GAS_COST; -// The gas cost for a transfer operation when the amount is zero -// - Reading allowed caller (2100 gas) (removed since Zero5) -// - Reading blocked from (2100 gas) -// - Reading blocked to (2100 gas) -const TRANSFER_GAS_COST_WITH_ZERO_AMOUNT: u64 = 3 * PRECOMPILE_SLOAD_GAS_COST; - -const TOTAL_SUPPLY_GAS_COST: u64 = PRECOMPILE_SLOAD_GAS_COST; - -// Error messages -const ERR_CANNOT_MINT: &str = "Not enabled native coin minter"; -const ERR_CANNOT_BURN: &str = "Not enabled native coin burner"; -const ERR_CANNOT_TRANSFER: &str = "Not enabled for native coin transfers"; -const ERR_OVERFLOW: &str = "Arithmetic overflow"; -const ERR_ZERO_AMOUNT: &str = "Zero amount invalid"; -use crate::helpers::ERR_ZERO_ADDRESS; - -sol! { - /// Native Coin Authority precompile interface - interface INativeCoinAuthority { - /// Mint new coins to the specified address - function mint(address to, uint256 amount) external returns (bool); - - /// Burn coins from the specified address - function burn(address from, uint256 amount) external returns (bool); - - /// Transfer coins between addresses - function transfer(address from, address to, uint256 amount) external returns (bool); - - /// Get the total supply of native coins - function totalSupply() external view returns (uint256 supply); - } - - /// Events - #[derive(Debug)] - event NativeCoinMinted(address indexed recipient, uint256 amount); - - #[derive(Debug)] - event NativeCoinBurned(address indexed from, uint256 amount); - - #[derive(Debug)] - event NativeCoinTransferred(address indexed from, address indexed to, uint256 amount); - - /// ERC-20 Transfer event (EIP-7708), used under Zero5 for native coin transfers - #[derive(Debug)] - event Transfer(address indexed from, address indexed to, uint256 value); -} - -/// Checks if the caller is authorized to call mutative native coin authority functions -fn is_authorized( - internals: &mut EvmInternals, - caller: Address, - gas_counter: &mut Gas, - hardfork_flags: ArcHardforkFlags, -) -> Result { - // Get allowed caller - let allowed_caller_output = read( - internals, - NATIVE_COIN_AUTHORITY_ADDRESS, - ALLOWED_CALLER_STORAGE_KEY, - gas_counter, - hardfork_flags, - )?; - - // Compare caller to allowed_caller_output - let caller_word = U256::from_be_slice(caller.as_ref()); - let allowed_caller_word = U256::from_be_slice(&allowed_caller_output); - Ok(caller_word == allowed_caller_word) -} - -fn is_blocklisted( - internals: &mut EvmInternals, - address: Address, - gas_counter: &mut Gas, - hardfork_flags: ArcHardforkFlags, -) -> Result { - // Get address storage slot for blocklist - let storage_slot = compute_is_blocklisted_storage_slot(address); - let storage_output = read( - internals, - NATIVE_COIN_CONTROL_ADDRESS, - storage_slot, - gas_counter, - hardfork_flags, - )?; - - Ok(!U256::from_be_slice(&storage_output).eq(&UNBLOCKLISTED_STATUS)) -} - -precompile!(run_native_coin_authority, precompile_input, hardfork_flags; { - INativeCoinAuthority::mintCall => |input| { - (|| -> Result { - let mut gas_counter = Gas::new(precompile_input.gas); - let mut precompile_input = precompile_input; - // Check if static call is attempting to modify state - check_staticcall( - &precompile_input, - &mut gas_counter, - )?; - - // Decode arguments passed to mint function - let args = abi_decode_raw_with_zero6_validation::( - input, - hardfork_flags, - ) - .map_err(|_| - PrecompileErrorOrRevert::new_reverted_with_penalty( - gas_counter, PRECOMPILE_EARLY_REVERT_GAS_PENALTY, ERR_EXECUTION_REVERTED) - )?; - - if hardfork_flags.is_active(ArcHardfork::Zero5) { - // Zero5+: Skip early gas check - warm/cold pricing makes upfront calculation unreliable - // Check authorization - if precompile_input.caller != ALLOWED_CALLER_ADDRESS { - return Err(new_reverted_with_early_penalty( - gas_counter, - ERR_CANNOT_MINT, - hardfork_flags, - )); - } - } else { - // Early return if not enough gas - check_gas_remaining(&gas_counter, MINT_GAS_COST)?; - - // Check authorization - if !is_authorized( - &mut precompile_input.internals, - precompile_input.caller, - &mut gas_counter, - hardfork_flags, - )? { - return Err(new_reverted_with_early_penalty(gas_counter, ERR_CANNOT_MINT, hardfork_flags)); - } - } - - // Prevent delegate calls - check_delegatecall( - NATIVE_COIN_AUTHORITY_ADDRESS, - &precompile_input, - &gas_counter, - )?; - - // Reject minting to zero address (Zero5+) - if hardfork_flags.is_active(ArcHardfork::Zero5) && args.to == Address::ZERO { - return Err(new_reverted_with_early_penalty(gas_counter, ERR_ZERO_ADDRESS, hardfork_flags)); - } - - // Check blocklist - if is_blocklisted(&mut precompile_input.internals, args.to, &mut gas_counter, hardfork_flags)? { - return Err(new_reverted_with_early_penalty(gas_counter, ERR_BLOCKED_ADDRESS, hardfork_flags)); - } - - // Validate amount - if args.amount == U256::ZERO { - return Err(new_reverted_with_early_penalty(gas_counter, ERR_ZERO_AMOUNT, hardfork_flags)); - } - - // Read current total supply - let total_supply_output = read( - &mut precompile_input.internals, - NATIVE_COIN_AUTHORITY_ADDRESS, - TOTAL_SUPPLY_STORAGE_KEY, - &mut gas_counter, - hardfork_flags, - )?; - let current_total_supply = U256::from_be_slice(&total_supply_output); - - // Check for overflow - let new_total_supply = match current_total_supply.checked_add(args.amount) { - Some(new_total_supply) => new_total_supply, - None => return Err(new_reverted_with_early_penalty(gas_counter, ERR_OVERFLOW, hardfork_flags)), - }; - - // Write new total supply - write( - &mut precompile_input.internals, - NATIVE_COIN_AUTHORITY_ADDRESS, - TOTAL_SUPPLY_STORAGE_KEY, - &new_total_supply.to_be_bytes_vec(), - &mut gas_counter, - hardfork_flags, - )?; - - // Update account balance - balance_incr(&mut precompile_input.internals, args.to, args.amount, &mut gas_counter, hardfork_flags)?; - - // Emit event: ERC-20 Transfer(0x0, to) under Zero5, NativeCoinMinted otherwise. - // Address::ZERO as `from` follows the ERC-20 convention for minting. This is - // intentionally allowed here even though CALL/CREATE value transfers reject - // Address::ZERO (see check_blocklist_and_create_log in evm.rs). - if hardfork_flags.is_active(ArcHardfork::Zero5) { - emit_event(&mut precompile_input.internals, SYSTEM_ADDRESS, &Transfer { - from: Address::ZERO, - to: args.to, - value: args.amount, - }, &mut gas_counter)?; - } else { - emit_event(&mut precompile_input.internals, NATIVE_COIN_AUTHORITY_ADDRESS, &NativeCoinMinted { - recipient: args.to, - amount: args.amount, - }, &mut gas_counter)?; - } - - let output = true.abi_encode(); - Ok(PrecompileOutput::new(gas_counter.used(), output.into())) - })() - }, - - INativeCoinAuthority::burnCall => |input| { - (|| -> Result { - let mut gas_counter = Gas::new(precompile_input.gas); - let mut precompile_input = precompile_input; - // Check if static call is attempting to modify state - check_staticcall( - &precompile_input, - &mut gas_counter, - )?; - - // Decode arguments passed to burn function - let args = abi_decode_raw_with_zero6_validation::( - input, - hardfork_flags, - ) - .map_err(|_| - PrecompileErrorOrRevert::new_reverted_with_penalty( - gas_counter, PRECOMPILE_EARLY_REVERT_GAS_PENALTY, ERR_EXECUTION_REVERTED, - ) - )?; - - // Check authorization - if hardfork_flags.is_active(ArcHardfork::Zero5) { - // Zero5+: Skip early gas check - warm/cold pricing makes upfront calculation unreliable - if precompile_input.caller != ALLOWED_CALLER_ADDRESS { - return Err(new_reverted_with_early_penalty( - gas_counter, - ERR_CANNOT_BURN, - hardfork_flags, - )); - } - } else { - // Early return if not enough gas - check_gas_remaining(&gas_counter, BURN_GAS_COST)?; - - if !(is_authorized(&mut precompile_input.internals, precompile_input.caller, &mut gas_counter, hardfork_flags)?) { - return Err(new_reverted_with_early_penalty(gas_counter, ERR_CANNOT_BURN, hardfork_flags)); - } - } - - // Prevent delegate calls - check_delegatecall( - NATIVE_COIN_AUTHORITY_ADDRESS, - &precompile_input, - &gas_counter, - )?; - - // Reject burning from zero address (Zero5+) - if hardfork_flags.is_active(ArcHardfork::Zero5) && args.from == Address::ZERO { - return Err(new_reverted_with_early_penalty(gas_counter, ERR_ZERO_ADDRESS, hardfork_flags)); - } - - // Check blocklist - if is_blocklisted(&mut precompile_input.internals, args.from, &mut gas_counter, hardfork_flags)? { - return Err(new_reverted_with_early_penalty(gas_counter, ERR_BLOCKED_ADDRESS, hardfork_flags)); - } - - // Validate amount - if args.amount == U256::ZERO { - return Err(new_reverted_with_early_penalty(gas_counter, ERR_ZERO_AMOUNT, hardfork_flags)); - } - - // Check balance and burn tokens - balance_decr(&mut precompile_input.internals, args.from, args.amount, &mut gas_counter, hardfork_flags)?; - - // Adjust total supply - let total_supply_output = read( - &mut precompile_input.internals, - NATIVE_COIN_AUTHORITY_ADDRESS, - TOTAL_SUPPLY_STORAGE_KEY, - &mut gas_counter, - hardfork_flags, - )?; - let current_total_supply = U256::from_be_slice(&total_supply_output); - - // Write new total supply - // Underflow cannot happen due to the balance check - write( - &mut precompile_input.internals, - NATIVE_COIN_AUTHORITY_ADDRESS, - TOTAL_SUPPLY_STORAGE_KEY, - ¤t_total_supply.saturating_sub(args.amount).to_be_bytes_vec(), - &mut gas_counter, - hardfork_flags, - )?; - - // Emit event: ERC-20 Transfer(from, 0x0) under Zero5, NativeCoinBurned otherwise. - // Address::ZERO as `to` follows the ERC-20 convention for burning. This is - // intentionally allowed here even though CALL/CREATE value transfers reject - // Address::ZERO (see check_blocklist_and_create_log in evm.rs). - if hardfork_flags.is_active(ArcHardfork::Zero5) { - emit_event(&mut precompile_input.internals, SYSTEM_ADDRESS, &Transfer { - from: args.from, - to: Address::ZERO, - value: args.amount, - }, &mut gas_counter)?; - } else { - emit_event(&mut precompile_input.internals, NATIVE_COIN_AUTHORITY_ADDRESS, &NativeCoinBurned { - from: args.from, - amount: args.amount, - }, &mut gas_counter)?; - } - - // Return response - let output = true.abi_encode(); - Ok(PrecompileOutput::new(gas_counter.used(), output.into())) - })() - }, - - INativeCoinAuthority::transferCall => |input| { - (|| -> Result { - let mut gas_counter = Gas::new(precompile_input.gas); - let mut precompile_input = precompile_input; - // Check if static call is attempting to modify state - check_staticcall( - &precompile_input, - &mut gas_counter, - )?; - - // Decode arguments passed to transfer function - let args = abi_decode_raw_with_zero6_validation::( - input, - hardfork_flags, - ) - .map_err(|_| - PrecompileErrorOrRevert::new_reverted_with_penalty( - gas_counter, PRECOMPILE_EARLY_REVERT_GAS_PENALTY, ERR_EXECUTION_REVERTED, - ) - )?; - - // Check authorization - if hardfork_flags.is_active(ArcHardfork::Zero5) { - // Zero5+: Skip early gas check - warm/cold pricing makes upfront calculation unreliable - if precompile_input.caller != ALLOWED_CALLER_ADDRESS { - return Err(new_reverted_with_early_penalty( - gas_counter, - ERR_CANNOT_TRANSFER, - hardfork_flags, - )); - } - } else { - // The gas cost is different if the amount is zero or not - let expect_gas_cost = if args.amount != U256::ZERO { - TRANSFER_GAS_COST - } else { - TRANSFER_GAS_COST_WITH_ZERO_AMOUNT - }; - - // Early return if not enough gas - check_gas_remaining(&gas_counter, expect_gas_cost)?; - - if !(is_authorized(&mut precompile_input.internals, precompile_input.caller, &mut gas_counter, hardfork_flags)?) { - return Err(new_reverted_with_early_penalty(gas_counter, ERR_CANNOT_TRANSFER, hardfork_flags)); - } - } - - // Prevent delegate calls - check_delegatecall( - NATIVE_COIN_AUTHORITY_ADDRESS, - &precompile_input, - &gas_counter, - )?; - - // Reject transfers involving zero address (Zero5+) - if hardfork_flags.is_active(ArcHardfork::Zero5) - && (args.from == Address::ZERO || args.to == Address::ZERO) - { - return Err(new_reverted_with_early_penalty(gas_counter, ERR_ZERO_ADDRESS, hardfork_flags)); - } - - // Check blocklist - if is_blocklisted(&mut precompile_input.internals, args.from, &mut gas_counter, hardfork_flags)? { - return Err(new_reverted_with_early_penalty(gas_counter, ERR_BLOCKED_ADDRESS, hardfork_flags)); - } - if is_blocklisted(&mut precompile_input.internals, args.to, &mut gas_counter, hardfork_flags)? { - return Err(new_reverted_with_early_penalty(gas_counter, ERR_BLOCKED_ADDRESS, hardfork_flags)); - } - - // Zero amount transfers are allowed, but do not emit an event - if args.amount != U256::ZERO { - // Note on self-transfers (from == to): REVM's transfer_loaded() early-returns - // without touching balances for self-transfers. Here we still call transfer() - // which performs balance_decr + balance_incr (net zero change). This is - // functionally correct and intentionally kept to preserve identical gas costs - // across the Zero5 hardfork boundary — skipping the balance ops would reduce - // gas consumption and break the "gas cost unchanged" invariant. - transfer(&mut precompile_input.internals, args.from, args.to, args.amount, &mut gas_counter, hardfork_flags)?; - - // Emit event: ERC-20 Transfer under Zero5, NativeCoinTransferred otherwise. - // EIP-7708: self-transfers (from == to) do not emit a log. - if hardfork_flags.is_active(ArcHardfork::Zero5) { - if args.from != args.to { - emit_event(&mut precompile_input.internals, SYSTEM_ADDRESS, &Transfer { - from: args.from, - to: args.to, - value: args.amount, - }, &mut gas_counter)?; - } - } else { - emit_event(&mut precompile_input.internals, NATIVE_COIN_AUTHORITY_ADDRESS, &NativeCoinTransferred { - from: args.from, - to: args.to, - amount: args.amount, - }, &mut gas_counter)?; - } - } - - // Return response - let output = true.abi_encode(); - Ok(PrecompileOutput::new(gas_counter.used(), output.into())) - })() - }, - INativeCoinAuthority::totalSupplyCall => |input| { - (|| -> Result { - let mut gas_counter = Gas::new(precompile_input.gas); - let mut precompile_input = precompile_input; - - if !hardfork_flags.is_active(ArcHardfork::Zero6) && !input.is_empty() { - return Err(PrecompileErrorOrRevert::new_reverted_with_penalty( - gas_counter, PRECOMPILE_EARLY_REVERT_GAS_PENALTY, ERR_EXECUTION_REVERTED)); - } - - // Early return if not enough gas - check_gas_remaining(&gas_counter, TOTAL_SUPPLY_GAS_COST)?; - - // Read the total supply - let total_supply_output = read( - &mut precompile_input.internals, - NATIVE_COIN_AUTHORITY_ADDRESS, - TOTAL_SUPPLY_STORAGE_KEY, - &mut gas_counter, - hardfork_flags, - )?; - let total_supply = U256::from_be_slice(&total_supply_output); - - // Return response - let output = total_supply.abi_encode(); - Ok(PrecompileOutput::new(gas_counter.used(), output.into())) - })() - }, -}); - -#[cfg(test)] -mod tests { - use super::*; - use crate::helpers::{ - ERR_CLEAR_EMPTY, ERR_DELEGATE_CALL_NOT_ALLOWED, ERR_INSUFFICIENT_FUNDS, - ERR_SELFDESTRUCTED_BALANCE_INCREASED, REVERT_SELECTOR, - }; - use crate::native_coin_control::{ - compute_is_blocklisted_storage_slot, run_native_coin_control, BLOCKLISTED_STATUS, - NATIVE_COIN_CONTROL_ADDRESS, UNBLOCKLISTED_STATUS, - }; - use alloy_primitives::Bytes; - use alloy_sol_types::SolEvent; - use arc_execution_config::hardforks::{ArcHardfork, ArcHardforkFlags}; - use reth_ethereum::evm::revm::{ - context::{Context, ContextTr, JournalTr}, - interpreter::{CallInput, CallInputs, CallScheme, CallValue, InstructionResult}, - MainContext, - }; - use reth_evm::precompiles::{DynPrecompile, PrecompilesMap}; - use revm::{ - bytecode::Bytecode, - handler::PrecompileProvider, - interpreter::InterpreterResult, - precompile::{PrecompileId, Precompiles}, - }; - use revm_context_interface::journaled_state::account::JournaledAccountTr; - use std::collections::HashSet; - - fn mock_context(hardfork_flags: ArcHardforkFlags) -> revm::Context { - let mut ctx = Context::mainnet(); - - // Set up native coin authority - ctx.journal_mut() - .load_account(NATIVE_COIN_AUTHORITY_ADDRESS) - .expect("Unable to load native coin authority account"); - - if !hardfork_flags.is_active(ArcHardfork::Zero5) { - ctx.journal_mut() - .sstore( - NATIVE_COIN_AUTHORITY_ADDRESS, - ALLOWED_CALLER_STORAGE_KEY.into(), - U256::from_be_slice(ALLOWED_CALLER_ADDRESS.as_ref()), - ) - .expect("Unable to write allowed caller"); - } - - // Preload native coin control account, it will load storage slot in tests. - ctx.journal_mut() - .load_account(NATIVE_COIN_CONTROL_ADDRESS) - .expect("Unable to load native coin authority account"); - - ctx - } - - fn call_native_coin_authority( - ctx: &mut Context, - inputs: &CallInputs, - hardfork_flags: ArcHardforkFlags, - ) -> Result, String> { - // The EvmInternals has no public constructor, so we can not test DynPrecompile directly. - let mut provider = PrecompilesMap::from_static(Precompiles::latest()); - let target_addr: Address = inputs.target_address; - provider.set_precompile_lookup(move |address: &Address| { - if *address == NATIVE_COIN_AUTHORITY_ADDRESS - || target_addr == NATIVE_COIN_AUTHORITY_ADDRESS - { - Some(DynPrecompile::new_stateful( - PrecompileId::Custom("NATIVE_COIN_AUTHORITY".into()), - move |input| run_native_coin_authority(input, hardfork_flags), - )) - } else if *address == NATIVE_COIN_CONTROL_ADDRESS { - Some(DynPrecompile::new_stateful( - PrecompileId::Custom("NATIVE_COIN_CONTROL".into()), - move |input| run_native_coin_control(input, hardfork_flags), - )) - } else { - None - } - }); - provider.run(ctx, inputs) - } - struct NativeCoinAuthorityTest { - name: &'static str, - caller: Address, - calldata: Bytes, - gas_limit: u64, - /// If set, overrides gas_limit for pre-Zero5 hardforks - pre_zero5_gas_limit: Option, - /// If set, overrides gas_limit when EIP-7708 (Zero5) is active (different event costs) - eip7708_gas_limit: Option, - /// If set, overrides gas_limit when Zero5 and Zero6 are both active. - /// Needed when Zero6's warm-account discount shifts the OOG boundary. - zero6_gas_limit: Option, - expected_revert_str: Option<&'static str>, - expected_result: InstructionResult, - return_data: Option, - blocklisted_addresses: Option>, - gas_used: u64, - /// If set, overrides gas_used for pre-Zero5 hardforks (before warm/cold aware storage costs) - pre_zero5_gas_used: Option, - /// If set, overrides gas_used when EIP-7708 (Zero5) is active. - eip7708_gas_used: Option, - /// If set, overrides gas_used when Zero5 and Zero6 are both active. - zero6_gas_used: Option, - target_address: Address, - bytecode_address: Address, - /// If true, skip this test case for hardfork combinations without Zero5 (EIP-7708). - /// Defaults to false when not specified (via `..Default::default()`). - eip7708_only: bool, - /// If true, skip this test case for hardfork combinations with Zero5 (EIP-7708). - /// Used for OOG tests whose gas boundary changes when EIP-7708 suppresses event emission. - pre_eip7708_only: bool, - } - - impl Default for NativeCoinAuthorityTest { - fn default() -> Self { - Self { - name: "", - caller: Address::ZERO, - calldata: Bytes::new(), - gas_limit: 0, - pre_zero5_gas_limit: None, - eip7708_gas_limit: None, - zero6_gas_limit: None, - expected_revert_str: None, - expected_result: InstructionResult::Stop, - return_data: None, - blocklisted_addresses: None, - gas_used: 0, - pre_zero5_gas_used: None, - eip7708_gas_used: None, - zero6_gas_used: None, - target_address: Address::ZERO, - bytecode_address: Address::ZERO, - eip7708_only: false, - pre_eip7708_only: false, - } - } - } - - // Test constants - const ADDRESS_A: Address = address!("1000000000000000000000000000000000000001"); - const ADDRESS_B: Address = address!("2000000000000000000000000000000000000002"); - const ADDRESS_C: Address = address!("300000D000000000000000000000000000000003"); - const NON_EMPTY_ADDRESS: Address = address!("400000D000000000000000000000000000000004"); - const ZERO6_EMPTY_ACCOUNT_GAS_DELTA: u64 = crate::helpers::PRECOMPILE_EMPTY_ACCOUNT_GAS_COST; - - fn assert_precompile_result( - precompile_res: Result, String>, - tc: &NativeCoinAuthorityTest, - hardfork_flags: ArcHardforkFlags, - tc_name: &str, - ) { - match precompile_res { - Ok(result) => { - assert!(result.is_some(), "{}: expected result to be some", tc.name); - let result = result.unwrap(); - - assert_eq!( - result.result, tc.expected_result, - "{tc_name}: expected result to match", - ); - - if let Some(expected_revert_str) = tc.expected_revert_str { - assert!( - result.is_revert(), - "{tc_name}: expected output to be reverted" - ); - let revert_reason = bytes_to_revert_message(result.output.as_ref()); - assert!(revert_reason.is_some(), "{tc_name}: expected revert reason"); - assert_eq!( - revert_reason.unwrap(), - expected_revert_str, - "{tc_name}: expected revert reason to match", - ); - } else { - assert!( - !result.is_revert(), - "{tc_name}: expected output not to be reverted" - ); - } - - if let Some(expected_return_data) = &tc.return_data { - assert_eq!( - result.output, *expected_return_data, - "{tc_name}: expected return data to match", - ); - } - - // Resolve expected gas per hardfork combination. Zero5 changes auth / event - // shape (EIP-7708). Zero6 changes account-load pricing inside the balance - // helpers. Zero6 is cumulative (implies Zero5), so the {!Zero5, Zero6} cell - // is filtered out by the test loop. - let expected_gas_used = if hardfork_flags.is_active(ArcHardfork::Zero6) { - tc.zero6_gas_used - .or(tc.eip7708_gas_used) - .unwrap_or(tc.gas_used) - } else if hardfork_flags.is_active(ArcHardfork::Zero5) { - tc.eip7708_gas_used.unwrap_or(tc.gas_used) - } else { - tc.pre_zero5_gas_used.unwrap_or(tc.gas_used) - }; - assert_eq!( - result.gas.used(), - expected_gas_used, - "{tc_name}: gas used to match" - ); - } - Err(e) => { - panic!("{tc_name}: unexpected error {:?}", e) - } - } - } - - /// Sets up blocklist entries in the test context - fn setup_blocklist(ctx: &mut Context, blocklisted_addresses: &Option>) { - if let Some(addresses) = blocklisted_addresses { - for &address in addresses { - let storage_slot = compute_is_blocklisted_storage_slot(address); - ctx.journal_mut() - .sstore( - NATIVE_COIN_CONTROL_ADDRESS, - storage_slot.into(), - BLOCKLISTED_STATUS, - ) - .expect("Unable to set blocklist status"); - } - } - } - - /// Cleans up blocklist entries after test - fn cleanup_blocklist(ctx: &mut Context, blocklisted_addresses: &Option>) { - if let Some(addresses) = blocklisted_addresses { - for &address in addresses { - let storage_slot = compute_is_blocklisted_storage_slot(address); - ctx.journal_mut() - .sstore( - NATIVE_COIN_CONTROL_ADDRESS, - storage_slot.into(), - UNBLOCKLISTED_STATUS, - ) - .expect("Unable to clear blocklist status"); - } - } - } - - /// Sets up initial state for test context (total supply, balances, code) - fn setup_initial_state(ctx: &mut Context, mock_initial_supply: U256) { - // Configure initial total supply - ctx.journal_mut() - .sstore( - NATIVE_COIN_AUTHORITY_ADDRESS, - TOTAL_SUPPLY_STORAGE_KEY.into(), - mock_initial_supply, - ) - .expect("Unable to write initial total supply"); - - // Configure initial balance for ADDRESS_A - ctx.journal_mut() - .load_account(ADDRESS_A) - .expect("Cannot load account"); - ctx.journal_mut() - .balance_incr(ADDRESS_A, mock_initial_supply) - .expect("Unable to write initial balance for ADDRESS_A"); - - // Configure a non-empty state for NON_EMPTY_ADDRESS - ctx.journal_mut() - .load_account(NON_EMPTY_ADDRESS) - .expect("Cannot load account"); - ctx.journal_mut().set_code( - NON_EMPTY_ADDRESS, - Bytecode::new_legacy(Bytes::from_static(&[0x60, 0x00, 0x60, 0x00, 0x56])), - ); - ctx.journal_mut() - .balance_incr(NON_EMPTY_ADDRESS, mock_initial_supply) - .expect("Unable to write initial balance for NON_EMPTY_ADDRESS"); - } - - /// Returns the appropriate gas limit based on hardfork - fn get_gas_limit(tc: &NativeCoinAuthorityTest, hardfork_flags: ArcHardforkFlags) -> u64 { - if hardfork_flags.is_active(ArcHardfork::Zero6) { - tc.zero6_gas_limit - .or(tc.eip7708_gas_limit) - .unwrap_or(tc.gas_limit) - } else if hardfork_flags.is_active(ArcHardfork::Zero5) { - tc.eip7708_gas_limit.unwrap_or(tc.gas_limit) - } else { - tc.pre_zero5_gas_limit.unwrap_or(tc.gas_limit) - } - } - - /// Validates test case configuration - fn validate_test_case(tc: &NativeCoinAuthorityTest) { - match tc.expected_result { - InstructionResult::Revert | InstructionResult::Return => {} - _ => { - assert!( - tc.return_data.is_none(), - "{}: expected no return data", - tc.name - ); - } - } - } - - #[test] - // These tests test the outputs of the native coin authority precompile, such as - // the InstructionResult, error conditions, and revert messages. - - // Tests for the state side effects (balance mutations and events) are tested separately - fn native_coin_authority_precompile_outputs() { - // Put the initial supply in a constant - let mock_initial_supply = U256::from(1_000_000_000); - - let cases: &[NativeCoinAuthorityTest] = &[ - // Authorization check is now a constant comparison (no SLOAD), reverts with 0 gas - NativeCoinAuthorityTest { - name: "mint() unauthorized caller reverts", - caller: ADDRESS_A, - calldata: INativeCoinAuthority::mintCall { - to: ADDRESS_B, - amount: U256::from(1000), - } - .abi_encode() - .into(), - gas_limit: 100_000, - pre_zero5_gas_limit: None, - expected_revert_str: Some(ERR_CANNOT_MINT), - expected_result: InstructionResult::Revert, - return_data: None, - blocklisted_addresses: None, - gas_used: 0, - pre_zero5_gas_used: Some(PRECOMPILE_SLOAD_GAS_COST), - zero6_gas_used: Some(PRECOMPILE_EARLY_REVERT_GAS_PENALTY), - target_address: NATIVE_COIN_AUTHORITY_ADDRESS, - bytecode_address: NATIVE_COIN_AUTHORITY_ADDRESS, - ..Default::default() - }, - // No auth SLOAD, blocklist SLOAD is cold (2100), reverts before other ops - NativeCoinAuthorityTest { - name: "mint() zero amount reverts", - caller: ALLOWED_CALLER_ADDRESS, - calldata: INativeCoinAuthority::mintCall { - to: ADDRESS_B, - amount: U256::ZERO, - } - .abi_encode() - .into(), - gas_limit: 100_000, - pre_zero5_gas_limit: None, - expected_revert_str: Some(ERR_ZERO_AMOUNT), - expected_result: InstructionResult::Revert, - return_data: None, - blocklisted_addresses: None, - gas_used: 2100, // blocklist check cold SLOAD only - pre_zero5_gas_used: Some(PRECOMPILE_SLOAD_GAS_COST * 2), - zero6_gas_used: Some(2100 + PRECOMPILE_EARLY_REVERT_GAS_PENALTY), - target_address: NATIVE_COIN_AUTHORITY_ADDRESS, - bytecode_address: NATIVE_COIN_AUTHORITY_ADDRESS, - ..Default::default() - }, - // No auth SLOAD, blocklist cold (2100), total supply warm (100) - warm because test setup writes it - NativeCoinAuthorityTest { - name: "mint() reverts on overflow", - caller: ALLOWED_CALLER_ADDRESS, - calldata: INativeCoinAuthority::mintCall { - to: ADDRESS_B, - amount: U256::MAX - mock_initial_supply + U256::from(1), - } - .abi_encode() - .into(), - gas_limit: 100_000, - pre_zero5_gas_limit: None, - expected_revert_str: Some(ERR_OVERFLOW), - expected_result: InstructionResult::Revert, - return_data: None, - blocklisted_addresses: None, - gas_used: 2200, // blocklist cold (2100) + total_supply warm (100) - pre_zero5_gas_used: Some(PRECOMPILE_SLOAD_GAS_COST * 3), - zero6_gas_used: Some(2200 + PRECOMPILE_EARLY_REVERT_GAS_PENALTY), - target_address: NATIVE_COIN_AUTHORITY_ADDRESS, - bytecode_address: NATIVE_COIN_AUTHORITY_ADDRESS, - ..Default::default() - }, - NativeCoinAuthorityTest { - name: "mint() insufficient gas errors with OOG", - caller: ALLOWED_CALLER_ADDRESS, - calldata: INativeCoinAuthority::mintCall { - to: ADDRESS_B, - amount: U256::from(1), - } - .abi_encode() - .into(), - // Pre-Zero5: uses early gas check with MINT_GAS_COST - PRECOMPILE_SLOAD_GAS_COST - 1 - // Zero5 (EIP-7708): uses Transfer event (9056 gas needed), so 8680 still triggers OOG - gas_limit: 8680, - pre_zero5_gas_limit: Some(MINT_GAS_COST - PRECOMPILE_SLOAD_GAS_COST - 1), - expected_revert_str: None, - expected_result: InstructionResult::PrecompileOOG, - return_data: None, - blocklisted_addresses: None, - gas_used: 0, // for OOG, it not the responsibility for the precompile layer - pre_zero5_gas_used: None, - target_address: NATIVE_COIN_AUTHORITY_ADDRESS, - bytecode_address: NATIVE_COIN_AUTHORITY_ADDRESS, - pre_eip7708_only: true, - ..Default::default() - }, - NativeCoinAuthorityTest { - name: "mint() invalid params errors with Execution Reverted", - caller: ALLOWED_CALLER_ADDRESS, - calldata: INativeCoinAuthority::mintCall::SELECTOR.into(), - gas_limit: MINT_GAS_COST, - pre_zero5_gas_limit: None, - expected_revert_str: Some(ERR_EXECUTION_REVERTED), - expected_result: InstructionResult::Revert, - return_data: None, - blocklisted_addresses: None, - gas_used: PRECOMPILE_EARLY_REVERT_GAS_PENALTY, - pre_zero5_gas_used: None, - target_address: NATIVE_COIN_AUTHORITY_ADDRESS, - bytecode_address: NATIVE_COIN_AUTHORITY_ADDRESS, - ..Default::default() - }, - NativeCoinAuthorityTest { - name: "mint() prevents calls if target != precompile address", - caller: ALLOWED_CALLER_ADDRESS, - calldata: INativeCoinAuthority::mintCall::SELECTOR.into(), - gas_limit: MINT_GAS_COST, - pre_zero5_gas_limit: None, - expected_revert_str: Some(ERR_EXECUTION_REVERTED), - expected_result: InstructionResult::Revert, - return_data: None, - blocklisted_addresses: None, - gas_used: PRECOMPILE_EARLY_REVERT_GAS_PENALTY, - pre_zero5_gas_used: None, - target_address: ADDRESS_B, - bytecode_address: NATIVE_COIN_AUTHORITY_ADDRESS, - ..Default::default() - }, - // No auth SLOAD, delegate check happens before any storage ops - NativeCoinAuthorityTest { - name: "mint() prevents calls if bytecode_address != precompile address", - caller: ALLOWED_CALLER_ADDRESS, - calldata: INativeCoinAuthority::mintCall { - to: ADDRESS_B, - amount: U256::from(1), - } - .abi_encode() - .into(), - gas_limit: MINT_GAS_COST, - pre_zero5_gas_limit: None, - expected_revert_str: Some(ERR_DELEGATE_CALL_NOT_ALLOWED), - expected_result: InstructionResult::Revert, - return_data: None, - blocklisted_addresses: None, - gas_used: 0, // No auth SLOAD in Zero5 - pre_zero5_gas_used: Some(PRECOMPILE_SLOAD_GAS_COST), // as it comes after the authorization check SLOAD - target_address: NATIVE_COIN_AUTHORITY_ADDRESS, - bytecode_address: ADDRESS_B, // different bytecode address - ..Default::default() - }, - // No auth SLOAD, delegate check happens before any storage ops - NativeCoinAuthorityTest { - name: "mint() prevents calls if target_address != precompile address", - caller: ALLOWED_CALLER_ADDRESS, - calldata: INativeCoinAuthority::mintCall { - to: ADDRESS_B, - amount: U256::from(1), - } - .abi_encode() - .into(), - gas_limit: MINT_GAS_COST, - pre_zero5_gas_limit: None, - expected_revert_str: Some(ERR_DELEGATE_CALL_NOT_ALLOWED), - expected_result: InstructionResult::Revert, - return_data: None, - blocklisted_addresses: None, - gas_used: 0, // No auth SLOAD in Zero5 - pre_zero5_gas_used: Some(PRECOMPILE_SLOAD_GAS_COST), // as it comes after the authorization check SLOAD - target_address: ADDRESS_B, // different target address - bytecode_address: NATIVE_COIN_AUTHORITY_ADDRESS, - ..Default::default() - }, - // Zero5: blocklist cold (2100) + total_supply warm read/write (200) - // + balance_incr fixed (5000) + Transfer event (1756) = 9056. - NativeCoinAuthorityTest { - name: "mint() success and returns true", - caller: ALLOWED_CALLER_ADDRESS, - calldata: INativeCoinAuthority::mintCall { - to: ADDRESS_B, - amount: U256::from(1), - } - .abi_encode() - .into(), - gas_limit: MINT_GAS_COST, - pre_zero5_gas_limit: None, - eip7708_gas_limit: Some(MINT_GAS_COST_EIP7708), - zero6_gas_limit: Some(MINT_GAS_COST_EIP7708 + ZERO6_EMPTY_ACCOUNT_GAS_DELTA), - expected_revert_str: None, - expected_result: InstructionResult::Return, - return_data: Some(true.abi_encode().into()), - blocklisted_addresses: None, - gas_used: 8681, - pre_zero5_gas_used: Some(MINT_GAS_COST), - eip7708_gas_used: Some(9056), - zero6_gas_used: Some(9556 + ZERO6_EMPTY_ACCOUNT_GAS_DELTA), - target_address: NATIVE_COIN_AUTHORITY_ADDRESS, - bytecode_address: NATIVE_COIN_AUTHORITY_ADDRESS, - ..Default::default() - }, - // Zero6: NON_EMPTY_ADDRESS is initialized in test setup, so balance_incr() - // must not charge the empty-account creation surcharge. - NativeCoinAuthorityTest { - name: "mint() to non-empty account succeeds without empty account surcharge", - caller: ALLOWED_CALLER_ADDRESS, - calldata: INativeCoinAuthority::mintCall { - to: NON_EMPTY_ADDRESS, - amount: U256::from(1), - } - .abi_encode() - .into(), - gas_limit: MINT_GAS_COST, - pre_zero5_gas_limit: None, - eip7708_gas_limit: Some(MINT_GAS_COST_EIP7708), - expected_revert_str: None, - expected_result: InstructionResult::Return, - return_data: Some(true.abi_encode().into()), - blocklisted_addresses: None, - gas_used: 8681, - pre_zero5_gas_used: Some(MINT_GAS_COST), - eip7708_gas_used: Some(9056), - zero6_gas_used: Some(7056), - target_address: NATIVE_COIN_AUTHORITY_ADDRESS, - bytecode_address: NATIVE_COIN_AUTHORITY_ADDRESS, - ..Default::default() - }, - // No auth SLOAD, zero-address check precedes blocklist SLOADs - NativeCoinAuthorityTest { - name: "mint() to zero address reverts (Zero5+)", - caller: ALLOWED_CALLER_ADDRESS, - calldata: INativeCoinAuthority::mintCall { - to: Address::ZERO, - amount: U256::from(1), - } - .abi_encode() - .into(), - gas_limit: MINT_GAS_COST, - pre_zero5_gas_limit: None, - expected_revert_str: Some(ERR_ZERO_ADDRESS), - expected_result: InstructionResult::Revert, - return_data: None, - blocklisted_addresses: None, - gas_used: 0, - pre_zero5_gas_used: None, - zero6_gas_used: Some(PRECOMPILE_EARLY_REVERT_GAS_PENALTY), - target_address: NATIVE_COIN_AUTHORITY_ADDRESS, - bytecode_address: NATIVE_COIN_AUTHORITY_ADDRESS, - eip7708_only: true, - ..Default::default() - }, - // No auth SLOAD, reverts immediately - NativeCoinAuthorityTest { - name: "burn() with unauthorized caller reverts", - caller: ADDRESS_A, - calldata: INativeCoinAuthority::burnCall { - from: ADDRESS_A, - amount: U256::from(1000), - } - .abi_encode() - .into(), - gas_limit: BURN_GAS_COST, - pre_zero5_gas_limit: None, - expected_revert_str: Some(ERR_CANNOT_BURN), - expected_result: InstructionResult::Revert, - return_data: None, - blocklisted_addresses: None, - gas_used: 0, - pre_zero5_gas_used: Some(PRECOMPILE_SLOAD_GAS_COST), - zero6_gas_used: Some(PRECOMPILE_EARLY_REVERT_GAS_PENALTY), - target_address: NATIVE_COIN_AUTHORITY_ADDRESS, - bytecode_address: NATIVE_COIN_AUTHORITY_ADDRESS, - ..Default::default() - }, - // No auth SLOAD, blocklist SLOAD cold (2100), reverts before balance ops - NativeCoinAuthorityTest { - name: "burn() with zero amount reverts", - caller: ALLOWED_CALLER_ADDRESS, - calldata: INativeCoinAuthority::burnCall { - from: ADDRESS_A, - amount: U256::ZERO, - } - .abi_encode() - .into(), - gas_limit: BURN_GAS_COST, - pre_zero5_gas_limit: None, - expected_revert_str: Some(ERR_ZERO_AMOUNT), - expected_result: InstructionResult::Revert, - return_data: None, - blocklisted_addresses: None, - gas_used: 2100, // blocklist cold SLOAD only - pre_zero5_gas_used: Some(PRECOMPILE_SLOAD_GAS_COST * 2), - zero6_gas_used: Some(2100 + PRECOMPILE_EARLY_REVERT_GAS_PENALTY), - target_address: NATIVE_COIN_AUTHORITY_ADDRESS, - bytecode_address: NATIVE_COIN_AUTHORITY_ADDRESS, - ..Default::default() - }, - // No auth SLOAD, blocklist cold (2100), balance check SLOAD (fixed 2100) - NativeCoinAuthorityTest { - name: "burn() more than balance reverts with insufficient funds", - caller: ALLOWED_CALLER_ADDRESS, - calldata: INativeCoinAuthority::burnCall { - from: ADDRESS_A, - amount: mock_initial_supply + U256::from(1), - } - .abi_encode() - .into(), - gas_limit: BURN_GAS_COST, - pre_zero5_gas_limit: None, - expected_revert_str: Some(ERR_INSUFFICIENT_FUNDS), - expected_result: InstructionResult::Revert, - return_data: None, - blocklisted_addresses: None, - gas_used: 4200, // blocklist cold + balance check fixed - pre_zero5_gas_used: Some(PRECOMPILE_SLOAD_GAS_COST * 3), - zero6_gas_used: Some(2200), - target_address: NATIVE_COIN_AUTHORITY_ADDRESS, - bytecode_address: NATIVE_COIN_AUTHORITY_ADDRESS, - ..Default::default() - }, - NativeCoinAuthorityTest { - name: "burn() with insufficient gas errors with OOG", - caller: ALLOWED_CALLER_ADDRESS, - calldata: INativeCoinAuthority::burnCall { - from: ADDRESS_A, - amount: U256::from(1), - } - .abi_encode() - .into(), - // Pre-Zero5: uses early gas check with BURN_GAS_COST - PRECOMPILE_SLOAD_GAS_COST - 1 - // Zero5 (EIP-7708): uses Transfer event (9056 gas needed), so 8680 still triggers OOG - gas_limit: 8680, - pre_zero5_gas_limit: Some(BURN_GAS_COST - PRECOMPILE_SLOAD_GAS_COST - 1), - expected_revert_str: None, - expected_result: InstructionResult::PrecompileOOG, - return_data: None, - blocklisted_addresses: None, - gas_used: 0, - pre_zero5_gas_used: None, - target_address: NATIVE_COIN_AUTHORITY_ADDRESS, - bytecode_address: NATIVE_COIN_AUTHORITY_ADDRESS, - pre_eip7708_only: true, - ..Default::default() - }, - NativeCoinAuthorityTest { - name: "burn() with invalid params reverts with Execution Reverted", - caller: ALLOWED_CALLER_ADDRESS, - calldata: INativeCoinAuthority::burnCall::SELECTOR.into(), - gas_limit: BURN_GAS_COST, - pre_zero5_gas_limit: None, - expected_revert_str: Some(ERR_EXECUTION_REVERTED), - expected_result: InstructionResult::Revert, - return_data: None, - blocklisted_addresses: None, - gas_used: PRECOMPILE_EARLY_REVERT_GAS_PENALTY, - pre_zero5_gas_used: None, - target_address: NATIVE_COIN_AUTHORITY_ADDRESS, - bytecode_address: NATIVE_COIN_AUTHORITY_ADDRESS, - ..Default::default() - }, - // No auth SLOAD, delegate check happens before storage ops - NativeCoinAuthorityTest { - name: "burn() reverts if target address != precompile address", - caller: ALLOWED_CALLER_ADDRESS, - calldata: INativeCoinAuthority::burnCall { - from: ADDRESS_A, - amount: U256::from(1), - } - .abi_encode() - .into(), - gas_limit: BURN_GAS_COST, - pre_zero5_gas_limit: None, - expected_revert_str: Some(ERR_DELEGATE_CALL_NOT_ALLOWED), - expected_result: InstructionResult::Revert, - return_data: None, - blocklisted_addresses: None, - gas_used: 0, - pre_zero5_gas_used: Some(PRECOMPILE_SLOAD_GAS_COST), - target_address: ADDRESS_B, - bytecode_address: NATIVE_COIN_AUTHORITY_ADDRESS, - ..Default::default() - }, - // No auth SLOAD, delegate check happens before storage ops - NativeCoinAuthorityTest { - name: "burn() reverts if bytecode address != precompile address", - caller: ALLOWED_CALLER_ADDRESS, - calldata: INativeCoinAuthority::burnCall { - from: ADDRESS_A, - amount: U256::from(1), - } - .abi_encode() - .into(), - gas_limit: BURN_GAS_COST, - pre_zero5_gas_limit: None, - expected_revert_str: Some(ERR_DELEGATE_CALL_NOT_ALLOWED), - expected_result: InstructionResult::Revert, - return_data: None, - blocklisted_addresses: None, - gas_used: 0, - pre_zero5_gas_used: Some(PRECOMPILE_SLOAD_GAS_COST), - target_address: NATIVE_COIN_AUTHORITY_ADDRESS, - bytecode_address: ADDRESS_B, - ..Default::default() - }, - // blocklist cold (2100) + balance_decr fixed (5000) + total_supply warm read (100) - // Zero5: blocklist cold (2100) + balance_decr fixed (5000) - // + total_supply warm read/write (200) + Transfer event (1756) = 9056. - NativeCoinAuthorityTest { - name: "burn() succeeds and returns true", - caller: ALLOWED_CALLER_ADDRESS, - calldata: INativeCoinAuthority::burnCall { - from: ADDRESS_A, - amount: U256::from(1), - } - .abi_encode() - .into(), - gas_limit: BURN_GAS_COST, - pre_zero5_gas_limit: None, - eip7708_gas_limit: Some(BURN_GAS_COST_EIP7708), - expected_revert_str: None, - expected_result: InstructionResult::Return, - return_data: Some(true.abi_encode().into()), - blocklisted_addresses: None, - gas_used: 8681, - pre_zero5_gas_used: Some(BURN_GAS_COST), - eip7708_gas_used: Some(9056), - zero6_gas_used: Some(7056), - target_address: NATIVE_COIN_AUTHORITY_ADDRESS, - bytecode_address: NATIVE_COIN_AUTHORITY_ADDRESS, - ..Default::default() - }, - // No auth SLOAD, zero-address check precedes blocklist SLOADs - NativeCoinAuthorityTest { - name: "burn() from zero address reverts (Zero5+)", - caller: ALLOWED_CALLER_ADDRESS, - calldata: INativeCoinAuthority::burnCall { - from: Address::ZERO, - amount: U256::from(1), - } - .abi_encode() - .into(), - gas_limit: BURN_GAS_COST, - pre_zero5_gas_limit: None, - expected_revert_str: Some(ERR_ZERO_ADDRESS), - expected_result: InstructionResult::Revert, - return_data: None, - blocklisted_addresses: None, - gas_used: 0, - pre_zero5_gas_used: None, - zero6_gas_used: Some(PRECOMPILE_EARLY_REVERT_GAS_PENALTY), - target_address: NATIVE_COIN_AUTHORITY_ADDRESS, - bytecode_address: NATIVE_COIN_AUTHORITY_ADDRESS, - eip7708_only: true, - ..Default::default() - }, - // No auth SLOAD, reverts immediately - NativeCoinAuthorityTest { - name: "transfer() with unauthorized caller reverts", - caller: ADDRESS_A, - calldata: INativeCoinAuthority::transferCall { - from: ADDRESS_A, - to: ADDRESS_B, - amount: U256::from(1000), - } - .abi_encode() - .into(), - gas_limit: TRANSFER_GAS_COST, - pre_zero5_gas_limit: None, - expected_revert_str: Some(ERR_CANNOT_TRANSFER), - expected_result: InstructionResult::Revert, - return_data: None, - blocklisted_addresses: None, - gas_used: 0, - pre_zero5_gas_used: Some(PRECOMPILE_SLOAD_GAS_COST), - zero6_gas_used: Some(PRECOMPILE_EARLY_REVERT_GAS_PENALTY), - target_address: NATIVE_COIN_AUTHORITY_ADDRESS, - bytecode_address: NATIVE_COIN_AUTHORITY_ADDRESS, - ..Default::default() - }, - // No auth SLOAD, from blocklist cold (2100), to blocklist cold (2100), balance check fixed (2100) - NativeCoinAuthorityTest { - name: "transfer() more than balance reverts with insufficient funds", - caller: ALLOWED_CALLER_ADDRESS, - calldata: INativeCoinAuthority::transferCall { - from: ADDRESS_A, - to: ADDRESS_B, - amount: mock_initial_supply + U256::from(1), - } - .abi_encode() - .into(), - gas_limit: TRANSFER_GAS_COST, - pre_zero5_gas_limit: None, - expected_revert_str: Some(ERR_INSUFFICIENT_FUNDS), - expected_result: InstructionResult::Revert, - return_data: None, - blocklisted_addresses: None, - gas_used: 6300, // 2 blocklist cold SLOADs (4200) + balance check (2100) - pre_zero5_gas_used: Some(PRECOMPILE_SLOAD_GAS_COST * 4), - // Zero6: warm from-account load (100) replaces fixed 2100 - zero6_gas_used: Some(PRECOMPILE_SLOAD_GAS_COST * 2 + 100), - target_address: NATIVE_COIN_AUTHORITY_ADDRESS, - bytecode_address: NATIVE_COIN_AUTHORITY_ADDRESS, - ..Default::default() - }, - NativeCoinAuthorityTest { - name: "transfer() with insufficient gas errors with OOG", - caller: ALLOWED_CALLER_ADDRESS, - calldata: INativeCoinAuthority::transferCall { - from: ADDRESS_A, - to: ADDRESS_B, - amount: U256::from(1), - } - .abi_encode() - .into(), - // Zero5: 15956 gas needed for success, use 15955 to trigger OOG - // Zero5 + Zero6: warm-from discount drops success to 14456, use 14455 - // Pre-Zero5: uses early gas check with TRANSFER_GAS_COST - PRECOMPILE_SLOAD_GAS_COST - 1 - gas_limit: 15955, - pre_zero5_gas_limit: Some(TRANSFER_GAS_COST - PRECOMPILE_SLOAD_GAS_COST - 1), - zero6_gas_limit: Some(14455), - expected_revert_str: None, - expected_result: InstructionResult::PrecompileOOG, - return_data: None, - blocklisted_addresses: None, - gas_used: 0, - pre_zero5_gas_used: None, - target_address: NATIVE_COIN_AUTHORITY_ADDRESS, - bytecode_address: NATIVE_COIN_AUTHORITY_ADDRESS, - ..Default::default() - }, - NativeCoinAuthorityTest { - name: "transfer() with invalid params reverts with Execution Reverted", - caller: ALLOWED_CALLER_ADDRESS, - calldata: INativeCoinAuthority::transferCall::SELECTOR.into(), - gas_limit: TRANSFER_GAS_COST, - pre_zero5_gas_limit: None, - expected_revert_str: Some(ERR_EXECUTION_REVERTED), - expected_result: InstructionResult::Revert, - return_data: None, - blocklisted_addresses: None, - gas_used: PRECOMPILE_EARLY_REVERT_GAS_PENALTY, - pre_zero5_gas_used: None, - target_address: NATIVE_COIN_AUTHORITY_ADDRESS, - bytecode_address: NATIVE_COIN_AUTHORITY_ADDRESS, - ..Default::default() - }, - // No auth SLOAD, delegate check happens before storage ops - NativeCoinAuthorityTest { - name: "transfer() reverts if target address != precompile address", - caller: ALLOWED_CALLER_ADDRESS, - calldata: INativeCoinAuthority::transferCall { - from: ADDRESS_A, - to: ADDRESS_B, - amount: U256::from(1), - } - .abi_encode() - .into(), - gas_limit: TRANSFER_GAS_COST, - pre_zero5_gas_limit: None, - expected_revert_str: Some(ERR_DELEGATE_CALL_NOT_ALLOWED), - expected_result: InstructionResult::Revert, - return_data: None, - blocklisted_addresses: None, - gas_used: 0, - pre_zero5_gas_used: Some(PRECOMPILE_SLOAD_GAS_COST), - target_address: ADDRESS_B, - bytecode_address: NATIVE_COIN_AUTHORITY_ADDRESS, - ..Default::default() - }, - // No auth SLOAD, delegate check happens before storage ops - NativeCoinAuthorityTest { - name: "transfer() reverts if bytecode address != precompile address", - caller: ALLOWED_CALLER_ADDRESS, - calldata: INativeCoinAuthority::transferCall { - from: ADDRESS_A, - to: ADDRESS_B, - amount: U256::from(1), - } - .abi_encode() - .into(), - gas_limit: TRANSFER_GAS_COST, - pre_zero5_gas_limit: None, - expected_revert_str: Some(ERR_DELEGATE_CALL_NOT_ALLOWED), - expected_result: InstructionResult::Revert, - return_data: None, - blocklisted_addresses: None, - gas_used: 0, - pre_zero5_gas_used: Some(PRECOMPILE_SLOAD_GAS_COST), - target_address: NATIVE_COIN_AUTHORITY_ADDRESS, - bytecode_address: ADDRESS_B, - ..Default::default() - }, - // No auth SLOAD, zero-address check precedes blocklist SLOADs - NativeCoinAuthorityTest { - name: "transfer() to zero address reverts (Zero5+)", - caller: ALLOWED_CALLER_ADDRESS, - calldata: INativeCoinAuthority::transferCall { - from: ADDRESS_A, - to: Address::ZERO, - amount: U256::from(1), - } - .abi_encode() - .into(), - gas_limit: TRANSFER_GAS_COST, - pre_zero5_gas_limit: None, - expected_revert_str: Some(ERR_ZERO_ADDRESS), - expected_result: InstructionResult::Revert, - return_data: None, - blocklisted_addresses: None, - gas_used: 0, - pre_zero5_gas_used: None, - zero6_gas_used: Some(PRECOMPILE_EARLY_REVERT_GAS_PENALTY), - target_address: NATIVE_COIN_AUTHORITY_ADDRESS, - bytecode_address: NATIVE_COIN_AUTHORITY_ADDRESS, - eip7708_only: true, - ..Default::default() - }, - NativeCoinAuthorityTest { - name: "transfer() from zero address reverts (Zero5+)", - caller: ALLOWED_CALLER_ADDRESS, - calldata: INativeCoinAuthority::transferCall { - from: Address::ZERO, - to: ADDRESS_B, - amount: U256::from(1), - } - .abi_encode() - .into(), - gas_limit: TRANSFER_GAS_COST, - pre_zero5_gas_limit: None, - expected_revert_str: Some(ERR_ZERO_ADDRESS), - expected_result: InstructionResult::Revert, - return_data: None, - blocklisted_addresses: None, - gas_used: 0, - pre_zero5_gas_used: None, - zero6_gas_used: Some(PRECOMPILE_EARLY_REVERT_GAS_PENALTY), - target_address: NATIVE_COIN_AUTHORITY_ADDRESS, - bytecode_address: NATIVE_COIN_AUTHORITY_ADDRESS, - eip7708_only: true, - ..Default::default() - }, - NativeCoinAuthorityTest { - name: "transfer() with both zero addresses reverts (Zero5+)", - caller: ALLOWED_CALLER_ADDRESS, - calldata: INativeCoinAuthority::transferCall { - from: Address::ZERO, - to: Address::ZERO, - amount: U256::from(1), - } - .abi_encode() - .into(), - gas_limit: TRANSFER_GAS_COST, - pre_zero5_gas_limit: None, - expected_revert_str: Some(ERR_ZERO_ADDRESS), - expected_result: InstructionResult::Revert, - return_data: None, - blocklisted_addresses: None, - gas_used: 0, - pre_zero5_gas_used: None, - zero6_gas_used: Some(PRECOMPILE_EARLY_REVERT_GAS_PENALTY), - target_address: NATIVE_COIN_AUTHORITY_ADDRESS, - bytecode_address: NATIVE_COIN_AUTHORITY_ADDRESS, - eip7708_only: true, - ..Default::default() - }, - // from blocklist cold (2100) + to blocklist cold (2100) + transfer fixed (10000) - // + event (1756) = 15956 - NativeCoinAuthorityTest { - name: "transfer() with non-zero amount succeeds and returns true", - caller: ALLOWED_CALLER_ADDRESS, - calldata: INativeCoinAuthority::transferCall { - from: ADDRESS_A, - to: ADDRESS_B, - amount: U256::from(1), - } - .abi_encode() - .into(), - gas_limit: TRANSFER_GAS_COST, - pre_zero5_gas_limit: None, - zero6_gas_limit: Some(TRANSFER_GAS_COST + ZERO6_EMPTY_ACCOUNT_GAS_DELTA), - expected_revert_str: None, - expected_result: InstructionResult::Return, - return_data: Some(true.abi_encode().into()), - blocklisted_addresses: None, - gas_used: 15956, - pre_zero5_gas_used: Some(TRANSFER_GAS_COST), - zero6_gas_used: Some(14456 + ZERO6_EMPTY_ACCOUNT_GAS_DELTA), - target_address: NATIVE_COIN_AUTHORITY_ADDRESS, - bytecode_address: NATIVE_COIN_AUTHORITY_ADDRESS, - ..Default::default() - }, - // Zero6: NON_EMPTY_ADDRESS is initialized in test setup, so transfer() - // must not charge the empty-account creation surcharge. - NativeCoinAuthorityTest { - name: "transfer() to non-empty account succeeds without empty account surcharge", - caller: ALLOWED_CALLER_ADDRESS, - calldata: INativeCoinAuthority::transferCall { - from: ADDRESS_A, - to: NON_EMPTY_ADDRESS, - amount: U256::from(1), - } - .abi_encode() - .into(), - gas_limit: TRANSFER_GAS_COST, - pre_zero5_gas_limit: None, - expected_revert_str: None, - expected_result: InstructionResult::Return, - return_data: Some(true.abi_encode().into()), - blocklisted_addresses: None, - gas_used: 15956, - pre_zero5_gas_used: Some(TRANSFER_GAS_COST), - zero6_gas_used: Some(11956), - target_address: NATIVE_COIN_AUTHORITY_ADDRESS, - bytecode_address: NATIVE_COIN_AUTHORITY_ADDRESS, - ..Default::default() - }, - // No auth SLOAD, from/to blocklist cold (4200), balance check (2100), balance decr SSTORE penalty (2900) - NativeCoinAuthorityTest { - name: "transfer() full balance for empty account reverts", - caller: ALLOWED_CALLER_ADDRESS, - calldata: INativeCoinAuthority::transferCall { - from: ADDRESS_A, - to: ADDRESS_B, - amount: U256::from(mock_initial_supply), - } - .abi_encode() - .into(), - gas_limit: TRANSFER_GAS_COST, - pre_zero5_gas_limit: None, - expected_revert_str: Some(ERR_CLEAR_EMPTY), - expected_result: InstructionResult::Revert, - return_data: None, - blocklisted_addresses: None, - gas_used: 9200, // 2 blocklist cold (4200) + balance (2100) + SSTORE penalty (2900) - pre_zero5_gas_used: Some( - PRECOMPILE_SLOAD_GAS_COST * 4 + PRECOMPILE_SSTORE_GAS_COST, - ), - zero6_gas_used: Some(7200), - target_address: NATIVE_COIN_AUTHORITY_ADDRESS, - bytecode_address: NATIVE_COIN_AUTHORITY_ADDRESS, - ..Default::default() - }, - // from blocklist cold (2100) + to blocklist cold (2100) + transfer fixed (10000) - // + event (1756) = 15956 - NativeCoinAuthorityTest { - name: "transfer() full balance for non-empty account does not revert", - caller: ALLOWED_CALLER_ADDRESS, - calldata: INativeCoinAuthority::transferCall { - from: NON_EMPTY_ADDRESS, - to: ADDRESS_B, - amount: U256::from(mock_initial_supply), - } - .abi_encode() - .into(), - gas_limit: TRANSFER_GAS_COST, - pre_zero5_gas_limit: None, - zero6_gas_limit: Some(TRANSFER_GAS_COST + ZERO6_EMPTY_ACCOUNT_GAS_DELTA), - expected_revert_str: None, - expected_result: InstructionResult::Return, - return_data: Some(true.abi_encode().into()), - blocklisted_addresses: None, - gas_used: 15956, - pre_zero5_gas_used: Some(TRANSFER_GAS_COST), - zero6_gas_used: Some(14456 + ZERO6_EMPTY_ACCOUNT_GAS_DELTA), - target_address: NATIVE_COIN_AUTHORITY_ADDRESS, - bytecode_address: NATIVE_COIN_AUTHORITY_ADDRESS, - ..Default::default() - }, - // No auth SLOAD, from/to blocklist cold (4200) - no balance ops for zero amount - NativeCoinAuthorityTest { - name: "transfer() with zero amount succeeds and returns true", - caller: ALLOWED_CALLER_ADDRESS, - calldata: INativeCoinAuthority::transferCall { - from: ADDRESS_A, - to: ADDRESS_B, - amount: U256::ZERO, - } - .abi_encode() - .into(), - gas_limit: TRANSFER_GAS_COST, - pre_zero5_gas_limit: None, - expected_revert_str: None, - expected_result: InstructionResult::Return, - return_data: Some(true.abi_encode().into()), - blocklisted_addresses: None, - gas_used: 4200, // 2 blocklist cold SLOADs - pre_zero5_gas_used: Some(TRANSFER_GAS_COST_WITH_ZERO_AMOUNT), - target_address: NATIVE_COIN_AUTHORITY_ADDRESS, - bytecode_address: NATIVE_COIN_AUTHORITY_ADDRESS, - ..Default::default() - }, - // total_supply warm SLOAD (100) - test setup writes it - NativeCoinAuthorityTest { - name: "totalSupply() returns the total supply", - caller: ADDRESS_A, - calldata: INativeCoinAuthority::totalSupplyCall::SELECTOR.into(), - gas_limit: TOTAL_SUPPLY_GAS_COST, - pre_zero5_gas_limit: None, - expected_revert_str: None, - expected_result: InstructionResult::Return, - return_data: Some(mock_initial_supply.abi_encode().into()), - blocklisted_addresses: None, - gas_used: 100, - pre_zero5_gas_used: Some(PRECOMPILE_SLOAD_GAS_COST), - target_address: NATIVE_COIN_AUTHORITY_ADDRESS, - bytecode_address: NATIVE_COIN_AUTHORITY_ADDRESS, - ..Default::default() - }, - NativeCoinAuthorityTest { - name: "totalSupply() errors with OOG if insufficient gas", - caller: ADDRESS_A, - calldata: INativeCoinAuthority::totalSupplyCall::SELECTOR.into(), - gas_limit: TOTAL_SUPPLY_GAS_COST - 1, // Not enough gas - pre_zero5_gas_limit: None, - expected_revert_str: None, - expected_result: InstructionResult::PrecompileOOG, - return_data: None, - blocklisted_addresses: None, - gas_used: 0, - pre_zero5_gas_used: None, - target_address: NATIVE_COIN_AUTHORITY_ADDRESS, - bytecode_address: NATIVE_COIN_AUTHORITY_ADDRESS, - ..Default::default() - }, - // Blocklist test cases - // blocklist warm (100) - test setup writes to blocklist slot, making it warm - NativeCoinAuthorityTest { - name: "mint() to blocklisted address reverts", - caller: ALLOWED_CALLER_ADDRESS, - calldata: INativeCoinAuthority::mintCall { - to: ADDRESS_B, - amount: U256::from(1000), - } - .abi_encode() - .into(), - gas_limit: MINT_GAS_COST, - pre_zero5_gas_limit: None, - expected_revert_str: Some(ERR_BLOCKED_ADDRESS), - expected_result: InstructionResult::Revert, - return_data: None, - blocklisted_addresses: Some(HashSet::from([ADDRESS_B])), - gas_used: 100, // blocklist warm (test setup wrote it) - pre_zero5_gas_used: Some(PRECOMPILE_SLOAD_GAS_COST * 2), - zero6_gas_used: Some(100 + PRECOMPILE_EARLY_REVERT_GAS_PENALTY), - target_address: NATIVE_COIN_AUTHORITY_ADDRESS, - bytecode_address: NATIVE_COIN_AUTHORITY_ADDRESS, - ..Default::default() - }, - // Zero5: blocklist cold (2100) + total_supply warm read/write (200) - // + balance_incr fixed (5000) + Transfer event (1756) = 9056. - NativeCoinAuthorityTest { - name: "mint() to non-blocklisted address succeeds", - caller: ALLOWED_CALLER_ADDRESS, - calldata: INativeCoinAuthority::mintCall { - to: ADDRESS_B, - amount: U256::from(1000), - } - .abi_encode() - .into(), - gas_limit: MINT_GAS_COST, - pre_zero5_gas_limit: None, - eip7708_gas_limit: Some(MINT_GAS_COST_EIP7708), - zero6_gas_limit: Some(MINT_GAS_COST_EIP7708 + ZERO6_EMPTY_ACCOUNT_GAS_DELTA), - expected_revert_str: None, - expected_result: InstructionResult::Return, - return_data: Some(true.abi_encode().into()), - blocklisted_addresses: Some(HashSet::from([ADDRESS_C])), - gas_used: 8681, - pre_zero5_gas_used: Some(MINT_GAS_COST), - eip7708_gas_used: Some(9056), - zero6_gas_used: Some(9556 + ZERO6_EMPTY_ACCOUNT_GAS_DELTA), - target_address: NATIVE_COIN_AUTHORITY_ADDRESS, - bytecode_address: NATIVE_COIN_AUTHORITY_ADDRESS, - ..Default::default() - }, - // blocklist warm (100) - test setup writes to blocklist slot, making it warm - NativeCoinAuthorityTest { - name: "burn() from blocklisted address reverts", - caller: ALLOWED_CALLER_ADDRESS, - calldata: INativeCoinAuthority::burnCall { - from: ADDRESS_B, - amount: U256::from(1000), - } - .abi_encode() - .into(), - gas_limit: BURN_GAS_COST, - pre_zero5_gas_limit: None, - expected_revert_str: Some(ERR_BLOCKED_ADDRESS), - expected_result: InstructionResult::Revert, - return_data: None, - blocklisted_addresses: Some(HashSet::from([ADDRESS_B])), - gas_used: 100, // blocklist warm (test setup wrote it) - pre_zero5_gas_used: Some(PRECOMPILE_SLOAD_GAS_COST * 2), - zero6_gas_used: Some(100 + PRECOMPILE_EARLY_REVERT_GAS_PENALTY), - target_address: NATIVE_COIN_AUTHORITY_ADDRESS, - bytecode_address: NATIVE_COIN_AUTHORITY_ADDRESS, - ..Default::default() - }, - // from blocklist warm (100) - test setup writes to blocklist slot - NativeCoinAuthorityTest { - name: "transfer() from blocklisted address reverts", - caller: ALLOWED_CALLER_ADDRESS, - calldata: INativeCoinAuthority::transferCall { - from: ADDRESS_A, - to: ADDRESS_B, - amount: U256::from(1000), - } - .abi_encode() - .into(), - gas_limit: TRANSFER_GAS_COST, - pre_zero5_gas_limit: None, - expected_revert_str: Some(ERR_BLOCKED_ADDRESS), - expected_result: InstructionResult::Revert, - return_data: None, - blocklisted_addresses: Some(HashSet::from([ADDRESS_A])), - gas_used: 100, // from blocklist warm (test setup wrote it) - pre_zero5_gas_used: Some(PRECOMPILE_SLOAD_GAS_COST * 2), - zero6_gas_used: Some(100 + PRECOMPILE_EARLY_REVERT_GAS_PENALTY), - target_address: NATIVE_COIN_AUTHORITY_ADDRESS, - bytecode_address: NATIVE_COIN_AUTHORITY_ADDRESS, - ..Default::default() - }, - // from blocklist cold (2100), to blocklist warm (100) - test setup writes to ADDRESS_B slot - NativeCoinAuthorityTest { - name: "transfer() to blocklisted address reverts", - caller: ALLOWED_CALLER_ADDRESS, - calldata: INativeCoinAuthority::transferCall { - from: ADDRESS_A, - to: ADDRESS_B, - amount: U256::from(1000), - } - .abi_encode() - .into(), - gas_limit: TRANSFER_GAS_COST, - pre_zero5_gas_limit: None, - expected_revert_str: Some(ERR_BLOCKED_ADDRESS), - expected_result: InstructionResult::Revert, - return_data: None, - blocklisted_addresses: Some(HashSet::from([ADDRESS_B])), - gas_used: 2200, // from blocklist cold (2100) + to blocklist warm (100) - pre_zero5_gas_used: Some(PRECOMPILE_SLOAD_GAS_COST * 3), - zero6_gas_used: Some(2200 + PRECOMPILE_EARLY_REVERT_GAS_PENALTY), - target_address: NATIVE_COIN_AUTHORITY_ADDRESS, - bytecode_address: NATIVE_COIN_AUTHORITY_ADDRESS, - ..Default::default() - }, - ]; - - for tc in cases { - for hardfork_flags in ArcHardforkFlags::all_combinations() { - // ZeroX hardforks are cumulative; Zero6 implies Zero5. - if hardfork_flags.is_active(ArcHardfork::Zero6) - && !hardfork_flags.is_active(ArcHardfork::Zero5) - { - continue; - } - if tc.eip7708_only && !hardfork_flags.is_active(ArcHardfork::Zero5) { - continue; - } - if tc.pre_eip7708_only && hardfork_flags.is_active(ArcHardfork::Zero5) { - continue; - } - - let tc_name = - tc.name.to_string() + &format!(" (hardfork_flags: {:?})", hardfork_flags); - - validate_test_case(tc); - - let mut ctx = mock_context(hardfork_flags); - setup_blocklist(&mut ctx, &tc.blocklisted_addresses); - setup_initial_state(&mut ctx, mock_initial_supply); - - let inputs = CallInputs { - scheme: CallScheme::Call, - target_address: tc.target_address, - bytecode_address: tc.bytecode_address, - known_bytecode: None, - value: CallValue::Transfer(U256::ZERO), - input: CallInput::Bytes(tc.calldata.clone()), - gas_limit: get_gas_limit(tc, hardfork_flags), - caller: tc.caller, - is_static: false, - return_memory_offset: 0..0, - }; - - let precompile_res = call_native_coin_authority(&mut ctx, &inputs, hardfork_flags); - assert_precompile_result(precompile_res, tc, hardfork_flags, &tc_name); - - cleanup_blocklist(&mut ctx, &tc.blocklisted_addresses); - } - } - } - - #[test] - fn mint_side_effects() { - for hardfork_flags in ArcHardforkFlags::all_combinations() { - // Initial supply - let initial_supply = U256::from(1_000_000_000); - let mint_amount = U256::from(1000); - let expected_total_supply = initial_supply + mint_amount; - - // Mock context with allowed caller - let mut ctx = mock_context(hardfork_flags); - - // Set initial total supply - ctx.journal_mut() - .sstore( - NATIVE_COIN_AUTHORITY_ADDRESS, - TOTAL_SUPPLY_STORAGE_KEY.into(), - initial_supply, - ) - .expect("Unable to write initial total supply"); - - // Prepare inputs - let inputs = CallInputs { - scheme: CallScheme::Call, - target_address: NATIVE_COIN_AUTHORITY_ADDRESS, - bytecode_address: NATIVE_COIN_AUTHORITY_ADDRESS, - known_bytecode: None, - caller: ALLOWED_CALLER_ADDRESS, - value: CallValue::Transfer(U256::ZERO), - input: CallInput::Bytes( - INativeCoinAuthority::mintCall { - to: ADDRESS_B, - amount: mint_amount, - } - .abi_encode() - .into(), - ), - gas_limit: 100_000, - is_static: false, - return_memory_offset: 0..0, - }; - - // Run precompile - let precompile_res = call_native_coin_authority(&mut ctx, &inputs, hardfork_flags); - - // Assert result - assert!(precompile_res.is_ok()); - let result = precompile_res.unwrap().unwrap(); - assert_eq!(result.result, InstructionResult::Return); - - // Check total supply updated - let total_supply_updated = ctx - .journal_mut() - .sload( - NATIVE_COIN_AUTHORITY_ADDRESS, - TOTAL_SUPPLY_STORAGE_KEY.into(), - ) - .expect("Failed to read total supply after mint"); - - assert_eq!(total_supply_updated.data, expected_total_supply); - - // Check recipient balance updated - let recipient_balance = ctx - .journal_mut() - .load_account(ADDRESS_B) - .expect("Failed to load recipient account") - .info - .balance; - - assert_eq!(recipient_balance, mint_amount); - - // Check event emission - let journal_mut = ctx.journal_mut(); - let logs = journal_mut.logs(); - - if hardfork_flags.is_active(ArcHardfork::Zero5) { - // Zero5 (EIP-7708): Transfer(0x0, recipient, amount) replaces NativeCoinMinted - assert_eq!( - logs.len(), - 1, - "Zero5: one EIP-7708 Transfer event expected for mint" - ); - let log = &logs[0]; - assert_eq!( - log.address, SYSTEM_ADDRESS, - "Log should be from EIP-7708 system address" - ); - let expected_log = Transfer { - from: Address::ZERO, - to: ADDRESS_B, - value: mint_amount, - } - .encode_log_data(); - assert_eq!(log.data, expected_log); - } else { - let expected_log = NativeCoinMinted { - recipient: ADDRESS_B, - amount: mint_amount, - } - .encode_log_data(); - assert_eq!(logs.len(), 1, "Expected one log event for mint"); - let log = &logs[0]; - assert_eq!( - log.address, NATIVE_COIN_AUTHORITY_ADDRESS, - "Log address mismatch" - ); - assert_eq!(log.data, expected_log); - } - } - } - - #[test] - fn burn_side_effects() { - for hardfork_flags in ArcHardforkFlags::all_combinations() { - // Initial supply and burn amount - let initial_supply = U256::from(1_000_000_000); - let burn_amount = U256::from(1000); - let expected_total_supply = initial_supply - burn_amount; - - // Mock context with allowed caller - let mut ctx = mock_context(hardfork_flags); - - // Set initial total supply - ctx.journal_mut() - .sstore( - NATIVE_COIN_AUTHORITY_ADDRESS, - TOTAL_SUPPLY_STORAGE_KEY.into(), - initial_supply, - ) - .expect("Unable to write initial total supply"); - - // Set initial balance for ADDRESS_A - ctx.journal_mut() - .load_account(ADDRESS_A) - .expect("Cannot load account"); - ctx.journal_mut() - .balance_incr(ADDRESS_A, initial_supply) - .expect("Unable to write initial balance for ADDRESS_A"); - - // Prepare inputs for burn - let inputs = CallInputs { - scheme: CallScheme::Call, - target_address: NATIVE_COIN_AUTHORITY_ADDRESS, - bytecode_address: NATIVE_COIN_AUTHORITY_ADDRESS, - known_bytecode: None, - caller: ALLOWED_CALLER_ADDRESS, - value: CallValue::Transfer(U256::ZERO), - input: CallInput::Bytes( - INativeCoinAuthority::burnCall { - from: ADDRESS_A, - amount: burn_amount, - } - .abi_encode() - .into(), - ), - gas_limit: 100_000, - is_static: false, - return_memory_offset: 0..0, - }; - - // Run precompile - let precompile_res = call_native_coin_authority(&mut ctx, &inputs, hardfork_flags); - - // Assert result - assert!(precompile_res.is_ok()); - let result = precompile_res.unwrap().unwrap(); - assert_eq!(result.result, InstructionResult::Return); - - // Check total supply updated - let total_supply_updated = ctx - .journal_mut() - .sload( - NATIVE_COIN_AUTHORITY_ADDRESS, - TOTAL_SUPPLY_STORAGE_KEY.into(), - ) - .expect("Failed to read total supply after burn"); - - assert_eq!(total_supply_updated.data, expected_total_supply); - - // Check account balance updated - let account_balance = ctx - .journal_mut() - .load_account(ADDRESS_A) - .expect("Failed to load account") - .info - .balance; - - assert_eq!(account_balance, initial_supply - burn_amount); - - // For the "current" hardfork, a burn should be a corresponding transfer - // to the zero address. In zero4, it should be a true burn. - let zero_account_balance = ctx - .journal_mut() - .load_account(Address::from([0u8; 20])) - .expect("Failed to load zero address") - .info - .balance; - - assert_eq!( - zero_account_balance, - U256::ZERO, - "Zero address balance should remain zero after burn" - ); - - // Check event emission - let journal_mut: &mut revm::Journal< - revm::database::EmptyDBTyped, - > = ctx.journal_mut(); - let logs = journal_mut.logs(); - - if hardfork_flags.is_active(ArcHardfork::Zero5) { - // Zero5 (EIP-7708): Transfer(from, 0x0, amount) replaces NativeCoinBurned - assert_eq!( - logs.len(), - 1, - "Zero5: one EIP-7708 Transfer event expected for burn" - ); - let log = &logs[0]; - assert_eq!( - log.address, SYSTEM_ADDRESS, - "Log should be from EIP-7708 system address" - ); - let expected_log = Transfer { - from: ADDRESS_A, - to: Address::ZERO, - value: burn_amount, - } - .encode_log_data(); - assert_eq!(log.data, expected_log); - } else { - let expected_log = NativeCoinBurned { - from: ADDRESS_A, - amount: burn_amount, - } - .encode_log_data(); - assert_eq!(logs.len(), 1, "Expected one log event for burn"); - let log = &logs[0]; - assert_eq!( - log.address, NATIVE_COIN_AUTHORITY_ADDRESS, - "Log address mismatch" - ); - assert_eq!(log.data, expected_log); - } - } - } - - #[test] - fn transfer_side_effects() { - for hardfork_flags in ArcHardforkFlags::all_combinations() { - // Initial supply and transfer amount - let initial_supply = U256::from(1_000_000_000); - let transfer_amount = U256::from(1000); - - // Mock context with allowed caller - let mut ctx = mock_context(hardfork_flags); - - // Set initial total supply - ctx.journal_mut() - .sstore( - NATIVE_COIN_AUTHORITY_ADDRESS, - TOTAL_SUPPLY_STORAGE_KEY.into(), - initial_supply, - ) - .expect("Unable to write initial total supply"); - - // Set initial balance for ADDRESS_A - ctx.journal_mut() - .load_account(ADDRESS_A) - .expect("Cannot load account"); - ctx.journal_mut() - .balance_incr(ADDRESS_A, initial_supply) - .expect("Unable to write initial balance for ADDRESS_A"); - - // --- Case 1: Transfer zero amount --- - let inputs_zero = CallInputs { - scheme: CallScheme::Call, - target_address: NATIVE_COIN_AUTHORITY_ADDRESS, - bytecode_address: NATIVE_COIN_AUTHORITY_ADDRESS, - known_bytecode: None, - caller: ALLOWED_CALLER_ADDRESS, - value: CallValue::Transfer(U256::ZERO), - input: CallInput::Bytes( - INativeCoinAuthority::transferCall { - from: ADDRESS_A, - to: ADDRESS_B, - amount: U256::ZERO, - } - .abi_encode() - .into(), - ), - gas_limit: 100_000, - is_static: false, - return_memory_offset: 0..0, - }; - - let precompile_res_zero = - call_native_coin_authority(&mut ctx, &inputs_zero, hardfork_flags); - assert!(precompile_res_zero.is_ok()); - let result_zero = precompile_res_zero.unwrap().unwrap(); - assert_eq!(result_zero.result, InstructionResult::Return); - - // Check total supply unchanged - let total_supply_after_zero = ctx - .journal_mut() - .sload( - NATIVE_COIN_AUTHORITY_ADDRESS, - TOTAL_SUPPLY_STORAGE_KEY.into(), - ) - .expect("Failed to read total supply after zero transfer"); - assert_eq!(total_supply_after_zero.data, initial_supply); - - // Check balances unchanged - let balance_a_zero = ctx - .journal_mut() - .load_account(ADDRESS_A) - .expect("Failed to load account A") - .info - .balance; - let balance_b_zero = ctx - .journal_mut() - .load_account(ADDRESS_B) - .expect("Failed to load account B") - .info - .balance; - - assert_eq!(balance_a_zero, initial_supply); - assert_eq!(balance_b_zero, U256::ZERO); - - // Check no event emitted - let logs_zero = ctx.journal().logs(); - assert_eq!( - logs_zero.len(), - 0, - "No event should be emitted for zero transfer" - ); - - // --- Case 2: Transfer non-zero amount --- - let inputs_nonzero = CallInputs { - scheme: CallScheme::Call, - target_address: NATIVE_COIN_AUTHORITY_ADDRESS, - bytecode_address: NATIVE_COIN_AUTHORITY_ADDRESS, - known_bytecode: None, - caller: ALLOWED_CALLER_ADDRESS, - value: CallValue::Transfer(U256::ZERO), - input: CallInput::Bytes( - INativeCoinAuthority::transferCall { - from: ADDRESS_A, - to: ADDRESS_B, - amount: transfer_amount, - } - .abi_encode() - .into(), - ), - gas_limit: 100_000, - is_static: false, - return_memory_offset: 0..0, - }; - - let precompile_res_nonzero = - call_native_coin_authority(&mut ctx, &inputs_nonzero, hardfork_flags); - assert!(precompile_res_nonzero.is_ok()); - let result_nonzero = precompile_res_nonzero.unwrap().unwrap(); - assert_eq!(result_nonzero.result, InstructionResult::Return); - - // Check total supply unchanged - let total_supply_after_nonzero = ctx - .journal_mut() - .sload( - NATIVE_COIN_AUTHORITY_ADDRESS, - TOTAL_SUPPLY_STORAGE_KEY.into(), - ) - .expect("Failed to read total supply after nonzero transfer"); - assert_eq!(total_supply_after_nonzero.data, initial_supply); - - // Check balances updated - let balance_a_nonzero = ctx - .journal_mut() - .load_account(ADDRESS_A) - .expect("Failed to load account A") - .info - .balance; - let balance_b_nonzero = ctx - .journal_mut() - .load_account(ADDRESS_B) - .expect("Failed to load account B") - .info - .balance; - assert_eq!(balance_a_nonzero, initial_supply - transfer_amount); - assert_eq!(balance_b_nonzero, transfer_amount); - - // Check event emission - let logs_nonzero = ctx.journal().logs(); - assert_eq!(logs_nonzero.len(), 1, "Expected one log event for transfer"); - let log = &logs_nonzero[0]; - - if hardfork_flags.is_active(ArcHardfork::Zero5) { - // Zero5 (EIP-7708): ERC-20 Transfer event with EIP-7708 emitter address - let expected_log = Transfer { - from: ADDRESS_A, - to: ADDRESS_B, - value: transfer_amount, - } - .encode_log_data(); - assert_eq!( - log.address, SYSTEM_ADDRESS, - "Zero5: log emitter should be SYSTEM_ADDRESS" - ); - assert_eq!(log.data, expected_log); - } else { - // Pre-Zero5: NativeCoinTransferred event with native coin authority address - let expected_log = NativeCoinTransferred { - from: ADDRESS_A, - to: ADDRESS_B, - amount: transfer_amount, - } - .encode_log_data(); - assert_eq!( - log.address, NATIVE_COIN_AUTHORITY_ADDRESS, - "Pre-Zero5: log emitter should be NATIVE_COIN_AUTHORITY_ADDRESS" - ); - assert_eq!(log.data, expected_log); - } - } - } - - /// EIP-7708: self-transfers (from == to) do not emit a Transfer log under Zero5. - /// Pre-Zero5: self-transfers still emit NativeCoinTransferred. - #[test] - fn transfer_self_transfer_no_log_under_zero5() { - for hardfork_flags in ArcHardforkFlags::all_combinations() { - let initial_supply = U256::from(1_000_000_000); - let transfer_amount = U256::from(1000); - - let mut ctx = mock_context(hardfork_flags); - setup_initial_state(&mut ctx, initial_supply); - - // Self-transfer: from == to == ADDRESS_A - let inputs = CallInputs { - scheme: CallScheme::Call, - target_address: NATIVE_COIN_AUTHORITY_ADDRESS, - bytecode_address: NATIVE_COIN_AUTHORITY_ADDRESS, - known_bytecode: None, - caller: ALLOWED_CALLER_ADDRESS, - value: CallValue::Transfer(U256::ZERO), - input: CallInput::Bytes( - INativeCoinAuthority::transferCall { - from: ADDRESS_A, - to: ADDRESS_A, - amount: transfer_amount, - } - .abi_encode() - .into(), - ), - gas_limit: 100_000, - is_static: false, - return_memory_offset: 0..0, - }; - - let precompile_res = call_native_coin_authority(&mut ctx, &inputs, hardfork_flags); - assert!(precompile_res.is_ok()); - let result = precompile_res.unwrap().unwrap(); - assert_eq!(result.result, InstructionResult::Return); - - // Balance should be unchanged (self-transfer) - let balance = ctx - .journal_mut() - .load_account(ADDRESS_A) - .expect("Failed to load account A") - .info - .balance; - assert_eq!(balance, initial_supply); - - let logs = ctx.journal().logs(); - - if hardfork_flags.is_active(ArcHardfork::Zero5) { - // Zero5 + EIP-7708: self-transfers do not emit a log - assert_eq!(logs.len(), 0, "Zero5: self-transfer should not emit a log"); - - // Self-transfer still executes the full transfer path (balance_decr + - // balance_incr) to preserve gas invariants across the Zero5 boundary — - // only event emission is suppressed. Under Zero6, the transfer() helper - // uses warm/cold account-load pricing: ADDRESS_A is pre-warmed by the - // test setup, so both account loads hit the warm path. - let (from_account_load, to_account_load) = - if hardfork_flags.is_active(ArcHardfork::Zero6) { - ( - revm_interpreter::gas::WARM_STORAGE_READ_COST, - revm_interpreter::gas::WARM_STORAGE_READ_COST, - ) - } else { - (PRECOMPILE_SLOAD_GAS_COST, PRECOMPILE_SLOAD_GAS_COST) - }; - let expected_gas = 2100 - + 100 - + from_account_load - + to_account_load - + 2 * PRECOMPILE_SSTORE_GAS_COST; - assert_eq!( - result.gas.used(), - expected_gas, - "Zero5: self-transfer gas should include transfer path but no event (flags={hardfork_flags:?})", - ); - } else { - // Pre-Zero5: self-transfers still emit NativeCoinTransferred - assert_eq!( - logs.len(), - 1, - "Pre-Zero5: self-transfer emits NativeCoinTransferred" - ); - let log = &logs[0]; - assert_eq!(log.address, NATIVE_COIN_AUTHORITY_ADDRESS); - let expected_log = NativeCoinTransferred { - from: ADDRESS_A, - to: ADDRESS_A, - amount: transfer_amount, - } - .encode_log_data(); - assert_eq!(log.data, expected_log); - - // Gas accounting: auth SLOAD + 2 blocklist SLOADs + transfer + event. - // Under Zero6, transfer()'s account loads use warm/cold pricing — - // ADDRESS_A is pre-warmed by setup, so both loads hit the warm path. - let zero6_account_load_delta = if hardfork_flags.is_active(ArcHardfork::Zero6) { - 2 * (PRECOMPILE_SLOAD_GAS_COST - revm_interpreter::gas::WARM_STORAGE_READ_COST) - } else { - 0 - }; - assert_eq!( - result.gas.used(), - TRANSFER_GAS_COST - zero6_account_load_delta, - "Pre-Zero5: self-transfer gas should match TRANSFER_GAS_COST (flags={hardfork_flags:?})", - ); - } - } - } - - #[test] - fn transfer_recipient_overflow_preserves_pre_zero6_gas() { - for hardfork_flags in ArcHardforkFlags::all_combinations() { - if hardfork_flags.is_active(ArcHardfork::Zero6) { - continue; - } - - let mut ctx = mock_context(hardfork_flags); - setup_initial_state(&mut ctx, U256::from(1_000_000_000)); - ctx.journal_mut() - .load_account(ADDRESS_B) - .expect("Cannot load recipient account"); - ctx.journal_mut() - .balance_incr(ADDRESS_B, U256::MAX) - .expect("Unable to set recipient max balance"); - - let inputs = CallInputs { - scheme: CallScheme::Call, - target_address: NATIVE_COIN_AUTHORITY_ADDRESS, - bytecode_address: NATIVE_COIN_AUTHORITY_ADDRESS, - known_bytecode: None, - caller: ALLOWED_CALLER_ADDRESS, - value: CallValue::Transfer(U256::ZERO), - input: CallInput::Bytes( - INativeCoinAuthority::transferCall { - from: ADDRESS_A, - to: ADDRESS_B, - amount: U256::from(1), - } - .abi_encode() - .into(), - ), - gas_limit: 100_000, - is_static: false, - return_memory_offset: 0..0, - }; - - let result = call_native_coin_authority(&mut ctx, &inputs, hardfork_flags) - .expect("call should not error") - .expect("call should return interpreter result"); - - assert_eq!(result.result, InstructionResult::Revert); - assert_eq!( - bytes_to_revert_message(result.output.as_ref()).as_deref(), - Some(ERR_OVERFLOW), - ); - - let expected_gas = if hardfork_flags.is_active(ArcHardfork::Zero5) { - 2 * PRECOMPILE_SLOAD_GAS_COST - + 2 * PRECOMPILE_SLOAD_GAS_COST - + 2 * PRECOMPILE_SSTORE_GAS_COST - } else { - 3 * PRECOMPILE_SLOAD_GAS_COST - + 2 * PRECOMPILE_SLOAD_GAS_COST - + 2 * PRECOMPILE_SSTORE_GAS_COST - }; - assert_eq!(result.gas.used(), expected_gas); - assert_eq!(result.gas.refunded(), 0); - } - } - - // Helper to convert bytes to a revert error string - fn bytes_to_revert_message(input: &[u8]) -> Option { - // Expect at least 4 bytes for the selector. - if input.len() < 4 { - return None; - } - // Check the selector matches the standard Error(string) selector. - if input[0..4] != REVERT_SELECTOR { - return None; - } - - String::abi_decode(&input[4..]).ok() - } - - #[test] - fn test_static_call_reverts_state_modifying_functions() { - use crate::helpers::ERR_STATE_CHANGE_DURING_STATIC_CALL; - - let state_modifying_calldatas: &[(&str, Bytes)] = &[ - ( - "mint", - INativeCoinAuthority::mintCall { - to: ADDRESS_A, - amount: U256::from(100), - } - .abi_encode() - .into(), - ), - ( - "burn", - INativeCoinAuthority::burnCall { - from: ADDRESS_A, - amount: U256::from(100), - } - .abi_encode() - .into(), - ), - ( - "transfer", - INativeCoinAuthority::transferCall { - from: ADDRESS_A, - to: ADDRESS_B, - amount: U256::from(100), - } - .abi_encode() - .into(), - ), - ]; - - for hardfork_flags in ArcHardforkFlags::all_combinations() { - // State-modifying functions must revert under static call - for (fn_name, calldata) in state_modifying_calldatas { - let mut ctx = mock_context(hardfork_flags); - let inputs = CallInputs { - scheme: CallScheme::Call, - target_address: NATIVE_COIN_AUTHORITY_ADDRESS, - bytecode_address: NATIVE_COIN_AUTHORITY_ADDRESS, - known_bytecode: None, - caller: ALLOWED_CALLER_ADDRESS, - value: CallValue::Transfer(U256::ZERO), - input: CallInput::Bytes(calldata.clone()), - gas_limit: 100_000, - is_static: true, - return_memory_offset: 0..0, - }; - - let result = call_native_coin_authority(&mut ctx, &inputs, hardfork_flags) - .expect("call should not error") - .expect("result should be Some"); - - assert_eq!( - result.result, - InstructionResult::Revert, - "{fn_name} (hardfork_flags: {hardfork_flags:?}): expected Revert", - ); - let revert_reason = bytes_to_revert_message(result.output.as_ref()); - assert_eq!( - revert_reason.as_deref(), - Some(ERR_STATE_CHANGE_DURING_STATIC_CALL), - "{fn_name} (hardfork_flags: {hardfork_flags:?}): wrong revert reason", - ); - } - - // Read-only function (totalSupply) must succeed under static call - { - let mut ctx = mock_context(hardfork_flags); - let inputs = CallInputs { - scheme: CallScheme::Call, - target_address: NATIVE_COIN_AUTHORITY_ADDRESS, - bytecode_address: NATIVE_COIN_AUTHORITY_ADDRESS, - known_bytecode: None, - caller: ADDRESS_A, - value: CallValue::Transfer(U256::ZERO), - input: CallInput::Bytes( - INativeCoinAuthority::totalSupplyCall {}.abi_encode().into(), - ), - gas_limit: 100_000, - is_static: true, - return_memory_offset: 0..0, - }; - - let result = call_native_coin_authority(&mut ctx, &inputs, hardfork_flags) - .expect("call should not error") - .expect("result should be Some"); - - assert_eq!( - result.result, - InstructionResult::Return, - "totalSupply (hardfork_flags: {hardfork_flags:?}): expected Return under static call", - ); - } - } - } - - #[test] - fn transfer_or_mint_to_selfdestructed_account_should_revert() { - let amount = U256::from(1000); - let hardfork_flags = ArcHardforkFlags::with(&[ArcHardfork::Zero4, ArcHardfork::Zero5]); - let mut ctx = mock_context(hardfork_flags); - let spec_id = ctx.cfg.spec; - - // Prepare ADDRESS_A as a destructed account. - let journal = ctx.journal_mut(); - journal - .load_account_mut_optional_code(ADDRESS_A, false) - .expect("load ADDRESS_A") - .set_balance(amount + amount); - journal.load_account(ADDRESS_B).expect("load ADDRESS_B"); - journal - .create_account_checkpoint(ADDRESS_A, ADDRESS_B, amount, spec_id) - .unwrap(); - journal - .selfdestruct(ADDRESS_B, ADDRESS_A, false) - .expect("selfdestruct"); - - // Prepare mint inputs - let mut inputs = CallInputs { - scheme: CallScheme::Call, - target_address: NATIVE_COIN_AUTHORITY_ADDRESS, - bytecode_address: NATIVE_COIN_AUTHORITY_ADDRESS, - known_bytecode: None, - caller: ALLOWED_CALLER_ADDRESS, - value: CallValue::Transfer(U256::ZERO), - input: CallInput::Bytes( - INativeCoinAuthority::mintCall { - to: ADDRESS_B, - amount, - } - .abi_encode() - .into(), - ), - gas_limit: 100_000, - is_static: false, - return_memory_offset: 0..0, - }; - - // Mint to destructed account should revert - let precompile_res = call_native_coin_authority(&mut ctx, &inputs, hardfork_flags); - assert!(precompile_res.is_ok()); - let result = precompile_res.unwrap().unwrap(); - assert_eq!(result.result, InstructionResult::Revert); - assert_eq!( - bytes_to_revert_message(result.output.as_ref()), - Some(ERR_SELFDESTRUCTED_BALANCE_INCREASED.to_string()) - ); - - // Prepare transfer inputs - inputs.input = CallInput::Bytes( - INativeCoinAuthority::transferCall { - from: ADDRESS_A, - to: ADDRESS_B, - amount, - } - .abi_encode() - .into(), - ); - - // Transfer to destructed account should revert - let precompile_res = call_native_coin_authority(&mut ctx, &inputs, hardfork_flags); - assert!(precompile_res.is_ok()); - let result = precompile_res.unwrap().unwrap(); - assert_eq!(result.result, InstructionResult::Revert); - assert_eq!( - bytes_to_revert_message(result.output.as_ref()), - Some(ERR_SELFDESTRUCTED_BALANCE_INCREASED.to_string()) - ); - } - - fn total_supply_calldata_with_trailing_bytes() -> Bytes { - let mut calldata = Vec::with_capacity(4 + 32); - calldata.extend_from_slice(&INativeCoinAuthority::totalSupplyCall::SELECTOR); - calldata.extend_from_slice(&[0u8; 32]); - calldata.into() - } - - #[test] - fn total_supply_rejects_extra_input_pre_zero6() { - for hardfork_flags in ArcHardforkFlags::all_combinations() { - if hardfork_flags.is_active(ArcHardfork::Zero6) { - continue; - } - - let mut ctx = mock_context(hardfork_flags); - let inputs = CallInputs { - scheme: CallScheme::Call, - target_address: NATIVE_COIN_AUTHORITY_ADDRESS, - bytecode_address: NATIVE_COIN_AUTHORITY_ADDRESS, - known_bytecode: None, - caller: ADDRESS_A, - value: CallValue::Transfer(U256::ZERO), - input: CallInput::Bytes(total_supply_calldata_with_trailing_bytes()), - gas_limit: 100_000, - is_static: false, - return_memory_offset: 0..0, - }; - - let result = call_native_coin_authority(&mut ctx, &inputs, hardfork_flags) - .expect("call should not error") - .expect("result should be Some"); - - assert_eq!( - result.result, - InstructionResult::Revert, - "({hardfork_flags:?}): expected Revert with trailing calldata pre-Zero6", - ); - assert_eq!( - bytes_to_revert_message(result.output.as_ref()).as_deref(), - Some(ERR_EXECUTION_REVERTED), - "({hardfork_flags:?}): expected execution reverted message", - ); - } - } - - /// Under Zero6, account helpers (`transfer`, `balance_incr`, `balance_decr`) - /// charge `WARM_STORAGE_READ_COST` (100) for warm accounts and - /// `COLD_ACCOUNT_ACCESS_COST` (2600) for cold accounts after the load. - /// - /// The target is pre-funded (non-empty) so the Zero6 empty-account - /// creation surcharge does not apply — the OOG is isolated to the - /// cold-account access charge. - /// - /// This test mints to both a cold and a pre-warmed address at the same - /// gas budget (`full_cold_gas - cold_delta`). The warm mint succeeds - /// (account load costs only 100); the cold mint OOGs at the cold-account - /// charge after the load. Together they prove the OOG is caused - /// specifically by the cold-account surcharge. - #[test] - fn account_load_cold_oog() { - use revm_interpreter::gas::{COLD_ACCOUNT_ACCESS_COST, WARM_STORAGE_READ_COST}; - - let zero6_flags = ArcHardforkFlags::with(&[ArcHardfork::Zero5, ArcHardfork::Zero6]); - let mock_initial_supply = U256::from(1_000_000_000); - - let cold_target: Address = address!("9999999999999999999999999999999999999999"); - - let make_inputs = |target: Address, gas_limit: u64| CallInputs { - scheme: CallScheme::Call, - target_address: NATIVE_COIN_AUTHORITY_ADDRESS, - bytecode_address: NATIVE_COIN_AUTHORITY_ADDRESS, - known_bytecode: None, - caller: ALLOWED_CALLER_ADDRESS, - value: CallValue::Transfer(U256::ZERO), - input: CallInput::Bytes( - INativeCoinAuthority::mintCall { - to: target, - amount: U256::from(1), - } - .abi_encode() - .into(), - ), - gas_limit, - is_static: false, - return_memory_offset: 0..0, - }; - - /// Pre-fund cold_target so it is non-empty (avoids Zero6 empty-account - /// creation surcharge), then commit the tx to clear warm addresses. - fn prefund_cold_target(ctx: &mut revm::Context, target: Address) { - ctx.journal_mut().load_account(target).expect("load target"); - ctx.journal_mut() - .balance_incr(target, U256::from(1)) - .expect("fund target"); - ctx.journal_mut().commit_tx(); - } - - // Observe full gas for a cold, non-empty target mint. - let mut ctx = mock_context(zero6_flags); - setup_initial_state(&mut ctx, mock_initial_supply); - prefund_cold_target(&mut ctx, cold_target); - let baseline = - call_native_coin_authority(&mut ctx, &make_inputs(cold_target, 1_000_000), zero6_flags) - .unwrap() - .unwrap(); - assert_eq!( - baseline.result, - InstructionResult::Return, - "baseline must succeed" - ); - let full_cold_gas = baseline.gas.used(); - - // Budget that covers warm (100) but not cold (2600) at the account load. - #[allow(clippy::arithmetic_side_effects)] - let boundary_gas = full_cold_gas - COLD_ACCOUNT_ACCESS_COST + WARM_STORAGE_READ_COST; - - // Cold target at boundary: OOG at the cold delta charge. - let mut ctx = mock_context(zero6_flags); - setup_initial_state(&mut ctx, mock_initial_supply); - prefund_cold_target(&mut ctx, cold_target); - let res = call_native_coin_authority( - &mut ctx, - &make_inputs(cold_target, boundary_gas), - zero6_flags, - ) - .unwrap() - .unwrap(); - assert_eq!( - res.result, - InstructionResult::PrecompileOOG, - "cold account at boundary_gas must OOG" - ); - - // Warm target at the same boundary: succeeds, proving the OOG above is - // caused specifically by the cold delta. - let mut ctx = mock_context(zero6_flags); - setup_initial_state(&mut ctx, mock_initial_supply); - prefund_cold_target(&mut ctx, cold_target); - // Pre-warm cold_target by loading it before the precompile call. - ctx.journal_mut() - .load_account(cold_target) - .expect("pre-warm target"); - let res = call_native_coin_authority( - &mut ctx, - &make_inputs(cold_target, boundary_gas), - zero6_flags, - ) - .unwrap() - .unwrap(); - assert_eq!( - res.result, - InstructionResult::Return, - "warm account at boundary_gas must succeed" - ); - } - - #[test] - fn total_supply_accepts_extra_input_with_zero6() { - let mock_initial_supply = U256::from(1_000_000_000); - - for hardfork_flags in ArcHardforkFlags::all_combinations() { - if !hardfork_flags.is_active(ArcHardfork::Zero6) { - continue; - } - - let mut ctx = mock_context(hardfork_flags); - setup_initial_state(&mut ctx, mock_initial_supply); - - let inputs = CallInputs { - scheme: CallScheme::Call, - target_address: NATIVE_COIN_AUTHORITY_ADDRESS, - bytecode_address: NATIVE_COIN_AUTHORITY_ADDRESS, - known_bytecode: None, - caller: ADDRESS_A, - value: CallValue::Transfer(U256::ZERO), - input: CallInput::Bytes(total_supply_calldata_with_trailing_bytes()), - gas_limit: 100_000, - is_static: false, - return_memory_offset: 0..0, - }; - - let result = call_native_coin_authority(&mut ctx, &inputs, hardfork_flags) - .expect("call should not error") - .expect("result should be Some"); - - assert_eq!( - result.result, - InstructionResult::Return, - "({hardfork_flags:?}): expected Return with trailing calldata under Zero6", - ); - let returned = U256::abi_decode(result.output.as_ref()).expect("decode total supply"); - assert_eq!( - returned, mock_initial_supply, - "({hardfork_flags:?}): expected initial supply returned", - ); - } - } -} diff --git a/crates/precompiles/src/native_coin_control.rs b/crates/precompiles/src/native_coin_control.rs deleted file mode 100644 index 211ff27..0000000 --- a/crates/precompiles/src/native_coin_control.rs +++ /dev/null @@ -1,1172 +0,0 @@ -// Copyright 2025 Circle Internet Group, Inc. All rights reserved. -// -// SPDX-License-Identifier: Apache-2.0 -// -// Licensed under the Apache License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. -// You may obtain a copy of the License at -// -// http://www.apache.org/licenses/LICENSE-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// See the License for the specific language governing permissions and -// limitations under the License. - -//! Native Coin Control Precompile -//! -//! This precompile implements native coin control operations including -//! blocklisting and unblocklisting addresses from receiving native coin transfers. - -use crate::helpers::{ - abi_decode_raw_with_zero6_validation, check_delegatecall, check_gas_remaining, - check_staticcall, emit_event, new_reverted_with_early_penalty, read, write, - PrecompileErrorOrRevert, ERR_EXECUTION_REVERTED, LOG_BASE_COST, LOG_TOPIC_COST, - NATIVE_FIAT_TOKEN_ADDRESS, PRECOMPILE_EARLY_REVERT_GAS_PENALTY, PRECOMPILE_SLOAD_GAS_COST, - PRECOMPILE_SSTORE_GAS_COST, -}; -use crate::precompile; -use alloy_evm::EvmInternals; -use alloy_primitives::{address, Address, StorageKey, U256}; -use alloy_sol_types::{sol, SolCall, SolValue}; -use arc_execution_config::hardforks::{ArcHardfork, ArcHardforkFlags}; -use arc_execution_config::native_coin_control as native_coin_control_config; -use reth_ethereum::evm::revm::precompile::PrecompileOutput; -use revm_interpreter::Gas; - -// Native coin control precompile address -pub const NATIVE_COIN_CONTROL_ADDRESS: Address = - address!("0x1800000000000000000000000000000000000001"); - -// Allowed caller form NativeFiatToken -const ALLOWED_CALLER_ADDRESS: Address = NATIVE_FIAT_TOKEN_ADDRESS; - -/// Exported error message / revert string -pub const BLOCKLISTED_ERROR_MESSAGE: &str = "address is blocklisted"; - -// Storage key for allowed caller (deprecated since Zero5) -const ALLOWED_CALLER_STORAGE_KEY: StorageKey = StorageKey::new([ - 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, -]); - -// Gas costs -const BLOCKLISTED_EVENT_GAS_COST: u64 = LOG_BASE_COST + 2 * LOG_TOPIC_COST; // 2 topics -const UNBLOCKLISTED_EVENT_GAS_COST: u64 = LOG_BASE_COST + 2 * LOG_TOPIC_COST; // 2 topics - -// Total gas costs for each operation - -// - Reading allowed caller (2100 gas) -// - Writing blocklist storage (2900 gas) -// - Emitting event (1125 gas) -// Total: 6125 gas -const BLOCKLIST_GAS_COST: u64 = - PRECOMPILE_SLOAD_GAS_COST + PRECOMPILE_SSTORE_GAS_COST + BLOCKLISTED_EVENT_GAS_COST; - -// - Reading blocklist storage (2100 gas) -// Total: 2100 gas -pub const IS_BLOCKLISTED_GAS_COST: u64 = PRECOMPILE_SLOAD_GAS_COST; - -// - Reading allowed caller (2100 gas) -// - Writing blocklist storage (2900 gas) -// - Emitting event (1125 gas) -// Total: 6125 gas -const UNBLOCKLIST_GAS_COST: u64 = - PRECOMPILE_SLOAD_GAS_COST + PRECOMPILE_SSTORE_GAS_COST + UNBLOCKLISTED_EVENT_GAS_COST; - -// Storage values -pub const BLOCKLISTED_STATUS: U256 = U256::from_limbs([1, 0, 0, 0]); // 0x01 -pub const UNBLOCKLISTED_STATUS: U256 = U256::ZERO; // 0x00 - -// Error messages -const ERR_CANNOT_BLOCKLIST: &str = "Not enabled for blocklisting"; -const ERR_CANNOT_UNBLOCKLIST: &str = "Not enabled for unblocklisting"; - -sol! { - /// Native Coin Control precompile interface - interface INativeCoinControl { - /// Add an address to the blocklist - function blocklist(address account) external returns (bool success); - - /// Check if an address is blocklisted - function isBlocklisted(address account) external view returns (bool _isBlocklisted); - - /// Remove an address from the blocklist - function unBlocklist(address account) external returns (bool success); - } - - /// Events - #[derive(Debug)] - event Blocklisted(address indexed account); - - #[derive(Debug)] - event UnBlocklisted(address indexed account); -} - -/// Checks if the caller is authorized to call mutative native coin control functions -fn is_authorized( - internals: &mut EvmInternals, - caller: Address, - gas_counter: &mut Gas, - hardfork_flags: ArcHardforkFlags, -) -> Result { - // Get allowed caller - let allowed_caller_output = read( - internals, - NATIVE_COIN_CONTROL_ADDRESS, - ALLOWED_CALLER_STORAGE_KEY, - gas_counter, - hardfork_flags, - )?; - - // Compare caller to allowed_caller_output - let caller_word = U256::from_be_slice(caller.as_ref()); - let allowed_caller_word = U256::from_be_slice(&allowed_caller_output); - Ok(caller_word == allowed_caller_word) -} - -/// Computes the storage slot for a mapping key of type address -/// -/// Delegates to the execution-config canonical implementation. -pub fn compute_is_blocklisted_storage_slot(key: Address) -> StorageKey { - native_coin_control_config::compute_is_blocklisted_storage_slot(key) -} - -precompile!(run_native_coin_control, precompile_input, hardfork_flags; { - INativeCoinControl::blocklistCall => |input| { - (|| -> Result { - let mut gas_counter = Gas::new(precompile_input.gas); - let mut precompile_input = precompile_input; - - // Check if static call is attempting to modify state - check_staticcall( - &precompile_input, - &mut gas_counter, - )?; - - // Decode arguments passed to blocklist function - let args = abi_decode_raw_with_zero6_validation::( - input, - hardfork_flags, - ) - .map_err(|_| - PrecompileErrorOrRevert::new_reverted_with_penalty( - gas_counter, PRECOMPILE_EARLY_REVERT_GAS_PENALTY, ERR_EXECUTION_REVERTED, - ) - )?; - - if hardfork_flags.is_active(ArcHardfork::Zero5) { - if hardfork_flags.is_active(ArcHardfork::Zero6) { - // Auth first so the Zero6 early-revert penalty is reachable - // regardless of remaining gas; otherwise the success-path - // floor below OOGs callers in the 200..4024 gas window. - if precompile_input.caller != ALLOWED_CALLER_ADDRESS { - return Err(new_reverted_with_early_penalty( - gas_counter, - ERR_CANNOT_BLOCKLIST, - hardfork_flags, - )); - } - check_gas_remaining( - &gas_counter, - BLOCKLIST_GAS_COST - PRECOMPILE_SLOAD_GAS_COST, - )?; - } else { - // Zero5-only: keep the original order to preserve consensus - // on networks already past the Zero5 activation block. - check_gas_remaining( - &gas_counter, - BLOCKLIST_GAS_COST - PRECOMPILE_SLOAD_GAS_COST, - )?; - if precompile_input.caller != ALLOWED_CALLER_ADDRESS { - return Err(new_reverted_with_early_penalty( - gas_counter, - ERR_CANNOT_BLOCKLIST, - hardfork_flags, - )); - } - } - } else { - // Early return if not enough gas - check_gas_remaining(&gas_counter, BLOCKLIST_GAS_COST)?; - - // Check authorization - if !(is_authorized( - &mut precompile_input.internals, - precompile_input.caller, - &mut gas_counter, - hardfork_flags, - )?) { - return Err(new_reverted_with_early_penalty(gas_counter, ERR_CANNOT_BLOCKLIST, hardfork_flags)); - } - } - - // Check delegate call - check_delegatecall( - NATIVE_COIN_CONTROL_ADDRESS, - &precompile_input, - &gas_counter, - )?; - - // Add to blocklist - let storage_slot = compute_is_blocklisted_storage_slot(args.account); - write( - &mut precompile_input.internals, - NATIVE_COIN_CONTROL_ADDRESS, - storage_slot, - &BLOCKLISTED_STATUS.to_be_bytes_vec(), - &mut gas_counter, - hardfork_flags, - )?; - - // Emit event - emit_event( - &mut precompile_input.internals, - NATIVE_COIN_CONTROL_ADDRESS, - &Blocklisted { - account: args.account, - }, - &mut gas_counter, - )?; - - let output = true.abi_encode(); - Ok(PrecompileOutput::new(gas_counter.used(), output.into())) - })() - }, - - INativeCoinControl::isBlocklistedCall => |input| { - (|| -> Result { - let mut gas_counter = Gas::new(precompile_input.gas); - let mut precompile_input = precompile_input; - - // Decode arguments passed to isBlocklisted function - let args = - abi_decode_raw_with_zero6_validation::( - input, - hardfork_flags, - ) - .map_err(|_| - PrecompileErrorOrRevert::new_reverted_with_penalty( - gas_counter, PRECOMPILE_EARLY_REVERT_GAS_PENALTY, ERR_EXECUTION_REVERTED, - ) - )?; - - // Early return if not enough gas - check_gas_remaining(&gas_counter, IS_BLOCKLISTED_GAS_COST)?; - - // Check if address is blocklisted - let storage_slot = compute_is_blocklisted_storage_slot(args.account); - let storage_output = read( - &mut precompile_input.internals, - NATIVE_COIN_CONTROL_ADDRESS, - storage_slot, - &mut gas_counter, - hardfork_flags, - )?; - - let status = U256::from_be_slice(&storage_output); - // Pessimistically assume blocklisted unless strictly matching unblocklisted status - let is_blocked = status != UNBLOCKLISTED_STATUS; - - let output = is_blocked.abi_encode(); - Ok(PrecompileOutput::new(gas_counter.used(), output.into())) - })() - }, - - INativeCoinControl::unBlocklistCall => |input| { - (|| -> Result { - let mut gas_counter = Gas::new(precompile_input.gas); - let mut precompile_input = precompile_input; - - // Check if static call is attempting to modify state - check_staticcall( - &precompile_input, - &mut gas_counter, - )?; - - // Decode arguments passed to unBlocklist function - let args = abi_decode_raw_with_zero6_validation::( - input, - hardfork_flags, - ) - .map_err(|_| - PrecompileErrorOrRevert::new_reverted_with_penalty( - gas_counter, PRECOMPILE_EARLY_REVERT_GAS_PENALTY, ERR_EXECUTION_REVERTED, - ) - )?; - - if hardfork_flags.is_active(ArcHardfork::Zero5) { - if hardfork_flags.is_active(ArcHardfork::Zero6) { - // Auth first so the Zero6 early-revert penalty is reachable - // regardless of remaining gas; otherwise the success-path - // floor below OOGs callers in the 200..4024 gas window. - if precompile_input.caller != ALLOWED_CALLER_ADDRESS { - return Err(new_reverted_with_early_penalty( - gas_counter, - ERR_CANNOT_UNBLOCKLIST, - hardfork_flags, - )); - } - check_gas_remaining( - &gas_counter, - UNBLOCKLIST_GAS_COST - PRECOMPILE_SLOAD_GAS_COST, - )?; - } else { - // Zero5-only: keep the original order to preserve consensus - // on networks already past the Zero5 activation block. - check_gas_remaining( - &gas_counter, - UNBLOCKLIST_GAS_COST - PRECOMPILE_SLOAD_GAS_COST, - )?; - if precompile_input.caller != ALLOWED_CALLER_ADDRESS { - return Err(new_reverted_with_early_penalty( - gas_counter, - ERR_CANNOT_UNBLOCKLIST, - hardfork_flags, - )); - } - } - } else { - // Early return if not enough gas - check_gas_remaining(&gas_counter, UNBLOCKLIST_GAS_COST)?; - - // Check authorization - if !(is_authorized( - &mut precompile_input.internals, - precompile_input.caller, - &mut gas_counter, - hardfork_flags, - )?) { - return Err(new_reverted_with_early_penalty(gas_counter, ERR_CANNOT_UNBLOCKLIST, hardfork_flags)); - } - } - - // Check delegate call - check_delegatecall( - NATIVE_COIN_CONTROL_ADDRESS, - &precompile_input, - &gas_counter, - )?; - - // Remove from blocklist - let storage_slot = compute_is_blocklisted_storage_slot(args.account); - write( - &mut precompile_input.internals, - NATIVE_COIN_CONTROL_ADDRESS, - storage_slot, - &UNBLOCKLISTED_STATUS.to_be_bytes_vec(), - &mut gas_counter, - hardfork_flags, - )?; - - // Emit event - emit_event( - &mut precompile_input.internals, - NATIVE_COIN_CONTROL_ADDRESS, - &UnBlocklisted { - account: args.account, - }, - &mut gas_counter, - )?; - - let output = true.abi_encode(); - Ok(PrecompileOutput::new(gas_counter.used(), output.into())) - })() - }, -}); - -#[cfg(test)] -mod tests { - use crate::helpers::ERR_DELEGATE_CALL_NOT_ALLOWED; - use arc_execution_config::hardforks::ArcHardforkFlags; - - use super::*; - use alloy_primitives::Bytes; - use alloy_sol_types::SolEvent; - use reth_ethereum::evm::revm::{ - context::{Context, ContextTr, JournalTr}, - interpreter::{CallInput, CallInputs, CallScheme, CallValue, InstructionResult}, - MainContext, - }; - use reth_evm::precompiles::{DynPrecompile, PrecompilesMap}; - use revm::{ - handler::PrecompileProvider, - interpreter::InterpreterResult, - precompile::{PrecompileId, Precompiles}, - }; - - fn mock_context(hardfork_flags: ArcHardforkFlags) -> revm::Context { - let mut ctx = Context::mainnet(); - - ctx.journal_mut() - .load_account(NATIVE_COIN_CONTROL_ADDRESS) - .expect("Unable to load native coin control account"); - - if !hardfork_flags.is_active(ArcHardfork::Zero5) { - ctx.journal_mut() - .sstore( - NATIVE_COIN_CONTROL_ADDRESS, - ALLOWED_CALLER_STORAGE_KEY.into(), - U256::from_be_slice(ALLOWED_CALLER_ADDRESS.as_ref()), - ) - .expect("Unable to write allowed caller"); - } - - ctx - } - - fn call_native_coin_control( - ctx: &mut Context, - inputs: &CallInputs, - hardfork_flags: ArcHardforkFlags, - ) -> Result, String> { - let mut provider = PrecompilesMap::from_static(Precompiles::latest()); - let target_addr: Address = inputs.target_address; - provider.set_precompile_lookup(move |address: &Address| { - if *address == NATIVE_COIN_CONTROL_ADDRESS || target_addr == NATIVE_COIN_CONTROL_ADDRESS - { - Some(DynPrecompile::new_stateful( - PrecompileId::Custom("NATIVE_COIN_CONTROL".into()), - move |input| run_native_coin_control(input, hardfork_flags), - )) - } else { - None - } - }); - provider.run(ctx, inputs) - } - - struct NativeCoinControlTest { - name: &'static str, - caller: Address, - calldata: Bytes, - gas_limit: u64, - expected_result: InstructionResult, - expected_revert_str: Option<&'static str>, - return_data: Option, - gas_used: u64, - /// If set, overrides gas_used for pre-Zero5 hardforks (before EIP-2929/2200 storage costs) - pre_zero5_gas_used: Option, - /// If set, overrides gas_used when Zero6 is active (auth/validation reverts now - /// charge `PRECOMPILE_EARLY_REVERT_GAS_PENALTY`). - zero6_gas_used: Option, - target_address: Address, - bytecode_address: Address, - /// If true, skip this test case for hardfork combinations without Zero6. - /// Used for cases whose result shape differs under Zero6 (e.g. penalty - /// revert vs OOG for low-gas unauthorized calls). - zero6_only: bool, - /// If true, skip this test case for hardfork combinations with Zero6. - pre_zero6_only: bool, - } - - // Test constants - const ADDRESS_A: Address = address!("1000000000000000000000000000000000000001"); - const ADDRESS_B: Address = address!("2000000000000000000000000000000000000002"); - - fn assert_precompile_result( - precompile_res: Result, String>, - tc: &NativeCoinControlTest, - hardfork_flags: ArcHardforkFlags, - tc_name: &str, - ) { - match precompile_res { - Ok(result) => { - assert!(result.is_some(), "{}: expected result to be some", tc.name); - let result = result.unwrap(); - - assert_eq!( - result.result, tc.expected_result, - "{tc_name}: expected result to match", - ); - - if let Some(expected_revert_str) = tc.expected_revert_str { - assert!( - result.is_revert(), - "{tc_name}: expected output to be reverted" - ); - let revert_reason = bytes_to_revert_message(result.output.as_ref()); - assert!(revert_reason.is_some(), "{tc_name}: expected revert reason"); - assert_eq!( - revert_reason.unwrap(), - expected_revert_str, - "{tc_name}: expected revert reason to match", - ); - } else { - assert!( - !result.is_revert(), - "{tc_name}: expected output not to be reverted" - ); - } - - if let Some(expected_return_data) = &tc.return_data { - assert_eq!( - result.output, *expected_return_data, - "{tc_name}: expected return data to match", - ); - } - - let expected_gas_used = if hardfork_flags.is_active(ArcHardfork::Zero6) { - tc.zero6_gas_used.unwrap_or(tc.gas_used) - } else if hardfork_flags.is_active(ArcHardfork::Zero5) { - tc.gas_used - } else { - tc.pre_zero5_gas_used.unwrap_or(tc.gas_used) - }; - assert_eq!( - result.gas.used(), - expected_gas_used, - "{tc_name}: gas used to match" - ); - } - Err(e) => { - panic!("{tc_name}: unexpected error {:?}", e) - } - } - } - - #[test] - fn native_coin_control_precompile_basic_functionality() { - let cases: &[NativeCoinControlTest] = &[ - // SSTORE (0→1, cold) = 22100, event = 1125, total = 23225 - NativeCoinControlTest { - name: "blocklist() succeeds and returns true", - caller: ALLOWED_CALLER_ADDRESS, - calldata: INativeCoinControl::blocklistCall { account: ADDRESS_B } - .abi_encode() - .into(), - gas_limit: 100_000, - expected_result: InstructionResult::Return, - expected_revert_str: None, - return_data: Some(true.abi_encode().into()), - gas_used: 23225, - pre_zero5_gas_used: Some(BLOCKLIST_GAS_COST), - zero6_gas_used: None, - zero6_only: false, - pre_zero6_only: false, - target_address: NATIVE_COIN_CONTROL_ADDRESS, - bytecode_address: NATIVE_COIN_CONTROL_ADDRESS, - }, - // Reverts before storage ops, 0 gas - NativeCoinControlTest { - name: "blocklist() unauthorized caller reverts", - caller: ADDRESS_A, - calldata: INativeCoinControl::blocklistCall { account: ADDRESS_B } - .abi_encode() - .into(), - gas_limit: 100_000, - expected_result: InstructionResult::Revert, - expected_revert_str: Some(ERR_CANNOT_BLOCKLIST), - return_data: None, - gas_used: 0, - pre_zero5_gas_used: Some(PRECOMPILE_SLOAD_GAS_COST), - zero6_gas_used: Some(PRECOMPILE_EARLY_REVERT_GAS_PENALTY), - zero6_only: false, - pre_zero6_only: false, - target_address: NATIVE_COIN_CONTROL_ADDRESS, - bytecode_address: NATIVE_COIN_CONTROL_ADDRESS, - }, - NativeCoinControlTest { - name: "blocklist() insufficient gas errors with OOG", - caller: ALLOWED_CALLER_ADDRESS, - calldata: INativeCoinControl::blocklistCall { account: ADDRESS_B } - .abi_encode() - .into(), - gas_limit: BLOCKLIST_GAS_COST - PRECOMPILE_SLOAD_GAS_COST - 1, // Not enough gas - expected_result: InstructionResult::PrecompileOOG, - expected_revert_str: None, - return_data: None, - gas_used: 0, - pre_zero5_gas_used: None, - zero6_gas_used: None, - zero6_only: false, - pre_zero6_only: false, - target_address: NATIVE_COIN_CONTROL_ADDRESS, - bytecode_address: NATIVE_COIN_CONTROL_ADDRESS, - }, - NativeCoinControlTest { - name: "blocklist() invalid params errors with Execution Reverted", - caller: ALLOWED_CALLER_ADDRESS, - calldata: INativeCoinControl::blocklistCall::SELECTOR.into(), - gas_limit: BLOCKLIST_GAS_COST, - expected_result: InstructionResult::Revert, - expected_revert_str: Some(ERR_EXECUTION_REVERTED), - return_data: None, - gas_used: PRECOMPILE_EARLY_REVERT_GAS_PENALTY, - pre_zero5_gas_used: None, - zero6_gas_used: None, - zero6_only: false, - pre_zero6_only: false, - target_address: NATIVE_COIN_CONTROL_ADDRESS, - bytecode_address: NATIVE_COIN_CONTROL_ADDRESS, - }, - // Reverts before storage ops, 0 gas - NativeCoinControlTest { - name: "blocklist() with target address != precompile address reverts", - caller: ALLOWED_CALLER_ADDRESS, - calldata: INativeCoinControl::blocklistCall { account: ADDRESS_B } - .abi_encode() - .into(), - gas_limit: BLOCKLIST_GAS_COST, - expected_result: InstructionResult::Revert, - expected_revert_str: Some(ERR_DELEGATE_CALL_NOT_ALLOWED), - return_data: None, - gas_used: 0, - pre_zero5_gas_used: Some(PRECOMPILE_SLOAD_GAS_COST), - zero6_gas_used: None, - zero6_only: false, - pre_zero6_only: false, - target_address: ADDRESS_B, - bytecode_address: NATIVE_COIN_CONTROL_ADDRESS, - }, - // Reverts before storage ops, 0 gas - NativeCoinControlTest { - name: "blocklist() with bytecode address != precompile address reverts", - caller: ALLOWED_CALLER_ADDRESS, - calldata: INativeCoinControl::blocklistCall { account: ADDRESS_B } - .abi_encode() - .into(), - gas_limit: BLOCKLIST_GAS_COST, - expected_result: InstructionResult::Revert, - expected_revert_str: Some(ERR_DELEGATE_CALL_NOT_ALLOWED), - return_data: None, - gas_used: 0, - pre_zero5_gas_used: Some(PRECOMPILE_SLOAD_GAS_COST), - zero6_gas_used: None, - zero6_only: false, - pre_zero6_only: false, - target_address: NATIVE_COIN_CONTROL_ADDRESS, - bytecode_address: ADDRESS_B, - }, - // SLOAD cold = 2100 (same for Zero5 and pre-Zero5) - NativeCoinControlTest { - name: "isBlocklisted() returns false for non-blocklisted address", - caller: ADDRESS_A, // Authorization not required for view function - calldata: INativeCoinControl::isBlocklistedCall { account: ADDRESS_B } - .abi_encode() - .into(), - gas_limit: 100_000, - expected_result: InstructionResult::Return, - expected_revert_str: None, - return_data: Some(false.abi_encode().into()), - gas_used: PRECOMPILE_SLOAD_GAS_COST, - pre_zero5_gas_used: None, - zero6_gas_used: None, - zero6_only: false, - pre_zero6_only: false, - target_address: NATIVE_COIN_CONTROL_ADDRESS, - bytecode_address: NATIVE_COIN_CONTROL_ADDRESS, - }, - NativeCoinControlTest { - name: "isBlocklisted() insufficient gas errors with OOG", - caller: ADDRESS_A, - calldata: INativeCoinControl::isBlocklistedCall { account: ADDRESS_B } - .abi_encode() - .into(), - gas_limit: IS_BLOCKLISTED_GAS_COST - 1, // Not enough gas - expected_result: InstructionResult::PrecompileOOG, - expected_revert_str: None, - return_data: None, - gas_used: 0, - pre_zero5_gas_used: None, - zero6_gas_used: None, - zero6_only: false, - pre_zero6_only: false, - target_address: NATIVE_COIN_CONTROL_ADDRESS, - bytecode_address: NATIVE_COIN_CONTROL_ADDRESS, - }, - NativeCoinControlTest { - name: "isBlocklisted() invalid params errors with Execution Reverted", - caller: ADDRESS_A, - calldata: INativeCoinControl::isBlocklistedCall::SELECTOR.into(), - gas_limit: IS_BLOCKLISTED_GAS_COST, - expected_result: InstructionResult::Revert, - expected_revert_str: Some(ERR_EXECUTION_REVERTED), - return_data: None, - gas_used: PRECOMPILE_EARLY_REVERT_GAS_PENALTY, - pre_zero5_gas_used: None, - zero6_gas_used: None, - zero6_only: false, - pre_zero6_only: false, - target_address: NATIVE_COIN_CONTROL_ADDRESS, - bytecode_address: NATIVE_COIN_CONTROL_ADDRESS, - }, - // SSTORE (0→0, cold) = 2200, event = 1125, total = 3325 - NativeCoinControlTest { - name: "unBlocklist() succeeds and returns true", - caller: ALLOWED_CALLER_ADDRESS, - calldata: INativeCoinControl::unBlocklistCall { account: ADDRESS_B } - .abi_encode() - .into(), - gas_limit: 100_000, - expected_result: InstructionResult::Return, - expected_revert_str: None, - return_data: Some(true.abi_encode().into()), - gas_used: 3325, - pre_zero5_gas_used: Some(UNBLOCKLIST_GAS_COST), - zero6_gas_used: None, - zero6_only: false, - pre_zero6_only: false, - target_address: NATIVE_COIN_CONTROL_ADDRESS, - bytecode_address: NATIVE_COIN_CONTROL_ADDRESS, - }, - // Reverts before storage ops, 0 gas - NativeCoinControlTest { - name: "unBlocklist() with target != precompile address reverts", - caller: ALLOWED_CALLER_ADDRESS, - calldata: INativeCoinControl::unBlocklistCall { account: ADDRESS_B } - .abi_encode() - .into(), - gas_limit: 100_000, - expected_result: InstructionResult::Revert, - expected_revert_str: Some(ERR_DELEGATE_CALL_NOT_ALLOWED), - return_data: None, - gas_used: 0, - pre_zero5_gas_used: Some(PRECOMPILE_SLOAD_GAS_COST), - zero6_gas_used: None, - zero6_only: false, - pre_zero6_only: false, - target_address: ADDRESS_B, - bytecode_address: NATIVE_COIN_CONTROL_ADDRESS, - }, - // Reverts before storage ops, 0 gas - NativeCoinControlTest { - name: "unBlocklist() with bytecode_address != precompile address reverts", - caller: ALLOWED_CALLER_ADDRESS, - calldata: INativeCoinControl::unBlocklistCall { account: ADDRESS_B } - .abi_encode() - .into(), - gas_limit: 100_000, - expected_result: InstructionResult::Revert, - expected_revert_str: Some(ERR_DELEGATE_CALL_NOT_ALLOWED), - return_data: None, - gas_used: 0, - pre_zero5_gas_used: Some(PRECOMPILE_SLOAD_GAS_COST), - zero6_gas_used: None, - zero6_only: false, - pre_zero6_only: false, - target_address: NATIVE_COIN_CONTROL_ADDRESS, - bytecode_address: ADDRESS_B, - }, - // Reverts before storage ops, 0 gas - NativeCoinControlTest { - name: "unBlocklist() unauthorized caller reverts", - caller: ADDRESS_A, - calldata: INativeCoinControl::unBlocklistCall { account: ADDRESS_B } - .abi_encode() - .into(), - gas_limit: 100_000, - expected_result: InstructionResult::Revert, - expected_revert_str: Some(ERR_CANNOT_UNBLOCKLIST), - return_data: None, - gas_used: 0, - pre_zero5_gas_used: Some(PRECOMPILE_SLOAD_GAS_COST), - zero6_gas_used: Some(PRECOMPILE_EARLY_REVERT_GAS_PENALTY), - zero6_only: false, - pre_zero6_only: false, - target_address: NATIVE_COIN_CONTROL_ADDRESS, - bytecode_address: NATIVE_COIN_CONTROL_ADDRESS, - }, - NativeCoinControlTest { - name: "unBlocklist() insufficient gas errors with OOG", - caller: ALLOWED_CALLER_ADDRESS, - calldata: INativeCoinControl::unBlocklistCall { account: ADDRESS_B } - .abi_encode() - .into(), - gas_limit: UNBLOCKLIST_GAS_COST - PRECOMPILE_SLOAD_GAS_COST - 1, // Not enough gas - expected_result: InstructionResult::PrecompileOOG, - expected_revert_str: None, - return_data: None, - gas_used: 0, - pre_zero5_gas_used: None, - zero6_gas_used: None, - zero6_only: false, - pre_zero6_only: false, - target_address: NATIVE_COIN_CONTROL_ADDRESS, - bytecode_address: NATIVE_COIN_CONTROL_ADDRESS, - }, - NativeCoinControlTest { - name: "unBlocklist() invalid params errors with Execution Reverted", - caller: ALLOWED_CALLER_ADDRESS, - calldata: INativeCoinControl::unBlocklistCall::SELECTOR.into(), - gas_limit: UNBLOCKLIST_GAS_COST, - expected_result: InstructionResult::Revert, - expected_revert_str: Some(ERR_EXECUTION_REVERTED), - return_data: None, - gas_used: PRECOMPILE_EARLY_REVERT_GAS_PENALTY, - pre_zero5_gas_used: None, - zero6_gas_used: None, - zero6_only: false, - pre_zero6_only: false, - target_address: NATIVE_COIN_CONTROL_ADDRESS, - bytecode_address: NATIVE_COIN_CONTROL_ADDRESS, - }, - // Under Zero6, the auth check runs before the success-path gas - // floor, so an unauthorized caller with gas >= 200 (the penalty) - // gets a penalized revert — not an OOG. - NativeCoinControlTest { - name: "blocklist() Zero6 low-gas unauthorized reverts with penalty", - caller: ADDRESS_A, - calldata: INativeCoinControl::blocklistCall { account: ADDRESS_B } - .abi_encode() - .into(), - gas_limit: 500, - expected_result: InstructionResult::Revert, - expected_revert_str: Some(ERR_CANNOT_BLOCKLIST), - return_data: None, - gas_used: PRECOMPILE_EARLY_REVERT_GAS_PENALTY, - pre_zero5_gas_used: None, - zero6_gas_used: None, - zero6_only: true, - pre_zero6_only: false, - target_address: NATIVE_COIN_CONTROL_ADDRESS, - bytecode_address: NATIVE_COIN_CONTROL_ADDRESS, - }, - // Pre-Zero6 (Zero3/Zero4/Zero5): gas floor runs before auth, so a - // low-gas unauthorized caller OOGs. Locks in the historical Zero5 - // behavior on devnet. - NativeCoinControlTest { - name: "blocklist() pre-Zero6 low-gas unauthorized OOGs", - caller: ADDRESS_A, - calldata: INativeCoinControl::blocklistCall { account: ADDRESS_B } - .abi_encode() - .into(), - gas_limit: 500, - expected_result: InstructionResult::PrecompileOOG, - expected_revert_str: None, - return_data: None, - gas_used: 0, - pre_zero5_gas_used: None, - zero6_gas_used: None, - zero6_only: false, - pre_zero6_only: true, - target_address: NATIVE_COIN_CONTROL_ADDRESS, - bytecode_address: NATIVE_COIN_CONTROL_ADDRESS, - }, - NativeCoinControlTest { - name: "unBlocklist() Zero6 low-gas unauthorized reverts with penalty", - caller: ADDRESS_A, - calldata: INativeCoinControl::unBlocklistCall { account: ADDRESS_B } - .abi_encode() - .into(), - gas_limit: 500, - expected_result: InstructionResult::Revert, - expected_revert_str: Some(ERR_CANNOT_UNBLOCKLIST), - return_data: None, - gas_used: PRECOMPILE_EARLY_REVERT_GAS_PENALTY, - pre_zero5_gas_used: None, - zero6_gas_used: None, - zero6_only: true, - pre_zero6_only: false, - target_address: NATIVE_COIN_CONTROL_ADDRESS, - bytecode_address: NATIVE_COIN_CONTROL_ADDRESS, - }, - NativeCoinControlTest { - name: "unBlocklist() pre-Zero6 low-gas unauthorized OOGs", - caller: ADDRESS_A, - calldata: INativeCoinControl::unBlocklistCall { account: ADDRESS_B } - .abi_encode() - .into(), - gas_limit: 500, - expected_result: InstructionResult::PrecompileOOG, - expected_revert_str: None, - return_data: None, - gas_used: 0, - pre_zero5_gas_used: None, - zero6_gas_used: None, - zero6_only: false, - pre_zero6_only: true, - target_address: NATIVE_COIN_CONTROL_ADDRESS, - bytecode_address: NATIVE_COIN_CONTROL_ADDRESS, - }, - ]; - - for tc in cases { - for hardfork_flags in ArcHardforkFlags::all_combinations() { - // ZeroX hardforks are cumulative; Zero6 implies Zero5. - if hardfork_flags.is_active(ArcHardfork::Zero6) - && !hardfork_flags.is_active(ArcHardfork::Zero5) - { - continue; - } - - if tc.zero6_only && !hardfork_flags.is_active(ArcHardfork::Zero6) { - continue; - } - if tc.pre_zero6_only && hardfork_flags.is_active(ArcHardfork::Zero6) { - continue; - } - - let tc_name = - tc.name.to_string() + &format!(" (hardfork_flags: {:?})", hardfork_flags); - - // Sanity check that we're not configuring test cases incorrectly - match tc.expected_result { - InstructionResult::Revert | InstructionResult::Return => {} - _ => { - assert!( - tc.return_data.is_none(), - "{tc_name}: expected no return data", - ); - } - } - - let mut ctx = mock_context(hardfork_flags); - - // Prepare inputs - let inputs = CallInputs { - scheme: CallScheme::Call, - target_address: tc.target_address, - bytecode_address: tc.bytecode_address, - known_bytecode: None, - caller: tc.caller, - value: CallValue::Transfer(U256::ZERO), - input: CallInput::Bytes(tc.calldata.clone()), - gas_limit: tc.gas_limit, - is_static: false, - return_memory_offset: 0..0, - }; - - let precompile_res = call_native_coin_control(&mut ctx, &inputs, hardfork_flags); - assert_precompile_result(precompile_res, tc, hardfork_flags, &tc_name); - } - } - } - - #[test] - fn blocklist_workflow_zero3() { - test_blocklist_workflow(ArcHardforkFlags::default()); - } - #[test] - fn blocklist_workflow_zero4() { - test_blocklist_workflow(ArcHardforkFlags::with(&[ArcHardfork::Zero4])); - } - #[test] - fn blocklist_workflow_zero5() { - test_blocklist_workflow(ArcHardforkFlags::with(&[ArcHardfork::Zero5])); - } - fn test_blocklist_workflow(hardfork_flags: ArcHardforkFlags) { - let mut ctx = mock_context(hardfork_flags); - - // Test 1: Initially address should not be blocklisted - let is_blocklisted_input = CallInputs { - scheme: CallScheme::Call, - target_address: NATIVE_COIN_CONTROL_ADDRESS, - bytecode_address: NATIVE_COIN_CONTROL_ADDRESS, - known_bytecode: None, - caller: ADDRESS_A, - value: CallValue::Transfer(U256::ZERO), - input: CallInput::Bytes( - INativeCoinControl::isBlocklistedCall { account: ADDRESS_B } - .abi_encode() - .into(), - ), - gas_limit: 100_000, - is_static: false, - return_memory_offset: 0..0, - }; - - let result = call_native_coin_control(&mut ctx, &is_blocklisted_input, hardfork_flags) - .unwrap() - .unwrap(); - assert_eq!(result.result, InstructionResult::Return); - assert_eq!(result.output, Bytes::from(false.abi_encode())); - - // Test 2: Blocklist the address - let blocklist_input = CallInputs { - scheme: CallScheme::Call, - target_address: NATIVE_COIN_CONTROL_ADDRESS, - bytecode_address: NATIVE_COIN_CONTROL_ADDRESS, - known_bytecode: None, - caller: ALLOWED_CALLER_ADDRESS, - value: CallValue::Transfer(U256::ZERO), - input: CallInput::Bytes( - INativeCoinControl::blocklistCall { account: ADDRESS_B } - .abi_encode() - .into(), - ), - gas_limit: 100_000, - is_static: false, - return_memory_offset: 0..0, - }; - - let result = call_native_coin_control(&mut ctx, &blocklist_input, hardfork_flags) - .unwrap() - .unwrap(); - assert_eq!(result.result, InstructionResult::Return); - assert_eq!(result.output, Bytes::from(true.abi_encode())); - - // Check event emission for blocklist - let journal_mut = ctx.journal_mut(); - let logs = journal_mut.logs(); - let expected_log = Blocklisted { account: ADDRESS_B }.encode_log_data(); - assert_eq!(logs.len(), 1, "Expected one log event for blocklist"); - let log = &logs[0]; - assert_eq!( - log.address, NATIVE_COIN_CONTROL_ADDRESS, - "Log address mismatch" - ); - assert_eq!( - log.data, expected_log, - "Log data mismatch for blocklist event" - ); - - // Test 3: Verify address is now blocklisted - let result = call_native_coin_control(&mut ctx, &is_blocklisted_input, hardfork_flags) - .unwrap() - .unwrap(); - assert_eq!(result.result, InstructionResult::Return); - assert_eq!(result.output, Bytes::from(true.abi_encode())); - - // Test 4: Unblocklist the address - let unblocklist_input = CallInputs { - scheme: CallScheme::Call, - target_address: NATIVE_COIN_CONTROL_ADDRESS, - bytecode_address: NATIVE_COIN_CONTROL_ADDRESS, - known_bytecode: None, - caller: ALLOWED_CALLER_ADDRESS, - value: CallValue::Transfer(U256::ZERO), - input: CallInput::Bytes( - INativeCoinControl::unBlocklistCall { account: ADDRESS_B } - .abi_encode() - .into(), - ), - gas_limit: 100_000, - is_static: false, - return_memory_offset: 0..0, - }; - - let result = call_native_coin_control(&mut ctx, &unblocklist_input, hardfork_flags) - .unwrap() - .unwrap(); - assert_eq!(result.result, InstructionResult::Return); - assert_eq!(result.output, Bytes::from(true.abi_encode())); - - // Check event emission for unblocklist - let journal_mut = ctx.journal_mut(); - let logs = journal_mut.logs(); - assert_eq!(logs.len(), 2, "Expected two log events after unblocklist"); - - // Verify the second event is UnBlocklisted - let expected_unblocklist_log = UnBlocklisted { account: ADDRESS_B }.encode_log_data(); - let unblocklist_log = &logs[1]; - assert_eq!( - unblocklist_log.address, NATIVE_COIN_CONTROL_ADDRESS, - "Log address mismatch for unblocklist" - ); - assert_eq!( - unblocklist_log.data, expected_unblocklist_log, - "Log data mismatch for unblocklist event" - ); - - // Test 5: Verify address is no longer blocklisted - let result = call_native_coin_control(&mut ctx, &is_blocklisted_input, hardfork_flags) - .unwrap() - .unwrap(); - assert_eq!(result.result, InstructionResult::Return); - assert_eq!(result.output, Bytes::from(false.abi_encode())); - } - - // Helper to convert bytes to a revert error string - fn bytes_to_revert_message(input: &[u8]) -> Option { - use crate::helpers::REVERT_SELECTOR; - use alloy_sol_types::SolValue; - - // Expect at least 4 bytes for the selector. - if input.len() < 4 { - return None; - } - // Check the selector matches the standard Error(string) selector. - if input[0..4] != REVERT_SELECTOR { - return None; - } - - String::abi_decode(&input[4..]).ok() - } - - #[test] - fn test_static_call_reverts_state_modifying_functions() { - use crate::helpers::ERR_STATE_CHANGE_DURING_STATIC_CALL; - - let state_modifying_calldatas: &[(&str, Bytes)] = &[ - ( - "blocklist", - INativeCoinControl::blocklistCall { account: ADDRESS_B } - .abi_encode() - .into(), - ), - ( - "unBlocklist", - INativeCoinControl::unBlocklistCall { account: ADDRESS_B } - .abi_encode() - .into(), - ), - ]; - - for hardfork_flags in ArcHardforkFlags::all_combinations() { - // State-modifying functions must revert under static call - for (fn_name, calldata) in state_modifying_calldatas { - let mut ctx = mock_context(hardfork_flags); - let inputs = CallInputs { - scheme: CallScheme::Call, - target_address: NATIVE_COIN_CONTROL_ADDRESS, - bytecode_address: NATIVE_COIN_CONTROL_ADDRESS, - known_bytecode: None, - caller: ALLOWED_CALLER_ADDRESS, - value: CallValue::Transfer(U256::ZERO), - input: CallInput::Bytes(calldata.clone()), - gas_limit: 100_000, - is_static: true, - return_memory_offset: 0..0, - }; - - let result = call_native_coin_control(&mut ctx, &inputs, hardfork_flags) - .expect("call should not error") - .expect("result should be Some"); - - assert_eq!( - result.result, - InstructionResult::Revert, - "{fn_name} (hardfork_flags: {hardfork_flags:?}): expected Revert", - ); - let revert_reason = bytes_to_revert_message(result.output.as_ref()); - assert_eq!( - revert_reason.as_deref(), - Some(ERR_STATE_CHANGE_DURING_STATIC_CALL), - "{fn_name} (hardfork_flags: {hardfork_flags:?}): wrong revert reason", - ); - } - - // Read-only function (isBlocklisted) must succeed under static call - { - let mut ctx = mock_context(hardfork_flags); - let inputs = CallInputs { - scheme: CallScheme::Call, - target_address: NATIVE_COIN_CONTROL_ADDRESS, - bytecode_address: NATIVE_COIN_CONTROL_ADDRESS, - known_bytecode: None, - caller: ADDRESS_A, - value: CallValue::Transfer(U256::ZERO), - input: CallInput::Bytes( - INativeCoinControl::isBlocklistedCall { account: ADDRESS_B } - .abi_encode() - .into(), - ), - gas_limit: 100_000, - is_static: true, - return_memory_offset: 0..0, - }; - - let result = call_native_coin_control(&mut ctx, &inputs, hardfork_flags) - .expect("call should not error") - .expect("result should be Some"); - - assert_eq!( - result.result, - InstructionResult::Return, - "isBlocklisted (hardfork_flags: {hardfork_flags:?}): expected Return under static call", - ); - } - } - } -} diff --git a/crates/precompiles/src/pq.rs b/crates/precompiles/src/pq.rs deleted file mode 100644 index 74200a5..0000000 --- a/crates/precompiles/src/pq.rs +++ /dev/null @@ -1,360 +0,0 @@ -// Copyright 2026 Circle Internet Group, Inc. All rights reserved. -// -// SPDX-License-Identifier: Apache-2.0 -// -// Licensed under the Apache License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. -// You may obtain a copy of the License at -// -// http://www.apache.org/licenses/LICENSE-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// See the License for the specific language governing permissions and -// limitations under the License. -use crate::helpers::{ - record_cost_or_out_of_gas, PrecompileErrorOrRevert, ERR_EXECUTION_REVERTED, - PRECOMPILE_EARLY_REVERT_GAS_PENALTY, -}; -use crate::precompile; -use alloy_primitives::{address, Address}; -use alloy_sol_types::{sol, SolCall, SolValue}; -use reth_ethereum::evm::revm::precompile::PrecompileOutput; -use revm_interpreter::gas::KECCAK256WORD; -use revm_interpreter::Gas; -use slh_dsa::{signature::Verifier, Sha2_128s, Signature, VerifyingKey as SlhDsaVerifyingKey}; - -pub const PQ_ADDRESS: Address = address!("1800000000000000000000000000000000000004"); - -/// Base gas for SLH-DSA-SHA2-128s verification. -/// -/// Conservative relative to the SHA-256 precompile's per-word work anchor. See -/// `crates/precompiles/benches/pq.rs` for the benchmark context comparing this -/// price against SLH-DSA-SHA2-128s verification and 64-byte SHA-256 / KECCAK256 -/// work. -const VERIFY_BASE_GAS: u64 = 230_000; - -/// Dynamic gas cost per 32-byte word of message input. -/// -/// SLH-DSA-SHA2-128s hashes the message once via `H_msg` (SHA-256 + MGF1). -/// This is comparable to KECCAK256, so we use the same per-word rate. -const GAS_PER_MSG_WORD: u64 = KECCAK256WORD; - -sol! { - /// Experimental PQ Signature Verifier precompile interface. - interface IPQ { - /// Verify an SLH-DSA-SHA2-128s signature. - /// - /// Since PQ signatures are still very new, we recommend not to solely - /// rely on them for authentication, but pair them with classical - /// signatures. - /// - /// Gas cost: 230,000 base + 6 per 32-byte word of message (same as KECCAK256) - function verifySlhDsaSha2128s(bytes calldata vk, bytes calldata message, bytes calldata sig) external returns (bool isValid); - } -} - -precompile!(run_pq, precompile_input, hardfork_flags; { - IPQ::verifySlhDsaSha2128sCall => |input| { - (|| -> Result { - let _ = hardfork_flags; - let mut gas_counter = Gas::new(precompile_input.gas); - - let args = IPQ::verifySlhDsaSha2128sCall::abi_decode_raw_validate(input).map_err(|_| { - PrecompileErrorOrRevert::new_reverted_with_penalty( - gas_counter, - PRECOMPILE_EARLY_REVERT_GAS_PENALTY, - ERR_EXECUTION_REVERTED, - ) - })?; - - // Charge base gas, then per-word message gas, then validate inputs - record_cost_or_out_of_gas(&mut gas_counter, VERIFY_BASE_GAS)?; - - // GAS_PER_MSG_WORD (6) < 32, so the product cannot exceed u64::MAX - #[allow(clippy::arithmetic_side_effects)] - let msg_word_gas = (args.message.len() as u64).div_ceil(32) * GAS_PER_MSG_WORD; - record_cost_or_out_of_gas(&mut gas_counter, msg_word_gas)?; - - // SLH-DSA-SHA2-128s constants from FIPS 205 - const VK_LEN: usize = 32; - const SIG_LEN: usize = 7856; - - if args.vk.len() != VK_LEN { - return Err(PrecompileErrorOrRevert::new_reverted( - gas_counter, - "Invalid verifying key length", - )); - } - - if args.sig.len() != SIG_LEN { - return Err(PrecompileErrorOrRevert::new_reverted( - gas_counter, - "Invalid signature length", - )); - } - - let verifying_key = SlhDsaVerifyingKey::::try_from(args.vk.as_ref()) - .map_err(|_| PrecompileErrorOrRevert::new_reverted(gas_counter, "Failed to parse verifying key"))?; - - let signature = Signature::::try_from(args.sig.as_ref()) - .map_err(|_| PrecompileErrorOrRevert::new_reverted(gas_counter, "Failed to parse signature"))?; - - let is_valid = verifying_key.verify(args.message.as_ref(), &signature).is_ok(); - - Ok(PrecompileOutput::new(gas_counter.used(), is_valid.abi_encode().into())) - })() - }, -}); - -#[cfg(test)] -mod tests { - use super::*; - use alloy_evm::EvmFactory; - use alloy_primitives::{address, hex, Address, TxKind}; - use alloy_sol_types::SolCall; - use arc_evm::{ArcEvm, ArcEvmFactory}; - use arc_execution_config::chainspec::ArcChainSpec; - use reth_evm::{eth::EthEvmContext, precompiles::PrecompilesMap, Evm}; - use revm::{ - context::{ - result::{EVMError, ExecResultAndState, ExecutionResult}, - TxEnv, - }, - context_interface::result::HaltReason, - database::{CacheDB, InMemoryDB}, - handler::instructions::EthInstructions, - inspector::NoOpInspector, - interpreter::interpreter::EthInterpreter, - }; - use std::sync::Arc; - - const TEST_USER_ADDRESS: Address = address!("0x1234567890123456789012345678901234567890"); - - type TestEvm = ArcEvm< - EthEvmContext>, - NoOpInspector, - EthInstructions>>, - PrecompilesMap, - >; - - /// Helper to create an EVM with Zero6 hardfork activated - fn create_test_evm() -> TestEvm { - let db = CacheDB::new(InMemoryDB::default()); - let mut inner_chain_spec = reth_chainspec::ChainSpecBuilder::mainnet() - .berlin_activated() - .london_activated() - .paris_activated() - .shanghai_activated() - .build(); - - // Activate Zero6 hardfork at block 0 (PQ precompile is gated on Zero6) - inner_chain_spec.hardforks.insert( - arc_execution_config::hardforks::ArcHardfork::Zero6, - reth_ethereum_forks::ForkCondition::Block(0), - ); - - let chain_spec = Arc::new(ArcChainSpec::new(inner_chain_spec)); - let factory = ArcEvmFactory::new(chain_spec.clone()); - factory.create_evm(db, reth_evm::EvmEnv::default()) - } - - /// Helper to call SLH-DSA-SHA2-128s verifier with given inputs. - /// Param order matches the ABI: (vk, message, sig). - fn transact_slh_dsa_verifier( - evm: &mut TestEvm, - vk: Vec, - message: Vec, - sig: Vec, - ) -> Result>, EVMError> - { - evm.transact_raw(TxEnv { - caller: TEST_USER_ADDRESS, - kind: TxKind::Call(PQ_ADDRESS), - data: IPQ::verifySlhDsaSha2128sCall { - vk: vk.into(), - message: message.into(), - sig: sig.into(), - } - .abi_encode() - .into(), - ..Default::default() - }) - } - - /// Assert transaction succeeded and extract boolean result - fn assert_success_and_decode( - result: &Result< - ExecResultAndState>, - EVMError, - >, - ) -> bool { - assert!(result.is_ok(), "Transaction should succeed"); - let exec_result = result.as_ref().unwrap(); - - if !exec_result.result.is_success() { - match &exec_result.result { - ExecutionResult::Revert { output, .. } => { - eprintln!("Reverted with output: {}", hex::encode(output)); - } - ExecutionResult::Halt { reason, .. } => { - eprintln!("Halted with reason: {:?}", reason); - } - _ => {} - } - } - - assert!( - exec_result.result.is_success(), - "Transaction should be successful" - ); - - IPQ::verifySlhDsaSha2128sCall::abi_decode_returns(exec_result.result.output().unwrap()) - .expect("Should decode return value") - } - - /// Assert transaction completed but failed (revert/halt) - fn assert_failure( - result: &Result< - ExecResultAndState>, - EVMError, - >, - reason: &str, - ) { - assert!(result.is_ok(), "Transaction should complete (not panic)"); - let exec_result = result.as_ref().unwrap(); - assert!( - !exec_result.result.is_success(), - "Transaction should fail: {}", - reason - ); - } - - #[test] - fn test_verify_valid_slh_dsa_sha2_128s_signature() { - use slh_dsa::signature::{Keypair, Signer}; - use slh_dsa::SigningKey; - - let mut evm = create_test_evm(); - - // Generate keypair from seeds for deterministic testing - // SLH-DSA requires 3 seeds: sk_seed, sk_prf, and pk_seed (each 16 bytes for SHA2-128s) - let sk_seed = [1u8; 16]; - let sk_prf = [2u8; 16]; - let pk_seed = [3u8; 16]; - let signing_key = SigningKey::::slh_keygen_internal(&sk_seed, &sk_prf, &pk_seed); - let verifying_key = signing_key.verifying_key(); - - // Sign a message (note: SLH-DSA sign() is deterministic) - let msg = b"Hello quantum-resistant world"; - let signature = signing_key.sign(msg); - - let result = transact_slh_dsa_verifier( - &mut evm, - verifying_key.to_bytes().to_vec(), - msg.to_vec(), - signature.to_bytes().to_vec(), - ); - - let is_valid = assert_success_and_decode(&result); - assert!(is_valid, "Valid SLH-DSA signature should verify"); - } - - #[test] - fn test_slh_dsa_sha2_128s_gas_cost() { - use slh_dsa::signature::{Keypair, Signer}; - use slh_dsa::SigningKey; - - let mut evm = create_test_evm(); - - let sk_seed = [1u8; 16]; - let sk_prf = [2u8; 16]; - let pk_seed = [3u8; 16]; - let signing_key = SigningKey::::slh_keygen_internal(&sk_seed, &sk_prf, &pk_seed); - let verifying_key = signing_key.verifying_key(); - - // Test with 32-byte message - let msg = [0xBB; 32]; - let signature = signing_key.sign(&msg); - - let result = transact_slh_dsa_verifier( - &mut evm, - verifying_key.to_bytes().to_vec(), - msg.to_vec(), - signature.to_bytes().to_vec(), - ); - - assert!(result.is_ok(), "Transaction should succeed"); - let exec_result = result.as_ref().unwrap(); - - // Our precompile cost: 230,000 (base) + 1 word * 6 (message) = 230,006 - // Plus EVM calldata cost for large signature (7856 bytes) - let actual_gas = exec_result.result.gas_used(); - - // SLH-DSA has largest signatures, expect ~370-390K total gas - assert!( - actual_gas > 350_000 && actual_gas < 400_000, - "Gas cost should be ~380K (precompile + large calldata): got {}", - actual_gas - ); - - println!( - "SLH-DSA-SHA2-128s total gas (32-byte msg): {} gas", - actual_gas - ); - } - - #[test] - fn test_verify_invalid_slh_dsa_sha2_128s_signature() { - use slh_dsa::signature::{Keypair, Signer}; - use slh_dsa::SigningKey; - - let mut evm = create_test_evm(); - - // Generate keypair from seeds for deterministic testing - let sk_seed = [1u8; 16]; - let sk_prf = [2u8; 16]; - let pk_seed = [3u8; 16]; - let signing_key = SigningKey::::slh_keygen_internal(&sk_seed, &sk_prf, &pk_seed); - let verifying_key = signing_key.verifying_key(); - - // Sign one message, verify with different message - let msg1 = b"Hello quantum-resistant world"; - let msg2 = b"Hello quantum-resistant world!"; - let signature = signing_key.sign(msg1); - - let result = transact_slh_dsa_verifier( - &mut evm, - verifying_key.to_bytes().to_vec(), - msg2.to_vec(), - signature.to_bytes().to_vec(), - ); - - let is_valid = assert_success_and_decode(&result); - assert!(!is_valid, "Invalid SLH-DSA signature should not verify"); - } - - #[test] - fn test_slh_dsa_malformed_input_handling() { - let mut evm = create_test_evm(); - - // Test cases: (vk_len, sig_len, description) - let test_cases = [ - (10, 7856, "verifying key too short"), - (32, 100, "signature too short"), - (100, 7856, "verifying key too long"), - (32, 10000, "signature too long"), - ]; - - for (vk_len, sig_len, desc) in test_cases { - let result = transact_slh_dsa_verifier( - &mut evm, - vec![0u8; vk_len], - b"test".to_vec(), - vec![0u8; sig_len], - ); - assert_failure(&result, desc); - } - } -} diff --git a/crates/precompiles/src/pq_test_vectors.rs b/crates/precompiles/src/pq_test_vectors.rs deleted file mode 100644 index 5f509fc..0000000 --- a/crates/precompiles/src/pq_test_vectors.rs +++ /dev/null @@ -1,80 +0,0 @@ -// Copyright 2026 Circle Internet Group, Inc. All rights reserved. -// -// SPDX-License-Identifier: Apache-2.0 -// -// Licensed under the Apache License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. -// You may obtain a copy of the License at -// -// http://www.apache.org/licenses/LICENSE-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// See the License for the specific language governing permissions and -// limitations under the License. - -//! Deterministic SLH-DSA-SHA2-128s test vectors for PQ precompile tests. -//! -//! Generates keypairs and signatures from fixed seeds so that the output is -//! identical across runs. Used by both unit tests (`crates/precompiles`) and -//! e2e tests (`crates/execution-e2e`). -//! -//! The same seeds power the `generate_pq_test_vectors` binary — if that -//! binary's output changes, this module's output changes in lockstep. - -use slh_dsa::{ - signature::{Keypair as SlhDsaKeypair, Signer as SlhDsaSigner}, - Sha2_128s, SigningKey as SlhDsaSigningKey, VerifyingKey as SlhDsaVerifyingKey, -}; - -/// Fixed seeds matching the `generate_pq_test_vectors` binary. -const SK_SEED: [u8; 16] = [1u8; 16]; -const SK_PRF: [u8; 16] = [2u8; 16]; -const PK_SEED: [u8; 16] = [3u8; 16]; - -/// Messages used across test vectors. -pub const MSG_HELLO_WORLD: &[u8] = b"Hello, World!"; -pub const MSG_EMPTY: &[u8] = b""; -pub const MSG_GOODBYE_WORLD: &[u8] = b"Goodbye, World!"; - -/// Pre-computed test vectors for SLH-DSA-SHA2-128s. -pub struct PqTestVectors { - pub verifying_key: Vec, - pub sig_hello_world: Vec, - pub sig_empty_message: Vec, -} - -impl PqTestVectors { - /// Generate all test vectors from deterministic seeds. - pub fn generate() -> Self { - let signing_key = - SlhDsaSigningKey::::slh_keygen_internal(&SK_SEED, &SK_PRF, &PK_SEED); - let vk: SlhDsaVerifyingKey = signing_key.verifying_key().clone(); - - let sig_hello = signing_key.sign(MSG_HELLO_WORLD); - let sig_empty = signing_key.sign(MSG_EMPTY); - - Self { - verifying_key: vk.to_bytes().to_vec(), - sig_hello_world: sig_hello.to_bytes().to_vec(), - sig_empty_message: sig_empty.to_bytes().to_vec(), - } - } - - /// Verifying key with last byte flipped — valid 32-byte encoding, wrong key. - pub fn wrong_vk(&self) -> Vec { - let mut vk = self.verifying_key.clone(); - let last = vk.last_mut().expect("vk is non-empty"); - *last ^= 0x01; - vk - } -} - -/// Cache vectors per-process so repeated calls (e.g. parameterised rstest) don't -/// re-derive ~8 KB signatures each time. -pub fn cached_vectors() -> &'static PqTestVectors { - use std::sync::LazyLock; - static VECTORS: LazyLock = LazyLock::new(PqTestVectors::generate); - &VECTORS -} diff --git a/crates/precompiles/src/precompile_provider.rs b/crates/precompiles/src/precompile_provider.rs deleted file mode 100644 index 8bcb38d..0000000 --- a/crates/precompiles/src/precompile_provider.rs +++ /dev/null @@ -1,166 +0,0 @@ -// Copyright 2025 Circle Internet Group, Inc. All rights reserved. -// -// SPDX-License-Identifier: Apache-2.0 -// -// Licensed under the Apache License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. -// You may obtain a copy of the License at -// -// http://www.apache.org/licenses/LICENSE-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// See the License for the specific language governing permissions and -// limitations under the License. - -extern crate alloc; - -use crate::native_coin_authority::{run_native_coin_authority, NATIVE_COIN_AUTHORITY_ADDRESS}; -use crate::native_coin_control::{run_native_coin_control, NATIVE_COIN_CONTROL_ADDRESS}; -use crate::pq::{run_pq, PQ_ADDRESS}; -use crate::system_accounting::{run_system_accounting, SYSTEM_ACCOUNTING_ADDRESS}; -use alloy_evm::precompiles::PrecompilesMap; -use alloy_primitives::Address; -use arc_execution_config::hardforks::{ArcHardfork, ArcHardforkFlags}; -use reth_ethereum::evm::revm::precompile::PrecompileSpecId; -use reth_ethereum::evm::revm::precompile::Precompiles; -use reth_evm::precompiles::DynPrecompile; -use revm::precompile::PrecompileId; -use revm::primitives::hardfork::SpecId; -use revm_primitives::address; - -/// Custom precompile provider that extends Ethereum's standard precompiles -/// with Arc functionality. -/// -/// This provider supports: -/// - **Stateful precompiles**: Registered dynamically via `set_precompile_lookup` -#[derive(Debug)] -pub struct ArcPrecompileProvider; - -impl ArcPrecompileProvider { - /// Creates a precompiles map based on the spec and hardfork flags - pub fn create_precompiles_map( - spec: SpecId, - hardfork_flags: ArcHardforkFlags, - ) -> PrecompilesMap { - let base_precompiles = Precompiles::new(PrecompileSpecId::from_spec_id(spec)); - let mut precompile_map = PrecompilesMap::from_static(base_precompiles); - precompile_map.ensure_dynamic_precompiles(); - precompile_map.set_precompile_lookup(move |address: &Address| match *address { - NATIVE_COIN_AUTHORITY_ADDRESS => Some(DynPrecompile::new_stateful( - PrecompileId::Custom("NATIVE_COIN_AUTHORITY".into()), - move |input| run_native_coin_authority(input, hardfork_flags), - )), - NATIVE_COIN_CONTROL_ADDRESS => Some(DynPrecompile::new_stateful( - PrecompileId::Custom("NATIVE_COIN_CONTROL".into()), - move |input| run_native_coin_control(input, hardfork_flags), - )), - SYSTEM_ACCOUNTING_ADDRESS => Some(DynPrecompile::new_stateful( - PrecompileId::Custom("SYSTEM_ACCOUNTING".into()), - move |input| run_system_accounting(input, hardfork_flags), - )), - PQ_ADDRESS => { - // Only register PQ precompile if Zero6 hardfork is active - if !hardfork_flags.is_active(ArcHardfork::Zero6) { - return None; - } - Some(DynPrecompile::new_stateful( - PrecompileId::Custom("PQ".into()), - move |input| run_pq(input, hardfork_flags), - )) - } - _ => handle_unknown_precompile(address), - }); - precompile_map - } - - /// The P256 (secp256r1) precompile address as defined in EIP-7212. - /// This precompile is available starting from the Osaka hardfork. - pub const P256_PRECOMPILE_ADDRESS: Address = - address!("0x0000000000000000000000000000000000000100"); -} - -fn handle_unknown_precompile(_address: &Address) -> Option { - #[cfg(any(test, feature = "integration"))] - return match_e2e_precompile(_address); - #[cfg(not(any(test, feature = "integration")))] - None -} - -#[cfg(any(test, feature = "integration"))] -pub const PANIC_PRECOMPILE_ADDRESS: Address = - address!("0xdead000000000000000000000000000000000001"); -#[cfg(any(test, feature = "integration"))] -fn match_e2e_precompile(address: &Address) -> Option { - match *address { - PANIC_PRECOMPILE_ADDRESS => Some(DynPrecompile::new_stateful( - PrecompileId::Custom("PANICKING_TEST".into()), - |_input| panic!("test panicking precompile"), - )), - _ => None, - } -} - -#[cfg(test)] -mod tests { - use super::*; - - #[test] - fn test_p256_precompile_available_with_osaka() { - let precompiles = ArcPrecompileProvider::create_precompiles_map( - SpecId::OSAKA, - ArcHardforkFlags::default(), - ); - - // Verify P256 precompile exists at address 0x100 with Osaka - assert!( - precompiles - .get(&ArcPrecompileProvider::P256_PRECOMPILE_ADDRESS) - .is_some(), - "P256 precompile should be available with Osaka hardfork" - ); - } - - #[test] - fn test_pq_precompile_available_with_zero6() { - let precompiles = ArcPrecompileProvider::create_precompiles_map( - SpecId::PRAGUE, - ArcHardforkFlags::with(&[ArcHardfork::Zero6]), - ); - - assert!( - precompiles.get(&PQ_ADDRESS).is_some(), - "PQ precompile should be available when Zero6 is active" - ); - } - - #[test] - fn test_pq_precompile_not_available_without_zero6() { - let precompiles = ArcPrecompileProvider::create_precompiles_map( - SpecId::PRAGUE, - ArcHardforkFlags::default(), - ); - - assert!( - precompiles.get(&PQ_ADDRESS).is_none(), - "PQ precompile should NOT be available without Zero6" - ); - } - - #[test] - fn test_p256_precompile_not_available_with_prague() { - let precompiles = ArcPrecompileProvider::create_precompiles_map( - SpecId::PRAGUE, - ArcHardforkFlags::default(), - ); - - // P256 precompile should NOT be available with Prague - assert!( - precompiles - .get(&ArcPrecompileProvider::P256_PRECOMPILE_ADDRESS) - .is_none(), - "P256 precompile should NOT be available with Prague hardfork" - ); - } -} diff --git a/crates/precompiles/src/subcall.rs b/crates/precompiles/src/subcall.rs deleted file mode 100644 index 65f989e..0000000 --- a/crates/precompiles/src/subcall.rs +++ /dev/null @@ -1,148 +0,0 @@ -// Copyright 2026 Circle Internet Group, Inc. All rights reserved. -// -// SPDX-License-Identifier: Apache-2.0 -// -// Licensed under the Apache License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. -// You may obtain a copy of the License at -// -// http://www.apache.org/licenses/LICENSE-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// See the License for the specific language governing permissions and -// limitations under the License. - -//! Subcall precompile trait and types. -//! -//! Defines the two-phase execution interface for precompiles that need to spawn -//! child EVM call frames. Implementations live alongside this module (e.g., -//! [`crate::call_from::CallFromPrecompile`]). The registry and continuation storage -//! that drives the execution loop live in `arc-evm`. - -use alloy_primitives::Bytes; -use revm::handler::FrameResult; -use revm::interpreter::interpreter_action::CallInputs; -use std::any::Any; - -/// Trait for precompiles that spawn child EVM call frames. -/// -/// Implementors define a two-phase execution model: -/// - [`init_subcall`](SubcallPrecompile::init_subcall): Decodes the precompile input and returns a -/// child call request plus any continuation state needed for completion. -/// - [`complete_subcall`](SubcallPrecompile::complete_subcall): Receives the child call result and -/// continuation state, producing the final precompile output. -/// -/// # Constraint: child target must not be a subcall precompile -/// -/// The child frame returned by `init_subcall` is dispatched via revm's standard -/// `init_with_context`, which does **not** consult the subcall registry. If -/// `child_inputs.target_address` is another subcall precompile address, the call will hit the -/// stub bytecode at that genesis address (typically `0x01` / `ADD`) and silently revert, rather -/// than triggering the target's two-phase execution. -/// -/// Indirect calls work fine: if the child targets a regular contract that then CALLs a subcall -/// precompile, the CALL opcode triggers a new `frame_init` cycle through `ArcEvm`, which -/// consults the subcall registry as expected. -/// -/// # Checkpoint and revert semantics -/// -/// The subcall framework takes a journal checkpoint **before** dispatching the child frame. -/// If `complete_subcall` returns `success: false` or `Err` when the child succeeded, the -/// framework reverts the child's committed state changes using that checkpoint. -/// -/// Returning `success: true` when the child failed is fine (e.g., `CallFromPrecompile` -/// always succeeds and encodes the child's outcome in its output bytes). The child's -/// checkpoint was already reverted, so no state leaks. -pub trait SubcallPrecompile: Send + Sync { - /// Decode precompile input and produce a child call request. - /// - /// Called during `frame_init` when the EVM encounters a call to this precompile's address. - /// Returns either a subcall request (with continuation data for `complete_subcall`) or an - /// error. - /// - /// # Sender constraint - /// - /// The framework enforces that `child_inputs.caller` must equal `call_inputs.caller` - /// (the address that called this precompile) or `tx.origin` (the signing EOA). - /// Setting `child_inputs.caller` to any other address will cause the framework to - /// revert with "sender spoofing requires tx.origin as sender". - fn init_subcall(&self, inputs: &CallInputs) -> Result; - - /// Finalize the precompile result after the child call completes. - /// - /// Called during `frame_return_result` when the child frame finishes execution. - /// Receives the continuation data from `init_subcall` and the child's frame result. - /// - /// # Note on checkpoint semantics - /// - /// Returning `success: false` or `Err` when the child succeeded will cause the - /// framework to revert the child's committed state changes. See the trait-level docs. - fn complete_subcall( - &self, - continuation_data: SubcallContinuationData, - child_result: &FrameResult, - ) -> Result; - - /// Construct the child [`CallInputs`] for tracing purposes. - /// - /// Called by `ArcEvm::inspect_frame_init` *before* `init_subcall` to obtain the - /// child call's identity (caller, target, gas limit, calldata). The returned - /// inputs are passed to the inspector's `frame_start`, making the precompile - /// transparent in `debug_traceTransaction` output — the trace shows the logical - /// child call instead of the precompile address. - /// - /// The returned `CallInputs` should match what `init_subcall` would produce - /// (same caller, target, gas limit, calldata). Implementations should share - /// the decoding logic with `init_subcall` to keep them in sync. - /// - /// Returns `None` if the input cannot be decoded (e.g., malformed calldata). - /// In that case the trace falls back to the precompile's own address, so the - /// debug trace for error cases will differ from the successful case (showing - /// the precompile call instead of the logical child call). - fn trace_child_call(&self, _inputs: &CallInputs) -> Option { - None - } -} - -/// Result of a successful [`SubcallPrecompile::init_subcall`] execution. -pub struct SubcallInitResult { - /// The child call inputs to execute. - pub child_inputs: Box, - /// Opaque state carried from `init_subcall` to `complete_subcall`. - pub continuation_data: SubcallContinuationData, - /// Gas consumed by the precompile itself (ABI decoding, validation). - /// Deducted from the caller's gas budget before the 63/64ths split. - pub gas_overhead: u64, -} - -/// Opaque state blob carried between `init_subcall` and `complete_subcall`. -pub struct SubcallContinuationData { - /// Precompile-specific state (e.g., memo bytes, decoded parameters). - pub state: Box, -} - -/// Result of a successful [`SubcallPrecompile::complete_subcall`] execution. -pub struct SubcallCompletionResult { - /// The output bytes to return to the caller. - pub output: Bytes, - /// Whether the precompile considers the call successful. - pub success: bool, - /// Gas consumed by the completion phase (e.g., ABI encoding the return data). - /// Added to the total gas_used for the precompile invocation. - pub gas_overhead: u64, -} - -/// Errors that can occur during subcall precompile execution. -#[derive(Debug, thiserror::Error)] -pub enum SubcallError { - #[error("ABI decode error: {0}")] - AbiDecodeError(String), - #[error("unexpected frame result type (expected Call)")] - UnexpectedFrameResult, - #[error("insufficient gas: {0}")] - InsufficientGas(String), - #[error("internal error: {0}")] - InternalError(String), -} diff --git a/crates/precompiles/src/system_accounting.rs b/crates/precompiles/src/system_accounting.rs deleted file mode 100644 index fee3f24..0000000 --- a/crates/precompiles/src/system_accounting.rs +++ /dev/null @@ -1,1388 +0,0 @@ -// Copyright 2025 Circle Internet Group, Inc. All rights reserved. -// -// SPDX-License-Identifier: Apache-2.0 -// -// Licensed under the Apache License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. -// You may obtain a copy of the License at -// -// http://www.apache.org/licenses/LICENSE-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// See the License for the specific language governing permissions and -// limitations under the License. - -use crate::helpers::{ - abi_decode_raw_with_zero6_validation, check_delegatecall, check_staticcall, - new_reverted_with_early_penalty, read, record_cost_or_out_of_gas, write, - PrecompileErrorOrRevert, ERR_EXECUTION_REVERTED, ERR_INVALID_CALLER, - PRECOMPILE_EARLY_REVERT_GAS_PENALTY, PRECOMPILE_SLOAD_GAS_COST, -}; -use crate::precompile; -use alloy_evm::Evm; -use alloy_primitives::B256; -use alloy_primitives::{address, keccak256, Address, Bytes, StorageKey}; -use alloy_sol_types::{sol, SolCall, SolValue}; -use arc_execution_config::hardforks::ArcHardfork; -use reth_ethereum::evm::revm::precompile::PrecompileOutput; -use revm::handler::SYSTEM_ADDRESS; -use revm::state::EvmState; -use revm::DatabaseCommit; -use revm_interpreter::Gas; -use thiserror::Error; - -#[derive(Debug, Error)] -pub enum SystemAccountingError { - #[error("EVM execution failed: {0}")] - Execution(E), - #[error("ABI decode error: {0}")] - AbiDecode(String), - #[error("System call reverted")] - Reverted(), - #[error("Unable to store value")] - StoreFailed(), -} - -// System Accounting precompile address -pub const SYSTEM_ACCOUNTING_ADDRESS: Address = - address!("0x1800000000000000000000000000000000000002"); - -// Storage key for storing gas values -const GAS_VALUES_STORAGE_KEY: StorageKey = StorageKey::new([ - 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, -]); - -/// Ring buffer capacity for historical gas values. Consensus reads only -/// freshly-written slots (the executor reads the parent slot for EMA smoothing; -/// the assembler reads the current slot just written by `finish()`), so no -/// history depth is required for correctness. The extra capacity exists purely -/// as headroom for external readers (RPC, monitoring) and is otherwise arbitrary. -const GAS_VALUES_RING_BUFFER_SIZE: u64 = 64; - -// Arc system-accounting caller. -const ARC_SYSTEM_CALLER: Address = SYSTEM_ADDRESS; - -sol! { - struct GasValues { - uint64 gasUsed; - uint64 gasUsedSmoothed; - /// store the computed base fee for next block - /// max value is 2^64 - 1 ~= 18 USDC - uint64 nextBaseFee; - } - - interface ISystemAccounting { - /// Writes `gasValues` into ring-buffer slot - /// `blockNumber % GAS_VALUES_RING_BUFFER_SIZE`, overwriting whatever - /// the slot previously held. ARC_SYSTEM_CALLER-gated; no validation on - /// `blockNumber`, since writes happen once per block from the block - /// executor. - function storeGasValues(uint64 blockNumber, GasValues calldata gasValues) external returns (bool); - - /// Returns ring-buffer slot `blockNumber % GAS_VALUES_RING_BUFFER_SIZE` - /// as-is, without any freshness check. If `blockNumber` has been - /// rotated out (more than `GAS_VALUES_RING_BUFFER_SIZE - 1` behind the - /// latest written block) or is in the future, the slot holds the last - /// block that mapped to it, i.e. a different block's values. Slots - /// that have never been written (possible only early in the chain's - /// life, before every slot has been reached once) read as zero. - /// Callers needing freshness must cross-check against their own view - /// of the chain tip. Consensus does not depend on freshness: the - /// executor reads only the parent slot for EMA smoothing, which was - /// written by the previous block's `finish()` (or reads as zero at - /// genesis, the correct EMA baseline), and the block assembler reads - /// only the current slot just written by the same block's `finish()`. - function getGasValues(uint64 blockNumber) external view returns (GasValues calldata gasValue); - } -} - -/// Computes the storage slot for a mapping key of type address -/// -/// A mapping, while slightly less efficient than a fixed size contiguous array, -/// is more flexible if additional gas values should be added in the future. -/// -/// Implements Solidity's mapping storage slot calculation: -/// Formula: keccak256(h(k) . p), where: -/// - k is the mapping key (uint64) -/// - p is the mapping slot position (GAS_VALUES_STORAGE_KEY) -/// - h left-pads the key to 32 bytes -/// - . is concatenation -/// -/// `block_number` is reduced mod `GAS_VALUES_RING_BUFFER_SIZE` before hashing, -/// so any two block numbers that differ by a multiple of the ring buffer size -/// collide on the same slot. The mapping carries no identity of the block that -/// last wrote the slot — callers who need that identity must track it -/// out-of-band. -pub fn compute_gas_values_storage_slot(block_number: u64) -> StorageKey { - // Map block number into ring buffer - let key_value = block_number % GAS_VALUES_RING_BUFFER_SIZE; - - // Left-pad 8 byte u64 to 32 bytes - let mut key_bytes = [0u8; 32]; - key_bytes[24..].copy_from_slice(key_value.to_be_bytes().as_ref()); - - // Use AVERAGED_HISTORICAL_GAS_STORAGE_KEY as the slot bytes - let slot_bytes = GAS_VALUES_STORAGE_KEY.0; - - // Concatenate key and slot, then hash - let mut data = [0u8; 64]; - data[..32].copy_from_slice(&key_bytes); - data[32..].copy_from_slice(&slot_bytes); - - StorageKey::new(keccak256(data).0) -} - -precompile!(run_system_accounting, precompile_input, hardfork_flags; { - ISystemAccounting::storeGasValuesCall => |input| { - (|| -> Result { - let mut gas_counter = Gas::new(precompile_input.gas); - let mut precompile_input = precompile_input; - - // Check if static call is attempting to modify state - check_staticcall( - &precompile_input, - &mut gas_counter, - )?; - - // Decode arguments passed to blocklist function - let args = abi_decode_raw_with_zero6_validation::( - input, - hardfork_flags, - ) - .map_err(|_| - PrecompileErrorOrRevert::new_reverted_with_penalty( - gas_counter, PRECOMPILE_EARLY_REVERT_GAS_PENALTY, ERR_EXECUTION_REVERTED, - ) - )?; - - // Redundant 2100-gas charge — no SLOAD occurs here, but kept pre-Zero6 to - // preserve consensus on already-finalized blocks. - if !hardfork_flags.is_active(ArcHardfork::Zero6) { - record_cost_or_out_of_gas(&mut gas_counter, PRECOMPILE_SLOAD_GAS_COST)?; - } - - // Check caller - if precompile_input.caller != ARC_SYSTEM_CALLER { - return Err(new_reverted_with_early_penalty(gas_counter, ERR_INVALID_CALLER, hardfork_flags)); - } - - // Check delegatecall - check_delegatecall( - SYSTEM_ACCOUNTING_ADDRESS, - &precompile_input, - &gas_counter, - )?; - - // Update storage - let storage_slot = compute_gas_values_storage_slot(args.blockNumber); - let updated_value_bytes = pack_gas_values_for_storage(args.gasValues); - write( - &mut precompile_input.internals, - SYSTEM_ACCOUNTING_ADDRESS, - storage_slot, - &updated_value_bytes, - &mut gas_counter, - hardfork_flags, - )?; - - let output = true.abi_encode(); - Ok(PrecompileOutput::new(gas_counter.used(), output.into())) - })() - }, - ISystemAccounting::getGasValuesCall => |input| { - (|| -> Result { - let mut gas_counter = Gas::new(precompile_input.gas); - let mut precompile_input = precompile_input; - - // Decode arguments passed to blocklist function - let args = abi_decode_raw_with_zero6_validation::( - input, - hardfork_flags, - ) - .map_err(|_| - PrecompileErrorOrRevert::new_reverted_with_penalty( - gas_counter, PRECOMPILE_EARLY_REVERT_GAS_PENALTY, ERR_EXECUTION_REVERTED, - ) - )?; - - // Read stored value - let storage_slot = compute_gas_values_storage_slot(args.blockNumber); - let slot_value = read( - &mut precompile_input.internals, - SYSTEM_ACCOUNTING_ADDRESS, - storage_slot, - &mut gas_counter, - hardfork_flags, - )?; - let gas_values = unpack_gas_values_from_storage(B256::from_slice(slot_value.as_ref())); - let output = gas_values.abi_encode(); - - Ok(PrecompileOutput::new(gas_counter.used(), output.into())) - })() - }, -}); - -/// Packs GasValues into a single 32-byte storage slot -/// The layout is: -/// - `gasUsedSmoothed` (u64): bytes [16..24] -/// - `gasUsed` (u64): bytes [24..32] -fn pack_gas_values_for_storage(g: GasValues) -> [u8; 32] { - let mut slot = [0u8; 32]; - slot[24..32].copy_from_slice(&g.gasUsed.to_be_bytes()); - slot[16..24].copy_from_slice(&g.gasUsedSmoothed.to_be_bytes()); - slot[8..16].copy_from_slice(&g.nextBaseFee.to_be_bytes()); - slot -} - -pub fn unpack_gas_values_from_storage(slot: B256) -> GasValues { - let bytes = slot.as_slice(); - let gas_used = u64::from_be_bytes( - bytes[24..32] - .try_into() - .expect("8-byte slice from 32-byte array"), - ); - let gas_used_smoothed = u64::from_be_bytes( - bytes[16..24] - .try_into() - .expect("8-byte slice from 32-byte array"), - ); - let next_base_fee = u64::from_be_bytes( - bytes[8..16] - .try_into() - .expect("8-byte slice from 32-byte array"), - ); - GasValues { - gasUsed: gas_used, - gasUsedSmoothed: gas_used_smoothed, - nextBaseFee: next_base_fee, - } -} - -/// Conducts system tx to retrieve an average historical gas used value -pub fn retrieve_gas_values( - block_number: u64, - evm: &mut E, -) -> Result> -where - E: Evm, - E::DB: DatabaseCommit, -{ - let call_data = ISystemAccounting::getGasValuesCall { - blockNumber: block_number, - } - .abi_encode(); - - let result_and_state = evm - .transact_system_call( - ARC_SYSTEM_CALLER, - SYSTEM_ACCOUNTING_ADDRESS, - Bytes::from(call_data), - ) - .map_err(SystemAccountingError::Execution)?; - - if !result_and_state.result.is_success() { - return Err(SystemAccountingError::Reverted()); - } - - let output = result_and_state - .result - .output() - .ok_or(SystemAccountingError::AbiDecode( - "No values to decode".to_string(), - ))?; - - let gas_values = ISystemAccounting::getGasValuesCall::abi_decode_returns(output) - .map_err(|e| SystemAccountingError::AbiDecode(format!("ABI decode error: {e}")))?; - - Ok(gas_values) -} - -/// Conducts a system tx to update a stored average historical gas used value -pub fn store_gas_values( - block_number: u64, - gas_values: GasValues, - evm: &mut E, -) -> Result> -where - E: Evm, - E::DB: DatabaseCommit, -{ - let call_data = ISystemAccounting::storeGasValuesCall { - blockNumber: block_number, - gasValues: gas_values, - } - .abi_encode(); - - let result_and_state = evm - .transact_system_call( - ARC_SYSTEM_CALLER, - SYSTEM_ACCOUNTING_ADDRESS, - Bytes::from(call_data), - ) - .map_err(SystemAccountingError::Execution)?; - - if !result_and_state.result.is_success() { - return Err(SystemAccountingError::Reverted()); - } - - let output = result_and_state - .result - .output() - .ok_or(SystemAccountingError::AbiDecode( - "No values to decode".to_string(), - ))?; - - let decoded = ISystemAccounting::storeGasValuesCall::abi_decode_returns(output) - .map_err(|e| SystemAccountingError::AbiDecode(e.to_string()))?; - - if !decoded { - return Err(SystemAccountingError::StoreFailed()); - } - - evm.db_mut().commit(result_and_state.state.clone()); - - Ok(result_and_state.state) -} - -#[cfg(test)] -mod tests { - #![allow(unused_imports, dead_code)] - use super::*; - use crate::helpers::{ - ERR_DELEGATE_CALL_NOT_ALLOWED, ERR_EXECUTION_REVERTED, ERR_INVALID_CALLER, - PRECOMPILE_EARLY_REVERT_GAS_PENALTY, PRECOMPILE_SLOAD_GAS_COST, PRECOMPILE_SSTORE_GAS_COST, - REVERT_SELECTOR, - }; - use arc_execution_config::hardforks::{ArcHardfork, ArcHardforkFlags}; - - // EIP-2929 warm/cold gas costs for Zero5 - const WARM_SLOAD_GAS_COST: u64 = 100; - // Cold SSTORE (0→non-zero) per EIP-2200 - const COLD_SSTORE_ZERO_TO_NONZERO_GAS_COST: u64 = 22100; - use alloy_primitives::{address, Bytes, U256}; - use alloy_sol_types::SolValue; - use reth_ethereum::evm::revm::{ - context::{Context, ContextTr, JournalTr}, - interpreter::{CallInput, CallInputs, CallScheme, CallValue, InstructionResult}, - MainContext, - }; - use reth_evm::precompiles::{DynPrecompile, PrecompilesMap}; - use revm::{ - handler::PrecompileProvider, - interpreter::InterpreterResult, - precompile::{PrecompileId, Precompiles}, - }; - use serde_with::NoneAsEmptyString; - - fn call_system_accounting( - ctx: &mut revm::context::Context< - revm::context::BlockEnv, - revm::context::TxEnv, - revm::context::CfgEnv, - DB, - revm::context::Journal, - >, - inputs: &CallInputs, - hardfork_flags: ArcHardforkFlags, - ) -> Result, String> { - let mut provider = PrecompilesMap::from_static(Precompiles::latest()); - let target_addr: Address = inputs.target_address; - provider.set_precompile_lookup(move |address: &Address| { - if *address == SYSTEM_ACCOUNTING_ADDRESS || target_addr == SYSTEM_ACCOUNTING_ADDRESS { - Some(DynPrecompile::new_stateful( - PrecompileId::Custom("SYSTEM_ACCOUNTING".into()), - move |input| run_system_accounting(input, hardfork_flags), - )) - } else { - None - } - }); - provider.run(ctx, inputs) - } - - // Helper to decode revert Error(string) - fn bytes_to_revert_message(input: &[u8]) -> Option { - if input.len() < 4 { - return None; - } - if input[0..4] != REVERT_SELECTOR { - return None; - } - String::abi_decode(&input[4..]).ok() - } - - // Test helpers to simplify calling the precompile within a shared Context - fn write( - ctx: &mut Context, - block_number: u64, - gas_values: GasValues, - gas_limit: u64, - ) -> InterpreterResult { - let inputs = CallInputs { - scheme: CallScheme::Call, - target_address: SYSTEM_ACCOUNTING_ADDRESS, - bytecode_address: SYSTEM_ACCOUNTING_ADDRESS, - known_bytecode: None, - caller: ARC_SYSTEM_CALLER, - value: CallValue::Transfer(U256::ZERO), - input: CallInput::Bytes( - ISystemAccounting::storeGasValuesCall { - blockNumber: block_number, - gasValues: gas_values, - } - .abi_encode() - .into(), - ), - gas_limit, - is_static: false, - return_memory_offset: 0..0, - }; - - call_system_accounting(ctx, &inputs, ArcHardforkFlags::with(&[ArcHardfork::Zero5])) - .unwrap() - .unwrap() - } - - fn read( - ctx: &mut Context, - block_number: u64, - gas_limit: u64, - ) -> (InterpreterResult, GasValues) { - let inputs = CallInputs { - scheme: CallScheme::Call, - target_address: SYSTEM_ACCOUNTING_ADDRESS, - bytecode_address: SYSTEM_ACCOUNTING_ADDRESS, - known_bytecode: None, - caller: ARC_SYSTEM_CALLER, - value: CallValue::Transfer(U256::ZERO), - input: CallInput::Bytes( - ISystemAccounting::getGasValuesCall { - blockNumber: block_number, - } - .abi_encode() - .into(), - ), - gas_limit, - is_static: false, - return_memory_offset: 0..0, - }; - - let res = - call_system_accounting(ctx, &inputs, ArcHardforkFlags::with(&[ArcHardfork::Zero5])) - .unwrap() - .unwrap(); - let decoded = ISystemAccounting::getGasValuesCall::abi_decode_returns(res.output.as_ref()) - .expect("decode getGasValues"); - (res, decoded) - } - - #[test] - fn pack_unpack_roundtrip() { - let samples = [ - GasValues { - gasUsed: 1, - gasUsedSmoothed: 2, - nextBaseFee: 5, - }, - GasValues { - gasUsed: 2, - gasUsedSmoothed: 1, - nextBaseFee: 0, - }, - GasValues { - gasUsed: u64::MAX, - gasUsedSmoothed: 0, - nextBaseFee: 100, - }, - GasValues { - gasUsed: 0, - gasUsedSmoothed: u64::MAX, - nextBaseFee: u64::MAX, - }, - GasValues { - gasUsed: 123_456_789, - gasUsedSmoothed: 987_654_321, - nextBaseFee: 123_411_331, - }, - ]; - - for g in samples { - let slot_bytes = pack_gas_values_for_storage(g.clone()); - let unpacked = unpack_gas_values_from_storage(B256::from(slot_bytes)); - assert_eq!(unpacked.gasUsed, g.clone().gasUsed); - assert_eq!(unpacked.gasUsedSmoothed, g.clone().gasUsedSmoothed); - } - } - - #[test] - fn get_gas_values_failure_case_table_tests() { - struct GetCase { - name: &'static str, - caller: Address, - calldata: Bytes, - gas_limit: u64, - expected_result: InstructionResult, - expected_revert_str: Option<&'static str>, - return_data: Option, - gas_used: u64, - } - - let block_zero = 0u64; - let cases: &[GetCase] = &[ - GetCase { - name: "get() default zero values", - caller: address!("0x1000000000000000000000000000000000000001"), - calldata: ISystemAccounting::getGasValuesCall { - blockNumber: block_zero, - } - .abi_encode() - .into(), - gas_limit: PRECOMPILE_SLOAD_GAS_COST, - expected_result: InstructionResult::Return, - expected_revert_str: None, - return_data: Some( - GasValues { - gasUsed: 0, - gasUsedSmoothed: 0, - nextBaseFee: 0, - } - .abi_encode() - .into(), - ), - gas_used: PRECOMPILE_SLOAD_GAS_COST, - }, - GetCase { - name: "get() invalid params reverts", - caller: ARC_SYSTEM_CALLER, - calldata: ISystemAccounting::getGasValuesCall::SELECTOR.into(), - gas_limit: PRECOMPILE_SLOAD_GAS_COST, - expected_result: InstructionResult::Revert, - expected_revert_str: Some(ERR_EXECUTION_REVERTED), - return_data: None, - gas_used: PRECOMPILE_EARLY_REVERT_GAS_PENALTY, - }, - GetCase { - name: "get() OOG", - caller: ARC_SYSTEM_CALLER, - calldata: ISystemAccounting::getGasValuesCall { blockNumber: 1 } - .abi_encode() - .into(), - gas_limit: PRECOMPILE_SLOAD_GAS_COST - 1, - expected_result: InstructionResult::PrecompileOOG, - expected_revert_str: None, - return_data: None, - gas_used: 0, - }, - ]; - - for tc in cases { - let mut ctx = Context::mainnet(); - ctx.journal_mut() - .load_account(SYSTEM_ACCOUNTING_ADDRESS) - .expect("Unable to load system accounting account"); - - // if let Some((bn, val)) = tc.prepopulate_block.clone() { - // let slot = super::compute_gas_values_storage_slot(bn); - // let stored_u256 = U256::from_be_slice(&pack_gas_values_for_storage(val)); - // ctx.journal_mut() - // .sstore(SYSTEM_ACCOUNTING_ADDRESS, slot.into(), stored_u256) - // .expect("sstore prepopulate"); - // } - - let inputs = CallInputs { - scheme: CallScheme::Call, - target_address: SYSTEM_ACCOUNTING_ADDRESS, - bytecode_address: SYSTEM_ACCOUNTING_ADDRESS, - known_bytecode: None, - caller: tc.caller, - value: CallValue::Transfer(U256::ZERO), - input: CallInput::Bytes(tc.calldata.clone()), - gas_limit: tc.gas_limit, - is_static: false, - return_memory_offset: 0..0, - }; - - let res = call_system_accounting( - &mut ctx, - &inputs, - ArcHardforkFlags::with(&[ArcHardfork::Zero5]), - ) - .unwrap() - .unwrap(); - - // Result - assert_eq!(res.result, tc.expected_result, "{}", tc.name); - - // Revert string - if let Some(expected_revert_str) = tc.expected_revert_str { - let reason = bytes_to_revert_message(res.output.as_ref()).expect("revert reason"); - assert_eq!(reason, expected_revert_str, "{}", tc.name); - } - - // Return data - if let Some(expected_return) = &tc.return_data { - assert_eq!(res.output, *expected_return, "{}", tc.name); - } - - // Gas used - assert_eq!(res.gas.used(), tc.gas_used, "{}", tc.name); - } - } - - #[test] - fn store_gas_values_table_tests() { - struct StoreCase { - name: &'static str, - caller: Address, - calldata: Bytes, - gas_limit: u64, - /// If set, overrides `gas_limit` when Zero6 is active. Needed when - /// the Zero6 early-revert penalty pushes required gas above the - /// Zero5 limit. - zero6_gas_limit: Option, - expected_result: InstructionResult, - expected_revert_str: Option<&'static str>, - return_data: Option, - gas_used: u64, - /// If set, overrides `gas_used` for pre-Zero5 hardforks (fixed - /// SSTORE cost vs. EIP-2929/EIP-2200 warm/cold pricing). - pre_zero5_gas_used: Option, - /// If set, overrides `gas_used` when Zero6 is active (auth reverts - /// charge `PRECOMPILE_EARLY_REVERT_GAS_PENALTY`). - zero6_gas_used: Option, - target_address: Address, - bytecode_address: Address, - } - - let bn_ok = 1024u64; - let val_ok = GasValues { - gasUsed: 11, - gasUsedSmoothed: 22, - nextBaseFee: 33, - }; - // Zero5: 2100 (redundant pre-auth charge, no real SLOAD) + 22100 (cold SSTORE 0→non-zero) - // = 24200. - // Zero6: 22100 only — redundant charge dropped (see `zero6_gas_used` override below). - // Pre-Zero5 uses the fixed SSTORE path (see `pre_zero5_gas_used` override below). - let expected_gas_success = PRECOMPILE_SLOAD_GAS_COST + COLD_SSTORE_ZERO_TO_NONZERO_GAS_COST; - - let cases: &[StoreCase] = &[ - StoreCase { - name: "successful insert", - caller: ARC_SYSTEM_CALLER, - calldata: ISystemAccounting::storeGasValuesCall { - blockNumber: bn_ok, - gasValues: val_ok.clone(), - } - .abi_encode() - .into(), - gas_limit: expected_gas_success, - zero6_gas_limit: None, - expected_result: InstructionResult::Return, - expected_revert_str: None, - return_data: Some(true.abi_encode().into()), - gas_used: expected_gas_success, - pre_zero5_gas_used: Some(PRECOMPILE_SLOAD_GAS_COST + PRECOMPILE_SSTORE_GAS_COST), - zero6_gas_used: Some(COLD_SSTORE_ZERO_TO_NONZERO_GAS_COST), - target_address: SYSTEM_ACCOUNTING_ADDRESS, - bytecode_address: SYSTEM_ACCOUNTING_ADDRESS, - }, - StoreCase { - name: "invalid calldata reverts", - caller: ARC_SYSTEM_CALLER, - calldata: ISystemAccounting::storeGasValuesCall::SELECTOR.into(), - gas_limit: PRECOMPILE_SLOAD_GAS_COST, - zero6_gas_limit: None, - expected_result: InstructionResult::Revert, - expected_revert_str: Some(ERR_EXECUTION_REVERTED), - return_data: None, - gas_used: PRECOMPILE_EARLY_REVERT_GAS_PENALTY, - pre_zero5_gas_used: None, - zero6_gas_used: None, - target_address: SYSTEM_ACCOUNTING_ADDRESS, - bytecode_address: SYSTEM_ACCOUNTING_ADDRESS, - }, - StoreCase { - name: "OOG while storing value", - caller: ARC_SYSTEM_CALLER, - calldata: ISystemAccounting::storeGasValuesCall { - blockNumber: bn_ok, - gasValues: val_ok.clone(), - } - .abi_encode() - .into(), - // Pre-Zero6: OOGs at the redundant 2100-gas pre-auth charge. - gas_limit: PRECOMPILE_SLOAD_GAS_COST - 1, - // Zero6: redundant charge dropped, so the next gas-charging point is the - // cold SSTORE inside `write()`. One gas short of that cost OOGs there. - zero6_gas_limit: Some(COLD_SSTORE_ZERO_TO_NONZERO_GAS_COST - 1), - expected_result: InstructionResult::PrecompileOOG, - expected_revert_str: None, - return_data: None, - gas_used: 0, - pre_zero5_gas_used: None, - zero6_gas_used: None, - target_address: SYSTEM_ACCOUNTING_ADDRESS, - bytecode_address: SYSTEM_ACCOUNTING_ADDRESS, - }, - StoreCase { - name: "reverts from unauthorized caller", - caller: address!("0x0000000000000000000000000000000000000123"), - calldata: ISystemAccounting::storeGasValuesCall { - blockNumber: bn_ok, - gasValues: val_ok.clone(), - } - .abi_encode() - .into(), - gas_limit: PRECOMPILE_SLOAD_GAS_COST, - // Zero6: redundant 2100-gas charge dropped, so only the early-revert - // penalty is consumed. Limit must still cover the penalty exactly. - zero6_gas_limit: Some(PRECOMPILE_EARLY_REVERT_GAS_PENALTY), - expected_result: InstructionResult::Revert, - expected_revert_str: Some(ERR_INVALID_CALLER), - return_data: None, - gas_used: PRECOMPILE_SLOAD_GAS_COST, - pre_zero5_gas_used: None, - zero6_gas_used: Some(PRECOMPILE_EARLY_REVERT_GAS_PENALTY), - target_address: SYSTEM_ACCOUNTING_ADDRESS, - bytecode_address: SYSTEM_ACCOUNTING_ADDRESS, - }, - StoreCase { - name: "reverts from zero-address caller (legacy system caller)", - caller: Address::ZERO, - calldata: ISystemAccounting::storeGasValuesCall { - blockNumber: bn_ok, - gasValues: val_ok.clone(), - } - .abi_encode() - .into(), - gas_limit: PRECOMPILE_SLOAD_GAS_COST, - zero6_gas_limit: Some(PRECOMPILE_EARLY_REVERT_GAS_PENALTY), - expected_result: InstructionResult::Revert, - expected_revert_str: Some(ERR_INVALID_CALLER), - return_data: None, - gas_used: PRECOMPILE_SLOAD_GAS_COST, - pre_zero5_gas_used: None, - zero6_gas_used: Some(PRECOMPILE_EARLY_REVERT_GAS_PENALTY), - target_address: SYSTEM_ACCOUNTING_ADDRESS, - bytecode_address: SYSTEM_ACCOUNTING_ADDRESS, - }, - StoreCase { - name: "reverts if target address != precompile address", - caller: ARC_SYSTEM_CALLER, - calldata: ISystemAccounting::storeGasValuesCall { - blockNumber: bn_ok, - gasValues: val_ok.clone(), - } - .abi_encode() - .into(), - gas_limit: expected_gas_success, - zero6_gas_limit: None, - expected_result: InstructionResult::Revert, - expected_revert_str: Some(ERR_DELEGATE_CALL_NOT_ALLOWED), - return_data: None, - gas_used: PRECOMPILE_SLOAD_GAS_COST, - pre_zero5_gas_used: None, - // Zero6: nothing is charged before check_delegatecall reverts (auth passes, - // redundant pre-auth charge dropped). System-tx callers never delegatecall in - // production, so the 0-gas exit here is unreachable on real workloads. - zero6_gas_used: Some(0), - target_address: address!("0x0000000000000000000000000000000000000123"), - bytecode_address: SYSTEM_ACCOUNTING_ADDRESS, - }, - StoreCase { - name: "reverts if bytecode address != precompile address", - caller: ARC_SYSTEM_CALLER, - calldata: ISystemAccounting::storeGasValuesCall { - blockNumber: bn_ok, - gasValues: val_ok.clone(), - } - .abi_encode() - .into(), - gas_limit: expected_gas_success, - zero6_gas_limit: None, - expected_result: InstructionResult::Revert, - expected_revert_str: Some(ERR_DELEGATE_CALL_NOT_ALLOWED), - return_data: None, - gas_used: PRECOMPILE_SLOAD_GAS_COST, - pre_zero5_gas_used: None, - zero6_gas_used: Some(0), - target_address: SYSTEM_ACCOUNTING_ADDRESS, - bytecode_address: address!("0x0000000000000000000000000000000000000123"), - }, - ]; - - for tc in cases { - for hardfork_flags in ArcHardforkFlags::all_combinations() { - // ZeroX hardforks are cumulative; Zero6 implies Zero5. - if hardfork_flags.is_active(ArcHardfork::Zero6) - && !hardfork_flags.is_active(ArcHardfork::Zero5) - { - continue; - } - - let tc_name = format!("{} (hardfork_flags: {:?})", tc.name, hardfork_flags); - - let gas_limit = if hardfork_flags.is_active(ArcHardfork::Zero6) { - tc.zero6_gas_limit.unwrap_or(tc.gas_limit) - } else { - tc.gas_limit - }; - - let expected_gas_used = if hardfork_flags.is_active(ArcHardfork::Zero6) { - tc.zero6_gas_used.unwrap_or(tc.gas_used) - } else if hardfork_flags.is_active(ArcHardfork::Zero5) { - tc.gas_used - } else { - tc.pre_zero5_gas_used.unwrap_or(tc.gas_used) - }; - - let mut ctx = Context::mainnet(); - ctx.journal_mut() - .load_account(SYSTEM_ACCOUNTING_ADDRESS) - .expect("Unable to load system accounting account"); - - let inputs = CallInputs { - scheme: CallScheme::Call, - target_address: tc.target_address, - bytecode_address: tc.bytecode_address, - known_bytecode: None, - caller: tc.caller, - value: CallValue::Transfer(U256::ZERO), - input: CallInput::Bytes(tc.calldata.clone()), - gas_limit, - is_static: false, - return_memory_offset: 0..0, - }; - - let res = call_system_accounting(&mut ctx, &inputs, hardfork_flags) - .unwrap() - .unwrap(); - // Check result - assert_eq!(res.result, tc.expected_result, "{tc_name}"); - - // Revert string - if let Some(expected_revert_str) = tc.expected_revert_str { - let reason = - bytes_to_revert_message(res.output.as_ref()).expect("revert reason"); - assert_eq!(reason, expected_revert_str, "{tc_name}"); - } - - // Return data - if let Some(expected_return) = &tc.return_data { - assert_eq!(res.output, *expected_return, "{tc_name}"); - } - // Gas used - assert_eq!(res.gas.used(), expected_gas_used, "{tc_name}"); - } - } - } - - #[test] - fn read_write_workflow() { - let mut ctx = Context::mainnet(); - ctx.journal_mut() - .load_account(SYSTEM_ACCOUNTING_ADDRESS) - .expect("Unable to load system accounting account"); - - let res = write( - &mut ctx, - 1, - GasValues { - gasUsed: 2, - gasUsedSmoothed: 3, - nextBaseFee: 6, - }, - 30_000_000, - ); - assert_eq!(res.result, InstructionResult::Return); - - // Read the value for the same block - slot is warm after write - let (res_read, decoded_read) = read(&mut ctx, 1, WARM_SLOAD_GAS_COST); - assert_eq!(res_read.result, InstructionResult::Return); - assert_eq!(res_read.gas.used(), WARM_SLOAD_GAS_COST); - assert_eq!(decoded_read.gasUsed, 2); - assert_eq!(decoded_read.gasUsedSmoothed, 3); - - // Now loop the ring buffer and overwrite the value - // Same slot (block 1 % 64 == block 65 % 64), so it's warm - let res_overwrite = write( - &mut ctx, - 1 + GAS_VALUES_RING_BUFFER_SIZE, - GasValues { - gasUsed: 4, - gasUsedSmoothed: 5, - nextBaseFee: 100000000000, - }, - 30_000_000, - ); - assert_eq!(res_overwrite.result, InstructionResult::Return); - - // Read the value again for the new block - slot still warm - let (res_read_new_block, decoded_read_new_block) = read( - &mut ctx, - 1 + GAS_VALUES_RING_BUFFER_SIZE, - WARM_SLOAD_GAS_COST, - ); - assert_eq!(res_read_new_block.result, InstructionResult::Return); - assert_eq!(res_read_new_block.gas.used(), WARM_SLOAD_GAS_COST); - assert_eq!(decoded_read_new_block.gasUsed, 4); - assert_eq!(decoded_read_new_block.gasUsedSmoothed, 5); - - // Read the value for the original block number - same slot, still warm - let (res_read_original_block, decoded_read_original_block) = - read(&mut ctx, 1, WARM_SLOAD_GAS_COST); - assert_eq!(res_read_original_block.result, InstructionResult::Return); - assert_eq!(res_read_original_block.gas.used(), WARM_SLOAD_GAS_COST); - assert_eq!(decoded_read_original_block.gasUsed, 4); - assert_eq!(decoded_read_original_block.gasUsedSmoothed, 5); - } - - /// Under Zero6+, any SSTORE through `helpers::write` must fail with - /// `PrecompileOOG` and consume zero gas when the remaining gas is at or - /// below `CALL_STIPEND` (2,300), mirroring revm's `ReentrancySentryOOG` - /// halt for the SSTORE opcode. - #[test] - fn store_gas_values_eip_2200_sentry_zero6() { - use revm_context_interface::cfg::gas::CALL_STIPEND; - - let zero6_flags = ArcHardforkFlags::with(&[ArcHardfork::Zero5, ArcHardfork::Zero6]); - - let calldata: Bytes = ISystemAccounting::storeGasValuesCall { - blockNumber: 1, - gasValues: GasValues { - gasUsed: 1, - gasUsedSmoothed: 2, - nextBaseFee: 3, - }, - } - .abi_encode() - .into(); - - let make_inputs = |gas_limit: u64| CallInputs { - scheme: CallScheme::Call, - target_address: SYSTEM_ACCOUNTING_ADDRESS, - bytecode_address: SYSTEM_ACCOUNTING_ADDRESS, - known_bytecode: None, - caller: ARC_SYSTEM_CALLER, - value: CallValue::Transfer(U256::ZERO), - input: CallInput::Bytes(calldata.clone()), - gas_limit, - is_static: false, - return_memory_offset: 0..0, - }; - - // gas_limit == CALL_STIPEND: sentry fires immediately; no auth check, - // no journal mutation, no gas consumed. - for gas_limit in [1, CALL_STIPEND - 1, CALL_STIPEND] { - let mut ctx = Context::mainnet(); - ctx.journal_mut() - .load_account(SYSTEM_ACCOUNTING_ADDRESS) - .expect("load system accounting account"); - - let res = call_system_accounting(&mut ctx, &make_inputs(gas_limit), zero6_flags) - .unwrap() - .unwrap(); - - assert_eq!( - res.result, - InstructionResult::PrecompileOOG, - "Zero6 sentry must OOG at gas_limit={gas_limit}" - ); - assert_eq!( - res.gas.used(), - 0, - "Zero6 sentry must charge zero gas at gas_limit={gas_limit}" - ); - } - - // gas_limit == CALL_STIPEND + 1: sentry passes; OOG happens later - // inside `write()` at the cold-SSTORE dynamic charge. Externally still - // PrecompileOOG, but proves the sentry boundary is exclusive. - let mut ctx = Context::mainnet(); - ctx.journal_mut() - .load_account(SYSTEM_ACCOUNTING_ADDRESS) - .expect("load system accounting account"); - let res = call_system_accounting(&mut ctx, &make_inputs(CALL_STIPEND + 1), zero6_flags) - .unwrap() - .unwrap(); - assert_eq!(res.result, InstructionResult::PrecompileOOG); - - // Sanity: with enough gas the same call succeeds and the sentry is a - // no-op on the happy path. - let happy_gas = COLD_SSTORE_ZERO_TO_NONZERO_GAS_COST; - let mut ctx = Context::mainnet(); - ctx.journal_mut() - .load_account(SYSTEM_ACCOUNTING_ADDRESS) - .expect("load system accounting account"); - let res = call_system_accounting(&mut ctx, &make_inputs(happy_gas), zero6_flags) - .unwrap() - .unwrap(); - assert_eq!(res.result, InstructionResult::Return); - assert_eq!(res.gas.used(), happy_gas); - } - - /// Under Zero6, `read()` probes slot warmth with `sload(key, true)`. - /// When the slot is cold the probe returns `ColdLoadSkipped` and the - /// helper must charge `COLD_SLOAD_COST` (2100) *before* retrying with - /// the real DB load. - /// - /// This test gives exactly `COLD_SLOAD_COST - 1` gas so the charge - /// fails before the retry I/O. A bug that deferred the charge would - /// succeed at this gas level (warm cost = 100 < 2099). - /// - /// Uses `TrackingDB` to prove zero storage reads occur before the OOG. - #[test] - fn read_cold_slot_oog_before_retry() { - use crate::helpers::test_utils::TrackingDB; - use revm_interpreter::gas::COLD_SLOAD_COST; - - let zero6_flags = ArcHardforkFlags::with(&[ArcHardfork::Zero5, ArcHardfork::Zero6]); - - let calldata: Bytes = ISystemAccounting::getGasValuesCall { blockNumber: 42 } - .abi_encode() - .into(); - let make_inputs = |gas_limit: u64| CallInputs { - scheme: CallScheme::Call, - target_address: SYSTEM_ACCOUNTING_ADDRESS, - bytecode_address: SYSTEM_ACCOUNTING_ADDRESS, - known_bytecode: None, - caller: ARC_SYSTEM_CALLER, - value: CallValue::Transfer(U256::ZERO), - input: CallInput::Bytes(calldata.clone()), - gas_limit, - is_static: false, - return_memory_offset: 0..0, - }; - - // Gas = COLD_SLOAD_COST - 1: must OOG at the cold charge, zero DB reads. - let (mut ctx, storage_reads) = TrackingDB::context(); - ctx.journal_mut() - .load_account(SYSTEM_ACCOUNTING_ADDRESS) - .expect("load"); - let res = call_system_accounting(&mut ctx, &make_inputs(COLD_SLOAD_COST - 1), zero6_flags) - .unwrap() - .unwrap(); - assert_eq!( - res.result, - InstructionResult::PrecompileOOG, - "cold read must OOG when gas < COLD_SLOAD_COST" - ); - assert_eq!( - storage_reads.get(), - 0, - "OOG must occur before any storage DB read" - ); - - // Gas = COLD_SLOAD_COST: must succeed (slot is cold, value is zero). - let (mut ctx, storage_reads) = TrackingDB::context(); - ctx.journal_mut() - .load_account(SYSTEM_ACCOUNTING_ADDRESS) - .expect("load"); - let res = call_system_accounting(&mut ctx, &make_inputs(COLD_SLOAD_COST), zero6_flags) - .unwrap() - .unwrap(); - assert_eq!( - res.result, - InstructionResult::Return, - "cold read must succeed at exactly COLD_SLOAD_COST" - ); - assert_eq!(res.gas.used(), COLD_SLOAD_COST); - assert!( - storage_reads.get() > 0, - "success path must hit the DB for the real sload" - ); - } - - /// Under Zero6, `write()` probes slot warmth with `sload(key, true)`. - /// When the slot is cold the probe returns `ColdLoadSkipped` and the - /// helper charges `COLD_SLOAD_COST` before retrying. This test verifies - /// that the cold charge + sstore base cost are both applied correctly - /// on a cold slot by testing at the exact boundary. - /// - /// For a 0→non-zero write: total = COLD_SLOAD_COST (2100) + SSTORE_SET - /// (20000) = 22100. Gas at 22099 must OOG; gas at 22100 must succeed. - /// The EIP-2200 sentry (CALL_STIPEND=2300) is below COLD_SLOAD_COST, so - /// it never gates the cold-load charge for 0→non-zero writes. - /// - /// Uses `TrackingDB` to prove zero storage reads occur before the OOG. - #[test] - fn write_cold_slot_oog_at_base_cost_boundary() { - use crate::helpers::test_utils::TrackingDB; - use revm_interpreter::gas::COLD_SLOAD_COST; - - let zero6_flags = ArcHardforkFlags::with(&[ArcHardfork::Zero5, ArcHardfork::Zero6]); - // 0→non-zero base cost is SSTORE_SET (20000); total = COLD_SLOAD_COST + 20000 - let exact_cost = COLD_SLOAD_COST + 20000; - - let calldata: Bytes = ISystemAccounting::storeGasValuesCall { - blockNumber: 99, - gasValues: GasValues { - gasUsed: 1, - gasUsedSmoothed: 2, - nextBaseFee: 3, - }, - } - .abi_encode() - .into(); - let make_inputs = |gas_limit: u64| CallInputs { - scheme: CallScheme::Call, - target_address: SYSTEM_ACCOUNTING_ADDRESS, - bytecode_address: SYSTEM_ACCOUNTING_ADDRESS, - known_bytecode: None, - caller: ARC_SYSTEM_CALLER, - value: CallValue::Transfer(U256::ZERO), - input: CallInput::Bytes(calldata.clone()), - gas_limit, - is_static: false, - return_memory_offset: 0..0, - }; - - // Gas = COLD_SLOAD_COST - 1: OOG at the cold charge, before the sload DB read. - let (mut ctx, storage_reads) = TrackingDB::context(); - ctx.journal_mut() - .load_account(SYSTEM_ACCOUNTING_ADDRESS) - .expect("load"); - let res = call_system_accounting(&mut ctx, &make_inputs(COLD_SLOAD_COST - 1), zero6_flags) - .unwrap() - .unwrap(); - assert_eq!( - res.result, - InstructionResult::PrecompileOOG, - "cold write must OOG when gas < COLD_SLOAD_COST" - ); - assert_eq!( - storage_reads.get(), - 0, - "OOG at cold charge must occur before any storage DB read" - ); - - // One gas short of full cost: OOG at sstore base cost, after the sload - // (whose gas was already charged). - let (mut ctx, storage_reads) = TrackingDB::context(); - ctx.journal_mut() - .load_account(SYSTEM_ACCOUNTING_ADDRESS) - .expect("load"); - let res = call_system_accounting(&mut ctx, &make_inputs(exact_cost - 1), zero6_flags) - .unwrap() - .unwrap(); - assert_eq!( - res.result, - InstructionResult::PrecompileOOG, - "cold write one gas short of sstore must OOG" - ); - assert_eq!( - storage_reads.get(), - 1, - "sload DB read happens after cold charge was paid" - ); - - // Exact cost: must succeed. - let (mut ctx, storage_reads) = TrackingDB::context(); - ctx.journal_mut() - .load_account(SYSTEM_ACCOUNTING_ADDRESS) - .expect("load"); - let res = call_system_accounting(&mut ctx, &make_inputs(exact_cost), zero6_flags) - .unwrap() - .unwrap(); - assert_eq!( - res.result, - InstructionResult::Return, - "cold write at exact cost must succeed" - ); - assert_eq!(res.gas.used(), exact_cost); - assert!( - storage_reads.get() > 0, - "success path must hit the DB for the real sload" - ); - } - - #[test] - fn test_compute_gas_values_storage_slot() { - use super::compute_gas_values_storage_slot; - - const EXPECTED_KEY_FOR_SLOT_0: &str = - "0xa6eef7e35abe7026729641147f7915573c7e97b47efa546f5f6e3230263bcb49"; - const EXPECTED_KEY_FOR_SLOT_1: &str = - "0xcc69885fda6bcc1a4ace058b4a62bf5e179ea78fd58a1ccd71c22cc9b688792f"; - - // Test basic block number mapping - let slot_0 = compute_gas_values_storage_slot(0); - assert_eq!(slot_0.to_string(), EXPECTED_KEY_FOR_SLOT_0); - let slot_1 = compute_gas_values_storage_slot(1); - assert_eq!(slot_1.to_string(), EXPECTED_KEY_FOR_SLOT_1); - - // Test ring buffer wrapping (64 block ring buffer) - let slot_64 = compute_gas_values_storage_slot(GAS_VALUES_RING_BUFFER_SIZE); - assert_eq!(slot_64.to_string(), EXPECTED_KEY_FOR_SLOT_0); - let slot_65 = compute_gas_values_storage_slot(1 + GAS_VALUES_RING_BUFFER_SIZE); - assert_eq!(slot_65.to_string(), EXPECTED_KEY_FOR_SLOT_1); - } - - sol! { - struct GasValues_Zero3 { - uint64 gasUsed; - uint64 gasUsedSmoothed; - } - - interface ISystemAccounting_Zero3 { - function storeGasValues(uint64 blockNumber, GasValues_Zero3 calldata gasValues) external returns (bool); - function getGasValues(uint64 blockNumber) external view returns (GasValues_Zero3 calldata gasValue); - } - } - - #[test] - fn system_accounting_slot_value_compatibility() { - /// Packs GasValues into a single 32-byte storage slot - /// The layout is: - /// - `gasUsedSmoothed` (u64): bytes [16..24] - /// - `gasUsed` (u64): bytes [24..32] - fn pack_gas_values_for_storage_zero3(g: GasValues_Zero3) -> [u8; 32] { - let mut slot = [0u8; 32]; - slot[24..32].copy_from_slice(&g.gasUsed.to_be_bytes()); - slot[16..24].copy_from_slice(&g.gasUsedSmoothed.to_be_bytes()); - slot - } - - fn unpack_gas_values_from_storage_zero3(slot: B256) -> GasValues_Zero3 { - let bytes = slot.as_slice(); - let gas_used = u64::from_be_bytes(bytes[24..32].try_into().unwrap()); - let gas_used_smoothed = u64::from_be_bytes(bytes[16..24].try_into().unwrap()); - GasValues_Zero3 { - gasUsed: gas_used, - gasUsedSmoothed: gas_used_smoothed, - } - } - - for gas_value in [ - GasValues_Zero3 { - gasUsed: 123, - gasUsedSmoothed: 456, - }, - GasValues_Zero3 { - gasUsed: 0, - gasUsedSmoothed: 0, - }, - GasValues_Zero3 { - gasUsed: u64::MAX, - gasUsedSmoothed: u64::MAX, - }, - ] { - let value = pack_gas_values_for_storage_zero3(gas_value.clone()); - let unpacked = unpack_gas_values_from_storage_zero3(B256::from(value)); - assert_eq!(unpacked.gasUsed, gas_value.gasUsed); - assert_eq!(unpacked.gasUsedSmoothed, gas_value.gasUsedSmoothed); - - // The slot value pack/unpack is compatible. - let unpacked_new = unpack_gas_values_from_storage(B256::from(value)); - assert_eq!(unpacked_new.gasUsed, gas_value.gasUsed); - assert_eq!(unpacked_new.gasUsedSmoothed, gas_value.gasUsedSmoothed); - assert_eq!(unpacked_new.nextBaseFee, 0); - } - } - - #[test] - fn system_accounting_interface_incompatible() { - let output: Bytes = GasValues_Zero3 { - gasUsed: 123, - gasUsedSmoothed: 456, - } - .abi_encode() - .into(); - assert_eq!(output.to_string(), "0x000000000000000000000000000000000000000000000000000000000000007b00000000000000000000000000000000000000000000000000000000000001c8"); - - let output_new: Bytes = GasValues { - gasUsed: 123, - gasUsedSmoothed: 456, - nextBaseFee: 0, - } - .abi_encode() - .into(); - assert_eq!(output_new.to_string(), "0x000000000000000000000000000000000000000000000000000000000000007b00000000000000000000000000000000000000000000000000000000000001c80000000000000000000000000000000000000000000000000000000000000000"); - - assert_ne!(output, output_new); - } - - #[test] - fn test_static_call_reverts_store_gas_values() { - use crate::helpers::ERR_STATE_CHANGE_DURING_STATIC_CALL; - - for hardfork_flags in ArcHardforkFlags::all_combinations() { - let mut ctx = Context::mainnet(); - ctx.journal_mut() - .load_account(SYSTEM_ACCOUNTING_ADDRESS) - .expect("Unable to load system accounting account"); - - // State-modifying function (storeGasValues) must revert under static call - let inputs = CallInputs { - scheme: CallScheme::Call, - target_address: SYSTEM_ACCOUNTING_ADDRESS, - bytecode_address: SYSTEM_ACCOUNTING_ADDRESS, - known_bytecode: None, - caller: ARC_SYSTEM_CALLER, - value: CallValue::Transfer(U256::ZERO), - input: CallInput::Bytes( - ISystemAccounting::storeGasValuesCall { - blockNumber: 1, - gasValues: GasValues { - gasUsed: 100, - gasUsedSmoothed: 200, - nextBaseFee: 50, - }, - } - .abi_encode() - .into(), - ), - gas_limit: 100_000, - is_static: true, - return_memory_offset: 0..0, - }; - - let result = call_system_accounting(&mut ctx, &inputs, hardfork_flags) - .expect("call should not error") - .expect("result should be Some"); - - assert_eq!( - result.result, - InstructionResult::Revert, - "storeGasValues ({hardfork_flags:?}): expected Revert under static call", - ); - let revert_reason = bytes_to_revert_message(result.output.as_ref()); - assert_eq!( - revert_reason.as_deref(), - Some(ERR_STATE_CHANGE_DURING_STATIC_CALL), - "storeGasValues ({hardfork_flags:?}): wrong revert reason", - ); - - // Read-only function (getGasValues) must succeed under static call - let read_inputs = CallInputs { - scheme: CallScheme::Call, - target_address: SYSTEM_ACCOUNTING_ADDRESS, - bytecode_address: SYSTEM_ACCOUNTING_ADDRESS, - known_bytecode: None, - caller: ARC_SYSTEM_CALLER, - value: CallValue::Transfer(U256::ZERO), - input: CallInput::Bytes( - ISystemAccounting::getGasValuesCall { blockNumber: 1 } - .abi_encode() - .into(), - ), - gas_limit: 100_000, - is_static: true, - return_memory_offset: 0..0, - }; - - let result = call_system_accounting(&mut ctx, &read_inputs, hardfork_flags) - .expect("call should not error") - .expect("result should be Some"); - - assert_eq!( - result.result, - InstructionResult::Return, - "getGasValues ({hardfork_flags:?}): expected Return under static call", - ); - } - } -} diff --git a/crates/quake/Cargo.toml b/crates/quake/Cargo.toml deleted file mode 100644 index a855458..0000000 --- a/crates/quake/Cargo.toml +++ /dev/null @@ -1,78 +0,0 @@ -[package] -name = "quake" -description = "Testnet management and e2e testing tool for Malachite Chains" -version.workspace = true -edition.workspace = true -repository.workspace = true -license.workspace = true -publish.workspace = true - -[[bin]] -name = "quake" -path = "src/main.rs" - -[dependencies] - -alloy-consensus = { workspace = true } -alloy-primitives = { workspace = true } -alloy-provider = { workspace = true, features = ["ws"] } -alloy-rpc-types-admin = { workspace = true } -alloy-rpc-types-txpool = { workspace = true } -alloy-signer = { workspace = true } -alloy-signer-local = { workspace = true } -alloy-sol-macro = { workspace = true } -alloy-sol-types = { workspace = true } -alloy-transport-ws = { workspace = true } -arc-checks = { workspace = true } -arc-consensus-types = { workspace = true } -arc-mesh-analysis = { workspace = true } -arc-node-consensus = { workspace = true } -arc-node-consensus-cli = { workspace = true } -arc-version = { workspace = true } -axum = { workspace = true } -backon = { workspace = true } -chrono = { workspace = true } -clap = { workspace = true, features = ["derive"] } -clap-verbosity-flag = { workspace = true, features = ["tracing"] } -color-eyre = { workspace = true } -deranged = { workspace = true, features = ["serde"] } -dotenvy = { workspace = true } -futures = { workspace = true } -futures-util = { workspace = true } -handlebars = "4.5" -hex = { workspace = true } -humantime.workspace = true -indexmap = { workspace = true, features = ["serde"] } -inventory = "0.3" -itertools = { workspace = true } -k256 = { workspace = true } -lz4 = { workspace = true } -malachitebft-config = { workspace = true } -once_cell = { workspace = true } -pathdiff = "0.2" -quake-macros = { path = "macros" } -rand = { workspace = true } -regex = "1.11.2" -reqwest = { workspace = true } -reth-network-peers = { workspace = true, features = ["secp256k1"] } -rmcp = { workspace = true } -schemars = { workspace = true } -secp256k1 = { workspace = true, features = ["rand"] } -serde = { workspace = true, features = ["derive"] } -serde_json = { workspace = true } -spammer = { workspace = true } -strum = { workspace = true } -strum_macros = { workspace = true } -thiserror = { workspace = true } -tokio = { workspace = true, features = ["full"] } -toml = { workspace = true, features = ["preserve_order"] } -tracing = { workspace = true } -tracing-subscriber = { workspace = true, features = ["ansi", "env-filter", "fmt"] } -url = { workspace = true, features = ["serde"] } - -[dev-dependencies] -bytesize = { workspace = true } -tempfile = { workspace = true } - -[lints] -workspace = true diff --git a/crates/quake/README.md b/crates/quake/README.md deleted file mode 100644 index 6a695a1..0000000 --- a/crates/quake/README.md +++ /dev/null @@ -1,2141 +0,0 @@ -# Quake - End-to-end testing management tool - -Quake is a tool for deploying Arc testnets and running end-to-end tests. - -> **Quake testnets vs. the Arc Testnet:** Arc has a public, persistent -> [Testnet](https://docs.arc.network/arc/tutorials/deploy-on-arc) open to -> external developers and validators. Quake testnets are different: they are -> private, ephemeral networks spun up on demand for development and CI, then -> torn down when testing is complete. All mentions of "testnet" in this -> repository refer to Quake testnets unless explicitly stated otherwise. - -#### Features -- Describe the testnet topology, node configurations, and test scenarios in TOML manifest files. -- CLI interface for deploying, managing, monitor testnets, and run tests on them. -- Deploy testnets locally in your machine or remotely in AWS infrastructure. -- Test the resilience of a running testnet: - - perturb the nodes (disconnect, kill, pause, and restart them), - - perform chaos testing (apply random perturbations, at random intervals, to random subsets of nodes or containers), - - change the voting power of validators in the validator set of a node, - - upgrade the running version of individual nodes. -- Emulate network latency between nodes by assigning data-center regions to nodes and injecting artificial latency between regions. -- Web-based topology viewer for real-time visualization of nodes, connections, peer status, and network health. -- MCP (Model Context Protocol) server for AI-assisted testnet management via Claude Code, Cursor, and other MCP-compatible clients. - -__Table of contents__ -- [Quake - End-to-end testing management tool](#quake---end-to-end-testing-management-tool) - - [Features](#features) - - [Usage](#usage) - - [Quick start (local deployment)](#quick-start-local-deployment) - - [Command workflow](#command-workflow) - - [Basic commands](#basic-commands) - - [The `load` and `spam` commands](#the-load-and-spam-commands) - - [The `perturb` command](#the-perturb-command) - - [Disconnect](#disconnect) - - [Kill](#kill) - - [Pause](#pause) - - [Restart](#restart) - - [Upgrade](#upgrade) - - [Chaos testing](#chaos-testing) - - [The `valset` command](#the-valset-command) - - [The `web` command](#the-web-command) - - [The `mcp` command](#the-mcp-command) - - [The `generate` command](#the-generate-command) - - [Manifest File Format](#manifest-file-format) - - [Basic Structure](#basic-structure) - - [Nodes](#nodes) - - [Node Configuration](#node-configuration) - - [Execution Layer Configuration](#execution-layer-configuration) - - [Configuration Syntax](#configuration-syntax) - - [Default Flags](#default-flags) - - [Reserved Flags](#reserved-flags-do-not-override) - - [Examples](#examples) - - [Voting Power](#voting-power) - - [Node Groups and Persistent Peers](#node-groups-and-persistent-peers) - - [Starting height](#starting-height) - - [Subnets](#subnets) - - [Latency emulation](#latency-emulation) - - [Remote deployment](#remote-deployment) - - [Communication via Control Center](#communication-via-control-center) - - [Quick start](#quick-start) - - [Instance sizing](#instance-sizing) - - [Requirements](#requirements) - - [GitHub token for pulling Docker images](#github-token-for-pulling-docker-images) - - [Custom Docker images](#custom-docker-images) - - [Remote commands](#remote-commands) - - [Sharing a remote testnet](#sharing-a-remote-testnet) - - [Cleaning up orphaned AWS resources](#cleaning-up-orphaned-aws-resources) - - [Profiling](#profiling) - - [Prerequisites](#prerequisites) - - [Feature environment variables](#feature-environment-variables) - - [Building with profiling](#building-with-profiling) - - [Available profiles](#available-profiles) - - [Pulling profiles](#pulling-profiles) - - [Remote testnets](#remote-testnets) - - [Testing with Testnet](#testing-with-testnet) - - [Running tests](#running-tests) - - [Available test groups](#available-test-groups) - - [Adding new tests](#adding-new-tests) - -## Usage - -Build Quake from the repository root directory: -```bash -cargo build -p quake -ln -s target/debug/quake quake # create a symbolic link to simplify calling the tool -``` - -### Quick start (local deployment) - -Deploying and starting a local testnet is as simple as running `start`: -```sh -./quake -f crates/quake/scenarios/examples/10nodes.toml start -``` -This will generate all necessary files to run the testnet and start running Arc in the nodes. - -Now you can display comprehensive information about the testnet and check its status: -```sh -./quake info -``` - -The `start` command implicitly performs other commands (mainly `build` and `setup`), which we describe below. -Check out the [command workflow](#command-workflow) for a complete picture of all available commands. - -Generate the files needed to start the testnet. If needed, you can manually -modify the generated configuration files before starting the nodes. -```bash -./quake -f crates/quake/scenarios/examples/10nodes.toml setup -``` - -> [!TIP] -> A command with the `-f` or `--file` option creates or updates a -> `.quake/.last_manifest` file containing the path to the most recently used -> manifest. This allows subsequent Quake commands to automatically reference the -> last manifest without requiring the `-f` flag. - -```bash -# Build Docker images for the Consensus Layer (CL) and Execution Layer (EL) -./quake build - -# Start the nodes. Implicitly calls setup. -./quake start - -# Shows comprehensive information about the testnet's nodes. -./quake info - -# Check the state of the testnet by displaying the latest heights of each node -./quake info heights -n 5 - -# Wait for the given nodes (or all nodes if unspecified) to reach height 100 -./quake wait height 100 validator1 validator2 - -# Wait for execution clients to finish syncing -./quake wait sync - -# Apply a pause for a random amount of time to the consensus layer containers of all validators -./quake perturb pause val*_cl - -# Send 1000 transactions per second during 30 seconds to one validator node -./quake load -t 30 -r 1000 --targets validator1 - -# Mixed EIP-1559 and legacy transfer load (70/30 split) -./quake load -t 30 -r 1000 --mix transfer=70,legacy=30 --targets validator1 - -# Mixed ERC-20 and native transfer load (70/30 split) -./quake load -t 30 -r 1000 --mix transfer=70,erc20=30 --targets validator1 - -# ERC-20 with mixed functions: 60% transfer, 30% approve, 10% transferFrom -./quake load -t 30 -r 1000 --mix erc20=100 --erc20-fn-weights transfer=60,approve=30,transfer-from=10 --targets validator1 - -# Stop one node (both CL and EL containers) -./quake stop val*3 - -# Stop all the nodes and remove generated files -./quake clean - -# Shorthand: clean and start the testnet in one step -./quake restart -``` - -### Command workflow - -A bold arrow from command A to B means that calling B will automatically execute -A. For example, `./quake start` implicitly calls `setup` (if it finds that no -config files were generated) and `build` (if it finds no Docker images exist). - -```mermaid -graph LR - init_local(("local
deployment")):::hidden - init_remote(("remote
deployment")):::hidden - - subgraph INIT["initialize infra"] - create["remote create"] - build - end - - subgraph START["
generate config files and start testnet"] - setup - provision["remote provision"] - ssm["remote ssm start"] - start - end - - subgraph ACTIONS["actions"] - info - wait - perturb - logs - valset - load - web - ssh["remote ssh"] - export["remote export"] - import["remote import"] - monitor["remote monitor"] - end - - subgraph STOP["stop testnet and clean data"] - stop - destroy["remote destroy"] - clean - restart - end - - init_local ~~~ init_remote - init_remote --> preinit - init_local --> build - preinit --> create ==> setup - build ==> start --> STOP - provision ==> setup ==> start --> ACTIONS --> STOP - ssm ==> setup - stop ==> clean - destroy ==> clean - ACTIONS ~~~ restart --> ACTIONS - - %% use invisible edges to give some order - info ~~~ wait - valset ~~~ perturb - load ~~~ logs - export ~~~ import - ssh ~~~ monitor - - classDef hidden width:0px; - - %% remote commands - classDef grey fill:#777 - class preinit grey - class create grey - class provision grey - class ssm grey - class destroy grey - class ssh grey - class export grey - class import grey - class monitor grey -``` - -### Basic commands - -Create the testnet files, including the node config files and genesis: -```bash -./quake -f crates/quake/scenarios/examples/10nodes.toml setup -``` - -This will create a directory `.quake/10nodes/` with: -- `compose.yaml`: Docker compose file with all the testnet containers. -- Node directories: Each node has its configuration files and a script for - configuring latency emulation (if set in the manifest; see below). These - directories also will store the Malachite app and Reth databases. -- `assets/`: Directory with files common to all nodes, such as `genesis.json` and - `prometheus.yml`. -- `monitoring/`: Directory for config files and data of Prometheus and Grafana. - -Build Docker images (as defined in the generated `.quake/10nodes/compose.yaml`): -```bash -./quake build -``` - -Start the nodes: -```bash -./quake start -``` -It will run `setup`, if not done before. - -Check out the logs of a node's CL or EL: -``` -./quake logs validator1_cl -./quake tail validator1_el -``` -Alternatively, the logs of all CL and EL containers are accessible in `.quake//logs/`. - -Wait for the given nodes (or all nodes if unspecified) to reach height 100: -```bash -./quake wait height 100 validator1 validator2 -``` - -Wait for execution clients to finish syncing (useful after node restarts or upgrades): -```bash -./quake wait sync validator1 validator2 -``` -The `sync` subcommand waits until `eth_syncing` returns `false` for all specified nodes. -You can customize the timeout and number of retries for transient RPC failures: -```bash -./quake wait sync --timeout 120 --max-retries 5 -``` - -> [!TIP] -> When a command takes node or container names directly, we can use a -> wildcard `*`. For example, `val*_cl` expands to the names of all -> consensus-layer containers of validator nodes (`validator1_cl`, -> `validator2_cl`, etc.). This does not apply to `load` or `spam` -> `--targets`, which accept exact node names and manifest node groups. - -Apply a pause of 300ms to the consensus layer containers of all validators: -```bash -./quake perturb pause val*_cl --time-off 300ms -``` -If no time is specified, Quake will apply the perturbation for a random amount -of time. For more on perturbations, see below. - -Send 1000 transactions per second during 30 seconds to one validator node: -```bash -./quake load -t 30 -r 1000 --targets validator1 -``` - -Send a mixed workload (ERC-20 and native transfers) at 500 TPS: -```bash -./quake load -t 60 -r 500 --mix transfer=50,erc20=50 --targets validator1 -``` - -Send ERC-20 traffic with diverse function calls (approve, transferFrom alongside transfer): -```bash -./quake load -t 60 -r 500 --mix erc20=100 --erc20-fn-weights transfer=60,approve=30,transfer-from=10 --targets validator1 -``` - -Stop the nodes -```bash -./quake stop -``` - -Remove generated files -```bash -./quake clean -``` -It will stop the nodes first if needed. Monitoring services are managed -separately with `quake monitoring`. - -Clean and restart the testnet in one step: -```bash -./quake restart -``` -This is equivalent to running `clean` followed by `start`. It accepts clean -scope flags such as `--all` and `--data`, plus the regular `start` flags: -```bash -# Clean everything (including monitoring data) and restart without monitoring services -./quake restart --all --monitoring=false - -# Clean all nodes and restart specific nodes -./quake restart validator1 validator2 -``` -If you want to restart just some nodes leaving the others running, use `quake perturb restart `. - -### The `load` and `spam` commands - -Both commands send transaction load to a running testnet using the [spammer](../spammer/README.md). -Transactions are dispatched over WebSocket JSON-RPC. They -accept the same flags as the spammer and differ only in the adopted send mode: - -| Command | Send mode | Nonce handling | Error recovery | Best for | -|---------|-----------|---------------|----------------|----------| -| `load` | Backpressure | Advances only on acceptance | Re-queries nonce on rejection, skips after 3 consecutive failures | Correctness-sensitive workloads, reproducible tests | -| `spam` | Fire-and-forget | Incremented optimistically | None (transactions may be lost) | Peak-throughput stress tests | - -**Backpressure mode** (`load`) waits for each JSON-RPC response for a transaction before submitting -the next one. If a transaction is rejected the sender re-queries the -node for the correct nonce and retries. This is slower but guarantees that every -submitted transaction has a valid nonce and is accepted by RPC endpoint. - -**Fire-and-forget mode** (`spam`) pushes transactions into a 10,000-item -buffered channel and dispatches them without waiting for responses. Nonces are -incremented at generation time, so nonce gaps can occur when transactions are -rejected. Use `--wait-response` (`-w`) to optionally wait for each response -while still using optimistic nonces; expect multiple `nonce too low` errors to be produced. - -Both commands support blending transaction types with `--mix`: -```bash -# 1000 TPS of native transfers for 30 seconds (backpressure) -./quake load -t 30 -r 1000 --targets validator1 - -# Same workload in fire-and-forget mode -./quake spam -t 30 -r 1000 --targets validator1 - -# Mixed workload: 70% native transfers, 30% ERC-20 -./quake load -t 60 -r 500 --mix transfer=70,erc20=30 --targets validator1 - -# Gas-intensive workload with diverse guzzler functions -./quake load -t 60 -r 200 --mix guzzler=100 \ - --guzzler-fn-weights hash-loop=70@2000,storage-write=30@600 \ - --targets validator1 - -# Fire-and-forget at high throughput, targeting all nodes -./quake spam -t 120 -r 5000 -``` - -`--targets` accepts a comma-separated list of explicit node names or manifest -node groups such as `ALL_VALIDATORS`, `ALL_NON_VALIDATORS`, `ALL_NODES`, or -custom groups defined under `[node_groups]`. If `--targets` is omitted, -transactions are sent to all manifest nodes. - -Common flags (see `./quake load --help` for the full list): - -| Flag | Short | Default | Description | -|------|-------|---------|-------------| -| `--rate` | `-r` | `1000` | Target TPS across all generators | -| `--time` | `-t` | `0` | Max duration in seconds (0 = unlimited) | -| `--num-txs` | `-n` | `0` | Max total transactions (0 = unlimited) | -| `--num-generators` | `-g` | `1` | Parallel generators, each with its own account slice | -| `--mix` | | `transfer=100` | Transaction type blend: `transfer`, `erc20`, `guzzler` | -| `--tx-latency` | | `false` | Record submit-to-finalized latency to CSV | - -#### Latency tracking (`--tx-latency`) - -The `--tx-latency` flag measures end-to-end transaction latency: the wall-clock -time between `eth_sendRawTransaction` submission and finalized block inclusion. -See the [spammer README](../spammer/README.md#transaction-latency-tracking) for -full details on the tracking architecture, CSV output format, and analysis tools. - -```bash -./quake load -t 30 -r 1000 --tx-latency --targets validator1 -./quake load -t 30 -r 1000 --tx-latency --csv-dir .quake/results --targets validator1 -``` - -**Recording behavior by mode:** - -- **Backpressure** (`load`): only transactions accepted by the node are tracked. - Rejected and transient errors are skipped. -- **Fire-and-forget with `--wait-response`** (`spam -w`): same, only accepted - transactions. -- **Fire-and-forget without `--wait-response`** (`spam`): all dispatched - transactions are tracked, including those later rejected by the node. These - unmatched entries remain in memory and are evicted after 5 minutes without - appearing in the CSV. - -Transactions that are never included in a block (dropped from mempool, rejected -after submission) do not appear in the CSV. - -### The `perturb` command -Quake offers a number of perturbations that can be applied to nodes in the testnet. -These are available as subcommands of the `perturb` command: -- `disconnect`: Disconnect the node from the network -- `kill`: Kill the node using `SIGKILL`, wait for a given amount of time, and restart it -- `pause`: Pause the node, wait for a given amount of time, and unpause it -- `restart`: Restart the node -- `upgrade`: Upgrade the consensus and/or execution layer of a node to newer Docker image (will stop the node to do so). -- `chaos`: Apply random perturbations, at random intervals, to a list of nodes or containers. - -All perturbations accept a single node, a list of nodes, a single container, or a -list of containers as arguments: -- With "node" we refer to the logical node as defined in the manifest. For example, - with these manifest files: - - ```toml - [[nodes]] - [validator1] - [validator2] - ``` - or - ```toml - [nodes.validator1] - [nodes.validator2] - ``` - you should pass `validator1` or `validator2` as arguments to the `perturb` command. - Passing the node name will apply the perturbation to both the consensus layer (CL) - and execution layer (EL) containers of the node. -- With "container" we refer to the actual docker container running in the testnet. For - example, if you have a node named `validator1`, it will have two containers named - `validator1_cl` and `validator1_el`. You can pass either of these container names - as arguments to the `perturb` command to apply the perturbation to a specific - container. -- You can also use wildcards to match multiple nodes or containers. For - example, `val*` will match all nodes starting with `val`, and `val*_cl` will match - all consensus layer containers. - -We'll go a bit deeper into each perturbation in the following sections. - -#### Disconnect -The `disconnect` perturbation disconnects one or more nodes/containers from the -network defined in the docker compose file, waits for a given, configurable amount of -time, and reconnects them to the network. -By default, the disconnect time is a random value between 250ms and 10s. -Note that the command will return an error if the disconnect time is less than 250ms, -or greater than 10s. - -Usage examples: -``` -# Disconnect a single node (both CL and EL containers) for a random amount of time -quake perturb disconnect validator1 - -# Disconnect multiple nodes (both CL and EL containers) for 500ms -quake perturb disconnect validator1 validator2 --time-off 500ms - -# Disconnect a single consensus container for 3 seconds -quake perturb disconnect validator1_cl -t 3s - -# Disconnect multiple execution containers for a random amount of time -quake perturb disconnect val*_el -``` - -#### Kill -The `kill` perturbation kills one or more nodes/containers using `SIGKILL` (i.e., -ungraceful shutdown), waits for a given, configurable amount of time, and restarts them. -By default, the time it waits before restarting them is a random value between 250ms and 10s. -Note that the command will return an error if the wait time is less than 250ms, -or greater than 10s. - -Usage examples: -``` -# Kill a single node (both CL and EL containers) for a random amount of time -quake perturb kill validator1 - -# Kill multiple nodes (both CL and EL containers) for 500ms -quake perturb kill validator1 validator2 --time-off 500ms - -# Kill a single consensus container for 3 seconds -quake perturb kill validator1_cl -t 3s - -# Kill multiple execution containers for a random amount of time -quake perturb kill val*_el -``` - -#### Pause -The `pause` perturbation pauses one or more nodes/containers, waits for a given, -configurable amount of time, and resumes them. -By default, the time it waits before resuming them is a random value between 250ms -and 10s. -Note that the command will return an error if the pause time is less than 250ms, -or greater than 10s. - -Usage examples: -``` -# Pause a single node (both CL and EL containers) for a random amount of time -quake perturb pause validator1 - -# Pause multiple nodes (both CL and EL containers) for 500ms -quake perturb pause validator1 validator2 -t 500ms - -# Pause a single consensus container for 3 seconds -quake perturb pause validator1_cl --time-off 3s - -# Pause multiple execution containers for a random amount of time -quake perturb pause val*_el -``` - -#### Restart -The `restart` perturbation restarts one or more nodes/containers. -The node/containers are gracefully stopped before restarting them, unlike the `kill` -perturbation, which uses `SIGKILL`. -Note that this command does not accept a time argument, since the restart is immediate. - -Usage examples: -``` -# Restart a single node (both CL and EL containers) -quake perturb restart validator1 - -# Restart multiple nodes (both CL and EL containers) -quake perturb restart validator1 validator2 - -# Restart a single consensus container -quake perturb restart validator1_cl - -# Restart multiple execution containers -quake perturb restart val*_el -``` - -#### Upgrade -The `upgrade` perturbation upgrades one or more nodes/containers to a new Docker image. -The node/containers are gracefully stopped before upgrading them, and then restarted -with the new image. -Note that this command does not accept a time argument, since the upgrade is immediate. - -You must declare the new image name and tag in the manifest file before running -this command, otherwise it will fail. For example: -```toml -image_cl="arc_consensus:current" # Starting image for CL containers -image_el="arc_execution:current" # Starting image for EL containers -image_cl_upgrade="arc_consensus:new" # Upgrade image for CL containers -image_el_upgrade="arc_execution:new" # Upgrade image for EL containers - -[[nodes]] -... node definitions ... -``` -The `image_cl` and `image_el` fields are optional and specify which Docker image -to use when starting the testnet. If not specified, the default images -from the deployment YAML files will be used (namely, `arc_consensus:latest` and -`arc_execution:latest`). - -The `image_cl_upgrade` and `image_el_upgrade` fields specify which Docker image -to use when upgrading nodes with the `upgrade` command. - -These are global settings that apply to all nodes, so you only need to declare them -once. At the moment we don't support per-node tags. - -Usage examples: -``` -# Upgrade a single node (both CL and EL containers) -quake perturb upgrade validator1 - -# Upgrade multiple nodes (both CL and EL containers) -quake perturb upgrade validator1 validator2 -``` - -**Important Note**: The `upgrade` command is a one-time operation. -You should run it only once per node. - - -#### Chaos testing - -The `perturb chaos` sub-command applies random perturbations on a running testnet. - -For example, the following command will run chaos testing for 30 minutes, randomly killing, pausing, or restarting up to a third of the targeted containers, waiting between 5 s and 20 s between actions, and keeping affected containers offline for at most 1 minute. -``` -./quake perturb --max-time-off 1m chaos --time 30m --min-wait 5s --max-wait 20s --perturbations kill,pause,restart -``` - -### The `valset` command -This command updates the voting power of one or more validators. -Under the hood, each validator sends a transaction to its local reth instance's -validator manager smart contract via RPC (the `updateValidatorVotingPower` call). - -Because each validator performs this update independently, the changes are not -atomic and it may take several heights for the changes to take effect. - -Example run: -```bash -quake valset validator1:30 validator2:0 -``` -Setting a validator's voting power to 0 will remove it from the validator set, while -keeping its controller account. - -Notes: -- the command accepts only node names (`validator1`, `validator2`, etc.), that is, -do not use container names (`validator1_cl`, `validator2_cl`). -- it does not accept `*` wildcards. - -### The `web` command - -The `web` command starts a browser-based topology viewer that visualizes the testnet in real time and allows you to control the testnet. -Open `http://localhost:7777` in a browser to see the web application. -Currently, it only works in local mode. - -There is one tab per topology: -- **Manifest**: expected topology from manifest peers and subnets (always available) -- **CL Consensus / Liveness / Proposal Parts**: gossipsub mesh per topic (live) -- **EL Peers**: execution layer devp2p peer connections (live) - -Two views: **Graph** (force-directed layout with subnet clustering) and **Map** (world map with nodes at their AWS region coordinates). - -#### Data architecture - -- **CL data** (mesh topology, proposer, rounds): Fetched via HTTP from each node's `/network-state` and `/status` endpoints during each topology poll. -- **EL data** (block heights, peers, mempool): Collected via a single WebSocket connection per node. Block heights arrive in real-time via `eth_subscribe(newHeads)`. Peer data (`admin_peers`) and mempool status (`txpool_status`) are polled periodically on the same connection. -- **Container statuses**: Tracked by two background tasks: a `docker events` subscriber for real-time state changes (start, stop, pause, die) and a periodic `docker inspect` poller for network disconnect detection. - -For a deeper dive (server state, background tasks, topology assembly, frontend rendering pipeline), see [`docs/web-architecture.md`](docs/web-architecture.md). - -#### Options - -| Flag | Default | Description | -|------|---------|-------------| -| `--host` | `127.0.0.1` | Bind address for the web server | -| `--port` | `7777` | Web server port | -| `--refresh-ms` | `1000` | Frontend topology poll interval (ms) | -| `--el-refresh-ms` | `1000` | EL peer refresh poller interval (ms) | -| `--container-refresh-ms` | `1000` | Docker container status poller interval (ms) | - -### The `mcp` command - -The `mcp` command starts a [Model Context Protocol](https://modelcontextprotocol.io/) (MCP) server that exposes Quake's testnet tools to AI assistants like Claude Code, Cursor, and other MCP-compatible clients. This lets you observe, manage, and test a running testnet through natural language. - -The server automatically discovers the most recently used testnet via `.quake/.last_manifest`, so there's no need to specify a manifest path. - -#### Quick setup - -1. Start a testnet as usual: - ```bash - ./quake -f crates/quake/scenarios/examples/10nodes.toml start - ``` - -2. Start Claude Code or your preferred MCP client (or restart it if it was already running). - It will read `.mcp.json` and automatically spawn `quake mcp` as a subprocess using stdio transport. - -3. Interact with the testnet: - ``` - > What's the current status of the testnet? - > Pause validator3 for 5 seconds - > Run the probe tests - ``` - -#### Transport modes - -The MCP server supports two transport modes: - -- **stdio** (default): The server communicates over stdin/stdout. This is the standard mode used by Claude Code, Cursor, and similar clients that spawn the server as a subprocess. - ```bash - quake mcp - ``` - -- **HTTP+SSE** (`--http`): The server listens on a network port for remote MCP clients. Useful when the testnet is running on a remote machine. - ```bash - quake mcp --http --port 8080 - ``` - -#### Available tools - -The MCP server exposes 20 tools organized into five categories: - -- **Observability** (read-only): `testnet_status`, `list_nodes`, `get_block_heights`, `get_mempool`, `get_peers` -- **Lifecycle**: `start_nodes`, `stop_nodes`, `restart_testnet`, `clean_testnet` -- **Perturbations**: `perturb_disconnect`, `perturb_kill`, `perturb_pause`, `perturb_restart`, `perturb_upgrade` -- **Testing**: `run_tests`, `wait_height`, `valset_update` -- **Remote** (remote testnets only): `remote_ssh`, `remote_ssm`, `remote_provision` - -#### Resources - -The server also exposes two MCP resources: - -| URI | Description | -|-----|-------------| -| `quake://manifest` | The current testnet manifest (TOML configuration file) | -| `quake://nodes` | All node metadata as JSON | - - -### The `generate` command - -The `generate` command (alias `gen`) creates random manifest files for testing. It is useful for nightly or ad‑hoc runs that exercise many topologies and configurations without writing manifests by hand. - -**Behavior** - -- Writes one or more TOML manifests into the given output directory. -- For certain **combinations** of topology, height strategy, and region strategy, it generates `count` manifests (each with a different seed). -- **Seeding:** If you pass `--seed S` (before the `generate` command), that value is used as the base seed. The first manifest gets seed `S`, the next gets `S+1`, then `S+2`, and so on. This makes runs reproducible: the same `--seed` and options produce the same manifests. Without `--seed`, the base seed is chosen at random (different each run), so output varies between invocations. - - Example: `quake --seed 42 gen -o out -c 2` produces `2 x 9 combinations = 18 manifests` with seeds `42`, `43`, `44`, ... , `59`; running again with the same arguments yields identical manifests. - -**Randomization strategies** - -| Dimension | Options | -|-----------|--------| -| **Network topology** | 1 node \| 5 nodes \| complex | -| **Height start** | All nodes at 0 \| some nodes start at 100 | -| **Region assignment** | Single region \| uniform random \| clustered | - -The **complex** topology creates a sentry architecture with the following structure: -- **Sentry group 1**: 1–3 validators (randomly chosen per manifest), fully meshed with each other and connected to `sentry-1` -- **Sentry group 2**: 1–3 validators (randomly chosen per manifest), fully meshed with each other and connected to `sentry-2` -- **Sentries**: `sentry-1` and `sentry-2` are connected to each other, to their respective validator groups, and to the `relayer` -- **Relayer**: Connected to both sentries and to 1 full node -- **Full node**: `full-1` is connected to the `relayer` - -All connections use persistent peers, creating a structured network topology that isolates validators behind sentry nodes. - -For each combination, it generates `count` manifests (default 1). The combinations are: -- 1 combination with single node (no region or height strategy variation) -- 6 combinations with 5 nodes (2 height strategies × 3 region strategies) -- 2 combinations with complex topology (2 height strategies, all nodes within a single region) - -Total manifests generated = 9 × `count`. - - -**Randomized per manifest** - -- Consensus Layer: logging, p2p transport (tcp/quic), value_sync parameters, runtime flavor, pruning, and related options. -- Execution Layer: txpool, builder, and engine options (within safe ranges). -- Manifest-level: engine API connection (IPC vs RPC), initial hardfork (e.g. zero6/zero5). - -Consensus, value_sync, and RPC are always enabled so that `setup` → `start` → `wait height` → `test` works on every generated manifest. - -**Options** - -| Option | Short | Default | Description | -|--------|-------|---------|-------------| -| `--output-dir` | `-o` | `.quake/generated` | Directory to write manifest files into. | -| `--count` | `-c` | `1` | Number of manifests to generate **per combination**. | -| `--seed` | — | *(random)* | Base seed for the RNG (use for reproducible runs). | - -**Examples** - -```bash -# Generate 1 manifest per combination (9 total) with a fixed seed -quake --seed 42 generate --output-dir target/manifests - -# Generate 10 manifests per combination (90 total), reproducible -quake --seed 123 generate -o target/manifests -c 10 - -# Generate 1 per combination with a random seed (different each run) -quake generate -o target/manifests -``` - -**Nightly CI** - -A nightly workflow runs daily at 3 AM UTC and can be triggered: -- **Scheduled/PR runs**: Use seed `42` for reproducibility. -- **Manual dispatch** (`workflow_dispatch`): Optionally specify a base seed (must be a non-negative integer; leave blank to use `42`) and a per-job count (positive integer ≤ 50; leave blank to use `3`). - -The workflow runs **10 parallel jobs** (matrix indices 0–9). Each job computes its effective seed as `BASE_SEED + 10000 × index`, then calls `quake --seed generate --count ` to produce `COUNT × 9` manifests (default: `3 × 9 = 27` per job, `270` total across all jobs). Each job then runs the full test pipeline on every manifest: `setup` → `start` → `wait height 140` → `test` → `clean`. Logs and reports are uploaded to the artifacts bucket with per-job artifact names (`-idx`). The driving script is [`scripts/scenarios/nightly-random-manifests.sh`](../../scripts/scenarios/nightly-random-manifests.sh). - -PRs labeled `test-random` will also trigger this workflow. - -### The `clean` command - -By default, `clean` stops the testnet and removes all node data and configuration, -but leaves monitoring data alone. The following flags control what is removed: - -| Flag | Short | Description | -|------|-------|-------------| -| `--all` | `-a` | Remove everything, including monitoring services and their data. Cannot be combined with data flags. | -| `--data` | `-d` | Remove only execution and consensus layer data, preserving configuration. Cannot be combined with `--execution-data` or `--consensus-data`. | -| `--execution-data` | `-x` | Remove only execution layer (Reth) data. Cannot be combined with `--data` or `--consensus-data`. | -| `--consensus-data` | `-c` | Remove only consensus layer (Malachite) data. Cannot be combined with `--data` or `--execution-data`. | - -```bash -# Remove node data only (keep config, monitoring intact) -./quake clean --data - -# Remove only execution layer data -./quake clean --execution-data - -# Remove only consensus layer data -./quake clean --consensus-data - -# Remove everything including monitoring -./quake clean --all -``` - -### The `monitoring` command - -Monitoring services (Prometheus, Grafana, cAdvisor, Blockscout) can be controlled with the `monitoring` command: - -```bash -# Start monitoring services -./quake monitoring start - -# Stop monitoring services -./quake monitoring stop - -# Stop monitoring services and remove monitoring data -./quake monitoring clean -``` - -## Manifest File Format - -The manifest is a TOML file. - -Before parsing, all `${VAR_NAME}` patterns are -replaced with values from the process environment and `.env` files. This allows -any field to reference environment variables. For example: -```toml -image_cl="${IMAGE_REGISTRY_URL}/arc-consensus:abc123" -``` - -### Basic Structure - -Optional top-level settings: -- **name**: Name of the test scenario -- **description**: Description of the test scenario -- **engine_api_connection**: Connection method between Consensus Layer (CL) and Execution Layer (EL). Valid values: "ipc" (default), "rpc". -- **image_cl** and **image_el**: Docker images for CL and EL containers. If omitted, defaults to - - for local mode: `arc_consensus:latest` and `arc_execution:latest`, or - - for remote mode: `${IMAGE_REGISTRY_URL}/arc-consensus:` and - `${IMAGE_REGISTRY_URL}/arc-execution:`, where `IMAGE_REGISTRY_URL` - is taken from the `.env` file (see [Custom Docker images](#custom-docker-images)). -- **image_cl_upgrade**, **image_el_upgrade**: Docker images to use when upgrading containers with `quake perturb upgrade`. Required for upgrade scenarios; not supported in remote mode. -- **node_size**: EC2 instance type for validator/full nodes (e.g. `"m6a.4xlarge"`). Equivalent to - the `--node-size` CLI flag. See [Instance sizing](#instance-sizing) for available options. - **Remote mode only** — ignored in local mode (a warning is printed). -- **cc_size**: EC2 instance type for the Control Center. Equivalent to `--cc-size`. - **Remote mode only** — ignored in local mode. -- **node_disk_gb**: Root EBS volume size in GiB for each node. Must be ≥ 8. Equivalent to - `--node-disk-gb`. Omit to keep the AMI default. **Remote mode only** — ignored in local mode. -- **cc_disk_gb**: Root EBS volume size in GiB for the Control Center. Must be ≥ 8. Equivalent to - `--cc-disk-gb`. **Remote mode only** — ignored in local mode. -- **node_volume_type**: AWS EBS volume type for each node's root disk; tunes disk - cost/performance (e.g. match a production disk profile). General Purpose SSD - (`gp2`, `gp3`), Provisioned IOPS SSD (`io1`, `io2`), Throughput Optimized HDD (`st1`), - Cold HDD (`sc1`). Default: `gp3`. See [AWS EBS volume types][ebs-types]. Equivalent - to `--node-volume-type`. **Remote mode only**. -- **node_volume_iops**: Provisioned IOPS for the node root EBS volume; raises the I/O - ceiling above the volume type's baseline. Only valid with `gp3`, `io1`, `io2`; range - 100–256000. Default: AMI's baseline IOPS for the chosen type. Equivalent to - `--node-volume-iops`. **Remote mode only**. -- **el_cpu_limit**: Hard CPU cap for each EL container; reproduces production CPU quotas - on the testnet. Whole or fractional CPUs (e.g. `0.5`). Maps to Docker Compose - [`cpus`][compose-cpus]. Default: no limit (container uses all host CPUs). -- **el_memory_limit_gb**: Hard memory cap for each EL container in GiB; fractional values - (e.g. `2.5`) are allowed. Maps to Docker Compose [`mem_limit`][compose-mem-limit]. - Default: no limit locally; 2.5 GiB on remote. -- **cl_cpu_limit**: Hard CPU cap for each CL container; same semantics as `el_cpu_limit`. -- **cl_memory_limit_gb**: Hard memory cap for each CL container in GiB; fractional values - allowed. Default: no limit locally; 1 GiB on remote. - -[ebs-types]: https://docs.aws.amazon.com/ebs/latest/userguide/ebs-volume-types.html -[compose-cpus]: https://docs.docker.com/reference/compose-file/services/#cpus -[compose-mem-limit]: https://docs.docker.com/reference/compose-file/services/#mem_limit - -### Nodes - -Nodes are defined as individual TOML sections with names starting with `validator` or `node`. -```toml -[[nodes]] -[validator1] -[validator2] -[node1] -[node2] -``` - -### Node Configuration - -Consensus Layer (CL) configuration is set under `cl.config.*` keys. The -schema depends on the CL image version (`image_cl`): - -- **Modern CL (>= v0.5.0)**: the schema matches the `StartCmd` struct in - [`crates/malachite-cli/src/cmd/start.rs`](../malachite-cli/src/cmd/start.rs). - Keys are flat and map 1:1 to the `arc-node-consensus start` CLI flags - (e.g. `cl.config.log_level = "debug"` → `--log-level=debug`). Quake - translates the merged config into CLI flags at setup time and the node is - launched with no `config.toml`. -- **Legacy CL (< v0.5.0)**: the schema matches the `Config` struct in - [`crates/types/src/config.rs`](../types/src/config.rs). Keys are nested - (e.g. `cl.config.logging.log_level = "debug"`) and the merged config is - written to `config.toml` at setup time. Legacy mode is scheduled for - deprecation. - -Quake detects which schema to use by parsing the `image_cl` tag; `latest`, -missing tags, and unparsable tags are treated as Modern. The two formats -are not interchangeable — `cl.config.log_level` on a Legacy image (and -`cl.config.logging.log_level` on a Modern image) will fail to parse. -Upgrading a running testnet across the legacy/modern boundary with -`perturb upgrade` is **not supported**: the upgraded binary would start -with no CLI flags. For upgrade scenarios, start the testnet on a Modern -version. - -#### Matching Flags to the Target Image Version - -Upgrade scenarios can pin an older `arc_consensus` image tag (e.g. -`v0.6.0`). Quake derives CLI flags from the `StartCmd` definition -compiled into its own binary, which may have gained, renamed, or removed -flags since that image shipped. Before handing the flags to the -container, Quake rewrites them to match the target version, i.e., older -images receive a compatible subset, and `"latest"`, missing, or -unparsable tags pass through unchanged. - -If pinning an older image fails with `unexpected argument`, that version -likely needs a new compatible entry. See `apply_version_compat` in -[`src/cli_version.rs`](src/cli_version.rs) for the rustdoc describing -how to add one. - -The default configuration of Reth (Execution Layer) is defined in -[`crates/quake/src/manifest.rs`](src/manifest.rs). It can be set globally -or for each node by prefixing the config field with `el.config.`. - -For example (Modern CL): - -```toml -# Global settings that apply to all nodes -engine_api_connection = "rpc" # or "ipc" (default) -cl.config.log_level = "debug" -el.config.disable-discovery = true - -[[nodes]] -[validator1] -# Node-specific settings -cl.config.discovery_num_outbound_peers = 30 -[validator2] -# Node-specific settings -el.config.builder.deadline = 5 -[node1] -[node2] -``` - -In general, node configuration options are applied with the following precedence, from -lowest to highest priority: - -1. **Global manifest configs**: defined at the top level of your manifest file -3. **Per-node manifest configs**: defined within each node's section - -Higher-priority configs override lower-priority ones when their keys match. - -#### Execution Layer Configuration -In addition to the general node configuration described above, the EL configuration -adds another layer of defaults defined in `crates/quake/src/manifest.rs`. -This means that Quake manages Reth (Execution Layer) flags through a three-tier -configuration system. Configs are applied with the following precedence, from -lowest to highest priority: - -1. **Default configs**: defined in `crates/quake/src/manifest.rs` -2. **Global manifest configs**: defined at the top level of your manifest file -3. **Per-node manifest configs**: defined within each node's section - -As before, higher-priority configs override lower-priority ones when their keys match. -For a full list of reth CLI flags that you can set in your manifest, see the -[Reth documentation](https://reth.rs/cli/op-reth/node). - -##### Configuration Syntax - -EL configs use TOML table syntax under the `el.config` key. -Boolean flags (like `--http` or `--disable-discovery`) use `true`/`false`, -while flags with values use their appropriate types (strings, integers, arrays). - -```toml -# Global EL config that applies to all nodes -[el.config] -http.enable = true -http.api = ["admin", "net", "eth"] -engine.persistence-threshold = 5 -disable-discovery = true - -[nodes.validator1] -# Per-node config that overrides global and defaults for this node only -el.config.engine.persistence-threshold = 10 -el.config.builder.deadline = 5 - -[nodes.validator2] -# No per-node config, so it inherits global + defaults - -[nodes.full1] -# Per-node config can also use table syntax -[nodes.full1.el.config] -txpool.nolocals = false -``` - -**Special syntax notes:** -- `http.enable = true` produces `--http` (the `.enable` suffix is stripped) -- `ws.enable = true` produces `--ws` -- `disable-discovery = true` produces `--disable-discovery` -- `disable-discovery = false` omits the flag entirely -- Array values like `http.api = ["admin", "net"]` produce `--http.api=admin,net` -- Array values are **replaced**, not merged. If a node defines `http.api = ["admin"]`, - it completely overrides the default array, not appends to it. -- Quake does not validate flag names. Misspelled flags (e.g., `htpp.port = 8545`) - will be passed to Reth, which will fail at startup with an unrecognized flag error. - -#### Default Flags - -The following flags are applied to all nodes by default. They are defined in -`crates/quake/src/manifest.rs` and can be overridden in your manifest: - -| Flag | Default Value | Description | -|------|---------------|-------------| -| `http.enable` | `true` | Enable the HTTP-RPC server | -| `http.api` | `["admin", "net", "eth", "web3", "debug", "txpool", "trace", "reth"]` | APIs exposed over HTTP | -| `ws.enable` | `true` | Enable the WebSocket-RPC server | -| `ws.api` | `["admin", "net", "eth", "web3", "debug", "txpool", "trace", "reth"]` | APIs exposed over WebSocket | -| `engine.persistence-threshold` | `0` | Persistence threshold for engine payloads | -| `engine.memory-block-buffer-target` | `0` | Memory block buffer target | -| `enable-arc-rpc` | `true` | Enable Arc-specific RPC methods | -| `rpc.txfeecap` | `1000` | Maximum transaction fee cap | -| `txpool.nolocals` | `true` | Treat all transactions equally (no local priority) | - -To override a default, simply define the flag in your manifest's global or -per-node `el.config` section. - -#### Reserved Flags (Do Not Override) - -The following flags are managed by Docker Compose templates and **must not be -set in manifests**. If present, they are silently ignored: - -| Flag | Value (Local) | Value (Remote) | Notes | -|------|---------------|----------------|-------| -| `datadir` | `/data/reth/execution-data` | `/data/reth/execution-data` | Data directory path | -| `chain` | `/app/assets/genesis.json` | `/app/assets/genesis.json` | Genesis file path | -| `http.port` | `8545` | `8545` | HTTP-RPC port | -| `http.addr` | `0.0.0.0` | `0.0.0.0` | HTTP-RPC bind address | -| `http.corsdomain` | `*` | `*` | CORS allowed origins | -| `ws.port` | `8546` | `8546` | WebSocket-RPC port | -| `ws.addr` | `0.0.0.0` | `0.0.0.0` | WebSocket-RPC bind address | -| `ws.origins` | `*` | `*` | WebSocket allowed origins | -| `metrics` | `0.0.0.0:9001` | `0.0.0.0:9001` | Metrics endpoint | -| `authrpc.addr` | `0.0.0.0` | `0.0.0.0` | Auth server address to listen on (RPC mode) | -| `authrpc.port` | `8551` | `8551` | Auth server port to listen on (RPC mode) | -| `authrpc.jwtsecret` | `/app/assets/jwtsecret` | `/assets/jwtsecret` | JWT secret path (RPC mode) | -| `ipcdisable` | (set) | (set) | Disable IPC (RPC mode only) | -| `ipcpath` | `/sockets/reth.ipc` | `/sockets/reth.ipc` | IPC socket path (IPC mode) | -| `auth-ipc` | (set) | (set) | Enable authenticated IPC (IPC mode) | -| `auth-ipc.path` | `/sockets/auth.ipc` | `/sockets/auth.ipc` | Auth IPC socket path (IPC mode) | -| `p2p-secret-key` | `/data/reth/execution-data/nodekey` | `/data/reth/execution-data/nodekey` | Pre-generated secp256k1 key for P2P identity | -| `trusted-peers` | *(auto-generated)* | *(auto-generated)* | Comma-separated enode URLs of all other nodes | - -The IPC vs RPC flags are automatically selected based on the connection mode. -Use `quake setup --rpc` to switch from IPC (default) to RPC connections between -the Consensus Layer and Execution Layer. You can also set this in your manifest using -the `engine_api_connection` top-level key. - -#### Examples - -**Example 1: Override a default flag globally** - -```toml -# Disable the txpool.nolocals default for all nodes -[el.config] -txpool.nolocals = false - -[nodes.validator1] -[nodes.validator2] -``` - -**Example 2: Per-node override** - -```toml -[nodes.validator1] -# This node uses a custom persistence threshold -el.config.engine.persistence-threshold = 10 -el.config.builder.deadline = 5 - -[nodes.validator2] -# Uses defaults only -``` - -**Example 3: Mixed global and per-node configuration** - -```toml -# Global: disable discovery for all nodes -[el.config] -disable-discovery = true -engine.persistence-threshold = 5 - -[nodes.validator1] -# Override: re-enable discovery for this node -el.config.disable-discovery = false - -[nodes.validator2] -# Uses global config (discovery disabled, threshold=5) - -[nodes.full1] -# Override: different persistence threshold -el.config.engine.persistence-threshold = 20 -``` - -> [!NOTE] -> When you set `enable-arc-rpc = true` (the default), `--arc-rpc-upstream-url=` -> is automatically added to Reth's configuration. You don't need to include it -> manually. - -### Voting Power - -By default every validator in genesis receives a voting power of 20. To override this, set `cl_voting_power` on each validator node: - -```toml -[nodes.validator-1] -cl_voting_power = 2000 - -[nodes.validator-2] -cl_voting_power = 2000 - -[nodes.validator-3] -cl_voting_power = 1000 - -[nodes.full1] -``` - -If `cl_voting_power` is specified for any validator, it must be specified for all validators (all-or-nothing). This prevents accidental power imbalances where one validator silently defaults to 20 while others are set to much higher values. Non-validator nodes ignore this field. - -### Node Groups and Persistent Peers - -> **Note:** The `cl_persistent_peers` setting described below applies to the **Consensus Layer** (Malachite) P2P connections. **Execution Layer** (Reth) P2P: during setup, a secp256k1 nodekey is pre-generated for each node. Reth's `--trusted-peers` is built from each node's `el.config.trusted_peers` when set (same format as `cl_persistent_peers`: node names or group names, resolved to enodes); when `el.config.trusted_peers` is not set for a node, that node gets a full mesh of all other nodes. - -You can define custom groups of nodes and use them to configure peer connections. This is useful for setting up network topologies where certain nodes should only connect to specific subsets of other nodes. - -**Pre-defined node groups:** -- `ALL_NODES` - All nodes in the manifest -- `ALL_VALIDATORS` - All validator nodes, that is, nodes with names starting with `val` (e.g., `validator1`, `val2`) -- `ALL_NON_VALIDATORS` - All nodes that are not validators - -These names are reserved built-ins and cannot be redefined under -`[node_groups]`. - -**Custom node groups** are defined in the `[node_groups]` section. Groups can reference individual node names, pre-defined groups, or other groups previously declared: - -```toml -[node_groups] -FULL_NODES = ["full1", "full2"] -TRUSTED = ["ALL_VALIDATORS", "FULL_NODES", "other_node"] - -[nodes.validator1] -cl_persistent_peers = ["TRUSTED"] -[nodes.validator2] -[nodes.validator3] -[nodes.validator4] -[nodes.full1] -cl_persistent_peers = ["ALL_NON_VALIDATORS"] -[nodes.full2] -cl_persistent_peers = ["ALL_VALIDATORS"] -[nodes.sentry] -cl_persistent_peers = ["ALL_NODES"] -[nodes.other_node] -``` - -In this example: -- `FULL_NODES` is a custom group containing `full1` and `full2` -- `TRUSTED` combines the `ALL_VALIDATORS` group, the `FULL_NODES` group, and the individual node `other_node` -- `validator1` will have persistent peers: `validator2`, `validator3`, `validator4`, `full1`, `full2`, `other_node` (the `TRUSTED` group, excluding itself) -- `full1` will connect to all non-validators: `full2`, `sentry`, `other_node` -- `sentry` will connect to all nodes except itself - -The same group names can also be used as `quake load` and `quake spam` -targets. For example: - -```bash -./quake load -t 60 -r 500 --targets ALL_VALIDATORS -./quake spam -t 30 -r 1000 --targets TRUSTED -``` - -To distinguish group references from individual nodes in peer lists, by convention we use lowercase for node names and uppercase for node group names. - -Note: A node is automatically excluded from its own persistent peers list. - -**Default behavior for `cl_persistent_peers`:** -- If `cl_persistent_peers` is **not specified** for a node, it will connect to **all other nodes** in the network (default behavior for simple testnets). -- If `cl_persistent_peers` is **specified as an empty array** (`cl_persistent_peers = []`), the node will have **no persistent peers**. -- If `cl_persistent_peers` is **specified with values**, the node will connect only to those specific peers. - -**`el.config.trusted_peers` (Execution Layer):** identical behavior to `cl_persistent_peers`. - -### Starting height - -By default, all nodes (Consensus Layer and Execution Layer containers) will -start when the `start` CLI command is invoked, unless a node has a -`start_at` height set in the manifest. -```toml -[nodes.validator1] -[nodes.validator2] -[nodes.full1] -start_at = 30 -``` - -### Subnets - -By default, all nodes are connected to a single Docker network named `default`. -You can isolate nodes into separate sub-networks and create bridge nodes that -connect multiple sub-networks by using the `subnets` field. - -Each subnet is assigned a dedicated private IP address range. In local mode, -subnets use `172..0.0/16` CIDR blocks where `N` starts at 21 and increments -for each subnet. - -Subnets work in both **local** and **remote** deployments: -- **Local**: Isolation is enforced via separate Docker networks. Each subnet is - assigned a dedicated private IP address range using `172..0.0/16` - CIDR blocks where `N` starts at 21. Containers in different networks cannot - communicate directly. Network perturbations (disconnect/connect) use - `docker network disconnect` and `docker network connect` to detach and - reattach containers from their subnet networks. -- **Remote**: Isolation is enforced at the AWS infrastructure level. Each - logical network maps to a separate VPC subnet with its own security group. - Nodes belonging to multiple networks (bridge nodes) have multiple network - interfaces (ENIs) attached, one per network. Network perturbations - (disconnect/connect) use host-level `iptables` rules to block/unblock - traffic between nodes' VPC IPs. - -The following example has 5 nodes across multiple isolated networks: `trusted`, `untrusted`, and `default`: -```toml -[nodes.validator1] -subnets = ["trusted"] - -[nodes.validator2] -subnets = ["trusted"] - -[nodes.validator3] -subnets = ["trusted", "untrusted"] - -[nodes.validator4] -subnets = ["untrusted", "default"] - -[nodes.full1] -# No subnets specified, defaults to ["default"] -``` - -In this example: -- `validator1` and `validator2` are isolated in the `trusted` subnet -- `validator3` bridges the `trusted` and `untrusted` subnets -- `validator4` bridges the `untrusted` and `default` subnets -- `full1` is in the `default` subnet - -Quake validates that the network topology forms a **connected graph**. If -networks are completely isolated from each other (no bridge nodes), the manifest -validation will fail. - -#### Implementation approach - -Subnet isolation is enforced at two levels: - -1. **Infrastructure level**: - - **Local**: Each subnet maps to a separate Docker network configured as - `internal: true`, which prevents routing through Docker's gateway. This - enforces isolation even on Docker Desktop where bridge networks would - otherwise be able to communicate. Containers are also connected to a shared - `host-access` network (non-internal) for port publishing. Network - perturbations (disconnect/connect) use `docker network disconnect` and - `docker network connect` to detach and reattach containers from their - subnet networks. - - **Remote**: Each subnet maps to a separate VPC subnet with its own security - group that only allows traffic within that subnet. Bridge nodes get multiple - ENIs (one per subnet). Network perturbations use `iptables` DROP rules - installed only on the target node's EC2 host, blocking peer IPs in the - INPUT, OUTPUT, and FORWARD chains. This unidirectional approach avoids - altering peer hosts. On reconnect, the rules are removed and Malachite's - persistent peer reconnection handles re-establishing connections on both - sides. - -2. **Application level**: Consensus layer nodes are configured with `cl_persistent_peers` - that only include nodes sharing at least one subnet. This ensures nodes only attempt - to connect to peers they can actually reach. - -Both layers are necessary for robust isolation. Infrastructure-level isolation works -reliably on native Linux Docker and AWS, but Docker Desktop (Mac/Windows) does not -fully isolate bridge networks. The application-level peer configuration provides -defense-in-depth and ensures correct behavior across all platforms. - -### Latency emulation - -All nodes in the testnet are typically deployed to the same private network configuration either in a local machine or remotely in one cloud region. -We can emulate latency between nodes by artificially increasing the latency of outbound traffic with the Linux `tc` (traffic control) command. - -Unless we set `latency_emulation = false`, latency emulation will be enabled by default. -We can assign in the manifest an [AWS-region](https://docs.aws.amazon.com/global-infrastructure/latest/regions/aws-regions.html#available-regions) to each node. -Nodes that don't have an explicit region in the manifest will be assigned a random one. -Then, Quake will simulate network latency between containers in different regions, using real-world average latency values between each region. -``` -latency_emulation = true -[[nodes]] -[validator1] -region = "eu-central-1" -[validator2] -[validator3] -``` - -> This latency emulation mechanism is a re-implementation of the one in -> CometBFT's [e2e framework](https://github.com/cometbft/cometbft/blob/e069791d48381d4a2032087c760079f3a52f22d4/test/e2e/runner/latency_emulation.go). -> In turn, the latter was adapted from https://github.com/paulo-coelho/latency-setter. - -## Remote deployment - -Quake can deploy a testnet to remote infrastructure (AWS EC2 instances). - -The remote setup consists of: -- one EC2 instance per node, where - - each node consists of a Consensus Layer and a Execution Layer, each in its own Docker container -- one extra instance for a Control Center (CC) server - - for monitoring services, and - - for generating and sending transaction load to the nodes. - -Currently all instances are deployed in one AWS region (`us-east-1` by default) -and we rely on [latency emulation](#latency-emulation) to make the node -communication behavior more realistic. - -```mermaid -graph TB - subgraph nodes[" "] - direction LR - subgraph node_3["Node 3 instance"] - direction TB - CL3["CL"] - EL3["EL"] - end - subgraph node_2["Node 2 instance"] - direction TB - CL2["CL"] - EL2["EL"] - end - subgraph node_1["Node 1 instance"] - direction TB - CL1["CL"] - EL1["EL"] - end - end - CC - - CC --> node_1 - CC --> node_2 - CC --> node_3 - CL1 --- EL1 - CL2 --- EL2 - CL3 --- EL3 -``` - -### Communication via Control Center - -All communication with remote nodes is routed through the Control Center (CC): -- **RPC requests** are routed via an nginx reverse proxy on CC -- **Pprof requests** are routed via a separate nginx reverse proxy on CC -- **SSH/SCP commands** are routed via CC using nodes' private IPs - -This architecture requires only a single SSM session to CC, avoiding AWS API -throttling that would occur with per-node SSM sessions. - -```mermaid -flowchart LR - subgraph local [Local Machine] - Quake - end - subgraph aws [AWS VPC] - CC[Control Center] - RPC[RPC proxy :8080] - PProf[pprof proxy :6060] - N0[Node 0] - N1[Node 1] - NN[Node N] - end - - Quake -->|SSM Session| CC - CC --- RPC - CC --- PProf - RPC -->|Private IP :8545| N0 - RPC -->|Private IP :8545| N1 - RPC -->|Private IP :8545| NN - PProf -->|Private IP :6060/:6161| N0 - PProf -->|Private IP :6060/:6161| N1 - PProf -->|Private IP :6060/:6161| NN - CC -->|SSH via Private IP| N0 - CC -->|SSH via Private IP| N1 - CC -->|SSH via Private IP| NN -``` - -**RPC Proxy:** -- A single SSM tunnel forwards `localhost:18080` to CC's port `8080` -- The nginx proxy routes requests by URL path: - - `//el` forwards to the node's Execution Layer (Reth JSON-RPC) on port 8545 - - `//el/ws` forwards to the node's Execution Layer (Reth WebSocket) on port 8546 - - `//cl` forwards to the node's Consensus Layer (Malachite RPC) on port 31000 - - `//cl/metrics` forwards to the node's Consensus Layer metrics on port 29000 -- Additional endpoints: `/health` (health check), `/nodes` (list available nodes) - -**Pprof Proxy:** -- A single SSM tunnel forwards `localhost:16060` to CC's port `6060` -- The nginx proxy routes by URL path: `/pprof/cl//...` forwards to the node's CL pprof (port 6060), `/pprof/el//...` to the EL pprof (port 6161) -- The proxy has a 600-second read timeout because CPU profiling requests block for the sampling duration (`?seconds=N`, default 30s). For CPU profiles longer than 10 minutes, SSH into the node and query pprof locally. -- Additional endpoints: `/health` (health check), `/nodes` (list available nodes) - -**SSH/SCP Routing:** -- SSH to CC uses a direct SSM session -- SSH to nodes is routed through CC: Quake SSHs to CC, then CC SSHs to the node using its private IP -- Parallel commands to multiple nodes run from a single SSH session to CC - -**Benefits:** -- Avoids AWS API throttling: only one SSM session needed instead of N+1 -- Simpler session management with only one tunnel to maintain -- Low latency between CC and nodes (same VPC/region) - -### Quick start - -All basic commands for local deployment work in remote mode. -It just requires a few extra steps. - -To quickly deploy and start a remote testnet with one command, run: -```sh -quake -f crates/quake/scenarios/examples/5nodes.toml start --remote -``` -which is equivalent to -```sh -quake -f crates/quake/scenarios/examples/5nodes.toml remote create --yes -quake start -``` -Check out `quake remote --help` for details on every sub-command. - -For testnets running longer than ~20 hours, add `--node-size t3.large` (see [Instance sizing](#instance-sizing)). - -Setting up the required tools to make this work requires a few extra steps, -described below. - -### Instance sizing - -The `--node-size` and `--cc-size` flags let you override the default EC2 instance -types when creating remote infrastructure. The `--node-disk-gb` and `--cc-disk-gb` -flags set the root EBS volume size in GiB for nodes and the Control Center; -omit them to keep the AMI default volume size. - -```bash -# Use larger nodes for a multi-day testnet -quake remote create --node-size t3.large --cc-size t3.2xlarge - -# Larger root volume for long runs (disk fills before RAM on default volume) -quake remote create --node-size t3.large --node-disk-gb 100 --cc-disk-gb 100 - -# Or with the shorthand -quake start --remote --node-size t3.large --node-disk-gb 100 -``` - -#### Node instances - -Each node runs an Execution Layer (EL) and a Consensus Layer (CL) container, -plus the OS, Docker daemon, NFS client, and SSM agent. The EL is the dominant -consumer of both memory (~2.5 GiB) and disk (debug logs grow at ~200 MiB/hr). - -| Instance | vCPU | RAM | Max testnet duration | Best for | -|---|---|---|---|---| -| `t3.medium` (default) | 2 | 4 GiB | ~20 hours | Short tests, CI smoke runs | -| `t3.large` | 2 | 8 GiB | 1–3 days | Day-long testnets, moderate load | -| `t3.xlarge` | 4 | 16 GiB | Multi-day | Heavy load, large state, long-running | - -The duration estimates assume debug-level logging with no log rotation on a 20 -GiB root volume. The primary constraint is **disk space**: the 4 GiB swap file, -~9 GiB of Docker images, and growing log files fill the default 20 GiB volume in -roughly 20 hours. Larger instances do not increase disk size; use `--node-disk-gb` -for that. Larger instances do provide more RAM headroom, reducing swap pressure -and making the node more resilient to memory spikes. - -> [!TIP] -> For testnets that need to run longer than 20 hours, consider both upgrading -> the instance size (for RAM) **and** passing `--node-disk-gb` (and `--cc-disk-gb` -> if the CC needs more space) for disk. - -#### Control Center (CC) instance - -The CC runs Prometheus, Grafana, Blockscout (backend + frontend + DB), an RPC -reverse proxy, a pprof reverse proxy, node-exporter, and optionally spammer -containers. - -| Instance | vCPU | RAM | Notes | -|---|---|---|---| -| `t3.large` | 2 | 8 GiB | **Insufficient** — Blockscout + Prometheus exceed 8 GiB | -| `t3.xlarge` (default) | 4 | 16 GiB | Standard monitoring stack | -| `t3.2xlarge` | 8 | 32 GiB | Many nodes (>15) or heavy Blockscout indexing | - -### Requirements - -Install: -- `aws` CLI tool -- `aws`'s [Session Manager plugin](https://docs.aws.amazon.com/systems-manager/latest/userguide/session-manager-working-with-install-plugin.html) -- Terraform: - ```sh - brew tap hashicorp/tap - brew install hashicorp/tap/terraform - ``` - -### Custom Docker images - -By default, remote nodes pull the latest `arc-consensus` and `arc-execution` -Docker images from Circle's private container registry. To access them, you must -provide a GitHub personal access token (PAT) that has permission to read -packages. - -You can also override the defaults to use images from your own registry, to test -changes from a branch that hasn't been merged or published yet. For example, if -stored in GHCR, set `image_cl` and `image_el` in your manifest to: -```toml -image_cl = "ghcr.io///arc-consensus:" -image_el = "ghcr.io///arc-execution:" -``` -Both Terraform (for pre-pulling images during provisioning) and the per-node -Docker Compose files will use these values. - -The subsections below explain how to set up credentials to access the private -registry and, optionally, build and push your own images. - -#### Setting up a GitHub token - -Example setup for GitHub Container Registry (ghcr.io): -1. Visit https://github.com/settings/tokens/new -2. Create a **Classic** personal access token (PAT) with the `read:packages` scope. - - Make sure to select **Classic** PAT, not a Fine-Grained token. - - Add a descriptive note and set an expiration date (recommended). -3. If the image is hosted under an organization, you might need to authorize the PAT for that organization (e.g. Under your tokens list in GitHub, **Configure SSO** on the token, and select the org). -4. Store your GitHub username and the generated PAT (it starts with `ghp_`) in a file named `.env` in the repository root directory. - ``` - GITHUB_USER= - GITHUB_TOKEN= - ``` - -#### Step-by-step: build locally and deploy to remote - -1. **Prerequisites** - - Docker installed and running. - - A `.env` file with `GITHUB_USER` and `GITHUB_TOKEN` as described [above](#setting-up-a-github-token). Your PAT needs the additional `write:packages` scope (to push images) on top of the `read:packages` scope required for pulling. - -2. **Log in to ghcr.io** - ```bash - source .env - echo $GITHUB_TOKEN | docker login ghcr.io -u $GITHUB_USER --password-stdin - ``` - -3. **Build images for `linux/amd64`** - - EC2 instances run on `amd64`. Do **not** use `./quake build` for this — on - Apple Silicon Macs it produces `arm64` images that will fail on EC2 with a - misleading `unauthorized` error (see [troubleshooting](#troubleshooting-misleading-unauthorized-errors-from-ghcrio) below). - - Build each image explicitly with `--platform linux/amd64`. Replace `` - with your GitHub username (e.g. `your-github-username`) and `` with - your chosen tag (e.g. `dev`). - - From the repository root: - ```bash - # Execution layer - docker build --platform linux/amd64 \ - --build-context certs=deployments/certs \ - --build-arg GIT_COMMIT_HASH=$(git rev-parse HEAD) \ - --build-arg GIT_VERSION=$(git describe --tags --always) \ - --build-arg GIT_SHORT_HASH=$(git rev-parse --short HEAD) \ - --target dev-runtime \ - -t ghcr.io///arc-execution: \ - -f deployments/Dockerfile.execution . - - # Consensus layer - docker build --platform linux/amd64 \ - --build-context certs=deployments/certs \ - --build-arg GIT_COMMIT_HASH=$(git rev-parse HEAD) \ - --build-arg GIT_VERSION=$(git describe --tags --always) \ - --build-arg GIT_SHORT_HASH=$(git rev-parse --short HEAD) \ - --target dev-runtime \ - -t ghcr.io///arc-consensus: \ - -f deployments/Dockerfile.consensus . - ``` - -4. **Push images to your registry** - ```bash - docker push ghcr.io///arc-execution: - docker push ghcr.io///arc-consensus: - ``` - -5. **Set manifest fields** - - Make sure `image_cl` and `image_el` in your manifest match what you pushed: - ```toml - image_cl = "ghcr.io///arc-consensus:" - image_el = "ghcr.io///arc-execution:" - ``` - -6. **Create and start the remote testnet** - ```bash - ./quake clean # remove any previous local state - ./quake -f start --remote # start the network - ``` - -> [!IMPORTANT] -> Your ghcr.io packages must remain **private**. EC2 nodes authenticate -> using the `GITHUB_USER` and `GITHUB_TOKEN` from your `.env` file, -> which are passed to the instances during provisioning. - -#### Troubleshooting: misleading "unauthorized" errors from `ghcr.io` - -When pulling images from `ghcr.io`, Docker returns an `unauthorized` error for -**several different problems**, not just missing credentials. The error always -looks like this: - -``` -Error response from daemon: Head "https://ghcr.io/v2////manifests/": unauthorized -``` - -This same error appears when: - -1. **The image tag does not exist** — e.g. you pushed an image with tag `dev` but - your manifest specifies a different tag in `image_cl` or `image_el`. Docker - reports `unauthorized` instead of "not found". -2. **The image was built for the wrong architecture** — e.g. you built on an - Apple Silicon Mac (arm64) and pushed the image, but EC2 nodes are amd64. - The manifest exists but has no matching platform, and Docker reports - `unauthorized` instead of a platform mismatch. -3. **Actual authorization failure** — e.g. the `.env` token is expired, missing - `read:packages` scope, or not authorized for SSO. - -### Remote commands - -Initialize Terraform plugins and state. This step is required only once. -```bash -./quake remote preinit -``` - -Create EC2 instances for each node in the testnet, plus one extra for the Control Center (CC) server. -```bash -./quake [-f ] remote create [--dry-run] [--yes] [--node-size ] [--cc-size ] [--node-disk-gb ] [--cc-disk-gb ] -``` -See [Instance sizing](#instance-sizing) for recommended instance types. - -This will create a `/infra.json` file with the names, IP addresses, -and instance IDs of the created nodes. - -For accessing the instances via SSH, we need to start SSM sessions that create -tunnels from local ports to remote ports. -```bash -./quake remote ssm start -``` -Note that tunnels are closed automatically after 20 minutes of inactivity. - -Once the SSM session are established, we can log in via SSH to a node or CC, or -run commands in the instances directly from the terminal: -```bash -./quake remote ssh validator1 -./quake remote ssh cc docker ps -``` - -Show information on the EC2 instances just created. -```bash -./quake info -``` - -Create configuration files locally and upload them to the remote nodes. -```bash -./quake setup -``` -After creating the configuration files, this command implicitly will run: -- `remote provision`, to upload the generated files to the remote nodes, and -- `remote ssm start`, to create SSM tunnels from local ports to ports in remote -nodes. - -Start Arc on the testnet nodes -```bash -./quake start -``` - -Check that the validators are creating blocks. -```bash -./quake info heights -``` - -Monitor health of all nodes and the Control Center: -```bash -./quake remote monitor # all hosts, one-shot -./quake remote monitor --follow # all hosts, 30s continuous refresh -./quake remote monitor validator1 # single node, one-shot -./quake remote monitor validator1 -f # single node, 5s time-series -./quake remote monitor cc -f # Control Center only, 5s time-series -./quake remote monitor -f -i 10 # all hosts, custom 10s refresh -``` -The dashboard combines block heights, peer counts (via RPC), and memory/CPU/disk -usage with per-container memory in MiB (via SSH). Without `--follow` (`-f`), data -is collected and printed once. With `--follow`, the multi-host dashboard refreshes -continuously and the single-host view appends a new row each tick. Press Ctrl+C to -stop. - -Send transaction load: -```sh -./quake load --targets validator1,validator2 -r 1000 -t 60 -``` -`quake load` and `quake spam` auto-dispatch based on testnet type: local testnets -run the spammer directly, remote testnets forward to the Control Center via SSH. -All Spammer options are supported. `--targets` accepts comma-separated selectors -including manifest node groups such as `ALL_VALIDATORS` or custom `[node_groups]`. - -Download diagnostic artifacts from the remote testnet: -```bash -# Download all Prometheus metrics (covers the current head block, ~2h by default) -./quake remote download metrics - -# Download metrics for a specific time range -./quake remote download metrics --from 2024-01-15T10:30:00Z --to 2024-01-15T12:00:00Z - -# Download specific metrics only (metric names go after --) -./quake remote download metrics -- reth_db_size_bytes go_goroutines - -# Download node databases (both execution and consensus layers, all nodes) -./quake remote download db - -# Download execution layer only, from specific nodes -./quake remote download db --execution-only -- validator1 validator2 - -# Save to a custom output path -./quake remote download metrics -o /tmp/my-metrics.tar.gz -./quake remote download db -o /tmp/my-db.tar.gz -``` - -Both `download` subcommands output a `.tar.gz` archive named `quake-metrics-.tar.gz` / -`quake-db-.tar.gz` unless overridden with `-o`. - -Once finished with your tests, remember to destroy the remote infrastructure! -```bash -./quake remote destroy -``` -or -```bash -./quake clean -``` - -### Sharing a remote testnet - -You can give a colleague or another user access to a running remote testnet -without them having to create the infrastructure. The `export` / `import` -commands bundle and restore all the files Quake needs into a single JSON file. - -The export bundle contains: -- Manifest content -- Infrastructure metadata (instance IDs, IPs) -- SSH private key -- Controllers config (validator keys and addresses) -- Terraform state (so the importer can also destroy the infrastructure) - -**On your machine** (the one that created the testnet): -```bash -./quake remote export -``` -This writes `.quake//-export.json`. Send this file to -your colleague. - -To create a lighter bundle without Terraform state (e.g. for read-only access -where the recipient won't need to destroy the infrastructure): -```bash -./quake remote export --exclude-terraform -``` - -**On the colleague's machine:** -```bash -./quake remote import path/to/export.json -``` -`remote import` is supported on Unix-like platforms only, because Quake must set -SSH private key permissions to `0600`. - -After importing, all Quake commands work — including `quake info`, `quake remote ssh`, -and `quake remote destroy`. If the bundle includes Terraform state, the importer can -also tear down the infrastructure when done. Bundles exported with -`--exclude-terraform` support all commands except `quake remote destroy` and -`quake clean` (which skip the infrastructure destroy step gracefully). - -> [!IMPORTANT] -> The export file contains the SSH private key and Terraform state for the remote -> infrastructure. Treat it as sensitive material: transfer it securely and do not -> commit it to version control. - -### Cleaning up orphaned AWS resources - -`quake clean` relies on Terraform state to delete the AWS infrastructure for a -remote testnet. If that state is lost (e.g. the `.quake/` directory was deleted, -or `quake remote create` crashed before state was written), the nodes, VPC, -security groups, and related resources stay in AWS with no supported way for -`quake` to remove them. - -`crates/quake/scripts/aws-resources.sh` is the recovery path. It discovers -resources by the project name they were created with -(`arc--testnet-`) and removes them using the AWS CLI directly, -no Terraform state required. - -```bash -# Summarize every orphaned project for the current user. -./crates/quake/scripts/aws-resources.sh list - -# Show the full resource plan for a single testnet (no deletion). -./crates/quake/scripts/aws-resources.sh list - -# Delete orphaned resources for a testnet. Without --yes, the script prints the -# plan and prompts before deleting; with --yes, it proceeds non-interactively. -./crates/quake/scripts/aws-resources.sh remove -./crates/quake/scripts/aws-resources.sh remove --yes -``` - -`list` is read-only. `remove` defaults to an interactive confirmation, so a -bare invocation never deletes anything without an explicit `y`. The script -scopes to the current user (`$GITHUB_USER`, or `--user NAME`); `remove` and -`list TESTNET` refuse to touch a project belonging to another user unless -`--user` is passed explicitly, and the bare `list` summary surfaces a notice -when `--user` overrides the current `$GITHUB_USER`. Run `--help` for the full -option set. - -> [!IMPORTANT] -> Prefer `quake clean` whenever the Terraform state is still available. This -> script is a recovery tool for orphaned infrastructure only. It matches -> resources on AWS-side names and tags alone, so a mistyped testnet or the -> wrong `--region` can tear down infrastructure belonging to an active -> testnet. - -## Profiling - -Both the consensus (CL) and execution (EL) binaries support heap and CPU -profiling via a pprof-compatible HTTP server. This is gated behind the -`pprof` Cargo feature flag and requires building with the `profiling` -build profile (release optimizations + debug symbols for readable -flamegraphs). - -### Prerequisites - -Analyzing profiles requires either Go or the standalone `pprof` -tool. Install one of them: - -```bash -# Option 1: standalone pprof via Homebrew (no Go required) -brew install pprof - -# Option 2: use go tool pprof (bundled with any Go installation) -# Install Go from https://go.dev/dl/ -``` - -Flamegraph rendering requires Graphviz: - -```bash -brew install graphviz -``` - -### Feature environment variables - -Each layer has its own feature environment variable so they can be -configured independently: - -| Variable | Layer | Default | -|----------|-------|---------| -| `CL_FEATURES` | Consensus | _(empty)_ | -| `EL_FEATURES` | Execution | `default js-tracer` | - -### Building with profiling - -> [!Note] -> `quake start` skips building when the image tag already exists. -> If you previously built without profiling, run `quake build` -> explicitly before `quake start` to rebuild the images with the -> `pprof` feature. - -Use `quake build` with `-p profiling` to select the `profiling` Cargo -build profile. Set the feature variable for the layer(s) you want to -profile: - -```bash -# CL only -CL_FEATURES=pprof ./quake -f build -p profiling - -# EL only (include base features) -EL_FEATURES="default js-tracer pprof" \ - ./quake -f build -p profiling - -# Both layers -CL_FEATURES=pprof EL_FEATURES="default js-tracer pprof" \ - ./quake -f build -p profiling -``` - -The same variables work with `make build-docker`: - -```bash -CL_FEATURES=pprof BUILD_PROFILE=profiling make build-docker -``` - -After building, start the testnet normally: - -```bash -./quake -f start -``` - -`quake build` builds for the local architecture only. For remote testnets, -follow the [Custom docker images](#custom-docker-images) instructions to build -`linux/amd64` images and push them to your registry. Add the profiling build -args to the `docker build` commands: - -```bash -# Execution layer — add these flags: ---build-arg BUILD_PROFILE=profiling \ ---build-arg FEATURES="default js-tracer pprof" - -# Consensus layer — add these flags: ---build-arg BUILD_PROFILE=profiling \ ---build-arg FEATURES=pprof -``` - -### Available profiles - -| Endpoint | Type | Description | -|----------|------|-------------| -| `/debug/pprof/allocs` | Heap | Snapshot of in-use memory allocations | -| `/debug/pprof/heap` | Heap | Alias for `/debug/pprof/allocs` | -| `/debug/pprof/profile?seconds=N` | CPU | CPU sampling over N seconds | - -### Pulling profiles - -| Layer | Internal port | Local host port | -|-------|---------------|-----------------| -| CL | 6060 | `6060 + node_index` | -| EL | 6061 | `6161 + node_index * 100` | - -```bash -# CL heap profile from the first validator -pprof -text http://localhost:6060/debug/pprof/heap -go tool pprof -text http://localhost:6060/debug/pprof/heap - -# EL heap profile from the first validator -pprof -text http://localhost:6161/debug/pprof/heap -go tool pprof -text http://localhost:6161/debug/pprof/heap - -# 30-second CPU profile from the first validator (CL) -pprof -text 'http://localhost:6060/debug/pprof/profile?seconds=30' -go tool pprof -text 'http://localhost:6060/debug/pprof/profile?seconds=30' - -# Interactive web UI with flamegraph -pprof -http :8080 http://localhost:6060/debug/pprof/heap -go tool pprof -http :8080 http://localhost:6060/debug/pprof/heap -``` - -> [!NOTE] -> When the `pprof` feature is not compiled in, the server is a no-op. -> Pprof ports are always mapped in compose but nothing will listen -> unless the binary was built with the `pprof` feature. - -#### Remote testnets - -Pprof requests are proxied through CC (see -[Communication via Control Center](#communication-via-control-center)). -Start the SSM tunnel with `quake remote ssm start`, then access: - -```bash -# CL heap profile for first validator -pprof -http :8080 http://localhost:16060/pprof/cl/validator1/debug/pprof/allocs -go tool pprof -http :8080 http://localhost:16060/pprof/cl/validator1/debug/pprof/allocs - -# EL 30-second CPU profile for first validator -pprof -text 'http://localhost:16060/pprof/el/validator1/debug/pprof/profile?seconds=30' -go tool pprof -text 'http://localhost:16060/pprof/el/validator1/debug/pprof/profile?seconds=30' -``` - -## Testing with Testnet - -The `test` command provides runtime validation of a running testnet. Tests verify connectivity, sync status, peer connections, and other operational aspects of the testnet. The idea is to have a test suite to run against arbitrary quake scenarios. - -TODO: more testing scenarios to come - -### Running tests - -Run all tests (except excluded groups: `validation`, `health`, `validator_set`, `perf`): -```bash -./quake test -``` - -Run an excluded group explicitly: -```bash -./quake test perf:block_time -./quake test validation:basic -``` - -Run all tests in a specific group: -```bash -./quake test probe -``` - -Run a single test: -```bash -./quake test probe:connectivity -``` - -Run multiple specific tests: -```bash -./quake test probe:connectivity,sync -``` - -**Glob pattern matching** - Use `*` (any characters) and `?` (single character) for flexible test selection. Quote patterns to prevent shell expansion: -```bash -# Run tests in groups starting with 'n' -./quake test 'n*' - -# Run all tests named 'sync' in any group -./quake test '*:sync' - -# Run probe tests starting with 'conn' -./quake test 'probe:conn*' - -# Run tests containing 'peer' in groups starting with 'n' -./quake test 'n*:*peer*' - -# Run tests starting with 's' in groups starting with 'p' -./quake test 'p*:s*' -``` - -**List tests without running** - Use `--dry-run` to see which tests would be executed: -```bash -# List all available test groups and tests -./quake test --dry-run - -# List tests in a specific group -./quake test probe --dry-run - -# List tests matching a pattern -./quake test 'n*:*peer*' --dry-run -``` - -Configure RPC timeout for tests (default is 1 second): -```bash -./quake test --rpc-timeout 5s -``` - -### Available test groups - -**probe** - Basic connectivity and sync validation: -- `connectivity` - Verifies all nodes are reachable via RPC and returns their current block height -- `sync` - Checks that all nodes have completed syncing (not currently syncing) - -**net** - Network peer validation: -- `peer_count` - Ensures all nodes have at least one peer connection -- `cl_persistent_peers` - Verifies that persistent peers defined in the manifest are actually connected - -### Adding new tests - -Tests automatically register themselves using the `#[quake_test]` macro. To add new tests: - -1. **Create a new test file** in `crates/quake/src/tests/` (e.g., `foobar.rs`) or add to an existing file: - ```rust - use tracing::debug; - - use super::{quake_test, in_parallel, CheckResult, RpcClientFactory, TestOutcome, TestResult}; - use crate::testnet::Testnet; - - /// Test description - #[quake_test(group = "foobar", name = "my_test")] - fn my_test<'a>( - testnet: &'a Testnet, - factory: &'a RpcClientFactory, - ) -> TestResult<'a> { - Box::pin(async move { - debug!("Running my test..."); - - // Example: Check all nodes are reachable - let node_urls = testnet.nodes_metadata.all_execution_urls(); - let results = in_parallel(&node_urls, factory, |client| async move { - client.get_block_number().await - }) - .await; - - // Use structured test results - let mut outcome = TestOutcome::new(); - - for (name, url, result) in results { - match result { - Ok(block_number) => { - outcome.add_check(CheckResult::success( - name, - format!("{} (block #{})", url, block_number), - )); - } - Err(e) => { - outcome.add_check(CheckResult::failure( - name, - format!("{} - Error: {}", url, e), - )); - } - } - } - - outcome.with_summary("All nodes checked").into_result() - }) - } - ``` - -2. **Add module declaration** in `crates/quake/src/tests/mod.rs`: - ```rust - mod foobar; - ``` - -3. **Run your new test**: - ```bash - ./quake test foobar:my_test - ``` - -**Key points:** -- Tests register automatically via `#[quake_test(group = "...", name = "...")]` macro -- Each `group:name` combination must be unique (enforced at compile time) -- Tests use async functions that return `Pin>>>` wrapped in `Box::pin(async move { ... })` -- The `RpcClientFactory` provides RPC clients with consistent timeout configuration -- Use the `in_parallel()` helper for concurrent RPC operations across nodes -- Use `TestOutcome` and `CheckResult` for structured test reporting with consistent formatting -- All test output goes to stdout with `✓` for success and `✗` for failure - -### nightly-chaos-testing -Tests network resilience under chaos conditions with transaction load and validator -set changes. - -```bash -./scripts/scenarios/nightly-chaos-testing.sh [scenario] [spam_duration_in_seconds] [tx_rate] -``` - -Our CI workflow runs this script every night with: -```bash -./scripts/scenarios/nightly-chaos-testing.sh crates/quake/scenarios/nightly-chaos-testing.toml 3600 1000 -``` -Which loads the network for 3600s (1 hour) at a 1000tx/s rate, while continuously -running chaos testing. diff --git a/crates/quake/docs/web-architecture.md b/crates/quake/docs/web-architecture.md deleted file mode 100644 index 3feb78b..0000000 --- a/crates/quake/docs/web-architecture.md +++ /dev/null @@ -1,312 +0,0 @@ -# Quake Web Architecture - -The `quake web` command serves a single-page application for real-time testnet monitoring and control. The backend is an Axum HTTP server; the frontend is a self-contained HTML file with inline CSS and JS using D3.js for graph visualization. - -## Architecture diagram - -``` -Browser (user) - | - | HTTP poll (every refresh_ms) - v -+---------------------------------------------------------------+ -| Axum Web Server (web.rs) | -| | -| GET / HTML SPA (web_index.html, refresh_ms) | -| GET /api/topology TopologyResponse JSON | -| POST /api/node/* Node actions (start/stop/kill/pause/...) | -| POST /api/mempool Mempool toggle (on/off) | -| | -| AppState (shared via Arc>) | -| +------------------+ +------------------+ +--------------+ | -| | ElLiveData | | ContainerStatuses| | Testnet | | -| | heights (map) | | name -> status | | manifest | | -| | peers (map) | | (from Docker) | | nodes_meta | | -| | mempool (map) | | | | infra | | -| | errors (map) | | | | | | -| +--------+---------+ +--------+---------+ +--------------+ | -| | | | -+-----------+----------------------+----------------------------+ - | | - Background tasks | - | | - +--------+--------+ +---+------------+ - | | | | - v v v v -+----------+ +----------+ +------------+ +---------------+ -| EL Node | | EL Node | | Docker | | Docker | -| Task #1 | | Task #N | | Events | | Inspect | -| (WS) | | (WS) | | Subscriber | | Poller | -+----+-----+ +----+-----+ +-----+------+ +------+--------+ - | | | | - | WebSocket | WebSocket | docker events | docker inspect - | | | (real-time) | (periodic) - v v v v -+----------+ +----------+ +---------------------------------+ -| EL Node | | EL Node | | Docker Daemon | -| (reth) | | (reth) | | (local or remote via SSH) | -| | | | | | -| - blocks | | - blocks | | Container states: | -| - peers | | - peers | | running/exited/paused/ | -| - txpool | | - txpool | | disconnected (net_count <= 1) | -+----------+ +----------+ +---------------------------------+ - - +-------------------------------------------+ - | | - | CL HTTP (per topology request) | - | (skips containers not running) | - v v - +-------------+ +-------------+ - | CL Node #1 | ... (parallel) ... | CL Node #N | - | (malachite) | | (malachite) | - | | | | - | /network-state (gossipsub mesh, peers) | | - | /status (proposer, round) | | - +-------------+ +-------------+ -``` - -### Data flow per topology poll - -``` -build_topology() - | - +-- Manifest data (always) - | build_node_list() - | build_manifest_topology_edges() - | build_node_regions() - | resolve_mempool_max() - | - +-- EL cached data (from background WS tasks) - | el_live_data.heights -> node.height - | el_live_data.peers -> EL Peers edges + node.el_peers - | el_live_data.mempool -> node.mempool - | el_live_data.errors -> error messages - | - +-- Docker cached data (from events + inspect) - | container_statuses -> node.cl_status, node.el_status - | populate_container_statuses() - | - +-- CL fresh fetches (parallel HTTP, skips non-running) - | fetch_cl_live_topology() -> CL topic edges + node.cl_peers - | fetch_current_proposer() -> proposer name + node.round - | - +-- Error detection (only when live data available) - | merge_per_node_errors() (CL + EL unified, skips disconnected) - | detect_unreachable_nodes() (unreachable vs disconnected) - | - +-> TopologyResponse JSON - { testnet_name, source, latest_height, current_proposer, - nodes, networks, errors, node_regions, mempool_max } -``` - -### Frontend rendering pipeline - -``` -fetchTopology() --poll--> /api/topology - | - +-- Detect status transitions (shake with 5s cooldown) - +-- Clear stale pending actions (button spinners, 10s timeout) - | - +-- updateStatus() header: testnet name - +-- updateTabs() tab bar: topologies label + network tabs + Graph/Map - +-- updateErrors() error banner (max 20) - +-- renderGraph() if viewMode == 'graph' - | or renderWorldView() if viewMode == 'world' - | | - | +-- resolveEdges() returns edges + directedMode - | +-- buildNodeData() D3 node datum (incl. disconnected flag) - | +-- [persistent peer overlay] purple directional arrows (Manifest tab) - | +-- [EL trusted peer overlay] orange directional arrows (Manifest tab) - | +-- renderNodeCircles() circles with status colors - | +-- renderProposerStar() gold star overlay - | +-- renderMempoolBars() queued/pending columns - | +-- renderNodeLabels() text with outline - | +-- wireNodeInteraction() click/highlight/drag - | +-- drawSubnetHulls() dashed hull shapes (graph only) - | +-- applyHighlight() dim/brighten based on selection/subnet - | - +-- updateDetailPanel() right: node inspector - +-- updateHeightsList() left: heights, rounds, mempool - +-- buildLegend() bottom-left: node types, subnets (clickable) -``` - -## Server - -### Entry point - -`run_server()` in `web.rs` binds the Axum server and spawns background tasks before serving requests. - -### Shared state (`AppState`) - -| Field | Type | Description | -|-------|------|-------------| -| `testnet` | `Arc>` | Manifest, nodes metadata, infra provider | -| `el_live_data` | `Arc>` | Block heights, peer lists, mempool counts, EL errors | -| `container_statuses` | `Arc>` | Docker container status per service name | -| `mempool_active` | `Arc` | Controls whether txpool_status is polled | -| `refresh_ms` | `u64` | Frontend poll interval (injected into HTML) | - -### Routes - -| Route | Method | Description | -|-------|--------|-------------| -| `/` | GET | Serves the HTML SPA with `refresh_ms` injected | -| `/api/topology` | GET | Returns `TopologyResponse` JSON | -| `/api/node/{name}/{action}` | POST | Node/container action. Optional `?target=cl\|el`. Returns `{"ok": true}` or `{"ok": false, "error": "..."}` | -| `/api/mempool/{on\|off}` | POST | Toggle mempool polling | - -### Node actions - -start, stop, restart, kill, pause, unpause, disconnect, reconnect. Actions execute per-container (not batched). Partial failures are reported with error details. - -## Background tasks - -### Per-node EL WebSocket task (`el_node_task`) - -One long-lived async task per EL node. Maintains a single WebSocket connection for all EL data: - -- **Block subscription** (push): `eth_subscribe newHeads` updates `el_live_data.heights` -- **Peer polling** (every `el_refresh_ms`): `admin_peers` via `raw_request` updates `el_live_data.peers` -- **Mempool polling** (every `el_refresh_ms`, only when `mempool_active`): `txpool_status` via `raw_request` updates `el_live_data.mempool` - -Both branches run concurrently via `tokio::select!`. On disconnect, the task records an error in `el_live_data.errors`, preserves the last known height, and reconnects after 2 seconds. Tolerates up to 3 consecutive `admin_peers` failures before reconnecting. - -### Docker events subscriber - -Subscribes to `docker events` for real-time container state detection (millisecond latency). Maps Docker actions to statuses: - -| Docker action | Status | -|---------------|--------| -| die, kill, stop | exited | -| start | running | -| pause | paused | -| unpause | running | - -Reconnects automatically if the event stream ends. stderr suppressed for containers not yet created. - -### Docker inspect poller - -Periodic `docker inspect` (every `container_refresh_ms`) for state that events don't cover: - -- Derives status from `State.Status`, `State.Paused`, `State.Restarting` -- Detects **network disconnection**: a running container with only 1 network (host-access) is reported as `disconnected` -- Container names come from the manifest at startup -- stderr suppressed (containers with `start_at` delays don't exist yet) - -## Topology response assembly - -`build_topology()` reads all caches and assembles the response on each `/api/topology` call. - -### Data sources - -| Source | Data | Availability | -|--------|------|-------------| -| Manifest | Node list, subnets, regions, config, peer expectations, explicit CL/EL peers | Always | -| `region_assignments.json` | Node-to-region mapping | Always (if latency emulation enabled) | -| `el_live_data` | Block heights, EL peers, mempool, EL errors | Best-effort (requires running EL) | -| Docker inspect/events | Container statuses (running, exited, paused, disconnected) | Best-effort (requires Docker) | -| CL HTTP fetches | Gossipsub mesh topology, proposer, rounds | Best-effort (fresh per request, body parsed in parallel) | - -### Assembly pipeline - -1. Build node list from manifest (`build_node_list`) with explicit CL/EL peer lists -2. Build manifest-based network (Manifest Topology from subnet defaults) -3. If live data available: - - Populate heights and container statuses (`populate_container_statuses`) - - Build EL peer edges and detail (`build_el_live_edges`, `populate_el_peer_details`) - - Fetch CL data in parallel (`fetch_cl_live_topology`, `fetch_current_proposer`), skipping containers that are not running - - Merge errors (`merge_per_node_errors`, skips disconnected containers) and detect unreachable/disconnected nodes (`detect_unreachable_nodes`) - - Populate mempool data (`populate_mempool_data`) -4. Build node regions and mempool max from manifest - -### Unreachable vs disconnected - -- **Unreachable** (red): CL or EL failed to respond, container is not in `disconnected` Docker state -- **Disconnected** (orange): failure is due to the container being network-isolated (only host-access network attached) -- Error messages use "disconnected" vs "not responding" accordingly - -## Frontend - -### Poll loop - -`fetchTopology()` runs every `refresh_ms`. On each poll: - -1. Fetch `/api/topology` -2. Detect status transitions (trigger shake with 5-second cooldown to prevent doubles) -3. Clear stale pending actions (button spinners, 10-second timeout) -4. Update all UI components - -### Rendering pipeline - -| Function | Description | -|----------|-------------| -| `updateStatus()` | Header testnet name and browser title | -| `updateTabs()` | Tab bar: topologies label + network tabs (dimmed when empty) + Graph/Map | -| `updateErrors()` | Error banner (max 20 messages) | -| `renderGraph()` / `renderWorldView()` | Main visualization (branched by `viewMode`) | -| `updateDetailPanel()` | Right-side node inspector | -| `updateHeightsList()` | Left sidebar heights, rounds, mempool columns | -| `buildLegend()` | Bottom-left legend (node types, clickable subnets) | - -### Shared rendering helpers - -Extracted to avoid duplication between Graph and Map views: - -| Helper | Description | -|--------|-------------| -| `resolveEdges(net)` | Returns edges and directedMode | -| `buildNodeData(n, activeNodeNames, proposer)` | Builds D3 node datum from GraphNode (includes disconnected flag) | -| `renderNodeCircles(node)` | Appends circle with type-based fill and status-based stroke | -| `renderProposerStar(node, proposer)` | Appends gold star to proposer node | -| `renderMempoolBars(node)` | Appends queued/pending square columns | -| `renderNodeLabels(node)` | Appends text label with outline | -| `wireNodeInteraction(node, link)` | Attaches click handlers, highlight, subnet filter | - -### View modes - -**Graph** (default): D3 force simulation with link, charge, center, collide, and subnet attraction forces. Runs to equilibrium synchronously (no animation). Nodes are draggable and positions persist across polls. Initial zoom-to-fit is instant. Fit-to-page accounts for sidebar and detail panel. - -**Map**: D3 NaturalEarth1 projection with land masses from world-atlas TopoJSON (CDN). Nodes placed at AWS region coordinates, spread in small circles when multiple nodes share a region. Edges drawn as straight lines. Region labels above clusters. - -### Toggles - -| Toggle | Visibility | Backend | Frontend effect | -|--------|-----------|---------|-----------------| -| Show proposer | Always | None | Gold star on proposer node, purple name in heights list | -| Show mempools | Always | POST `/api/mempool/on\|off` | Queued/pending bar columns on nodes, extra columns in heights list | -| Show latencies | Always | None | Inter-region latency labels on edges (from AWS matrix) | -| Show subnets | Always | None | Dashed hull shapes around subnet-grouped nodes (Graph only) | -| Show CL persistent peers | Manifest tab | None | Purple directional arrows from manifest cl_persistent_peers | -| Show EL trusted peers | Manifest tab | None | Orange directional arrows from manifest el_trusted_peers | - -Peer overlay arrows (CL persistent, EL trusted) use inline SVG path elements instead of SVG markers for instant opacity response when selecting nodes. - -### Node detail panel - -Right-side panel, resizable via drag handle: - -1. **Title row**: node name (bold/larger when selected) + action buttons for both layers -2. **Per-layer rows**: CL/EL status badge + per-container action buttons -3. **Peers section** (scrollable): merged CL + EL peer table with columns Dir, C L P E, Score, T I S -4. **Configuration**: non-default manifest fields -5. **Region**: AWS region with city/country label - -### Heights panel - -Left sidebar showing per-node block heights and consensus rounds. Heights are color-coded by lag tiers: at latest, 1 block behind, 2-5 blocks behind, 6+ blocks behind, unreachable (no data). Rounds are color-coded by severity: round 0, round 1, round > 1. - -Resizable via drag handle. Node names are clickable (selects node on graph). A status dot next to each name indicates node type (validator, non-validator) or failure mode (disconnected, unreachable). - -### Subnet filtering - -Clicking a subnet name in the legend dims all nodes not in that subnet and fades unrelated edges (including overlay arrows). Subnet names underline on hover and stay underlined when selected. Click again, click a node, or click the background to clear. - -### Button spinning - -When an action button is clicked, it spins until Docker reports a status change. Tracked via `pendingActions` map with a 10-second timeout. Spinning buttons are disabled (dimmed and non-interactive). Shake animation triggers before the API call for instant feedback. - -### Shake animation - -- Triggers on button click for: restart, stop, kill, pause, disconnect -- Triggers from poll for status transitions to exited, paused, disconnected (terminal perturbations) -- 5-second cooldown after any shake prevents double-shakes diff --git a/crates/quake/files/entrypoint_cl.sh b/crates/quake/files/entrypoint_cl.sh deleted file mode 100755 index 32b19ae..0000000 --- a/crates/quake/files/entrypoint_cl.sh +++ /dev/null @@ -1,36 +0,0 @@ -#!/bin/sh - -# Copyright 2026 Circle Internet Group, Inc. All rights reserved. -# -# SPDX-License-Identifier: Apache-2.0 -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. - - -set -e - -# Set up latency emulation -if [ -f /usr/local/bin/latency_setup.sh ]; then - /usr/local/bin/latency_setup.sh -fi - -# If ARC_LOG_FILE is set, stream stdout/stderr to that file using tee -if [ -n "$ARC_LOG_FILE" ]; then - mkdir -p "$(dirname "$ARC_LOG_FILE")" - rm -f /tmp/arc-log-pipe - mkfifo /tmp/arc-log-pipe - tee -a "$ARC_LOG_FILE" < /tmp/arc-log-pipe & - exec "/usr/local/bin/arc-node-consensus" "$@" > /tmp/arc-log-pipe 2>&1 -else - exec "/usr/local/bin/arc-node-consensus" "$@" -fi diff --git a/crates/quake/files/entrypoint_el.sh b/crates/quake/files/entrypoint_el.sh deleted file mode 100755 index e2ea0df..0000000 --- a/crates/quake/files/entrypoint_el.sh +++ /dev/null @@ -1,40 +0,0 @@ -#!/bin/sh - -# Copyright 2026 Circle Internet Group, Inc. All rights reserved. -# -# SPDX-License-Identifier: Apache-2.0 -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. - - -set -e - -# Remove stale IPC sockets left by a previous SIGKILL / docker kill. -# Without this, Reth fails to bind() on restart with EOPNOTSUPP. -rm -f /sockets/*.ipc - -# Set up latency emulation -if [ -f /usr/local/bin/latency_setup.sh ]; then - /usr/local/bin/latency_setup.sh -fi - -# If ARC_LOG_FILE is set, stream stdout/stderr to that file using tee -if [ -n "$ARC_LOG_FILE" ]; then - mkdir -p "$(dirname "$ARC_LOG_FILE")" - rm -f /tmp/arc-log-pipe - mkfifo /tmp/arc-log-pipe - tee -a "$ARC_LOG_FILE" < /tmp/arc-log-pipe & - exec "/usr/local/bin/arc-node-execution" "$@" > /tmp/arc-log-pipe 2>&1 -else - exec "/usr/local/bin/arc-node-execution" "$@" -fi diff --git a/crates/quake/files/web_index.html b/crates/quake/files/web_index.html deleted file mode 100644 index a909136..0000000 --- a/crates/quake/files/web_index.html +++ /dev/null @@ -1,2145 +0,0 @@ - - - - -Quake Live - - - - -
- Quake Live - -
- -
-
- Manifest expected topology from manifest configuration
- CL Consensus gossipsub mesh for consensus messages
- CL Liveness gossipsub mesh for liveness/keep-alive
- CL Proposal Parts gossipsub mesh for block proposals
- EL Peers execution layer devp2p peer connections -
- -
- Dir CL connection direction: ← inbound, → outbound (who initiated the TCP connection)
- C L P CL gossipsub mesh membership per topic: - connected, - not connected
-   C = Consensus, L = Liveness, P = Proposal Parts
- E CL explicit/persistent peer (direct delivery, bypasses mesh)
- Score CL gossipsub peer score
- T I S EL peer flags from admin_peers:
-   T = Trusted, - I = Inbound, - S = Static
- red dots = layer disconnected
- name = both CL and EL disconnected
-
- Data sources:
- CL columns from each node's /network-state endpoint (gossipsub mesh, peer scores).
- EL columns from each node's admin_peers RPC via WebSocket.
- Peer list = manifest peers + any additional live peers discovered via gossipsub or devp2p. -
- -
-
No topology data yet
- - -
-
-
-
- -
- - - - - - diff --git a/crates/quake/macros/Cargo.toml b/crates/quake/macros/Cargo.toml deleted file mode 100644 index a15a312..0000000 --- a/crates/quake/macros/Cargo.toml +++ /dev/null @@ -1,12 +0,0 @@ -[package] -name = "quake-macros" -version = "0.1.0" -edition = "2021" - -[lib] -proc-macro = true - -[dependencies] -syn = { version = "2.0", features = ["full"] } -quote = "1.0" -proc-macro2 = "1.0" diff --git a/crates/quake/macros/src/lib.rs b/crates/quake/macros/src/lib.rs deleted file mode 100644 index 4b8e7e5..0000000 --- a/crates/quake/macros/src/lib.rs +++ /dev/null @@ -1,136 +0,0 @@ -// Copyright 2026 Circle Internet Group, Inc. All rights reserved. -// -// SPDX-License-Identifier: Apache-2.0 -// -// Licensed under the Apache License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. -// You may obtain a copy of the License at -// -// http://www.apache.org/licenses/LICENSE-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// See the License for the specific language governing permissions and -// limitations under the License. - -use proc_macro::TokenStream; -use quote::quote; -use syn::{parse_macro_input, ItemFn, Meta, MetaNameValue}; - -/// Attribute macro for registering test functions. -/// -/// # Example -/// ```ignore -/// #[quake_test(group = "probe", name = "connectivity")] -/// fn connectivity_test<'a>( -/// testnet: &'a Testnet, -/// factory: &'a RpcClientFactory, -/// _params: &'a TestParams, -/// ) -> TestResult<'a> { -/// Box::pin(async move { -/// // test implementation -/// }) -/// } -/// -/// // Disabled test (will not run) -/// #[quake_test(group = "probe", name = "flaky", disabled = true)] -/// fn flaky_test<'a>( -/// testnet: &'a Testnet, -/// factory: &'a RpcClientFactory, -/// _params: &'a TestParams, -/// ) -> TestResult<'a> { -/// Box::pin(async move { -/// // test implementation -/// }) -/// } -/// ``` -#[proc_macro_attribute] -pub fn quake_test(attr: TokenStream, item: TokenStream) -> TokenStream { - let input_fn = parse_macro_input!(item as ItemFn); - - // Parse attribute tokens to extract group, name, and disabled - let mut group = None; - let mut name = None; - let mut disabled = false; - - let attr_parser = syn::parse::Parser::parse2( - |input: syn::parse::ParseStream| { - while !input.is_empty() { - let meta = input.parse::()?; - if let Meta::NameValue(MetaNameValue { path, value, .. }) = meta { - if path.is_ident("group") { - if let syn::Expr::Lit(syn::ExprLit { - lit: syn::Lit::Str(lit_str), - .. - }) = value - { - group = Some(lit_str.value()); - } - } else if path.is_ident("name") { - if let syn::Expr::Lit(syn::ExprLit { - lit: syn::Lit::Str(lit_str), - .. - }) = value - { - name = Some(lit_str.value()); - } - } else if path.is_ident("disabled") { - if let syn::Expr::Lit(syn::ExprLit { - lit: syn::Lit::Bool(lit_bool), - .. - }) = value - { - disabled = lit_bool.value; - } - } - } - - // Handle comma separator - if input.peek(syn::Token![,]) { - input.parse::()?; - } - } - Ok(()) - }, - proc_macro2::TokenStream::from(attr), - ); - - if let Err(e) = attr_parser { - return e.to_compile_error().into(); - } - - let group = group.expect("quake_test macro requires 'group' attribute"); - let name = name.expect("quake_test macro requires 'name' attribute"); - - let fn_name = &input_fn.sig.ident; - - // Generate a unique constant name to enforce uniqueness at compile time - // If two tests have the same group+name, this will cause a duplicate const error - let uniqueness_const = format!( - "_QUAKE_TEST_REGISTRATION_{}_{}", - group.to_uppercase().replace("-", "_"), - name.to_uppercase().replace("-", "_") - ); - let uniqueness_ident = syn::Ident::new(&uniqueness_const, proc_macro2::Span::call_site()); - - // Generate the registration code - let expanded = quote! { - #input_fn - - // Compile-time uniqueness check: this const will conflict if group+name is duplicated - #[allow(non_upper_case_globals)] - const #uniqueness_ident: () = (); - - ::inventory::submit! { - crate::tests::TestRegistration { - group: #group, - name: #name, - test_fn: #fn_name, - disabled: #disabled, - } - } - }; - - TokenStream::from(expanded) -} diff --git a/crates/quake/scenarios/db-upgrade-v0-v1.toml b/crates/quake/scenarios/db-upgrade-v0-v1.toml deleted file mode 100644 index 4340650..0000000 --- a/crates/quake/scenarios/db-upgrade-v0-v1.toml +++ /dev/null @@ -1,28 +0,0 @@ -name = "db-upgrade" -description = "DB upgrade testing scenario with 3 validators. Tests upgrade of a single validator (call perturb upgrade validator1)." - -# Use RPC connection for Engine API -engine_api_connection = "rpc" - -# Starting versions (what the testnet begins with). -# Both starting and upgrade images must be Modern (>= v0.5.0) — the upgrade -# path does not currently regenerate CLI flags when crossing the legacy/modern -# boundary, so Legacy-to-Modern upgrades would start the new binary with no -# flags. -# IMAGE_REGISTRY_URL should be defined as an environment variable or in the .env file. -image_cl="${IMAGE_REGISTRY_URL}/arc-consensus:0.5.0" -image_el="${IMAGE_REGISTRY_URL}/arc-execution:0.5.0" - -# Upgrade versions (what we upgrade to during the test) -image_cl_upgrade="arc_consensus:latest" -image_el_upgrade="arc_execution:latest" - -# Global config -cl.config.log_level = "info" - -[nodes.validator1] -[nodes.validator2] -[nodes.validator3] - -[nodes.full1] -[nodes.full2] diff --git a/crates/quake/scenarios/examples/10nodes.toml b/crates/quake/scenarios/examples/10nodes.toml deleted file mode 100644 index 04aa75f..0000000 --- a/crates/quake/scenarios/examples/10nodes.toml +++ /dev/null @@ -1,18 +0,0 @@ -# Global config -cl.config.log_level = "info" - -[nodes.validator1] -[nodes.validator2] -[nodes.validator3] -[nodes.validator4] -[nodes.validator5] -[nodes.validator6] -[nodes.validator7] -start_at = 10 - -[nodes.full1] -[nodes.full2] -start_at = 20 - -[nodes.full3] -start_at = 10 diff --git a/crates/quake/scenarios/examples/10nodes_simple.toml b/crates/quake/scenarios/examples/10nodes_simple.toml deleted file mode 100644 index 939c862..0000000 --- a/crates/quake/scenarios/examples/10nodes_simple.toml +++ /dev/null @@ -1,10 +0,0 @@ -[nodes.validator1] -[nodes.validator2] -[nodes.validator3] -[nodes.validator4] -[nodes.validator5] -[nodes.validator6] -[nodes.validator7] -[nodes.full1] -[nodes.full2] -[nodes.full3] diff --git a/crates/quake/scenarios/examples/14nodes.toml b/crates/quake/scenarios/examples/14nodes.toml deleted file mode 100644 index 2922dfe..0000000 --- a/crates/quake/scenarios/examples/14nodes.toml +++ /dev/null @@ -1,81 +0,0 @@ -# 14 nodes (7 validators, 4 sentries, 1 relay, 2 full) across 6 subnets. -# Mostly hub-and-spoke: each sentry bridges one isolated subnet to untrusted, -# except group1 which uses a two-hop bridge (sentry2 + relay1). -# Validators are isolated within a single subnet; full nodes sit on untrusted only. -# -# ┌──────────────┐ ┌────────────────┐ ┌──────────────┐ ┌──────────────┐ -# │ _trusted_ │ │ _group1_ext_ │ │ _group2_ │ │ _external_ │ -# │ │ │ │ │ │ │ │ -# │ validator1 │ │ validator3 │ │ validator5 │ │ validator6 │ -# │ validator2 │ │ validator4 │ │ │ │ validator7 │ -# └──────┬───────┘ └───────┬────────┘ └──────┬───────┘ └──────┬───────┘ -# sentry1 sentry2 sentry3 sentry4 -# │ ┌─────┴──────┐ │ │ -# │ │ _group1_ │ │ │ -# │ │ relay1 │ │ │ -# │ └─────┬──────┘ │ │ -# ┌──────┴──────────────────┴──────────────────┴─────────────────┴─────┐ -# │ _untrusted_ │ -# │ full1 full2 │ -# └────────────────────────────────────────────────────────────────────┘ - -cl.config.discovery_num_inbound_peers = 2 -cl.config.discovery_num_outbound_peers = 2 - -[node_groups] -TRUSTED_VALIDATORS = ["validator1", "validator2"] -GROUP1_VALIDATORS = ["validator3", "validator4"] -EXTERNAL_VALIDATORS = ["validator6", "validator7"] - -[nodes.validator1] -subnets = ["trusted"] -cl_persistent_peers = ["TRUSTED_VALIDATORS"] -cl_gossipsub.explicit_peering = true -el.config.trusted_peers = ["TRUSTED_VALIDATORS"] - -[nodes.validator2] -subnets = ["trusted"] -cl_persistent_peers = ["TRUSTED_VALIDATORS"] - -[nodes.validator3] -subnets = ["group1_ext"] - -[nodes.validator4] -subnets = ["group1_ext"] - -[nodes.validator5] -subnets = ["group2"] - -[nodes.validator6] -subnets = ["external"] -cl_persistent_peers = ["EXTERNAL_VALIDATORS"] - -[nodes.validator7] -subnets = ["external"] -el.config.trusted_peers = ["EXTERNAL_VALIDATORS"] - -[nodes.sentry1] -subnets = ["trusted", "untrusted"] -cl_gossipsub.mesh_prioritization = true -cl_gossipsub.load = "high" -el.config.trusted_peers = ["TRUSTED_VALIDATORS"] -el.config.disable_discovery = true - -[nodes.relay1] -subnets = ["group1", "untrusted"] - -[nodes.sentry2] -subnets = ["group1", "group1_ext"] -el.config.trusted_peers = ["GROUP1_VALIDATORS"] - -[nodes.sentry3] -subnets = ["group2", "untrusted"] - -[nodes.sentry4] -subnets = ["external", "untrusted"] - -[nodes.full1] -subnets = ["untrusted"] - -[nodes.full2] -subnets = ["untrusted"] diff --git a/crates/quake/scenarios/examples/3nodes.toml b/crates/quake/scenarios/examples/3nodes.toml deleted file mode 100644 index 0e961db..0000000 --- a/crates/quake/scenarios/examples/3nodes.toml +++ /dev/null @@ -1,7 +0,0 @@ -# Minimal 3-validator network. Works with the default Average load profile -# even though mesh_n_low=4 exceeds the 2 available peers. Gossipsub treats -# mesh_n_low as a graft trigger, not a hard minimum, so nodes simply attempt -# to add peers on each heartbeat and proceed when none are available. -[nodes.validator1] -[nodes.validator2] -[nodes.validator3] diff --git a/crates/quake/scenarios/examples/5nodes.toml b/crates/quake/scenarios/examples/5nodes.toml deleted file mode 100644 index 2fb85af..0000000 --- a/crates/quake/scenarios/examples/5nodes.toml +++ /dev/null @@ -1,5 +0,0 @@ -[nodes.validator1] -[nodes.validator2] -[nodes.validator3] -[nodes.validator4] -[nodes.validator5] diff --git a/crates/quake/scenarios/examples/5nodes_upgrade.toml b/crates/quake/scenarios/examples/5nodes_upgrade.toml deleted file mode 100644 index 4cd1e79..0000000 --- a/crates/quake/scenarios/examples/5nodes_upgrade.toml +++ /dev/null @@ -1,13 +0,0 @@ -engine_api_connection = "rpc" - -# IMAGE_REGISTRY_URL should be defined as an environment variable or in the .env file. -image_cl="${IMAGE_REGISTRY_URL}/arc-consensus:0.5.0-rc1" -image_el="${IMAGE_REGISTRY_URL}/arc-execution:0.5.0-rc1" -image_cl_upgrade="arc_consensus:latest" -image_el_upgrade="arc_execution:latest" - -[nodes.validator1] -[nodes.validator2] -[nodes.validator3] -[nodes.validator4] -[nodes.validator5] diff --git a/crates/quake/scenarios/examples/arc-node.toml b/crates/quake/scenarios/examples/arc-node.toml deleted file mode 100644 index 11fefbc..0000000 --- a/crates/quake/scenarios/examples/arc-node.toml +++ /dev/null @@ -1,67 +0,0 @@ -# Follow Mode + Pruning + Snapshot Recovery Test -# -# Test plan: -# 1. Network starts: 4 validators + full-p2p + snapshot (syncs via libp2p) + arc-node (RPC follow) -# 2. Wait for ~120 blocks (~60s) so pruning kicks in -# 3. Stop snapshot, take EL+CL snapshot, restart snapshot -# 4. Copy snapshot's data to arc-node's data directory -# 5. Start arc-node (catches up via RPC follow from full-p2p) -# 6. Query arc-node: eth_getBalance, eth_getTransactionByHash, eth_getLogs -# -# Topology: -# validators ──── full-p2p ──── snapshot -# │ -# arc-node (RPC follow + tx forwarding via full-p2p) -# -# Pruning profiles: -# - Validators: --minimal preset + bodies.distance=100 for short test runs -# - full-p2p: default (archive) — serves as follow target and tx forwarder -# - snapshot: --full preset + bodies.distance=100 (serves queries + snapshots) -# - arc-node: --full preset + bodies.distance=100 (serves RPC queries after recovery) -# -# bodies.distance=100 gives ~50s at 0.5s blocks — enough for snapshot operations. -# Production would use the preset default (237600 / ~33hr). - -# Global defaults: --minimal preset with short bodies retention for testing -el.config.prune.preset = "minimal" -el.config.prune.bodies.distance = 100 -el.config.prune.block-interval = 5 - -cl.config.prune_certificates_distance = 100 - -# Trusted perimeter: only propagate txs to trusted peers. -# Validators, full-p2p, and snapshot gossip freely among themselves; -# arc-node (external) is excluded and receives no transaction gossip. -el.config.trusted_peers = ["ALL_VALIDATORS", "full-p2p", "snapshot"] -el.config.tx_propagation_policy = "Trusted" - -[nodes.validator1] -[nodes.validator2] -[nodes.validator3] -[nodes.validator4] - -# full-p2p is the only externally-exposed node; narrow the RPC surface to -# the safe set for MEV protection. Pending-tx hiding is the default, so no -# explicit flag is needed. arc-node forwards transactions here via -# --rpc.forwarder. -[nodes.full-p2p] -el.config.tx_propagation_policy = "Trusted" -el.config.http.api = ["eth", "net", "web3"] -el.config.ws.api = ["eth", "net", "web3"] - -# libp2p sync-only full node — --full profile (snapshot source + follow target) -[nodes.snapshot] -start_at = 10 -cl.config.no_consensus = true -el.config.prune.preset = "full" - -# RPC follow node — quake starts it at block 11, script stops it immediately, -# restores from snapshot, then restarts via docker compose. -# Transactions submitted to arc-node are forwarded to full-p2p via --rpc.forwarder. -[nodes.arc-node] -follow = true -start_at = 11 -follow_endpoints = ["full-p2p"] -el.config.trusted_peers = [] -el.config.prune.preset = "full" -el.config.rpc.forwarder = "http://full-p2p_el:8545" diff --git a/crates/quake/scenarios/examples/multi_subnets.toml b/crates/quake/scenarios/examples/multi_subnets.toml deleted file mode 100644 index 00bdabd..0000000 --- a/crates/quake/scenarios/examples/multi_subnets.toml +++ /dev/null @@ -1,16 +0,0 @@ -# 5 nodes across multiple isolated sub-networks (default, trusted, and untrusted) -# with validator3 bridging trusted/untrusted and validator4 bridging untrusted/default - -[nodes.validator1] -subnets = ["trusted"] - -[nodes.validator2] -subnets = ["trusted"] - -[nodes.validator3] -subnets = ["trusted", "untrusted"] - -[nodes.validator4] -subnets = ["untrusted", "default"] - -[nodes.full1] diff --git a/crates/quake/scenarios/examples/rpc-sync.toml b/crates/quake/scenarios/examples/rpc-sync.toml deleted file mode 100644 index 1e7fce0..0000000 --- a/crates/quake/scenarios/examples/rpc-sync.toml +++ /dev/null @@ -1,42 +0,0 @@ -# Follow Mode Example -# -# This scenario demonstrates follow mode where a full node -# fetches blocks from validators via RPC instead of P2P consensus. -# -# - 4 validators participate in consensus -# - 1 full node syncs via libp2p, with consensus disabled (sync_only mode) -# - Multiple full nodes sync via RPC (follow mode) - -[nodes.validator1] -[nodes.validator2] -[nodes.validator3] -[nodes.validator4] - -# libp2p sync-only full node (no consensus/gossip, only sync protocol) -[nodes.full_p2p] -cl.config.no_consensus = true - -# Follow full nodes -[nodes.full1] -follow = true -follow_endpoints = ["validator1", "validator2", "validator3", "validator4"] - -[nodes.full2] -start_at = 100 -follow = true -follow_endpoints = ["full1"] - -[nodes.full3] -start_at = 200 -follow = true -follow_endpoints = ["full2"] - -[nodes.full4] -start_at = 200 -follow = true -follow_endpoints = ["full3"] - -[nodes.full5] -start_at = 200 -follow = true -follow_endpoints = ["full4"] diff --git a/crates/quake/scenarios/examples/sentry.toml b/crates/quake/scenarios/examples/sentry.toml deleted file mode 100644 index 9ef577e..0000000 --- a/crates/quake/scenarios/examples/sentry.toml +++ /dev/null @@ -1,96 +0,0 @@ -# Symmetric Explicit Peering Sentry Topology -# -# Demonstrates symmetric explicit peering between validators and sentries. -# Both validators and sentries list each other as persistent peers with -# explicit peering enabled, guaranteeing eager push message delivery -# regardless of mesh membership. -# -# GossipSub explicit peering must be symmetric — both sides must list each -# other. If only one side enables it, the explicit side will PRUNE GRAFTs -# from the other, preventing mesh formation. See sentry_asymmetric.toml for -# an asymmetric alternative using mesh prioritization. -# -# Network topology: -# -# validator1 ──┐ -# validator2 ──┤── sentry-1 ◄── full1 -# validator3 ──┤── sentry-2 ◄── full2 -# validator4 ──┘ -# -# (◄── means full nodes connect out to sentries; sentries do not list full nodes as peers) -# -# Validators peer with all sentries and other validators (explicit, persistent_peers_only). -# Sentries peer with all validators and each other (explicit, high load). - -# Validators only peer with their sentries and other validators. -[nodes.validator1] -cl_persistent_peers = ["sentry-1", "sentry-2", "ALL_VALIDATORS"] -cl_persistent_peers_only = true -cl_gossipsub.explicit_peering = true -subnets = ["trusted"] -[nodes.validator1.el.config] -trusted_peers = ["sentry-1", "sentry-2", "ALL_VALIDATORS"] -trusted_only = true - -[nodes.validator2] -cl_persistent_peers = ["sentry-1", "sentry-2", "ALL_VALIDATORS"] -cl_persistent_peers_only = true -cl_gossipsub.explicit_peering = true -subnets = ["trusted"] -[nodes.validator2.el.config] -trusted_peers = ["sentry-1", "sentry-2", "ALL_VALIDATORS"] -trusted_only = true - -[nodes.validator3] -cl_persistent_peers = ["sentry-1", "sentry-2", "ALL_VALIDATORS"] -cl_persistent_peers_only = true -cl_gossipsub.explicit_peering = true -subnets = ["trusted"] -[nodes.validator3.el.config] -trusted_peers = ["sentry-1", "sentry-2", "ALL_VALIDATORS"] -trusted_only = true - -[nodes.validator4] -cl_persistent_peers = ["sentry-1", "sentry-2", "ALL_VALIDATORS"] -cl_persistent_peers_only = true -cl_gossipsub.explicit_peering = true -subnets = ["trusted"] -[nodes.validator4.el.config] -trusted_peers = ["sentry-1", "sentry-2", "ALL_VALIDATORS"] -trusted_only = true - -# Sentries connect only to their validators and other sentries (full nodes connect out to sentries, not the reverse). -[nodes.sentry-1] -cl_persistent_peers = ["sentry-2", "ALL_VALIDATORS"] -cl_gossipsub.explicit_peering = true -cl_gossipsub.load = "high" -subnets = ["trusted", "untrusted"] -[nodes.sentry-1.el.config] -trusted_peers = ["ALL_VALIDATORS"] -tx_propagation_policy = "Trusted" -disable_discovery = true - -[nodes.sentry-2] -cl_persistent_peers = ["sentry-1", "ALL_VALIDATORS"] -cl_gossipsub.explicit_peering = true -cl_gossipsub.load = "high" -subnets = ["trusted", "untrusted"] -[nodes.sentry-2.el.config] -trusted_peers = ["ALL_VALIDATORS"] -tx_propagation_policy = "Trusted" -disable_discovery = true - -# Full nodes only peer with their sentry. -[nodes.full1] -cl_persistent_peers = ["sentry-1"] -subnets = ["untrusted"] -[nodes.full1.el.config] -trusted_peers = ["sentry-1"] -tx_propagation_policy = "Trusted" - -[nodes.full2] -cl_persistent_peers = ["sentry-2"] -subnets = ["untrusted"] -[nodes.full2.el.config] -trusted_peers = ["sentry-2"] -tx_propagation_policy = "Trusted" diff --git a/crates/quake/scenarios/examples/sentry_asymmetric.toml b/crates/quake/scenarios/examples/sentry_asymmetric.toml deleted file mode 100644 index 97b5d32..0000000 --- a/crates/quake/scenarios/examples/sentry_asymmetric.toml +++ /dev/null @@ -1,83 +0,0 @@ -# Explicit Peering Sentry Topology -# -# Demonstrates asymmetric explicit peering: validators use explicit peering -# among themselves for guaranteed eager push, while sentries use mesh -# prioritization and high gossipsub load to relay messages to/from validators. -# -# Explicit peering cannot be asymmetric in GossipSub — if a sentry marks a -# validator as an explicit peer, the sentry will PRUNE any GRAFT from that -# validator, preventing normal mesh formation. Instead, sentries rely on -# mesh prioritization (persistent peers scored higher) and high load profile -# (mesh_n=10) to keep validators in their mesh. -# -# Network topology: -# -# validator1 ──┐ -# validator2 ──┤── sentry-1 ◄── full1 -# validator3 ──┤── sentry-2 ◄── full2 -# validator4 ──┘ -# -# (◄── means full nodes connect out to sentries; sentries do not list full nodes as peers) -# -# Validators peer only with other validators (explicit peering). -# Sentries peer with all validators and each other (mesh prioritization + high load). - -# Validators only peer with other validators in the same network. -# Explicit peering ensures all other validators will receive the eager push regardless of the mesh_n factor -[nodes.validator1] -subnets = ["trusted"] -cl_persistent_peers = ["ALL_VALIDATORS"] -cl_gossipsub.explicit_peering = true -el.config.trusted_peers = ["ALL_VALIDATORS"] - -[nodes.validator2] -subnets = ["trusted"] -cl_persistent_peers = ["ALL_VALIDATORS"] -cl_gossipsub.explicit_peering = true -el.config.trusted_peers = ["ALL_VALIDATORS"] - -[nodes.validator3] -subnets = ["trusted"] -cl_persistent_peers = ["ALL_VALIDATORS"] -cl_gossipsub.explicit_peering = true -el.config.trusted_peers = ["ALL_VALIDATORS"] - -[nodes.validator4] -subnets = ["trusted"] -cl_persistent_peers = ["ALL_VALIDATORS"] -cl_gossipsub.explicit_peering = true -el.config.trusted_peers = ["ALL_VALIDATORS"] - - -# Sentries connect only to their validators and other sentries (full nodes connect out to sentries, not the reverse). -# Gossibsub explicit peering cannot be asymmetric (validators don't includes sentries as persistent peers) -# Instead, enable mesh prioritization to ensure sentries eager push messages to validators and other sentries -# Increase the mesh size on validators to accomodate all other nodes. -# Note: explicit peers do not count against the mesh size. With "high" this means at most 15 other (full) nodes can directly mesh with this sentry - -[nodes.sentry-1] -subnets = ["trusted", "untrusted"] -cl_persistent_peers = ["sentry-2", "ALL_VALIDATORS"] -cl_gossipsub.mesh_prioritization = true -cl_gossipsub.load = "high" -el.config.trusted_peers = ["sentry-2", "ALL_VALIDATORS"] - -[nodes.sentry-2] -subnets = ["trusted", "untrusted"] -cl_persistent_peers = ["sentry-1", "ALL_VALIDATORS"] -cl_gossipsub.mesh_prioritization = true -cl_gossipsub.load = "high" -el.config.trusted_peers = ["sentry-1", "ALL_VALIDATORS"] - -# Full nodes only peer with their sentry. -[nodes.full1] -subnets = ["untrusted"] -cl_persistent_peers = ["sentry-1"] -el.config.trusted_peers = ["sentry-1"] -el.config.tx_propagation_policy = "Trusted" - -[nodes.full2] -subnets = ["untrusted"] -cl_persistent_peers = ["sentry-2"] -el.config.trusted_peers = ["sentry-2"] -el.config.tx_propagation_policy = "Trusted" diff --git a/crates/quake/scenarios/examples/testnet-small-default.toml b/crates/quake/scenarios/examples/testnet-small-default.toml deleted file mode 100644 index 3f994bd..0000000 --- a/crates/quake/scenarios/examples/testnet-small-default.toml +++ /dev/null @@ -1,141 +0,0 @@ -name = "testnet-small-default" -description = "Topology with 6 geo-distributed validators, 2 sentries, 1 RPC full node, 1 snapshot node, 1 arc node." - -# ============================================================================== -# GLOBAL CONFIGURATION -# ============================================================================== -el.config.engine.legacy_state_root = true - -cl.config.discovery_num_inbound_peers = 50 -cl.config.discovery_num_outbound_peers = 50 - -latency_emulation = true -engine_api_connection = "rpc" -monitoring_bind_host = "0.0.0.0" - -# ============================================================================== -# NODE GROUPS -# ============================================================================== - -[node_groups] -CORE_VALIDATORS = ["validator1", "validator2", "validator3", "validator4", "validator5"] -RPC_NODES = ["rpc-full", "arc-node"] - -# ============================================================================== -# TOPOLOGY -# -# [private1] [public] [private2] -# validator1 ──┐ -# validator2 ──┤ -# validator3 ──┼── sentry-1 ──── sentry-2 ──── validator6 -# validator4 ──┤ │ \ -# validator5 ──┘ │ ------ -# | | -# rpc-full snapshot -# | | -# | ------ -# | / -# arc-node -# ============================================================================== - -# --- Core validators (5 across 5 regions) on private1 --- - -[nodes.validator1] -region = "us-east-2" -subnets = ["private1"] -cl_persistent_peers = ["CORE_VALIDATORS"] -el.config.trusted_peers = ["CORE_VALIDATORS"] - -[nodes.validator2] -region = "us-west-2" -subnets = ["private1"] -cl_persistent_peers = ["CORE_VALIDATORS"] -el.config.trusted_peers = ["CORE_VALIDATORS"] - -[nodes.validator3] -region = "eu-central-1" -subnets = ["private1"] -cl_persistent_peers = ["CORE_VALIDATORS"] -el.config.trusted_peers = ["CORE_VALIDATORS"] - -[nodes.validator4] -region = "ap-northeast-1" -subnets = ["private1"] -cl_persistent_peers = ["CORE_VALIDATORS"] -el.config.trusted_peers = ["CORE_VALIDATORS"] - -[nodes.validator5] -region = "eu-west-1" -subnets = ["private1"] -cl_persistent_peers = ["CORE_VALIDATORS"] -el.config.trusted_peers = ["CORE_VALIDATORS"] - -# --- Sentries --- - -[nodes.sentry-1] -region = "us-east-2" -subnets = ["private1", "public"] -cl_persistent_peers = ["sentry-2", "CORE_VALIDATORS"] -el.config.trusted_peers = ["sentry-2", "CORE_VALIDATORS"] -el.config.disable_discovery = true - -[nodes.sentry-2] -region = "eu-central-1" -subnets = ["public", "private2"] -external = true -start_at = 25 -cl_persistent_peers = ["sentry-1", "validator6"] -el.config.trusted_peers = ["sentry-1", "validator6"] -el.config.disable_discovery = true - -# --- External validator connected via sentry chain on private2 --- - -[nodes.validator6] -region = "ap-southeast-1" -subnets = ["private2"] -external = true -start_at = 30 -cl_persistent_peers = [] -el.config.trusted_peers = [] - -# --- RPC full node (non-validator, externally exposed) --- - -[nodes.rpc-full] -region = "us-west-2" -subnets = ["public"] -external = true -start_at = 25 -cl_persistent_peers = ["sentry-1"] -el.config.trusted_peers = ["sentry-1"] -el.config.tx_propagation_policy = "Trusted" -el.config.disable_discovery = true -el.config.http.api = ["eth", "net", "web3"] -el.config.ws.api = ["eth", "net", "web3"] -el.config.prune.preset = "full" - -# --- Snapshot node (full profile, snapshot source) --- - -[nodes.snapshot] -region = "us-east-2" -subnets = ["public"] -external = true -start_at = 30 -cl_persistent_peers = ["sentry-1"] -cl.config.no_consensus = true -cl.config.prune_certificates_distance = 100 -el.config.trusted_peers = ["sentry-1"] -el.config.disable_discovery = true -el.config.prune.preset = "full" - -# --- Arc node (RPC follow from rpc-full and snapshot) --- - -[nodes.arc-node] -region = "us-east-2" -subnets = ["public"] -external = true -start_at = 30 -follow = true -follow_endpoints = ["rpc-full"] -el.config.trusted_peers = ["rpc-full"] -el.config.prune.preset = "full" -el.config.rpc.forwarder = "http://sentry-1_el:8545" diff --git a/crates/quake/scenarios/examples/testnet-small.toml b/crates/quake/scenarios/examples/testnet-small.toml deleted file mode 100644 index fe499b2..0000000 --- a/crates/quake/scenarios/examples/testnet-small.toml +++ /dev/null @@ -1,153 +0,0 @@ -name = "testnet-small" -description = "Topology with 6 geo-distributed validators, 2 sentries, 1 RPC full node, 1 snapshot node, 1 arc node." - -# ============================================================================== -# GLOBAL CONFIGURATION -# ============================================================================== -el.config.engine.legacy_state_root = true - -cl.config.discovery_num_inbound_peers = 50 -cl.config.discovery_num_outbound_peers = 50 - -latency_emulation = true -engine_api_connection = "rpc" -monitoring_bind_host = "0.0.0.0" - -# ============================================================================== -# NODE GROUPS -# ============================================================================== - -[node_groups] -CORE_VALIDATORS = ["validator1", "validator2", "validator3", "validator4", "validator5"] -RPC_NODES = ["rpc-full", "arc-node"] - -# ============================================================================== -# TOPOLOGY -# -# [private1] [public] [private2] -# validator1 ──┐ -# validator2 ──┤ -# validator3 ──┼── sentry-1 ──── sentry-2 ──── validator6 -# validator4 ──┤ │ \ -# validator5 ──┘ │ ------ -# | | -# rpc-full snapshot -# | | -# | ------ -# | / -# arc-node -# ============================================================================== - -# --- Core validators (5 across 5 regions) on private1 --- -# Explicit peering among validators guarantees eager push regardless of mesh_n. - -[nodes.validator1] -region = "us-east-2" -subnets = ["private1"] -cl_persistent_peers = ["CORE_VALIDATORS"] -cl_gossipsub.explicit_peering = true -el.config.trusted_peers = ["CORE_VALIDATORS"] - -[nodes.validator2] -region = "us-west-2" -subnets = ["private1"] -cl_persistent_peers = ["CORE_VALIDATORS"] -cl_gossipsub.explicit_peering = true -el.config.trusted_peers = ["CORE_VALIDATORS"] - -[nodes.validator3] -region = "eu-central-1" -subnets = ["private1"] -cl_persistent_peers = ["CORE_VALIDATORS"] -cl_gossipsub.explicit_peering = true -el.config.trusted_peers = ["CORE_VALIDATORS"] - -[nodes.validator4] -region = "ap-northeast-1" -subnets = ["private1"] -cl_persistent_peers = ["CORE_VALIDATORS"] -cl_gossipsub.explicit_peering = true -el.config.trusted_peers = ["CORE_VALIDATORS"] - -[nodes.validator5] -region = "eu-west-1" -subnets = ["private1"] -cl_persistent_peers = ["CORE_VALIDATORS"] -cl_gossipsub.explicit_peering = true -el.config.trusted_peers = ["CORE_VALIDATORS"] - -# --- Sentries --- -# Asymmetric: sentries use mesh prioritization + high load instead of explicit peering -# (explicit peering would cause sentries to PRUNE GRAFTs from validators). - -[nodes.sentry-1] -region = "us-east-2" -subnets = ["private1", "public"] -cl_persistent_peers = ["sentry-2", "CORE_VALIDATORS"] -cl_gossipsub.mesh_prioritization = true -cl_gossipsub.load = "high" -el.config.trusted_peers = ["sentry-2", "CORE_VALIDATORS"] -el.config.disable_discovery = true - -[nodes.sentry-2] -region = "eu-central-1" -subnets = ["public", "private2"] -external = true -start_at = 25 -cl_persistent_peers = ["sentry-1", "validator6"] -cl_gossipsub.mesh_prioritization = true -cl_gossipsub.load = "high" -el.config.trusted_peers = ["sentry-1", "validator6"] -el.config.disable_discovery = true - -# --- External validator connected via sentry chain on private2 --- - -[nodes.validator6] -region = "ap-southeast-1" -subnets = ["private2"] -external = true -start_at = 30 -cl_persistent_peers = [] -el.config.trusted_peers = [] - -# --- RPC full node (non-validator, externally exposed) --- - -[nodes.rpc-full] -region = "us-west-2" -subnets = ["public"] -external = true -start_at = 25 -cl_persistent_peers = ["sentry-1"] -el.config.trusted_peers = ["sentry-1"] -el.config.tx_propagation_policy = "Trusted" -el.config.disable_discovery = true -el.config.http.api = ["eth", "net", "web3"] -el.config.ws.api = ["eth", "net", "web3"] -el.config.prune.preset = "full" - -# --- Snapshot node (full profile, snapshot source) --- - -[nodes.snapshot] -region = "us-east-2" -subnets = ["public"] -external = true -start_at = 30 -cl_persistent_peers = ["sentry-1"] -cl.config.no_consensus = true -cl.config.prune_certificates_distance = 100 -el.config.trusted_peers = ["sentry-1"] -el.config.disable_discovery = true -el.config.prune.preset = "full" - -# --- Arc node (RPC follow from rpc-full and snapshot) --- - -[nodes.arc-node] -region = "us-east-2" -subnets = ["public"] -external = true -start_at = 30 -follow = true -follow_endpoints = ["rpc-full"] -el.config.trusted_peers = ["rpc-full"] -el.config.prune.preset = "full" -el.config.rpc.forwarder = "http://sentry-1_el:8545" diff --git a/crates/quake/scenarios/localdev-remote-signer.toml b/crates/quake/scenarios/localdev-remote-signer.toml deleted file mode 100644 index 4eafc98..0000000 --- a/crates/quake/scenarios/localdev-remote-signer.toml +++ /dev/null @@ -1,18 +0,0 @@ -[nodes.validator1] -remote_signer = 1 - -[nodes.validator2] -remote_signer = 2 - -[nodes.validator3] -remote_signer = 3 - -[nodes.validator4] -# Triggers an error if uncommented, -# because only 3 predefined keys for remote signers are currently available -# remote_signer = 4 - -[nodes.validator5] -# Triggers an error if uncommented, -# because only 3 predefined keys for remote signers are currently available -# remote_signer = 5 diff --git a/crates/quake/scenarios/localdev.toml b/crates/quake/scenarios/localdev.toml deleted file mode 100644 index a997c75..0000000 --- a/crates/quake/scenarios/localdev.toml +++ /dev/null @@ -1,25 +0,0 @@ -# Enable persistence backpressure in CI to exercise the feature until it is -# enabled by default. -cl.config.execution_persistence_backpressure = true - -# Each validator uses a distinct, visually-distinctive fee recipient so tests -# can tell which validator proposed a given block. Paired with genesis where -# `ProtocolConfig.rewardBeneficiary = 0x0`, this causes the EL to honour each -# validator's `--suggested-fee-recipient`. See `LOCALDEV_FEE_RECIPIENTS` in -# `tests/helpers/networks/localdev.ts` (which mirrors these addresses index-for-index). -[nodes.validator1] -cl_suggested_fee_recipient = "0x1111111111111111111111111111111111111111" - -[nodes.validator2] -cl_suggested_fee_recipient = "0x2222222222222222222222222222222222222222" - -[nodes.validator3] -cl_suggested_fee_recipient = "0x3333333333333333333333333333333333333333" - -[nodes.validator4] -cl_suggested_fee_recipient = "0x4444444444444444444444444444444444444444" - -[nodes.validator5] -cl_suggested_fee_recipient = "0x5555555555555555555555555555555555555555" - -[nodes.full1] diff --git a/crates/quake/scenarios/nightly-chaos-testing.toml b/crates/quake/scenarios/nightly-chaos-testing.toml deleted file mode 100644 index de55903..0000000 --- a/crates/quake/scenarios/nightly-chaos-testing.toml +++ /dev/null @@ -1,79 +0,0 @@ -name = "nightly-chaos-testing" - -# Global config -cl.config.log_level = "debug" - -latency_emulation = true -engine_api_connection = "rpc" - -# Topology: -# 10 validators in 4 AWS regions -# - blue, green, purple: us-east-2 -# - cyan: us-west-2 -# - red, orange, yellow: eu-central-1 -# - pink, grey, brown: ap-northeast-1 -# -# 2 full nodes in 2 locations -# -# Internal full nodes: -# - 1 full node - us-east-1 -# - 1 full node - us-west-2 - -# us-east-2 validators -[nodes.validator-blue] -region = "us-east-2" -cl_persistent_peers = ["validator-green", "validator-purple", "validator-cyan", "validator-red", "validator-orange", "validator-yellow", "validator-pink", "validator-grey", "validator-brown"] - -[nodes.validator-green] -region = "us-east-2" -cl_persistent_peers = ["validator-blue", "validator-purple", "validator-cyan", "validator-red", "validator-orange", "validator-yellow", "validator-pink", "validator-grey", "validator-brown"] - -[nodes.validator-purple] -region = "us-east-2" -cl_persistent_peers = ["validator-blue", "validator-green", "validator-cyan", "validator-red", "validator-orange", "validator-yellow", "validator-pink", "validator-grey", "validator-brown"] - -# us-west-2 validator -[nodes.validator-cyan] -region = "us-west-2" -cl_persistent_peers = ["validator-blue", "validator-green", "validator-purple", "validator-red", "validator-orange", "validator-yellow", "validator-pink", "validator-grey", "validator-brown"] - -# eu-central-1 validators -[nodes.validator-red] -region = "eu-central-1" -cl_persistent_peers = ["validator-blue", "validator-green", "validator-purple", "validator-cyan", "validator-orange", "validator-yellow", "validator-pink", "validator-grey", "validator-brown"] - -[nodes.validator-orange] -region = "eu-central-1" -cl_persistent_peers = ["validator-blue", "validator-green", "validator-purple", "validator-cyan", "validator-red", "validator-yellow", "validator-pink", "validator-grey", "validator-brown"] - -[nodes.validator-yellow] -region = "eu-central-1" -cl_persistent_peers = ["validator-blue", "validator-green", "validator-purple", "validator-cyan", "validator-red", "validator-orange", "validator-pink", "validator-grey", "validator-brown"] - -# ap-northeast-1 validators -[nodes.validator-pink] -region = "ap-northeast-1" -cl_persistent_peers = ["validator-blue", "validator-green", "validator-purple", "validator-cyan", "validator-red", "validator-orange", "validator-yellow", "validator-grey", "validator-brown"] -start_at = 30 - -[nodes.validator-grey] -region = "ap-northeast-1" -cl_persistent_peers = ["validator-blue", "validator-green", "validator-purple", "validator-cyan", "validator-red", "validator-orange", "validator-yellow", "validator-pink", "validator-brown"] -start_at = 30 - -[nodes.validator-brown] -region = "ap-northeast-1" -cl_persistent_peers = ["validator-blue", "validator-green", "validator-purple", "validator-cyan", "validator-red", "validator-orange", "validator-yellow", "validator-pink", "validator-grey"] -start_at = 30 - -# us-east-1 full node -[nodes.full-stg-blue] -region = "us-east-1" -cl_persistent_peers = ["validator-blue", "validator-green", "validator-purple", "validator-cyan", "validator-red", "validator-orange", "validator-yellow", "validator-pink", "validator-grey", "validator-brown"] -start_at = 150 - -# us-west-2 full node -[nodes.full-prod-teal] -region = "us-west-2" -cl_persistent_peers = ["validator-blue", "validator-green", "validator-purple", "validator-cyan", "validator-red", "validator-orange", "validator-yellow", "validator-pink", "validator-grey", "validator-brown"] -start_at = 150 diff --git a/crates/quake/scenarios/nightly-perf.toml b/crates/quake/scenarios/nightly-perf.toml deleted file mode 100644 index 70041e6..0000000 --- a/crates/quake/scenarios/nightly-perf.toml +++ /dev/null @@ -1,12 +0,0 @@ -# Deterministic 5-validator scenario for reproducible performance benchmarking. -# No latency emulation, no chaos, no staggered starts. -latency_emulation = false - -# This scenario requires metrics to be collected, action runner cannot connect to 127.0.0.1 -monitoring_bind_host = "0.0.0.0" - -[nodes.validator1] -[nodes.validator2] -[nodes.validator3] -[nodes.validator4] -[nodes.validator5] diff --git a/crates/quake/scenarios/nightly-tx-propagation.toml b/crates/quake/scenarios/nightly-tx-propagation.toml deleted file mode 100644 index 82db1e2..0000000 --- a/crates/quake/scenarios/nightly-tx-propagation.toml +++ /dev/null @@ -1,97 +0,0 @@ -name = "nightly-tx-propagation" -description = "Nightly sentry-topology transaction propagation check after a quiet period." - -# Four validators are isolated behind two sentries. Full nodes connect to the -# sentries, but sentries only trust validators for transaction propagation. - -[node_groups] -SENTRIES = ["sentry-1", "sentry-2"] -FULLNODES = ["full-blue", "full-green", "full-purple"] - -[nodes.validator1] -subnets = ["trusted"] -cl_persistent_peers = ["SENTRIES", "ALL_VALIDATORS"] -cl_persistent_peers_only = true -cl_gossipsub.explicit_peering = true -cl_suggested_fee_recipient = "0x1111111111111111111111111111111111111111" -[nodes.validator1.el.config] -trusted_peers = ["SENTRIES", "ALL_VALIDATORS"] -trusted_only = true -disable_discovery = true - -[nodes.validator2] -subnets = ["trusted"] -cl_persistent_peers = ["SENTRIES", "ALL_VALIDATORS"] -cl_persistent_peers_only = true -cl_gossipsub.explicit_peering = true -cl_suggested_fee_recipient = "0x2222222222222222222222222222222222222222" -[nodes.validator2.el.config] -trusted_peers = ["SENTRIES", "ALL_VALIDATORS"] -trusted_only = true -disable_discovery = true - -[nodes.validator3] -subnets = ["trusted"] -cl_persistent_peers = ["SENTRIES", "ALL_VALIDATORS"] -cl_persistent_peers_only = true -cl_gossipsub.explicit_peering = true -cl_suggested_fee_recipient = "0x3333333333333333333333333333333333333333" -[nodes.validator3.el.config] -trusted_peers = ["SENTRIES", "ALL_VALIDATORS"] -trusted_only = true -disable_discovery = true - -[nodes.validator4] -subnets = ["trusted"] -cl_persistent_peers = ["SENTRIES", "ALL_VALIDATORS"] -cl_persistent_peers_only = true -cl_gossipsub.explicit_peering = true -cl_suggested_fee_recipient = "0x4444444444444444444444444444444444444444" -[nodes.validator4.el.config] -trusted_peers = ["SENTRIES", "ALL_VALIDATORS"] -trusted_only = true -disable_discovery = true - -[nodes.sentry-1] -subnets = ["trusted", "untrusted"] -cl_persistent_peers = ["sentry-2", "ALL_VALIDATORS"] -cl_gossipsub.explicit_peering = true -cl_gossipsub.load = "high" -[nodes.sentry-1.el.config] -trusted_peers = ["ALL_VALIDATORS"] -tx_propagation_policy = "Trusted" -disable_discovery = true - -[nodes.sentry-2] -subnets = ["trusted", "untrusted"] -cl_persistent_peers = ["sentry-1", "ALL_VALIDATORS"] -cl_gossipsub.explicit_peering = true -cl_gossipsub.load = "high" -[nodes.sentry-2.el.config] -trusted_peers = ["ALL_VALIDATORS"] -tx_propagation_policy = "Trusted" -disable_discovery = true - -[nodes.full-blue] -subnets = ["untrusted"] -cl_persistent_peers = ["sentry-1", "sentry-2"] -[nodes.full-blue.el.config] -trusted_peers = ["sentry-1", "sentry-2"] -tx_propagation_policy = "Trusted" -disable_discovery = true - -[nodes.full-green] -subnets = ["untrusted"] -cl_persistent_peers = ["sentry-1", "sentry-2"] -[nodes.full-green.el.config] -trusted_peers = ["sentry-1", "sentry-2"] -tx_propagation_policy = "Trusted" -disable_discovery = true - -[nodes.full-purple] -subnets = ["untrusted"] -cl_persistent_peers = ["sentry-1", "sentry-2"] -[nodes.full-purple.el.config] -trusted_peers = ["sentry-1", "sentry-2"] -tx_propagation_policy = "Trusted" -disable_discovery = true diff --git a/crates/quake/scenarios/nightly-upgrade.toml b/crates/quake/scenarios/nightly-upgrade.toml deleted file mode 100644 index 3d60aae..0000000 --- a/crates/quake/scenarios/nightly-upgrade.toml +++ /dev/null @@ -1,28 +0,0 @@ -name = "nightly-upgrade" -description = "Nightly upgrade testing scenario with 5 validators. Tests rolling upgrade with configurable delay between stop and restart. Use 'quake wait rounds' after upgrades to verify consensus health." - -# Use RPC connection for Engine API -engine_api_connection = "rpc" - -# Starting versions (what the testnet begins with) -# IMAGE_REGISTRY_URL should be defined as an environment variable or in the .env file. -image_cl="${IMAGE_REGISTRY_URL}/arc-consensus:latest" -image_el="${IMAGE_REGISTRY_URL}/arc-execution:latest" - -# Set the execution layer initial hardfork to the highest version supported by the image. -el_init_hardfork="zero4" - -# Upgrade versions (what we upgrade to during the test) -image_cl_upgrade="arc_consensus:latest" -image_el_upgrade="arc_execution:latest" - -# Global config -cl.config.log_level = "info" - -# 5 validators ensures BFT consensus continues during single node upgrade -# Byzantine fault tolerance: f = (n-1)/3, so with 5 nodes we can tolerate 1 failure -[nodes.validator1] -[nodes.validator2] -[nodes.validator3] -[nodes.validator4] -[nodes.validator5] diff --git a/crates/quake/scenarios/public-testnet.toml b/crates/quake/scenarios/public-testnet.toml deleted file mode 100644 index 9ade2c2..0000000 --- a/crates/quake/scenarios/public-testnet.toml +++ /dev/null @@ -1,160 +0,0 @@ -name = "testnet" -description = "A reproduction of the testnet configuration with latency emulation enabled." - -# Global config -cl.config.log_level = "debug" - -cl.config.discovery_num_inbound_peers = 50 -cl.config.discovery_num_outbound_peers = 50 - -latency_emulation = true -engine_api_connection = "rpc" - -# Topology (testnet reproduction): -# 10 validators in 4 AWS regions -# - blue, green, purple: us-east-2 -# - cyan: us-west-2 -# - red, orange, yellow: eu-central-1 -# - pink, grey, brown: ap-northeast-1 -# -# 15 full nodes in 5 locations -# -# Internal full nodes: -# - 5 full nodes - us-east-1 -# - 1 full node - us-west-2 -# -# External full nodes: -# (Note: exact locations and counts are unknown; this is an approximation -# based on the number of connections observed in validator metrics) -# - 6 full nodes - eu-west-2 (QuickNode) -# - 1 full node - us-west-1 (Alchemy) -# - 1 full node - us-west-2 (Blockdaemon) -# - 1 full node - eu-central-1 (DRPC) - -[node_groups] -US_EU_VALIDATORS = ["validator-blue", "validator-green", "validator-purple", "validator-cyan", "validator-red", "validator-orange", "validator-yellow"] - -# us-east-2 validators -[nodes.validator-blue] -region = "us-east-2" -cl_persistent_peers = ["ALL_VALIDATORS"] - -[nodes.validator-green] -region = "us-east-2" -cl_persistent_peers = ["ALL_VALIDATORS"] - -[nodes.validator-purple] -region = "us-east-2" -cl_persistent_peers = ["ALL_VALIDATORS"] - -# us-west-2 validator -[nodes.validator-cyan] -region = "us-west-2" -cl_persistent_peers = ["ALL_VALIDATORS"] - -# eu-central-1 validators -[nodes.validator-red] -region = "eu-central-1" -cl_persistent_peers = ["ALL_VALIDATORS"] - -[nodes.validator-orange] -region = "eu-central-1" -cl_persistent_peers = ["ALL_VALIDATORS"] - -[nodes.validator-yellow] -region = "eu-central-1" -cl_persistent_peers = ["ALL_VALIDATORS"] - -# ap-northeast-1 validators -[nodes.validator-pink] -region = "ap-northeast-1" -cl_persistent_peers = ["ALL_VALIDATORS"] -start_at = 30 - -[nodes.validator-grey] -region = "ap-northeast-1" -cl_persistent_peers = ["ALL_VALIDATORS"] -start_at = 30 - -[nodes.validator-brown] -region = "ap-northeast-1" -cl_persistent_peers = ["ALL_VALIDATORS"] -start_at = 30 - -# us-east-1 full nodes -[nodes.full-stg-blue] -region = "us-east-1" -cl_persistent_peers = ["ALL_VALIDATORS"] -start_at = 150 - -[nodes.full-stg-green] -region = "us-east-1" -cl_persistent_peers = ["ALL_VALIDATORS"] -start_at = 150 - -[nodes.full-blue] -region = "us-east-1" -cl_persistent_peers = ["ALL_VALIDATORS"] -start_at = 20 - -[nodes.full-green] -region = "us-east-1" -cl_persistent_peers = ["ALL_VALIDATORS"] -start_at = 20 - -[nodes.full-stg-purple] -region = "us-east-1" -cl_persistent_peers = ["ALL_VALIDATORS"] -start_at = 150 - -# us-west-2 full node -[nodes.full-prod-teal] -region = "us-west-2" -cl_persistent_peers = ["ALL_VALIDATORS"] -start_at = 150 - -# external full nodes -[nodes.full-quicknode1] -region = "eu-west-2" -cl_persistent_peers = ["US_EU_VALIDATORS"] -start_at = 50 - -[nodes.full-quicknode2] -region = "eu-west-2" -cl_persistent_peers = ["US_EU_VALIDATORS"] -start_at = 50 - -[nodes.full-quicknode3] -region = "eu-west-2" -cl_persistent_peers = ["US_EU_VALIDATORS"] -start_at = 50 - -[nodes.full-quicknode4] -region = "eu-west-2" -cl_persistent_peers = ["US_EU_VALIDATORS"] -start_at = 50 - -[nodes.full-quicknode5] -region = "eu-west-2" -cl_persistent_peers = ["US_EU_VALIDATORS"] -start_at = 50 - -[nodes.full-quicknode6] -region = "eu-west-2" -cl_persistent_peers = ["US_EU_VALIDATORS"] -start_at = 50 - -[nodes.full-alchemy1] -region = "us-west-1" -cl_persistent_peers = ["US_EU_VALIDATORS"] -start_at = 50 - -[nodes.full-blockdaemon1] -region = "us-west-2" -cl_persistent_peers = ["US_EU_VALIDATORS"] -start_at = 50 - -[nodes.full-drpc1] -region = "eu-central-1" -cl_persistent_peers = ["US_EU_VALIDATORS"] -start_at = 50 diff --git a/crates/quake/scripts/aws-resources.sh b/crates/quake/scripts/aws-resources.sh deleted file mode 100755 index ac49cb3..0000000 --- a/crates/quake/scripts/aws-resources.sh +++ /dev/null @@ -1,1077 +0,0 @@ -#!/usr/bin/env bash -# -# List and remove AWS resources created by `quake remote create` when the -# Terraform state is no longer available to drive `quake clean`. -# -# Identification: anchored VPC traversal. The VPC is found by its Name tag -# (equal to the project name); child resources are enumerated via `--filters -# Name=vpc-id,Values=`. The key pair and CloudWatch alarm live outside the -# VPC and are looked up by deterministic name. -# -# Project name: arc--testnet-, -# mirroring Terraform's `lower(replace(x, "/[/.]/", "-"))` sanitization. -# -# Requirements: -# - AWS CLI v2 (uses JMESPath filter syntax such as Tags[?Key==`Name`]), with -# AWS credentials configured. - -# `set -u` is intentionally omitted: Bash 3.2 (macOS default) reports -# `"${arr[@]}"` as an unbound variable for empty arrays, even when the array -# was declared with `arr=()`. Keeping nounset would require wrapping every -# array expansion in `${arr[@]+"${arr[@]}"}`, which is worse than losing it. -set -eo pipefail - -# --- Constants --------------------------------------------------------------- - -# Marker tag key applied to every Quake EC2 instance via var.tags. -MARKER_TAG_KEY="arc-quake-testnet" - -# Project name prefix and infix used for all Quake projects. -PROJECT_PREFIX="arc-" -PROJECT_INFIX="-testnet-" - -# Instance-state filter values for live (non-terminated) instances. Used in -# describe-instances filters as Name=instance-state-name,Values=$INSTANCE_LIVE_STATES. -INSTANCE_LIVE_STATES="pending,running,stopping,stopped,shutting-down" - -# --- Globals (populated by parse_args) --------------------------------------- - -SUBCOMMAND="" -REGION="" -USER_OVERRIDE="" -USER_OVERRIDE_PASSED=false -SKIP_PROMPT=false -VERBOSE=false -TESTNET_ARG="" - -# State shared between plan and apply. -declare -a FAILURES=() - -# --- Output helpers ---------------------------------------------------------- - -# Data on stdout; progress, warnings, and errors on stderr. - -out() { printf '%s\n' "$*"; } -log() { printf '%s\n' "$*" >&2; } -warn() { printf 'warning: %s\n' "$*" >&2; } -err() { printf 'error: %s\n' "$*" >&2; } - -die() { - err "$1" - exit "${2:-1}" -} - -die_usage() { - err "$1" - usage >&2 - exit 2 -} - -# TTY-aware color codes. Only active when stdout is a terminal. -if [[ -t 1 ]]; then - C_BOLD=$'\033[1m' - C_DIM=$'\033[2m' - C_RESET=$'\033[0m' -else - C_BOLD="" - C_DIM="" - C_RESET="" -fi - -# --- Usage ------------------------------------------------------------------- - -usage() { - local prog - prog="$(basename -- "$0")" - # Unquoted heredoc so ${prog} expands; env-var references are escaped as \$. - cat </dev/null 2>&1; then - die "required command not found: $1" - fi -} - -# True when an AWS CLI text-output value indicates "no such resource". The CLI -# emits an empty string when a query returns nothing, and the literal "None" -# when a queried field is null on an existing resource. -is_absent_or_none() { - [[ -z "${1:-}" || "$1" == "None" ]] -} - -# Mirror Terraform's lower(replace(x, "/[/.]/", "-")). -sanitize() { - local input="${1:-}" - [[ -z "$input" ]] && return - printf '%s' "$input" | tr '[:upper:]' '[:lower:]' | tr '/.' '--' -} - -# Resolve the repository root. Prefer git; fall back to path math from BASH_SOURCE. -resolve_repo_root() { - local script_dir root - script_dir="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" - if root="$(git -C "$script_dir" rev-parse --show-toplevel 2>/dev/null)"; then - printf '%s' "$root" - return - fi - # Fallback: this script lives at crates/quake/scripts/, so repo root is 3 up. - (cd "$script_dir/../../.." && pwd) -} - -# Source /.env only if GITHUB_USER is unset. -load_dotenv_if_needed() { - if [[ -n "${GITHUB_USER:-}" ]]; then - return - fi - local dotenv="$1/.env" - if [[ -f "$dotenv" ]]; then - set -a - # shellcheck disable=SC1090 - source "$dotenv" - set +a - fi -} - -# Resolved effective user for project-name construction (already sanitized). -# --user takes precedence over $GITHUB_USER; missing both is a usage error. -resolve_effective_user() { - if [[ -n "$USER_OVERRIDE" ]]; then - sanitize "$USER_OVERRIDE" - return - fi - if [[ -z "${GITHUB_USER:-}" ]]; then - die_usage "--user not provided and GITHUB_USER is not set" - fi - sanitize "$GITHUB_USER" -} - -# AWS CLI wrapper that pins the region. -aws_cli() { - aws --region "$REGION" "$@" -} - -# Run an AWS describe/query and emit its `--output text` result split by whitespace -# (tabs and newlines) into one line per value. Silent on missing resources. -# Intended only for single-column queries. -collect() { - local raw - raw="$(aws_cli "$@" --output text 2>/dev/null || true)" - if is_absent_or_none "$raw"; then - return - fi - # AWS text output separates list elements with tabs; normalize. - tr '[:space:]' '\n' <<<"$raw" | sed '/^$/d' -} - -# Run an AWS describe/query expected to return multi-column text output where -# the first column is the resource ID and the second is a human-readable name -# (or "None" when absent). Emits one tab-separated record per line; silent on -# missing resources. -collect_pairs() { - local raw - raw="$(aws_cli "$@" --output text 2>/dev/null || true)" - if [[ -z "$raw" ]]; then - return - fi - local line first - while IFS= read -r line; do - [[ -z "$line" ]] && continue - first="${line%%$'\t'*}" - is_absent_or_none "$first" && continue - printf '%s\n' "$line" - done <<< "$raw" -} - -# Bash 3 replacement for `mapfile -t NAME < <(cmd)`. Reads newline-delimited -# input on stdin and assigns each line to the named array, escaping values -# through `%q` so whitespace or shell metacharacters survive the eval. -read_lines() { - local __name="$1" - eval "$__name=()" - local __line __escaped - while IFS= read -r __line; do - printf -v __escaped '%q' "$__line" - eval "$__name+=($__escaped)" - done -} - -# --- Project name handling --------------------------------------------------- - -# True if the argument looks like a fully-qualified project name. -# Requires both the arc- prefix and the -testnet- infix to reduce false positives -# on testnet basenames that happen to contain "-testnet-" as a substring. -# Caller MUST pass a sanitized (lowercase) string — the constants are lowercase -# and bash glob matches are case-sensitive. -is_full_project_name() { - local arg="$1" - [[ "$arg" == ${PROJECT_PREFIX}* && "$arg" == *${PROJECT_INFIX}* ]] -} - -# Given a testnet argument and the effective (sanitized) user, return the project name. -# The argument is sanitized before classification so an uppercased full project -# name (e.g. ARC-LOCALDEV-TESTNET-ALICE) is not mistaken for a testnet basename -# and double-wrapped with the arc- prefix and -testnet- infix. -project_name_from_arg() { - local arg="$1" effective_user="$2" - local normalized - normalized="$(sanitize "$arg")" - if is_full_project_name "$normalized"; then - printf '%s' "$normalized" - else - local testnet - testnet="$(sanitize "$(basename -- "$arg")")" - printf '%s%s%s%s' "$PROJECT_PREFIX" "$testnet" "$PROJECT_INFIX" "$effective_user" - fi -} - -# Extract the user segment (text after the last "-testnet-") from a project name. -user_from_project() { - local project="$1" - printf '%s' "${project##*${PROJECT_INFIX}}" -} - -# Extract the testnet segment (text between the "arc-" prefix and the last -# "-testnet-" infix) from a project name. -testnet_from_project() { - local project="$1" - local without_prefix="${project#${PROJECT_PREFIX}}" - # %${PROJECT_INFIX}* is shortest-match from end, i.e. strips from the LAST - # occurrence of "-testnet-" onwards. This keeps testnet names that happen to - # contain "-testnet-" as a substring intact. - printf '%s' "${without_prefix%${PROJECT_INFIX}*}" -} - -# Cross-user guardrail: refuse to operate on someone else's project unless --user -# was explicitly passed. Only relevant when the argument was a full project name; -# for testnet basenames, the constructed project name always uses the effective user. -check_cross_user_guardrail() { - local project="$1" - if [[ "$USER_OVERRIDE_PASSED" == "true" ]]; then - return - fi - local project_user gh_sanitized - project_user="$(user_from_project "$project")" - gh_sanitized="$(sanitize "${GITHUB_USER:-}")" - if [[ -n "$gh_sanitized" && "$project_user" != "$gh_sanitized" ]]; then - die_usage "project '$project' belongs to user '$project_user' but --user was not passed (current GITHUB_USER resolves to '$gh_sanitized'). Pass --user $project_user to confirm." - fi -} - -# --- Discovery: per-project ------------------------------------------------- -# -# Side effects: this function OVERWRITES module-level globals listed below. -# It is the only writer of these arrays; callers must not assume the values -# survive across calls. cmd_list_summary deliberately re-invokes per project -# in a loop, relying on this clean-slate behavior. -# -# Populates parallel *_IDS and *_NAMES arrays for each resource class, indexed -# together (NAMES[i] is the Name tag / GroupName of IDS[i], or "" if absent). -# Key pairs and CloudWatch alarms have no separate name field, so only KEY_NAMES -# and ALARM_NAMES are populated for those. -# -# VPC_IDS/VPC_NAMES VPCs with Name tag = -# INSTANCE_IDS/INSTANCE_NAMES instances tagged project= OR in the VPCs -# ENI_IDS/ENI_NAMES ENIs tagged project= OR in the VPCs -# VOLUME_IDS/VOLUME_NAMES EBS volumes tagged project= -# SUBNET_IDS/SUBNET_NAMES subnets inside the VPCs -# RTB_IDS/RTB_NAMES non-main route tables inside the VPCs -# SG_IDS/SG_NAMES non-default security groups inside the VPCs (name = GroupName) -# IGW_IDS/IGW_NAMES internet gateways attached to the VPCs -# KEY_NAMES key pairs named -key (ID == name) -# ALARM_NAMES CloudWatch alarms -cc-auto-recovery (ID == name) -# -# EBS volumes: Terraform sets `volume_tags` on aws_instance so root volumes -# inherit `project=`. With delete_on_termination=true (AMI default), -# in-use root volumes vanish on instance termination — discovery here only -# surfaces detached/orphaned volumes, plus root volumes whose instance is -# still running at discovery time. The delete phase re-checks status. -# Volumes created before this tagging change have no `project` tag and are -# invisible to this script; they will still be cleaned up by their attached -# instance's termination via delete_on_termination. - -# Scan a describe query into parallel IDS/NAMES arrays, deduping via a seen-map. -# Called once per data source; union scans (instances/ENIs) call it twice with -# the same target arrays and seen-map. -# -# _scan_into IDS_ARR NAMES_ARR SEEN_VAR aws-cli-args... -# -# Bash 3 port: namerefs and associative arrays are unavailable, so array -# identities are passed by name and resolved via `eval`, and the seen-map is -# a colon-delimited string probed with a `case` pattern match. -_scan_into() { - local ids_name="$1" names_name="$2" seen_name="$3" - shift 3 - local id name id_esc name_esc seen_val - while IFS=$'\t' read -r id name _; do - [[ -z "$id" ]] && continue - eval "seen_val=\"\${$seen_name}\"" - case ":${seen_val}:" in - *":$id:"*) continue ;; - esac - eval "$seen_name=\"\${$seen_name}:\$id\"" - [[ "$name" == "None" ]] && name="" - printf -v id_esc '%q' "$id" - printf -v name_esc '%q' "$name" - eval "$ids_name+=($id_esc)" - eval "$names_name+=($name_esc)" - done < <(collect_pairs "$@") -} - -discover_project() { - local project="$1" vpc - - VPC_IDS=(); VPC_NAMES=(); local _s_vpc="" - INSTANCE_IDS=(); INSTANCE_NAMES=(); local _s_inst="" - ENI_IDS=(); ENI_NAMES=(); local _s_eni="" - VOLUME_IDS=(); VOLUME_NAMES=(); local _s_vol="" - SUBNET_IDS=(); SUBNET_NAMES=(); local _s_subnet="" - RTB_IDS=(); RTB_NAMES=(); local _s_rtb="" - SG_IDS=(); SG_NAMES=(); local _s_sg="" - IGW_IDS=(); IGW_NAMES=(); local _s_igw="" - - local inst_state="Name=instance-state-name,Values=${INSTANCE_LIVE_STATES}" - local inst_query='Reservations[].Instances[].[InstanceId, Tags[?Key==`Name`].Value|[0]]' - local eni_query='NetworkInterfaces[].[NetworkInterfaceId, Tags[?Key==`Name`].Value|[0]]' - - # VPCs anchor the traversal. - _scan_into VPC_IDS VPC_NAMES _s_vpc \ - ec2 describe-vpcs \ - --filters "Name=tag:Name,Values=${project}" \ - --query 'Vpcs[].[VpcId, Tags[?Key==`Name`].Value|[0]]' - - # Instances and ENIs: tag-based first; VPC-scoped later in the per-VPC loop. - _scan_into INSTANCE_IDS INSTANCE_NAMES _s_inst \ - ec2 describe-instances \ - --filters "Name=tag:project,Values=${project}" "$inst_state" \ - --query "$inst_query" - _scan_into ENI_IDS ENI_NAMES _s_eni \ - ec2 describe-network-interfaces \ - --filters "Name=tag:project,Values=${project}" \ - --query "$eni_query" - - # EBS volumes: tag-based only. Volumes have no VPC association, so there is - # no per-VPC fallback. Pulled regardless of state — `available` are the true - # orphans we will delete; `in-use` ones may attach to instances we are about - # to terminate (delete_on_termination handles the cleanup) and are surfaced - # here for visibility only. The delete phase re-checks status to decide. - _scan_into VOLUME_IDS VOLUME_NAMES _s_vol \ - ec2 describe-volumes \ - --filters "Name=tag:project,Values=${project}" \ - --query 'Volumes[].[VolumeId, Tags[?Key==`Name`].Value|[0]]' - - for vpc in "${VPC_IDS[@]}"; do - _scan_into INSTANCE_IDS INSTANCE_NAMES _s_inst \ - ec2 describe-instances \ - --filters "Name=vpc-id,Values=${vpc}" "$inst_state" \ - --query "$inst_query" - _scan_into ENI_IDS ENI_NAMES _s_eni \ - ec2 describe-network-interfaces \ - --filters "Name=vpc-id,Values=${vpc}" \ - --query "$eni_query" - _scan_into SUBNET_IDS SUBNET_NAMES _s_subnet \ - ec2 describe-subnets \ - --filters "Name=vpc-id,Values=${vpc}" \ - --query 'Subnets[].[SubnetId, Tags[?Key==`Name`].Value|[0]]' - _scan_into RTB_IDS RTB_NAMES _s_rtb \ - ec2 describe-route-tables \ - --filters "Name=vpc-id,Values=${vpc}" \ - --query 'RouteTables[?Associations[?Main!=`true`] || length(Associations)==`0`].[RouteTableId, Tags[?Key==`Name`].Value|[0]]' - _scan_into SG_IDS SG_NAMES _s_sg \ - ec2 describe-security-groups \ - --filters "Name=vpc-id,Values=${vpc}" \ - --query 'SecurityGroups[?GroupName!=`default`].[GroupId, GroupName]' - _scan_into IGW_IDS IGW_NAMES _s_igw \ - ec2 describe-internet-gateways \ - --filters "Name=attachment.vpc-id,Values=${vpc}" \ - --query 'InternetGateways[].[InternetGatewayId, Tags[?Key==`Name`].Value|[0]]' - done - - # Key pairs and CloudWatch alarms: ID is the only identifier. - read_lines KEY_NAMES < <( - collect ec2 describe-key-pairs \ - --filters "Name=key-name,Values=${project}-key" \ - --query 'KeyPairs[].KeyName' - ) - read_lines ALARM_NAMES < <( - collect cloudwatch describe-alarms \ - --alarm-names "${project}-cc-auto-recovery" \ - --query 'MetricAlarms[].AlarmName' - ) -} - -# --- Discovery: cross-project (for `list` summary) --------------------------- - -# Return one project name per line for projects discovered in the region. -# Union of three sources, in order of confidence: -# 1. describe-vpcs filtered by tag-key=$MARKER_TAG_KEY; the VPC's `project` -# tag value is the project name. This is the authoritative source for -# VPCs created with the current Terraform. -# 2. describe-vpcs by Name tag matching `arc-*-testnet-*`. Back-compat for -# VPCs created before the marker tag was added; may include unrelated -# VPCs that happen to share the Name pattern. -# 3. describe-instances carrying the marker tag key; their `project` tag -# values. Catches projects whose VPC was already deleted but whose -# instances are still terminating. -discover_all_projects() { - local -a vpc_marker_projects=() vpc_names=() instance_projects=() - - read_lines vpc_marker_projects < <( - collect ec2 describe-vpcs \ - --filters "Name=tag-key,Values=${MARKER_TAG_KEY}" \ - --query 'Vpcs[].Tags[?Key==`project`].Value[]' - ) - read_lines vpc_names < <( - collect ec2 describe-vpcs \ - --filters "Name=tag-key,Values=Name" \ - --query 'Vpcs[].Tags[?Key==`Name`].Value[]' - ) - read_lines instance_projects < <( - collect ec2 describe-instances \ - --filters "Name=tag-key,Values=${MARKER_TAG_KEY}" \ - "Name=instance-state-name,Values=${INSTANCE_LIVE_STATES}" \ - --query 'Reservations[].Instances[].Tags[?Key==`project`].Value[]' - ) - - # Filter to values shaped like a project name. Returns 0 even when no input - # matches, so the function is safe to call inside `set -e` contexts. - _emit_projects() { - local item - for item in "$@"; do - [[ -z "$item" ]] && continue - if is_full_project_name "$item"; then - printf '%s\n' "$item" - fi - done - return 0 - } - { - _emit_projects "${vpc_marker_projects[@]}" - _emit_projects "${vpc_names[@]}" - _emit_projects "${instance_projects[@]}" - } | sort -u -} - -# --- Rendering --------------------------------------------------------------- - -# Print only the per-class resource rows (no header, no total). Used both by the -# full plan and by the no-arg `list` summary when expanding leftover resources. -# EC2 instances are always rendered (even with count 0) so the user sees at a -# glance whether anything is still running. Other classes are hidden when empty -# to keep the output tight. Each class header is followed by one resource per -# line, indented two spaces further than the header. -show_plan_classes() { - local indent="${1:-}" - local item_indent="${indent} " - - # Paired row (ID + name). When `always` is "true", render the row even with - # count zero, showing an indented "(none)". Array identities are passed by - # name and resolved via `eval` for Bash 3 compatibility. - _row_pair() { - local label="$1" ids_name="$2" names_name="$3" always="${4:-false}" - local count - eval "count=\${#$ids_name[@]}" - if ((count == 0)); then - if [[ "$always" == "true" ]]; then - printf '%s%s (%d):\n' "$indent" "$label" "$count" - printf '%s(none)\n' "$item_indent" - fi - return - fi - printf '%s%s (%d):\n' "$indent" "$label" "$count" - local i id name - for ((i=0; i